From 07ea422696ffdfed22725c70e5e43692fd21bee2 Mon Sep 17 00:00:00 2001 From: Federico Ressi <fressi@redhat.com> Date: Mon, 24 Jun 2019 09:36:28 +0200 Subject: [PATCH] Implement Glance image management (setup and cleanup) Change-Id: Ibba2b2ea4ca2e919aca3bbdde225db8f0b79450c --- devstack/plugin.sh | 40 ++- devstack/settings | 11 +- doc/source/user/config.rst | 8 +- infrared/tasks/templates/tobiko.conf.j2 | 8 +- tobiko/openstack/glance/__init__.py | 9 + tobiko/openstack/glance/_client.py | 65 ++-- tobiko/openstack/glance/_image.py | 284 ++++++++++++++++-- tobiko/openstack/glance/config.py | 22 +- tobiko/openstack/images/__init__.py | 2 +- tobiko/openstack/images/_cirros.py | 25 +- tobiko/openstack/nova/config.py | 8 +- tobiko/openstack/stacks/_neutron.py | 3 +- .../tests/functional/openstack/test_glance.py | 8 +- 13 files changed, 414 insertions(+), 79 deletions(-) diff --git a/devstack/plugin.sh b/devstack/plugin.sh index b55c02570..d17aaee9d 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -18,6 +18,8 @@ function configure_tobiko { fi configure_tobiko_default "${tobiko_config}" + configure_tobiko_cirros "${tobiko_config}" + configure_tobiko_glance "${tobiko_config}" configure_tobiko_keystone "${tobiko_config}" configure_tobiko_nova "${tobiko_config}" configure_tobiko_neutron "${tobiko_config}" @@ -34,6 +36,18 @@ function configure_tobiko { } +function configure_tobiko_cirros { + echo_summary "Write [cirros] section to ${TOBIKO_CONFIG}" + local tobiko_config=$1 + + iniset_nonempty "${tobiko_config}" cirros name "${TOBIKO_CIRROS_IMAGE_NAME}" + iniset_nonempty "${tobiko_config}" cirros url "${TOBIKO_CIRROS_IMAGE_URL}" + iniset_nonempty "${tobiko_config}" cirros file "${TOBIKO_CIRROS_IMAGE_FILE}" + iniset_nonempty "${tobiko_config}" cirros username "${TOBIKO_CIRROS_USERNAME}" + iniset_nonempty "${tobiko_config}" cirros password "${TOBIKO_CIRROS_PASSWORD}" +} + + function configure_tobiko_default { echo_summary "Write [DEFAULT] section to ${TOBIKO_CONFIG}" local tobiko_config=$1 @@ -44,6 +58,15 @@ function configure_tobiko_default { iniset ${tobiko_config} DEFAULT debug "${TOBIKO_DEBUG}" } + +function configure_tobiko_glance { + echo_summary "Write [glance] section to ${TOBIKO_CONFIG}" + local tobiko_config=$1 + + iniset_nonempty "${tobiko_config}" glance image_dir "${TOBIKO_GLANCE_IMAGE_DIR}" +} + + function configure_tobiko_keystone { echo_summary "Write [keystone] section to ${TOBIKO_CONFIG}" local tobiko_config=$1 @@ -95,15 +118,6 @@ function configure_tobiko_nova { echo_summary "Write [nova] section to ${TOBIKO_CONFIG}" local tobiko_config=$1 - # Write image ID - local image_name=${TOBIKO_NOVA_IMAGE:-} - if [ "${image_name}" != "" ]; then - local image_id=$(openstack image show -f value -c id "${image_name}") - else - local image_id=$(openstack image list --limit 1 -f value -c ID --public --status active) - fi - iniset "${tobiko_config}" nova image "${image_id}" - # Write flavor ID local flavor_name=${TOBIKO_NOVA_FLAVOR:-} if [ "${flavor_name}" != "" ]; then @@ -138,6 +152,14 @@ function configure_tobiko_neutron { } +function iniset_nonempty { + # Calls iniset only when option value is not an empty string + if [ -n "$4" ]; then + iniset "$@" + fi +} + + if [[ "$1" == "stack" ]]; then case "$2" in install) diff --git a/devstack/settings b/devstack/settings index ee445d221..e4333d5ae 100644 --- a/devstack/settings +++ b/devstack/settings @@ -13,6 +13,16 @@ TOBIKO_DEBUG=${TOBIKO_DEBUG:-True} TOBIKO_LOG_DIR=${TOBIKO_LOG_DIR:-${LOGDIR:-}} TOBIKO_LOG_FILE=${TOBIKO_LOG_FILE:-tobiko.log} +# --- Glance settings --- +TOBIKO_GLANCE_IMAGE_DIR=${TOBIKO_GLANCE_IMAGE_DIR:-} + +# --- Cirros image settings --- +TOBIKO_CIRROS_IMAGE_NAME=${TOBIKO_CIRROS_IMAGE_NAME:-${DEFAULT_IMAGE_NAME}} +TOBIKO_CIRROS_IMAGE_URL=${TOBIKO_CIRROS_IMAGE_URL:-} +TOBIKO_CIRROS_IMAGE_FILE=${TOBIKO_CIRROS_IMAGE_FILE:-} +TOBIKO_CIRROS_USERNAME=${TOBIKO_CIRROS_USERNAME:-} +TOBIKO_CIRROS_PASSWORD=${TOBIKO_CIRROS_PASSWORD:-} + # --- Keystone settings --- # See ``lib/keystone`` where these users and tenants are set up TOBIKO_KEYSTONE_USERNAME=${TOBIKO_KEYSTONE_USERNAME:-${ADMIN_USERNAME:-admin}} @@ -24,7 +34,6 @@ TOBIKO_KEYSTONE_TRUST_ID=${TOBIKO_KEYSTONE_TRUST_ID:-} TOBIKO_KEYSTONE_USER_ROLE=${TOBIKO_KEYSTONE_USER_ROLE:-admin} # --- Nova settings --- -TOBIKO_NOVA_IMAGE=${TOBIKO_NOVA_IMAGE:-${DEFAULT_IMAGE_NAME}} TOBIKO_NOVA_FLAVOR=${TOBIKO_NOVA_FLAVOR:-${DEFAULT_INSTANCE_TYPE}} TOBIKO_NOVA_KEY_FILE=${TOBIKO_NOVA_KEY_FILE:-~/.ssh/id_rsa} diff --git a/doc/source/user/config.rst b/doc/source/user/config.rst index 8a2ee3e54..35e4f30b0 100644 --- a/doc/source/user/config.rst +++ b/doc/source/user/config.rst @@ -238,12 +238,17 @@ set:: for Nova instances created by Tobiko:: wget http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img - openstack image create cirros \ + openstack image create cirros-0.4.0 \ --file cirros-0.4.0-x86_64-disk.img \ --disk-format qcow2 \ --container-format bare \ --public +Add reference to above image into your :ref:`tobiko-conf` file:: + + [glance] + cirros_image = cirros-0.4.0 + Create a flavor to be used with above image:: openstack flavor create --vcpus 1 --ram 64 --disk 1 m1.tiny @@ -255,7 +260,6 @@ Create an SSH key file to be used to ssh to Nova server instances:: Add reference to above resources into your :ref:`tobiko-conf` file:: [nova] - image = cirros flavor = m1.tiny key_file=~/.ssh/id_rsa diff --git a/infrared/tasks/templates/tobiko.conf.j2 b/infrared/tasks/templates/tobiko.conf.j2 index 1d920925e..e91bd4579 100644 --- a/infrared/tasks/templates/tobiko.conf.j2 +++ b/infrared/tasks/templates/tobiko.conf.j2 @@ -2,9 +2,13 @@ debug = true log_file = tobiko.log log_dir = . + +[cirros] +image_name = cirros + [nova] -image = cirros flavor = m1.tiny key_file = ~/.ssh/id_rsa + [neutron] -floating_network = "{{ test.floating_network }}" \ No newline at end of file +floating_network = "{{ test.floating_network }}" diff --git a/tobiko/openstack/glance/__init__.py b/tobiko/openstack/glance/__init__.py index ecdb7dd67..f59db3bb1 100644 --- a/tobiko/openstack/glance/__init__.py +++ b/tobiko/openstack/glance/__init__.py @@ -13,13 +13,22 @@ # under the License. from __future__ import absolute_import +from tobiko.openstack import _find from tobiko.openstack.glance import _client from tobiko.openstack.glance import _image glance_client = _client.glance_client get_glance_client = _client.get_glance_client GlanceClientFixture = _client.GlanceClientFixture +GlanceImageNotFound = _client.GlanceImageNotFound +create_image = _client.create_image +get_image = _client.get_image find_image = _client.find_image list_images = _client.list_images +delete_image = _client.delete_image + +ResourceNotFound = _find.ResourceNotFound GlanceImageFixture = _image.GlanceImageFixture +FileGlanceImageFixture = _image.FileGlanceImageFixture +URLGlanceImageFixture = _image.URLGlanceImageFixture diff --git a/tobiko/openstack/glance/_client.py b/tobiko/openstack/glance/_client.py index 5dfa52e59..9a115de0d 100644 --- a/tobiko/openstack/glance/_client.py +++ b/tobiko/openstack/glance/_client.py @@ -14,6 +14,7 @@ from __future__ import absolute_import from glanceclient.v2 import client as glanceclient +from glanceclient import exc import tobiko from tobiko.openstack import _client @@ -35,36 +36,64 @@ class GlanceClientManager(_client.OpenstackClientManager): CLIENTS = GlanceClientManager() -def glance_client(obj): - if not obj: - return get_glance_client() +def glance_client(obj=None): + obj = obj or default_glance_client() + if tobiko.is_fixture(obj): + obj = tobiko.setup_fixture(obj).client + return tobiko.check_valid_type(obj, glanceclient.Client) - if isinstance(obj, glanceclient.Client): - return obj - fixture = tobiko.setup_fixture(obj) - if isinstance(fixture, GlanceClientFixture): - return fixture.client - - message = "Object {!r} is not a NovaClientFixture".format(obj) - raise TypeError(message) +def default_glance_client(): + return get_glance_client() def get_glance_client(session=None, shared=True, init_client=None, manager=None): manager = manager or CLIENTS - client = manager.get_client(session=session, shared=shared, - init_client=init_client) - tobiko.setup_fixture(client) - return client.client + fixture = manager.get_client(session=session, shared=shared, + init_client=init_client) + return glance_client(fixture) + + +def create_image(client=None, **params): + """Look for the unique network matching some property values""" + return glance_client(client).images.create(**params) + + +def delete_image(image_id, client=None, **params): + try: + glance_client(client).images.delete(image_id, **params) + except exc.HTTPNotFound: + pass + + +def get_image(image_id, client=None): + try: + return glance_client(client).images.get(image_id=image_id) + except exc.HTTPNotFound as ex: + raise GlanceImageNotFound(cause=ex) def find_image(obj=None, properties=None, client=None, **params): """Look for the unique network matching some property values""" - return _find.find_resource( - obj=obj, resource_type='image', properties=properties, - resources=list_images(client=client, **params), **params) + + images = list_images(client=client, limit=1, **params) + for image in _find.find_resources(obj, images, properties=properties): + return image + + raise GlanceImageNotFound(obj=obj, properties=properties, params=params) def list_images(client=None, **params): return list(glance_client(client).images.list(**params)) + + +def upload_image(image_id, image_data, client=None, **params): + """Look for the unique network matching some property values""" + return glance_client(client).images.upload( + image_id=image_id, image_data=image_data, **params) + + +class GlanceImageNotFound(_find.ResourceNotFound): + message = ("No such image found for obj={obj!r}, " + "properties={properties!r} and params={params!r}") diff --git a/tobiko/openstack/glance/_image.py b/tobiko/openstack/glance/_image.py index 6b3eb8d7f..89f224a6a 100644 --- a/tobiko/openstack/glance/_image.py +++ b/tobiko/openstack/glance/_image.py @@ -13,47 +13,295 @@ # under the License. from __future__ import absolute_import +import io +import os +import tempfile +import time + +from oslo_log import log +import requests + import tobiko from tobiko.openstack.glance import _client -from tobiko.openstack import _find + +LOG = log.getLogger(__name__) + + +class GlanceImageStatus(object): + + #: The Image service reserved an image ID for the image in the catalog but + # did not yet upload any image data. + QUEUED = 'queued' + + #: The Image service is in the process of saving the raw data for the + # image into the backing store. + SAVING = 'saving' + + #: The image is active and ready for consumption in the Image service. + ACTIVE = 'active' + + #: An image data upload error occurred. + KILLED = 'killed' + + #: The Image service retains information about the image but the image is + # no longer available for use. + DELETED = 'deleted' + + #: Similar to the deleted status. An image in this state is not + # recoverable. + PENDING_DELETE = 'pending_delete' + + #: The image data is not available for use. + DEACTIVATE = 'deactivated' + + #: Data has been staged as part of the interoperable image import process. + # It is not yet available for use. (Since Image API 2.6) + UPLOADING = 'uploading' + + #: The image data is being processed as part of the interoperable image + # import process, but is not yet available for use. (Since Image API 2.6) + IMPORTING = 'importing' class GlanceImageFixture(tobiko.SharedFixture): client = None - image = None - image_details = None + image_name = None + username = None + password = None + _image = None + sleep_interval = 1. - def __init__(self, client=None, image=None): + def __init__(self, image_name=None, username=None, password=None, + client=None): super(GlanceImageFixture, self).__init__() + if client: self.client = client - if image: - self.image = image - elif not self.image: - self.image = self.fixture_name + + if image_name: + self.image_name = image_name + elif not self.image_name: + self.image_name = self.fixture_name + tobiko.check_valid_type(self.image_name, str) + + if username: + self.username = username + + if password: + self.password = password def setup_fixture(self): self.setup_client() self.setup_image() + def cleanup_fixture(self): + self.delete_image() + def setup_client(self): self.client = _client.glance_client(self.client) def setup_image(self): - try: - self.image_details = _client.find_image(self.image, - client=self.client) - except _find.ResourceNotFound: - self.image_details = self.create_image() + return self.wait_for_image_active() - def create_image(self): - raise NotImplementedError + def wait_for_image_active(self): + image = self.get_image() + while GlanceImageStatus.ACTIVE != image.status: + check_image_status(image, {GlanceImageStatus.QUEUED, + GlanceImageStatus.SAVING}) + LOG.debug('Waiting for image %r to change from %r to %r...', + self.image_name, image.status, GlanceImageStatus.ACTIVE) + time.sleep(self.sleep_interval) + image = self.get_image() + + @property + def image(self): + return self._image or self.get_image() + + def get_image(self, **kwargs): + self._image = image = _client.find_image( + self.image_name, client=self.client, **kwargs) + LOG.debug('Got image %r: %r', self.image_name, image) + return image + + def delete_image(self, image_id=None): + try: + if not image_id: + image_id = self.image_id + self._image = None + _client.delete_image(image_id=image_id, client=self.client) + except _client.GlanceImageNotFound: + LOG.debug('Image %r not deleted because not found', + image_id or self.image_name) + return None + else: + LOG.debug("Deleted image %r: %r", self.image_name, image_id) @property def image_id(self): - return self.image_details['id'] + return self.image.id @property - def image_name(self): - return self.image_details['name'] + def image_status(self): + return self.image.status + + +class UploadGranceImageFixture(GlanceImageFixture): + + disk_format = "raw" + container_format = "bare" + + def __init__(self, disk_format=None, container_format=None, **kwargs): + super(UploadGranceImageFixture, self).__init__(**kwargs) + + if container_format: + self.container_format = disk_format + tobiko.check_valid_type(self.container_format, str) + + if disk_format: + self.disk_format = disk_format + tobiko.check_valid_type(self.disk_format, str) + + def setup_image(self): + try: + return self.wait_for_image_active() + except _client.GlanceImageNotFound: + pass + except InvalidGlanceImageStatus as ex: + self.delete_image(image_id=ex.image_id) + + new_image = self.create_image() + image = self.get_image() + if image['id'] != new_image['id']: + self.delete_image(image_id=new_image['id']) + else: + check_image_status(image, {GlanceImageStatus.QUEUED}) + self.upload_image() + return self.wait_for_image_active() + + def create_image(self): + image = _client.create_image(client=self.client, + name=self.image_name, + disk_format=self.disk_format, + container_format=self.container_format) + LOG.debug("Created image %r: %r", self.image_name, image) + return image + + def upload_image(self): + image_data, image_size = self.get_image_data() + with image_data: + _client.upload_image(image_id=self.image_id, + image_data=image_data, + image_size=image_size) + LOG.debug("Image uploaded %r", self.image_name) + + def get_image_data(self): + raise NotImplementedError + + +class FileGlanceImageFixture(UploadGranceImageFixture): + + image_file = None + image_dir = None + + def __init__(self, image_file=None, image_dir=None, **kwargs): + super(FileGlanceImageFixture, self).__init__(**kwargs) + + if image_file: + self.image_file = image_file + elif not self.image_file: + self.image_file = self.fixture_name + tobiko.check_valid_type(self.image_file, str) + + if image_dir: + self.image_dir = image_dir + elif not self.image_dir: + from tobiko import config + CONF = config.CONF + self.image_dir = CONF.tobiko.glance.image_dir or "." + tobiko.check_valid_type(self.image_dir, str) + + @property + def real_image_dir(self): + return os.path.realpath(os.path.expanduser(self.image_dir)) + + @property + def real_image_file(self): + return os.path.join(self.real_image_dir, self.image_file) + + def get_image_data(self): + image_file = self.real_image_file + image_size = os.path.getsize(image_file) + image_data = io.open(image_file, 'rb') + LOG.debug('Reading image %r data from file %r (%d bytes)', + self.image_name, image_file, image_size) + return image_data, image_size + + +class URLGlanceImageFixture(FileGlanceImageFixture): + + image_url = None + + def __init__(self, image_url=None, **kwargs): + super(URLGlanceImageFixture, self).__init__(**kwargs) + if image_url: + self.image_url = image_url + else: + image_url = self.image_url + tobiko.check_valid_type(image_url, str) + + def get_image_data(self): + http_request = requests.get(self.image_url, stream=True) + expected_size = int(http_request.headers.get('content-length', 0)) + image_file = self.real_image_file + chunks = http_request.iter_content(chunk_size=io.DEFAULT_BUFFER_SIZE) + try: + if expected_size: + actual_size = os.path.getsize(image_file) + if actual_size == expected_size: + LOG.debug("Cached image %r file %r found (%d bytes)", + self.image_name, image_file, actual_size) + return super(URLGlanceImageFixture, self).get_image_data() + + except Exception as ex: + LOG.debug("Unable to get image %r file %r size: %s", + self.image_name, image_file, ex) + + LOG.debug('Downloading image %r from URL %r to file %r (%d bytes)', + self.image_name, self.image_url, image_file, + expected_size) + + image_dir = os.path.dirname(image_file) + if not os.path.isdir(image_dir): + LOG.debug('Creating image directory: %r', image_dir) + os.makedirs(image_dir) + + fd, temp_file = tempfile.mkstemp(dir=image_dir) + with io.open(fd, 'wb', io.DEFAULT_BUFFER_SIZE) as image_data: + for chunk in chunks: + image_data.write(chunk) + + actual_size = os.path.getsize(temp_file) + LOG.debug('Downloaded image %r from URL %r to file %r (%d bytes)', + self.image_name, self.image_url, image_file, + actual_size) + + if expected_size and actual_size != expected_size: + message = "Download file size mismatch: {!s} != {!r}".format( + expected_size, actual_size) + raise RuntimeError(message) + os.rename(temp_file, image_file) + return super(URLGlanceImageFixture, self).get_image_data() + + +def check_image_status(image, expected_status): + if image.status not in expected_status: + raise InvalidGlanceImageStatus(image_name=image.name, + image_id=image.id, + actual_status=image.status, + expected_status=expected_status) + + +class InvalidGlanceImageStatus(tobiko.TobikoException): + message = ("Invalid image {image_name!r} (id {image_id!r}) status: " + "{actual_status!r} not in {expected_status!r}") diff --git a/tobiko/openstack/glance/config.py b/tobiko/openstack/glance/config.py index f7bcdef8e..a597b9f61 100644 --- a/tobiko/openstack/glance/config.py +++ b/tobiko/openstack/glance/config.py @@ -16,7 +16,27 @@ from __future__ import absolute_import from oslo_config import cfg +CIRROS_IMAGE_URL = \ + 'http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img' + + def register_tobiko_options(conf): conf.register_opts( group=cfg.OptGroup('glance'), - opts=[]) + opts=[cfg.StrOpt('image_dir', + default='~/.tobiko/cache/glance/images', + help=("Default directory where to look for image " + "files")), ]) + + conf.register_opts( + group=cfg.OptGroup('cirros'), + opts=[cfg.StrOpt('image_name', + help="Default CirrOS image name"), + cfg.StrOpt('image_url', + help="Default CirrOS image URL"), + cfg.StrOpt('image_file', + help="Default CirrOS image filename"), + cfg.StrOpt('username', + help="Default CirrOS username"), + cfg.StrOpt('password', + help="Default CirrOS password"), ]) diff --git a/tobiko/openstack/images/__init__.py b/tobiko/openstack/images/__init__.py index 7cd3d609a..5a079e754 100644 --- a/tobiko/openstack/images/__init__.py +++ b/tobiko/openstack/images/__init__.py @@ -16,4 +16,4 @@ from __future__ import absolute_import from tobiko.openstack.images import _cirros -CirrosImageFixture = _cirros.CirrosImageFixture +CirrosGlanceImageFixture = _cirros.CirrosGlanceImageFixture diff --git a/tobiko/openstack/images/_cirros.py b/tobiko/openstack/images/_cirros.py index 5802d252d..edb800b2b 100644 --- a/tobiko/openstack/images/_cirros.py +++ b/tobiko/openstack/images/_cirros.py @@ -13,29 +13,20 @@ # under the License. from __future__ import absolute_import - from tobiko import config from tobiko.openstack import glance CONF = config.CONF -class CirrosImageFixture(glance.GlanceImageFixture): +CIRROS_IMAGE_URL = \ + 'http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img' - @property - def image(self): - """glance image used to create a Nova server instance""" - return CONF.tobiko.nova.image - @property - def username(self): - """username used to login to a Nova server instance""" - return CONF.tobiko.nova.username +class CirrosGlanceImageFixture(glance.URLGlanceImageFixture): - @property - def password(self): - """password used to login to a Nova server instance""" - return CONF.tobiko.nova.password - - def create_image(self): - raise NotImplementedError + image_url = CIRROS_IMAGE_URL + image_name = CONF.tobiko.cirros.image_name + image_file = CONF.tobiko.cirros.image_file + username = CONF.tobiko.cirros.username or 'cirros' + password = CONF.tobiko.cirros.password or 'gocubsgo' diff --git a/tobiko/openstack/nova/config.py b/tobiko/openstack/nova/config.py index 8c662b4ce..ada6416b0 100644 --- a/tobiko/openstack/nova/config.py +++ b/tobiko/openstack/nova/config.py @@ -19,14 +19,8 @@ from oslo_config import cfg def register_tobiko_options(conf): conf.register_opts( group=cfg.OptGroup('nova'), - opts=[cfg.StrOpt('image', - help="Default image for new server instances"), - cfg.StrOpt('flavor', + opts=[cfg.StrOpt('flavor', help="Default flavor for new server instances"), cfg.StrOpt('key_file', default='~/.ssh/id_rsa', help="Default SSH key to login to server instances"), - cfg.StrOpt('username', default='cirros', - help="Default username to login to server instances"), - cfg.StrOpt('password', default='gocubsgo', - help="Default password to login to server instances"), ]) diff --git a/tobiko/openstack/stacks/_neutron.py b/tobiko/openstack/stacks/_neutron.py index 599f1c0b3..60ca6cc6d 100644 --- a/tobiko/openstack/stacks/_neutron.py +++ b/tobiko/openstack/stacks/_neutron.py @@ -169,7 +169,8 @@ class FloatingIpServerStackFixture(heat.HeatStackFixture): network_stack = tobiko.required_setup_fixture(NetworkStackFixture) #: Glance image used to create a Nova server instance - image_fixture = tobiko.required_setup_fixture(images.CirrosImageFixture) + image_fixture = tobiko.required_setup_fixture( + images.CirrosGlanceImageFixture) @property def image(self): diff --git a/tobiko/tests/functional/openstack/test_glance.py b/tobiko/tests/functional/openstack/test_glance.py index 190d19450..d5dbf9ccb 100644 --- a/tobiko/tests/functional/openstack/test_glance.py +++ b/tobiko/tests/functional/openstack/test_glance.py @@ -23,10 +23,14 @@ from tobiko.openstack import images class GlanceApiTestCase(testtools.TestCase): - """Tests network creation""" + """Tests glance images API""" #: Stack of resources with a network with a gateway router - fixture = tobiko.required_setup_fixture(images.CirrosImageFixture) + fixture = tobiko.required_setup_fixture(images.CirrosGlanceImageFixture) + + def test_get_image(self): + image = glance.get_image(self.fixture.image_id) + self.assertEqual(self.fixture.image_id, image['id']) def test_find_image_with_id(self): image = glance.find_image(self.fixture.image_id)