Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 9553da85

History | View | Annotate | Download (22.6 kB)

1
# Copyright 2012 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.path import abspath
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_items, 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
47
from kamaki.cli.commands.cyclades import _init_cyclades
48
from kamaki.cli.commands import _command_init, errors, _optional_output_cmd
49
from kamaki.cli.errors import raiseCLIError
50

    
51

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

    
58

    
59
about_image_id = [
60
    'To see a list of available image ids: /image list']
61

    
62

    
63
log = getLogger(__name__)
64

    
65

    
66
class _init_image(_command_init):
67
    @errors.generic.all
68
    def _run(self):
69
        token = self.config.get('image', 'token')\
70
            or self.config.get('compute', 'token')\
71
            or self.config.get('global', 'token')
72
        base_url = self.config.get('image', 'url')\
73
            or self.config.get('compute', 'url')\
74
            or self.config.get('global', 'url')
75
        self.client = ImageClient(base_url=base_url, token=token)
76
        self._set_log_params()
77
        self._update_max_threads()
78

    
79
    def main(self):
80
        self._run()
81

    
82

    
83
# Plankton Image Commands
84

    
85

    
86
def _validate_image_props(json_dict, return_str=False):
87
    """
88
    :param json_dict" (dict) json-formated, of the form
89
        {"key1": "val1", "key2": "val2", ...}
90

91
    :param return_str: (boolean) if true, return a json dump
92

93
    :returns: (dict)
94

95
    :raises TypeError, AttributeError: Invalid json format
96

97
    :raises AssertionError: Valid json but invalid image properties dict
98
    """
99
    json_str = dumps(json_dict, indent=2)
100
    for k, v in json_dict.items():
101
        dealbreaker = isinstance(v, dict) or isinstance(v, list)
102
        assert not dealbreaker, 'Invalid property value for key %s' % k
103
        dealbreaker = ' ' in k
104
        assert not dealbreaker, 'Invalid key [%s]' % k
105
        json_dict[k] = '%s' % v
106
    return json_str if return_str else json_dict
107

    
108

    
109
def _load_image_props(filepath):
110
    """
111
    :param filepath: (str) the (relative) path of the metafile
112

113
    :returns: (dict) json_formated
114

115
    :raises TypeError, AttributeError: Invalid json format
116

117
    :raises AssertionError: Valid json but invalid image properties dict
118
    """
119
    with open(abspath(filepath)) as f:
120
        meta_dict = load(f)
121
        try:
122
            return _validate_image_props(meta_dict)
123
        except AssertionError:
124
            log.debug('Failed to load properties from file %s' % filepath)
125
            raise
126

    
127

    
128
@command(image_cmds)
129
class image_list(_init_image):
130
    """List images accessible by user"""
131

    
132
    arguments = dict(
133
        detail=FlagArgument('show detailed output', ('-l', '--details')),
134
        container_format=ValueArgument(
135
            'filter by container format',
136
            '--container-format'),
137
        disk_format=ValueArgument('filter by disk format', '--disk-format'),
138
        name=ValueArgument('filter by name', '--name'),
139
        name_pref=ValueArgument(
140
            'filter by name prefix (case insensitive)',
141
            '--name-prefix'),
142
        name_suff=ValueArgument(
143
            'filter by name suffix (case insensitive)',
144
            '--name-suffix'),
145
        name_like=ValueArgument(
146
            'print only if name contains this (case insensitive)',
147
            '--name-like'),
148
        size_min=IntArgument('filter by minimum size', '--size-min'),
149
        size_max=IntArgument('filter by maximum size', '--size-max'),
150
        status=ValueArgument('filter by status', '--status'),
151
        owner=ValueArgument('filter by owner', '--owner'),
152
        order=ValueArgument(
153
            'order by FIELD ( - to reverse order)',
154
            '--order',
155
            default=''),
156
        limit=IntArgument('limit number of listed images', ('-n', '--number')),
157
        more=FlagArgument(
158
            'output results in pages (-n to set items per page, default 10)',
159
            '--more'),
160
        enum=FlagArgument('Enumerate results', '--enumerate'),
161
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
162
    )
163

    
164
    def _filtered_by_owner(self, detail, *list_params):
165
        images = []
