Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (33.4 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 (
46
    FlagArgument, ValueArgument, RepeatableArgument, KeyValueArgument,
47
    IntArgument, ProgressBarArgument)
48
from kamaki.cli.commands.cyclades import _init_cyclades
49
from kamaki.cli.errors import raiseCLIError, CLIBaseUrlError
50
from kamaki.cli.commands import _command_init, errors, addLogSettings
51
from kamaki.cli.commands import (
52
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
53

    
54

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

    
61

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

    
71
about_image_id = ['To see a list of available image ids: /image list']
72

    
73

    
74
log = getLogger(__name__)
75

    
76

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

    
101
    def main(self):
102
        self._run()
103

    
104

    
105
# Plankton Image Commands
106

    
107

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

113
    :param return_str: (boolean) if true, return a json dump
114

115
    :returns: (dict) if return_str is not True, else return str
116

117
    :raises TypeError, AttributeError: Invalid json format
118

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

    
137

    
138
def _load_image_meta(filepath):
139
    """
140
    :param filepath: (str) the (relative) path of the metafile
141

142
    :returns: (dict) json_formated
143

144
    :raises TypeError, AttributeError: Invalid json format
145

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

    
156

    
157
def _validate_image_location(location):
158
    """
159
    :param location: (str) pithos://<user-id>/<container>/<image-path>
160

161
    :returns: (<user-id>, <container>, <image-path>)
162

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

    
176

    
177
@command(image_cmds)
178
class image_list(_init_image, _optional_json, _name_filter, _id_filter):
179
    """List images accessible by user"""
180

    
181
    PERMANENTS = (
182
        'id', 'name',
183
        'status', 'container_format', 'disk_format', 'size')
184

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

    
211
    def _filter_by_owner(self, images):
212
        ouuid = self['owner'] or self._username2uuid(self['owner_name'])
213
        return filter_dicts_by_dict(images, dict(owner=ouuid))
214

    
215
    def _add_owner_name(self, images):
216
        uuids = self._uuids2usernames(
217
            list(set([img['owner'] for img in images])))
218
        for img in images:
219
            img['owner'] += ' (%s)' % uuids[img['owner']]
220
        return images
221

    
222
    def _filter_by_properties(self, images):
223
        new_images = []
224
        for img in images:
225
            props = [dict(img['properties'])]
226
            if self['prop']:
227
                props = filter_dicts_by_dict(props, self['prop'])
228
            if props and self['prop_like']:
229
                props = filter_dicts_by_dict(
230
                    props, self['prop_like'], exact_match=False)
231
            if props:
232
                new_images.append(img)
233
        return new_images
234

    
235
    @errors.generic.all
236
    @errors.cyclades.connection
237
    def _run(self):
238
        super(self.__class__, self)._run()
239
        filters = {}
240
        for arg in set([
241
                'container_format',
242
                'disk_format',
243
                'name',
244
                'size_min',
245
                'size_max',
246
                'status']).intersection(self.arguments):
247
            filters[arg] = self[arg]
248

    
249
        order = self['order']
250
        detail = self['detail'] or (
251
            self['prop'] or self['prop_like']) or (
252
            self['owner'] or self['owner_name'])
253

    
254
        images = self.client.list_public(detail, filters, order)
255

    
256
        if self['owner'] or self['owner_name']:
257
            images = self._filter_by_owner(images)
258
        if self['prop'] or self['prop_like']:
259
            images = self._filter_by_properties(images)
260
        images = self._filter_by_id(images)
261
        images = self._non_exact_name_filter(images)
262

    
263
        if self['detail'] and not self['json_output']:
264
            images = self._add_owner_name(images)
265
        elif detail and not self['detail']:
266
            for img in images:
267
                for key in set(img).difference(self.PERMANENTS):
268
                    img.pop(key)
269
        kwargs = dict(with_enumeration=self['enum'])
270
        if self['more']:
271
            kwargs['page_size'] = self['limit'] or 10
272
        elif self['limit']:
273
            images = images[:self['limit']]
274
        self._print(images, **kwargs)
275

    
276
    def main(self):
277
        super(self.__class__, self)._run()
278
        self._run()
279

    
280

    
281
@command(image_cmds)
282
class image_meta(_init_image):
283
    """Manage image metadata and custom properties"""
284

    
285

    
286
@command(image_cmds)
287
class image_info(_init_image, _optional_json):
288
    """Get image metadata
289
    Image metadata include:
290
    - image file information (location, size, etc.)
291
    - image information (id, name, etc.)
292
    - image os properties (os, fs, etc.)
293
    """
294

    
295
    @errors.generic.all
296
    @errors.plankton.connection
297
    @errors.plankton.id
298
    def _run(self, image_id):
299
        meta = self.client.get_meta(image_id)
300
        if not self['json_output']:
301
            meta['owner'] += ' (%s)' % self._uuid2username(meta['owner'])
302
        self._print(meta, print_dict)
303

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

    
308

    
309
@command(image_cmds)
310
class image_meta_set(_init_image, _optional_output_cmd):
311
    """Add / update metadata and properties for an image
312
    The original image preserves the values that are not affected
313
    """
314

    
315
    arguments = dict(
316
        name=ValueArgument('Set a new name', ('--name')),
317
        disk_format=ValueArgument('Set a new disk format', ('--disk-format')),
318
        container_format=ValueArgument(
319
            'Set a new container format', ('--container-format')),
320
        status=ValueArgument('Set a new status', ('--status')),
321
        publish=FlagArgument('publish the image', ('--publish')),
322
        unpublish=FlagArgument('unpublish the image', ('--unpublish')),
323
        properties=KeyValueArgument(
324
            'set property in key=value form (can be repeated)',
325
            ('-p', '--property'))
326
    )
327

    
328
    def _check_empty(self):
329
        for term in (
330
                'name', 'disk_format', 'container_format', 'status', 'publish',
331
                'unpublish', 'properties'):
332
            if self['term']:
333
                if self['publish'] and self['unpublish']:
334
                    raiseCLIError(
335
                        '--publish and --unpublish are mutually exclusive')
336
                return
337
        raiseCLIError(
338
            'Nothing to update, please use arguments (-h for a list)')
339

    
340
    @errors.generic.all
341
    @errors.plankton.connection
342
    @errors.plankton.id
343
    def _run(self, image_id):
344
        self._check_empty()
345
        meta = self.client.get_meta(image_id)
346
        for k, v in self['properties'].items():
347
            meta['properties'][k.upper()] = v
348
        self._optional_output(self.client.update_image(
349
            image_id,
350
            name=self['name'],
351
            disk_format=self['disk_format'],
352
            container_format=self['container_format'],
353
            status=self['status'],
354
            public=self['publish'] or self['unpublish'] or None,
355
            **meta['properties']))
356

    
357
    def main(self, image_id):
358
        super(self.__class__, self)._run()
359
        self._run(image_id=image_id)
360

    
361

    
362
@command(image_cmds)
363
class image_meta_delete(_init_image, _optional_output_cmd):
364
    """Remove/empty image metadata and/or custom properties"""
365

    
366
    arguments = dict(
367
        disk_format=FlagArgument('Empty disk format', ('--disk-format')),
368
        container_format=FlagArgument(
369
            'Empty container format', ('--container-format')),
370
        status=FlagArgument('Empty status', ('--status')),
371
        properties=RepeatableArgument(
372
            'Property keys to remove', ('-p', '--property'))
373
    )
374

    
375
    def _check_empty(self):
376
        for term in (
377
                'disk_format', 'container_format', 'status', 'properties'):
378
            if self[term]:
379
                return
380
        raiseCLIError(
381
            'Nothing to update, please use arguments (-h for a list)')
382

    
383
    @errors.generic.all
384
    @errors.plankton.connection
385
    @errors.plankton.id
386
    def _run(self, image_id):
387
        self._check_empty()
388
        meta = self.client.get_meta(image_id)
389
        for k in self['properties']:
390
            meta['properties'].pop(k.upper(), None)
391
        self._optional_output(self.client.update_image(
392
            image_id,
393
            disk_format='' if self['disk_format'] else None,
394
            container_format='' if self['container_format'] else None,
395
            status='' if self['status'] else None,
396
            **meta['properties']))
397

    
398
    def main(self, image_id):
399
        super(self.__class__, self)._run()
400
        self._run(image_id=image_id)
401

    
402

    
403
@command(image_cmds)
404
class image_register(_init_image, _optional_json):
405
    """(Re)Register an image"""
406

    
407
    container_info_cache = {}
408

    
409
    arguments = dict(
410
        checksum=ValueArgument('set image checksum', '--checksum'),
411
        container_format=ValueArgument(
412
            'set container format',
413
            '--container-format'),
414
        disk_format=ValueArgument('set disk format', '--disk-format'),
415
        #owner=ValueArgument('set image owner (admin only)', '--owner'),
416
        properties=KeyValueArgument(
417
            'add property in key=value form (can be repeated)',
418
            ('-p', '--property')),
419
        is_public=FlagArgument('mark image as public', '--public'),
420
        size=IntArgument('set image size', '--size'),
421
        metafile=ValueArgument(
422
            'Load metadata from a json-formated file <img-file>.meta :'
423
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
424
            ('--metafile')),
425
        metafile_force=FlagArgument(
426
            'Store remote metadata object, even if it already exists',
427
            ('-f', '--force')),
428
        no_metafile_upload=FlagArgument(
429
            'Do not store metadata in remote meta file',
430
            ('--no-metafile-upload')),
431
        container=ValueArgument(
432
            'Pithos+ container containing the image file',
433
            ('-C', '--container')),
434
        uuid=ValueArgument('Custom user uuid', '--uuid'),
435
        local_image_path=ValueArgument(
436
            'Local image file path to upload and register '
437
            '(still need target file in the form container:remote-path )',
438
            '--upload-image-file'),
439
        progress_bar=ProgressBarArgument(
440
            'Do not use progress bar', '--no-progress-bar', default=False)
441
    )
442

    
443
    def _get_user_id(self):
444
        atoken = self.client.token
445
        if getattr(self, 'auth_base', False):
446
            return self.auth_base.term('id', atoken)
447
        else:
448
            astakos_url = self.config.get('user', 'url')\
449
                or self.config.get('astakos', 'url')
450
            if not astakos_url:
451
                raise CLIBaseUrlError(service='astakos')
452
            user = AstakosClient(astakos_url, atoken)
453
            return user.term('id')
454

    
455
    def _get_pithos_client(self, container):
456
        if self['no_metafile_upload']:
457
            return None
458
        ptoken = self.client.token
459
        if getattr(self, 'auth_base', False):
460
            pithos_endpoints = self.auth_base.get_service_endpoints(
461
                'object-store')
462
            purl = pithos_endpoints['publicURL']
463
        else:
464
            purl = self.config.get_cloud('pithos', 'url')
465
        if not purl:
466
            raise CLIBaseUrlError(service='pithos')
467
        return PithosClient(purl, ptoken, self._get_user_id(), container)
468

    
469
    def _store_remote_metafile(self, pclient, remote_path, metadata):
470
        return pclient.upload_from_string(
471
            remote_path, _validate_image_meta(metadata, return_str=True),
472
            container_info_cache=self.container_info_cache)
473

    
474
    def _load_params_from_file(self, location):
475
        params, properties = dict(), dict()
476
        pfile = self['metafile']
477
        if pfile:
478
            try:
479
                for k, v in _load_image_meta(pfile).items():
480
                    key = k.lower().replace('-', '_')
481
                    if k == 'properties':
482
                        for pk, pv in v.items():
483
                            properties[pk.upper().replace('-', '_')] = pv
484
                    elif key == 'name':
485
                            continue
486
                    elif key == 'location':
487
                        if location:
488
                            continue
489
                        location = v
490
                    else:
491
                        params[key] = v
492
            except Exception as e:
493
                raiseCLIError(e, 'Invalid json metadata config file')
494
        return params, properties, location
495

    
496
    def _load_params_from_args(self, params, properties):
497
        for key in set([
498
                'checksum',
499
                'container_format',
500
                'disk_format',
501
                'owner',
502
                'size',
503
                'is_public']).intersection(self.arguments):
504
            params[key] = self[key]
505
        for k, v in self['properties'].items():
506
            properties[k.upper().replace('-', '_')] = v
507

    
508
    def _validate_location(self, location):
509
        if not location:
510
            raiseCLIError(
511
                'No image file location provided',
512
                importance=2, details=[
513
                    'An image location is needed. Image location format:',
514
                    '  pithos://<user-id>/<container>/<path>',
515
                    ' where an image file at the above location must exist.'
516
                    ] + howto_image_file)
517
        try:
518
            return _validate_image_location(location)
519
        except AssertionError as ae:
520
            raiseCLIError(
521
                ae, 'Invalid image location format',
522
                importance=1, details=[
523
                    'Valid image location format:',
524
                    '  pithos://<user-id>/<container>/<img-file-path>'
525
                    ] + howto_image_file)
526

    
527
    def _mine_location(self, container_path):
528
        uuid = self['uuid'] or self._get_user_id()
529
        if self['container']:
530
            return uuid, self['container'], container_path
531
        container, sep, path = container_path.partition(':')
532
        if not (bool(container) and bool(path)):
533
            raiseCLIError(
534
                'Incorrect container-path format', importance=1, details=[
535
                'Use : to seperate container form path',
536
                '  <container>:<image-path>',
537
                'OR',
538
                'Use -C to specifiy a container',
539
                '  -C <container> <image-path>'] + howto_image_file)
540

    
541
        return uuid, container, path
542

    
543
    @errors.generic.all
544
    @errors.plankton.connection
545
    @errors.pithos.container
546
    def _run(self, name, uuid, dst_cont, img_path):
547
        if self['local_image_path']:
548
            with open(self['local_image_path']) as f:
549
                pithos = self._get_pithos_client(dst_cont)
550
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
551
                if pbar:
552
                    hash_bar = pbar.clone()
553
                    hash_cb = hash_bar.get_generator('Calculating hashes')
554
                pithos.upload_object(
555
                    img_path, f,
556
                    hash_cb=hash_cb, upload_cb=upload_cb,
557
                    container_info_cache=self.container_info_cache)
558
                pbar.finish()
559

    
560
        location = 'pithos://%s/%s/%s' % (uuid, dst_cont, img_path)
561
        (params, properties, new_loc) = self._load_params_from_file(location)
562
        if location != new_loc:
563
            uuid, dst_cont, img_path = self._validate_location(new_loc)
564
        self._load_params_from_args(params, properties)
565
        pclient = self._get_pithos_client(dst_cont)
566

    
567
        #check if metafile exists
568
        meta_path = '%s.meta' % img_path
569
        if pclient and not self['metafile_force']:
570
            try:
571
                pclient.get_object_info(meta_path)
572
                raiseCLIError(
573
                    'Metadata file %s:%s already exists, abort' % (
574
                        dst_cont, meta_path),
575
                    details=['Registration ABORTED', 'Try -f to overwrite'])
576
            except ClientError as ce:
577
                if ce.status != 404:
578
                    raise
579

    
580
        #register the image
581
        try:
582
            r = self.client.register(name, location, params, properties)
583
        except ClientError as ce:
584
            if ce.status in (400, 404):
585
                raiseCLIError(
586
                    ce, 'Nonexistent image file location\n\t%s' % location,
587
                    details=[
588
                        'Does the image file %s exist at container %s ?' % (
589
                            img_path, dst_cont)] + howto_image_file)
590
            raise
591
        r['owner'] += '( %s)' % self._uuid2username(r['owner'])
592
        self._print(r, print_dict)
593

    
594
        #upload the metadata file
595
        if pclient:
596
            try:
597
                meta_headers = pclient.upload_from_string(
598
                    meta_path, dumps(r, indent=2),
599
                    container_info_cache=self.container_info_cache)
600
            except TypeError:
601
                print('Failed to dump metafile %s:%s' % (dst_cont, meta_path))
602
                return
603
            if self['json_output']:
604
                print_json(dict(
605
                    metafile_location='%s:%s' % (dst_cont, meta_path),
606
                    headers=meta_headers))
607
            else:
608
                print('Metadata file uploaded as %s:%s (version %s)' % (
609
                    dst_cont, meta_path, meta_headers['x-object-version']))
610

    
611
    def main(self, name, container___image_path):
612
        super(self.__class__, self)._run()
613
        self._run(name, *self._mine_location(container___image_path))
614

    
615

    
616
@command(image_cmds)
617
class image_unregister(_init_image, _optional_output_cmd):
618
    """Unregister an image (does not delete the image file)"""
619

    
620
    @errors.generic.all
621
    @errors.plankton.connection
622
    @errors.plankton.id
623
    def _run(self, image_id):
624
        self._optional_output(self.client.unregister(image_id))
625

    
626
    def main(self, image_id):
627
        super(self.__class__, self)._run()
628
        self._run(image_id=image_id)
629

    
630

    
631
@command(image_cmds)
632
class image_shared(_init_image, _optional_json):
633
    """List images shared by a member"""
634

    
635
    @errors.generic.all
636
    @errors.plankton.connection
637
    def _run(self, member):
638
        r = self.client.list_shared(member)
639
        if r:
640
            uuid = self._username2uuid(member)
641
            r = self.client.list_shared(uuid) if uuid else []
642
        self._print(r, title=('image_id',))
643

    
644
    def main(self, member_id_or_username):
645
        super(self.__class__, self)._run()
646
        self._run(member_id_or_username)
647

    
648

    
649
@command(image_cmds)
650
class image_members(_init_image):
651
    """Manage members. Members of an image are users who can modify it"""
652

    
653

    
654
@command(image_cmds)
655
class image_members_list(_init_image, _optional_json):
656
    """List members of an image"""
657

    
658
    @errors.generic.all
659
    @errors.plankton.connection
660
    @errors.plankton.id
661
    def _run(self, image_id):
662
        members = self.client.list_members(image_id)
663
        if not self['json_output']:
664
            uuids = [member['member_id'] for member in members]
665
            usernames = self._uuids2usernames(uuids)
666
            for member in members:
667
                member['member_id'] += ' (%s)' % usernames[member['member_id']]
668
        self._print(members, title=('member_id',))
669

    
670
    def main(self, image_id):
671
        super(self.__class__, self)._run()
672
        self._run(image_id=image_id)
673

    
674

    
675
@command(image_cmds)
676
class image_members_add(_init_image, _optional_output_cmd):
677
    """Add a member to an image"""
678

    
679
    @errors.generic.all
680
    @errors.plankton.connection
681
    @errors.plankton.id
682
    def _run(self, image_id=None, member=None):
683
            self._optional_output(self.client.add_member(image_id, member))
684

    
685
    def main(self, image_id, member_id):
686
        super(self.__class__, self)._run()
687
        self._run(image_id=image_id, member=member_id)
688

    
689

    
690
@command(image_cmds)
691
class image_members_delete(_init_image, _optional_output_cmd):
692
    """Remove a member from an image"""
693

    
694
    @errors.generic.all
695
    @errors.plankton.connection
696
    @errors.plankton.id
697
    def _run(self, image_id=None, member=None):
698
            self._optional_output(self.client.remove_member(image_id, member))
699

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

    
704

    
705
@command(image_cmds)
706
class image_members_set(_init_image, _optional_output_cmd):
707
    """Set the members of an image"""
708

    
709
    @errors.generic.all
710
    @errors.plankton.connection
711
    @errors.plankton.id
712
    def _run(self, image_id, members):
713
            self._optional_output(self.client.set_members(image_id, members))
714

    
715
    def main(self, image_id, *member_ids):
716
        super(self.__class__, self)._run()
717
        self._run(image_id=image_id, members=member_ids)
718

    
719
# Compute Image Commands
720

    
721

    
722
@command(image_cmds)
723
class image_compute(_init_cyclades):
724
    """Cyclades/Compute API image commands"""
725

    
726

    
727
@command(image_cmds)
728
class image_compute_list(
729
        _init_cyclades, _optional_json, _name_filter, _id_filter):
730
    """List images"""
731

    
732
    PERMANENTS = ('id', 'name')
733

    
734
    arguments = dict(
735
        detail=FlagArgument('show detailed output', ('-l', '--details')),
736
        limit=IntArgument('limit number listed images', ('-n', '--number')),
737
        more=FlagArgument(
738
            'output results in pages (-n to set items per page, default 10)',
739
            '--more'),
740
        enum=FlagArgument('Enumerate results', '--enumerate'),
741
        user_id=ValueArgument('filter by user_id', '--user-id'),
742
        user_name=ValueArgument('filter by username', '--user-name'),
743
        meta=KeyValueArgument(
744
            'filter by metadata key=value (can be repeated)', ('--metadata')),
745
        meta_like=KeyValueArgument(
746
            'filter by metadata key=value (can be repeated)',
747
            ('--metadata-like'))
748
    )
749

    
750
    def _filter_by_metadata(self, images):
751
        new_images = []
752
        for img in images:
753
            meta = [dict(img['metadata'])]
754
            if self['meta']:
755
                meta = filter_dicts_by_dict(meta, self['meta'])
756
            if meta and self['meta_like']:
757
                meta = filter_dicts_by_dict(
758
                    meta, self['meta_like'], exact_match=False)
759
            if meta:
760
                new_images.append(img)
761
        return new_images
762

    
763
    def _filter_by_user(self, images):
764
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
765
        return filter_dicts_by_dict(images, dict(user_id=uuid))
766

    
767
    def _add_name(self, images, key='user_id'):
768
        uuids = self._uuids2usernames(
769
            list(set([img[key] for img in images])))
770
        for img in images:
771
            img[key] += ' (%s)' % uuids[img[key]]
772
        return images
773

    
774
    @errors.generic.all
775
    @errors.cyclades.connection
776
    def _run(self):
777
        withmeta = bool(self['meta'] or self['meta_like'])
778
        withuser = bool(self['user_id'] or self['user_name'])
779
        detail = self['detail'] or withmeta or withuser
780
        images = self.client.list_images(detail)
781
        images = self._filter_by_name(images)
782
        images = self._filter_by_id(images)
783
        if withuser:
784
            images = self._filter_by_user(images)
785
        if withmeta:
786
            images = self._filter_by_metadata(images)
787
        if self['detail'] and not self['json_output']:
788
            images = self._add_name(self._add_name(images, 'tenant_id'))
789
        elif detail and not self['detail']:
790
            for img in images:
791
                for key in set(img).difference(self.PERMANENTS):
792
                    img.pop(key)
793
        kwargs = dict(with_enumeration=self['enum'])
794
        if self['more']:
795
            kwargs['page_size'] = self['limit'] or 10
796
        elif self['limit']:
797
            images = images[:self['limit']]
798
        self._print(images, **kwargs)
799

    
800
    def main(self):
801
        super(self.__class__, self)._run()
802
        self._run()
803

    
804

    
805
@command(image_cmds)
806
class image_compute_info(_init_cyclades, _optional_json):
807
    """Get detailed information on an image"""
808

    
809
    @errors.generic.all
810
    @errors.cyclades.connection
811
    @errors.plankton.id
812
    def _run(self, image_id):
813
        image = self.client.get_image_details(image_id)
814
        uuids = [image['user_id'], image['tenant_id']]
815
        usernames = self._uuids2usernames(uuids)
816
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
817
        image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
818
        self._print(image, print_dict)
819

    
820
    def main(self, image_id):
821
        super(self.__class__, self)._run()
822
        self._run(image_id=image_id)
823

    
824

    
825
@command(image_cmds)
826
class image_compute_delete(_init_cyclades, _optional_output_cmd):
827
    """Delete an image (WARNING: image file is also removed)"""
828

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

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

    
839

    
840
@command(image_cmds)
841
class image_compute_properties(_init_cyclades):
842
    """Manage properties related to OS installation in an image"""
843

    
844

    
845
@command(image_cmds)
846
class image_compute_properties_list(_init_cyclades, _optional_json):
847
    """List all image properties"""
848

    
849
    @errors.generic.all
850
    @errors.cyclades.connection
851
    @errors.plankton.id
852
    def _run(self, image_id):
853
        self._print(self.client.get_image_metadata(image_id), print_dict)
854

    
855
    def main(self, image_id):
856
        super(self.__class__, self)._run()
857
        self._run(image_id=image_id)
858

    
859

    
860
@command(image_cmds)
861
class image_compute_properties_get(_init_cyclades, _optional_json):
862
    """Get an image property"""
863

    
864
    @errors.generic.all
865
    @errors.cyclades.connection
866
    @errors.plankton.id
867
    @errors.plankton.metadata
868
    def _run(self, image_id, key):
869
        self._print(self.client.get_image_metadata(image_id, key), print_dict)
870

    
871
    def main(self, image_id, key):
872
        super(self.__class__, self)._run()
873
        self._run(image_id=image_id, key=key)
874

    
875

    
876
#@command(image_cmds)
877
#class image_compute_properties_add(_init_cyclades, _optional_json):
878
#    """Add a property to an image"""
879
#
880
#    @errors.generic.all
881
#    @errors.cyclades.connection
882
#    @errors.plankton.id
883
#    @errors.plankton.metadata
884
#    def _run(self, image_id, key, val):
885
#        self._print(
886
#            self.client.create_image_metadata(image_id, key, val), print_dict)
887
#
888
#    def main(self, image_id, key, val):
889
#        super(self.__class__, self)._run()
890
#        self._run(image_id=image_id, key=key, val=val)
891

    
892

    
893
@command(image_cmds)
894
class image_compute_properties_set(_init_cyclades, _optional_json):
895
    """Add / update a set of properties for an image
896
    properties must be given in the form key=value, e.v.
897
    /image compute properties set <image-id> key1=val1 key2=val2
898
    """
899

    
900
    @errors.generic.all
901
    @errors.cyclades.connection
902
    @errors.plankton.id
903
    def _run(self, image_id, keyvals):
904
        meta = dict()
905
        for keyval in keyvals:
906
            key, sep, val = keyval.partition('=')
907
            meta[key] = val
908
        self._print(
909
            self.client.update_image_metadata(image_id, **meta), print_dict)
910

    
911
    def main(self, image_id, *key_equals_value):
912
        super(self.__class__, self)._run()
913
        self._run(image_id=image_id, keyvals=key_equals_value)
914

    
915

    
916
@command(image_cmds)
917
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
918
    """Delete a property from an image"""
919

    
920
    @errors.generic.all
921
    @errors.cyclades.connection
922
    @errors.plankton.id
923
    @errors.plankton.metadata
924
    def _run(self, image_id, key):
925
        self._optional_output(self.client.delete_image_metadata(image_id, key))
926

    
927
    def main(self, image_id, key):
928
        super(self.__class__, self)._run()
929
        self._run(image_id=image_id, key=key)