Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.2 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.image_file
323
    @errors.plankton.connection
324
    def _run(self, name, container_path):
325
        container, path = self._get_container_path(container_path)
326
        uuid = self._get_uuid()
327
        prop_path = '%s.meta' % path
328

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

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

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

    
353
        #load properties
354
        properties = dict()
355
        pfile = self['property_file']
356
        if pfile:
357
            try:
358
                for k, v in _load_image_props(pfile).items():
359
                    properties[k.lower()] = v
360
            except Exception as e:
361
                raiseCLIError(
362
                    e, 'Format error in property file %s' % pfile,
363
                    details=[
364
                        'Expected content format:',
365
                        '  {',
366
                        '    "key1": "value1",',
367
                        '    "key2": "value2",',
368
                        '    ...',
369
                        '  }',
370
                        '',
371
                        'Parser:'
372
                    ])
373
        for k, v in self['properties'].items():
374
            properties[k.lower()] = v
375

    
376
        printer = print_json if self['json_output'] else print_dict
377
        printer(self.client.register(name, location, params, properties))
378

    
379
        if pclient:
380
            prop_headers = pclient.upload_from_string(
381
                prop_path, _validate_image_props(properties, return_str=True))
382
            if self['json_output']:
383
                print_json(dict(
384
                    property_file_location='%s:%s' % (container, prop_path),
385
                    headers=prop_headers))
386
            else:
387
                print('Property file uploaded as %s:%s (version %s)' % (
388
                    container, prop_path, prop_headers['x-object-version']))
389

    
390
    def main(self, name, container___path):
391
        super(self.__class__, self)._run()
392
        self._run(name, container___path)
393

    
394

    
395
@command(image_cmds)
396
class image_unregister(_init_image, _optional_output_cmd):
397
    """Unregister an image (does not delete the image file)"""
398

    
399
    @errors.generic.all
400
    @errors.plankton.connection
401
    @errors.plankton.id
402
    def _run(self, image_id):
403
        self._optional_output(self.client.unregister(image_id))
404

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

    
409

    
410
@command(image_cmds)
411
class image_shared(_init_image):
412
    """List images shared by a member"""
413

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

    
418
    @errors.generic.all
419
    @errors.plankton.connection
420
    def _run(self, member):
421
        r = self.client.list_shared(member)
422
        if self['json_output']:
423
            print_json(r)
424
        else:
425
            print_items(r, title=('image_id',))
426

    
427
    def main(self, member):
428
        super(self.__class__, self)._run()
429
        self._run(member)
430

    
431

    
432
@command(image_cmds)
433
class image_members(_init_image):
434
    """Manage members. Members of an image are users who can modify it"""
435

    
436

    
437
@command(image_cmds)
438
class image_members_list(_init_image):
439
    """List members of an image"""
440

    
441
    arguments = dict(
442
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
443
    )
444

    
445
    @errors.generic.all
446
    @errors.plankton.connection
447
    @errors.plankton.id
448
    def _run(self, image_id):
449
        members = self.client.list_members(image_id)
450
        if self['json_output']:
451
            print_json(members)
452
        else:
453
            print_items(members, title=('member_id',), with_redundancy=True)
454

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

    
459

    
460
@command(image_cmds)
461
class image_members_add(_init_image, _optional_output_cmd):
462
    """Add a member to an image"""
463

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

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

    
474

    
475
@command(image_cmds)
476
class image_members_delete(_init_image, _optional_output_cmd):
477
    """Remove a member from an image"""
478

    
479
    @errors.generic.all
480
    @errors.plankton.connection
481
    @errors.plankton.id
482
    def _run(self, image_id=None, member=None):
483
            self._optional_output(self.client.remove_member(image_id, member))
484

    
485
    def main(self, image_id, member):
486
        super(self.__class__, self)._run()
487
        self._run(image_id=image_id, member=member)
488

    
489

    
490
@command(image_cmds)
491
class image_members_set(_init_image, _optional_output_cmd):
492
    """Set the members of an image"""
493

    
494
    @errors.generic.all
495
    @errors.plankton.connection
496
    @errors.plankton.id
497
    def _run(self, image_id, members):
498
            self._optional_output(self.client.set_members(image_id, members))
499

    
500
    def main(self, image_id, *members):
501
        super(self.__class__, self)._run()
502
        self._run(image_id=image_id, members=members)
503

    
504

    
505
# Compute Image Commands
506

    
507

    
508
@command(image_cmds)
509
class image_compute(_init_cyclades):
510
    """Cyclades/Compute API image commands"""
511

    
512

    
513
@command(image_cmds)
514
class image_compute_list(_init_cyclades):
515
    """List images"""
