-# Copyright 2012 GRNET S.A. All rights reserved.
+# Copyright 2012-2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# or implied, of GRNET S.A.command
from json import load, dumps
-from os.path import abspath
+from os import path
from logging import getLogger
+from io import StringIO
+from pydoc import pager
from kamaki.cli import command
from kamaki.cli.command_tree import CommandTree
-from kamaki.cli.utils import print_dict, print_json
+from kamaki.cli.utils import filter_dicts_by_dict
from kamaki.clients.image import ImageClient
from kamaki.clients.pithos import PithosClient
-from kamaki.clients.astakos import AstakosClient
from kamaki.clients import ClientError
-from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
-from kamaki.cli.argument import IntArgument
+from kamaki.cli.argument import (
+ FlagArgument, ValueArgument, RepeatableArgument, KeyValueArgument,
+ IntArgument, ProgressBarArgument)
from kamaki.cli.commands.cyclades import _init_cyclades
-from kamaki.cli.errors import raiseCLIError, CLIBaseUrlError
+from kamaki.cli.errors import (
+ raiseCLIError, CLIBaseUrlError, CLIInvalidArgument)
from kamaki.cli.commands import _command_init, errors, addLogSettings
-from kamaki.cli.commands import _optional_output_cmd, _optional_json
+from kamaki.cli.commands import (
+ _optional_output_cmd, _optional_json, _name_filter, _id_filter)
-image_cmds = CommandTree(
- 'image',
- 'Cyclades/Plankton API image commands\n'
- 'image compute:\tCyclades/Compute API image commands')
-_commands = [image_cmds]
+image_cmds = CommandTree('image', 'Cyclades/Plankton API image commands')
+imagecompute_cmds = CommandTree(
+ 'imagecompute', 'Cyclades/Compute API image commands')
+_commands = [image_cmds, imagecompute_cmds]
howto_image_file = [
'Kamaki commands to:',
- ' get current user id: /user authenticate',
- ' check available containers: /file list',
- ' create a new container: /file create <container>',
- ' check container contents: /file list <container>',
- ' upload files: /file upload <image file> <container>']
+ ' get current user id: kamaki user info',
+ ' check available containers: kamaki container list',
+ ' create a new container: kamaki container create CONTAINER',
+ ' check container contents: kamaki file list /CONTAINER',
+ ' upload files: kamaki file upload IMAGE_FILE /CONTAINER[/PATH]',
+ ' register an image:',
+ ' kamaki image register --name=IMAGE_NAME --location=/CONTAINER/PATH']
about_image_id = ['To see a list of available image ids: /image list']
if getattr(self, 'cloud', None):
img_url = self._custom_url('image') or self._custom_url('plankton')
if img_url:
- token = self._custom_token('image')\
- or self._custom_token('plankton')\
- or self.config.get_cloud(self.cloud, 'token')
+ token = self._custom_token('image') or self._custom_token(
+ 'plankton') or self.config.get_cloud(self.cloud, 'token')
self.client = ImageClient(base_url=img_url, token=token)
return
if getattr(self, 'auth_base', False):
:raises AssertionError: Valid json but invalid image properties dict
"""
- with open(abspath(filepath)) as f:
+ with open(path.abspath(filepath)) as f:
meta_dict = load(f)
try:
return _validate_image_meta(meta_dict)
def _validate_image_location(location):
"""
- :param location: (str) pithos://<user-id>/<container>/<img-file-path>
+ :param location: (str) pithos://<user-id>/<container>/<image-path>
- :returns: (<user-id>, <container>, <img-file-path>)
+ :returns: (<user-id>, <container>, <image-path>)
:raises AssertionError: if location is invalid
"""
@command(image_cmds)
-class image_list(_init_image, _optional_json):
+class image_list(_init_image, _optional_json, _name_filter, _id_filter):
"""List images accessible by user"""
+ PERMANENTS = (
+ 'id', 'name',
+ 'status', 'container_format', 'disk_format', 'size')
+
arguments = dict(
detail=FlagArgument('show detailed output', ('-l', '--details')),
container_format=ValueArgument(
'filter by container format',
'--container-format'),
disk_format=ValueArgument('filter by disk format', '--disk-format'),
- name=ValueArgument('filter by name', '--name'),
- name_pref=ValueArgument(
- 'filter by name prefix (case insensitive)',
- '--name-prefix'),
- name_suff=ValueArgument(
- 'filter by name suffix (case insensitive)',
- '--name-suffix'),
- name_like=ValueArgument(
- 'print only if name contains this (case insensitive)',
- '--name-like'),
size_min=IntArgument('filter by minimum size', '--size-min'),
size_max=IntArgument('filter by maximum size', '--size-max'),
status=ValueArgument('filter by status', '--status'),
owner=ValueArgument('filter by owner', '--owner'),
+ owner_name=ValueArgument('filter by owners username', '--owner-name'),
order=ValueArgument(
'order by FIELD ( - to reverse order)',
'--order',
more=FlagArgument(
'output results in pages (-n to set items per page, default 10)',
'--more'),
- enum=FlagArgument('Enumerate results', '--enumerate')
+ enum=FlagArgument('Enumerate results', '--enumerate'),
+ prop=KeyValueArgument('filter by property key=value', ('--property')),
+ prop_like=KeyValueArgument(
+ 'fliter by property key=value where value is part of actual value',
+ ('--property-like')),
+ image_ID_for_members=ValueArgument(
+ 'List members of an image', '--members-of'),
)
- def _filtered_by_owner(self, detail, *list_params):
- images = []
- MINKEYS = set([
- 'id', 'size', 'status', 'disk_format', 'container_format', 'name'])
- for img in self.client.list_public(True, *list_params):
- if img['owner'] == self['owner']:
- if not detail:
- for key in set(img.keys()).difference(MINKEYS):
- img.pop(key)
- images.append(img)
+ def _filter_by_owner(self, images):
+ ouuid = self['owner'] or self._username2uuid(self['owner_name'])
+ return filter_dicts_by_dict(images, dict(owner=ouuid))
+
+ def _add_owner_name(self, images):
+ uuids = self._uuids2usernames(
+ list(set([img['owner'] for img in images])))
+ for img in images:
+ img['owner'] += ' (%s)' % uuids[img['owner']]
return images
- def _filtered_by_name(self, images):
- np, ns, nl = self['name_pref'], self['name_suff'], self['name_like']
- return [img for img in images if (
- (not np) or img['name'].lower().startswith(np.lower())) and (
- (not ns) or img['name'].lower().endswith(ns.lower())) and (
- (not nl) or nl.lower() in img['name'].lower())]
+ def _filter_by_properties(self, images):
+ new_images = []
+ for img in images:
+ props = [dict(img['properties'])]
+ if self['prop']:
+ props = filter_dicts_by_dict(props, self['prop'])
+ if props and self['prop_like']:
+ props = filter_dicts_by_dict(
+ props, self['prop_like'], exact_match=False)
+ if props:
+ new_images.append(img)
+ return new_images
+
+ def _members(self, image_id):
+ members = self.client.list_members(image_id)
+ if not (self['json_output'] or self['output_format']):
+ uuids = [member['member_id'] for member in members]
+ usernames = self._uuids2usernames(uuids)
+ for member in members:
+ member['member_id'] += ' (%s)' % usernames[member['member_id']]
+ self._print(members, title=('member_id',))
@errors.generic.all
@errors.cyclades.connection
def _run(self):
super(self.__class__, self)._run()
+ if self['image_ID_for_members']:
+ return self._members(self['image_ID_for_members'])
filters = {}
for arg in set([
'container_format',
filters[arg] = self[arg]
order = self['order']
- detail = self['detail']
- if self['owner']:
- images = self._filtered_by_owner(detail, filters, order)
- else:
- images = self.client.list_public(detail, filters, order)
-
- images = self._filtered_by_name(images)
+ detail = self['detail'] or (
+ self['prop'] or self['prop_like']) or (
+ self['owner'] or self['owner_name'])
+
+ images = self.client.list_public(detail, filters, order)
+
+ if self['owner'] or self['owner_name']:
+ images = self._filter_by_owner(images)
+ if self['prop'] or self['prop_like']:
+ images = self._filter_by_properties(images)
+ images = self._filter_by_id(images)
+ images = self._non_exact_name_filter(images)
+
+ if self['detail'] and not (
+ self['json_output'] or self['output_format']):
+ images = self._add_owner_name(images)
+ elif detail and not self['detail']:
+ for img in images:
+ for key in set(img).difference(self.PERMANENTS):
+ img.pop(key)
kwargs = dict(with_enumeration=self['enum'])
- if self['more']:
- kwargs['page_size'] = self['limit'] or 10
- elif self['limit']:
+ if self['limit']:
images = images[:self['limit']]
+ if self['more']:
+ kwargs['out'] = StringIO()
+ kwargs['title'] = ()
self._print(images, **kwargs)
+ if self['more']:
+ pager(kwargs['out'].getvalue())
def main(self):
super(self.__class__, self)._run()
@command(image_cmds)
-class image_meta(_init_image, _optional_json):
- """Get image metadata
- Image metadata include:
- - image file information (location, size, etc.)
- - image information (id, name, etc.)
- - image os properties (os, fs, etc.)
+class image_info(_init_image, _optional_json):
+ """Get image metadata"""
+
+ @errors.generic.all
+ @errors.plankton.connection
+ @errors.plankton.id
+ def _run(self, image_id):
+ meta = self.client.get_meta(image_id)
+ if not (self['json_output'] or self['output_format']):
+ meta['owner'] += ' (%s)' % self._uuid2username(meta['owner'])
+ self._print(meta, self.print_dict)
+
+ def main(self, image_id):
+ super(self.__class__, self)._run()
+ self._run(image_id=image_id)
+
+
+@command(image_cmds)
+class image_modify(_init_image, _optional_output_cmd):
+ """Add / update metadata and properties for an image
+ The original image preserves the values that are not affected
"""
+ arguments = dict(
+ image_name=ValueArgument('Change name', '--name'),
+ disk_format=ValueArgument('Change disk format', '--disk-format'),
+ container_format=ValueArgument(
+ 'Change container format', '--container-format'),
+ status=ValueArgument('Change status', '--status'),
+ publish=FlagArgument('Make the image public', '--public'),
+ unpublish=FlagArgument('Make the image private', '--private'),
+ property_to_set=KeyValueArgument(
+ 'set property in key=value form (can be repeated)',
+ ('-p', '--property-set')),
+ property_to_del=RepeatableArgument(
+ 'Delete property by key (can be repeated)', '--property-del'),
+ member_ID_to_add=RepeatableArgument(
+ 'Add member to image (can be repeated)', '--member-add'),
+ member_ID_to_remove=RepeatableArgument(
+ 'Remove a member (can be repeated)', '--member-del'),
+ )
+ required = [
+ 'image_name', 'disk_format', 'container_format', 'status', 'publish',
+ 'unpublish', 'property_to_set', 'member_ID_to_add',
+ 'member_ID_to_remove', 'property_to_del']
+
@errors.generic.all
@errors.plankton.connection
@errors.plankton.id
def _run(self, image_id):
- self._print([self.client.get_meta(image_id)])
+ for mid in (self['member_ID_to_add'] or []):
+ self.client.add_member(image_id, mid)
+ for mid in (self['member_ID_to_remove'] or []):
+ self.client.remove_member(image_id, mid)
+ meta = self.client.get_meta(image_id)
+ for k, v in self['property_to_set'].items():
+ meta['properties'][k.upper()] = v
+ for k in (self['property_to_del'] or []):
+ meta['properties'][k.upper()] = None
+ self._optional_output(self.client.update_image(
+ image_id,
+ name=self['image_name'],
+ disk_format=self['disk_format'],
+ container_format=self['container_format'],
+ status=self['status'],
+ public=self['publish'] or (False if self['unpublish'] else None),
+ **meta['properties']))
+ if self['with_output']:
+ self._optional_output(self.get_image_details(image_id))
def main(self, image_id):
super(self.__class__, self)._run()
self._run(image_id=image_id)
+class PithosLocationArgument(ValueArgument):
+ """Resolve pithos URI, return in the form pithos://uuid/container[/path]
+
+ UPDATE: URLs without a path are also resolvable. Therefore, caller methods
+ should check if there is a path or not
+ """
+
+ def __init__(
+ self, help=None, parsed_name=None, default=None, user_uuid=None):
+ super(PithosLocationArgument, self).__init__(
+ help=help, parsed_name=parsed_name, default=default)
+ self.uuid, self.container, self.path = user_uuid, None, None
+
+ def setdefault(self, term, value):
+ if not getattr(self, term, None):
+ setattr(self, term, value)
+
+ @property
+ def value(self):
+ path = ('/%s' % self.path) if self.path else ''
+ return 'pithos://%s/%s%s' % (self.uuid, self.container, path)
+
+ @value.setter
+ def value(self, location):
+ if location:
+ from kamaki.cli.commands.pithos import _pithos_container as pc
+ try:
+ uuid, self.container, self.path = pc._resolve_pithos_url(
+ location)
+ self.uuid = uuid or self.uuid
+ assert self.container, 'No container in pithos URI'
+ except Exception as e:
+ raise CLIInvalidArgument(
+ 'Invalid Pithos+ location %s (%s)' % (location, e),
+ details=[
+ 'The image location must be a valid Pithos+',
+ 'location. There are two valid formats:',
+ ' pithos://USER_UUID/CONTAINER[/PATH]',
+ 'OR',
+ ' /CONTAINER[/PATH]',
+ 'To see all containers:',
+ ' [kamaki] container list',
+ 'To list the contents of a container:',
+ ' [kamaki] container list CONTAINER'])
+
+
@command(image_cmds)
class image_register(_init_image, _optional_json):
- """(Re)Register an image"""
+ """(Re)Register an image file to an Image service
+ The image file must be stored at a pithos repository
+ Some metadata can be set by user (e.g., disk-format) while others are set
+ only automatically (e.g., image id). There are also some custom user
+ metadata, called properties.
+ A register command creates a remote meta file at
+ /<container>/<image path>.meta
+ Users may download and edit this file and use it to re-register one or more
+ images.
+ In case of a meta file, runtime arguments for metadata or properties
+ override meta file settings.
+ """
+
+ container_info_cache = {}
arguments = dict(
- checksum=ValueArgument('set image checksum', '--checksum'),
+ checksum=ValueArgument('Set image checksum', '--checksum'),
container_format=ValueArgument(
- 'set container format',
- '--container-format'),
- disk_format=ValueArgument('set disk format', '--disk-format'),
- owner=ValueArgument('set image owner (admin only)', '--owner'),
+ 'Set container format', '--container-format'),
+ disk_format=ValueArgument('Set disk format', '--disk-format'),
+ owner_name=ValueArgument('Set user uuid by user name', '--owner-name'),
properties=KeyValueArgument(
- 'add property in key=value form (can be repeated)',
+ 'Add property (user-specified metadata) in key=value form'
+ '(can be repeated)',
('-p', '--property')),
- is_public=FlagArgument('mark image as public', '--public'),
- size=IntArgument('set image size', '--size'),
+ is_public=FlagArgument('Mark image as public', '--public'),
+ size=IntArgument('Set image size in bytes', '--size'),
metafile=ValueArgument(
'Load metadata from a json-formated file <img-file>.meta :'
'{"key1": "val1", "key2": "val2", ..., "properties: {...}"}',
('--metafile')),
- metafile_force=FlagArgument(
- 'Store remote metadata object, even if it already exists',
+ force_upload=FlagArgument(
+ 'Overwrite remote files (image file, metadata file)',
('-f', '--force')),
no_metafile_upload=FlagArgument(
'Do not store metadata in remote meta file',
('--no-metafile-upload')),
-
+ container=ValueArgument(
+ 'Pithos+ container containing the image file',
+ ('-C', '--container')),
+ uuid=ValueArgument('Custom user uuid', '--uuid'),
+ local_image_path=ValueArgument(
+ 'Local image file path to upload and register '
+ '(still need target file in the form /container/remote-path )',
+ '--upload-image-file'),
+ progress_bar=ProgressBarArgument(
+ 'Do not use progress bar', '--no-progress-bar', default=False),
+ name=ValueArgument('The name of the new image', '--name'),
+ pithos_location=PithosLocationArgument(
+ 'The Pithos+ image location to put the image at. Format: '
+ 'pithos://USER_UUID/CONTAINER/IMAGE or '
+ '/CONTAINER/IMAGE',
+ '--location')
)
+ required = ('name', 'pithos_location')
- def _get_user_id(self):
- atoken = self.client.token
- if getattr(self, 'auth_base', False):
- return self.auth_base.term('id', atoken)
- else:
- astakos_url = self.config.get('user', 'url')\
- or self.config.get('astakos', 'url')
- if not astakos_url:
- raise CLIBaseUrlError(service='astakos')
- user = AstakosClient(astakos_url, atoken)
- return user.term('id')
-
- def _get_pithos_client(self, container):
- if self['no_metafile_upload']:
- return None
+ def _get_pithos_client(self, locator):
ptoken = self.client.token
if getattr(self, 'auth_base', False):
pithos_endpoints = self.auth_base.get_service_endpoints(
purl = self.config.get_cloud('pithos', 'url')
if not purl:
raise CLIBaseUrlError(service='pithos')
- return PithosClient(purl, ptoken, self._get_user_id(), container)
-
- def _store_remote_metafile(self, pclient, remote_path, metadata):
- return pclient.upload_from_string(
- remote_path, _validate_image_meta(metadata, return_str=True))
+ return PithosClient(purl, ptoken, locator.uuid, locator.container)
def _load_params_from_file(self, location):
params, properties = dict(), dict()
try:
for k, v in _load_image_meta(pfile).items():
key = k.lower().replace('-', '_')
- if k == 'properties':
+ if key == 'properties':
for pk, pv in v.items():
properties[pk.upper().replace('-', '_')] = pv
elif key == 'name':
for k, v in self['properties'].items():
properties[k.upper().replace('-', '_')] = v
- def _validate_location(self, location):
- if not location:
- raiseCLIError(
- 'No image file location provided',
- importance=2, details=[
- 'An image location is needed. Image location format:',
- ' pithos://<user-id>/<container>/<path>',
- ' an image file at the above location must exist.'
- ] + howto_image_file)
- try:
- return _validate_image_location(location)
- except AssertionError as ae:
- raiseCLIError(
- ae, 'Invalid image location format',
- importance=1, details=[
- 'Valid image location format:',
- ' pithos://<user-id>/<container>/<img-file-path>'
- ] + howto_image_file)
+ def _assert_remote_file_not_exist(self, pithos, path):
+ if pithos and not self['force_upload']:
+ try:
+ pithos.get_object_info(path)
+ raiseCLIError(
+ 'Remote file /%s/%s already exists' % (
+ pithos.container, path),
+ importance=2,
+ details=[
+ 'Registration ABORTED',
+ 'Use %s to force upload' % self.arguments[
+ 'force_upload'].lvalue])
+ except ClientError as ce:
+ if ce.status != 404:
+ raise
@errors.generic.all
@errors.plankton.connection
- def _run(self, name, location):
- (params, properties, location) = self._load_params_from_file(location)
- uuid, container, img_path = self._validate_location(location)
+ def _run(self, name, locator):
+ location, pithos = locator.value, None
+ if self['local_image_path']:
+ with open(self['local_image_path']) as f:
+ pithos = self._get_pithos_client(locator)
+ self._assert_remote_file_not_exist(pithos, locator.path)
+ (pbar, upload_cb) = self._safe_progress_bar('Uploading')
+ if pbar:
+ hash_bar = pbar.clone()
+ hash_cb = hash_bar.get_generator('Calculating hashes')
+ pithos.upload_object(
+ locator.path, f,
+ hash_cb=hash_cb, upload_cb=upload_cb,
+ container_info_cache=self.container_info_cache)
+ pbar.finish()
+
+ (params, properties, new_loc) = self._load_params_from_file(location)
+ if location != new_loc:
+ locator.value = new_loc
self._load_params_from_args(params, properties)
- pclient = self._get_pithos_client(container)
- #check if metafile exists
- meta_path = '%s.meta' % img_path
- if pclient and not self['metafile_force']:
- try:
- pclient.get_object_info(meta_path)
- raiseCLIError('Metadata file %s:%s already exists' % (
- container, meta_path))
- except ClientError as ce:
- if ce.status != 404:
- raise
+ if not self['no_metafile_upload']:
+ #check if metafile exists
+ pithos = pithos or self._get_pithos_client(locator)
+ meta_path = '%s.meta' % locator.path
+ self._assert_remote_file_not_exist(pithos, meta_path)
#register the image
try:
r = self.client.register(name, location, params, properties)
except ClientError as ce:
- if ce.status in (400, ):
+ if ce.status in (400, 404):
raiseCLIError(
- ce, 'Nonexistent image file location %s' % location,
+ ce, 'Nonexistent image file location\n\t%s' % location,
details=[
- 'Make sure the image file exists'] + howto_image_file)
+ 'Does the image file %s exist at container %s ?' % (
+ locator.path,
+ locator.container)] + howto_image_file)
raise
- self._print(r, print_dict)
+ r['owner'] += ' (%s)' % self._uuid2username(r['owner'])
+ self._print(r, self.print_dict)
#upload the metadata file
- if pclient:
+ if not self['no_metafile_upload']:
try:
- meta_headers = pclient.upload_from_string(
- meta_path, dumps(r, indent=2))
+ meta_headers = pithos.upload_from_string(
+ meta_path, dumps(r, indent=2),
+ sharing=dict(read='*' if params.get('is_public') else ''),
+ container_info_cache=self.container_info_cache)
except TypeError:
- print('Failed to dump metafile %s:%s' % (container, meta_path))
+ self.error(
+ 'Failed to dump metafile /%s/%s' % (
+ locator.container, meta_path))
return
- if self['json_output']:
- print_json(dict(
- metafile_location='%s:%s' % (container, meta_path),
+ if self['json_output'] or self['output_format']:
+ self.print_json(dict(
+ metafile_location='/%s/%s' % (
+ locator.container, meta_path),
headers=meta_headers))
else:
- print('Metadata file uploaded as %s:%s (version %s)' % (
- container, meta_path, meta_headers['x-object-version']))
+ self.error('Metadata file uploaded as /%s/%s (version %s)' % (
+ locator.container,
+ meta_path,
+ meta_headers['x-object-version']))
- def main(self, name, location):
+ def main(self):
super(self.__class__, self)._run()
- self._run(name, location)
+
+ locator, pithos = self.arguments['pithos_location'], None
+ locator.setdefault('uuid', self.auth_base.user_term('id'))
+ locator.path = locator.path or path.basename(
+ self['local_image_path'] or '')
+ if not locator.path:
+ raise CLIInvalidArgument(
+ 'Missing the image file or object', details=[
+ 'Pithos+ URI %s does not point to a physical image' % (
+ locator.value),
+ 'A physical image is necessary.',
+ 'It can be a remote Pithos+ object or a local file.',
+ 'To specify a remote image object:',
+ ' %s [pithos://UUID]/CONTAINER/PATH' % locator.lvalue,
+ 'To specify a local file:',
+ ' %s [pithos://UUID]/CONTAINER[/PATH] %s LOCAL_PATH' % (
+ locator.lvalue,
+ self.arguments['local_image_path'].lvalue)
+ ])
+ self.arguments['pithos_location'].setdefault(
+ 'uuid', self.auth_base.user_term('id'))
+ self._run(self['name'], locator)
@command(image_cmds)
self._run(image_id=image_id)
-@command(image_cmds)
-class image_shared(_init_image, _optional_json):
- """List images shared by a member"""
-
- @errors.generic.all
- @errors.plankton.connection
- def _run(self, member):
- self._print(self.client.list_shared(member), title=('image_id',))
-
- def main(self, member):
- super(self.__class__, self)._run()
- self._run(member)
-
-
-@command(image_cmds)
-class image_members(_init_image):
- """Manage members. Members of an image are users who can modify it"""
-
-
-@command(image_cmds)
-class image_members_list(_init_image, _optional_json):
- """List members of an image"""
-
- @errors.generic.all
- @errors.plankton.connection
- @errors.plankton.id
- def _run(self, image_id):
- self._print(self.client.list_members(image_id), title=('member_id',))
-
- def main(self, image_id):
- super(self.__class__, self)._run()
- self._run(image_id=image_id)
-
-
-@command(image_cmds)
-class image_members_add(_init_image, _optional_output_cmd):
- """Add a member to an image"""
-
- @errors.generic.all
- @errors.plankton.connection
- @errors.plankton.id
- def _run(self, image_id=None, member=None):
- self._optional_output(self.client.add_member(image_id, member))
-
- def main(self, image_id, member):
- super(self.__class__, self)._run()
- self._run(image_id=image_id, member=member)
-
-
-@command(image_cmds)
-class image_members_delete(_init_image, _optional_output_cmd):
- """Remove a member from an image"""
-
- @errors.generic.all
- @errors.plankton.connection
- @errors.plankton.id
- def _run(self, image_id=None, member=None):
- self._optional_output(self.client.remove_member(image_id, member))
-
- def main(self, image_id, member):
- super(self.__class__, self)._run()
- self._run(image_id=image_id, member=member)
-
-
-@command(image_cmds)
-class image_members_set(_init_image, _optional_output_cmd):
- """Set the members of an image"""
-
- @errors.generic.all
- @errors.plankton.connection
- @errors.plankton.id
- def _run(self, image_id, members):
- self._optional_output(self.client.set_members(image_id, members))
-
- def main(self, image_id, *members):
- super(self.__class__, self)._run()
- self._run(image_id=image_id, members=members)
-
-
# Compute Image Commands
-
-@command(image_cmds)
-class image_compute(_init_cyclades):
- """Cyclades/Compute API image commands"""
-
-
-@command(image_cmds)
-class image_compute_list(_init_cyclades, _optional_json):
+@command(imagecompute_cmds)
+class imagecompute_list(
+ _init_cyclades, _optional_json, _name_filter, _id_filter):
"""List images"""
+ PERMANENTS = ('id', 'name')
+
arguments = dict(
detail=FlagArgument('show detailed output', ('-l', '--details')),
limit=IntArgument('limit number listed images', ('-n', '--number')),
- more=FlagArgument(
- 'output results in pages (-n to set items per page, default 10)',
- '--more'),
- enum=FlagArgument('Enumerate results', '--enumerate')
+ more=FlagArgument('handle long lists of results', '--more'),
+ enum=FlagArgument('Enumerate results', '--enumerate'),
+ user_id=ValueArgument('filter by user_id', '--user-id'),
+ user_name=ValueArgument('filter by username', '--user-name'),
+ meta=KeyValueArgument(
+ 'filter by metadata key=value (can be repeated)', ('--metadata')),
+ meta_like=KeyValueArgument(
+ 'filter by metadata key=value (can be repeated)',
+ ('--metadata-like'))
)
+ def _filter_by_metadata(self, images):
+ new_images = []
+ for img in images:
+ meta = [dict(img['metadata'])]
+ if self['meta']:
+ meta = filter_dicts_by_dict(meta, self['meta'])
+ if meta and self['meta_like']:
+ meta = filter_dicts_by_dict(
+ meta, self['meta_like'], exact_match=False)
+ if meta:
+ new_images.append(img)
+ return new_images
+
+ def _filter_by_user(self, images):
+ uuid = self['user_id'] or self._username2uuid(self['user_name'])
+ return filter_dicts_by_dict(images, dict(user_id=uuid))
+
+ def _add_name(self, images, key='user_id'):
+ uuids = self._uuids2usernames(
+ list(set([img[key] for img in images])))
+ for img in images:
+ img[key] += ' (%s)' % uuids[img[key]]
+ return images
+
@errors.generic.all
@errors.cyclades.connection
def _run(self):
- images = self.client.list_images(self['detail'])
+ withmeta = bool(self['meta'] or self['meta_like'])
+ withuser = bool(self['user_id'] or self['user_name'])
+ detail = self['detail'] or withmeta or withuser
+ images = self.client.list_images(detail)
+ images = self._filter_by_name(images)
+ images = self._filter_by_id(images)
+ if withuser:
+ images = self._filter_by_user(images)
+ if withmeta:
+ images = self._filter_by_metadata(images)
+ if self['detail'] and not (
+ self['json_output'] or self['output_format']):
+ images = self._add_name(self._add_name(images, 'tenant_id'))
+ elif detail and not self['detail']:
+ for img in images:
+ for key in set(img).difference(self.PERMANENTS):
+ img.pop(key)
kwargs = dict(with_enumeration=self['enum'])
- if self['more']:
- kwargs['page_size'] = self['limit'] or 10
- elif self['limit']:
+ if self['limit']:
images = images[:self['limit']]
+ if self['more']:
+ kwargs['out'] = StringIO()
+ kwargs['title'] = ()
self._print(images, **kwargs)
+ if self['more']:
+ pager(kwargs['out'].getvalue())
def main(self):
super(self.__class__, self)._run()
self._run()
-@command(image_cmds)
-class image_compute_info(_init_cyclades, _optional_json):
+@command(imagecompute_cmds)
+class imagecompute_info(_init_cyclades, _optional_json):
"""Get detailed information on an image"""
@errors.generic.all
@errors.plankton.id
def _run(self, image_id):
image = self.client.get_image_details(image_id)
- self._print(image, print_dict)
+ uuids = [image['user_id'], image['tenant_id']]
+ usernames = self._uuids2usernames(uuids)
+ image['user_id'] += ' (%s)' % usernames[image['user_id']]
+ image['tenant_id'] += ' (%s)' % usernames[image['tenant_id']]
+ self._print(image, self.print_dict)
def main(self, image_id):
super(self.__class__, self)._run()
self._run(image_id=image_id)
-@command(image_cmds)
-class image_compute_delete(_init_cyclades, _optional_output_cmd):
+@command(imagecompute_cmds)
+class imagecompute_delete(_init_cyclades, _optional_output_cmd):
"""Delete an image (WARNING: image file is also removed)"""
@errors.generic.all
self._run(image_id=image_id)
-@command(image_cmds)
-class image_compute_properties(_init_cyclades):
- """Manage properties related to OS installation in an image"""
-
+@command(imagecompute_cmds)
+class imagecompute_modify(_init_cyclades, _optional_output_cmd):
+ """Modify image properties (metadata)"""
-@command(image_cmds)
-class image_compute_properties_list(_init_cyclades, _optional_json):
- """List all image properties"""
+ arguments = dict(
+ property_to_add=KeyValueArgument(
+ 'Add property in key=value format (can be repeated)',
+ ('--property-add')),
+ property_to_del=RepeatableArgument(
+ 'Delete property by key (can be repeated)',
+ ('--property-del'))
+ )
+ required = ['property_to_add', 'property_to_del']
@errors.generic.all
@errors.cyclades.connection
@errors.plankton.id
def _run(self, image_id):
- self._print(self.client.get_image_metadata(image_id), print_dict)
+ if self['property_to_add']:
+ self.client.update_image_metadata(
+ image_id, **self['property_to_add'])
+ for key in (self['property_to_del'] or []):
+ self.client.delete_image_metadata(image_id, key)
+ if self['with_output']:
+ self._optional_output(self.client.get_image_details(image_id))
def main(self, image_id):
super(self.__class__, self)._run()
self._run(image_id=image_id)
-
-
-@command(image_cmds)
-class image_compute_properties_get(_init_cyclades, _optional_json):
- """Get an image property"""
-
- @errors.generic.all
- @errors.cyclades.connection
- @errors.plankton.id
- @errors.plankton.metadata
- def _run(self, image_id, key):
- self._print(self.client.get_image_metadata(image_id, key), print_dict)
-
- def main(self, image_id, key):
- super(self.__class__, self)._run()
- self._run(image_id=image_id, key=key)
-
-
-@command(image_cmds)
-class image_compute_properties_add(_init_cyclades, _optional_json):
- """Add a property to an image"""
-
- @errors.generic.all
- @errors.cyclades.connection
- @errors.plankton.id
- @errors.plankton.metadata
- def _run(self, image_id, key, val):
- self._print(
- self.client.create_image_metadata(image_id, key, val), print_dict)
-
- def main(self, image_id, key, val):
- super(self.__class__, self)._run()
- self._run(image_id=image_id, key=key, val=val)
-
-
-@command(image_cmds)
-class image_compute_properties_set(_init_cyclades, _optional_json):
- """Add / update a set of properties for an image
- proeprties must be given in the form key=value, e.v.
- /image compute properties set <image-id> key1=val1 key2=val2
- """
-
- @errors.generic.all
- @errors.cyclades.connection
- @errors.plankton.id
- def _run(self, image_id, keyvals):
- meta = dict()
- for keyval in keyvals:
- key, val = keyval.split('=')
- meta[key] = val
- self._print(
- self.client.update_image_metadata(image_id, **meta), print_dict)
-
- def main(self, image_id, *key_equals_value):
- super(self.__class__, self)._run()
- self._run(image_id=image_id, keyvals=key_equals_value)
-
-
-@command(image_cmds)
-class image_compute_properties_delete(_init_cyclades, _optional_output_cmd):
- """Delete a property from an image"""
-
- @errors.generic.all
- @errors.cyclades.connection
- @errors.plankton.id
- @errors.plankton.metadata
- def _run(self, image_id, key):
- self._optional_output(self.client.delete_image_metadata(image_id, key))
-
- def main(self, image_id, key):
- super(self.__class__, self)._run()
- self._run(image_id=image_id, key=key)