Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 00b1248e

History | View | Annotate | Download (32.8 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
from io import StringIO
38
from pydoc import pager
39

    
40
from kamaki.cli import command
41
from kamaki.cli.command_tree import CommandTree
42
from kamaki.cli.utils import filter_dicts_by_dict
43
from kamaki.clients.image import ImageClient
44
from kamaki.clients.pithos import PithosClient
45
from kamaki.clients.astakos import AstakosClient
46
from kamaki.clients import ClientError
47
from kamaki.cli.argument import (
48
    FlagArgument, ValueArgument, RepeatableArgument, KeyValueArgument,
49
    IntArgument, ProgressBarArgument)
50
from kamaki.cli.commands.cyclades import _init_cyclades
51
from kamaki.cli.errors import raiseCLIError, CLIBaseUrlError
52
from kamaki.cli.commands import _command_init, errors, addLogSettings
53
from kamaki.cli.commands import (
54
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
55

    
56

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

    
63

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

    
73
about_image_id = ['To see a list of available image ids: /image list']
74

    
75

    
76
log = getLogger(__name__)
77

    
78

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

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

    
105

    
106
# Plankton Image Commands
107

    
108

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

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

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

118
    :raises TypeError, AttributeError: Invalid json format
119

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

    
138

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

143
    :returns: (dict) json_formated
144

145
    :raises TypeError, AttributeError: Invalid json format
146

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

    
157

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

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

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

    
177

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

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

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

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

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

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

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

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

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

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

    
264
        if self['detail'] and not (
265
                self['json_output'] or self['output_format']):
266
            images = self._add_owner_name(images)
267
        elif detail and not self['detail']:
268
            for img in images:
269
                for key in set(img).difference(self.PERMANENTS):
270
                    img.pop(key)
271
        kwargs = dict(with_enumeration=self['enum'])
272
        if self['limit']:
273
            images = images[:self['limit']]
274
        if self['more']:
275
            kwargs['out'] = StringIO()
276
            kwargs['title'] = ()
277
        self._print(images, **kwargs)
278
        if self['more']:
279
            pager(kwargs['out'].getvalue())
280

    
281
    def main(self):
282
        super(self.__class__, self)._run()
283
        self._run()
284

    
285

    
286
@command(image_cmds)
287
class image_info(_init_image, _optional_json):
288
    """Get image metadata"""
289

    
290
    @errors.generic.all
291
    @errors.plankton.connection
292
    @errors.plankton.id
293
    def _run(self, image_id):
294
        meta = self.client.get_meta(image_id)
295
        if not (self['json_output'] or self['output_format']):
296
            meta['owner'] += ' (%s)' % self._uuid2username(meta['owner'])
297
        self._print(meta, self.print_dict)
298

    
299
    def main(self, image_id):
300
        super(self.__class__, self)._run()
301
        self._run(image_id=image_id)
302

    
303

    
304
@command(image_cmds)
305
class image_modify(_init_image, _optional_json):
306
    """Add / update metadata and properties for an image
307
    The original image preserves the values that are not affected
308
    """
309

    
310
    arguments = dict(
311
        image_name=ValueArgument('Change name', '--name'),
312
        disk_format=ValueArgument('Change disk format', '--disk-format'),
313
        container_format=ValueArgument(
314
            'Change container format', '--container-format'),
315
        status=ValueArgument('Change status', '--status'),
316
        publish=FlagArgument('Publish the image', '--publish'),
317
        unpublish=FlagArgument('Unpublish the image', '--unpublish'),
318
        property_to_set=KeyValueArgument(
319
            'set property in key=value form (can be repeated)',
320
            ('-p', '--property-set')),
321
        property_to_del=RepeatableArgument(
322
            'Delete property by key (can be repeated)', '--property-del')
323
    )
324
    required = [
325
        'image_name', 'disk_format', 'container_format', 'status', 'publish',
326
        'unpublish', 'property_to_set']
327

    
328
    @errors.generic.all
329
    @errors.plankton.connection
330
    @errors.plankton.id
331
    def _run(self, image_id):
332
        meta = self.client.get_meta(image_id)
333
        for k, v in self['property_to_set'].items():
334
            meta['properties'][k.upper()] = v
335
        for k in self['property_to_del']:
336
            meta['properties'][k.upper()] = None
337
        self._optional_output(self.client.update_image(
338
            image_id,
339
            name=self['image_name'],
340
            disk_format=self['disk_format'],
341
            container_format=self['container_format'],
342
            status=self['status'],
343
            public=self['publish'] or self['unpublish'] or None,
344
            **meta['properties']))
345

    
346
    def main(self, image_id):
347
        super(self.__class__, self)._run()
348
        self._run(image_id=image_id)
349

    
350

    
351
@command(image_cmds)
352
class image_register(_init_image, _optional_json):
353
    """(Re)Register an image file to an Image service
354
    The image file must be stored at a pithos repository
355
    Some metadata can be set by user (e.g., disk-format) while others are set
356
    only automatically (e.g., image id). There are also some custom user
357
    metadata, called properties.
358
    A register command creates a remote meta file at
359
    .  <container>:<image path>.meta
360
    Users may download and edit this file and use it to re-register one or more
361
    images.
362
    In case of a meta file, runtime arguments for metadata or properties
363
    override meta file settings.
364
    """
365

    
366
    container_info_cache = {}
367

    
368
    arguments = dict(
369
        checksum=ValueArgument('Set image checksum', '--checksum'),
370
        container_format=ValueArgument(
371
            'Set container format', '--container-format'),
372
        disk_format=ValueArgument('Set disk format', '--disk-format'),
373
        owner_name=ValueArgument('Set user uuid by user name', '--owner-name'),
374
        properties=KeyValueArgument(
375
            'Add property (user-specified metadata) in key=value form'
376
            '(can be repeated)',
377
            ('-p', '--property')),
378
        is_public=FlagArgument('Mark image as public', '--public'),
379
        size=IntArgument('Set image size in bytes', '--size'),
380
        metafile=ValueArgument(
381
            'Load metadata from a json-formated file <img-file>.meta :'
382
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
383
            ('--metafile')),
384
        metafile_force=FlagArgument(
385
            'Overide remote metadata file', ('-f', '--force')),
386
        no_metafile_upload=FlagArgument(
387
            'Do not store metadata in remote meta file',
388
            ('--no-metafile-upload')),
389
        container=ValueArgument(
390
            'Pithos+ container containing the image file',
391
            ('-C', '--container')),
392
        uuid=ValueArgument('Custom user uuid', '--uuid'),
393
        local_image_path=ValueArgument(
394
            'Local image file path to upload and register '
395
            '(still need target file in the form container:remote-path )',
396
            '--upload-image-file'),
397
        progress_bar=ProgressBarArgument(
398
            'Do not use progress bar', '--no-progress-bar', default=False)
399
    )
400

    
401
    def _get_user_id(self):
402
        atoken = self.client.token
403
        if getattr(self, 'auth_base', False):
404
            return self.auth_base.term('id', atoken)
405
        else:
406
            astakos_url = self.config.get('user', 'url') or self.config.get(
407
                'astakos', 'url')
408
            if not astakos_url:
409
                raise CLIBaseUrlError(service='astakos')
410
            user = AstakosClient(astakos_url, atoken)
411
            return user.term('id')
412

    
413
    def _get_pithos_client(self, container):
414
        if self['no_metafile_upload']:
415
            return None
416
        ptoken = self.client.token
417
        if getattr(self, 'auth_base', False):
418
            pithos_endpoints = self.auth_base.get_service_endpoints(
419
                'object-store')
420
            purl = pithos_endpoints['publicURL']
421
        else:
422
            purl = self.config.get_cloud('pithos', 'url')
423
        if not purl:
424
            raise CLIBaseUrlError(service='pithos')
425
        return PithosClient(purl, ptoken, self._get_user_id(), container)
426

    
427
    def _store_remote_metafile(self, pclient, remote_path, metadata):
428
        return pclient.upload_from_string(
429
            remote_path, _validate_image_meta(metadata, return_str=True),
430
            container_info_cache=self.container_info_cache)
431

    
432
    def _load_params_from_file(self, location):
433
        params, properties = dict(), dict()
434
        pfile = self['metafile']
435
        if pfile:
436
            try:
437
                for k, v in _load_image_meta(pfile).items():
438
                    key = k.lower().replace('-', '_')
439
                    if key == 'properties':
440
                        for pk, pv in v.items():
441
                            properties[pk.upper().replace('-', '_')] = pv
442
                    elif key == 'name':
443
                            continue
444
                    elif key == 'location':
445
                        if location:
446
                            continue
447
                        location = v
448
                    else:
449
                        params[key] = v
450
            except Exception as e:
451
                raiseCLIError(e, 'Invalid json metadata config file')
452
        return params, properties, location
453

    
454
    def _load_params_from_args(self, params, properties):
455
        for key in set([
456
                'checksum',
457
                'container_format',
458
                'disk_format',
459
                'owner',
460
                'size',
461
                'is_public']).intersection(self.arguments):
462
            params[key] = self[key]
463
        for k, v in self['properties'].items():
464
            properties[k.upper().replace('-', '_')] = v
465

    
466
    def _validate_location(self, location):
467
        if not location:
468
            raiseCLIError(
469
                'No image file location provided',
470
                importance=2, details=[
471
                    'An image location is needed. Image location format:',
472
                    '  <container>:<path>',
473
                    ' where an image file at the above location must exist.'
474
                    ] + howto_image_file)
475
        try:
476
            return _validate_image_location(location)
477
        except AssertionError as ae:
478
            raiseCLIError(
479
                ae, 'Invalid image location format',
480
                importance=1, details=[
481
                    'Valid image location format:',
482
                    '  <container>:<img-file-path>'
483
                    ] + howto_image_file)
484

    
485
    @staticmethod
486
    def _old_location_format(location):
487
        prefix = 'pithos://'
488
        try:
489
            if location.startswith(prefix):
490
                uuid, sep, rest = location[len(prefix):].partition('/')
491
                container, sep, path = rest.partition('/')
492
                return (uuid, container, path)
493
        except Exception as e:
494
            raiseCLIError(e, 'Invalid location format', details=[
495
                'Correct location format:', '  <container>:<image path>'])
496
        return ()
497

    
498
    def _mine_location(self, container_path):
499
        old_response = self._old_location_format(container_path)
500
        if old_response:
501
            return old_response
502
        uuid = self['uuid'] or (self._username2uuid(self['owner_name']) if (
503
                    self['owner_name']) else self._get_user_id())
504
        if not uuid:
505
            if self['owner_name']:
506
                raiseCLIError('No user with username %s' % self['owner_name'])
507
            raiseCLIError('Failed to get user uuid', details=[
508
                'For details on current user:',
509
                '  /user whoami',
510
                'To authenticate a new user through a user token:',
511
                '  /user authenticate <token>'])
512
        if self['container']:
513
            return uuid, self['container'], container_path
514
        container, sep, path = container_path.partition(':')
515
        if not (bool(container) and bool(path)):
516
            raiseCLIError(
517
                'Incorrect container-path format', importance=1, details=[
518
                'Use : to seperate container form path',
519
                '  <container>:<image-path>',
520
                'OR',
521
                'Use -C to specifiy a container',
522
                '  -C <container> <image-path>'] + howto_image_file)
523

    
524
        return uuid, container, path
525

    
526
    @errors.generic.all
527
    @errors.plankton.connection
528
    @errors.pithos.container
529
    def _run(self, name, uuid, dst_cont, img_path):
530
        if self['local_image_path']:
531
            with open(self['local_image_path']) as f:
532
                pithos = self._get_pithos_client(dst_cont)
533
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
534
                if pbar:
535
                    hash_bar = pbar.clone()
536
                    hash_cb = hash_bar.get_generator('Calculating hashes')
537
                pithos.upload_object(
538
                    img_path, f,
539
                    hash_cb=hash_cb, upload_cb=upload_cb,
540
                    container_info_cache=self.container_info_cache)
541
                pbar.finish()
542

    
543
        location = 'pithos://%s/%s/%s' % (uuid, dst_cont, img_path)
544
        (params, properties, new_loc) = self._load_params_from_file(location)
545
        if location != new_loc:
546
            uuid, dst_cont, img_path = self._validate_location(new_loc)
547
        self._load_params_from_args(params, properties)
548
        pclient = self._get_pithos_client(dst_cont)
549

    
550
        #check if metafile exists
551
        meta_path = '%s.meta' % img_path
552
        if pclient and not self['metafile_force']:
553
            try:
554
                pclient.get_object_info(meta_path)
555
                raiseCLIError(
556
                    'Metadata file %s:%s already exists, abort' % (
557
                        dst_cont, meta_path),
558
                    details=['Registration ABORTED', 'Try -f to overwrite'])
559
            except ClientError as ce:
560
                if ce.status != 404:
561
                    raise
562

    
563
        #register the image
564
        try:
565
            r = self.client.register(name, location, params, properties)
566
        except ClientError as ce:
567
            if ce.status in (400, 404):
568
                raiseCLIError(
569
                    ce, 'Nonexistent image file location\n\t%s' % location,
570
                    details=[
571
                        'Does the image file %s exist at container %s ?' % (
572
                            img_path, dst_cont)] + howto_image_file)
573
            raise
574
        r['owner'] += ' (%s)' % self._uuid2username(r['owner'])
575
        self._print(r, self.print_dict)
576

    
577
        #upload the metadata file
578
        if pclient:
579
            try:
580
                meta_headers = pclient.upload_from_string(
581
                    meta_path, dumps(r, indent=2),
582
                    container_info_cache=self.container_info_cache)
583
            except TypeError:
584
                self.error(
585
                    'Failed to dump metafile %s:%s' % (dst_cont, meta_path))
586
                return
587
            if self['json_output'] or self['output_format']:
588
                self.print_json(dict(
589
                    metafile_location='%s:%s' % (dst_cont, meta_path),
590
                    headers=meta_headers))
591
            else:
592
                self.error('Metadata file uploaded as %s:%s (version %s)' % (
593
                    dst_cont, meta_path, meta_headers['x-object-version']))
594

    
595
    def main(self, name, container___image_path):
596
        super(self.__class__, self)._run()
597
        self._run(name, *self._mine_location(container___image_path))
598

    
599

    
600
@command(image_cmds)
601
class image_unregister(_init_image, _optional_output_cmd):
602
    """Unregister an image (does not delete the image file)"""
603

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

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

    
614

    
615
@command(image_cmds)
616
class image_shared(_init_image, _optional_json):
617
    """List images shared by a member"""
618

    
619
    @errors.generic.all
620
    @errors.plankton.connection
621
    def _run(self, member):
622
        r = self.client.list_shared(member)
623
        self._print(r, title=('image_id',))
624

    
625
    def main(self, member_id_or_username):
626
        super(self.__class__, self)._run()
627
        self._run(member_id_or_username)
628

    
629

    
630
@command(image_cmds)
631
class image_members(_init_image):
632
    """Manage members. Members of an image are users who can modify it"""
633

    
634

    
635
@command(image_cmds)
636
class image_members_list(_init_image, _optional_json):
637
    """List members of an image"""
638

    
639
    @errors.generic.all
640
    @errors.plankton.connection
641
    @errors.plankton.id
642
    def _run(self, image_id):
643
        members = self.client.list_members(image_id)
644
        if not (self['json_output'] or self['output_format']):
645
            uuids = [member['member_id'] for member in members]
646
            usernames = self._uuids2usernames(uuids)
647
            for member in members:
648
                member['member_id'] += ' (%s)' % usernames[member['member_id']]
649
        self._print(members, title=('member_id',))
650

    
651
    def main(self, image_id):
652
        super(self.__class__, self)._run()
653
        self._run(image_id=image_id)
654

    
655

    
656
@command(image_cmds)
657
class image_members_add(_init_image, _optional_output_cmd):
658
    """Add a member to an image"""
659

    
660
    @errors.generic.all
661
    @errors.plankton.connection
662
    @errors.plankton.id
663
    def _run(self, image_id=None, member=None):
664
            self._optional_output(self.client.add_member(image_id, member))
665

    
666
    def main(self, image_id, member_id):
667
        super(self.__class__, self)._run()
668
        self._run(image_id=image_id, member=member_id)
669

    
670

    
671
@command(image_cmds)
672
class image_members_delete(_init_image, _optional_output_cmd):
673
    """Remove a member from an image"""
674

    
675
    @errors.generic.all
676
    @errors.plankton.connection
677
    @errors.plankton.id
678
    def _run(self, image_id=None, member=None):
679
            self._optional_output(self.client.remove_member(image_id, member))
680

    
681
    def main(self, image_id, member):
682
        super(self.__class__, self)._run()
683
        self._run(image_id=image_id, member=member)
684

    
685

    
686
@command(image_cmds)
687
class image_members_set(_init_image, _optional_output_cmd):
688
    """Set the members of an image"""
689

    
690
    @errors.generic.all
691
    @errors.plankton.connection
692
    @errors.plankton.id
693
    def _run(self, image_id, members):
694
            self._optional_output(self.client.set_members(image_id, members))
695

    
696
    def main(self, image_id, *member_ids):
697
        super(self.__class__, self)._run()
698
        self._run(image_id=image_id, members=member_ids)
699

    
700
# Compute Image Commands
701

    
702

    
703
@command(image_cmds)
704
class image_compute(_init_cyclades):
705
    """Cyclades/Compute API image commands"""
706

    
707

    
708
@command(image_cmds)
709
class image_compute_list(
710
        _init_cyclades, _optional_json, _name_filter, _id_filter):
711
    """List images"""
712

    
713
    PERMANENTS = ('id', 'name')
714

    
715
    arguments = dict(
716
        detail=FlagArgument('show detailed output', ('-l', '--details')),
717
        limit=IntArgument('limit number listed images', ('-n', '--number')),
718
        more=FlagArgument('handle long lists of results', '--more'),
719
        enum=FlagArgument('Enumerate results', '--enumerate'),
720
        user_id=ValueArgument('filter by user_id', '--user-id'),
721
        user_name=ValueArgument('filter by username', '--user-name'),
722
        meta=KeyValueArgument(
723
            'filter by metadata key=value (can be repeated)', ('--metadata')),
724
        meta_like=KeyValueArgument(
725
            'filter by metadata key=value (can be repeated)',
726
            ('--metadata-like'))
727
    )
728

    
729
    def _filter_by_metadata(self, images):
730
        new_images = []
731
        for img in images:
732
            meta = [dict(img['metadata'])]
733
            if self['meta']:
734
                meta = filter_dicts_by_dict(meta, self['meta'])
735
            if meta and self['meta_like']:
736
                meta = filter_dicts_by_dict(
737
                    meta, self['meta_like'], exact_match=False)
738
            if meta:
739
                new_images.append(img)
740
        return new_images
741

    
742
    def _filter_by_user(self, images):
743
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
744
        return filter_dicts_by_dict(images, dict(user_id=uuid))
745

    
746
    def _add_name(self, images, key='user_id'):
747
        uuids = self._uuids2usernames(
748
            list(set([img[key] for img in images])))
749
        for img in images:
750
            img[key] += ' (%s)' % uuids[img[key]]
751
        return images
752

    
753
    @errors.generic.all
754
    @errors.cyclades.connection
755
    def _run(self):
756
        withmeta = bool(self['meta'] or self['meta_like'])
757
        withuser = bool(self['user_id'] or self['user_name'])
758
        detail = self['detail'] or withmeta or withuser
759
        images = self.client.list_images(detail)
760
        images = self._filter_by_name(images)
761
        images = self._filter_by_id(images)
762
        if withuser:
763
            images = self._filter_by_user(images)
764
        if withmeta:
765
            images = self._filter_by_metadata(images)
766
        if self['detail'] and not (
767
                self['json_output'] or self['output_format']):
768
            images = self._add_name(self._add_name(images, 'tenant_id'))
769
        elif detail and not self['detail']:
770
            for img in images:
771
                for key in set(img).difference(self.PERMANENTS):
772
                    img.pop(key)
773
        kwargs = dict(with_enumeration=self['enum'])
774
        if self['limit']:
775
            images = images[:self['limit']]
776
        if self['more']:
777
            kwargs['out'] = StringIO()
778
            kwargs['title'] = ()
779
        self._print(images, **kwargs)
780
        if self['more']:
781
            pager(kwargs['out'].getvalue())
782

    
783
    def main(self):
784
        super(self.__class__, self)._run()
785
        self._run()
786

    
787

    
788
@command(image_cmds)
789
class image_compute_info(_init_cyclades, _optional_json):
790
    """Get detailed information on an image"""
791

    
792
    @errors.generic.all
793
    @errors.cyclades.connection
794
    @errors.plankton.id
795
    def _run(self, image_id):
796
        image = self.client.get_image_details(image_id)
797
        uuids = [image['user_id'], image['tenant_id']]
798
        usernames = self._uuids2usernames(uuids)
799
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
800
        image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
801
        self._print(image, self.print_dict)
802

    
803
    def main(self, image_id):
804
        super(self.__class__, self)._run()
805
        self._run(image_id=image_id)
806

    
807

    
808
@command(image_cmds)
809
class image_compute_delete(_init_cyclades, _optional_output_cmd):
810
    """Delete an image (WARNING: image file is also removed)"""
811

    
812
    @errors.generic.all
813
    @errors.cyclades.connection
814
    @errors.plankton.id
815
    def _run(self, image_id):
816
        self._optional_output(self.client.delete_image(image_id))
817

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

    
822

    
823
@command(image_cmds)
824
class image_compute_properties(_init_cyclades):
825
    """Manage properties related to OS installation in an image"""
826

    
827

    
828
@command(image_cmds)
829
class image_compute_properties_list(_init_cyclades, _optional_json):
830
    """List all image properties"""
831

    
832
    @errors.generic.all
833
    @errors.cyclades.connection
834
    @errors.plankton.id
835
    def _run(self, image_id):
836
        self._print(self.client.get_image_metadata(image_id), self.print_dict)
837

    
838
    def main(self, image_id):
839
        super(self.__class__, self)._run()
840
        self._run(image_id=image_id)
841

    
842

    
843
@command(image_cmds)
844
class image_compute_properties_get(_init_cyclades, _optional_json):
845
    """Get an image property"""
846

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

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

    
859

    
860
@command(image_cmds)
861
class image_compute_properties_set(_init_cyclades, _optional_json):
862
    """Add / update a set of properties for an image
863
    properties must be given in the form key=value, e.v.
864
    /image compute properties set <image-id> key1=val1 key2=val2
865
    """
866

    
867
    @errors.generic.all
868
    @errors.cyclades.connection
869
    @errors.plankton.id
870
    def _run(self, image_id, keyvals):
871
        meta = dict()
872
        for keyval in keyvals:
873
            key, sep, val = keyval.partition('=')
874
            meta[key] = val
875
        self._print(
876
            self.client.update_image_metadata(image_id, **meta),
877
            self.print_dict)
878

    
879
    def main(self, image_id, *key_equals_value):
880
        super(self.__class__, self)._run()
881
        self._run(image_id=image_id, keyvals=key_equals_value)
882

    
883

    
884
@command(image_cmds)
885
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
886
    """Delete a property from an image"""
887

    
888
    @errors.generic.all
889
    @errors.cyclades.connection
890
    @errors.plankton.id
891
    @errors.plankton.metadata
892
    def _run(self, image_id, key):
893
        self._optional_output(self.client.delete_image_metadata(image_id, key))
894

    
895
    def main(self, image_id, key):
896
        super(self.__class__, self)._run()
897
        self._run(image_id=image_id, key=key)