Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 95641ecc

History | View | Annotate | Download (27.2 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
        owner_name=ValueArgument('filter by owners username', '--owner-name'),
203
        order=ValueArgument(
204
            'order by FIELD ( - to reverse order)',
205
            '--order',
206
            default=''),
207
        limit=IntArgument('limit number of listed images', ('-n', '--number')),
208
        more=FlagArgument(
209
            'output results in pages (-n to set items per page, default 10)',
210
            '--more'),
211
        enum=FlagArgument('Enumerate results', '--enumerate'),
212
        prop=KeyValueArgument('filter by property key=value', ('--property'))
213
    )
214

    
215
    def _filtered_by_owner(self, detail, *list_params):
216
        images = []
217
        ouuid = self['owner'] or self._username2uuid(self['owner_name'])
218
        if not ouuid:
219
            return images
220
        for img in self.client.list_public(True, *list_params):
221
            if img['owner'] == ouuid:
222
                if not detail:
223
                    for key in set(img.keys()).difference(self.PERMANENTS):
224
                        img.pop(key)
225
                images.append(img)
226
        return images
227

    
228
    def _filtered_by_name(self, images):
229
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
230

    
231
        def augment_owner(img):
232
            uuid = img.get('owner', None)
233
            if uuid and not self['json_output']:
234
                img['owner'] = '%s (%s)' % (uuid, self._uuid2username(uuid))
235
            return img
236

    
237
        return [augment_owner(img) for img in images if (
238
            (not np) or img['name'].lower().startswith(np.lower())) and (
239
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
240
            (not nl) or nl.lower() in img['name'].lower())]
241

    
242
    def _filtered_by_properties(self, images):
243
        new_images = []
244
        for img in images:
245
            if set(self['prop'].items()).difference(img['properties'].items()):
246
                continue
247
            if self['detail']:
248
                new_images.append(dict(img))
249
            else:
250
                new_images.append(dict())
251
                for k in set(img).intersection(self.PERMANENTS):
252
                    new_images[-1][k] = img[k]
253
        return new_images
254

    
255
    @errors.generic.all
256
    @errors.cyclades.connection
257
    def _run(self):
258
        super(self.__class__, self)._run()
259
        filters = {}
260
        for arg in set([
261
                'container_format',
262
                'disk_format',
263
                'name',
264
                'size_min',
265
                'size_max',
266
                'status']).intersection(self.arguments):
267
            filters[arg] = self[arg]
268

    
269
        order = self['order']
270
        detail = self['detail'] or self['prop']
271
        if self['owner'] or self['owner_name']:
272
            images = self._filtered_by_owner(detail, filters, order)
273
        else:
274
            images = self.client.list_public(detail, filters, order)
275

    
276
        images = self._filtered_by_name(images)
277
        if self['prop']:
278
            images = self._filtered_by_properties(images)
279
        kwargs = dict(with_enumeration=self['enum'])
280
        if self['more']:
281
            kwargs['page_size'] = self['limit'] or 10
282
        elif self['limit']:
283
            images = images[:self['limit']]
284
        self._print(images, **kwargs)
285

    
286
    def main(self):
287
        super(self.__class__, self)._run()
288
        self._run()
289

    
290

    
291
@command(image_cmds)
292
class image_meta(_init_image, _optional_json):
293
    """Get image metadata
294
    Image metadata include:
295
    - image file information (location, size, etc.)
296
    - image information (id, name, etc.)
297
    - image os properties (os, fs, etc.)
298
    """
299

    
300
    @errors.generic.all
301
    @errors.plankton.connection
302
    @errors.plankton.id
303
    def _run(self, image_id):
304
        self._print([self.client.get_meta(image_id)])
305

    
306
    def main(self, image_id):
307
        super(self.__class__, self)._run()
308
        self._run(image_id=image_id)
309

    
310

    
311
@command(image_cmds)
312
class image_register(_init_image, _optional_json):
313
    """(Re)Register an image"""
314

    
315
    container_info_cache = {}
316

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

    
351
    def _get_user_id(self):
352
        atoken = self.client.token
353
        if getattr(self, 'auth_base', False):
354
            return self.auth_base.term('id', atoken)
355
        else:
356
            astakos_url = self.config.get('user', 'url')\
357
                or self.config.get('astakos', 'url')
358
            if not astakos_url:
359
                raise CLIBaseUrlError(service='astakos')
360
            user = AstakosClient(astakos_url, atoken)
361
            return user.term('id')
362

    
363
    def _get_pithos_client(self, container):
364
        if self['no_metafile_upload']:
365
            return None
366
        ptoken = self.client.token
367
        if getattr(self, 'auth_base', False):
