Browse Source

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
changes/93/803693/39
Itsuro Oda 10 months ago
parent
commit
5f35b695bf
  1. 10
      .zuul.yaml
  2. 18
      etc/tacker/api-paste.ini
  3. 24
      releasenotes/notes/add-multi-version-api-support-0653df1edb67162e.yaml
  4. 12
      tacker/api/validation/validators.py
  5. 5
      tacker/api/vnflcm/v1/controller.py
  6. 4
      tacker/api/vnflcm/v1/router.py
  7. 2
      tacker/cmd/eventlet/tacker_server.py
  8. 5
      tacker/common/rpc.py
  9. 5
      tacker/conductor/conductor_server.py
  10. 9
      tacker/context.py
  11. 2
      tacker/db/migration/alembic_migrations/versions/HEAD
  12. 97
      tacker/db/migration/alembic_migrations/versions/a23ebee909a8_introduce_sol_refactored_models.py
  13. 7
      tacker/db/migration/models/head.py
  14. 2
      tacker/policies/__init__.py
  15. 11
      tacker/policies/vnf_lcm.py
  16. 115
      tacker/sol_refactored/api/api_version.py
  17. 170
      tacker/sol_refactored/api/policies/vnflcm_v2.py
  18. 54
      tacker/sol_refactored/api/router.py
  19. 0
      tacker/sol_refactored/api/schemas/__init__.py
  20. 244
      tacker/sol_refactored/api/schemas/common_types.py
  21. 252
      tacker/sol_refactored/api/schemas/vnflcm_v2.py
  22. 49
      tacker/sol_refactored/api/validator.py
  23. 180
      tacker/sol_refactored/api/wsgi.py
  24. 79
      tacker/sol_refactored/common/config.py
  25. 69
      tacker/sol_refactored/common/coordinate.py
  26. 212
      tacker/sol_refactored/common/exceptions.py
  27. 237
      tacker/sol_refactored/common/http_client.py
  28. 181
      tacker/sol_refactored/common/lcm_op_occ_utils.py
  29. 269
      tacker/sol_refactored/common/subscription_utils.py
  30. 79
      tacker/sol_refactored/common/vim_utils.py
  31. 77
      tacker/sol_refactored/common/vnf_instance_utils.py
  32. 353
      tacker/sol_refactored/common/vnfd_utils.py
  33. 0
      tacker/sol_refactored/conductor/__init__.py
  34. 40
      tacker/sol_refactored/conductor/conductor_rpc_v2.py
  35. 131
      tacker/sol_refactored/conductor/conductor_v2.py
  36. 29
      tacker/sol_refactored/conductor/v2_hook.py
  37. 379
      tacker/sol_refactored/conductor/vnflcm_driver_v2.py
  38. 0
      tacker/sol_refactored/controller/__init__.py
  39. 338
      tacker/sol_refactored/controller/vnflcm_v2.py
  40. 27
      tacker/sol_refactored/controller/vnflcm_versions.py
  41. 366
      tacker/sol_refactored/controller/vnflcm_view.py
  42. 0
      tacker/sol_refactored/db/__init__.py
  43. 24
      tacker/sol_refactored/db/api.py
  44. 0
      tacker/sol_refactored/db/sqlalchemy/__init__.py
  45. 100
      tacker/sol_refactored/db/sqlalchemy/models.py
  46. 130
      tacker/sol_refactored/infra_drivers/openstack/heat_utils.py
  47. 535
      tacker/sol_refactored/infra_drivers/openstack/openstack.py
  48. 86
      tacker/sol_refactored/infra_drivers/openstack/userdata_default.py
  49. 59
      tacker/sol_refactored/infra_drivers/openstack/userdata_main.py
  50. 216
      tacker/sol_refactored/infra_drivers/openstack/userdata_utils.py
  51. 67
      tacker/sol_refactored/mgmt_drivers/sample_script.py
  52. 58
      tacker/sol_refactored/nfvo/glance_utils.py
  53. 305
      tacker/sol_refactored/nfvo/local_nfvo.py
  54. 143
      tacker/sol_refactored/nfvo/nfvo_client.py
  55. 125
      tacker/sol_refactored/objects/__init__.py
  56. 440
      tacker/sol_refactored/objects/base.py
  57. 171
      tacker/sol_refactored/test-tools/cli.py
  58. 111
      tacker/sol_refactored/test-tools/notif_endpoint_app.py
  59. 0
      tacker/tests/functional/sol_v2/__init__.py
  60. 198
      tacker/tests/functional/sol_v2/base_v2.py
  61. 241
      tacker/tests/functional/sol_v2/paramgen.py
  62. 87
      tacker/tests/functional/sol_v2/samples/sample1/contents/BaseHOT/simple/nested/VDU1.yaml
  63. 125
      tacker/tests/functional/sol_v2/samples/sample1/contents/BaseHOT/simple/sample1.yaml
  64. 202
      tacker/tests/functional/sol_v2/samples/sample1/contents/Definitions/etsi_nfv_sol001_common_types.yaml
  65. 1463
      tacker/tests/functional/sol_v2/samples/sample1/contents/Definitions/etsi_nfv_sol001_vnfd_types.yaml
  66. 406
      tacker/tests/functional/sol_v2/samples/sample1/contents/Definitions/v2_sample1_df_simple.yaml
  67. 31
      tacker/tests/functional/sol_v2/samples/sample1/contents/Definitions/v2_sample1_top.vnfd.yaml
  68. 55
      tacker/tests/functional/sol_v2/samples/sample1/contents/Definitions/v2_sample1_types.yaml
  69. 4
      tacker/tests/functional/sol_v2/samples/sample1/contents/TOSCA-Metadata/TOSCA.meta
  70. 58
      tacker/tests/functional/sol_v2/samples/sample1/pkggen.py
  71. 89
      tacker/tests/functional/sol_v2/samples/sample2/contents/BaseHOT/simple/nested/VDU1.yaml
  72. 136
      tacker/tests/functional/sol_v2/samples/sample2/contents/BaseHOT/simple/sample2.yaml
  73. 202
      tacker/tests/functional/sol_v2/samples/sample2/contents/Definitions/etsi_nfv_sol001_common_types.yaml
  74. 1463
      tacker/tests/functional/sol_v2/samples/sample2/contents/Definitions/etsi_nfv_sol001_vnfd_types.yaml
  75. 406
      tacker/tests/functional/sol_v2/samples/sample2/contents/Definitions/v2_sample2_df_simple.yaml
  76. 31
      tacker/tests/functional/sol_v2/samples/sample2/contents/Definitions/v2_sample2_top.vnfd.yaml
  77. 55
      tacker/tests/functional/sol_v2/samples/sample2/contents/Definitions/v2_sample2_types.yaml
  78. 67
      tacker/tests/functional/sol_v2/samples/sample2/contents/Scripts/sample_script.py
  79. 4
      tacker/tests/functional/sol_v2/samples/sample2/contents/TOSCA-Metadata/TOSCA.meta
  80. 86
      tacker/tests/functional/sol_v2/samples/sample2/contents/UserData/userdata_default.py
  81. 51
      tacker/tests/functional/sol_v2/samples/sample2/pkggen.py
  82. 147
      tacker/tests/functional/sol_v2/test_vnflcm_basic.py
  83. 72
      tacker/tests/functional/sol_v2/utils.py
  84. 0
      tacker/tests/unit/sol_refactored/__init__.py
  85. 0
      tacker/tests/unit/sol_refactored/api/__init__.py
  86. 62
      tacker/tests/unit/sol_refactored/api/test_api_version.py
  87. 80
      tacker/tests/unit/sol_refactored/api/test_validator.py
  88. 48
      tacker/tests/unit/sol_refactored/api/test_wsgi.py
  89. 0
      tacker/tests/unit/sol_refactored/common/__init__.py
  90. 80
      tacker/tests/unit/sol_refactored/common/test_coordinate.py
  91. 67
      tacker/tests/unit/sol_refactored/common/test_vnf_instance_utils.py
  92. 151
      tacker/tests/unit/sol_refactored/common/test_vnfd_utils.py
  93. 0
      tacker/tests/unit/sol_refactored/controller/__init__.py
  94. 94
      tacker/tests/unit/sol_refactored/controller/test_vnflcm_v2.py
  95. 153
      tacker/tests/unit/sol_refactored/controller/test_vnflcm_view.py
  96. 0
      tacker/tests/unit/sol_refactored/infra_drivers/__init__.py
  97. 0
      tacker/tests/unit/sol_refactored/infra_drivers/openstack/__init__.py
  98. 218
      tacker/tests/unit/sol_refactored/infra_drivers/openstack/test_userdata_utils.py
  99. 0
      tacker/tests/unit/sol_refactored/objects/__init__.py
  100. 134
      tacker/tests/unit/sol_refactored/objects/test_base.py
  101. Some files were not shown because too many files have changed in this diff Show More

