From 8b479f42682de16fbb394df05064e10ba925a8a3 Mon Sep 17 00:00:00 2001 From: Tim Rozet Date: Fri, 1 Jul 2016 16:13:59 -0400 Subject: [PATCH] Implements VNFFG into NFVO implements blueprint: tacker-vnffg Change-Id: I9e2fd8b14fd6eaf05aa7813b5f0fab6daa4abd43 Signed-off-by: Tim Rozet --- requirements.txt | 1 + .../versions/507122918800_adds_vnffg.py | 145 +++ .../alembic_migrations/versions/HEAD | 2 +- tacker/db/migration/models/head.py | 1 + tacker/db/nfvo/vnffg_db.py | 908 ++++++++++++++++++ tacker/extensions/nfvo.py | 397 ++++++++ tacker/extensions/nfvo_plugins/__init__.py | 0 tacker/extensions/nfvo_plugins/vnffg.py | 83 ++ .../nfvo/drivers/vim/abstract_vim_driver.py | 11 + tacker/nfvo/drivers/vim/openstack_driver.py | 69 +- tacker/nfvo/drivers/vnffg/sfc_drivers/noop.py | 72 ++ tacker/nfvo/nfvo_plugin.py | 224 ++++- tacker/tests/unit/db/utils.py | 32 + .../data/tosca_invalid_vnffgd_template.yaml | 41 + .../heat/data/tosca_vnffgd_template.yaml | 41 + .../heat/data/vnffgd_template.yaml | 32 + tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py | 286 +++++- tacker/vnfm/tosca/lib/tacker_nfv_defs.yaml | 170 ++++ 18 files changed, 2508 insertions(+), 7 deletions(-) create mode 100644 tacker/db/migration/alembic_migrations/versions/507122918800_adds_vnffg.py create mode 100644 tacker/db/nfvo/vnffg_db.py create mode 100644 tacker/extensions/nfvo_plugins/__init__.py create mode 100644 tacker/extensions/nfvo_plugins/vnffg.py create mode 100644 tacker/nfvo/drivers/vnffg/sfc_drivers/noop.py create mode 100644 tacker/tests/unit/vm/infra_drivers/heat/data/tosca_invalid_vnffgd_template.yaml create mode 100644 tacker/tests/unit/vm/infra_drivers/heat/data/tosca_vnffgd_template.yaml create mode 100644 tacker/tests/unit/vm/infra_drivers/heat/data/vnffgd_template.yaml diff --git a/requirements.txt b/requirements.txt index a74582b05..52a15237d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,6 +37,7 @@ oslo.serialization>=1.10.0 # Apache-2.0 oslo.service>=1.10.0 # Apache-2.0 oslo.utils>=3.16.0 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 +python-neutronclient>=5.1.0 # Apache-2.0 python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 tosca-parser>=0.5.0 # Apache-2.0 heat-translator>=0.4.0 # Apache-2.0 diff --git a/tacker/db/migration/alembic_migrations/versions/507122918800_adds_vnffg.py b/tacker/db/migration/alembic_migrations/versions/507122918800_adds_vnffg.py new file mode 100644 index 000000000..641a5223b --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/507122918800_adds_vnffg.py @@ -0,0 +1,145 @@ +# Copyright 2016 OpenStack Foundation +# +# 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. +# + +"""adds_VNFFG + +Revision ID: 507122918800 +Revises: 4ee19c8a6d0a +Create Date: 2016-07-29 21:48:18.816277 + +""" + +# revision identifiers, used by Alembic. +revision = '507122918800' +down_revision = '4ee19c8a6d0a' + +import sqlalchemy as sa + +from alembic import op +from tacker.db.types import Json + + +def upgrade(active_plugins=None, options=None): + + op.create_table( + 'vnffgtemplates', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('template', Json), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table( + 'vnffgs', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=64), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('vnffgd_id', sa.String(length=36), nullable=False), + sa.Column('status', sa.String(length=255), nullable=False), + sa.Column('vnf_mapping', Json), + sa.ForeignKeyConstraint(['vnffgd_id'], ['vnffgtemplates.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table( + 'vnffgnfps', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=64), nullable=False), + sa.Column('vnffg_id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=False), + sa.Column('status', sa.String(length=255), nullable=False), + sa.Column('path_id', sa.String(length=255), nullable=False), + sa.Column('symmetrical', sa.Boolean, default=False), + sa.ForeignKeyConstraint(['vnffg_id'], ['vnffgs.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table( + 'vnffgchains', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=64), nullable=False), + sa.Column('instance_id', sa.String(length=255), nullable=True), + sa.Column('nfp_id', sa.String(length=36), nullable=False), + sa.Column('status', sa.String(length=255), nullable=False), + sa.Column('path_id', sa.String(length=255), nullable=False), + sa.Column('symmetrical', sa.Boolean, default=False), + sa.Column('chain', Json), + sa.ForeignKeyConstraint(['nfp_id'], ['vnffgnfps.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table( + 'vnffgclassifiers', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('tenant_id', sa.String(length=64), nullable=False), + sa.Column('nfp_id', sa.String(length=36), nullable=False), + sa.Column('instance_id', sa.String(length=255), nullable=True), + sa.Column('chain_id', sa.String(length=36), nullable=False), + sa.Column('status', sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint(['nfp_id'], ['vnffgnfps.id'], ), + sa.ForeignKeyConstraint(['chain_id'], ['vnffgchains.id'], ), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) + + op.create_table( + 'aclmatchcriterias', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('vnffgc_id', sa.String(length=36), nullable=False), + sa.Column('eth_src', sa.String(length=36), nullable=True), + sa.Column('eth_dst', sa.String(length=36), nullable=True), + sa.Column('eth_type', sa.String(length=36), nullable=True), + sa.Column('vlan_id', sa.Integer, nullable=True), + sa.Column('vlan_pcp', sa.Integer, nullable=True), + sa.Column('mpls_label', sa.Integer, nullable=True), + sa.Column('mpls_tc', sa.Integer, nullable=True), + sa.Column('ip_dscp', sa.Integer, nullable=True), + sa.Column('ip_ecn', sa.Integer, nullable=True), + sa.Column('ip_src_prefix', sa.String(length=36), nullable=True), + sa.Column('ip_dst_prefix', sa.String(length=36), nullable=True), + sa.Column('source_port_min', sa.Integer, nullable=True), + sa.Column('source_port_max', sa.Integer, nullable=True), + sa.Column('destination_port_min', sa.Integer, nullable=True), + sa.Column('destination_port_max', sa.Integer, nullable=True), + sa.Column('ip_proto', sa.Integer, nullable=True), + sa.Column('network_id', sa.String(length=36), nullable=True), + sa.Column('network_src_port_id', sa.String(length=36), nullable=True), + sa.Column('network_dst_port_id', sa.String(length=36), nullable=True), + sa.Column('tenant_id', sa.String(length=64), nullable=True), + sa.Column('icmpv4_type', sa.Integer, nullable=True), + sa.Column('icmpv4_code', sa.Integer, nullable=True), + sa.Column('arp_op', sa.Integer, nullable=True), + sa.Column('arp_spa', sa.Integer, nullable=True), + sa.Column('arp_tpa', sa.Integer, nullable=True), + sa.Column('arp_sha', sa.Integer, nullable=True), + sa.Column('arp_tha', sa.Integer, nullable=True), + sa.Column('ipv6_src', sa.String(36), nullable=True), + sa.Column('ipv6_dst', sa.String(36), nullable=True), + sa.Column('ipv6_flabel', sa.Integer, nullable=True), + sa.Column('icmpv6_type', sa.Integer, nullable=True), + sa.Column('icmpv6_code', sa.Integer, nullable=True), + sa.Column('ipv6_nd_target', sa.String(36), nullable=True), + sa.Column('ipv6_nd_sll', sa.String(36), nullable=True), + sa.Column('ipv6_nd_tll', sa.String(36), nullable=True), + sa.ForeignKeyConstraint(['vnffgc_id'], ['vnffgclassifiers.id'], ), + sa.PrimaryKeyConstraint('id'), + ) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index 021b3bd5b..1005058d1 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -4ee19c8a6d0a \ No newline at end of file +507122918800 diff --git a/tacker/db/migration/models/head.py b/tacker/db/migration/models/head.py index 0d274e4ed..efa163e2c 100644 --- a/tacker/db/migration/models/head.py +++ b/tacker/db/migration/models/head.py @@ -23,6 +23,7 @@ Based on this comparison database can be healed with healing migration. from tacker.db import model_base from tacker.db.nfvo import nfvo_db # noqa +from tacker.db.nfvo import vnffg_db # noqa from tacker.db.vm import vm_db # noqa diff --git a/tacker/db/nfvo/vnffg_db.py b/tacker/db/nfvo/vnffg_db.py new file mode 100644 index 000000000..102097053 --- /dev/null +++ b/tacker/db/nfvo/vnffg_db.py @@ -0,0 +1,908 @@ +# Copyright 2016 Red Hat Inc +# 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 random +import sqlalchemy as sa +import uuid + +from oslo_log import log as logging +from sqlalchemy import orm +from sqlalchemy.orm import exc as orm_exc +from tacker._i18n import _ +from tacker.db import db_base +from tacker.db import model_base +from tacker.db import models_v1 +from tacker.db import types +from tacker.extensions import nfvo +from tacker.extensions.nfvo_plugins import vnffg +from tacker import manager +from tacker.plugins.common import constants + + +LOG = logging.getLogger(__name__) +_ACTIVE_UPDATE = (constants.ACTIVE, constants.PENDING_UPDATE) +_ACTIVE_UPDATE_ERROR_DEAD = ( + constants.PENDING_CREATE, constants.ACTIVE, constants.PENDING_UPDATE, + constants.ERROR, constants.DEAD) +_VALID_VNFFG_UPDATE_ATTRIBUTES = ('name', 'description', 'vnf_mapping') +_VALID_SFC_UPDATE_ATTRIBUTES = ('chain', 'symmetrical') +_VALID_FC_UPDATE_ATTRIBUTES = () +MATCH_CRITERIA = ( + 'eth_type', 'eth_src', 'eth_dst', 'vlan_id', 'vlan_pcp', 'mpls_label', + 'mpls_tc', 'ip_dscp', 'ip_ecn', 'ip_src_prefix', 'ip_dst_prefix', + 'ip_proto', 'destination_port_range', 'source_port_range', + 'network_src_port_id', 'network_dst_port_id', 'network_id', 'network_name', + 'tenant_id', 'icmpv4_type', 'icmpv4_code', 'arp_op', 'arp_spa', + 'arp_tpa', 'arp_sha', 'arp_tha', 'ipv6_src', 'ipv6_dst', 'ipv6_flabel', + 'icmpv6_type', 'icmpv6_code', 'ipv6_nd_target', 'ipv6_nd_sll', + 'ipv6_nd_tll') + +MATCH_DB_KEY_LIST = ( + 'eth_type', 'eth_src', 'eth_dst', 'vlan_id', 'vlan_pcp', 'mpls_label', + 'mpls_tc', 'ip_dscp', 'ip_ecn', 'ip_src_prefix', 'ip_dst_prefix', + 'ip_proto', 'destination_port_min', 'destination_port_max', + 'source_port_min', 'source_port_max', 'network_src_port_id', + 'network_dst_port_id', 'network_id', 'tenant_id', 'icmpv4_type', + 'icmpv4_code', 'arp_op', 'arp_spa', 'arp_tpa', 'arp_sha', 'arp_tha', + 'ipv6_src', 'ipv6_dst', 'ipv6_flabel', 'icmpv6_type', 'icmpv6_code', + 'ipv6_nd_target', 'ipv6_nd_sll', 'ipv6_nd_tll' +) + +CP = 'connection_points' + + +class VnffgTemplate(model_base.BASE, models_v1.HasId, models_v1.HasTenant): + """Represents template to create a VNF Forwarding Graph.""" + + # Descriptive name + name = sa.Column(sa.String(255), nullable=False) + description = sa.Column(sa.Text) + + # Vnffg template + template = sa.Column(types.Json) + + +class Vnffg(model_base.BASE, models_v1.HasTenant, models_v1.HasId): + """VNF Forwarding Graph Data Model""" + + name = sa.Column(sa.String(255), nullable=False) + description = sa.Column(sa.String(255), nullable=True) + + # List of associated NFPs + forwarding_paths = orm.relationship("VnffgNfp", backref="vnffg") + + vnffgd_id = sa.Column(types.Uuid, sa.ForeignKey('vnffgtemplates.id')) + vnffgd = orm.relationship('VnffgTemplate') + + status = sa.Column(sa.String(255), nullable=False) + + # Mapping of VNFD to VNF instance names + vnf_mapping = sa.Column(types.Json) + + +class VnffgNfp(model_base.BASE, models_v1.HasTenant, models_v1.HasId): + """Network Forwarding Path Data Model""" + + name = sa.Column(sa.String(255), nullable=False) + vnffg_id = sa.Column(types.Uuid, sa.ForeignKey('vnffgs.id'), + nullable=False) + classifier = orm.relationship('VnffgClassifier', backref='nfp', + uselist=False) + chain = orm.relationship('VnffgChain', backref='nfp', + uselist=False) + + status = sa.Column(sa.String(255), nullable=False) + path_id = sa.Column(sa.String(255), nullable=False) + + # symmetry of forwarding path + symmetrical = sa.Column(sa.Boolean(), default=False) + + +class VnffgChain(model_base.BASE, models_v1.HasTenant, models_v1.HasId): + """Service Function Chain Data Model""" + + status = sa.Column(sa.String(255), nullable=False) + + instance_id = sa.Column(sa.String(255), nullable=True) + + # symmetry of forwarding path + symmetrical = sa.Column(sa.Boolean(), default=False) + + # chain + chain = sa.Column(types.Json) + + path_id = sa.Column(sa.String(255), nullable=False) + nfp_id = sa.Column(types.Uuid, sa.ForeignKey('vnffgnfps.id')) + + +class VnffgClassifier(model_base.BASE, models_v1.HasTenant, models_v1.HasId): + """VNFFG NFP Classifier Data Model""" + + status = sa.Column(sa.String(255), nullable=False) + + instance_id = sa.Column(sa.String(255), nullable=True) + + chain_id = sa.Column(types.Uuid, sa.ForeignKey('vnffgchains.id')) + chain = orm.relationship('VnffgChain', backref='classifier', + uselist=False, foreign_keys=[chain_id]) + nfp_id = sa.Column(types.Uuid, sa.ForeignKey('vnffgnfps.id')) + # match criteria + match = orm.relationship('ACLMatchCriteria') + + +class ACLMatchCriteria(model_base.BASE, models_v1.HasId): + """Represents ACL match criteria of a classifier.""" + + vnffgc_id = sa.Column(types.Uuid, sa.ForeignKey('vnffgclassifiers.id')) + eth_src = sa.Column(sa.String(36), nullable=True) + eth_dst = sa.Column(sa.String(36), nullable=True) + eth_type = sa.Column(sa.String(36), nullable=True) + vlan_id = sa.Column(sa.Integer, nullable=True) + vlan_pcp = sa.Column(sa.Integer, nullable=True) + mpls_label = sa.Column(sa.Integer, nullable=True) + mpls_tc = sa.Column(sa.Integer, nullable=True) + ip_dscp = sa.Column(sa.Integer, nullable=True) + ip_ecn = sa.Column(sa.Integer, nullable=True) + ip_src_prefix = sa.Column(sa.String(36), nullable=True) + ip_dst_prefix = sa.Column(sa.String(36), nullable=True) + source_port_min = sa.Column(sa.Integer, nullable=True) + source_port_max = sa.Column(sa.Integer, nullable=True) + destination_port_min = sa.Column(sa.Integer, nullable=True) + destination_port_max = sa.Column(sa.Integer, nullable=True) + ip_proto = sa.Column(sa.Integer, nullable=True) + network_id = sa.Column(types.Uuid, nullable=True) + network_src_port_id = sa.Column(types.Uuid, nullable=True) + network_dst_port_id = sa.Column(types.Uuid, nullable=True) + tenant_id = sa.Column(sa.String(64), nullable=True) + icmpv4_type = sa.Column(sa.Integer, nullable=True) + icmpv4_code = sa.Column(sa.Integer, nullable=True) + arp_op = sa.Column(sa.Integer, nullable=True) + arp_spa = sa.Column(sa.String(36), nullable=True) + arp_tpa = sa.Column(sa.String(36), nullable=True) + arp_sha = sa.Column(sa.String(36), nullable=True) + arp_tha = sa.Column(sa.String(36), nullable=True) + ipv6_src = sa.Column(sa.String(36), nullable=True) + ipv6_dst = sa.Column(sa.String(36), nullable=True) + ipv6_flabel = sa.Column(sa.Integer, nullable=True) + icmpv6_type = sa.Column(sa.Integer, nullable=True) + icmpv6_code = sa.Column(sa.Integer, nullable=True) + ipv6_nd_target = sa.Column(sa.String(36), nullable=True) + ipv6_nd_sll = sa.Column(sa.String(36), nullable=True) + ipv6_nd_tll = sa.Column(sa.String(36), nullable=True) + + +class VnffgPluginDbMixin(vnffg.VNFFGPluginBase, db_base.CommonDbMixin): + + def __init__(self): + super(VnffgPluginDbMixin, self).__init__() + + def create_vnffg(self, context, vnffg): + vnffg_dict = self._create_vnffg_pre(context, vnffg) + sfc_instance = str(uuid.uuid4()) + fc_instance = str(uuid.uuid4()) + self._create_vnffg_post(context, sfc_instance, + fc_instance, vnffg_dict) + self._create_vnffg_status(context, vnffg_dict) + return vnffg_dict + + def get_vnffg(self, context, vnffg_id, fields=None): + vnffg_db = self._get_resource(context, Vnffg, vnffg_id) + return self._make_vnffg_dict(vnffg_db, fields) + + def get_vnffgs(self, context, filters=None, fields=None): + return self._get_collection(context, Vnffg, self._make_vnffg_dict, + filters=filters, fields=fields) + + def update_vnffg(self, context, vnffg_id, vnffg): + vnffg_dict = self._update_vnffg_pre(context, vnffg_id) + self._update_vnffg_post(context, vnffg_id, constants.ACTIVE, vnffg) + return vnffg_dict + + def delete_vnffg(self, context, vnffg_id): + self._delete_vnffg_pre(context, vnffg_id) + self._delete_vnffg_post(context, vnffg_id, False) + + def create_vnffgd(self, context, vnffgd): + template = vnffgd['vnffgd'] + LOG.debug(_('template %s'), template) + tenant_id = self._get_tenant_id_for_create(context, template) + + with context.session.begin(subtransactions=True): + template_id = str(uuid.uuid4()) + template_db = VnffgTemplate( + id=template_id, + tenant_id=tenant_id, + name=template.get('name'), + description=template.get('description'), + template=template.get('template')) + context.session.add(template_db) + + LOG.debug(_('template_db %(template_db)s'), + {'template_db': template_db}) + return self._make_template_dict(template_db) + + def get_vnffgd(self, context, vnffgd_id, fields=None): + template_db = self._get_resource(context, VnffgTemplate, + vnffgd_id) + return self._make_template_dict(template_db, fields) + + def get_vnffgds(self, context, filters=None, fields=None): + return self._get_collection(context, VnffgTemplate, + self._make_template_dict, + filters=filters, fields=fields) + + def delete_vnffgd(self, context, vnffgd_id): + with context.session.begin(subtransactions=True): + vnffg_db = context.session.query(Vnffg).filter_by( + vnffgd_id=vnffgd_id).first() + if vnffg_db is not None: + raise nfvo.VnffgdInUse(vnffgd_id=vnffgd_id) + + template_db = self._get_resource(context, VnffgTemplate, + vnffgd_id) + context.session.delete(template_db) + + def get_classifier(self, context, classifier_id, fields=None): + classifier_db = self._get_resource(context, VnffgClassifier, + classifier_id) + return self._make_classifier_dict(classifier_db, fields) + + def get_classifiers(self, context, filters=None, fields=None): + return self._get_collection(context, VnffgClassifier, + self._make_classifier_dict, + filters=filters, fields=fields) + + def get_nfp(self, context, nfp_id, fields=None): + nfp_db = self._get_resource(context, VnffgNfp, nfp_id) + return self._make_nfp_dict(nfp_db, fields) + + def get_nfps(self, context, filters=None, fields=None): + return self._get_collection(context, VnffgNfp, + self._make_nfp_dict, + filters=filters, fields=fields) + + def get_sfc(self, context, sfc_id, fields=None): + chain_db = self._get_resource(context, VnffgChain, sfc_id) + return self._make_chain_dict(chain_db, fields) + + def get_sfcs(self, context, filters=None, fields=None): + return self._get_collection(context, VnffgChain, + self._make_chain_dict, + filters=filters, fields=fields) + + # called internally, not by REST API + def _create_vnffg_pre(self, context, vnffg): + vnffg = vnffg['vnffg'] + LOG.debug(_('vnffg %s'), vnffg) + tenant_id = self._get_tenant_id_for_create(context, vnffg) + name = vnffg.get('name') + vnffg_id = vnffg.get('id') or str(uuid.uuid4()) + template_id = vnffg['vnffgd_id'] + symmetrical = vnffg['symmetrical'] + + with context.session.begin(subtransactions=True): + template_db = self._get_resource(context, VnffgTemplate, + template_id) + LOG.debug(_('vnffg template %s'), template_db) + vnf_members = self._get_vnffg_property(template_db, + 'constituent_vnfs') + LOG.debug(_('Constituent VNFs: %s'), vnf_members) + vnf_mapping = self._get_vnf_mapping(context, vnffg.get( + 'vnf_mapping'), vnf_members) + LOG.debug(_('VNF Mapping: %s'), vnf_mapping) + # create NFP dict + nfp_dict = self._create_nfp_pre(template_db) + vnffg_db = Vnffg(id=vnffg_id, + tenant_id=tenant_id, + name=name, + description=template_db.description, + vnf_mapping=vnf_mapping, + vnffgd_id=template_id, + status=constants.PENDING_CREATE) + context.session.add(vnffg_db) + + nfp_id = str(uuid.uuid4()) + sfc_id = str(uuid.uuid4()) + classifier_id = str(uuid.uuid4()) + + nfp_db = VnffgNfp(id=nfp_id, vnffg_id=vnffg_id, + tenant_id=tenant_id, + name=nfp_dict['name'], + status=constants.PENDING_CREATE, + path_id=nfp_dict['path_id'], + symmetrical=symmetrical) + context.session.add(nfp_db) + + chain = self._create_port_chain(context, vnf_mapping, template_db, + nfp_dict['name']) + LOG.debug(_('chain: %s'), chain) + sfc_db = VnffgChain(id=sfc_id, + tenant_id=tenant_id, + status=constants.PENDING_CREATE, + symmetrical=symmetrical, + chain=chain, + nfp_id=nfp_id, + path_id=nfp_dict['path_id']) + + context.session.add(sfc_db) + + sfcc_db = VnffgClassifier(id=classifier_id, + tenant_id=tenant_id, + status=constants.PENDING_CREATE, + nfp_id=nfp_id, + chain_id=sfc_id) + context.session.add(sfcc_db) + + match = self._policy_to_acl_criteria(context, template_db, + nfp_dict['name'], + vnf_mapping) + LOG.debug(_('acl_match %s'), match) + + match_db_table = ACLMatchCriteria( + id=str(uuid.uuid4()), + vnffgc_id=classifier_id, + **match) + + context.session.add(match_db_table) + + return self._make_vnffg_dict(vnffg_db) + + @staticmethod + def _create_nfp_pre(template_db): + template = template_db.template['vnffgd']['topology_template'] + nfp_dict = dict() + vnffg_name = template['groups'].keys()[0] + # we assume only one NFP for initial implementation + nfp_dict['name'] = template['groups'][vnffg_name]['members'][0] + nfp_dict['path_id'] = template['node_templates'][nfp_dict['name']][ + 'properties']['id'] + + if not nfp_dict['path_id']: + # TODO(trozet): do we need to check if this path ID is already + # taken by another VNFFG + nfp_dict['path_id'] = random.randint(1, 16777216) + + return nfp_dict + + def _create_port_chain(self, context, vnf_mapping, template_db, nfp_name): + """Creates a list of physical port ids to represent an ordered chain + + :param context: SQL session context + :param vnf_mapping: dict of VNFD to VNF instance mappings + :param template_db: VNFFG Descriptor + :param nfp_name: name of the forwarding path with chain requirements + :return: list of port chain including vnf name and list of CPs + """ + chain_list = [] + prev_forwarder = None + vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] + # Build the list of logical chain representation + logical_chain = self._get_nfp_attribute(template_db.template, + nfp_name, 'path') + # Build physical port chain + for element in logical_chain: + if element['forwarder'] not in vnf_mapping.keys(): + raise nfvo.NfpForwarderNotFoundException(vnfd=element[ + 'forwarder'], + mapping=vnf_mapping) + # TODO(trozet): validate CP in VNFD has forwarding capability + # Find VNF resources + vnf = vnfm_plugin.get_vnf_resources(context, + vnf_mapping[element[ + 'forwarder']] + ) + vnf_info = vnfm_plugin.get_vnf(context, + vnf_mapping[element['forwarder']]) + vnf_cp = None + for resource in vnf: + if resource['name'] == element['capability']: + vnf_cp = resource['id'] + break + if vnf_cp is None: + raise nfvo.VnffgCpNotFoundException(cp_id=element[ + 'capability'], vnf_id=vnf_mapping[element['forwarder']]) + # Check if this is a new VNF entry in the chain + if element['forwarder'] != prev_forwarder: + chain_list.append({'name': vnf_info['name'], + CP: [vnf_cp]}) + prev_forwarder = element['forwarder'] + # Must be an egress CP + else: + if len(chain_list[-1][CP]) > 1: + raise nfvo.NfpRequirementsException(vnfd=element[ + 'forwarder']) + else: + chain_list[-1]['connection_points'].append(vnf_cp) + + return chain_list + + @staticmethod + def _get_vnffg_property(template_db, vnffg_property): + template = template_db.template['vnffgd']['topology_template'] + vnffg_name = template['groups'].keys()[0] + try: + return template['groups'][vnffg_name]['properties'][vnffg_property] + except KeyError: + raise nfvo.VnffgPropertyNotFoundException( + vnffg_property=vnffg_property) + + @staticmethod + def _get_nfp_attribute(template, nfp, attribute): + """Finds any attribute of an NFP described in a template + + :param template: VNFFGD template + :param nfp: name of NFP + :param attribute: attribute to find + :return: value of attribute from template + """ + template = template['vnffgd']['topology_template'] + try: + attr_val = VnffgPluginDbMixin._search_value( + template['node_templates'][nfp], attribute) + if attr_val is None: + print(template['node_templates'][nfp]) + raise nfvo.NfpAttributeNotFoundException(attribute=attribute) + else: + return attr_val + except KeyError: + raise nfvo.NfpAttributeNotFoundException(attribute=attribute) + + @staticmethod + def _search_value(search_dict, search_key): + for k, v in search_dict.iteritems(): + if k == search_key: + return v + elif isinstance(v, dict): + val = VnffgPluginDbMixin._search_value(v, search_key) + if val is not None: + return val + + def _get_vnf_mapping(self, context, vnf_mapping, vnf_members): + """Creates/validates a mapping of VNFD names to VNF IDs for NFP. + + :param context: SQL session context + :param vnf_mapping: dict of requested VNFD:VNF_ID mappings + :param vnf_members: list of constituent VNFs from a VNFFG + :return: dict of VNFD:VNF_ID mappings + """ + vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] + new_mapping = dict() + + for vnfd in vnf_members: + # there should only be one ID returned for a unique name + try: + vnfd_id = vnfm_plugin.get_vnfds(context, {'name': [vnfd]}, + fields=['id']).pop()['id'] + except Exception: + raise nfvo.VnffgdVnfdNotFoundException(vnfd_name=vnfd) + if vnfd_id is None: + raise nfvo.VnffgdVnfdNotFoundException(vnfd_name=vnfd) + else: + # if no VNF mapping, we need to abstractly look for instances + # that match VNFD + if vnf_mapping is None or vnfd not in vnf_mapping.keys(): + # find suitable VNFs from vnfd_id + LOG.debug(_('Searching VNFS with id %s'), vnfd_id) + vnf_list = vnfm_plugin.get_vnfs(context, + {'vnfd_id': [vnfd_id]}, + fields=['id']) + if vnf_list is None: + raise nfvo.VnffgInvalidMappingException(vnfd_name=vnfd) + else: + LOG.debug(_('Matching VNFs found %s'), vnf_list) + vnf_list = [vnf['id'] for vnf in vnf_list] + if len(vnf_list) > 1: + new_mapping[vnfd] = random.choice(vnf_list) + else: + new_mapping[vnfd] = vnf_list[0] + # if VNF mapping, validate instances exist and match the VNFD + else: + vnf_vnfd = vnfm_plugin.get_vnf(context, vnf_mapping[vnfd], + fields=['vnfd_id']) + if vnf_vnfd is not None: + vnf_vnfd_id = vnf_vnfd['vnfd_id'] + else: + raise nfvo.VnffgInvalidMappingException(vnfd_name=vnfd) + if vnfd_id != vnf_vnfd_id: + raise nfvo.VnffgInvalidMappingException(vnfd_name=vnfd) + else: + new_mapping[vnfd] = vnf_mapping.pop(vnfd) + self._validate_vim(context, new_mapping.values()) + return new_mapping + + def _validate_vim(self, context, vnfs): + """Validates all VNFs are in the same VIM + + :param context: SQL Session Context + :param vnfs: List of VNF instance IDs + :return: None + """ + LOG.debug(_('validating vim for vnfs %s'), vnfs) + vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] + vim_id = None + for vnf in vnfs: + vnf_dict = vnfm_plugin.get_vnf(context, vnf) + if vim_id is None: + vim_id = vnf_dict['vim_id'] + elif vnf_dict['vim_id'] != vim_id: + raise nfvo.VnffgVimMappingException(vnf_id=vnf, vim_id=vim_id) + + def _policy_to_acl_criteria(self, context, template_db, nfp_name, + vnf_mapping): + template = template_db.template['vnffgd']['topology_template'] + nfp = template['node_templates'][nfp_name] + try: + policy = nfp['properties']['policy'] + except KeyError: + raise nfvo.NfpPolicyNotFoundException(policy=nfp) + + if 'type' in policy: + if policy['type'] != 'ACL': + raise nfvo.NfpPolicyTypeError(type=policy['type']) + + if 'criteria' not in policy: + raise nfvo.NfpPolicyCriteriaError(error="Missing criteria in " + "policy") + match = dict() + for criteria in policy['criteria']: + for key, val in criteria.iteritems(): + if key in MATCH_CRITERIA: + match.update(self._convert_criteria(context, key, val, + vnf_mapping)) + else: + raise nfvo.NfpPolicyCriteriaError(error="Unsupported " + "criteria: " + "{}".format(key)) + return match + + def _convert_criteria(self, context, criteria, value, vnf_mapping): + """Method is used to convert criteria to proper db value from template + + :param context: SQL session context + :param criteria: input criteria name + :param value: input value + :param vnf_mapping: mapping of VNFD to VNF instances + :return: converted dictionary + """ + + if criteria.endswith('_range'): + prefix = criteria[:-6] + criteria_min = prefix + "_min" + criteria_max = prefix + "_max" + try: + min_val, max_val = value.split('-') + except ValueError: + raise nfvo.NfpPolicyCriteriaError(error="Range missing or " + "incorrect for " + "%s".format(criteria)) + return {criteria_min: int(min_val), criteria_max: int(max_val)} + + elif criteria.endswith('_name'): + prefix = criteria[:-5] + vnf_id = vnf_mapping.values()[0] + new_value = self._vim_resource_name_to_id(context, prefix, value, + vnf_id) + new_name = prefix + "_id" + return {new_name: new_value} + + else: + return {criteria: value} + + def _vim_resource_name_to_id(self, context, resource, name, vnf_id): + """Converts a VIM resource name to its ID + + :param context: SQL session context + :param resource: resource type to find (network, subnet, etc) + :param name: name of the resource to find its ID + :param vnf_id: A VNF instance ID that is part of the chain to which + the classifier will apply to + :return: ID of the resource name + """ + # this should be overridden with driver call to find ID given name + # for resource + return str(uuid.uuid4()) + + # called internally, not by REST API + # instance_id = None means error on creation + def _create_vnffg_post(self, context, sfc_instance_id, + fc_instance_id, vnffg_dict): + LOG.debug(_('SFC created instance is %s'), sfc_instance_id) + LOG.debug(_('Flow Classifier created instance is %s'), + fc_instance_id) + nfp_dict = self.get_nfp(context, vnffg_dict['forwarding_paths']) + sfc_id = nfp_dict['chain_id'] + classifier_id = nfp_dict['classifier_id'] + with context.session.begin(subtransactions=True): + query = (self._model_query(context, VnffgChain). + filter(VnffgChain.id == sfc_id). + filter(VnffgChain.status == constants.PENDING_CREATE). + one()) + query.update({'instance_id': sfc_instance_id}) + if sfc_instance_id is None: + query.update({'status': constants.ERROR}) + else: + query.update({'status': constants.ACTIVE}) + + query = (self._model_query(context, VnffgClassifier). + filter(VnffgClassifier.id == classifier_id). + filter(VnffgClassifier.status == + constants.PENDING_CREATE). + one()) + query.update({'instance_id': fc_instance_id}) + + if fc_instance_id is None: + query.update({'status': constants.ERROR}) + else: + query.update({'status': constants.ACTIVE}) + + def _create_vnffg_status(self, context, vnffg): + nfp = self.get_nfp(context, vnffg['forwarding_paths']) + chain = self.get_sfc(context, nfp['chain_id']) + classifier = self.get_classifier(context, nfp['classifier_id']) + + if classifier['status'] == constants.ERROR or chain['status'] ==\ + constants.ERROR: + self._update_all_status(context, vnffg['id'], nfp['id'], + constants.ERROR) + elif classifier['status'] == constants.ACTIVE and \ + chain['status'] == constants.ACTIVE: + self._update_all_status(context, vnffg['id'], nfp['id'], + constants.ACTIVE) + + def _update_all_status(self, context, vnffg_id, nfp_id, status): + with context.session.begin(subtransactions=True): + query = (self._model_query(context, Vnffg). + filter(Vnffg.id == vnffg_id)) + query.update({'status': status}) + nfp_query = (self._model_query(context, VnffgNfp). + filter(VnffgNfp.id == nfp_id)) + nfp_query.update({'status': status}) + + def _make_vnffg_dict(self, vnffg_db, fields=None): + LOG.debug(_('vnffg_db %s'), vnffg_db) + LOG.debug(_('vnffg_db nfp %s'), vnffg_db.forwarding_paths) + res = { + 'forwarding_paths': vnffg_db.forwarding_paths[0]['id'] + } + key_list = ('id', 'tenant_id', 'name', 'description', + 'vnf_mapping', 'status', 'vnffgd_id') + res.update((key, vnffg_db[key]) for key in key_list) + return self._fields(res, fields) + + def _update_vnffg_pre(self, context, vnffg_id): + vnffg = self.get_vnffg(context, vnffg_id) + nfp = self.get_nfp(context, vnffg['forwarding_paths']) + sfc = self.get_sfc(context, nfp['chain_id']) + fc = self.get_classifier(context, nfp['classifier_id']) + with context.session.begin(subtransactions=True): + vnffg_db = self._get_vnffg_db(context, vnffg['id'], _ACTIVE_UPDATE, + constants.PENDING_UPDATE) + self._get_nfp_db(context, nfp['id'], _ACTIVE_UPDATE, + constants.PENDING_UPDATE) + self._get_sfc_db(context, sfc['id'], _ACTIVE_UPDATE, + constants.PENDING_UPDATE) + self._get_classifier_db(context, fc['id'], _ACTIVE_UPDATE, + constants.PENDING_UPDATE) + return self._make_vnffg_dict(vnffg_db) + + def _update_vnffg_post(self, context, vnffg_id, new_status, + new_vnffg=None): + vnffg = self.get_vnffg(context, vnffg_id) + nfp = self.get_nfp(context, vnffg['forwarding_paths']) + with context.session.begin(subtransactions=True): + query = (self._model_query(context, Vnffg). + filter(Vnffg.id == vnffg['id']). + filter(Vnffg.status == constants.PENDING_UPDATE)) + query.update({'status': new_status}) + + nfp_query = (self._model_query(context, VnffgNfp). + filter(VnffgNfp.id == nfp['id']). + filter(VnffgNfp.status == constants.PENDING_UPDATE)) + nfp_query.update({'status': new_status}) + + if new_vnffg is not None: + for key in _VALID_VNFFG_UPDATE_ATTRIBUTES: + query.update({key: new_vnffg[key]}) + nfp_query.update({'symmetrical': new_vnffg['symmetrical']}) + + def _update_sfc_post(self, context, sfc_id, new_status, new_sfc=None): + with context.session.begin(subtransactions=True): + sfc_query = (self._model_query(context, VnffgChain). + filter(VnffgChain.id == sfc_id). + filter(VnffgChain.status == constants.PENDING_UPDATE)) + sfc_query.update({'status': new_status}) + + if new_sfc is not None: + for key in _VALID_SFC_UPDATE_ATTRIBUTES: + sfc_query.update({key: new_sfc[key]}) + + def _update_classifier_post(self, context, sfc_id, new_status, + new_fc=None): + with context.session.begin(subtransactions=True): + fc_query = (self._model_query(context, VnffgClassifier). + filter(VnffgClassifier.id == sfc_id). + filter(VnffgClassifier.status == + constants.PENDING_UPDATE)) + fc_query.update({'status': new_status}) + + if new_fc is not None: + for key in _VALID_FC_UPDATE_ATTRIBUTES: + fc_query.update({key: new_fc[key]}) + + def _get_vnffg_db(self, context, vnffg_id, current_statuses, new_status): + try: + vnffg_db = ( + self._model_query(context, Vnffg). + filter(Vnffg.id == vnffg_id). + filter(Vnffg.status.in_(current_statuses)). + with_lockmode('update').one()) + except orm_exc.NoResultFound: + raise nfvo.VnffgNotFoundException(vnffg_id=vnffg_id) + if vnffg_db.status == constants.PENDING_UPDATE: + raise nfvo.VnffgInUse(vnffg_id=vnffg_id) + vnffg_db.update({'status': new_status}) + return vnffg_db + + def _get_nfp_db(self, context, nfp_id, current_statuses, new_status): + try: + nfp_db = ( + self._model_query(context, VnffgNfp). + filter(VnffgNfp.id == nfp_id). + filter(VnffgNfp.status.in_(current_statuses)). + with_lockmode('update').one()) + except orm_exc.NoResultFound: + raise nfvo.NfpNotFoundException(nfp_id=nfp_id) + if nfp_db.status == constants.PENDING_UPDATE: + raise nfvo.NfpInUse(nfp_id=nfp_id) + nfp_db.update({'status': new_status}) + return nfp_db + + def _get_sfc_db(self, context, sfc_id, current_statuses, new_status): + try: + sfc_db = ( + self._model_query(context, VnffgChain). + filter(VnffgChain.id == sfc_id). + filter(VnffgChain.status.in_(current_statuses)). + with_lockmode('update').one()) + except orm_exc.NoResultFound: + raise nfvo.SfcNotFoundException(sfc_id=sfc_id) + if sfc_db.status == constants.PENDING_UPDATE: + raise nfvo.SfcInUse(sfc_id=sfc_id) + sfc_db.update({'status': new_status}) + return sfc_db + + def _get_classifier_db(self, context, fc_id, current_statuses, new_status): + try: + fc_db = ( + self._model_query(context, VnffgClassifier). + filter(VnffgClassifier.id == fc_id). + filter(VnffgClassifier.status.in_(current_statuses)). + with_lockmode('update').one()) + except orm_exc.NoResultFound: + raise nfvo.ClassifierNotFoundException(fc_id=fc_id) + if fc_db.status == constants.PENDING_UPDATE: + raise nfvo.ClassifierInUse(fc_id=fc_id) + fc_db.update({'status': new_status}) + return fc_db + + def _delete_vnffg_pre(self, context, vnffg_id): + vnffg = self.get_vnffg(context, vnffg_id) + nfp = self.get_nfp(context, vnffg['forwarding_paths']) + chain = self.get_sfc(context, nfp['chain_id']) + classifier = self.get_classifier(context, nfp['classifier_id']) + with context.session.begin(subtransactions=True): + vnffg_db = self._get_vnffg_db( + context, vnffg['id'], _ACTIVE_UPDATE_ERROR_DEAD, + constants.PENDING_DELETE) + self._get_nfp_db(context, nfp['id'], _ACTIVE_UPDATE_ERROR_DEAD, + constants.PENDING_DELETE) + self._get_sfc_db(context, chain['id'], _ACTIVE_UPDATE_ERROR_DEAD, + constants.PENDING_DELETE) + self._get_classifier_db(context, classifier['id'], + _ACTIVE_UPDATE_ERROR_DEAD, + constants.PENDING_DELETE) + + return self._make_vnffg_dict(vnffg_db) + + def _delete_vnffg_post(self, context, vnffg_id, error): + vnffg = self.get_vnffg(context, vnffg_id) + nfp = self.get_nfp(context, vnffg['forwarding_paths']) + chain = self.get_sfc(context, nfp['chain_id']) + classifier = self.get_classifier(context, nfp['classifier_id']) + with context.session.begin(subtransactions=True): + vnffg_query = ( + self._model_query(context, Vnffg). + filter(Vnffg.id == vnffg['id']). + filter(Vnffg.status == constants.PENDING_DELETE)) + nfp_query = ( + self._model_query(context, VnffgNfp). + filter(VnffgNfp.id == nfp['id']). + filter(VnffgNfp.status == constants.PENDING_DELETE)) + sfc_query = ( + self._model_query(context, VnffgChain). + filter(VnffgChain.id == chain['id']). + filter(VnffgChain.status == constants.PENDING_DELETE)) + fc_query = ( + self._model_query(context, VnffgClassifier). + filter(VnffgClassifier.id == classifier['id']). + filter(VnffgClassifier.status == constants.PENDING_DELETE)) + match_query = ( + self._model_query(context, ACLMatchCriteria). + filter(ACLMatchCriteria.vnffgc_id == classifier['id'])) + if error: + vnffg_query.update({'status': constants.ERROR}) + nfp_query.update({'status': constants.ERROR}) + sfc_query.update({'status': constants.ERROR}) + fc_query.update({'status': constants.ERROR}) + else: + match_query.delete() + fc_query.delete() + sfc_query.delete() + nfp_query.delete() + vnffg_query.delete() + + def _make_template_dict(self, template, fields=None): + res = {} + key_list = ('id', 'tenant_id', 'name', 'description', 'template') + res.update((key, template[key]) for key in key_list) + return self._fields(res, fields) + + def _make_acl_match_dict(self, acl_match_db): + key_list = MATCH_DB_KEY_LIST + return {key: entry[key] for key in key_list for entry in acl_match_db + if entry[key]} + + def _make_classifier_dict(self, classifier_db, fields=None): + LOG.debug(_('classifier_db %s'), classifier_db) + LOG.debug(_('classifier_db match %s'), classifier_db.match) + res = { + 'match': self._make_acl_match_dict(classifier_db.match) + } + key_list = ('id', 'tenant_id', 'instance_id', 'status', 'chain_id', + 'nfp_id') + res.update((key, classifier_db[key]) for key in key_list) + return self._fields(res, fields) + + def _make_nfp_dict(self, nfp_db, fields=None): + LOG.debug(_('nfp_db %s'), nfp_db) + res = {'chain_id': nfp_db.chain['id'], + 'classifier_id': nfp_db.classifier['id']} + key_list = ('name', 'id', 'tenant_id', 'symmetrical', 'status', + 'path_id') + res.update((key, nfp_db[key]) for key in key_list) + return self._fields(res, fields) + + def _make_chain_dict(self, chain_db, fields=None): + LOG.debug(_('chain_db %s'), chain_db) + res = {} + key_list = ('id', 'tenant_id', 'symmetrical', 'status', 'chain', + 'path_id', 'nfp_id', 'instance_id') + res.update((key, chain_db[key]) for key in key_list) + return self._fields(res, fields) + + def _get_resource(self, context, model, res_id): + try: + return self._get_by_id(context, model, res_id) + except orm_exc.NoResultFound: + if issubclass(model, Vnffg): + raise nfvo.VnffgNotFoundException(vnffg_id=res_id) + elif issubclass(model, VnffgClassifier): + raise nfvo.ClassifierNotFoundException(classifier_id=res_id) + if issubclass(model, VnffgTemplate): + raise nfvo.VnffgdNotFoundException(vnffgd_id=res_id) + if issubclass(model, VnffgChain): + raise nfvo.SfcNotFoundException(sfc_id=res_id) + else: + raise diff --git a/tacker/extensions/nfvo.py b/tacker/extensions/nfvo.py index c82031f79..e1e3601ed 100644 --- a/tacker/extensions/nfvo.py +++ b/tacker/extensions/nfvo.py @@ -17,6 +17,7 @@ import abc import six +from tacker._i18n import _ from tacker.api import extensions from tacker.api.v1 import attributes as attr from tacker.api.v1 import resource_helper @@ -73,6 +74,151 @@ class VimDuplicateUrlException(exceptions.TackerException): "duplicate VIM") +class VimUnsupportedResourceTypeException(exceptions.TackerException): + message = _("Resource type %(type) is unsupported by VIM") + + +class VimGetResourceException(exceptions.TackerException): + message = _("Error while trying to issue %(cmd)s to find resource type " + "%(type)s") + + +class VimFromVnfNotFoundException(exceptions.NotFound): + message = _('VIM from VNF %(vnf_id)s could not be found') + + +class ToscaParserFailed(exceptions.InvalidInput): + message = _("tosca-parser failed: - %(error_msg_details)s") + + +class VnffgdInvalidTemplate(exceptions.InvalidInput): + message = _("Invalid VNFFG template input: %(template)s") + + +class VnffgdDuplicateForwarderException(exceptions.InvalidInput): + message = _("Invalid Forwarding Path contains duplicate forwarder not in " + "order: %(forwarder)s") + + +class VnffgdDuplicateCPException(exceptions.InvalidInput): + message = _("Invalid Forwarding Path contains duplicate connection point " + ": %(cp)s") + + +class VnffgdVnfdNotFoundException(exceptions.NotFound): + message = _("Specified VNFD %(vnfd_name)s in VNFFGD does not exist. " + "Please create VNFDs before creating VNFFG") + + +class VnffgdCpNotFoundException(exceptions.NotFound): + message = _("Specified CP %(cp_id)s could not be found in VNFD " + "%(vnfd_name)s. Please check VNFD for correct Connection " + "Point.") + + +class VnffgdCpNoForwardingException(exceptions.TackerException): + message = _("Specified CP %(cp_id)s in VNFD %(vnfd_name)s " + "does not have forwarding capability, which is required to be " + "included in forwarding path") + + +class VnffgdInUse(exceptions.InUse): + message = _('VNFFGD %(vnffgd_id)s is still in use') + + +class VnffgdNotFoundException(exceptions.NotFound): + message = _('VNFFG Template %(vnffgd_id)s could not be found') + + +class VnffgCreateFailed(exceptions.TackerException): + message = _('Creating VNFFG based on %(vnffgd_id)s failed') + + +class VnffgInvalidMappingException(exceptions.TackerException): + message = _("Matching VNF Instance for VNFD %(vnfd_name)s could not be " + "found. Please create an instance of this VNFD before " + "creating/updating VNFFG.") + + +class VnffgVimMappingException(exceptions.TackerException): + message = _("VNF Instance VNF %(vnf_id)s does not match VIM ID %(vim_id).") + + +class VnffgPropertyNotFoundException(exceptions.NotFound): + message = _('VNFFG Property %(vnffg_property)s could not be found') + + +class VnffgCpNotFoundException(exceptions.NotFound): + message = _("Specified CP %(cp_id)s could not be found in VNF " + "%(vnf_id)s.") + + +class VnffgNotFoundException(exceptions.NotFound): + message = _('VNFFG %(vnffg_id)s could not be found') + + +class VnffgInUse(exceptions.InUse): + message = _('VNFFG %(vnffg_id)s is still in use') + + +class VnffgVnfNotFoundException(exceptions.NotFound): + message = _("Specified VNF instance %(vnf_name)s in VNF Mapping could not " + "be found") + + +class VnffgDeleteFailed(exceptions.TackerException): + message = _('Deleting VNFFG %(vnffg_id)s failed') + + +class NfpAttributeNotFoundException(exceptions.NotFound): + message = _('NFP attribute %(attribute)s could not be found') + + +class NfpNotFoundException(exceptions.NotFound): + message = _('NFP %(nfp_id)s could not be found') + + +class NfpInUse(exceptions.InUse): + message = _('NFP %(nfp_id)s is still in use') + + +class NfpPolicyCriteriaError(exceptions.PolicyCheckError): + message = _('%(error)s in policy') + + +class NfpPolicyNotFoundException(exceptions.NotFound): + message = _('Policy not found in NFP %(nfp)s') + + +class NfpPolicyTypeError(exceptions.PolicyCheckError): + message = _('Unsupported Policy Type: %(type)s') + + +class NfpForwarderNotFoundException(exceptions.NotFound): + message = _('VNFD Forwarder %(vnfd)s not found in VNF Mapping %(mapping)s') + + +class NfpRequirementsException(exceptions.TackerException): + message = _('VNFD Forwarder %(vnfd) specified more than twice in ' + 'requirements path') + + +class SfcInUse(exceptions.InUse): + message = _('SFC %(sfc_id)s is still in use') + + +class SfcNotFoundException(exceptions.NotFound): + message = _('Service Function Chain %(sfc_id)s could not be found') + + +class ClassifierInUse(exceptions.InUse): + message = _('Classifier %(classifier_id)s is still in use') + + +class ClassifierNotFoundException(exceptions.NotFound): + message = _('Classifier %(classifier_id)s could not be found') + + RESOURCE_ATTRIBUTE_MAP = { 'vims': { @@ -162,6 +308,257 @@ RESOURCE_ATTRIBUTE_MAP = { 'allow_put': False, 'is_visible': True, }, + }, + + 'vnffgds': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True, + }, + 'tenant_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True, + }, + 'name': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + }, + 'description': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'template': { + 'allow_post': True, + 'allow_put': False, + 'convert_to': attr.convert_none_to_empty_dict, + 'validate': {'type:dict_or_nodata': None}, + 'is_visible': True, + 'default': None, + }, + }, + + 'vnffgs': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True + }, + 'tenant_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True + }, + 'vnffgd_id': { + 'allow_post': True, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'name': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + }, + 'description': { + 'allow_post': True, + 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, + 'default': '', + }, + 'vnf_mapping': { + 'allow_post': True, + 'allow_put': True, + 'convert_to': attr.convert_none_to_empty_dict, + 'validate': {'type:dict_or_nodata': None}, + 'is_visible': True, + 'default': None, + }, + 'symmetrical': { + 'allow_post': True, + 'allow_put': True, + 'is_visible': True, + 'validate': {'type:boolean': None}, + 'default': False, + }, + 'forwarding_paths': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'status': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + }, + + 'nfps': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True + }, + 'tenant_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True + }, + 'vnffg_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'name': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True, + }, + 'classifier_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'chain_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'path_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:string': None}, + 'is_visible': True, + }, + 'symmetrical': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + 'validate': {'type:boolean': None}, + 'default': False, + }, + 'status': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + }, + 'sfcs': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True + }, + 'tenant_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True + }, + 'nfp_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'instance_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'chain': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'path_id': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'symmetrical': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + 'validate': {'type:boolean': None}, + 'default': False, + }, + 'status': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + }, + 'classifiers': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True + }, + 'tenant_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:string': None}, + 'required_by_policy': True, + 'is_visible': True + }, + 'nfp_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'instance_id': { + 'allow_post': False, + 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + }, + 'match': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'chain_id': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'status': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, } } diff --git a/tacker/extensions/nfvo_plugins/__init__.py b/tacker/extensions/nfvo_plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/extensions/nfvo_plugins/vnffg.py b/tacker/extensions/nfvo_plugins/vnffg.py new file mode 100644 index 000000000..e490d208a --- /dev/null +++ b/tacker/extensions/nfvo_plugins/vnffg.py @@ -0,0 +1,83 @@ +# Copyright 2016 Red Hat Inc +# 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 abc +import six + +from tacker.services import service_base + + +@six.add_metaclass(abc.ABCMeta) +class VNFFGPluginBase(service_base.NFVPluginBase): + + @abc.abstractmethod + def create_vnffgd(self, context, vnffgd): + pass + + @abc.abstractmethod + def delete_vnffgd(self, context, vnffgd_id): + pass + + @abc.abstractmethod + def get_vnffgd(self, context, vnffgd_id, fields=None): + pass + + @abc.abstractmethod + def get_vnffgds(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def create_vnffg(self, context, vnffg): + pass + + @abc.abstractmethod + def get_vnffgs(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def get_vnffg(self, context, vnffg_id, fields=None): + pass + + @abc.abstractmethod + def update_vnffg(self, context, vnffg_id, vnffg): + pass + + @abc.abstractmethod + def delete_vnffg(self, context, vnffg_id): + pass + + @abc.abstractmethod + def get_nfp(self, context, nfp_id, fields=None): + pass + + @abc.abstractmethod + def get_nfps(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def get_sfcs(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def get_sfc(self, context, sfc_id, fields=None): + pass + + @abc.abstractmethod + def get_classifiers(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def get_classifier(self, context, classifier_id, fields=None): + pass diff --git a/tacker/nfvo/drivers/vim/abstract_vim_driver.py b/tacker/nfvo/drivers/vim/abstract_vim_driver.py index 611a24c9c..853bf764d 100644 --- a/tacker/nfvo/drivers/vim/abstract_vim_driver.py +++ b/tacker/nfvo/drivers/vim/abstract_vim_driver.py @@ -90,3 +90,14 @@ class VimAbstractDriver(extensions.PluginInterface): Checks the health status of VIM and return a boolean value """ pass + + @abc.abstractmethod + def get_vim_resource_id(self, vim_obj, resource_type, resource_name): + """Parses a VIM resource ID from a given type and name + + :param vim_obj: VIM information + :param resource_type: type of resource, such as network, compute + :param resource_name: name of resource, such at "test-network" + :return: ID of of resource + """ + pass diff --git a/tacker/nfvo/drivers/vim/openstack_driver.py b/tacker/nfvo/drivers/vim/openstack_driver.py index 84894e5c3..d80e4afa9 100644 --- a/tacker/nfvo/drivers/vim/openstack_driver.py +++ b/tacker/nfvo/drivers/vim/openstack_driver.py @@ -16,11 +16,15 @@ import os +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 from keystoneclient import exceptions +from keystoneclient import session +from neutronclient.v2_0 import client as neutron_client from oslo_config import cfg from oslo_log import log as logging -from tacker._i18n import _LW +from tacker._i18n import _LW, _ from tacker.agent.linux import utils as linux_utils from tacker.common import log from tacker.extensions import nfvo @@ -46,6 +50,11 @@ OPENSTACK_OPTS = [ cfg.CONF.register_opts(OPTS, 'vim_keys') cfg.CONF.register_opts(OPENSTACK_OPTS, 'vim_monitor') +_VALID_RESOURCE_TYPES = {'network': {'client': neutron_client.Client, + 'cmd': 'list_' + } + } + def config_opts(): return [('vim_keys', OPTS), ('vim_monitor', OPENSTACK_OPTS)] @@ -78,10 +87,15 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): Initialize keystoneclient with provided authentication attributes. """ + auth_url = vim_obj['auth_url'] + keystone_version = self._validate_auth_url(auth_url) + auth_cred = self._get_auth_creds(keystone_version, vim_obj) + return self._initialize_keystone(keystone_version, auth_cred) + + def _get_auth_creds(self, keystone_version, vim_obj): auth_url = vim_obj['auth_url'] auth_cred = vim_obj['auth_cred'] vim_project = vim_obj['vim_project'] - keystone_version = self._validate_auth_url(auth_url) if keystone_version not in auth_url: vim_obj['auth_url'] = auth_url + '/' + keystone_version @@ -97,7 +111,15 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): auth_cred.pop('user_domain_name', None) auth_cred.pop('user_id', None) auth_cred['auth_url'] = vim_obj['auth_url'] - return self._initialize_keystone(keystone_version, auth_cred) + return auth_cred + + def _get_auth_plugin(self, version, **kwargs): + if version == 'v2.0': + auth_plugin = v2.Password(**kwargs) + else: + auth_plugin = v3.Password(**kwargs) + + return auth_plugin def _validate_auth_url(self, auth_url): try: @@ -207,3 +229,44 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): except RuntimeError: LOG.warning(_LW("Cannot ping ip address: %s"), vim_ip) return False + + @log.log + def get_vim_resource_id(self, vim_obj, resource_type, resource_name): + """Locates openstack resource by type/name and returns ID + + :param vim_obj: VIM info used to access openstack instance + :param resource_type: type of resource to find + :param resource_name: name of resource to locate + :return: ID of resource + """ + if resource_type in _VALID_RESOURCE_TYPES.keys(): + client_type = _VALID_RESOURCE_TYPES[resource_type]['client'] + cmd_prefix = _VALID_RESOURCE_TYPES[resource_type]['cmd'] + else: + raise nfvo.VimUnsupportedResourceTypeException(type=resource_type) + + client = self._get_client(vim_obj, client_type) + cmd = str(cmd_prefix) + str(resource_name) + try: + resources = getattr(client, "%s" % cmd)() + LOG.debug(_('resources output %s'), resources) + for resource in resources[resource_type]: + if resource['name'] == resource_name: + return resource['id'] + except Exception: + raise nfvo.VimGetResourceException(cmd=cmd, type=resource_type) + + @log.log + def _get_client(self, vim_obj, client_type): + """Initializes and returns an openstack client + + :param vim_obj: VIM Information + :param client_type: openstack client to initialize + :return: initialized client + """ + auth_url = vim_obj['auth_url'] + keystone_version = self._validate_auth_url(auth_url) + auth_cred = self._get_auth_creds(keystone_version, vim_obj) + auth_plugin = self._get_auth_plugin(keystone_version, **auth_cred) + sess = session.Session(auth=auth_plugin) + return client_type(session=sess) diff --git a/tacker/nfvo/drivers/vnffg/sfc_drivers/noop.py b/tacker/nfvo/drivers/vnffg/sfc_drivers/noop.py new file mode 100644 index 000000000..492c9bf8a --- /dev/null +++ b/tacker/nfvo/drivers/vnffg/sfc_drivers/noop.py @@ -0,0 +1,72 @@ +# Copyright 2016 Red Hat Inc +# 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 uuid + +from oslo_log import log as logging +from tacker.common import log +from tacker.nfvo.drivers.vnffg.sfc_drivers import abstract_driver + +LOG = logging.getLogger(__name__) + + +class VNFFGNoop(abstract_driver.SfcAbstractDriver): + + """Noop driver for VNFFG tests""" + + def __init__(self): + super(VNFFGNoop, self).__init__() + self._instances = set() + + def get_type(self): + return 'noop' + + def get_name(self): + return 'noop' + + def get_description(self): + return 'VNFFG Noop driver' + + @log.log + def create_chain(self, fc_id, vnfs, auth_attr=None): + instance_id = str(uuid.uuid4()) + self._instances.add(instance_id) + return instance_id + + @log.log + def update_chain(self, chain_id, fc_ids, vnfs, auth_attr=None): + if chain_id not in self._instances: + LOG.debug(_('Chain not found')) + raise ValueError('No chain instance %s' % chain_id) + + @log.log + def delete_chain(self, chain_id, auth_attr=None): + self._instances.remove(chain_id) + + @log.log + def create_flow_classifier(self, fc, auth_attr=None): + instance_id = str(uuid.uuid4()) + self._instances.add(instance_id) + return instance_id + + @log.log + def update_flow_classifier(self, fc_id, fc, auth_attr=None): + if fc_id not in self._instances: + LOG.debug(_('FC not found')) + raise ValueError('No FC instance %s' % fc_id) + + @log.log + def delete_flow_classifier(self, fc_id, auth_attr=None): + self._instances.remove(fc_id) diff --git a/tacker/nfvo/nfvo_plugin.py b/tacker/nfvo/nfvo_plugin.py index 3a8cdc410..1452e7e73 100644 --- a/tacker/nfvo/nfvo_plugin.py +++ b/tacker/nfvo/nfvo_plugin.py @@ -23,11 +23,18 @@ from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import strutils +from tacker._i18n import _ from tacker.common import driver_manager from tacker.common import log from tacker.common import utils from tacker import context as t_context from tacker.db.nfvo import nfvo_db +from tacker.db.nfvo import vnffg_db +from tacker.extensions import nfvo +from tacker import manager +from tacker.plugins.common import constants +from tacker.vnfm.tosca import utils as toscautils +from toscaparser import tosca_template LOG = logging.getLogger(__name__) @@ -36,7 +43,7 @@ def config_opts(): return [('nfvo', NfvoPlugin.OPTS)] -class NfvoPlugin(nfvo_db.NfvoPluginDb): +class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin): """NFVO reference plugin for NFVO extension Implements the NFVO extension and defines public facing APIs for VIM @@ -138,3 +145,218 @@ class NfvoPlugin(nfvo_db.NfvoPluginDb): t_context.get_admin_context(), vim_id, status) self._created_vims[vim_id]["status"] = status + + @log.log + def validate_tosca(self, template): + if "tosca_definitions_version" not in template: + raise nfvo.ToscaParserFailed( + error_msg_details='tosca_definitions_version missing in ' + 'template' + ) + + LOG.debug(_('template yaml: %s'), template) + + toscautils.updateimports(template) + + try: + tosca_template.ToscaTemplate( + a_file=False, yaml_dict_tpl=template) + except Exception as e: + LOG.exception(_("tosca-parser error: %s"), str(e)) + raise nfvo.ToscaParserFailed(error_msg_details=str(e)) + + @log.log + def create_vnffgd(self, context, vnffgd): + template = vnffgd['vnffgd'] + + if 'vnffgd' not in template.get('template'): + raise nfvo.VnffgdInvalidTemplate(template=template.get('template')) + else: + self.validate_tosca(template['template']['vnffgd']) + temp = template['template']['vnffgd']['topology_template'] + vnffg_name = temp['groups'].keys()[0] + nfp_name = temp['groups'][vnffg_name]['members'][0] + path = self._get_nfp_attribute(template['template'], nfp_name, + 'path') + prev_element = None + known_forwarders = set() + for element in path: + if element.get('forwarder') in known_forwarders: + if prev_element is not None and element.get('forwarder')\ + != prev_element['forwarder']: + raise nfvo.VnffgdDuplicateForwarderException( + forwarder=element.get('forwarder') + ) + elif prev_element is not None and element.get( + 'capability') == prev_element['capability']: + raise nfvo.VnffgdDuplicateCPException( + cp=element.get('capability') + ) + else: + known_forwarders.add(element.get('forwarder')) + prev_element = element + return super(NfvoPlugin, self).create_vnffgd(context, vnffgd) + + @log.log + def create_vnffg(self, context, vnffg): + vnffg_dict = super(NfvoPlugin, self)._create_vnffg_pre(context, vnffg) + nfp = super(NfvoPlugin, self).get_nfp(context, + vnffg_dict['forwarding_paths']) + sfc = super(NfvoPlugin, self).get_sfc(context, nfp['chain_id']) + match = super(NfvoPlugin, self).get_classifier(context, + nfp['classifier_id'], + fields='match') + # grab the first VNF to check it's VIM type + # we have already checked that all VNFs are in the same VIM + vim_auth = self._get_vim_from_vnf(context, + vnffg_dict['vnf_mapping'].values()[0] + ) + # TODO(trozet): figure out what auth info we actually need to pass + # to the driver. Is it a session, or is full vim obj good enough? + driver_type = vim_auth['type'] + try: + fc_id = self._vim_drivers.invoke(driver_type, + 'create_flow_classifier', + fc=match, auth_attr=vim_auth, + symmetrical=sfc['symmetrical']) + sfc_id = self._vim_drivers.invoke(driver_type, 'create_chain', + vnfs=sfc['chain'], fc_id=fc_id, + symmetrical=sfc['symmetrical'], + auth_attr=vim_auth) + except Exception: + with excutils.save_and_reraise_exception(): + self.delete_vnffg(context, vnffg_id=vnffg_dict['id']) + super(NfvoPlugin, self)._create_vnffg_post(context, sfc_id, fc_id, + vnffg_dict) + super(NfvoPlugin, self)._create_vnffg_status(context, vnffg_dict) + return vnffg_dict + + @log.log + def update_vnffg(self, context, vnffg_id, vnffg): + vnffg_dict = super(NfvoPlugin, self)._update_vnffg_pre(context, + vnffg_id) + new_vnffg = vnffg['vnffg'] + LOG.debug(_('vnffg update: %s'), vnffg) + nfp = super(NfvoPlugin, self).get_nfp(context, + vnffg_dict['forwarding_paths']) + sfc = super(NfvoPlugin, self).get_sfc(context, nfp['chain_id']) + + fc = super(NfvoPlugin, self).get_classifier(context, + nfp['classifier_id']) + template_db = self._get_resource(context, vnffg_db.VnffgTemplate, + vnffg_dict['vnffgd_id']) + vnf_members = self._get_vnffg_property(template_db, + 'constituent_vnfs') + new_vnffg['vnf_mapping'] = super(NfvoPlugin, self)._get_vnf_mapping( + context, new_vnffg.get('vnf_mapping'), vnf_members) + template_id = vnffg_dict['vnffgd_id'] + template_db = self._get_resource(context, vnffg_db.VnffgTemplate, + template_id) + # functional attributes that allow update are vnf_mapping, + # and symmetrical. Therefore we need to figure out the new chain if + # it was updated by new vnf_mapping. Symmetrical is handled by driver. + + chain = super(NfvoPlugin, self)._create_port_chain(context, + new_vnffg[ + 'vnf_mapping'], + template_db, + nfp['name']) + LOG.debug(_('chain update: %s'), chain) + sfc['chain'] = chain + sfc['symmetrical'] = new_vnffg['symmetrical'] + vim_auth = self._get_vim_from_vnf(context, + vnffg_dict['vnf_mapping'].values()[0] + ) + driver_type = vim_auth['type'] + try: + # we don't support updating the match criteria in first iteration + # so this is essentially a noop. Good to keep for future use + # though. + self._vim_drivers.invoke(driver_type, 'update_flow_classifier', + fc_id=fc['instance_id'], fc=fc['match'], + auth_attr=vim_auth, + symmetrical=new_vnffg['symmetrical']) + self._vim_drivers.invoke(driver_type, 'update_chain', + vnfs=sfc['chain'], + fc_ids=[fc['instance_id']], + chain_id=sfc['instance_id'], + auth_attr=vim_auth, + symmetrical=new_vnffg['symmetrical']) + except Exception: + with excutils.save_and_reraise_exception(): + vnffg_dict['status'] = constants.ERROR + super(NfvoPlugin, self)._update_vnffg_post(context, vnffg_id, + constants.ERROR) + super(NfvoPlugin, self)._update_vnffg_post(context, vnffg_id, + constants.ACTIVE, new_vnffg) + # update chain + super(NfvoPlugin, self)._update_sfc_post(context, sfc['id'], + constants.ACTIVE, sfc) + # update classifier - this is just updating status until functional + # updates are supported to classifier + super(NfvoPlugin, self)._update_classifier_post(context, fc['id'], + constants.ACTIVE) + return vnffg_dict + + @log.log + def delete_vnffg(self, context, vnffg_id): + vnffg_dict = super(NfvoPlugin, self)._delete_vnffg_pre(context, + vnffg_id) + nfp = super(NfvoPlugin, self).get_nfp(context, + vnffg_dict['forwarding_paths']) + sfc = super(NfvoPlugin, self).get_sfc(context, nfp['chain_id']) + + fc = super(NfvoPlugin, self).get_classifier(context, + nfp['classifier_id']) + vim_auth = self._get_vim_from_vnf(context, + vnffg_dict['vnf_mapping'].values()[0] + ) + driver_type = vim_auth['type'] + try: + if sfc['instance_id'] is not None: + self._vim_drivers.invoke(driver_type, 'delete_chain', + chain_id=sfc['instance_id'], + auth_attr=vim_auth) + if fc['instance_id'] is not None: + self._vim_drivers.invoke(driver_type, + 'delete_flow_classifier', + fc_id=fc['instance_id'], + auth_attr=vim_auth) + except Exception: + with excutils.save_and_reraise_exception(): + vnffg_dict['status'] = constants.ERROR + super(NfvoPlugin, self)._delete_vnffg_post(context, vnffg_id, + True) + super(NfvoPlugin, self)._delete_vnffg_post(context, vnffg_id, False) + return vnffg_dict + + def _get_vim_from_vnf(self, context, vnf_id): + """Figures out VIM based on a VNF + + :param context: SQL Session Context + :param vnf_id: VNF ID + :return: VIM or VIM properties if fields are provided + """ + vnfm_plugin = manager.TackerManager.get_service_plugins()['VNFM'] + vim_id = vnfm_plugin.get_vnf(context, vnf_id, fields=['vim_id']) + vim_obj = self.get_vim(context, vim_id['vim_id']) + if vim_obj is None: + raise nfvo.VimFromVnfNotFoundException(vnf_id=vnf_id) + return vim_obj + + def _vim_resource_name_to_id(self, context, resource, name, vnf_id): + """Converts a VIM resource name to its ID + + :param resource: resource type to find (network, subnet, etc) + :param name: name of the resource to find its ID + :param vnf_id: A VNF instance ID that is part of the chain to which + the classifier will apply to + :return: ID of the resource name + """ + vim_auth = self._get_vim_from_vnf(context, vnf_id) + driver_type = vim_auth['type'] + return self._vim_drivers.invoke(driver_type, + 'get_vim_resource_id', + vim_auth=vim_auth, + resource_type=resource, + resource_name=name) diff --git a/tacker/tests/unit/db/utils.py b/tacker/tests/unit/db/utils.py index 0e2ee8519..098592d3a 100644 --- a/tacker/tests/unit/db/utils.py +++ b/tacker/tests/unit/db/utils.py @@ -31,6 +31,10 @@ ipparams = _get_template('vnf_cirros_param_values_ipaddr.yaml') vnfd_userdata_template = _get_template('vnf_cirros_template_user_data.yaml') userdata_params = _get_template('vnf_cirros_param_values_user_data.yaml') config_data = _get_template('config_data.yaml') +vnffgd_template = yaml.load(_get_template('vnffgd_template.yaml')) +vnffgd_tosca_template = yaml.load(_get_template('tosca_vnffgd_template.yaml')) +vnffgd_invalid_tosca_template = yaml.load(_get_template( + 'tosca_invalid_vnffgd_template.yaml')) def get_dummy_vnfd_obj(): @@ -165,3 +169,31 @@ def get_vim_auth_obj(): 'auth_url': 'http://localhost:5000/v3', 'user_domain_name': 'default', 'project_domain_name': 'default'} + + +def get_dummy_vnffgd_obj(): + return {u'vnffgd': {'name': 'dummy_vnfd', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + u'template': {u'vnffgd': vnffgd_tosca_template}, + 'description': 'dummy_vnfd_description'}} + + +def get_dummy_vnffg_obj(): + return {'vnffg': {'description': 'dummy_vnf_description', + 'vnffgd_id': u'eb094833-995e-49f0-a047-dfb56aaf7c4e', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'name': 'dummy_vnffg', + 'vnf_mapping': {}, + 'symmetrical': False}} + + +def get_dummy_vnffg_obj_vnf_mapping(): + return {'vnffg': {'description': 'dummy_vnf_description', + 'vnffgd_id': u'eb094833-995e-49f0-a047-dfb56aaf7c4e', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'name': 'dummy_vnffg', + 'vnf_mapping': { + 'VNF1': '91e32c20-6d1f-47a4-9ba7-08f5e5effe07', + 'VNF3': '7168062e-9fa1-4203-8cb7-f5c99ff3ee1b' + }, + 'symmetrical': False}} diff --git a/tacker/tests/unit/vm/infra_drivers/heat/data/tosca_invalid_vnffgd_template.yaml b/tacker/tests/unit/vm/infra_drivers/heat/data/tosca_invalid_vnffgd_template.yaml new file mode 100644 index 000000000..862c85883 --- /dev/null +++ b/tacker/tests/unit/vm/infra_drivers/heat/data/tosca_invalid_vnffgd_template.yaml @@ -0,0 +1,41 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: example template + +topology_template: + description: Example VNFFG template + + node_templates: + + Forwarding_path1: + type: tosca.nodes.nfv.FP.Tacker + description: creates path (CP11->CP12->CP32) + properties: + id: 51 + policy: + type: ACL + criteria: + - blah: tenant1_net + - destination_port_range: 80-1024 + - ip_proto: 6 + - ip_dst_prefix: 192.168.1.2/24 + path: + - forwarder: VNF1 + capability: CP11 + - forwarder: VNF1 + capability: CP12 + - forwarder: VNF3 + capability: CP32 + + groups: + VNFFG1: + type: tosca.groups.nfv.VNFFG + description: HTTP to Corporate Net + properties: + vendor: tacker + version: 1.0 + number_of_endpoints: 5 + dependent_virtual_link: [VL1,VL2,VL3] + connection_point: [CP11,CP12,CP32] + constituent_vnfs: [VNF1,VNF3] + members: [Forwarding_path1] diff --git a/tacker/tests/unit/vm/infra_drivers/heat/data/tosca_vnffgd_template.yaml b/tacker/tests/unit/vm/infra_drivers/heat/data/tosca_vnffgd_template.yaml new file mode 100644 index 000000000..189b10836 --- /dev/null +++ b/tacker/tests/unit/vm/infra_drivers/heat/data/tosca_vnffgd_template.yaml @@ -0,0 +1,41 @@ +tosca_definitions_version: tosca_simple_profile_for_nfv_1_0_0 + +description: example template + +topology_template: + description: Example VNFFG template + + node_templates: + + Forwarding_path1: + type: tosca.nodes.nfv.FP.Tacker + description: creates path (CP11->CP12->CP32) + properties: + id: 51 + policy: + type: ACL + criteria: + - network_name: tenant1_net + - destination_port_range: 80-1024 + - ip_proto: 6 + - ip_dst_prefix: 192.168.1.2/24 + path: + - forwarder: VNF1 + capability: CP11 + - forwarder: VNF1 + capability: CP12 + - forwarder: VNF3 + capability: CP32 + + groups: + VNFFG1: + type: tosca.groups.nfv.VNFFG + description: HTTP to Corporate Net + properties: + vendor: tacker + version: 1.0 + number_of_endpoints: 5 + dependent_virtual_link: [VL1,VL2,VL3] + connection_point: [CP11,CP12,CP32] + constituent_vnfs: [VNF1,VNF3] + members: [Forwarding_path1] diff --git a/tacker/tests/unit/vm/infra_drivers/heat/data/vnffgd_template.yaml b/tacker/tests/unit/vm/infra_drivers/heat/data/vnffgd_template.yaml new file mode 100644 index 000000000..fb9cbf780 --- /dev/null +++ b/tacker/tests/unit/vm/infra_drivers/heat/data/vnffgd_template.yaml @@ -0,0 +1,32 @@ + Forwarding_path1: + type: tosca.nodes.nfv.FP + id: 51 + description: creates path (CP11->CP12->CP32) + properties: + policy: + type: ACL + criteria: + - network_name: tenant1_net + - destination_port_range: 80-1024 + - ip_proto: 6 + - ip_dst_prefix: 192.168.1.2/24 + requirements: + - forwarder: VNF1 + capability: CP11 + - forwarder: VNF1 + capability: CP12 + - forwarder: VNF3 + capability: CP32 + + groups: + VNFFG1: + type: tosca.groups.nfv.VNFFG + description: HTTP to Corporate Net + properties: + vendor: tacker + version: 1.0 + number_of_endpoints: 5 + dependent_virtual_link: [VL1,VL2,VL3] + connection_point: [CP11,CP12,CP32] + constituent_vnfs: [VNF1,VNF3] + members: [Forwarding_path1] diff --git a/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py index f7f599f7e..b9285efcd 100644 --- a/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py +++ b/tacker/tests/unit/vm/nfvo/test_nfvo_plugin.py @@ -13,26 +13,120 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import uuid -import mock +from mock import patch from tacker import context from tacker.db.common_services import common_services_db from tacker.db.nfvo import nfvo_db +from tacker.db.nfvo import vnffg_db +from tacker.extensions import nfvo +from tacker.manager import TackerManager from tacker.nfvo import nfvo_plugin from tacker.plugins.common import constants from tacker.tests.unit.db import base as db_base +from tacker.tests.unit.db import utils SECRET_PASSWORD = '***' +def dummy_get_vim(*args, **kwargs): + vim_auth = utils.get_vim_auth_obj() + vim_auth['type'] = 'openstack' + return vim_auth + + class FakeDriverManager(mock.Mock): def invoke(self, *args, **kwargs): - if 'create' in args: + if any(x in ['create', 'create_chain', 'create_flow_classifier'] for + x in args): return str(uuid.uuid4()) +class FakeVNFMPlugin(mock.Mock): + + def __init__(self): + super(FakeVNFMPlugin, self).__init__() + self.vnf1_vnfd_id = 'eb094833-995e-49f0-a047-dfb56aaf7c4e' + self.vnf1_vnf_id = '91e32c20-6d1f-47a4-9ba7-08f5e5effe07' + self.vnf3_vnfd_id = 'e4015e9f-1ef2-49fb-adb6-070791ad3c45' + self.vnf3_vnf_id = '7168062e-9fa1-4203-8cb7-f5c99ff3ee1b' + self.vnf3_update_vnf_id = '10f66bc5-b2f1-45b7-a7cd-6dd6ad0017f5' + + self.cp11_id = 'd18c8bae-898a-4932-bff8-d5eac981a9c9' + self.cp12_id = 'c8906342-3e30-4b2a-9401-a251a7a9b5dd' + self.cp32_id = '3d1bd2a2-bf0e-44d1-87af-a2c6b2cad3ed' + self.cp32_update_id = '064c0d99-5a61-4711-9597-2a44dc5da14b' + + def get_vnfds(self, *args, **kwargs): + if {'name': ['VNF1']} in args: + return [{'id': self.vnf1_vnfd_id}] + elif {'name': ['VNF3']} in args: + return [{'id': self.vnf3_vnfd_id}] + else: + return None + + def get_vnfs(self, *args, **kwargs): + if {'vnfd_id': [self.vnf1_vnfd_id]} in args: + return [{'id': self.vnf1_vnf_id}] + elif {'vnfd_id': [self.vnf3_vnfd_id]} in args: + return [{'id': self.vnf3_vnf_id}] + else: + return None + + def get_vnf(self, *args, **kwargs): + if self.vnf1_vnf_id in args: + return self.get_dummy_vnf1() + elif self.vnf3_vnf_id in args: + return self.get_dummy_vnf3() + elif self.vnf3_update_vnf_id in args: + return self.get_dummy_vnf3_update() + + def get_vnf_resources(self, *args, **kwargs): + if self.vnf1_vnf_id in args: + return self.get_dummy_vnf1_details() + elif self.vnf3_vnf_id in args: + return self.get_dummy_vnf3_details() + elif self.vnf3_update_vnf_id in args: + return self.get_dummy_vnf3_update_details() + + def get_dummy_vnf1_details(self): + return [{'name': 'CP11', 'id': self.cp11_id}, + {'name': 'CP12', 'id': self.cp12_id}] + + def get_dummy_vnf3_details(self): + return [{'name': 'CP32', 'id': self.cp32_id}] + + def get_dummy_vnf3_update_details(self): + return [{'name': 'CP32', 'id': self.cp32_update_id}] + + def get_dummy_vnf1(self): + return {'description': 'dummy_vnf_description', + 'vnfd_id': self.vnf1_vnfd_id, + 'vim_id': u'6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'name': 'dummy_vnf1', + 'attributes': {}} + + def get_dummy_vnf3(self): + return {'description': 'dummy_vnf_description', + 'vnfd_id': self.vnf3_vnfd_id, + 'vim_id': u'6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'name': 'dummy_vnf2', + 'attributes': {}} + + def get_dummy_vnf3_update(self): + return {'description': 'dummy_vnf_description', + 'vnfd_id': self.vnf3_vnfd_id, + 'vim_id': u'6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'tenant_id': u'ad7ebc56538745a08ef7c5e97f8bd437', + 'name': 'dummy_vnf_update', + 'attributes': {}} + + class TestNfvoPlugin(db_base.SqlTestCase): def setUp(self): super(TestNfvoPlugin, self).setUp() @@ -40,6 +134,8 @@ class TestNfvoPlugin(db_base.SqlTestCase): self.context = context.get_admin_context() self._mock_driver_manager() mock.patch('tacker.nfvo.nfvo_plugin.NfvoPlugin.__run__').start() + mock.patch('tacker.nfvo.nfvo_plugin.NfvoPlugin._get_vim_from_vnf', + side_effect=dummy_get_vim).start() self.nfvo_plugin = nfvo_plugin.NfvoPlugin() mock.patch('tacker.db.common_services.common_services_db.' 'CommonServicesPluginDb.create_event' @@ -139,3 +235,189 @@ class TestNfvoPlugin(db_base.SqlTestCase): self.context, evt_type=constants.RES_EVT_UPDATE, res_id=mock.ANY, res_state=mock.ANY, res_type=constants.RES_TYPE_VIM, tstamp=mock.ANY) + + def _insert_dummy_vnffg_template(self): + session = self.context.session + vnffg_template = vnffg_db.VnffgTemplate( + id='eb094833-995e-49f0-a047-dfb56aaf7c4e', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='fake_template', + description='fake_template_description', + template={u'vnffgd': utils.vnffgd_tosca_template}) + session.add(vnffg_template) + session.flush() + return vnffg_template + + def _insert_dummy_vnffg(self): + session = self.context.session + vnffg = vnffg_db.Vnffg( + id='ffc1a59b-65bb-4874-94d3-84f639e63c74', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + name='dummy_vnffg', + description="fake vnffg", + vnffgd_id='eb094833-995e-49f0-a047-dfb56aaf7c4e', + status='ACTIVE', + vnf_mapping={'VNF1': '91e32c20-6d1f-47a4-9ba7-08f5e5effe07', + 'VNF3': '7168062e-9fa1-4203-8cb7-f5c99ff3ee1b'}) + session.add(vnffg) + nfp = vnffg_db.VnffgNfp( + id='768f76a7-9025-4acd-b51c-0da609759983', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + status="ACTIVE", + name='Forwarding_path1', + vnffg_id='ffc1a59b-65bb-4874-94d3-84f639e63c74', + path_id=51, + symmetrical=False) + session.add(nfp) + sfc = vnffg_db.VnffgChain( + id='f28e33bc-1061-4762-b942-76060bbd59c4', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + symmetrical=False, + chain=[{'connection_points': [ + 'd18c8bae-898a-4932-bff8-d5eac981a9c9', + 'c8906342-3e30-4b2a-9401-a251a7a9b5dd'], + 'name': 'dummy_vnf1'}, + {'connection_points': ['3d1bd2a2-bf0e-44d1-87af-a2c6b2cad3ed'], + 'name': 'dummy_vnf2'}], + path_id=51, + status='ACTIVE', + nfp_id='768f76a7-9025-4acd-b51c-0da609759983', + instance_id='bcfb295e-578e-405b-a349-39f06b25598c') + session.add(sfc) + fc = vnffg_db.VnffgClassifier( + id='a85f21b5-f446-43f0-86f4-d83bdc5590ab', + tenant_id='ad7ebc56538745a08ef7c5e97f8bd437', + status='ACTIVE', + instance_id='3007dc2d-30dc-4651-9184-f1e6273cc0b6', + chain_id='f28e33bc-1061-4762-b942-76060bbd59c4', + nfp_id='768f76a7-9025-4acd-b51c-0da609759983') + session.add(fc) + match = vnffg_db.ACLMatchCriteria( + id='bdb0f2db-d4c2-42a2-a1df-426079ecc443', + vnffgc_id='a85f21b5-f446-43f0-86f4-d83bdc5590ab', + eth_src=None, eth_dst=None, eth_type=None, vlan_id=None, + vlan_pcp=None, mpls_label=None, mpls_tc=None, ip_dscp=None, + ip_ecn=None, ip_src_prefix=None, ip_dst_prefix='192.168.1.2/24', + source_port_min=None, source_port_max=None, + destination_port_min=80, destination_port_max=1024, ip_proto=6, + network_id=None, network_src_port_id=None, + network_dst_port_id=None, tenant_id=None, icmpv4_type=None, + icmpv4_code=None, arp_op=None, arp_spa=None, arp_tpa=None, + arp_sha=None, arp_tha=None, ipv6_src=None, ipv6_dst=None, + ipv6_flabel=None, icmpv6_type=None, icmpv6_code=None, + ipv6_nd_target=None, ipv6_nd_sll=None, ipv6_nd_tll=None) + session.add(match) + session.flush() + return vnffg + + def test_validate_tosca(self): + template = utils.vnffgd_tosca_template + self.nfvo_plugin.validate_tosca(template) + + def test_validate_tosca_missing_tosca_ver(self): + template = utils.vnffgd_template + self.assertRaises(nfvo.ToscaParserFailed, + self.nfvo_plugin.validate_tosca, + template) + + def test_validate_tosca_invalid(self): + template = utils.vnffgd_invalid_tosca_template + self.assertRaises(nfvo.ToscaParserFailed, + self.nfvo_plugin.validate_tosca, + template) + + def test_create_vnffgd(self): + vnffgd_obj = utils.get_dummy_vnffgd_obj() + result = self.nfvo_plugin.create_vnffgd(self.context, vnffgd_obj) + self.assertIsNotNone(result) + self.assertIn('id', result) + self.assertIn('template', result) + + def test_create_vnffg_abstract_types(self): + with patch.object(TackerManager, 'get_service_plugins') as \ + mock_plugins: + mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} + mock.patch('tacker.common.driver_manager.DriverManager', + side_effect=FakeDriverManager()).start() + self._insert_dummy_vnffg_template() + vnffg_obj = utils.get_dummy_vnffg_obj() + result = self.nfvo_plugin.create_vnffg(self.context, vnffg_obj) + self.assertIsNotNone(result) + self.assertIn('id', result) + self.assertIn('status', result) + self.assertEqual('PENDING_CREATE', result['status']) + self._driver_manager.invoke.assert_called_with(mock.ANY, mock.ANY, + vnfs=mock.ANY, + fc_id=mock.ANY, + auth_attr=mock.ANY, + symmetrical=mock.ANY + ) + + def test_create_vnffg_vnf_mapping(self): + with patch.object(TackerManager, 'get_service_plugins') as \ + mock_plugins: + mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} + mock.patch('tacker.common.driver_manager.DriverManager', + side_effect=FakeDriverManager()).start() + self._insert_dummy_vnffg_template() + vnffg_obj = utils.get_dummy_vnffg_obj_vnf_mapping() + result = self.nfvo_plugin.create_vnffg(self.context, vnffg_obj) + self.assertIsNotNone(result) + self.assertIn('id', result) + self.assertIn('status', result) + self.assertEqual('PENDING_CREATE', result['status']) + self._driver_manager.invoke.assert_called_with(mock.ANY, mock.ANY, + vnfs=mock.ANY, + fc_id=mock.ANY, + auth_attr=mock.ANY, + symmetrical=mock.ANY + ) + + def test_update_vnffg_nonexistent_vnf(self): + with patch.object(TackerManager, 'get_service_plugins') as \ + mock_plugins: + mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} + mock.patch('tacker.common.driver_manager.DriverManager', + side_effect=FakeDriverManager()).start() + self._insert_dummy_vnffg_template() + vnffg = self._insert_dummy_vnffg() + updated_vnffg = utils.get_dummy_vnffg_obj_vnf_mapping() + updated_vnffg['vnffg']['symmetrical'] = True + updated_vnf_mapping = \ + {'VNF1': '91e32c20-6d1f-47a4-9ba7-08f5e5effe07', + 'VNF3': '5c7f5631-9e74-46e8-b3d2-397c0eda9d0b'} + updated_vnffg['vnffg']['vnf_mapping'] = updated_vnf_mapping + self.assertRaises(nfvo.VnffgInvalidMappingException, + self.nfvo_plugin.update_vnffg, + self.context, vnffg['id'], updated_vnffg) + + def test_update_vnffg(self): + with patch.object(TackerManager, 'get_service_plugins') as \ + mock_plugins: + mock_plugins.return_value = {'VNFM': FakeVNFMPlugin()} + mock.patch('tacker.common.driver_manager.DriverManager', + side_effect=FakeDriverManager()).start() + self._insert_dummy_vnffg_template() + vnffg = self._insert_dummy_vnffg() + updated_vnffg = utils.get_dummy_vnffg_obj_vnf_mapping() + updated_vnffg['vnffg']['symmetrical'] = True + updated_vnf_mapping = \ + {'VNF1': '91e32c20-6d1f-47a4-9ba7-08f5e5effe07', + 'VNF3': '10f66bc5-b2f1-45b7-a7cd-6dd6ad0017f5'} + updated_vnffg['vnffg']['vnf_mapping'] = updated_vnf_mapping + self.nfvo_plugin.update_vnffg(self.context, vnffg['id'], + updated_vnffg) + self._driver_manager.invoke.assert_called_with(mock.ANY, mock.ANY, + vnfs=mock.ANY, + fc_ids=mock.ANY, + chain_id=mock.ANY, + auth_attr=mock.ANY, + symmetrical=True) + + def test_delete_vnffg(self): + self._insert_dummy_vnffg_template() + vnffg = self._insert_dummy_vnffg() + self.nfvo_plugin.delete_vnffg(self.context, vnffg['id']) + self._driver_manager.invoke.assert_called_with(mock.ANY, mock.ANY, + fc_id=mock.ANY, + auth_attr=mock.ANY) diff --git a/tacker/vnfm/tosca/lib/tacker_nfv_defs.yaml b/tacker/vnfm/tosca/lib/tacker_nfv_defs.yaml index d2d089847..ee655f3ab 100644 --- a/tacker/vnfm/tosca/lib/tacker_nfv_defs.yaml +++ b/tacker/vnfm/tosca/lib/tacker_nfv_defs.yaml @@ -1,3 +1,157 @@ +data_types: + tosca.nfv.datatypes.pathType: + properties: + forwarder: + type: string + required: true + capability: + type: string + required: true + + tosca.nfv.datatypes.aclType: + properties: + eth_type: + type: string + required: false + eth_src: + type: string + required: false + eth_dst: + type: string + required: false + vlan_id: + type: integer + constraints: + - in_range: [ 1, 4094 ] + required: false + vlan_pcp: + type: integer + constraints: + - in_range: [ 0, 7 ] + required: false + mpls_label: + type: integer + constraints: + - in_range: [ 16, 1048575] + required: false + mpls_tc: + type: integer + constraints: + - in_range: [ 0, 7 ] + required: false + ip_dscp: + type: integer + constraints: + - in_range: [ 0, 63 ] + required: false + ip_ecn: + type: integer + constraints: + - in_range: [ 0, 3 ] + required: false + ip_src_prefix: + type: string + required: false + ip_dst_prefix: + type: string + required: false + ip_proto: + type: integer + constraints: + - in_range: [ 1, 254 ] + required: false + destination_port_range: + type: string + required: false + source_port_range: + type: string + required: false + network_src_port_id: + type: string + required: false + network_dst_port_id: + type: string + required: false + network_id: + type: string + required: false + network_name: + type: string + required: false + tenant_id: + type: string + required: false + icmpv4_type: + type: integer + constraints: + - in_range: [ 0, 254 ] + required: false + icmpv4_code: + type: integer + constraints: + - in_range: [ 0, 15 ] + required: false + arp_op: + type: integer + constraints: + - in_range: [ 1, 25 ] + required: false + arp_spa: + type: string + required: false + arp_tpa: + type: string + required: false + arp_sha: + type: string + required: false + arp_tha: + type: string + required: false + ipv6_src: + type: string + required: false + ipv6_dst: + type: string + required: false + ipv6_flabel: + type: integer + constraints: + - in_range: [ 0, 1048575] + required: false + icmpv6_type: + type: integer + constraints: + - in_range: [ 0, 255] + required: false + icmpv6_code: + type: integer + constraints: + - in_range: [ 0, 7] + required: false + ipv6_nd_target: + type: string + required: false + ipv6_nd_sll: + type: string + required: false + ipv6_nd_tll: + type: string + required: false + + tosca.nfv.datatypes.policyType: + properties: + type: + type: string + required: false + constraints: + - valid_values: [ ACL ] + criteria: + type: list + required: true + entry_schema: + type: tosca.nfv.datatypes.aclType + node_types: tosca.nodes.nfv.VDU.Tacker: derived_from: tosca.nodes.nfv.VDU @@ -75,3 +229,19 @@ node_types: required: false constraints: - valid_values: [ sriov, vnic ] + + tosca.nodes.nfv.FP.Tacker: + derived_from: tosca.nodes.Root + properties: + id: + type: integer + required: false + policy: + type: tosca.nfv.datatypes.policyType + required: true + description: policy to use to match traffic for this FP + path: + type: list + required: true + entry_schema: + type: tosca.nfv.datatypes.pathType