Multi version API support

This patch provides a base to support multi version API.

The existing code of functions for SOL specification was hard to
understand and enhance since it is based on the code of legacy tacker
API and they are connected with each other complicatedly.

Therefore the code for SOL specification is newly created which
is independent to the legacy tacker API so that it will be easy to
maintain and enhance.

This patch supports vnflcm v2 API (api_version 2.0.0) as a starting
point. It supports less functions than the exsisting v1 API at the
moment(Xena) but it will catch up with by the next release (Y).

This patch makes supporting another API version easy when it will
be supported in the future. Possibly it may thought to add v1 API to
this code base.

TODO: enhance UT/FT
UT/FT is not sufficient at the moment. Additional UTs and FTs will
be provided with another patches.

Implements: blueprint multi-version-api
Implements: blueprint support-nfv-solv3-start-and-terminate-vnf
Implements: blueprint support-nfv-solv3-query-vnf-instances
Implements: blueprint support-nfv-solv3-query-operation-occurrences
Implements: blueprint support-nfv-solv3-subscriptions
Change-Id: If76f315d8b3856e0eef9b8808b90f0b15d80d488
This commit is contained in:
Itsuro Oda 2021-08-06 02:40:20 +00:00
parent 49ab5f9a15
commit 5f35b695bf
107 changed files with 14264 additions and 9 deletions

View File

@ -258,6 +258,15 @@
controller-tacker:
tox_envlist: dsvm-functional-sol
- job:
name: tacker-functional-devstack-multinode-sol-v2
parent: tacker-functional-devstack-multinode-sol
description: |
Multinodes job for SOL V2 devstack-based functional tests
host-vars:
controller-tacker:
tox_envlist: dsvm-functional-sol-v2
- job:
name: tacker-functional-devstack-multinode-sol-separated-nfvo
parent: tacker-functional-devstack-multinode-sol
@ -507,3 +516,4 @@
- tacker-functional-devstack-multinode-sol-separated-nfvo
- tacker-functional-devstack-multinode-sol-kubernetes
- tacker-functional-devstack-multinode-libs-master
- tacker-functional-devstack-multinode-sol-v2

View File

@ -3,7 +3,9 @@ use = egg:Paste#urlmap
/: tackerversions
/v1.0: tackerapi_v1_0
/vnfpkgm/v1: vnfpkgmapi_v1
/vnflcm: vnflcm_versions
/vnflcm/v1: vnflcm_v1
/vnflcm/v2: vnflcm_v2
[composite:tackerapi_v1_0]
use = call:tacker.auth:pipeline_factory
@ -20,6 +22,16 @@ use = call:tacker.auth:pipeline_factory
noauth = request_id catch_errors vnflcmaapp_v1
keystone = request_id catch_errors authtoken keystonecontext vnflcmaapp_v1
[composite:vnflcm_v2]
use = call:tacker.auth:pipeline_factory
noauth = request_id catch_errors vnflcmaapp_v2
keystone = request_id catch_errors authtoken keystonecontext vnflcmaapp_v2
[composite:vnflcm_versions]
use = call:tacker.auth:pipeline_factory
noauth = request_id catch_errors vnflcm_api_versions
keystone = request_id catch_errors authtoken keystonecontext vnflcm_api_versions
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
@ -49,3 +61,9 @@ paste.app_factory = tacker.api.vnfpkgm.v1.router:VnfpkgmAPIRouter.factory
[app:vnflcmaapp_v1]
paste.app_factory = tacker.api.vnflcm.v1.router:VnflcmAPIRouter.factory
[app:vnflcmaapp_v2]
paste.app_factory = tacker.sol_refactored.api.router:VnflcmAPIRouterV2.factory
[app:vnflcm_api_versions]
paste.app_factory = tacker.sol_refactored.api.router:VnflcmVersions.factory

View File

@ -0,0 +1,24 @@
---
features:
- |
Support multi-version of RESTfulAPI. The client can use
both VNF LCM API "1.3.0" and "2.0.0" defined by ETSI NFV.
- |
Add new RESTful APIs of List VNF LCM API versions
and Show VNF LCM API versions based on ETSI NFV specifications.
They enable the client to retrieve supported versions of VNF LCM API.
- |
Add the following new version of RESTful APIs
based on ETSI NFV specifications.
Version "2.0.0" API of Create VNF, Delete VNF,
Instantiate VNF, Terminate VNF, List VNF, Show VNF,
List VNF LCM operation occurrence, Show VNF LCM operation occurrence,
Create subscription, List subscription, and Show subscription are added.
- |
VNF LCM API "2.0.0" provides a new type of userdata script
and utility functions to describe it.
They enable the user to freely operate HEAT to meet the unique
requirements of VNF.

