commit 996678547b2d762cfe66b12705fe12de7bcb6507
Author: Giorgos Korfiatis <gkorf@grnet.gr>
Date:   Thu Sep 26 12:36:23 2013 +0300

    cyclades: Reassign resources
    
    Add API calls to assign a VM, network, or floating IP to a new project.

diff --git a/docs/compute-api-guide.rst b/docs/compute-api-guide.rst
index f0b2141..fc52860 100644
--- a/docs/compute-api-guide.rst
+++ b/docs/compute-api-guide.rst
@@ -1623,6 +1623,7 @@ Operations                                      Cyclades OS/Compute
 `Reboot <#reboot-server>`_                      ✔        ✔
 `Get Console <#get-server-console>`_            ✔        **✘**
 `Set Firewall <#set-server-firewall-profile>`_  ✔        **✘**
+`Reassign <#reassign-server>`_                  ✔        **✘**
 `Change Admin Password <#os-compute-specific>`_ **✘**    ✔
 `Rebuild <#os-compute-specific>`_               **✘**    ✔
 `Resize <#os-compute-specific>`_                **✘**    ✔
@@ -1804,6 +1805,23 @@ Request body contents::
 
 .. note:: Response body should be empty
 
+Reassign Server
+...............
+
+This operation assigns the VM to a different project.
+
+Request body contents::
+
+  reassign: { project: <project-id>}
+
+*Example Action reassign: JSON**
+
+.. code-block:: javascript
+
+  {"reassign": {"project": "9969f2fd-86d8-45d6-9106-5e251f7dd92f"}}
+
+.. note:: Response body should be empty
+
 OS/Compute Specific
 ...................
 
@@ -2756,6 +2774,7 @@ Description                                     URI
 `Delete <#delete-network>`_                     ``/networks/<network-id>``        DELETE
 `Connect <#connect-network-to-server>`_         ``/networks/<network-id>/action`` POST
 `Disconnect <#disconnect-network-from-server>`_ ``/networks/<network-id>/action`` POST
+`Reassign <#reassign-network>`_                 ``/networks/<network-id>/action`` POST
 =============================================== ================================= ======
 
 
@@ -3331,6 +3350,66 @@ Return Code                 Description
 
 .. note:: In case of a 202 code, the request body should be empty
 
+Reassign Network
+................
+
+Assign a network to a different project.
+
+.. rubric:: Request
+
+================================= ======
+URI                               Method
+================================= ======
+``/networks/<network-id>/action`` POST
+================================= ======
+
+* **network-id** is the identifier of the network
+
+|
+
+==============  =========================
+Request Header  Value
+==============  =========================
+X-Auth-Token    User authentication token
+Content-Type    Type or request body
+Content-Length  Length of request body
+==============  =========================
+
+**Example Request Headers**::
+
+  X-Auth-Token:   z31uRXUn1LZy45p1r7V==
+  Content-Type:   application/json
+  Content-Length: 31
+
+.. note:: Request parameters should be empty
+
+Response body content (reassign)::
+
+  reassign {project: <project-id>}
+
+*Example Action Reassign: JSON*
+
+.. code-block:: javascript
+
+  {"reassign" : {"project" : "9969f2fd-86d8-45d6-9106-5e251f7dd92f"}}
+
+.. rubric:: Response
+
+=========================== =====================
+Return Code                 Description
+=========================== =====================
+200 (OK)                    Request succeeded
+400 (Bad Request)           Malformed request
+401 (Unauthorized)          Missing or expired user token
+403 (Forbidden)             Not allowed to modify this network (e.g. public)
+404 (Not Found)             Network not found
+500 (Internal Server Error) The request cannot be completed because of an
+\                           internal error
+503 (Service Unavailable)   The service is not currently available
+=========================== =====================
+
+.. note:: In case of a 200 code, the request body should be empty
+
 Index of Attributes
 -------------------
 
diff --git a/snf-cyclades-app/synnefo/api/floating_ips.py b/snf-cyclades-app/synnefo/api/floating_ips.py
index 3e8a83f..973a5ed 100644
--- a/snf-cyclades-app/synnefo/api/floating_ips.py
+++ b/snf-cyclades-app/synnefo/api/floating_ips.py
@@ -62,7 +62,9 @@ ips_urlpatterns = patterns(
     'synnefo.api.floating_ips',
     (r'^(?:/|.json|.xml)?$', 'demux'),
     (r'^/detail(?:.json|.xml)?$', 'list_floating_ips', {'detail': True}),
-    (r'^/(\w+)(?:/|.json|.xml)?$', 'floating_ip_demux'))
+    (r'^/(\w+)(?:/|.json|.xml)?$', 'floating_ip_demux'),
+    (r'^/(\w+)/action(?:.json|.xml)?$', 'floating_ip_action_demux'),
+)
 
 
 def demux(request):
@@ -87,6 +89,29 @@ def floating_ip_demux(request, floating_ip_id):
                                           allowed_methods=['GET', 'DELETE'])
 
 
