Commit d1df42a7 authored by Stavros Sachtouris's avatar Stavros Sachtouris
Browse files

Merge branch 'feature-image-property-commands' into develop

parents 900341f4 9c9169ac
......@@ -14,6 +14,7 @@ Changes:
- Enrich client API docs with examples and astakos endpoint information [#4135]
- Show user names in /file sharers [#4203]
- Show user name in image/server/network list [#4228]
- Remove kamaki compute image properties add [#4231]
Features:
......@@ -50,4 +51,5 @@ Features:
list of user uuids [#4203]
- Use multiformed ranges in file/pithos [#4059]
- Implement more filters in image/network/flavor/server/file list [#4220]
- Implement /image meta list/set/delete [#4231] metadata and properties
......@@ -240,6 +240,103 @@ Register the image (don't forget the -f parameter, to override the metafile).
Metadata file uploaded as pithos:debian_base3.diskdump.meta (version 1359)
[kamaki]:
Metadata and Property modification
----------------------------------
Image metadata and custom properties can be modified even after the image is
registered. Metadata are fixed image attributes, like name, disk format etc.
while custom properties are set by the image owner and, usually, refer to
attributes of the images OS.
Let's rename the image:
.. code-block:: console
[kamaki]: image meta set 7h1rd-1m4g3-1d --name='Changed Name'
[kamaki]:
If we, now, list the image metadata, we will see that the name is changed:
.. code-block:: console
[kamaki]: image meta list 7h1rd-1m4g3-1d
checksum: 3cb03556ec971f...e8dd6190443b560cb7
container-format: bare
created-at: 2013-06-19 08:00:22
deleted-at:
disk-format: diskdump
id: 7h1rd-1m4g3-1d
is-public: False
location: pithos://s0m3-u53r-1d/pithos/debian_base3.diskdump
name: Changed Name
owner: s0m3-u53r-1d
properties:
OS: Linux
USER: root
size: 903471104
status: available
updated-at: 2013-06-19 08:01:00
[kamaki]:
We can use the same idea to change the values of other metadata like disk
format, container format or status. On the other hand, we cannot modify the
id, owner, location, checksum and dates. E.g., to publish and unpublish:
.. code-block:: console
[kamaki]: image meta set 7h1rd-1m4g3-1d --publish --name='Debian Base Gama'
[kamaki]: image meta set 7h1rd-1m4g3-1d --unpublish
[kamaki]:
The first call published the image (set is-public to True) and also restored
the name to "Debian Base Gama". The second one unpublished the image (set
is-public to False).
To delete metadata, use the image meta delete method:
.. code-block:: console
[kamaki]: image meta delete 7h1rd-1m4g3-1d status
[kamaki]:
will empty the value of "status".
These operations can be used for properties with the same semantics:
.. code-block:: console
[kamaki]: image meta set 7h1rd-1m4g3-1d -p user=user
[kamaki]: image meta list 7h1rd-1m4g3-1d
...
properties:
OS: Linux
USER: user
...
[kamaki]:
Just to test the feature, let's create a property "greet" with value
"hi there", and then remove it. Also, let's restore the value of USER:
.. code-block:: console
[kamaki]: image meta set 7h1rd-1m4g3-1d -p greet='Hi there' -p user=root
[kamaki]: image meta list 7h1rd-1m4g3-1d
...
properties:
OS: Linux
USER: root
GREET: Hi there
...
[kamaki]: image meta delete 7h1rd-1m4g3-1d -p greet
[kamaki]: image meta list 7h1rd-1m4g3-1d
...
properties:
OS: Linux
USER: root
...
[kamaki]:
Reregistration: priorities and overrides
----------------------------------------
......
......@@ -69,7 +69,10 @@ image (Plankton commands + Compute Image subcommands)
.. code-block:: text
list : List images accessible by user
meta : Get image metadata
meta : Manage image metadata
list : Get image metadata
set : Add / update metadata and properties for an image
delete : Remove/empty image metadata and/or custom properties
register : (Re)Register an image
unregister: Unregister an image (does not delete the image file)
shared : List shared images
......@@ -78,7 +81,6 @@ image (Plankton commands + Compute Image subcommands)
delete : Delete image
info : Get image details
properties: Manage properties related to OS installation in an image
add : Add a property to an image
delete: Delete a property from an image
get : Get an image property
list : List all image properties
......@@ -162,7 +164,6 @@ server (Compute/Cyclades)
list : List server metadata
set : Add / update server metadata
delete: Delete a piece of server metadata
meta : Get a server's metadata
reboot : Reboot a server
rename : Update a server's name
shutdown: Shutdown a server
......
......@@ -153,7 +153,10 @@ image commands
**************
* list List images accessible by user
* meta Get image metadata
* meta Manage image metadata
* list Get image metadata
* set Add / update metadata and properties for an image
* delete Remove/empty image metadata and/or custom properties
* register (Re)Register an image
* unregister Unregister an image (does not delete the image file)
* shared List shared images
......@@ -162,7 +165,6 @@ image commands
* delete Delete image
* info Get image details
* properties Manage properties related to OS installation in an image
* add Add a property to an image
* delete Delete a property from an image
* get Get an image property
* list List all image properties
......
......@@ -492,7 +492,7 @@ Here is a list of settings needed:
.. code-block:: console
$ kamaki image meta <img id> -j > img.details
$ kamaki image meta list <img id> -j > img.details
* livetest.image_id = <A valid image id used for testing>
* livetest.image_local_path = <The local path of the testing image>
......
......@@ -274,8 +274,16 @@ class VersionArgument(FlagArgument):
print('kamaki %s' % kamaki.__version__)
class RepeatableArgument(Argument):
"""A value argument that can be repeated"""
def __init__(self, help='', parsed_name=None, default=[]):
super(RepeatableArgument, self).__init__(
-1, help, parsed_name, default)
class KeyValueArgument(Argument):
"""A Value Argument that can be repeated
"""A Key=Value Argument that can be repeated
:syntax: --<arg> key1=value1 --<arg> key2=value2 ...
"""
......
......@@ -291,6 +291,16 @@ class VersionArgument(TestCase):
self.assertEqual(va.value, 'some value')
class RepeatableArgument(TestCase):
@patch('%s.Argument.__init__' % arg_path)
def test___init__(self, init):
help, pname, default = 'help', 'pname', 'default'
kva = argument.RepeatableArgument(help, pname, default)
self.assertTrue(isinstance(kva, argument.RepeatableArgument))
self.assertEqual(init.mock_calls[-1], call(-1, help, pname, default))
class KeyValueArgument(TestCase):
@patch('%s.Argument.__init__' % arg_path)
......@@ -497,6 +507,7 @@ if __name__ == '__main__':
runTestCase(IntArgument, 'IntArgument', argv[1:])
runTestCase(DateArgument, 'DateArgument', argv[1:])
runTestCase(VersionArgument, 'VersionArgument', argv[1:])
runTestCase(RepeatableArgument, 'RepeatableArgument', argv[1:])
runTestCase(KeyValueArgument, 'KeyValueArgument', argv[1:])
runTestCase(ProgressBarArgument, 'ProgressBarArgument', argv[1:])
runTestCase(ArgumentParseManager, 'ArgumentParseManager', argv[1:])
......@@ -42,8 +42,9 @@ 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, ProgressBarArgument
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.commands import _command_init, errors, addLogSettings
......@@ -277,7 +278,12 @@ class image_list(_init_image, _optional_json, _name_filter, _id_filter):
@command(image_cmds)
class image_meta(_init_image, _optional_json):
class image_meta(_init_image):
"""Manage image metadata and custom properties"""
@command(image_cmds)
class image_meta_list(_init_image, _optional_json):
"""Get image metadata
Image metadata include:
- image file information (location, size, etc.)
......@@ -289,7 +295,104 @@ class image_meta(_init_image, _optional_json):
@errors.plankton.connection
@errors.plankton.id
def _run(self, image_id):
self._print([self.client.get_meta(image_id)])
meta = self.client.get_meta(image_id)
if not self['json_output']:
meta['owner'] += ' (%s)' % self._uuid2username(meta['owner'])
self._print(meta, print_dict)
def main(self, image_id):
super(self.__class__, self)._run()
self._run(image_id=image_id)
@command(image_cmds)
class image_meta_set(_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(
name=ValueArgument('Set a new name', ('--name')),
disk_format=ValueArgument('Set a new disk format', ('--disk-format')),
container_format=ValueArgument(
'Set a new container format', ('--container-format')),
status=ValueArgument('Set a new status', ('--status')),
publish=FlagArgument('publish the image', ('--publish')),
unpublish=FlagArgument('unpublish the image', ('--unpublish')),
properties=KeyValueArgument(
'set property in key=value form (can be repeated)',
('-p', '--property'))
)
def _check_empty(self):
for term in (
'name', 'disk_format', 'container_format', 'status', 'publish',
'unpublish', 'properties'):
if self['term']:
if self['publish'] and self['unpublish']:
raiseCLIError(
'--publish and --unpublish are mutually exclusive')
return
raiseCLIError(
'Nothing to update, please use arguments (-h for a list)')
@errors.generic.all
@errors.plankton.connection
@errors.plankton.id
def _run(self, image_id):
self._check_empty()
meta = self.client.get_meta(image_id)
for k, v in self['properties'].items():
meta['properties'][k.upper()] = v
self._optional_output(self.client.update_image(
image_id,
name=self['name'],
disk_format=self['disk_format'],
container_format=self['container_format'],
status=self['status'],
public=self['publish'] or self['unpublish'] or None,
**meta['properties']))
def main(self, image_id):
super(self.__class__, self)._run()
self._run(image_id=image_id)
@command(image_cmds)
class image_meta_delete(_init_image, _optional_output_cmd):
"""Remove/empty image metadata and/or custom properties"""
arguments = dict(
disk_format=FlagArgument('Empty disk format', ('--disk-format')),
container_format=FlagArgument(
'Empty container format', ('--container-format')),
status=FlagArgument('Empty status', ('--status')),
properties=RepeatableArgument(
'Property keys to remove', ('-p', '--property'))
)
def _check_empty(self):
for term in (
'disk_format', 'container_format', 'status', 'properties'):
if self[term]:
return
raiseCLIError(
'Nothing to update, please use arguments (-h for a list)')
@errors.generic.all
@errors.plankton.connection
@errors.plankton.id
def _run(self, image_id):
self._check_empty()
meta = self.client.get_meta(image_id)
for k in self['properties']:
meta['properties'].pop(k.upper(), None)
self._optional_output(self.client.update_image(
image_id,
disk_format='' if self['disk_format'] else None,
container_format='' if self['container_format'] else None,
status='' if self['status'] else None,
**meta['properties']))
def main(self, image_id):
super(self.__class__, self)._run()
......@@ -308,7 +411,7 @@ class image_register(_init_image, _optional_json):
'set container format',
'--container-format'),
disk_format=ValueArgument('set disk format', '--disk-format'),
owner=ValueArgument('set image owner (admin only)', '--owner'),
#owner=ValueArgument('set image owner (admin only)', '--owner'),
properties=KeyValueArgument(
'add property in key=value form (can be repeated)',
('-p', '--property')),
......@@ -610,7 +713,6 @@ class image_members_set(_init_image, _optional_output_cmd):
super(self.__class__, self)._run()
self._run(image_id=image_id, members=member_ids)
# Compute Image Commands
......@@ -768,21 +870,21 @@ class image_compute_properties_get(_init_cyclades, _optional_json):
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_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)
......
......@@ -41,7 +41,8 @@ from kamaki.cli.command_tree.test import Command, CommandTree
from kamaki.cli.argument.test import (
Argument, ConfigArgument, RuntimeConfigArgument, FlagArgument,
ValueArgument, IntArgument, DateArgument, VersionArgument,
KeyValueArgument, ProgressBarArgument, ArgumentParseManager)
RepeatableArgument, KeyValueArgument, ProgressBarArgument,
ArgumentParseManager)
from kamaki.cli.utils.test import UtilsMethods
......
......@@ -370,19 +370,19 @@ class ComputeClient(ComputeRestClient):
response_headers[k] = r.headers.get(k, v)
return r.json['meta' if key else 'metadata']
def create_image_metadata(self, image_id, key, val):
"""
:param image_id: integer (str or int)
# def create_image_metadata(self, image_id, key, val):
# """
# :param image_id: integer (str or int)
:param key: (str) metadatum key
# :param key: (str) metadatum key
:param val: (str) metadatum value
# :param val: (str) metadatum value
:returns: (dict) updated metadata
"""
req = {'meta': {key: val}}
r = self.images_metadata_put(image_id, key, json_data=req)
return r.json['meta']
# :returns: (dict) updated metadata
# """
# req = {'meta': {key: val}}
# r = self.images_metadata_put(image_id, key, json_data=req)
# return r.json['meta']
def update_image_metadata(
self, image_id,
......
......@@ -703,15 +703,15 @@ class ComputeClient(TestCase):
self.client.delete_image(img_ref)
ID.assert_called_once_with(img_ref)
@patch('%s.images_metadata_put' % compute_pkg, return_value=FR())
def test_create_image_metadata(self, IP):
(key, val) = ('k1', 'v1')
FR.json = dict(meta=img_recv['image'])
r = self.client.create_image_metadata(img_ref, key, val)
IP.assert_called_once_with(
img_ref, '%s' % key,
json_data=dict(meta={key: val}))
self.assert_dicts_are_equal(r, img_recv['image'])
# @patch('%s.images_metadata_put' % compute_pkg, return_value=FR())
# def test_create_image_metadata(self, IP):
# (key, val) = ('k1', 'v1')
# FR.json = dict(meta=img_recv['image'])
# r = self.client.create_image_metadata(img_ref, key, val)
# IP.assert_called_once_with(
# img_ref, '%s' % key,
# json_data=dict(meta={key: val}))
# self.assert_dicts_are_equal(r, img_recv['image'])
@patch('%s.images_metadata_post' % compute_pkg, return_value=FR())
def test_update_image_metadata(self, IP):
......
......@@ -198,3 +198,26 @@ class ImageClient(Client):
req = {'memberships': [{'member_id': member} for member in members]}
r = self.put(path, json=req, success=204)
return r.headers
def update_image(
self, image_id,
name=None, disk_format=None, container_format=None,
status=None, public=None, owner_id=None, **properties):
path = path4url('images', image_id)
if name is not None:
self.set_header('X-Image-Meta-Name', name)
if disk_format is not None:
self.set_header('X-Image-Meta-Disk-Format', disk_format)
if container_format is not None:
self.set_header('X-Image-Meta-Container-Format', container_format)
if status is not None:
self.set_header('X-Image-Meta-Status', status)
if public is not None:
self.set_header('X-Image-Meta-Is-Public', bool(public))
if owner_id is not None:
self.set_header('X-Image-Meta-Owner', owner_id)
for k, v in properties.items():
self.set_header('X-Image-Meta-Property-%s' % k, v)
self.set_header('Content-Length', 0)
r = self.put(path, success=200)
return r.headers
......@@ -290,6 +290,44 @@ class ImageClient(TestCase):
for i in range(len(r)):
self.assert_dicts_are_equal(r[i], example_images[i])
@patch('%s.put' % image_pkg, return_value=FR())
@patch('%s.set_header' % image_pkg)
def test_update_image(self, set_header, put):
FR.headers = 'some headers'
hcnt = 0
for args in product(
('some id', 'other id'),
('image name', None), ('disk fmt', None), ('cnt format', None),
('status', None), (True, False, None), ('owner id', None),
(dict(k1='v1', k2='v2'), {})):
r = self.client.update_image(*args[:-1], **args[-1])
(image_id, name, disk_format, container_format,
status, public, owner_id, properties) = args
self.assertEqual(r, FR.headers)
header_calls = [call('Content-Length', 0), ]
prf = 'X-Image-Meta-'
if name:
header_calls.append(call('%sName' % prf, name))
if disk_format:
header_calls.append(call('%sDisk-Format' % prf, disk_format))
if container_format:
header_calls.append(
call('%sContainer-Format' % prf, container_format))
if status:
header_calls.append(call('%sStatus' % prf, status))
if public is not None:
header_calls.append(call('%sIs-Public' % prf, public))
if owner_id:
header_calls.append(call('%sOwner' % prf, owner_id))
for k, v in properties.items():
header_calls.append(call('%sProperty-%s' % (prf, k), v))
self.assertEqual(
sorted(set_header.mock_calls[hcnt:]), sorted(header_calls))
hcnt = len(set_header.mock_calls)
self.assertEqual(
put.mock_calls[-1], call('/images/%s' % image_id, success=200))
if __name__ == '__main__':
from sys import argv
from kamaki.clients.test import runTestCase
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment