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