1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.command
34 from json import load, dumps
36 from logging import getLogger
37 from io import StringIO
38 from pydoc import pager
40 from kamaki.cli import command
41 from kamaki.cli.command_tree import CommandTree
42 from kamaki.cli.utils import filter_dicts_by_dict
43 from kamaki.clients.image import ImageClient
44 from kamaki.clients.pithos import PithosClient
45 from kamaki.clients.astakos import AstakosClient
46 from kamaki.clients import ClientError
47 from kamaki.cli.argument import (
48 FlagArgument, ValueArgument, RepeatableArgument, KeyValueArgument,
49 IntArgument, ProgressBarArgument)
50 from kamaki.cli.commands.cyclades import _init_cyclades
51 from kamaki.cli.errors import raiseCLIError, CLIBaseUrlError
52 from kamaki.cli.commands import _command_init, errors, addLogSettings
53 from kamaki.cli.commands import (
54 _optional_output_cmd, _optional_json, _name_filter, _id_filter)
57 image_cmds = CommandTree(
59 'Cyclades/Plankton API image commands\n'
60 'image compute:\tCyclades/Compute API image commands')
61 _commands = [image_cmds]
65 'Kamaki commands to:',
66 ' get current user id: /user authenticate',
67 ' check available containers: /file list',
68 ' create a new container: /file create <container>',
69 ' check container contents: /file list <container>',
70 ' upload files: /file upload <image file> <container>',
71 ' register an image: /image register <image name> <container>:<path>']
73 about_image_id = ['To see a list of available image ids: /image list']
76 log = getLogger(__name__)
79 class _init_image(_command_init):
83 if getattr(self, 'cloud', None):
84 img_url = self._custom_url('image') or self._custom_url('plankton')
86 token = self._custom_token('image') or self._custom_token(
87 'plankton') or self.config.get_cloud(self.cloud, 'token')
88 self.client = ImageClient(base_url=img_url, token=token)
90 if getattr(self, 'auth_base', False):
91 plankton_endpoints = self.auth_base.get_service_endpoints(
92 self._custom_type('image') or self._custom_type(
93 'plankton') or 'image',
94 self._custom_version('image') or self._custom_version(
96 base_url = plankton_endpoints['publicURL']
97 token = self.auth_base.token
99 raise CLIBaseUrlError(service='plankton')
100 self.client = ImageClient(base_url=base_url, token=token)
106 # Plankton Image Commands
109 def _validate_image_meta(json_dict, return_str=False):
111 :param json_dict" (dict) json-formated, of the form
112 {"key1": "val1", "key2": "val2", ...}
114 :param return_str: (boolean) if true, return a json dump
116 :returns: (dict) if return_str is not True, else return str
118 :raises TypeError, AttributeError: Invalid json format
120 :raises AssertionError: Valid json but invalid image properties dict
122 json_str = dumps(json_dict, indent=2)
123 for k, v in json_dict.items():
124 if k.lower() == 'properties':
125 for pk, pv in v.items():
126 prop_ok = not (isinstance(pv, dict) or isinstance(pv, list))
127 assert prop_ok, 'Invalid property value for key %s' % pk
128 key_ok = not (' ' in k or '-' in k)
129 assert key_ok, 'Invalid property key %s' % k
131 meta_ok = not (isinstance(v, dict) or isinstance(v, list))
132 assert meta_ok, 'Invalid value for meta key %s' % k
133 meta_ok = ' ' not in k
134 assert meta_ok, 'Invalid meta key [%s]' % k
135 json_dict[k] = '%s' % v
136 return json_str if return_str else json_dict
139 def _load_image_meta(filepath):
141 :param filepath: (str) the (relative) path of the metafile
143 :returns: (dict) json_formated
145 :raises TypeError, AttributeError: Invalid json format
147 :raises AssertionError: Valid json but invalid image properties dict
149 with open(path.abspath(filepath)) as f:
152 return _validate_image_meta(meta_dict)
153 except AssertionError:
154 log.debug('Failed to load properties from file %s' % filepath)
158 def _validate_image_location(location):
160 :param location: (str) pithos://<user-id>/<container>/<image-path>
162 :returns: (<user-id>, <container>, <image-path>)
164 :raises AssertionError: if location is invalid
167 msg = 'Invalid prefix for location %s , try: %s' % (location, prefix)
168 assert location.startswith(prefix), msg
169 service, sep, rest = location.partition('://')
170 assert sep and rest, 'Location %s is missing user-id' % location
171 uuid, sep, rest = rest.partition('/')
172 assert sep and rest, 'Location %s is missing container' % location
173 container, sep, img_path = rest.partition('/')
174 assert sep and img_path, 'Location %s is missing image path' % location
175 return uuid, container, img_path
179 class image_list(_init_image, _optional_json, _name_filter, _id_filter):
180 """List images accessible by user"""
184 'status', 'container_format', 'disk_format', 'size')
187 detail=FlagArgument('show detailed output', ('-l', '--details')),
188 container_format=ValueArgument(
189 'filter by container format',
190 '--container-format'),
191 disk_format=ValueArgument('filter by disk format', '--disk-format'),
192 size_min=IntArgument('filter by minimum size', '--size-min'),
193 size_max=IntArgument('filter by maximum size', '--size-max'),
194 status=ValueArgument('filter by status', '--status'),
195 owner=ValueArgument('filter by owner', '--owner'),
196 owner_name=ValueArgument('filter by owners username', '--owner-name'),
198 'order by FIELD ( - to reverse order)',
201 limit=IntArgument('limit number of listed images', ('-n', '--number')),
203 'output results in pages (-n to set items per page, default 10)',
205 enum=FlagArgument('Enumerate results', '--enumerate'),
206 prop=KeyValueArgument('filter by property key=value', ('--property')),
207 prop_like=KeyValueArgument(
208 'fliter by property key=value where value is part of actual value',
209 ('--property-like')),
212 def _filter_by_owner(self, images):
213 ouuid = self['owner'] or self._username2uuid(self['owner_name'])
214 return filter_dicts_by_dict(images, dict(owner=ouuid))
216 def _add_owner_name(self, images):
217 uuids = self._uuids2usernames(
218 list(set([img['owner'] for img in images])))
220 img['owner'] += ' (%s)' % uuids[img['owner']]
223 def _filter_by_properties(self, images):
226 props = [dict(img['properties'])]
228 props = filter_dicts_by_dict(props, self['prop'])
229 if props and self['prop_like']:
230 props = filter_dicts_by_dict(
231 props, self['prop_like'], exact_match=False)
233 new_images.append(img)
237 @errors.cyclades.connection
239 super(self.__class__, self)._run()
247 'status']).intersection(self.arguments):
248 filters[arg] = self[arg]
250 order = self['order']
251 detail = self['detail'] or (
252 self['prop'] or self['prop_like']) or (
253 self['owner'] or self['owner_name'])
255 images = self.client.list_public(detail, filters, order)
257 if self['owner'] or self['owner_name']:
258 images = self._filter_by_owner(images)
259 if self['prop'] or self['prop_like']:
260 images = self._filter_by_properties(images)
261 images = self._filter_by_id(images)
262 images = self._non_exact_name_filter(images)
264 if self['detail'] and not (
265 self['json_output'] or self['output_format']):
266 images = self._add_owner_name(images)
267 elif detail and not self['detail']:
269 for key in set(img).difference(self.PERMANENTS):
271 kwargs = dict(with_enumeration=self['enum'])
273 images = images[:self['limit']]
275 kwargs['out'] = StringIO()
277 self._print(images, **kwargs)
279 pager(kwargs['out'].getvalue())
282 super(self.__class__, self)._run()
287 class image_info(_init_image, _optional_json):
288 """Get image metadata"""
291 @errors.plankton.connection
293 def _run(self, image_id):
294 meta = self.client.get_meta(image_id)
295 if not (self['json_output'] or self['output_format']):
296 meta['owner'] += ' (%s)' % self._uuid2username(meta['owner'])
297 self._print(meta, self.print_dict)
299 def main(self, image_id):
300 super(self.__class__, self)._run()
301 self._run(image_id=image_id)
305 class image_modify(_init_image, _optional_json):
306 """Add / update metadata and properties for an image
307 The original image preserves the values that are not affected
311 image_name=ValueArgument('Change name', '--name'),
312 disk_format=ValueArgument('Change disk format', '--disk-format'),
313 container_format=ValueArgument(
314 'Change container format', '--container-format'),
315 status=ValueArgument('Change status', '--status'),
316 publish=FlagArgument('Publish the image', '--publish'),
317 unpublish=FlagArgument('Unpublish the image', '--unpublish'),
318 property_to_set=KeyValueArgument(
319 'set property in key=value form (can be repeated)',
320 ('-p', '--property-set')),
321 property_to_del=RepeatableArgument(
322 'Delete property by key (can be repeated)', '--property-del')
325 'image_name', 'disk_format', 'container_format', 'status', 'publish',
326 'unpublish', 'property_to_set']
329 @errors.plankton.connection
331 def _run(self, image_id):
332 meta = self.client.get_meta(image_id)
333 for k, v in self['property_to_set'].items():
334 meta['properties'][k.upper()] = v
335 for k in self['property_to_del']:
336 meta['properties'][k.upper()] = None
337 self._optional_output(self.client.update_image(
339 name=self['image_name'],
340 disk_format=self['disk_format'],
341 container_format=self['container_format'],
342 status=self['status'],
343 public=self['publish'] or self['unpublish'] or None,
344 **meta['properties']))
346 def main(self, image_id):
347 super(self.__class__, self)._run()
348 self._run(image_id=image_id)
352 class image_register(_init_image, _optional_json):
353 """(Re)Register an image file to an Image service
354 The image file must be stored at a pithos repository
355 Some metadata can be set by user (e.g., disk-format) while others are set
356 only automatically (e.g., image id). There are also some custom user
357 metadata, called properties.
358 A register command creates a remote meta file at
359 . <container>:<image path>.meta
360 Users may download and edit this file and use it to re-register one or more
362 In case of a meta file, runtime arguments for metadata or properties
363 override meta file settings.
366 container_info_cache = {}
369 checksum=ValueArgument('Set image checksum', '--checksum'),
370 container_format=ValueArgument(
371 'Set container format', '--container-format'),
372 disk_format=ValueArgument('Set disk format', '--disk-format'),
373 owner_name=ValueArgument('Set user uuid by user name', '--owner-name'),
374 properties=KeyValueArgument(
375 'Add property (user-specified metadata) in key=value form'
377 ('-p', '--property')),
378 is_public=FlagArgument('Mark image as public', '--public'),
379 size=IntArgument('Set image size in bytes', '--size'),
380 metafile=ValueArgument(
381 'Load metadata from a json-formated file <img-file>.meta :'
382 '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
384 metafile_force=FlagArgument(
385 'Overide remote metadata file', ('-f', '--force')),
386 no_metafile_upload=FlagArgument(
387 'Do not store metadata in remote meta file',
388 ('--no-metafile-upload')),
389 container=ValueArgument(
390 'Pithos+ container containing the image file',
391 ('-C', '--container')),
392 uuid=ValueArgument('Custom user uuid', '--uuid'),
393 local_image_path=ValueArgument(
394 'Local image file path to upload and register '
395 '(still need target file in the form container:remote-path )',
396 '--upload-image-file'),
397 progress_bar=ProgressBarArgument(
398 'Do not use progress bar', '--no-progress-bar', default=False)
401 def _get_user_id(self):
402 atoken = self.client.token
403 if getattr(self, 'auth_base', False):
404 return self.auth_base.term('id', atoken)
406 astakos_url = self.config.get('user', 'url') or self.config.get(
409 raise CLIBaseUrlError(service='astakos')
410 user = AstakosClient(astakos_url, atoken)
411 return user.term('id')
413 def _get_pithos_client(self, container):
414 if self['no_metafile_upload']:
416 ptoken = self.client.token
417 if getattr(self, 'auth_base', False):
418 pithos_endpoints = self.auth_base.get_service_endpoints(
420 purl = pithos_endpoints['publicURL']
422 purl = self.config.get_cloud('pithos', 'url')
424 raise CLIBaseUrlError(service='pithos')
425 return PithosClient(purl, ptoken, self._get_user_id(), container)
427 def _store_remote_metafile(self, pclient, remote_path, metadata):
428 return pclient.upload_from_string(
429 remote_path, _validate_image_meta(metadata, return_str=True),
430 container_info_cache=self.container_info_cache)
432 def _load_params_from_file(self, location):
433 params, properties = dict(), dict()
434 pfile = self['metafile']
437 for k, v in _load_image_meta(pfile).items():
438 key = k.lower().replace('-', '_')
439 if key == 'properties':
440 for pk, pv in v.items():
441 properties[pk.upper().replace('-', '_')] = pv
444 elif key == 'location':
450 except Exception as e:
451 raiseCLIError(e, 'Invalid json metadata config file')
452 return params, properties, location
454 def _load_params_from_args(self, params, properties):
461 'is_public']).intersection(self.arguments):
462 params[key] = self[key]
463 for k, v in self['properties'].items():
464 properties[k.upper().replace('-', '_')] = v
466 def _validate_location(self, location):
469 'No image file location provided',
470 importance=2, details=[
471 'An image location is needed. Image location format:',
472 ' <container>:<path>',
473 ' where an image file at the above location must exist.'
474 ] + howto_image_file)
476 return _validate_image_location(location)
477 except AssertionError as ae:
479 ae, 'Invalid image location format',
480 importance=1, details=[
481 'Valid image location format:',
482 ' <container>:<img-file-path>'
483 ] + howto_image_file)
486 def _old_location_format(location):
489 if location.startswith(prefix):
490 uuid, sep, rest = location[len(prefix):].partition('/')
491 container, sep, path = rest.partition('/')
492 return (uuid, container, path)
493 except Exception as e:
494 raiseCLIError(e, 'Invalid location format', details=[
495 'Correct location format:', ' <container>:<image path>'])
498 def _mine_location(self, container_path):
499 old_response = self._old_location_format(container_path)
502 uuid = self['uuid'] or (self._username2uuid(self['owner_name']) if (
503 self['owner_name']) else self._get_user_id())
505 if self['owner_name']:
506 raiseCLIError('No user with username %s' % self['owner_name'])
507 raiseCLIError('Failed to get user uuid', details=[
508 'For details on current user:',
510 'To authenticate a new user through a user token:',
511 ' /user authenticate <token>'])
512 if self['container']:
513 return uuid, self['container'], container_path
514 container, sep, path = container_path.partition(':')
515 if not (bool(container) and bool(path)):
517 'Incorrect container-path format', importance=1, details=[
518 'Use : to seperate container form path',
519 ' <container>:<image-path>',
521 'Use -C to specifiy a container',
522 ' -C <container> <image-path>'] + howto_image_file)
524 return uuid, container, path
527 @errors.plankton.connection
528 @errors.pithos.container
529 def _run(self, name, uuid, dst_cont, img_path):
530 if self['local_image_path']:
531 with open(self['local_image_path']) as f:
532 pithos = self._get_pithos_client(dst_cont)
533 (pbar, upload_cb) = self._safe_progress_bar('Uploading')
535 hash_bar = pbar.clone()
536 hash_cb = hash_bar.get_generator('Calculating hashes')
537 pithos.upload_object(
539 hash_cb=hash_cb, upload_cb=upload_cb,
540 container_info_cache=self.container_info_cache)
543 location = 'pithos://%s/%s/%s' % (uuid, dst_cont, img_path)
544 (params, properties, new_loc) = self._load_params_from_file(location)
545 if location != new_loc:
546 uuid, dst_cont, img_path = self._validate_location(new_loc)
547 self._load_params_from_args(params, properties)
548 pclient = self._get_pithos_client(dst_cont)
550 #check if metafile exists
551 meta_path = '%s.meta' % img_path
552 if pclient and not self['metafile_force']:
554 pclient.get_object_info(meta_path)
556 'Metadata file %s:%s already exists, abort' % (
557 dst_cont, meta_path),
558 details=['Registration ABORTED', 'Try -f to overwrite'])
559 except ClientError as ce:
565 r = self.client.register(name, location, params, properties)
566 except ClientError as ce:
567 if ce.status in (400, 404):
569 ce, 'Nonexistent image file location\n\t%s' % location,
571 'Does the image file %s exist at container %s ?' % (
572 img_path, dst_cont)] + howto_image_file)
574 r['owner'] += ' (%s)' % self._uuid2username(r['owner'])
575 self._print(r, self.print_dict)
577 #upload the metadata file
580 meta_headers = pclient.upload_from_string(
581 meta_path, dumps(r, indent=2),
582 container_info_cache=self.container_info_cache)
585 'Failed to dump metafile %s:%s' % (dst_cont, meta_path))
587 if self['json_output'] or self['output_format']:
588 self.print_json(dict(
589 metafile_location='%s:%s' % (dst_cont, meta_path),
590 headers=meta_headers))
592 self.error('Metadata file uploaded as %s:%s (version %s)' % (
593 dst_cont, meta_path, meta_headers['x-object-version']))
595 def main(self, name, container___image_path):
596 super(self.__class__, self)._run()
597 self._run(name, *self._mine_location(container___image_path))
601 class image_unregister(_init_image, _optional_output_cmd):
602 """Unregister an image (does not delete the image file)"""
605 @errors.plankton.connection
607 def _run(self, image_id):
608 self._optional_output(self.client.unregister(image_id))
610 def main(self, image_id):
611 super(self.__class__, self)._run()
612 self._run(image_id=image_id)
616 class image_shared(_init_image, _optional_json):
617 """List images shared by a member"""
620 @errors.plankton.connection
621 def _run(self, member):
622 r = self.client.list_shared(member)
623 self._print(r, title=('image_id',))
625 def main(self, member_id_or_username):
626 super(self.__class__, self)._run()
627 self._run(member_id_or_username)
631 class image_members(_init_image):
632 """Manage members. Members of an image are users who can modify it"""
636 class image_members_list(_init_image, _optional_json):
637 """List members of an image"""
640 @errors.plankton.connection
642 def _run(self, image_id):
643 members = self.client.list_members(image_id)
644 if not (self['json_output'] or self['output_format']):
645 uuids = [member['member_id'] for member in members]
646 usernames = self._uuids2usernames(uuids)
647 for member in members:
648 member['member_id'] += ' (%s)' % usernames[member['member_id']]
649 self._print(members, title=('member_id',))
651 def main(self, image_id):
652 super(self.__class__, self)._run()
653 self._run(image_id=image_id)
657 class image_members_add(_init_image, _optional_output_cmd):
658 """Add a member to an image"""
661 @errors.plankton.connection
663 def _run(self, image_id=None, member=None):
664 self._optional_output(self.client.add_member(image_id, member))
666 def main(self, image_id, member_id):
667 super(self.__class__, self)._run()
668 self._run(image_id=image_id, member=member_id)
672 class image_members_delete(_init_image, _optional_output_cmd):
673 """Remove a member from an image"""
676 @errors.plankton.connection
678 def _run(self, image_id=None, member=None):
679 self._optional_output(self.client.remove_member(image_id, member))
681 def main(self, image_id, member):
682 super(self.__class__, self)._run()
683 self._run(image_id=image_id, member=member)
687 class image_members_set(_init_image, _optional_output_cmd):
688 """Set the members of an image"""
691 @errors.plankton.connection
693 def _run(self, image_id, members):
694 self._optional_output(self.client.set_members(image_id, members))
696 def main(self, image_id, *member_ids):
697 super(self.__class__, self)._run()
698 self._run(image_id=image_id, members=member_ids)
700 # Compute Image Commands
704 class image_compute(_init_cyclades):
705 """Cyclades/Compute API image commands"""
709 class image_compute_list(
710 _init_cyclades, _optional_json, _name_filter, _id_filter):
713 PERMANENTS = ('id', 'name')
716 detail=FlagArgument('show detailed output', ('-l', '--details')),
717 limit=IntArgument('limit number listed images', ('-n', '--number')),
718 more=FlagArgument('handle long lists of results', '--more'),
719 enum=FlagArgument('Enumerate results', '--enumerate'),
720 user_id=ValueArgument('filter by user_id', '--user-id'),
721 user_name=ValueArgument('filter by username', '--user-name'),
722 meta=KeyValueArgument(
723 'filter by metadata key=value (can be repeated)', ('--metadata')),
724 meta_like=KeyValueArgument(
725 'filter by metadata key=value (can be repeated)',
729 def _filter_by_metadata(self, images):
732 meta = [dict(img['metadata'])]
734 meta = filter_dicts_by_dict(meta, self['meta'])
735 if meta and self['meta_like']:
736 meta = filter_dicts_by_dict(
737 meta, self['meta_like'], exact_match=False)
739 new_images.append(img)
742 def _filter_by_user(self, images):
743 uuid = self['user_id'] or self._username2uuid(self['user_name'])
744 return filter_dicts_by_dict(images, dict(user_id=uuid))
746 def _add_name(self, images, key='user_id'):
747 uuids = self._uuids2usernames(
748 list(set([img[key] for img in images])))
750 img[key] += ' (%s)' % uuids[img[key]]
754 @errors.cyclades.connection
756 withmeta = bool(self['meta'] or self['meta_like'])
757 withuser = bool(self['user_id'] or self['user_name'])
758 detail = self['detail'] or withmeta or withuser
759 images = self.client.list_images(detail)
760 images = self._filter_by_name(images)
761 images = self._filter_by_id(images)
763 images = self._filter_by_user(images)
765 images = self._filter_by_metadata(images)
766 if self['detail'] and not (
767 self['json_output'] or self['output_format']):
768 images = self._add_name(self._add_name(images, 'tenant_id'))
769 elif detail and not self['detail']:
771 for key in set(img).difference(self.PERMANENTS):
773 kwargs = dict(with_enumeration=self['enum'])
775 images = images[:self['limit']]
777 kwargs['out'] = StringIO()
779 self._print(images, **kwargs)
781 pager(kwargs['out'].getvalue())
784 super(self.__class__, self)._run()
789 class image_compute_info(_init_cyclades, _optional_json):
790 """Get detailed information on an image"""
793 @errors.cyclades.connection
795 def _run(self, image_id):
796 image = self.client.get_image_details(image_id)
797 uuids = [image['user_id'], image['tenant_id']]
798 usernames = self._uuids2usernames(uuids)
799 image['user_id'] += ' (%s)' % usernames[image['user_id']]
800 image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
801 self._print(image, self.print_dict)
803 def main(self, image_id):
804 super(self.__class__, self)._run()
805 self._run(image_id=image_id)
809 class image_compute_delete(_init_cyclades, _optional_output_cmd):
810 """Delete an image (WARNING: image file is also removed)"""
813 @errors.cyclades.connection
815 def _run(self, image_id):
816 self._optional_output(self.client.delete_image(image_id))
818 def main(self, image_id):
819 super(self.__class__, self)._run()
820 self._run(image_id=image_id)
824 class image_compute_properties(_init_cyclades):
825 """Manage properties related to OS installation in an image"""
829 class image_compute_properties_list(_init_cyclades, _optional_json):
830 """List all image properties"""
833 @errors.cyclades.connection
835 def _run(self, image_id):
836 self._print(self.client.get_image_metadata(image_id), self.print_dict)
838 def main(self, image_id):
839 super(self.__class__, self)._run()
840 self._run(image_id=image_id)
844 class image_compute_properties_get(_init_cyclades, _optional_json):
845 """Get an image property"""
848 @errors.cyclades.connection
850 @errors.plankton.metadata
851 def _run(self, image_id, key):
853 self.client.get_image_metadata(image_id, key), self.print_dict)
855 def main(self, image_id, key):
856 super(self.__class__, self)._run()
857 self._run(image_id=image_id, key=key)
861 class image_compute_properties_set(_init_cyclades, _optional_json):
862 """Add / update a set of properties for an image
863 properties must be given in the form key=value, e.v.
864 /image compute properties set <image-id> key1=val1 key2=val2
868 @errors.cyclades.connection
870 def _run(self, image_id, keyvals):
872 for keyval in keyvals:
873 key, sep, val = keyval.partition('=')
876 self.client.update_image_metadata(image_id, **meta),
879 def main(self, image_id, *key_equals_value):
880 super(self.__class__, self)._run()
881 self._run(image_id=image_id, keyvals=key_equals_value)
885 class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
886 """Delete a property from an image"""
889 @errors.cyclades.connection
891 @errors.plankton.metadata
892 def _run(self, image_id, key):
893 self._optional_output(self.client.delete_image_metadata(image_id, key))
895 def main(self, image_id, key):
896 super(self.__class__, self)._run()
897 self._run(image_id=image_id, key=key)