Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 6778681e

History | View | Annotate | Download (34.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_meta(_init_image):
288
    """Manage image metadata and custom properties"""
289

    
290

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

    
300
    @errors.generic.all
301
    @errors.plankton.connection
302
    @errors.plankton.id
303
    def _run(self, image_id):
304
        meta = self.client.get_meta(image_id)
305
        if not (self['json_output'] or self['output_format']):
306
            meta['owner'] += ' (%s)' % self._uuid2username(meta['owner'])
307
        self._print(meta, self.print_dict)
308

    
309
    def main(self, image_id):
310
        super(self.__class__, self)._run()
311
        self._run(image_id=image_id)
312

    
313

    
314
@command(image_cmds)
315
class image_meta_set(_init_image, _optional_output_cmd):
316
    """Add / update metadata and properties for an image
317
    The original image preserves the values that are not affected
318
    """
319

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

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

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

    
362
    def main(self, image_id):
363
        super(self.__class__, self)._run()
364
        self._run(image_id=image_id)
365

    
366

    
367
@command(image_cmds)
368
class image_meta_delete(_init_image, _optional_output_cmd):
369
    """Remove/empty image metadata and/or custom properties"""
370

    
371
    arguments = dict(
372
        disk_format=FlagArgument('Empty disk format', ('--disk-format')),
373
        container_format=FlagArgument(
374
            'Empty container format', ('--container-format')),
375
        status=FlagArgument('Empty status', ('--status')),
376
        properties=RepeatableArgument(
377
            'Property keys to remove', ('-p', '--property'))
378
    )
379

    
380
    def _check_empty(self):
381
        for t in ('disk_format', 'container_format', 'status', 'properties'):
382
            if self[t]:
383
                return
384
        raiseCLIError(
385
            'Nothing to update, please use arguments (-h for a list)')
386

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

    
402
    def main(self, image_id):
403
        super(self.__class__, self)._run()
404
        self._run(image_id=image_id)
405

    
406

    
407
@command(image_cmds)
408
class image_register(_init_image, _optional_json):
409
    """(Re)Register an image file to an Image service
410
    The image file must be stored at a pithos repository
411
    Some metadata can be set by user (e.g., disk-format) while others are set
412
    only automatically (e.g., image id). There are also some custom user
413
    metadata, called properties.
414
    A register command creates a remote meta file at
415
    .  <container>:<image path>.meta
416
    Users may download and edit this file and use it to re-register one or more
417
    images.
418
    In case of a meta file, runtime arguments for metadata or properties
419
    override meta file settings.
420
    """
421

    
422
    container_info_cache = {}
423

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

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

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

    
483
    def _store_remote_metafile(self, pclient, remote_path, metadata):
484
        return pclient.upload_from_string(
485
            remote_path, _validate_image_meta(metadata, return_str=True),
486
            container_info_cache=self.container_info_cache)
487

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

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

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

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

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

    
580
        return uuid, container, path
581

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

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

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

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

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

    
651
    def main(self, name, container___image_path):
652
        super(self.__class__, self)._run()
653
        self._run(name, *self._mine_location(container___image_path))
654

    
655

    
656
@command(image_cmds)
657
class image_unregister(_init_image, _optional_output_cmd):
658
    """Unregister an image (does not delete the image file)"""
659

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

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

    
670

    
671
@command(image_cmds)
672
class image_shared(_init_image, _optional_json):
673
    """List images shared by a member"""
674

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

    
684
    def main(self, member_id_or_username):
685
        super(self.__class__, self)._run()
686
        self._run(member_id_or_username)
687

    
688

    
689
@command(image_cmds)
690
class image_members(_init_image):
691
    """Manage members. Members of an image are users who can modify it"""
692

    
693

    
694
@command(image_cmds)
695
class image_members_list(_init_image, _optional_json):
696
    """List members of an image"""
697

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

    
710
    def main(self, image_id):
711
        super(self.__class__, self)._run()
712
        self._run(image_id=image_id)
713

    
714

    
715
@command(image_cmds)
716
class image_members_add(_init_image, _optional_output_cmd):
717
    """Add a member to an image"""
718

    
719
    @errors.generic.all
720
    @errors.plankton.connection
721
    @errors.plankton.id
722
    def _run(self, image_id=None, member=None):
723
            self._optional_output(self.client.add_member(image_id, member))
724

    
725
    def main(self, image_id, member_id):
726
        super(self.__class__, self)._run()
727
        self._run(image_id=image_id, member=member_id)
728

    
729

    
730
@command(image_cmds)
731
class image_members_delete(_init_image, _optional_output_cmd):
732
    """Remove a member from an image"""
733

    
734
    @errors.generic.all
735
    @errors.plankton.connection
736
    @errors.plankton.id
737
    def _run(self, image_id=None, member=None):
738
            self._optional_output(self.client.remove_member(image_id, member))
739

    
740
    def main(self, image_id, member):
741
        super(self.__class__, self)._run()
742
        self._run(image_id=image_id, member=member)
743

    
744

    
745
@command(image_cmds)
746
class image_members_set(_init_image, _optional_output_cmd):
747
    """Set the members of an image"""
748

    
749
    @errors.generic.all
750
    @errors.plankton.connection
751
    @errors.plankton.id
752
    def _run(self, image_id, members):
753
            self._optional_output(self.client.set_members(image_id, members))
754

    
755
    def main(self, image_id, *member_ids):
756
        super(self.__class__, self)._run()
757
        self._run(image_id=image_id, members=member_ids)
758

    
759
# Compute Image Commands
760

    
761

    
762
@command(image_cmds)
763
class image_compute(_init_cyclades):
764
    """Cyclades/Compute API image commands"""
765

    
766

    
767
@command(image_cmds)
768
class image_compute_list(
769
        _init_cyclades, _optional_json, _name_filter, _id_filter):
770
    """List images"""
771

    
772
    PERMANENTS = ('id', 'name')
773

    
774
    arguments = dict(
775
        detail=FlagArgument('show detailed output', ('-l', '--details')),
776
        limit=IntArgument('limit number listed images', ('-n', '--number')),
777
        more=FlagArgument('handle long lists of results', '--more'),
778
        enum=FlagArgument('Enumerate results', '--enumerate'),
779
        user_id=ValueArgument('filter by user_id', '--user-id'),
780
        user_name=ValueArgument('filter by username', '--user-name'),
781
        meta=KeyValueArgument(
782
            'filter by metadata key=value (can be repeated)', ('--metadata')),
783
        meta_like=KeyValueArgument(
784
            'filter by metadata key=value (can be repeated)',
785
            ('--metadata-like'))
786
    )
787

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

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

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

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

    
842
    def main(self):
843
        super(self.__class__, self)._run()
844
        self._run()
845

    
846

    
847
@command(image_cmds)
848
class image_compute_info(_init_cyclades, _optional_json):
849
    """Get detailed information on an image"""
850

    
851
    @errors.generic.all
852
    @errors.cyclades.connection
853
    @errors.plankton.id
854
    def _run(self, image_id):
855
        image = self.client.get_image_details(image_id)
856
        uuids = [image['user_id'], image['tenant_id']]
857
        usernames = self._uuids2usernames(uuids)
858
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
859
        image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
860
        self._print(image, self.print_dict)
861

    
862
    def main(self, image_id):
863
        super(self.__class__, self)._run()
864
        self._run(image_id=image_id)
865

    
866

    
867
@command(image_cmds)
868
class image_compute_delete(_init_cyclades, _optional_output_cmd):
869
    """Delete an image (WARNING: image file is also removed)"""
870

    
871
    @errors.generic.all
872
    @errors.cyclades.connection
873
    @errors.plankton.id
874
    def _run(self, image_id):
875
        self._optional_output(self.client.delete_image(image_id))
876

    
877
    def main(self, image_id):
878
        super(self.__class__, self)._run()
879
        self._run(image_id=image_id)
880

    
881

    
882
@command(image_cmds)
883
class image_compute_properties(_init_cyclades):
884
    """Manage properties related to OS installation in an image"""
885

    
886

    
887
@command(image_cmds)
888
class image_compute_properties_list(_init_cyclades, _optional_json):
889
    """List all image properties"""
890

    
891
    @errors.generic.all
892
    @errors.cyclades.connection
893
    @errors.plankton.id
894
    def _run(self, image_id):
895
        self._print(self.client.get_image_metadata(image_id), self.print_dict)
896

    
897
    def main(self, image_id):
898
        super(self.__class__, self)._run()
899
        self._run(image_id=image_id)
900

    
901

    
902
@command(image_cmds)
903
class image_compute_properties_get(_init_cyclades, _optional_json):
904
    """Get an image property"""
905

    
906
    @errors.generic.all
907
    @errors.cyclades.connection
908
    @errors.plankton.id
909
    @errors.plankton.metadata
910
    def _run(self, image_id, key):
911
        self._print(
912
            self.client.get_image_metadata(image_id, key), self.print_dict)
913

    
914
    def main(self, image_id, key):
915
        super(self.__class__, self)._run()
916
        self._run(image_id=image_id, key=key)
917

    
918

    
919
@command(image_cmds)
920
class image_compute_properties_set(_init_cyclades, _optional_json):
921
    """Add / update a set of properties for an image
922
    properties must be given in the form key=value, e.v.
923
    /image compute properties set <image-id> key1=val1 key2=val2
924
    """
925

    
926
    @errors.generic.all
927
    @errors.cyclades.connection
928
    @errors.plankton.id
929
    def _run(self, image_id, keyvals):
930
        meta = dict()
931
        for keyval in keyvals:
932
            key, sep, val = keyval.partition('=')
933
            meta[key] = val
934
        self._print(
935
            self.client.update_image_metadata(image_id, **meta),
936
            self.print_dict)
937

    
938
    def main(self, image_id, *key_equals_value):
939
        super(self.__class__, self)._run()
940
        self._run(image_id=image_id, keyvals=key_equals_value)
941

    
942

    
943
@command(image_cmds)
944
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
945
    """Delete a property from an image"""
946

    
947
    @errors.generic.all
948
    @errors.cyclades.connection
949
    @errors.plankton.id
950
    @errors.plankton.metadata
951
    def _run(self, image_id, key):
952
        self._optional_output(self.client.delete_image_metadata(image_id, key))
953

    
954
    def main(self, image_id, key):
955
        super(self.__class__, self)._run()
956
        self._run(image_id=image_id, key=key)