5f35b695bf
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
367 lines
13 KiB
Python
367 lines
13 KiB
Python
# Copyright (C) 2021 Nippon Telegraph and Telephone Corporation
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from datetime import datetime
|
|
import re
|
|
|
|
from dateutil import parser
|
|
|
|
from oslo_log import log as logging
|
|
|
|
from tacker.sol_refactored.common import exceptions as sol_ex
|
|
from tacker.sol_refactored.common import lcm_op_occ_utils as lcmocc_utils
|
|
from tacker.sol_refactored.common import subscription_utils as subsc_utils
|
|
from tacker.sol_refactored.common import vnf_instance_utils as inst_utils
|
|
from tacker.sol_refactored import objects
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class FilterExpr(object):
|
|
def __init__(self, op, attr, values):
|
|
self.op = op
|
|
self.attr = attr
|
|
self.values = values
|
|
|
|
def match_eq(self, val):
|
|
return val == self.values[0]
|
|
|
|
def match_neq(self, val):
|
|
return val != self.values[0]
|
|
|
|
def match_in(self, val):
|
|
return val in self.values
|
|
|
|
def match_nin(self, val):
|
|
return val not in self.values
|
|
|
|
def match_gt(self, val):
|
|
return val > self.values[0]
|
|
|
|
def match_gte(self, val):
|
|
return val >= self.values[0]
|
|
|
|
def match_lt(self, val):
|
|
return val < self.values[0]
|
|
|
|
def match_lte(self, val):
|
|
return val <= self.values[0]
|
|
|
|
def match_cont(self, val):
|
|
for v in self.values:
|
|
if v in val:
|
|
return True
|
|
return False
|
|
|
|
def match_ncont(self, val):
|
|
return not self.match_cont(val)
|
|
|
|
def match(self, val):
|
|
try:
|
|
for a in self.attr:
|
|
# TODO(toshii): handle "@key"
|
|
val = val[a]
|
|
except KeyError:
|
|
LOG.debug("Attr %s not found in %s", self.attr, val)
|
|
return False
|
|
LOG.debug("Key %s type %s", self.attr, type(val))
|
|
# If not str, assume type conversion is already done.
|
|
# Note: It is assumed that the type doesn't change between calls,
|
|
# which can be problematic with KeyValuePairs.
|
|
if isinstance(self.values[0], str):
|
|
if isinstance(val, datetime):
|
|
self.values[0] = parser.isoparse(self.values[0])
|
|
elif isinstance(val, bool):
|
|
self.values[0] = bool(self.values[0])
|
|
elif isinstance(val, int):
|
|
self.values = [int(v) for v in self.values]
|
|
elif isinstance(val, float):
|
|
self.values = [float(v) for v in self.values]
|
|
|
|
return getattr(self, "match_" + self.op)(val)
|
|
|
|
|
|
class AttributeSelector(object):
|
|
def __init__(self, default_exclude_list, all_fields=None, fields=None,
|
|
exclude_fields=None, exclude_default=None):
|
|
self.exclude_fields = []
|
|
self.fields = []
|
|
if all_fields is not None:
|
|
if fields is not None or exclude_fields is not None or \
|
|
exclude_default is not None:
|
|
raise sol_ex.InvalidAttributeSelector()
|
|
# Nothing to do
|
|
elif fields is not None:
|
|
if exclude_fields is not None:
|
|
raise sol_ex.InvalidAttributeSelector()
|
|
self.fields = fields.split(',')
|
|
if exclude_default is not None:
|
|
self.exclude_fields = [v for v in default_exclude_list
|
|
if v not in self.fields]
|
|
elif exclude_fields is not None:
|
|
if exclude_default is not None:
|
|
raise sol_ex.InvalidAttributeSelector()
|
|
self.exclude_fields = exclude_fields.split(',')
|
|
else:
|
|
self.exclude_fields = default_exclude_list
|
|
|
|
def filter(self, obj, odict):
|
|
deleted = {}
|
|
if self.exclude_fields:
|
|
excl_fields = self.exclude_fields
|
|
else:
|
|
if not self.fields:
|
|
# Implies all_fields
|
|
return odict
|
|
excl_fields = [k for k in odict.keys() if k not in self.fields]
|
|
|
|
for k in excl_fields:
|
|
klist = k.split('/')
|
|
if len(klist) > 1:
|
|
# TODO(toshii): check if this nested field is nullable
|
|
pass
|
|
else:
|
|
if not obj.fields[klist[0]].nullable:
|
|
continue
|
|
val = odict
|
|
deleted_ptr = deleted
|
|
try:
|
|
for i, k1 in enumerate(klist, start=1):
|
|
if i == len(klist):
|
|
deleted_ptr[k1] = val[k1]
|
|
del val[k1]
|
|
else:
|
|
val = val[k1]
|
|
if k1 not in deleted_ptr:
|
|
deleted_ptr[k1] = {}
|
|
deleted_ptr = deleted_ptr[k1]
|
|
except KeyError:
|
|
pass
|
|
if not self.fields:
|
|
return odict
|
|
|
|
# Readd partial dictionary content
|
|
for k in self.fields:
|
|
klist = k.split('/')
|
|
val = odict
|
|
deleted_ptr = deleted
|
|
try:
|
|
for i, k1 in enumerate(klist, start=1):
|
|
if i == len(klist):
|
|
val[k1] = deleted_ptr[k1]
|
|
else:
|
|
if k1 not in val:
|
|
val[k1] = {}
|
|
val = val[k1]
|
|
deleted_ptr = deleted_ptr[k1]
|
|
except KeyError:
|
|
LOG.debug("Key %s not found in %s or %s", k1, val, deleted_ptr)
|
|
return odict
|
|
|
|
|
|
class BaseViewBuilder(object):
|
|
value_regexp = r"([^',)]+|('[^']*')+)"
|
|
value_re = re.compile(value_regexp)
|
|
simpleFilterExpr_re = re.compile(r"\(([a-z]+),([^,]+)(," +
|
|
value_regexp + r")+\)")
|
|
tildeEscape_re = re.compile(r"~([1ab])")
|
|
opOne = ['eq', 'neq', 'gt', 'gte', 'lt', 'lte']
|
|
opMulti = ['in', 'nin', 'cont', 'ncont']
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def parse_attr(self, attr):
|
|
def tilde_unescape(string):
|
|
def repl(m):
|
|
if m.group(1) == '1':
|
|
return '/'
|
|
elif m.group(1) == 'a':
|
|
return ','
|
|
elif m.group(1) == 'b':
|
|
return '@'
|
|
|
|
s1 = self.tildeEscape_re.sub(repl, string)
|
|
return re.sub('~0', '~', s1)
|
|
|
|
attrs = attr.split('/')
|
|
# TODO(toshii): handle "@key"
|
|
return [tilde_unescape(a) for a in attrs]
|
|
|
|
def parse_values(self, values):
|
|
loc = 0
|
|
res = []
|
|
while loc < len(values):
|
|
if values[loc] != ",":
|
|
LOG.debug("comma expected, %s at loc %d", values, loc)
|
|
raise sol_ex.InvalidAttributeFilter(
|
|
sol_detail=("value parse error. comma expected, %s" %
|
|
values))
|
|
loc += 1
|
|
m = self.value_re.match(values[loc:])
|
|
if m is None:
|
|
LOG.debug("value parse error, %s at loc %d", values, loc)
|
|
raise sol_ex.InvalidAttributeFilter(
|
|
sol_detail="value parse error")
|
|
loc += m.end()
|
|
if m.group(0).startswith("'"):
|
|
res.append(re.sub("''", "'", m.group(0)[1:-1]))
|
|
else:
|
|
res.append(m.group(0))
|
|
return res
|
|
|
|
def parse_filter(self, filter):
|
|
"""Implement SOL013 5.2 Attribute-based filtering"""
|
|
|
|
loc = 0
|
|
res = []
|
|
while True:
|
|
m = self.simpleFilterExpr_re.match(filter[loc:])
|
|
if m is None:
|
|
LOG.debug("filter %s parse error at char %d", filter, loc)
|
|
raise sol_ex.InvalidAttributeFilter(
|
|
sol_detail="filter parse error")
|
|
op = m.group(1)
|
|
if op not in self.opOne and op not in self.opMulti:
|
|
raise sol_ex.InvalidAttributeFilter(
|
|
sol_detail=("Invalid op %s" % op))
|
|
values = self.parse_values(
|
|
filter[(loc + m.end(2)):(loc + m.end(3))])
|
|
if len(values) > 1 and op not in self.opMulti:
|
|
raise sol_ex.InvalidAttributeFilter(
|
|
sol_detail=("Only one value is allowed for op %s" % op))
|
|
res.append(FilterExpr(op, self.parse_attr(m.group(2)), values))
|
|
loc += m.end()
|
|
if loc == len(filter):
|
|
return res
|
|
if filter[loc] != ';':
|
|
LOG.debug("filter %s parse error at char %d "
|
|
"(semicolon expected)", filter, loc)
|
|
raise sol_ex.InvalidAttributeFilter(
|
|
sol_detail="filter parse error. semicolon expected.")
|
|
loc += 1
|
|
|
|
def parse_selector(self, req):
|
|
"""Implement SOL013 5.3 Attribute selectors"""
|
|
params = {}
|
|
for k in ['all_fields', 'fields', 'exclude_fields', 'exclude_default']:
|
|
v = req.get(k)
|
|
if v is not None:
|
|
params[k] = v
|
|
return AttributeSelector(self._EXCLUDE_DEFAULT, **params)
|
|
|
|
def match_filters(self, val, filters):
|
|
if filters is None:
|
|
return True
|
|
|
|
for f in filters:
|
|
if not f.match(val):
|
|
return False
|
|
return True
|
|
|
|
def detail_list(self, values, filters, selector):
|
|
return [self.detail(v, selector) for v in values
|
|
if self.match_filters(v, filters)]
|
|
|
|
|
|
class InstanceViewBuilder(BaseViewBuilder):
|
|
_EXCLUDE_DEFAULT = ['vnfConfigurableProperties',
|
|
'vimConnectionInfo',
|
|
'instantiatedVnfInfo',
|
|
'metadata',
|
|
'extensions']
|
|
|
|
def __init__(self, endpoint):
|
|
self.endpoint = endpoint
|
|
|
|
def parse_filter(self, filter):
|
|
return super().parse_filter(filter)
|
|
|
|
def detail(self, inst, selector=None):
|
|
# NOTE: _links is not saved in DB. create when it is necessary.
|
|
if not inst.obj_attr_is_set('_links'):
|
|
inst._links = inst_utils.make_inst_links(inst, self.endpoint)
|
|
|
|
resp = inst.to_dict()
|
|
|
|
# remove password from vim_connection_info
|
|
# see SOL003 4.4.1.6
|
|
for vim_info in resp.get('vimConnectionInfo', {}).values():
|
|
if ('accessInfo' in vim_info and
|
|
'password' in vim_info['accessInfo']):
|
|
vim_info['accessInfo'].pop('password')
|
|
|
|
if selector is not None:
|
|
resp = selector.filter(inst, resp)
|
|
return resp
|
|
|
|
def detail_list(self, insts, filters, selector):
|
|
return super().detail_list(insts, filters, selector)
|
|
|
|
|
|
class LcmOpOccViewBuilder(BaseViewBuilder):
|
|
_EXCLUDE_DEFAULT = ['operationParams',
|
|
'error',
|
|
'resourceChanges',
|
|
'changedInfo',
|
|
'changedExtConnectivity']
|
|
|
|
def __init__(self, endpoint):
|
|
self.endpoint = endpoint
|
|
|
|
def parse_filter(self, filter):
|
|
return super().parse_filter(filter)
|
|
|
|
def detail(self, lcmocc, selector=None):
|
|
# NOTE: _links is not saved in DB. create when it is necessary.
|
|
if not lcmocc.obj_attr_is_set('_links'):
|
|
lcmocc._links = lcmocc_utils.make_lcmocc_links(lcmocc,
|
|
self.endpoint)
|
|
resp = lcmocc.to_dict()
|
|
if selector is not None:
|
|
resp = selector.filter(lcmocc, resp)
|
|
return resp
|
|
|
|
def detail_list(self, lcmoccs, filters, selector):
|
|
return super().detail_list(lcmoccs, filters, selector)
|
|
|
|
|
|
class SubscriptionViewBuilder(BaseViewBuilder):
|
|
def __init__(self, endpoint):
|
|
self.endpoint = endpoint
|
|
|
|
def parse_filter(self, filter):
|
|
return super().parse_filter(filter)
|
|
|
|
def detail(self, subsc, selector=None):
|
|
# NOTE: _links is not saved in DB. create when it is necessary.
|
|
if not subsc.obj_attr_is_set('_links'):
|
|
self_href = subsc_utils.subsc_href(subsc.id, self.endpoint)
|
|
subsc._links = objects.LccnSubscriptionV2_Links()
|
|
subsc._links.self = objects.Link(href=self_href)
|
|
|
|
resp = subsc.to_dict()
|
|
|
|
# NOTE: authentication is not included in LccnSubscription
|
|
resp.pop('authentication', None)
|
|
|
|
if selector is not None:
|
|
resp = selector.filter(subsc, resp)
|
|
return resp
|
|
|
|
def detail_list(self, subscs, filters):
|
|
return super().detail_list(subscs, filters, None)
|