Apply naming conventions to 1st level image cmds
[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.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)
55
56
57 image_cmds = CommandTree(
58     'image',
59     'Cyclades/Plankton API image commands\n'
60     'image compute:\tCyclades/Compute API image commands')
61 _commands = [image_cmds]
62
63
64 howto_image_file = [
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>']
72
73 about_image_id = ['To see a list of available image ids: /image list']
74
75
76 log = getLogger(__name__)
77
78
79 class _init_image(_command_init):
80     @errors.generic.all
81     @addLogSettings
82     def _run(self):
83         if getattr(self, 'cloud', None):
84             img_url = self._custom_url('image') or self._custom_url('plankton')
85             if img_url:
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)
89                 return
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(
95                     'plankton') or '')
96             base_url = plankton_endpoints['publicURL']
97             token = self.auth_base.token
98         else:
99             raise CLIBaseUrlError(service='plankton')
100         self.client = ImageClient(base_url=base_url, token=token)
101
102     def main(self):
103         self._run()
104
105
106 # Plankton Image Commands
107
108
109 def _validate_image_meta(json_dict, return_str=False):
110     """
111     :param json_dict" (dict) json-formated, of the form
112         {"key1": "val1", "key2": "val2", ...}
113
114     :param return_str: (boolean) if true, return a json dump
115
116     :returns: (dict) if return_str is not True, else return str
117
118     :raises TypeError, AttributeError: Invalid json format
119
120     :raises AssertionError: Valid json but invalid image properties dict
121     """
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
130             continue
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
137
138
139 def _load_image_meta(filepath):
140     """
141     :param filepath: (str) the (relative) path of the metafile
142
143     :returns: (dict) json_formated
144
145     :raises TypeError, AttributeError: Invalid json format
146
147     :raises AssertionError: Valid json but invalid image properties dict
148     """
149     with open(path.abspath(filepath)) as f:
150         meta_dict = load(f)
151         try:
152             return _validate_image_meta(meta_dict)
153         except AssertionError:
154             log.debug('Failed to load properties from file %s' % filepath)
155             raise
156
157
158 def _validate_image_location(location):
159     """
160     :param location: (str) pithos://<user-id>/<container>/<image-path>
161
162     :returns: (<user-id>, <container>, <image-path>)
163
164     :raises AssertionError: if location is invalid
165     """
166     prefix = 'pithos://'
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
176
177
178 @command(image_cmds)
179 class image_list(_init_image, _optional_json, _name_filter, _id_filter):
180     """List images accessible by user"""
181
182     PERMANENTS = (
183         'id', 'name',
184         'status', 'container_format', 'disk_format', 'size')
185
186     arguments = dict(
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'),
197         order=ValueArgument(
198             'order by FIELD ( - to reverse order)',
199             '--order',
200             default=''),
201         limit=IntArgument('limit number of listed images', ('-n', '--number')),
202         more=FlagArgument(
203             'output results in pages (-n to set items per page, default 10)',
204             '--more'),
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')),
210     )
211
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))
215
216     def _add_owner_name(self, images):
217         uuids = self._uuids2usernames(
218             list(set([img['owner'] for img in images])))
219         for img in images:
220             img['owner'] += ' (%s)' % uuids[img['owner']]
221         return images
222
223     def _filter_by_properties(self, images):
224         new_images = []
225         for img in images:
226             props = [dict(img['properties'])]
227             if self['prop']:
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)
232             if props:
233                 new_images.append(img)
234         return new_images
235
236     @errors.generic.all
237     @errors.cyclades.connection
238     def _run(self):
239         super(self.__class__, self)._run()
240         filters = {}
241         for arg in set([
242                 'container_format',
243                 'disk_format',
244                 'name',
245                 'size_min',
246                 'size_max',
247                 'status']).intersection(self.arguments):
248             filters[arg] = self[arg]
249
250         order = self['order']
251         detail = self['detail'] or (
252             self['prop'] or self['prop_like']) or (
253             self['owner'] or self['owner_name'])
254
255         images = self.client.list_public(detail, filters, order)
256
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)
263
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']:
268             for img in images:
269                 for key in set(img).difference(self.PERMANENTS):
270                     img.pop(key)
271         kwargs = dict(with_enumeration=self['enum'])
272         if self['limit']:
273             images = images[:self['limit']]
274         if self['more']:
275             kwargs['out'] = StringIO()
276             kwargs['title'] = ()
277         self._print(images, **kwargs)
278         if self['more']:
279             pager(kwargs['out'].getvalue())
280
281     def main(self):
282         super(self.__class__, self)._run()
283         self._run()
284
285
286 @command(image_cmds)
287 class image_info(_init_image, _optional_json):
288     """Get image metadata"""
289
290     @errors.generic.all
291     @errors.plankton.connection
292     @errors.plankton.id
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)
298
299     def main(self, image_id):
300         super(self.__class__, self)._run()
301         self._run(image_id=image_id)
302
303
304 @command(image_cmds)
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
308     """
309
310     arguments = dict(
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')
323     )
324     required = [
325         'image_name', 'disk_format', 'container_format', 'status', 'publish',
326         'unpublish', 'property_to_set']
327
328     @errors.generic.all
329     @errors.plankton.connection
330     @errors.plankton.id
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(
338             image_id,
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']))
345
346     def main(self, image_id):
347         super(self.__class__, self)._run()
348         self._run(image_id=image_id)
349
350
351 @command(image_cmds)
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
361     images.
362     In case of a meta file, runtime arguments for metadata or properties
363     override meta file settings.
364     """
365
366     container_info_cache = {}
367
368     arguments = dict(
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'
376             '(can be repeated)',
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: {...}"}',
383             ('--metafile')),
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)
399     )
400
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)
405         else:
406             astakos_url = self.config.get('user', 'url') or self.config.get(
407                 'astakos', 'url')
408             if not astakos_url:
409                 raise CLIBaseUrlError(service='astakos')
410             user = AstakosClient(astakos_url, atoken)
411             return user.term('id')
412
413     def _get_pithos_client(self, container):
414         if self['no_metafile_upload']:
415             return None
416         ptoken = self.client.token
417         if getattr(self, 'auth_base', False):
418             pithos_endpoints = self.auth_base.get_service_endpoints(
419                 'object-store')
420             purl = pithos_endpoints['publicURL']
421         else:
422             purl = self.config.get_cloud('pithos', 'url')
423         if not purl:
424             raise CLIBaseUrlError(service='pithos')
425         return PithosClient(purl, ptoken, self._get_user_id(), container)
426
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)
431
432     def _load_params_from_file(self, location):
433         params, properties = dict(), dict()
434         pfile = self['metafile']
435         if pfile:
436             try:
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
442                     elif key == 'name':
443                             continue
444                     elif key == 'location':
445                         if location:
446                             continue
447                         location = v
448                     else:
449                         params[key] = v
450             except Exception as e:
451                 raiseCLIError(e, 'Invalid json metadata config file')
452         return params, properties, location
453
454     def _load_params_from_args(self, params, properties):
455         for key in set([
456                 'checksum',
457                 'container_format',
458                 'disk_format',
459                 'owner',
460                 'size',
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
465
466     def _validate_location(self, location):
467         if not location:
468             raiseCLIError(
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)
475         try:
476             return _validate_image_location(location)
477         except AssertionError as ae:
478             raiseCLIError(
479                 ae, 'Invalid image location format',
480                 importance=1, details=[
481                     'Valid image location format:',
482                     '  <container>:<img-file-path>'
483                     ] + howto_image_file)
484
485     @staticmethod
486     def _old_location_format(location):
487         prefix = 'pithos://'
488         try:
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>'])
496         return ()
497
498     def _mine_location(self, container_path):
499         old_response = self._old_location_format(container_path)
500         if old_response:
501             return old_response
502         uuid = self['uuid'] or (self._username2uuid(self['owner_name']) if (
503                     self['owner_name']) else self._get_user_id())
504         if not uuid:
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:',
509                 '  /user whoami',
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)):
516             raiseCLIError(
517                 'Incorrect container-path format', importance=1, details=[
518                 'Use : to seperate container form path',
519                 '  <container>:<image-path>',
520                 'OR',
521                 'Use -C to specifiy a container',
522                 '  -C <container> <image-path>'] + howto_image_file)
523
524         return uuid, container, path
525
526     @errors.generic.all
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')
534                 if pbar:
535                     hash_bar = pbar.clone()
536                     hash_cb = hash_bar.get_generator('Calculating hashes')
537                 pithos.upload_object(
538                     img_path, f,
539                     hash_cb=hash_cb, upload_cb=upload_cb,
540                     container_info_cache=self.container_info_cache)
541                 pbar.finish()
542
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)
549
550         #check if metafile exists
551         meta_path = '%s.meta' % img_path
552         if pclient and not self['metafile_force']:
553             try:
554                 pclient.get_object_info(meta_path)
555                 raiseCLIError(
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:
560                 if ce.status != 404:
561                     raise
562
563         #register the image
564         try:
565             r = self.client.register(name, location, params, properties)
566         except ClientError as ce:
567             if ce.status in (400, 404):
568                 raiseCLIError(
569                     ce, 'Nonexistent image file location\n\t%s' % location,
570                     details=[
571                         'Does the image file %s exist at container %s ?' % (
572                             img_path, dst_cont)] + howto_image_file)
573             raise
574         r['owner'] += ' (%s)' % self._uuid2username(r['owner'])
575         self._print(r, self.print_dict)
576
577         #upload the metadata file
578         if pclient:
579             try:
580                 meta_headers = pclient.upload_from_string(
581                     meta_path, dumps(r, indent=2),
582                     container_info_cache=self.container_info_cache)
583             except TypeError:
584                 self.error(
585                     'Failed to dump metafile %s:%s' % (dst_cont, meta_path))
586                 return
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))
591             else:
592                 self.error('Metadata file uploaded as %s:%s (version %s)' % (
593                     dst_cont, meta_path, meta_headers['x-object-version']))
594
595     def main(self, name, container___image_path):
596         super(self.__class__, self)._run()
597         self._run(name, *self._mine_location(container___image_path))
598
599
600 @command(image_cmds)
601 class image_unregister(_init_image, _optional_output_cmd):
602     """Unregister an image (does not delete the image file)"""
603
604     @errors.generic.all
605     @errors.plankton.connection
606     @errors.plankton.id
607     def _run(self, image_id):
608         self._optional_output(self.client.unregister(image_id))
609
610     def main(self, image_id):
611         super(self.__class__, self)._run()
612         self._run(image_id=image_id)
613
614
615 @command(image_cmds)
616 class image_shared(_init_image, _optional_json):
617     """List images shared by a member"""
618
619     @errors.generic.all
620     @errors.plankton.connection
621     def _run(self, member):
622         r = self.client.list_shared(member)
623         self._print(r, title=('image_id',))
624
625     def main(self, member_id_or_username):
626         super(self.__class__, self)._run()
627         self._run(member_id_or_username)
628
629
630 @command(image_cmds)
631 class image_members(_init_image):
632     """Manage members. Members of an image are users who can modify it"""
633
634
635 @command(image_cmds)
636 class image_members_list(_init_image, _optional_json):
637     """List members of an image"""
638
639     @errors.generic.all
640     @errors.plankton.connection
641     @errors.plankton.id
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',))
650
651     def main(self, image_id):
652         super(self.__class__, self)._run()
653         self._run(image_id=image_id)
654
655
656 @command(image_cmds)
657 class image_members_add(_init_image, _optional_output_cmd):
658     """Add a member to an image"""
659
660     @errors.generic.all
661     @errors.plankton.connection
662     @errors.plankton.id
663     def _run(self, image_id=None, member=None):
664             self._optional_output(self.client.add_member(image_id, member))
665
666     def main(self, image_id, member_id):
667         super(self.__class__, self)._run()
668         self._run(image_id=image_id, member=member_id)
669
670
671 @command(image_cmds)
672 class image_members_delete(_init_image, _optional_output_cmd):
673     """Remove a member from an image"""
674
675     @errors.generic.all
676     @errors.plankton.connection
677     @errors.plankton.id
678     def _run(self, image_id=None, member=None):
679             self._optional_output(self.client.remove_member(image_id, member))
680
681     def main(self, image_id, member):
682         super(self.__class__, self)._run()
683         self._run(image_id=image_id, member=member)
684
685
686 @command(image_cmds)
687 class image_members_set(_init_image, _optional_output_cmd):
688     """Set the members of an image"""
689
690     @errors.generic.all
691     @errors.plankton.connection
692     @errors.plankton.id
693     def _run(self, image_id, members):
694             self._optional_output(self.client.set_members(image_id, members))
695
696     def main(self, image_id, *member_ids):
697         super(self.__class__, self)._run()
698         self._run(image_id=image_id, members=member_ids)
699
700 # Compute Image Commands
701
702
703 @command(image_cmds)
704 class image_compute(_init_cyclades):
705     """Cyclades/Compute API image commands"""
706
707
708 @command(image_cmds)
709 class image_compute_list(
710         _init_cyclades, _optional_json, _name_filter, _id_filter):
711     """List images"""
712
713     PERMANENTS = ('id', 'name')
714
715     arguments = dict(
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)',
726             ('--metadata-like'))
727     )
728
729     def _filter_by_metadata(self, images):
730         new_images = []
731         for img in images:
732             meta = [dict(img['metadata'])]
733             if self['meta']:
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)
738             if meta:
739                 new_images.append(img)
740         return new_images
741
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))
745
746     def _add_name(self, images, key='user_id'):
747         uuids = self._uuids2usernames(
748             list(set([img[key] for img in images])))
749         for img in images:
750             img[key] += ' (%s)' % uuids[img[key]]
751         return images
752
753     @errors.generic.all
754     @errors.cyclades.connection
755     def _run(self):
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)
762         if withuser:
763             images = self._filter_by_user(images)
764         if withmeta:
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']:
770             for img in images:
771                 for key in set(img).difference(self.PERMANENTS):
772                     img.pop(key)
773         kwargs = dict(with_enumeration=self['enum'])
774         if self['limit']:
775             images = images[:self['limit']]
776         if self['more']:
777             kwargs['out'] = StringIO()
778             kwargs['title'] = ()
779         self._print(images, **kwargs)
780         if self['more']:
781             pager(kwargs['out'].getvalue())
782
783     def main(self):
784         super(self.__class__, self)._run()
785         self._run()
786
787
788 @command(image_cmds)
789 class image_compute_info(_init_cyclades, _optional_json):
790     """Get detailed information on an image"""
791
792     @errors.generic.all
793     @errors.cyclades.connection
794     @errors.plankton.id
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)
802
803     def main(self, image_id):
804         super(self.__class__, self)._run()
805         self._run(image_id=image_id)
806
807
808 @command(image_cmds)
809 class image_compute_delete(_init_cyclades, _optional_output_cmd):
810     """Delete an image (WARNING: image file is also removed)"""
811
812     @errors.generic.all
813     @errors.cyclades.connection
814     @errors.plankton.id
815     def _run(self, image_id):
816         self._optional_output(self.client.delete_image(image_id))
817
818     def main(self, image_id):
819         super(self.__class__, self)._run()
820         self._run(image_id=image_id)
821
822
823 @command(image_cmds)
824 class image_compute_properties(_init_cyclades):
825     """Manage properties related to OS installation in an image"""
826
827
828 @command(image_cmds)
829 class image_compute_properties_list(_init_cyclades, _optional_json):
830     """List all image properties"""
831
832     @errors.generic.all
833     @errors.cyclades.connection
834     @errors.plankton.id
835     def _run(self, image_id):
836         self._print(self.client.get_image_metadata(image_id), self.print_dict)
837
838     def main(self, image_id):
839         super(self.__class__, self)._run()
840         self._run(image_id=image_id)
841
842
843 @command(image_cmds)
844 class image_compute_properties_get(_init_cyclades, _optional_json):
845     """Get an image property"""
846
847     @errors.generic.all
848     @errors.cyclades.connection
849     @errors.plankton.id
850     @errors.plankton.metadata
851     def _run(self, image_id, key):
852         self._print(
853             self.client.get_image_metadata(image_id, key), self.print_dict)
854
855     def main(self, image_id, key):
856         super(self.__class__, self)._run()
857         self._run(image_id=image_id, key=key)
858
859
860 @command(image_cmds)
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
865     """
866
867     @errors.generic.all
868     @errors.cyclades.connection
869     @errors.plankton.id
870     def _run(self, image_id, keyvals):
871         meta = dict()
872         for keyval in keyvals:
873             key, sep, val = keyval.partition('=')
874             meta[key] = val
875         self._print(
876             self.client.update_image_metadata(image_id, **meta),
877             self.print_dict)
878
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)
882
883
884 @command(image_cmds)
885 class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
886     """Delete a property from an image"""
887
888     @errors.generic.all
889     @errors.cyclades.connection
890     @errors.plankton.id
891     @errors.plankton.metadata
892     def _run(self, image_id, key):
893         self._optional_output(self.client.delete_image_metadata(image_id, key))
894
895     def main(self, image_id, key):
896         super(self.__class__, self)._run()
897         self._run(image_id=image_id, key=key)