Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 6d190dd1

History | View | Annotate | Download (28.8 kB)

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

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.utils import print_dict, print_json, filter_dicts_by_dict
41
from kamaki.clients.image import ImageClient
42
from kamaki.clients.pithos import PithosClient
43
from kamaki.clients.astakos import AstakosClient
44
from kamaki.clients import ClientError
45
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
46
from kamaki.cli.argument import IntArgument, ProgressBarArgument
47
from kamaki.cli.commands.cyclades import _init_cyclades
48
from kamaki.cli.errors import raiseCLIError, CLIBaseUrlError
49
from kamaki.cli.commands import _command_init, errors, addLogSettings
50
from kamaki.cli.commands import (
51
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
52

    
53

    
54
image_cmds = CommandTree(
55
    'image',
56
    'Cyclades/Plankton API image commands\n'
57
    'image compute:\tCyclades/Compute API image commands')
58
_commands = [image_cmds]
59

    
60

    
61
howto_image_file = [
62
    'Kamaki commands to:',
63
    ' get current user id: /user authenticate',
64
    ' check available containers: /file list',
65
    ' create a new container: /file create <container>',
66
    ' check container contents: /file list <container>',
67
    ' upload files: /file upload <image file> <container>']
68

    
69
about_image_id = ['To see a list of available image ids: /image list']
70

    
71

    
72
log = getLogger(__name__)
73

    
74

    
75
class _init_image(_command_init):
76
    @errors.generic.all
77
    @addLogSettings
78
    def _run(self):
79
        if getattr(self, 'cloud', None):
80
            img_url = self._custom_url('image') or self._custom_url('plankton')
81
            if img_url:
82
                token = self._custom_token('image')\
83
                    or self._custom_token('plankton')\
84
                    or self.config.get_cloud(self.cloud, 'token')
85
                self.client = ImageClient(base_url=img_url, token=token)
86
                return
87
        if getattr(self, 'auth_base', False):
88
            plankton_endpoints = self.auth_base.get_service_endpoints(
89
                self._custom_type('image') or self._custom_type(
90
                    'plankton') or 'image',
91
                self._custom_version('image') or self._custom_version(
92
                    'plankton') or '')
93
            base_url = plankton_endpoints['publicURL']
94
            token = self.auth_base.token
95
        else:
96
            raise CLIBaseUrlError(service='plankton')
97
        self.client = ImageClient(base_url=base_url, token=token)
98

    
99
    def main(self):
100
        self._run()
101

    
102

    
103
# Plankton Image Commands
104

    
105

    
106
def _validate_image_meta(json_dict, return_str=False):
107
    """
108
    :param json_dict" (dict) json-formated, of the form
109
        {"key1": "val1", "key2": "val2", ...}
110

111
    :param return_str: (boolean) if true, return a json dump
112

113
    :returns: (dict) if return_str is not True, else return str
114

115
    :raises TypeError, AttributeError: Invalid json format
116

117
    :raises AssertionError: Valid json but invalid image properties dict
118
    """
119
    json_str = dumps(json_dict, indent=2)
120
    for k, v in json_dict.items():
121
        if k.lower() == 'properties':
122
            for pk, pv in v.items():
123
                prop_ok = not (isinstance(pv, dict) or isinstance(pv, list))
124
                assert prop_ok, 'Invalid property value for key %s' % pk
125
                key_ok = not (' ' in k or '-' in k)
126
                assert key_ok, 'Invalid property key %s' % k
127
            continue
128
        meta_ok = not (isinstance(v, dict) or isinstance(v, list))
129
        assert meta_ok, 'Invalid value for meta key %s' % k
130
        meta_ok = ' ' not in k
131
        assert meta_ok, 'Invalid meta key [%s]' % k
132
        json_dict[k] = '%s' % v
133
    return json_str if return_str else json_dict
134

    
135

    
136
def _load_image_meta(filepath):
137
    """
138
    :param filepath: (str) the (relative) path of the metafile
139

140
    :returns: (dict) json_formated
141

142
    :raises TypeError, AttributeError: Invalid json format
143

144
    :raises AssertionError: Valid json but invalid image properties dict
145
    """
146
    with open(path.abspath(filepath)) as f:
147
        meta_dict = load(f)
148
        try:
149
            return _validate_image_meta(meta_dict)
150
        except AssertionError:
151
            log.debug('Failed to load properties from file %s' % filepath)
152
            raise
153

    
154

    
155
def _validate_image_location(location):
156
    """
157
    :param location: (str) pithos://<user-id>/<container>/<image-path>
158

159
    :returns: (<user-id>, <container>, <image-path>)
160

161
    :raises AssertionError: if location is invalid
162
    """
163
    prefix = 'pithos://'
164
    msg = 'Invalid prefix for location %s , try: %s' % (location, prefix)
165
    assert location.startswith(prefix), msg
166
    service, sep, rest = location.partition('://')
167
    assert sep and rest, 'Location %s is missing user-id' % location
168
    uuid, sep, rest = rest.partition('/')
169
    assert sep and rest, 'Location %s is missing container' % location
170
    container, sep, img_path = rest.partition('/')
171
    assert sep and img_path, 'Location %s is missing image path' % location
172
    return uuid, container, img_path
173

    
174

    
175
@command(image_cmds)
176
class image_list(_init_image, _optional_json, _name_filter, _id_filter):
177
    """List images accessible by user"""
178

    
179
    PERMANENTS = (
180
        'id', 'name',
181
        'status', 'container_format', 'disk_format', 'size')
182

    
183
    arguments = dict(
184
        detail=FlagArgument('show detailed output', ('-l', '--details')),
185
        container_format=ValueArgument(
186
            'filter by container format',
187
            '--container-format'),
188
        disk_format=ValueArgument('filter by disk format', '--disk-format'),
189
        size_min=IntArgument('filter by minimum size', '--size-min'),
190
        size_max=IntArgument('filter by maximum size', '--size-max'),
191
        status=ValueArgument('filter by status', '--status'),
192
        owner=ValueArgument('filter by owner', '--owner'),
193
        owner_name=ValueArgument('filter by owners username', '--owner-name'),
194
        order=ValueArgument(
195
            'order by FIELD ( - to reverse order)',
196
            '--order',
197
            default=''),
198
        limit=IntArgument('limit number of listed images', ('-n', '--number')),
199
        more=FlagArgument(
200
            'output results in pages (-n to set items per page, default 10)',
201
            '--more'),
202
        enum=FlagArgument('Enumerate results', '--enumerate'),
203
        prop=KeyValueArgument('filter by property key=value', ('--property')),
204
        prop_like=KeyValueArgument(
205
            'fliter by property key=value where value is part of actual value',
206
            ('--property-like')),
207
    )
208

    
209
    def _filter_by_owner(self, images):
210
        ouuid = self['owner'] or self._username2uuid(self['owner_name'])
211
        return filter_dicts_by_dict(images, dict(owner=ouuid))
212

    
213
    def _add_owner_name(self, images):
214
        uuids = self._uuids2usernames(
215
            list(set([img['owner'] for img in images])))
216
        for img in images:
217
            img['owner'] += ' (%s)' % uuids[img['owner']]
218
        return images
219

    
220
    def _filter_by_properties(self, images):
221
        new_images = []
222
        for img in images:
223
            props = [dict(img['properties'])]
224
            if self['prop']:
225
                props = filter_dicts_by_dict(props, self['prop'])
226
            if props and self['prop_like']:
227
                props = filter_dicts_by_dict(
228
                    props, self['prop_like'], exact_match=False)
229
            if props:
230
                new_images.append(img)
231
        return new_images
232

    
233
    @errors.generic.all
234
    @errors.cyclades.connection
235
    def _run(self):
236
        super(self.__class__, self)._run()
237
        filters = {}
238
        for arg in set([
239
                'container_format',
240
                'disk_format',
241
                'name',
242
                'size_min',
243
                'size_max',
244
                'status']).intersection(self.arguments):
245
            filters[arg] = self[arg]
246

    
247
        order = self['order']
248
        detail = self['detail'] or (
249
            self['prop'] or self['prop_like']) or (
250
            self['owner'] or self['owner_name'])
251

    
252
        images = self.client.list_public(detail, filters, order)
253

    
254
        if self['owner'] or self['owner_name']:
255
            images = self._filter_by_owner(images)
256
        if self['prop'] or self['prop_like']:
257
            images = self._filter_by_properties(images)
258
        images = self._filter_by_id(images)
259
        images = self._non_exact_name_filter(images)
260

    
261
        if self['detail'] and not self['json_output']:
262
            images = self._add_owner_name(images)
263
        elif detail and not self['detail']:
264
            for img in images:
265
                for key in set(img).difference(self.PERMANENTS):
266
                    img.pop(key)
267
        kwargs = dict(with_enumeration=self['enum'])
268
        if self['more']:
269
            kwargs['page_size'] = self['limit'] or 10
270
        elif self['limit']:
271
            images = images[:self['limit']]
272
        self._print(images, **kwargs)
273

    
274
    def main(self):
275
        super(self.__class__, self)._run()
276
        self._run()
277

    
278

    
279
@command(image_cmds)
280
class image_meta(_init_image, _optional_json):
281
    """Get image metadata
282
    Image metadata include:
283
    - image file information (location, size, etc.)
284
    - image information (id, name, etc.)
285
    - image os properties (os, fs, etc.)
286
    """
287

    
288
    @errors.generic.all
289
    @errors.plankton.connection
290
    @errors.plankton.id
291
    def _run(self, image_id):
292
        self._print([self.client.get_meta(image_id)])
293

    
294
    def main(self, image_id):
295
        super(self.__class__, self)._run()
296
        self._run(image_id=image_id)
297

    
298

    
299
@command(image_cmds)
300
class image_register(_init_image, _optional_json):
301
    """(Re)Register an image"""
302

    
303
    container_info_cache = {}
304

    
305
    arguments = dict(
306
        checksum=ValueArgument('set image checksum', '--checksum'),
307
        container_format=ValueArgument(
308
            'set container format',
309
            '--container-format'),
310
        disk_format=ValueArgument('set disk format', '--disk-format'),
311
        owner=ValueArgument('set image owner (admin only)', '--owner'),
312
        properties=KeyValueArgument(
313
            'add property in key=value form (can be repeated)',
314
            ('-p', '--property')),
315
        is_public=FlagArgument('mark image as public', '--public'),
316
        size=IntArgument('set image size', '--size'),
317
        metafile=ValueArgument(
318
            'Load metadata from a json-formated file <img-file>.meta :'
319
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
320
            ('--metafile')),
321
        metafile_force=FlagArgument(
322
            'Store remote metadata object, even if it already exists',
323
            ('-f', '--force')),
324
        no_metafile_upload=FlagArgument(
325
            'Do not store metadata in remote meta file',
326
            ('--no-metafile-upload')),
327
        container=ValueArgument(
328
            'Pithos+ container containing the image file',
329
            ('-C', '--container')),
330
        uuid=ValueArgument('Custom user uuid', '--uuid'),
331
        local_image_path=ValueArgument(
332
            'Local image file path to upload and register '
333
            '(still need target file in the form container:remote-path )',
334
            '--upload-image-file'),
335
        progress_bar=ProgressBarArgument(
336
            'Do not use progress bar', '--no-progress-bar', default=False)
337
    )
338

    
339
    def _get_user_id(self):
340
        atoken = self.client.token
341
        if getattr(self, 'auth_base', False):
342
            return self.auth_base.term('id', atoken)
343
        else:
344
            astakos_url = self.config.get('user', 'url')\
345
                or self.config.get('astakos', 'url')
346
            if not astakos_url:
347
                raise CLIBaseUrlError(service='astakos')
348
            user = AstakosClient(astakos_url, atoken)
349
            return user.term('id')
350

    
351
    def _get_pithos_client(self, container):
352
        if self['no_metafile_upload']:
353
            return None
354
        ptoken = self.client.token
355
        if getattr(self, 'auth_base', False):
356
            pithos_endpoints = self.auth_base.get_service_endpoints(
357
                'object-store')
358
            purl = pithos_endpoints['publicURL']
359
        else:
360
            purl = self.config.get_cloud('pithos', 'url')
361
        if not purl:
362
            raise CLIBaseUrlError(service='pithos')
363
        return PithosClient(purl, ptoken, self._get_user_id(), container)
364

    
365
    def _store_remote_metafile(self, pclient, remote_path, metadata):
366
        return pclient.upload_from_string(
367
            remote_path, _validate_image_meta(metadata, return_str=True),
368
            container_info_cache=self.container_info_cache)
369

    
370
    def _load_params_from_file(self, location):
371
        params, properties = dict(), dict()
372
        pfile = self['metafile']
373
        if pfile:
374
            try:
375
                for k, v in _load_image_meta(pfile).items():
376
                    key = k.lower().replace('-', '_')
377
                    if k == 'properties':
378
                        for pk, pv in v.items():
379
                            properties[pk.upper().replace('-', '_')] = pv
380
                    elif key == 'name':
381
                            continue
382
                    elif key == 'location':
383
                        if location:
384
                            continue
385
                        location = v
386
                    else:
387
                        params[key] = v
388
            except Exception as e:
389
                raiseCLIError(e, 'Invalid json metadata config file')
390
        return params, properties, location
391

    
392
    def _load_params_from_args(self, params, properties):
393
        for key in set([
394
                'checksum',
395
                'container_format',
396
                'disk_format',
397
                'owner',
398
                'size',
399
                'is_public']).intersection(self.arguments):
400
            params[key] = self[key]
401
        for k, v in self['properties'].items():
402
            properties[k.upper().replace('-', '_')] = v
403

    
404
    def _validate_location(self, location):
405
        if not location:
406
            raiseCLIError(
407
                'No image file location provided',
408
                importance=2, details=[
409
                    'An image location is needed. Image location format:',
410
                    '  pithos://<user-id>/<container>/<path>',
411
                    ' where an image file at the above location must exist.'
412
                    ] + howto_image_file)
413
        try:
414
            return _validate_image_location(location)
415
        except AssertionError as ae:
416
            raiseCLIError(
417
                ae, 'Invalid image location format',
418
                importance=1, details=[
419
                    'Valid image location format:',
420
                    '  pithos://<user-id>/<container>/<img-file-path>'
421
                    ] + howto_image_file)
422

    
423
    def _mine_location(self, container_path):
424
        uuid = self['uuid'] or self._get_user_id()
425
        if self['container']:
426
            return uuid, self['container'], container_path
427
        container, sep, path = container_path.partition(':')
428
        if not (bool(container) and bool(path)):
429
            raiseCLIError(
430
                'Incorrect container-path format', importance=1, details=[
431
                'Use : to seperate container form path',
432
                '  <container>:<image-path>',
433
                'OR',
434
                'Use -C to specifiy a container',
435
                '  -C <container> <image-path>'] + howto_image_file)
436

    
437
        return uuid, container, path
438

    
439
    @errors.generic.all
440
    @errors.plankton.connection
441
    def _run(self, name, uuid, container, img_path):
442
        if self['local_image_path']:
443
            with open(self['local_image_path']) as f:
444
                pithos = self._get_pithos_client(container)
445
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
446
                if pbar:
447
                    hash_bar = pbar.clone()
448
                    hash_cb = hash_bar.get_generator('Calculating hashes')
449
                pithos.upload_object(
450
                    img_path, f,
451
                    hash_cb=hash_cb, upload_cb=upload_cb,
452
                    container_info_cache=self.container_info_cache)
453
                pbar.finish()
454

    
455
        location = 'pithos://%s/%s/%s' % (uuid, container, img_path)
456
        (params, properties, new_loc) = self._load_params_from_file(location)
457
        if location != new_loc:
458
            uuid, container, img_path = self._validate_location(new_loc)
459
        self._load_params_from_args(params, properties)
460
        pclient = self._get_pithos_client(container)
461

    
462
        #check if metafile exists
463
        meta_path = '%s.meta' % img_path
464
        if pclient and not self['metafile_force']:
465
            try:
466
                pclient.get_object_info(meta_path)
467
                raiseCLIError(
468
                    'Metadata file %s:%s already exists, abort' % (
469
                        container, meta_path),
470
                    details=['Registration ABORTED', 'Try -f to overwrite'])
471
            except ClientError as ce:
472
                if ce.status != 404:
473
                    raise
474

    
475
        #register the image
476
        try:
477
            r = self.client.register(name, location, params, properties)
478
        except ClientError as ce:
479
            if ce.status in (400, ):
480
                raiseCLIError(
481
                    ce, 'Nonexistent image file location %s' % location,
482
                    details=[
483
                        'Make sure the image file exists'] + howto_image_file)
484
            raise
485
        self._print(r, print_dict)
486

    
487
        #upload the metadata file
488
        if pclient:
489
            try:
490
                meta_headers = pclient.upload_from_string(
491
                    meta_path, dumps(r, indent=2),
492
                    container_info_cache=self.container_info_cache)
493
            except TypeError:
494
                print('Failed to dump metafile %s:%s' % (container, meta_path))
495
                return
496
            if self['json_output']:
497
                print_json(dict(
498
                    metafile_location='%s:%s' % (container, meta_path),
499
                    headers=meta_headers))
500
            else:
501
                print('Metadata file uploaded as %s:%s (version %s)' % (
502
                    container, meta_path, meta_headers['x-object-version']))
503

    
504
    def main(self, name, container___image_path):
505
        super(self.__class__, self)._run()
506
        self._run(name, *self._mine_location(container___image_path))
507

    
508

    
509
@command(image_cmds)
510
class image_unregister(_init_image, _optional_output_cmd):
511
    """Unregister an image (does not delete the image file)"""
512

    
513
    @errors.generic.all
514
    @errors.plankton.connection
515
    @errors.plankton.id
516
    def _run(self, image_id):
517
        self._optional_output(self.client.unregister(image_id))
518

    
519
    def main(self, image_id):
520
        super(self.__class__, self)._run()
521
        self._run(image_id=image_id)
522

    
523

    
524
@command(image_cmds)
525
class image_shared(_init_image, _optional_json):
526
    """List images shared by a member"""
527

    
528
    @errors.generic.all
529
    @errors.plankton.connection
530
    def _run(self, member):
531
        self._print(self.client.list_shared(member), title=('image_id',))
532

    
533
    def main(self, member):
534
        super(self.__class__, self)._run()
535
        self._run(member)
536

    
537

    
538
@command(image_cmds)
539
class image_members(_init_image):
540
    """Manage members. Members of an image are users who can modify it"""
541

    
542

    
543
@command(image_cmds)
544
class image_members_list(_init_image, _optional_json):
545
    """List members of an image"""
546

    
547
    @errors.generic.all
548
    @errors.plankton.connection
549
    @errors.plankton.id
550
    def _run(self, image_id):
551
        self._print(self.client.list_members(image_id), title=('member_id',))
552

    
553
    def main(self, image_id):
554
        super(self.__class__, self)._run()
555
        self._run(image_id=image_id)
556

    
557

    
558
@command(image_cmds)
559
class image_members_add(_init_image, _optional_output_cmd):
560
    """Add a member to an image"""
561

    
562
    @errors.generic.all
563
    @errors.plankton.connection
564
    @errors.plankton.id
565
    def _run(self, image_id=None, member=None):
566
            self._optional_output(self.client.add_member(image_id, member))
567

    
568
    def main(self, image_id, member):
569
        super(self.__class__, self)._run()
570
        self._run(image_id=image_id, member=member)
571

    
572

    
573
@command(image_cmds)
574
class image_members_delete(_init_image, _optional_output_cmd):
575
    """Remove a member from an image"""
576

    
577
    @errors.generic.all
578
    @errors.plankton.connection
579
    @errors.plankton.id
580
    def _run(self, image_id=None, member=None):
581
            self._optional_output(self.client.remove_member(image_id, member))
582

    
583
    def main(self, image_id, member):
584
        super(self.__class__, self)._run()
585
        self._run(image_id=image_id, member=member)
586

    
587

    
588
@command(image_cmds)
589
class image_members_set(_init_image, _optional_output_cmd):
590
    """Set the members of an image"""
591

    
592
    @errors.generic.all
593
    @errors.plankton.connection
594
    @errors.plankton.id
595
    def _run(self, image_id, members):
596
            self._optional_output(self.client.set_members(image_id, members))
597

    
598
    def main(self, image_id, *members):
599
        super(self.__class__, self)._run()
600
        self._run(image_id=image_id, members=members)
601

    
602

    
603
# Compute Image Commands
604

    
605

    
606
@command(image_cmds)
607
class image_compute(_init_cyclades):
608
    """Cyclades/Compute API image commands"""
609

    
610

    
611
@command(image_cmds)
612
class image_compute_list(
613
        _init_cyclades, _optional_json, _name_filter, _id_filter):
614
    """List images"""
615

    
616
    PERMANENTS = ('id', 'name')
617

    
618
    arguments = dict(
619
        detail=FlagArgument('show detailed output', ('-l', '--details')),
620
        limit=IntArgument('limit number listed images', ('-n', '--number')),
621
        more=FlagArgument(
622
            'output results in pages (-n to set items per page, default 10)',
623
            '--more'),
624
        enum=FlagArgument('Enumerate results', '--enumerate'),
625
        user_id=ValueArgument('filter by user_id', '--user-id'),
626
        user_name=ValueArgument('filter by username', '--user-name'),
627
        meta=KeyValueArgument(
628
            'filter by metadata key=value (can be repeated)', ('--metadata')),
629
        meta_like=KeyValueArgument(
630
            'filter by metadata key=value (can be repeated)',
631
            ('--metadata-like'))
632
    )
633

    
634
    def _filter_by_metadata(self, images):
635
        new_images = []
636
        for img in images:
637
            meta = [dict(img['metadata'])]
638
            if self['meta']:
639
                meta = filter_dicts_by_dict(meta, self['meta'])
640
            if meta and self['meta_like']:
641
                meta = filter_dicts_by_dict(
642
                    meta, self['meta_like'], exact_match=False)
643
            if meta:
644
                new_images.append(img)
645
        return new_images
646

    
647
    def _filter_by_user(self, images):
648
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
649
        return filter_dicts_by_dict(images, dict(user_id=uuid))
650

    
651
    def _add_name(self, images, key='user_id'):
652
        uuids = self._uuids2usernames(
653
            list(set([img[key] for img in images])))
654
        for img in images:
655
            img[key] += ' (%s)' % uuids[img[key]]
656
        return images
657

    
658
    @errors.generic.all
659
    @errors.cyclades.connection
660
    def _run(self):
661
        withmeta = bool(self['meta'] or self['meta_like'])
662
        withuser = bool(self['user_id'] or self['user_name'])
663
        detail = self['detail'] or withmeta or withuser
664
        images = self.client.list_images(detail)
665
        images = self._filter_by_name(images)
666
        images = self._filter_by_id(images)
667
        if withuser:
668
            images = self._filter_by_user(images)
669
        if withmeta:
670
            images = self._filter_by_metadata(images)
671
        if self['detail'] and not self['json_output']:
672
            images = self._add_name(self._add_name(images, 'tenant_id'))
673
        elif detail and not self['detail']:
674
            for img in images:
675
                for key in set(img).difference(self.PERMANENTS):
676
                    img.pop(key)
677
        kwargs = dict(with_enumeration=self['enum'])
678
        if self['more']:
679
            kwargs['page_size'] = self['limit'] or 10
680
        elif self['limit']:
681
            images = images[:self['limit']]
682
        self._print(images, **kwargs)
683

    
684
    def main(self):
685
        super(self.__class__, self)._run()
686
        self._run()
687

    
688

    
689
@command(image_cmds)
690
class image_compute_info(_init_cyclades, _optional_json):
691
    """Get detailed information on an image"""
692

    
693
    @errors.generic.all
694
    @errors.cyclades.connection
695
    @errors.plankton.id
696
    def _run(self, image_id):
697
        image = self.client.get_image_details(image_id)
698
        self._print(image, print_dict)
699

    
700
    def main(self, image_id):
701
        super(self.__class__, self)._run()
702
        self._run(image_id=image_id)
703

    
704

    
705
@command(image_cmds)
706
class image_compute_delete(_init_cyclades, _optional_output_cmd):
707
    """Delete an image (WARNING: image file is also removed)"""
708

    
709
    @errors.generic.all
710
    @errors.cyclades.connection
711
    @errors.plankton.id
712
    def _run(self, image_id):
713
        self._optional_output(self.client.delete_image(image_id))
714

    
715
    def main(self, image_id):
716
        super(self.__class__, self)._run()
717
        self._run(image_id=image_id)
718

    
719

    
720
@command(image_cmds)
721
class image_compute_properties(_init_cyclades):
722
    """Manage properties related to OS installation in an image"""
723

    
724

    
725
@command(image_cmds)
726
class image_compute_properties_list(_init_cyclades, _optional_json):
727
    """List all image properties"""
728

    
729
    @errors.generic.all
730
    @errors.cyclades.connection
731
    @errors.plankton.id
732
    def _run(self, image_id):
733
        self._print(self.client.get_image_metadata(image_id), print_dict)
734

    
735
    def main(self, image_id):
736
        super(self.__class__, self)._run()
737
        self._run(image_id=image_id)
738

    
739

    
740
@command(image_cmds)
741
class image_compute_properties_get(_init_cyclades, _optional_json):
742
    """Get an image property"""
743

    
744
    @errors.generic.all
745
    @errors.cyclades.connection
746
    @errors.plankton.id
747
    @errors.plankton.metadata
748
    def _run(self, image_id, key):
749
        self._print(self.client.get_image_metadata(image_id, key), print_dict)
750

    
751
    def main(self, image_id, key):
752
        super(self.__class__, self)._run()
753
        self._run(image_id=image_id, key=key)
754

    
755

    
756
@command(image_cmds)
757
class image_compute_properties_add(_init_cyclades, _optional_json):
758
    """Add a property to an image"""
759

    
760
    @errors.generic.all
761
    @errors.cyclades.connection
762
    @errors.plankton.id
763
    @errors.plankton.metadata
764
    def _run(self, image_id, key, val):
765
        self._print(
766
            self.client.create_image_metadata(image_id, key, val), print_dict)
767

    
768
    def main(self, image_id, key, val):
769
        super(self.__class__, self)._run()
770
        self._run(image_id=image_id, key=key, val=val)
771

    
772

    
773
@command(image_cmds)
774
class image_compute_properties_set(_init_cyclades, _optional_json):
775
    """Add / update a set of properties for an image
776
    properties must be given in the form key=value, e.v.
777
    /image compute properties set <image-id> key1=val1 key2=val2
778
    """
779

    
780
    @errors.generic.all
781
    @errors.cyclades.connection
782
    @errors.plankton.id
783
    def _run(self, image_id, keyvals):
784
        meta = dict()
785
        for keyval in keyvals:
786
            key, sep, val = keyval.partition('=')
787
            meta[key] = val
788
        self._print(
789
            self.client.update_image_metadata(image_id, **meta), print_dict)
790

    
791
    def main(self, image_id, *key_equals_value):
792
        super(self.__class__, self)._run()
793
        print key_equals_value
794
        self._run(image_id=image_id, keyvals=key_equals_value)
795

    
796

    
797
@command(image_cmds)
798
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
799
    """Delete a property from an image"""
800

    
801
    @errors.generic.all
802
    @errors.cyclades.connection
803
    @errors.plankton.id
804
    @errors.plankton.metadata
805
    def _run(self, image_id, key):
806
        self._optional_output(self.client.delete_image_metadata(image_id, key))
807

    
808
    def main(self, image_id, key):
809
        super(self.__class__, self)._run()
810
        self._run(image_id=image_id, key=key)