commit ca5528f1a70fe3d325a7e3eb88d7afa17b50712e
Author: Stavros Sachtouris <saxtouri@admin.grnet.gr>
Date:   Tue Aug 27 16:38:54 2013 +0300

    Add meta/properties image methods, whip compute
    
    Refs: #4231
    
    New methods:
    /image meta list/set/delete --<meta>[=value] ... -p k1[=v1] [-p k2[=v2]] ...

diff --git a/kamaki/cli/argument/__init__.py b/kamaki/cli/argument/__init__.py
index 57a96e7..c383b4b 100644
--- a/kamaki/cli/argument/__init__.py
+++ b/kamaki/cli/argument/__init__.py
@@ -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 ...
     """
diff --git a/kamaki/cli/argument/test.py b/kamaki/cli/argument/test.py
index 3e90018..f3ead74 100644
--- a/kamaki/cli/argument/test.py
+++ b/kamaki/cli/argument/test.py
@@ -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:])
diff --git a/kamaki/cli/commands/image.py b/kamaki/cli/commands/image.py
index 55acc82..dc51462 100644
--- a/kamaki/cli/commands/image.py
+++ b/kamaki/cli/commands/image.py
@@ -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)
diff --git a/kamaki/cli/test.py b/kamaki/cli/test.py
index b9e3cc4..7527313 100644
--- a/kamaki/cli/test.py
+++ b/kamaki/cli/test.py
@@ -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
 
 
diff --git a/kamaki/clients/compute/__init__.py b/kamaki/clients/compute/__init__.py
index 3cc7a86..34e2f34 100644
--- a/kamaki/clients/compute/__init__.py
+++ b/kamaki/clients/compute/__init__.py
@@ -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,
diff --git a/kamaki/clients/compute/test.py b/kamaki/clients/compute/test.py
index 874b0f0..7e63738 100644
--- a/kamaki/clients/compute/test.py
+++ b/kamaki/clients/compute/test.py
@@ -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):
diff --git a/kamaki/clients/image/__init__.py b/kamaki/clients/image/__init__.py
index 94522ec..b5d9329 100644
--- a/kamaki/clients/image/__init__.py
+++ b/kamaki/clients/image/__init__.py
@@ -218,5 +218,6 @@ class ImageClient(Client):
             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
diff --git a/kamaki/clients/image/test.py b/kamaki/clients/image/test.py
index a617ba3..531a9ca 100644
--- a/kamaki/clients/image/test.py
+++ b/kamaki/clients/image/test.py
@@ -304,7 +304,7 @@ class ImageClient(TestCase):
             (image_id, name, disk_format, container_format,
             status, public, owner_id, properties) = args
             self.assertEqual(r, FR.headers)
-            header_calls = []
+            header_calls = [call('Content-Length', 0), ]
             prf = 'X-Image-Meta-'
             if name:
                 header_calls.append(call('%sName' % prf, name))