516

    
517
    arguments = dict(
518
        detail=FlagArgument('show detailed output', ('-l', '--details')),
519
        limit=IntArgument('limit number listed images', ('-n', '--number')),
520
        more=FlagArgument(
521
            'output results in pages (-n to set items per page, default 10)',
522
            '--more'),
523
        enum=FlagArgument('Enumerate results', '--enumerate'),
524
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
525
    )
526

    
527
    def _make_results_pretty(self, images):
528
        for img in images:
529
            if 'metadata' in img:
530
                img['metadata'] = img['metadata']['values']
531

    
532
    @errors.generic.all
533
    @errors.cyclades.connection
534
    def _run(self):
535
        images = self.client.list_images(self['detail'])
536
        if self['json_output']:
537
            print_json(images)
538
            return
539
        if self['detail']:
540
            self._make_results_pretty(images)
541
        if self['more']:
542
            print_items(
543
                images,
544
                page_size=self['limit'] or 10, with_enumeration=self['enum'])
545
        else:
546
            print_items(images[:self['limit']], with_enumeration=self['enum'])
547

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

    
552

    
553
@command(image_cmds)
554
class image_compute_info(_init_cyclades):
555
    """Get detailed information on an image"""
556

    
557
    arguments = dict(
558
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
559
    )
560

    
561
    @errors.generic.all
562
    @errors.cyclades.connection
563
    @errors.plankton.id
564
    def _run(self, image_id):
565
        image = self.client.get_image_details(image_id)
566
        if self['json_output']:
567
            print_json(image)
568
            return
569
        if 'metadata' in image:
570
            image['metadata'] = image['metadata']['values']
571
        print_dict(image)
572

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

    
577

    
578
@command(image_cmds)
579
class image_compute_delete(_init_cyclades, _optional_output_cmd):
580
    """Delete an image (WARNING: image file is also removed)"""
581

    
582
    @errors.generic.all
583
    @errors.cyclades.connection
584
    @errors.plankton.id
585
    def _run(self, image_id):
586
        self._optional_output(self.client.delete_image(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(_init_cyclades):
595
    """Manage properties related to OS installation in an image"""
596

    
597

    
598
@command(image_cmds)
599
class image_compute_properties_list(_init_cyclades):
600
    """List all image properties"""
601

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

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

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

    
617

    
618
@command(image_cmds)
619
class image_compute_properties_get(_init_cyclades):
620
    """Get an image property"""
621

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

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

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

    
638

    
639
@command(image_cmds)
640
class image_compute_properties_add(_init_cyclades):
641
    """Add a property to an image"""
642

    
643
    arguments = dict(
644
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
645
    )
646

    
647
    @errors.generic.all
648
    @errors.cyclades.connection
649
    @errors.plankton.id
650
    @errors.plankton.metadata
651
    def _run(self, image_id, key, val):
652
        printer = print_json if self['json_output'] else print_dict
653
        printer(self.client.create_image_metadata(image_id, key, val))
654

    
655
    def main(self, image_id, key, val):
656
        super(self.__class__, self)._run()
657
        self._run(image_id=image_id, key=key, val=val)
658

    
659

    
660
@command(image_cmds)
661
class image_compute_properties_set(_init_cyclades):
662
    """Add / update a set of properties for an image
663
    proeprties must be given in the form key=value, e.v.
664
    /image compute properties set <image-id> key1=val1 key2=val2
665
    """
666
    arguments = dict(
667
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
668
    )
669

    
670
    @errors.generic.all
671
    @errors.cyclades.connection
672
    @errors.plankton.id
673
    def _run(self, image_id, keyvals):
674
        metadata = dict()
675
        for keyval in keyvals:
676
            key, val = keyval.split('=')
677
            metadata[key] = val
678
        printer = print_json if self['json_output'] else print_dict
679
        printer(self.client.update_image_metadata(image_id, **metadata))
680

    
681
    def main(self, image_id, *key_equals_value):
682
        super(self.__class__, self)._run()
683
        self._run(image_id=image_id, keyvals=key_equals_value)
684

    
685

    
686
@command(image_cmds)
687
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
688
    """Delete a property from an image"""
689

    
690
    @errors.generic.all
691
    @errors.cyclades.connection
692
    @errors.plankton.id
693
    @errors.plankton.metadata
694
    def _run(self, image_id, key):
695
        self._optional_output(self.client.delete_image_metadata(image_id, key))
696

    
697
    def main(self, image_id, key):
698
        super(self.__class__, self)._run()
699
        self._run(image_id=image_id, key=key)