Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.4 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 = dict()
353
        if self['property_file']:
354
            for k, v in _load_image_props(self['property_file']).items():
355
                properties[k.lower()] = v
356
        for k, v in self['properties'].items():
357
            properties[k.lower()] = v
358

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

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

    
368
    def main(self, name, container___path):
369
        super(self.__class__, self)._run()
370
        self._run(name, container___path)
371

    
372

    
373
@command(image_cmds)
374
class image_unregister(_init_image, _optional_output_cmd):
375
    """Unregister an image (does not delete the image file)"""
376

    
377
    @errors.generic.all
378
    @errors.plankton.connection
379
    @errors.plankton.id
380
    def _run(self, image_id):
381
        self._optional_output(self.client.unregister(image_id))
382

    
383
    def main(self, image_id):
384
        super(self.__class__, self)._run()
385
        self._run(image_id=image_id)
386

    
387

    
388
@command(image_cmds)
389
class image_shared(_init_image):
390
    """List images shared by a member"""
391

    
392
    arguments = dict(
393
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
394
    )
395

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

    
405
    def main(self, member):
406
        super(self.__class__, self)._run()
407
        self._run(member)
408

    
409

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

    
414

    
415
@command(image_cmds)
416
class image_members_list(_init_image):
417
    """List members of an image"""
418

    
419
    arguments = dict(
420
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
421
    )
422

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

    
433
    def main(self, image_id):
434
        super(self.__class__, self)._run()
435
        self._run(image_id=image_id)
436

    
437

    
438
@command(image_cmds)
439
class image_members_add(_init_image, _optional_output_cmd):
440
    """Add a member to an image"""
441

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

    
448
    def main(self, image_id, member):
449
        super(self.__class__, self)._run()
450
        self._run(image_id=image_id, member=member)
451

    
452

    
453
@command(image_cmds)
454
class image_members_delete(_init_image, _optional_output_cmd):
455
    """Remove a member from an image"""
456

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

    
463
    def main(self, image_id, member):
464
        super(self.__class__, self)._run()
465
        self._run(image_id=image_id, member=member)
466

    
467

    
468
@command(image_cmds)
469
class image_members_set(_init_image, _optional_output_cmd):
470
    """Set the members of an image"""
471

    
472
    @errors.generic.all
473
    @errors.plankton.connection
474
    @errors.plankton.id
475
    def _run(self, image_id, members):
476
            self._optional_output(self.client.set_members(image_id, members))
477

    
478
    def main(self, image_id, *members):
479
        super(self.__class__, self)._run()
480
        self._run(image_id=image_id, members=members)
481

    
482

    
483
# Compute Image Commands
484

    
485

    
486
@command(image_cmds)
487
class image_compute(_init_cyclades):
488
    """Cyclades/Compute API image commands"""
489

    
490

    
491
@command(image_cmds)
492
class image_compute_list(_init_cyclades):
493
    """List images"""
494

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

    
505
    def _make_results_pretty(self, images):
506
        for img in images:
507
            if 'metadata' in img:
508
                img['metadata'] = img['metadata']['values']
509

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

    
526
    def main(self):
527
        super(self.__class__, self)._run()
528
        self._run()
529

    
530

    
531
@command(image_cmds)
532
class image_compute_info(_init_cyclades):
533
    """Get detailed information on an image"""
534

    
535
    arguments = dict(
536
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
537
    )
538

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

    
551
    def main(self, image_id):
552
        super(self.__class__, self)._run()
553
        self._run(image_id=image_id)
554

    
555

    
556
@command(image_cmds)
557
class image_compute_delete(_init_cyclades, _optional_output_cmd):
558
    """Delete an image (WARNING: image file is also removed)"""
559

    
560
    @errors.generic.all
561
    @errors.cyclades.connection
562
    @errors.plankton.id
563
    def _run(self, image_id):
564
        self._optional_output(self.client.delete_image(image_id))
565

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

    
570

    
571
@command(image_cmds)
572
class image_compute_properties(_init_cyclades):
573
    """Manage properties related to OS installation in an image"""
574

    
575

    
576
@command(image_cmds)
577
class image_compute_properties_list(_init_cyclades):
578
    """List all image properties"""
579

    
580
    arguments = dict(
581
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
582
    )
583

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

    
591
    def main(self, image_id):
592
        super(self.__class__, self)._run()
593
        self._run(image_id=image_id)
594

    
595

    
596
@command(image_cmds)
597
class image_compute_properties_get(_init_cyclades):
598
    """Get an image property"""
599

    
600
    arguments = dict(
601
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
602
    )
603

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

    
612
    def main(self, image_id, key):
613
        super(self.__class__, self)._run()
614
        self._run(image_id=image_id, key=key)
615

    
616

    
617
@command(image_cmds)
618
class image_compute_properties_add(_init_cyclades):
619
    """Add a property to an image"""
620

    
621
    arguments = dict(
622
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
623
    )
624

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

    
633
    def main(self, image_id, key, val):
634
        super(self.__class__, self)._run()
635
        self._run(image_id=image_id, key=key, val=val)
636

    
637

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

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

    
659
    def main(self, image_id, *key_equals_value):
660
        super(self.__class__, self)._run()
661
        self._run(image_id=image_id, keyvals=key_equals_value)
662

    
663

    
664
@command(image_cmds)
665
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
666
    """Delete a property from an image"""
667

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

    
675
    def main(self, image_id, key):
676
        super(self.__class__, self)._run()
677
        self._run(image_id=image_id, key=key)