tacker/tacker/api/vnfpkgm/v1/controller.py

669 lines
28 KiB
Python

# Copyright (C) 2019 NTT DATA
# 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 http import client as http_client
from io import BytesIO
import json
import mimetypes
import os
import webob
import zipfile
from zipfile import ZipFile
from glance_store import exceptions as store_exceptions
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import excutils
from oslo_utils import uuidutils
from tacker._i18n import _
from tacker.api.schemas import vnf_packages
from tacker.api import validation
from tacker.api.views import vnf_packages as vnf_packages_view
from tacker.common import csar_utils
from tacker.common import exceptions
from tacker.common import utils
from tacker.conductor.conductorrpc import vnf_pkgm_rpc
from tacker.glance_store import store as glance_store
from tacker.objects import fields
from tacker.objects import vnf_package as vnf_package_obj
from tacker.policies import vnf_package as vnf_package_policies
from tacker import wsgi
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class VnfPkgmController(wsgi.Controller):
_view_builder_class = vnf_packages_view.ViewBuilder
def __init__(self):
super(VnfPkgmController, self).__init__()
self.rpc_api = vnf_pkgm_rpc.VNFPackageRPCAPI()
glance_store.initialize_glance_store()
def _get_vnf_package(self, id, request):
# check if id is of type uuid format
if not uuidutils.is_uuid_like(id):
msg = _("Can not find requested vnf package: %s") % id
raise webob.exc.HTTPNotFound(explanation=msg)
try:
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
request.context, id)
except exceptions.VnfPackageNotFound:
msg = _("Can not find requested vnf package: %s") % id
raise webob.exc.HTTPNotFound(explanation=msg)
return vnf_package
@wsgi.response(http_client.CREATED)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN))
@validation.schema(vnf_packages.create)
def create(self, request, body):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'create')
vnf_package = vnf_package_obj.VnfPackage(context=request.context)
vnf_package.onboarding_state = (
fields.PackageOnboardingStateType.CREATED)
vnf_package.operational_state = (
fields.PackageOperationalStateType.DISABLED)
vnf_package.usage_state = fields.PackageUsageStateType.NOT_IN_USE
vnf_package.user_data = body.get('userDefinedData', dict())
vnf_package.tenant_id = request.context.project_id
vnf_package.create()
return self._view_builder.create(request, vnf_package)
@wsgi.response(http_client.OK)
@wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND))
def show(self, request, id):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'show')
# check if id is of type uuid format
if not uuidutils.is_uuid_like(id):
msg = _("Can not find requested vnf package: %s") % id
raise webob.exc.HTTPNotFound(explanation=msg)
try:
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
request.context, id, expected_attrs=[
"vnf_deployment_flavours", "vnfd", "vnf_artifacts"])
except exceptions.VnfPackageNotFound:
msg = _("Can not find requested vnf package: %s") % id
raise webob.exc.HTTPNotFound(explanation=msg)
return self._view_builder.show(request, vnf_package)
@wsgi.response(http_client.OK)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN))
@validation.query_schema(vnf_packages.query_params_v1)
def index(self, request):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'index')
search_opts = {}
search_opts.update(request.GET)
def _key_exists(key, validate_value=True):
try:
request.GET[key]
except KeyError:
return False
return True
all_fields = _key_exists('all_fields')
exclude_default = _key_exists('exclude_default')
fields = request.GET.get('fields')
exclude_fields = request.GET.get('exclude_fields')
filters = request.GET.get('filter')
if not (all_fields or fields or exclude_fields):
exclude_default = True
self._view_builder.validate_attribute_fields(all_fields=all_fields,
fields=fields, exclude_fields=exclude_fields,
exclude_default=exclude_default)
filters = self._view_builder.validate_filter(filters)
vnf_packages = vnf_package_obj.VnfPackagesList.get_by_filters(
request.context, read_deleted='no', filters=filters)
return self._view_builder.index(request, vnf_packages,
all_fields=all_fields, exclude_fields=exclude_fields,
fields=fields, exclude_default=exclude_default)
@wsgi.response(http_client.NO_CONTENT)
@wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND,
http_client.CONFLICT))
def delete(self, request, id):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'delete')
vnf_package = self._get_vnf_package(id, request)
if (vnf_package.operational_state ==
fields.PackageOperationalStateType.ENABLED or
vnf_package.usage_state ==
fields.PackageUsageStateType.IN_USE):
msg = _("VNF Package %(id)s cannot be deleted as it's "
"operational state is %(operational_state)s and usage "
"state is %(usage_state)s.")
raise webob.exc.HTTPConflict(
explanation=msg % {
"id": id,
"operational_state": vnf_package.operational_state,
"usage_state": vnf_package.usage_state})
# Delete vnf_package
self.rpc_api.delete_vnf_package(context, vnf_package)
@wsgi.response(http_client.ACCEPTED)
@wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND,
http_client.CONFLICT,
http_client.REQUESTED_RANGE_NOT_SATISFIABLE))
def fetch_vnf_package_content(self, request, id):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'fetch_package_content')
vnf_package = self._get_vnf_package(id, request)
if vnf_package.onboarding_state != \
fields.PackageOnboardingStateType.ONBOARDED:
msg = _("VNF Package %(id)s onboarding state "
"is not %(onboarding)s")
raise webob.exc.HTTPConflict(explanation=msg % {"id": id,
"onboarding": fields.PackageOnboardingStateType.ONBOARDED})
if vnf_package.size == 0:
try:
zip_file_size = glance_store.get_csar_size(id,
vnf_package.location_glance_store)
vnf_package.size = zip_file_size
vnf_package.save()
except exceptions.VnfPackageLocationInvalid:
msg = _("Vnf package not present at location")
raise webob.exc.HTTPNotFound(explanation=msg)
else:
zip_file_size = vnf_package.size
range_val = self._get_range_from_request(request, zip_file_size)
return self._download(
request.response, range_val, id, vnf_package.location_glance_store,
zip_file_size)
def _download(self, response, range_val, uuid, location, zip_file_size):
offset, chunk_size = 0, None
if range_val:
if isinstance(range_val, webob.byterange.Range):
response_end = zip_file_size - 1
# NOTE(sameert): webob parsing is zero-indexed.
# i.e.,to download first 5 bytes of a 10 byte image,
# request should be "bytes=0-4" and the response would be
# "bytes 0-4/10".
# Range if validated, will never have 'start' object as None.
if range_val.start >= 0:
offset = range_val.start
else:
# NOTE(sameert): Negative start values needs to be
# processed to allow suffix-length for Range request
# like "bytes=-2" as per rfc7233.
if abs(range_val.start) < zip_file_size:
offset = zip_file_size + range_val.start
if range_val.end is not None and range_val.end < zip_file_size:
chunk_size = range_val.end - offset
response_end = range_val.end - 1
else:
chunk_size = zip_file_size - offset
response.status_int = 206
response.headers['Content-Type'] = 'application/zip'
response.app_iter = self._get_csar_zip_data(uuid,
location, offset, chunk_size)
# NOTE(sameert): In case of a full zip download, when
# chunk_size was none, reset it to zip.size to set the
# response header's Content-Length.
if chunk_size is not None:
response.headers['Content-Range'] = 'bytes %s-%s/%s'\
% (offset,
response_end,
zip_file_size)
else:
chunk_size = zip_file_size
response.headers['Content-Length'] = str(chunk_size)
return response
def _get_csar_zip_data(self, uuid, location, offset=0, chunk_size=None):
try:
resp, size = glance_store.load_csar_iter(
uuid, location, offset=offset, chunk_size=chunk_size)
except exceptions.VnfPackageLocationInvalid:
msg = _("Vnf package not present at location")
raise webob.exc.HTTPServerError(explanation=msg)
return resp
def _get_range_from_request(self, request, zip_file_size):
range_str = request._headers.environ.get('HTTP_RANGE')
if range_str is not None:
# NOTE(sameert): We do not support multi range requests.
if ',' in range_str:
msg = _("Requests with multiple ranges are not supported in "
"Tacker. You may make multiple single-range requests "
"instead.")
raise webob.exc.HTTPBadRequest(explanation=msg)
range_ = webob.byterange.Range.parse(range_str)
if range_ is None:
range_err_msg = _("The byte range passed in the 'Range' header"
" did not match any available byte range in the VNF package"
" file")
raise webob.exc.HTTPRequestRangeNotSatisfiable(
explanation=range_err_msg)
# NOTE(sameert): Ensure that a range like bytes=4- for an zip
# size of 3 is invalidated as per rfc7233.
if range_.start >= zip_file_size:
msg = _("Invalid start position in Range header. "
"Start position MUST be in the inclusive range"
"[0, %s].") % (zip_file_size - 1)
raise webob.exc.HTTPRequestRangeNotSatisfiable(
explanation=msg)
return range_
@wsgi.response(http_client.ACCEPTED)
@wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND,
http_client.CONFLICT))
def upload_vnf_package_content(self, request, id, body):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'upload_package_content')
# check if id is of type uuid format
if not uuidutils.is_uuid_like(id):
msg = _("Can not find requested vnf package: %s") % id
return self._make_problem_detail('Not Found', msg, 404)
try:
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
request.context, id)
except exceptions.VnfPackageNotFound:
msg = _("Can not find requested vnf package: %s") % id
return self._make_problem_detail('Not Found', msg, 404)
except Exception as e:
return self._make_problem_detail(
'Internal Server Error', str(e), 500)
if vnf_package.onboarding_state != \
fields.PackageOnboardingStateType.CREATED:
msg = _("VNF Package %(id)s onboarding state "
"is not %(onboarding)s")
return self._make_problem_detail('Conflict', msg % {"id": id,
"onboarding": fields.PackageOnboardingStateType.CREATED},
409)
vnf_package.onboarding_state = (
fields.PackageOnboardingStateType.UPLOADING)
try:
vnf_package.save()
except Exception as e:
return self._make_problem_detail(
'Internal Server Error', str(e), 500)
try:
(location, size, checksum, multihash,
loc_meta) = glance_store.store_csar(context, id, body)
except exceptions.UploadFailedToGlanceStore:
with excutils.save_and_reraise_exception():
vnf_package.onboarding_state = (
fields.PackageOnboardingStateType.CREATED)
try:
vnf_package.save()
except Exception as e:
return self._make_problem_detail(
'Internal Server Error', str(e), 500)
vnf_package.algorithm = CONF.vnf_package.hashing_algorithm
vnf_package.hash = multihash
vnf_package.location_glance_store = location
vnf_package.size = size
try:
vnf_package.save()
except Exception as e:
vnf_package.onboarding_state = (
fields.PackageOnboardingStateType.CREATED)
try:
vnf_package.save()
except Exception as e:
return self._make_problem_detail(
'Internal Server Error', str(e), 500)
return self._make_problem_detail(
'Internal Server Error', str(e), 500)
# process vnf_package
self.rpc_api.upload_vnf_package_content(context, vnf_package)
@wsgi.response(http_client.ACCEPTED)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN,
http_client.NOT_FOUND, http_client.CONFLICT))
@validation.schema(vnf_packages.upload_from_uri)
def upload_vnf_package_from_uri(self, request, id, body):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'upload_from_uri')
url = body['addressInformation']
if not utils.is_valid_url(url):
msg = _("Vnf package url '%s' is invalid") % url
raise webob.exc.HTTPBadRequest(explanation=msg)
vnf_package = self._get_vnf_package(id, request)
if vnf_package.onboarding_state != \
fields.PackageOnboardingStateType.CREATED:
msg = _("VNF Package %(id)s onboarding state is not "
"%(onboarding)s")
raise webob.exc.HTTPConflict(explanation=msg % {"id": id,
"onboarding": fields.PackageOnboardingStateType.CREATED})
vnf_package.onboarding_state = (
fields.PackageOnboardingStateType.UPLOADING)
vnf_package.save()
# process vnf_package
self.rpc_api.upload_vnf_package_from_uri(context, vnf_package,
body['addressInformation'],
user_name=body.get('userName'),
password=body.get('password'))
@wsgi.response(http_client.OK)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN,
http_client.NOT_FOUND, http_client.CONFLICT))
@validation.schema(vnf_packages.patch)
def patch(self, request, id, body):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'patch')
old_vnf_package = self._get_vnf_package(id, request)
vnf_package = old_vnf_package.obj_clone()
user_data = body.get('userDefinedData')
operational_state = body.get('operationalState')
if operational_state:
if vnf_package.onboarding_state == \
fields.PackageOnboardingStateType.ONBOARDED:
if vnf_package.operational_state == operational_state:
msg = _("VNF Package %(id)s is already in "
"%(operationState)s operational state") % {
"id": id,
"operationState": vnf_package.operational_state}
raise webob.exc.HTTPConflict(explanation=msg)
else:
# update vnf_package operational state,
# if vnf_package Onboarding State is ONBOARDED
vnf_package.operational_state = operational_state
else:
if not user_data:
msg = _("Updating operational state is not allowed for VNF"
" Package %(id)s when onboarding state is not "
"%(onboarded)s")
raise webob.exc.HTTPBadRequest(
explanation=msg % {"id": id, "onboarded": fields.
PackageOnboardingStateType.ONBOARDED})
# update user data
if user_data:
for key, value in list(user_data.items()):
if vnf_package.user_data.get(key) == value:
del user_data[key]
if not user_data:
msg = _("The userDefinedData provided in update request is as"
" the existing userDefinedData of vnf package %(id)s."
" Nothing to update.")
raise webob.exc.HTTPConflict(
explanation=msg % {"id": id})
vnf_package.user_data = user_data
vnf_package.save()
return self._view_builder.patch(old_vnf_package, vnf_package)
@wsgi.response(http_client.OK)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN,
http_client.NOT_FOUND, http_client.NOT_ACCEPTABLE,
http_client.CONFLICT,
http_client.INTERNAL_SERVER_ERROR))
def get_vnf_package_vnfd(self, request, id):
context = request.environ['tacker.context']
context.can(vnf_package_policies.VNFPKGM % 'get_vnf_package_vnfd')
valid_accept_headers = ['application/zip', 'text/plain']
accept_headers = request.headers['Accept'].split(',')
for header in accept_headers:
if header not in valid_accept_headers:
msg = _("Accept header %(accept)s is invalid, it should be one"
" of these values: %(valid_values)s")
raise webob.exc.HTTPNotAcceptable(
explanation=msg % {"accept": header,
"valid_values": ",".join(
valid_accept_headers)})
vnf_package = self._get_vnf_package(id, request)
if vnf_package.onboarding_state != \
fields.PackageOnboardingStateType.ONBOARDED:
msg = _("VNF Package %(id)s state is not "
"%(onboarded)s")
raise webob.exc.HTTPConflict(explanation=msg % {"id": id,
"onboarded": fields.PackageOnboardingStateType.ONBOARDED})
try:
vnfd_files_and_data = self.rpc_api.\
get_vnf_package_vnfd(context, vnf_package)
except exceptions.FailedToGetVnfdData as e:
LOG.error(e.msg)
raise webob.exc.HTTPInternalServerError(explanation=str(e.msg))
if 'text/plain' in accept_headers:
# Checking for yaml files only. This is required when there is
# TOSCA.meta file along with single yaml file.
# In such case we need to return single yaml file.
file_list = list(vnfd_files_and_data.keys())
yaml_files = [file for file in file_list if file.endswith(
('.yaml', '.yml'))]
if len(yaml_files) == 1:
request.response.headers['Content-Type'] = 'text/plain'
return vnfd_files_and_data[yaml_files[0]]
elif 'application/zip' in accept_headers:
request.response.headers['Content-Type'] = 'application/zip'
return self._create_vnfd_zip(vnfd_files_and_data)
else:
msg = _("VNFD is implemented as multiple yaml files,"
" Accept header should be 'application/zip'.")
raise webob.exc.HTTPBadRequest(explanation=msg)
else:
request.response.headers['Content-Type'] = 'application/zip'
return self._create_vnfd_zip(vnfd_files_and_data)
@wsgi.response(http_client.OK)
@wsgi.expected_errors((http_client.BAD_REQUEST, http_client.FORBIDDEN,
http_client.NOT_FOUND, http_client.CONFLICT,
http_client.REQUESTED_RANGE_NOT_SATISFIABLE))
def fetch_vnf_package_artifacts(self, request, id, artifact_path):
context = request.environ['tacker.context']
# get policy
context.can(vnf_package_policies.VNFPKGM % 'fetch_artifact')
# get vnf_package
if not uuidutils.is_uuid_like(id):
msg = _("Can not find requested vnf package: %s") % id
raise webob.exc.HTTPNotFound(explanation=msg)
try:
vnf_package = vnf_package_obj.VnfPackage.get_by_id(
request.context, id,
expected_attrs=["vnf_artifacts"])
except exceptions.VnfPackageNotFound:
msg = _("Can not find requested vnf package: %s") % id
raise webob.exc.HTTPNotFound(explanation=msg)
if vnf_package.onboarding_state != \
fields.PackageOnboardingStateType.ONBOARDED:
msg = _("VNF Package %(id)s state is not "
"%(onboarded)s")
raise webob.exc.HTTPConflict(explanation=msg % {"id": id,
"onboarded": fields.PackageOnboardingStateType.ONBOARDED})
offset, chunk_size = 0, None
# get all artifact's path
artifact_file_paths = []
for item in vnf_package.vnf_artifacts:
artifact_file_paths.append(item.artifact_path)
if artifact_path in artifact_file_paths:
# get file's size
csar_path = self._get_csar_path(vnf_package)
absolute_artifact_path = os.path.join(csar_path, artifact_path)
if not os.path.isfile(absolute_artifact_path):
msg = _(
"This type of path(url) '%s' is currently not supported") \
% artifact_path
raise webob.exc.HTTPBadRequest(explanation=msg)
artifact_size = os.path.getsize(absolute_artifact_path)
range_val = self._get_range_from_request(request, artifact_size)
# range_val exists
if range_val:
if isinstance(range_val, webob.byterange.Range):
# get the position of the last byte in the artifact file
response_end = artifact_size - 1
if range_val.start >= 0:
offset = range_val.start
else:
if abs(range_val.start) < artifact_size:
offset = artifact_size + range_val.start
if range_val.end is not None and \
range_val.end < artifact_size:
chunk_size = range_val.end - offset
response_end = range_val.end - 1
else:
chunk_size = artifact_size - offset
request.response.status_int = 206
# range_val does not exist, download the whole content of file
else:
offset = 0
chunk_size = artifact_size
# get file's mineType;
mime_type = mimetypes.guess_type(artifact_path.split('/')[-1])[0]
if mime_type:
request.response.headers['Content-Type'] = mime_type
else:
request.response.headers['Content-Type'] = \
'application/octet-stream'
try:
artifact_data = self._download_vnf_artifact(
absolute_artifact_path, offset, chunk_size)
except exceptions.FailedToGetVnfArtifact as e:
LOG.error(e.msg)
raise webob.exc.HTTPInternalServerError(
explanation=e.msg)
request.response.text = artifact_data.decode('utf-8')
if request.response.status_int == 206:
request.response.headers['Content-Range'] = 'bytes %s-%s/%s' \
% (offset,
response_end,
artifact_size)
else:
chunk_size = artifact_size
request.response.headers['Content-Length'] = chunk_size
return request.response
else:
msg = _("Not Found Artifact File.")
raise webob.exc.HTTPNotFound(explanation=msg)
def _get_csar_path(self, vnf_package):
csar_path = os.path.join(CONF.vnf_package.vnf_package_csar_path,
vnf_package.id)
if not os.path.isdir(csar_path):
location = vnf_package.location_glance_store
try:
zip_path = glance_store.load_csar(vnf_package.id, location)
csar_utils.extract_csar_zip_file(zip_path, csar_path)
except (store_exceptions.GlanceStoreException) as e:
exc_msg = encodeutils.exception_to_unicode(e)
msg = (_("Exception raised from glance store can be "
"unrecoverable if it is not related to connection"
" error. Error: %s.") % exc_msg)
raise exceptions.FailedToGetVnfArtifact(error=msg)
return csar_path
def _download_vnf_artifact(self, artifact_file_path, offset=0,
chunk_size=None):
try:
with open(artifact_file_path, 'rb') as f:
f.seek(offset, 1)
vnf_artifact_data = f.read(chunk_size)
return vnf_artifact_data
except Exception as e:
exc_msg = encodeutils.exception_to_unicode(e)
msg = (_("Exception raised while reading artifact file"
" Error: %s.") % exc_msg)
raise exceptions.FailedToGetVnfArtifact(error=msg)
def _create_vnfd_zip(self, vnfd_files_and_data):
buff = BytesIO()
with ZipFile(buff, 'w', zipfile.ZIP_DEFLATED) as zip_archive:
for file_path, file_data in vnfd_files_and_data.items():
zip_archive.writestr(file_path, file_data)
return buff.getvalue()
def _make_problem_detail(self, title, detail, status):
res = webob.Response(content_type='application/problem+json')
problemDetails = {}
problemDetails['title'] = title
problemDetails['detail'] = detail
problemDetails['status'] = status
res.text = json.dumps(problemDetails)
res.status_int = status
return res
def create_resource():
body_deserializers = {
'application/zip': wsgi.ZipDeserializer()
}
deserializer = wsgi.RequestDeserializer(
body_deserializers=body_deserializers)
return wsgi.Resource(VnfPkgmController(), deserializer=deserializer)