commit ece5581b0bfbd5eaa1ef5714bd6b5322d9ae2064
Author: Christos Stavrakakis <cstavr@grnet.gr>
Date:   Fri Jun 14 11:40:29 2013 +0300

    cyclades: Separate logic for networks from API
    
    Remove code handling networks from network API views to logic.networks

diff --git a/snf-cyclades-app/synnefo/api/networks.py b/snf-cyclades-app/synnefo/api/networks.py
index 74b8091..6b40c59 100644
--- a/snf-cyclades-app/synnefo/api/networks.py
+++ b/snf-cyclades-app/synnefo/api/networks.py
@@ -1,4 +1,4 @@
-# Copyright 2011-2012 GRNET S.A. All rights reserved.
+# Copyright 2011-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
@@ -30,11 +30,8 @@
 # documentation are those of the authors and should not be
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
-
-
 from django.conf import settings
 from django.conf.urls.defaults import patterns
-from django.db import transaction
 from django.db.models import Q
 from django.http import HttpResponse
 from django.template.loader import render_to_string
@@ -44,11 +41,8 @@ from snf_django.lib import api
 from snf_django.lib.api import faults, utils
 from synnefo.api import util
 from synnefo.api.servers import network_actions
-from synnefo import quotas
 from synnefo.db.models import Network
-from synnefo.db.utils import validate_mac
-from synnefo.db.pools import EmptyPool
-from synnefo.logic import backend
+from synnefo.logic import networks
 
 
 from logging import getLogger
@@ -59,7 +53,7 @@ urlpatterns = patterns(
     (r'^(?:/|.json|.xml)?$', 'demux'),
     (r'^/detail(?:.json|.xml)?$', 'list_networks', {'detail': True}),
     (r'^/(\w+)(?:.json|.xml)?$', 'network_demux'),
-    (r'^/(\w+)/action(?:.json|.xml)?$', 'network_action'),
+    (r'^/(\w+)/action(?:.json|.xml)?$', 'demux_network_action'),
 )
 
 
@@ -137,21 +131,20 @@ def list_networks(request, detail=False):
     else:
         user_networks = user_networks.filter(deleted=False)
 
-    networks = [network_to_dict(network, request.user_uniq, detail)
-                for network in user_networks.order_by('id')]
+    networks_dict = [network_to_dict(network, request.user_uniq, detail)
+                     for network in user_networks.order_by('id')]
 
     if request.serialization == 'xml':
         data = render_to_string('list_networks.xml', {
-            'networks': networks,
+            'networks': networks_dict,
             'detail': detail})
     else:
-        data = json.dumps({'networks': networks})
+        data = json.dumps({'networks': networks_dict})
 
     return HttpResponse(data, status=200)
 
 
 @api.api_method(http_method='POST', user_required=True, logger=log)
-@transaction.commit_manually
 def create_network(request):
     # Normal Response Code: 202
     # Error Response Codes: computeFault (400, 500),
@@ -162,75 +155,40 @@ def create_network(request):
     #                       forbidden (403)
     #                       overLimit (413)
 
+    req = utils.get_request_dict(request)
+    log.info('create_network %s', req)
+    user_id = request.user_uniq
     try:
-        req = utils.get_request_dict(request)
-        log.info('create_network %s', req)
-
-        user_id = request.user_uniq
-        try:
-            d = req['network']
-            name = d['name']
-        except KeyError:
-            raise faults.BadRequest("Malformed request")
-
-        # Get and validate flavor. Flavors are still exposed as 'type' in the
-        # API.
-        flavor = d.get("type", None)
-        if flavor is None:
-            raise faults.BadRequest("Missing request parameter 'type'")
-        elif flavor not in Network.FLAVORS.keys():
-            raise faults.BadRequest("Invalid network type '%s'" % flavor)
-        elif flavor not in settings.API_ENABLED_NETWORK_FLAVORS:
+        d = req['network']
+        name = d['name']
+    except KeyError:
+        raise faults.BadRequest("Malformed request")
+
+    # Get and validate flavor. Flavors are still exposed as 'type' in the
+    # API.
+    flavor = d.get("type", None)
+    if flavor is None:
+        raise faults.BadRequest("Missing request parameter 'type'")
+    elif flavor not in Network.FLAVORS.keys():
+        raise faults.BadRequest("Invalid network type '%s'" % flavor)
+    elif flavor not in settings.API_ENABLED_NETWORK_FLAVORS:
             raise faults.Forbidden("Can not create network of type '%s'" %
                                    flavor)
