commit d6b241307f4e8fb6c20462bc31944ad81d11bde0
Author: Marios Kogias <marioskogias@grnet.gr>
Date:   Mon Sep 23 12:10:41 2013 +0300

    Start neutron api
    
    Add views for network, subnet, port. Fix urls. Create first version
    of /networks. Start testing networks

diff --git a/snf-cyclades-app/synnefo/neutron/__init__.py b/snf-cyclades-app/synnefo/neutron/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/snf-cyclades-app/synnefo/neutron/models.py b/snf-cyclades-app/synnefo/neutron/models.py
new file mode 100644
index 0000000..21bd3f8
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/models.py
@@ -0,0 +1,267 @@
+# Copyright 2011-2012 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 THE REGENTS AND CONTRIBUTORS ``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 THE REGENTS 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.
+
+import datetime
+
+from copy import deepcopy
+from django.conf import settings
+from django.db import models
+from django.db import IntegrityError
+
+from contextlib import contextmanager
+from hashlib import sha1
+from snf_django.lib.api import faults
+from django.conf import settings as snf_settings
+
+from synnefo.db.managers import ForUpdateManager, ProtectedDeleteManager
+from synnefo.db import pools
+
+from synnefo.db.models import VirtualMachine, QuotaHolderSerial
+
+from synnefo.logic.rapi_pool import (get_rapi_client,
+                                     put_rapi_client)
+
+import logging
+log = logging.getLogger(__name__)
+
+
+
+class Network(models.Model):
+    OPER_STATES = (
+        ('PENDING', 'Pending'),  # Unused because of lazy networks
+        ('ACTIVE', 'Active'),
+        ('DELETED', 'Deleted'),
+        ('ERROR', 'Error')
+    )
+
+    ACTIONS = (
+        ('CREATE', 'Create Network'),
+        ('DESTROY', 'Destroy Network'),
+        ('ADD', 'Add server to Network'),
+        ('REMOVE', 'Remove server from Network'),
+    )
+
+    RSAPI_STATE_FROM_OPER_STATE = {
+        'PENDING': 'PENDING',
+        'ACTIVE': 'ACTIVE',
+        'DELETED': 'DELETED',
+        'ERROR': 'ERROR'
+    }
+
+    FLAVORS = {
+        'CUSTOM': {
+            'mode': 'bridged',
+            'link': settings.DEFAULT_BRIDGE,
+            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
+            'tags': None,
+            'desc': "Basic flavor used for a bridged network",
+        },
+        'IP_LESS_ROUTED': {
+            'mode': 'routed',
+            'link': settings.DEFAULT_ROUTING_TABLE,
+            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
+            'tags': 'ip-less-routed',
+            'desc': "Flavor used for an IP-less routed network using"
+                    " Proxy ARP",
+        },
+        'MAC_FILTERED': {
+            'mode': 'bridged',
+            'link': settings.DEFAULT_MAC_FILTERED_BRIDGE,
+            'mac_prefix': 'pool',
+            'tags': 'private-filtered',
+            'desc': "Flavor used for bridged networks that offer isolation"
+                    " via filtering packets based on their src "
+                    " MAC (ebtables)",
+        },
+        'PHYSICAL_VLAN': {
+            'mode': 'bridged',
+            'link': 'pool',
+            'mac_prefix': settings.DEFAULT_MAC_PREFIX,
+            'tags': 'physical-vlan',
+            'desc': "Flavor used for bridged network that offer isolation"
+                    " via dedicated physical vlan",
+        },
+    }
+
+    name = models.CharField('Network Name', max_length=128)
+    userid = models.CharField('User ID of the owner', max_length=128,
+                              null=True, db_index=True)
+    flavor = models.CharField('Flavor', max_length=32, null=False)
+    mode = models.CharField('Network Mode', max_length=16, null=True)
+    link = models.CharField('Network Link', max_length=32, null=True)
+    mac_prefix = models.CharField('MAC Prefix', max_length=32, null=False)
+    tags = models.CharField('Network Tags', max_length=128, null=True)
+    public = models.BooleanField(default=False, db_index=True)
+    created = models.DateTimeField(auto_now_add=True)
+    updated = models.DateTimeField(auto_now=True)
+    deleted = models.BooleanField('Deleted', default=False, db_index=True)
+    state = models.CharField(choices=OPER_STATES, max_length=32,
+                             default='PENDING')
+    machines = models.ManyToManyField(VirtualMachine,
+                                      through='NetworkInterface', related_name='neutron_machines')
+    action = models.CharField(choices=ACTIONS, max_length=32, null=True,
+                              default=None)
+    drained = models.BooleanField("Drained", default=False, null=False) # this is the opposite of admin_state_up
+    floating_ip_pool = models.BooleanField('Floating IP Pool', null=False,
+                                           default=False)
+    #serial = models.ForeignKey(QuotaHolderSerial, related_name='network',
+    #                           null=True)
+
+    objects = ForUpdateManager()
+
+    def __unicode__(self):
+        return "<Network: %s>" % str(self.id)
+
+    @property
+    def backend_id(self):
+        """Return the backend id by prepending backend-prefix."""
+        if not self.id:
+            raise Network.InvalidBackendIdError("self.id is None")
+        return "%snet-%s" % (settings.BACKEND_PREFIX_ID, str(self.id))
+
+    @property
+    def backend_tag(self):
+        """Return the network tag to be used in backend
+
+        """
+        if self.tags:
+            return self.tags.split(',')
+        else:
+            return []
+
+    def create_backend_network(self, backend=None):
+        """Create corresponding BackendNetwork entries."""
+
+        backends = [backend] if backend else\
+            Backend.objects.filter(offline=False)
+        for backend in backends:
+            backend_exists =\
+                BackendNetwork.objects.filter(backend=backend, network=self)\
+                                      .exists()
+            if not backend_exists:
+                BackendNetwork.objects.create(backend=backend, network=self)
+
+    def get_pool(self, with_lock=True):
+        if not self.pool_id:
+            self.pool = IPPoolTable.objects.create(available_map='',
+                                                   reserved_map='',
+                                                   size=0)
+            self.save()
+        objects = IPPoolTable.objects
+        if with_lock:
+            objects = objects.select_for_update()
+        return objects.get(id=self.pool_id).pool
+
+    def reserve_address(self, address):
+        pool = self.get_pool()
+        pool.reserve(address)
+        pool.save()
+
+    def release_address(self, address):
+        pool = self.get_pool()
+        pool.put(address)
+        pool.save()
+
+    class InvalidBackendIdError(Exception):
+        def __init__(self, value):
+            self.value = value
+
+        def __str__(self):
+            return repr(self.value)
+
+    class InvalidBackendMsgError(Exception):
+        def __init__(self, opcode, status):
+            self.opcode = opcode
+            self.status = status
+
+        def __str__(self):
+            return repr('<opcode: %s, status: %s>'
+                        % (self.opcode, self.status))
+
+    class InvalidActionError(Exception):
+        def __init__(self, action):
+            self._action = action
+
+        def __str__(self):
+            return repr(str(self._action))
+
+class Subnet(models.Model):
+
+    name = models.CharField('Network Name', max_length=128)
+    subnet_id = models.CharField('ID of the subnet', max_length=128,
+                              null=True, db_index=True)
+    network_id = models.ForeignKey('Network')
+    # subnet will be null for IPv6 only networks
+    subnet = models.CharField('Subnet', max_length=32, null=True)
+    # subnet6 will be null for IPv4 only networks
+    subnet6 = models.CharField('IPv6 Subnet', max_length=64, null=True)
+    gateway = models.CharField('Gateway', max_length=32, null=True)
+    gateway6 = models.CharField('IPv6 Gateway', max_length=64, null=True)
+    dhcp = models.BooleanField('DHCP', default=True)
+    #pool = models.OneToOneField('IPPoolTable', related_name='network',
+    #                            default=lambda: IPPoolTable.objects.create(
+    #                                                        available_map='',
+    #                                                        reserved_map='',
+    #                                                        size=0),
+    #                           null=True)
+
+class NetworkInterface(models.Model):
+    STATES = (
+        ("ACTIVE", "Active"),
+        ("BUILDING", "Building"),
+    )
+
+    name = models.CharField('Network Name', max_length=128)
+    machine = models.ForeignKey(VirtualMachine, related_name='neutron_nics')
+    network = models.ForeignKey(Network, related_name='neutron_nics')
+    created = models.DateTimeField(auto_now_add=True)
+    updated = models.DateTimeField(auto_now=True)
+    index = models.IntegerField(null=True)
+    mac = models.CharField(max_length=32, null=True, unique=True)
+    ipv4 = models.CharField(max_length=15, null=True)
+    ipv6 = models.CharField(max_length=100, null=True)
+    #firewall_profile = models.CharField(choices=FIREWALL_PROFILES,
+    #                                    max_length=30, null=True)
+    dirty = models.BooleanField(default=False)
+    state = models.CharField(max_length=32, null=False, default="ACTIVE",
+                             choices=STATES)
+    admin_state_up = models.BooleanField(default=False, db_index=True)
+
+    def __unicode__(self):
+        return "<%s:vm:%s network:%s ipv4:%s ipv6:%s>" % \
+            (self.index, self.machine_id, self.network_id, self.ipv4,
+             self.ipv6)
+
+    @property
+    def is_floating_ip(self):
+        network = self.network
+        if self.ipv4 and network.floating_ip_pool:
+            return network.floating_ips.filter(machine=self.machine,
+                                               ipv4=self.ipv4,
+                                               deleted=False).exists()
diff --git a/snf-cyclades-app/synnefo/neutron/network_views.py b/snf-cyclades-app/synnefo/neutron/network_views.py
new file mode 100644
index 0000000..01d1957
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/network_views.py
@@ -0,0 +1,260 @@
+from django.http import HttpResponse
+from django.utils import simplejson as json
+from django.db import transaction
+from django.db.models import Q
+from synnefo.db.pools import EmptyPool
+from synnefo.db.utils import validate_mac
+from django.conf import settings
+from snf_django.lib import api
+from snf_django.lib.api import utils
+from synnefo.logic import backend
+from django.template.loader import render_to_string
+from synnefo.api import util
+from models import Network
+
+from logging import getLogger
+
+from synnefo import quotas
+
+
+log = getLogger(__name__)
+
+def demux(request):
+    if request.method == 'GET':
+        #return HttpResponse("in network get")
+        return list_networks(request)
+    elif request.method == 'POST':
+        #return create_network(request)
+        return HttpResponse("in network post")
+    else:
+        return api.api_method_not_allowed(request)
+
+def network_demux(request,offset):
+
+    if request.method == 'GET':
+        return get_network(request,offset)
+        #return HttpResponse("in network det get")
+    elif request.method == 'DELETE':
+        return delete_network(request,offset)
+        #return HttpResponse("in network det delete")
+    elif request.method == 'PUT':
+        #return update_network(request,offset)
+        return HttpResponse("in network det put")
+    else:
+        return api.api_method_not_allowed(request)
+
+@api.api_method(http_method='GET', user_required=True, logger=log)
+def list_networks(request):
+    log.debug('list_networks')
+
+    user_networks = Network.objects.filter(Q(userid=request.user_uniq) |
+            Q(public=True))
+
+    user_networks = user_networks.filter(deleted=False)
+
+    networks = [network_to_dict(network)
+                for network in user_networks.order_by('id')]
+
+    if request.serialization == 'xml':
+        data = render_to_string('list_networks.xml', {
+            'networks': networks })
+    else:
+        data = json.dumps({'networks': networks})
+
+    return HttpResponse(data, status=200)
+
+@api.api_method(http_method='POST', user_required=True, logger=log)
+@transaction.commit_manually
+def create_network (request):
+    '''
+    This operation does not require a rest body. If specified, the body might
+    include one or more of the following attributes:
+
+    name: a string specifying a symbolic name for the network, which is not
+        required to be unique;
+
+    admin_state_up: a bool value specifying the administrative status of the network;
+
+    2 more attirbutes for administrative users (not supported)
+    '''
+
+    try:
+        user_id = request.user_uniq
+        if request.raw_post_data:
+            req = utils.get_request_dict(request)
+            log.info('create_network %s', req)
+            try:
+                 d = req['network']
+                 name = d['name']
+            except KeyError:
+                 raise api.faults.BadRequest("Malformed request")
+
+            flavor = d.get("type", None)
+            if flavor is None:
+                #raise faults.BadRequest("Missing request parameter 'type'")
+                # set default flavor
+                #FIX ME!!!
+                flavor="MAC_FILTERED"
+                log.info("not found flavor")
+            elif flavor not in Network.FLAVORS.keys():
+                raise api.faults.BadRequest("Invalid network type '%s'" % flavor)
+                log.info("found flavor 1")
+            elif flavor not in settings.API_ENABLED_NETWORK_FLAVORS:
+                log.info("found flavor2")
+                raise api.faults.Forbidden("Can not create network of type '%s'" %flavor)
+            # Get and validate flavor. Flavors are still exposed as 'type' in the
+            # API.
+        else:
+            name = ""
+            flavor = "MAC_FILTERED" # this is the default FIX ME
+
+        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,
+                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 api.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)
+        # COME BACK....
+
+    except:
+        transaction.rollback()
+        log.info("roll")
+        raise
+    else:
+        transaction.commit()
+        log.info("commit")
+    networkdict = network_to_dict(network)
+    response = render_network(request, networkdict, status=201)
+
+    return response
+
+@api.api_method(http_method='GET', user_required=True, logger=log)
+def get_network(request,network_id):
+    log.debug('get_network_details %s', network_id)
+    net = get_network_fromdb(network_id, request.user_uniq)
+
+    #needs discussion
+    if net.deleted:
+        raise api.faults.BadRequest("Network has been deleted.")
+    else:
+        netdict = network_to_dict(net)
+        return render_network(request, netdict)
+
+@api.api_method(http_method='DELETE', user_required=True, logger=log)
+@transaction.commit_on_success
+def delete_network(request,network_id):
+    log.info('delete_network %s', network_id)
+    net = get_network_fromdb(network_id, request.user_uniq, for_update=True)
+
+    log.info(net.name)
+    if net.public:
+        raise api.faults.Forbidden('Can not delete the public network.')
+
+    if net.deleted:
+        raise api.faults.BadRequest("Network has been deleted.")
+
+    if net.machines.all():  # Nics attached on network
+        raise api.faults.NetworkInUse('Machines are connected to network.')
+
+    net.action = 'DESTROY'
+    net.save()
+    '''
+    skip the backend part...
+    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)
+    '''
+
+    #the following has to leave when fix the backend thing
+    net.deleted = True
+    net.save()
+
+    return HttpResponse(status=204)
+
+
+@api.api_method(http_method='PUT', user_required=True, logger=log)
+def update_network(request,network_id):
+    '''
+    You can update only name and admin_state_up
+    '''
+    net = get_network_fromdb(network_id, request.user_uniq, for_update=True)
+    info = utils.get_request_dict(request)
+
+    updatable = set(["name","admin_state_up"])
+    try:
+		new_vals = info["network"]
+    except Keyerror:
+        raise api.faults.BadRequest("Malformed request")
+
+    for key,val in new_vals.iteritems():
+        if key in updatable:
+            setattr(net,key,val)
+        else:
+            raise api.faults.BadRequest("Wrong field update")
+	net.save()
+    #net_dic = network_to_dict(net)
+    #data = json.dumps({"network":net_dic})
+    netdict = network_to_dict(net)
+    return render_network(request, netdict, 200)
+
+def network_to_dict(network):
+    d = {'id': str(network.id), 'name': network.name}
+    d['links'] = util.network_to_links(network.id)
+    d['user_id'] = network.userid
+    d['tenant_id'] = network.userid
+    d['type'] = network.flavor
+    d['updated'] = utils.isoformat(network.updated)
+    d['created'] = utils.isoformat(network.created)
+    d['status'] = network.state
+    d['public'] = network.public
+    d['admin_state_up'] = "true"
+    subnet_cidr = [s.subnet_id for s in network.subnet_set.all()]
+    d['subnets'] = subnet_cidr
+    return d
+
+
+
+
+
+def render_network(request, networkdict, status=200):
+    if request.serialization == 'xml':
+        data = render_to_string('network.xml', {'network': networkdict})
+    else:
+        data = json.dumps({'network': networkdict})
+    return HttpResponse(data, status=status)
+
+
+
+
+def get_network_fromdb(network_id, user_id, for_update=False):
+    """
+    Return a Network instance or raise ItemNotFound.
+    This is the same as util.get_network
+    """
+    try:
+        network_id = int(network_id)
+        objects = Network.objects
+        if for_update:
+            objects = objects.select_for_update()
+        return objects.get(Q(userid=user_id) | Q(public=True), id=network_id)
+    except (ValueError, Network.DoesNotExist):
+        raise api.faults.ItemNotFound('Network not found.')
diff --git a/snf-cyclades-app/synnefo/neutron/port_views.py b/snf-cyclades-app/synnefo/neutron/port_views.py
new file mode 100644
index 0000000..c981f0e
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/port_views.py
@@ -0,0 +1,133 @@
+from django.http import HttpResponse
+from django.utils import simplejson as json
+from django.db import transaction
+from django.db.models import Q
+from synnefo.db.pools import EmptyPool
+from synnefo.db.utils import validate_mac
+from django.conf import settings
+from snf_django.lib import api
+from snf_django.lib.api import utils
+from synnefo.logic import backend
+from django.template.loader import render_to_string
+from synnefo.api import util
+from models import NetworkInterface
+
+from logging import getLogger
+
+
+
+log = getLogger(__name__)
+
+def demux(request):
+    if request.method == 'GET':
+        return HttpResponse("list ports")
+        #return list_ports(request)
+    elif request.method == 'POST':
+        #return create_port(request)
+        return HttpResponse("create port")
+    else:
+        return api.api_method_not_allowed(request)
+
+def port_demux(request,offset):
+
+    if request.method == 'GET':
+        return HttpResponse("get single port")
+        #return get_port(request,offset)
+    elif request.method == 'DELETE':
+        return HttpResponse("delete port")
+        #return delete_port(request,offset)
+    elif request.method == 'PUT':
+        return HttpResponse("put port")
+        #return update_port(request,offset)
+    else:
+        return api.api_method_not_allowed(request)
+"""
+@api.api_method(http_method='GET', user_required=True, logger=log)
+def list_ports(request):
+    log.debug('list_networks')
+
+    user_networks = Network.objects.filter(Q(userid=request.user_uniq) |
+            Q(public=True))
+
+    user_networks = user_networks.filter(deleted=False)
+
+    networks = [network_to_dict(network)
+                for network in user_networks.order_by('id')]
+
+    if request.serialization == 'xml':
+        data = render_to_string('list_networks.xml', {
+            'networks': networks })
+    else:
+        data = json.dumps({'networks': networks})
+
+    return HttpResponse(data, status=200)
+"""
+@api.api_method(http_method='POST', user_required=True, logger=log)
+@transaction.commit_manually
+def create_port (request):
+    '''
+    '''
+
+    try:
+        user_id = request.user_uniq
+        req = utils.get_request_dict(request)
+        log.info('create_network %s', req)
+        try:
+             d = req['port']
+             name = d['name']
+        except KeyError:
+             raise api.faults.BadRequest("Malformed request")
+
+        flavor = d.get("type", None)
+        if flavor is None:
+            #raise faults.BadRequest("Missing request parameter 'type'")
+            # set default flavor
+            #FIX ME!!!
+            flavor="MAC_FILTERED"
+            log.info("not found flavor")
+        elif flavor not in Network.FLAVORS.keys():
+            raise api.faults.BadRequest("Invalid network type '%s'" % flavor)
+            log.info("found flavor 1")
+        elif flavor not in settings.API_ENABLED_NETWORK_FLAVORS:
+            log.info("found flavor2")
+            raise api.faults.Forbidden("Can not create network of type '%s'" %flavor)
+        # Get and validate flavor. Flavors are still exposed as 'type' in the
+        # API.
+
+        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,
+                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 api.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)
+        # COME BACK....
+
+    except:
+        transaction.rollback()
+        log.info("roll")
+        raise
+    else:
+        transaction.commit()
+        log.info("commit")
+    networkdict = network_to_dict(network)
+    response = render_network(request, networkdict, status=201)
+
+    return response
+
diff --git a/snf-cyclades-app/synnefo/neutron/subnet_views.py b/snf-cyclades-app/synnefo/neutron/subnet_views.py
new file mode 100644
index 0000000..e28944a
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/subnet_views.py
@@ -0,0 +1,109 @@
+from django.http import HttpResponse
+from django.utils import simplejson as json
+
+from snf_django.lib import api
+from snf_django.lib.api import utils
+
+from models import Network
+
+from logging import getLogger
+
+log = getLogger(__name__)
+
+def demux(request):
+    if request.method == 'GET':
+        return list_networks(request)
+        #return HttpResponse("in subnet get")
+    elif request.method == 'POST':
+        #return create_network(request)
+        return HttpResponse("in subnet POST")
+    else:
+        return api.api_method_not_allowed(request)
+
+def subnet_demux(request,offset):
+    if request.method == 'GET':
+        #return get_network(request,offset)
+        return HttpResponse("in subnet det get")
+    elif request.method == 'DELETE':
+        #return delete_network(request,offset)
+        return HttpResponse("in subnet det delete")
+    elif request.method == 'PUT':
+        #return update_network(request,offset)
+        return HttpResponse("in subnet det put")
+    else:
+        return api.api_method_not_allowed(request)
+
+@api.api_method(http_method='GET', user_required=True, logger=log)
+def list_networks(request):
+    user_networks = Network.objects.all()
+    networks_dict = [network_to_dict(net)
+                    for net in user_networks.order_by('name')]
+    data = json.dumps({'networks': networks_dict})
+    return HttpResponse(data, status=200)
+
+@api.api_method(http_method='POST', user_required=True, logger=log)
+def create_network(request):
+    dic = utils.get_request_dict(request)
+    try:
+    	net = dic["network"]
+    	name = net["name"]
+    	user_id = request.user_uniq
+    except KeyError:
+        raise api.faults.BadRequest("Wrong data")
+    # get more info from the JSON
+    new_net = Network(name=name,user_id=user_id)
+    new_net.save()
+    net_dic = network_to_dict(new_net)
+    data = json.dumps({'network':net_dic})
+    return HttpResponse("hello world")
+
+
+@api.api_method(http_method='GET', user_required=True, logger=log)
+def get_network(request,offset):
+    try:
+        net = Network.objects.get(id=offset)
+    except Network.DoesNotExist:
+        raise api.faults.ItemNotFound("Network not found")
+
+    net_dic = network_to_dict(net)
+    data = json.dumps({'network': net_dic})
+    return HttpResponse(data,status=200)
+
+@api.api_method(http_method='DELETE', user_required=True, logger=log)
+def delete_network(request,offset):
+   try:
+        net = Network.objects.get(id=offset)
+   except Network.DoesNotExist:
+        raise api.faults.ItemNotFound("Network not found")
+
+   net.delete()
+   return HttpResponse(status=204)
+
+@api.api_method(http_method='PUT', user_required=True, logger=log)
+def update_network(request,offset):
+    try:
+        net = Network.objects.get(id=offset)
+    except Network.DoesNotExist:
+        raise api.faults.ItemNotFound("Network not found")
+    info = utils.get_request_dict(request)
+    updatable = set(["name","driver"])
+    try:
+		new_vals = info["network"]
+    except Keyerror:
+        raise api.faults.BadRequest()
+
+    for key,val in new_vals.iteritems():
+        if key in updatable:
+            setattr(net,key,val)
+        else:
+            raise api.faults.BadRequest()
+	net.save()
+    net_dic = network_to_dict(net)
+    data = json.dumps({"network":net_dic})
+    return HttpResponse(data)
+
+def network_to_dict(net):
+    d = dict(name=net.name,driver=net.driver,driver_ops=net.driver_ops,id=net.id,user_id=net.user_id)
+    return d
+
+
diff --git a/snf-cyclades-app/synnefo/neutron/tests/__init__.py b/snf-cyclades-app/synnefo/neutron/tests/__init__.py
new file mode 100644
index 0000000..3cfac2e
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/tests/__init__.py
@@ -0,0 +1 @@
+from synnefo.neutron.tests.api import *
diff --git a/snf-cyclades-app/synnefo/neutron/tests/api.py b/snf-cyclades-app/synnefo/neutron/tests/api.py
new file mode 100644
index 0000000..bfe396b
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/tests/api.py
@@ -0,0 +1,37 @@
+from snf_django.utils.testing import BaseAPITest
+from django.utils import simplejson as json
+from synnefo.cyclades_settings import cyclades_services
+from synnefo.lib.services import get_service_path
+from synnefo.lib import join_urls
+
+
+NEUTRON_URL = get_service_path(cyclades_services, "neutron", "v2.0")
+NETWORKS_URL = join_urls(NEUTRON_URL, "networks/")
+
+
+class NetworkTest(BaseAPITest):
+    def test_list_networks(self):
+        response = self.get(NETWORKS_URL)
+        self.assertSuccess(response)
+        networks = json.loads(response.content)
+        self.assertEqual(networks, {'networks': []})
+
+    def test_create_network(self):
+        request = {}
+        response = self.post(NETWORKS_URL, json.dumps(request), "json")
+        code = response.status_code
+        self.assertEqual(code, 501)
+
+    def test_get_unfound_network(self):
+        url = join_urls(NETWORKS_URL,"123")
+        response = self.get(url)
+        self.assertItemNotFound(response)
+
+    def test_delete_unfound_network(self):
+        url = join_urls(NETWORKS_URL,"123")
+        response = self.delete(url)
+        self.assertItemNotFound(response)
+
+
+    def marios(self):
+        print "hello world"
diff --git a/snf-cyclades-app/synnefo/neutron/urls.py b/snf-cyclades-app/synnefo/neutron/urls.py
new file mode 100644
index 0000000..533feee
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/urls.py
@@ -0,0 +1,56 @@
+# Copyright 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 django.conf.urls.defaults import patterns, include
+from django.http import HttpResponseNotAllowed
+from snf_django.lib.api import api_endpoint_not_found
+
+from synnefo.neutron import views
+
+
+
+
+neutron_v2_patterns = patterns('',
+        (r'^networks(?:/|.json|.xml)?$', 'synnefo.neutron.network_views.demux'),
+        (r'^networks/(\w+)(?:/|.json|.xml)?$', 'synnefo.neutron.network_views.network_demux'),
+        (r'^subnets(?:/|.json|.xml)?$', 'synnefo.neutron.subnet_views.demux'),
+        (r'^subnets/(\w+)(?:/|.json|.xml)?$', 'synnefo.neutron.subnet_views.subnet_demux'),
+        (r'^ports(?:/|.json|.xml)?$', 'synnefo.neutron.port_views.demux'),
+        (r'^ports/(\w+)(?:/|.json|.xml)?$', 'synnefo.neutron.port_views.port_demux')
+        )
+
+urlpatterns = patterns(
+    '',
+    (r'^v2.0/', include(neutron_v2_patterns)),
+    (r'^.*', api_endpoint_not_found),
+)
diff --git a/snf-cyclades-app/synnefo/neutron/views.py b/snf-cyclades-app/synnefo/neutron/views.py
new file mode 100644
index 0000000..39b66e4
--- /dev/null
+++ b/snf-cyclades-app/synnefo/neutron/views.py
@@ -0,0 +1,55 @@
+# Copyright 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.
+
+import json
+
+from logging import getLogger
+#from django.conf import settings
+from django.http import HttpResponse
+
+from snf_django.lib import api
+from snf_django.lib.api import faults
+
+log = getLogger('synnefo.neutron')
+
+
+@api.api_method(http_method="GET", user_required=True, logger=log)
+def list_networks(request, detail=False):
+    networks = []
+    data = json.dumps(networks)
+    return HttpResponse(data)
+
+
+@api.api_method(http_method="POST", user_required=True, logger=log)
+def add_network(request):
+    raise faults.NotImplemented(message="Adding a network is not supported.")
