Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (26.9 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
    PERMANENTS = (
179
        'id', 'name',
180
        'status', 'container_format', 'disk_format', 'size')
181

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

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

    
226
    def _filtered_by_name(self, images):
227
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
228
        return [img for img in images if (
229
            (not np) or img['name'].lower().startswith(np.lower())) and (
230
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
231
            (not nl) or nl.lower() in img['name'].lower())]
232

    
233
    def _filtered_by_properties(self, images):
234
        new_images = []
235
        for img in images:
236
            if set(self['prop'].items()).difference(img['properties'].items()):
237
                continue
238
            if self['detail']:
239
                new_images.append(dict(img))
240
            else:
241
                new_images.append(dict())
242
                for k in set(img).intersection(self.PERMANENTS):
243
                    new_images[-1][k] = img[k]
244
        return new_images
245

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

    
260
        order = self['order']
261
        detail = self['detail'] or self['prop']
262
        if self['owner']:
263
            images = self._filtered_by_owner(detail, filters, order)
264
        else:
265
            images = self.client.list_public(detail, filters, order)
266

    
267
        images = self._filtered_by_name(images)
268
        if self['prop']:
269
            images = self._filtered_by_properties(images)
270
        kwargs = dict(with_enumeration=self['enum'])
271
        if self['more']:
272
            kwargs['page_size'] = self['limit'] or 10
273
        elif self['limit']:
274
            images = images[:self['limit']]
275
        self._print(images, **kwargs)
276

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

    
281

    
282
@command(image_cmds)
283
class image_meta(_init_image, _optional_json):
284
    """Get image metadata
285
    Image metadata include:
286
    - image file information (location, size, etc.)
287
    - image information (id, name, etc.)
288
    - image os properties (os, fs, etc.)
289
    """
290

    
291
    @errors.generic.all
292
    @errors.plankton.connection
293
    @errors.plankton.id
294
    def _run(self, image_id):
295
        self._print([self.client.get_meta(image_id)])
296

    
297
    def main(self, image_id):
298
        super(self.__class__, self)._run()
299
        self._run(image_id=image_id)
300

    
301

    
302
@command(image_cmds)
303
class image_register(_init_image, _optional_json):
304
    """(Re)Register an image"""
305

    
306
    container_info_cache = {}
307

    
308
    arguments = dict(
309
        checksum=ValueArgument('set image checksum', '--checksum'),
310
        container_format=ValueArgument(
311
            'set container format',
312
            '--container-format'),
313
        disk_format=ValueArgument('set disk format', '--disk-format'),
314
        owner=ValueArgument('set image owner (admin only)', '--owner'),
315
        properties=KeyValueArgument(
316
            'add property in key=value form (can be repeated)',
317
            ('-p', '--property')),
318
        is_public=FlagArgument('mark image as public', '--public'),
319
        size=IntArgument('set image size', '--size'),
320
        metafile=ValueArgument(
321
            'Load metadata from a json-formated file <img-file>.meta :'
322
            '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
323
            ('--metafile')),
324
        metafile_force=FlagArgument(
325
            'Store remote metadata object, even if it already exists',
326
            ('-f', '--force')),
327
        no_metafile_upload=FlagArgument(
328
            'Do not store metadata in remote meta file',
329
            ('--no-metafile-upload')),
330
        container=ValueArgument(
331
            'Pithos+ container containing the image file',
332
            ('-C', '--container')),
333
        uuid=ValueArgument('Custom user uuid', '--uuid'),
334
        local_image_path=ValueArgument(
335
            'Local image file path to upload and register '
336
            '(still need target file in the form container:remote-path )',
337
            '--upload-image-file'),
338
        progress_bar=ProgressBarArgument(
339
            'Do not use progress bar', '--no-progress-bar', default=False)
340
    )
341

    
342
    def _get_user_id(self):
343
        atoken = self.client.token
344
        if getattr(self, 'auth_base', False):
345
            return self.auth_base.term('id', atoken)
346
        else:
347
            astakos_url = self.config.get('user', 'url')\
348
                or self.config.get('astakos', 'url')
349
            if not astakos_url:
350
                raise CLIBaseUrlError(service='astakos')
351
            user = AstakosClient(astakos_url, atoken)
352
            return user.term('id')
353

    
354
    def _get_pithos_client(self, container):
355
        if self['no_metafile_upload']:
356
            return None
357
        ptoken = self.client.token
358
        if getattr(self, 'auth_base', False):
359
            pithos_endpoints = self.auth_base.get_service_endpoints(
360
                'object-store')
361
            purl = pithos_endpoints['publicURL']
362
        else:
363
            purl = self.config.get_cloud('pithos', 'url')
364
        if not purl:
365
            raise CLIBaseUrlError(service='pithos')
366
        return PithosClient(purl, ptoken, self._get_user_id(), container)
367

    
368
    def _store_remote_metafile(self, pclient, remote_path, metadata):
369
        return pclient.upload_from_string(
370
            remote_path, _validate_image_meta(metadata, return_str=True),
371
            container_info_cache=self.container_info_cache)
372

    
373
    def _load_params_from_file(self, location):
374
        params, properties = dict(), dict()
375
        pfile = self['metafile']
376
        if pfile:
377
            try:
378
                for k, v in _load_image_meta(pfile).items():
379
                    key = k.lower().replace('-', '_')
380
                    if k == 'properties':
381
                        for pk, pv in v.items():
382
                            properties[pk.upper().replace('-', '_')] = pv
383
                    elif key == 'name':
384
                            continue
385
                    elif key == 'location':
386
                        if location:
387
                            continue
388
                        location = v
389
                    else:
390
                        params[key] = v
391
            except Exception as e:
392
                raiseCLIError(e, 'Invalid json metadata config file')
393
        return params, properties, location
394

    
395
    def _load_params_from_args(self, params, properties):
396
        for key in set([
397
                'checksum',
398
                'container_format',
399
                'disk_format',
400
                'owner',
401
                'size',
402
                'is_public']).intersection(self.arguments):
403
            params[key] = self[key]
404
        for k, v in self['properties'].items():
405
            properties[k.upper().replace('-', '_')] = v
406

    
407
    def _validate_location(self, location):
408
        if not location:
409
            raiseCLIError(
410
                'No image file location provided',
411
                importance=2, details=[
412
                    'An image location is needed. Image location format:',
413
                    '  pithos://<user-id>/<container>/<path>',
414
                    ' where an image file at the above location must exist.'
415
                    ] + howto_image_file)
416
        try:
417
            return _validate_image_location(location)
418
        except AssertionError as ae:
419
            raiseCLIError(
420
                ae, 'Invalid image location format',
421
                importance=1, details=[
422
                    'Valid image location format:',
423
                    '  pithos://<user-id>/<container>/<img-file-path>'
424
                    ] + howto_image_file)
425

    
426
    def _mine_location(self, container_path):
427
        uuid = self['uuid'] or self._get_user_id()
428
        if self['container']:
429
            return uuid, self['container'], container_path
430
        container, sep, path = container_path.partition(':')
431
        if not (bool(container) and bool(path)):
432
            raiseCLIError(
433
                'Incorrect container-path format', importance=1, details=[
434
                'Use : to seperate container form path',
435
                '  <container>:<image-path>',
436
                'OR',
437
                'Use -C to specifiy a container',
438
                '  -C <container> <image-path>'] + howto_image_file)
439

    
440
        return uuid, container, path
441

    
442
    @errors.generic.all
443
    @errors.plankton.connection
444
    def _run(self, name, uuid, container, img_path):
445
        if self['local_image_path']:
446
            with open(self['local_image_path']) as f:
447
                pithos = self._get_pithos_client(container)
448
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
449
                if pbar:
450
                    hash_bar = pbar.clone()
451
                    hash_cb = hash_bar.get_generator('Calculating hashes')
452
                pithos.upload_object(
453
                    img_path, f,
454
                    hash_cb=hash_cb, upload_cb=upload_cb,
455
                    container_info_cache=self.container_info_cache)
456
                pbar.finish()
457

    
458
        location = 'pithos://%s/%s/%s' % (uuid, container, img_path)
459
        (params, properties, new_loc) = self._load_params_from_file(location)
460
        if location != new_loc:
461
            uuid, container, img_path = self._validate_location(new_loc)
462
        self._load_params_from_args(params, properties)
463
        pclient = self._get_pithos_client(container)
464

    
465
        #check if metafile exists
466
        meta_path = '%s.meta' % img_path
467
        if pclient and not self['metafile_force']:
468
            try:
469
                pclient.get_object_info(meta_path)
470
                raiseCLIError(
471
                    'Metadata file %s:%s already exists, abort' % (
472
                        container, meta_path),
473
                    details=['Registration ABORTED', 'Try -f to overwrite'])
474
            except ClientError as ce:
475
                if ce.status != 404:
476
                    raise
477

    
478
        #register the image
479
        try:
480
            r = self.client.register(name, location, params, properties)
481
        except ClientError as ce:
482
            if ce.status in (400, ):
483
                raiseCLIError(
484
                    ce, 'Nonexistent image file location %s' % location,
485
                    details=[
486
                        'Make sure the image file exists'] + howto_image_file)
487
            raise
488
        self._print(r, print_dict)
489

    
490
        #upload the metadata file
491
        if pclient:
492
            try:
493
                meta_headers = pclient.upload_from_string(
494
                    meta_path, dumps(r, indent=2),
495
                    container_info_cache=self.container_info_cache)
496
            except TypeError:
497
                print('Failed to dump metafile %s:%s' % (container, meta_path))
498
                return
499
            if self['json_output']:
500
                print_json(dict(
501
                    metafile_location='%s:%s' % (container, meta_path),
502
                    headers=meta_headers))
503
            else:
504
                print('Metadata file uploaded as %s:%s (version %s)' % (
505
                    container, meta_path, meta_headers['x-object-version']))
506

    
507
    def main(self, name, container___image_path):
508
        super(self.__class__, self)._run()
509
        self._run(name, *self._mine_location(container___image_path))
510

    
511

    
512
@command(image_cmds)
513
class image_unregister(_init_image, _optional_output_cmd):
514
    """Unregister an image (does not delete the image file)"""
515

    
516
    @errors.generic.all
517
    @errors.plankton.connection
518
    @errors.plankton.id
519
    def _run(self, image_id):
520
        self._optional_output(self.client.unregister(image_id))
521

    
522
    def main(self, image_id):
523
        super(self.__class__, self)._run()
524
        self._run(image_id=image_id)
525

    
526

    
527
@command(image_cmds)
528
class image_shared(_init_image, _optional_json):
529
    """List images shared by a member"""
530

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

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

    
540

    
541
@command(image_cmds)
542
class image_members(_init_image):
543
    """Manage members. Members of an image are users who can modify it"""
544

    
545

    
546
@command(image_cmds)
547
class image_members_list(_init_image, _optional_json):
548
    """List members of an image"""
549

    
550
    @errors.generic.all
551
    @errors.plankton.connection
552
    @errors.plankton.id
553
    def _run(self, image_id):
554
        self._print(self.client.list_members(image_id), title=('member_id',))
555

    
556
    def main(self, image_id):
557
        super(self.__class__, self)._run()
558
        self._run(image_id=image_id)
559

    
560

    
561
@command(image_cmds)
562
class image_members_add(_init_image, _optional_output_cmd):
563
    """Add a member to an image"""
564

    
565
    @errors.generic.all
566
    @errors.plankton.connection
567
    @errors.plankton.id
568
    def _run(self, image_id=None, member=None):
569
            self._optional_output(self.client.add_member(image_id, member))
570

    
571
    def main(self, image_id, member):
572
        super(self.__class__, self)._run()
573
        self._run(image_id=image_id, member=member)
574

    
575

    
576
@command(image_cmds)
577
class image_members_delete(_init_image, _optional_output_cmd):
578
    """Remove a member from an image"""
579

    
580
    @errors.generic.all
581
    @errors.plankton.connection
582
    @errors.plankton.id
583
    def _run(self, image_id=None, member=None):
584
            self._optional_output(self.client.remove_member(image_id, member))
585

    
586
    def main(self, image_id, member):
587
        super(self.__class__, self)._run()
588
        self._run(image_id=image_id, member=member)
589

    
590

    
591
@command(image_cmds)
592
class image_members_set(_init_image, _optional_output_cmd):
593
    """Set the members of an image"""
594

    
595
    @errors.generic.all
596
    @errors.plankton.connection
597
    @errors.plankton.id
598
    def _run(self, image_id, members):
599
            self._optional_output(self.client.set_members(image_id, members))
600

    
601
    def main(self, image_id, *members):
602
        super(self.__class__, self)._run()
603
        self._run(image_id=image_id, members=members)
604

    
605

    
606
# Compute Image Commands
607

    
608

    
609
@command(image_cmds)
610
class image_compute(_init_cyclades):
611
    """Cyclades/Compute API image commands"""
612

    
613

    
614
@command(image_cmds)
615
class image_compute_list(_init_cyclades, _optional_json):
616
    """List images"""
617

    
618
    arguments = dict(
619
        detail=FlagArgument('show detailed output', ('-l', '--details')),
620
        limit=IntArgument('limit number listed images', ('-n', '--number')),
621
        more=FlagArgument(
622
            'output results in pages (-n to set items per page, default 10)',
623
            '--more'),
624
        enum=FlagArgument('Enumerate results', '--enumerate')
625
    )
626

    
627
    @errors.generic.all
628
    @errors.cyclades.connection
629
    def _run(self):
630
        images = self.client.list_images(self['detail'])
631
        kwargs = dict(with_enumeration=self['enum'])
632
        if self['more']:
633
            kwargs['page_size'] = self['limit'] or 10
634
        elif self['limit']:
635
            images = images[:self['limit']]
636
        self._print(images, **kwargs)
637

    
638
    def main(self):
639
        super(self.__class__, self)._run()
640
        self._run()
641

    
642

    
643
@command(image_cmds)
644
class image_compute_info(_init_cyclades, _optional_json):
645
    """Get detailed information on an image"""
646

    
647
    @errors.generic.all
648
    @errors.cyclades.connection
649
    @errors.plankton.id
650
    def _run(self, image_id):
651
        image = self.client.get_image_details(image_id)
652
        self._print(image, print_dict)
653

    
654
    def main(self, image_id):
655
        super(self.__class__, self)._run()
656
        self._run(image_id=image_id)
657

    
658

    
659
@command(image_cmds)
660
class image_compute_delete(_init_cyclades, _optional_output_cmd):
661
    """Delete an image (WARNING: image file is also removed)"""
662

    
663
    @errors.generic.all
664
    @errors.cyclades.connection
665
    @errors.plankton.id
666
    def _run(self, image_id):
667
        self._optional_output(self.client.delete_image(image_id))
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(_init_cyclades):
676
    """Manage properties related to OS installation in an image"""
677

    
678

    
679
@command(image_cmds)
680
class image_compute_properties_list(_init_cyclades, _optional_json):
681
    """List all image properties"""
682

    
683
    @errors.generic.all
684
    @errors.cyclades.connection
685
    @errors.plankton.id
686
    def _run(self, image_id):
687
        self._print(self.client.get_image_metadata(image_id), print_dict)
688

    
689
    def main(self, image_id):
690
        super(self.__class__, self)._run()
691
        self._run(image_id=image_id)
692

    
693

    
694
@command(image_cmds)
695
class image_compute_properties_get(_init_cyclades, _optional_json):
696
    """Get an image property"""
697

    
698
    @errors.generic.all
699
    @errors.cyclades.connection
700
    @errors.plankton.id
701
    @errors.plankton.metadata
702
    def _run(self, image_id, key):
703
        self._print(self.client.get_image_metadata(image_id, key), print_dict)
704

    
705
    def main(self, image_id, key):
706
        super(self.__class__, self)._run()
707
        self._run(image_id=image_id, key=key)
708

    
709

    
710
@command(image_cmds)
711
class image_compute_properties_add(_init_cyclades, _optional_json):
712
    """Add a property to an image"""
713

    
714
    @errors.generic.all
715
    @errors.cyclades.connection
716
    @errors.plankton.id
717
    @errors.plankton.metadata
718
    def _run(self, image_id, key, val):
719
        self._print(
720
            self.client.create_image_metadata(image_id, key, val), print_dict)
721

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

    
726

    
727
@command(image_cmds)
728
class image_compute_properties_set(_init_cyclades, _optional_json):
729
    """Add / update a set of properties for an image
730
    proeprties must be given in the form key=value, e.v.
731
    /image compute properties set <image-id> key1=val1 key2=val2
732
    """
733

    
734
    @errors.generic.all
735
    @errors.cyclades.connection
736
    @errors.plankton.id
737
    def _run(self, image_id, keyvals):
738
        meta = dict()
739
        for keyval in keyvals:
740
            key, val = keyval.split('=')
741
            meta[key] = val
742
        self._print(
743
            self.client.update_image_metadata(image_id, **meta), print_dict)
744

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

    
749

    
750
@command(image_cmds)
751
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
752
    """Delete a property from an image"""
753

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

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