Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (30.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('Make the image public', '--public'),
330
        unpublish=FlagArgument('Make the image private', '--private'),
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 URI, return in the form pithos://uuid/container[/path]
377

378
    UPDATE: URLs without a path are also resolvable. Therefore, caller methods
379
    should check if there is a path or not
380
    """
381

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

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

    
392
    @property
393
    def value(self):
394
        path = ('/%s' % self.path) if self.path else ''
395
        return 'pithos://%s/%s%s' % (self.uuid, self.container, path)
396

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

    
420

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

    
436
    container_info_cache = {}
437

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

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

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

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

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

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

    
559
        (params, properties, new_loc) = self._load_params_from_file(location)
560
        if location != new_loc:
561
            locator.value = new_loc
562
        self._load_params_from_args(params, properties)
563

    
564
        if not self['no_metafile_upload']:
565
            #check if metafile exists
566
            pithos = pithos or self._get_pithos_client(locator)
567
            meta_path = '%s.meta' % locator.path
568
            self._assert_remote_file_not_exist(pithos, meta_path)
569

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

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

    
608
    def main(self):
609
        super(self.__class__, self)._run()
610

    
611
        locator, pithos = self.arguments['pithos_location'], None
612
        locator.setdefault('uuid', self.auth_base.user_term('id'))
613
        locator.path = locator.path or path.basename(
614
            self['local_image_path'] or '')
615
        if not locator.path:
616
            raise CLIInvalidArgument(
617
                'Missing the image file or object', details=[
618
                    'Pithos+ URI %s does not point to a physical image' % (
619
                        locator.value),
620
                    'A physical image is necessary.',
621
                    'It can be a remote Pithos+ object or a local file.',
622
                    'To specify a remote image object:',
623
                    '  %s [pithos://UUID]/CONTAINER/PATH' % locator.lvalue,
624
                    'To specify a local file:',
625
                    '  %s [pithos://UUID]/CONTAINER[/PATH] %s LOCAL_PATH' % (
626
                        locator.lvalue,
627
                        self.arguments['local_image_path'].lvalue)
628
                ])
629
        self.arguments['pithos_location'].setdefault(
630
            'uuid', self.auth_base.user_term('id'))
631
        self._run(self['name'], locator)
632

    
633

    
634
@command(image_cmds)
635
class image_unregister(_init_image, _optional_output_cmd):
636
    """Unregister an image (does not delete the image file)"""
637

    
638
    @errors.generic.all
639
    @errors.plankton.connection
640
    @errors.plankton.id
641
    def _run(self, image_id):
642
        self._optional_output(self.client.unregister(image_id))
643

    
644
    def main(self, image_id):
645
        super(self.__class__, self)._run()
646
        self._run(image_id=image_id)
647

    
648

    
649
# Compute Image Commands
650

    
651
@command(imagecompute_cmds)
652
class imagecompute_list(
653
        _init_cyclades, _optional_json, _name_filter, _id_filter):
654
    """List images"""
655

    
656
    PERMANENTS = ('id', 'name')
657

    
658
    arguments = dict(
659
        detail=FlagArgument('show detailed output', ('-l', '--details')),
660
        limit=IntArgument('limit number listed images', ('-n', '--number')),
661
        more=FlagArgument('handle long lists of results', '--more'),
662
        enum=FlagArgument('Enumerate results', '--enumerate'),
663
        user_id=ValueArgument('filter by user_id', '--user-id'),
664
        user_name=ValueArgument('filter by username', '--user-name'),
665
        meta=KeyValueArgument(
666
            'filter by metadata key=value (can be repeated)', ('--metadata')),
667
        meta_like=KeyValueArgument(
668
            'filter by metadata key=value (can be repeated)',
669
            ('--metadata-like'))
670
    )
671

    
672
    def _filter_by_metadata(self, images):
673
        new_images = []
674
        for img in images:
675
            meta = [dict(img['metadata'])]
676
            if self['meta']:
677
                meta = filter_dicts_by_dict(meta, self['meta'])
678
            if meta and self['meta_like']:
679
                meta = filter_dicts_by_dict(
680
                    meta, self['meta_like'], exact_match=False)
681
            if meta:
682
                new_images.append(img)
683
        return new_images
684

    
685
    def _filter_by_user(self, images):
686
        uuid = self['user_id'] or self._username2uuid(self['user_name'])
687
        return filter_dicts_by_dict(images, dict(user_id=uuid))
688

    
689
    def _add_name(self, images, key='user_id'):
690
        uuids = self._uuids2usernames(
691
            list(set([img[key] for img in images])))
692
        for img in images:
693
            img[key] += ' (%s)' % uuids[img[key]]
694
        return images
695

    
696
    @errors.generic.all
697
    @errors.cyclades.connection
698
    def _run(self):
699
        withmeta = bool(self['meta'] or self['meta_like'])
700
        withuser = bool(self['user_id'] or self['user_name'])
701
        detail = self['detail'] or withmeta or withuser
702
        images = self.client.list_images(detail)
703
        images = self._filter_by_name(images)
704
        images = self._filter_by_id(images)
705
        if withuser:
706
            images = self._filter_by_user(images)
707
        if withmeta:
708
            images = self._filter_by_metadata(images)
709
        if self['detail'] and not (
710
                self['json_output'] or self['output_format']):
711
            images = self._add_name(self._add_name(images, 'tenant_id'))
712
        elif detail and not self['detail']:
713
            for img in images:
714
                for key in set(img).difference(self.PERMANENTS):
715
                    img.pop(key)
716
        kwargs = dict(with_enumeration=self['enum'])
717
        if self['limit']:
718
            images = images[:self['limit']]
719
        if self['more']:
720
            kwargs['out'] = StringIO()
721
            kwargs['title'] = ()
722
        self._print(images, **kwargs)
723
        if self['more']:
724
            pager(kwargs['out'].getvalue())
725

    
726
    def main(self):
727
        super(self.__class__, self)._run()
728
        self._run()
729

    
730

    
731
@command(imagecompute_cmds)
732
class imagecompute_info(_init_cyclades, _optional_json):
733
    """Get detailed information on an image"""
734

    
735
    @errors.generic.all
736
    @errors.cyclades.connection
737
    @errors.plankton.id
738
    def _run(self, image_id):
739
        image = self.client.get_image_details(image_id)
740
        uuids = [image['user_id']]
741
        usernames = self._uuids2usernames(uuids)
742
        image['user_id'] += ' (%s)' % usernames[image['user_id']]
743
        self._print(image, self.print_dict)
744

    
745
    def main(self, image_id):
746
        super(self.__class__, self)._run()
747
        self._run(image_id=image_id)
748

    
749

    
750
@command(imagecompute_cmds)
751
class imagecompute_delete(_init_cyclades, _optional_output_cmd):
752
    """Delete an image (WARNING: image file is also removed)"""
753

    
754
    @errors.generic.all
755
    @errors.cyclades.connection
756
    @errors.plankton.id
757
    def _run(self, image_id):
758
        self._optional_output(self.client.delete_image(image_id))
759

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

    
764

    
765
@command(imagecompute_cmds)
766
class imagecompute_modify(_init_cyclades, _optional_output_cmd):
767
    """Modify image properties (metadata)"""
768

    
769
    arguments = dict(
770
        property_to_add=KeyValueArgument(
771
            'Add property in key=value format (can be repeated)',
772
            ('--property-add')),
773
        property_to_del=RepeatableArgument(
774
            'Delete property by key (can be repeated)',
775
            ('--property-del'))
776
    )
777
    required = ['property_to_add', 'property_to_del']
778

    
779
    @errors.generic.all
780
    @errors.cyclades.connection
781
    @errors.plankton.id
782
    def _run(self, image_id):
783
        if self['property_to_add']:
784
            self.client.update_image_metadata(
785
                image_id, **self['property_to_add'])
786
        for key in (self['property_to_del'] or []):
787
            self.client.delete_image_metadata(image_id, key)
788
        if self['with_output']:
789
            self._optional_output(self.client.get_image_details(image_id))
790

    
791
    def main(self, image_id):
792
        super(self.__class__, self)._run()
793
        self._run(image_id=image_id)