+@api.api_method(http_method='POST', user_required=True, logger=log,
+                serializations=["json"])
+def floating_ip_action_demux(request, floating_ip_id):
+    userid = request.user_uniq
+    req = utils.get_request_dict(request)
+    log.debug('floating_ip_action %s %s', floating_ip_id, req)
+    if len(req) != 1:
+        raise faults.BadRequest('Malformed request.')
+    floating_ip = util.get_floating_ip_by_id(userid,
+                                             floating_ip_id,
+                                             for_update=True)
+    action = req.keys()[0]
+    try:
+        f = FLOATING_IP_ACTIONS[action]
+    except KeyError:
+        raise faults.BadRequest("Action %s not supported." % action)
+    action_args = req[action]
+    if not isinstance(action_args, dict):
+        raise faults.BadRequest("Invalid argument.")
+
+    return f(request, floating_ip, action_args)
+
+
 def ip_to_dict(floating_ip):
     machine_id = None
     port_id = None
@@ -236,6 +261,20 @@ def list_floating_ip_pools(request):
     return HttpResponse(data, status=200)
 
 
+@transaction.commit_on_success
+def reassign(request, floating_ip, args):
+    project = args.get("project")
+    if project is None:
+        raise faults.BadRequest("Missing 'project' attribute.")
+    ips.reassign_floating_ip(floating_ip, project)
+    return HttpResponse(status=200)
+
+
+FLOATING_IP_ACTIONS = {
+    "reassign": reassign,
+}
+
+
 def network_to_floating_ip_pool(network):
     """Convert a 'Network' object to a floating IP pool dict."""
     total, free = network.ip_count()
diff --git a/snf-cyclades-app/synnefo/api/networks.py b/snf-cyclades-app/synnefo/api/networks.py
index 82a798b..885fef7 100644
--- a/snf-cyclades-app/synnefo/api/networks.py
+++ b/snf-cyclades-app/synnefo/api/networks.py
@@ -40,6 +40,7 @@ from django.db.models import Q
 from django.template.loader import render_to_string
 
 from snf_django.lib import api
+from snf_django.lib.api import utils
 
 from synnefo.api import util
 from synnefo.db.models import Network
@@ -53,7 +54,9 @@ urlpatterns = patterns(
     'synnefo.api.networks',
     (r'^(?:/|.json|.xml)?$', 'demux'),
     (r'^/detail(?:.json|.xml)?$', 'list_networks', {'detail': True}),
-    (r'^/(\w+)(?:/|.json|.xml)?$', 'network_demux'))
+    (r'^/(\w+)(?:/|.json|.xml)?$', 'network_demux'),
+    (r'^/(\w+)/action(?:/|.json|.xml)?$', 'network_action_demux'),
+)
 
 
 def demux(request):
@@ -81,6 +84,22 @@ def network_demux(request, network_id):
                                                            'DELETE'])
 
 
+@api.api_method(http_method='POST', user_required=True, logger=log)
+def network_action_demux(request, network_id):
+    req = utils.get_request_dict(request)
+    network = util.get_network(network_id, request.user_uniq, for_update=True)
+    action = req.keys()[0]
+    try:
+        f = NETWORK_ACTIONS[action]
+    except KeyError:
+        raise faults.BadRequest("Action %s not supported." % action)
+    action_args = req[action]
+    if not isinstance(action_args, dict):
+        raise faults.BadRequest("Invalid argument.")
+
+    return f(request, network, action_args)
+
+
 @api.api_method(http_method='GET', user_required=True, logger=log)
 def list_networks(request, detail=True):
     log.debug('list_networks detail=%s', detail)
@@ -196,6 +215,20 @@ def network_to_dict(network, detail=True):
     return d
 
 
+@transaction.commit_on_success
+def reassign_network(request, network, args):
+    project = args.get("project")
+    if project is None:
+        raise faults.BadRequest("Missing 'project' attribute.")
+    networks.reassign(network, project)
+    return HttpResponse(status=200)
+
+
+NETWORK_ACTIONS = {
+    "reassign": reassign_network,
+}
+
+
 def render_network(request, networkdict, status=200):
     if request.serialization == 'xml':
         data = render_to_string('network.xml', {'network': networkdict})
diff --git a/snf-cyclades-app/synnefo/api/servers.py b/snf-cyclades-app/synnefo/api/servers.py
index 46a2f56..8c3bcab 100644
--- a/snf-cyclades-app/synnefo/api/servers.py
+++ b/snf-cyclades-app/synnefo/api/servers.py
@@ -479,7 +479,7 @@ def delete_server(request, server_id):
 
 
 # additional server actions
-ARBITRARY_ACTIONS = ['console', 'firewallProfile']
+ARBITRARY_ACTIONS = ['console', 'firewallProfile', 'reassign']
 
 
 def key_to_action(key):
@@ -878,6 +878,15 @@ def revert_resize(request, vm, args):
     raise faults.NotImplemented('Resize not supported.')
 
 
+@server_action('reassign')
+def reassign(request, vm, args):
+    project = args.get("project")
+    if project is None:
+        raise faults.BadRequest("Missing 'project' attribute.")
+    servers.reassign(vm, project)
+    return HttpResponse(status=200)
+
+
 @network_action('add')
 @transaction.commit_on_success
 def add(request, net, args):
