Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ ec6c3949

History | View | Annotate | Download (29.3 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 (
52
    raiseCLIError, CLIBaseUrlError, CLIInvalidArgument)
53
from kamaki.cli.commands import _command_init, errors, addLogSettings
54
from kamaki.cli.commands import (
55
    _optional_output_cmd, _optional_json, _name_filter, _id_filter)
56

    
57

    
58
image_cmds = CommandTree('image', 'Cyclades/Plankton API image commands')
59
imagecompute_cmds = CommandTree(
60
    'imagecompute', 'Cyclades/Compute API image commands')
61
_commands = [image_cmds, imagecompute_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
        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']
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']:
351
            self.client.add_member(image_id, mid)
352
        for mid in self['member_ID_to_remove']:
353
            self.client.remove_member(image_id, mid)
354
        if len([term for term in self.required if (
355
                self[term] and not term.startswith('member_ID'))]) > 1:
356
            meta = self.client.get_meta(image_id)
357
            for k, v in self['property_to_set'].items():
358
                meta['properties'][k.upper()] = v
359
            for k in self['property_to_del']:
360
                meta['properties'][k.upper()] = None
361
            self._optional_output(self.client.update_image(
362
                image_id,
363
                name=self['image_name'],
364
                disk_format=self['disk_format'],
365
                container_format=self['container_format'],
366
                status=self['status'],
367
                public=self['publish'] or self['unpublish'] or None,
368
                **meta['properties']))
369
        if self['with_output']:
370
            self._optional_output(self.get_image_details(image_id))
371

    
372
    def main(self, image_id):
373
        super(self.__class__, self)._run()
374
        self._run(image_id=image_id)
375

    
376

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

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

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

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

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

    
418

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

    
434
    container_info_cache = {}
435

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

    
476
    def _get_pithos_client(self, locator):
477
        if self['no_metafile_upload']:
478
            return None
479
        ptoken = self.client.token
480
        if getattr(self, 'auth_base', False):
481
            pithos_endpoints = self.auth_base.get_service_endpoints(
482
                'object-store')
483
            purl = pithos_endpoints['publicURL']
484
        else:
485
            purl = self.config.get_cloud('pithos', 'url')
486
        if not purl:
487
            raise CLIBaseUrlError(service='pithos')
488
        return PithosClient(purl, ptoken, locator.uuid, locator.container)
489

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

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

    
524
    @errors.generic.all
525
    @errors.plankton.connection
526
    def _run(self, name, location):
527
        locator = self.arguments['pithos_location']
528
        if self['local_image_path']:
529
            with open(self['local_image_path']) as f:
530
                pithos = self._get_pithos_client(locator)
531
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
532
                if pbar:
533
                    hash_bar = pbar.clone()
534
                    hash_cb = hash_bar.get_generator('Calculating hashes')
535
                pithos.upload_object(
536
                    locator.path, f,
537
                    hash_cb=hash_cb, upload_cb=upload_cb,
538
                    container_info_cache=self.container_info_cache)
539
                pbar.finish()
540

    
541
        (params, properties, new_loc) = self._load_params_from_file(location)
542
        if location != new_loc:
543
            locator.value = new_loc
544
        self._load_params_from_args(params, properties)
545
        pclient = self._get_pithos_client(locator)
546

    
547
        #check if metafile exists
548
        meta_path = '%s.meta' % locator.path
549
        if pclient and not self['metafile_force']:
550
            try:
551
                pclient.get_object_info(meta_path)
552
                raiseCLIError(
553
                    'Metadata file /%s/%s already exists, abort' % (
554
                        locator.container, meta_path),
555
                    details=['Registration ABORTED', 'Try -f to overwrite'])
556
            except ClientError as ce:
557
                if ce.status != 404:
558
                    raise
559

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

    
575
        #upload the metadata file
576
        if pclient:
577
            try:
578
                meta_headers = pclient.upload_from_string(
579
                    meta_path, dumps(r, indent=2),
580
                    container_info_cache=self.container_info_cache)
581
            except TypeError:
582
                self.error(
583
                    'Failed to dump metafile /%s/%s' % (
584
                        locator.container, meta_path))
585
                return
586
            if self['json_output'] or self['output_format']:
587
                self.print_json(dict(
588
                    metafile_location='/%s/%s' % (
589
                        locator.container, meta_path),
590
                    headers=meta_headers))
591
            else:
592
                self.error('Metadata file uploaded as /%s/%s (version %s)' % (
593
                    locator.container,
594
                    meta_path,
595
                    meta_headers['x-object-version']))
596

    
597
    def main(self):
598
        super(self.__class__, self)._run()
599
        self.arguments['pithos_location'].setdefault(
600
            'uuid', self.auth_base.user_term('id'))
601
        self._run(self['name'], self['pithos_location'])
602

    
603

    
604
@command(image_cmds)
605
class image_unregister(_init_image, _optional_output_cmd):
606
    """Unregister an image (does not delete the image file)"""
607

    
608
    @errors.generic.all
609
    @errors.plankton.connection
610
    @errors.plankton.id
611
    def _run(self, image_id):
612
        self._optional_output(self.client.unregister(image_id))
613

    
614
    def main(self, image_id):
615
        super(self.__class__, self)._run()
616
        self._run(image_id=image_id)
617

    
618

    
619
# Compute Image Commands
620

    
621
@command(imagecompute_cmds)
622
class imagecompute_list(
623
        _init_cyclades, _optional_json, _name_filter, _id_filter):
624
    """List images"""
625

    
626
    PERMANENTS = ('id', 'name')
627

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

    
642
    def _filter_by_metadata(self, images):
643
        new_images = []
644
        for img in images:
645
            meta = [dict(img['metadata'])]
646
            if self['meta']:
647
                meta = filter_dicts_by_dict(meta, self['meta'])
648
            if meta and self['meta_like']:
649
                meta = filter_dicts_by_dict(
650
                    meta, self['meta_like'], exact_match=False)
651
            if meta:
652
                new_images.append(img)
653
        return new_images
654

    
655
    def _filter_by_user(self, images):
656
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
657
        return filter_dicts_by_dict(images, dict(user_id=uuid))
658

    
659
    def _add_name(self, images, key='user_id'):
660
        uuids = self._uuids2usernames(
661
            list(set([img[key] for img in images])))
662
        for img in images:
663
            img[key] += ' (%s)' % uuids[img[key]]
664
        return images
665

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

    
696
    def main(self):
697
        super(self.__class__, self)._run()
698
        self._run()
699

    
700

    
701
@command(imagecompute_cmds)
702
class imagecompute_info(_init_cyclades, _optional_json):
703
    """Get detailed information on an image"""
704

    
705
    @errors.generic.all
706
    @errors.cyclades.connection
707
    @errors.plankton.id
708
    def _run(self, image_id):
709
        image = self.client.get_image_details(image_id)
710
        uuids = [image['user_id'], image['tenant_id']]
711
        usernames = self._uuids2usernames(uuids)
712
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
713
        image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
714
        self._print(image, self.print_dict)
715

    
716
    def main(self, image_id):
717
        super(self.__class__, self)._run()
718
        self._run(image_id=image_id)
719

    
720

    
721
@command(imagecompute_cmds)
722
class imagecompute_delete(_init_cyclades, _optional_output_cmd):
723
    """Delete an image (WARNING: image file is also removed)"""
724

    
725
    @errors.generic.all
726
    @errors.cyclades.connection
727
    @errors.plankton.id
728
    def _run(self, image_id):
729
        self._optional_output(self.client.delete_image(image_id))
730

    
731
    def main(self, image_id):
732
        super(self.__class__, self)._run()
733
        self._run(image_id=image_id)
734

    
735

    
736
@command(imagecompute_cmds)
737
class imagecompute_modify(_init_cyclades, _optional_output_cmd):
738
    """Modify image properties (metadata)"""
739

    
740
    arguments = dict(
741
        property_to_add=KeyValueArgument(
742
            'Add property in key=value format (can be repeated)',
743
            ('--property-add')),
744
        property_to_del=RepeatableArgument(
745
            'Delete property by key (can be repeated)',
746
            ('--property-del'))
747
    )
748
    required = ['property_to_add', 'property_to_del']
749

    
750
    @errors.generic.all
751
    @errors.cyclades.connection
752
    @errors.plankton.id
753
    def _run(self, image_id):
754
        if self['property_to_add']:
755
            self.client.update_image_metadata(
756
                image_id, **self['property_to_add'])
757
        for key in self['property_to_del']:
758
            self.client.delete_image_metadata(image_id, key)
759
        if self['with_output']:
760
            self._optional_output(self.client.get_image_details(image_id))
761

    
762
    def main(self, image_id):
763
        super(self.__class__, self)._run()
764
        self._run(image_id=image_id)