tacker/tacker/sol_refactored/controller/vnflcm_view.py

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)