Skip to content

Commit b2e72e6

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Add image create support for image v2"
2 parents 0857da7 + f5b50df commit b2e72e6

4 files changed

Lines changed: 393 additions & 2 deletions

File tree

doc/source/command-objects/image.rst

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Image v1, v2
77
image create
88
------------
99

10-
*Only supported for Image v1*
10+
*Image v1, v2*
1111

1212
Create/upload an image
1313

@@ -32,6 +32,7 @@ Create/upload an image
3232
[--protected | --unprotected]
3333
[--public | --private]
3434
[--property <key=value> [...] ]
35+
[--tag <tag> [...] ]
3536
<image-name>
3637
3738
.. option:: --id <id>
@@ -42,6 +43,8 @@ Create/upload an image
4243

4344
Upload image to this store
4445

46+
*Image version 1 only.*
47+
4548
.. option:: --container-format <container-format>
4649

4750
Image container format (default: bare)
@@ -54,10 +57,14 @@ Create/upload an image
5457

5558
Image owner project name or ID
5659

60+
*Image version 1 only.*
61+
5762
.. option:: --size <size>
5863

5964
Image size, in bytes (only used with --location and --copy-from)
6065

66+
*Image version 1 only.*
67+
6168
.. option:: --min-disk <disk-gb>
6269

6370
Minimum disk size needed to boot image, in gigabytes
@@ -70,10 +77,14 @@ Create/upload an image
7077

7178
Download image from an existing URL
7279

80+
*Image version 1 only.*
81+
7382
.. option:: --copy-from <image-url>
7483

7584
Copy image from the data store (similar to --location)
7685

86+
*Image version 1 only.*
87+
7788
.. option:: --file <file>
7889

7990
Upload image from local file
@@ -90,6 +101,8 @@ Create/upload an image
90101

91102
Image hash used for verification
92103

104+
*Image version 1 only.*
105+
93106
.. option:: --protected
94107

95108
Prevent image from being deleted
@@ -110,6 +123,12 @@ Create/upload an image
110123

111124
Set a property on this image (repeat for multiple values)
112125

126+
.. option:: --tag <tag>
127+
128+
Set a tag on this image (repeat for multiple values)
129+
130+
.. versionadded:: 2
131+
113132
.. describe:: <image-name>
114133

115134
New image name

openstackclient/image/v2/image.py

Lines changed: 186 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,19 @@
2222
from cliff import command
2323
from cliff import lister
2424
from cliff import show
25-
2625
from glanceclient.common import utils as gc_utils
26+
2727
from openstackclient.api import utils as api_utils
28+
from openstackclient.common import exceptions
2829
from openstackclient.common import parseractions
2930
from openstackclient.common import utils
3031
from openstackclient.identity import common
3132

3233

34+
DEFAULT_CONTAINER_FORMAT = 'bare'
35+
DEFAULT_DISK_FORMAT = 'raw'
36+
37+
3338
class AddProjectToImage(show.ShowOne):
3439
"""Associate project with image"""
3540

@@ -72,6 +77,186 @@ def take_action(self, parsed_args):
7277
return zip(*sorted(six.iteritems(image_member._info)))
7378

7479

