Revision aa82dd5a kamaki/cli/commands/image.py
b/kamaki/cli/commands/image.py | ||
---|---|---|
57 | 57 |
_commands = [image_cmds] |
58 | 58 |
|
59 | 59 |
|
60 |
about_image_id = [ |
|
61 |
'To see a list of available image ids: /image list'] |
|
60 |
howto_image_file = [ |
|
61 |
'Kamaki commands to:', |
|
62 |
' get current user uuid: /user authenticate', |
|
63 |
' check available containers: /file list', |
|
64 |
' create a new container: /file create <container>', |
|
65 |
' check container contents: /file list <container>', |
|
66 |
' upload files: /file upload <image file> <container>'] |
|
67 |
|
|
68 |
about_image_id = ['To see a list of available image ids: /image list'] |
|
62 | 69 |
|
63 | 70 |
|
64 | 71 |
log = getLogger(__name__) |
... | ... | |
84 | 91 |
# Plankton Image Commands |
85 | 92 |
|
86 | 93 |
|
87 |
def _validate_image_props(json_dict, return_str=False):
|
|
94 |
def _validate_image_meta(json_dict, return_str=False):
|
|
88 | 95 |
""" |
89 | 96 |
:param json_dict" (dict) json-formated, of the form |
90 | 97 |
{"key1": "val1", "key2": "val2", ...} |
91 | 98 |
|
92 | 99 |
:param return_str: (boolean) if true, return a json dump |
93 | 100 |
|
94 |
:returns: (dict) |
|
101 |
:returns: (dict) if return_str is not True, else return str
|
|
95 | 102 |
|
96 | 103 |
:raises TypeError, AttributeError: Invalid json format |
97 | 104 |
|
... | ... | |
99 | 106 |
""" |
100 | 107 |
json_str = dumps(json_dict, indent=2) |
101 | 108 |
for k, v in json_dict.items(): |
102 |
dealbreaker = isinstance(v, dict) or isinstance(v, list) |
|
103 |
assert not dealbreaker, 'Invalid property value for key %s' % k |
|
104 |
dealbreaker = ' ' in k |
|
105 |
assert not dealbreaker, 'Invalid key [%s]' % k |
|
109 |
if k.lower() == 'properties': |
|
110 |
for pk, pv in v.items(): |
|
111 |
prop_ok = not (isinstance(pv, dict) or isinstance(pv, list)) |
|
112 |
assert prop_ok, 'Invalid property value for key %s' % pk |
|
113 |
key_ok = not (' ' in k or '-' in k) |
|
114 |
assert key_ok, 'Invalid property key %s' % k |
|
115 |
continue |
|
116 |
meta_ok = not (isinstance(v, dict) or isinstance(v, list)) |
|
117 |
assert meta_ok, 'Invalid value for meta key %s' % k |
|
118 |
meta_ok = ' ' not in k |
|
119 |
assert meta_ok, 'Invalid meta key [%s]' % k |
|
106 | 120 |
json_dict[k] = '%s' % v |
107 | 121 |
return json_str if return_str else json_dict |
108 | 122 |
|
109 | 123 |
|
110 |
def _load_image_props(filepath):
|
|
124 |
def _load_image_meta(filepath):
|
|
111 | 125 |
""" |
112 | 126 |
:param filepath: (str) the (relative) path of the metafile |
113 | 127 |
|
... | ... | |
120 | 134 |
with open(abspath(filepath)) as f: |
121 | 135 |
meta_dict = load(f) |
122 | 136 |
try: |
123 |
return _validate_image_props(meta_dict)
|
|
137 |
return _validate_image_meta(meta_dict)
|
|
124 | 138 |
except AssertionError: |
125 | 139 |
log.debug('Failed to load properties from file %s' % filepath) |
126 | 140 |
raise |
127 | 141 |
|
128 | 142 |
|
143 |
def _validate_image_location(location): |
|
144 |
""" |
|
145 |
:param location: (str) pithos://<uuid>/<container>/<img-file-path> |
|
146 |
|
|
147 |
:returns: (<uuid>, <container>, <img-file-path>) |
|
148 |
|
|
149 |
:raises AssertionError: if location is invalid |
|
150 |
""" |
|
151 |
prefix = 'pithos://' |
|
152 |
msg = 'Invalid prefix for location %s , try: %s' % (location, prefix) |
|
153 |
assert location.startswith(prefix), msg |
|
154 |
service, sep, rest = location.partition('://') |
|
155 |
assert sep and rest, 'Location %s is missing uuid' % location |
|
156 |
uuid, sep, rest = rest.partition('/') |
|
157 |
assert sep and rest, 'Location %s is missing container' % location |
|
158 |
container, sep, img_path = rest.partition('/') |
|
159 |
assert sep and img_path, 'Location %s is missing image path' % location |
|
160 |
return uuid, container, img_path |
|
161 |
|
|
162 |
|
|
129 | 163 |
@command(image_cmds) |
130 | 164 |
class image_list(_init_image, _optional_json): |
131 | 165 |
"""List images accessible by user""" |
... | ... | |
244 | 278 |
'set container format', |
245 | 279 |
'--container-format'), |
246 | 280 |
disk_format=ValueArgument('set disk format', '--disk-format'), |
247 |
#id=ValueArgument('set image ID', '--id'), |
|
248 | 281 |
owner=ValueArgument('set image owner (admin only)', '--owner'), |
249 | 282 |
properties=KeyValueArgument( |
250 | 283 |
'add property in key=value form (can be repeated)', |
251 | 284 |
('-p', '--property')), |
252 | 285 |
is_public=FlagArgument('mark image as public', '--public'), |
253 | 286 |
size=IntArgument('set image size', '--size'), |
254 |
property_file=ValueArgument( |
|
255 |
'Load properties from a json-formated file <img-file>.meta :' |
|
256 |
'{"key1": "val1", "key2": "val2", ...}', |
|
257 |
('--property-file')), |
|
258 |
prop_file_force=FlagArgument( |
|
259 |
'Store remote property object, even it already exists', |
|
260 |
('-f', '--force-upload-property-file')), |
|
261 |
no_prop_file_upload=FlagArgument( |
|
262 |
'Do not store properties in remote property file', |
|
263 |
('--no-property-file-upload')), |
|
264 |
container=ValueArgument( |
|
265 |
'Remote image container', ('-C', '--container')), |
|
266 |
fileowner=ValueArgument( |
|
267 |
'UUID of the user who owns the image file', ('--fileowner')) |
|
287 |
metafile=ValueArgument( |
|
288 |
'Load metadata from a json-formated file <img-file>.meta :' |
|
289 |
'{"key1": "val1", "key2": "val2", ..., "properties: {...}"}', |
|
290 |
('--metafile')), |
|
291 |
metafile_force=FlagArgument( |
|
292 |
'Store remote metadata object, even if it already exists', |
|
293 |
('-f', '--force')), |
|
294 |
no_metafile_upload=FlagArgument( |
|
295 |
'Do not store metadata in remote meta file', |
|
296 |
('--no-metafile-upload')), |
|
268 | 297 |
|
269 | 298 |
) |
270 | 299 |
|
271 | 300 |
def _get_uuid(self): |
272 |
uuid = self['fileowner'] or self.config.get('image', 'fileowner') |
|
273 |
if uuid: |
|
274 |
return uuid |
|
275 | 301 |
atoken = self.client.token |
276 | 302 |
user = AstakosClient(self.config.get('user', 'url'), atoken) |
277 | 303 |
return user.term('uuid') |
278 | 304 |
|
279 |
def _get_pithos_client(self, uuid, container): |
|
305 |
def _get_pithos_client(self, container): |
|
306 |
if self['no_metafile_upload']: |
|
307 |
return None |
|
280 | 308 |
purl = self.config.get('file', 'url') |
281 | 309 |
ptoken = self.client.token |
282 |
return PithosClient(purl, ptoken, uuid, container)
|
|
310 |
return PithosClient(purl, ptoken, self._get_uuid(), container)
|
|
283 | 311 |
|
284 |
def _store_remote_property_file(self, pclient, remote_path, properties):
|
|
312 |
def _store_remote_metafile(self, pclient, remote_path, metadata):
|
|
285 | 313 |
return pclient.upload_from_string( |
286 |
remote_path, _validate_image_props(properties, return_str=True)) |
|
287 |
|
|
288 |
def _get_container_path(self, container_path): |
|
289 |
container = self['container'] or self.config.get('image', 'container') |
|
290 |
if container: |
|
291 |
return container, container_path |
|
292 |
|
|
293 |
container, sep, path = container_path.partition(':') |
|
294 |
if not sep or not container or not path: |
|
295 |
raiseCLIError( |
|
296 |
'%s is not a valid pithos+ remote location' % container_path, |
|
297 |
importance=2, |
|
298 |
details=[ |
|
299 |
'To set "image" as container and "my_dir/img.diskdump" as', |
|
300 |
'the image path, try one of the following as ' |
|
301 |
'container:path', |
|
302 |
'- <image container>:<remote path>', |
|
303 |
' e.g. image:/my_dir/img.diskdump', |
|
304 |
'- <remote path> -C <image container>', |
|
305 |
' e.g. /my_dir/img.diskdump -C image']) |
|
306 |
return container, path |
|
314 |
remote_path, _validate_image_meta(metadata, return_str=True)) |
|
307 | 315 |
|
308 |
@errors.generic.all |
|
309 |
@errors.plankton.image_file |
|
310 |
@errors.plankton.connection |
|
311 |
def _run(self, name, container_path): |
|
312 |
container, path = self._get_container_path(container_path) |
|
313 |
uuid = self._get_uuid() |
|
314 |
prop_path = '%s.meta' % path |
|
315 |
|
|
316 |
pclient = None if ( |
|
317 |
self['no_prop_file_upload']) else self._get_pithos_client( |
|
318 |
uuid, container) |
|
319 |
if pclient and not self['prop_file_force']: |
|
316 |
def _load_params_from_file(self, location): |
|
317 |
params, properties = dict(), dict() |
|
318 |
pfile = self['metafile'] |
|
319 |
if pfile: |
|
320 | 320 |
try: |
321 |
pclient.get_object_info(prop_path) |
|
322 |
raiseCLIError('Property file %s: %s already exists' % ( |
|
323 |
container, prop_path)) |
|
324 |
except ClientError as ce: |
|
325 |
if ce.status != 404: |
|
326 |
raise |
|
327 |
|
|
328 |
location = 'pithos://%s/%s/%s' % (uuid, container, path) |
|
321 |
for k, v in _load_image_meta(pfile).items(): |
|
322 |
key = k.lower().replace('-', '_') |
|
323 |
if k == 'properties': |
|
324 |
for pk, pv in v.items(): |
|
325 |
properties[pk.upper().replace('-', '_')] = pv |
|
326 |
elif key == 'name': |
|
327 |
continue |
|
328 |
elif key == 'location': |
|
329 |
if location: |
|
330 |
continue |
|
331 |
location = v |
|
332 |
else: |
|
333 |
params[key] = v |
|
334 |
except Exception as e: |
|
335 |
raiseCLIError(e, 'Invalid json metadata config file') |
|
336 |
return params, properties, location |
|
329 | 337 |
|
330 |
params = {}
|
|
338 |
def _load_params_from_args(self, params, properties):
|
|
331 | 339 |
for key in set([ |
332 | 340 |
'checksum', |
333 | 341 |
'container_format', |
... | ... | |
336 | 344 |
'size', |
337 | 345 |
'is_public']).intersection(self.arguments): |
338 | 346 |
params[key] = self[key] |
339 |
properties = self['properties'] |
|
347 |
for k, v in self['properties'].items(): |
|
348 |
properties[k.upper().replace('-', '_')] = v |
|
340 | 349 |
|
341 |
#load properties |
|
342 |
properties = dict() |
|
343 |
pfile = self['property_file'] |
|
344 |
if pfile: |
|
350 |
def _validate_location(self, location): |
|
351 |
if not location: |
|
352 |
raiseCLIError( |
|
353 |
'No image file location provided', |
|
354 |
importance=2, details=[ |
|
355 |
'An image location is needed. Image location format:', |
|
356 |
' pithos://<uuid>/<container>/<path>', |
|
357 |
' an image file at the above location must exist.' |
|
358 |
] + howto_image_file) |
|
359 |
try: |
|
360 |
return _validate_image_location(location) |
|
361 |
except AssertionError as ae: |
|
362 |
raiseCLIError( |
|
363 |
ae, 'Invalid image location format', |
|
364 |
importance=1, details=[ |
|
365 |
'Valid image location format:', |
|
366 |
' pithos://<uuid>/<container>/<img-file-path>' |
|
367 |
] + howto_image_file) |
|
368 |
|
|
369 |
@errors.generic.all |
|
370 |
@errors.plankton.connection |
|
371 |
def _run(self, name, location): |
|
372 |
(params, properties, location) = self._load_params_from_file(location) |
|
373 |
uuid, container, img_path = self._validate_location(location) |
|
374 |
self._load_params_from_args(params, properties) |
|
375 |
pclient = self._get_pithos_client(container) |
|
376 |
|
|
377 |
#check if metafile exists |
|
378 |
meta_path = '%s.meta' % img_path |
|
379 |
if pclient and not self['metafile_force']: |
|
345 | 380 |
try: |
346 |
for k, v in _load_image_props(pfile).items(): |
|
347 |
properties[k.lower()] = v |
|
348 |
except Exception as e: |
|
381 |
pclient.get_object_info(meta_path) |
|
382 |
raiseCLIError('Metadata file %s:%s already exists' % ( |
|
383 |
container, meta_path)) |
|
384 |
except ClientError as ce: |
|
385 |
if ce.status != 404: |
|
386 |
raise |
|
387 |
|
|
388 |
#register the image |
|
389 |
try: |
|
390 |
r = self.client.register(name, location, params, properties) |
|
391 |
except ClientError as ce: |
|
392 |
if ce.status in (400, ): |
|
349 | 393 |
raiseCLIError( |
350 |
e, 'Format error in property file %s' % pfile,
|
|
394 |
ce, 'Nonexistent image file location %s' % location,
|
|
351 | 395 |
details=[ |
352 |
'Expected content format:', |
|
353 |
' {', |
|
354 |
' "key1": "value1",', |
|
355 |
' "key2": "value2",', |
|
356 |
' ...', |
|
357 |
' }', |
|
358 |
'', |
|
359 |
'Parser:' |
|
360 |
]) |
|
361 |
for k, v in self['properties'].items(): |
|
362 |
properties[k.lower()] = v |
|
363 |
|
|
364 |
self._print([self.client.register(name, location, params, properties)]) |
|
396 |
'Make sure the image file exists'] + howto_image_file) |
|
397 |
raise |
|
398 |
self._print(r, print_dict) |
|
365 | 399 |
|
400 |
#upload the metadata file |
|
366 | 401 |
if pclient: |
367 |
prop_headers = pclient.upload_from_string( |
|
368 |
prop_path, _validate_image_props(properties, return_str=True)) |
|
402 |
try: |
|
403 |
meta_headers = pclient.upload_from_string( |
|
404 |
meta_path, dumps(r, indent=2)) |
|
405 |
except TypeError: |
|
406 |
print('Failed to dump metafile %s:%s' % (container, meta_path)) |
|
407 |
return |
|
369 | 408 |
if self['json_output']: |
370 | 409 |
print_json(dict( |
371 |
property_file_location='%s:%s' % (container, prop_path),
|
|
372 |
headers=prop_headers))
|
|
410 |
metafile_location='%s:%s' % (container, meta_path),
|
|
411 |
headers=meta_headers))
|
|
373 | 412 |
else: |
374 |
print('Property file uploaded as %s:%s (version %s)' % (
|
|
375 |
container, prop_path, prop_headers['x-object-version']))
|
|
413 |
print('Metadata file uploaded as %s:%s (version %s)' % (
|
|
414 |
container, meta_path, meta_headers['x-object-version']))
|
|
376 | 415 |
|
377 |
def main(self, name, container___path):
|
|
416 |
def main(self, name, location=None):
|
|
378 | 417 |
super(self.__class__, self)._run() |
379 |
self._run(name, container___path)
|
|
418 |
self._run(name, location)
|
|
380 | 419 |
|
381 | 420 |
|
382 | 421 |
@command(image_cmds) |
Also available in: Unified diff