commit e51c7d5bda5a7685b9fc7f046d91775d84220c6d
Author: Stavros Sachtouris <saxtouri@admin.grnet.gr>
Date:   Wed Jul 24 18:13:11 2013 +0300

    Complete basic compute v2 implementation
    
    Refs: #4139
    
    Pending: adjust unitests, test against an openstack deployment

diff --git a/kamaki/clients/compute/__init__.py b/kamaki/clients/compute/__init__.py
index cb13061..0bb9b53 100644
--- a/kamaki/clients/compute/__init__.py
+++ b/kamaki/clients/compute/__init__.py
@@ -301,13 +301,16 @@ class ComputeClient(ComputeRestClient):
         r = self.servers_delete(server_id, key)
         return r.headers
 
-    def list_flavors(self, detail=False):
+    def list_flavors(self, detail=False, response_headers=dict(
+            previous=None, next=None)):
         """
         :param detail: (bool) detailed flavor info if set, short if not
 
         :returns: (list) flavor info
         """
-        r = self.flavors_get(command='detail' if detail else '')
+        r = self.flavors_get(detail=bool(detail))
+        for k, v in response_headers.items():
+            response_headers[k] = r.headers.get(k, v)
         return r.json['flavors']
 
     def get_flavor_details(self, flavor_id):
@@ -319,14 +322,16 @@ class ComputeClient(ComputeRestClient):
         r = self.flavors_get(flavor_id)
         return r.json['flavor']
 
-    def list_images(self, detail=False):
+    def list_images(self, detail=False, response_headers=dict(
+            next=None, previous=None)):
         """
         :param detail: (bool) detailed info if set, short if not
 
         :returns: dict id,name + full info if detail
         """
-        detail = 'detail' if detail else ''
-        r = self.images_get(command=detail)
+        r = self.images_get(detail=bool(detail))
+        for k, v in response_headers.items():
+            response_headers[k] = r.headers.get(k, v)
         return r.json['images']
 
     def get_image_details(self, image_id, **kwargs):
@@ -351,7 +356,8 @@ class ComputeClient(ComputeRestClient):
         r = self.images_delete(image_id)
         return r.headers
 
-    def get_image_metadata(self, image_id, key=''):
+    def get_image_metadata(self, image_id, key='', response_headers=dict(
+            previous=None, next=None)):
         """
         :param image_id: (str)
 
@@ -359,8 +365,9 @@ class ComputeClient(ComputeRestClient):
 
         :returns (dict) metadata if key not set, specific metadatum otherwise
         """
-        command = path4url('metadata', key)
-        r = self.images_get(image_id, command)
+        r = self.images_metadata_get(image_id, key)
+        for k, v in response_headers.items():
+            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):
@@ -377,7 +384,9 @@ class ComputeClient(ComputeRestClient):
         r = self.images_put(image_id, 'metadata/' + key, json_data=req)
         return r.json['meta']
 
-    def update_image_metadata(self, image_id, **metadata):
+    def update_image_metadata(
+            self, image_id,
+            response_headers=dict(previous=None, next=None), **metadata):
         """
         :param image_id: (str)
 
@@ -386,7 +395,9 @@ class ComputeClient(ComputeRestClient):
         :returns: updated metadata
         """
         req = {'metadata': metadata}
-        r = self.images_post(image_id, 'metadata', json_data=req)
+        r = self.images_metadata_post(image_id, json_data=req)
+        for k, v in response_headers.items():
+            response_headers[k] = r.headers.get(k, v)
         return r.json['metadata']
 
     def delete_image_metadata(self, image_id, key):
diff --git a/kamaki/clients/compute/rest_api.py b/kamaki/clients/compute/rest_api.py
index c400b66..47af4f1 100644
--- a/kamaki/clients/compute/rest_api.py
+++ b/kamaki/clients/compute/rest_api.py
@@ -101,7 +101,7 @@ class ComputeRestClient(Client):
             self.set_param('status', status, iff=status)
             self.set_param('host', host, iff=host)
 