+    public = d.get("public", False)
+    if public:
+        raise faults.Forbidden("Can not create a public network.")
 
-        public = d.get("public", False)
-        if public:
-            raise faults.Forbidden("Can not create a public network.")
-
-        dhcp = d.get('dhcp', True)
-
-        # Get and validate network parameters
-        subnet = d.get('cidr', '192.168.1.0/24')
-        subnet6 = d.get('cidr6', None)
-        gateway = d.get('gateway', None)
-        gateway6 = d.get('gateway6', None)
-        # Check that user provided a valid subnet
-        util.validate_network_params(subnet, gateway, subnet6, gateway6)
-
-        try:
-            mode, link, mac_prefix, tags = util.values_from_flavor(flavor)
-            validate_mac(mac_prefix + "0:00:00:00")
-            network = Network.objects.create(
-                name=name,
-                userid=user_id,
-                subnet=subnet,
-                subnet6=subnet6,
-                gateway=gateway,
-                gateway6=gateway6,
-                dhcp=dhcp,
-                flavor=flavor,
-                mode=mode,
-                link=link,
-                mac_prefix=mac_prefix,
-                tags=tags,
-                action='CREATE',
-                state='ACTIVE')
-        except EmptyPool:
-            log.error("Failed to allocate resources for network of type: %s",
-                      flavor)
-            raise faults.ServiceUnavailable("Failed to allocate network"
-                                            " resources")
-
-        # Issue commission to Quotaholder and accept it since at the end of
-        # this transaction the Network object will be created in the DB.
-        # Note: the following call does a commit!
-        quotas.issue_and_accept_commission(network)
-    except:
-        transaction.rollback()
-        raise
-    else:
-        transaction.commit()
+    dhcp = d.get('dhcp', True)
+
+    # Get and validate network parameters
+    subnet = d.get('cidr', '192.168.1.0/24')
+    subnet6 = d.get('cidr6', None)
+    gateway = d.get('gateway', None)
+    gateway6 = d.get('gateway6', None)
+
+    network = networks.create(user_id=user_id, name=name, flavor=flavor,
+                              subnet=subnet, gateway=gateway, subnet6=subnet6,
+                              gateway6=gateway6, dhcp=dhcp, public=False)
 
     networkdict = network_to_dict(network, request.user_uniq)
     response = render_network(request, networkdict, status=202)
@@ -274,18 +232,14 @@ def update_network_name(request, network_id):
     except (TypeError, KeyError):
         raise faults.BadRequest('Malformed request.')
 
-    net = util.get_network(network_id, request.user_uniq)
-    if net.public:
+    network = util.get_network(network_id, request.user_uniq)
+    if network.public:
         raise faults.Forbidden('Can not rename the public network.')
-    if net.deleted:
-        raise faults.BadRequest("Network has been deleted.")
-    net.name = name
-    net.save()
+    network = networks.rename(network, name)
     return HttpResponse(status=204)
 
 
 @api.api_method(http_method='DELETE', user_required=True, logger=log)
-@transaction.commit_on_success
 def delete_network(request, network_id):
     # Normal Response Code: 204
     # Error Response Codes: computeFault (400, 500),
@@ -296,32 +250,19 @@ def delete_network(request, network_id):
     #                       overLimit (413)
 
     log.info('delete_network %s', network_id)
