# Copyright (C) 2019 NTT DATA # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from copy import deepcopy import ddt import os import tempfile import time import urllib import zipfile from oslo_serialization import jsonutils import tacker.conf from tacker.tests.functional import base from tacker.tests import utils CONF = tacker.conf.CONF @ddt.ddt class VnfPackageTest(base.BaseTackerTest): VNF_PACKAGE_DELETE_TIMEOUT = 120 VNF_PACKAGE_UPLOAD_TIMEOUT = 300 # The size of CSAR zip file used while verifying downloaded package. # NOTE: It should be updated if some contents of packages under # `tacker/tests/etc/samples` dir. SIZE_CSAR_ZIP = 16389096 def setUp(self): super(VnfPackageTest, self).setUp() self.base_url = "/vnfpkgm/v1/vnf_packages" # Here we create and upload vnf package. Also get 'show' api response # as reference data for attribute filter tests self.package_id1 = self._create_and_upload_vnf("vnfpkgm1") show_url = self.base_url + "/" + self.package_id1 resp, self.package1 = self.http_client.do_request(show_url, "GET") self.assertEqual(200, resp.status_code) self.package_id2 = self._create_and_upload_vnf("vnfpkgm2") show_url = self.base_url + "/" + self.package_id2 resp, self.package2 = self.http_client.do_request(show_url, "GET") self.assertEqual(200, resp.status_code) self.package_id3 = self._create_and_upload_vnf("vnfpkgm3") show_url = self.base_url + "/" + self.package_id3 resp, self.package3 = self.http_client.do_request(show_url, "GET") self.assertEqual(200, resp.status_code) def tearDown(self): for package_id in [self.package_id1, self.package_id2, self.package_id3]: self._disable_operational_state(package_id) self._delete_vnf_package(package_id) self._wait_for_delete(package_id) super(VnfPackageTest, self).tearDown() def _wait_for_delete(self, package_uuid): show_url = self.base_url + "/" + package_uuid timeout = self.VNF_PACKAGE_DELETE_TIMEOUT start_time = int(time.time()) while True: resp, body = self.http_client.do_request(show_url, "GET") if (404 == resp.status_code): break if (int(time.time()) - start_time) > timeout: raise Exception("Failed to delete package") time.sleep(1) def _wait_for_onboard(self, package_uuid): show_url = self.base_url + "/" + package_uuid timeout = self.VNF_PACKAGE_UPLOAD_TIMEOUT start_time = int(time.time()) while True: resp, body = self.http_client.do_request(show_url, "GET") if body['onboardingState'] == "ONBOARDED": break if ((int(time.time()) - start_time) > timeout): raise Exception("Failed to onboard vnf package") time.sleep(1) def _create_vnf_package(self, body): resp, response_body = self.http_client.do_request(self.base_url, "POST", body=body) self.assertIsNotNone(response_body['id']) self.assertEqual(201, resp.status_code) return response_body def _disable_operational_state(self, package_uuid): update_req_body = jsonutils.dumps({ "operationalState": "DISABLED"}) resp, resp_body = self.http_client.do_request( '{base_path}/{id}'.format(id=package_uuid, base_path=self.base_url), "PATCH", content_type='application/json', body=update_req_body) self.assertEqual(200, resp.status_code) def _delete_vnf_package(self, package_uuid): url = self.base_url + "/" + package_uuid resp, body = self.http_client.do_request(url, "DELETE") self.assertEqual(204, resp.status_code) def test_create_show_delete_vnf_package(self): """Creates and deletes a vnf package.""" # Create vnf package vnf_package_id = self._create_and_upload_vnf('vnfpkgm1') # show vnf package show_url = self.base_url + "/" + vnf_package_id resp, body = self.http_client.do_request(show_url, "GET") self.assertEqual(200, resp.status_code) # update vnf package self._disable_operational_state(vnf_package_id) # Delete vnf package self._delete_vnf_package(vnf_package_id) self._wait_for_delete(vnf_package_id) # show vnf package should fail as it's deleted resp, body = self.http_client.do_request(show_url, "GET") self.assertEqual(404, resp.status_code) def test_list(self): vnf_package_list = [] body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}}) # create two vnf packages vnf_package = self._create_vnf_package(body) self.addCleanup(self._delete_vnf_package, vnf_package['id']) vnf_package_list.append(vnf_package['id']) vnf_package = self._create_vnf_package(body) vnf_package_list.append(vnf_package['id']) self.addCleanup(self._delete_vnf_package, vnf_package['id']) # list vnf package resp, body = self.http_client.do_request(self.base_url, "GET") self.assertEqual(200, resp.status_code) package_uuids = [obj['id'] for obj in body] self.assertIn(vnf_package_list[0], package_uuids) self.assertIn(vnf_package_list[1], package_uuids) def _get_csar_dir_path(self, csar_name): csar_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../etc/samples/etsi/nfv", csar_name)) return csar_dir def _create_and_upload_vnf(self, sample_name): body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}}) vnf_package = self._create_vnf_package(body) csar_dir = self._get_csar_dir_path(sample_name) if os.path.exists(os.path.join(csar_dir, 'TOSCA-Metadata')) and \ sample_name != 'vnfpkgm2': file_path = utils.create_csar_with_unique_artifact( csar_dir) else: file_path, vnfd_id = utils.create_csar_with_unique_vnfd_id( csar_dir) self.addCleanup(os.remove, file_path) 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_upload_from_uri_without_auth_and_delete(self): csar_dir = self._get_csar_dir_path("sample_vnfpkg_no_meta_single_vnfd") file_path, vnfd_id = utils.create_csar_with_unique_vnfd_id(csar_dir) self.addCleanup(os.remove, file_path) cls_obj = utils.StaticHttpFileHandler(os.path.dirname(file_path)) self.addCleanup(cls_obj.stop) body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}}) vnf_package = self._create_vnf_package(body) csar_file_uri = 'http://localhost:{port}/{filename}'.format( port=cls_obj.port, filename=os.path.basename(file_path)) body = jsonutils.dumps({"addressInformation": csar_file_uri}) resp, resp_body = self.http_client.do_request( '{base_path}/{id}/package_content/upload_from_uri'.format( id=vnf_package['id'], base_path=self.base_url), "POST", body=body) self.assertEqual(202, resp.status_code) self._wait_for_onboard(vnf_package['id']) self._disable_operational_state(vnf_package['id']) self._delete_vnf_package(vnf_package['id']) self._wait_for_delete(vnf_package['id']) def test_upload_from_uri_with_auth_and_delete(self): csar_dir = self._get_csar_dir_path("sample_vnfpkg_no_meta_single_vnfd") file_path, vnfd_id = utils.create_csar_with_unique_vnfd_id(csar_dir) self.addCleanup(os.remove, file_path) cls_obj = utils.StaticHttpFileHandler(os.path.dirname(file_path)) self.addCleanup(cls_obj.stop) body = jsonutils.dumps({"userDefinedData": {"foo": "bar"}}) vnf_package = self._create_vnf_package(body) csar_file_uri = 'http://localhost:{port}/{filename}'.format( port=cls_obj.port, filename=os.path.basename(file_path)) body = jsonutils.dumps({"addressInformation": csar_file_uri, "userName": "username", "password": "password"}) resp, resp_body = self.http_client.do_request( '{base_path}/{id}/package_content/upload_from_uri'.format( id=vnf_package['id'], base_path=self.base_url), "POST", body=body) self.assertEqual(202, resp.status_code) self._wait_for_onboard(vnf_package['id']) self._disable_operational_state(vnf_package['id']) self._delete_vnf_package(vnf_package['id']) self._wait_for_delete(vnf_package['id']) def test_patch_in_onboarded_state(self): user_data = jsonutils.dumps( {"userDefinedData": {"key1": "val1", "key2": "val2", "key3": "val3"}}) vnf_package = self._create_vnf_package(user_data) update_req_body = jsonutils.dumps( {"operationalState": "DISABLED", "userDefinedData": {"key1": "changed_val1", "key2": "val2", "new_key": "new_val"}}) expected_result = {"operationalState": "DISABLED", "userDefinedData": { "key1": "changed_val1", "new_key": "new_val"}} csar_dir = self._get_csar_dir_path("vnfpkgm1") file_path = utils.create_csar_with_unique_artifact(csar_dir) self.addCleanup(os.remove, file_path) 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']) # Update vnf package which is onboarded resp, resp_body = self.http_client.do_request( '{base_path}/{id}'.format(id=vnf_package['id'], base_path=self.base_url), "PATCH", content_type='application/json', body=update_req_body) self.assertEqual(200, resp.status_code) self.assertEqual(expected_result, resp_body) self._delete_vnf_package(vnf_package['id']) self._wait_for_delete(vnf_package['id']) def test_index_attribute_filter(self): filter_expr = { 'filter': "(gt,softwareImages/minDisk,7);" "(eq,onboardingState,ONBOARDED);" "(eq,softwareImages/checksum/algorithm,'sha-512');" "(eq,additionalArtifacts/checksum/algorithm,'sha-256')" } filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) resp, body = self.http_client.do_request(filter_url, "GET") package = deepcopy(self.package2) for attr in ['softwareImages', 'checksum', 'userDefinedData', 'additionalArtifacts']: package.pop(attr, None) expected_result = [package] self.assertEqual(expected_result, body) def test_index_attribute_selector_all_fields(self): """Test for attribute selector 'all_fields' We intentionally use attribute filter along with attribute selector. It is because when these tests run with concurrency > 1, there will be multiple sample packages present at a time. Hence attribute selector will be applied on all of them. It will be difficult to predict the expected result. Hence we are limiting the result set by filtering one of the vnf package which was created for this speific test. """ filter_expr = {'filter': '(eq,id,%s)' % self.package_id1, 'all_fields': ''} filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) resp, body = self.http_client.do_request(filter_url, "GET") expected_result = [self.package1] self.assertEqual(expected_result, body) def test_index_attribute_selector_exclude_default(self): filter_expr = {'filter': '(eq,id,%s)' % self.package_id2, 'exclude_default': ''} filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) resp, body = self.http_client.do_request(filter_url, "GET") package2 = deepcopy(self.package2) for attr in ['softwareImages', 'checksum', 'userDefinedData', 'additionalArtifacts']: package2.pop(attr, None) expected_result = [package2] self.assertEqual(expected_result, body) def test_index_attribute_selector_exclude_fields(self): filter_expr = { 'filter': '(eq,id,%s)' % self.package_id2, 'exclude_fields': 'checksum,softwareImages/checksum,' 'additionalArtifacts/checksum'} filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) resp, body = self.http_client.do_request(filter_url, "GET") package2 = deepcopy(self.package2) for software_image in package2['softwareImages']: software_image.pop('checksum', None) for artifact in package2['additionalArtifacts']: artifact.pop('checksum', None) package2.pop('checksum', None) expected_result = [package2] self.assertEqual(expected_result, body) def test_index_attribute_selector_fields(self): filter_expr = {'filter': '(eq,id,%s)' % self.package_id1, 'fields': 'softwareImages/checksum/hash,' 'softwareImages/containerFormat,softwareImages/name,' 'userDefinedData,additionalArtifacts/checksum/hash,' 'additionalArtifacts/artifactPath'} filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) resp, body = self.http_client.do_request(filter_url, "GET") package1 = deepcopy(self.package1) # Prepare expected result for software_image in package1['softwareImages']: software_image['checksum'].pop('algorithm', None) for attr in ['createdAt', 'diskFormat', 'id', 'imagePath', 'minDisk', 'minRam', 'provider', 'size', 'userMetadata', 'version']: software_image.pop(attr, None) for artifact in package1['additionalArtifacts']: artifact['checksum'].pop('algorithm', None) artifact.pop('metadata', None) package1.pop('checksum', None) expected_result = [package1] self.assertEqual(expected_result, body) def test_get_vnfd_from_onboarded_vnf_package_for_content_type_zip(self): resp, resp_body = self.http_client.do_request( '{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) 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.assertCountEqual(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_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( '{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) 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( str(self.SIZE_CSAR_ZIP), 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-{}'.format(self.SIZE_CSAR_ZIP - 1) 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(self.SIZE_CSAR_ZIP, total_size) zip_file_path.close() def test_fetch_vnf_package_artifacts(self): # run download api response1 = self.http_client.do_request( '{base_path}/{id}/artifacts/{artifact_path}'.format( base_path=self.base_url, id=self.package_id1, artifact_path='Scripts/install.sh'), "GET", body={}, headers={}) response2 = self.http_client.do_request( '{base_path}/{id}/artifacts/{artifact_path}'.format( base_path=self.base_url, id=self.package_id2, artifact_path='Scripts/install.sh'), "GET", body={}, headers={}) response3 = self.http_client.do_request( '{base_path}/{id}/artifacts/{artifact_path}'.format( base_path=self.base_url, id=self.package_id3, artifact_path='Scripts/install.sh'), "GET", body={}, headers={}) # verification self.assertEqual(200, response1[0].status_code) self.assertEqual('33', response1[0].headers['Content-Length']) self.assertIsNotNone(response1[1]) self.assertEqual(200, response2[0].status_code) self.assertEqual('33', response2[0].headers['Content-Length']) self.assertIsNotNone(response2[1]) self.assertEqual(200, response3[0].status_code) self.assertEqual('33', response3[0].headers['Content-Length']) self.assertIsNotNone(response3[1]) def test_fetch_vnf_package_artifacts_partial_download_using_range(self): # get range range_ = 'bytes=3-8' # get headers headers = {'Range': range_} # request download api response = self.http_client.do_request( '{base_path}/{id}/artifacts/{artifact_path}'.format( base_path=self.base_url, id=self.package_id1, artifact_path='Scripts/install.sh'), "GET", body={}, headers=headers) # verification self.assertEqual(206, response[0].status_code) self.assertEqual('6', response[0].headers['Content-Length']) self.assertIsNotNone(response[1]) def test_vnf_package_software_image_path(self): filter_expr = { 'filter': '(eq,id,%s)' % self.package_id2, 'exclude_fields': 'checksum,softwareImages/checksum,' 'additionalArtifacts/checksum'} filter_url = self.base_url + "?" + urllib.parse.urlencode(filter_expr) resp, body = self.http_client.do_request(filter_url, "GET") image_path = body[0]['softwareImages'][0]['imagePath'] expected_result = self.package2.get('softwareImages')[0]['imagePath'] self.assertEqual(image_path, expected_result) def test_fetch_vnf_package_artifacts_range_exception(self): # get range range_ = 'bytes=9-8' # get headers headers = {'Range': range_} # request download api response = self.http_client.do_request( '{base_path}/{id}/artifacts/{artifact_path}'.format( base_path=self.base_url, id=self.package_id1, artifact_path='Scripts/install.sh'), "GET", body={}, headers=headers) # verification self.assertEqual(416, response[0].status_code) # get range range_ = 'bytes=0-33' # get headers headers = {'Range': range_} # request download api response = self.http_client.do_request( '{base_path}/{id}/artifacts/{artifact_path}'.format( base_path=self.base_url, id=self.package_id1, artifact_path='Scripts/install.sh'), "GET", body={}, headers=headers) # verification self.assertEqual(416, response[0].status_code) # get range range_ = 'bytes=33-' # get headers headers = {'Range': range_} # request download api response = self.http_client.do_request( '{base_path}/{id}/artifacts/{artifact_path}'.format( base_path=self.base_url, id=self.package_id1, artifact_path='Scripts/install.sh'), "GET", body={}, headers=headers) # verification self.assertEqual(416, response[0].status_code)