-        path = path4url('servers', 'detail' if detail else server_id)
+        path = path4url('servers', 'detail' if detail else (server_id or ''))
         return self.get(path, success=success, **kwargs)
 
     def servers_post(
@@ -263,163 +263,161 @@ class ComputeRestClient(Client):
         path = path4url('servers', server_id, 'action')
         return self.post(path, data=json_data, success=success, **kwargs)
 
-    """
-    def servers_actions_post
-    def servers_ips_get
-
-    def servers_get(self, server_id='', command='', success=200, **kwargs):
-        ""GET base_url/servers[/server_id][/command] request
-
-        :param server_id: integer (as int or str)
-
-        :param command: 'ips', 'stats', or ''
+    def servers_ips_get(
+            self, server_id,
+            network_id=None, changes_since=None, success=(304, 200),
+            **kwargs):
+        """GET base_url/servers/<server_id>/ips[/network_id]
 
-        :param success: success code or list or tupple of accepted success
-            codes. if server response code is not in this list, a ClientError
-            raises
+        :param changes_since: time/date stamp in UNIX/epoch time. Checks for
+            changes since a previous request.
 
         :returns: request response
-        ""
-        path = path4url('servers', server_id, command)
+        """
+        self.set_param('changes-since', changes_since, iff=changes_since)
+        path = path4url('servers', server_id, 'ips', network_id or '')
+        return self.get(path, success=success, **kwargs)
 
-    def servers_post(
+    def images_get(
             self,
-            server_id='',
-            command='',
-            json_data=None,
-            success=202,
+            image_id='',
+            detail=False,
+            changes_since=None,
+            server_name=None,
+            name=None,
+            status=None,
+            marker=None,
+            limit=None,
+            type=None,
+            success=200,
             **kwargs):
-        ""POST base_url/servers[/server_id]/[command] request
-
-        :param server_id: integer (as int or str)
+        """GET base_url[/image_id][/command]
 
-        :param command: 'ips', 'stats', or ''
+        :param image_id: (str) ignored if detail
 
-        :param json_data: a json-formated dict that will be send as data
+        :param detail: (bool)
 
-        :param success: success code or list or tupple of accepted success
-            codes. if server response code is not in this list, a ClientError
-            raises
+        --- Parameters ---
+        :param changes_since: when the image last changed status
 
-        :returns: request response
-        ""
-        data = json_data
-        if json_data:
-            data = json.dumps(json_data)
-            self.set_header('Content-Type', 'application/json')
-            self.set_header('Content-Length', len(data))
+        :param server_name: Name of the server in URL format
 
-        path = path4url('servers', server_id, command)
-        return self.post(path, data=data, success=success, **kwargs)
-    """
+        :param name: Name of the image
 
-    def flavors_get(self, flavor_id='', command='', success=200, **kwargs):
-        """GET base_url[/flavor_id][/command]
+        :param status: Status of the image (e.g. filter on "ACTIVE")
 
-        :param flavor_id: integer (str or int)
+        :param marker: UUID of the image at which you want to set a marker
 
-        :param command: flavor service command
+        :param limit: Integer value for the limit of values to return
 
-        :param success: success code or list or tupple of accepted success
-            codes. if server response code is not in this list, a ClientError
-            raises
+        :param type: Type of image (e.g. BASE, SERVER, or ALL)
 
         :returns: request response
         """
-        path = path4url('flavors', flavor_id, command)
-        return self.get(path, success=success, **kwargs)
+        if not image_id:
+            self.set_param('changes-since', changes_since, iff=changes_since)
+            self.set_param('name', name, iff=name)
+            self.set_param('status', status, iff=status)
+            self.set_param('marker', marker, iff=marker)
+            self.set_param('limit', limit, iff=limit)
+            self.set_param('type', type, iff=type)
+            self.set_param('server', server_name, iff=server_name)
 
-    def images_get(self, image_id='', command='', success=200, **kwargs):
-        """GET base_url[/image_id][/command]
+        path = path4url('images', 'detail' if detail else (image_id or ''))
+        return self.get(path, success=success, **kwargs)
 
-        :param image_id: string
+    def images_delete(self, image_id='', success=204, **kwargs):
+        """DEL ETE base_url/images/<image_id>
 
-        :param command: image server command
+        :returns: request response
+        """
+        path = path4url('images', image_id)
+        return self.delete(path, success=success, **kwargs)
 
-        :param success: success code or list or tupple of accepted success
-            codes. if server response code is not in this list, a ClientError
-            raises
+    def images_metadata_get(self, image_id, key=None, success=200, **kwargs):
+        """GET base_url/<image_id>/metadata[/key]
 
         :returns: request response
         """
-        path = path4url('images', image_id, command)
+        path = path4url('images', image_id, 'metadata', key or '')
         return self.get(path, success=success, **kwargs)
 
-    def images_delete(self, image_id='', command='', success=204, **kwargs):
-        """DELETE base_url[/image_id][/command]
-
-        :param image_id: string
-
-        :param command: image server command
-
-        :param success: success code or list or tuple of accepted success
-            codes. if server response code is not in this list, a ClientError
-            raises
+    def images_metadata_post(
+            self, image_id, json_data=None, success=201, **kwargs):
+        """POST base_url/images/<image_id>/metadata
 
         :returns: request response
         """
-        path = path4url('images', image_id, command)
-        return self.delete(path, success=success, **kwargs)
-
-    def images_post(
-            self,
-            image_id='',
-            command='',
-            json_data=None,
-            success=201,
-            **kwargs):
-        """POST base_url/images[/image_id]/[command] request
-
-        :param image_id: string
-
-        :param command: image server command
+        if json_data is not None:
+            json_data = json.dumps(json_data)
+            self.set_header('Content-Type', 'application/json')
+            self.set_header('Content-Length', len(json_data))
 
-        :param json_data: (dict) will be send as data
+        path = path4url('images', image_id, 'metadata')
+        return self.post(path, data=json_data, success=success, **kwargs)
 
-        :param success: success code or list or tuple of accepted success
-            codes. if server response code is not in this list, a ClientError
-            raises
+    def images_metadata_put(
+            self, image_id, key=None, json_data=None, success=201, **kwargs):
+        """PUT base_url/images/<image_id>/metadata
 
         :returns: request response
         """
-        data = json_data
         if json_data is not None:
-            data = json.dumps(json_data)
+            json_data = json.dumps(json_data)
             self.set_header('Content-Type', 'application/json')
-            self.set_header('Content-Length', len(data))
+            self.set_header('Content-Length', len(json_data))
+
+        path = path4url('images', image_id, 'metadata')
+        return self.put(path, data=json_data, success=success, **kwargs)
 
-        path = path4url('images', image_id, command)
-        return self.post(path, data=data, success=success, **kwargs)
+    def images_metadata_delete(self, image_id, key, success=204, **kwargs):
+        """DEL ETE base_url/images/<image_id>/metadata/key
 
-    def images_put(
+        :returns: request response
+        """
+        path = path4url('images', image_id, 'metadata', key)
+        return self.put(path, success=success, **kwargs)
+
+    def flavors_get(
             self,
-            image_id='',
-            command='',
-            json_data=None,
-            success=201,
+            flavor_id='',
+            detail=False,
+            changes_since=None,
+            minDisk=None,
+            minRam=None,
+            marker=None,
+            limit=None,
+            success=200,
             **kwargs):
-        """PUT base_url/images[/image_id]/[command] request
+        """GET base_url[/flavor_id][/command]
 
-        :param image_id: string
+        :param flavor_id: ignored if detail
 
-        :param command: image server command
+        :param detail: (bool)
 
-        :param json_data: (dict) will be send as data
+        --- Parameters ---
 
-        :param success: success code or list or tuple of accepted success
-            codes. if server response code is not in this list, a ClientError
-            raises
+        :param changes_since: when the flavor last changed
+
+        :param minDisk: minimum disk space in GB filter
+
+        :param minRam: minimum RAM filter
+
+        :param marker: UUID of the flavor at which to set a marker
+
+        :param limit: limit the number of returned values
 
         :returns: request response
         """
-        data = json_data
-        if json_data is not None:
-            data = json.dumps(json_data)
-            self.set_header('Content-Type', 'application/json')
-            self.set_header('Content-Length', len(data))
+        if not flavor_id:
+            self.set_param('changes-since', changes_since, iff=changes_since)
+            self.set_param('minDisk', minDisk, iff=minDisk)
+            self.set_param('minRam', minRam, iff=minRam)
+            self.set_param('marker', marker, iff=marker)
+            self.set_param('limit', limit, iff=limit)
 
-        path = path4url('images', image_id, command)
-        return self.put(path, data=data, success=success, **kwargs)
+        path = path4url('flavors', 'detail' if detail else (flavor_id or ''))
+        return self.get(path, success=success, **kwargs)
 
     def floating_ip_pools_get(self, tenant_id, success=200, **kwargs):
         path = path4url(tenant_id, 'os-floating-ip-pools')
diff --git a/kamaki/clients/cyclades/__init__.py b/kamaki/clients/cyclades/__init__.py
index 6ff3222..88f05f9 100644
--- a/kamaki/clients/cyclades/__init__.py
+++ b/kamaki/clients/cyclades/__init__.py
@@ -151,9 +151,8 @@ class CycladesClient(CycladesRestClient):
 
         :returns: (dict) network interface connections
         """
-        r = self.servers_get(server_id, 'ips')
+        r = self.servers_ips_get(server_id)
         return r.json['attachments']
-        #return r.json['addresses']
 
     def get_server_stats(self, server_id):
         """
@@ -161,7 +160,7 @@ class CycladesClient(CycladesRestClient):
 
         :returns: (dict) auto-generated graphs of statistics (urls)
         """
-        r = self.servers_get(server_id, 'stats')
+        r = self.servers_stats_get(server_id)
         return r.json['stats']
 
     def list_networks(self, detail=False):
diff --git a/kamaki/clients/cyclades/rest_api.py b/kamaki/clients/cyclades/rest_api.py
index f5dec75..8b1d4cd 100644
--- a/kamaki/clients/cyclades/rest_api.py
+++ b/kamaki/clients/cyclades/rest_api.py
@@ -39,6 +39,11 @@ import json
 class CycladesRestClient(ComputeClient):
     """Synnefo Cyclades REST API Client"""
 
+    def servers_stats_get(self, server_id, success=200, **kwargs):
+        """GET base_url/servers/<server_id>/stats"""
+        path = path4url('servers', server_id, 'stats')
+        return self.get(path, success=success, **kwargs)
+
     def networks_get(
             self,
             network_id='',