368
            pithos_endpoints = self.auth_base.get_service_endpoints(
369
                'object-store')
370
            purl = pithos_endpoints['publicURL']
371
        else:
372
            purl = self.config.get_cloud('pithos', 'url')
373
        if not purl:
374
            raise CLIBaseUrlError(service='pithos')
375
        return PithosClient(purl, ptoken, self._get_user_id(), container)
376

    
377
    def _store_remote_metafile(self, pclient, remote_path, metadata):
378
        return pclient.upload_from_string(
379
            remote_path, _validate_image_meta(metadata, return_str=True),
380
            container_info_cache=self.container_info_cache)
381

    
382
    def _load_params_from_file(self, location):
383
        params, properties = dict(), dict()
384
        pfile = self['metafile']
385
        if pfile:
386
            try:
387
                for k, v in _load_image_meta(pfile).items():
388
                    key = k.lower().replace('-', '_')
389
                    if k == 'properties':
390
                        for pk, pv in v.items():
391
                            properties[pk.upper().replace('-', '_')] = pv
392
                    elif key == 'name':
393
                            continue
394
                    elif key == 'location':
395
                        if location:
396
                            continue
397
                        location = v
398
                    else:
399
                        params[key] = v
400
            except Exception as e:
401
                raiseCLIError(e, 'Invalid json metadata config file')
402
        return params, properties, location
403

    
404
    def _load_params_from_args(self, params, properties):
405
        for key in set([
406
                'checksum',
407
                'container_format',
408
                'disk_format',
409
                'owner',
410
                'size',
411
                'is_public']).intersection(self.arguments):
412
            params[key] = self[key]
413
        for k, v in self['properties'].items():
414
            properties[k.upper().replace('-', '_')] = v
415

    
416
    def _validate_location(self, location):
417
        if not location:
418
            raiseCLIError(
419
                'No image file location provided',
420
                importance=2, details=[
421
                    'An image location is needed. Image location format:',
422
                    '  pithos://<user-id>/<container>/<path>',
423
                    ' where an image file at the above location must exist.'
424
                    ] + howto_image_file)
425
        try:
426
            return _validate_image_location(location)
427
        except AssertionError as ae:
428
            raiseCLIError(
429
                ae, 'Invalid image location format',
430
                importance=1, details=[
431
                    'Valid image location format:',
432
                    '  pithos://<user-id>/<container>/<img-file-path>'
433
                    ] + howto_image_file)
434

    
435
    def _mine_location(self, container_path):
436
        uuid = self['uuid'] or self._get_user_id()
437
        if self['container']:
438
            return uuid, self['container'], container_path
439
        container, sep, path = container_path.partition(':')
440
        if not (bool(container) and bool(path)):
441
            raiseCLIError(
442
                'Incorrect container-path format', importance=1, details=[
443
                'Use : to seperate container form path',
444
                '  <container>:<image-path>',
445
                'OR',
446
                'Use -C to specifiy a container',
447
                '  -C <container> <image-path>'] + howto_image_file)
448

    
449
        return uuid, container, path
450

    
451
    @errors.generic.all
452
    @errors.plankton.connection
453
    def _run(self, name, uuid, container, img_path):
454
        if self['local_image_path']:
455
            with open(self['local_image_path']) as f:
456
                pithos = self._get_pithos_client(container)
457
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
458
                if pbar:
459
                    hash_bar = pbar.clone()
460
                    hash_cb = hash_bar.get_generator('Calculating hashes')
461
                pithos.upload_object(
462
                    img_path, f,
463
                    hash_cb=hash_cb, upload_cb=upload_cb,
464
                    container_info_cache=self.container_info_cache)
465
                pbar.finish()
466

    
467
        location = 'pithos://%s/%s/%s' % (uuid, container, img_path)
468
        (params, properties, new_loc) = self._load_params_from_file(location)
469
        if location != new_loc:
470
            uuid, container, img_path = self._validate_location(new_loc)
471
        self._load_params_from_args(params, properties)
472
        pclient = self._get_pithos_client(container)
473

    
474
        #check if metafile exists
475
        meta_path = '%s.meta' % img_path
476
        if pclient and not self['metafile_force']:
477
            try:
478
                pclient.get_object_info(meta_path)
479
                raiseCLIError(
480
                    'Metadata file %s:%s already exists, abort' % (
481
                        container, meta_path),
482
                    details=['Registration ABORTED', 'Try -f to overwrite'])
483
            except ClientError as ce:
484
                if ce.status != 404:
485
                    raise
486

    
487
        #register the image
488
        try:
489
            r = self.client.register(name, location, params, properties)
490
        except ClientError as ce:
