d2b7e7e80269d0c06a492dcbc96fc47d78cda9fe
[kamaki] / kamaki / cli / commands / image.py
1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
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.
15 #
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.
28 #
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
33
34 from json import load, dumps
35 from os import path
36 from logging import getLogger
37 from io import StringIO
38 from pydoc import pager
39
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 import ClientError
46 from kamaki.cli.argument import (
47     FlagArgument, ValueArgument, RepeatableArgument, KeyValueArgument,
48     IntArgument, ProgressBarArgument)
49 from kamaki.cli.commands.cyclades import _init_cyclades
50 from kamaki.cli.errors import (
51     raiseCLIError, CLIBaseUrlError, CLIInvalidArgument)
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)
55
56
57 image_cmds = CommandTree('image', 'Cyclades/Plankton API image commands')
58 imagecompute_cmds = CommandTree(
59     'imagecompute', 'Cyclades/Compute API image commands')
60 _commands = [image_cmds, imagecompute_cmds]
61
62
63 howto_image_file = [
64     'Kamaki commands to:',
65     ' get current user id: /user authenticate',
66     ' check available containers: /file list',
67     ' create a new container: /file create <container>',
68     ' check container contents: /file list <container>',
69     ' upload files: /file upload <image file> <container>',
70     ' register an image: /image register <image name> <container>:<path>']
71
72 about_image_id = ['To see a list of available image ids: /image list']
73
74
75 log = getLogger(__name__)
76
77
78 class _init_image(_command_init):
79     @errors.generic.all
80     @addLogSettings
81     def _run(self):
82         if getattr(self, 'cloud', None):
83             img_url = self._custom_url('image') or self._custom_url('plankton')
84             if img_url:
85                 token = self._custom_token('image') or self._custom_token(
86                     'plankton') or self.config.get_cloud(self.cloud, 'token')
87                 self.client = ImageClient(base_url=img_url, token=token)
88                 return
89         if getattr(self, 'auth_base', False):
90             plankton_endpoints = self.auth_base.get_service_endpoints(
91                 self._custom_type('image') or self._custom_type(
92                     'plankton') or 'image',
93                 self._custom_version('image') or self._custom_version(
94                     'plankton') or '')
95             base_url = plankton_endpoints['publicURL']
96             token = self.auth_base.token
97         else:
98             raise CLIBaseUrlError(service='plankton')
99         self.client = ImageClient(base_url=base_url, token=token)
100
101     def main(self):
102         self._run()
103
104
105 # Plankton Image Commands
106
107
108 def _validate_image_meta(json_dict, return_str=False):
109     """
110     :param json_dict" (dict) json-formated, of the form
111         {"key1": "val1", "key2": "val2", ...}
112
113     :param return_str: (boolean) if true, return a json dump
114
115     :returns: (dict) if return_str is not True, else return str
116
117     :raises TypeError, AttributeError: Invalid json format
118
119     :raises AssertionError: Valid json but invalid image properties dict
120     """
121     json_str = dumps(json_dict, indent=2)
122     for k, v in json_dict.items():
123         if k.lower() == 'properties':
124             for pk, pv in v.items():
125                 prop_ok = not (isinstance(pv, dict) or isinstance(pv, list))
126                 assert prop_ok, 'Invalid property value for key %s' % pk
127                 key_ok = not (' ' in k or '-' in k)
128                 assert key_ok, 'Invalid property key %s' % k
129             continue
130         meta_ok = not (isinstance(v, dict) or isinstance(v, list))
131         assert meta_ok, 'Invalid value for meta key %s' % k
132         meta_ok = ' ' not in k
133         assert meta_ok, 'Invalid meta key [%s]' % k
134         json_dict[k] = '%s' % v
135     return json_str if return_str else json_dict
136
137
138 def _load_image_meta(filepath):
139     """
140     :param filepath: (str) the (relative) path of the metafile
141
142     :returns: (dict) json_formated
143
144     :raises TypeError, AttributeError: Invalid json format
145
146     :raises AssertionError: Valid json but invalid image properties dict
147     """
148     with open(path.abspath(filepath)) as f:
149         meta_dict = load(f)
150         try:
151             return _validate_image_meta(meta_dict)
152         except AssertionError:
153             log.debug('Failed to load properties from file %s' % filepath)
154             raise
155
156
157 def _validate_image_location(location):
158     """
159     :param location: (str) pithos://<user-id>/<container>/<image-path>
160
161     :returns: (<user-id>, <container>, <image-path>)
162
163     :raises AssertionError: if location is invalid
164     """
165     prefix = 'pithos://'
166     msg = 'Invalid prefix for location %s , try: %s' % (location, prefix)
167     assert location.startswith(prefix), msg
168     service, sep, rest = location.partition('://')
169     assert sep and rest, 'Location %s is missing user-id' % location
170     uuid, sep, rest = rest.partition('/')
171     assert sep and rest, 'Location %s is missing container' % location
172     container, sep, img_path = rest.partition('/')
173     assert sep and img_path, 'Location %s is missing image path' % location
174     return uuid, container, img_path
175
176
177 @command(image_cmds)
178 class image_list(_init_image, _optional_json, _name_filter, _id_filter):
179     """List images accessible by user"""
180
181     PERMANENTS = (
182         'id', 'name',
183         'status', 'container_format', 'disk_format', 'size')
184
185     arguments = dict(
186         detail=FlagArgument('show detailed output', ('-l', '--details')),
187         container_format=ValueArgument(
188             'filter by container format',
189             '--container-format'),
190         disk_format=ValueArgument('filter by disk format', '--disk-format'),
191         size_min=IntArgument('filter by minimum size', '--size-min'),
192         size_max=IntArgument('filter by maximum size', '--size-max'),
193         status=ValueArgument('filter by status', '--status'),
194         owner=ValueArgument('filter by owner', '--owner'),
195         owner_name=ValueArgument('filter by owners username', '--owner-name'),
196         order=ValueArgument(
197             'order by FIELD ( - to reverse order)',
198             '--order',
199             default=''),
200         limit=IntArgument('limit number of listed images', ('-n', '--number')),
201         more=FlagArgument(
202             'output results in pages (-n to set items per page, default 10)',
203             '--more'),
204         enum=FlagArgument('Enumerate results', '--enumerate'),
205         prop=KeyValueArgument('filter by property key=value', ('--property')),
206         prop_like=KeyValueArgument(
207             'fliter by property key=value where value is part of actual value',
208             ('--property-like')),
209         image_ID_for_members=ValueArgument(
210             'List members of an image', '--members-of')
211     )
212
213     def _filter_by_owner(self, images):
214         ouuid = self['owner'] or self._username2uuid(self['owner_name'])
215         return filter_dicts_by_dict(images, dict(owner=ouuid))
216
217     def _add_owner_name(self, images):
218         uuids = self._uuids2usernames(
219             list(set([img['owner'] for img in images])))
220         for img in images:
221             img['owner'] += ' (%s)' % uuids[img['owner']]
222         return images
223
224     def _filter_by_properties(self, images):
225         new_images = []
226         for img in images:
227             props = [dict(img['properties'])]
228             if self['prop']:
229                 props = filter_dicts_by_dict(props, self['prop'])
230             if props and self['prop_like']:
231                 props = filter_dicts_by_dict(
232                     props, self['prop_like'], exact_match=False)
233             if props:
234                 new_images.append(img)
235         return new_images
236
237     def _members(self, image_id):
238         members = self.client.list_members(image_id)
239         if not (self['json_output'] or self['output_format']):
240             uuids = [member['member_id'] for member in members]
241             usernames = self._uuids2usernames(uuids)
242             for member in members:
243                 member['member_id'] += ' (%s)' % usernames[member['member_id']]
244         self._print(members, title=('member_id',))
245
246     @errors.generic.all
247     @errors.cyclades.connection
248     def _run(self):
249         super(self.__class__, self)._run()
250         if self['image_ID_for_members']:
251             return self._members(self['image_ID_for_members'])
252         filters = {}
253         for arg in set([
254                 'container_format',
255                 'disk_format',
256                 'name',
257                 'size_min',
258                 'size_max',
259                 'status']).intersection(self.arguments):
260             filters[arg] = self[arg]
261
262         order = self['order']
263         detail = self['detail'] or (
264             self['prop'] or self['prop_like']) or (
265             self['owner'] or self['owner_name'])
266
267         images = self.client.list_public(detail, filters, order)
268
269         if self['owner'] or self['owner_name']:
270             images = self._filter_by_owner(images)
271         if self['prop'] or self['prop_like']:
272             images = self._filter_by_properties(images)
273         images = self._filter_by_id(images)
274         images = self._non_exact_name_filter(images)
275
276         if self['detail'] and not (
277                 self['json_output'] or self['output_format']):
278             images = self._add_owner_name(images)
279         elif detail and not self['detail']:
280             for img in images:
281                 for key in set(img).difference(self.PERMANENTS):
282                     img.pop(key)
283         kwargs = dict(with_enumeration=self['enum'])
284         if self['limit']:
285             images = images[:self['limit']]
286         if self['more']:
287             kwargs['out'] = StringIO()
288             kwargs['title'] = ()
289         self._print(images, **kwargs)
290         if self['more']:
291             pager(kwargs['out'].getvalue())
292
293     def main(self):
294         super(self.__class__, self)._run()
295         self._run()
296
297
298 @command(image_cmds)
299 class image_info(_init_image, _optional_json):
300     """Get image metadata"""
301
302     @errors.generic.all
303     @errors.plankton.connection
304     @errors.plankton.id
305     def _run(self, image_id):
306         meta = self.client.get_meta(image_id)
307         if not (self['json_output'] or self['output_format']):
308             meta['owner'] += ' (%s)' % self._uuid2username(meta['owner'])
309         self._print(meta, self.print_dict)
310
311     def main(self, image_id):
312         super(self.__class__, self)._run()
313         self._run(image_id=image_id)
314
315
316 @command(image_cmds)
317 class image_modify(_init_image, _optional_output_cmd):
318     """Add / update metadata and properties for an image
319     The original image preserves the values that are not affected
320     """
321
322     arguments = dict(
323         image_name=ValueArgument('Change name', '--name'),
324         disk_format=ValueArgument('Change disk format', '--disk-format'),
325         container_format=ValueArgument(
326             'Change container format', '--container-format'),
327         status=ValueArgument('Change status', '--status'),
328         publish=FlagArgument('Publish the image', '--publish'),
329         unpublish=FlagArgument('Unpublish the image', '--unpublish'),
330         property_to_set=KeyValueArgument(
331             'set property in key=value form (can be repeated)',
332             ('-p', '--property-set')),
333         property_to_del=RepeatableArgument(
334             'Delete property by key (can be repeated)', '--property-del'),
335         member_ID_to_add=RepeatableArgument(
336             'Add member to image (can be repeated)', '--member-add'),
337         member_ID_to_remove=RepeatableArgument(
338             'Remove a member (can be repeated)', '--member-del'),
339     )
340     required = [
341         'image_name', 'disk_format', 'container_format', 'status', 'publish',
342         'unpublish', 'property_to_set', 'member_ID_to_add',
343         'member_ID_to_remove']
344
345     @errors.generic.all
346     @errors.plankton.connection
347     @errors.plankton.id
348     def _run(self, image_id):
349         for mid in (self['member_ID_to_add'] or []):
350             self.client.add_member(image_id, mid)
351         for mid in (self['member_ID_to_remove'] or []):
352             self.client.remove_member(image_id, mid)
353         meta = self.client.get_meta(image_id)
354         for k, v in self['property_to_set'].items():
355             meta['properties'][k.upper()] = v
356         for k in (self['property_to_del'] or []):
357             meta['properties'][k.upper()] = None
358         self._optional_output(self.client.update_image(
359             image_id,
360             name=self['image_name'],
361             disk_format=self['disk_format'],
362             container_format=self['container_format'],
363             status=self['status'],
364             public=self['publish'] or self['unpublish'] or None,
365             **meta['properties']))
366         if self['with_output']:
367             self._optional_output(self.get_image_details(image_id))
368
369     def main(self, image_id):
370         super(self.__class__, self)._run()
371         self._run(image_id=image_id)
372
373
374 class PithosLocationArgument(ValueArgument):
375     """Resolve pithos url, return in the form pithos://uuid/container/path"""
376
377     def __init__(
378             self, help=None, parsed_name=None, default=None, user_uuid=None):
379         super(PithosLocationArgument, self).__init__(
380             help=help, parsed_name=parsed_name, default=default)
381         self.uuid, self.container, self.path = user_uuid, None, None
382
383     def setdefault(self, term, value):
384         if not getattr(self, term, None):
385             setattr(self, term, value)
386
387     @property
388     def value(self):
389         return 'pithos://%s/%s/%s' % (self.uuid, self.container, self.path)
390
391     @value.setter
392     def value(self, location):
393         if location:
394             from kamaki.cli.commands.pithos import _pithos_container as pc
395             try:
396                 uuid, self.container, self.path = pc._resolve_pithos_url(
397                     location)
398                 self.uuid = uuid or self.uuid
399                 for term in ('container', 'path'):
400                     assert getattr(self, term, None), 'No %s' % term
401             except Exception as e:
402                 raise CLIInvalidArgument(
403                     'Invalid Pithos+ location %s (%s)' % (location, e),
404                     details=[
405                         'The image location must be a valid Pithos+',
406                         'location. There are two valid formats:',
407                         '  pithos://USER_UUID/CONTAINER/PATH',
408                         'OR',
409                         '  /CONTAINER/PATH',
410                         'To see all containers:',
411                         '  [kamaki] container list',
412                         'To list the contents of a container:',
413                         '  [kamaki] container list CONTAINER'])
414
415
416 @command(image_cmds)
417 class image_register(_init_image, _optional_json):
418     """(Re)Register an image file to an Image service
419     The image file must be stored at a pithos repository
420     Some metadata can be set by user (e.g., disk-format) while others are set
421     only automatically (e.g., image id). There are also some custom user
422     metadata, called properties.
423     A register command creates a remote meta file at
424     /<container>/<image path>.meta
425     Users may download and edit this file and use it to re-register one or more
426     images.
427     In case of a meta file, runtime arguments for metadata or properties
428     override meta file settings.
429     """
430
431     container_info_cache = {}
432
433     arguments = dict(
434         checksum=ValueArgument('Set image checksum', '--checksum'),
435         container_format=ValueArgument(
436             'Set container format', '--container-format'),
437         disk_format=ValueArgument('Set disk format', '--disk-format'),
438         owner_name=ValueArgument('Set user uuid by user name', '--owner-name'),
439         properties=KeyValueArgument(
440             'Add property (user-specified metadata) in key=value form'
441             '(can be repeated)',
442             ('-p', '--property')),
443         is_public=FlagArgument('Mark image as public', '--public'),
444         size=IntArgument('Set image size in bytes', '--size'),
445         metafile=ValueArgument(
446             'Load metadata from a json-formated file <img-file>.meta :'
447             '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
448             ('--metafile')),
449         metafile_force=FlagArgument(
450             'Overide remote metadata file', ('-f', '--force')),
451         no_metafile_upload=FlagArgument(
452             'Do not store metadata in remote meta file',
453             ('--no-metafile-upload')),
454         container=ValueArgument(
455             'Pithos+ container containing the image file',
456             ('-C', '--container')),
457         uuid=ValueArgument('Custom user uuid', '--uuid'),
458         local_image_path=ValueArgument(
459             'Local image file path to upload and register '
460             '(still need target file in the form /container/remote-path )',
461             '--upload-image-file'),
462         progress_bar=ProgressBarArgument(
463             'Do not use progress bar', '--no-progress-bar', default=False),
464         name=ValueArgument('The name of the new image', '--name'),
465         pithos_location=PithosLocationArgument(
466             'The Pithos+ image location to put the image at. Format:       '
467             'pithos://USER_UUID/CONTAINER/IMAGE                  or   '
468             '/CONTAINER/IMAGE',
469             '--location')
470     )
471     required = ('name', 'pithos_location')
472
473     def _get_pithos_client(self, locator):
474         if self['no_metafile_upload']:
475             return None
476         ptoken = self.client.token
477         if getattr(self, 'auth_base', False):
478             pithos_endpoints = self.auth_base.get_service_endpoints(
479                 'object-store')
480             purl = pithos_endpoints['publicURL']
481         else:
482             purl = self.config.get_cloud('pithos', 'url')
483         if not purl:
484             raise CLIBaseUrlError(service='pithos')
485         return PithosClient(purl, ptoken, locator.uuid, locator.container)
486
487     def _load_params_from_file(self, location):
488         params, properties = dict(), dict()
489         pfile = self['metafile']
490         if pfile:
491             try:
492                 for k, v in _load_image_meta(pfile).items():
493                     key = k.lower().replace('-', '_')
494                     if key == 'properties':
495                         for pk, pv in v.items():
496                             properties[pk.upper().replace('-', '_')] = pv
497                     elif key == 'name':
498                             continue
499                     elif key == 'location':
500                         if location:
501                             continue
502                         location = v
503                     else:
504                         params[key] = v
505             except Exception as e:
506                 raiseCLIError(e, 'Invalid json metadata config file')
507         return params, properties, location
508
509     def _load_params_from_args(self, params, properties):
510         for key in set([
511                 'checksum',
512                 'container_format',
513                 'disk_format',
514                 'owner',
515                 'size',
516                 'is_public']).intersection(self.arguments):
517             params[key] = self[key]
518         for k, v in self['properties'].items():
519             properties[k.upper().replace('-', '_')] = v
520
521     @errors.generic.all
522     @errors.plankton.connection
523     def _run(self, name, location):
524         locator = self.arguments['pithos_location']
525         if self['local_image_path']:
526             with open(self['local_image_path']) as f:
527                 pithos = self._get_pithos_client(locator)
528                 (pbar, upload_cb) = self._safe_progress_bar('Uploading')
529                 if pbar:
530                     hash_bar = pbar.clone()
531                     hash_cb = hash_bar.get_generator('Calculating hashes')
532                 pithos.upload_object(
533                     locator.path, f,
534                     hash_cb=hash_cb, upload_cb=upload_cb,
535                     container_info_cache=self.container_info_cache)
536                 pbar.finish()
537
538         (params, properties, new_loc) = self._load_params_from_file(location)
539         if location != new_loc:
540             locator.value = new_loc
541         self._load_params_from_args(params, properties)
542         pclient = self._get_pithos_client(locator)
543
544         #check if metafile exists
545         meta_path = '%s.meta' % locator.path
546         if pclient and not self['metafile_force']:
547             try:
548                 pclient.get_object_info(meta_path)
549                 raiseCLIError(
550                     'Metadata file /%s/%s already exists, abort' % (
551                         locator.container, meta_path),
552                     details=['Registration ABORTED', 'Try -f to overwrite'])
553             except ClientError as ce:
554                 if ce.status != 404:
555                     raise
556
557         #register the image
558         try:
559             r = self.client.register(name, location, params, properties)
560         except ClientError as ce:
561             if ce.status in (400, 404):
562                 raiseCLIError(
563                     ce, 'Nonexistent image file location\n\t%s' % location,
564                     details=[
565                         'Does the image file %s exist at container %s ?' % (
566                             locator.path,
567                             locator.container)] + howto_image_file)
568             raise
569         r['owner'] += ' (%s)' % self._uuid2username(r['owner'])
570         self._print(r, self.print_dict)
571
572         #upload the metadata file
573         if pclient:
574             try:
575                 meta_headers = pclient.upload_from_string(
576                     meta_path, dumps(r, indent=2),
577                     container_info_cache=self.container_info_cache)
578             except TypeError:
579                 self.error(
580                     'Failed to dump metafile /%s/%s' % (
581                         locator.container, meta_path))
582                 return
583             if self['json_output'] or self['output_format']:
584                 self.print_json(dict(
585                     metafile_location='/%s/%s' % (
586                         locator.container, meta_path),
587                     headers=meta_headers))
588             else:
589                 self.error('Metadata file uploaded as /%s/%s (version %s)' % (
590                     locator.container,
591                     meta_path,
592                     meta_headers['x-object-version']))
593
594     def main(self):
595         super(self.__class__, self)._run()
596         self.arguments['pithos_location'].setdefault(
597             'uuid', self.auth_base.user_term('id'))
598         self._run(self['name'], self['pithos_location'])
599
600
601 @command(image_cmds)
602 class image_unregister(_init_image, _optional_output_cmd):
603     """Unregister an image (does not delete the image file)"""
604
605     @errors.generic.all
606     @errors.plankton.connection
607     @errors.plankton.id
608     def _run(self, image_id):
609         self._optional_output(self.client.unregister(image_id))
610
611     def main(self, image_id):
612         super(self.__class__, self)._run()
613         self._run(image_id=image_id)
614
615
616 # Compute Image Commands
617
618 @command(imagecompute_cmds)
619 class imagecompute_list(
620         _init_cyclades, _optional_json, _name_filter, _id_filter):
621     """List images"""
622
623     PERMANENTS = ('id', 'name')
624
625     arguments = dict(
626         detail=FlagArgument('show detailed output', ('-l', '--details')),
627         limit=IntArgument('limit number listed images', ('-n', '--number')),
628         more=FlagArgument('handle long lists of results', '--more'),
629         enum=FlagArgument('Enumerate results', '--enumerate'),
630         user_id=ValueArgument('filter by user_id', '--user-id'),
631         user_name=ValueArgument('filter by username', '--user-name'),
632         meta=KeyValueArgument(
633             'filter by metadata key=value (can be repeated)', ('--metadata')),
634         meta_like=KeyValueArgument(
635             'filter by metadata key=value (can be repeated)',
636             ('--metadata-like'))
637     )
638
639     def _filter_by_metadata(self, images):
640         new_images = []
641         for img in images:
642             meta = [dict(img['metadata'])]
643             if self['meta']:
644                 meta = filter_dicts_by_dict(meta, self['meta'])
645             if meta and self['meta_like']:
646                 meta = filter_dicts_by_dict(
647                     meta, self['meta_like'], exact_match=False)
648             if meta:
649                 new_images.append(img)
650         return new_images
651
652     def _filter_by_user(self, images):
653         uuid = self['user_id'] or self._username2uuid(self['user_name'])
654         return filter_dicts_by_dict(images, dict(user_id=uuid))
655
656     def _add_name(self, images, key='user_id'):
657         uuids = self._uuids2usernames(
658             list(set([img[key] for img in images])))
659         for img in images:
660             img[key] += ' (%s)' % uuids[img[key]]
661         return images
662
663     @errors.generic.all
664     @errors.cyclades.connection
665     def _run(self):
666         withmeta = bool(self['meta'] or self['meta_like'])
667         withuser = bool(self['user_id'] or self['user_name'])
668         detail = self['detail'] or withmeta or withuser
669         images = self.client.list_images(detail)
670         images = self._filter_by_name(images)
671         images = self._filter_by_id(images)
672         if withuser:
673             images = self._filter_by_user(images)
674         if withmeta:
675             images = self._filter_by_metadata(images)
676         if self['detail'] and not (
677                 self['json_output'] or self['output_format']):
678             images = self._add_name(self._add_name(images, 'tenant_id'))
679         elif detail and not self['detail']:
680             for img in images:
681                 for key in set(img).difference(self.PERMANENTS):
682                     img.pop(key)
683         kwargs = dict(with_enumeration=self['enum'])
684         if self['limit']:
685             images = images[:self['limit']]
686         if self['more']:
687             kwargs['out'] = StringIO()
688             kwargs['title'] = ()
689         self._print(images, **kwargs)
690         if self['more']:
691             pager(kwargs['out'].getvalue())
692
693     def main(self):
694         super(self.__class__, self)._run()
695         self._run()
696
697
698 @command(imagecompute_cmds)
699 class imagecompute_info(_init_cyclades, _optional_json):
700     """Get detailed information on an image"""
701
702     @errors.generic.all
703     @errors.cyclades.connection
704     @errors.plankton.id
705     def _run(self, image_id):
706         image = self.client.get_image_details(image_id)
707         uuids = [image['user_id'], image['tenant_id']]
708         usernames = self._uuids2usernames(uuids)
709         image['user_id'] += ' (%s)' % usernames[image['user_id']]
710         image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
711         self._print(image, self.print_dict)
712
713     def main(self, image_id):
714         super(self.__class__, self)._run()
715         self._run(image_id=image_id)
716
717
718 @command(imagecompute_cmds)
719 class imagecompute_delete(_init_cyclades, _optional_output_cmd):
720     """Delete an image (WARNING: image file is also removed)"""
721
722     @errors.generic.all
723     @errors.cyclades.connection
724     @errors.plankton.id
725     def _run(self, image_id):
726         self._optional_output(self.client.delete_image(image_id))
727
728     def main(self, image_id):
729         super(self.__class__, self)._run()
730         self._run(image_id=image_id)
731
732
733 @command(imagecompute_cmds)
734 class imagecompute_modify(_init_cyclades, _optional_output_cmd):
735     """Modify image properties (metadata)"""
736
737     arguments = dict(
738         property_to_add=KeyValueArgument(
739             'Add property in key=value format (can be repeated)',
740             ('--property-add')),
741         property_to_del=RepeatableArgument(
742             'Delete property by key (can be repeated)',
743             ('--property-del'))
744     )
745     required = ['property_to_add', 'property_to_del']
746
747     @errors.generic.all
748     @errors.cyclades.connection
749     @errors.plankton.id
750     def _run(self, image_id):
751         if self['property_to_add']:
752             self.client.update_image_metadata(
753                 image_id, **self['property_to_add'])
754         for key in (self['property_to_del'] or []):
755             self.client.delete_image_metadata(image_id, key)
756         if self['with_output']:
757             self._optional_output(self.client.get_image_details(image_id))
758
759     def main(self, image_id):
760         super(self.__class__, self)._run()
761         self._run(image_id=image_id)