Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 466636c9

History | View | Annotate | Download (30.1 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
        for img in images:
239
            props = [dict(img['properties'])]
240
            if self['prop']:
241
                props = filter_dicts_by_dict(props, self['prop'])
242
            if props and self['prop_like']:
243
                props = filter_dicts_by_dict(
244
                    props, self['prop_like'], exact_match=False)
245
            if props:
246
                new_images.append(img)
247
        return new_images
248

    
249
    @errors.generic.all
250
    @errors.cyclades.connection
251
    def _run(self):
252
        super(self.__class__, self)._run()
253
        filters = {}
254
        for arg in set([
255
                'container_format',
256
                'disk_format',
257
                'name',
258
                'size_min',
259
                'size_max',
260
                'status']).intersection(self.arguments):
261
            filters[arg] = self[arg]
262

    
263
        order = self['order']
264
        detail = self['detail'] or (
265
            self['prop'] or self['prop_like']) or (
266
            self['owner'] or self['owner_name'])
267

    
268
        images = self.client.list_public(detail, filters, order)
269

    
270
        if self['owner'] or self['owner_name']:
271
            images = self._filtered_by_owner(images)
272
        if self['prop'] or self['prop_like']:
273
            images = self._filtered_by_properties(images)
274
        images = self._filtered_by_name(images)
275

    
276
        if self['detail'] and not self['json_output']:
277
            images = self._add_owner_name(images)
278
        elif detail and not self['detail']:
279
            for img in images:
280
                for key in set(img).difference(self.PERMANENTS):
281
                    img.pop(key)
282
        kwargs = dict(with_enumeration=self['enum'])
283
        if self['more']:
284
            kwargs['page_size'] = self['limit'] or 10
285
        elif self['limit']:
286
            images = images[:self['limit']]
287
        self._print(images, **kwargs)
288

    
289
    def main(self):
290
        super(self.__class__, self)._run()
291
        self._run()
292

    
293

    
294
@command(image_cmds)
295
class image_meta(_init_image, _optional_json):
296
    """Get image metadata
297
    Image metadata include:
298
    - image file information (location, size, etc.)
299
    - image information (id, name, etc.)
300
    - image os properties (os, fs, etc.)
301
    """
302

    
303
    @errors.generic.all
304
    @errors.plankton.connection
305
    @errors.plankton.id
306
    def _run(self, image_id):
307
        self._print([self.client.get_meta(image_id)])
308

    
309
    def main(self, image_id):
310
        super(self.__class__, self)._run()
311
        self._run(image_id=image_id)
312

    
313

    
314
@command(image_cmds)
315
class image_register(_init_image, _optional_json):
316
    """(Re)Register an image"""
317

    
318
    container_info_cache = {}
319

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

    
354
    def _get_user_id(self):
355
        atoken = self.client.token
356
        if getattr(self, 'auth_base', False):
357
            return self.auth_base.term('id', atoken)
358
        else:
359
            astakos_url = self.config.get('user', 'url')\
360
                or self.config.get('astakos', 'url')
361
            if not astakos_url:
362
                raise CLIBaseUrlError(service='astakos')
363
            user = AstakosClient(astakos_url, atoken)
364
            return user.term('id')
365

    
366
    def _get_pithos_client(self, container):
367
        if self['no_metafile_upload']:
368
            return None
369
        ptoken = self.client.token
370
        if getattr(self, 'auth_base', False):
371
            pithos_endpoints = self.auth_base.get_service_endpoints(
372
                'object-store')
373
            purl = pithos_endpoints['publicURL']
374
        else:
375
            purl = self.config.get_cloud('pithos', 'url')
376
        if not purl:
377
            raise CLIBaseUrlError(service='pithos')
378
        return PithosClient(purl, ptoken, self._get_user_id(), container)
379

    
380
    def _store_remote_metafile(self, pclient, remote_path, metadata):
381
        return pclient.upload_from_string(
382
            remote_path, _validate_image_meta(metadata, return_str=True),
383
            container_info_cache=self.container_info_cache)
384

    
385
    def _load_params_from_file(self, location):
386
        params, properties = dict(), dict()
387
        pfile = self['metafile']
388
        if pfile:
389
            try:
390
                for k, v in _load_image_meta(pfile).items():
391
                    key = k.lower().replace('-', '_')
392
                    if k == 'properties':
393
                        for pk, pv in v.items():
394
                            properties[pk.upper().replace('-', '_')] = pv
395
                    elif key == 'name':
396
                            continue
397
                    elif key == 'location':
398
                        if location:
399
                            continue
400
                        location = v
401
                    else:
402
                        params[key] = v
403
            except Exception as e:
404
                raiseCLIError(e, 'Invalid json metadata config file')
405
        return params, properties, location
406

    
407
    def _load_params_from_args(self, params, properties):
408
        for key in set([
409
                'checksum',
410
                'container_format',
411
                'disk_format',
412
                'owner',
413
                'size',
414
                'is_public']).intersection(self.arguments):
415
            params[key] = self[key]
416
        for k, v in self['properties'].items():
417
            properties[k.upper().replace('-', '_')] = v
418

    
419
    def _validate_location(self, location):
420
        if not location:
421
            raiseCLIError(
422
                'No image file location provided',
423
                importance=2, details=[
424
                    'An image location is needed. Image location format:',
425
                    '  pithos://<user-id>/<container>/<path>',
426
                    ' where an image file at the above location must exist.'
427
                    ] + howto_image_file)
428
        try:
429
            return _validate_image_location(location)
430
        except AssertionError as ae:
431
            raiseCLIError(
432
                ae, 'Invalid image location format',
433
                importance=1, details=[
434
                    'Valid image location format:',
435
                    '  pithos://<user-id>/<container>/<img-file-path>'
436
                    ] + howto_image_file)
437

    
438
    def _mine_location(self, container_path):
439
        uuid = self['uuid'] or self._get_user_id()
440
        if self['container']:
441
            return uuid, self['container'], container_path
442
        container, sep, path = container_path.partition(':')
443
        if not (bool(container) and bool(path)):
444
            raiseCLIError(
445
                'Incorrect container-path format', importance=1, details=[
446
                'Use : to seperate container form path',
447
                '  <container>:<image-path>',
448
                'OR',
449
                'Use -C to specifiy a container',
450
                '  -C <container> <image-path>'] + howto_image_file)
451

    
452
        return uuid, container, path
453

    
454
    @errors.generic.all
455
    @errors.plankton.connection
456
    def _run(self, name, uuid, container, img_path):
457
        if self['local_image_path']:
458
            with open(self['local_image_path']) as f:
459
                pithos = self._get_pithos_client(container)
460
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
461
                if pbar:
462
                    hash_bar = pbar.clone()
463
                    hash_cb = hash_bar.get_generator('Calculating hashes')
464
                pithos.upload_object(
465
                    img_path, f,
466
                    hash_cb=hash_cb, upload_cb=upload_cb,
467
                    container_info_cache=self.container_info_cache)
468
                pbar.finish()
469

    
470
        location = 'pithos://%s/%s/%s' % (uuid, container, img_path)
471
        (params, properties, new_loc) = self._load_params_from_file(location)
472
        if location != new_loc:
473
            uuid, container, img_path = self._validate_location(new_loc)
474
        self._load_params_from_args(params, properties)
475
        pclient = self._get_pithos_client(container)
476

    
477
        #check if metafile exists
478
        meta_path = '%s.meta' % img_path
479
        if pclient and not self['metafile_force']:
480
            try:
481
                pclient.get_object_info(meta_path)
482
                raiseCLIError(
483
                    'Metadata file %s:%s already exists, abort' % (
484
                        container, meta_path),
485
                    details=['Registration ABORTED', 'Try -f to overwrite'])
486
            except ClientError as ce:
487
                if ce.status != 404:
488
                    raise
489

    
490
        #register the image
491
        try:
492
            r = self.client.register(name, location, params, properties)
493
        except ClientError as ce:
494
            if ce.status in (400, ):
495
                raiseCLIError(
496
                    ce, 'Nonexistent image file location %s' % location,
497
                    details=[
498
                        'Make sure the image file exists'] + howto_image_file)
499
            raise
500
        self._print(r, print_dict)
501

    
502
        #upload the metadata file
503
        if pclient:
504
            try:
505
                meta_headers = pclient.upload_from_string(
506
                    meta_path, dumps(r, indent=2),
507
                    container_info_cache=self.container_info_cache)
508
            except TypeError:
509
                print('Failed to dump metafile %s:%s' % (container, meta_path))
510
                return
511
            if self['json_output']:
512
                print_json(dict(
513
                    metafile_location='%s:%s' % (container, meta_path),
514
                    headers=meta_headers))
515
            else:
516
                print('Metadata file uploaded as %s:%s (version %s)' % (
517
                    container, meta_path, meta_headers['x-object-version']))
518

    
519
    def main(self, name, container___image_path):
520
        super(self.__class__, self)._run()
521
        self._run(name, *self._mine_location(container___image_path))
522

    
523

    
524
@command(image_cmds)
525
class image_unregister(_init_image, _optional_output_cmd):
526
    """Unregister an image (does not delete the image file)"""
527

    
528
    @errors.generic.all
529
    @errors.plankton.connection
530
    @errors.plankton.id
531
    def _run(self, image_id):
532
        self._optional_output(self.client.unregister(image_id))
533

    
534
    def main(self, image_id):
535
        super(self.__class__, self)._run()
536
        self._run(image_id=image_id)
537

    
538

    
539
@command(image_cmds)
540
class image_shared(_init_image, _optional_json):
541
    """List images shared by a member"""
542

    
543
    @errors.generic.all
544
    @errors.plankton.connection
545
    def _run(self, member):
546
        self._print(self.client.list_shared(member), title=('image_id',))
547

    
548
    def main(self, member):
549
        super(self.__class__, self)._run()
550
        self._run(member)
551

    
552

    
553
@command(image_cmds)
554
class image_members(_init_image):
555
    """Manage members. Members of an image are users who can modify it"""
556

    
557

    
558
@command(image_cmds)
559
class image_members_list(_init_image, _optional_json):
560
    """List members of an image"""
561

    
562
    @errors.generic.all
563
    @errors.plankton.connection
564
    @errors.plankton.id
565
    def _run(self, image_id):
566
        self._print(self.client.list_members(image_id), title=('member_id',))
567

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

    
572

    
573
@command(image_cmds)
574
class image_members_add(_init_image, _optional_output_cmd):
575
    """Add a member to 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.add_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_delete(_init_image, _optional_output_cmd):
590
    """Remove a member from an image"""
591

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

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

    
602

    
603
@command(image_cmds)
604
class image_members_set(_init_image, _optional_output_cmd):
605
    """Set the members of an image"""
606

    
607
    @errors.generic.all
608
    @errors.plankton.connection
609
    @errors.plankton.id
610
    def _run(self, image_id, members):
611
            self._optional_output(self.client.set_members(image_id, members))
612

    
613
    def main(self, image_id, *members):
614
        super(self.__class__, self)._run()
615
        self._run(image_id=image_id, members=members)
616

    
617

    
618
# Compute Image Commands
619

    
620

    
621
@command(image_cmds)
622
class image_compute(_init_cyclades):
623
    """Cyclades/Compute API image commands"""
624

    
625

    
626
@command(image_cmds)
627
class image_compute_list(_init_cyclades, _optional_json):
628
    """List images"""
629

    
630
    PERMANENTS = ('id', 'name')
631

    
632
    arguments = dict(
633
        detail=FlagArgument('show detailed output', ('-l', '--details')),
634
        limit=IntArgument('limit number listed images', ('-n', '--number')),
635
        more=FlagArgument(
636
            'output results in pages (-n to set items per page, default 10)',
637
            '--more'),
638
        enum=FlagArgument('Enumerate results', '--enumerate'),
639
        name=ValueArgument('filter by name', '--name'),
640
        name_pref=ValueArgument(
641
            'filter by name prefix (case insensitive)',
642
            '--name-prefix'),
643
        name_suff=ValueArgument(
644
            'filter by name suffix (case insensitive)',
645
            '--name-suffix'),
646
        name_like=ValueArgument(
647
            'print only if name contains this (case insensitive)',
648
            '--name-like'),
649
        user_id=ValueArgument('filter by user_id', '--user-id'),
650
        user_name=ValueArgument('filter by username', '--user-name'),
651
        meta=KeyValueArgument(
652
            'filter by metadata key=value (can be repeated)', ('--metadata')),
653
        meta_like=KeyValueArgument(
654
            'filter by metadata key=value (can be repeated)',
655
            ('--metadata-like'))
656
    )
657

    
658
    def _filtered_by_name(self, images):
659
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
660
        return [img for img in images if (
661
            (not np) or img['name'].lower().startswith(np.lower())) and (
662
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
663
            (not nl) or nl.lower() in img['name'].lower())]
664

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

    
678
    def _filter_by_user(self, images):
679
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
680
        return filter_dicts_by_dict(images, dict(user_id=uuid))
681

    
682
    def _add_name(self, images, key='user_id'):
683
        uuids = self._uuids2usernames(
684
            list(set([img[key] for img in images])))
685
        for img in images:
686
            img[key] += ' (%s)' % uuids[img[key]]
687
        return images
688

    
689
    @errors.generic.all
690
    @errors.cyclades.connection
691
    def _run(self):
692
        withmeta = bool(self['meta'] or self['meta_like'])
693
        withuser = bool(self['user_id'] or self['user_name'])
694
        detail = self['detail'] or withmeta or withuser
695
        images = self.client.list_images(detail)
696
        images = self._filtered_by_name(images)
697
        if withuser:
698
            images = self._filter_by_user(images)
699
        if withmeta:
700
            images = self._filter_by_metadata(images)
701
        if self['detail'] and not self['json_output']:
702
            images = self._add_name(self._add_name(images, 'tenant_id'))
703
        elif detail and not self['detail']:
704
            for img in images:
705
                for key in set(img).difference(self.PERMANENTS):
706
                    img.pop(key)
707
        kwargs = dict(with_enumeration=self['enum'])
708
        if self['more']:
709
            kwargs['page_size'] = self['limit'] or 10
710
        elif self['limit']:
711
            images = images[:self['limit']]
712
        self._print(images, **kwargs)
713

    
714
    def main(self):
715
        super(self.__class__, self)._run()
716
        self._run()
717

    
718

    
719
@command(image_cmds)
720
class image_compute_info(_init_cyclades, _optional_json):
721
    """Get detailed information on an image"""
722

    
723
    @errors.generic.all
724
    @errors.cyclades.connection
725
    @errors.plankton.id
726
    def _run(self, image_id):
727
        image = self.client.get_image_details(image_id)
728
        self._print(image, print_dict)
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_delete(_init_cyclades, _optional_output_cmd):
737
    """Delete an image (WARNING: image file is also removed)"""
738

    
739
    @errors.generic.all
740
    @errors.cyclades.connection
741
    @errors.plankton.id
742
    def _run(self, image_id):
743
        self._optional_output(self.client.delete_image(image_id))
744

    
745
    def main(self, image_id):
746
        super(self.__class__, self)._run()
747
        self._run(image_id=image_id)
748

    
749

    
750
@command(image_cmds)
751
class image_compute_properties(_init_cyclades):
752
    """Manage properties related to OS installation in an image"""
753

    
754

    
755
@command(image_cmds)
756
class image_compute_properties_list(_init_cyclades, _optional_json):
757
    """List all image properties"""
758

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

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

    
769

    
770
@command(image_cmds)
771
class image_compute_properties_get(_init_cyclades, _optional_json):
772
    """Get an image property"""
773

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

    
781
    def main(self, image_id, key):
782
        super(self.__class__, self)._run()
783
        self._run(image_id=image_id, key=key)
784

    
785

    
786
@command(image_cmds)
787
class image_compute_properties_add(_init_cyclades, _optional_json):
788
    """Add a property to an image"""
789

    
790
    @errors.generic.all
791
    @errors.cyclades.connection
792
    @errors.plankton.id
793
    @errors.plankton.metadata
794
    def _run(self, image_id, key, val):
795
        self._print(
796
            self.client.create_image_metadata(image_id, key, val), print_dict)
797

    
798
    def main(self, image_id, key, val):
799
        super(self.__class__, self)._run()
800
        self._run(image_id=image_id, key=key, val=val)
801

    
802

    
803
@command(image_cmds)
804
class image_compute_properties_set(_init_cyclades, _optional_json):
805
    """Add / update a set of properties for an image
806
    properties must be given in the form key=value, e.v.
807
    /image compute properties set <image-id> key1=val1 key2=val2
808
    """
809

    
810
    @errors.generic.all
811
    @errors.cyclades.connection
812
    @errors.plankton.id
813
    def _run(self, image_id, keyvals):
814
        meta = dict()
815
        for keyval in keyvals:
816
            key, val = keyval.split('=')
817
            meta[key] = val
818
        self._print(
819
            self.client.update_image_metadata(image_id, **meta), print_dict)
820

    
821
    def main(self, image_id, *key_equals_value):
822
        super(self.__class__, self)._run()
823
        self._run(image_id=image_id, keyvals=key_equals_value)
824

    
825

    
826
@command(image_cmds)
827
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
828
    """Delete a property from an image"""
829

    
830
    @errors.generic.all
831
    @errors.cyclades.connection
832
    @errors.plankton.id
833
    @errors.plankton.metadata
834
    def _run(self, image_id, key):
835
        self._optional_output(self.client.delete_image_metadata(image_id, key))
836

    
837
    def main(self, image_id, key):
838
        super(self.__class__, self)._run()
839
        self._run(image_id=image_id, key=key)