1 # Copyright 2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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
34 from json import load, dumps
35 from os.path import abspath
36 from logging import getLogger
38 from kamaki.cli import command
39 from kamaki.cli.command_tree import CommandTree
40 from kamaki.cli.utils import print_dict, 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.errors import raiseCLIError, CLIBaseUrlError
49 from kamaki.cli.commands import _command_init, errors
50 from kamaki.cli.commands import _optional_output_cmd, _optional_json
53 image_cmds = CommandTree(
55 'Cyclades/Plankton API image commands\n'
56 'image compute:\tCyclades/Compute API image commands')
57 _commands = [image_cmds]
61 'Kamaki commands to:',
62 ' get current user uuid: /user authenticate',
63 ' check available containers: /file list',
64 ' create a new container: /file create <container>',
65 ' check container contents: /file list <container>',
66 ' upload files: /file upload <image file> <container>']
68 about_image_id = ['To see a list of available image ids: /image list']
71 log = getLogger(__name__)
74 class _init_image(_command_init):
77 token = self.config.get('image', 'token')\
78 or self.config.get('global', 'token')
80 if getattr(self, 'auth_base', False):
81 plankton_endpoints = self.auth_base.get_service_endpoints(
82 self.config.get('plankton', 'type'),
83 self.config.get('plankton', 'version'))
84 base_url = plankton_endpoints['publicURL']
86 base_url = self.config.get('plankton', 'url')
88 raise CLIBaseUrlError(service='plankton')
90 self.client = ImageClient(base_url=base_url, token=token)
91 self._set_log_params()
92 self._update_max_threads()
98 # Plankton Image Commands
101 def _validate_image_meta(json_dict, return_str=False):
103 :param json_dict" (dict) json-formated, of the form
104 {"key1": "val1", "key2": "val2", ...}
106 :param return_str: (boolean) if true, return a json dump
108 :returns: (dict) if return_str is not True, else return str
110 :raises TypeError, AttributeError: Invalid json format
112 :raises AssertionError: Valid json but invalid image properties dict
114 json_str = dumps(json_dict, indent=2)
115 for k, v in json_dict.items():
116 if k.lower() == 'properties':
117 for pk, pv in v.items():
118 prop_ok = not (isinstance(pv, dict) or isinstance(pv, list))
119 assert prop_ok, 'Invalid property value for key %s' % pk
120 key_ok = not (' ' in k or '-' in k)
121 assert key_ok, 'Invalid property key %s' % k
123 meta_ok = not (isinstance(v, dict) or isinstance(v, list))
124 assert meta_ok, 'Invalid value for meta key %s' % k
125 meta_ok = ' ' not in k
126 assert meta_ok, 'Invalid meta key [%s]' % k
127 json_dict[k] = '%s' % v
128 return json_str if return_str else json_dict
131 def _load_image_meta(filepath):
133 :param filepath: (str) the (relative) path of the metafile
135 :returns: (dict) json_formated
137 :raises TypeError, AttributeError: Invalid json format
139 :raises AssertionError: Valid json but invalid image properties dict
141 with open(abspath(filepath)) as f:
144 return _validate_image_meta(meta_dict)
145 except AssertionError:
146 log.debug('Failed to load properties from file %s' % filepath)
150 def _validate_image_location(location):
152 :param location: (str) pithos://<uuid>/<container>/<img-file-path>
154 :returns: (<uuid>, <container>, <img-file-path>)
156 :raises AssertionError: if location is invalid
159 msg = 'Invalid prefix for location %s , try: %s' % (location, prefix)
160 assert location.startswith(prefix), msg
161 service, sep, rest = location.partition('://')
162 assert sep and rest, 'Location %s is missing uuid' % location
163 uuid, sep, rest = rest.partition('/')
164 assert sep and rest, 'Location %s is missing container' % location
165 container, sep, img_path = rest.partition('/')
166 assert sep and img_path, 'Location %s is missing image path' % location
167 return uuid, container, img_path
171 class image_list(_init_image, _optional_json):
172 """List images accessible by user"""
175 detail=FlagArgument('show detailed output', ('-l', '--details')),
176 container_format=ValueArgument(
177 'filter by container format',
178 '--container-format'),
179 disk_format=ValueArgument('filter by disk format', '--disk-format'),
180 name=ValueArgument('filter by name', '--name'),
181 name_pref=ValueArgument(
182 'filter by name prefix (case insensitive)',
184 name_suff=ValueArgument(
185 'filter by name suffix (case insensitive)',
187 name_like=ValueArgument(
188 'print only if name contains this (case insensitive)',
190 size_min=IntArgument('filter by minimum size', '--size-min'),
191 size_max=IntArgument('filter by maximum size', '--size-max'),
192 status=ValueArgument('filter by status', '--status'),
193 owner=ValueArgument('filter by owner', '--owner'),
195 'order by FIELD ( - to reverse order)',
198 limit=IntArgument('limit number of listed images', ('-n', '--number')),
200 'output results in pages (-n to set items per page, default 10)',
202 enum=FlagArgument('Enumerate results', '--enumerate')
205 def _filtered_by_owner(self, detail, *list_params):
208 'id', 'size', 'status', 'disk_format', 'container_format', 'name'])
209 for img in self.client.list_public(True, *list_params):
210 if img['owner'] == self['owner']:
212 for key in set(img.keys()).difference(MINKEYS):
217 def _filtered_by_name(self, images):
218 np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
219 return [img for img in images if (
220 (not np) or img['name'].lower().startswith(np.lower())) and (
221 (not ns) or img['name'].lower().endswith(ns.lower())) and (
222 (not nl) or nl.lower() in img['name'].lower())]
225 @errors.cyclades.connection
227 super(self.__class__, self)._run()
235 'status']).intersection(self.arguments):
236 filters[arg] = self[arg]
238 order = self['order']
239 detail = self['detail']
241 images = self._filtered_by_owner(detail, filters, order)
243 images = self.client.list_public(detail, filters, order)
245 images = self._filtered_by_name(images)
246 kwargs = dict(with_enumeration=self['enum'])
248 kwargs['page_size'] = self['limit'] or 10
250 images = images[:self['limit']]
251 self._print(images, **kwargs)
254 super(self.__class__, self)._run()
259 class image_meta(_init_image, _optional_json):
260 """Get image metadata
261 Image metadata include:
262 - image file information (location, size, etc.)
263 - image information (id, name, etc.)
264 - image os properties (os, fs, etc.)
268 @errors.plankton.connection
270 def _run(self, image_id):
271 self._print([self.client.get_meta(image_id)])
273 def main(self, image_id):
274 super(self.__class__, self)._run()
275 self._run(image_id=image_id)
279 class image_register(_init_image, _optional_json):
280 """(Re)Register an image"""
283 checksum=ValueArgument('set image checksum', '--checksum'),
284 container_format=ValueArgument(
285 'set container format',
286 '--container-format'),
287 disk_format=ValueArgument('set disk format', '--disk-format'),
288 owner=ValueArgument('set image owner (admin only)', '--owner'),
289 properties=KeyValueArgument(
290 'add property in key=value form (can be repeated)',
291 ('-p', '--property')),
292 is_public=FlagArgument('mark image as public', '--public'),
293 size=IntArgument('set image size', '--size'),
294 metafile=ValueArgument(
295 'Load metadata from a json-formated file <img-file>.meta :'
296 '{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
298 metafile_force=FlagArgument(
299 'Store remote metadata object, even if it already exists',
301 no_metafile_upload=FlagArgument(
302 'Do not store metadata in remote meta file',
303 ('--no-metafile-upload')),
308 atoken = self.client.token
309 #user = AstakosClient(self.config.get('user', 'url'), atoken)
310 #return user.term('uuid')
311 self.auth_base.term('uuid', atoken)
313 def _get_pithos_client(self, container):
314 if self['no_metafile_upload']:
316 pithos_endpoints = self.auth_base.get_service_endpoints(
317 self.config.get('pithos', 'type'),
318 self.config.get('pithos', 'version'))
319 purl = pithos_endpoints['publicURL']
320 ptoken = self.client.token
321 return PithosClient(purl, ptoken, self._get_uuid(), container)
323 def _store_remote_metafile(self, pclient, remote_path, metadata):
324 return pclient.upload_from_string(
325 remote_path, _validate_image_meta(metadata, return_str=True))
327 def _load_params_from_file(self, location):
328 params, properties = dict(), dict()
329 pfile = self['metafile']
332 for k, v in _load_image_meta(pfile).items():
333 key = k.lower().replace('-', '_')
334 if k == 'properties':
335 for pk, pv in v.items():
336 properties[pk.upper().replace('-', '_')] = pv
339 elif key == 'location':
345 except Exception as e:
346 raiseCLIError(e, 'Invalid json metadata config file')
347 return params, properties, location
349 def _load_params_from_args(self, params, properties):
356 'is_public']).intersection(self.arguments):
357 params[key] = self[key]
358 for k, v in self['properties'].items():
359 properties[k.upper().replace('-', '_')] = v
361 def _validate_location(self, location):
364 'No image file location provided',
365 importance=2, details=[
366 'An image location is needed. Image location format:',
367 ' pithos://<uuid>/<container>/<path>',
368 ' an image file at the above location must exist.'
369 ] + howto_image_file)
371 return _validate_image_location(location)
372 except AssertionError as ae:
374 ae, 'Invalid image location format',
375 importance=1, details=[
376 'Valid image location format:',
377 ' pithos://<uuid>/<container>/<img-file-path>'
378 ] + howto_image_file)
381 @errors.plankton.connection
382 def _run(self, name, location):
383 (params, properties, location) = self._load_params_from_file(location)
384 uuid, container, img_path = self._validate_location(location)
385 self._load_params_from_args(params, properties)
386 pclient = self._get_pithos_client(container)
388 #check if metafile exists
389 meta_path = '%s.meta' % img_path
390 if pclient and not self['metafile_force']:
392 pclient.get_object_info(meta_path)
393 raiseCLIError('Metadata file %s:%s already exists' % (
394 container, meta_path))
395 except ClientError as ce:
401 r = self.client.register(name, location, params, properties)
402 except ClientError as ce:
403 if ce.status in (400, ):
405 ce, 'Nonexistent image file location %s' % location,
407 'Make sure the image file exists'] + howto_image_file)
409 self._print(r, print_dict)
411 #upload the metadata file
414 meta_headers = pclient.upload_from_string(
415 meta_path, dumps(r, indent=2))
417 print('Failed to dump metafile %s:%s' % (container, meta_path))
419 if self['json_output']:
421 metafile_location='%s:%s' % (container, meta_path),
422 headers=meta_headers))
424 print('Metadata file uploaded as %s:%s (version %s)' % (
425 container, meta_path, meta_headers['x-object-version']))
427 def main(self, name, location=None):
428 super(self.__class__, self)._run()
429 self._run(name, location)
433 class image_unregister(_init_image, _optional_output_cmd):
434 """Unregister an image (does not delete the image file)"""
437 @errors.plankton.connection
439 def _run(self, image_id):
440 self._optional_output(self.client.unregister(image_id))
442 def main(self, image_id):
443 super(self.__class__, self)._run()
444 self._run(image_id=image_id)
448 class image_shared(_init_image, _optional_json):
449 """List images shared by a member"""
452 @errors.plankton.connection
453 def _run(self, member):
454 self._print(self.client.list_shared(member), title=('image_id',))
456 def main(self, member):
457 super(self.__class__, self)._run()
462 class image_members(_init_image):
463 """Manage members. Members of an image are users who can modify it"""
467 class image_members_list(_init_image, _optional_json):
468 """List members of an image"""
471 @errors.plankton.connection
473 def _run(self, image_id):
474 self._print(self.client.list_members(image_id), title=('member_id',))
476 def main(self, image_id):
477 super(self.__class__, self)._run()
478 self._run(image_id=image_id)
482 class image_members_add(_init_image, _optional_output_cmd):
483 """Add a member to an image"""
486 @errors.plankton.connection
488 def _run(self, image_id=None, member=None):
489 self._optional_output(self.client.add_member(image_id, member))
491 def main(self, image_id, member):
492 super(self.__class__, self)._run()
493 self._run(image_id=image_id, member=member)
497 class image_members_delete(_init_image, _optional_output_cmd):
498 """Remove a member from an image"""
501 @errors.plankton.connection
503 def _run(self, image_id=None, member=None):
504 self._optional_output(self.client.remove_member(image_id, member))
506 def main(self, image_id, member):
507 super(self.__class__, self)._run()
508 self._run(image_id=image_id, member=member)
512 class image_members_set(_init_image, _optional_output_cmd):
513 """Set the members of an image"""
516 @errors.plankton.connection
518 def _run(self, image_id, members):
519 self._optional_output(self.client.set_members(image_id, members))
521 def main(self, image_id, *members):
522 super(self.__class__, self)._run()
523 self._run(image_id=image_id, members=members)
526 # Compute Image Commands
530 class image_compute(_init_cyclades):
531 """Cyclades/Compute API image commands"""
535 class image_compute_list(_init_cyclades, _optional_json):
539 detail=FlagArgument('show detailed output', ('-l', '--details')),
540 limit=IntArgument('limit number listed images', ('-n', '--number')),
542 'output results in pages (-n to set items per page, default 10)',
544 enum=FlagArgument('Enumerate results', '--enumerate')
548 @errors.cyclades.connection
550 images = self.client.list_images(self['detail'])
551 kwargs = dict(with_enumeration=self['enum'])
553 kwargs['page_size'] = self['limit'] or 10
555 images = images[:self['limit']]
556 self._print(images, **kwargs)
559 super(self.__class__, self)._run()
564 class image_compute_info(_init_cyclades, _optional_json):
565 """Get detailed information on an image"""
568 @errors.cyclades.connection
570 def _run(self, image_id):
571 image = self.client.get_image_details(image_id)
572 self._print(image, print_dict)
574 def main(self, image_id):
575 super(self.__class__, self)._run()
576 self._run(image_id=image_id)
580 class image_compute_delete(_init_cyclades, _optional_output_cmd):
581 """Delete an image (WARNING: image file is also removed)"""
584 @errors.cyclades.connection
586 def _run(self, image_id):
587 self._optional_output(self.client.delete_image(image_id))
589 def main(self, image_id):
590 super(self.__class__, self)._run()
591 self._run(image_id=image_id)
595 class image_compute_properties(_init_cyclades):
596 """Manage properties related to OS installation in an image"""
600 class image_compute_properties_list(_init_cyclades, _optional_json):
601 """List all image properties"""
604 @errors.cyclades.connection
606 def _run(self, image_id):
607 self._print(self.client.get_image_metadata(image_id), print_dict)
609 def main(self, image_id):
610 super(self.__class__, self)._run()
611 self._run(image_id=image_id)
615 class image_compute_properties_get(_init_cyclades, _optional_json):
616 """Get an image property"""
619 @errors.cyclades.connection
621 @errors.plankton.metadata
622 def _run(self, image_id, key):
623 self._print(self.client.get_image_metadata(image_id, key), print_dict)
625 def main(self, image_id, key):
626 super(self.__class__, self)._run()
627 self._run(image_id=image_id, key=key)
631 class image_compute_properties_add(_init_cyclades, _optional_json):
632 """Add a property to an image"""
635 @errors.cyclades.connection
637 @errors.plankton.metadata
638 def _run(self, image_id, key, val):
640 self.client.create_image_metadata(image_id, key, val), print_dict)
642 def main(self, image_id, key, val):
643 super(self.__class__, self)._run()
644 self._run(image_id=image_id, key=key, val=val)
648 class image_compute_properties_set(_init_cyclades, _optional_json):
649 """Add / update a set of properties for an image
650 proeprties must be given in the form key=value, e.v.
651 /image compute properties set <image-id> key1=val1 key2=val2
655 @errors.cyclades.connection
657 def _run(self, image_id, keyvals):
659 for keyval in keyvals:
660 key, val = keyval.split('=')
663 self.client.update_image_metadata(image_id, **meta), print_dict)
665 def main(self, image_id, *key_equals_value):
666 super(self.__class__, self)._run()
667 self._run(image_id=image_id, keyvals=key_equals_value)
671 class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
672 """Delete a property from an image"""
675 @errors.cyclades.connection
677 @errors.plankton.metadata
678 def _run(self, image_id, key):
679 self._optional_output(self.client.delete_image_metadata(image_id, key))
681 def main(self, image_id, key):
682 super(self.__class__, self)._run()
683 self._run(image_id=image_id, key=key)