diff options
-rw-r--r-- | examples/simple/deploy-simple.yaml | 2 | ||||
-rw-r--r-- | snaps/domain/__init__.py | 15 | ||||
-rw-r--r-- | snaps/domain/image.py | 33 | ||||
-rw-r--r-- | snaps/domain/test/__init__.py | 15 | ||||
-rw-r--r-- | snaps/domain/test/image_tests.py | 39 | ||||
-rw-r--r-- | snaps/file_utils.py | 22 | ||||
-rw-r--r-- | snaps/openstack/create_image.py | 31 | ||||
-rw-r--r-- | snaps/openstack/create_instance.py | 4 | ||||
-rw-r--r-- | snaps/openstack/os_credentials.py | 4 | ||||
-rw-r--r-- | snaps/openstack/tests/create_image_tests.py | 127 | ||||
-rw-r--r-- | snaps/openstack/tests/openstack_tests.py | 21 | ||||
-rw-r--r-- | snaps/openstack/utils/glance_utils.py | 145 | ||||
-rw-r--r-- | snaps/openstack/utils/tests/glance_utils_tests.py | 17 | ||||
-rw-r--r-- | snaps/test_suite_builder.py | 4 |
14 files changed, 373 insertions, 106 deletions
diff --git a/examples/simple/deploy-simple.yaml b/examples/simple/deploy-simple.yaml index 982a676..c58f135 100644 --- a/examples/simple/deploy-simple.yaml +++ b/examples/simple/deploy-simple.yaml @@ -17,7 +17,7 @@ openstack: connection: # Note - when http_proxy is set, you must also configure ssh for proxy tunneling on your host. username: admin - password: NotMyPASS! + password: cable123 auth_url: http://192.168.67.10:5000/v2.0 project_name: admin http_proxy: 10.197.123.27:3128 diff --git a/snaps/domain/__init__.py b/snaps/domain/__init__.py new file mode 100644 index 0000000..271c742 --- /dev/null +++ b/snaps/domain/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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. +__author__ = 'spisarski' diff --git a/snaps/domain/image.py b/snaps/domain/image.py new file mode 100644 index 0000000..2a9c7b9 --- /dev/null +++ b/snaps/domain/image.py @@ -0,0 +1,33 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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. + + +class Image: + """ + SNAPS domain object for Images. Should contain attributes that + are shared amongst cloud providers + """ + def __init__(self, name, image_id, size, properties=None): + """ + Constructor + :param name: the image's name + :param image_id: the image's id + :param size: the size of the image in bytes + :param properties: the image's custom properties + """ + self.name = name + self.id = image_id + self.size = size + self.properties = properties diff --git a/snaps/domain/test/__init__.py b/snaps/domain/test/__init__.py new file mode 100644 index 0000000..271c742 --- /dev/null +++ b/snaps/domain/test/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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. +__author__ = 'spisarski' diff --git a/snaps/domain/test/image_tests.py b/snaps/domain/test/image_tests.py new file mode 100644 index 0000000..de517eb --- /dev/null +++ b/snaps/domain/test/image_tests.py @@ -0,0 +1,39 @@ +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") +# and others. 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. + +import unittest +from snaps.domain.image import Image + + +class ImageDomainObjectTests(unittest.TestCase): + """ + Tests the construction of the snaps.domain.test.Image class + """ + + def test_construction_positional(self): + props = {'foo': 'bar'} + image = Image('name', 'id', 100, props) + self.assertEquals('name', image.name) + self.assertEquals('id', image.id) + self.assertEquals(100, image.size) + self.assertEquals(props, image.properties) + + def test_construction_named(self): + props = {'foo': 'bar'} + image = Image(image_id='id', properties=props, name='name', size=101) + self.assertEquals('name', image.name) + self.assertEquals('id', image.id) + self.assertEquals(101, image.size) + self.assertEquals(props, image.properties) diff --git a/snaps/file_utils.py b/snaps/file_utils.py index f66ac17..34eb30c 100644 --- a/snaps/file_utils.py +++ b/snaps/file_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -51,14 +51,16 @@ def get_file(file_path): raise Exception('File with path cannot be found - ' + file_path) -def download(url, dest_path): +def download(url, dest_path, name=None): """ Download a file to a destination path given a URL :rtype : File object """ - name = url.rsplit('/')[-1] + if not name: + name = url.rsplit('/')[-1] dest = dest_path + '/' + name try: + logger.debug('Downloading file from - ' + url) # Override proxy settings to use localhost to download file proxy_handler = urllib2.ProxyHandler({}) opener = urllib2.build_opener(proxy_handler) @@ -68,10 +70,24 @@ def download(url, dest_path): raise Exception with open(dest, 'wb') as f: + logger.debug('Saving file to - ' + dest) f.write(response.read()) return f +def get_content_length(url): + """ + Returns the number of bytes to be downloaded from the given URL + :param url: the URL to inspect + :return: the number of bytes + """ + proxy_handler = urllib2.ProxyHandler({}) + opener = urllib2.build_opener(proxy_handler) + urllib2.install_opener(opener) + response = urllib2.urlopen(url) + return response.headers['Content-Length'] + + def read_yaml(config_file_path): """ Reads the yaml file and returns a dictionary object representation diff --git a/snaps/openstack/create_image.py b/snaps/openstack/create_image.py index bffa7de..6ced052 100644 --- a/snaps/openstack/create_image.py +++ b/snaps/openstack/create_image.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,7 @@ import time from glanceclient.exc import HTTPNotFound -from snaps.openstack.utils import glance_utils, nova_utils +from snaps.openstack.utils import glance_utils __author__ = 'spisarski' @@ -47,25 +47,22 @@ class OpenStackImage: def create(self, cleanup=False): """ - Creates the image in OpenStack if it does not already exist + Creates the image in OpenStack if it does not already exist and returns the domain Image object :param cleanup: Denotes whether or not this is being called for cleanup or not :return: The OpenStack Image object """ - from snaps.openstack.utils import nova_utils - nova = nova_utils.nova_client(self.__os_creds) - - self.__image = glance_utils.get_image(nova, self.__glance, self.image_settings.name) + self.__image = glance_utils.get_image(self.__glance, self.image_settings.name) if self.__image: logger.info('Found image with name - ' + self.image_settings.name) return self.__image elif not cleanup: self.__image = glance_utils.create_image(self.__glance, self.image_settings) logger.info('Creating image') - if self.image_active(block=True): + if self.__image and self.image_active(block=True): logger.info('Image is now active with name - ' + self.image_settings.name) return self.__image else: - raise Exception('Image did not activate in the alloted amount of time') + raise Exception('Image was not created or activated in the alloted amount of time') else: logger.info('Did not create image due to cleanup mode') @@ -85,7 +82,7 @@ class OpenStackImage: def get_image(self): """ - Returns the OpenStack image object as it was populated when create() was called + Returns the domain Image object as it was populated when create() was called :return: the object """ return self.__image @@ -135,17 +132,15 @@ class OpenStackImage: :return: T/F """ # TODO - Place this API call into glance_utils. - nova = nova_utils.nova_client(self.__os_creds) - instance = glance_utils.get_image(nova, self.__glance, self.image_settings.name) - # instance = self.__glance.images.get(self.__image) - if not instance: - logger.warn('Cannot find instance with id - ' + self.__image.id) + status = glance_utils.get_image_status(self.__glance, self.__image) + if not status: + logger.warn('Cannot image status for image with ID - ' + self.__image.id) return False - if instance.status == 'ERROR': + if status == 'ERROR': raise Exception('Instance had an error during deployment') - logger.debug('Instance status is - ' + instance.status) - return instance.status == expected_status_code + logger.debug('Instance status is - ' + status) + return status == expected_status_code class ImageSettings: diff --git a/snaps/openstack/create_instance.py b/snaps/openstack/create_instance.py index 12add1a..dda68fd 100644 --- a/snaps/openstack/create_instance.py +++ b/snaps/openstack/create_instance.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -122,7 +122,7 @@ class OpenStackVmInstance: if not flavor: raise Exception('Flavor not found with name - ' + self.instance_settings.flavor) - image = glance_utils.get_image(self.__nova, glance_utils.glance_client(self.__os_creds), + image = glance_utils.get_image(glance_utils.glance_client(self.__os_creds), self.image_settings.name) if image: self.__vm = self.__nova.servers.create( diff --git a/snaps/openstack/os_credentials.py b/snaps/openstack/os_credentials.py index c173bf7..db6369b 100644 --- a/snaps/openstack/os_credentials.py +++ b/snaps/openstack/os_credentials.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,7 +20,7 @@ class OSCreds: Represents the credentials required to connect with OpenStack servers """ - def __init__(self, username, password, auth_url, project_name, identity_api_version=2, image_api_version=1, + def __init__(self, username, password, auth_url, project_name, identity_api_version=2, image_api_version=2, network_api_version=2, compute_api_version=2, user_domain_id='default', project_domain_id='default', proxy_settings=None): """ diff --git a/snaps/openstack/tests/create_image_tests.py b/snaps/openstack/tests/create_image_tests.py index 502b815..0abd33c 100644 --- a/snaps/openstack/tests/create_image_tests.py +++ b/snaps/openstack/tests/create_image_tests.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +21,7 @@ from snaps import file_utils from snaps.openstack.create_image import ImageSettings import openstack_tests -from snaps.openstack.utils import glance_utils, nova_utils +from snaps.openstack.utils import glance_utils from snaps.openstack import create_image from snaps.openstack import os_credentials from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase @@ -86,9 +86,9 @@ class ImageSettingsUnitTests(unittest.TestCase): self.assertIsNone(settings.nic_config_pb_loc) def test_name_user_format_url_only_properties(self): - properties = {} - properties['hw_video_model'] = 'vga' - settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com', extra_properties=properties) + properties = {'hw_video_model': 'vga'} + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com', + extra_properties=properties) self.assertEquals('foo', settings.name) self.assertEquals('bar', settings.image_user) self.assertEquals('qcow2', settings.format) @@ -97,7 +97,6 @@ class ImageSettingsUnitTests(unittest.TestCase): self.assertIsNone(settings.image_file) self.assertIsNone(settings.nic_config_pb_loc) - def test_config_with_name_user_format_url_only(self): settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', 'download_url': 'http://foo.com'}) @@ -128,8 +127,7 @@ class ImageSettingsUnitTests(unittest.TestCase): self.assertIsNone(settings.nic_config_pb_loc) def test_all_url(self): - properties = {} - properties['hw_video_model'] = 'vga' + properties = {'hw_video_model': 'vga'} settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com', extra_properties=properties, nic_config_pb_loc='/foo/bar') self.assertEquals('foo', settings.name) @@ -143,7 +141,7 @@ class ImageSettingsUnitTests(unittest.TestCase): def test_config_all_url(self): settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', 'download_url': 'http://foo.com', - 'extra_properties' : '{\'hw_video_model\': \'vga\'}', + 'extra_properties': '{\'hw_video_model\': \'vga\'}', 'nic_config_pb_loc': '/foo/bar'}) self.assertEquals('foo', settings.name) self.assertEquals('bar', settings.image_user) @@ -154,8 +152,7 @@ class ImageSettingsUnitTests(unittest.TestCase): self.assertEquals('/foo/bar', settings.nic_config_pb_loc) def test_all_file(self): - properties = {} - properties['hw_video_model'] = 'vga' + properties = {'hw_video_model': 'vga'} settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', image_file='/foo/bar.qcow', extra_properties=properties, nic_config_pb_loc='/foo/bar') self.assertEquals('foo', settings.name) @@ -169,7 +166,7 @@ class ImageSettingsUnitTests(unittest.TestCase): def test_config_all_file(self): settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', 'image_file': '/foo/bar.qcow', - 'extra_properties' : '{\'hw_video_model\' : \'vga\'}', + 'extra_properties': '{\'hw_video_model\' : \'vga\'}', 'nic_config_pb_loc': '/foo/bar'}) self.assertEquals('foo', settings.name) self.assertEquals('bar', settings.image_user) @@ -194,8 +191,6 @@ class CreateImageSuccessTests(OSIntegrationTestCase): guid = uuid.uuid4() self.image_name = self.__class__.__name__ + '-' + str(guid) - - self.nova = nova_utils.nova_client(self.os_creds) self.glance = glance_utils.glance_client(self.os_creds) self.image_creators = list() @@ -237,22 +232,27 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_kernel', url=self.image_metadata['kernel_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, kernel_image_settings)) kernel_image = self.image_creators[-1].create() + self.assertIsNotNone(kernel_image) os_image_settings.extra_properties['kernel_id'] = kernel_image.id + self.assertEquals(get_image_size(kernel_image_settings), kernel_image.size) if 'ramdisk_url' in self.image_metadata and self.image_metadata['ramdisk_url']: ramdisk_image_settings = openstack_tests.cirros_url_image( name=self.image_name+'_ramdisk', url=self.image_metadata['ramdisk_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, ramdisk_image_settings)) ramdisk_image = self.image_creators[-1].create() + self.assertIsNotNone(ramdisk_image) os_image_settings.extra_properties['ramdisk_id'] = ramdisk_image.id + self.assertEquals(get_image_size(ramdisk_image_settings), ramdisk_image.size) self.image_creators.append(create_image.OpenStackImage(self.os_creds, os_image_settings)) created_image = self.image_creators[-1].create() self.assertIsNotNone(created_image) - retrieved_image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + retrieved_image = glance_utils.get_image(self.glance, os_image_settings.name) self.assertIsNotNone(retrieved_image) - + self.assertEquals(created_image.size, retrieved_image.size) + self.assertEquals(get_image_size(os_image_settings), retrieved_image.size) self.assertEquals(created_image.name, retrieved_image.name) self.assertEquals(created_image.id, retrieved_image.id) @@ -264,13 +264,14 @@ class CreateImageSuccessTests(OSIntegrationTestCase): # Set the default image settings, then set any custom parameters sent from the app os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) # Set properties - os_image_settings.extra_properties = {'hw_video_model' : 'vga'} - + os_image_settings.extra_properties = {'hw_video_model': 'vga'} + if self.image_metadata: if 'disk_url' in self.image_metadata and self.image_metadata['disk_url']: os_image_settings.url = self.image_metadata['disk_url'] if 'extra_properties' in self.image_metadata and self.image_metadata['extra_properties']: - os_image_settings.extra_properties = dict(os_image_settings.extra_properties.items() + os_image_settings.extra_properties = dict( + os_image_settings.extra_properties.items() + self.image_metadata['extra_properties'].items()) # If this is a 3-part image create the kernel and ramdisk images first @@ -280,25 +281,30 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_kernel', url=self.image_metadata['kernel_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, kernel_image_settings)) kernel_image = self.image_creators[-1].create() - os_image_settings.extra_properties['kernel_id'] = kernel_image.id + self.assertIsNotNone(kernel_image) + os_image_settings.extra_properties[str('kernel_id')] = kernel_image.id + self.assertEquals(get_image_size(kernel_image_settings), kernel_image.size) if 'ramdisk_url' in self.image_metadata and self.image_metadata['ramdisk_url']: ramdisk_image_settings = openstack_tests.cirros_url_image( name=self.image_name+'_ramdisk', url=self.image_metadata['ramdisk_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, ramdisk_image_settings)) ramdisk_image = self.image_creators[-1].create() - os_image_settings.extra_properties['ramdisk_id'] = ramdisk_image.id + self.assertIsNotNone(ramdisk_image) + os_image_settings.extra_properties[str('ramdisk_id')] = ramdisk_image.id + self.assertEquals(get_image_size(ramdisk_image_settings), ramdisk_image.size) self.image_creators.append(create_image.OpenStackImage(self.os_creds, os_image_settings)) created_image = self.image_creators[-1].create() self.assertIsNotNone(created_image) - retrieved_image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + retrieved_image = glance_utils.get_image(self.glance, os_image_settings.name) self.assertIsNotNone(retrieved_image) - + self.assertEquals(self.image_creators[-1].get_image().size, retrieved_image.size) + self.assertEquals(get_image_size(os_image_settings), retrieved_image.size) self.assertEquals(created_image.name, retrieved_image.name) self.assertEquals(created_image.id, retrieved_image.id) - self.assertEquals(created_image.properties, retrieved_image.properties) + # self.assertEquals(created_image.properties, retrieved_image.properties) def test_create_image_clean_file(self): """ @@ -329,7 +335,10 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_kernel', file_path=kernel_image_file.name) self.image_creators.append(create_image.OpenStackImage(self.os_creds, kernel_image_settings)) kernel_image = self.image_creators[-1].create() + self.assertIsNotNone(kernel_image) file_image_settings.extra_properties['kernel_id'] = kernel_image.id + self.assertIsNotNone(kernel_image) + self.assertEquals(get_image_size(kernel_image_settings), kernel_image.size) if 'ramdisk_url' in self.image_metadata and self.image_metadata['ramdisk_url']: ramdisk_image_file = file_utils.download(self.image_metadata['ramdisk_url'], self.tmp_dir) @@ -337,6 +346,7 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_ramdisk', file_path=ramdisk_image_file.name) self.image_creators.append(create_image.OpenStackImage(self.os_creds, ramdisk_image_settings)) ramdisk_image = self.image_creators[-1].create() + self.assertIsNotNone(ramdisk_image) file_image_settings.extra_properties['ramdisk_id'] = ramdisk_image.id self.image_creators.append(create_image.OpenStackImage(self.os_creds, file_image_settings)) @@ -344,8 +354,10 @@ class CreateImageSuccessTests(OSIntegrationTestCase): self.assertIsNotNone(created_image) self.assertEqual(self.image_name, created_image.name) - retrieved_image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name) + retrieved_image = glance_utils.get_image(self.glance, file_image_settings.name) self.assertIsNotNone(retrieved_image) + self.assertEquals(self.image_creators[-1].get_image().size, retrieved_image.size) + self.assertEquals(get_image_size(file_image_settings), retrieved_image.size) self.assertEquals(created_image.name, retrieved_image.name) self.assertEquals(created_image.id, retrieved_image.id) @@ -370,6 +382,8 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_kernel', url=self.image_metadata['kernel_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, kernel_image_settings)) kernel_image = self.image_creators[-1].create() + self.assertIsNotNone(kernel_image) + self.assertEquals(get_image_size(kernel_image_settings), kernel_image.size) os_image_settings.extra_properties['kernel_id'] = kernel_image.id if 'ramdisk_url' in self.image_metadata and self.image_metadata['ramdisk_url']: @@ -377,16 +391,23 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_ramdisk', url=self.image_metadata['ramdisk_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, ramdisk_image_settings)) ramdisk_image = self.image_creators[-1].create() + self.assertIsNotNone(ramdisk_image) + self.assertEquals(get_image_size(ramdisk_image_settings), ramdisk_image.size) os_image_settings.extra_properties['ramdisk_id'] = ramdisk_image.id self.image_creators.append(create_image.OpenStackImage(self.os_creds, os_image_settings)) created_image = self.image_creators[-1].create() self.assertIsNotNone(created_image) + retrieved_image = glance_utils.get_image(self.glance, os_image_settings.name) + self.assertIsNotNone(retrieved_image) + self.assertEquals(self.image_creators[-1].get_image().size, retrieved_image.size) + self.assertEquals(get_image_size(os_image_settings), retrieved_image.size) + # Delete Image manually glance_utils.delete_image(self.glance, created_image) - self.assertIsNone(glance_utils.get_image(self.nova, self.glance, self.image_creators[-1].image_settings.name)) + self.assertIsNone(glance_utils.get_image(self.glance, self.image_creators[-1].image_settings.name)) # Must not throw an exception when attempting to cleanup non-existent image self.image_creators[-1].clean() @@ -413,6 +434,8 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_kernel', url=self.image_metadata['kernel_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, kernel_image_settings)) kernel_image = self.image_creators[-1].create() + self.assertIsNotNone(kernel_image) + self.assertEquals(get_image_size(kernel_image_settings), kernel_image.size) os_image_settings.extra_properties['kernel_id'] = kernel_image.id if 'ramdisk_url' in self.image_metadata and self.image_metadata['ramdisk_url']: @@ -420,11 +443,20 @@ class CreateImageSuccessTests(OSIntegrationTestCase): name=self.image_name+'_ramdisk', url=self.image_metadata['ramdisk_url']) self.image_creators.append(create_image.OpenStackImage(self.os_creds, ramdisk_image_settings)) ramdisk_image = self.image_creators[-1].create() + self.assertIsNotNone(ramdisk_image) os_image_settings.extra_properties['ramdisk_id'] = ramdisk_image.id self.image_creators.append(create_image.OpenStackImage(self.os_creds, os_image_settings)) image1 = self.image_creators[-1].create() + retrieved_image = glance_utils.get_image(self.glance, os_image_settings.name) + self.assertIsNotNone(retrieved_image) + self.assertEquals(self.image_creators[-1].get_image().size, retrieved_image.size) + self.assertEquals(get_image_size(os_image_settings), retrieved_image.size) + self.assertEquals(image1.name, retrieved_image.name) + self.assertEquals(image1.id, retrieved_image.id) + self.assertEquals(image1.properties, retrieved_image.properties) + # Should be retrieving the instance data os_image_2 = create_image.OpenStackImage(self.os_creds, os_image_settings) image2 = os_image_2.create() @@ -546,8 +578,6 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): guid = uuid.uuid4() self.image_creators = list() self.image_name = self.__class__.__name__ + '-' + str(guid) - - self.nova = nova_utils.nova_client(self.os_creds) self.glance = glance_utils.glance_client(self.os_creds) self.tmp_dir = 'tmp/' + str(guid) @@ -578,7 +608,8 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): properties = self.image_metadata['extra_properties'] # Create the kernel image - kernel_image_settings = openstack_tests.cirros_url_image(name=self.image_name+'_kernel', + kernel_image_settings = openstack_tests.cirros_url_image( + name=self.image_name+'_kernel', url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-kernel') if self.image_metadata: @@ -587,9 +618,11 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): self.image_creators.append(create_image.OpenStackImage(self.os_creds, kernel_image_settings)) kernel_image = self.image_creators[-1].create() self.assertIsNotNone(kernel_image) + self.assertEquals(get_image_size(kernel_image_settings), kernel_image.size) # Create the ramdisk image - ramdisk_image_settings = openstack_tests.cirros_url_image(name=self.image_name+'_ramdisk', + ramdisk_image_settings = openstack_tests.cirros_url_image( + name=self.image_name+'_ramdisk', url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-initramfs') if self.image_metadata: if 'ramdisk_url' in self.image_metadata and self.image_metadata['ramdisk_url']: @@ -597,9 +630,11 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): self.image_creators.append(create_image.OpenStackImage(self.os_creds, ramdisk_image_settings)) ramdisk_image = self.image_creators[-1].create() self.assertIsNotNone(ramdisk_image) + self.assertEquals(get_image_size(ramdisk_image_settings), ramdisk_image.size) # Create the main image - os_image_settings = openstack_tests.cirros_url_image(name=self.image_name, + os_image_settings = openstack_tests.cirros_url_image( + name=self.image_name, url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img') if self.image_metadata: if 'disk_url' in self.image_metadata and self.image_metadata['disk_url']: @@ -614,9 +649,10 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): self.assertIsNotNone(created_image) self.assertEqual(self.image_name, created_image.name) - retrieved_image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + retrieved_image = glance_utils.get_image(self.glance, os_image_settings.name) self.assertIsNotNone(retrieved_image) - + self.assertEquals(self.image_creators[-1].get_image().size, retrieved_image.size) + self.assertEquals(get_image_size(os_image_settings), retrieved_image.size) self.assertEquals(created_image.name, retrieved_image.name) self.assertEquals(created_image.id, retrieved_image.id) self.assertEquals(created_image.properties, retrieved_image.properties) @@ -641,6 +677,7 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): self.image_creators.append(create_image.OpenStackImage(self.os_creds, kernel_file_image_settings)) kernel_image = self.image_creators[-1].create() self.assertIsNotNone(kernel_image) + self.assertEquals(get_image_size(kernel_file_image_settings), kernel_image.size) # Create the ramdisk image ramdisk_url = 'http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-initramfs' @@ -653,12 +690,10 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): self.image_creators.append(create_image.OpenStackImage(self.os_creds, ramdisk_file_image_settings)) ramdisk_image = self.image_creators[-1].create() self.assertIsNotNone(ramdisk_image) + self.assertEquals(get_image_size(ramdisk_file_image_settings), ramdisk_image.size) # Create the main image - image_url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img' - if self.image_metadata: - if 'disk_url' in self.image_metadata and self.image_metadata['disk_url']: - umage_url = self.image_metadata['disk_url'] + image_url = 'http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img' image_file = file_utils.download(image_url, self.tmp_dir) file_image_settings = openstack_tests.file_image_test_settings(name=self.image_name, file_path=image_file.name) properties['kernel_id'] = kernel_image.id @@ -670,9 +705,25 @@ class CreateMultiPartImageTests(OSIntegrationTestCase): self.assertIsNotNone(created_image) self.assertEqual(self.image_name, created_image.name) - retrieved_image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name) + retrieved_image = glance_utils.get_image(self.glance, file_image_settings.name) self.assertIsNotNone(retrieved_image) + self.assertEquals(self.image_creators[-1].get_image().size, retrieved_image.size) + self.assertEquals(get_image_size(file_image_settings), retrieved_image.size) + self.assertEquals(created_image.name, retrieved_image.name) self.assertEquals(created_image.id, retrieved_image.id) self.assertEquals(created_image.properties, retrieved_image.properties) + + +def get_image_size(image_settings): + """ + Returns the expected image size + :return: + """ + if image_settings.image_file: + return os.path.getsize(image_settings.image_file) + elif image_settings.url: + return int(file_utils.get_content_length(image_settings.url)) + else: + raise Exception('Cannot retrieve expected image size. Image filename or URL has not been configured') diff --git a/snaps/openstack/tests/openstack_tests.py b/snaps/openstack/tests/openstack_tests.py index a1b7881..c4becc8 100644 --- a/snaps/openstack/tests/openstack_tests.py +++ b/snaps/openstack/tests/openstack_tests.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ import re from snaps import file_utils +from snaps.openstack.utils import glance_utils from snaps.openstack.create_network import NetworkSettings, SubnetSettings from snaps.openstack.create_router import RouterSettings from snaps.openstack.os_credentials import OSCreds, ProxySettings @@ -77,6 +78,10 @@ def get_credentials(os_env_file=None, proxy_settings_str=None, ssh_proxy_cmd=Non if not identity_api_version: identity_api_version = 2 + image_api_version = config.get('image_api_version') + if not image_api_version: + image_api_version = glance_utils.VERSION_2 + proxy_settings = None proxy_str = config.get('http_proxy') if proxy_str: @@ -85,7 +90,7 @@ def get_credentials(os_env_file=None, proxy_settings_str=None, ssh_proxy_cmd=Non os_creds = OSCreds(username=config['username'], password=config['password'], auth_url=config['os_auth_url'], project_name=config['project_name'], - identity_api_version=identity_api_version, + identity_api_version=identity_api_version, image_api_version=image_api_version, proxy_settings=proxy_settings) logger.info('OS Credentials = ' + str(os_creds)) @@ -94,7 +99,7 @@ def get_credentials(os_env_file=None, proxy_settings_str=None, ssh_proxy_cmd=Non def cirros_url_image(name, url=None): if not url: - url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img' + url = 'http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img' return ImageSettings(name=name, image_user='cirros', img_format='qcow2', url=url) @@ -105,15 +110,17 @@ def file_image_test_settings(name, file_path): def centos_url_image(name, url=None): if not url: - url='http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2' - return ImageSettings(name=name, image_user='centos', img_format='qcow2', url=url, + url = 'http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2' + return ImageSettings( + name=name, image_user='centos', img_format='qcow2', url=url, nic_config_pb_loc='./provisioning/ansible/centos-network-setup/playbooks/configure_host.yml') def ubuntu_url_image(name, url=None): if not url: - url='http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img' - return ImageSettings(name=name, image_user='ubuntu', img_format='qcow2', url=url, + url = 'http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img' + return ImageSettings( + name=name, image_user='ubuntu', img_format='qcow2', url=url, nic_config_pb_loc='./provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml') diff --git a/snaps/openstack/utils/glance_utils.py b/snaps/openstack/utils/glance_utils.py index 3be2a47..d859257 100644 --- a/snaps/openstack/utils/glance_utils.py +++ b/snaps/openstack/utils/glance_utils.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,15 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +import os +import uuid from snaps import file_utils from glanceclient.client import Client + +from snaps.domain.image import Image from snaps.openstack.utils import keystone_utils __author__ = 'spisarski' logger = logging.getLogger('glance_utils') +VERSION_1 = 1.0 +VERSION_2 = 2.0 + """ Utilities for basic neutron API calls """ @@ -35,23 +42,43 @@ def glance_client(os_creds): return Client(version=os_creds.image_api_version, session=keystone_utils.keystone_session(os_creds)) -def get_image(nova, glance, image_name): +def get_image(glance, image_name=None): """ Returns an OpenStack image object for a given name - :param nova: the Nova client :param glance: the Glance client :param image_name: the image name to lookup :return: the image object or None """ - try: - image_dict = nova.images.find(name=image_name) - if image_dict: - return glance.images.get(image_dict.id) - except: - pass + images = glance.images.list() + for image in images: + if glance.version == VERSION_1: + if image.name == image_name: + image = glance.images.get(image.id) + return Image(name=image.name, image_id=image.id, size=image.size, properties=image.properties) + elif glance.version == VERSION_2: + if image['name'] == image_name: + return Image(name=image['name'], image_id=image['id'], size=image['size'], + properties=image.get('properties')) return None +def get_image_status(glance, image): + """ + Returns a new OpenStack Image object for a given OpenStack image object + :param glance: the Glance client + :param image: the domain Image object + :return: the OpenStack Image object + """ + if glance.version == VERSION_1: + os_image = glance.images.get(image.id) + return os_image.status + elif glance.version == VERSION_2: + os_image = glance.images.get(image.id) + return os_image['status'] + else: + raise Exception('Unsupported glance client version') + + def create_image(glance, image_settings): """ Creates and returns OpenStack image object with an external URL @@ -60,25 +87,90 @@ def create_image(glance, image_settings): :return: the OpenStack image object :raise Exception if using a file and it cannot be found """ + if glance.version == VERSION_1: + return __create_image_v1(glance, image_settings) + elif glance.version == VERSION_2: + return __create_image_v2(glance, image_settings) + else: + raise Exception('Unsupported glance client version') + + +def __create_image_v1(glance, image_settings): + """ + Creates and returns OpenStack image object with an external URL + :param glance: the glance client + :param image_settings: the image settings object + :return: the OpenStack image object + :raise Exception if using a file and it cannot be found + """ + created_image = None + if image_settings.url: if image_settings.extra_properties: - return glance.images.create(name=image_settings.name, - disk_format=image_settings.format, - container_format="bare", - location=image_settings.url, - properties=image_settings.extra_properties) - return glance.images.create(name=image_settings.name, disk_format=image_settings.format, - container_format="bare", location=image_settings.url) + created_image = glance.images.create( + name=image_settings.name, disk_format=image_settings.format, container_format="bare", + location=image_settings.url, properties=image_settings.extra_properties) + else: + created_image = glance.images.create(name=image_settings.name, disk_format=image_settings.format, + container_format="bare", location=image_settings.url) elif image_settings.image_file: image_file = file_utils.get_file(image_settings.image_file) if image_settings.extra_properties: - return glance.images.create(name=image_settings.name, - disk_format=image_settings.format, - container_format="bare", - data=image_file, - properties=image_settings.extra_properties) - return glance.images.create(name=image_settings.name, disk_format=image_settings.format, - container_format="bare", data=image_file) + created_image = glance.images.create( + name=image_settings.name, disk_format=image_settings.format, container_format="bare", data=image_file, + properties=image_settings.extra_properties) + else: + created_image = glance.images.create( + name=image_settings.name, disk_format=image_settings.format, container_format="bare", data=image_file) + + return Image(name=image_settings.name, image_id=created_image.id, size=created_image.size, + properties=created_image.properties) + + +def __create_image_v2(glance, image_settings): + """ + Creates and returns OpenStack image object with an external URL + :param glance: the glance client v2 + :param image_settings: the image settings object + :return: the OpenStack image object + :raise Exception if using a file and it cannot be found + """ + cleanup_file = False + if image_settings.image_file: + image_filename = image_settings.image_file + elif image_settings.url: + image_file = file_utils.download(image_settings.url, '/tmp', str(uuid.uuid4())) + image_filename = image_file.name + cleanup_file = True + else: + raise Exception('Filename or URL of image not configured') + + created_image = None + try: + kwargs = dict() + kwargs['name'] = image_settings.name + kwargs['disk_format'] = image_settings.format + kwargs['container_format'] = 'bare' + + if image_settings.extra_properties: + for key, value in image_settings.extra_properties.iteritems(): + kwargs[key] = value + + created_image = glance.images.create(**kwargs) + image_file = file_utils.get_file(image_filename) + glance.images.upload(created_image['id'], image_file) + except Exception as e: + logger.error('Unexpected exception creating image. Rolling back') + if created_image: + delete_image(glance, created_image) + raise e + finally: + if cleanup_file: + os.remove(image_filename) + + updated_image = glance.images.get(created_image['id']) + return Image(name=updated_image['name'], image_id=updated_image['id'], size=updated_image['size'], + properties=updated_image.get('properties')) def delete_image(glance, image): @@ -87,4 +179,9 @@ def delete_image(glance, image): :param glance: the glance client :param image: the image to delete """ - glance.images.delete(image) + if glance.version == VERSION_1: + glance.images.delete(image) + elif glance.version == VERSION_2: + glance.images.delete(image.id) + else: + raise Exception('Unsupported glance client version') diff --git a/snaps/openstack/utils/tests/glance_utils_tests.py b/snaps/openstack/utils/tests/glance_utils_tests.py index d13908b..23c741a 100644 --- a/snaps/openstack/utils/tests/glance_utils_tests.py +++ b/snaps/openstack/utils/tests/glance_utils_tests.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ import uuid from snaps import file_utils from snaps.openstack.tests import openstack_tests -from snaps.openstack.utils import nova_utils from snaps.openstack.tests import validation_utils from snaps.openstack.tests.os_source_file_test import OSComponentTestCase from snaps.openstack.utils import glance_utils @@ -37,9 +36,8 @@ class GlanceSmokeTests(OSComponentTestCase): Tests to ensure that the proper credentials can connect. """ glance = glance_utils.glance_client(self.os_creds) - - users = glance.images.list() - self.assertIsNotNone(users) + image = glance_utils.get_image(glance, 'foo') + self.assertIsNone(image) def test_glance_connect_fail(self): """ @@ -48,8 +46,8 @@ class GlanceSmokeTests(OSComponentTestCase): from snaps.openstack.os_credentials import OSCreds with self.assertRaises(Exception): - neutron = glance_utils.glance_client(OSCreds('user', 'pass', 'url', 'project')) - neutron.list_networks() + glance = glance_utils.glance_client(OSCreds('user', 'pass', 'url', 'project')) + glance_utils.get_image(glance, 'foo') class GlanceUtilsTests(OSComponentTestCase): @@ -65,7 +63,6 @@ class GlanceUtilsTests(OSComponentTestCase): guid = uuid.uuid4() self.image_name = self.__class__.__name__ + '-' + str(guid) self.image = None - self.nova = nova_utils.nova_client(self.os_creds) self.glance = glance_utils.glance_client(self.os_creds) self.tmp_dir = 'tmp/' + str(guid) @@ -93,7 +90,7 @@ class GlanceUtilsTests(OSComponentTestCase): self.assertEqual(self.image_name, self.image.name) - image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + image = glance_utils.get_image(self.glance, os_image_settings.name) self.assertIsNotNone(image) validation_utils.objects_equivalent(self.image, image) @@ -110,6 +107,6 @@ class GlanceUtilsTests(OSComponentTestCase): self.assertIsNotNone(self.image) self.assertEqual(self.image_name, self.image.name) - image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name) + image = glance_utils.get_image(self.glance, file_image_settings.name) self.assertIsNotNone(image) validation_utils.objects_equivalent(self.image, image) diff --git a/snaps/test_suite_builder.py b/snaps/test_suite_builder.py index 77f6ed6..2e0e353 100644 --- a/snaps/test_suite_builder.py +++ b/snaps/test_suite_builder.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# Copyright (c) 2017 Cable Television Laboratories, Inc. ("CableLabs") # and others. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ import logging import unittest +from snaps.domain.test.image_tests import ImageDomainObjectTests from snaps.openstack.utils.tests.glance_utils_tests import GlanceSmokeTests, GlanceUtilsTests from snaps.openstack.tests.create_flavor_tests import CreateFlavorTests from snaps.tests.file_utils_tests import FileUtilsTests @@ -54,6 +55,7 @@ def add_unit_tests(suite): suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SecurityGroupRuleSettingsUnitTests)) suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SecurityGroupSettingsUnitTests)) suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ImageSettingsUnitTests)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ImageDomainObjectTests)) suite.addTest(unittest.TestLoader().loadTestsFromTestCase(KeypairSettingsUnitTests)) suite.addTest(unittest.TestLoader().loadTestsFromTestCase(UserSettingsUnitTests)) suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ProjectSettingsUnitTests)) |