Implement RestFul API to read VNFD of an on-boarded VNF package
The VNFD can be implemented as a single file or as a collection of multiple files. If the VNFD is implemented in the form of multiple files, a ZIP file embedding these files shall be returned. If the VNFD is implemented as a single file, either that file or a ZIP file embedding that file shall be returned. Implemented below API:- * GET /vnf_packages/{vnfPkgId}/vnfd Change-Id: I4af9c8126fb7478da294bc99a176eabba3944564 Implements: bp enhance-vnf-package-support-part1
This commit is contained in:
parent
848ab62a8d
commit
f03b615bfe
|
@ -1,4 +1,12 @@
|
||||||
# variables in header
|
# variables in header
|
||||||
|
content_type:
|
||||||
|
description: |
|
||||||
|
If the VNFD is implemented in the form of multiple files, zip file will
|
||||||
|
be returned with Content-Type set as `application/zip` otherwise it will
|
||||||
|
be set to `text/plain` in the response header.
|
||||||
|
in: header
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
# variables in path
|
# variables in path
|
||||||
alias_path:
|
alias_path:
|
||||||
|
|
|
@ -46,6 +46,11 @@
|
||||||
405:
|
405:
|
||||||
default: |
|
default: |
|
||||||
Method is not valid for this endpoint.
|
Method is not valid for this endpoint.
|
||||||
|
406:
|
||||||
|
default: |
|
||||||
|
Not Acceptable, the requested resource is only capable of generating
|
||||||
|
content not acceptable according to the 'Accept' headers sent in the
|
||||||
|
request.
|
||||||
409:
|
409:
|
||||||
default: |
|
default: |
|
||||||
This operation conflicted with another operation on this resource.
|
This operation conflicted with another operation on this resource.
|
||||||
|
|
|
@ -305,3 +305,59 @@ Response Example
|
||||||
|
|
||||||
.. literalinclude:: samples/vnf_packages/vnf-packages-patch-response.json
|
.. literalinclude:: samples/vnf_packages/vnf-packages-patch-response.json
|
||||||
:language: javascript
|
:language: javascript
|
||||||
|
|
||||||
|
Read VNFD of an individual VNF package
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. rest_method:: GET /vnfpkgm/v1/vnf_packages/{vnf_package_id}/vnfd
|
||||||
|
|
||||||
|
Read VNFD of an on-boarded VNF package.
|
||||||
|
|
||||||
|
The VNFD can be implemented as a single file or as a collection of multiple
|
||||||
|
files. If the VNFD is implemented in the form of multiple files, a ZIP file
|
||||||
|
embedding these files shall be returned. If the VNFD is implemented as a
|
||||||
|
single file, either that file or a ZIP file embedding that file shall be
|
||||||
|
returned. The selection of the format is controlled by the "Accept" HTTP
|
||||||
|
header passed in the GET request.
|
||||||
|
|
||||||
|
If the "Accept" header contains only "text/plain" and the VNFD is implemented
|
||||||
|
as a single file, the file shall be returned; otherwise, an error message shall
|
||||||
|
be returned.
|
||||||
|
If the "Accept" header contains only "application/zip", the single file or
|
||||||
|
the multiple files that make up the VNFD shall be returned embedded in a ZIP
|
||||||
|
file.
|
||||||
|
If the "Accept" header contains both "text/plain" and "application/zip", it
|
||||||
|
is up to the NFVO to choose the format to return for a single-file VNFD; for a
|
||||||
|
multi-file VNFD, a ZIP file shall be returned.
|
||||||
|
|
||||||
|
Response Codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success status.yaml
|
||||||
|
|
||||||
|
- 200
|
||||||
|
|
||||||
|
.. rest_status_code:: error status.yaml
|
||||||
|
|
||||||
|
- 400
|
||||||
|
- 401
|
||||||
|
- 403
|
||||||
|
- 404
|
||||||
|
- 406
|
||||||
|
- 409
|
||||||
|
- 500
|
||||||
|
|
||||||
|
Request Parameters
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- vnf_package_id: vnf_package_id_path
|
||||||
|
- Accept: content_type
|
||||||
|
|
||||||
|
Response
|
||||||
|
--------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- Content-Type: content_type
|
||||||
|
|
|
@ -13,13 +13,17 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
|
import six
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
import webob
|
import webob
|
||||||
|
import zipfile
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from tacker._i18n import _
|
from tacker._i18n import _
|
||||||
from tacker.api.schemas import vnf_packages
|
from tacker.api.schemas import vnf_packages
|
||||||
|
@ -275,6 +279,72 @@ class VnfPkgmController(wsgi.Controller):
|
||||||
|
|
||||||
return self._view_builder.patch(old_vnf_package, vnf_package)
|
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=six.text_type(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)
|
||||||
|
|
||||||
|
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 create_resource():
|
def create_resource():
|
||||||
body_deserializers = {
|
body_deserializers = {
|
||||||
|
|
|
@ -74,3 +74,9 @@ class VnfpkgmAPIRouter(wsgi.Router):
|
||||||
self._setup_route(mapper,
|
self._setup_route(mapper,
|
||||||
"/vnf_packages/{id}/package_content/upload_from_uri",
|
"/vnf_packages/{id}/package_content/upload_from_uri",
|
||||||
methods, controller, default_resource)
|
methods, controller, default_resource)
|
||||||
|
|
||||||
|
# Allowed methods on /vnf_packages/{id}/vnfd
|
||||||
|
methods = {"GET": "get_vnf_package_vnfd"}
|
||||||
|
self._setup_route(mapper,
|
||||||
|
"/vnf_packages/{id}/vnfd",
|
||||||
|
methods, controller, default_resource)
|
||||||
|
|
|
@ -269,7 +269,7 @@ def _get_data_from_csar(tosca, context, id):
|
||||||
return vnf_data, flavours
|
return vnf_data, flavours
|
||||||
|
|
||||||
|
|
||||||
def _extract_csar_zip_file(file_path, extract_path):
|
def extract_csar_zip_file(file_path, extract_path):
|
||||||
try:
|
try:
|
||||||
with zipfile.ZipFile(file_path, 'r') as zf:
|
with zipfile.ZipFile(file_path, 'r') as zf:
|
||||||
zf.extractall(extract_path)
|
zf.extractall(extract_path)
|
||||||
|
@ -287,7 +287,7 @@ def load_csar_data(context, package_uuid, zip_path):
|
||||||
|
|
||||||
extract_zip_path = os.path.join(CONF.vnf_package.vnf_package_csar_path,
|
extract_zip_path = os.path.join(CONF.vnf_package.vnf_package_csar_path,
|
||||||
package_uuid)
|
package_uuid)
|
||||||
_extract_csar_zip_file(zip_path, extract_zip_path)
|
extract_csar_zip_file(zip_path, extract_zip_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tosca = ToscaTemplate(zip_path, None, True)
|
tosca = ToscaTemplate(zip_path, None, True)
|
||||||
|
|
|
@ -240,6 +240,10 @@ class UploadFailedToGlanceStore(Invalid):
|
||||||
"%(error)s")
|
"%(error)s")
|
||||||
|
|
||||||
|
|
||||||
|
class FailedToGetVnfdData(Invalid):
|
||||||
|
message = _("Failed to get csar zip file from glance store: %(error)s")
|
||||||
|
|
||||||
|
|
||||||
class InvalidCSAR(Invalid):
|
class InvalidCSAR(Invalid):
|
||||||
message = _("Invalid csar: %(error)s")
|
message = _("Invalid csar: %(error)s")
|
||||||
|
|
||||||
|
|
|
@ -16,17 +16,21 @@
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from glance_store import exceptions as store_exceptions
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import oslo_messaging
|
import oslo_messaging
|
||||||
from oslo_service import periodic_task
|
from oslo_service import periodic_task
|
||||||
from oslo_service import service
|
from oslo_service import service
|
||||||
|
from oslo_utils import encodeutils
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from sqlalchemy.orm import exc as orm_exc
|
from sqlalchemy.orm import exc as orm_exc
|
||||||
|
import yaml
|
||||||
|
|
||||||
from tacker.common import csar_utils
|
from tacker.common import csar_utils
|
||||||
from tacker.common import exceptions
|
from tacker.common import exceptions
|
||||||
|
@ -255,6 +259,79 @@ class Conductor(manager.Manager):
|
||||||
|
|
||||||
vnf_package.destroy(context)
|
vnf_package.destroy(context)
|
||||||
|
|
||||||
|
def get_vnf_package_vnfd(self, context, 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.FailedToGetVnfdData(error=msg)
|
||||||
|
try:
|
||||||
|
return self._read_vnfd_files(csar_path)
|
||||||
|
except Exception as e:
|
||||||
|
exc_msg = encodeutils.exception_to_unicode(e)
|
||||||
|
msg = (_("Exception raised while reading csar file"
|
||||||
|
" Error: %s.") % exc_msg)
|
||||||
|
raise exceptions.FailedToGetVnfdData(error=msg)
|
||||||
|
|
||||||
|
def _read_vnfd_files(self, csar_path):
|
||||||
|
"""Creating a dictionary with file path as key and file data as value.
|
||||||
|
|
||||||
|
It will contain YAML files representing the VNFD, and information
|
||||||
|
necessary to navigate the ZIP file and to identify the file that is
|
||||||
|
the entry point for parsing the VNFD such as TOSCA-meta is included.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _add_recursively_imported_files(imported_yamls, file_path_and_data,
|
||||||
|
dir_of_parent_definition_file=''):
|
||||||
|
for file in imported_yamls:
|
||||||
|
file_path = os.path.join(
|
||||||
|
csar_path, dir_of_parent_definition_file, file)
|
||||||
|
file_data = yaml.safe_load(io.open(file_path))
|
||||||
|
dest_file_path = os.path.abspath(file_path).split(
|
||||||
|
csar_path + '/')[-1]
|
||||||
|
file_path_and_data[dest_file_path] = yaml.dump(file_data)
|
||||||
|
|
||||||
|
if file_data.get('imports'):
|
||||||
|
dir_of_parent_definition_file = '/'.join(
|
||||||
|
file_path.split('/')[:-1])
|
||||||
|
_add_recursively_imported_files(
|
||||||
|
file_data['imports'], file_path_and_data,
|
||||||
|
dir_of_parent_definition_file)
|
||||||
|
|
||||||
|
file_path_and_data = {}
|
||||||
|
if 'TOSCA-Metadata' in os.listdir(csar_path) and os.path.isdir(
|
||||||
|
os.path.join(csar_path, 'TOSCA-Metadata')):
|
||||||
|
# This is CSAR containing a TOSCA-Metadata directory, which
|
||||||
|
# includes the TOSCA.meta metadata file providing an entry
|
||||||
|
# information for processing a CSAR file.
|
||||||
|
tosca_meta_data = yaml.safe_load(io.open(os.path.join(
|
||||||
|
csar_path, 'TOSCA-Metadata', 'TOSCA.meta')))
|
||||||
|
file_path_and_data['TOSCA-Metadata/TOSCA.meta'] = yaml.dump(
|
||||||
|
tosca_meta_data)
|
||||||
|
entry_defination_file = tosca_meta_data['Entry-Definitions']
|
||||||
|
_add_recursively_imported_files([entry_defination_file],
|
||||||
|
file_path_and_data)
|
||||||
|
else:
|
||||||
|
# This is a CSAR without a TOSCA-Metadata directory and containing
|
||||||
|
# a single yaml file with a .yml or .yaml extension at the root of
|
||||||
|
# the archive.
|
||||||
|
root_yaml_file = sorted(
|
||||||
|
os.listdir(csar_path),
|
||||||
|
key=lambda item: item.endswith(('yaml', '.yml')))[-1]
|
||||||
|
src_path = os.path.join(csar_path, root_yaml_file)
|
||||||
|
file_data = yaml.safe_load(io.open(src_path))
|
||||||
|
file_path_and_data[root_yaml_file] = yaml.dump(file_data)
|
||||||
|
|
||||||
|
return file_path_and_data
|
||||||
|
|
||||||
@periodic_task.periodic_task(spacing=CONF.vnf_package_delete_interval)
|
@periodic_task.periodic_task(spacing=CONF.vnf_package_delete_interval)
|
||||||
def _run_cleanup_vnf_packages(self, context):
|
def _run_cleanup_vnf_packages(self, context):
|
||||||
"""Delete orphan extracted csar zip and files from extracted path
|
"""Delete orphan extracted csar zip and files from extracted path
|
||||||
|
|
|
@ -61,3 +61,12 @@ class VNFPackageRPCAPI(object):
|
||||||
rpc_method = cctxt.cast if cast else cctxt.call
|
rpc_method = cctxt.cast if cast else cctxt.call
|
||||||
return rpc_method(context, 'delete_vnf_package',
|
return rpc_method(context, 'delete_vnf_package',
|
||||||
vnf_package=vnf_package)
|
vnf_package=vnf_package)
|
||||||
|
|
||||||
|
def get_vnf_package_vnfd(self, context, vnf_package, cast=False):
|
||||||
|
serializer = objects_base.TackerObjectSerializer()
|
||||||
|
client = rpc.get_client(self.target, version_cap=None,
|
||||||
|
serializer=serializer)
|
||||||
|
cctxt = client.prepare()
|
||||||
|
rpc_method = cctxt.cast if cast else cctxt.call
|
||||||
|
return rpc_method(context, 'get_vnf_package_vnfd',
|
||||||
|
vnf_package=vnf_package)
|
||||||
|
|
|
@ -94,7 +94,16 @@ rules = [
|
||||||
'path': '/vnf_packages/{vnf_package_id}'
|
'path': '/vnf_packages/{vnf_package_id}'
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=VNFPKGM % 'get_vnf_package_vnfd',
|
||||||
|
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||||
|
description="reads the content of the VNFD within a VNF package.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'GET',
|
||||||
|
'path': '/vnf_packages/{vnf_package_id}/vnfd'
|
||||||
|
}
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,9 @@ class SessionClient(adapter.Adapter):
|
||||||
|
|
||||||
def _decode_json(self, response):
|
def _decode_json(self, response):
|
||||||
body = response.text
|
body = response.text
|
||||||
if body:
|
if body and response.headers['Content-Type'] == 'text/plain':
|
||||||
|
return body
|
||||||
|
elif body:
|
||||||
return jsonutils.loads(body)
|
return jsonutils.loads(body)
|
||||||
else:
|
else:
|
||||||
return ""
|
return ""
|
||||||
|
@ -68,6 +70,8 @@ class SessionClient(adapter.Adapter):
|
||||||
def do_request(self, url, method, **kwargs):
|
def do_request(self, url, method, **kwargs):
|
||||||
kwargs.setdefault('authenticated', True)
|
kwargs.setdefault('authenticated', True)
|
||||||
resp = self.request(url, method, **kwargs)
|
resp = self.request(url, method, **kwargs)
|
||||||
|
if resp.headers['Content-Type'] == 'application/zip':
|
||||||
|
return resp, resp.content
|
||||||
body = self._decode_json(resp)
|
body = self._decode_json(resp)
|
||||||
return resp, body
|
return resp, body
|
||||||
|
|
||||||
|
|
|
@ -13,14 +13,22 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import ddt
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
|
|
||||||
|
import tacker.conf
|
||||||
from tacker.tests.functional import base
|
from tacker.tests.functional import base
|
||||||
|
|
||||||
|
|
||||||
|
CONF = tacker.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
class VnfPackageTest(base.BaseTackerTest):
|
class VnfPackageTest(base.BaseTackerTest):
|
||||||
|
|
||||||
VNF_PACKAGE_DELETE_TIMEOUT = 120
|
VNF_PACKAGE_DELETE_TIMEOUT = 120
|
||||||
|
@ -169,3 +177,68 @@ class VnfPackageTest(base.BaseTackerTest):
|
||||||
self.assertEqual(expected_result, resp_body)
|
self.assertEqual(expected_result, resp_body)
|
||||||
self._delete_vnf_package(vnf_package['id'])
|
self._delete_vnf_package(vnf_package['id'])
|
||||||
self._wait_for_delete(vnf_package['id'])
|
self._wait_for_delete(vnf_package['id'])
|
||||||
|
|
||||||
|
def _create_and_onboard_vnf_package(self, file_name=None):
|
||||||
|
body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}})
|
||||||
|
vnf_package = self._create_vnf_package(body)
|
||||||
|
if file_name is None:
|
||||||
|
file_name = "sample_vnf_package_csar.zip"
|
||||||
|
file_path = self._get_csar_file_path(file_name)
|
||||||
|
with open(file_path, 'rb') as file_object:
|
||||||
|
resp, resp_body = self.http_client.do_request(
|
||||||
|
'{base_path}/{id}/package_content'.format(
|
||||||
|
id=vnf_package['id'],
|
||||||
|
base_path=self.base_url),
|
||||||
|
"PUT", body=file_object, content_type='application/zip')
|
||||||
|
self.assertEqual(202, resp.status_code)
|
||||||
|
self._wait_for_onboard(vnf_package['id'])
|
||||||
|
|
||||||
|
return vnf_package['id']
|
||||||
|
|
||||||
|
def test_get_vnfd_from_onboarded_vnf_package_for_content_type_zip(self):
|
||||||
|
vnf_package_id = self._create_and_onboard_vnf_package()
|
||||||
|
self.addCleanup(self._delete_vnf_package, vnf_package_id)
|
||||||
|
resp, resp_body = self.http_client.do_request(
|
||||||
|
'{base_path}/{id}/vnfd'.format(id=vnf_package_id,
|
||||||
|
base_path=self.base_url),
|
||||||
|
"GET", content_type='application/zip')
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
self.assertEqual('application/zip', resp.headers['Content-Type'])
|
||||||
|
self.assert_resp_contents(resp)
|
||||||
|
|
||||||
|
def assert_resp_contents(self, resp):
|
||||||
|
expected_file_list = ['Definitions/helloworld3_top.vnfd.yaml',
|
||||||
|
'Definitions/helloworld3_df_simple.yaml',
|
||||||
|
'Definitions/etsi_nfv_sol001_vnfd_types.yaml',
|
||||||
|
'Definitions/etsi_nfv_sol001_common_types.yaml',
|
||||||
|
'Definitions/helloworld3_types.yaml',
|
||||||
|
'TOSCA-Metadata/TOSCA.meta']
|
||||||
|
|
||||||
|
tmp = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
try:
|
||||||
|
tmp.write(resp.content)
|
||||||
|
finally:
|
||||||
|
# checking response.content is valid zip file
|
||||||
|
self.assertTrue(zipfile.is_zipfile(tmp))
|
||||||
|
with zipfile.ZipFile(tmp, 'r') as zipObj:
|
||||||
|
# Get list of files names in zip
|
||||||
|
actual_file_list = zipObj.namelist()
|
||||||
|
self.assertEqual(expected_file_list, actual_file_list)
|
||||||
|
|
||||||
|
tmp.close()
|
||||||
|
|
||||||
|
@ddt.data('text/plain', 'application/zip,text/plain')
|
||||||
|
def test_get_vnfd_from_onboarded_vnf_package_for_content_type_text(
|
||||||
|
self, accept_header):
|
||||||
|
# Uploading vnf package with single yaml file csar.
|
||||||
|
single_yaml_csar = "sample_vnfpkg_no_meta_single_vnfd.zip"
|
||||||
|
vnf_package_id = self._create_and_onboard_vnf_package(
|
||||||
|
single_yaml_csar)
|
||||||
|
self.addCleanup(self._delete_vnf_package, vnf_package_id)
|
||||||
|
resp, resp_body = self.http_client.do_request(
|
||||||
|
'{base_path}/{id}/vnfd'.format(id=vnf_package_id,
|
||||||
|
base_path=self.base_url),
|
||||||
|
"GET", content_type=accept_header)
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
self.assertEqual('text/plain', resp.headers['Content-Type'])
|
||||||
|
self.assertIsNotNone(resp.text)
|
||||||
|
|
|
@ -34,7 +34,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
self.base_path, "../../etc/samples", file_name)
|
self.base_path, "../../etc/samples", file_name)
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data(self, mock_extract_csar_zip_file):
|
def test_load_csar_data(self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path("sample_vnf_package_csar.zip")
|
file_path = self._get_csar_file_path("sample_vnf_package_csar.zip")
|
||||||
vnf_data, flavours = csar_utils.load_csar_data(
|
vnf_data, flavours = csar_utils.load_csar_data(
|
||||||
|
@ -44,7 +44,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
self.assertEqual(flavours[0]['flavour_id'], 'simple')
|
self.assertEqual(flavours[0]['flavour_id'], 'simple')
|
||||||
self.assertIsNotNone(flavours[0]['sw_images'])
|
self.assertIsNotNone(flavours[0]['sw_images'])
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_with_single_yaml(
|
def test_load_csar_data_with_single_yaml(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -56,7 +56,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
self.assertEqual(flavours[0]['flavour_id'], 'simple')
|
self.assertEqual(flavours[0]['flavour_id'], 'simple')
|
||||||
self.assertIsNotNone(flavours[0]['sw_images'])
|
self.assertIsNotNone(flavours[0]['sw_images'])
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_without_instantiation_level(
|
def test_load_csar_data_without_instantiation_level(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -68,7 +68,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
' "tosca.policies.nfv.InstantiationLevels is not defined.')
|
' "tosca.policies.nfv.InstantiationLevels is not defined.')
|
||||||
self.assertEqual(msg, exc.format_message())
|
self.assertEqual(msg, exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_with_invalid_instantiation_level(
|
def test_load_csar_data_with_invalid_instantiation_level(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -81,7 +81,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
"defined levels %s") % ",".join(sorted(levels))
|
"defined levels %s") % ",".join(sorted(levels))
|
||||||
self.assertEqual(msg, exc.format_message())
|
self.assertEqual(msg, exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_with_invalid_default_instantiation_level(
|
def test_load_csar_data_with_invalid_default_instantiation_level(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -94,7 +94,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
"defined levels %s") % ",".join(sorted(levels))
|
"defined levels %s") % ",".join(sorted(levels))
|
||||||
self.assertEqual(msg, exc.format_message())
|
self.assertEqual(msg, exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_without_vnfd_info(
|
def test_load_csar_data_without_vnfd_info(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -104,7 +104,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
self.context, constants.UUID, file_path)
|
self.context, constants.UUID, file_path)
|
||||||
self.assertEqual("VNF properties are mandatory", exc.format_message())
|
self.assertEqual("VNF properties are mandatory", exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_with_artifacts_and_without_sw_image_data(
|
def test_load_csar_data_with_artifacts_and_without_sw_image_data(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -116,7 +116,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
' type tosca.artifacts.nfv.SwImage for node VDU1.')
|
' type tosca.artifacts.nfv.SwImage for node VDU1.')
|
||||||
self.assertEqual(msg, exc.format_message())
|
self.assertEqual(msg, exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_with_multiple_sw_image_data(
|
def test_load_csar_data_with_multiple_sw_image_data(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -128,7 +128,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
' is added more than one time for node VDU1.')
|
' is added more than one time for node VDU1.')
|
||||||
self.assertEqual(msg, exc.format_message())
|
self.assertEqual(msg, exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_csar_with_missing_sw_image_data_in_main_template(
|
def test_csar_with_missing_sw_image_data_in_main_template(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -140,7 +140,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
' type tosca.artifacts.nfv.SwImage for node VDU1.')
|
' type tosca.artifacts.nfv.SwImage for node VDU1.')
|
||||||
self.assertEqual(msg, exc.format_message())
|
self.assertEqual(msg, exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_without_flavour_info(
|
def test_load_csar_data_without_flavour_info(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path("csar_without_flavour_info.zip")
|
file_path = self._get_csar_file_path("csar_without_flavour_info.zip")
|
||||||
|
@ -149,7 +149,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
self.context, constants.UUID, file_path)
|
self.context, constants.UUID, file_path)
|
||||||
self.assertEqual("No VNF flavours are available", exc.format_message())
|
self.assertEqual("No VNF flavours are available", exc.format_message())
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_without_flavour_info_in_main_template(
|
def test_load_csar_data_without_flavour_info_in_main_template(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path(
|
file_path = self._get_csar_file_path(
|
||||||
|
@ -167,7 +167,7 @@ class TestCSARUtils(testtools.TestCase):
|
||||||
mock_rmtree.assert_called()
|
mock_rmtree.assert_called()
|
||||||
mock_remove.assert_called()
|
mock_remove.assert_called()
|
||||||
|
|
||||||
@mock.patch('tacker.common.csar_utils._extract_csar_zip_file')
|
@mock.patch('tacker.common.csar_utils.extract_csar_zip_file')
|
||||||
def test_load_csar_data_without_policies(
|
def test_load_csar_data_without_policies(
|
||||||
self, mock_extract_csar_zip_file):
|
self, mock_extract_csar_zip_file):
|
||||||
file_path = self._get_csar_file_path("csar_without_policies.zip")
|
file_path = self._get_csar_file_path("csar_without_policies.zip")
|
||||||
|
|
|
@ -79,3 +79,18 @@ class VnfPackageRPCTestCase(base.BaseTestCase):
|
||||||
self.context, 'delete_vnf_package',
|
self.context, 'delete_vnf_package',
|
||||||
vnf_package=vnf_package_obj)
|
vnf_package=vnf_package_obj)
|
||||||
_test()
|
_test()
|
||||||
|
|
||||||
|
def test_get_vnf_package_vnfd(self):
|
||||||
|
|
||||||
|
@mock.patch.object(BackingOffClient, 'prepare')
|
||||||
|
def _test(prepare_mock):
|
||||||
|
prepare_mock.return_value = self.cctxt_mock
|
||||||
|
vnf_package_obj = vnf_package.VnfPackage(self.context,
|
||||||
|
**fakes.VNF_DATA)
|
||||||
|
self.rpc_api.get_vnf_package_vnfd(self.context,
|
||||||
|
vnf_package_obj, cast=False)
|
||||||
|
prepare_mock.assert_called()
|
||||||
|
self.cctxt_mock.call.assert_called_once_with(
|
||||||
|
self.context, 'get_vnf_package_vnfd',
|
||||||
|
vnf_package=vnf_package_obj)
|
||||||
|
_test()
|
||||||
|
|
|
@ -13,6 +13,14 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
from oslo_config import cfg
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import yaml
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from tacker.tests import uuidsentinel
|
from tacker.tests import uuidsentinel
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,3 +57,59 @@ VNF_PACKAGE_DATA = {'algorithm': None, 'hash': None,
|
||||||
'usage_state': 'NOT_IN_USE',
|
'usage_state': 'NOT_IN_USE',
|
||||||
'user_data': {'abc': 'xyz'}
|
'user_data': {'abc': 'xyz'}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_vnfd_files_list(csar_path):
|
||||||
|
files_list = []
|
||||||
|
# Checking for directory exist
|
||||||
|
if not os.path.isdir(csar_path):
|
||||||
|
return
|
||||||
|
ext = ['.yaml', '.meta']
|
||||||
|
for _, _, files in os.walk(csar_path):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith(tuple(ext)):
|
||||||
|
files_list.append(file)
|
||||||
|
|
||||||
|
return files_list
|
||||||
|
|
||||||
|
|
||||||
|
def create_fake_csar_dir(vnf_package_id, single_yaml_csar=False):
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
csar_file = ('sample_vnfpkg_no_meta_single_vnfd.zip' if single_yaml_csar
|
||||||
|
else 'sample_vnf_package_csar.zip')
|
||||||
|
sample_vnf_package_zip = os.path.join(base_path, "../../etc/samples",
|
||||||
|
csar_file)
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
fake_csar = os.path.join('/tmp/', vnf_package_id)
|
||||||
|
os.rename(tmpdir, fake_csar)
|
||||||
|
|
||||||
|
with zipfile.ZipFile(sample_vnf_package_zip, 'r') as zf:
|
||||||
|
zf.extractall(fake_csar)
|
||||||
|
cfg.CONF.set_override('vnf_package_csar_path', '/tmp',
|
||||||
|
group='vnf_package')
|
||||||
|
return fake_csar
|
||||||
|
|
||||||
|
|
||||||
|
def get_expected_vnfd_data():
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sample_vnf_package_zip = os.path.join(
|
||||||
|
base_path, "../../etc/samples/sample_vnf_package_csar.zip")
|
||||||
|
|
||||||
|
csar_temp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(sample_vnf_package_zip, 'r') as zf:
|
||||||
|
zf.extractall(csar_temp_dir)
|
||||||
|
|
||||||
|
file_names = ['TOSCA-Metadata/TOSCA.meta',
|
||||||
|
'Definitions/etsi_nfv_sol001_vnfd_types.yaml',
|
||||||
|
'Definitions/helloworld3_types.yaml',
|
||||||
|
'Definitions/helloworld3_df_simple.yaml',
|
||||||
|
'Definitions/helloworld3_top.vnfd.yaml',
|
||||||
|
'Definitions/etsi_nfv_sol001_common_types.yaml']
|
||||||
|
file_path_and_data = {}
|
||||||
|
for file_name in file_names:
|
||||||
|
file_path_and_data.update({file_name: yaml.dump(yaml.safe_load(
|
||||||
|
io.open(os.path.join(csar_temp_dir, file_name))))})
|
||||||
|
|
||||||
|
shutil.rmtree(csar_temp_dir)
|
||||||
|
return file_path_and_data
|
||||||
|
|
|
@ -13,13 +13,19 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import mock
|
|
||||||
import os
|
import os
|
||||||
|
from oslo_config import cfg
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from glance_store import exceptions as store_exceptions
|
||||||
|
import mock
|
||||||
|
import yaml
|
||||||
|
|
||||||
from tacker.common import csar_utils
|
from tacker.common import csar_utils
|
||||||
|
from tacker.common import exceptions
|
||||||
from tacker.conductor import conductor_server
|
from tacker.conductor import conductor_server
|
||||||
|
import tacker.conf
|
||||||
from tacker import context
|
from tacker import context
|
||||||
from tacker.glance_store import store as glance_store
|
from tacker.glance_store import store as glance_store
|
||||||
from tacker import objects
|
from tacker import objects
|
||||||
|
@ -28,6 +34,8 @@ from tacker.tests.unit.conductor import fakes
|
||||||
from tacker.tests.unit.db.base import SqlTestCase
|
from tacker.tests.unit.db.base import SqlTestCase
|
||||||
from tacker.tests import uuidsentinel
|
from tacker.tests import uuidsentinel
|
||||||
|
|
||||||
|
CONF = tacker.conf.CONF
|
||||||
|
|
||||||
|
|
||||||
class TestConductor(SqlTestCase):
|
class TestConductor(SqlTestCase):
|
||||||
|
|
||||||
|
@ -92,6 +100,59 @@ class TestConductor(SqlTestCase):
|
||||||
self.conductor.delete_vnf_package(self.context, self.vnf_package)
|
self.conductor.delete_vnf_package(self.context, self.vnf_package)
|
||||||
mock_delete_csar.assert_called()
|
mock_delete_csar.assert_called()
|
||||||
|
|
||||||
|
def test_get_vnf_package_vnfd_with_tosca_meta_file_in_csar(self):
|
||||||
|
fake_csar = fakes.create_fake_csar_dir(self.vnf_package.id)
|
||||||
|
expected_data = fakes.get_expected_vnfd_data()
|
||||||
|
result = self.conductor.get_vnf_package_vnfd(self.context,
|
||||||
|
self.vnf_package)
|
||||||
|
self.assertEqual(expected_data, result)
|
||||||
|
shutil.rmtree(fake_csar)
|
||||||
|
|
||||||
|
def test_get_vnf_package_vnfd_with_single_yaml_csar(self):
|
||||||
|
fake_csar = fakes.create_fake_csar_dir(self.vnf_package.id,
|
||||||
|
single_yaml_csar=True)
|
||||||
|
result = self.conductor.get_vnf_package_vnfd(self.context,
|
||||||
|
self.vnf_package)
|
||||||
|
# only one key present in the result shows that it contains only one
|
||||||
|
# yaml file
|
||||||
|
self.assertEqual(1, len(result.keys()))
|
||||||
|
shutil.rmtree(fake_csar)
|
||||||
|
|
||||||
|
@mock.patch.object(glance_store, 'load_csar')
|
||||||
|
def test_get_vnf_package_vnfd_download_from_glance_store(self,
|
||||||
|
mock_load_csar):
|
||||||
|
fake_csar = os.path.join('/tmp/', self.vnf_package.id)
|
||||||
|
cfg.CONF.set_override('vnf_package_csar_path', '/tmp',
|
||||||
|
group='vnf_package')
|
||||||
|
# Scenario in which csar path is not present in the local storage.
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sample_vnf_package = os.path.join(
|
||||||
|
base_path, "../../etc/samples/sample_vnf_package_csar.zip")
|
||||||
|
mock_load_csar.return_value = sample_vnf_package
|
||||||
|
expected_data = fakes.get_expected_vnfd_data()
|
||||||
|
result = self.conductor.get_vnf_package_vnfd(self.context,
|
||||||
|
self.vnf_package)
|
||||||
|
self.assertEqual(expected_data, result)
|
||||||
|
shutil.rmtree(fake_csar)
|
||||||
|
|
||||||
|
@mock.patch.object(glance_store, 'load_csar')
|
||||||
|
def test_get_vnf_package_vnfd_exception_from_glance_store(self,
|
||||||
|
mock_load_csar):
|
||||||
|
mock_load_csar.side_effect = store_exceptions.NotFound
|
||||||
|
self.assertRaises(exceptions.FailedToGetVnfdData,
|
||||||
|
self.conductor.get_vnf_package_vnfd, self.context,
|
||||||
|
self.vnf_package)
|
||||||
|
|
||||||
|
@mock.patch.object(conductor_server.Conductor, '_read_vnfd_files')
|
||||||
|
def test_get_vnf_package_vnfd_exception_from_read_vnfd_files(
|
||||||
|
self, mock_read_vnfd_files):
|
||||||
|
fake_csar = fakes.create_fake_csar_dir(self.vnf_package.id)
|
||||||
|
mock_read_vnfd_files.side_effect = yaml.YAMLError
|
||||||
|
self.assertRaises(exceptions.FailedToGetVnfdData,
|
||||||
|
self.conductor.get_vnf_package_vnfd, self.context,
|
||||||
|
self.vnf_package)
|
||||||
|
shutil.rmtree(fake_csar)
|
||||||
|
|
||||||
@mock.patch.object(os, 'remove')
|
@mock.patch.object(os, 'remove')
|
||||||
@mock.patch.object(shutil, 'rmtree')
|
@mock.patch.object(shutil, 'rmtree')
|
||||||
@mock.patch.object(os.path, 'exists')
|
@mock.patch.object(os.path, 'exists')
|
||||||
|
|
|
@ -15,8 +15,14 @@
|
||||||
|
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import io
|
||||||
import iso8601
|
import iso8601
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
import webob
|
import webob
|
||||||
|
import yaml
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from tacker.api.vnfpkgm.v1.router import VnfpkgmAPIRouter
|
from tacker.api.vnfpkgm.v1.router import VnfpkgmAPIRouter
|
||||||
from tacker import context
|
from tacker import context
|
||||||
|
@ -117,7 +123,7 @@ def return_vnf_package_user_data(**updates):
|
||||||
return model_obj
|
return model_obj
|
||||||
|
|
||||||
|
|
||||||
def return_vnf_package(**updates):
|
def return_vnf_package(onboarded=False, **updates):
|
||||||
model_obj = models.VnfPackage()
|
model_obj = models.VnfPackage()
|
||||||
if 'user_data' in updates:
|
if 'user_data' in updates:
|
||||||
metadata = []
|
metadata = []
|
||||||
|
@ -126,14 +132,27 @@ def return_vnf_package(**updates):
|
||||||
**{'key': key, 'value': value})
|
**{'key': key, 'value': value})
|
||||||
metadata.extend([vnf_package_user_data])
|
metadata.extend([vnf_package_user_data])
|
||||||
model_obj._metadata = metadata
|
model_obj._metadata = metadata
|
||||||
model_obj.update(fake_vnf_package(**updates))
|
|
||||||
|
if onboarded:
|
||||||
|
updates = {'onboarding_state': 'ONBOARDED',
|
||||||
|
'operational_state': 'ENABLED',
|
||||||
|
'algorithm': 'test',
|
||||||
|
'hash': 'test',
|
||||||
|
'location_glance_store': 'file:test/path/pkg-uuid',
|
||||||
|
'updated_at': datetime.datetime(
|
||||||
|
1900, 1, 1, 1, 1, 1, tzinfo=iso8601.UTC)}
|
||||||
|
model_obj.update(fake_vnf_package(**updates))
|
||||||
|
else:
|
||||||
|
model_obj.update(fake_vnf_package(**updates))
|
||||||
|
|
||||||
return model_obj
|
return model_obj
|
||||||
|
|
||||||
|
|
||||||
def return_vnfpkg_obj(**updates):
|
def return_vnfpkg_obj(onboarded=False, **updates):
|
||||||
vnf_package = vnf_package_obj.VnfPackage._from_db_object(
|
vnf_package = vnf_package_obj.VnfPackage._from_db_object(
|
||||||
context, vnf_package_obj.VnfPackage(),
|
context, vnf_package_obj.VnfPackage(),
|
||||||
return_vnf_package(**updates), expected_attrs=None)
|
return_vnf_package(onboarded=onboarded, **updates),
|
||||||
|
expected_attrs=None)
|
||||||
return vnf_package
|
return vnf_package
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,3 +170,29 @@ def wsgi_app_v1(fake_auth_context=None):
|
||||||
uuidsentinel.project_id, is_admin=True)
|
uuidsentinel.project_id, is_admin=True)
|
||||||
api_v1 = InjectContext(ctxt, inner_app_v1)
|
api_v1 = InjectContext(ctxt, inner_app_v1)
|
||||||
return api_v1
|
return api_v1
|
||||||
|
|
||||||
|
|
||||||
|
def return_vnfd_data(multiple_yaml_files=True):
|
||||||
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sample_vnf_package_zip = os.path.join(
|
||||||
|
base_path, "../../etc/samples/sample_vnf_package_csar.zip")
|
||||||
|
|
||||||
|
csar_temp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(sample_vnf_package_zip, 'r') as zf:
|
||||||
|
zf.extractall(csar_temp_dir)
|
||||||
|
|
||||||
|
file_names = ['Definitions/etsi_nfv_sol001_vnfd_types.yaml']
|
||||||
|
if multiple_yaml_files:
|
||||||
|
file_names.extend(['TOSCA-Metadata/TOSCA.meta',
|
||||||
|
'Definitions/helloworld3_types.yaml',
|
||||||
|
'Definitions/helloworld3_df_simple.yaml',
|
||||||
|
'Definitions/helloworld3_top.vnfd.yaml',
|
||||||
|
'Definitions/etsi_nfv_sol001_common_types.yaml'])
|
||||||
|
file_path_and_data = {}
|
||||||
|
for file_name in file_names:
|
||||||
|
file_path_and_data.update({file_name: yaml.dump(yaml.safe_load(
|
||||||
|
io.open(os.path.join(csar_temp_dir, file_name))))})
|
||||||
|
|
||||||
|
shutil.rmtree(csar_temp_dir)
|
||||||
|
return file_path_and_data
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import ddt
|
||||||
import mock
|
import mock
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
|
@ -20,6 +21,7 @@ from six.moves import urllib
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
from tacker.api.vnfpkgm.v1 import controller
|
from tacker.api.vnfpkgm.v1 import controller
|
||||||
|
from tacker.common import exceptions
|
||||||
from tacker.conductor.conductorrpc.vnf_pkgm_rpc import VNFPackageRPCAPI
|
from tacker.conductor.conductorrpc.vnf_pkgm_rpc import VNFPackageRPCAPI
|
||||||
from tacker.glance_store import store as glance_store
|
from tacker.glance_store import store as glance_store
|
||||||
from tacker import objects
|
from tacker import objects
|
||||||
|
@ -32,6 +34,7 @@ from tacker.tests.unit import fake_request
|
||||||
from tacker.tests.unit.vnfpkgm import fakes
|
from tacker.tests.unit.vnfpkgm import fakes
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
class TestController(base.TestCase):
|
class TestController(base.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -413,3 +416,121 @@ class TestController(base.TestCase):
|
||||||
self.assertRaises(exc.HTTPBadRequest,
|
self.assertRaises(exc.HTTPBadRequest,
|
||||||
self.controller.patch,
|
self.controller.patch,
|
||||||
req, constants.UUID, body=body)
|
req, constants.UUID, body=body)
|
||||||
|
|
||||||
|
@mock.patch.object(VNFPackageRPCAPI, "get_vnf_package_vnfd")
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
@ddt.data('application/zip', 'text/plain,application/zip',
|
||||||
|
'application/zip,text/plain')
|
||||||
|
def test_get_vnf_package_vnfd_with_valid_accept_headers(
|
||||||
|
self, accept_headers, mock_vnf_by_id, mock_get_vnf_package_vnfd):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True)
|
||||||
|
mock_get_vnf_package_vnfd.return_value = fakes.return_vnfd_data()
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Accept'] = accept_headers
|
||||||
|
req.method = 'GET'
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
self.assertEqual(http_client.OK, resp.status_code)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_get_vnf_package_vnfd_with_invalid_accept_header(
|
||||||
|
self, mock_vnf_by_id):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True)
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Accept'] = 'test-invalid-header'
|
||||||
|
req.method = 'GET'
|
||||||
|
self.assertRaises(exc.HTTPNotAcceptable,
|
||||||
|
self.controller.get_vnf_package_vnfd,
|
||||||
|
req, constants.UUID)
|
||||||
|
|
||||||
|
@mock.patch.object(VNFPackageRPCAPI, "get_vnf_package_vnfd")
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_get_vnf_package_vnfd_failed_with_bad_request(
|
||||||
|
self, mock_vnf_by_id, mock_get_vnf_package_vnfd):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True)
|
||||||
|
mock_get_vnf_package_vnfd.return_value = fakes.return_vnfd_data()
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Accept'] = 'text/plain'
|
||||||
|
req.method = 'GET'
|
||||||
|
self.assertRaises(exc.HTTPBadRequest,
|
||||||
|
self.controller.get_vnf_package_vnfd,
|
||||||
|
req, constants.UUID)
|
||||||
|
|
||||||
|
@mock.patch.object(VNFPackageRPCAPI, "get_vnf_package_vnfd")
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_get_vnf_package_vnfd_for_content_type_text_plain(self,
|
||||||
|
mock_vnf_by_id,
|
||||||
|
mock_get_vnf_package_vnfd):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True)
|
||||||
|
fake_vnfd_data = fakes.return_vnfd_data(multiple_yaml_files=False)
|
||||||
|
mock_get_vnf_package_vnfd.return_value = fake_vnfd_data
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Accept'] = 'text/plain'
|
||||||
|
req.method = 'GET'
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
self.assertEqual(http_client.OK, resp.status_code)
|
||||||
|
self.assertEqual('text/plain', resp.content_type)
|
||||||
|
self.assertEqual(fake_vnfd_data[list(fake_vnfd_data.keys())[0]],
|
||||||
|
resp.text)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_get_vnf_package_vnfd_failed_with_invalid_status(
|
||||||
|
self, mock_vnf_by_id):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=False)
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Accept'] = 'application/zip'
|
||||||
|
req.method = 'GET'
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
self.assertEqual(http_client.CONFLICT, resp.status_code)
|
||||||
|
|
||||||
|
def test_get_vnf_package_vnfd_with_invalid_uuid(self):
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.INVALID_UUID)
|
||||||
|
req.headers['Accept'] = 'application/zip'
|
||||||
|
req.method = 'GET'
|
||||||
|
exception = self.assertRaises(exc.HTTPNotFound,
|
||||||
|
self.controller.get_vnf_package_vnfd,
|
||||||
|
req, constants.INVALID_UUID)
|
||||||
|
self.assertEqual(
|
||||||
|
"Can not find requested vnf package: %s" % constants.INVALID_UUID,
|
||||||
|
exception.explanation)
|
||||||
|
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_get_vnf_package_vnfd_with_non_existing_vnf_packagee(
|
||||||
|
self, mock_vnf_by_id):
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Accept'] = 'application/zip'
|
||||||
|
req.method = 'GET'
|
||||||
|
mock_vnf_by_id.side_effect = exceptions.VnfPackageNotFound
|
||||||
|
self.assertRaises(exc.HTTPNotFound,
|
||||||
|
self.controller.get_vnf_package_vnfd, req,
|
||||||
|
constants.UUID)
|
||||||
|
|
||||||
|
@mock.patch.object(VNFPackageRPCAPI, "get_vnf_package_vnfd")
|
||||||
|
@mock.patch.object(vnf_package.VnfPackage, "get_by_id")
|
||||||
|
def test_get_vnf_package_vnfd_failed_with_internal_server_error(
|
||||||
|
self, mock_vnf_by_id, mock_get_vnf_package_vnfd):
|
||||||
|
mock_vnf_by_id.return_value = fakes.return_vnfpkg_obj(onboarded=True)
|
||||||
|
mock_get_vnf_package_vnfd.side_effect = exceptions.FailedToGetVnfdData
|
||||||
|
req = fake_request.HTTPRequest.blank(
|
||||||
|
'/vnf_packages/%s/vnfd'
|
||||||
|
% constants.UUID)
|
||||||
|
req.headers['Accept'] = 'application/zip'
|
||||||
|
req.method = 'GET'
|
||||||
|
resp = req.get_response(self.app)
|
||||||
|
self.assertRaises(exc.HTTPInternalServerError,
|
||||||
|
self.controller.get_vnf_package_vnfd,
|
||||||
|
req, constants.UUID)
|
||||||
|
self.assertEqual(http_client.INTERNAL_SERVER_ERROR, resp.status_code)
|
||||||
|
|
|
@ -396,7 +396,7 @@ class Request(webob.Request):
|
||||||
type_from_header = self.get_content_type()
|
type_from_header = self.get_content_type()
|
||||||
if type_from_header:
|
if type_from_header:
|
||||||
return type_from_header
|
return type_from_header
|
||||||
ctypes = ['application/json']
|
ctypes = ['application/json', 'text/plain', 'application/zip']
|
||||||
|
|
||||||
# Finally search in Accept-* headers
|
# Finally search in Accept-* headers
|
||||||
bm = self.accept.best_match(ctypes)
|
bm = self.accept.best_match(ctypes)
|
||||||
|
@ -935,9 +935,11 @@ class ResponseObject(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer = self.serializer
|
serializer = self.serializer
|
||||||
|
if self.obj is None:
|
||||||
body = None
|
body = None
|
||||||
if self.obj is not None:
|
elif content_type == 'text/plain':
|
||||||
|
body = self.obj
|
||||||
|
else:
|
||||||
body = serializer.serialize(self.obj)
|
body = serializer.serialize(self.obj)
|
||||||
response = webob.Response(body=body)
|
response = webob.Response(body=body)
|
||||||
response.status_int = self.code
|
response.status_int = self.code
|
||||||
|
@ -1032,7 +1034,8 @@ class Resource(Application):
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
resp_obj = None
|
resp_obj = None
|
||||||
if type(action_result) is dict or action_result is None:
|
if isinstance(action_result, (dict, str)) \
|
||||||
|
or action_result is None:
|
||||||
resp_obj = ResponseObject(action_result)
|
resp_obj = ResponseObject(action_result)
|
||||||
elif isinstance(action_result, ResponseObject):
|
elif isinstance(action_result, ResponseObject):
|
||||||
resp_obj = action_result
|
resp_obj = action_result
|
||||||
|
|
Loading…
Reference in New Issue