Merge "Add image create support for image v2"

This commit is contained in:
Jenkins 2015-09-21 21:35:33 +00:00 committed by Gerrit Code Review
commit b2e72e6aee
4 changed files with 393 additions and 2 deletions
doc/source/command-objects
openstackclient
image/v2
tests/image/v2
setup.cfg

@ -7,7 +7,7 @@ Image v1, v2
image create image create
------------ ------------
*Only supported for Image v1* *Image v1, v2*
Create/upload an image Create/upload an image
@ -32,6 +32,7 @@ Create/upload an image
[--protected | --unprotected] [--protected | --unprotected]
[--public | --private] [--public | --private]
[--property <key=value> [...] ] [--property <key=value> [...] ]
[--tag <tag> [...] ]
<image-name> <image-name>
.. option:: --id <id> .. option:: --id <id>
@ -42,6 +43,8 @@ Create/upload an image
Upload image to this store Upload image to this store
*Image version 1 only.*
.. option:: --container-format <container-format> .. option:: --container-format <container-format>
Image container format (default: bare) Image container format (default: bare)
@ -54,10 +57,14 @@ Create/upload an image
Image owner project name or ID Image owner project name or ID
*Image version 1 only.*
.. option:: --size <size> .. option:: --size <size>
Image size, in bytes (only used with --location and --copy-from) Image size, in bytes (only used with --location and --copy-from)
*Image version 1 only.*
.. option:: --min-disk <disk-gb> .. option:: --min-disk <disk-gb>
Minimum disk size needed to boot image, in gigabytes Minimum disk size needed to boot image, in gigabytes
@ -70,10 +77,14 @@ Create/upload an image
Download image from an existing URL Download image from an existing URL
*Image version 1 only.*
.. option:: --copy-from <image-url> .. option:: --copy-from <image-url>
Copy image from the data store (similar to --location) Copy image from the data store (similar to --location)
*Image version 1 only.*
.. option:: --file <file> .. option:: --file <file>
Upload image from local file Upload image from local file
@ -90,6 +101,8 @@ Create/upload an image
Image hash used for verification Image hash used for verification
*Image version 1 only.*
.. option:: --protected .. option:: --protected
Prevent image from being deleted Prevent image from being deleted
@ -110,6 +123,12 @@ Create/upload an image
Set a property on this image (repeat for multiple values) Set a property on this image (repeat for multiple values)
.. option:: --tag <tag>
Set a tag on this image (repeat for multiple values)
.. versionadded:: 2
.. describe:: <image-name> .. describe:: <image-name>
New image name New image name

@ -22,14 +22,19 @@ import six
from cliff import command from cliff import command
from cliff import lister from cliff import lister
from cliff import show from cliff import show
from glanceclient.common import utils as gc_utils from glanceclient.common import utils as gc_utils
from openstackclient.api import utils as api_utils from openstackclient.api import utils as api_utils
from openstackclient.common import exceptions
from openstackclient.common import parseractions from openstackclient.common import parseractions
from openstackclient.common import utils from openstackclient.common import utils
from openstackclient.identity import common from openstackclient.identity import common
DEFAULT_CONTAINER_FORMAT = 'bare'
DEFAULT_DISK_FORMAT = 'raw'
class AddProjectToImage(show.ShowOne): class AddProjectToImage(show.ShowOne):
"""Associate project with image""" """Associate project with image"""
@ -72,6 +77,186 @@ class AddProjectToImage(show.ShowOne):
return zip(*sorted(six.iteritems(image_member._info))) 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)
return zip(*sorted(six.iteritems(image)))
class DeleteImage(command.Command): class DeleteImage(command.Command):
"""Delete image(s)""" """Delete image(s)"""

@ -19,6 +19,7 @@ import mock
import warlock import warlock
from glanceclient.v2 import schemas from glanceclient.v2 import schemas
from openstackclient.common import exceptions
from openstackclient.image.v2 import image from openstackclient.image.v2 import image
from openstackclient.tests import fakes from openstackclient.tests import fakes
from openstackclient.tests.identity.v3 import fakes as identity_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() 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): class TestAddProjectToImage(TestImage):
def setUp(self): def setUp(self):

@ -316,6 +316,7 @@ openstack.image.v1 =
openstack.image.v2 = openstack.image.v2 =
image_add_project = openstackclient.image.v2.image:AddProjectToImage image_add_project = openstackclient.image.v2.image:AddProjectToImage
image_create = openstackclient.image.v2.image:CreateImage
image_delete = openstackclient.image.v2.image:DeleteImage image_delete = openstackclient.image.v2.image:DeleteImage
image_list = openstackclient.image.v2.image:ListImage image_list = openstackclient.image.v2.image:ListImage
image_remove_project = openstackclient.image.v2.image:RemoveProjectImage image_remove_project = openstackclient.image.v2.image:RemoveProjectImage