-    net = util.get_network(network_id, request.user_uniq, for_update=True)
-    if net.public:
+    network = util.get_network(network_id, request.user_uniq, for_update=True)
+    if network.public:
         raise faults.Forbidden('Can not delete the public network.')
+    networks.delete(network)
+    return HttpResponse(status=204)
 
-    if net.deleted:
-        raise faults.BadRequest("Network has been deleted.")
-
-    if net.machines.all():  # Nics attached on network
-        raise faults.NetworkInUse('Machines are connected to network.')
-
-    if net.floating_ips.filter(deleted=False).exists():
-        raise faults.NetworkInUse("Network has allocated floating IP addresses")
-
-    net.action = 'DESTROY'
-    net.save()
 
-    backend_networks = net.backend_networks.exclude(operstate="DELETED")
-    for bnet in backend_networks:
-        backend.delete_network(net, bnet.backend)
-    if not backend_networks:
-        backend.update_network_state(net)
-    return HttpResponse(status=204)
+def key_to_action(action):
+    return action.upper()
 
 
 @api.api_method(http_method='POST', user_required=True, logger=log)
-def network_action(request, network_id):
+def demux_network_action(request, network_id):
     req = utils.get_request_dict(request)
     log.debug('network_action %s %s', network_id, req)
     if len(req) != 1:
@@ -333,12 +274,10 @@ def network_action(request, network_id):
     if net.deleted:
         raise faults.BadRequest("Network has been deleted.")
 
-    try:
-        key = req.keys()[0]
-        val = req[key]
-        assert isinstance(val, dict)
-        return network_actions[key](request, net, req[key])
-    except KeyError:
-        raise faults.BadRequest('Unknown action.')
-    except AssertionError:
-        raise faults.BadRequest('Invalid argument.')
+    action = req.keys()[0]
+    if key_to_action(action) not in [x[0] for x in Network.ACTIONS]:
+        raise faults.BadRequest("Action %s not supported." % action)
+    action_args = req[action]
+    if not isinstance(action_args, dict):
+        raise faults.BadRequest("Invalid argument.")
+    return network_actions[action](request, net, action_args)
diff --git a/snf-cyclades-app/synnefo/api/servers.py b/snf-cyclades-app/synnefo/api/servers.py
index 2bb9284..c684c3d 100644
--- a/snf-cyclades-app/synnefo/api/servers.py
+++ b/snf-cyclades-app/synnefo/api/servers.py
@@ -407,10 +407,7 @@ def demux_server_action(request, server_id):
     vm = util.get_vm(server_id, request.user_uniq, for_update=True,
                      non_deleted=True, non_suspended=True)
 
-    try:
-        action = req.keys()[0]
-    except KeyError:
-        raise faults.BadRequest("Unknown action")
+    action = req.keys()[0]
 
     if key_to_action(action) not in [x[0] for x in VirtualMachine.ACTIONS]:
         if action not in ARBITRARY_ACTIONS:
diff --git a/snf-cyclades-app/synnefo/api/test/networks.py b/snf-cyclades-app/synnefo/api/test/networks.py
index e00ad40..8d2e661 100644
--- a/snf-cyclades-app/synnefo/api/test/networks.py
+++ b/snf-cyclades-app/synnefo/api/test/networks.py
@@ -327,7 +327,8 @@ class NetworkAPITest(ComputeAPITest):
 
     def test_add_nic_to_deleted_network(self, mrapi):
         user = 'userr'
-        vm = mfactory.VirtualMachineFactory(name='yo', userid=user)
+        vm = mfactory.VirtualMachineFactory(name='yo', userid=user,
+                                            operstate="ACTIVE")
         net = mfactory.NetworkFactory(state='ACTIVE', userid=user,
                                       deleted=True)
         request = {'add': {'serverRef': vm.id}}
