# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.


import io
import os
import shutil
import tempfile
import yaml
import zipfile

from oslo_log import log as logging

from tacker.sol_refactored.common import exceptions as sol_ex


LOG = logging.getLogger(__name__)


class Vnfd(object):

    def __init__(self, vnfd_id):
        self.vnfd_id = vnfd_id
        self.tosca_meta = {}
        self.definitions = {}
        self.vnfd_flavours = {}
        self.csar_dir = None
        self.csar_dir_is_tmp = False

    def init_from_csar_dir(self, csar_dir):
        self.csar_dir = csar_dir
        self.init_vnfd()

    def init_from_zip_file(self, zip_file):
        # NOTE: This is used when external NFVO is used.
        # TODO(oda-g): There is no delete route at the moment.
        # A possible enhance is that introducing cache management for
        # extracted vnf packages from external NFVO.
        self.csar_dir = tempfile.mkdtemp()
        self.csar_dir_is_tmp = True

        buff = io.BytesIO(zip_file)
        with zipfile.ZipFile(buff, 'r') as zf:
            zf.extractall(self.csar_dir)

        self.init_vnfd()

    def init_vnfd(self):
        # assume TOSCA-Metadata format
        path = os.path.join(self.csar_dir, 'TOSCA-Metadata', 'TOSCA.meta')
        if not os.path.isfile(path):
            raise sol_ex.InvalidVnfdFormat()

        # expand from yaml to dict for TOSCA.meta and Definitions
        with open(path, 'r') as f:
            self.tosca_meta = yaml.safe_load(f.read())

        path = os.path.join(self.csar_dir, 'Definitions')
        for entry in os.listdir(path):
            if entry.endswith(('.yaml', '.yml')):
                with open(os.path.join(path, entry), 'r') as f:
                    content = yaml.safe_load(f.read())
                self.definitions[entry] = content

    def delete(self):
        if self.csar_dir_is_tmp:
            shutil.rmtree(self.csar_dir)

    def get_vnfd_flavour(self, flavour_id):
        if flavour_id in self.vnfd_flavours:
            return self.vnfd_flavours[flavour_id]

        for data in self.definitions.values():
            fid = (data
                   .get('topology_template', {})
                   .get('substitution_mappings', {})
                   .get('properties', {})
                   .get('flavour_id'))
            if fid == flavour_id:
                self.vnfd_flavours[flavour_id] = data
                return data

        # NOT found.
        # NOTE: checked by the caller. basically check is necessary at
        # instantiate start only.

    def get_sw_image(self, flavour_id):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd
                 .get('topology_template', {})
                 .get('node_templates', {}))
        types = ['tosca.nodes.nfv.Vdu.Compute',
                 'tosca.nodes.nfv.Vdu.VirtualBlockStorage']
        sw_image = {}
        for name, data in nodes.items():
            if (data['type'] in types and
                    data.get('properties', {}).get('sw_image_data')):
                image = data['properties']['sw_image_data']['name']
                sw_image[name] = image

        return sw_image

    def get_sw_image_data(self, flavour_id):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd
                 .get('topology_template', {})
                 .get('node_templates', {}))
        types = ['tosca.nodes.nfv.Vdu.Compute',
                 'tosca.nodes.nfv.Vdu.VirtualBlockStorage']
        sw_image = {}
        for name, data in nodes.items():
            if (data['type'] in types and
                    data.get('properties', {}).get('sw_image_data')):
                sw_image[name] = data['properties']['sw_image_data']
                sw_file = (data
                           .get('artifacts', {})
                           .get('sw_image', {})
                           .get('file'))
                if sw_file:
                    sw_image[name]['file'] = sw_file

        return sw_image

    def get_vnfd_properties(self):
        """return properties used by instantiate"""
        # get from node_templates of VNF of
        # - ['properties']['configurable_properties']
        # - ['properties']['modifiable_attributes']['extensions']
        # - ['properties']['modifiable_attributes']['metadata']
        # NOTE: In etsi_nfv_sol001_vnfd_types.yaml which used by
        # tacker examples, definitions of these properties are commented out.

        prop = {
            'vnfConfigurableProperties': {},
            'extensions': {},
            'metadata': {}
        }
        return prop

    def get_nodes(self, flavour_id, node_type):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd
                 .get('topology_template', {})
                 .get('node_templates', {}))

        res = {name: data
            for name, data in nodes.items() if data['type'] == node_type}

        return res

    def get_vdu_nodes(self, flavour_id):
        return self.get_nodes(flavour_id, 'tosca.nodes.nfv.Vdu.Compute')

    def get_storage_nodes(self, flavour_id):
        return self.get_nodes(flavour_id,
                              'tosca.nodes.nfv.Vdu.VirtualBlockStorage')

    def get_virtual_link_nodes(self, flavour_id):
        return self.get_nodes(flavour_id,
                              'tosca.nodes.nfv.VnfVirtualLink')

    def get_vducp_nodes(self, flavour_id):
        return self.get_nodes(flavour_id, 'tosca.nodes.nfv.VduCp')

    def get_vdu_cps(self, flavour_id, vdu_name):
        cp_nodes = self.get_vducp_nodes(flavour_id)
        cps = []
        for cp_name, cp_data in cp_nodes.items():
            reqs = cp_data.get('requirements', [])
            for req in reqs:
                if req.get('virtual_binding') == vdu_name:
                    cps.append(cp_name)
                    break
        return cps

    def get_base_hot(self, flavour_id):
        # NOTE: this method is openstack specific
        hot_dict = {}
        path = os.path.join(self.csar_dir, 'BaseHOT', flavour_id)
        if not os.path.isdir(path):
            return hot_dict

        for entry in os.listdir(path):
            if entry.endswith(('.yaml', '.yml')):
                with open(os.path.join(path, entry), 'r') as f:
                    content = yaml.safe_load(f.read())
                    hot_dict['template'] = content
                    break

        nested = os.path.join(path, 'nested')
        if not os.path.isdir(nested):
            return hot_dict

        for entry in os.listdir(nested):
            if entry.endswith(('.yaml', '.yml')):
                with open(os.path.join(nested, entry), 'r') as f:
                    content = yaml.safe_load(f.read())
                    hot_dict.setdefault('files', {})
                    hot_dict['files'][entry] = content

        return hot_dict

    def get_vl_name_from_cp(self, flavour_id, cp_data):
        for req in cp_data.get('requirements', []):
            if 'virtual_link' in req:
                return req['virtual_link']

    def get_compute_flavor(self, flavour_id, vdu_name):
        vnfd = self.get_vnfd_flavour(flavour_id)
        flavor = (vnfd.get('topology_template', {})
                      .get('node_templates', {})
                      .get(vdu_name, {})
                      .get('capabilities', {})
                      .get('virtual_compute', {})
                      .get('properties', {})
                      .get('requested_additional_capabilities', {})
                      .get('properties', {})
                      .get('requested_additional_capability_name'))
        if flavor:
            return flavor

    def make_tmp_csar_dir(self):
        # If this fails, 500 which is not caused by programming error
        # but true 'Internal server error' raises.
        tmp_dir = tempfile.mkdtemp()
        shutil.copytree(self.csar_dir, tmp_dir,
                ignore=shutil.ignore_patterns('Files'),
                dirs_exist_ok=True)
        return tmp_dir

    def remove_tmp_csar_dir(self, tmp_dir):
        try:
            shutil.rmtree(tmp_dir)
        except Exception:
            LOG.exception("rmtree %s failed", tmp_dir)
            # as this error does not disturb the process, continue.

    def get_policy_values_by_type(self, flavour_id, policy_type):
        vnfd = self.get_vnfd_flavour(flavour_id)
        policies = (vnfd.get('topology_template', {})
                        .get('policies', []))
        if isinstance(policies, dict):
            policies = [policies]

        ret = [value
            for policy in policies for value in policy.values()
            if value['type'] == policy_type]

        return ret

    def get_default_instantiation_level(self, flavour_id):
        policies = self.get_policy_values_by_type(flavour_id,
                'tosca.policies.nfv.InstantiationLevels')
        if policies:
            return policies[0].get('properties', {}).get('default_level')

    def get_vdu_num(self, flavour_id, vdu_name, instantiation_level):
        policies = self.get_policy_values_by_type(flavour_id,
                'tosca.policies.nfv.VduInstantiationLevels')
        for policy in policies:
            if vdu_name in policy.get('targets', []):
                return (policy.get('properties', {})
                              .get('levels', {})
                              .get(instantiation_level, {})
                              .get('number_of_instances'))
        return 0

    def get_placement_groups(self, flavour_id):
        vnfd = self.get_vnfd_flavour(flavour_id)
        groups = (vnfd.get('topology_template', {})
                      .get('groups', []))
        if isinstance(groups, dict):
            groups = [groups]

        ret = {key: value['members']
            for group in groups for key, value in group.items()
            if value['type'] == 'tosca.groups.nfv.PlacementGroup'}

        return ret

    def _get_targets(self, flavour_id, affinity_type):
        policies = self.get_policy_values_by_type(flavour_id, affinity_type)
        groups = self.get_placement_groups(flavour_id)

        ret = []
        for policy in policies:
            scope = policy['properties']['scope']
            if scope not in ['zone', 'nfvi_node']:
                continue

            targets = []
            for target in policy['targets']:
                if target in list(groups.keys()):
                    targets += groups[target]
                else:
                    targets.append(target)

            ret.append((targets, scope))

        return ret

    def get_affinity_targets(self, flavour_id):
        return self._get_targets(flavour_id,
                'tosca.policies.nfv.AffinityRule')

    def get_anti_affinity_targets(self, flavour_id):
        return self._get_targets(flavour_id,
                'tosca.policies.nfv.AntiAffinityRule')

    def get_interface_script(self, flavour_id, operation):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd.get('topology_template', {})
                     .get('node_templates', {}))
        for node in nodes.values():
            if 'interfaces' not in node:
                continue
            op_value = (node['interfaces'].get('Vnflcm', {})
                                          .get(operation))
            if not isinstance(op_value, dict):
                # op_value may be []
                return

            artifact = op_value.get('implementation')
            if artifact is None:
                # no script specified for the operation
                return

            script = (node.get('artifacts', {})
                          .get(artifact, {})
                          .get('file'))
            if script is None:
                # can not happen if vnf package is correct.
                return

            script_type = node['artifacts'][artifact].get('type')
            if script_type != 'tosca.artifacts.Implementation.Python':
                # support python script only at the moment
                msg = "Unsupported script type {}".format(script_type)
                raise sol_ex.SolHttpError422(sol_detail=msg)

            return script

# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.


import io
import os
import shutil
import tempfile
import yaml
import zipfile

from oslo_log import log as logging

from tacker.sol_refactored.common import exceptions as sol_ex


LOG = logging.getLogger(__name__)


class Vnfd(object):

    def __init__(self, vnfd_id):
        self.vnfd_id = vnfd_id
        self.tosca_meta = {}
        self.definitions = {}
        self.vnfd_flavours = {}
        self.csar_dir = None
        self.csar_dir_is_tmp = False

    def init_from_csar_dir(self, csar_dir):
        self.csar_dir = csar_dir
        self.init_vnfd()

    def init_from_zip_file(self, zip_file):
        # NOTE: This is used when external NFVO is used.
        # TODO(oda-g): There is no delete route at the moment.
        # A possible enhance is that introducing cache management for
        # extracted vnf packages from external NFVO.
        self.csar_dir = tempfile.mkdtemp()
        self.csar_dir_is_tmp = True

        buff = io.BytesIO(zip_file)
        with zipfile.ZipFile(buff, 'r') as zf:
            zf.extractall(self.csar_dir)

        self.init_vnfd()

    def init_vnfd(self):
        # assume TOSCA-Metadata format
        path = os.path.join(self.csar_dir, 'TOSCA-Metadata', 'TOSCA.meta')
        if not os.path.isfile(path):
            raise sol_ex.InvalidVnfdFormat()

        # expand from yaml to dict for TOSCA.meta and Definitions
        with open(path, 'r') as f:
            self.tosca_meta = yaml.safe_load(f.read())

        path = os.path.join(self.csar_dir, 'Definitions')
        for entry in os.listdir(path):
            if entry.endswith(('.yaml', '.yml')):
                with open(os.path.join(path, entry), 'r') as f:
                    content = yaml.safe_load(f.read())
                self.definitions[entry] = content

    def delete(self):
        if self.csar_dir_is_tmp:
            shutil.rmtree(self.csar_dir)

    def get_vnfd_flavour(self, flavour_id):
        if flavour_id in self.vnfd_flavours:
            return self.vnfd_flavours[flavour_id]

        for data in self.definitions.values():
            fid = (data
                   .get('topology_template', {})
                   .get('substitution_mappings', {})
                   .get('properties', {})
                   .get('flavour_id'))
            if fid == flavour_id:
                self.vnfd_flavours[flavour_id] = data
                return data

        # NOT found.
        # NOTE: checked by the caller. basically check is necessary at
        # instantiate start only.

    def get_sw_image(self, flavour_id):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd
                 .get('topology_template', {})
                 .get('node_templates', {}))
        types = ['tosca.nodes.nfv.Vdu.Compute',
                 'tosca.nodes.nfv.Vdu.VirtualBlockStorage']
        sw_image = {}
        for name, data in nodes.items():
            if (data['type'] in types and
                    data.get('properties', {}).get('sw_image_data')):
                image = data['properties']['sw_image_data']['name']
                sw_image[name] = image

        return sw_image

    def get_sw_image_data(self, flavour_id):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd
                 .get('topology_template', {})
                 .get('node_templates', {}))
        types = ['tosca.nodes.nfv.Vdu.Compute',
                 'tosca.nodes.nfv.Vdu.VirtualBlockStorage']
        sw_image = {}
        for name, data in nodes.items():
            if (data['type'] in types and
                    data.get('properties', {}).get('sw_image_data')):
                sw_image[name] = data['properties']['sw_image_data']
                sw_file = (data
                           .get('artifacts', {})
                           .get('sw_image', {})
                           .get('file'))
                if sw_file:
                    sw_image[name]['file'] = sw_file

        return sw_image

    def get_vnfd_properties(self):
        """return properties used by instantiate"""
        # get from node_templates of VNF of
        # - ['properties']['configurable_properties']
        # - ['properties']['modifiable_attributes']['extensions']
        # - ['properties']['modifiable_attributes']['metadata']
        # NOTE: In etsi_nfv_sol001_vnfd_types.yaml which used by
        # tacker examples, definitions of these properties are commented out.

        prop = {
            'vnfConfigurableProperties': {},
            'extensions': {},
            'metadata': {}
        }
        return prop

    def get_nodes(self, flavour_id, node_type):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd
                 .get('topology_template', {})
                 .get('node_templates', {}))

        res = {name: data
            for name, data in nodes.items() if data['type'] == node_type}

        return res

    def get_vdu_nodes(self, flavour_id):
        return self.get_nodes(flavour_id, 'tosca.nodes.nfv.Vdu.Compute')

    def get_storage_nodes(self, flavour_id):
        return self.get_nodes(flavour_id,
                              'tosca.nodes.nfv.Vdu.VirtualBlockStorage')

    def get_virtual_link_nodes(self, flavour_id):
        return self.get_nodes(flavour_id,
                              'tosca.nodes.nfv.VnfVirtualLink')

    def get_vducp_nodes(self, flavour_id):
        return self.get_nodes(flavour_id, 'tosca.nodes.nfv.VduCp')

    def get_vdu_cps(self, flavour_id, vdu_name):
        cp_nodes = self.get_vducp_nodes(flavour_id)
        cps = []
        for cp_name, cp_data in cp_nodes.items():
            reqs = cp_data.get('requirements', [])
            for req in reqs:
                if req.get('virtual_binding') == vdu_name:
                    cps.append(cp_name)
                    break
        return cps

    def get_base_hot(self, flavour_id):
        # NOTE: this method is openstack specific
        hot_dict = {}
        path = os.path.join(self.csar_dir, 'BaseHOT', flavour_id)
        if not os.path.isdir(path):
            return hot_dict

        for entry in os.listdir(path):
            if entry.endswith(('.yaml', '.yml')):
                with open(os.path.join(path, entry), 'r') as f:
                    content = yaml.safe_load(f.read())
                    hot_dict['template'] = content
                    break

        nested = os.path.join(path, 'nested')
        if not os.path.isdir(nested):
            return hot_dict

        for entry in os.listdir(nested):
            if entry.endswith(('.yaml', '.yml')):
                with open(os.path.join(nested, entry), 'r') as f:
                    content = yaml.safe_load(f.read())
                    hot_dict.setdefault('files', {})
                    hot_dict['files'][entry] = content

        return hot_dict

    def get_vl_name_from_cp(self, flavour_id, cp_data):
        for req in cp_data.get('requirements', []):
            if 'virtual_link' in req:
                return req['virtual_link']

    def get_compute_flavor(self, flavour_id, vdu_name):
        vnfd = self.get_vnfd_flavour(flavour_id)
        flavor = (vnfd.get('topology_template', {})
                      .get('node_templates', {})
                      .get(vdu_name, {})
                      .get('capabilities', {})
                      .get('virtual_compute', {})
                      .get('properties', {})
                      .get('requested_additional_capabilities', {})
                      .get('properties', {})
                      .get('requested_additional_capability_name'))
        if flavor:
            return flavor

    def make_tmp_csar_dir(self):
        # If this fails, 500 which is not caused by programming error
        # but true 'Internal server error' raises.
        tmp_dir = tempfile.mkdtemp()
        shutil.copytree(self.csar_dir, tmp_dir,
                ignore=shutil.ignore_patterns('Files'),
                dirs_exist_ok=True)
        return tmp_dir

    def remove_tmp_csar_dir(self, tmp_dir):
        try:
            shutil.rmtree(tmp_dir)
        except Exception:
            LOG.exception("rmtree %s failed", tmp_dir)
            # as this error does not disturb the process, continue.

    def get_policy_values_by_type(self, flavour_id, policy_type):
        vnfd = self.get_vnfd_flavour(flavour_id)
        policies = (vnfd.get('topology_template', {})
                        .get('policies', []))
        if isinstance(policies, dict):
            policies = [policies]

        ret = [value
            for policy in policies for value in policy.values()
            if value['type'] == policy_type]

        return ret

    def get_default_instantiation_level(self, flavour_id):
        policies = self.get_policy_values_by_type(flavour_id,
                'tosca.policies.nfv.InstantiationLevels')
        if policies:
            return policies[0].get('properties', {}).get('default_level')

    def get_vdu_num(self, flavour_id, vdu_name, instantiation_level):
        policies = self.get_policy_values_by_type(flavour_id,
                'tosca.policies.nfv.VduInstantiationLevels')
        for policy in policies:
            if vdu_name in policy.get('targets', []):
                return (policy.get('properties', {})
                              .get('levels', {})
                              .get(instantiation_level, {})
                              .get('number_of_instances'))
        return 0

    def get_placement_groups(self, flavour_id):
        vnfd = self.get_vnfd_flavour(flavour_id)
        groups = (vnfd.get('topology_template', {})
                      .get('groups', []))
        if isinstance(groups, dict):
            groups = [groups]

        ret = {key: value['members']
            for group in groups for key, value in group.items()
            if value['type'] == 'tosca.groups.nfv.PlacementGroup'}

        return ret

    def _get_targets(self, flavour_id, affinity_type):
        policies = self.get_policy_values_by_type(flavour_id, affinity_type)
        groups = self.get_placement_groups(flavour_id)

        ret = []
        for policy in policies:
            scope = policy['properties']['scope']
            if scope not in ['zone', 'nfvi_node']:
                continue

            targets = []
            for target in policy['targets']:
                if target in list(groups.keys()):
                    targets += groups[target]
                else:
                    targets.append(target)

            ret.append((targets, scope))

        return ret

    def get_affinity_targets(self, flavour_id):
        return self._get_targets(flavour_id,
                'tosca.policies.nfv.AffinityRule')

    def get_anti_affinity_targets(self, flavour_id):
        return self._get_targets(flavour_id,
                'tosca.policies.nfv.AntiAffinityRule')

    def get_interface_script(self, flavour_id, operation):
        vnfd = self.get_vnfd_flavour(flavour_id)
        nodes = (vnfd.get('topology_template', {})
                     .get('node_templates', {}))
        for node in nodes.values():
            if 'interfaces' not in node:
                continue
            op_value = (node['interfaces'].get('Vnflcm', {})
                                          .get(operation))
            if not isinstance(op_value, dict):
                # op_value may be []
                return

            artifact = op_value.get('implementation')
            if artifact is None:
                # no script specified for the operation
                return

            script = (node.get('artifacts', {})
                          .get(artifact, {})
                          .get('file'))
            if script is None:
                # can not happen if vnf package is correct.
                return

            script_type = node['artifacts'][artifact].get('type')
            if script_type != 'tosca.artifacts.Implementation.Python':
                # support python script only at the moment
                msg = "Unsupported script type {}".format(script_type)
                raise sol_ex.SolHttpError422(sol_detail=msg)

            return script

# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation # All Rights Reserved. # #    Licensed under the Apache License, Version 2.0 (the "License"); you may #    not use this file except in compliance with the License. You may obtain #    a copy of the License at # #         http://www.apache.org/licenses/LICENSE-2.0 # #    Unless required by applicable law or agreed to in writing, software #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the #    License for the specific language governing permissions and limitations #    under the License. # NOTE: This is used when external NFVO is used. # TODO(oda-g): There is no delete route at the moment. # A possible enhance is that introducing cache management for # extracted vnf packages from external NFVO. # assume TOSCA-Metadata format # expand from yaml to dict for TOSCA.meta and Definitions # NOT found. # NOTE: checked by the caller. basically check is necessary at # instantiate start only. return properties used by instantiate # get from node_templates of VNF of # - ['properties']['configurable_properties'] # - ['properties']['modifiable_attributes']['extensions'] # - ['properties']['modifiable_attributes']['metadata'] # NOTE: In etsi_nfv_sol001_vnfd_types.yaml which used by # tacker examples, definitions of these properties are commented out. # NOTE: this method is openstack specific # If this fails, 500 which is not caused by programming error # but true 'Internal server error' raises. # as this error does not disturb the process, continue. # op_value may be [] # no script specified for the operation # can not happen if vnf package is correct. # support python script only at the moment