166
        MINKEYS = set([
167
            'id', 'size', 'status', 'disk_format', 'container_format', 'name'])
168
        for img in self.client.list_public(True, *list_params):
169
            if img['owner'] == self['owner']:
170
                if not detail:
171
                    for key in set(img.keys()).difference(MINKEYS):
172
                        img.pop(key)
173
                images.append(img)
174
        return images
175

    
176
    def _filtered_by_name(self, images):
177
        np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
178
        return [img for img in images if (
179
            (not np) or img['name'].lower().startswith(np.lower())) and (
180
            (not ns) or img['name'].lower().endswith(ns.lower())) and (
181
            (not nl) or nl.lower() in img['name'].lower())]
182

    
183
    @errors.generic.all
184
    @errors.cyclades.connection
185
    def _run(self):
186
        super(self.__class__, self)._run()
187
        filters = {}
188
        for arg in set([
189
                'container_format',
190
                'disk_format',
191
                'name',
192
                'size_min',
193
                'size_max',
194
                'status']).intersection(self.arguments):
195
            filters[arg] = self[arg]
196

    
197
        order = self['order']
198
        detail = self['detail']
199
        if self['owner']:
200
            images = self._filtered_by_owner(detail, filters, order)
201
        else:
202
            images = self.client.list_public(detail, filters, order)
203

    
204
        if self['json_output']:
205
            print_json(images)
206
            return
207
        images = self._filtered_by_name(images)
208
        if self['more']:
209
            print_items(
210
                images,
211
                with_enumeration=self['enum'], page_size=self['limit'] or 10)
212
        elif self['limit']:
213
            print_items(images[:self['limit']], with_enumeration=self['enum'])
214
        else:
215
            print_items(images, with_enumeration=self['enum'])
216

    
217
    def main(self):
218
        super(self.__class__, self)._run()
219
        self._run()
220

    
221

    
222
@command(image_cmds)
223
class image_meta(_init_image):
224
    """Get image metadata
225
    Image metadata include:
226
    - image file information (location, size, etc.)
227
    - image information (id, name, etc.)
228
    - image os properties (os, fs, etc.)
229
    """
230

    
231
    arguments = dict(
232
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
233
    )
234

    
235
    @errors.generic.all
236
    @errors.plankton.connection
237
    @errors.plankton.id
238
    def _run(self, image_id):
239
        printer = print_json if self['json_output'] else print_dict
240
        printer(self.client.get_meta(image_id))
241

    
242
    def main(self, image_id):
243
        super(self.__class__, self)._run()
244
        self._run(image_id=image_id)
245

    
246

    
247
@command(image_cmds)
248
class image_register(_init_image):
249
    """(Re)Register an image"""
250

    
251
    arguments = dict(
252
        checksum=ValueArgument('set image checksum', '--checksum'),
253
        container_format=ValueArgument(
254
            'set container format',
255
            '--container-format'),
256
        disk_format=ValueArgument('set disk format', '--disk-format'),
257
        #id=ValueArgument('set image ID', '--id'),
258
        owner=ValueArgument('set image owner (admin only)', '--owner'),
259
        properties=KeyValueArgument(
260
            'add property in key=value form (can be repeated)',
261
            ('-p', '--property')),
262
        is_public=FlagArgument('mark image as public', '--public'),
263
        size=IntArgument('set image size', '--size'),
264
        #update=FlagArgument(
265
        #    'update existing image properties',
266
        #    ('-u', '--update')),
267
        json_output=FlagArgument('Show results in json', ('-j', '--json')),
268
        property_file=ValueArgument(
269
            'Load properties from a json-formated file <img-file>.meta :'
270
            '{"key1": "val1", "key2": "val2", ...}',
271
            ('--property-file')),
272
        prop_file_force=FlagArgument(
273
            'Store remote property object, even it already exists',
274
            ('-f', '--force-upload-property-file')),
275
        no_prop_file_upload=FlagArgument(
276
            'Do not store properties in remote property file',
277
            ('--no-property-file-upload')),
278
        container=ValueArgument(
279
            'Remote image container', ('-C', '--container')),
280
        fileowner=ValueArgument(
281
            'UUID of the user who owns the image file', ('--fileowner'))
282
    )