View File

@ -62,6 +62,18 @@ def validate_mac_address_or_none(instance):
return True
@jsonschema.FormatChecker.cls_checks('mac_address',
webob.exc.HTTPBadRequest)
def validate_mac_address(instance):
"""Validate instance is a MAC address"""
if not netaddr.valid_mac(instance):
msg = _("'%s' is not a valid mac address")
raise webob.exc.HTTPBadRequest(explanation=msg % instance)
return True
def _validate_query_parameter_without_value(parameter_name, instance):
"""The query parameter is a flag without a value."""
if not (isinstance(instance, str) and len(instance)):

View File

@ -562,6 +562,11 @@ class VnfLcmController(wsgi.Controller):
return self._view_builder.show(vnf_instance)
@wsgi.response(http_client.OK)
def api_versions(self, request):
return {'uriPrefix': '/vnflcm/v1',
'apiVersions': [{'version': '1.3.0', 'isDeprecated': False}]}
@wsgi.response(http_client.OK)
@wsgi.expected_errors((http_client.FORBIDDEN, http_client.BAD_REQUEST))
@api_common.validate_supported_params({'filter'})

View File

@ -60,6 +60,10 @@ class VnflcmAPIRouter(wsgi.Router):
controller = vnf_lcm_controller.create_resource()
methods = {"GET": "api_versions"}
self._setup_route(mapper, "/api_versions",
methods, controller, default_resource)
# Allowed methods on /vnflcm/v1/vnf_instances resource
methods = {"GET": "index", "POST": "create"}
self._setup_route(mapper, "/vnf_instances",

View File

@ -26,6 +26,7 @@ from tacker._i18n import _
from tacker.common import config
from tacker import objects
from tacker import service
from tacker.sol_refactored import objects as sol_objects
oslo_i18n.install("tacker")
@ -35,6 +36,7 @@ def main():
# the configuration will be read into the cfg.CONF global data structure
config.init(sys.argv[1:])
objects.register_all()
sol_objects.register_all()
if not cfg.CONF.config_file:
sys.exit(_("ERROR: Unable to find configuration file via the default"
" search paths (~/.tacker/, ~/, /etc/tacker/, /etc/) and"

View File

@ -296,11 +296,12 @@ class Connection(object):
self.servers = []
def create_consumer(self, topic, endpoints, fanout=False,
exchange='tacker', host=None):
exchange='tacker', host=None, serializer=None):
target = oslo_messaging.Target(
topic=topic, server=host or cfg.CONF.host, fanout=fanout,
exchange=exchange)
serializer = objects_base.TackerObjectSerializer()
if not serializer:
serializer = objects_base.TackerObjectSerializer()
server = get_server(target, endpoints, serializer)
self.servers.append(server)

View File

@ -65,6 +65,8 @@ from tacker.objects import vnfd as vnfd_db
from tacker.objects import vnfd_attribute as vnfd_attribute_db
from tacker.plugins.common import constants
from tacker import service as tacker_service
from tacker.sol_refactored.conductor import v2_hook
from tacker.sol_refactored import objects as sol_objects
from tacker import version
from tacker.vnflcm import utils as vnflcm_utils
from tacker.vnflcm import vnflcm_driver
@ -296,7 +298,7 @@ def grant_error_common(function):
return decorated_function
class Conductor(manager.Manager):
class Conductor(manager.Manager, v2_hook.ConductorV2Hook):
def __init__(self, host, conf=None):
if conf:
self.conf = conf
@ -2334,6 +2336,7 @@ def init(args, **kwargs):
def main(manager='tacker.conductor.conductor_server.Conductor'):
init(sys.argv[1:])
objects.register_all()
sol_objects.register_all()
logging.setup(CONF, "tacker")
oslo_messaging.set_transport_defaults(control_exchange='tacker')
logging.setup(CONF, "tacker")

View File

@ -169,6 +169,7 @@ class Context(ContextBaseWithSession):
def __init__(self, *args, **kwargs):
super(Context, self).__init__(*args, **kwargs)
self._session = None
self._api_version = None
@property
def session(self):
@ -180,6 +181,14 @@ class Context(ContextBaseWithSession):
self._session = db_api.get_session()
return self._session
@property
def api_version(self):
return self._api_version
@api_version.setter
def api_version(self, api_version):
self._api_version = api_version
def get_admin_context():
return Context(user_id=None,

View File

@ -1 +1 @@
6dc60a5760e5
a23ebee909a8

View File

@ -0,0 +1,97 @@
# Copyright 2021 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.
#
"""introduce_sol_refactored_models
Revision ID: a23ebee909a8
Revises: 6dc60a5760e5
Create Date: 2021-04-20 15:33:42.686284
"""
# flake8: noqa: E402
# revision identifiers, used by Alembic.
revision = 'a23ebee909a8'
down_revision = '6dc60a5760e5'
from alembic import op
import sqlalchemy as sa
def upgrade(active_plugins=None, options=None):
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('LccnSubscriptionV2',
sa.Column('id', sa.String(length=255), nullable=False),
sa.Column('filter', sa.JSON(), nullable=True),
sa.Column('callbackUri', sa.String(length=255), nullable=False),
sa.Column('authentication', sa.JSON(), nullable=True),
sa.Column('verbosity', sa.Enum('FULL', 'SHORT'), nullable=False),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB'
)
op.create_table('VnfInstanceV2',
sa.Column('id', sa.String(length=255), nullable=False),
sa.Column('vnfInstanceName', sa.String(length=255), nullable=True),
sa.Column('vnfInstanceDescription', sa.Text(), nullable=True),
sa.Column('vnfdId', sa.String(length=255), nullable=False),
sa.Column('vnfProvider', sa.String(length=255), nullable=False),
sa.Column('vnfProductName', sa.String(length=255), nullable=False),
sa.Column('vnfSoftwareVersion', sa.String(length=255), nullable=False),
sa.Column('vnfdVersion', sa.String(length=255), nullable=False),
sa.Column('vnfConfigurableProperties', sa.JSON(), nullable=True),
sa.Column('vimConnectionInfo', sa.JSON(), nullable=True),
sa.Column('instantiationState',
sa.Enum('NOT_INSTANTIATED', 'INSTANTIATED'),
nullable=False),
sa.Column('instantiatedVnfInfo', sa.JSON(), nullable=True),
sa.Column('metadata', sa.JSON(), nullable=True),
sa.Column('extensions', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB'
)
op.create_table('VnfLcmOpOccV2',
sa.Column('id', sa.String(length=255), nullable=False),
sa.Column('operationState',
sa.Enum('STARTING', 'PROCESSING', 'COMPLETED', 'FAILED_TEMP',
'FAILED', 'ROLLING_BACK', 'ROLLED_BACK'),
nullable=False),
sa.Column('stateEnteredTime', sa.DateTime(), nullable=False),
sa.Column('startTime', sa.DateTime(), nullable=False),
sa.Column('vnfInstanceId', sa.String(length=255), nullable=False),
sa.Column('grantId', sa.String(length=255), nullable=True),
sa.Column('operation',
sa.Enum('INSTANTIATE', 'SCALE', 'SCALE_TO_LEVEL',
'CHANGE_FLAVOUR', 'TERMINATE', 'HEAL', 'OPERATE',
'CHANGE_EXT_CONN', 'MODIFY_INFO', 'CREATE_SNAPSHOT',
'REVERT_TO_SNAPSHOT', 'CHANGE_VNFPKG'),
nullable=False),
sa.Column('isAutomaticInvocation', sa.Boolean(), nullable=False),
sa.Column('operationParams', sa.JSON(), nullable=True),
sa.Column('isCancelPending', sa.Boolean(), nullable=False),
sa.Column('cancelMode', sa.Enum('GRACEFUL', 'FORCEFUL'), nullable=True),
sa.Column('error', sa.JSON(), nullable=True),
sa.Column('resourceChanges', sa.JSON(), nullable=True),
sa.Column('changedInfo', sa.JSON(), nullable=True),
sa.Column('changedExtConnectivity', sa.JSON(), nullable=True),
sa.Column('modificationsTriggeredByVnfPkgChange', sa.JSON(),
nullable=True),
sa.Column('vnfSnapshotInfoId', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB'
)
# ### end Alembic commands ###

View File

@ -21,12 +21,9 @@ Based on this comparison database can be healed with healing migration.
"""
from tacker.db import model_base
from tacker.db import model_base # noqa
from tacker.db.nfvo import nfvo_db # noqa
from tacker.db.nfvo import ns_db # noqa
from tacker.db.nfvo import vnffg_db # noqa
from tacker.db.vnfm import vnfm_db # noqa
def get_metadata():
return model_base.BASE.metadata
from tacker.sol_refactored.db.sqlalchemy import models # noqa

View File

@ -19,6 +19,7 @@ import itertools
from tacker.policies import base
from tacker.policies import vnf_lcm
from tacker.policies import vnf_package
from tacker.sol_refactored.api.policies import vnflcm_v2
def list_rules():
@ -26,4 +27,5 @@ def list_rules():
base.list_rules(),
vnf_package.list_rules(),
vnf_lcm.list_rules(),
vnflcm_v2.list_rules(),
)

View File

@ -22,6 +22,17 @@ from tacker.policies import base
VNFLCM = 'os_nfv_orchestration_api:vnf_instances:%s'
rules = [
policy.DocumentedRuleDefault(
name=VNFLCM % 'api_versions',
check_str=base.RULE_ANY,
description="Get API Versions.",
operations=[
{
'method': 'GET',
'path': '/vnflcm/v1/api_versions'
}
]
),
policy.DocumentedRuleDefault(
name=VNFLCM % 'create',
check_str=base.RULE_ADMIN_OR_OWNER,

View File

@ -0,0 +1,115 @@
# 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 re
from tacker.sol_refactored.common import exceptions as sol_ex
supported_versions_v1 = {
'uriPrefix': '/vnflcm/v1',
'apiVersions': [
{'version': '1.3.0', 'isDeprecated': False}
]
}
supported_versions_v2 = {
'uriPrefix': '/vnflcm/v2',
'apiVersions': [
{'version': '2.0.0', 'isDeprecated': False}
]
}
CURRENT_VERSION = '2.0.0'
supported_versions = [
item['version'] for item in supported_versions_v2['apiVersions']
]
class APIVersion(object):
def __init__(self, version_string=None):
self.ver_major = 0
self.ver_minor = 0
self.ver_patch = 0
if version_string is None:
return
version_string = self._get_version_id(version_string)
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)\.([1-9]\d*|0)$",
version_string)
if match:
self.ver_major = int(match.group(1))
self.ver_minor = int(match.group(2))
self.ver_patch = int(match.group(3))
else:
raise sol_ex.InvalidAPIVersionString(version=version_string)
if version_string not in supported_versions:
raise sol_ex.APIVersionNotSupported(version=version_string)
def _get_version_id(self, version_string):
# version example (see. SOL013 Table 4.2.2-1)
# `1.2.0` or `1.2.0-impl:example.com:myProduct:4`
# This method checks the later case and return the part of
# version identifier. check is loose.
if '-' not in version_string:
return version_string
items = version_string.split('-')
if len(items) == 2 and items[1].startswith("impl:"):
return items[0]
raise sol_ex.InvalidAPIVersionString(version=version_string)
def is_null(self):
return (self.ver_major, self.ver_minor, self.ver_patch) == (0, 0, 0)
def __str__(self):
return "%d.%d.%d" % (self.ver_major, self.ver_minor, self.ver_patch)
def __lt__(self, other):
return ((self.ver_major, self.ver_minor, self.ver_patch) <
(other.ver_major, other.ver_minor, other.ver_patch))
def __eq__(self, other):
return ((self.ver_major, self.ver_minor, self.ver_patch) ==
(other.ver_major, other.ver_minor, other.ver_patch))
def __gt__(self, other):
return ((self.ver_major, self.ver_minor, self.ver_patch) >
(other.ver_major, other.ver_minor, other.ver_patch))
def __le__(self, other):
return self < other or self == other
def __ne__(self, other):
return not self.__eq__(other)
def __ge__(self, other):
return self > other or self == other
def matches(self, min_version, max_version):
if self.is_null():
return False
if max_version.is_null() and min_version.is_null():
return True
elif max_version.is_null():
return min_version <= self
elif min_version.is_null():
return self <= max_version
else:
return min_version <= self <= max_version

View File

@ -0,0 +1,170 @@
# 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.
from oslo_policy import policy
POLICY_NAME = 'os_nfv_orchestration_api_v2:vnf_instances:{}'
RULE_ANY = '@'
V2_PATH = '/vnflcm/v2'
API_VERSIONS_PATH = V2_PATH + '/api_versions'
VNF_INSTANCES_PATH = V2_PATH + '/vnf_instances'
VNF_INSTANCES_ID_PATH = VNF_INSTANCES_PATH + '/{vnfInstanceId}'
SUBSCRIPTIONS_PATH = V2_PATH + '/subscriptions'
SUBSCRIPTIONS_ID_PATH = VNF_INSTANCES_PATH + '/{subscriptionId}'
VNF_LCM_OP_OCCS_PATH = V2_PATH + '/vnf_lcm_op_occs'
VNF_LCM_OP_OCCS_ID_PATH = VNF_LCM_OP_OCCS_PATH + '/{vnfLcmOpOccId}'
rules = [
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('api_versions'),
check_str=RULE_ANY,
description="Get API Versions.",
operations=[
{'method': 'GET',
'path': API_VERSIONS_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('create'),
check_str=RULE_ANY,
description="Creates vnf instance.",
operations=[
{'method': 'POST',
'path': VNF_INSTANCES_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('index'),
check_str=RULE_ANY,
description="Query VNF instances.",
operations=[
{'method': 'GET',
'path': VNF_INSTANCES_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('show'),
check_str=RULE_ANY,
description="Query an Individual VNF instance.",
operations=[
{'method': 'GET',
'path': VNF_INSTANCES_ID_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('delete'),
check_str=RULE_ANY,
description="Delete an Individual VNF instance.",
operations=[
{'method': 'DELETE',
'path': VNF_INSTANCES_ID_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('instantiate'),
check_str=RULE_ANY,
description="Instantiate vnf instance.",
operations=[
{'method': 'POST',
'path': VNF_INSTANCES_ID_PATH + '/instantiate'}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('terminate'),
check_str=RULE_ANY,
description="Terminate vnf instance.",
operations=[
{'method': 'POST',
'path': VNF_INSTANCES_ID_PATH + '/terminate'}
]
),
# TODO(oda-g): add more lcm operations etc when implemented.
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('subscription_create'),
check_str=RULE_ANY,
description="Create subscription.",
operations=[
{'method': 'POST',
'path': SUBSCRIPTIONS_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('subscription_list'),
check_str=RULE_ANY,
description="List subscription.",
operations=[
{'method': 'GET',
'path': SUBSCRIPTIONS_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('subscription_show'),
check_str=RULE_ANY,
description="Show subscription.",
operations=[
{'method': 'GET',
'path': SUBSCRIPTIONS_ID_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('subscription_delete'),
check_str=RULE_ANY,
description="Delete subscription.",
operations=[
{'method': 'DELETE',
'path': SUBSCRIPTIONS_ID_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('lcm_op_occ_list'),
check_str=RULE_ANY,
description="List VnfLcmOpOcc.",
operations=[
{'method': 'GET',
'path': VNF_LCM_OP_OCCS_PATH}
]
),
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('lcm_op_occ_show'),
check_str=RULE_ANY,
description="Show VnfLcmOpOcc.",
operations=[
{'method': 'GET',
'path': VNF_LCM_OP_OCCS_ID_PATH}
]
),
# NOTE: 'DELETE' is not defined in the specification. It is for test
# use since it is convenient to be able to delete under development.
# It is available when config parameter
# v2_vnfm.test_enable_lcm_op_occ_delete set to True.
policy.DocumentedRuleDefault(
name=POLICY_NAME.format('lcm_op_occ_delete'),
check_str=RULE_ANY,
description="Delete VnfLcmOpOcc.",
operations=[
{'method': 'DELETE',
'path': VNF_LCM_OP_OCCS_ID_PATH}
]
),
]
def list_rules():
return rules

View File

@ -0,0 +1,54 @@
# 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.
from tacker.sol_refactored.api.policies import vnflcm_v2 as vnflcm_policy_v2
from tacker.sol_refactored.api import wsgi as sol_wsgi
from tacker.sol_refactored.controller import vnflcm_v2
from tacker.sol_refactored.controller import vnflcm_versions
class VnflcmVersions(sol_wsgi.SolAPIRouter):
controller = sol_wsgi.SolResource(
vnflcm_versions.VnfLcmVersionsController())
route_list = [("/api_versions", {"GET": "index"})]
class VnflcmAPIRouterV2(sol_wsgi.SolAPIRouter):
controller = sol_wsgi.SolResource(vnflcm_v2.VnfLcmControllerV2(),
policy_name=vnflcm_policy_v2.POLICY_NAME)
route_list = [
("/vnf_instances", {"GET": "index", "POST": "create"}),
("/vnf_instances/{id}",
{"DELETE": "delete", "GET": "show", "PATCH": "update"}),
("/vnf_instances/{id}/instantiate", {"POST": "instantiate"}),
("/vnf_instances/{id}/heal", {"POST": "heal"}),
("/vnf_instances/{id}/terminate", {"POST": "terminate"}),
("/vnf_instances/{id}/scale", {"POST": "scale"}),
("/api_versions", {"GET": "api_versions"}),
("/subscriptions", {"GET": "subscription_list",
"POST": "subscription_create"}),
("/subscriptions/{id}", {"GET": "subscription_show",
"DELETE": "subscription_delete"}),
("/vnf_lcm_op_occs", {"GET": "lcm_op_occ_list"}),
# NOTE: 'DELETE' is not defined in the specification. It is for test
# use since it is convenient to be able to delete under development.
# It is available when config parameter
# v2_vnfm.test_enable_lcm_op_occ_delete set to True.
("/vnf_lcm_op_occs/{id}", {"GET": "lcm_op_occ_show",
"DELETE": "lcm_op_occ_delete"})
]

View File

@ -0,0 +1,244 @@
# 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.
from tacker.api.validation import parameter_types
# SOL013 7.2.2
Identifier = {
'type': 'string', 'minLength': 1, 'maxLength': 255
}
# SOL003 4.4.2.2
IdentifierInVnfd = {
'type': 'string', 'minLength': 1, 'maxLength': 255
}
# SOL003 4.4.2.2
IdentifierInVim = {
'type': 'string', 'minLength': 1, 'maxLength': 255
}
# SOL003 4.4.2.2
IdentifierInVnf = {
'type': 'string', 'minLength': 1, 'maxLength': 255
}
# SOL003 4.4.2.2
IdentifierLocal = {
'type': 'string', 'minLength': 1, 'maxLength': 255
}
# SOL003 4.4.1.7
ResourceHandle = {
'type': 'object',
'properties': {
'vimConnectionId': Identifier,
'resourceProviderId': Identifier,
'resourceId': IdentifierInVim,
'vimLevelResourceType': {'type': 'string', 'maxLength': 255},
},
'required': ['resourceId'],
'additionalProperties': True,
}
# SOL003 4.4.1.6
VimConnectionInfo = {
'type': 'object',
'properties': {
'vimId': Identifier,
'vimType': {'type': 'string', 'minLength': 1, 'maxLength': 255},
'interfaceInfo': parameter_types.keyvalue_pairs,
'accessInfo': parameter_types.keyvalue_pairs,
'extra': parameter_types.keyvalue_pairs,
},
'required': ['vimType'],
'additionalProperties': True,
}
# SOL003 4.4.1.10c (inner)
_IpAddresses = {
'type': 'object',
'properties': {
'type': {'enum': ('IPV4', 'IPV6')},
'fixedAddresses': {'type': 'array'},
'numDynamicAddresses': parameter_types.positive_integer,
'addressRange': {'type': 'object'},
'subnetId': IdentifierInVim
},
'if': {'properties': {'type': {'const': 'IPV4'}}},
'then': {
'properties': {
'fixedAddresses': {
'type': 'array',
'items': {'type': 'string', 'format': 'ipv4'}
},
'addressRange': {
'type': 'object',
'properties': {
'minAddress': {'type': 'string', 'format': 'ipv4'},
'maxAddress': {'type': 'string', 'format': 'ipv4'}
},
'required': ['minAddress', 'maxAddress'],
'additionalProperties': True
},
}
},
'else': {
'properties': {
'fixedAddresses': {
'type': 'array',
'items': {'type': 'string', 'format': 'ipv6'}
},
'addressRange': {
'type': 'object',
'properties': {
'minAddress': {'type': 'string', 'format': 'ipv6'},
'maxAddress': {'type': 'string', 'format': 'ipv6'}
},
'required': ['minAddress', 'maxAddress'],
'additionalProperties': True
},
}
},
'required': ['type'],
'oneOf': [
{'required': ['numDynamicAddresses']},
{'required': ['fixedAddresses']},
{'required': ['addressRange']},
],
'additionalProperties': True
}
# SOL003 4.4.1.10c
IpOverEthernetAddressData = {
'type': 'object',
'properties': {
'macAddress': {'type': 'string', 'format': 'mac_address'},
'segmentationId': {'type': 'string'},
'ipAddresses': {
'type': 'array',
'items': _IpAddresses}
},
'anyOf': [
{'required': ['macAddress']},
{'required': ['ipAddresses']}
],
'additionalProperties': True
}
# SOL003 4.4.1.10b
CpProtocolData = {
'type': 'object',
'properties': {
'layerProtocol': {
'type': 'string',
'enum': 'IP_OVER_ETHERNET'},
'ipOverEthernet': IpOverEthernetAddressData,
},
'required': ['layerProtocol'],
'additionalProperties': True,
}
# SOL003 4.4.1.10a
VnfExtCpConfig = {
'type': 'object',
'properties': {
'parentCpConfigId': IdentifierInVnf,
'linkPortId': Identifier,
'cpProtocolData': {
'type': 'array',
'items': CpProtocolData}
},
'additionalProperties': True
}
# SOL003 4.4.1.10
VnfExtCpData = {
'type': 'object',
'properties': {
'cpdId': IdentifierInVnfd,
'cpConfig': {
'type': 'object',
'minProperties': 1,
'patternProperties': {
'^.*$': VnfExtCpConfig
}
}
},
'required': ['cpdId', 'cpConfig'],
'additionalProperties': True
}
# SOL003 4.4.1.14
ExtLinkPortData = {
'type': 'object',
'properties': {
'id': Identifier,
'resourceHandle': ResourceHandle,
},
'required': ['id', 'resourceHandle'],
'additionalProperties': True,
}
# SOL003 4.4.1.11
ExtVirtualLinkData = {
'type': 'object',
'properties': {
'id': Identifier,
'vimConnectionId': Identifier,
'resourceProviderId': Identifier,
'resourceId': IdentifierInVim,
'extCps': {
'type': 'array',
'minItems': 1,
'items': VnfExtCpData},
'extLinkPorts': {
'type': 'array',
'items': ExtLinkPortData}
},
'required': ['id', 'resourceId', 'extCps'],
'additionalProperties': True
}
# SOL003 5.5.3.18
VnfLinkPortData = {
'type': 'object',
'properties': {
'vnfLinkPortId': Identifier,
'resourceHandle': ResourceHandle
},
'required': ['vnfLinkPortId', 'resourceHandle'],
'additionalProperties': True,
}
# SOL003 4.4.1.12
ExtManagedVirtualLinkData = {
'type': 'object',
'properties': {
'id': Identifier,
'vnfVirtualLinkDescId': IdentifierInVnfd,
'vimConnectionId': Identifier,
'resourceProviderId': Identifier,
'resourceId': IdentifierInVim,
'vnfLinkPort': {
'type': 'array',
'items': VnfLinkPortData},
'extManagedMultisiteVirtualLinkId': Identifier
},
'required': ['id', 'vnfVirtualLinkDescId', 'resourceId'],
'additionalProperties': True,
}

View File

@ -0,0 +1,252 @@
# 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.
from tacker.api.validation import parameter_types
from tacker.sol_refactored.api.schemas import common_types
# SOL003 5.5.2.3
CreateVnfRequest_V200 = {
'type': 'object',
'properties': {
'vnfdId': common_types.Identifier,
'vnfInstanceName': {'type': 'string', 'maxLength': 255},
'vnfInstanceDescription': {'type': 'string', 'maxLength': 1024},
'metadata': parameter_types.keyvalue_pairs,
},
'required': ['vnfdId'],
'additionalProperties': True,
}
# SOL003 5.5.2.4
InstantiateVnfRequest_V200 = {
'type': 'object',
'properties': {
'flavourId': common_types.IdentifierInVnfd,
'instantiationLevelId': common_types.IdentifierInVnfd,
'extVirtualLinks': {
'type': 'array',
'items': common_types.ExtVirtualLinkData},
'extManagedVirtualLinks': {
'type': 'array',
'items': common_types.ExtManagedVirtualLinkData},
'vimConnectionInfo': {
'type': 'object',
'patternProperties': {
'^.*$': common_types.VimConnectionInfo
},
},
'localizationLanguage': {'type': 'string', 'maxLength': 255},
'additionalParams': parameter_types.keyvalue_pairs,
'extensions': parameter_types.keyvalue_pairs,
'vnfConfigurableProperties': parameter_types.keyvalue_pairs
},
'required': ['flavourId'],
'additionalProperties': True,
}
# SOL003 5.5.2.8
TerminateVnfRequest_V200 = {
'type': 'object',
'properties': {
'terminationType': {
'type': 'string',
'enum': [
'FORCEFUL',
'GRACEFUL']
},
'gracefulTerminationTimeout': {
'type': 'integer', 'minimum': 1
},
'additionalParams': parameter_types.keyvalue_pairs,
},
'required': ['terminationType'],
'additionalProperties': True,
}
# SOL013 8.3.4
_SubscriptionAuthentication = {
'type': 'object',
'properties': {
'authType': {
'type': 'array',
'items': {
'type': 'string',
'enum': [
'BASIC',
'OAUTH2_CLIENT_CREDENTIALS',
'TLS_CERT']
}
},
'paramsBasic': {
'type': 'object',
'properties': {
'userName': {'type': 'string'},
'password': {'type': 'string'}
},
# NOTE: must be specified since the way to specify them out of
# band is not supported.
'required': ['userName', 'password']
},
'paramsOauth2ClientCredentials': {
'type': 'object',
'properties': {
'clientId': {'type': 'string'},
'clientPassword': {'type': 'string'},
'tokenEndpoint': {'type': 'string'}
},
# NOTE: must be specified since the way to specify them out of
# band is not supported.
'required': ['clientId', 'clientPassword', 'tokenEndpoint']
}
},
'required': ['authType'],
'additionalProperties': True,
}
# SOL003 4.4.1.5 inner
_VnfProductVersions = {
'type': 'array',
'items': {
'type': 'objects',
'properties': {
'vnfSoftwareVersion': {'type': 'string'},
'vnfdVersions': {
'type': 'array',
'items': {'type': 'string'}
}
},
'required': ['vnfSoftwareVersion'],
'additionalProperties': True,
}
}
# SOL003 4.4.1.5 inner
_VnfProducts = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'vnfProductName': {'type': 'string'},
'versions': _VnfProductVersions
},
'required': ['vnfProductName'],
'additionalProperties': True,
}
}
# SOL003 4.4.1.5 inner
_VnfProductsFromProviders = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'type': 'object',
'properties': {
'vnfProvider': {'type': 'string'},
'vnfProducts': _VnfProducts
}
},
'required': ['vnfProvider'],
'additionalProperties': True,
}
}
# SOL003 4.4.1.5
_VnfInstanceSubscriptionFilter = {
'type': 'object',
'properties': {
'vnfdIds': {
'type': 'array',
'items': common_types.Identifier
},
'vnfProductsFromProviders': _VnfProductsFromProviders,
'vnfInstanceIds': {
'type': 'array',
'items': common_types.Identifier
},
'vnfInstanceNames': {
'type': 'array',
'items': {'type': 'string'}
}
},
'additionalProperties': True,
}
# SOL003 5.5.3.12
_LifecycleChangeNotificationsFilter = {
'type': 'object',
'properties': {
'vnfInstanceSubscriptionFilter': _VnfInstanceSubscriptionFilter,
'notificationTypes': {
'type': 'array',
'items': {
'type': 'string',
'enum': [
'VnfLcmOperationOccurrenceNotification',
'VnfIdentifierCreationNotification',
'VnfIdentifierDeletionNotification']
}
},
'operationTypes': {
'type': 'array',
'items': {
'type': 'string',
'enum': [
'INSTANTIATE',
'SCALE',
'SCALE_TO_LEVEL',
'CHANGE_FLAVOUR',
'TERMINATE',
'HEAL',
'OPERATE',
'CHANGE_EXT_CONN',
'MODIFY_INFO']
}
},
'operationStates': {
'type': 'array',
'items': {
'type': 'string',
'enum': [
'STARTING',
'PROCESSING',
'COMPLETED',
'FAILED_TEMP',
'FAILED',
'ROLLING_BACK',
'ROLLED_BACK']
}
}
},
'additionalProperties': True,
}
# SOL003 5.5.2.15
LccnSubscriptionRequest_V200 = {
'type': 'object',
'properties': {
'filter': _LifecycleChangeNotificationsFilter,
'callbackUri': {'type': 'string', 'maxLength': 255},
'authentication': _SubscriptionAuthentication,
'verbosity': {
'type': 'string',
'enum': ['FULL', 'SHORT']
}
},
'required': ['callbackUri'],
'additionalProperties': True,
}

View File

@ -0,0 +1,49 @@
# 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 functools
from tacker.api.validation import validators
from tacker.common import exceptions as tacker_ex
from tacker.sol_refactored.api import api_version
from tacker.sol_refactored.common import exceptions as sol_ex
class SolSchemaValidator(validators._SchemaValidator):
def validate(self, *args, **kwargs):
try:
super(SolSchemaValidator, self).validate(*args, **kwargs)
except tacker_ex.ValidationError as ex:
raise sol_ex.SolValidationError(detail=str(ex))
def schema(request_body_schema, min_version, max_version=None):
def add_validator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
ver = kwargs['request'].context.api_version
min_ver = api_version.APIVersion(min_version)
max_ver = api_version.APIVersion(max_version)
if ver.matches(min_ver, max_ver):
schema_validator = SolSchemaValidator(request_body_schema)
schema_validator.validate(kwargs['body'])
return func(*args, **kwargs)
return wrapper
return add_validator

View File

@ -0,0 +1,180 @@
# 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 routes
import webob
import oslo_i18n as i18n
from oslo_log import log as logging
from tacker.common import exceptions as common_ex
from tacker import wsgi
from tacker.sol_refactored.api import api_version
from tacker.sol_refactored.common import config
from tacker.sol_refactored.common import exceptions as sol_ex
LOG = logging.getLogger(__name__)
class SolResponse(object):
# SOL013 4.2.3 Response header field
allowed_headers = ['version', 'location', 'content_type',
'www_authenticate', 'accept_ranges', 'content_range',
'retry_after', 'link']
def __init__(self, status, body, **kwargs):
self.status = status
self.body = body
self.headers = {}
for hdr in self.allowed_headers:
if hdr in kwargs:
self.headers[hdr] = kwargs[hdr]
self.headers.setdefault('version', api_version.CURRENT_VERSION)
self.headers.setdefault('accept-ranges', 'none')
def serialize(self, request, content_type):
self.headers.setdefault('content_type', content_type)
content_type = self.headers['content_type']
if self.body is None:
body = None
elif content_type == 'text/plain':
body = self.body
elif content_type == 'application/zip':
body = self.body
else: # 'application/json'
serializer = wsgi.JSONDictSerializer()
body = serializer.serialize(self.body)
if len(body) > config.CONF.v2_vnfm.max_content_length:
raise sol_ex.ResponseTooBig(
size=config.CONF.v2_vnfm.max_content_length)
response = w