Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (26.1 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

    
38
from kamaki.cli import command
39
from kamaki.cli.command_tree import CommandTree
40
from kamaki.cli.utils import print_dict, print_json
41
from kamaki.clients.image import ImageClient
42
from kamaki.clients.pithos import PithosClient
43
from kamaki.clients.astakos import AstakosClient
44
from kamaki.clients import ClientError
45
from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
46
from kamaki.cli.argument import IntArgument, ProgressBarArgument
47
from kamaki.cli.commands.cyclades import _init_cyclades
48
from kamaki.cli.errors import raiseCLIError, CLIBaseUrlError
49
from kamaki.cli.commands import _command_init, errors, addLogSettings
50
from kamaki.cli.commands import _optional_output_cmd, _optional_json
51

    
52

    
53
image_cmds = CommandTree(
54
    'image',
55
    'Cyclades/Plankton API image commands\n'
56
    'image compute:\tCyclades/Compute API image commands')
57
_commands = [image_cmds]
58

    
59

    
60
howto_image_file = [
61
    'Kamaki commands to:',
62
    ' get current user id: /user authenticate',
63
    ' check available containers: /file list',
64
    ' create a new container: /file create <container>',
65
    ' check container contents: /file list <container>',
66
    ' upload files: /file upload <image file> <container>']
67

    
68
about_image_id = ['To see a list of available image ids: /image list']
69

    
70

    
71
log = getLogger(__name__)
72

    
73

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

    
98
    def main(self):
99
        self._run()
100

    
101

    
102
# Plankton Image Commands
103

    
104

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

110
    :param return_str: (boolean) if true, return a json dump
111

112
    :returns: (dict) if return_str is not True, else return str
113

114
    :raises TypeError, AttributeError: Invalid json format
115

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

    
134

    
135
def _load_image_meta(filepath):
136
    """
137
    :param filepath: (str) the (relative) path of the metafile
138

139
    :returns: (dict) json_formated
140

141
    :raises TypeError, AttributeError: Invalid json format
142

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

    
153

    
154
def _validate_image_location(location):
155
    """
156
    :param location: (str) pithos://<user-id>/<container>/<image-path>
157

158
    :returns: (<user-id>, <container>, <image-path>)
159

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

    
173

    
174
@command(image_cmds)
175
class image_list(_init_image, _optional_json):
176
    """List images accessible by user"""
177

    
178
    arguments = dict(
179
        detail=FlagArgument('show detailed output', ('-l', '--details')),
180
        container_format=ValueArgument(
181
            'filter by container format',
182
            '--container-format'),
183
        disk_format=ValueArgument('filter by disk format', '--disk-format'),
184
        name=ValueArgument('filter by name', '--name'),
185
        name_pref=ValueArgument(
186
            'filter by name prefix (case insensitive)',
187
            '--name-prefix'),
188
        name_suff=ValueArgument(
189
            'filter by name suffix (case insensitive)',
190
            '--name-suffix'),
191
        name_like=ValueArgument(
192
            'print only if name contains this (case insensitive)',
193
            '--name-like'),
194
        size_min=IntArgument('filter by minimum size', '--size-min'),
195
        size_max=IntArgument('filter by maximum size', '--size-max'),
196
        status=ValueArgument('filter by status', '--status'),
197
        owner=ValueArgument('filter by owner', '--owner'),
198
        order=ValueArgument(
199
            'order by FIELD ( - to reverse order)',
200
            '--order',
201
            default=''),
202
        limit=IntArgument('limit number of listed images', ('-n', '--number')),
203
        more=FlagArgument(
204
            'output results in pages (-n to set items per page, default 10)',
205
            '--more'),
206
        enum=FlagArgument('Enumerate results', '--enumerate')
207
    )
208

    
209
    def _filtered_by_owner(self, detail, *list_params):
210
        images = []
211
        MINKEYS = set([
212
            'id', 'size', 'status', 'disk_format', 'container_format', 'name'])
213
        for img in self.client.list_public(True, *list_params):
214
            if img['owner'] == self['owner']:
215
                if not detail:
216
                    for key in set(img.keys()).difference(MINKEYS):
217
                        img.pop(key)
218
                images.append(img)
219
        return images
220

    
221
    def _filtered_by_name(self, images):
222
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
223
        return [img for img in images if (
224
            (not np) or img['name'].lower().startswith(np.lower())) and (
225
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
226
            (not nl) or nl.lower() in img['name'].lower())]
227

    
228
    @errors.generic.all
229
    @errors.cyclades.connection
230
    def _run(self):
231
        super(self.__class__, self)._run()
232
        filters = {}
233
        for arg in set([
234
                'container_format',
235
                'disk_format',
236
                'name',
237
                'size_min',
238
                'size_max',
239
                'status']).intersection(self.arguments):
240
            filters[arg] = self[arg]
241

    
242
        order = self['order']
243
        detail = self['detail']
244
        if self['owner']:
245
            images = self._filtered_by_owner(detail, filters, order)
246
        else:
247
            images = self.client.list_public(detail, filters, order)
248

    
249
        images = self._filtered_by_name(images)
250
        kwargs = dict(with_enumeration=self['enum'])
251
        if self['more']:
252
            kwargs['page_size'] = self['limit'] or 10
253
        elif self['limit']:
254
            images = images[:self['limit']]
255
        self._print(images, **kwargs)
256

    
257
    def main(self):
258
        super(self.__class__, self)._run()
259
        self._run()
260

    
261

    
262
@command(image_cmds)
263
class image_meta(_init_image, _optional_json):
264
    """Get image metadata
265
    Image metadata include:
266
    - image file information (location, size, etc.)
267
    - image information (id, name, etc.)
268
    - image os properties (os, fs, etc.)
269
    """
270

    
271
    @errors.generic.all
272
    @errors.plankton.connection
273
    @errors.plankton.id
274
    def _run(self, image_id):
275
        self._print([self.client.get_meta(image_id)])
276

    
277
    def main(self, image_id):
278
        super(self.__class__, self)._run()
279
        self._run(image_id=image_id)
280

    
281

    
282
@command(image_cmds)
283
class image_register(_init_image, _optional_json):
284
    """(Re)Register an image"""
285

    
286
    container_info_cache = {}
287

    
288
    arguments = dict(
289
        checksum=ValueArgument('set image checksum', '--checksum'),
290
        container_format=ValueArgument(
291
            'set container format',
292
            '--container-format'),
293
        disk_format=ValueArgument('set disk format', '--disk-format'),
294
        owner=ValueArgument('set image owner (admin only)', '--owner'),
295
        properties=KeyValueArgument(
296
            'add property in key=value form (can be repeated)',
297
            ('-p', '--property')),
298
        is_public=FlagArgument('mark image as public', '--public'),
299
        size=IntArgument('set image size', '--size'),
300
        metafile=ValueArgument(
301
            'Load metadata from a json-formated file <img-file>.meta :'
302
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
303
            ('--metafile')),
304
        metafile_force=FlagArgument(
305
            'Store remote metadata object, even if it already exists',
306
            ('-f', '--force')),
307
        no_metafile_upload=FlagArgument(
308
            'Do not store metadata in remote meta file',
309
            ('--no-metafile-upload')),
310
        container=ValueArgument(
311
            'Pithos+ container containing the image file',
312
            ('-C', '--container')),
313
        uuid=ValueArgument('Custom user uuid', '--uuid'),
314
        local_image_path=ValueArgument(
315
            'Local image file path to upload and register '
316
            '(still need target file in the form container:remote-path )',
317
            '--upload-image-file'),
318
        progress_bar=ProgressBarArgument(
319
            'Do not use progress bar', '--no-progress-bar', default=False)
320
    )
321

    
322
    def _get_user_id(self):
323
        atoken = self.client.token
324
        if getattr(self, 'auth_base', False):
325
            return self.auth_base.term('id', atoken)
326
        else:
327
            astakos_url = self.config.get('user', 'url')\
328
                or self.config.get('astakos', 'url')
329
            if not astakos_url:
330
                raise CLIBaseUrlError(service='astakos')
331
            user = AstakosClient(astakos_url, atoken)
332
            return user.term('id')
333

    
334
    def _get_pithos_client(self, container):
335
        if self['no_metafile_upload']:
336
            return None
337
        ptoken = self.client.token
338
        if getattr(self, 'auth_base', False):
339
            pithos_endpoints = self.auth_base.get_service_endpoints(
340
                'object-store')
341
            purl = pithos_endpoints['publicURL']
342
        else:
343
            purl = self.config.get_cloud('pithos', 'url')
344
        if not purl:
345
            raise CLIBaseUrlError(service='pithos')
346
        return PithosClient(purl, ptoken, self._get_user_id(), container)
347

    
348
    def _store_remote_metafile(self, pclient, remote_path, metadata):
349
        return pclient.upload_from_string(
350
            remote_path, _validate_image_meta(metadata, return_str=True),
351
            container_info_cache=self.container_info_cache)
352

    
353
    def _load_params_from_file(self, location):
354
        params, properties = dict(), dict()
355
        pfile = self['metafile']
356
        if pfile:
357
            try:
358
                for k, v in _load_image_meta(pfile).items():
359
                    key = k.lower().replace('-', '_')
360
                    if k == 'properties':
361
                        for pk, pv in v.items():
362
                            properties[pk.upper().replace('-', '_')] = pv
363
                    elif key == 'name':
364
                            continue
365
                    elif key == 'location':
366
                        if location:
367
                            continue
368
                        location = v
369
                    else:
370
                        params[key] = v
371
            except Exception as e:
372
                raiseCLIError(e, 'Invalid json metadata config file')
373
        return params, properties, location
374

    
375
    def _load_params_from_args(self, params, properties):
376
        for key in set([
377
                'checksum',
378
                'container_format',
379
                'disk_format',
380
                'owner',
381
                'size',
382
                'is_public']).intersection(self.arguments):
383
            params[key] = self[key]
384
        for k, v in self['properties'].items():
385
            properties[k.upper().replace('-', '_')] = v
386

    
387
    def _validate_location(self, location):
388
        if not location:
389
            raiseCLIError(
390
                'No image file location provided',
391
                importance=2, details=[
392
                    'An image location is needed. Image location format:',
393
                    '  pithos://<user-id>/<container>/<path>',
394
                    ' where an image file at the above location must exist.'
395
                    ] + howto_image_file)
396
        try:
397
            return _validate_image_location(location)
398
        except AssertionError as ae:
399
            raiseCLIError(
400
                ae, 'Invalid image location format',
401
                importance=1, details=[
402
                    'Valid image location format:',
403
                    '  pithos://<user-id>/<container>/<img-file-path>'
404
                    ] + howto_image_file)
405

    
406
    def _mine_location(self, container_path):
407
        uuid = self['uuid'] or self._get_user_id()
408
        if self['container']:
409
            return uuid, self['container'], container_path
410
        container, sep, path = container_path.partition(':')
411
        if not (bool(container) and bool(path)):
412
            raiseCLIError(
413
                'Incorrect container-path format', importance=1, details=[
414
                'Use : to seperate container form path',
415
                '  <container>:<image-path>',
416
                'OR',
417
                'Use -C to specifiy a container',
418
                '  -C <container> <image-path>'] + howto_image_file)
419

    
420
        return uuid, container, path
421

    
422
    @errors.generic.all
423
    @errors.plankton.connection
424
    def _run(self, name, uuid, container, img_path):
425
        if self['local_image_path']:
426
            with open(self['local_image_path']) as f:
427
                pithos = self._get_pithos_client(container)
428
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
429
                if pbar:
430
                    hash_bar = pbar.clone()
431
                    hash_cb = hash_bar.get_generator('Calculating hashes')
432
                pithos.upload_object(
433
                    img_path, f,
434
                    hash_cb=hash_cb, upload_cb=upload_cb,
435
                    container_info_cache=self.container_info_cache)
436
                pbar.finish()
437

    
438
        location = 'pithos://%s/%s/%s' % (uuid, container, img_path)
439
        (params, properties, new_loc) = self._load_params_from_file(location)
440
        if location != new_loc:
441
            uuid, container, img_path = self._validate_location(new_loc)
442
        self._load_params_from_args(params, properties)
443
        pclient = self._get_pithos_client(container)
444

    
445
        #check if metafile exists
446
        meta_path = '%s.meta' % img_path
447
        if pclient and not self['metafile_force']:
448
            try:
449
                pclient.get_object_info(meta_path)
450
                raiseCLIError(
451
                    'Metadata file %s:%s already exists, abort' % (
452
                        container, meta_path),
453
                    details=['Registration ABORTED', 'Try -f to overwrite'])
454
            except ClientError as ce:
455
                if ce.status != 404:
456
                    raise
457

    
458
        #register the image
459
        try:
460
            r = self.client.register(name, location, params, properties)
461
        except ClientError as ce:
462
            if ce.status in (400, ):
463
                raiseCLIError(
464
                    ce, 'Nonexistent image file location %s' % location,
465
                    details=[
466
                        'Make sure the image file exists'] + howto_image_file)
467
            raise
468
        self._print(r, print_dict)
469

    
470
        #upload the metadata file
471
        if pclient:
472
            try:
473
                meta_headers = pclient.upload_from_string(
474
                    meta_path, dumps(r, indent=2),
475
                    container_info_cache=self.container_info_cache)
476
            except TypeError:
477
                print('Failed to dump metafile %s:%s' % (container, meta_path))
478
                return
479
            if self['json_output']:
480
                print_json(dict(
481
                    metafile_location='%s:%s' % (container, meta_path),
482
                    headers=meta_headers))
483
            else:
484
                print('Metadata file uploaded as %s:%s (version %s)' % (
485
                    container, meta_path, meta_headers['x-object-version']))
486

    
487
    def main(self, name, container___image_path):
488
        super(self.__class__, self)._run()
489
        self._run(name, *self._mine_location(container___image_path))
490

    
491

    
492
@command(image_cmds)
493
class image_unregister(_init_image, _optional_output_cmd):
494
    """Unregister an image (does not delete the image file)"""
495

    
496
    @errors.generic.all
497
    @errors.plankton.connection
498
    @errors.plankton.id
499
    def _run(self, image_id):
500
        self._optional_output(self.client.unregister(image_id))
501

    
502
    def main(self, image_id):
503
        super(self.__class__, self)._run()
504
        self._run(image_id=image_id)
505

    
506

    
507
@command(image_cmds)
508
class image_shared(_init_image, _optional_json):
509
    """List images shared by a member"""
510

    
511
    @errors.generic.all
512
    @errors.plankton.connection
513
    def _run(self, member):
514
        self._print(self.client.list_shared(member), title=('image_id',))
515

    
516
    def main(self, member):
517
        super(self.__class__, self)._run()
518
        self._run(member)
519

    
520

    
521
@command(image_cmds)
522
class image_members(_init_image):
523
    """Manage members. Members of an image are users who can modify it"""
524

    
525

    
526
@command(image_cmds)
527
class image_members_list(_init_image, _optional_json):
528
    """List members of an image"""
529

    
530
    @errors.generic.all
531
    @errors.plankton.connection
532
    @errors.plankton.id
533
    def _run(self, image_id):
534
        self._print(self.client.list_members(image_id), title=('member_id',))
535

    
536
    def main(self, image_id):
537
        super(self.__class__, self)._run()
538
        self._run(image_id=image_id)
539

    
540

    
541
@command(image_cmds)
542
class image_members_add(_init_image, _optional_output_cmd):
543
    """Add a member to an image"""
544

    
545
    @errors.generic.all
546
    @errors.plankton.connection
547
    @errors.plankton.id
548
    def _run(self, image_id=None, member=None):
549
            self._optional_output(self.client.add_member(image_id, member))
550

    
551
    def main(self, image_id, member):
552
        super(self.__class__, self)._run()
553
        self._run(image_id=image_id, member=member)
554

    
555

    
556
@command(image_cmds)
557
class image_members_delete(_init_image, _optional_output_cmd):
558
    """Remove a member from an image"""
559

    
560
    @errors.generic.all
561
    @errors.plankton.connection
562
    @errors.plankton.id
563
    def _run(self, image_id=None, member=None):
564
            self._optional_output(self.client.remove_member(image_id, member))
565

    
566
    def main(self, image_id, member):
567
        super(self.__class__, self)._run()
568
        self._run(image_id=image_id, member=member)
569

    
570

    
571
@command(image_cmds)
572
class image_members_set(_init_image, _optional_output_cmd):
573
    """Set the members of an image"""
574

    
575
    @errors.generic.all
576
    @errors.plankton.connection
577
    @errors.plankton.id
578
    def _run(self, image_id, members):
579
            self._optional_output(self.client.set_members(image_id, members))
580

    
581
    def main(self, image_id, *members):
582
        super(self.__class__, self)._run()
583
        self._run(image_id=image_id, members=members)
584

    
585

    
586
# Compute Image Commands
587

    
588

    
589
@command(image_cmds)
590
class image_compute(_init_cyclades):
591
    """Cyclades/Compute API image commands"""
592

    
593

    
594
@command(image_cmds)
595
class image_compute_list(_init_cyclades, _optional_json):
596
    """List images"""
597

    
598
    arguments = dict(
599
        detail=FlagArgument('show detailed output', ('-l', '--details')),
600
        limit=IntArgument('limit number listed images', ('-n', '--number')),
601
        more=FlagArgument(
602
            'output results in pages (-n to set items per page, default 10)',
603
            '--more'),
604
        enum=FlagArgument('Enumerate results', '--enumerate')
605
    )
606

    
607
    @errors.generic.all
608
    @errors.cyclades.connection
609
    def _run(self):
610
        images = self.client.list_images(self['detail'])
611
        kwargs = dict(with_enumeration=self['enum'])
612
        if self['more']:
613
            kwargs['page_size'] = self['limit'] or 10
614
        elif self['limit']:
615
            images = images[:self['limit']]
616
        self._print(images, **kwargs)
617

    
618
    def main(self):
619
        super(self.__class__, self)._run()
620
        self._run()
621

    
622

    
623
@command(image_cmds)
624
class image_compute_info(_init_cyclades, _optional_json):
625
    """Get detailed information on an image"""
626

    
627
    @errors.generic.all
628
    @errors.cyclades.connection
629
    @errors.plankton.id
630
    def _run(self, image_id):
631
        image = self.client.get_image_details(image_id)
632
        self._print(image, print_dict)
633

    
634
    def main(self, image_id):
635
        super(self.__class__, self)._run()
636
        self._run(image_id=image_id)
637

    
638

    
639
@command(image_cmds)
640
class image_compute_delete(_init_cyclades, _optional_output_cmd):
641
    """Delete an image (WARNING: image file is also removed)"""
642

    
643
    @errors.generic.all
644
    @errors.cyclades.connection
645
    @errors.plankton.id
646
    def _run(self, image_id):
647
        self._optional_output(self.client.delete_image(image_id))
648

    
649
    def main(self, image_id):
650
        super(self.__class__, self)._run()
651
        self._run(image_id=image_id)
652

    
653

    
654
@command(image_cmds)
655
class image_compute_properties(_init_cyclades):
656
    """Manage properties related to OS installation in an image"""
657

    
658

    
659
@command(image_cmds)
660
class image_compute_properties_list(_init_cyclades, _optional_json):
661
    """List all image properties"""
662

    
663
    @errors.generic.all
664
    @errors.cyclades.connection
665
    @errors.plankton.id
666
    def _run(self, image_id):
667
        self._print(self.client.get_image_metadata(image_id), print_dict)
668

    
669
    def main(self, image_id):
670
        super(self.__class__, self)._run()
671
        self._run(image_id=image_id)
672

    
673

    
674
@command(image_cmds)
675
class image_compute_properties_get(_init_cyclades, _optional_json):
676
    """Get an image property"""
677

    
678
    @errors.generic.all
679
    @errors.cyclades.connection
680
    @errors.plankton.id
681
    @errors.plankton.metadata
682
    def _run(self, image_id, key):
683
        self._print(self.client.get_image_metadata(image_id, key), print_dict)
684

    
685
    def main(self, image_id, key):
686
        super(self.__class__, self)._run()
687
        self._run(image_id=image_id, key=key)
688

    
689

    
690
@command(image_cmds)
691
class image_compute_properties_add(_init_cyclades, _optional_json):
692
    """Add a property to an image"""
693

    
694
    @errors.generic.all
695
    @errors.cyclades.connection
696
    @errors.plankton.id
697
    @errors.plankton.metadata
698
    def _run(self, image_id, key, val):
699
        self._print(
700
            self.client.create_image_metadata(image_id, key, val), print_dict)
701

    
702
    def main(self, image_id, key, val):
703
        super(self.__class__, self)._run()
704
        self._run(image_id=image_id, key=key, val=val)
705

    
706

    
707
@command(image_cmds)
708
class image_compute_properties_set(_init_cyclades, _optional_json):
709
    """Add / update a set of properties for an image
710
    proeprties must be given in the form key=value, e.v.
711
    /image compute properties set <image-id> key1=val1 key2=val2
712
    """
713

    
714
    @errors.generic.all
715
    @errors.cyclades.connection
716
    @errors.plankton.id
717
    def _run(self, image_id, keyvals):
718
        meta = dict()
719
        for keyval in keyvals:
720
            key, val = keyval.split('=')
721
            meta[key] = val
722
        self._print(
723
            self.client.update_image_metadata(image_id, **meta), print_dict)
724

    
725
    def main(self, image_id, *key_equals_value):
726
        super(self.__class__, self)._run()
727
        self._run(image_id=image_id, keyvals=key_equals_value)
728

    
729

    
730
@command(image_cmds)
731
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
732
    """Delete a property from an image"""
733

    
734
    @errors.generic.all
735
    @errors.cyclades.connection
736
    @errors.plankton.id
737
    @errors.plankton.metadata
738
    def _run(self, image_id, key):
739
        self._optional_output(self.client.delete_image_metadata(image_id, key))
740

    
741
    def main(self, image_id, key):
742
        super(self.__class__, self)._run()
743
        self._run(image_id=image_id, key=key)