283

    
284
    def _get_uuid(self):
285
        uuid = self['fileowner'] or self.config.get('image', 'fileowner')
286
        if uuid:
287
            return uuid
288
        atoken = self.client.token
289
        user = AstakosClient(self.config.get('user', 'url'), atoken)
290
        return user.term('uuid')
291

    
292
    def _get_pithos_client(self, uuid, container):
293
        purl = self.config.get('file', 'url')
294
        ptoken = self.client.token
295
        return PithosClient(purl, ptoken, uuid, container)
296

    
297
    def _store_remote_property_file(self, pclient, remote_path, properties):
298
        return pclient.upload_from_string(
299
            remote_path, _validate_image_props(properties, return_str=True))
300

    
301
    def _get_container_path(self, container_path):
302
        container = self['container'] or self.config.get('image', 'container')
303
        if container:
304
            return container, container_path
305

    
306
        container, sep, path = container_path.partition(':')
307
        if not sep or not container or not path:
308
            raiseCLIError(
309
                '%s is not a valid pithos+ remote location' % container_path,
310
                importance=2,
311
                details=[
312
                    'To set "image" as container and "my_dir/img.diskdump" as',
313
                    'the image path, try one of the following as '
314
                    'container:path',
315
                    '- <image container>:<remote path>',
316
                    '    e.g. image:/my_dir/img.diskdump',
317
                    '- <remote path> -C <image container>',
318
                    '    e.g. /my_dir/img.diskdump -C image'])
319
        return container, path
320

    
321
    @errors.generic.all
322
    @errors.plankton.connection
323
    def _run(self, name, container_path):
324
        container, path = self._get_container_path(container_path)
325
        uuid = self._get_uuid()
326
        prop_path = '%s.meta' % path
327

    
328
        pclient = None if (
329
            self['no_prop_file_upload']) else self._get_pithos_client(
330
                uuid, container)
331
        if pclient and not self['prop_file_force']:
332
            try:
333
                pclient.get_object_info(prop_path)
334
                raiseCLIError('Property file %s: %s already exists' % (
335
                    container, prop_path))
336
            except ClientError as ce:
337
                if ce.status != 404:
338
                    raise
339

    
340
        location = 'pithos://%s/%s/%s' % (uuid, container, path)
341

    
342
        params = {}
343
        for key in set([
344
                'checksum',
345
                'container_format',
346
                'disk_format',
347
                'owner',
348
                'size',
349
                'is_public']).intersection(self.arguments):
350
            params[key] = self[key]
351

    
352
        #load properties
353
        properties = dict()
354
        if self['property_file']:
355
            for k, v in _load_image_props(self['property_file']).items():
356
                properties[k.lower()] = v
357
        for k, v in self['properties'].items():
358
            properties[k.lower()] = v
359

    
360
        printer = print_json if self['json_output'] else print_dict
361
        printer(self.client.register(name, location, params, properties))
362

    
363
        if pclient:
364
            prop_headers = pclient.upload_from_string(
365
                prop_path, _validate_image_props(properties, return_str=True))
366
            if self['json_output']:
367
                print_json(dict(
368
                    property_file_location='%s:%s' % (container, prop_path),
369
                    headers=prop_headers))
370
            else:
371
                print('Property file location is %s:%s with version %s' % (
372
                    container, prop_path, prop_headers['x-object-version']))
373

    
374
    def main(self, name, container___path):
375
        super(self.__class__, self)._run()
376
        self._run(name, container___path)
377

    
378

    
379
@command(image_cmds)
380
class image_unregister(_init_image, _optional_output_cmd):
381
    """Unregister an image (does not delete the image file)"""
382

    
383
    @errors.generic.all
384
    @errors.plankton.connection
385
    @errors.plankton.id
386
    def _run(self, image_id):
387
        self._optional_output(self.client.unregister(image_id))
388

    
389
    def main(self, image_id):
390
        super(self.__class__, self)._run()
391
        self._run(image_id=image_id)
392

    
393

    
394
@command(image_cmds)
395
class image_shared(_init_image):
396
    """List images shared by a member"""
397

    
398
    arguments = dict(
399
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
400
    )
401

    
402
    @errors.generic.all
