Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (27.4 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
        uuids = {}
231

    
232
        def fish_uuids(img):
233
            if self['detail'] and not self['json_output']:
234
                uuids[img['owner']] = ''
235
            return img
236

    
237
        r = [fish_uuids(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
        if self['detail'] and not self['json_output']:
243
            uuids = self._uuids2usernames(uuids.keys())
244
            for img in r:
245
                img['owner'] += ' (%s)' % uuids[img['owner']]
246

    
247
        return r
248

    
249
    def _filtered_by_properties(self, images):
250
        new_images = []
251
        for img in images:
252
            if set(self['prop'].items()).difference(img['properties'].items()):
253
                continue
254
            if self['detail']:
255
                new_images.append(dict(img))
256
            else:
257
                new_images.append(dict())
258
                for k in set(img).intersection(self.PERMANENTS):
259
                    new_images[-1][k] = img[k]
260
        return new_images
261

    
262
    @errors.generic.all
263
    @errors.cyclades.connection
264
    def _run(self):
265
        super(self.__class__, self)._run()
266
        filters = {}
267
        for arg in set([
268
                'container_format',
269
                'disk_format',
270
                'name',
271
                'size_min',
272
                'size_max',
273
                'status']).intersection(self.arguments):
274
            filters[arg] = self[arg]
275

    
276
        order = self['order']
277
        detail = self['detail'] or self['prop']
278
        if self['owner'] or self['owner_name']:
279
            images = self._filtered_by_owner(detail, filters, order)
280
        else:
281
            images = self.client.list_public(detail, filters, order)
282

    
283
        images = self._filtered_by_name(images)
284
        if self['prop']:
285
            images = self._filtered_by_properties(images)
286
        kwargs = dict(with_enumeration=self['enum'])
287
        if self['more']:
288
            kwargs['page_size'] = self['limit'] or 10
289
        elif self['limit']:
290
            images = images[:self['limit']]
291
        self._print(images, **kwargs)
292

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

    
297

    
298
@command(image_cmds)
299
class image_meta(_init_image, _optional_json):
300
    """Get image metadata
301
    Image metadata include:
302
    - image file information (location, size, etc.)
303
    - image information (id, name, etc.)
304
    - image os properties (os, fs, etc.)
305
    """
306

    
307
    @errors.generic.all
308
    @errors.plankton.connection
309
    @errors.plankton.id
310
    def _run(self, image_id):
311
        self._print([self.client.get_meta(image_id)])
312

    
313
    def main(self, image_id):
314
        super(self.__class__, self)._run()
315
        self._run(image_id=image_id)
316

    
317

    
318
@command(image_cmds)
319
class image_register(_init_image, _optional_json):
320
    """(Re)Register an image"""
321

    
322
    container_info_cache = {}
323

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

    
358
    def _get_user_id(self):
359
        atoken = self.client.token
360
        if getattr(self, 'auth_base', False):
361
            return self.auth_base.term('id', atoken)
362
        else:
363
            astakos_url = self.config.get('user', 'url')\
364
                or self.config.get('astakos', 'url')
365
            if not astakos_url:
366
                raise CLIBaseUrlError(service='astakos')
367
            user = AstakosClient(astakos_url, atoken)
368
            return user.term('id')
369

    
370
    def _get_pithos_client(self, container):
371
        if self['no_metafile_upload']:
372
            return None
373
        ptoken = self.client.token
374
        if getattr(self, 'auth_base', False):
375
            pithos_endpoints = self.auth_base.get_service_endpoints(
376
                'object-store')
377
            purl = pithos_endpoints['publicURL']
378
        else:
379
            purl = self.config.get_cloud('pithos', 'url')
380
        if not purl:
381
            raise CLIBaseUrlError(service='pithos')
382
        return PithosClient(purl, ptoken, self._get_user_id(), container)
383

    
384
    def _store_remote_metafile(self, pclient, remote_path, metadata):
385
        return pclient.upload_from_string(
386
            remote_path, _validate_image_meta(metadata, return_str=True),
387
            container_info_cache=self.container_info_cache)
388

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

    
411
    def _load_params_from_args(self, params, properties):
412
        for key in set([
413
                'checksum',
414
                'container_format',
415
                'disk_format',
416
                'owner',
417
                'size',
418
                'is_public']).intersection(self.arguments):
419
            params[key] = self[key]
420
        for k, v in self['properties'].items():
421
            properties[k.upper().replace('-', '_')] = v
422

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

    
442
    def _mine_location(self, container_path):
443
        uuid = self['uuid'] or self._get_user_id()
444
        if self['container']:
445
            return uuid, self['container'], container_path
446
        container, sep, path = container_path.partition(':')
447
        if not (bool(container) and bool(path)):
448
            raiseCLIError(
449
                'Incorrect container-path format', importance=1, details=[
450
                'Use : to seperate container form path',
451
                '  <container>:<image-path>',
452
                'OR',
453
                'Use -C to specifiy a container',
454
                '  -C <container> <image-path>'] + howto_image_file)
455

    
456
        return uuid, container, path
457

    
458
    @errors.generic.all
459
    @errors.plankton.connection
460
    def _run(self, name, uuid, container, img_path):
461
        if self['local_image_path']:
462
            with open(self['local_image_path']) as f:
463
                pithos = self._get_pithos_client(container)
464
                (pbar, upload_cb) = self._safe_progress_bar('Uploading')
465
                if pbar:
466
                    hash_bar = pbar.clone()
467
                    hash_cb = hash_bar.get_generator('Calculating hashes')
468
                pithos.upload_object(
469
                    img_path, f,
470
                    hash_cb=hash_cb, upload_cb=upload_cb,
471
                    container_info_cache=self.container_info_cache)
472
                pbar.finish()
473

    
474
        location = 'pithos://%s/%s/%s' % (uuid, container, img_path)
475
        (params, properties, new_loc) = self._load_params_from_file(location)
476
        if location != new_loc:
477
            uuid, container, img_path = self._validate_location(new_loc)
478
        self._load_params_from_args(params, properties)
479
        pclient = self._get_pithos_client(container)
480

    
481
        #check if metafile exists
482
        meta_path = '%s.meta' % img_path
483
        if pclient and not self['metafile_force']:
484
            try:
485
                pclient.get_object_info(meta_path)
486
                raiseCLIError(
487
                    'Metadata file %s:%s already exists, abort' % (
488
                        container, meta_path),
489
                    details=['Registration ABORTED', 'Try -f to overwrite'])
490
            except ClientError as ce:
491
                if ce.status != 404:
492
                    raise
493

    
494
        #register the image
495
        try:
496
            r = self.client.register(name, location, params, properties)
497
        except ClientError as ce:
498
            if ce.status in (400, ):
499
                raiseCLIError(
500
                    ce, 'Nonexistent image file location %s' % location,
501
                    details=[
502
                        'Make sure the image file exists'] + howto_image_file)
503
            raise
504
        self._print(r, print_dict)
505

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

    
523
    def main(self, name, container___image_path):
524
        super(self.__class__, self)._run()
525
        self._run(name, *self._mine_location(container___image_path))
526

    
527

    
528
@command(image_cmds)
529
class image_unregister(_init_image, _optional_output_cmd):
530
    """Unregister an image (does not delete the image file)"""
531

    
532
    @errors.generic.all
533
    @errors.plankton.connection
534
    @errors.plankton.id
535
    def _run(self, image_id):
536
        self._optional_output(self.client.unregister(image_id))
537

    
538
    def main(self, image_id):
539
        super(self.__class__, self)._run()
540
        self._run(image_id=image_id)
541

    
542

    
543
@command(image_cmds)
544
class image_shared(_init_image, _optional_json):
545
    """List images shared by a member"""
546

    
547
    @errors.generic.all
548
    @errors.plankton.connection
549
    def _run(self, member):
550
        self._print(self.client.list_shared(member), title=('image_id',))
551

    
552
    def main(self, member):
553
        super(self.__class__, self)._run()
554
        self._run(member)
555

    
556

    
557
@command(image_cmds)
558
class image_members(_init_image):
559
    """Manage members. Members of an image are users who can modify it"""
560

    
561

    
562
@command(image_cmds)
563
class image_members_list(_init_image, _optional_json):
564
    """List members of an image"""
565

    
566
    @errors.generic.all
567
    @errors.plankton.connection
568
    @errors.plankton.id
569
    def _run(self, image_id):
570
        self._print(self.client.list_members(image_id), title=('member_id',))
571

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

    
576

    
577
@command(image_cmds)
578
class image_members_add(_init_image, _optional_output_cmd):
579
    """Add a member to an image"""
580

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

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

    
591

    
592
@command(image_cmds)
593
class image_members_delete(_init_image, _optional_output_cmd):
594
    """Remove a member from an image"""
595

    
596
    @errors.generic.all
597
    @errors.plankton.connection
598
    @errors.plankton.id
599
    def _run(self, image_id=None, member=None):
600
            self._optional_output(self.client.remove_member(image_id, member))
601

    
602
    def main(self, image_id, member):
603
        super(self.__class__, self)._run()
604
        self._run(image_id=image_id, member=member)
605

    
606

    
607
@command(image_cmds)
608
class image_members_set(_init_image, _optional_output_cmd):
609
    """Set the members of an image"""
610

    
611
    @errors.generic.all
612
    @errors.plankton.connection
613
    @errors.plankton.id
614
    def _run(self, image_id, members):
615
            self._optional_output(self.client.set_members(image_id, members))
616

    
617
    def main(self, image_id, *members):
618
        super(self.__class__, self)._run()
619
        self._run(image_id=image_id, members=members)
620

    
621

    
622
# Compute Image Commands
623

    
624

    
625
@command(image_cmds)
626
class image_compute(_init_cyclades):
627
    """Cyclades/Compute API image commands"""
628

    
629

    
630
@command(image_cmds)
631
class image_compute_list(_init_cyclades, _optional_json):
632
    """List images"""
633

    
634
    arguments = dict(
635
        detail=FlagArgument('show detailed output', ('-l', '--details')),
636
        limit=IntArgument('limit number listed images', ('-n', '--number')),
637
        more=FlagArgument(
638
            'output results in pages (-n to set items per page, default 10)',
639
            '--more'),
640
        enum=FlagArgument('Enumerate results', '--enumerate')
641
    )
642

    
643
    @errors.generic.all
644
    @errors.cyclades.connection
645
    def _run(self):
646
        images = self.client.list_images(self['detail'])
647
        kwargs = dict(with_enumeration=self['enum'])
648
        if self['more']:
649
            kwargs['page_size'] = self['limit'] or 10
650
        elif self['limit']:
651
            images = images[:self['limit']]
652
        self._print(images, **kwargs)
653

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

    
658

    
659
@command(image_cmds)
660
class image_compute_info(_init_cyclades, _optional_json):
661
    """Get detailed information on an image"""
662

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

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

    
674

    
675
@command(image_cmds)
676
class image_compute_delete(_init_cyclades, _optional_output_cmd):
677
    """Delete an image (WARNING: image file is also removed)"""
678

    
679
    @errors.generic.all
680
    @errors.cyclades.connection
681
    @errors.plankton.id
682
    def _run(self, image_id):
683
        self._optional_output(self.client.delete_image(image_id))
684

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

    
689

    
690
@command(image_cmds)
691
class image_compute_properties(_init_cyclades):
692
    """Manage properties related to OS installation in an image"""
693

    
694

    
695
@command(image_cmds)
696
class image_compute_properties_list(_init_cyclades, _optional_json):
697
    """List all image properties"""
698

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

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

    
709

    
710
@command(image_cmds)
711
class image_compute_properties_get(_init_cyclades, _optional_json):
712
    """Get an image property"""
713

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

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

    
725

    
726
@command(image_cmds)
727
class image_compute_properties_add(_init_cyclades, _optional_json):
728
    """Add a property to an image"""
729

    
730
    @errors.generic.all
731
    @errors.cyclades.connection
732
    @errors.plankton.id
733
    @errors.plankton.metadata
734
    def _run(self, image_id, key, val):
735
        self._print(
736
            self.client.create_image_metadata(image_id, key, val), print_dict)
737

    
738
    def main(self, image_id, key, val):
739
        super(self.__class__, self)._run()
740
        self._run(image_id=image_id, key=key, val=val)
741

    
742

    
743
@command(image_cmds)
744
class image_compute_properties_set(_init_cyclades, _optional_json):
745
    """Add / update a set of properties for an image
746
    proeprties must be given in the form key=value, e.v.
747
    /image compute properties set <image-id> key1=val1 key2=val2
748
    """
749

    
750
    @errors.generic.all
751
    @errors.cyclades.connection
752
    @errors.plankton.id
753
    def _run(self, image_id, keyvals):
754
        meta = dict()
755
        for keyval in keyvals:
756
            key, val = keyval.split('=')
757
            meta[key] = val
758
        self._print(
759
            self.client.update_image_metadata(image_id, **meta), print_dict)
760

    
761
    def main(self, image_id, *key_equals_value):
762
        super(self.__class__, self)._run()
763
        self._run(image_id=image_id, keyvals=key_equals_value)
764

    
765

    
766
@command(image_cmds)
767
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
768
    """Delete a property from an image"""
769

    
770
    @errors.generic.all
771
    @errors.cyclades.connection
772
    @errors.plankton.id
773
    @errors.plankton.metadata
774
    def _run(self, image_id, key):
775
        self._optional_output(self.client.delete_image_metadata(image_id, key))
776

    
777
    def main(self, image_id, key):
778
        super(self.__class__, self)._run()
779
        self._run(image_id=image_id, key=key)