Fetch an on-boarded VNF package with HTTP_RANGE
The GET method fetches the content of a VNF package identified by the VNF package identifier allocated by the NFVO. The 'HTTP_RANGE' header is inspected for a valid value. If it is present and valid, then the package content within the range is fetched.If not present the entire content is fetched A 'size' column has been added to vnf_packages db table. When the VNF package content is uploaded, the size of the csar zip is persisted in this newly added 'size' column of vnf_packages db table. When VNF_Package content is fetched for the first time, its size is retrieved from glance store and the size column in vnf packages table is populated with this value. For subsequent fetch calls the size of csar file content is retrieved from 'size' column in vnf_packages table. A lot of the content fetching code has been reused from glance/api/v2/image_data.py ResponseSerializer class download function Blueprint: bp/enhance-vnf-package-support-part1 Change-Id: Ib49ef8b1e81ca4a4b7e3ac4a0836c111ce7da4a3
This commit is contained in:
parent
4a2fd6c292
commit
d3cc82139e
@ -7,6 +7,20 @@ content_type:
|
||||
in: header
|
||||
required: true
|
||||
type: string
|
||||
fetch_content_type:
|
||||
description: |
|
||||
The fetched VNF Package will be returned with Content-Type set
|
||||
as `application/zip` in the response header.
|
||||
in: header
|
||||
required: true
|
||||
type: string
|
||||
range:
|
||||
description: |
|
||||
The byte range of VNF Package to be downloaded is specified in the Range
|
||||
header.
|
||||
in: header
|
||||
required: false
|
||||
type: string
|
||||
|
||||
# variables in path
|
||||
alias_path:
|
||||
|
@ -15,6 +15,10 @@
|
||||
204:
|
||||
default: |
|
||||
The server has fulfilled the request by deleting the resource.
|
||||
206:
|
||||
default: |
|
||||
Partial Content, The server has fulfilled the partial GET request for the
|
||||
resource.
|
||||
300:
|
||||
default: |
|
||||
There are multiple choices for resources. The request has to be more
|
||||
@ -56,6 +60,13 @@
|
||||
This operation conflicted with another operation on this resource.
|
||||
duplcate_zone: |
|
||||
There is already a zone with this name.
|
||||
416:
|
||||
default: |
|
||||
Requested Range Not Satisfiable, A server SHOULD return a response with
|
||||
this status code if a request included a Range request-header field,
|
||||
and none of the range-specifier values in this field overlap the current
|
||||
extent of the selected resource, and the request did not include an
|
||||
If-Range request-header field.
|
||||
500:
|
||||
default: |
|
||||
Something went wrong inside the service. This should not happen usually.
|
||||
|
@ -361,3 +361,50 @@ Response
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- Content-Type: content_type
|
||||
|
||||
Fetch an on-boarded VNF package with HTTP_RANGE
|
||||
================================================
|
||||
|
||||
.. rest_method:: GET /vnfpkgm/v1/vnf_packages/{vnf_package_id}/package_content
|
||||
|
||||
Fetch an on-boarded VNF package with HTTP_RANGE.
|
||||
|
||||
The GET method fetches the content of a VNF package identified by
|
||||
the VNF package identifier allocated by the NFVO.
|
||||
|
||||
The 'HTTP_RANGE' header is inspected for a valid value.
|
||||
If it is present and valid, then the package content within
|
||||
the range is fetched. If not present the entire content is
|
||||
fetched.
|
||||
|
||||
Response Codes
|
||||
--------------
|
||||
|
||||
.. rest_status_code:: success status.yaml
|
||||
|
||||
- 200
|
||||
- 206
|
||||
|
||||
.. rest_status_code:: error status.yaml
|
||||
|
||||
- 403
|
||||
- 404
|
||||
- 409
|
||||
- 416
|
||||
- 500
|
||||
|
||||
Request Parameters
|
||||
------------------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- vnf_package_id: vnf_package_id_path
|
||||
- Accept: fetch_content_type
|
||||
- Range: range
|
||||
|
||||
Response
|
||||
--------
|
||||
|
||||
.. rest_parameters:: parameters.yaml
|
||||
|
||||
- Content-Type: fetch_content_type
|
||||
|
@ -171,6 +171,123 @@ class VnfPkgmController(wsgi.Controller):
|
||||
# Delete vnf_package
|
||||
self.rpc_api.delete_vnf_package(context, vnf_package)
|
||||
|
||||
@wsgi.response(http_client.ACCEPTED)
|
||||
@wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND,
|
||||
http_client.CONFLICT,
|
||||
http_client.REQUESTED_RANGE_NOT_SATISFIABLE))
|
||||
def fetch_vnf_package_content(self, request, id):
|
||||
context = request.environ['tacker.context']
|
||||
context.can(vnf_package_policies.VNFPKGM % 'fetch_package_content')
|
||||
|
||||
vnf_package = self._get_vnf_package(id, request)
|
||||
|
||||
if vnf_package.onboarding_state != \
|
||||
fields.PackageOnboardingStateType.ONBOARDED:
|
||||
msg = _("VNF Package %(id)s onboarding state "
|
||||
"is not %(onboarding)s")
|
||||
raise webob.exc.HTTPConflict(explanation=msg % {"id": id,
|
||||
"onboarding": fields.PackageOnboardingStateType.ONBOARDED})
|
||||
|
||||
if vnf_package.size == 0:
|
||||
|
||||
try:
|
||||
zip_file_size = glance_store.get_csar_size(id,
|
||||
vnf_package.location_glance_store)
|
||||
vnf_package.size = zip_file_size
|
||||
vnf_package.save()
|
||||
except exceptions.VnfPackageLocationInvalid:
|
||||
msg = _("Vnf package not present at location")
|
||||
raise webob.exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
else:
|
||||
zip_file_size = vnf_package.size
|
||||
|
||||
range_val = self._get_range_from_request(request, zip_file_size)
|
||||
|
||||
return self._download(
|
||||
request.response, range_val, id, vnf_package.location_glance_store,
|
||||
zip_file_size)
|
||||
|
||||
def _download(self, response, range_val, uuid, location, zip_file_size):
|
||||
offset, chunk_size = 0, None
|
||||
if range_val:
|
||||
if isinstance(range_val, webob.byterange.Range):
|
||||
response_end = zip_file_size - 1
|
||||
# NOTE(sameert): webob parsing is zero-indexed.
|
||||
# i.e.,to download first 5 bytes of a 10 byte image,
|
||||
# request should be "bytes=0-4" and the response would be
|
||||
# "bytes 0-4/10".
|
||||
# Range if validated, will never have 'start' object as None.
|
||||
if range_val.start >= 0:
|
||||
offset = range_val.start
|
||||
else:
|
||||
# NOTE(sameert): Negative start values needs to be
|
||||
# processed to allow suffix-length for Range request
|
||||
# like "bytes=-2" as per rfc7233.
|
||||
if abs(range_val.start) < zip_file_size:
|
||||
offset = zip_file_size + range_val.start
|
||||
|
||||
if range_val.end is not None and range_val.end < zip_file_size:
|
||||
chunk_size = range_val.end - offset
|
||||
response_end = range_val.end - 1
|
||||
else:
|
||||
chunk_size = zip_file_size - offset
|
||||
|
||||
response.status_int = 206
|
||||
|
||||
response.headers['Content-Type'] = 'application/zip'
|
||||
|
||||
response.app_iter = self._get_csar_zip_data(uuid,
|
||||
location, offset, chunk_size)
|
||||
# NOTE(sameert): In case of a full zip download, when
|
||||
# chunk_size was none, reset it to zip.size to set the
|
||||
# response header's Content-Length.
|
||||
if chunk_size is not None:
|
||||
response.headers['Content-Range'] = 'bytes %s-%s/%s'\
|
||||
% (offset,
|
||||
response_end,
|
||||
zip_file_size)
|
||||
else:
|
||||
chunk_size = zip_file_size
|
||||
response.headers['Content-Length'] = six.text_type(chunk_size)
|
||||
return response
|
||||
|
||||
def _get_csar_zip_data(self, uuid, location, offset=0, chunk_size=None):
|
||||
try:
|
||||
resp, size = glance_store.load_csar_iter(
|
||||
uuid, location, offset=offset, chunk_size=chunk_size)
|
||||
except exceptions.VnfPackageLocationInvalid:
|
||||
msg = _("Vnf package not present at location")
|
||||
raise webob.exc.HTTPServerError(explanation=msg)
|
||||
return resp
|
||||
|
||||
def _get_range_from_request(self, request, zip_file_size):
|
||||
range_str = request._headers.environ.get('HTTP_RANGE')
|
||||
if range_str is not None:
|
||||
# NOTE(sameert): We do not support multi range requests.
|
||||
if ',' in range_str:
|
||||
msg = _("Requests with multiple ranges are not supported in "
|
||||
"Tacker. You may make multiple single-range requests "
|
||||
"instead.")
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
range_ = webob.byterange.Range.parse(range_str)
|
||||
if range_ is None:
|
||||
range_err_msg = _("The byte range passed in the 'Range' header"
|
||||
"did not match any available byte range in the VNF package"
|
||||
"file")
|
||||
raise webob.exc.HTTPRequestRangeNotSatisfiable(
|
||||
explanation=range_err_msg)
|
||||
# NOTE(sameert): Ensure that a range like bytes=4- for an zip
|
||||
# size of 3 is invalidated as per rfc7233.
|
||||
if range_.start >= zip_file_size:
|
||||
msg = _("Invalid start position in Range header. "
|
||||
"Start position MUST be in the inclusive range"
|
||||
"[0, %s].") % (zip_file_size - 1)
|
||||
raise webob.exc.HTTPRequestRangeNotSatisfiable(
|
||||
explanation=msg)
|
||||
return range_
|
||||
|
||||
@wsgi.response(http_client.ACCEPTED)
|
||||
@wsgi.expected_errors((http_client.FORBIDDEN, http_client.NOT_FOUND,
|
||||
http_client.CONFLICT))
|
||||
@ -207,7 +324,7 @@ class VnfPkgmController(wsgi.Controller):
|
||||
vnf_package.algorithm = CONF.vnf_package.hashing_algorithm
|
||||
vnf_package.hash = multihash
|
||||
vnf_package.location_glance_store = location
|
||||
|
||||
vnf_package.size = size
|
||||
vnf_package.save()
|
||||
|
||||
# process vnf_package
|
||||
|
@ -64,7 +64,8 @@ class VnfpkgmAPIRouter(wsgi.Router):
|
||||
methods, controller, default_resource)
|
||||
|
||||
# Allowed methods on /vnf_packages/{id}/package_content resource
|
||||
methods = {"PUT": "upload_vnf_package_content"}
|
||||
methods = {"PUT": "upload_vnf_package_content",
|
||||
"GET": "fetch_vnf_package_content"}
|
||||
self._setup_route(mapper, "/vnf_packages/{id}/package_content",
|
||||
methods, controller, default_resource)
|
||||
|
||||
|
@ -286,6 +286,10 @@ class VNFPackageURLInvalid(Invalid):
|
||||
message = _("Failed to open URL %(url)s")
|
||||
|
||||
|
||||
class VnfPackageLocationInvalid(Invalid):
|
||||
message = _("Failed to find location: %(location)")
|
||||
|
||||
|
||||
class InvalidZipFile(Invalid):
|
||||
message = _("Invalid zip file : %(path)s")
|
||||
|
||||
@ -299,6 +303,10 @@ class FailedToGetVnfdData(Invalid):
|
||||
message = _("Failed to get csar zip file from glance store: %(error)s")
|
||||
|
||||
|
||||
class FailedToGetVnfPackageDetails(Invalid):
|
||||
message = _("Failed to get vnf package details: %(error)s")
|
||||
|
||||
|
||||
class InvalidCSAR(Invalid):
|
||||
message = _("Invalid csar: %(error)s")
|
||||
|
||||
|
@ -265,7 +265,6 @@ class Conductor(manager.Manager):
|
||||
def upload_vnf_package_from_uri(self, context, vnf_package,
|
||||
address_information, user_name=None,
|
||||
password=None):
|
||||
|
||||
body = {"address_information": address_information}
|
||||
(location, size, checksum, multihash,
|
||||
loc_meta) = glance_store.store_csar(context, vnf_package.id, body)
|
||||
@ -276,7 +275,7 @@ class Conductor(manager.Manager):
|
||||
vnf_package.algorithm = CONF.vnf_package.hashing_algorithm
|
||||
vnf_package.hash = multihash
|
||||
vnf_package.location_glance_store = location
|
||||
|
||||
vnf_package.size = size
|
||||
vnf_package.save()
|
||||
|
||||
zip_path = glance_store.load_csar(vnf_package.id, location)
|
||||
|
@ -141,6 +141,7 @@ class VnfPackage(model_base.BASE, models.SoftDeleteMixin,
|
||||
algorithm = sa.Column(sa.String(64), nullable=True)
|
||||
hash = sa.Column(sa.String(128), nullable=True)
|
||||
location_glance_store = sa.Column(sa.Text(), nullable=True)
|
||||
size = sa.Column(sa.BigInteger, nullable=False, default=0)
|
||||
|
||||
_metadata = orm.relationship(
|
||||
VnfPackageUserData,
|
||||
|
@ -1 +1 @@
|
||||
985e28392890
|
||||
d2e39e01d540
|
||||
|
@ -0,0 +1,37 @@
|
||||
# Copyright 2019 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""add size column to vnf_packages table
|
||||
|
||||
Revision ID: d2e39e01d540
|
||||
Revises: 985e28392890
|
||||
Create Date: 2019-11-27 13:30:23.599865
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd2e39e01d540'
|
||||
down_revision = '985e28392890'
|
||||
|
||||
|
||||
# Added 'size' column to an existing table. Server_default bit will make
|
||||
# existing rows 0 for that column
|
||||
def upgrade(active_plugins=None, options=None):
|
||||
op.add_column('vnf_packages', sa.Column('size', sa.BigInteger,
|
||||
nullable=False,
|
||||
server_default='0'))
|
@ -54,7 +54,6 @@ def get_csar_data_iter(body):
|
||||
def store_csar(context, package_uuid, body):
|
||||
|
||||
data_iter = get_csar_data_iter(body)
|
||||
|
||||
try:
|
||||
# store CSAR file in glance_store
|
||||
(location, size, checksum, multihash,
|
||||
@ -91,16 +90,21 @@ def delete_csar(context, package_uuid, location):
|
||||
{"uuid": package_uuid})
|
||||
|
||||
|
||||
def get_csar_size(package_uuid, location):
|
||||
|
||||
try:
|
||||
return glance_store.backend.get_size_from_backend(location)
|
||||
except Exception:
|
||||
LOG.exception("Failed to get csar data from glance store %(location)s"
|
||||
"for package %(uuid)s", {"location": location, "uuid": package_uuid})
|
||||
raise exceptions.VnfPackageLocationInvalid(location=location)
|
||||
|
||||
|
||||
def load_csar(package_uuid, location):
|
||||
zip_path = os.path.join(CONF.vnf_package.vnf_package_csar_path,
|
||||
package_uuid + ".zip")
|
||||
|
||||
try:
|
||||
resp, size = glance_store.backend.get_from_backend(location)
|
||||
except Exception:
|
||||
LOG.info("Failed to get csar data from glance store %(location)s for "
|
||||
"package %(uuid)s",
|
||||
{"location": location, "uuid": package_uuid})
|
||||
resp, size = _get_csar_chunks(
|
||||
package_uuid, location, offset=0, chunk_size=None)
|
||||
|
||||
try:
|
||||
temp_data = open(zip_path, 'wb')
|
||||
@ -115,3 +119,21 @@ def load_csar(package_uuid, location):
|
||||
'error': encodeutils.exception_to_unicode(exp)})
|
||||
|
||||
return zip_path
|
||||
|
||||
|
||||
def load_csar_iter(package_uuid, location, offset=0, chunk_size=None):
|
||||
resp, size = _get_csar_chunks(
|
||||
package_uuid, location, offset=offset, chunk_size=chunk_size)
|
||||
return resp, size
|
||||
|
||||
|
||||
def _get_csar_chunks(package_uuid, location, offset, chunk_size):
|
||||
try:
|
||||
resp, size = glance_store.backend.get_from_backend(location,
|
||||
offset=offset,
|
||||
chunk_size=chunk_size)
|
||||
return resp, size
|
||||
except Exception:
|
||||
LOG.exception("Failed to get csar data from glance store %(location)s"
|
||||
"for package %(uuid)s", {"location": location, "uuid": package_uuid})
|
||||
raise exceptions.VnfPackageLocationInvalid(location=location)
|
||||
|
@ -18,6 +18,7 @@ from oslo_serialization import jsonutils as json
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
from oslo_utils import versionutils
|
||||
from oslo_versionedobjects import base as ovoo_base
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql import func
|
||||
@ -305,8 +306,8 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject,
|
||||
COMPLEX_ATTRIBUTES.extend(
|
||||
vnf_software_image.VnfSoftwareImage.COMPLEX_ATTRIBUTES)
|
||||
|
||||
# Version 1.0: Initial version
|
||||
VERSION = '1.0'
|
||||
# Version 1.1: Added 'size' to persist size of VnfPackage.
|
||||
VERSION = '1.1'
|
||||
|
||||
fields = {
|
||||
'id': fields.UUIDField(nullable=False),
|
||||
@ -323,8 +324,19 @@ class VnfPackage(base.TackerObject, base.TackerPersistentObject,
|
||||
'vnf_deployment_flavours': fields.ObjectField(
|
||||
'VnfDeploymentFlavoursList', nullable=True),
|
||||
'vnfd': fields.ObjectField('VnfPackageVnfd', nullable=True),
|
||||
'size': fields.IntegerField(nullable=False, default=0),
|
||||
}
|
||||
|
||||
def __init__(self, context=None, **kwargs):
|
||||
super(VnfPackage, self).__init__(context, **kwargs)
|
||||
self.obj_set_defaults()
|
||||
|
||||
def obj_make_compatible(self, primitive, target_version):
|
||||
super(VnfPackage, self).obj_make_compatible(primitive, target_version)
|
||||
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||
if target_version < (1, 1) and 'size' in primitive:
|
||||
del primitive['size']
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(context, vnf_package, db_vnf_package,
|
||||
expected_attrs=None):
|
||||
|
@ -62,6 +62,17 @@ rules = [
|
||||
'path': '/vnf_packages/{vnf_package_id}'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=VNFPKGM % 'fetch_package_content',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||
description="fetch the contents of an on-boarded VNF Package",
|
||||
operations=[
|
||||
{
|
||||
'method': 'GET',
|
||||
'path': '/vnf_packages/{vnf_package_id}/'
|
||||
'package_content'
|
||||
}
|
||||
]),
|
||||
policy.DocumentedRuleDefault(
|
||||
name=VNFPKGM % 'upload_package_content',
|
||||
check_str=base.RULE_ADMIN_OR_OWNER,
|
||||
|
@ -0,0 +1 @@
|
||||
#### THIS IS A DUMMY FILE TO SAVE FILE SIZE #####
|
File diff suppressed because it is too large
Load Diff
@ -289,30 +289,9 @@ class VnfPackageTest(base.BaseTackerTest):
|
||||
expected_result = [package1]
|
||||
self.assertEqual(expected_result, body)
|
||||
|
||||
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 = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'../../etc/samples/' + 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)
|
||||
self.addCleanup(self._disable_operational_state, vnf_package_id)
|
||||
resp, resp_body = self.http_client.do_request(
|
||||
'{base_path}/{id}/vnfd'.format(id=vnf_package_id,
|
||||
'{base_path}/{id}/vnfd'.format(id=self.package_id1,
|
||||
base_path=self.base_url),
|
||||
"GET", content_type='application/zip')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
@ -344,9 +323,9 @@ class VnfPackageTest(base.BaseTackerTest):
|
||||
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)
|
||||
single_yaml_csar_dir = "sample_vnfpkg_no_meta_single_vnfd"
|
||||
vnf_package_id = self._create_and_upload_vnf(
|
||||
single_yaml_csar_dir)
|
||||
self.addCleanup(self._delete_vnf_package, vnf_package_id)
|
||||
self.addCleanup(self._disable_operational_state, vnf_package_id)
|
||||
resp, resp_body = self.http_client.do_request(
|
||||
@ -356,3 +335,62 @@ class VnfPackageTest(base.BaseTackerTest):
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual('text/plain', resp.headers['Content-Type'])
|
||||
self.assertIsNotNone(resp.text)
|
||||
|
||||
def test_fetch_vnf_package_content_partial_download_using_range(self):
|
||||
"""Test partial download using 'Range' requests for csar zip"""
|
||||
# test for success on satisfiable Range request.
|
||||
range_ = 'bytes=3-8'
|
||||
headers = {'Range': range_}
|
||||
response = self.http_client.do_request(
|
||||
'{base_path}/{id}/package_content'.format(
|
||||
id=self.package_id1, base_path=self.base_url),
|
||||
"GET", body={}, headers=headers)
|
||||
self.assertEqual(206, response[0].status_code)
|
||||
self.assertEqual(
|
||||
'\x04\x14\x00\x00\x00\x00', response[0].content.decode(
|
||||
'utf-8', 'ignore'))
|
||||
self.assertEqual('6', response[0].headers['Content-Length'])
|
||||
|
||||
def test_fetch_vnf_package_content_full_download(self):
|
||||
"""Test full download for csar zip"""
|
||||
response = self.http_client.do_request(
|
||||
'{base_path}/{id}/package_content'.format(
|
||||
id=self.package_id1, base_path=self.base_url),
|
||||
"GET", body={}, headers={})
|
||||
self.assertEqual(200, response[0].status_code)
|
||||
self.assertEqual('12802866', response[0].headers['Content-Length'])
|
||||
|
||||
def test_fetch_vnf_package_content_combined_download(self):
|
||||
"""Combine two partial downloads using 'Range' requests for csar zip"""
|
||||
|
||||
zip_file_path = tempfile.NamedTemporaryFile(delete=True)
|
||||
zipf = zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_STORED)
|
||||
|
||||
# Partial download 1
|
||||
range_ = 'bytes=0-10'
|
||||
headers = {'Range': range_}
|
||||
response_1 = self.http_client.do_request(
|
||||
'{base_path}/{id}/package_content'.format(
|
||||
id=self.package_id1, base_path=self.base_url),
|
||||
"GET", body={}, headers=headers)
|
||||
size_1 = int(response_1[0].headers['Content-Length'])
|
||||
data = response_1[0].content
|
||||
file_path = self._get_csar_dir_path("data.txt")
|
||||
zipf.writestr(file_path, data)
|
||||
|
||||
# Partial download 2
|
||||
range_ = 'bytes=11-12802866'
|
||||
headers = {'Range': range_}
|
||||
response_2 = self.http_client.do_request(
|
||||
'{base_path}/{id}/package_content'.format(
|
||||
id=self.package_id1, base_path=self.base_url),
|
||||
"GET", body={}, headers=headers)
|
||||
|
||||
data = response_2[0].content
|
||||
zipf.writestr(file_path, data)
|
||||
zipf.close()
|
||||
size_2 = int(response_2[0].headers['Content-Length'])
|
||||
total_size = size_1 + size_2
|
||||
self.assertEqual(True, zipfile.is_zipfile(zip_file_path))
|
||||
self.assertEqual(12802866, total_size)
|
||||
zip_file_path.close()
|
||||
|
39
tacker/tests/unit/common/test_store.py
Normal file
39
tacker/tests/unit/common/test_store.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright (c) 2019 NTT DATA.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import testtools
|
||||
|
||||
from tacker.common import exceptions
|
||||
from tacker import context
|
||||
from tacker.glance_store import store
|
||||
from tacker.tests import constants
|
||||
|
||||
|
||||
class TestStore(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestStore, self).setUp()
|
||||
self.context = context.get_admin_context()
|
||||
self.base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def test_get_csar_size_invalid_path(self):
|
||||
self.assertRaises(
|
||||
exceptions.VnfPackageLocationInvalid, store.get_csar_size,
|
||||
constants.UUID, 'Invalid/path')
|
||||
|
||||
def test_load_csar_iter_invalid_path(self):
|
||||
self.assertRaises(
|
||||
exceptions.VnfPackageLocationInvalid, store.load_csar_iter,
|
||||
constants.UUID, 'Invalid/path')
|
@ -108,7 +108,7 @@ class TestConductor(SqlTestCase):
|
||||
mock_load_csar_data.return_value = (mock.ANY, mock.ANY)
|
||||
mock_load_csar.return_value = '/var/lib/tacker/5f5d99c6-844a' \
|
||||
'-4c31-9e6d-ab21b87dcfff.zip'
|
||||
mock_store.return_value = 'location', 'size', 'checksum',\
|
||||
mock_store.return_value = 'location', 0, 'checksum',\
|
||||
'multihash', 'loc_meta'
|
||||
self.conductor.upload_vnf_package_from_uri(self.context,
|
||||
self.vnf_package,
|
||||
|
@ -30,7 +30,8 @@ vnf_package_data = {'algorithm': None, 'hash': None,
|
||||
'user_data': {'abc': 'xyz'},
|
||||
'created_at': datetime.datetime(
|
||||
2019, 8, 8, 0, 0, 0, tzinfo=iso8601.UTC),
|
||||
'deleted': False
|
||||
'deleted': False,
|
||||
'size': 0
|
||||
}
|
||||
|
||||
software_image = {
|
||||
|
@ -46,6 +46,7 @@ class TestVnfPackage(SqlTestCase):
|
||||
self.assertEqual('CREATED', vnfpkgm.onboarding_state)
|
||||
self.assertEqual('NOT_IN_USE', vnfpkgm.usage_state)
|
||||
self.assertEqual('DISABLED', vnfpkgm.operational_state)
|
||||
self.assertEqual(0, vnfpkgm.size)
|
||||
|
||||
def test_create_without_user_define_data(self):
|
||||
vnf_pack = fakes.vnf_package_data
|
||||
@ -177,3 +178,11 @@ class TestVnfPackage(SqlTestCase):
|
||||
vnfpkgm_list = objects.VnfPackagesList.get_by_filters(
|
||||
self.context, filters=filters)
|
||||
self.assertEqual(1, len(vnfpkgm_list))
|
||||
|
||||
def test_obj_make_compatible(self):
|
||||
data = {'id': self.vnf_package.id}
|
||||
vnf_package_obj = objects.VnfPackage(context=self.context, **data)
|
||||
fake_vnf_package_obj = objects.VnfPackage(context=self.context, **data)
|
||||
obj_primitive = fake_vnf_package_obj.obj_to_primitive('1.0')
|
||||
obj = vnf_package_obj.obj_from_primitive(obj_primitive)
|
||||
self.assertIn('size', obj.fields)
|
||||
|
@ -526,7 +526,7 @@ class TestController(base.TestCase):
|
||||
vnf_package_obj = objects.VnfPackage(**vnf_package_dict)
|
||||
mock_vnf_by_id.return_value = vnf_package_obj
|
||||
mock_vnf_pack_save.return_value = vnf_package_obj
|
||||
mock_glance_store.return_value = 'location', 'size', 'checksum',\
|
||||
mock_glance_store.return_value = 'location', 0, 'checksum',\
|
||||
'multihash', 'loc_meta'
|
||||
req = fake_request.HTTPRequest.blank(
|
||||
'/vnf_packages/%s/package_content'
|
||||
@ -919,3 +919,27 @@ class TestController(base.TestCase):
|
||||
self.controller.get_vnf_package_vnfd,
|
||||
req, constants.UUID)
|
||||
self.assertEqual(http_client.INTERNAL_SERVER_ERROR, resp.status_code)
|
||||
|
||||
def test_fetch_vnf_package_content_valid_range(self):
|
||||
request = fake_request.HTTPRequest.blank(
|
||||
'/vnf_packages/%s/package_content/')
|
||||
request.headers["Range"] = 'bytes=10-99'
|
||||
range_ = self.controller._get_range_from_request(request, 120)
|
||||
self.assertEqual(10, range_.start)
|
||||
self.assertEqual(100, range_.end) # non-inclusive
|
||||
|
||||
def test_fetch_vnf_package_content_invalid_range(self):
|
||||
request = fake_request.HTTPRequest.blank(
|
||||
'/vnf_packages/%s/package_content/')
|
||||
request.headers["Range"] = 'bytes=150-'
|
||||
self.assertRaises(exc.HTTPRequestRangeNotSatisfiable,
|
||||
self.controller._get_range_from_request,
|
||||
request, 120)
|
||||
|
||||
def test_fetch_vnf_package_content_invalid_multiple_range(self):
|
||||
request = fake_request.HTTPRequest.blank(
|
||||
'/vnf_packages/%s/package_content/')
|
||||
request.headers["Range"] = 'bytes=10-20,21-30'
|
||||
self.assertRaises(exc.HTTPBadRequest,
|
||||
self.controller._get_range_from_request, request,
|
||||
120)
|
||||
|
Loading…
Reference in New Issue
Block a user