Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 2c3671ff

History | View | Annotate | Download (34.7 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
        self._print(r, title=('image_id',))
680

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

    
685

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

    
690

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

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

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

    
711

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

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

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

    
726

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

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

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

    
741

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

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

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

    
756
# Compute Image Commands
757

    
758

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

    
763

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

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

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

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

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

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

    
809
    @errors.generic.all
810
    @errors.cyclades.connection
811
    def _run(self):
812
        withmeta = bool(self['meta'] or self['meta_like'])
813
        withuser = bool(self['user_id'] or self['user_name'])
814
        detail = self['detail'] or withmeta or withuser
815
        images = self.client.list_images(detail)
816
        images = self._filter_by_name(images)
817
        images = self._filter_by_id(images)
818
        if withuser:
819
            images = self._filter_by_user(images)
820
        if withmeta:
821
            images = self._filter_by_metadata(images)
822
        if self['detail'] and not (
823
                self['json_output'] or self['output_format']):
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['limit']:
831
            images = images[:self['limit']]
832
        if self['more']:
833
            kwargs['out'] = StringIO()
834
            kwargs['title'] = ()
835
        self._print(images, **kwargs)
836
        if self['more']:
837
            pager(kwargs['out'].getvalue())
838

    
839
    def main(self):
840
        super(self.__class__, self)._run()
841
        self._run()
842

    
843

    
844
@command(image_cmds)
845
class image_compute_info(_init_cyclades, _optional_json):
846
    """Get detailed information on an image"""
847

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

    
859
    def main(self, image_id):
860
        super(self.__class__, self)._run()
861
        self._run(image_id=image_id)
862

    
863

    
864
@command(image_cmds)
865
class image_compute_delete(_init_cyclades, _optional_output_cmd):
866
    """Delete an image (WARNING: image file is also removed)"""
867

    
868
    @errors.generic.all
869
    @errors.cyclades.connection
870
    @errors.plankton.id
871
    def _run(self, image_id):
872
        self._optional_output(self.client.delete_image(image_id))
873

    
874
    def main(self, image_id):
875
        super(self.__class__, self)._run()
876
        self._run(image_id=image_id)
877

    
878

    
879
@command(image_cmds)
880
class image_compute_properties(_init_cyclades):
881
    """Manage properties related to OS installation in an image"""
882

    
883

    
884
@command(image_cmds)
885
class image_compute_properties_list(_init_cyclades, _optional_json):
886
    """List all image properties"""
887

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

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

    
898

    
899
@command(image_cmds)
900
class image_compute_properties_get(_init_cyclades, _optional_json):
901
    """Get an image property"""
902

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

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

    
915

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

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

    
935
    def main(self, image_id, *key_equals_value):
936
        super(self.__class__, self)._run()
937
        self._run(image_id=image_id, keyvals=key_equals_value)
938

    
939

    
940
@command(image_cmds)
941
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
942
    """Delete a property from an image"""
943

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

    
951
    def main(self, image_id, key):
952
        super(self.__class__, self)._run()
953
        self._run(image_id=image_id, key=key)