491
            if ce.status in (400, ):
492
                raiseCLIError(
493
                    ce, 'Nonexistent image file location %s' % location,
494
                    details=[
495
                        'Make sure the image file exists'] + howto_image_file)
496
            raise
497
        self._print(r, print_dict)
498

    
499
        #upload the metadata file
500
        if pclient:
501
            try:
502
                meta_headers = pclient.upload_from_string(
503
                    meta_path, dumps(r, indent=2),
504
                    container_info_cache=self.container_info_cache)
505
            except TypeError:
506
                print('Failed to dump metafile %s:%s' % (container, meta_path))
507
                return
508
            if self['json_output']:
509
                print_json(dict(
510
                    metafile_location='%s:%s' % (container, meta_path),
511
                    headers=meta_headers))
512
            else:
513
                print('Metadata file uploaded as %s:%s (version %s)' % (
514
                    container, meta_path, meta_headers['x-object-version']))
515

    
516
    def main(self, name, container___image_path):
517
        super(self.__class__, self)._run()
518
        self._run(name, *self._mine_location(container___image_path))
519

    
520

    
521
@command(image_cmds)
522
class image_unregister(_init_image, _optional_output_cmd):
523
    """Unregister an image (does not delete the image file)"""
524

    
525
    @errors.generic.all
526
    @errors.plankton.connection
527
    @errors.plankton.id
528
    def _run(self, image_id):
529
        self._optional_output(self.client.unregister(image_id))
530

    
531
    def main(self, image_id):
532
        super(self.__class__, self)._run()
533
        self._run(image_id=image_id)
534

    
535

    
536
@command(image_cmds)
537
class image_shared(_init_image, _optional_json):
538
    """List images shared by a member"""
539

    
540
    @errors.generic.all
541
    @errors.plankton.connection
542
    def _run(self, member):
543
        self._print(self.client.list_shared(member), title=('image_id',))
544

    
545
    def main(self, member):
546
        super(self.__class__, self)._run()
547
        self._run(member)
548

    
549

    
550
@command(image_cmds)
551
class image_members(_init_image):
552
    """Manage members. Members of an image are users who can modify it"""
553

    
554

    
555
@command(image_cmds)
556
class image_members_list(_init_image, _optional_json):
557
    """List members of an image"""
558

    
559
    @errors.generic.all
560
    @errors.plankton.connection
561
    @errors.plankton.id
562
    def _run(self, image_id):
563
        self._print(self.client.list_members(image_id), title=('member_id',))
564

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

    
569

    
570
@command(image_cmds)
571
class image_members_add(_init_image, _optional_output_cmd):
572
    """Add a member to an image"""
573

    
574
    @errors.generic.all
575
    @errors.plankton.connection
576
    @errors.plankton.id
577
    def _run(self, image_id=None, member=None):
578
            self._optional_output(self.client.add_member(image_id, member))
579

    
580
    def main(self, image_id, member):
581
        super(self.__class__, self)._run()
582
        self._run(image_id=image_id, member=member)
583

    
584

    
585
@command(image_cmds)
586
class image_members_delete(_init_image, _optional_output_cmd):
587
    """Remove a member from an image"""
588

    
589
    @errors.generic.all
590
    @errors.plankton.connection
591
    @errors.plankton.id
592
    def _run(self, image_id=None, member=None):
593
            self._optional_output(self.client.remove_member(image_id, member))
594

    
595
    def main(self, image_id, member):
596
        super(self.__class__, self)._run()
597
        self._run(image_id=image_id, member=member)
598

    
599

    
600
@command(image_cmds)
601
class image_members_set(_init_image, _optional_output_cmd):
602
    """Set the members of an image"""
603

    
604
    @errors.generic.all
605
    @errors.plankton.connection
606
    @errors.plankton.id
607
    def _run(self, image_id, members):
608
            self._optional_output(self.client.set_members(image_id, members))
609

    
610
    def main(self, image_id, *members):
611
        super(self.__class__, self)._run()
612
        self._run(image_id=image_id, members=members)
613

    
614

    
615
# Compute Image Commands
616

    
617

    
618
@command(image_cmds)
619
class image_compute(_init_cyclades):
620
    """Cyclades/Compute API image commands"""
621

    
622

    
623
@command(image_cmds)
624
class image_compute_list(_init_cyclades, _optional_json):
625
    """List images"""
626

    
627
    arguments = dict(
628
        detail=FlagArgument('show detailed output', ('-l', '--details')),
629
        limit=IntArgument('limit number listed images', ('-n', '--number')),
630
        more=FlagArgument(
631
            'output results in pages (-n to set items per page, default 10)',
632
            '--more'),
633
        enum=FlagArgument('Enumerate results', '--enumerate')
634
    )
