Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / commands / image.py @ 623a4ceb

History | View | Annotate | Download (19.7 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.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
43
from kamaki.cli.argument import IntArgument
44
from kamaki.cli.commands.cyclades import _init_cyclades
45
from kamaki.cli.commands import _command_init, errors, _optional_output_cmd
46

    
47

    
48
image_cmds = CommandTree(
49
    'image',
50
    'Cyclades/Plankton API image commands\n'
51
    'image compute:\tCyclades/Compute API image commands')
52
_commands = [image_cmds]
53

    
54

    
55
about_image_id = [
56
    'To see a list of available image ids: /image list']
57

    
58

    
59
log = getLogger(__name__)
60

    
61

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

    
75
    def main(self):
76
        self._run()
77

    
78

    
79
# Plankton Image Commands
80

    
81

    
82
def _validate_image_props(json_dict, return_str=False):
83
    """
84
    :param json_dict" (dict) json-formated, of the form
85
        {"key1": "val1", "key2": "val2", ...}
86

87
    :param return_str: (boolean) if true, return a json dump
88

89
    :returns: (dict)
90

91
    :raises TypeError, AttributeError: Invalid json format
92

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

    
104

    
105
def _load_image_props(filepath):
106
    """
107
    :param filepath: (str) the (relative) path of the metafile
108

109
    :returns: (dict) json_formated
110

111
    :raises TypeError, AttributeError: Invalid json format
112

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

    
123

    
124
@command(image_cmds)
125
class image_list(_init_image):
126
    """List images accessible by user"""
127

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

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

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

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

    
193
        order = self['order']
194
        detail = self['detail']
195
        if self['owner']:
196
            images = self._filtered_by_owner(detail, filters, order)
197
        else:
198
            images = self.client.list_public(detail, filters, order)
199

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

    
213
    def main(self):
214
        super(self.__class__, self)._run()
215
        self._run()
216

    
217

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

    
227
    arguments = dict(
228
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
229
    )
230

    
231
    @errors.generic.all
232
    @errors.plankton.connection
233
    @errors.plankton.id
234
    def _run(self, image_id):
235
        printer = print_json if self['json_output'] else print_dict
236
        printer(self.client.get_meta(image_id))
237

    
238
    def main(self, image_id):
239
        super(self.__class__, self)._run()
240
        self._run(image_id=image_id)
241

    
242

    
243
@command(image_cmds)
244
class image_register(_init_image):
245
    """(Re)Register an image"""
246

    
247
    arguments = dict(
248
        checksum=ValueArgument('set image checksum', '--checksum'),
249
        container_format=ValueArgument(
250
            'set container format',
251
            '--container-format'),
252
        disk_format=ValueArgument('set disk format', '--disk-format'),
253
        #id=ValueArgument('set image ID', '--id'),
254
        owner=ValueArgument('set image owner (admin only)', '--owner'),
255
        properties=KeyValueArgument(
256
            'add property in key=value form (can be repeated)',
257
            ('-p', '--property')),
258
        is_public=FlagArgument('mark image as public', '--public'),
259
        size=IntArgument('set image size', '--size'),
260
        #update=FlagArgument(
261
        #    'update existing image properties',
262
        #    ('-u', '--update')),
263
        json_output=FlagArgument('Show results in json', ('-j', '--json')),
264
        property_file=ValueArgument(
265
            'Load properties from a json-formated file. Contents:'
266
            '{"key1": "val1", "key2": "val2", ...}',
267
            ('--property-file'))
268
    )
269

    
270
    @errors.generic.all
271
    @errors.plankton.connection
272
    def _run(self, name, location):
273
        if not location.startswith('pithos://'):
274
            account = self.config.get('file', 'account') \
275
                or self.config.get('global', 'account')
276
            assert account, 'No user account provided'
277
            if account[-1] == '/':
278
                account = account[:-1]
279
            container = self.config.get('file', 'container') \
280
                or self.config.get('global', 'container')
281
            if not container:
282
                location = 'pithos://%s/%s' % (account, location)
283
            else:
284
                location = 'pithos://%s/%s/%s' % (account, container, location)
285

    
286
        params = {}
287
        for key in set([
288
                'checksum',
289
                'container_format',
290
                'disk_format',
291
                'owner',
292
                'size',
293
                'is_public']).intersection(self.arguments):
294
            params[key] = self[key]
295

    
296
            #load properties
297
            properties = _load_image_props(self['property_file']) if (
298
                self['property_file']) else dict()
299
            properties.update(self['properties'])
300

    
301
        printer = print_json if self['json_output'] else print_dict
302
        printer(self.client.register(name, location, params, properties))
303

    
304
    def main(self, name, location):
305
        super(self.__class__, self)._run()
306
        self._run(name, location)
307

    
308

    
309
@command(image_cmds)
310
class image_unregister(_init_image, _optional_output_cmd):
311
    """Unregister an image (does not delete the image file)"""
312

    
313
    @errors.generic.all
314
    @errors.plankton.connection
315
    @errors.plankton.id
316
    def _run(self, image_id):
317
        self._optional_output(self.client.unregister(image_id))
318

    
319
    def main(self, image_id):
320
        super(self.__class__, self)._run()
321
        self._run(image_id=image_id)
322

    
323

    
324
@command(image_cmds)
325
class image_shared(_init_image):
326
    """List images shared by a member"""
327

    
328
    arguments = dict(
329
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
330
    )
331

    
332
    @errors.generic.all
333
    @errors.plankton.connection
334
    def _run(self, member):
335
        r = self.client.list_shared(member)
336
        if self['json_output']:
337
            print_json(r)
338
        else:
339
            print_items(r, title=('image_id',))
340

    
341
    def main(self, member):
342
        super(self.__class__, self)._run()
343
        self._run(member)
344

    
345

    
346
@command(image_cmds)
347
class image_members(_init_image):
348
    """Manage members. Members of an image are users who can modify it"""
349

    
350

    
351
@command(image_cmds)
352
class image_members_list(_init_image):
353
    """List members of an image"""
354

    
355
    arguments = dict(
356
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
357
    )
358

    
359
    @errors.generic.all
360
    @errors.plankton.connection
361
    @errors.plankton.id
362
    def _run(self, image_id):
363
        members = self.client.list_members(image_id)
364
        if self['json_output']:
365
            print_json(members)
366
        else:
367
            print_items(members, title=('member_id',), with_redundancy=True)
368

    
369
    def main(self, image_id):
370
        super(self.__class__, self)._run()
371
        self._run(image_id=image_id)
372

    
373

    
374
@command(image_cmds)
375
class image_members_add(_init_image, _optional_output_cmd):
376
    """Add a member to an image"""
377

    
378
    @errors.generic.all
379
    @errors.plankton.connection
380
    @errors.plankton.id
381
    def _run(self, image_id=None, member=None):
382
            self._optional_output(self.client.add_member(image_id, member))
383

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

    
388

    
389
@command(image_cmds)
390
class image_members_delete(_init_image, _optional_output_cmd):
391
    """Remove a member from an image"""
392

    
393
    @errors.generic.all
394
    @errors.plankton.connection
395
    @errors.plankton.id
396
    def _run(self, image_id=None, member=None):
397
            self._optional_output(self.client.remove_member(image_id, member))
398

    
399
    def main(self, image_id, member):
400
        super(self.__class__, self)._run()
401
        self._run(image_id=image_id, member=member)
402

    
403

    
404
@command(image_cmds)
405
class image_members_set(_init_image, _optional_output_cmd):
406
    """Set the members of an image"""
407

    
408
    @errors.generic.all
409
    @errors.plankton.connection
410
    @errors.plankton.id
411
    def _run(self, image_id, members):
412
            self._optional_output(self.client.set_members(image_id, members))
413

    
414
    def main(self, image_id, *members):
415
        super(self.__class__, self)._run()
416
        self._run(image_id=image_id, members=members)
417

    
418

    
419
# Compute Image Commands
420

    
421

    
422
@command(image_cmds)
423
class image_compute(_init_cyclades):
424
    """Cyclades/Compute API image commands"""
425

    
426

    
427
@command(image_cmds)
428
class image_compute_list(_init_cyclades):
429
    """List images"""
430

    
431
    arguments = dict(
432
        detail=FlagArgument('show detailed output', ('-l', '--details')),
433
        limit=IntArgument('limit number listed images', ('-n', '--number')),
434
        more=FlagArgument(
435
            'output results in pages (-n to set items per page, default 10)',
436
            '--more'),
437
        enum=FlagArgument('Enumerate results', '--enumerate'),
438
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
439
    )
440

    
441
    def _make_results_pretty(self, images):
442
        for img in images:
443
            if 'metadata' in img:
444
                img['metadata'] = img['metadata']['values']
445

    
446
    @errors.generic.all
447
    @errors.cyclades.connection
448
    def _run(self):
449
        images = self.client.list_images(self['detail'])
450
        if self['json_output']:
451
            print_json(images)
452
            return
453
        if self['detail']:
454
            self._make_results_pretty(images)
455
        if self['more']:
456
            print_items(
457
                images,
458
                page_size=self['limit'] or 10, with_enumeration=self['enum'])
459
        else:
460
            print_items(images[:self['limit']], with_enumeration=self['enum'])
461

    
462
    def main(self):
463
        super(self.__class__, self)._run()
464
        self._run()
465

    
466

    
467
@command(image_cmds)
468
class image_compute_info(_init_cyclades):
469
    """Get detailed information on an image"""
470

    
471
    arguments = dict(
472
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
473
    )
474

    
475
    @errors.generic.all
476
    @errors.cyclades.connection
477
    @errors.plankton.id
478
    def _run(self, image_id):
479
        image = self.client.get_image_details(image_id)
480
        if self['json_output']:
481
            print_json(image)
482
            return
483
        if 'metadata' in image:
484
            image['metadata'] = image['metadata']['values']
485
        print_dict(image)
486

    
487
    def main(self, image_id):
488
        super(self.__class__, self)._run()
489
        self._run(image_id=image_id)
490

    
491

    
492
@command(image_cmds)
493
class image_compute_delete(_init_cyclades, _optional_output_cmd):
494
    """Delete an image (WARNING: image file is also removed)"""
495

    
496
    @errors.generic.all
497
    @errors.cyclades.connection
498
    @errors.plankton.id
499
    def _run(self, image_id):
500
        self._optional_output(self.client.delete_image(image_id))
501

    
502
    def main(self, image_id):
503
        super(self.__class__, self)._run()
504
        self._run(image_id=image_id)
505

    
506

    
507
@command(image_cmds)
508
class image_compute_properties(_init_cyclades):
509
    """Manage properties related to OS installation in an image"""
510

    
511

    
512
@command(image_cmds)
513
class image_compute_properties_list(_init_cyclades):
514
    """List all image properties"""
515

    
516
    arguments = dict(
517
        json_output=FlagArgument('Show results in json', ('-j', '--json'))
518
    )
519

    
520
    @errors.generic.all
521
    @errors.cyclades.connection
522
    @errors.plankton.id
523
    def _run(self, image_id):
524
        printer = print_json if self['json_output'] else print_dict
525
        printer(self.client.get_image_metadata(image_id))
526

    
527
    def main(self, image_id):
528
        super(self.__class__, self)._run()
529
        self._run(image_id=image_id)
530

    
531

    
532
@command(image_cmds)
533
class image_compute_properties_get(_init_cyclades):
534
    """Get an image property"""
535

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

    
540
    @errors.generic.all
541
    @errors.cyclades.connection
542
    @errors.plankton.id
543
    @errors.plankton.metadata
544
    def _run(self, image_id, key):
545
        printer = print_json if self['json_output'] else print_dict
546
        printer(self.client.get_image_metadata(image_id, key))
547

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

    
552

    
553
@command(image_cmds)
554
class image_compute_properties_add(_init_cyclades):
555
    """Add a property to 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
    @errors.plankton.metadata
565
    def _run(self, image_id, key, val):
566
        printer = print_json if self['json_output'] else print_dict
567
        printer(self.client.create_image_metadata(image_id, key, val))
568

    
569
    def main(self, image_id, key, val):
570
        super(self.__class__, self)._run()
571
        self._run(image_id=image_id, key=key, val=val)
572

    
573

    
574
@command(image_cmds)
575
class image_compute_properties_set(_init_cyclades):
576
    """Add / update a set of properties for an image
577
    proeprties must be given in the form key=value, e.v.
578
    /image compute properties set <image-id> key1=val1 key2=val2
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, keyvals):
588
        metadata = dict()
589
        for keyval in keyvals:
590
            key, val = keyval.split('=')
591
            metadata[key] = val
592
        printer = print_json if self['json_output'] else print_dict
593
        printer(self.client.update_image_metadata(image_id, **metadata))
594

    
595
    def main(self, image_id, *key_equals_value):
596
        super(self.__class__, self)._run()
597
        self._run(image_id=image_id, keyvals=key_equals_value)
598

    
599

    
600
@command(image_cmds)
601
class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
602
    """Delete a property from an image"""
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
        self._optional_output(self.client.delete_image_metadata(image_id, key))
610

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