Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 1716a15d

History | View | Annotate | Download (29.6 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
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, detail, *list_params):
219
        images = []
220
        ouuid = self['owner'] or self._username2uuid(self['owner_name'])
221
        if not ouuid:
222
            return images
223
        for img in self.client.list_public(True, *list_params):
224
            if img['owner'] == ouuid:
225
                if not detail:
226
                    for key in set(img.keys()).difference(self.PERMANENTS):
227
                        img.pop(key)
228
                images.append(img)
229
        return images
230

    
231
    def _filtered_by_name(self, images):
232
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
233
        return [img for img in images if (
234
            (not np) or img['name'].lower().startswith(np.lower())) and (
235
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
236
            (not nl) or nl.lower() in img['name'].lower())]
237

    
238
    def _add_owner_name(self, images):
239
        uuids = self._uuids2usernames(
240
            list(set([img['owner'] for img in images])))
241
        for img in images:
242
            img['owner'] += ' (%s)' % uuids[img['owner']]
243
        return images
244

    
245
    def _filtered_by_properties(self, images):
246
        new_images = []
247

    
248
        def like_properties(props):
249
            plike = self['prop_like']
250
            for k, v in plike.items():
251
                likestr = props.get(k, '').lower()
252
                if v.lower() not in likestr:
253
                    return False
254
            return True
255

    
256
        for img in images:
257
            props = img['properties']
258
            if (
259
                    self['prop'] and set(
260
                        self['prop'].items()).difference(props.items())) or (
261
                    self['prop_like'] and not like_properties(props)):
262
                continue
263
            elif self['detail']:
264
                new_images.append(dict(img))
265
            else:
266
                new_images.append(dict())
267
                for k in set(img).intersection(self.PERMANENTS):
268
                    new_images[-1][k] = img[k]
269
        return new_images
270

    
271
    @errors.generic.all
272
    @errors.cyclades.connection
273
    def _run(self):
274
        super(self.__class__, self)._run()
275
        filters = {}
276
        for arg in set([
277
                'container_format',
278
                'disk_format',
279
                'name',
280
                'size_min',
281
                'size_max',
282
                'status']).intersection(self.arguments):
283
            filters[arg] = self[arg]
284

    
285
        order = self['order']
286
        detail = self['detail'] or self['prop'] or self['prop_like']
287
        if self['owner'] or self['owner_name']:
288
            images = self._filtered_by_owner(detail, filters, order)
289
        else:
290
            images = self.client.list_public(detail, filters, order)
291

    
292
        images = self._filtered_by_name(images)
293
        if self['detail'] and not self['json_output']:
294
            images = self._add_owner_name(images)
295
        if self['prop'] or self['prop_like']:
296
            images = self._filtered_by_properties(images)
297
        kwargs = dict(with_enumeration=self['enum'])
298
        if self['more']:
299
            kwargs['page_size'] = self['limit'] or 10
300
        elif self['limit']:
301
            images = images[:self['limit']]
302
        self._print(images, **kwargs)
303

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

    
308

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

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

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

    
328

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

    
333
    container_info_cache = {}
334

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

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

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

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

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

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

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

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

    
467
        return uuid, container, path
468

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

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

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

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

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

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

    
538

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

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

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

    
553

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

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

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

    
567

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

    
572

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

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

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

    
587

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

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

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

    
617

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

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

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

    
632

    
633
# Compute Image Commands
634

    
635

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

    
640

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

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

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

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

    
664
        def like_metadata(meta):
665
            mlike = self['meta_like']
666
            for k, v in mlike.items():
667
                likestr = meta.get(k, '').lower()
668
                if v.lower() not in likestr:
669
                    return False
670
            return True
671

    
672
        for img in images:
673
            meta = img['metadata']
674
            if (
675
                    self['meta'] and set(
676
                        self['meta'].items()).difference(meta.items())) or (
677
                    self['meta_like'] and not like_metadata(meta)):