635

    
636
    @errors.generic.all
637
    @errors.cyclades.connection
638
    def _run(self):
639
        images = self.client.list_images(self['detail'])
640
        kwargs = dict(with_enumeration=self['enum'])
641
        if self['more']:
642
            kwargs['page_size'] = self['limit'] or 10
643
        elif self['limit']:
644
            images = images[:self['limit']]
645
        self._print(images, **kwargs)
646

    
647
    def main(self):
648
        super(self.__class__, self)._run()
649
        self._run()
650

    
651

    
652
@command(image_cmds)
653
class image_compute_info(_init_cyclades, _optional_json):
654
    """Get detailed information on an image"""
655

    
656
    @errors.generic.all
657
    @errors.cyclades.connection
658
    @errors.plankton.id
659
    def _run(self, image_id):
660
        image = self.client.get_image_details(image_id)
661
        self._print(image, print_dict)
662

    
663
    def main(self, image_id):
664
        super(self.__class__, self)._run()
665
        self._run(image_id=image_id)
666

    
667

    
668
@command(image_cmds)
669
class image_compute_delete(_init_cyclades, _optional_output_cmd):
670
    """Delete an image (WARNING: image file is also removed)"""
671

    
672
    @errors.generic.all
673
    @errors.cyclades.connection
674
    @errors.plankton.id
675
    def _run(self, image_id):
676
        self._optional_output(self.client.delete_image(image_id))
677

    
678
    def main(self, image_id):
679
        super(self.__class__, self)._run()
680
        self._run(image_id=image_id)
681

    
682

    
683
@command(image_cmds)
684
class image_compute_properties(_init_cyclades):
685
    """Manage properties related to OS installation in an image"""
686

    
687

    
688
@command(image_cmds)
689
class image_compute_properties_list(_init_cyclades, _optional_json):
690
    """List all image properties"""
691

    
692
    @errors.generic.all
693
    @errors.cyclades.connection
694
    @errors.plankton.id
695
    def _run(self, image_id):
696
        self._print(self.client.get_image_metadata(image_id), print_dict)
697

    
698
    def main(self, image_id):
699
        super(self.__class__, self)._run()
700
        self._run(image_id=image_id)
701

    
702

    
703
@command(image_cmds)
704
class image_compute_properties_get(_init_cyclades, _optional_json):
705
    """Get an image property"""
706

    
707
    @errors.generic.all
708
    @errors.cyclades.connection
709
    @errors.plankton.id
710
    @errors.plankton.metadata
711
    def _run(self, image_id, key):
712
        self._print(self.client.get_image_metadata(image_id, key), print_dict)
713

    
714
    def main(self, image_id, key):
715
        super(self.__class__, self)._run()
716
        self._run(image_id=image_id, key=key)
717

    
718

    
719
@command(image_cmds)
720
class image_compute_properties_add(_init_cyclades, _optional_json):
721
    """Add a property to an image"""
722

    
723
    @errors.generic.all
724
    @errors.cyclades.connection
725
    @errors.plankton.id
726
    @errors.plankton.metadata
727
    def _run(self, image_id, key, val):
728
        self._print(
729
            self.client.create_image_metadata(image_id, key, val), print_dict)
730

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

    
735

    
736
@command(image_cmds)
737
class image_compute_properties_set(_init_cyclades, _optional_json):
738
    """Add / update a set of properties for an image
739
    proeprties must be given in the form key=value, e.v.
740
    /image compute properties set <image-id> key1=val1 key2=val2
741
    """
742

    
743
    @errors.generic.all
744
    @errors.cyclades.connection
745
    @errors.plankton.id
746
    def _run(self, image_id, keyvals):
747
        meta = dict()
748
        for keyval in keyvals:
749
            key, val = keyval.split('=')
750
            meta[key] = val
751
        self._print(
752
            self.client.update_image_metadata(image_id, **meta), print_dict)
753

    
754
    def main(self, image_id, *key_equals_value):
755
        super(self.__class__, self)._run()
756
        self._run(image_id=image_id, keyvals=key_equals_value)
757

    
758

    
759
@command(image_cmds)
760
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
761
    """Delete a property from an image"""
762

    
763
    @errors.generic.all
764
    @errors.cyclades.connection
765
    @errors.plankton.id
766
    @errors.plankton.metadata
767
    def _run(self, image_id, key):
768
        self._optional_output(self.client.delete_image_metadata(image_id, key))
769

    
770
    def main(self, image_id, key):
771
        super(self.__class__, self)._run()
772
        self._run(image_id=image_id, key=key)