Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 3f36ba1d

History | View | Annotate | Download (29.5 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: /user authenticate',
66
    ' check available containers: /file list',
67
    ' create a new container: /file create <container>',
68
    ' check container contents: /file list <container>',
69
    ' upload files: /file upload <image file> <container>',
70
    ' register an image: /image register <image name> <container>:<path>']
71

    
72
about_image_id = ['To see a list of available image ids: /image list']
73

    
74

    
75
log = getLogger(__name__)
76

    
77

    
78
class _init_image(_command_init):
79
    @errors.generic.all
80
    @addLogSettings
81
    def _run(self):
82
        if getattr(self, 'cloud', None):
83
            img_url = self._custom_url('image') or self._custom_url('plankton')
84
            if img_url:
85
                token = self._custom_token('image') or self._custom_token(
86
                    'plankton') or self.config.get_cloud(self.cloud, 'token')
87
                self.client = ImageClient(base_url=img_url, token=token)
88
                return
89
        if getattr(self, 'auth_base', False):
90
            plankton_endpoints = self.auth_base.get_service_endpoints(
91
                self._custom_type('image') or self._custom_type(
92
                    'plankton') or 'image',
93
                self._custom_version('image') or self._custom_version(
94
                    'plankton') or '')
95
            base_url = plankton_endpoints['publicURL']
96
            token = self.auth_base.token
97
        else:
98
            raise CLIBaseUrlError(service='plankton')
99
        self.client = ImageClient(base_url=base_url, token=token)
100

    
101
    def main(self):
102
        self._run()
103

    
104

    
105
# Plankton Image Commands
106

    
107

    
108
def _validate_image_meta(json_dict, return_str=False):
109
    """
110
    :param json_dict" (dict) json-formated, of the form
111
        {"key1": "val1", "key2": "val2", ...}
112

113
    :param return_str: (boolean) if true, return a json dump
114

115
    :returns: (dict) if return_str is not True, else return str
116

117
    :raises TypeError, AttributeError: Invalid json format
118

119
    :raises AssertionError: Valid json but invalid image properties dict
120
    """
121
    json_str = dumps(json_dict, indent=2)
122
    for k, v in json_dict.items():
123
        if k.lower() == 'properties':
124
            for pk, pv in v.items():
125
                prop_ok = not (isinstance(pv, dict) or isinstance(pv, list))
126
                assert prop_ok, 'Invalid property value for key %s' % pk
127
                key_ok = not (' ' in k or '-' in k)
128
                assert key_ok, 'Invalid property key %s' % k
129
            continue
130
        meta_ok = not (isinstance(v, dict) or isinstance(v, list))
131
        assert meta_ok, 'Invalid value for meta key %s' % k
132
        meta_ok = ' ' not in k
133
        assert meta_ok, 'Invalid meta key [%s]' % k
134
        json_dict[k] = '%s' % v
135
    return json_str if return_str else json_dict
136

    
137

    
138
def _load_image_meta(filepath):
139
    """
140
    :param filepath: (str) the (relative) path of the metafile
141

142
    :returns: (dict) json_formated
143

144
    :raises TypeError, AttributeError: Invalid json format
145

146
    :raises AssertionError: Valid json but invalid image properties dict
147
    """
148
    with open(path.abspath(filepath)) as f:
149
        meta_dict = load(f)
150
        try:
151
            return _validate_image_meta(meta_dict)
152
        except AssertionError:
153
            log.debug('Failed to load properties from file %s' % filepath)
154
            raise
155

    
156

    
157
def _validate_image_location(location):
158
    """
159
    :param location: (str) pithos://<user-id>/<container>/<image-path>
160

161
    :returns: (<user-id>, <container>, <image-path>)
162

163
    :raises AssertionError: if location is invalid
164
    """
165
    prefix = 'pithos://'
166
    msg = 'Invalid prefix for location %s , try: %s' % (location, prefix)
167
    assert location.startswith(prefix), msg
168
    service, sep, rest = location.partition('://')
169
    assert sep and rest, 'Location %s is missing user-id' % location
170
    uuid, sep, rest = rest.partition('/')
171
    assert sep and rest, 'Location %s is missing container' % location
172
    container, sep, img_path = rest.partition('/')
173
    assert sep and img_path, 'Location %s is missing image path' % location
174
    return uuid, container, img_path
175

    
176

    
177
@command(image_cmds)
178
class image_list(_init_image, _optional_json, _name_filter, _id_filter):
179
    """List images accessible by user"""
180

    
181
    PERMANENTS = (
182
        'id', 'name',
183
        'status', 'container_format', 'disk_format', 'size')
184

    
185
    arguments = dict(
186
        detail=FlagArgument('show detailed output', ('-l', '--details')),
187
        container_format=ValueArgument(
188
            'filter by container format',
189
            '--container-format'),
190
        disk_format=ValueArgument('filter by disk format', '--disk-format'),
191
        size_min=IntArgument('filter by minimum size', '--size-min'),
192
        size_max=IntArgument('filter by maximum size', '--size-max'),
193
        status=ValueArgument('filter by status', '--status'),
194
        owner=ValueArgument('filter by owner', '--owner'),
195
        owner_name=ValueArgument('filter by owners username', '--owner-name'),
196
        order=ValueArgument(
197
            'order by FIELD ( - to reverse order)',
198
            '--order',
199
            default=''),
200
        limit=IntArgument('limit number of listed images', ('-n', '--number')),
201
        more=FlagArgument(
202
            'output results in pages (-n to set items per page, default 10)',
203
            '--more'),
204
        enum=FlagArgument('Enumerate results', '--enumerate'),
205
        prop=KeyValueArgument('filter by property key=value', ('--property')),
206
        prop_like=KeyValueArgument(
207
            'fliter by property key=value where value is part of actual value',
208
            ('--property-like')),
209
        image_ID_for_members=ValueArgument(
210
            'List members of an image', '--members-of')
211
    )
212

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

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

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

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

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

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

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

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

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

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

    
297

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

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

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

    
315

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

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

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

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

    
373

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

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

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

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

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

    
415

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

    
431
    container_info_cache = {}
432

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

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

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

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

    
520
    def _assert_remote_file_not_exist(self, pithos, path):
521
        if pithos and not self['force_upload']:
522
            try:
523
                pithos.get_object_info(path)
524
                raiseCLIError(
525
                    'Remote file /%s/%s already exists' % (
526
                        pithos.container, path),
527
                    importance=2,
528
                    details=[
529
                        'Registration ABORTED',
530
                        'Use %s to force upload' % self.arguments[
531
                            'force_upload'].lvalue])
532
            except ClientError as ce:
533
                if ce.status != 404:
534
                    raise
535

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

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

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

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

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

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

    
608

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

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

    
619
    def main(self, image_id):
620
        super(self.__class__, self)._run()
621
        self._run(image_id=image_id)
622

    
623

    
624
# Compute Image Commands
625

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

    
631
    PERMANENTS = ('id', 'name')
632

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

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

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

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

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

    
701
    def main(self):
702
        super(self.__class__, self)._run()
703
        self._run()
704

    
705

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

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

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

    
725

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

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

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

    
740

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

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

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

    
767
    def main(self, image_id):
768
        super(self.__class__, self)._run()
769
        self._run(image_id=image_id)