Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ cf115aed

History | View | Annotate | Download (29.5 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
        r['owner'] += '( %s)' % self._uuid2username(r['owner'])
486
        self._print(r, print_dict)
487

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

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

    
509

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

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

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

    
524

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

    
529
    @errors.generic.all
530
    @errors.plankton.connection
531
    def _run(self, member):
532
        r = self.client.list_shared(member)
533
        if r:
534
            uuid = self._username2uuid(member)
535
            r = self.client.list_shared(uuid) if uuid else []
536
        self._print(r, title=('image_id',))
537

    
538
    def main(self, member_id_or_username):
539
        super(self.__class__, self)._run()
540
        self._run(member_id_or_username)
541

    
542

    
543
@command(image_cmds)
544
class image_members(_init_image):
545
    """Manage members. Members of an image are users who can modify it"""
546

    
547

    
548
@command(image_cmds)
549
class image_members_list(_init_image, _optional_json):
550
    """List members of an image"""
551

    
552
    @errors.generic.all
553
    @errors.plankton.connection
554
    @errors.plankton.id
555
    def _run(self, image_id):
556
        members = self.client.list_members(image_id)
557
        if not self['json_output']:
558
            uuids = [member['member_id'] for member in members]
559
            usernames = self._uuids2usernames(uuids)
560
            for member in members:
561
                member['member_id'] += ' (%s)' % usernames[member['member_id']]
562
        self._print(members, title=('member_id',))
563

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

    
568

    
569
@command(image_cmds)
570
class image_members_add(_init_image, _optional_output_cmd):
571
    """Add a member to an image"""
572

    
573
    @errors.generic.all
574
    @errors.plankton.connection
575
    @errors.plankton.id
576
    def _run(self, image_id=None, member=None):
577
            self._optional_output(self.client.add_member(image_id, member))
578

    
579
    def main(self, image_id, member_id):
580
        super(self.__class__, self)._run()
581
        self._run(image_id=image_id, member=member_id)
582

    
583

    
584
@command(image_cmds)
585
class image_members_delete(_init_image, _optional_output_cmd):
586
    """Remove a member from an image"""
587

    
588
    @errors.generic.all
589
    @errors.plankton.connection
590
    @errors.plankton.id
591
    def _run(self, image_id=None, member=None):
592
            self._optional_output(self.client.remove_member(image_id, member))
593

    
594
    def main(self, image_id, member):
595
        super(self.__class__, self)._run()
596
        self._run(image_id=image_id, member=member)
597

    
598

    
599
@command(image_cmds)
600
class image_members_set(_init_image, _optional_output_cmd):
601
    """Set the members of an image"""
602

    
603
    @errors.generic.all
604
    @errors.plankton.connection
605
    @errors.plankton.id
606
    def _run(self, image_id, members):
607
            self._optional_output(self.client.set_members(image_id, members))
608

    
609
    def main(self, image_id, *member_ids):
610
        super(self.__class__, self)._run()
611
        self._run(image_id=image_id, members=member_ids)
612

    
613

    
614
# Compute Image Commands
615

    
616

    
617
@command(image_cmds)
618
class image_compute(_init_cyclades):
619
    """Cyclades/Compute API image commands"""
620

    
621

    
622
@command(image_cmds)
623
class image_compute_list(
624
        _init_cyclades, _optional_json, _name_filter, _id_filter):
625
    """List images"""
626

    
627
    PERMANENTS = ('id', 'name')
628

    
629
    arguments = dict(
630
        detail=FlagArgument('show detailed output', ('-l', '--details')),
631
        limit=IntArgument('limit number listed images', ('-n', '--number')),
632
        more=FlagArgument(
633
            'output results in pages (-n to set items per page, default 10)',
634
            '--more'),
635
        enum=FlagArgument('Enumerate results', '--enumerate'),
636
        user_id=ValueArgument('filter by user_id', '--user-id'),
637
        user_name=ValueArgument('filter by username', '--user-name'),
638
        meta=KeyValueArgument(
639
            'filter by metadata key=value (can be repeated)', ('--metadata')),
640
        meta_like=KeyValueArgument(
641
            'filter by metadata key=value (can be repeated)',
642
            ('--metadata-like'))
643
    )
644

    
645
    def _filter_by_metadata(self, images):
646
        new_images = []
647
        for img in images:
648
            meta = [dict(img['metadata'])]
649
            if self['meta']:
650
                meta = filter_dicts_by_dict(meta, self['meta'])
651
            if meta and self['meta_like']:
652
                meta = filter_dicts_by_dict(
653
                    meta, self['meta_like'], exact_match=False)
654
            if meta:
655
                new_images.append(img)
656
        return new_images
657

    
658
    def _filter_by_user(self, images):
659
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
660
        return filter_dicts_by_dict(images, dict(user_id=uuid))
661

    
662
    def _add_name(self, images, key='user_id'):
663
        uuids = self._uuids2usernames(
664
            list(set([img[key] for img in images])))
665
        for img in images:
666
            img[key] += ' (%s)' % uuids[img[key]]
667
        return images
668

    
669
    @errors.generic.all
670
    @errors.cyclades.connection
671
    def _run(self):
672
        withmeta = bool(self['meta'] or self['meta_like'])
673
        withuser = bool(self['user_id'] or self['user_name'])
674
        detail = self['detail'] or withmeta or withuser
675
        images = self.client.list_images(detail)
676
        images = self._filter_by_name(images)
677
        images = self._filter_by_id(images)
678
        if withuser:
679
            images = self._filter_by_user(images)
680
        if withmeta:
681
            images = self._filter_by_metadata(images)
682
        if self['detail'] and not self['json_output']:
683
            images = self._add_name(self._add_name(images, 'tenant_id'))
684
        elif detail and not self['detail']:
685
            for img in images:
686
                for key in set(img).difference(self.PERMANENTS):
687
                    img.pop(key)
688
        kwargs = dict(with_enumeration=self['enum'])
689
        if self['more']:
690
            kwargs['page_size'] = self['limit'] or 10
691
        elif self['limit']:
692
            images = images[:self['limit']]
693
        self._print(images, **kwargs)
694

    
695
    def main(self):
696
        super(self.__class__, self)._run()
697
        self._run()
698

    
699

    
700
@command(image_cmds)
701
class image_compute_info(_init_cyclades, _optional_json):
702
    """Get detailed information on an image"""
703

    
704
    @errors.generic.all
705
    @errors.cyclades.connection
706
    @errors.plankton.id
707
    def _run(self, image_id):
708
        image = self.client.get_image_details(image_id)
709
        uuids = [image['user_id'], image['tenant_id']]
710
        usernames = self._uuids2usernames(uuids)
711
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
712
        image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
713
        self._print(image, print_dict)
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_delete(_init_cyclades, _optional_output_cmd):
722
    """Delete an image (WARNING: image file is also removed)"""
723

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

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

    
734

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

    
739

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

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

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

    
754

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

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

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

    
770

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

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

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

    
787

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

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

    
806
    def main(self, image_id, *key_equals_value):
807
        super(self.__class__, self)._run()
808
        print key_equals_value
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)