Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 38db356b

History | View | Annotate | Download (35 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 file to an Image service
406
    The image file must be stored at a pithos repository
407
    Some metadata can be set by user (e.g. disk-format) while others are set
408
    only automatically (e.g. image id). There are also some custom user
409
    metadata, called properties.
410
    A register command creates a remote meta file at
411
       <container>:<image path>.meta
412
    Users may download and edit this file and use it to re-register one or more
413
    images.
414
    In case of a meta file, runtime arguments for metadata or properties
415
    override meta file settings.
416
    """
417

    
418
    container_info_cache = {}
419

    
420
    arguments = dict(
421
        checksum=ValueArgument('Set image checksum', '--checksum'),
422
        container_format=ValueArgument(
423
            'Set container format',
424
            '--container-format'),
425
        disk_format=ValueArgument('Set disk format', '--disk-format'),
426
        owner_name=ValueArgument('Set user uuid by user name', '--owner-name'),
427
        properties=KeyValueArgument(
428
            'Add property (user-specified metadata) in key=value form'
429
            '(can be repeated)',
430
            ('-p', '--property')),
431
        is_public=FlagArgument('Mark image as public', '--public'),
432
        size=IntArgument('Set image size in bytes', '--size'),
433
        metafile=ValueArgument(
434
            'Load metadata from a json-formated file <img-file>.meta :'
435
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
436
            ('--metafile')),
437
        metafile_force=FlagArgument(
438
            'Overide remote metadata file', ('-f', '--force')),
439
        no_metafile_upload=FlagArgument(
440
            'Do not store metadata in remote meta file',
441
            ('--no-metafile-upload')),
442
        container=ValueArgument(
443
            'Pithos+ container containing the image file',
444
            ('-C', '--container')),
445
        uuid=ValueArgument('Custom user uuid', '--uuid'),
446
        local_image_path=ValueArgument(
447
            'Local image file path to upload and register '
448
            '(still need target file in the form container:remote-path )',
449
            '--upload-image-file'),
450
        progress_bar=ProgressBarArgument(
451
            'Do not use progress bar', '--no-progress-bar', default=False)
452
    )
453

    
454
    def _get_user_id(self):
455
        atoken = self.client.token
456
        if getattr(self, 'auth_base', False):
457
            return self.auth_base.term('id', atoken)
458
        else:
459
            astakos_url = self.config.get('user', 'url')\
460
                or self.config.get('astakos', 'url')
461
            if not astakos_url:
462
                raise CLIBaseUrlError(service='astakos')
463
            user = AstakosClient(astakos_url, atoken)
464
            return user.term('id')
465

    
466
    def _get_pithos_client(self, container):
467
        if self['no_metafile_upload']:
468
            return None
469
        ptoken = self.client.token
470
        if getattr(self, 'auth_base', False):
471
            pithos_endpoints = self.auth_base.get_service_endpoints(
472
                'object-store')
473
            purl = pithos_endpoints['publicURL']
474
        else:
475
            purl = self.config.get_cloud('pithos', 'url')
476
        if not purl:
477
            raise CLIBaseUrlError(service='pithos')
478
        return PithosClient(purl, ptoken, self._get_user_id(), container)
479

    
480
    def _store_remote_metafile(self, pclient, remote_path, metadata):
481
        return pclient.upload_from_string(
482
            remote_path, _validate_image_meta(metadata, return_str=True),
483
            container_info_cache=self.container_info_cache)
484

    
485
    def _load_params_from_file(self, location):
486
        params, properties = dict(), dict()
487
        pfile = self['metafile']
488
        if pfile:
489
            try:
490
                for k, v in _load_image_meta(pfile).items():
491
                    key = k.lower().replace('-', '_')
492
                    if k == 'properties':
493
                        for pk, pv in v.items():
494
                            properties[pk.upper().replace('-', '_')] = pv
495
                    elif key == 'name':
496
                            continue
497
                    elif key == 'location':
498
                        if location:
499
                            continue
500
                        location = v
501
                    else:
502
                        params[key] = v
503
            except Exception as e:
504
                raiseCLIError(e, 'Invalid json metadata config file')
505
        return params, properties, location
506

    
507
    def _load_params_from_args(self, params, properties):
508
        for key in set([
509
                'checksum',
510
                'container_format',
511
                'disk_format',
512
                'owner',
513
                'size',
514
                'is_public']).intersection(self.arguments):
515
            params[key] = self[key]
516
        for k, v in self['properties'].items():
517
            properties[k.upper().replace('-', '_')] = v
518

    
519
    def _validate_location(self, location):
520
        if not location:
521
            raiseCLIError(
522
                'No image file location provided',
523
                importance=2, details=[
524
                    'An image location is needed. Image location format:',
525
                    '  <container>:<path>',
526
                    ' where an image file at the above location must exist.'
527
                    ] + howto_image_file)
528
        try:
529
            return _validate_image_location(location)
530
        except AssertionError as ae:
531
            raiseCLIError(
532
                ae, 'Invalid image location format',
533
                importance=1, details=[
534
                    'Valid image location format:',
535
                    '  <container>:<img-file-path>'
536
                    ] + howto_image_file)
537

    
538
    @staticmethod
539
    def _old_location_format(location):
540
        prefix = 'pithos://'
541
        try:
542
            if location.startswith(prefix):
543
                uuid, sep, rest = location[len(prefix):].partition('/')
544
                container, sep, path = rest.partition('/')
545
                return (uuid, container, path)
546
        except Exception as e:
547
            raiseCLIError(e, 'Invalid location format', details=[
548
                'Correct location format:', '  <container>:<image path>'])
549
        return ()
550

    
551
    def _mine_location(self, container_path):
552
        old_response = self._old_location_format(container_path)
553
        if old_response:
554
            return old_response
555
        uuid = self['uuid'] or (self._username2uuid(self['owner_name']) if (
556
                    self['owner_name']) else self._get_user_id())
557
        if not uuid:
558
            if self['owner_name']:
559
                raiseCLIError('No user with username %s' % self['owner_name'])
560
            raiseCLIError('Failed to get user uuid', details=[
561
                'For details on current user:',
562
                '  /user whoami',
563
                'To authenticate a new user through a user token:',
564
                '  /user authenticate <token>'])
565
        if self['container']:
566
            return uuid, self['container'], container_path
567
        container, sep, path = container_path.partition(':')
568
        if not (bool(container) and bool(path)):
569
            raiseCLIError(
570
                'Incorrect container-path format', importance=1, details=[
571
                'Use : to seperate container form path',
572
                '  <container>:<image-path>',
573
                'OR',
574
                'Use -C to specifiy a container',
575
                '  -C <container> <image-path>'] + howto_image_file)
576

    
577
        return uuid, container, path
578

    
579
    @errors.generic.all
580
    @errors.plankton.connection
581
    @errors.pithos.container
582
    def _run(self, name, uuid, dst_cont, img_path):
583
        if self['local_image_path']:
584
            with open(self['local_image_path']) as f:
585
                pithos = self._get_pithos_client(dst_cont)
586
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
587
                if pbar:
588
                    hash_bar = pbar.clone()
589
                    hash_cb = hash_bar.get_generator('Calculating hashes')
590
                pithos.upload_object(
591
                    img_path, f,
592
                    hash_cb=hash_cb, upload_cb=upload_cb,
593
                    container_info_cache=self.container_info_cache)
594
                pbar.finish()
595

    
596
        location = 'pithos://%s/%s/%s' % (uuid, dst_cont, img_path)
597
        (params, properties, new_loc) = self._load_params_from_file(location)
598
        if location != new_loc:
599
            uuid, dst_cont, img_path = self._validate_location(new_loc)
600
        self._load_params_from_args(params, properties)
601
        pclient = self._get_pithos_client(dst_cont)
602

    
603
        #check if metafile exists
604
        meta_path = '%s.meta' % img_path
605
        if pclient and not self['metafile_force']:
606
            try:
607
                pclient.get_object_info(meta_path)
608
                raiseCLIError(
609
                    'Metadata file %s:%s already exists, abort' % (
610
                        dst_cont, meta_path),
611
                    details=['Registration ABORTED', 'Try -f to overwrite'])
612
            except ClientError as ce:
613
                if ce.status != 404:
614
                    raise
615

    
616
        #register the image
617
        try:
618
            r = self.client.register(name, location, params, properties)
619
        except ClientError as ce:
620
            if ce.status in (400, 404):
621
                raiseCLIError(
622
                    ce, 'Nonexistent image file location\n\t%s' % location,
623
                    details=[
624
                        'Does the image file %s exist at container %s ?' % (
625
                            img_path, dst_cont)] + howto_image_file)
626
            raise
627
        r['owner'] += '( %s)' % self._uuid2username(r['owner'])
628
        self._print(r, print_dict)
629

    
630
        #upload the metadata file
631
        if pclient:
632
            try:
633
                meta_headers = pclient.upload_from_string(
634
                    meta_path, dumps(r, indent=2),
635
                    container_info_cache=self.container_info_cache)
636
            except TypeError:
637
                print('Failed to dump metafile %s:%s' % (dst_cont, meta_path))
638
                return
639
            if self['json_output']:
640
                print_json(dict(
641
                    metafile_location='%s:%s' % (dst_cont, meta_path),
642
                    headers=meta_headers))
643
            else:
644
                print('Metadata file uploaded as %s:%s (version %s)' % (
645
                    dst_cont, meta_path, meta_headers['x-object-version']))
646

    
647
    def main(self, name, container___image_path):
648
        super(self.__class__, self)._run()
649
        self._run(name, *self._mine_location(container___image_path))
650

    
651

    
652
@command(image_cmds)
653
class image_unregister(_init_image, _optional_output_cmd):
654
    """Unregister an image (does not delete the image file)"""
655

    
656
    @errors.generic.all
657
    @errors.plankton.connection
658
    @errors.plankton.id
659
    def _run(self, image_id):
660
        self._optional_output(self.client.unregister(image_id))
661

    
662
    def main(self, image_id):
663
        super(self.__class__, self)._run()
664
        self._run(image_id=image_id)
665

    
666

    
667
@command(image_cmds)
668
class image_shared(_init_image, _optional_json):
669
    """List images shared by a member"""
670

    
671
    @errors.generic.all
672
    @errors.plankton.connection
673
    def _run(self, member):
674
        r = self.client.list_shared(member)
675
        if r:
676
            uuid = self._username2uuid(member)
677
            r = self.client.list_shared(uuid) if uuid else []
678
        self._print(r, title=('image_id',))
679

    
680
    def main(self, member_id_or_username):
681
        super(self.__class__, self)._run()
682
        self._run(member_id_or_username)
683

    
684

    
685
@command(image_cmds)
686
class image_members(_init_image):
687
    """Manage members. Members of an image are users who can modify it"""
688

    
689

    
690
@command(image_cmds)
691
class image_members_list(_init_image, _optional_json):
692
    """List members of an image"""
693

    
694
    @errors.generic.all
695
    @errors.plankton.connection
696
    @errors.plankton.id
697
    def _run(self, image_id):
698
        members = self.client.list_members(image_id)
699
        if not self['json_output']:
700
            uuids = [member['member_id'] for member in members]
701
            usernames = self._uuids2usernames(uuids)
702
            for member in members:
703
                member['member_id'] += ' (%s)' % usernames[member['member_id']]
704
        self._print(members, title=('member_id',))
705

    
706
    def main(self, image_id):
707
        super(self.__class__, self)._run()
708
        self._run(image_id=image_id)
709

    
710

    
711
@command(image_cmds)
712
class image_members_add(_init_image, _optional_output_cmd):
713
    """Add a member to an image"""
714

    
715
    @errors.generic.all
716
    @errors.plankton.connection
717
    @errors.plankton.id
718
    def _run(self, image_id=None, member=None):
719
            self._optional_output(self.client.add_member(image_id, member))
720

    
721
    def main(self, image_id, member_id):
722
        super(self.__class__, self)._run()
723
        self._run(image_id=image_id, member=member_id)
724

    
725

    
726
@command(image_cmds)
727
class image_members_delete(_init_image, _optional_output_cmd):
728
    """Remove a member from an image"""
729

    
730
    @errors.generic.all
731
    @errors.plankton.connection
732
    @errors.plankton.id
733
    def _run(self, image_id=None, member=None):
734
            self._optional_output(self.client.remove_member(image_id, member))
735

    
736
    def main(self, image_id, member):
737
        super(self.__class__, self)._run()
738
        self._run(image_id=image_id, member=member)
739

    
740

    
741
@command(image_cmds)
742
class image_members_set(_init_image, _optional_output_cmd):
743
    """Set the members of an image"""
744

    
745
    @errors.generic.all
746
    @errors.plankton.connection
747
    @errors.plankton.id
748
    def _run(self, image_id, members):
749
            self._optional_output(self.client.set_members(image_id, members))
750

    
751
    def main(self, image_id, *member_ids):
752
        super(self.__class__, self)._run()
753
        self._run(image_id=image_id, members=member_ids)
754

    
755
# Compute Image Commands
756

    
757

    
758
@command(image_cmds)
759
class image_compute(_init_cyclades):
760
    """Cyclades/Compute API image commands"""
761

    
762

    
763
@command(image_cmds)
764
class image_compute_list(
765
        _init_cyclades, _optional_json, _name_filter, _id_filter):
766
    """List images"""
767

    
768
    PERMANENTS = ('id', 'name')
769

    
770
    arguments = dict(
771
        detail=FlagArgument('show detailed output', ('-l', '--details')),
772
        limit=IntArgument('limit number listed images', ('-n', '--number')),
773
        more=FlagArgument(
774
            'output results in pages (-n to set items per page, default 10)',
775
            '--more'),
776
        enum=FlagArgument('Enumerate results', '--enumerate'),
777
        user_id=ValueArgument('filter by user_id', '--user-id'),
778
        user_name=ValueArgument('filter by username', '--user-name'),
779
        meta=KeyValueArgument(
780
            'filter by metadata key=value (can be repeated)', ('--metadata')),
781
        meta_like=KeyValueArgument(
782
            'filter by metadata key=value (can be repeated)',
783
            ('--metadata-like'))
784
    )
785

    
786
    def _filter_by_metadata(self, images):
787
        new_images = []
788
        for img in images:
789
            meta = [dict(img['metadata'])]
790
            if self['meta']:
791
                meta = filter_dicts_by_dict(meta, self['meta'])
792
            if meta and self['meta_like']:
793
                meta = filter_dicts_by_dict(
794
                    meta, self['meta_like'], exact_match=False)
795
            if meta:
796
                new_images.append(img)
797
        return new_images
798

    
799
    def _filter_by_user(self, images):
800
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
801
        return filter_dicts_by_dict(images, dict(user_id=uuid))
802

    
803
    def _add_name(self, images, key='user_id'):
804
        uuids = self._uuids2usernames(
805
            list(set([img[key] for img in images])))
806
        for img in images:
807
            img[key] += ' (%s)' % uuids[img[key]]
808
        return images
809

    
810
    @errors.generic.all
811
    @errors.cyclades.connection
812
    def _run(self):
813
        withmeta = bool(self['meta'] or self['meta_like'])
814
        withuser = bool(self['user_id'] or self['user_name'])
815
        detail = self['detail'] or withmeta or withuser
816
        images = self.client.list_images(detail)
817
        images = self._filter_by_name(images)
818
        images = self._filter_by_id(images)
819
        if withuser:
820
            images = self._filter_by_user(images)
821
        if withmeta:
822
            images = self._filter_by_metadata(images)
823
        if self['detail'] and not self['json_output']:
824
            images = self._add_name(self._add_name(images, 'tenant_id'))
825
        elif detail and not self['detail']:
826
            for img in images:
827
                for key in set(img).difference(self.PERMANENTS):
828
                    img.pop(key)
829
        kwargs = dict(with_enumeration=self['enum'])
830
        if self['more']:
831
            kwargs['page_size'] = self['limit'] or 10
832
        elif self['limit']:
833
            images = images[:self['limit']]
834
        self._print(images, **kwargs)
835

    
836
    def main(self):
837
        super(self.__class__, self)._run()
838
        self._run()
839

    
840

    
841
@command(image_cmds)
842
class image_compute_info(_init_cyclades, _optional_json):
843
    """Get detailed information on an image"""
844

    
845
    @errors.generic.all
846
    @errors.cyclades.connection
847
    @errors.plankton.id
848
    def _run(self, image_id):
849
        image = self.client.get_image_details(image_id)
850
        uuids = [image['user_id'], image['tenant_id']]
851
        usernames = self._uuids2usernames(uuids)
852
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
853
        image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
854
        self._print(image, print_dict)
855

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

    
860

    
861
@command(image_cmds)
862
class image_compute_delete(_init_cyclades, _optional_output_cmd):
863
    """Delete an image (WARNING: image file is also removed)"""
864

    
865
    @errors.generic.all
866
    @errors.cyclades.connection
867
    @errors.plankton.id
868
    def _run(self, image_id):
869
        self._optional_output(self.client.delete_image(image_id))
870

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

    
875

    
876
@command(image_cmds)
877
class image_compute_properties(_init_cyclades):
878
    """Manage properties related to OS installation in an image"""
879

    
880

    
881
@command(image_cmds)
882
class image_compute_properties_list(_init_cyclades, _optional_json):
883
    """List all image properties"""
884

    
885
    @errors.generic.all
886
    @errors.cyclades.connection
887
    @errors.plankton.id
888
    def _run(self, image_id):
889
        self._print(self.client.get_image_metadata(image_id), print_dict)
890

    
891
    def main(self, image_id):
892
        super(self.__class__, self)._run()
893
        self._run(image_id=image_id)
894

    
895

    
896
@command(image_cmds)
897
class image_compute_properties_get(_init_cyclades, _optional_json):
898
    """Get an image property"""
899

    
900
    @errors.generic.all
901
    @errors.cyclades.connection
902
    @errors.plankton.id
903
    @errors.plankton.metadata
904
    def _run(self, image_id, key):
905
        self._print(self.client.get_image_metadata(image_id, key), print_dict)
906

    
907
    def main(self, image_id, key):
908
        super(self.__class__, self)._run()
909
        self._run(image_id=image_id, key=key)
910

    
911

    
912
#@command(image_cmds)
913
#class image_compute_properties_add(_init_cyclades, _optional_json):
914
#    """Add a property to an image"""
915
#
916
#    @errors.generic.all
917
#    @errors.cyclades.connection
918
#    @errors.plankton.id
919
#    @errors.plankton.metadata
920
#    def _run(self, image_id, key, val):
921
#        self._print(
922
#            self.client.create_image_metadata(image_id, key, val), print_dict)
923
#
924
#    def main(self, image_id, key, val):
925
#        super(self.__class__, self)._run()
926
#        self._run(image_id=image_id, key=key, val=val)
927

    
928

    
929
@command(image_cmds)
930
class image_compute_properties_set(_init_cyclades, _optional_json):
931
    """Add / update a set of properties for an image
932
    properties must be given in the form key=value, e.v.
933
    /image compute properties set <image-id> key1=val1 key2=val2
934
    """
935

    
936
    @errors.generic.all
937
    @errors.cyclades.connection
938
    @errors.plankton.id
939
    def _run(self, image_id, keyvals):
940
        meta = dict()
941
        for keyval in keyvals:
942
            key, sep, val = keyval.partition('=')
943
            meta[key] = val
944
        self._print(
945
            self.client.update_image_metadata(image_id, **meta), print_dict)
946

    
947
    def main(self, image_id, *key_equals_value):
948
        super(self.__class__, self)._run()
949
        self._run(image_id=image_id, keyvals=key_equals_value)
950

    
951

    
952
@command(image_cmds)
953
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
954
    """Delete a property from an image"""
955

    
956
    @errors.generic.all
957
    @errors.cyclades.connection
958
    @errors.plankton.id
959
    @errors.plankton.metadata
960
    def _run(self, image_id, key):
961
        self._optional_output(self.client.delete_image_metadata(image_id, key))
962

    
963
    def main(self, image_id, key):
964
        super(self.__class__, self)._run()
965
        self._run(image_id=image_id, key=key)