80+
class CreateImage(show.ShowOne):
81+
"""Create/upload an image"""
82+
83+
log = logging.getLogger(__name__ + ".CreateImage")
84+
deadopts = ('owner', 'size', 'location', 'copy-from', 'checksum', 'store')
85+
86+
def get_parser(self, prog_name):
87+
parser = super(CreateImage, self).get_parser(prog_name)
88+
# TODO(mordred): add --volume and --force parameters and support
89+
# TODO(bunting): There are additional arguments that v1 supported
90+
# that v2 either doesn't support or supports weirdly.
91+
# --checksum - could be faked clientside perhaps?
92+
# --owner - could be set as an update after the put?
93+
# --location - maybe location add?
94+
# --size - passing image size is actually broken in python-glanceclient
95+
# --copy-from - does not exist in v2
96+
# --store - does not exits in v2
97+
parser.add_argument(
98+
"name",
99+
metavar="<image-name>",
100+
help="New image name",
101+
)
102+
parser.add_argument(
103+
"--id",
104+
metavar="<id>",
105+
help="Image ID to reserve",
106+
)
107+
parser.add_argument(
108+
"--container-format",
109+
default=DEFAULT_CONTAINER_FORMAT,
110+
metavar="<container-format>",
111+
help="Image container format "
112+
"(default: %s)" % DEFAULT_CONTAINER_FORMAT,
113+
)
114+
parser.add_argument(
115+
"--disk-format",
116+
default=DEFAULT_DISK_FORMAT,
117+
metavar="<disk-format>",
118+
help="Image disk format "
119+
"(default: %s)" % DEFAULT_DISK_FORMAT,
120+
)
121+
parser.add_argument(
122+
"--min-disk",
123+
metavar="<disk-gb>",
124+
type=int,
125+
help="Minimum disk size needed to boot image, in gigabytes",
126+
)
127+
parser.add_argument(
128+
"--min-ram",
129+
metavar="<ram-mb>",
130+
type=int,
131+
help="Minimum RAM size needed to boot image, in megabytes",
132+
)
133+
parser.add_argument(
134+
"--file",
135+
metavar="<file>",
136+
help="Upload image from local file",
137+
)
138+
protected_group = parser.add_mutually_exclusive_group()
139+
protected_group.add_argument(
140+
"--protected",
141+
action="store_true",
142+
help="Prevent image from being deleted",
143+
)
144+
protected_group.add_argument(
145+
"--unprotected",
146+
action="store_true",
147+
help="Allow image to be deleted (default)",
148+
)
149+
public_group = parser.add_mutually_exclusive_group()
150+
public_group.add_argument(
151+
"--public",
152+
action="store_true",
153+
help="Image is accessible to the public",
154+
)
155+
public_group.add_argument(
156+
"--private",
157+
action="store_true",
158+
help="Image is inaccessible to the public (default)",
159+
)
160+
parser.add_argument(
161+
"--property",
162+
dest="properties",
163+
metavar="<key=value>",
164+
action=parseractions.KeyValueAction,
165+
help="Set a property on this image "
166+
"(repeat option to set multiple properties)",
167+
)
168+
parser.add_argument(
169+
"--tag",
170+
dest="tags",
171+
metavar="<tag>",
172+
action='append',
173+
help="Set a tag on this image "
174+
"(repeat option to set multiple tags)",
175+
)
176+
for deadopt in self.deadopts:
177+
parser.add_argument(
178+
"--%s" % deadopt,
179+
metavar="<%s>" % deadopt,
180+
dest=deadopt.replace('-', '_'),
181+
help=argparse.SUPPRESS
182+
)
183+
return parser
184+
185+
def take_action(self, parsed_args):
186+
self.log.debug("take_action(%s)", parsed_args)
187+
image_client = self.app.client_manager.image
188+
189+
for deadopt in self.deadopts:
190+
if getattr(parsed_args, deadopt.replace('-', '_'), None):
191+
raise exceptions.CommandError(
192+
"ERROR: --%s was given, which is an Image v1 option"
193+
" that is no longer supported in Image v2" % deadopt)
194+
195+
# Build an attribute dict from the parsed args, only include
196+
# attributes that were actually set on the command line
197+
kwargs = {}
198+
copy_attrs = ('name', 'id',
199+
'container_format', 'disk_format',
200+
'min_disk', 'min_ram',
201+
'tags')
202+
for attr in copy_attrs:
203+
if attr in parsed_args:
204+
val = getattr(parsed_args, attr, None)
205+
if val:
206+
# Only include a value in kwargs for attributes that
207+
# are actually present on the command line
208+
kwargs[attr] = val
209+
# properties should get flattened into the general kwargs
210+
if getattr(parsed_args, 'properties', None):
211+
for k, v in six.iteritems(parsed_args.properties):
212+
kwargs[k] = str(v)
213+
# Handle exclusive booleans with care
214+
# Avoid including attributes in kwargs if an option is not
215+
# present on the command line. These exclusive booleans are not
216+
# a single value for the pair of options because the default must be
217+
# to do nothing when no options are present as opposed to always
218+
# setting a default.
219+
if parsed_args.protected:
220+
kwargs['protected'] = True
221+
if parsed_args.unprotected:
222+
kwargs['protected'] = False
223+
if parsed_args.public:
224+
kwargs['visibility'] = 'public'
225+
if parsed_args.private:
226+
kwargs['visibility'] = 'private'
227+
228+
# open the file first to ensure any failures are handled before the
229+
# image is created
230+
fp = gc_utils.get_data_file(parsed_args)
231+
232+
if fp is None and parsed_args.file:
233+
self.log.warning("Failed to get an image file.")
234+
return {}, {}
235+
236+
image = image_client.images.create(**kwargs)
237+
238+
if fp is not None:
239+
with fp:
240+
try:
241+
image_client.images.upload(image.id, fp)
242+
except Exception as e:
243+
# If the upload fails for some reason attempt to remove the
244+
# dangling queued image made by the create() call above but
245+
# only if the user did not specify an id which indicates
246+
# the Image already exists and should be left alone.
247+
try:
248+
if 'id' not in kwargs:
249+
image_client.images.delete(image.id)
250+
except Exception:
251+
pass # we don't care about this one
252+
raise e # now, throw the upload exception again
253+
254+
# update the image after the data has been uploaded
255+
image = image_client.images.get(image.id)
256+
257+
return zip(*sorted(six.iteritems(image)))
258+
259+
75260
class DeleteImage(command.Command):
76261
"""Delete image(s)"""
77262

0 commit comments

Comments
 (0)