Skip to content

Commit f5b50df

Browse files
emontyNiall BuntingSean Perry
authored andcommitted
Add image create support for image v2
We have it for v1, but v2 is the future. There are two differences, things in v2 do not go into a properties dict, and the actual image data needs to get uploaded as a second step. Closes-Bug: 1405562 Co-Authored-By: Niall Bunting <niall.bunting@hp.com> Co-Authored-By: Sean Perry <sean.perry@hp.com> Change-Id: If7b81c4a6746c8a1eb0302c96e045fb0f457d67b
1 parent b288fbf commit f5b50df

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)