Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 89ea97e1

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)', '--name-prefix'),
191
        name_suff=ValueArgument(
192
            'filter by name suffix (case insensitive)', '--name-suffix'),
193
        name_like=ValueArgument(
194
            'print only if name contains this (case insensitive)',
195
            '--name-like'),
196
        size_min=IntArgument('filter by minimum size', '--size-min'),
197
        size_max=IntArgument('filter by maximum size', '--size-max'),
198
        status=ValueArgument('filter by status', '--status'),
199
        owner=ValueArgument('filter by owner', '--owner'),
200
        owner_name=ValueArgument('filter by owners username', '--owner-name'),
201
        order=ValueArgument(
202
            'order by FIELD ( - to reverse order)',
203
            '--order',
204
            default=''),
205
        limit=IntArgument('limit number of listed images', ('-n', '--number')),
206
        more=FlagArgument(
207
            'output results in pages (-n to set items per page, default 10)',
208
            '--more'),
209
        enum=FlagArgument('Enumerate results', '--enumerate'),
210
        prop=KeyValueArgument('filter by property key=value', ('--property')),
211
        prop_like=KeyValueArgument(
212
            'fliter by property key=value where value is part of actual value',
213
            ('--property-like')),
214
    )
215

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

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

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

    
234
    def _filtered_by_properties(self, images):
235
        new_images = []
236
        for img in images:
237
            props = [dict(img['properties'])]
238
            if self['prop']:
239
                props = filter_dicts_by_dict(props, self['prop'])
240
            if props and self['prop_like']:
241
                props = filter_dicts_by_dict(
242
                    props, self['prop_like'], exact_match=False)
243
            if props:
244
                new_images.append(img)
245
        return new_images
246

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

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

    
266
        images = self.client.list_public(detail, filters, order)
267

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

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

    
287
    def main(self):
288
        super(self.__class__, self)._run()
289
        self._run()
290

    
291

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

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

    
307
    def main(self, image_id):
308
        super(self.__class__, self)._run()
309
        self._run(image_id=image_id)
310

    
311

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

    
316
    container_info_cache = {}
317

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

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

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

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

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

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

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

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

    
450
        return uuid, container, path
451

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

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

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

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

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

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

    
521

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

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

    
532
    def main(self, image_id):
533
        super(self.__class__, self)._run()
534
        self._run(image_id=image_id)
535

    
536

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

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

    
546
    def main(self, member):
547
        super(self.__class__, self)._run()
548
        self._run(member)
549

    
550

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

    
555

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

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

    
566
    def main(self, image_id):
567
        super(self.__class__, self)._run()
568
        self._run(image_id=image_id)
569

    
570

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

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

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

    
585

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

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

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

    
600

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

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

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

    
615

    
616
# Compute Image Commands
617

    
618

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

    
623

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

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

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

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

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

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

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

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

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

    
716

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

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

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

    
732

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

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

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

    
747

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

    
752

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

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

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

    
767

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

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

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

    
783

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

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

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

    
800

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

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

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

    
824

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

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

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