diff --git a/snf-cyclades-app/synnefo/db/models.py b/snf-cyclades-app/synnefo/db/models.py
index 7e71c80..92b7225 100644
--- a/snf-cyclades-app/synnefo/db/models.py
+++ b/snf-cyclades-app/synnefo/db/models.py
@@ -446,6 +446,8 @@ class Network(models.Model):
     ACTIONS = (
         ('CREATE', 'Create Network'),
         ('DESTROY', 'Destroy Network'),
+        ('ADD', 'Add server to Network'),
+        ('REMOVE', 'Remove server from Network'),
     )
 
     RSAPI_STATE_FROM_OPER_STATE = {
diff --git a/snf-cyclades-app/synnefo/logic/networks.py b/snf-cyclades-app/synnefo/logic/networks.py
new file mode 100644
index 0000000..a1aca54
--- /dev/null
+++ b/snf-cyclades-app/synnefo/logic/networks.py
@@ -0,0 +1,133 @@
+# Copyright 2011-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
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+from functools import wraps
+from django.db import transaction
+
+from snf_django.lib.api import faults
+from synnefo.api import util
+from synnefo import quotas
+from synnefo.db.models import Network
+from synnefo.db.utils import validate_mac
+from synnefo.db.pools import EmptyPool
+from synnefo.logic import backend
+
+from logging import getLogger
+log = getLogger(__name__)
+
+
+def validate_network_action(network, action):
+    if network.deleted:
+        raise faults.BadRequest("Network has been deleted.")
+
+
+def network_command(action):
+    def decorator(func):
+        @wraps(func)
+        @transaction.commit_on_success()
+        def wrapper(network, *args, **kwargs):
+            validate_network_action(network, action)
+            return func(network, *args, **kwargs)
+        return wrapper
+    return decorator
+
+
+@transaction.commit_on_success
+def create(user_id, name, flavor, subnet, gateway=None, subnet6=None,
+           gateway6=None, public=False, dhcp=True):
+    if flavor is None:
+        raise faults.BadRequest("Missing request parameter 'type'")
+    elif flavor not in Network.FLAVORS.keys():
+        raise faults.BadRequest("Invalid network type '%s'" % flavor)
+
+    # Check that network parameters are valid
+    util.validate_network_params(subnet, gateway, subnet6, gateway6)
+
+    try:
+        mode, link, mac_prefix, tags = util.values_from_flavor(flavor)
+    except EmptyPool:
+        log.error("Failed to allocate resources for network of type: %s",
+                  flavor)
+        msg = "Failed to allocate resources for network."
+        raise faults.ServiceUnavailable(msg)
+    validate_mac(mac_prefix + "0:00:00:00")
+
+    network = Network.objects.create(
+        name=name,
+        userid=user_id,
+        subnet=subnet,
+        subnet6=subnet6,
+        gateway=gateway,
+        gateway6=gateway6,
+        dhcp=dhcp,
+        flavor=flavor,
+        mode=mode,
+        link=link,
+        mac_prefix=mac_prefix,
+        tags=tags,
+        action='CREATE',
+        state='ACTIVE')
+
+    # Issue commission to Quotaholder and accept it since at the end of
+    # this transaction the Network object will be created in the DB.
+    # Note: the following call does a commit!
+    quotas.issue_and_accept_commission(network)
+    return network
+
+
+@network_command("RENAME")
+def rename(network, name):
+    network.name = name
+    network.save()
+    return network
+
+
+@network_command("DESTROY")
+def delete(network):
+    if network.machines.exists():
+        raise faults.NetworkInUse("Can not delete network. Servers connected"
+                                  " to this network exists.")
+    if network.floating_ips.filter(deleted=False).exists():
+        msg = "Can not delete netowrk. Network has allocated floating IPs."
+        raise faults.NetworkInUse(msg)
+
+    network.action = "DESTROY"
+    network.save()
+
+    # Delete network to all backends that exists
+    backend_networks = network.backend_networks.exclude(operstate="DELETED")
+    for bnet in backend_networks:
+        backend.delete_network(network, bnet.backend)
+    # If network does not exist in any backend, update the network state
+    if not backend_networks:
+        backend.update_network_state(network)
+    return network
