Add command to read vnfd of vnf package API
Added command support for fetching VNFD of the vnf package. Please see results here:- http://paste.openstack.org/show/789212/ Change-Id: Ifa15ddf7ff306ed2705cf7a170d9e9f512491fba Implements: bp enhance-vnf-package-support-part1
This commit is contained in:
@@ -83,6 +83,7 @@ openstack.tackerclient.v1 =
|
|||||||
vnf_package_upload = tackerclient.osc.v1.vnfpkgm.vnf_package:UploadVnfPackage
|
vnf_package_upload = tackerclient.osc.v1.vnfpkgm.vnf_package:UploadVnfPackage
|
||||||
vnf_package_delete = tackerclient.osc.v1.vnfpkgm.vnf_package:DeleteVnfPackage
|
vnf_package_delete = tackerclient.osc.v1.vnfpkgm.vnf_package:DeleteVnfPackage
|
||||||
vnf_package_update = tackerclient.osc.v1.vnfpkgm.vnf_package:UpdateVnfPackage
|
vnf_package_update = tackerclient.osc.v1.vnfpkgm.vnf_package:UpdateVnfPackage
|
||||||
|
vnf_package_download = tackerclient.osc.v1.vnfpkgm.vnf_package:DownloadVnfPackage
|
||||||
vnflcm_create = tackerclient.osc.v1.vnflcm.vnflcm:CreateVnfLcm
|
vnflcm_create = tackerclient.osc.v1.vnflcm.vnflcm:CreateVnfLcm
|
||||||
vnflcm_show = tackerclient.osc.v1.vnflcm.vnflcm:ShowVnfLcm
|
vnflcm_show = tackerclient.osc.v1.vnflcm.vnflcm:ShowVnfLcm
|
||||||
vnflcm_list = tackerclient.osc.v1.vnflcm.vnflcm:ListVnfLcm
|
vnflcm_list = tackerclient.osc.v1.vnflcm.vnflcm:ListVnfLcm
|
||||||
|
@@ -144,6 +144,8 @@ class HTTPClient(object):
|
|||||||
verify=self.verify_cert,
|
verify=self.verify_cert,
|
||||||
timeout=self.timeout,
|
timeout=self.timeout,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
if resp.headers.get('content-type') == 'application/zip':
|
||||||
|
return resp, resp.content
|
||||||
|
|
||||||
return resp, resp.text
|
return resp, resp.text
|
||||||
|
|
||||||
@@ -163,6 +165,7 @@ class HTTPClient(object):
|
|||||||
# re-authenticate and try again. If it still fails, bail.
|
# re-authenticate and try again. If it still fails, bail.
|
||||||
try:
|
try:
|
||||||
kwargs.setdefault('headers', {})
|
kwargs.setdefault('headers', {})
|
||||||
|
kwargs.setdefault('content_type', kwargs.get('content_type'))
|
||||||
if self.auth_token is None:
|
if self.auth_token is None:
|
||||||
self.auth_token = ""
|
self.auth_token = ""
|
||||||
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
||||||
@@ -290,6 +293,10 @@ class SessionClient(adapter.Adapter):
|
|||||||
headers.setdefault('Content-Type', content_type)
|
headers.setdefault('Content-Type', content_type)
|
||||||
|
|
||||||
resp = super(SessionClient, self).request(*args, **kwargs)
|
resp = super(SessionClient, self).request(*args, **kwargs)
|
||||||
|
|
||||||
|
if resp.headers.get('content-type') == 'application/zip':
|
||||||
|
return resp, resp.content
|
||||||
|
|
||||||
return resp, resp.text
|
return resp, resp.text
|
||||||
|
|
||||||
def _check_uri_length(self, url):
|
def _check_uri_length(self, url):
|
||||||
|
@@ -106,6 +106,23 @@ class DictModel(dict):
|
|||||||
return ', '.join(sorted(pairs))
|
return ', '.join(sorted(pairs))
|
||||||
|
|
||||||
|
|
||||||
|
def save_data(data, path):
|
||||||
|
"""Save data to the specified path.
|
||||||
|
|
||||||
|
:param data: binary or string data
|
||||||
|
:param path: file path to save data
|
||||||
|
"""
|
||||||
|
if path is None:
|
||||||
|
vnfpackage = getattr(sys.stdout, 'buffer', sys.stdout)
|
||||||
|
else:
|
||||||
|
mode = 'wb' if isinstance(data, bytes) else 'w'
|
||||||
|
vnfpackage = open(path, mode)
|
||||||
|
try:
|
||||||
|
vnfpackage.write(data)
|
||||||
|
finally:
|
||||||
|
vnfpackage.close()
|
||||||
|
|
||||||
|
|
||||||
def exit(msg=None, exit_code=1):
|
def exit(msg=None, exit_code=1):
|
||||||
if msg:
|
if msg:
|
||||||
print(encodeutils.safe_decode(msg))
|
print(encodeutils.safe_decode(msg))
|
||||||
|
@@ -14,6 +14,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
from osc_lib.cli import parseractions
|
from osc_lib.cli import parseractions
|
||||||
from osc_lib.command import command
|
from osc_lib.command import command
|
||||||
@@ -258,6 +259,66 @@ class DeleteVnfPackage(command.Command):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadVnfPackage(command.Command):
|
||||||
|
_description = _("Read VNFD of an on-boarded VNF Package")
|
||||||
|
|
||||||
|
def get_parser(self, prog_name):
|
||||||
|
parser = super(DownloadVnfPackage, self).get_parser(prog_name)
|
||||||
|
parser.add_argument(
|
||||||
|
"vnf_package",
|
||||||
|
metavar="<vnf-package>",
|
||||||
|
help=_("VNF package ID")
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--file",
|
||||||
|
metavar="<FILE>",
|
||||||
|
help=_("Local file to save downloaded vnfd data. "
|
||||||
|
"If this is not specified and there is no redirection "
|
||||||
|
"then vnfd data will not be saved.")
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--vnfd",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help=_("Download VNFD of an on-boarded vnf package."),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--type",
|
||||||
|
default="application/zip",
|
||||||
|
metavar="<type>",
|
||||||
|
choices=["text/plain", "application/zip", "both"],
|
||||||
|
help=_("Provide text/plain when VNFD is implemented as a single "
|
||||||
|
"YAML file otherwise use application/zip. If you are not "
|
||||||
|
"aware whether VNFD is a single or multiple yaml files, "
|
||||||
|
"then you can specify 'both' option value. "
|
||||||
|
"Provide this option only when --vnfd is set.")
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def take_action(self, parsed_args):
|
||||||
|
client = self.app.client_manager.tackerclient
|
||||||
|
if parsed_args.vnfd:
|
||||||
|
if sys.stdout.isatty() and not (parsed_args.file and
|
||||||
|
parsed_args.type != "text/plain"):
|
||||||
|
msg = ("No redirection or local file specified for downloaded "
|
||||||
|
"VNFD data. Please specify a local file with --file to "
|
||||||
|
"save downloaded VNFD data or use redirection.")
|
||||||
|
sdk_utils.exit(msg)
|
||||||
|
|
||||||
|
body = client.download_vnfd_from_vnf_package(
|
||||||
|
parsed_args.vnf_package, parsed_args.type)
|
||||||
|
|
||||||
|
if parsed_args.file:
|
||||||
|
sdk_utils.save_data(body, parsed_args.file)
|
||||||
|
else:
|
||||||
|
print(body)
|
||||||
|
|
||||||
|
else:
|
||||||
|
msg = ("Currently only download vnfd from on-boarded vnf package "
|
||||||
|
"is supported. use --vnfd")
|
||||||
|
sdk_utils.exit(msg)
|
||||||
|
|
||||||
|
|
||||||
class UpdateVnfPackage(command.ShowOne):
|
class UpdateVnfPackage(command.ShowOne):
|
||||||
_description = _("Update information about an individual VNF package")
|
_description = _("Update information about an individual VNF package")
|
||||||
|
|
||||||
|
@@ -13,12 +13,15 @@
|
|||||||
# 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 filecmp
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
import mock
|
import mock
|
||||||
|
import zipfile
|
||||||
|
|
||||||
from tackerclient.common import exceptions
|
from tackerclient.common import exceptions
|
||||||
from tackerclient.osc import utils as tacker_osc_utils
|
from tackerclient.osc import utils as tacker_osc_utils
|
||||||
@@ -421,3 +424,139 @@ class TestUpdateVnfPackage(TestVnfPackage):
|
|||||||
verifylist)
|
verifylist)
|
||||||
self.assertRaises(SystemExit, self.update_vnf_package.take_action,
|
self.assertRaises(SystemExit, self.update_vnf_package.take_action,
|
||||||
parsed_args)
|
parsed_args)
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class TestDownloadVnfPackage(TestVnfPackage):
|
||||||
|
|
||||||
|
# The new vnf package created.
|
||||||
|
_vnf_package = vnf_package_fakes.vnf_package_obj(
|
||||||
|
attrs={'userDefinedData': {'Test_key': 'Test_value'}})
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDownloadVnfPackage, self).setUp()
|
||||||
|
self.download_vnf_package = vnf_package.DownloadVnfPackage(
|
||||||
|
self.app, self.app_args, cmd_name='vnf package download')
|
||||||
|
|
||||||
|
def test_download_no_options(self):
|
||||||
|
self.assertRaises(base.ParserException, self.check_parser,
|
||||||
|
self.download_vnf_package, [], [])
|
||||||
|
|
||||||
|
def _mock_request_url_for_download_vnfd(self, content_type, vnfd_data):
|
||||||
|
self.header = {'content-type': content_type}
|
||||||
|
url = os.path.join(self.url, 'vnfpkgm/v1/vnf_packages',
|
||||||
|
self._vnf_package['id'], 'vnfd')
|
||||||
|
|
||||||
|
if content_type == 'text/plain':
|
||||||
|
self.requests_mock.register_uri('GET', url,
|
||||||
|
headers=self.header,
|
||||||
|
text=vnfd_data)
|
||||||
|
else:
|
||||||
|
self.requests_mock.register_uri('GET', url,
|
||||||
|
headers=self.header,
|
||||||
|
content=vnfd_data)
|
||||||
|
|
||||||
|
def _get_arglist_and_verifylist(self, accept_type, file_name):
|
||||||
|
arglist = [
|
||||||
|
self._vnf_package['id'],
|
||||||
|
'--vnfd',
|
||||||
|
'--type', accept_type,
|
||||||
|
'--file', file_name
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('type', accept_type),
|
||||||
|
('vnfd', True),
|
||||||
|
('vnf_package', self._vnf_package['id']),
|
||||||
|
('file', file_name)
|
||||||
|
]
|
||||||
|
return arglist, verifylist
|
||||||
|
|
||||||
|
def test_download_vnfd_from_vnf_package_for_type_text_plain(self):
|
||||||
|
test_file = ('./tackerclient/tests//unit/osc/v1/fixture_data/'
|
||||||
|
'sample_vnf_package/Definitions/'
|
||||||
|
'etsi_nfv_sol001_common_types.yaml')
|
||||||
|
|
||||||
|
local_file = tempfile.NamedTemporaryFile(suffix='vnfd_data.yaml')
|
||||||
|
vnfd_data = open(test_file, 'r').read()
|
||||||
|
arglist, verifylist = self._get_arglist_and_verifylist(
|
||||||
|
'text/plain', local_file.name)
|
||||||
|
parsed_args = self.check_parser(self.download_vnf_package, arglist,
|
||||||
|
verifylist)
|
||||||
|
self._mock_request_url_for_download_vnfd('text/plain', vnfd_data)
|
||||||
|
self.download_vnf_package.take_action(parsed_args)
|
||||||
|
self.assertTrue(filecmp.cmp(test_file, local_file.name),
|
||||||
|
"Downloaded contents don't match test file")
|
||||||
|
|
||||||
|
@ddt.data('application/zip', 'both')
|
||||||
|
def test_download_vnfd_from_vnf_package(self, accept_type):
|
||||||
|
test_file, temp_dir = _create_zip()
|
||||||
|
# File in which VNFD data will be stored.
|
||||||
|
# For testing purpose we are creating temporary zip file.
|
||||||
|
local_file = tempfile.NamedTemporaryFile(suffix='vnfd_data.zip')
|
||||||
|
vnfd_data = open(test_file, 'rb').read()
|
||||||
|
arglist, verifylist = self._get_arglist_and_verifylist(
|
||||||
|
accept_type, local_file.name)
|
||||||
|
parsed_args = self.check_parser(self.download_vnf_package, arglist,
|
||||||
|
verifylist)
|
||||||
|
# When --type argument is set to 'both', then 'Accept' header in
|
||||||
|
# request is set to 'text/plain,application/zip' now it is up to the
|
||||||
|
# NFVO to choose the format to return for a single-file VNFD and for
|
||||||
|
# a multi-file VNFD, a ZIP file shall be returned. Here we have taken
|
||||||
|
# the example of multi-file vnfd hence its retuning zip file and
|
||||||
|
# setting the 'Content-Type' as 'application/zip' in response header.
|
||||||
|
self._mock_request_url_for_download_vnfd('application/zip', vnfd_data)
|
||||||
|
self.download_vnf_package.take_action(parsed_args)
|
||||||
|
self.assertTrue(filecmp.cmp(test_file, local_file.name),
|
||||||
|
"Downloaded contents don't match test file")
|
||||||
|
self.assertTrue(self._check_valid_zip_file(local_file.name))
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
|
def _check_valid_zip_file(self, zip_file):
|
||||||
|
with zipfile.ZipFile(zip_file) as zf:
|
||||||
|
ret = zf.testzip()
|
||||||
|
return False if ret else True
|
||||||
|
|
||||||
|
@mock.patch('builtins.print')
|
||||||
|
def test_download_vnfd_from_vnf_package_without_file_arg(self, mock_print):
|
||||||
|
# --file argument is optional when --type is set to 'text/plain'.
|
||||||
|
arglist = [
|
||||||
|
self._vnf_package['id'],
|
||||||
|
'--vnfd',
|
||||||
|
'--type', 'text/plain',
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('type', 'text/plain'),
|
||||||
|
('vnfd', True),
|
||||||
|
('vnf_package', self._vnf_package['id']),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.download_vnf_package, arglist,
|
||||||
|
verifylist)
|
||||||
|
test_file = ('./tackerclient/tests//unit/osc/v1/fixture_data/'
|
||||||
|
'sample_vnf_package/Definitions/'
|
||||||
|
'etsi_nfv_sol001_common_types.yaml')
|
||||||
|
|
||||||
|
vnfd_data = open(test_file, 'r').read()
|
||||||
|
self._mock_request_url_for_download_vnfd('text/plain', vnfd_data)
|
||||||
|
self.download_vnf_package.take_action(parsed_args)
|
||||||
|
mock_print.assert_called_once_with(vnfd_data)
|
||||||
|
|
||||||
|
@ddt.data('application/zip', 'both')
|
||||||
|
def test_download_vnfd_from_vnf_package_failed_with_no_file_arg(
|
||||||
|
self, accept_type):
|
||||||
|
arglist = [
|
||||||
|
self._vnf_package['id'],
|
||||||
|
'--vnfd',
|
||||||
|
'--type', accept_type,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('type', accept_type),
|
||||||
|
('vnfd', True),
|
||||||
|
('vnf_package', self._vnf_package['id']),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.download_vnf_package, arglist,
|
||||||
|
verifylist)
|
||||||
|
with mock.patch.object(sys.stdout, "isatty") as mock_isatty:
|
||||||
|
mock_isatty.return_value = True
|
||||||
|
self.assertRaises(SystemExit,
|
||||||
|
self.download_vnf_package.take_action,
|
||||||
|
parsed_args)
|
||||||
|
@@ -205,8 +205,11 @@ class ClientBase(object):
|
|||||||
action, method, body=body,
|
action, method, body=body,
|
||||||
content_type=self.content_type())
|
content_type=self.content_type())
|
||||||
|
|
||||||
if ('application/json' in resp.headers.get('Content-Type',
|
if 'application/zip' == resp.headers.get('Content-Type'):
|
||||||
'application/json')):
|
self.format = 'zip'
|
||||||
|
elif 'text/plain' == resp.headers.get('Content-Type'):
|
||||||
|
self.format = 'text'
|
||||||
|
else:
|
||||||
self.format = 'json'
|
self.format = 'json'
|
||||||
|
|
||||||
status_code = resp.status_code
|
status_code = resp.status_code
|
||||||
@@ -231,7 +234,7 @@ class ClientBase(object):
|
|||||||
"""
|
"""
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
elif self.format == 'zip':
|
elif self.format in ('zip', 'text'):
|
||||||
return data
|
return data
|
||||||
elif type(data) is dict:
|
elif type(data) is dict:
|
||||||
return serializer.Serializer(
|
return serializer.Serializer(
|
||||||
@@ -242,7 +245,7 @@ class ClientBase(object):
|
|||||||
|
|
||||||
def deserialize(self, data, status_code):
|
def deserialize(self, data, status_code):
|
||||||
"""Deserializes an XML or JSON string into a dictionary."""
|
"""Deserializes an XML or JSON string into a dictionary."""
|
||||||
if status_code in (204, 202):
|
if status_code in (204, 202) or self.format in ('zip', 'text'):
|
||||||
return data
|
return data
|
||||||
return serializer.Serializer(self.get_attr_metadata()).deserialize(
|
return serializer.Serializer(self.get_attr_metadata()).deserialize(
|
||||||
data, self.content_type())['body']
|
data, self.content_type())['body']
|
||||||
@@ -261,11 +264,16 @@ class ClientBase(object):
|
|||||||
constants.EXT_NS: ns}
|
constants.EXT_NS: ns}
|
||||||
|
|
||||||
def content_type(self, _format=None):
|
def content_type(self, _format=None):
|
||||||
"""Returns the mime-type for either 'xml' or 'json'.
|
"""Returns the mime-type for either 'xml', 'json, 'text', or 'zip'.
|
||||||
|
|
||||||
Defaults to the currently set format.
|
Defaults to the currently set format.
|
||||||
"""
|
"""
|
||||||
_format = _format or self.format
|
_format = _format or self.format
|
||||||
|
if self.format == 'text':
|
||||||
|
return "text/plain"
|
||||||
|
elif self.format == 'both':
|
||||||
|
return "text/plain,application/zip"
|
||||||
|
else:
|
||||||
return "application/%s" % (_format)
|
return "application/%s" % (_format)
|
||||||
|
|
||||||
def retry_request(self, method, action, body=None,
|
def retry_request(self, method, action, body=None,
|
||||||
@@ -745,6 +753,7 @@ class VnfPackageClient(ClientBase):
|
|||||||
|
|
||||||
vnfpackages_path = '/vnfpkgm/v1/vnf_packages'
|
vnfpackages_path = '/vnfpkgm/v1/vnf_packages'
|
||||||
vnfpackage_path = '/vnfpkgm/v1/vnf_packages/%s'
|
vnfpackage_path = '/vnfpkgm/v1/vnf_packages/%s'
|
||||||
|
vnfpackage_vnfd_path = '/vnfpkgm/v1/vnf_packages/%s/vnfd'
|
||||||
|
|
||||||
def build_action(self, action):
|
def build_action(self, action):
|
||||||
return action
|
return action
|
||||||
@@ -785,6 +794,31 @@ class VnfPackageClient(ClientBase):
|
|||||||
base_path=self.vnfpackages_path),
|
base_path=self.vnfpackages_path),
|
||||||
body=file_data)
|
body=file_data)
|
||||||
|
|
||||||
|
@APIParamsCall
|
||||||
|
def download_vnfd_from_vnf_package(self, vnf_package, accept):
|
||||||
|
"""Read VNFD of an on-boarded VNF Package.
|
||||||
|
|
||||||
|
:param vnf_package: The value can be either the ID of a vnf package
|
||||||
|
or a :class:`~openstack.nfv_orchestration.v1.
|
||||||
|
vnf_package` instance.
|
||||||
|
:param accept: Valid values are 'text/plain', 'application/zip' and
|
||||||
|
'both'. According to these values 'Accept' header will
|
||||||
|
be set as 'text/plain', 'application/zip',
|
||||||
|
'text/plain,application/zip' respectively.
|
||||||
|
|
||||||
|
:returns: 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.
|
||||||
|
"""
|
||||||
|
if accept == 'text/plain':
|
||||||
|
self.format = 'text'
|
||||||
|
elif accept == 'application/zip':
|
||||||
|
self.format = 'zip'
|
||||||
|
else:
|
||||||
|
self.format = 'both'
|
||||||
|
return self.get(self.vnfpackage_vnfd_path % vnf_package)
|
||||||
|
|
||||||
@APIParamsCall
|
@APIParamsCall
|
||||||
def update_vnf_package(self, vnf_package, body):
|
def update_vnf_package(self, vnf_package, body):
|
||||||
return self.patch(self.vnfpackage_path % vnf_package, body=body)
|
return self.patch(self.vnfpackage_path % vnf_package, body=body)
|
||||||
@@ -1110,3 +1144,7 @@ class Client(object):
|
|||||||
|
|
||||||
def update_vnf_package(self, vnf_package, body):
|
def update_vnf_package(self, vnf_package, body):
|
||||||
return self.vnf_package_client.update_vnf_package(vnf_package, body)
|
return self.vnf_package_client.update_vnf_package(vnf_package, body)
|
||||||
|
|
||||||
|
def download_vnfd_from_vnf_package(self, vnf_package, accept):
|
||||||
|
return self.vnf_package_client.download_vnfd_from_vnf_package(
|
||||||
|
vnf_package, accept)
|
||||||
|
Reference in New Issue
Block a user