From 9cad4715db5a211de9a15e5e3a5e480819f92a4a Mon Sep 17 00:00:00 2001
From: Leonidas Poulopoulos <leopoul@noc.grnet.gr>
Date: Mon, 21 Nov 2011 19:34:42 +0200
Subject: [PATCH] Added celery and beanstalk

---
 flowspec/admin.py          |   4 +-
 flowspec/forms.py          |  57 +++++++
 flowspec/models.py         |  35 ++--
 flowspec/tasks.py          |  40 +++++
 flowspec/tests.py          |  23 ---
 flowspec/views.py          |  25 ++-
 flowspec_dev.db            | Bin 70656 -> 121856 bytes
 templates/apply.html       |  86 ++++------
 templates/base.html        |   2 +
 templates/user_routes.html |  28 +++-
 urls.py                    |   1 +
 utils/beanstalkc.py        | 328 +++++++++++++++++++++++++++++++++++++
 12 files changed, 532 insertions(+), 97 deletions(-)
 create mode 100644 flowspec/forms.py
 create mode 100644 flowspec/tasks.py
 delete mode 100644 flowspec/tests.py
 create mode 100644 utils/beanstalkc.py

diff --git a/flowspec/admin.py b/flowspec/admin.py
index f6c3ad9e..35052d16 100644
--- a/flowspec/admin.py
+++ b/flowspec/admin.py
@@ -5,7 +5,7 @@ from utils import proxy as PR
 class RouteAdmin(admin.ModelAdmin):
     
     actions = ['deactivate']
-
+    
     def deactivate(self, request, queryset):
         applier = PR.Applier(route_objects=queryset)
         commit, response = applier.apply(configuration=applier.delete_routes())
