# 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() headers = {"location": '/vnfpkgm/v1/vnf_packages/%s' % vnf_package.id} result = self._view_builder.create(vnf_package) return wsgi.ResponseObject(result, headers=headers) @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(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(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)