Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (29.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 import ClientError
46
from kamaki.cli.argument import (
47
    FlagArgument, ValueArgument, RepeatableArgument, KeyValueArgument,
48
    IntArgument, ProgressBarArgument)
49
from kamaki.cli.commands.cyclades import _init_cyclades
50
from kamaki.cli.errors import (
51
    raiseCLIError, CLIBaseUrlError, CLIInvalidArgument)
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('image', 'Cyclades/Plankton API image commands')
58
imagecompute_cmds = CommandTree(
59
    'imagecompute', 'Cyclades/Compute API image commands')
60
_commands = [image_cmds, imagecompute_cmds]
61

    
62

    
63
howto_image_file = [
64
    'Kamaki commands to:',
65
    ' get current user id: kamaki user info',
66
    ' check available containers: kamaki container list',
67
    ' create a new container: kamaki container create CONTAINER',
68
    ' check container contents: kamaki file list /CONTAINER',
69
    ' upload files: kamaki file upload IMAGE_FILE /CONTAINER[/PATH]',
70
    ' register an image:',
71
    '   kamaki image register --name=IMAGE_NAME --location=/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
        image_ID_for_members=ValueArgument(
211
            'List members of an image', '--members-of')
212
    )
213

    
214
    def _filter_by_owner(self, images):
215
        ouuid = self['owner'] or self._username2uuid(self['owner_name'])
216
        return filter_dicts_by_dict(images, dict(owner=ouuid))
217

    
218
    def _add_owner_name(self, images):
219
        uuids = self._uuids2usernames(
220
            list(set([img['owner'] for img in images])))
221
        for img in images:
222
            img['owner'] += ' (%s)' % uuids[img['owner']]
223
        return images
224

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

    
238
    def _members(self, image_id):
239
        members = self.client.list_members(image_id)
240
        if not (self['json_output'] or self['output_format']):
241
            uuids = [member['member_id'] for member in members]
242
            usernames = self._uuids2usernames(uuids)
243
            for member in members:
244
                member['member_id'] += ' (%s)' % usernames[member['member_id']]
245
        self._print(members, title=('member_id',))
246

    
247
    @errors.generic.all
248
    @errors.cyclades.connection
249
    def _run(self):
250
        super(self.__class__, self)._run()
251
        if self['image_ID_for_members']:
252
            return self._members(self['image_ID_for_members'])
253
        filters = {}
254
        for arg in set([
255
                'container_format',
256
                'disk_format',
257
                'name',
258
                'size_min',
259
                'size_max',
260
                'status']).intersection(self.arguments):
261
            filters[arg] = self[arg]
262

    
263
        order = self['order']
264
        detail = self['detail'] or (
265
            self['prop'] or self['prop_like']) or (
266
            self['owner'] or self['owner_name'])
267

    
268
        images = self.client.list_public(detail, filters, order)
269

    
270
        if self['owner'] or self['owner_name']:
271
            images = self._filter_by_owner(images)
272
        if self['prop'] or self['prop_like']:
273
            images = self._filter_by_properties(images)
274
        images = self._filter_by_id(images)
275
        images = self._non_exact_name_filter(images)
276

    
277
        if self['detail'] and not (
278
                self['json_output'] or self['output_format']):
279
            images = self._add_owner_name(images)
280
        elif detail and not self['detail']:
281
            for img in images:
282
                for key in set(img).difference(self.PERMANENTS):
283
                    img.pop(key)
284
        kwargs = dict(with_enumeration=self['enum'])
285
        if self['limit']:
286
            images = images[:self['limit']]
287
        if self['more']:
288
            kwargs['out'] = StringIO()
289
            kwargs['title'] = ()
290
        self._print(images, **kwargs)
291
        if self['more']:
292
            pager(kwargs['out'].getvalue())
293

    
294
    def main(self):
295
        super(self.__class__, self)._run()
296
        self._run()
297

    
298

    
299
@command(image_cmds)
300
class image_info(_init_image, _optional_json):
301
    """Get image metadata"""
302

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

    
312
    def main(self, image_id):
313
        super(self.__class__, self)._run()
314
        self._run(image_id=image_id)
315

    
316

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

    
323
    arguments = dict(
324
        image_name=ValueArgument('Change name', '--name'),
325
        disk_format=ValueArgument('Change disk format', '--disk-format'),
326
        container_format=ValueArgument(
327
            'Change container format', '--container-format'),
328
        status=ValueArgument('Change status', '--status'),
329
        publish=FlagArgument('Publish the image', '--publish'),
330
        unpublish=FlagArgument('Unpublish the image', '--unpublish'),
331
        property_to_set=KeyValueArgument(
332
            'set property in key=value form (can be repeated)',
333
            ('-p', '--property-set')),
334
        property_to_del=RepeatableArgument(
335
            'Delete property by key (can be repeated)', '--property-del'),
336
        member_ID_to_add=RepeatableArgument(
337
            'Add member to image (can be repeated)', '--member-add'),
338
        member_ID_to_remove=RepeatableArgument(
339
            'Remove a member (can be repeated)', '--member-del'),
340
    )
341
    required = [
342
        'image_name', 'disk_format', 'container_format', 'status', 'publish',
343
        'unpublish', 'property_to_set', 'member_ID_to_add',
344
        'member_ID_to_remove', 'property_to_del']
345

    
346
    @errors.generic.all
347
    @errors.plankton.connection
348
    @errors.plankton.id
349
    def _run(self, image_id):
350
        for mid in (self['member_ID_to_add'] or []):
351
            self.client.add_member(image_id, mid)
352
        for mid in (self['member_ID_to_remove'] or []):
353
            self.client.remove_member(image_id, mid)
354
        meta = self.client.get_meta(image_id)
355
        for k, v in self['property_to_set'].items():
356
            meta['properties'][k.upper()] = v
357
        for k in (self['property_to_del'] or []):
358
            meta['properties'][k.upper()] = None
359
        self._optional_output(self.client.update_image(
360
            image_id,
361
            name=self['image_name'],
362
            disk_format=self['disk_format'],
363
            container_format=self['container_format'],
364
            status=self['status'],
365
            public=self['publish'] or (False if self['unpublish'] else None),
366
            **meta['properties']))
367
        if self['with_output']:
368
            self._optional_output(self.get_image_details(image_id))
369

    
370
    def main(self, image_id):
371
        super(self.__class__, self)._run()
372
        self._run(image_id=image_id)
373

    
374

    
375
class PithosLocationArgument(ValueArgument):
376
    """Resolve pithos url, return in the form pithos://uuid/container/path"""
377

    
378
    def __init__(
379
            self, help=None, parsed_name=None, default=None, user_uuid=None):
380
        super(PithosLocationArgument, self).__init__(
381
            help=help, parsed_name=parsed_name, default=default)
382
        self.uuid, self.container, self.path = user_uuid, None, None
383

    
384
    def setdefault(self, term, value):
385
        if not getattr(self, term, None):
386
            setattr(self, term, value)
387

    
388
    @property
389
    def value(self):
390
        return 'pithos://%s/%s/%s' % (self.uuid, self.container, self.path)
391

    
392
    @value.setter
393
    def value(self, location):
394
        if location:
395
            from kamaki.cli.commands.pithos import _pithos_container as pc
396
            try:
397
                uuid, self.container, self.path = pc._resolve_pithos_url(
398
                    location)
399
                self.uuid = uuid or self.uuid
400
                for term in ('container', 'path'):
401
                    assert getattr(self, term, None), 'No %s' % term
402
            except Exception as e:
403
                raise CLIInvalidArgument(
404
                    'Invalid Pithos+ location %s (%s)' % (location, e),
405
                    details=[
406
                        'The image location must be a valid Pithos+',
407
                        'location. There are two valid formats:',
408
                        '  pithos://USER_UUID/CONTAINER/PATH',
409
                        'OR',
410
                        '  /CONTAINER/PATH',
411
                        'To see all containers:',
412
                        '  [kamaki] container list',
413
                        'To list the contents of a container:',
414
                        '  [kamaki] container list CONTAINER'])
415

    
416

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

    
432
    container_info_cache = {}
433

    
434
    arguments = dict(
435
        checksum=ValueArgument('Set image checksum', '--checksum'),
436
        container_format=ValueArgument(
437
            'Set container format', '--container-format'),
438
        disk_format=ValueArgument('Set disk format', '--disk-format'),
439
        owner_name=ValueArgument('Set user uuid by user name', '--owner-name'),
440
        properties=KeyValueArgument(
441
            'Add property (user-specified metadata) in key=value form'
442
            '(can be repeated)',
443
            ('-p', '--property')),
444
        is_public=FlagArgument('Mark image as public', '--public'),
445
        size=IntArgument('Set image size in bytes', '--size'),
446
        metafile=ValueArgument(
447
            'Load metadata from a json-formated file <img-file>.meta :'
448
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
449
            ('--metafile')),
450
        force_upload=FlagArgument(
451
            'Overwrite remote files (image file, metadata file)',
452
            ('-f', '--force')),
453
        no_metafile_upload=FlagArgument(
454
            'Do not store metadata in remote meta file',
455
            ('--no-metafile-upload')),
456
        container=ValueArgument(
457
            'Pithos+ container containing the image file',
458
            ('-C', '--container')),
459
        uuid=ValueArgument('Custom user uuid', '--uuid'),
460
        local_image_path=ValueArgument(
461
            'Local image file path to upload and register '
462
            '(still need target file in the form /container/remote-path )',
463
            '--upload-image-file'),
464
        progress_bar=ProgressBarArgument(
465
            'Do not use progress bar', '--no-progress-bar', default=False),
466
        name=ValueArgument('The name of the new image', '--name'),
467
        pithos_location=PithosLocationArgument(
468
            'The Pithos+ image location to put the image at. Format:       '
469
            'pithos://USER_UUID/CONTAINER/IMAGE                  or   '
470
            '/CONTAINER/IMAGE',
471
            '--location')
472
    )
473
    required = ('name', 'pithos_location')
474

    
475
    def _get_pithos_client(self, locator):
476
        ptoken = self.client.token
477
        if getattr(self, 'auth_base', False):
478
            pithos_endpoints = self.auth_base.get_service_endpoints(
479
                'object-store')
480
            purl = pithos_endpoints['publicURL']
481
        else:
482
            purl = self.config.get_cloud('pithos', 'url')
483
        if not purl:
484
            raise CLIBaseUrlError(service='pithos')
485
        return PithosClient(purl, ptoken, locator.uuid, locator.container)
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 _assert_remote_file_not_exist(self, pithos, path):
522
        if pithos and not self['force_upload']:
523
            try:
524
                pithos.get_object_info(path)
525
                raiseCLIError(
526
                    'Remote file /%s/%s already exists' % (
527
                        pithos.container, path),
528
                    importance=2,
529
                    details=[
530
                        'Registration ABORTED',
531
                        'Use %s to force upload' % self.arguments[
532
                            'force_upload'].lvalue])
533
            except ClientError as ce:
534
                if ce.status != 404:
535
                    raise
536

    
537
    @errors.generic.all
538
    @errors.plankton.connection
539
    def _run(self, name, location):
540
        locator, pithos = self.arguments['pithos_location'], None
541
        if self['local_image_path']:
542
            with open(self['local_image_path']) as f:
543
                pithos = self._get_pithos_client(locator)
544
                self._assert_remote_file_not_exist(pithos, locator.path)
545
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
546
                if pbar:
547
                    hash_bar = pbar.clone()
548
                    hash_cb = hash_bar.get_generator('Calculating hashes')
549
                pithos.upload_object(
550
                    locator.path, f,
551
                    hash_cb=hash_cb, upload_cb=upload_cb,
552
                    container_info_cache=self.container_info_cache)
553
                pbar.finish()
554

    
555
        (params, properties, new_loc) = self._load_params_from_file(location)
556
        if location != new_loc:
557
            locator.value = new_loc
558
        self._load_params_from_args(params, properties)
559

    
560
        if not self['no_metafile_upload']:
561
            #check if metafile exists
562
            pithos = pithos or self._get_pithos_client(locator)
563
            meta_path = '%s.meta' % locator.path
564
            self._assert_remote_file_not_exist(pithos, meta_path)
565

    
566
        #register the image
567
        try:
568
            r = self.client.register(name, location, params, properties)
569
        except ClientError as ce:
570
            if ce.status in (400, 404):
571
                raiseCLIError(
572
                    ce, 'Nonexistent image file location\n\t%s' % location,
573
                    details=[
574
                        'Does the image file %s exist at container %s ?' % (
575
                            locator.path,
576
                            locator.container)] + howto_image_file)
577
            raise
578
        r['owner'] += ' (%s)' % self._uuid2username(r['owner'])
579
        self._print(r, self.print_dict)
580

    
581
        #upload the metadata file
582
        if not self['no_metafile_upload']:
583
            try:
584
                meta_headers = pithos.upload_from_string(
585
                    meta_path, dumps(r, indent=2),
586
                    sharing=dict(read='*' if params.get('is_public') else ''),
587
                    container_info_cache=self.container_info_cache)
588
            except TypeError:
589
                self.error(
590
                    'Failed to dump metafile /%s/%s' % (
591
                        locator.container, meta_path))
592
                return
593
            if self['json_output'] or self['output_format']:
594
                self.print_json(dict(
595
                    metafile_location='/%s/%s' % (
596
                        locator.container, meta_path),
597
                    headers=meta_headers))
598
            else:
599
                self.error('Metadata file uploaded as /%s/%s (version %s)' % (
600
                    locator.container,
601
                    meta_path,
602
                    meta_headers['x-object-version']))
603

    
604
    def main(self):
605
        super(self.__class__, self)._run()
606
        self.arguments['pithos_location'].setdefault(
607
            'uuid', self.auth_base.user_term('id'))
608
        self._run(self['name'], self['pithos_location'])
609

    
610

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

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

    
621
    def main(self, image_id):
622
        super(self.__class__, self)._run()
623
        self._run(image_id=image_id)
624

    
625

    
626
# Compute Image Commands
627

    
628
@command(imagecompute_cmds)
629
class imagecompute_list(
630
        _init_cyclades, _optional_json, _name_filter, _id_filter):
631
    """List images"""
632

    
633
    PERMANENTS = ('id', 'name')
634

    
635
    arguments = dict(
636
        detail=FlagArgument('show detailed output', ('-l', '--details')),
637
        limit=IntArgument('limit number listed images', ('-n', '--number')),
638
        more=FlagArgument('handle long lists of results', '--more'),
639
        enum=FlagArgument('Enumerate results', '--enumerate'),
640
        user_id=ValueArgument('filter by user_id', '--user-id'),
641
        user_name=ValueArgument('filter by username', '--user-name'),
642
        meta=KeyValueArgument(
643
            'filter by metadata key=value (can be repeated)', ('--metadata')),
644
        meta_like=KeyValueArgument(
645
            'filter by metadata key=value (can be repeated)',
646
            ('--metadata-like'))
647
    )
648

    
649
    def _filter_by_metadata(self, images):
650
        new_images = []
651
        for img in images:
652
            meta = [dict(img['metadata'])]
653
            if self['meta']:
654
                meta = filter_dicts_by_dict(meta, self['meta'])
655
            if meta and self['meta_like']:
656
                meta = filter_dicts_by_dict(
657
                    meta, self['meta_like'], exact_match=False)
658
            if meta:
659
                new_images.append(img)
660
        return new_images
661

    
662
    def _filter_by_user(self, images):
663
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
664
        return filter_dicts_by_dict(images, dict(user_id=uuid))
665

    
666
    def _add_name(self, images, key='user_id'):
667
        uuids = self._uuids2usernames(
668
            list(set([img[key] for img in images])))
669
        for img in images:
670
            img[key] += ' (%s)' % uuids[img[key]]
671
        return images
672

    
673
    @errors.generic.all
674
    @errors.cyclades.connection
675
    def _run(self):
676
        withmeta = bool(self['meta'] or self['meta_like'])
677
        withuser = bool(self['user_id'] or self['user_name'])
678
        detail = self['detail'] or withmeta or withuser
679
        images = self.client.list_images(detail)
680
        images = self._filter_by_name(images)
681
        images = self._filter_by_id(images)
682
        if withuser:
683
            images = self._filter_by_user(images)
684
        if withmeta:
685
            images = self._filter_by_metadata(images)
686
        if self['detail'] and not (
687
                self['json_output'] or self['output_format']):
688
            images = self._add_name(self._add_name(images, 'tenant_id'))
689
        elif detail and not self['detail']:
690
            for img in images:
691
                for key in set(img).difference(self.PERMANENTS):
692
                    img.pop(key)
693
        kwargs = dict(with_enumeration=self['enum'])
694
        if self['limit']:
695
            images = images[:self['limit']]
696
        if self['more']:
697
            kwargs['out'] = StringIO()
698
            kwargs['title'] = ()
699
        self._print(images, **kwargs)
700
        if self['more']:
701
            pager(kwargs['out'].getvalue())
702

    
703
    def main(self):
704
        super(self.__class__, self)._run()
705
        self._run()
706

    
707

    
708
@command(imagecompute_cmds)
709
class imagecompute_info(_init_cyclades, _optional_json):
710
    """Get detailed information on an image"""
711

    
712
    @errors.generic.all
713
    @errors.cyclades.connection
714
    @errors.plankton.id
715
    def _run(self, image_id):
716
        image = self.client.get_image_details(image_id)
717
        uuids = [image['user_id'], image['tenant_id']]
718
        usernames = self._uuids2usernames(uuids)
719
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
720
        image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
721
        self._print(image, self.print_dict)
722

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

    
727

    
728
@command(imagecompute_cmds)
729
class imagecompute_delete(_init_cyclades, _optional_output_cmd):
730
    """Delete an image (WARNING: image file is also removed)"""
731

    
732
    @errors.generic.all
733
    @errors.cyclades.connection
734
    @errors.plankton.id
735
    def _run(self, image_id):
736
        self._optional_output(self.client.delete_image(image_id))
737

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

    
742

    
743
@command(imagecompute_cmds)
744
class imagecompute_modify(_init_cyclades, _optional_output_cmd):
745
    """Modify image properties (metadata)"""
746

    
747
    arguments = dict(
748
        property_to_add=KeyValueArgument(
749
            'Add property in key=value format (can be repeated)',
750
            ('--property-add')),
751
        property_to_del=RepeatableArgument(
752
            'Delete property by key (can be repeated)',
753
            ('--property-del'))
754
    )
755
    required = ['property_to_add', 'property_to_del']
756

    
757
    @errors.generic.all
758
    @errors.cyclades.connection
759
    @errors.plankton.id
760
    def _run(self, image_id):
761
        if self['property_to_add']:
762
            self.client.update_image_metadata(
763
                image_id, **self['property_to_add'])
764
        for key in (self['property_to_del'] or []):
765
            self.client.delete_image_metadata(image_id, key)
766
        if self['with_output']:
767
            self._optional_output(self.client.get_image_details(image_id))
768

    
769
    def main(self, image_id):
770
        super(self.__class__, self)._run()
771
        self._run(image_id=image_id)