403
    @errors.plankton.connection
404
    def _run(self, member):
405
        r = self.client.list_shared(member)
406
        if self['json_output']:
407
            print_json(r)
408
        else:
409
            print_items(r, title=('image_id',))
410

    
411
    def main(self, member):
412
        super(self.__class__, self)._run()
413
        self._run(member)
414

    
415

    
416
@command(image_cmds)
417
class image_members(_init_image):
418
    """Manage members. Members of an image are users who can modify it"""
419

    
420

    
421
@command(image_cmds)
422
class image_members_list(_init_image):
423
    """List members of an image"""
424

    
425
    arguments = dict(
426
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
427
    )
428

    
429
    @errors.generic.all
430
    @errors.plankton.connection
431
    @errors.plankton.id
432
    def _run(self, image_id):
433
        members = self.client.list_members(image_id)
434
        if self['json_output']:
435
            print_json(members)
436
        else:
437
            print_items(members, title=('member_id',), with_redundancy=True)
438

    
439
    def main(self, image_id):
440
        super(self.__class__, self)._run()
441
        self._run(image_id=image_id)
442

    
443

    
444
@command(image_cmds)
445
class image_members_add(_init_image, _optional_output_cmd):
446
    """Add a member to an image"""
447

    
448
    @errors.generic.all
449
    @errors.plankton.connection
450
    @errors.plankton.id
451
    def _run(self, image_id=None, member=None):
452
            self._optional_output(self.client.add_member(image_id, member))
453

    
454
    def main(self, image_id, member):
455
        super(self.__class__, self)._run()
456
        self._run(image_id=image_id, member=member)
457

    
458

    
459
@command(image_cmds)
460
class image_members_delete(_init_image, _optional_output_cmd):
461
    """Remove a member from an image"""
462

    
463
    @errors.generic.all
464
    @errors.plankton.connection
465
    @errors.plankton.id
466
    def _run(self, image_id=None, member=None):
467
            self._optional_output(self.client.remove_member(image_id, member))
468

    
469
    def main(self, image_id, member):
470
        super(self.__class__, self)._run()
471
        self._run(image_id=image_id, member=member)
472

    
473

    
474
@command(image_cmds)
475
class image_members_set(_init_image, _optional_output_cmd):
476
    """Set the members of an image"""
477

    
478
    @errors.generic.all
479
    @errors.plankton.connection
480
    @errors.plankton.id
481
    def _run(self, image_id, members):
482
            self._optional_output(self.client.set_members(image_id, members))
483

    
484
    def main(self, image_id, *members):
485
        super(self.__class__, self)._run()
486
        self._run(image_id=image_id, members=members)
487

    
488

    
489
# Compute Image Commands
490

    
491

    
492
@command(image_cmds)
493
class image_compute(_init_cyclades):
494
    """Cyclades/Compute API image commands"""
495

    
496

    
497
@command(image_cmds)
498
class image_compute_list(_init_cyclades):
499
    """List images"""
500

    
501
    arguments = dict(
502
        detail=FlagArgument('show detailed output', ('-l', '--details')),
503
        limit=IntArgument('limit number listed images', ('-n', '--number')),
504
        more=FlagArgument(
505
            'output results in pages (-n to set items per page, default 10)',
506
            '--more'),
507
        enum=FlagArgument('Enumerate results', '--enumerate'),
508
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
509
    )
510

    
511
    def _make_results_pretty(self, images):
512
        for img in images:
513
            if 'metadata' in img:
514
                img['metadata'] = img['metadata']['values']
515

    
516
    @errors.generic.all
517
    @errors.cyclades.connection
518
    def _run(self):
519
        images = self.client.list_images(self['detail'])
520
        if self['json_output']:
521
            print_json(images)
522
            return
523
        if self['detail']:
524
            self._make_results_pretty(images)
525
        if self['more']:
526
            print_items(
527
                images,
528
                page_size=self['limit'] or 10, with_enumeration=self['enum'])
529
        else:
530
            print_items(images[:self['limit']], with_enumeration=self['enum'])
531

    
532
    def main(self):
533
        super(self.__class__, self)._run()
534
        self._run()
535

    
536

    
537
@command(image_cmds)
538
class image_compute_info(_init_cyclades):
539
    """Get detailed information on an image"""
