Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 854222c7

History | View | Annotate | Download (29.2 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 _optional_output_cmd, _optional_json
51

    
52

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

    
59

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

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

    
70

    
71
log = getLogger(__name__)
72

    
73

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

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

    
101

    
102
# Plankton Image Commands
103

    
104

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

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

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

114
    :raises TypeError, AttributeError: Invalid json format
115

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

    
134

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

139
    :returns: (dict) json_formated
140

141
    :raises TypeError, AttributeError: Invalid json format
142

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

    
153

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

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

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

    
173

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

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

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

    
218
    def _filtered_by_owner(self, images):
219
        ouuid = self['owner'] or self._username2uuid(self['owner_name'])
220
        return filter_dicts_by_dict(images, dict(owner=ouuid))
221

    
222
    def _filtered_by_name(self, images):
223
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
224
        return [img for img in images if (
225
            (not np) or img['name'].lower().startswith(np.lower())) and (
226
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
227
            (not nl) or nl.lower() in img['name'].lower())]
228

    
229
    def _add_owner_name(self, images):
230
        uuids = self._uuids2usernames(
231
            list(set([img['owner'] for img in images])))
232
        for img in images:
233
            img['owner'] += ' (%s)' % uuids[img['owner']]
234
        return images
235

    
236
    def _filtered_by_properties(self, images):
237
        new_images = []
238

    
239
        # def like_properties(props):
240
        #     plike = self['prop_like']
241
        #     for k, v in plike.items():
242
        #         likestr = props.get(k, '').lower()
243
        #         if v.lower() not in likestr:
244
        #             return False
245
        #     return True
246

    
247
        for img in images:
248
            props = [dict(img['properties'])]
249
            if self['prop']:
250
                props = filter_dicts_by_dict(props, self['prop'])
251
            if props and self['prop_like']:
252
                props = filter_dicts_by_dict(
253
                    props, self['prop_like'], exact_match=False)
254
            if props:
255
                new_images.append(img)
256

    
257
            #if (
258
            #        self['prop'] and set(
259
            #            self['prop'].items()).difference(props.items())) or (
260
            #        self['prop_like'] and not like_properties(props)):
261
            #    continue
262
            #new_images.append(img)
263
        return new_images
264

    
265
    @errors.generic.all
266
    @errors.cyclades.connection
267
    def _run(self):
268
        super(self.__class__, self)._run()
269
        filters = {}
270
        for arg in set([
271
                'container_format',
272
                'disk_format',
273
                'name',
274
                'size_min',
275
                'size_max',
276
                'status']).intersection(self.arguments):
277
            filters[arg] = self[arg]
278

    
279
        order = self['order']
280
        detail = self['detail'] or (
281
            self['prop'] or self['prop_like']) or (
282
            self['owner'] or self['owner_name'])
283

    
284
        images = self.client.list_public(detail, filters, order)
285

    
286
        if self['owner'] or self['owner_name']:
287
            images = self._filtered_by_owner(images)
288
        if self['prop'] or self['prop_like']:
289
            images = self._filtered_by_properties(images)
290
        images = self._filtered_by_name(images)
291

    
292
        if self['detail'] and not self['json_output']:
293
            images = self._add_owner_name(images)
294
        elif detail and not self['detail']:
295
            for img in images:
296
                for key in set(img).difference(self.PERMANENTS):
297
                    img.pop(key)
298
        kwargs = dict(with_enumeration=self['enum'])
299
        if self['more']:
300
            kwargs['page_size'] = self['limit'] or 10
301
        elif self['limit']:
302
            images = images[:self['limit']]
303
        self._print(images, **kwargs)
304

    
305
    def main(self):
306
        super(self.__class__, self)._run()
307
        self._run()
308

    
309

    
310
@command(image_cmds)
311
class image_meta(_init_image, _optional_json):
312
    """Get image metadata
313
    Image metadata include:
314
    - image file information (location, size, etc.)
315
    - image information (id, name, etc.)
316
    - image os properties (os, fs, etc.)
317
    """
318

    
319
    @errors.generic.all
320
    @errors.plankton.connection
321
    @errors.plankton.id
322
    def _run(self, image_id):
323
        self._print([self.client.get_meta(image_id)])
324

    
325
    def main(self, image_id):
326
        super(self.__class__, self)._run()
327
        self._run(image_id=image_id)
328

    
329

    
330
@command(image_cmds)
331
class image_register(_init_image, _optional_json):
332
    """(Re)Register an image"""
333

    
334
    container_info_cache = {}
335

    
336
    arguments = dict(
337
        checksum=ValueArgument('set image checksum', '--checksum'),
338
        container_format=ValueArgument(
339
            'set container format',
340
            '--container-format'),
341
        disk_format=ValueArgument('set disk format', '--disk-format'),
342
        owner=ValueArgument('set image owner (admin only)', '--owner'),
343
        properties=KeyValueArgument(
344
            'add property in key=value form (can be repeated)',
345
            ('-p', '--property')),
346
        is_public=FlagArgument('mark image as public', '--public'),
347
        size=IntArgument('set image size', '--size'),
348
        metafile=ValueArgument(
349
            'Load metadata from a json-formated file <img-file>.meta :'
350
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
351
            ('--metafile')),
352
        metafile_force=FlagArgument(
353
            'Store remote metadata object, even if it already exists',
354
            ('-f', '--force')),
355
        no_metafile_upload=FlagArgument(
356
            'Do not store metadata in remote meta file',
357
            ('--no-metafile-upload')),
358
        container=ValueArgument(
359
            'Pithos+ container containing the image file',
360
            ('-C', '--container')),
361
        uuid=ValueArgument('Custom user uuid', '--uuid'),
362
        local_image_path=ValueArgument(
363
            'Local image file path to upload and register '
364
            '(still need target file in the form container:remote-path )',
365
            '--upload-image-file'),
366
        progress_bar=ProgressBarArgument(
367
            'Do not use progress bar', '--no-progress-bar', default=False)
368
    )
369

    
370
    def _get_user_id(self):
371
        atoken = self.client.token
372
        if getattr(self, 'auth_base', False):
373
            return self.auth_base.term('id', atoken)
374
        else:
375
            astakos_url = self.config.get('user', 'url')\
376
                or self.config.get('astakos', 'url')
377
            if not astakos_url:
378
                raise CLIBaseUrlError(service='astakos')
379
            user = AstakosClient(astakos_url, atoken)
380
            return user.term('id')
381

    
382
    def _get_pithos_client(self, container):
383
        if self['no_metafile_upload']:
384
            return None
385
        ptoken = self.client.token
386
        if getattr(self, 'auth_base', False):
387
            pithos_endpoints = self.auth_base.get_service_endpoints(
388
                'object-store')
389
            purl = pithos_endpoints['publicURL']
390
        else:
391
            purl = self.config.get_cloud('pithos', 'url')
392
        if not purl:
393
            raise CLIBaseUrlError(service='pithos')
394
        return PithosClient(purl, ptoken, self._get_user_id(), container)
395

    
396
    def _store_remote_metafile(self, pclient, remote_path, metadata):
397
        return pclient.upload_from_string(
398
            remote_path, _validate_image_meta(metadata, return_str=True),
399
            container_info_cache=self.container_info_cache)
400

    
401
    def _load_params_from_file(self, location):
402
        params, properties = dict(), dict()
403
        pfile = self['metafile']
404
        if pfile:
405
            try:
406
                for k, v in _load_image_meta(pfile).items():
407
                    key = k.lower().replace('-', '_')
408
                    if k == 'properties':
409
                        for pk, pv in v.items():
410
                            properties[pk.upper().replace('-', '_')] = pv
411
                    elif key == 'name':
412
                            continue
413
                    elif key == 'location':
414
                        if location:
415
                            continue
416
                        location = v
417
                    else:
418
                        params[key] = v
419
            except Exception as e:
420
                raiseCLIError(e, 'Invalid json metadata config file')
421
        return params, properties, location
422

    
423
    def _load_params_from_args(self, params, properties):
424
        for key in set([
425
                'checksum',
426
                'container_format',
427
                'disk_format',
428
                'owner',
429
                'size',
430
                'is_public']).intersection(self.arguments):
431
            params[key] = self[key]
432
        for k, v in self['properties'].items():
433
            properties[k.upper().replace('-', '_')] = v
434

    
435
    def _validate_location(self, location):
436
        if not location:
437
            raiseCLIError(
438
                'No image file location provided',
439
                importance=2, details=[
440
                    'An image location is needed. Image location format:',
441
                    '  pithos://<user-id>/<container>/<path>',
442
                    ' where an image file at the above location must exist.'
443
                    ] + howto_image_file)
444
        try:
445
            return _validate_image_location(location)
446
        except AssertionError as ae:
447
            raiseCLIError(
448
                ae, 'Invalid image location format',
449
                importance=1, details=[
450
                    'Valid image location format:',
451
                    '  pithos://<user-id>/<container>/<img-file-path>'
452
                    ] + howto_image_file)
453

    
454
    def _mine_location(self, container_path):
455
        uuid = self['uuid'] or self._get_user_id()
456
        if self['container']:
457
            return uuid, self['container'], container_path
458
        container, sep, path = container_path.partition(':')
459
        if not (bool(container) and bool(path)):
460
            raiseCLIError(
461
                'Incorrect container-path format', importance=1, details=[
462
                'Use : to seperate container form path',
463
                '  <container>:<image-path>',
464
                'OR',
465
                'Use -C to specifiy a container',
466
                '  -C <container> <image-path>'] + howto_image_file)
467

    
468
        return uuid, container, path
469

    
470
    @errors.generic.all
471
    @errors.plankton.connection
472
    def _run(self, name, uuid, container, img_path):
473
        if self['local_image_path']:
474
            with open(self['local_image_path']) as f:
475
                pithos = self._get_pithos_client(container)
476
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
477
                if pbar:
478
                    hash_bar = pbar.clone()
479
                    hash_cb = hash_bar.get_generator('Calculating hashes')
480
                pithos.upload_object(
481
                    img_path, f,
482
                    hash_cb=hash_cb, upload_cb=upload_cb,
483
                    container_info_cache=self.container_info_cache)
484
                pbar.finish()
485

    
486
        location = 'pithos://%s/%s/%s' % (uuid, container, img_path)
487
        (params, properties, new_loc) = self._load_params_from_file(location)
488
        if location != new_loc:
489
            uuid, container, img_path = self._validate_location(new_loc)
490
        self._load_params_from_args(params, properties)
491
        pclient = self._get_pithos_client(container)
492

    
493
        #check if metafile exists
494
        meta_path = '%s.meta' % img_path
495
        if pclient and not self['metafile_force']:
496
            try:
497
                pclient.get_object_info(meta_path)
498
                raiseCLIError(
499
                    'Metadata file %s:%s already exists, abort' % (
500
                        container, meta_path),
501
                    details=['Registration ABORTED', 'Try -f to overwrite'])
502
            except ClientError as ce:
503
                if ce.status != 404:
504
                    raise
505

    
506
        #register the image
507
        try:
508
            r = self.client.register(name, location, params, properties)
509
        except ClientError as ce:
510
            if ce.status in (400, ):
511
                raiseCLIError(
512
                    ce, 'Nonexistent image file location %s' % location,
513
                    details=[
514
                        'Make sure the image file exists'] + howto_image_file)
515
            raise
516
        self._print(r, print_dict)
517

    
518
        #upload the metadata file
519
        if pclient:
520
            try:
521
                meta_headers = pclient.upload_from_string(
522
                    meta_path, dumps(r, indent=2),
523
                    container_info_cache=self.container_info_cache)
524
            except TypeError:
525
                print('Failed to dump metafile %s:%s' % (container, meta_path))
526
                return
527
            if self['json_output']:
528
                print_json(dict(
529
                    metafile_location='%s:%s' % (container, meta_path),
530
                    headers=meta_headers))
531
            else:
532
                print('Metadata file uploaded as %s:%s (version %s)' % (
533
                    container, meta_path, meta_headers['x-object-version']))
534

    
535
    def main(self, name, container___image_path):
536
        super(self.__class__, self)._run()
537
        self._run(name, *self._mine_location(container___image_path))
538

    
539

    
540
@command(image_cmds)
541
class image_unregister(_init_image, _optional_output_cmd):
542
    """Unregister an image (does not delete the image file)"""
543

    
544
    @errors.generic.all
545
    @errors.plankton.connection
546
    @errors.plankton.id
547
    def _run(self, image_id):
548
        self._optional_output(self.client.unregister(image_id))
549

    
550
    def main(self, image_id):
551
        super(self.__class__, self)._run()
552
        self._run(image_id=image_id)
553

    
554

    
555
@command(image_cmds)
556
class image_shared(_init_image, _optional_json):
557
    """List images shared by a member"""
558

    
559
    @errors.generic.all
560
    @errors.plankton.connection
561
    def _run(self, member):
562
        self._print(self.client.list_shared(member), title=('image_id',))
563

    
564
    def main(self, member):
565
        super(self.__class__, self)._run()
566
        self._run(member)
567

    
568

    
569
@command(image_cmds)
570
class image_members(_init_image):
571
    """Manage members. Members of an image are users who can modify it"""
572

    
573

    
574
@command(image_cmds)
575
class image_members_list(_init_image, _optional_json):
576
    """List members of an image"""
577

    
578
    @errors.generic.all
579
    @errors.plankton.connection
580
    @errors.plankton.id
581
    def _run(self, image_id):
582
        self._print(self.client.list_members(image_id), title=('member_id',))
583

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

    
588

    
589
@command(image_cmds)
590
class image_members_add(_init_image, _optional_output_cmd):
591
    """Add a member to an image"""
592

    
593
    @errors.generic.all
594
    @errors.plankton.connection
595
    @errors.plankton.id
596
    def _run(self, image_id=None, member=None):
597
            self._optional_output(self.client.add_member(image_id, member))
598

    
599
    def main(self, image_id, member):
600
        super(self.__class__, self)._run()
601
        self._run(image_id=image_id, member=member)
602

    
603

    
604
@command(image_cmds)
605
class image_members_delete(_init_image, _optional_output_cmd):
606
    """Remove a member from an image"""
607

    
608
    @errors.generic.all
609
    @errors.plankton.connection
610
    @errors.plankton.id
611
    def _run(self, image_id=None, member=None):
612
            self._optional_output(self.client.remove_member(image_id, member))
613

    
614
    def main(self, image_id, member):
615
        super(self.__class__, self)._run()
616
        self._run(image_id=image_id, member=member)
617

    
618

    
619
@command(image_cmds)
620
class image_members_set(_init_image, _optional_output_cmd):
621
    """Set the members of an image"""
622

    
623
    @errors.generic.all
624
    @errors.plankton.connection
625
    @errors.plankton.id
626
    def _run(self, image_id, members):
627
            self._optional_output(self.client.set_members(image_id, members))
628

    
629
    def main(self, image_id, *members):
630
        super(self.__class__, self)._run()
631
        self._run(image_id=image_id, members=members)
632

    
633

    
634
# Compute Image Commands
635

    
636

    
637
@command(image_cmds)
638
class image_compute(_init_cyclades):
639
    """Cyclades/Compute API image commands"""
640

    
641

    
642
@command(image_cmds)
643
class image_compute_list(_init_cyclades, _optional_json):
644
    """List images"""
645

    
646
    PERMANENTS = ('id', 'name')
647

    
648
    arguments = dict(
649
        detail=FlagArgument('show detailed output', ('-l', '--details')),
650
        limit=IntArgument('limit number listed images', ('-n', '--number')),
651
        more=FlagArgument(
652
            'output results in pages (-n to set items per page, default 10)',
653
            '--more'),
654
        enum=FlagArgument('Enumerate results', '--enumerate'),
655
        meta=KeyValueArgument(
656
            'filter by metadata key=value (can be repeated)', ('--metadata')),
657
        meta_like=KeyValueArgument(
658
            'filter by metadata key=value (can be repeated)',
659
            ('--metadata-like'))
660
    )
661

    
662
    def _filter_by_metadata(self, images):
663
        new_images = []
664

    
665
        for img in images:
666
            meta = [dict(img['metadata'])]
667
            if self['meta']:
668
                meta = filter_dicts_by_dict(meta, self['meta'])
669
            if meta and self['meta_like']:
670
                meta = filter_dicts_by_dict(
671
                    meta, self['meta_like'], exact_match=False)
672
            if meta:
673
                new_images.append(img)
674
        return new_images
675

    
676
    def _add_name(self, images, key='user_id'):
677
        uuids = self._uuids2usernames(
678
            list(set([img[key] for img in images])))
679
        for img in images:
680
            img[key] += ' (%s)' % uuids[img[key]]
681
        return images
682

    
683
    @errors.generic.all
684
    @errors.cyclades.connection
685
    def _run(self):
686
        withmeta = bool(self['meta'] or self['meta_like'])
687
        detail = self['detail'] or withmeta
688
        images = self.client.list_images(detail)
689
        if withmeta:
690
            images = self._filter_by_metadata(images)
691
        if self['detail'] and not self['json_output']:
692
            images = self._add_name(self._add_name(images, 'tenant_id'))
693
        kwargs = dict(with_enumeration=self['enum'])
694
        if self['more']:
695
            kwargs['page_size'] = self['limit'] or 10
696
        elif self['limit']:
697
            images = images[:self['limit']]
698
        self._print(images, **kwargs)
699

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

    
704

    
705
@command(image_cmds)
706
class image_compute_info(_init_cyclades, _optional_json):
707
    """Get detailed information on an image"""
708

    
709
    @errors.generic.all
710
    @errors.cyclades.connection
711
    @errors.plankton.id
712
    def _run(self, image_id):
713
        image = self.client.get_image_details(image_id)
714
        self._print(image, print_dict)
715

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

    
720

    
721
@command(image_cmds)
722
class image_compute_delete(_init_cyclades, _optional_output_cmd):
723
    """Delete an image (WARNING: image file is also removed)"""
724

    
725
    @errors.generic.all
726
    @errors.cyclades.connection
727
    @errors.plankton.id
728
    def _run(self, image_id):
729
        self._optional_output(self.client.delete_image(image_id))
730

    
731
    def main(self, image_id):
732
        super(self.__class__, self)._run()
733
        self._run(image_id=image_id)
734

    
735

    
736
@command(image_cmds)
737
class image_compute_properties(_init_cyclades):
738
    """Manage properties related to OS installation in an image"""
739

    
740

    
741
@command(image_cmds)
742
class image_compute_properties_list(_init_cyclades, _optional_json):
743
    """List all image properties"""
744

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

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

    
755

    
756
@command(image_cmds)
757
class image_compute_properties_get(_init_cyclades, _optional_json):
758
    """Get an image property"""
759

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

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

    
771

    
772
@command(image_cmds)
773
class image_compute_properties_add(_init_cyclades, _optional_json):
774
    """Add a property to an image"""
775

    
776
    @errors.generic.all
777
    @errors.cyclades.connection
778
    @errors.plankton.id
779
    @errors.plankton.metadata
780
    def _run(self, image_id, key, val):
781
        self._print(
782
            self.client.create_image_metadata(image_id, key, val), print_dict)
783

    
784
    def main(self, image_id, key, val):
785
        super(self.__class__, self)._run()
786
        self._run(image_id=image_id, key=key, val=val)
787

    
788

    
789
@command(image_cmds)
790
class image_compute_properties_set(_init_cyclades, _optional_json):
791
    """Add / update a set of properties for an image
792
    proeprties must be given in the form key=value, e.v.
793
    /image compute properties set <image-id> key1=val1 key2=val2
794
    """
795

    
796
    @errors.generic.all
797
    @errors.cyclades.connection
798
    @errors.plankton.id
799
    def _run(self, image_id, keyvals):
800
        meta = dict()
801
        for keyval in keyvals:
802
            key, val = keyval.split('=')
803
            meta[key] = val
804
        self._print(
805
            self.client.update_image_metadata(image_id, **meta), print_dict)
806

    
807
    def main(self, image_id, *key_equals_value):
808
        super(self.__class__, self)._run()
809
        self._run(image_id=image_id, keyvals=key_equals_value)
810

    
811

    
812
@command(image_cmds)
813
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
814
    """Delete a property from an image"""
815

    
816
    @errors.generic.all
817
    @errors.cyclades.connection
818
    @errors.plankton.id
819
    @errors.plankton.metadata
820
    def _run(self, image_id, key):
821
        self._optional_output(self.client.delete_image_metadata(image_id, key))
822

    
823
    def main(self, image_id, key):
824
        super(self.__class__, self)._run()
825
        self._run(image_id=image_id, key=key)