10
.zuul.yaml

@ -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

18
etc/tacker/api-paste.ini

@ -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

24
releasenotes/notes/add-multi-version-api-support-0653df1edb67162e.yaml

@ -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.

12
tacker/api/validation/validators.py

@ -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)):

5
tacker/api/vnflcm/v1/controller.py

@ -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'})

4
tacker/api/vnflcm/v1/router.py

@ -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",

2
tacker/cmd/eventlet/tacker_server.py

@ -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"

5
tacker/common/rpc.py

@ -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)

5
tacker/conductor/conductor_server.py

@ -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")

9
tacker/context.py

@ -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,

2
tacker/db/migration/alembic_migrations/versions/HEAD

@ -1 +1 @@
6dc60a5760e5
a23ebee909a8

97
tacker/db/migration/alembic_migrations/versions/a23ebee909a8_introduce_sol_refactored_models.py

@ -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 ###

7
tacker/db/migration/models/head.py

@ -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

2
tacker/policies/__init__.py

@ -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(),
)

11
tacker/policies/vnf_lcm.py

@ -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,

115
tacker/sol_refactored/api/api_version.py

@ -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

170
tacker/sol_refactored/api/policies/vnflcm_v2.py

@ -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

54
tacker/sol_refactored/api/router.py

@ -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"})
]

0
tacker/sol_refactored/api/schemas/__init__.py

244
tacker/sol_refactored/api/schemas/common_types.py

@ -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,
}

252
tacker/sol_refactored/api/schemas/vnflcm_v2.py

@ -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,
}

49
tacker/sol_refactored/api/validator.py

@ -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):