Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 54c90711

History | View | Annotate | Download (33.2 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

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

    
72

    
73
log = getLogger(__name__)
74

    
75

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

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

    
103

    
104
# Plankton Image Commands
105

    
106

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

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

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

116
    :raises TypeError, AttributeError: Invalid json format
117

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

    
136

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

141
    :returns: (dict) json_formated
142

143
    :raises TypeError, AttributeError: Invalid json format
144

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

    
155

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

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

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

    
175

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

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

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

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

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

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

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

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

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

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

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

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

    
279

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

    
284

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

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

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

    
307

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

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

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

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

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

    
360

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

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

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

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

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

    
401

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

    
406
    container_info_cache = {}
407

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

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

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

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

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

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

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

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

    
540
        return uuid, container, path
541

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

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

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

    
578
        #register the image
579
        try:
580
            r = self.client.register(name, location, params, properties)
581
        except ClientError as ce:
582
            if ce.status in (400, ):
583
                raiseCLIError(
584
                    ce, 'Nonexistent image file location %s' % location,
585
                    details=[
586
                        'Make sure the image file exists'] + howto_image_file)
587
            raise
588
        r['owner'] += '( %s)' % self._uuid2username(r['owner'])
589
        self._print(r, print_dict)
590

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

    
608
    def main(self, name, container___image_path):
609
        super(self.__class__, self)._run()
610
        self._run(name, *self._mine_location(container___image_path))
611

    
612

    
613
@command(image_cmds)
614
class image_unregister(_init_image, _optional_output_cmd):
615
    """Unregister an image (does not delete the image file)"""
616

    
617
    @errors.generic.all
618
    @errors.plankton.connection
619
    @errors.plankton.id
620
    def _run(self, image_id):
621
        self._optional_output(self.client.unregister(image_id))
622

    
623
    def main(self, image_id):
624
        super(self.__class__, self)._run()
625
        self._run(image_id=image_id)
626

    
627

    
628
@command(image_cmds)
629
class image_shared(_init_image, _optional_json):
630
    """List images shared by a member"""
631

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

    
641
    def main(self, member_id_or_username):
642
        super(self.__class__, self)._run()
643
        self._run(member_id_or_username)
644

    
645

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

    
650

    
651
@command(image_cmds)
652
class image_members_list(_init_image, _optional_json):
653
    """List members of an image"""
654

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

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

    
671

    
672
@command(image_cmds)
673
class image_members_add(_init_image, _optional_output_cmd):
674
    """Add a member to an image"""
675

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

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

    
686

    
687
@command(image_cmds)
688
class image_members_delete(_init_image, _optional_output_cmd):
689
    """Remove a member from an image"""
690

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

    
697
    def main(self, image_id, member):
698
        super(self.__class__, self)._run()
699
        self._run(image_id=image_id, member=member)
700

    
701

    
702
@command(image_cmds)
703
class image_members_set(_init_image, _optional_output_cmd):
704
    """Set the members of an image"""
705

    
706
    @errors.generic.all
707
    @errors.plankton.connection
708
    @errors.plankton.id
709
    def _run(self, image_id, members):
710
            self._optional_output(self.client.set_members(image_id, members))
711

    
712
    def main(self, image_id, *member_ids):
713
        super(self.__class__, self)._run()
714
        self._run(image_id=image_id, members=member_ids)
715

    
716
# Compute Image Commands
717

    
718

    
719
@command(image_cmds)
720
class image_compute(_init_cyclades):
721
    """Cyclades/Compute API image commands"""
722

    
723

    
724
@command(image_cmds)
725
class image_compute_list(
726
        _init_cyclades, _optional_json, _name_filter, _id_filter):
727
    """List images"""
728

    
729
    PERMANENTS = ('id', 'name')
730

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

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

    
760
    def _filter_by_user(self, images):
761
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
762
        return filter_dicts_by_dict(images, dict(user_id=uuid))
763

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

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

    
797
    def main(self):
798
        super(self.__class__, self)._run()
799
        self._run()
800

    
801

    
802
@command(image_cmds)
803
class image_compute_info(_init_cyclades, _optional_json):
804
    """Get detailed information on an image"""
805

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

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

    
821

    
822
@command(image_cmds)
823
class image_compute_delete(_init_cyclades, _optional_output_cmd):
824
    """Delete an image (WARNING: image file is also removed)"""
825

    
826
    @errors.generic.all
827
    @errors.cyclades.connection
828
    @errors.plankton.id
829
    def _run(self, image_id):
830
        self._optional_output(self.client.delete_image(image_id))
831

    
832
    def main(self, image_id):
833
        super(self.__class__, self)._run()
834
        self._run(image_id=image_id)
835

    
836

    
837
@command(image_cmds)
838
class image_compute_properties(_init_cyclades):
839
    """Manage properties related to OS installation in an image"""
840

    
841

    
842
@command(image_cmds)
843
class image_compute_properties_list(_init_cyclades, _optional_json):
844
    """List all image properties"""
845

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

    
852
    def main(self, image_id):
853
        super(self.__class__, self)._run()
854
        self._run(image_id=image_id)
855

    
856

    
857
@command(image_cmds)
858
class image_compute_properties_get(_init_cyclades, _optional_json):
859
    """Get an image property"""
860

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

    
868
    def main(self, image_id, key):
869
        super(self.__class__, self)._run()
870
        self._run(image_id=image_id, key=key)
871

    
872

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

    
889

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

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

    
908
    def main(self, image_id, *key_equals_value):
909
        super(self.__class__, self)._run()
910
        print key_equals_value
911
        self._run(image_id=image_id, keyvals=key_equals_value)
912

    
913

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

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

    
925
    def main(self, image_id, key):
926
        super(self.__class__, self)._run()
927
        self._run(image_id=image_id, key=key)