678
                continue
679
            elif self['detail']:
680
                new_images.append(dict(img))
681
            else:
682
                new_images.append(dict())
683
                for k in set(img).intersection(self.PERMANENTS):
684
                    new_images[-1][k] = img[k]
685
        return new_images
686

    
687
    def _add_name(self, images, key='user_id'):
688
        uuids = self._uuids2usernames(
689
            list(set([img[key] for img in images])))
690
        for img in images:
691
            img[key] += ' (%s)' % uuids[img[key]]
692
        return images
693

    
694
    @errors.generic.all
695
    @errors.cyclades.connection
696
    def _run(self):
697
        withmeta = bool(self['meta'] or self['meta_like'])
698
        detail = self['detail'] or withmeta
699
        images = self.client.list_images(detail)
700
        if withmeta:
701
            images = self._filter_by_metadata(images)
702
        if self['detail'] and not self['json_output']:
703
            images = self._add_name(self._add_name(images, 'tenant_id'))
704
        kwargs = dict(with_enumeration=self['enum'])
705
        if self['more']:
706
            kwargs['page_size'] = self['limit'] or 10
707
        elif self['limit']:
708
            images = images[:self['limit']]
709
        self._print(images, **kwargs)
710

    
711
    def main(self):
712
        super(self.__class__, self)._run()
713
        self._run()
714

    
715

    
716
@command(image_cmds)
717
class image_compute_info(_init_cyclades, _optional_json):
718
    """Get detailed information on an image"""
719

    
720
    @errors.generic.all
721
    @errors.cyclades.connection
722
    @errors.plankton.id
723
    def _run(self, image_id):
724
        image = self.client.get_image_details(image_id)
725
        self._print(image, print_dict)
726

    
727
    def main(self, image_id):
728
        super(self.__class__, self)._run()
729
        self._run(image_id=image_id)
730

    
731

    
732
@command(image_cmds)
733
class image_compute_delete(_init_cyclades, _optional_output_cmd):
734
    """Delete an image (WARNING: image file is also removed)"""
735

    
736
    @errors.generic.all
737
    @errors.cyclades.connection
738
    @errors.plankton.id
739
    def _run(self, image_id):
740
        self._optional_output(self.client.delete_image(image_id))
741

    
742
    def main(self, image_id):
743
        super(self.__class__, self)._run()
744
        self._run(image_id=image_id)
745

    
746

    
747
@command(image_cmds)
748
class image_compute_properties(_init_cyclades):
749
    """Manage properties related to OS installation in an image"""
750

    
751

    
752
@command(image_cmds)
753
class image_compute_properties_list(_init_cyclades, _optional_json):
754
    """List all image properties"""
755

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

    
762
    def main(self, image_id):
763
        super(self.__class__, self)._run()
764
        self._run(image_id=image_id)
765

    
766

    
767
@command(image_cmds)
768
class image_compute_properties_get(_init_cyclades, _optional_json):
769
    """Get an image property"""
770

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

    
778
    def main(self, image_id, key):
779
        super(self.__class__, self)._run()
780
        self._run(image_id=image_id, key=key)
781

    
782

    
783
@command(image_cmds)
784
class image_compute_properties_add(_init_cyclades, _optional_json):
785
    """Add a property to an image"""
786

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

    
795
    def main(self, image_id, key, val):
796
        super(self.__class__, self)._run()
797
        self._run(image_id=image_id, key=key, val=val)
798

    
799

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

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

    
818
    def main(self, image_id, *key_equals_value):
819
        super(self.__class__, self)._run()
820
        self._run(image_id=image_id, keyvals=key_equals_value)
821

    
822

    
823
@command(image_cmds)
824
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
825
    """Delete a property from an image"""
826

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

    
834
    def main(self, image_id, key):
835
        super(self.__class__, self)._run()
836
        self._run(image_id=image_id, key=key)