summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/common/quota.py3
-rw-r--r--openstackclient/common/utils.py34
-rw-r--r--openstackclient/compute/client.py10
-rw-r--r--openstackclient/identity/v3/role.py2
-rw-r--r--openstackclient/identity/v3/user.py10
-rw-r--r--openstackclient/image/v2/image.py289
-rw-r--r--openstackclient/shell.py10
-rw-r--r--openstackclient/tests/common/test_quota.py45
-rw-r--r--openstackclient/tests/common/test_utils.py54
-rw-r--r--openstackclient/tests/image/v2/test_image.py189
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(