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:
Shubham Potale 2019-09-25 20:08:15 +05:30 committed by Prashant Bhole
parent 848ab62a8d
commit f03b615bfe
19 changed files with 656 additions and 26 deletions

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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 = {

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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'
}
]),
] ]

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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()

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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