Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 00336c85

History | View | Annotate | Download (22.3 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)
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
                details=[
311
                    'To set "image" as container and "my_dir/img.diskdump" as',
312
                    'the image path, try one of the following as '
313
                    'container:path',
314
                    '- <image container>:<remote path>',
315
                    '    e.g. image:/my_dir/img.diskdump',
316
                    '- <remote path> -C <image container>',
317
                    '    e.g. /my_dir/img.diskdump -C image'])
318
        return container, path
319

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

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

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

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

    
351
            #load properties
352
            properties = _load_image_props(self['property_file']) if (
353
                self['property_file']) else dict()
354
            properties.update(self['properties'])
355

    
356
        printer = print_json if self['json_output'] else print_dict
357
        printer(self.client.register(name, location, params, properties))
358

    
359
        if pclient:
360
            prop_headers = pclient.upload_from_string(
361
                prop_path, _validate_image_props(properties, return_str=True))
362
            print('Property file location is %s: %s' % (container, prop_path))
363
            print('\twith version %s' % prop_headers['x-object-version'])
364

    
365
    def main(self, name, container___path):
366
        super(self.__class__, self)._run()
367
        self._run(name, container___path)
368

    
369

    
370
@command(image_cmds)
371
class image_unregister(_init_image, _optional_output_cmd):
372
    """Unregister an image (does not delete the image file)"""
373

    
374
    @errors.generic.all
375
    @errors.plankton.connection
376
    @errors.plankton.id
377
    def _run(self, image_id):
378
        self._optional_output(self.client.unregister(image_id))
379

    
380
    def main(self, image_id):
381
        super(self.__class__, self)._run()
382
        self._run(image_id=image_id)
383

    
384

    
385
@command(image_cmds)
386
class image_shared(_init_image):
387
    """List images shared by a member"""
388

    
389
    arguments = dict(
390
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
391
    )
392

    
393
    @errors.generic.all
394
    @errors.plankton.connection
395
    def _run(self, member):
396
        r = self.client.list_shared(member)
397
        if self['json_output']:
398
            print_json(r)
399
        else:
400
            print_items(r, title=('image_id',))
401

    
402
    def main(self, member):
403
        super(self.__class__, self)._run()
404
        self._run(member)
405

    
406

    
407
@command(image_cmds)
408
class image_members(_init_image):
409
    """Manage members. Members of an image are users who can modify it"""
410

    
411

    
412
@command(image_cmds)
413
class image_members_list(_init_image):
414
    """List members of an image"""
415

    
416
    arguments = dict(
417
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
418
    )
419

    
420
    @errors.generic.all
421
    @errors.plankton.connection
422
    @errors.plankton.id
423
    def _run(self, image_id):
424
        members = self.client.list_members(image_id)
425
        if self['json_output']:
426
            print_json(members)
427
        else:
428
            print_items(members, title=('member_id',), with_redundancy=True)
429

    
430
    def main(self, image_id):
431
        super(self.__class__, self)._run()
432
        self._run(image_id=image_id)
433

    
434

    
435
@command(image_cmds)
436
class image_members_add(_init_image, _optional_output_cmd):
437
    """Add a member to an image"""
438

    
439
    @errors.generic.all
440
    @errors.plankton.connection
441
    @errors.plankton.id
442
    def _run(self, image_id=None, member=None):
443
            self._optional_output(self.client.add_member(image_id, member))
444

    
445
    def main(self, image_id, member):
446
        super(self.__class__, self)._run()
447
        self._run(image_id=image_id, member=member)
448

    
449

    
450
@command(image_cmds)
451
class image_members_delete(_init_image, _optional_output_cmd):
452
    """Remove a member from an image"""
453

    
454
    @errors.generic.all
455
    @errors.plankton.connection
456
    @errors.plankton.id
457
    def _run(self, image_id=None, member=None):
458
            self._optional_output(self.client.remove_member(image_id, member))
459

    
460
    def main(self, image_id, member):
461
        super(self.__class__, self)._run()
462
        self._run(image_id=image_id, member=member)
463

    
464

    
465
@command(image_cmds)
466
class image_members_set(_init_image, _optional_output_cmd):
467
    """Set the members of an image"""
468

    
469
    @errors.generic.all
470
    @errors.plankton.connection
471
    @errors.plankton.id
472
    def _run(self, image_id, members):
473
            self._optional_output(self.client.set_members(image_id, members))
474

    
475
    def main(self, image_id, *members):
476
        super(self.__class__, self)._run()
477
        self._run(image_id=image_id, members=members)
478

    
479

    
480
# Compute Image Commands
481

    
482

    
483
@command(image_cmds)
484
class image_compute(_init_cyclades):
485
    """Cyclades/Compute API image commands"""
486

    
487

    
488
@command(image_cmds)
489
class image_compute_list(_init_cyclades):
490
    """List images"""
491

    
492
    arguments = dict(
493
        detail=FlagArgument('show detailed output', ('-l', '--details')),
494
        limit=IntArgument('limit number listed images', ('-n', '--number')),
495
        more=FlagArgument(
496
            'output results in pages (-n to set items per page, default 10)',
497
            '--more'),
498
        enum=FlagArgument('Enumerate results', '--enumerate'),
499
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
500
    )
501

    
502
    def _make_results_pretty(self, images):
