diff options
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/common/quota.py | 3 | ||||
| -rw-r--r-- | openstackclient/common/utils.py | 34 | ||||
| -rw-r--r-- | openstackclient/compute/client.py | 10 | ||||
| -rw-r--r-- | openstackclient/identity/v3/role.py | 2 | ||||
| -rw-r--r-- | openstackclient/identity/v3/user.py | 10 | ||||
| -rw-r--r-- | openstackclient/image/v2/image.py | 289 | ||||
| -rw-r--r-- | openstackclient/shell.py | 10 | ||||
| -rw-r--r-- | openstackclient/tests/common/test_quota.py | 45 | ||||
| -rw-r--r-- | openstackclient/tests/common/test_utils.py | 54 | ||||
| -rw-r--r-- | openstackclient/tests/image/v2/test_image.py | 189 |
10 files changed, 608 insertions, 38 deletions
diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 65367e03..e092feff 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -105,8 +105,7 @@ class SetQuota(command.Command): volume_kwargs = {} for k, v in VOLUME_QUOTAS.items(): - # TODO(jiaxi): Should use k or v needs discuss - value = getattr(parsed_args, v, None) + value = getattr(parsed_args, k, None) if value is not None: if parsed_args.volume_type: k = k + '_%s' % parsed_args.volume_type diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index b6726bfa..51e2a2f9 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -94,12 +94,15 @@ def find_resource(manager, name_or_id, **kwargs): if len(kwargs) == 0: kwargs = {} - # Prepare the kwargs for calling find - if 'NAME_ATTR' in manager.resource_class.__dict__: - # novaclient does this for oddball resources - kwargs[manager.resource_class.NAME_ATTR] = name_or_id - else: - kwargs['name'] = name_or_id + try: + # Prepare the kwargs for calling find + if 'NAME_ATTR' in manager.resource_class.__dict__: + # novaclient does this for oddball resources + kwargs[manager.resource_class.NAME_ATTR] = name_or_id + else: + kwargs['name'] = name_or_id + except Exception: + pass # finally try to find entity by name try: @@ -118,7 +121,24 @@ def find_resource(manager, name_or_id, **kwargs): (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) else: - raise + pass + + try: + for resource in manager.list(): + # short circuit and return the first match + if (resource.get('id') == name_or_id or + resource.get('name') == name_or_id): + return resource + else: + # we found no match, keep going to bomb out + pass + except Exception: + # in case the list fails for some reason + pass + + # if we hit here, we've failed, report back this error: + msg = "Could not find resource %s" % name_or_id + raise exceptions.CommandError(msg) def format_dict(data): diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 8ac5f324..23a4deca 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -34,13 +34,8 @@ _compute_api_version = None def make_client(instance): """Returns a compute service client.""" - # Defer client imports until we actually need them + # Defer client import until we actually need them from novaclient import client as nova_client - from novaclient import extension - try: - from novaclient.v2.contrib import list_extensions - except ImportError: - from novaclient.v1_1.contrib import list_extensions if _compute_api_version is not None: version = _compute_api_version @@ -52,7 +47,8 @@ def make_client(instance): # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG - extensions = [extension.Extension('list_extensions', list_extensions)] + extensions = [ext for ext in nova_client.discover_extensions(version) + if ext.name == "list_extensions"] # Remember interface only if it is set kwargs = utils.build_kwargs_dict('endpoint_type', instance._interface) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 46e2e440..c72de477 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -39,7 +39,7 @@ def _add_identity_and_resource_options_to_parser(parser): domain_or_project.add_argument( '--project', metavar='<project>', - help='Include `<project>` (name or ID)', + help='Include <project> (name or ID)', ) user_or_group = parser.add_mutually_exclusive_group() user_or_group.add_argument( diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index f23a90f7..0e894544 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -214,13 +214,11 @@ class ListUser(lister.Lister): domain = common.find_domain(identity_client, parsed_args.domain).id + group = None if parsed_args.group: - group = utils.find_resource( - identity_client.groups, - parsed_args.group, - ).id - else: - group = None + group = common.find_group(identity_client, + parsed_args.group, + parsed_args.domain).id if parsed_args.project: if domain is not None: diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 4c019db6..fff26c02 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -22,14 +22,51 @@ import six from cliff import command from cliff import lister from cliff import show - from glanceclient.common import utils as gc_utils + from openstackclient.api import utils as api_utils +from openstackclient.common import exceptions from openstackclient.common import parseractions from openstackclient.common import utils from openstackclient.identity import common +DEFAULT_CONTAINER_FORMAT = 'bare' +DEFAULT_DISK_FORMAT = 'raw' + + +def _format_image(image): + """Format an image to make it more consistent with OSC operations. """ + + info = {} + properties = {} + + # the only fields we're not including is "links", "tags" and the properties + fields_to_show = ['status', 'name', 'container_format', 'created_at', + 'size', 'disk_format', 'updated_at', 'visibility', + 'min_disk', 'protected', 'id', 'file', 'checksum', + 'owner', 'virtual_size', 'min_ram', 'schema'] + + # split out the usual key and the properties which are top-level + for key in six.iterkeys(image): + if key in fields_to_show: + info[key] = image.get(key) + elif key == 'tags': + continue # handle this later + else: + properties[key] = image.get(key) + + # format the tags if they are there + if image.get('tags'): + info['tags'] = utils.format_list(image.get('tags')) + + # add properties back into the dictionary as a top-level key + if properties: + info['properties'] = utils.format_dict(properties) + + return info + + class AddProjectToImage(show.ShowOne): """Associate project with image""" @@ -72,6 +109,187 @@ class AddProjectToImage(show.ShowOne): return zip(*sorted(six.iteritems(image_member._info))) +class CreateImage(show.ShowOne): + """Create/upload an image""" + + log = logging.getLogger(__name__ + ".CreateImage") + deadopts = ('owner', 'size', 'location', 'copy-from', 'checksum', 'store') + + def get_parser(self, prog_name): + parser = super(CreateImage, self).get_parser(prog_name) + # TODO(mordred): add --volume and --force parameters and support + # TODO(bunting): There are additional arguments that v1 supported + # that v2 either doesn't support or supports weirdly. + # --checksum - could be faked clientside perhaps? + # --owner - could be set as an update after the put? + # --location - maybe location add? + # --size - passing image size is actually broken in python-glanceclient + # --copy-from - does not exist in v2 + # --store - does not exits in v2 + parser.add_argument( + "name", + metavar="<image-name>", + help="New image name", + ) + parser.add_argument( + "--id", + metavar="<id>", + help="Image ID to reserve", + ) + parser.add_argument( + "--container-format", + default=DEFAULT_CONTAINER_FORMAT, + metavar="<container-format>", + help="Image container format " + "(default: %s)" % DEFAULT_CONTAINER_FORMAT, + ) + parser.add_argument( + "--disk-format", + default=DEFAULT_DISK_FORMAT, + metavar="<disk-format>", + help="Image disk format " + "(default: %s)" % DEFAULT_DISK_FORMAT, + ) + parser.add_argument( + "--min-disk", + metavar="<disk-gb>", + type=int, + help="Minimum disk size needed to boot image, in gigabytes", + ) + parser.add_argument( + "--min-ram", + metavar="<ram-mb>", + type=int, + help="Minimum RAM size needed to boot image, in megabytes", + ) + parser.add_argument( + "--file", + metavar="<file>", + help="Upload image from local file", + ) + protected_group = parser.add_mutually_exclusive_group() + protected_group.add_argument( + "--protected", + action="store_true", + help="Prevent image from being deleted", + ) + protected_group.add_argument( + "--unprotected", + action="store_true", + help="Allow image to be deleted (default)", + ) + public_group = parser.add_mutually_exclusive_group() + public_group.add_argument( + "--public", + action="store_true", + help="Image is accessible to the public", + ) + public_group.add_argument( + "--private", + action="store_true", + help="Image is inaccessible to the public (default)", + ) + parser.add_argument( + "--property", + dest="properties", + metavar="<key=value>", + action=parseractions.KeyValueAction, + help="Set a property on this image " + "(repeat option to set multiple properties)", + ) + parser.add_argument( + "--tag", + dest="tags", + metavar="<tag>", + action='append', + help="Set a tag on this image " + "(repeat option to set multiple tags)", + ) + for deadopt in self.deadopts: + parser.add_argument( + "--%s" % deadopt, + metavar="<%s>" % deadopt, + dest=deadopt.replace('-', '_'), + help=argparse.SUPPRESS + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + image_client = self.app.client_manager.image + + for deadopt in self.deadopts: + if getattr(parsed_args, deadopt.replace('-', '_'), None): + raise exceptions.CommandError( + "ERROR: --%s was given, which is an Image v1 option" + " that is no longer supported in Image v2" % deadopt) + + # Build an attribute dict from the parsed args, only include + # attributes that were actually set on the command line + kwargs = {} + copy_attrs = ('name', 'id', + 'container_format', 'disk_format', + 'min_disk', 'min_ram', + 'tags') + for attr in copy_attrs: + if attr in parsed_args: + val = getattr(parsed_args, attr, None) + if val: + # Only include a value in kwargs for attributes that + # are actually present on the command line + kwargs[attr] = val + # properties should get flattened into the general kwargs + if getattr(parsed_args, 'properties', None): + for k, v in six.iteritems(parsed_args.properties): + kwargs[k] = str(v) + # Handle exclusive booleans with care + # Avoid including attributes in kwargs if an option is not + # present on the command line. These exclusive booleans are not + # a single value for the pair of options because the default must be + # to do nothing when no options are present as opposed to always + # setting a default. + if parsed_args.protected: + kwargs['protected'] = True + if parsed_args.unprotected: + kwargs['protected'] = False + if parsed_args.public: + kwargs['visibility'] = 'public' + if parsed_args.private: + kwargs['visibility'] = 'private' + + # open the file first to ensure any failures are handled before the + # image is created + fp = gc_utils.get_data_file(parsed_args) + + if fp is None and parsed_args.file: + self.log.warning("Failed to get an image file.") + return {}, {} + + image = image_client.images.create(**kwargs) + + if fp is not None: + with fp: + try: + image_client.images.upload(image.id, fp) + except Exception as e: + # If the upload fails for some reason attempt to remove the + # dangling queued image made by the create() call above but + # only if the user did not specify an id which indicates + # the Image already exists and should be left alone. + try: + if 'id' not in kwargs: + image_client.images.delete(image.id) + except Exception: + pass # we don't care about this one + raise e # now, throw the upload exception again + + # update the image after the data has been uploaded + image = image_client.images.get(image.id) + + info = _format_image(image) + return zip(*sorted(six.iteritems(info))) + + class DeleteImage(command.Command): """Delete image(s)""" @@ -229,7 +447,7 @@ class ListImage(lister.Lister): s, columns, formatters={ - 'tags': utils.format_dict, + 'tags': utils.format_list, }, ) for s in data) ) @@ -327,8 +545,7 @@ class ShowImage(show.ShowOne): parsed_args.image, ) - info = {} - info.update(image) + info = _format_image(image) return zip(*sorted(six.iteritems(info))) @@ -336,9 +553,22 @@ class SetImage(show.ShowOne): """Set image properties""" log = logging.getLogger(__name__ + ".SetImage") + deadopts = ('size', 'store', 'location', 'copy-from', 'checksum') def get_parser(self, prog_name): parser = super(SetImage, self).get_parser(prog_name) + # TODO(bunting): There are additional arguments that v1 supported + # --size - does not exist in v2 + # --store - does not exist in v2 + # --location - maybe location add? + # --copy-from - does not exist in v2 + # --file - should be able to upload file + # --volume - needs adding + # --force - needs adding + # --checksum - maybe could be done client side + # --stdin - could be implemented + # --property - needs adding + # --tags - needs adding parser.add_argument( "image", metavar="<image>", @@ -354,12 +584,28 @@ class SetImage(show.ShowOne): metavar="<architecture>", help="Operating system Architecture" ) - parser.add_argument( + protected_group = parser.add_mutually_exclusive_group() + protected_group.add_argument( "--protected", - dest="protected", action="store_true", help="Prevent image from being deleted" ) + protected_group.add_argument( + "--unprotected", + action="store_true", + help="Allow image to be deleted (default)" + ) + public_group = parser.add_mutually_exclusive_group() + public_group.add_argument( + "--public", + action="store_true", + help="Image is accessible to the public", + ) + public_group.add_argument( + "--private", + action="store_true", + help="Image is inaccessible to the public (default)", + ) parser.add_argument( "--instance-uuid", metavar="<instance_uuid>", @@ -372,12 +618,11 @@ class SetImage(show.ShowOne): help="Minimum disk size needed to boot image, in gigabytes" ) visibility_choices = ["public", "private"] - parser.add_argument( + public_group.add_argument( "--visibility", metavar="<visibility>", choices=visibility_choices, - help="Scope of image accessibility. Valid values: %s" - % visibility_choices + help=argparse.SUPPRESS ) help_msg = ("ID of image in Glance that should be used as the kernel" " when booting an AMI-style image") @@ -432,12 +677,25 @@ class SetImage(show.ShowOne): choices=container_choices, help=help_msg ) + for deadopt in self.deadopts: + parser.add_argument( + "--%s" % deadopt, + metavar="<%s>" % deadopt, + dest=deadopt.replace('-', '_'), + help=argparse.SUPPRESS + ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) image_client = self.app.client_manager.image + for deadopt in self.deadopts: + if getattr(parsed_args, deadopt.replace('-', '_'), None): + raise exceptions.CommandError( + "ERROR: --%s was given, which is an Image v1 option" + " that is no longer supported in Image v2" % deadopt) + kwargs = {} copy_attrs = ('architecture', 'container_format', 'disk_format', 'file', 'kernel_id', 'locations', 'name', @@ -451,10 +709,21 @@ class SetImage(show.ShowOne): # Only include a value in kwargs for attributes that are # actually present on the command line kwargs[attr] = val + + # Handle exclusive booleans with care + # Avoid including attributes in kwargs if an option is not + # present on the command line. These exclusive booleans are not + # a single value for the pair of options because the default must be + # to do nothing when no options are present as opposed to always + # setting a default. if parsed_args.protected: kwargs['protected'] = True - else: + if parsed_args.unprotected: kwargs['protected'] = False + if parsed_args.public: + kwargs['visibility'] = 'public' + if parsed_args.private: + kwargs['visibility'] = 'private' if not kwargs: self.log.warning("No arguments specified") diff --git a/openstackclient/shell.py b/openstackclient/shell.py index c08d619d..5b36b8b2 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -79,6 +79,10 @@ class OpenStackShell(app.App): help.HelpCommand.auth_required = False complete.CompleteCommand.auth_required = False + # Slight change to the meaning of --debug + self.DEFAULT_DEBUG_VALUE = None + self.DEFAULT_DEBUG_HELP = 'Set debug logging and traceback on errors.' + super(OpenStackShell, self).__init__( description=__doc__.strip(), version=openstackclient.__version__, @@ -197,13 +201,15 @@ class OpenStackShell(app.App): # Parent __init__ parses argv into self.options super(OpenStackShell, self).initialize_app(argv) + self.log.info("START with options: %s", self.command_options) + self.log.debug("options: %s", self.options) # Set the default plugin to token_endpoint if url and token are given if (self.options.url and self.options.token): # Use service token authentication auth_type = 'token_endpoint' else: - auth_type = 'osc_password' + auth_type = 'password' project_id = getattr(self.options, 'project_id', None) project_name = getattr(self.options, 'project_name', None) @@ -239,8 +245,6 @@ class OpenStackShell(app.App): self.log_configurator.configure(self.cloud) self.dump_stack_trace = self.log_configurator.dump_trace - self.log.info("START with options: %s", self.command_options) - self.log.debug("options: %s", self.options) self.log.debug("defaults: %s", cc.defaults) self.log.debug("cloud cfg: %s", self.cloud.config) diff --git a/openstackclient/tests/common/test_quota.py b/openstackclient/tests/common/test_quota.py index f0013e48..b6ad1566 100644 --- a/openstackclient/tests/common/test_quota.py +++ b/openstackclient/tests/common/test_quota.py @@ -12,6 +12,8 @@ import copy +import mock + from openstackclient.common import quota from openstackclient.tests.compute.v2 import fakes as compute_fakes from openstackclient.tests import fakes @@ -38,6 +40,11 @@ class TestQuota(compute_fakes.TestComputev2): super(TestQuota, self).setUp() self.quotas_mock = self.app.client_manager.compute.quotas self.quotas_mock.reset_mock() + volume_mock = mock.Mock() + volume_mock.quotas = mock.Mock() + self.app.client_manager.volume = volume_mock + self.volume_quotas_mock = volume_mock.quotas + self.volume_quotas_mock.reset_mock() class TestQuotaSet(TestQuota): @@ -57,6 +64,18 @@ class TestQuotaSet(TestQuota): loaded=True, ) + self.volume_quotas_mock.find.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + + self.volume_quotas_mock.update.return_value = FakeQuotaResource( + None, + copy.deepcopy(compute_fakes.QUOTA), + loaded=True, + ) + self.cmd = quota.SetQuota(self.app, None) def test_quota_set(self): @@ -87,3 +106,29 @@ class TestQuotaSet(TestQuota): } self.quotas_mock.update.assert_called_with('project_test', **kwargs) + + def test_quota_set_volume(self): + arglist = [ + '--gigabytes', str(compute_fakes.floating_ip_num), + '--snapshots', str(compute_fakes.fix_ip_num), + '--volumes', str(compute_fakes.injected_file_num), + compute_fakes.project_name, + ] + verifylist = [ + ('gigabytes', compute_fakes.floating_ip_num), + ('snapshots', compute_fakes.fix_ip_num), + ('volumes', compute_fakes.injected_file_num), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + kwargs = { + 'gigabytes': compute_fakes.floating_ip_num, + 'snapshots': compute_fakes.fix_ip_num, + 'volumes': compute_fakes.injected_file_num, + } + + self.volume_quotas_mock.update.assert_called_with('project_test', + **kwargs) diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index a25a5ba5..373c0de4 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -20,6 +20,7 @@ import mock from openstackclient.common import exceptions from openstackclient.common import utils +from openstackclient.tests import fakes from openstackclient.tests import utils as test_utils PASSWORD = "Pa$$w0rd" @@ -27,6 +28,18 @@ WASSPORD = "Wa$$p0rd" DROWSSAP = "dr0w$$aP" +class FakeOddballResource(fakes.FakeResource): + + def get(self, attr): + """get() is needed for utils.find_resource()""" + if attr == 'id': + return self.id + elif attr == 'name': + return self.name + else: + return None + + class TestUtils(test_utils.TestCase): def test_get_password_good(self): @@ -242,6 +255,47 @@ class TestFindResource(test_utils.TestCase): self.manager.get.assert_called_with(self.name) self.manager.find.assert_called_with(name=self.name) + def test_find_resource_silly_resource(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + silly_resource = FakeOddballResource( + None, + {'id': '12345', 'name': self.name}, + loaded=True, + ) + self.manager.list = mock.Mock( + return_value=[silly_resource, ], + ) + result = utils.find_resource(self.manager, self.name) + self.assertEqual(silly_resource, result) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + + def test_find_resource_silly_resource_not_found(self): + # We need a resource with no resource_class for this test, start fresh + self.manager = mock.Mock() + self.manager.get = mock.Mock(side_effect=Exception('Boom!')) + self.manager.find = mock.Mock( + side_effect=AttributeError( + "'Controller' object has no attribute 'find'", + ) + ) + self.manager.list = mock.Mock(return_value=[]) + result = self.assertRaises(exceptions.CommandError, + utils.find_resource, + self.manager, + self.name) + self.assertEqual("Could not find resource legos", + str(result)) + self.manager.get.assert_called_with(self.name) + self.manager.find.assert_called_with(name=self.name) + def test_format_dict(self): expected = "a='b', c='d', e='f'" self.assertEqual(expected, diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index bfb94765..65d5e555 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -19,6 +19,7 @@ import mock import warlock from glanceclient.v2 import schemas +from openstackclient.common import exceptions from openstackclient.image.v2 import image from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes @@ -41,6 +42,191 @@ class TestImage(image_fakes.TestImagev2): self.domain_mock.reset_mock() +class TestImageCreate(TestImage): + + def setUp(self): + super(TestImageCreate, self).setUp() + + self.images_mock.create.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + # This is the return value for utils.find_resource() + self.images_mock.get.return_value = copy.deepcopy(image_fakes.IMAGE) + self.images_mock.update.return_value = fakes.FakeResource( + None, + copy.deepcopy(image_fakes.IMAGE), + loaded=True, + ) + + # Get the command object to test + self.cmd = image.CreateImage(self.app, None) + + def test_image_reserve_no_options(self): + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + arglist = [ + image_fakes.image_name, + ] + verifylist = [ + ('container_format', image.DEFAULT_CONTAINER_FORMAT), + ('disk_format', image.DEFAULT_DISK_FORMAT), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.images_mock.upload.assert_called_with( + mock.ANY, mock.ANY, + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) + + @mock.patch('glanceclient.common.utils.get_data_file', name='Open') + def test_image_reserve_options(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = None + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + arglist = [ + '--container-format', 'ovf', + '--disk-format', 'fs', + '--min-disk', '10', + '--min-ram', '4', + '--protected', + '--private', + image_fakes.image_name, + ] + verifylist = [ + ('container_format', 'ovf'), + ('disk_format', 'fs'), + ('min_disk', 10), + ('min_ram', 4), + ('protected', True), + ('unprotected', False), + ('public', False), + ('private', True), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format='ovf', + disk_format='fs', + min_disk=10, + min_ram=4, + protected=True, + visibility='private', + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.images_mock.upload.assert_called_with( + mock.ANY, mock.ANY, + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) + + @mock.patch('glanceclient.common.utils.get_data_file', name='Open') + def test_image_create_file(self, mock_open): + mock_file = mock.MagicMock(name='File') + mock_open.return_value = mock_file + mock_open.read.return_value = image_fakes.IMAGE_data + mock_exception = { + 'find.side_effect': exceptions.CommandError('x'), + } + self.images_mock.configure_mock(**mock_exception) + + arglist = [ + '--file', 'filer', + '--unprotected', + '--public', + '--property', 'Alpha=1', + '--property', 'Beta=2', + '--tag', 'awesome', + '--tag', 'better', + image_fakes.image_name, + ] + verifylist = [ + ('file', 'filer'), + ('protected', False), + ('unprotected', True), + ('public', True), + ('private', False), + ('properties', {'Alpha': '1', 'Beta': '2'}), + ('tags', ['awesome', 'better']), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # DisplayCommandBase.take_action() returns two tuples + columns, data = self.cmd.take_action(parsed_args) + + # ImageManager.create(name=, **) + self.images_mock.create.assert_called_with( + name=image_fakes.image_name, + container_format=image.DEFAULT_CONTAINER_FORMAT, + disk_format=image.DEFAULT_DISK_FORMAT, + protected=False, + visibility='public', + Alpha='1', + Beta='2', + tags=['awesome', 'better'], + ) + + # Verify update() was not called, if it was show the args + self.assertEqual(self.images_mock.update.call_args_list, []) + + self.images_mock.upload.assert_called_with( + mock.ANY, mock.ANY, + ) + + self.assertEqual(image_fakes.IMAGE_columns, columns) + self.assertEqual(image_fakes.IMAGE_data, data) + + def test_image_dead_options(self): + + arglist = [ + '--owner', 'nobody', + image_fakes.image_name, + ] + verifylist = [ + ('owner', 'nobody'), + ('name', image_fakes.image_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, parsed_args) + + class TestAddProjectToImage(TestImage): def setUp(self): @@ -527,8 +713,7 @@ class TestImageSet(TestImage): 'name': 'new-name', 'owner': 'new-owner', 'min_disk': 2, - 'min_ram': 4, - 'protected': False + 'min_ram': 4 } # ImageManager.update(image, **kwargs) self.images_mock.update.assert_called_with( |