540

    
541
    arguments = dict(
542
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
543
    )
544

    
545
    @errors.generic.all
546
    @errors.cyclades.connection
547
    @errors.plankton.id
548
    def _run(self, image_id):
549
        image = self.client.get_image_details(image_id)
550
        if self['json_output']:
551
            print_json(image)
552
            return
553
        if 'metadata' in image:
554
            image['metadata'] = image['metadata']['values']
555
        print_dict(image)
556

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

    
561

    
562
@command(image_cmds)
563
class image_compute_delete(_init_cyclades, _optional_output_cmd):
564
    """Delete an image (WARNING: image file is also removed)"""
565

    
566
    @errors.generic.all
567
    @errors.cyclades.connection
568
    @errors.plankton.id
569
    def _run(self, image_id):
570
        self._optional_output(self.client.delete_image(image_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_compute_properties(_init_cyclades):
579
    """Manage properties related to OS installation in an image"""
580

    
581

    
582
@command(image_cmds)
583
class image_compute_properties_list(_init_cyclades):
584
    """List all image properties"""
585

    
586
    arguments = dict(
587
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
588
    )
589

    
590
    @errors.generic.all
591
    @errors.cyclades.connection
592
    @errors.plankton.id
593
    def _run(self, image_id):
594
        printer = print_json if self['json_output'] else print_dict
595
        printer(self.client.get_image_metadata(image_id))
596

    
597
    def main(self, image_id):
598
        super(self.__class__, self)._run()
599
        self._run(image_id=image_id)
600

    
601

    
602
@command(image_cmds)
603
class image_compute_properties_get(_init_cyclades):
604
    """Get an image property"""
605

    
606
    arguments = dict(
607
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
608
    )
609

    
610
    @errors.generic.all
611
    @errors.cyclades.connection
612
    @errors.plankton.id
613
    @errors.plankton.metadata
614
    def _run(self, image_id, key):
615
        printer = print_json if self['json_output'] else print_dict
616
        printer(self.client.get_image_metadata(image_id, key))
617

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

    
622

    
623
@command(image_cmds)
624
class image_compute_properties_add(_init_cyclades):
625
    """Add a property to an image"""
626

    
627
    arguments = dict(
628
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
629
    )
630

    
631
    @errors.generic.all
632
    @errors.cyclades.connection
633
    @errors.plankton.id
634
    @errors.plankton.metadata
635
    def _run(self, image_id, key, val):
636
        printer = print_json if self['json_output'] else print_dict
637
        printer(self.client.create_image_metadata(image_id, key, val))
638

    
639
    def main(self, image_id, key, val):
640
        super(self.__class__, self)._run()
641
        self._run(image_id=image_id, key=key, val=val)
642

    
643

    
644
@command(image_cmds)
645
class image_compute_properties_set(_init_cyclades):
646
    """Add / update a set of properties for an image
647
    proeprties must be given in the form key=value, e.v.
648
    /image compute properties set <image-id> key1=val1 key2=val2
649
    """
650
    arguments = dict(
651
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
652
    )
653

    
654
    @errors.generic.all
655
    @errors.cyclades.connection
656
    @errors.plankton.id
657
    def _run(self, image_id, keyvals):
658
        metadata = dict()
659
        for keyval in keyvals:
660
            key, val = keyval.split('=')
661
            metadata[key] = val
662
        printer = print_json if self['json_output'] else print_dict
663
        printer(self.client.update_image_metadata(image_id, **metadata))
664

    
665
    def main(self, image_id, *key_equals_value):
666
        super(self.__class__, self)._run()
667
        self._run(image_id=image_id, keyvals=key_equals_value)
668

    
669

    
670
@command(image_cmds)
671
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
672
    """Delete a property from an image"""
673

    
674
    @errors.generic.all
675
    @errors.cyclades.connection
676
    @errors.plankton.id
677
    @errors.plankton.metadata
678
    def _run(self, image_id, key):
679
        self._optional_output(self.client.delete_image_metadata(image_id, key))
680

    
681
    def main(self, image_id, key):
682
        super(self.__class__, self)._run()
683
        self._run(image_id=image_id, key=key)