417 lines
15 KiB
Python
417 lines
15 KiB
Python
# 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_vdu_storages(self, vdu_node):
|
|
storages = [req['virtual_storage']
|
|
for req in vdu_node.get('requirements', [])
|
|
if 'virtual_storage' in req]
|
|
|
|
return storages
|
|
|
|
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
|
|
|
|
def get_scale_vdu_and_num(self, flavour_id, aspect_id):
|
|
aspects = self.get_policy_values_by_type(flavour_id,
|
|
'tosca.policies.nfv.ScalingAspects')
|
|
delta = None
|
|
for aspect in aspects:
|
|
value = aspect['properties']['aspects'].get(aspect_id)
|
|
if value is not None:
|
|
# expect there is one delta.
|
|
# NOTE: Tacker does not support non-uniform deltas defined in
|
|
# ETSI NFV SOL001 8. Therefore, uniform delta corresponding
|
|
# to number_of_instances can be set and number_of_instances is
|
|
# the same regardless of scale_level.
|
|
delta = value['step_deltas'][0]
|
|
break
|
|
|
|
if delta is None:
|
|
return {}
|
|
|
|
aspect_deltas = self.get_policy_values_by_type(flavour_id,
|
|
'tosca.policies.nfv.VduScalingAspectDeltas')
|
|
vdu_num_inst = {}
|
|
for aspect_delta in aspect_deltas:
|
|
if aspect_delta.get('properties', {}).get('aspect') == aspect_id:
|
|
num_inst = (aspect_delta['properties']['deltas']
|
|
.get(delta, {}).get('number_of_instances'))
|
|
# NOTE: it is not checked whether 'delta' defined in
|
|
# ScaleingAspects exists in VduScalingAspectDeltas at
|
|
# the loading of vnf package. this is a mistake of the
|
|
# VNFD definition.
|
|
if num_inst is None:
|
|
raise sol_ex.DeltaMissingInVnfd(delta=delta)
|
|
for vdu_name in aspect_delta['targets']:
|
|
vdu_num_inst[vdu_name] = num_inst
|
|
|
|
return vdu_num_inst
|
|
|
|
def get_scale_info_from_inst_level(self, flavour_id, inst_level):
|
|
policies = self.get_policy_values_by_type(flavour_id,
|
|
'tosca.policies.nfv.InstantiationLevels')
|
|
for policy in policies:
|
|
return (policy['properties']['levels']
|
|
.get(inst_level, {})
|
|
.get('scale_info', {}))
|
|
return {}
|
|
|
|
def get_max_scale_level(self, flavour_id, aspect_id):
|
|
aspects = self.get_policy_values_by_type(flavour_id,
|
|
'tosca.policies.nfv.ScalingAspects')
|
|
for aspect in aspects:
|
|
value = aspect['properties']['aspects'].get(aspect_id)
|
|
if value is not None:
|
|
return value['max_scale_level']
|
|
|
|
# should not occur
|
|
return 0
|