tacker/tacker/sol_refactored/controller/vnflcm_view.py
Itsuro Oda 5f35b695bf 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
2021-09-16 01:19:51 +00:00

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)