@@ -19,7 +19,7 @@ class RouteAdmin(admin.ModelAdmin):
 
     list_display = ('name', 'is_online', 'applier', 'get_match', 'get_then', 'response')
     fieldsets = [
-        (None,               {'fields': ['name',]}),
+        (None,               {'fields': ['name','applier']}),
         ("Match",               {'fields': ['source', 'sourceport', 'destination', 'destinationport', 'port']}),
         ('Advanced Match Statements', {'fields': ['dscp', 'fragmenttype', 'icmpcode', 'icmptype', 'packetlength', 'protocol', 'tcpflag'], 'classes': ['collapse']}),
         ("Then",               {'fields': ['then' ]}),
diff --git a/flowspec/forms.py b/flowspec/forms.py
new file mode 100644
index 00000000..bdd78a69
--- /dev/null
+++ b/flowspec/forms.py
@@ -0,0 +1,57 @@
+from django import forms
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext as _
+from django.utils.translation import ugettext_lazy
+from django.template.defaultfilters import filesizeformat
+
+from flowspy.flowspec.models import * 
+from ipaddr import *
+
+class RouteForm(forms.ModelForm):
+#    name = forms.CharField(help_text=ugettext_lazy("A unique route name,"
+#                                         " e.g. uoa_block_p80"), label=ugettext_lazy("Route Name"), required=False)
+#    source = forms.CharField(help_text=ugettext_lazy("A qualified IP Network address. CIDR notation,"
+#                                         " e.g.10.10.0.1/32"), label=ugettext_lazy("Source Address"), required=False)
+#    source_ports = forms.ModelMultipleChoiceField(queryset=MatchPort.objects.all(), help_text=ugettext_lazy("A set of source ports to block"), label=ugettext_lazy("Source Ports"), required=False)
+#    destination = forms.CharField(help_text=ugettext_lazy("A qualified IP Network address. CIDR notation,"
+#                                         " e.g.10.10.0.1/32"), label=ugettext_lazy("Destination Address"), required=False)
+#    destination_ports = forms.ModelMultipleChoiceField(queryset=MatchPort.objects.all(), help_text=ugettext_lazy("A set of destination ports to block"), label=ugettext_lazy("Destination Ports"), required=False)
+#    ports = forms.ModelMultipleChoiceField(queryset=MatchPort.objects.all(), help_text=ugettext_lazy("A set of ports to block"), label=ugettext_lazy("Ports"), required=False)
+    class Meta:
+        model = Route
+    
+    def clean_source(self):
+        data = self.cleaned_data['source']
+        if data:
+            try:
+                address = IPNetwork(data)
+                return self.cleaned_data["source"]
+            except Exception:
+                raise forms.ValidationError('Invalid network address format')
+
+    def clean_destination(self):
+        data = self.cleaned_data['destination']
+        if data:
+            try:
+                address = IPNetwork(data)
+                return self.cleaned_data["destination"]
+            except Exception:
+                raise forms.ValidationError('Invalid network address format')
+
+    def clean(self):
+        source = self.cleaned_data.get('source', None)
+        sourceports = self.cleaned_data.get('sourceport', None)
+        ports = self.cleaned_data.get('port', None)
+        destination = self.cleaned_data.get('destination', None)
+        destinationports = self.cleaned_data.get('destinationport', None)
+        if (sourceports and ports):
+            raise forms.ValidationError('Cannot create rule for source ports and ports at the same time. Select either ports or source ports')
+        if (destinationports and ports):
+            raise forms.ValidationError('Cannot create rule for destination ports and ports at the same time. Select either ports or destination ports')
+        if sourceports and not source:
+            raise forms.ValidationError('Once source port is matched, source has to be filled as well. Either deselect source port or fill source address')
+        if destinationports and not destination:
+            raise forms.ValidationError('Once destination port is matched, destination has to be filled as well. Either deselect destination port or fill destination address')
+        if not (source or sourceports or ports or destination or destinationports):
+            raise forms.ValidationError('Fill at least a Route Match Condition')
+        return self.cleaned_data
\ No newline at end of file
diff --git a/flowspec/models.py b/flowspec/models.py
index b3f917b8..257c6603 100644
--- a/flowspec/models.py
+++ b/flowspec/models.py
@@ -8,6 +8,8 @@ from utils import proxy as PR
 from ipaddr import *
 from datetime import *
 import logging
+from flowspec.tasks import *
+from time import sleep
 
 FORMAT = '%(asctime)s %(levelname)s: %(message)s'
 logging.basicConfig(format=FORMAT)
@@ -61,7 +63,7 @@ class ThenAction(models.Model):
 
 class Route(models.Model):
     name = models.CharField(max_length=128)
-    applier = models.ForeignKey(User)
+    applier = models.ForeignKey(User, blank=True, null=True)
     source = models.CharField(max_length=32, blank=True, null=True, help_text=u"Network address. Use address/CIDR notation", verbose_name="Source Address")
     sourceport = models.ManyToManyField(MatchPort, blank=True, null=True, related_name="matchSourcePort", verbose_name="Source Port")
     destination = models.CharField(max_length=32, blank=True, null=True, help_text=u"Network address. Use address/CIDR notation", verbose_name="Destination Address")
@@ -79,7 +81,7 @@ class Route(models.Model):
     last_updated = models.DateTimeField(auto_now=True)
     is_online = models.BooleanField(default=False)
     is_active = models.BooleanField(default=False)
-    expires = models.DateField(default=days_offset)
+    expires = models.DateField(default=days_offset, blank=True, null=True,)
     response = models.CharField(max_length=512, blank=True, null=True)
     comments = models.TextField(null=True, blank=True, verbose_name="Comments")
 
@@ -106,17 +108,24 @@ class Route(models.Model):
             except Exception:
                 raise ValidationError('Invalid network address format at Source Field')
     
-    def save(self, *args, **kwargs):
-        applier = PR.Applier(route_object=self)
-        commit, response = applier.apply()
-        if commit:
-            self.is_online = True
-            self.is_active = True
-            self.response = response
-        else:
-            self.is_online = False
-            self.response = response
-        super(Route, self).save(*args, **kwargs)
+#    def save(self, *args, **kwargs):
+#        edit = False
+#        if self.pk:
+#            #This is an edit
+#            edit = True
+#        super(Route, self).save(*args, **kwargs)
+#        if not edit:
+#            response = add.delay(self)
+#            logger.info("Got save job id: %s" %response)
+    
+    def commit_add(self, *args, **kwargs):
+        response = add.delay(self)
+        logger.info("Got save job id: %s" %response)
+#    
+#    def delete(self, *args, **kwargs):
+#        response = delete.delay(self)
+#        logger.info("Got delete job id: %s" %response)
+        
 
     def is_synced(self):
         
diff --git a/flowspec/tasks.py b/flowspec/tasks.py
new file mode 100644
index 00000000..3a997af2
--- /dev/null
+++ b/flowspec/tasks.py
@@ -0,0 +1,40 @@
+from utils import proxy as PR
+from celery.task import task
+
+@task
+def add(route):
+    applier = PR.Applier(route_object=route)
+    commit, response = applier.apply()
+    if commit:
+        is_online = True
+        is_active = True
+    else:
+        is_online = False
+        is_active = True
+    route.is_online = is_online
+    route.is_active = is_active
+    route.response = response
+    route.save()
+#
+#@task
+#def delete(route):
+#    
+#    applier = PR.Applier(route_object=route)
+#    commit, response = applier.apply(configuration=applier.delete_routes())
+#    if commit:
+#            rows = queryset.update(is_online=False, is_active=False)
+#            queryset.update(response="Successfully removed route from network")
+#            self.message_user(request, "Successfully removed %s routes from network" % rows)
+#        else:
+#            self.message_user(request, "Could not remove routes from network")
+#    if commit:
+#        is_online = False
+#        is_active = False
+#        response = "Successfully removed route from network"
+#    else:
+#        is_online = False
+#        is_active = True
+#    route.is_online = is_online
+#    route.is_active = is_active
+#    route.response = response
+#    route.save()
\ No newline at end of file
diff --git a/flowspec/tests.py b/flowspec/tests.py
deleted file mode 100644
index 2247054b..00000000
--- a/flowspec/tests.py
+++ /dev/null
@@ -1,23 +0,0 @@
-"""
-This file demonstrates two different styles of tests (one doctest and one
-unittest). These will both pass when you run "manage.py test".
-
-Replace these with more appropriate tests for your application.
-"""
-
-from django.test import TestCase
-
-class SimpleTest(TestCase):
-    def test_basic_addition(self):
-        """
-        Tests that 1 + 1 always equals 2.
-        """
-        self.failUnlessEqual(1 + 1, 2)
-
-__test__ = {"doctest": """
-Another way to test that 1 + 1 is equal to 2.
-
->>> 1 + 1 == 2
-True
-"""}
-
diff --git a/flowspec/views.py b/flowspec/views.py
index 77ae75a2..d8216fb0 100644
--- a/flowspec/views.py
+++ b/flowspec/views.py
@@ -3,8 +3,8 @@ import urllib2
 import re
 import socket
 from django import forms
-from django.core.cache import cache
 from django.views.decorators.csrf import csrf_exempt
+from django.core import urlresolvers
 from django.contrib.auth.decorators import login_required
 from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse
 from django.shortcuts import get_object_or_404, render_to_response
@@ -15,14 +15,35 @@ from django.utils import simplejson
 from django.core.urlresolvers import reverse
 from django.contrib import messages
 
+from flowspy.flowspec.forms import * 
 from flowspy.flowspec.models import *
 
+def days_offset(): return datetime.now() + timedelta(days = settings.EXPIRATION_DAYS_OFFSET)
+
 def user_routes(request):
     if request.user.is_anonymous():
         return HttpResponseRedirect(reverse('login'))
     user_routes = Route.objects.filter(applier=request.user)
-    print user_routes
     return render_to_response('user_routes.html', {'routes': user_routes},
                               context_instance=RequestContext(request))
 
 
+def add_route(request):
+    if request.method == "GET":
+        form = RouteForm()
+        return render_to_response('apply.html', {'form': form},
+                                  context_instance=RequestContext(request))
+
+    else:
+        form = RouteForm(request.POST)
+        if form.is_valid():
+            route=form.save(commit=False)
+            route.applier = request.user
+            route.expires = days_offset()
+            route.save()
+            form.save_m2m()
+            route.commit_add()
+            return HttpResponseRedirect(urlresolvers.reverse("user-routes"))
+        else:
+            return render_to_response('apply.html', {'form': form},
+                                      context_instance=RequestContext(request))
diff --git a/flowspec_dev.db b/flowspec_dev.db
index 9a32e30710d85f24c691b2c2a8e55e0697254adc..fdb41ff491466431e7a3eddb16d82718f09bcdc7 100644
GIT binary patch
literal 121856
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%0A_ZH8;?jcD
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
QSB*MoGz3ONfTke;0At@Hs{jB1

delta 1295
zcmZqJ!QQZfWrCCtLnZ?QgDV39Xin5I0*UHAl4fOKVBlgt2a<D|SRk@lkj0mo-O7N0
zfx$?7@(pGkE=K0xAW2>3-<t(_-ZKX?=ty!gFfgbvDl#y?X1>h4n|U?!4CYqmGUgQK
zAZ7<<ZDuiM2Bzms=a}{|EoYj<)XY@K6wl<%WWprN#L4)H@iyZz#&wL-7~2?&850<N
z8O<3LCkwJ%v=?Pz)Mn&KNzF+uNsZ6RPfyJ&DXLU~h$d$w=A|P<m4#RsRT+5_Q&Qjx
zm8M^aW0WxChN>*eFD*$`;Adf!XXFMc1M}s@Ss3*hxu8~*WTfULCYNO9=gCd4jboG$
z6=7l2WaI*=Mo7s_4~%0JuNPxs)Mey^nw^_ilAMuJoLnF!%)+S7$O+N`7n1}l<$&4&
z7B0vyDv^K)Bbxvc76+Ta0XG38CML+jsLaR?HK91OBvsT5A_Ub95)$EKVU%TL2WbRx
zg~4uTgXu{vF3!x)6PlhF%_yo3*2@M~2@w~NU|}?5WQ8hD&d)1J%_}LXEJ)>_E*s4#
zSq~0!R*(iHIbN_0EKp6ksl~;K>8U&rL8!Z+g4|#WSwQ-sB3xh*W~koM;?yEehzL|8
zNQ49IA?C!Cl=^rOpB)^<Oi(@PMfs%#Y!D%+Ca@4IIEI-(c7gdU;236v>MBSr%FP4^
z1v5k(sudy51oi+UNIzVVDUWdz&sA+f0|f&kD^n9IV>3MiOLJ2r!_Cjk<#=Rxn3gjz
zKV&|`ypwqWa~X3avn#VQGb7VorXx(tHw&_~Gi_e(7RxBZ!z{?aEXaJ9c^~r|=JU)8
znf;m5m{pj~nR}RPHw&_SWZEp~{hoz~mw|zSky)65S(xebW<i#hOiYZt6CGuy3!i6v
z%frQdn}Lye8w2w;=G&VESynN#s4)97PM1E<sId7~xR$_X78xdv&1?lsESoua{)%sA
z?fK8Wn1zR9Gs}WM%wiy;*_k#oFkfX}!d$`Z#w^M7hH3L=L6*5p+c&Rb3}jr)vVud3
zo5_xW`7-k|=2~VyW_hqjb}~(2%4D+JEXbn3wApUcF-B$;W{t`0TRb-lvfN<Wtg-zL
zj{qCP0S1NxjM+>wO!JwQnWs(`WLdL0@Q4HRvK&T<O;gxdcmf$10~rr6MKFC~uGlQd
zV$V2v;_>{=RZkq4Hl1J**vztniAUfm1Cuw?HfA;EiHx5a3mBeG7Gya;U6qZod-@_)
z#^&i>tc($xcRh1soPL{yF`toZGb0Z(ix4~WH3sHi%wL$_F~4Ab#C(VO+GatP1I*K}
V@iXedB;SE0zA(RlOA0e;0RURqXk7pR

diff --git a/templates/apply.html b/templates/apply.html
index 23b15f1b..5c83c45d 100644
--- a/templates/apply.html
+++ b/templates/apply.html
@@ -1,7 +1,7 @@
 {% extends "base.html" %}
 {% load i18n %}
-{% block title %}{% trans "Create new Virtual Machine" %}{% endblock %}
-{% block breadcrumbs %}:: {% trans "Create Instance" %}{% endblock %}
+{% block title %}{% trans "Create new Route" %}{% endblock %}
+{% block breadcrumbs %}:: {% trans "Create Route" %}{% endblock %}
 {% block content %}
 <style type="text/css">
 th {
@@ -17,80 +17,54 @@ th {
 </style>
 
 <div align="center">
-<h3>{% trans "Apply for a new instance" %}</h3>
+<h3>{% trans "Apply for a new route" %}</h3>
 <form method="POST">
 {% csrf_token %}
+{% if form.non_field_errors %}
+<p class="error">{{ form.non_field_errors|join:", "}}</p>
+{% endif %}
 <fieldset>
-<legend>{% trans "Instance information" %}</legend>
+	<legend>{% trans "Route Basic Info" %}</legend>
 <table>
-<tr><th>{{ form.hostname.label_tag }}</th><td>{{ form.hostname }}<span class="error">{{ form.hostname.errors|join:", " }}</span></td></tr>
-<tr class="help"><td></td><td>{{ form.hostname.help_text }}</td></tr>
-<tr><th>{{ form.memory.label_tag }}</th><td>{{ form.memory }}<span class="error">{{ form.memory.errors|join:", " }}</span></td></tr>
-<tr><th>{{ form.vcpus.label_tag }}</th><td>{{ form.vcpus }}<span class="error">{{ form.vcpus.errors|join:", " }}</span></td></tr>
-<tr><th>{{ form.disk_size.label_tag }}</th><td>{{ form.disk_size }}<span class="error">{{ form.disk_size.errors|join:", " }}</span></td></tr>
-<tr class="help"><td></td><td>{{ form.disk_size.help_text }}</td></tr>
-<tr><th>{{ form.hosts_mail_server.label_tag }}</th><td>{{ form.hosts_mail_server }}<span class="error">{{ form.hosts_mail_server.errors|join:", " }}</span></td></tr>
-<tr class="help"><td></td><td>{{ form.hosts_mail_server.help_text }}</td></tr>
-<tr><th>{{ form.operating_system.label_tag }}</th><td>{{ form.operating_system }}<span class="error">{{ form.operating_system.errors|join:", " }}</span></td></tr>
-{% if form.network %}
-<tr><th>{{ form.network.label_tag }}</th><td>{{ form.network }}<span class="error">{{ form.network.errors|join:", " }}</span></td></tr>
-<tr class="help"><td></td><td>{{ form.network.help_text|linebreaks }}</td></tr>
-{% endif %}
+<tr><th>{{ form.name.label_tag }}</th><td>{{ form.name }}<span class="error">{{ form.name.errors|join:", " }}</span></td></tr>
+<tr class="help"><td></td><td>{{ form.name.help_text }}</td></tr>
 </table>
 </fieldset>
 
 <fieldset>
-<legend>{% trans "Use/Comments" %}</legend>
-{% blocktrans %}
-<p>Give a short description of the intended use of this virtual machine, that justifies the parameter selection above. Feel free to include any additional comments.</p>
-{% endblocktrans %}
-<p>{{ form.comments }}
-{% if form.errors %}<br /><span class="error">{{ form.comments.errors|join:", " }}</span>{% endif %}
-</p>
+<legend>{% trans "Route Match Conditions" %}</legend>
+<table>
+<tr><th>{{ form.source.label_tag }}</th><td>{{ form.source }}<span class="error">{{ form.source.errors|join:", " }}</span></td></tr>
+<tr class="help"><td></td><td>{{ form.source.help_text }}</td></tr>
+<tr><th>{{ form.sourceport.label_tag }}</th><td>{{ form.sourceport }}<span class="error">{{ form.sourceport.errors|join:", " }}</span></td></tr>
+<tr class="help"><td></td><td>{{ form.sourceport.help_text }}</td></tr>
+<tr><th>{{ form.destination.label_tag }}</th><td>{{ form.destination }}<span class="error">{{ form.destination.errors|join:", " }}</span></td></tr>
+<tr class="help"><td></td><td>{{ form.destination.help_text }}</td></tr>
+<tr><th>{{ form.destinationport.label_tag }}</th><td>{{ form.destinationport }}<span class="error">{{ form.destinationport.errors|join:", " }}</span></td></tr>
+<tr class="help"><td></td><td>{{ form.destinationport.help_text }}</td></tr>
+<tr><th>{{ form.port.label_tag }}</th><td>{{ form.port }}<span class="error">{{ form.port.errors|join:", " }}</span></td></tr>
+<tr class="help"><td></td><td>{{ form.port.help_text }}</td></tr>
+</table>
 </fieldset>
-
 <fieldset>
-<legend>{% trans "Administrative contact" %}</legend>
-{% blocktrans %}
-<p>If you are applying on behalf of a NOC under GRNET's constituency, please select the appropriate organization. Otherwise, fill-in the admin contact information below.</p>
-{% endblocktrans %}
-
-{% if form.non_field_errors %}
-<p class="error">{{ form.non_field_errors|join:", "}}</p>
-{% endif %}
-
+<legend>{% trans "Route Actions" %}</legend>
 <table>
-<tr><th>{{ form.organization.label_tag }}</th><td>{{ form.organization }}<span class="error">{{ form.organization.errors|join:", " }}</span></td></tr>
-
-
-<tr><td colspan="3"><div align="center">{% trans "OR" %}</div></td></tr>
-
-
-<tr><th colspan="3"><div align="center">{% trans "Administrative contact" %}</div></th></tr>
-<tr><th>{% trans "Name" %}</th><td>{{ form.admin_contact_name }}<span class="error">{{ form.admin_contact_name.errors|join:", " }}</span></td></tr>
-<tr><th>E-mail</th><td>{{ form.admin_contact_email }}<span class="error">{{ form.admin_contact_email.errors|join:", " }}</span></td></tr>
-<tr><th>{% trans "Phone" %}</th><td>{{ form.admin_contact_phone }}<span class="error">{{ form.admin_contact_phone.errors|join:", " }}</span></td></tr>
+<tr><th>{{ form.then.label_tag }}</th><td>{{ form.then }}<span class="error">{{ form.then.errors|join:", " }}</span></td></tr>
+<tr class="help"><td></td><td>{{ form.then.help_text }}</td></tr>
 </table>
 </fieldset>
-
 <fieldset>
-<legend>{% trans "Miscellaneous" %}</legend>
+<legend>{% trans "Use/Comments" %}</legend>
 {% blocktrans %}
-<p>We kindly remind you of the following:</p>
-<ul align="left">
-<li>You are solely responsible for the data on your VM. You have to take care of back-ups etc.</li>
-<li>We reserve the right to temporarily suspend the operation of your VM in case it causes malfunctions to our infrastructure</li>
-</ul>
+<p>Give a short description of the intended use of this route, that justifies the parameter selection above. Feel free to include any additional comments.</p>
 {% endblocktrans %}
-<p>{{ form.accept_tos }} {% trans "I have read the above and accept them, along with the" %} <a href="/about/terms-of-service/" target="_blank">{% trans "Terms of Service" %}</a></p>
-{% if form.accept_tos.errors %}
-<p class="error">
-{% trans "You must accept the terms of service before continuing." %}
+<p>{{ form.comments }}
+{% if form.errors %}<br /><span class="error">{{ form.comments.errors|join:", " }}</span>{% endif %}
 </p>
-{% endif %}
 </fieldset>
 
 <p><input type="submit" value="{% trans "Apply" %}" /></p>
 </form>
 </div>
+
 {% endblock %}
diff --git a/templates/base.html b/templates/base.html
index 19973f06..9ce5e2e2 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -2,6 +2,8 @@
 <html>
 <head>
 <title>GRNET's FoD :: {% block title %}{% endblock %} </title>
+<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
+<META HTTP-EQUIV="Expires" CONTENT="-1">
 <script src="/static/js/jquery.min.js" type="text/javascript"></script>
 <link rel="stylesheet" type="text/css" href="/static/css/base.css">
 <link rel="stylesheet" type="text/css" href="/static/css/smoothness/jquery-ui-1.8.13.custom.css">
diff --git a/templates/user_routes.html b/templates/user_routes.html
index 6fe303d7..c29c4f7c 100644
--- a/templates/user_routes.html
+++ b/templates/user_routes.html
@@ -4,6 +4,12 @@
 <script type="text/javascript" src="/static/js/jquery.dataTables.js"></script>
 <script type="text/javascript">
 	$(document).ready( function(){
+		$('#create_dialog').dialog({
+			height: 400,
+            width: 500,
+			modal: true,
+			autoOpen: false,
+		});
 			$('#routes_table').dataTable( {
 			"bJQueryUI": true,
 			"oLanguage": {
@@ -11,12 +17,26 @@
 			},
 			"iDisplayLength": 25,
 	} );
+	$( ".button_place #routebutton" ).button({
+            icons: {
+                primary: "ui-icon-circle-plus"
+            },
+			});
 		});
+		
+		
+		
+
 </script>
 {% endblock %}
 {% block title %}{% trans "My routes" %}{% endblock %}
 {% block content %}
-<h3>{% trans "My routes" %}</h3>
+<div style="float:left">
+	<h3 style="margin-top: 0px;">{% trans "My routes" %}</h3>
+</div>
+<div class='button_place' style="float:right">
+	<a href="{% url add-route %}" id="routebutton">Add Route</a>
+</div>
 
 <table class="display" width="100%" id="routes_table">
 <thead>
@@ -27,6 +47,7 @@
 	<th style="text-align: center;">{% trans "Status" %}</th>
 	{% comment %}<th style="text-align: center;">{% trans "Details" %}</th>{% endcomment %}
 	<th style="text-align: center;">{% trans "Expires" %}</th>
+	<th style="text-align: center;">{% trans "Response" %}</th>
 </tr>
 </thead>
 
@@ -40,10 +61,15 @@
 	<td style="text-align: center;">{% if route.is_online %}Online{% else %}Offline{% endif %}</td>
 	{% comment %}<td style="text-align: center;">{{ route.response }}</td>{% endcomment %}
 	<td style="text-align: center;">{{ route.expires }}</td>
+	<td style="text-align: center;">{{ route.response }}</td>
 </tr>
 
 {% endfor %}
 </tbody>
 </table>
 
+<div id="create_dialog" title="Add a new Route">
+	KOKO
+	</div>
+
 {% endblock %}
diff --git a/urls.py b/urls.py
index 7fa85489..8cc86e65 100644
--- a/urls.py
+++ b/urls.py
@@ -8,6 +8,7 @@ urlpatterns = patterns('',
     # Example:
     # (r'^flowspy/', include('flowspy.foo.urls')),
     url(r'^/?$', 'flowspy.flowspec.views.user_routes', name="user-routes"),
+    url(r'^add/?$', 'flowspy.flowspec.views.add_route', name="add-route"),
     url(r'^user/login/?', 'django.contrib.auth.views.login', {'template_name': 'login.html'}, name="login"),
     url(r'^user/logout/?', 'django.contrib.auth.views.logout', {'next_page': '/'}, name="logout"),
     (r'^setlang/?$', 'django.views.i18n.set_language'),
diff --git a/utils/beanstalkc.py b/utils/beanstalkc.py
new file mode 100644
index 00000000..bb976df7
--- /dev/null
+++ b/utils/beanstalkc.py
@@ -0,0 +1,328 @@
+#!/usr/bin/env python
+"""beanstalkc - A beanstalkd Client Library for Python"""
+
+__license__ = '''
+Copyright (C) 2008-2010 Andreas Bolka
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+'''
+
+__version__ = '0.2.0'
+
+import logging
+import socket
+import re
+
+
+DEFAULT_HOST = 'localhost'
+DEFAULT_PORT = 11300
+DEFAULT_PRIORITY = 2**31
+DEFAULT_TTR = 120
+DEFAULT_TIMEOUT = 1
+
+
+class BeanstalkcException(Exception): pass
+class UnexpectedResponse(BeanstalkcException): pass
+class CommandFailed(BeanstalkcException): pass
+class DeadlineSoon(BeanstalkcException): pass
+class SocketError(BeanstalkcException): pass
+
+
+class Connection(object):
+    def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT,
+                 connection_timeout=DEFAULT_TIMEOUT):
+        self._socket = None
+        self.host = host
+        self.port = port
+        self.connection_timeout = connection_timeout
+        self.connect()
+
+    def connect(self):
+        """Connect to beanstalkd server, unless already connected."""
+        if not self.closed:
+            return
+        try:
+            self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            self._socket.settimeout(self.connection_timeout)
+            self._socket.connect((self.host, self.port))
+            self._socket.settimeout(None)
+            self._socket_file = self._socket.makefile('rb')
+        except socket.error, e:
+            self._socket = None
+            raise SocketError(e)
+
+    def close(self):
+        """Close connection to server, if it is open."""
+        if self.closed:
+            return
+        try:
+            self._socket.sendall('quit\r\n')
+            self._socket.close()
+        except socket.error:
+            pass
+        finally:
+            self._socket = None
+
+    @property
+    def closed(self):
+        return self._socket is None
+
+    def _interact(self, command, expected_ok, expected_err=[], size_field=None):
+        try:
+            self._socket.sendall(command)
+            status, results = self._read_response()
+            if status in expected_ok:
+                if size_field is not None:
+                    results.append(self._read_body(int(results[size_field])))
+                return results
+            elif status in expected_err:
+                raise CommandFailed(command.split()[0], status, results)
+            else:
+                raise UnexpectedResponse(command.split()[0], status, results)
+        except socket.error, e:
+            self.close()
+            raise SocketError(e)
+
+    def _read_response(self):
+        line = self._socket_file.readline()
+        if not line:
+            raise socket.error('no data read')
+        response = line.split()
+        return response[0], response[1:]
+
+    def _read_body(self, size):
+        body = self._socket_file.read(size)
+        self._socket_file.read(2) # trailing crlf
+        if size > 0 and not body:
+            raise socket.error('no data read')
+        return body
+
+    def _interact_value(self, command, expected_ok, expected_err=[]):
+        return self._interact(command, expected_ok, expected_err)[0]
+
+    def _interact_job(self, command, expected_ok, expected_err, reserved=True):
+        jid, _, body = self._interact(command, expected_ok, expected_err,
+                                      size_field=1)
+        return Job(self, int(jid), body, reserved)
+
+    def _interact_yaml_dict(self, command, expected_ok, expected_err=[]):
+        _, body, = self._interact(command, expected_ok, expected_err,
+                                  size_field=0)
+        return parse_yaml_dict(body)
+
+    def _interact_yaml_list(self, command, expected_ok, expected_err=[]):
+        _, body, = self._interact(command, expected_ok, expected_err,
+                                  size_field=0)
+        return parse_yaml_list(body)
+
+    def _interact_peek(self, command):
+        try:
+            return self._interact_job(command, ['FOUND'], ['NOT_FOUND'], False)
+        except CommandFailed, (_, status, results):
+            return None
+
+    # -- public interface --
+
+    def put(self, body, priority=DEFAULT_PRIORITY, delay=0, ttr=DEFAULT_TTR):
+        """Put a job into the current tube. Returns job id."""
+        assert isinstance(body, str), 'Job body must be a str instance'
+        jid = self._interact_value(
+                'put %d %d %d %d\r\n%s\r\n' %
+                    (priority, delay, ttr, len(body), body),
+                ['INSERTED', 'BURIED'], ['JOB_TOO_BIG'])
+        return int(jid)
+
+    def reserve(self, timeout=None):
+        """Reserve a job from one of the watched tubes, with optional timeout in
+        seconds. Returns a Job object, or None if the request times out."""
+        if timeout is not None:
+            command = 'reserve-with-timeout %d\r\n' % timeout
+        else:
+            command = 'reserve\r\n'
+        try:
+            return self._interact_job(command,
+                                      ['RESERVED'],
+                                      ['DEADLINE_SOON', 'TIMED_OUT'])
+        except CommandFailed, (_, status, results):
+            if status == 'TIMED_OUT':
+                return None
+            elif status == 'DEADLINE_SOON':
+                raise DeadlineSoon(results)
+
+    def kick(self, bound=1):
+        """Kick at most bound jobs into the ready queue."""
+        return int(self._interact_value('kick %d\r\n' % bound, ['KICKED']))
+
+    def peek(self, jid):
+        """Peek at a job. Returns a Job, or None."""
+        return self._interact_peek('peek %d\r\n' % jid)
+
+    def peek_ready(self):
+        """Peek at next ready job. Returns a Job, or None."""
+        return self._interact_peek('peek-ready\r\n')
+
+    def peek_delayed(self):
+        """Peek at next delayed job. Returns a Job, or None."""
+        return self._interact_peek('peek-delayed\r\n')
+
+    def peek_buried(self):
+        """Peek at next buried job. Returns a Job, or None."""
+        return self._interact_peek('peek-buried\r\n')
+
+    def tubes(self):
+        """Return a list of all existing tubes."""
+        return self._interact_yaml_list('list-tubes\r\n', ['OK'])
+
+    def using(self):
+        """Return a list of all tubes currently being used."""
+        return self._interact_value('list-tube-used\r\n', ['USING'])
+
+    def use(self, name):
+        """Use a given tube."""
+        return self._interact_value('use %s\r\n' % name, ['USING'])
+
+    def watching(self):
+        """Return a list of all tubes being watched."""
+        return self._interact_yaml_list('list-tubes-watched\r\n', ['OK'])
+
+    def watch(self, name):
+        """Watch a given tube."""
+        return int(self._interact_value('watch %s\r\n' % name, ['WATCHING']))
+
+    def ignore(self, name):
+        """Stop watching a given tube."""
+        try:
+            return int(self._interact_value('ignore %s\r\n' % name,
+                                            ['WATCHING'],
+                                            ['NOT_IGNORED']))
+        except CommandFailed:
+            return 1
+
+    def stats(self):
+        """Return a dict of beanstalkd statistics."""
+        return self._interact_yaml_dict('stats\r\n', ['OK'])
+
+    def stats_tube(self, name):
+        """Return a dict of stats about a given tube."""
+        return self._interact_yaml_dict('stats-tube %s\r\n' % name,
+                                        ['OK'],
+                                        ['NOT_FOUND'])
+
+    def pause_tube(self, name, delay):
+        """Pause a tube for a given delay time, in seconds."""
+        self._interact('pause-tube %s %d\r\n' %(name, delay),
+                       ['PAUSED'],
+                       ['NOT_FOUND'])
+
+    # -- job interactors --
+
+    def delete(self, jid):
+        """Delete a job, by job id."""
+        self._interact('delete %d\r\n' % jid, ['DELETED'], ['NOT_FOUND'])
+
+    def release(self, jid, priority=DEFAULT_PRIORITY, delay=0):
+        """Release a reserved job back into the ready queue."""
+        self._interact('release %d %d %d\r\n' % (jid, priority, delay),
+                       ['RELEASED', 'BURIED'],
+                       ['NOT_FOUND'])
+
+    def bury(self, jid, priority=DEFAULT_PRIORITY):
+        """Bury a job, by job id."""
+        self._interact('bury %d %d\r\n' % (jid, priority),
+                       ['BURIED'],
+                       ['NOT_FOUND'])
+
+    def touch(self, jid):
+        """Touch a job, by job id, requesting more time to work on a reserved
+        job before it expires."""
+        self._interact('touch %d\r\n' % jid, ['TOUCHED'], ['NOT_FOUND'])
+
+    def stats_job(self, jid):
+        """Return a dict of stats about a job, by job id."""
+        return self._interact_yaml_dict('stats-job %d\r\n' % jid,
+                                        ['OK'],
+                                        ['NOT_FOUND'])
+
+
+class Job(object):
+    def __init__(self, conn, jid, body, reserved=True):
+        self.conn = conn
+        self.jid = jid
+        self.body = body
+        self.reserved = reserved
+
+    def _priority(self):
+        stats = self.stats()
+        if isinstance(stats, dict):
+            return stats['pri']
+        return DEFAULT_PRIORITY
+
+    # -- public interface --
+
+    def delete(self):
+        """Delete this job."""
+        self.conn.delete(self.jid)
+        self.reserved = False
+
+    def release(self, priority=None, delay=0):
+        """Release this job back into the ready queue."""
+        if self.reserved:
+            self.conn.release(self.jid, priority or self._priority(), delay)
+            self.reserved = False
+
+    def bury(self, priority=None):
+        """Bury this job."""
+        if self.reserved:
+            self.conn.bury(self.jid, priority or self._priority())
+            self.reserved = False
+
+    def touch(self):
+        """Touch this reserved job, requesting more time to work on it before it
+        expires."""
+        if self.reserved:
+            self.conn.touch(self.jid)
+
+    def stats(self):
+        """Return a dict of stats about this job."""
+        return self.conn.stats_job(self.jid)
+
+def parse_yaml_dict(yaml):
+    """Parse a YAML dict, in the form returned by beanstalkd."""
+    dict = {}
+    for m in re.finditer(r'^\s*([^:\s]+)\s*:\s*([^\s]*)$', yaml, re.M):
+        key, val = m.group(1), m.group(2)
+        # Check the type of the value, and parse it.
+        if key == 'name' or key == 'tube' or key == 'version':
+            dict[key] = val   # String, even if it looks like a number
+        elif re.match(r'^(0|-?[1-9][0-9]*)$', val) is not None:
+            dict[key] = int(val) # Integer value
+        elif re.match(r'^(-?\d+(\.\d+)?(e[-+]?[1-9][0-9]*)?)$', val) is not None:
+            dict[key] = float(val) # Float value
+        else:
+            dict[key] = val     # String value
+    return dict
+
+def parse_yaml_list(yaml):
+    """Parse a YAML list, in the form returned by beanstalkd."""
+    return re.findall(r'^- (.*)$', yaml, re.M)
+
+if __name__ == '__main__':
+    import doctest, os, signal
+    try:
+        pid = os.spawnlp(os.P_NOWAIT,
+                         'beanstalkd',
+                         'beanstalkd', '-l', '127.0.0.1', '-p', '14711')
+        doctest.testfile('TUTORIAL.md', optionflags=doctest.ELLIPSIS)
+        doctest.testfile('test/network.doctest', optionflags=doctest.ELLIPSIS)
+    finally:
+        os.kill(pid, signal.SIGTERM)
-- 
GitLab