diff --git a/snf-cyclades-app/synnefo/logic/ips.py b/snf-cyclades-app/synnefo/logic/ips.py
index 2e8b3bf..2e409da 100644
--- a/snf-cyclades-app/synnefo/logic/ips.py
+++ b/snf-cyclades-app/synnefo/logic/ips.py
@@ -230,3 +230,13 @@ def delete_floating_ip(floating_ip):
     log.info("Deleted floating IP '%s' of user '%s", floating_ip,
              floating_ip.userid)
     floating_ip.delete()
+
+
+@transaction.commit_on_success
+def reassign_floating_ip(floating_ip, project):
+    action_fields = {"to_project": project,
+                     "from_project": floating_ip.project}
+    floating_ip.project = project
+    floating_ip.save()
+    quotas.issue_and_accept_commission(floating_ip, action="REASSIGN",
+                                       action_fields=action_fields)
diff --git a/snf-cyclades-app/synnefo/logic/networks.py b/snf-cyclades-app/synnefo/logic/networks.py
index fa14ad1..a8e4ee6 100644
--- a/snf-cyclades-app/synnefo/logic/networks.py
+++ b/snf-cyclades-app/synnefo/logic/networks.py
@@ -174,3 +174,13 @@ def delete(network):
         # If network does not exist in any backend, update the network state
         backend_mod.update_network_state(network)
     return network
+
+
+@network_command("REASSIGN")
+def reassign(network, project):
+    action_fields = {"to_project": project, "from_project": network.project}
+    network.project = project
+    network.save()
+    quotas.issue_and_accept_commission(network, action="REASSIGN",
+                                       action_fields=action_fields)
+    return network
diff --git a/snf-cyclades-app/synnefo/logic/servers.py b/snf-cyclades-app/synnefo/logic/servers.py
index d15e308..2096e7a 100644
--- a/snf-cyclades-app/synnefo/logic/servers.py
+++ b/snf-cyclades-app/synnefo/logic/servers.py
@@ -333,6 +333,15 @@ def _resize(vm, flavor):
     return backend.resize_instance(vm, vcpus=flavor.cpu, memory=flavor.ram)
 
 
+@transaction.commit_on_success
+def reassign(vm, project):
+    action_fields = {"to_project": project, "from_project": vm.project}
+    vm.project = project
+    vm.save()
+    quotas.issue_and_accept_commission(vm, action="REASSIGN",
+                                       action_fields=action_fields)
+
+
 @server_command("SET_FIREWALL_PROFILE")
 def set_firewall_profile(vm, profile, nic):
     log.info("Setting VM %s, NIC %s, firewall %s", vm, nic, profile)
diff --git a/snf-cyclades-app/synnefo/quotas/__init__.py b/snf-cyclades-app/synnefo/quotas/__init__.py
index 161ac62..63538b9 100644
--- a/snf-cyclades-app/synnefo/quotas/__init__.py
+++ b/snf-cyclades-app/synnefo/quotas/__init__.py
@@ -131,7 +131,21 @@ def issue_commission(resource, action, name="", force=False, auto_accept=False,
     source = resource.project
 
     qh = Quotaholder.get()
-    if True:  # placeholder
+    if action == "REASSIGN":
+        try:
+            from_project = action_fields["from_project"]
+            to_project = action_fields["to_project"]
+        except KeyError:
+            raise Exception("Missing project attribute.")
+
+        projects = [from_project, to_project]
+        with AstakosClientExceptionHandler(user=user, projects=projects):
+            serial = qh.issue_resource_reassignment(user,
+                                                    from_project, to_project,
+                                                    provisions, name=name,
+                                                    force=force,
+                                                    auto_accept=auto_accept)
+    else:
         with AstakosClientExceptionHandler(user=user, projects=[source]):
             serial = qh.issue_one_commission(user, source,
                                              provisions, name=name,
@@ -352,6 +366,10 @@ def get_commission_info(resource, action, action_fields=None):
             ram = beparams.get("maxmem", flavor.ram)
             return {"cyclades.total_cpu": cpu - flavor.cpu,
                     "cyclades.total_ram": 1048576 * (ram - flavor.ram)}
+        elif action == "REASSIGN":
+            if resource.operstate in ["STARTED", "BUILD", "ERROR"]:
+                resources.update(online_resources)
+            return resources
         else:
             #["CONNECT", "DISCONNECT", "SET_FIREWALL_PROFILE"]:
             return None
@@ -361,6 +379,8 @@ def get_commission_info(resource, action, action_fields=None):
             return resources
         elif action == "DESTROY":
             return reverse_quantities(resources)
+        elif action == "REASSIGN":
+            return resources
     elif isinstance(resource, IPAddress):
         if resource.floating_ip:
             resources = {"cyclades.floating_ip": 1}
@@ -368,6 +388,8 @@ def get_commission_info(resource, action, action_fields=None):
                 return resources
             elif action == "DESTROY":
                 return reverse_quantities(resources)
+            elif action == "REASSIGN":
+                return resources
         else:
             return None
 