503
        for img in images:
504
            if 'metadata' in img:
505
                img['metadata'] = img['metadata']['values']
506

    
507
    @errors.generic.all
508
    @errors.cyclades.connection
509
    def _run(self):
510
        images = self.client.list_images(self['detail'])
511
        if self['json_output']:
512
            print_json(images)
513
            return
514
        if self['detail']:
515
            self._make_results_pretty(images)
516
        if self['more']:
517
            print_items(
518
                images,
519
                page_size=self['limit'] or 10, with_enumeration=self['enum'])
520
        else:
521
            print_items(images[:self['limit']], with_enumeration=self['enum'])
522

    
523
    def main(self):
524
        super(self.__class__, self)._run()
525
        self._run()
526

    
527

    
528
@command(image_cmds)
529
class image_compute_info(_init_cyclades):
530
    """Get detailed information on an image"""
531

    
532
    arguments = dict(
533
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
534
    )
535

    
536
    @errors.generic.all
537
    @errors.cyclades.connection
538
    @errors.plankton.id
539
    def _run(self, image_id):
540
        image = self.client.get_image_details(image_id)
541
        if self['json_output']:
542
            print_json(image)
543
            return
544
        if 'metadata' in image:
545
            image['metadata'] = image['metadata']['values']
546
        print_dict(image)
547

    
548
    def main(self, image_id):
549
        super(self.__class__, self)._run()
550
        self._run(image_id=image_id)
551

    
552

    
553
@command(image_cmds)
554
class image_compute_delete(_init_cyclades, _optional_output_cmd):
555
    """Delete an image (WARNING: image file is also removed)"""
556

    
557
    @errors.generic.all
558
    @errors.cyclades.connection
559
    @errors.plankton.id
560
    def _run(self, image_id):
561
        self._optional_output(self.client.delete_image(image_id))
562

    
563
    def main(self, image_id):
564
        super(self.__class__, self)._run()
565
        self._run(image_id=image_id)
566

    
567

    
568
@command(image_cmds)
569
class image_compute_properties(_init_cyclades):
570
    """Manage properties related to OS installation in an image"""
571

    
572

    
573
@command(image_cmds)
574
class image_compute_properties_list(_init_cyclades):
575
    """List all image properties"""
576

    
577
    arguments = dict(
578
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
579
    )
580

    
581
    @errors.generic.all
582
    @errors.cyclades.connection
583
    @errors.plankton.id
584
    def _run(self, image_id):
585
        printer = print_json if self['json_output'] else print_dict
586
        printer(self.client.get_image_metadata(image_id))
587

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

    
592

    
593
@command(image_cmds)
594
class image_compute_properties_get(_init_cyclades):
595
    """Get an image property"""
596

    
597
    arguments = dict(
598
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
599
    )
600

    
601
    @errors.generic.all
602
    @errors.cyclades.connection
603
    @errors.plankton.id
604
    @errors.plankton.metadata
605
    def _run(self, image_id, key):
606
        printer = print_json if self['json_output'] else print_dict
607
        printer(self.client.get_image_metadata(image_id, key))
608

    
609
    def main(self, image_id, key):
610
        super(self.__class__, self)._run()
611
        self._run(image_id=image_id, key=key)
612

    
613

    
614
@command(image_cmds)
615
class image_compute_properties_add(_init_cyclades):
616
    """Add a property to an image"""
617

    
618
    arguments = dict(
619
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
620
    )
621

    
622
    @errors.generic.all
623
    @errors.cyclades.connection
624
    @errors.plankton.id
625
    @errors.plankton.metadata
626
    def _run(self, image_id, key, val):
627
        printer = print_json if self['json_output'] else print_dict
628
        printer(self.client.create_image_metadata(image_id, key, val))
629

    
630
    def main(self, image_id, key, val):
631
        super(self.__class__, self)._run()
632
        self._run(image_id=image_id, key=key, val=val)
633

    
634

    
635
@command(image_cmds)
636
class image_compute_properties_set(_init_cyclades):
637
    """Add / update a set of properties for an image
638
    proeprties must be given in the form key=value, e.v.
639
    /image compute properties set <image-id> key1=val1 key2=val2
640
    """
641
    arguments = dict(
642
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
643
    )
644

    
645
    @errors.generic.all
646
    @errors.cyclades.connection
647
    @errors.plankton.id
648
    def _run(self, image_id, keyvals):
649
        metadata = dict()
650
        for keyval in keyvals:
651
            key, val = keyval.split('=')
652
            metadata[key] = val
653
        printer = print_json if self['json_output'] else print_dict
654
        printer(self.client.update_image_metadata(image_id, **metadata))
655

    
656
    def main(self, image_id, *key_equals_value):
657
        super(self.__class__, self)._run()
658
        self._run(image_id=image_id, keyvals=key_equals_value)
659

    
660

    
661
@command(image_cmds)
662
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
663
    """Delete a property from an image"""
664

    
665
    @errors.generic.all
666
    @errors.cyclades.connection
667
    @errors.plankton.id
668
    @errors.plankton.metadata
669
    def _run(self, image_id, key):
670
        self._optional_output(self.client.delete_image_metadata(image_id, key))
671

    
672
    def main(self, image_id, key):
673
        super(self.__class__, self)._run()
674
        self._run(image_id=image_id, key=key)