Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 97086fcd

History | View | Annotate | Download (34.6 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 self['json_output']:
265
            images = self._add_owner_name(images)
266
        elif detail and not self['detail']:
267
            for img in images:
268
                for key in set(img).difference(self.PERMANENTS):
269
                    img.pop(key)
270
        kwargs = dict(with_enumeration=self['enum'])
271
        if self['limit']:
272
            images = images[:self['limit']]
273
        if self['more']:
274
            kwargs['out'] = StringIO()
275
            kwargs['title'] = ()
276
        self._print(images, **kwargs)
277
        if self['more']:
278
            pager(kwargs['out'].getvalue())
279

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

    
284

    
285
@command(image_cmds)
286
class image_meta(_init_image):
287
    """Manage image metadata and custom properties"""
288

    
289

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

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

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

    
312

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

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

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

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

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

    
365

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

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

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

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

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

    
405

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

    
421
    container_info_cache = {}
422

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

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

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

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

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

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

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

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

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

    
579
        return uuid, container, path
580

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

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

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

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

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

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

    
654

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

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

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

    
669

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

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

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

    
687

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

    
692

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

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

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

    
713

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

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

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

    
728

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

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

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

    
743

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

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

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

    
758
# Compute Image Commands
759

    
760

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

    
765

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

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

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

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

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

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

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

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

    
844

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

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

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

    
864

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

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

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

    
879

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

    
884

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

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

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

    
899

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

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

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

    
916

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

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

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

    
940

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

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

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