Revision 97e42c7d

b/apache/django.wsgi
1
import os
2
import sys
3

  
4
sys.path.append('/home/leopoul/projects/')
5
sys.path.append('/home/leopoul/projects/flowspy')
6

  
7
os.environ['DJANGO_SETTINGS_MODULE'] = 'flowspy.settings'
8

  
9
from gevent import monkey; monkey.patch_all()
10

  
11

  
12
import django.core.handlers.wsgi
13
application = django.core.handlers.wsgi.WSGIHandler()
b/djangobackends/shibauthBackend.py
1
# -*- coding: utf-8 -*- vim:encoding=utf-8:
2
# vim: tabstop=4:shiftwidth=4:softtabstop=4:expandtab
3

  
4
from django.contrib.auth.models import User, UserManager, Permission, Group
5
from django.conf import settings
6
from flowspy.peers.models import *
7
from flowspy.accounts.models import * 
8

  
9
class shibauthBackend:
10
    def authenticate(self, **kwargs):
11
        
12
        username = kwargs.get('username')
13
        firstname = kwargs.get('firstname')
14
        lastname = kwargs.get('lastname')
15
        mail = kwargs.get('mail')
16
        affiliation = kwargs.get('affiliation')
17
        organization = kwargs.get('organization')
18
        user = self._auth_user(username, firstname, lastname, mail, affiliation, organization)
19
        if not user:
20
            return None
21
        return user
22

  
23
    def _auth_user(self, username, firstname, lastname, mail, affiliation, organization):
24

  
25
        try:
26
            user = User.objects.get(username__exact=username)
27
        # The user did not exist. Create one with no privileges
28
        except:
29
            user = User.objects.create_user(username, mail, None)
30
            user.first_name = firstname
31
            user.last_name = lastname
32
            user.is_staff = False
33
            user.is_superuser = False
34
#            if organization == settings.SHIB_ADMIN_DOMAIN:
35
#                user.is_staff = True
36
#                user.is_superuser = True
37
            user.is_active = True
38
        try:
39
            peer = Peer.objects.get(domain_name=organization)
40
            up = UserProfile.objects.get_or_create(user=user,peer=peer)
41
        except:
42
            pass
43
        return user
44

  
45
    def get_user(self, user_id):
46
        try:
47
            return User.objects.get(pk=user_id)
48
        except User.DoesNotExist:
49
            return None
b/flowspec/admin.py
3 3
from flowspy.accounts.models import *
4 4
from utils import proxy as PR
5 5

  
6
class RouteAdmin(admin.ModelAdmin):
7
    
8
    actions = ['deactivate']
9
    
10
    def deactivate(self, request, queryset):
11
        applier = PR.Applier(route_objects=queryset)
12
        commit, response = applier.apply(configuration=applier.delete_routes())
13
        if commit:
14
            rows = queryset.update(is_online=False, is_active=False)
15
            queryset.update(response="Successfully removed route from network")
16
            self.message_user(request, "Successfully removed %s routes from network" % rows)
17
        else:
18
            self.message_user(request, "Could not remove routes from network")
19
    deactivate.short_description = "Deactivate selected routes from network"
20

  
21
    list_display = ('name', 'is_online', 'applier', 'get_match', 'get_then', 'response')
22
    fieldsets = [
23
        (None,               {'fields': ['name','applier']}),
24
        ("Match",               {'fields': ['source', 'sourceport', 'destination', 'destinationport', 'port']}),
25
        ('Advanced Match Statements', {'fields': ['dscp', 'fragmenttype', 'icmpcode', 'icmptype', 'packetlength', 'protocol', 'tcpflag'], 'classes': ['collapse']}),
26
        ("Then",               {'fields': ['then' ]}),
27
        (None,               {'fields': ['comments',]}),
28
        
29
    ]
6
#class RouteAdmin(admin.ModelAdmin):
7
#    
8
#    actions = ['deactivate']
9
#    
10
#    def deactivate(self, request, queryset):
11
#        applier = PR.Applier(route_objects=queryset)
12
#        commit, response = applier.apply(configuration=applier.delete_routes())
13
#        if commit:
14
#            rows = queryset.update(is_online=False, is_active=False)
15
#            queryset.update(response="Successfully removed route from network")
16
#            self.message_user(request, "Successfully removed %s routes from network" % rows)
17
#        else:
18
#            self.message_user(request, "Could not remove routes from network")
19
#    deactivate.short_description = "Deactivate selected routes from network"
20
#
21
#    list_display = ('name', 'is_online', 'applier', 'get_match', 'get_then', 'response')
22
#    fieldsets = [
23
#        (None,               {'fields': ['name','applier']}),
24
#        ("Match",               {'fields': ['source', 'sourceport', 'destination', 'destinationport', 'port']}),
25
#        ('Advanced Match Statements', {'fields': ['dscp', 'fragmenttype', 'icmpcode', 'icmptype', 'packetlength', 'protocol', 'tcpflag'], 'classes': ['collapse']}),
26
#        ("Then",               {'fields': ['then' ]}),
27
#        (None,               {'fields': ['comments',]}),
28
#        
29
#    ]
30 30
#    fields = ('name', 'applier', 'expires')
31 31

  
32 32
    #def formfield_for_dbfield(self, db_field, **kwargs):
......
47 47
admin.site.register(ThenAction)
48 48
#admin.site.register(ThenStatement)
49 49
#admin.site.register(MatchStatement)
50
admin.site.register(Route, RouteAdmin)
50
admin.site.register(Route)
51 51

  
52 52
admin.site.disable_action('delete_selected')
53 53

  
b/flowspec/forms.py
3 3
from django.utils.translation import ugettext as _
4 4
from django.utils.translation import ugettext_lazy
5 5
from django.template.defaultfilters import filesizeformat
6

  
7 6
from flowspy.flowspec.models import * 
8 7
from ipaddr import *
8
from django.contrib.auth.models import User
9 9

  
10 10
class RouteForm(forms.ModelForm):
11 11
#    name = forms.CharField(help_text=ugettext_lazy("A unique route name,"
......
44 44
        ports = self.cleaned_data.get('port', None)
45 45
        destination = self.cleaned_data.get('destination', None)
46 46
        destinationports = self.cleaned_data.get('destinationport', None)
47
        user = self.cleaned_data.get('applier', None)
48
        networks = user.get_profile().peer.networks.all()
49
        mynetwork = False
50
        if destination:
51
            for network in networks:
52
                net = IPNetwork(network.network)
53
                if IPNetwork(destination) in net:
54
                    mynetwork = True
55
            if not mynetwork:
56
                 raise forms.ValidationError('Destination address/network should belong to your administrative address space. Check My Profile to review your networks')
47 57
        if (sourceports and ports):
48 58
            raise forms.ValidationError('Cannot create rule for source ports and ports at the same time. Select either ports or source ports')
49 59
        if (destinationports and ports):
......
54 64
            raise forms.ValidationError('Once destination port is matched, destination has to be filled as well. Either deselect destination port or fill destination address')
55 65
        if not (source or sourceports or ports or destination or destinationports):
56 66
            raise forms.ValidationError('Fill at least a Route Match Condition')
57
        return self.cleaned_data
67
        return self.cleaned_data
68

  
69
class ThenPlainForm(forms.ModelForm):
70
#    action = forms.CharField(initial='rate-limit')
71
    class Meta:
72
        model = ThenAction
73
    
74
    def clean_action_value(self):
75
        action_value = self.cleaned_data['action_value']
76
        if action_value:
77
            try:
78
                assert(int(action_value))
79
                return "%s" %self.cleaned_data["action_value"]
80
            except:
81
                raise forms.ValidationError('Rate-limiting should be an integer')
82
            if int(action_value) < 50:
83
                raise forms.ValidationError('Rate-limiting cannot be < 50kbps')
84
        else:
85
            raise forms.ValidationError('Cannot be empty')
86

  
87
    def clean_action(self):
88
        action = self.cleaned_data['action']
89
        if action != 'rate-limit':
90
            raise forms.ValidationError('Cannot select something other than rate-limit')
91
        else:
92
            return self.cleaned_data["action"]
93

  
94
class PortPlainForm(forms.ModelForm):
95
#    action = forms.CharField(initial='rate-limit')
96
    class Meta:
97
        model = MatchPort
98
    
99
    def clean_port(self):
100
        port = self.cleaned_data['port']
101
        if port:
102
            try:
103
                assert(int(port))
104
                return "%s" %self.cleaned_data["port"]
105
            except:
106
                raise forms.ValidationError('Port should be an integer')
107
        else:
108
            raise forms.ValidationError('Cannot be empty')
b/flowspec/models.py
12 12
from time import sleep
13 13

  
14 14
from flowspy.utils import beanstalkc
15
from flowspy.utils.randomizer import id_generator as id_gen
15 16

  
16 17

  
17 18
FORMAT = '%(asctime)s %(levelname)s: %(message)s'
......
38 39
    ("sample", "Sample")                
39 40
)
40 41

  
42
ROUTE_STATES = (
43
    ("ACTIVE", "ACTIVE"),
44
    ("ERROR", "ERROR"),
45
    ("EXPIRED", "EXPIRED"),
46
    ("PENDING", "PENDING"),
47
    ("OUTOFSYNC", "OUTOFSYNC"),
48
    ("INACTIVE", "INACTIVE"),            
49
)
50

  
41 51

  
42 52
def days_offset(): return datetime.now() + timedelta(days = settings.EXPIRATION_DAYS_OFFSET)
43 53
    
44 54
class MatchPort(models.Model):
45
    port = models.CharField(max_length=24)
55
    port = models.CharField(max_length=24, unique=True)
46 56
    def __unicode__(self):
47 57
        return self.port
48 58
    class Meta:
......
60 70
    action = models.CharField(max_length=60, choices=THEN_CHOICES, verbose_name="Action")
61 71
    action_value = models.CharField(max_length=255, blank=True, null=True, verbose_name="Action Value")
62 72
    def __unicode__(self):
63
        return "%s: %s" %(self.action, self.action_value)
73
        ret = "%s:%s" %(self.action, self.action_value)
74
        return ret.rstrip(":")
64 75
    class Meta:
65 76
        db_table = u'then_action'
66 77

  
......
82 93
    then = models.ManyToManyField(ThenAction, verbose_name="Then")
83 94
    filed = models.DateTimeField(auto_now_add=True)
84 95
    last_updated = models.DateTimeField(auto_now=True)
85
    is_online = models.BooleanField(default=False)
86
    is_active = models.BooleanField(default=False)
96
    status = models.CharField(max_length=20, choices=ROUTE_STATES, blank=True, null=True, verbose_name="Status", default="PENDING")
97
#    is_online = models.BooleanField(default=False)
98
#    is_active = models.BooleanField(default=False)
87 99
    expires = models.DateField(default=days_offset, blank=True, null=True,)
88 100
    response = models.CharField(max_length=512, blank=True, null=True)
89 101
    comments = models.TextField(null=True, blank=True, verbose_name="Comments")
......
93 105
        return self.name
94 106
    
95 107
    class Meta:
96
        unique_together = (("name", "is_active"),)
97 108
        db_table = u'route'
98 109
    
110
    def save(self, *args, **kwargs):
111
        if not self.pk:
112
            hash = id_gen()
113
            self.name = "%s_%s" %(self.name, hash)
114
        super(Route, self).save(*args, **kwargs) # Call the "real" save() method.
115

  
116
        
99 117
    def clean(self, *args, **kwargs):
100 118
        from django.core.exceptions import ValidationError
101 119
        if self.destination:
......
122 140
#            logger.info("Got save job id: %s" %response)
123 141
    
124 142
    def commit_add(self, *args, **kwargs):
125
        send_message("Adding route %s. Please wait..." %self.name, self.applier)
143
        peer = self.applier.get_profile().peer.domain_name
144
        send_message("[%s] Adding route %s. Please wait..." %(self.applier.username, self.name), peer)
126 145
        response = add.delay(self)
127 146
        logger.info("Got save job id: %s" %response)
128 147

  
129 148
    def deactivate(self):
130
        self.is_online = False
131
        self.is_active = False
149
        self.status = "INACTIVE"
132 150
        self.save()
133 151
#    def delete(self, *args, **kwargs):
134 152
#        response = delete.delay(self)
135 153
#        logger.info("Got delete job id: %s" %response)
136 154
        
137 155
    def commit_edit(self, *args, **kwargs):
138
        send_message("Editing route %s. Please wait..." %self.name, self.applier)
156
        peer = self.applier.get_profile().peer.domain_name
157
        send_message("[%s] Editing route %s. Please wait..." %(self.applier.username, self.name), peer)
139 158
        response = edit.delay(self)
140 159
        logger.info("Got edit job id: %s" %response)
141 160

  
142 161
    def commit_delete(self, *args, **kwargs):
143
        send_message("Removing route %s. Please wait..." %self.name, self.applier)
162
        peer = self.applier.get_profile().peer.domain_name
163
        send_message("[%s] Removing route %s. Please wait..." %(self.applier.username, self.name), peer)
144 164
        response = delete.delay(self)
145 165
        logger.info("Got edit job id: %s" %response)
146 166
#    
147 167
#    def delete(self, *args, **kwargs):
148 168
#        response = delete.delay(self)
149 169
#        logger.info("Got delete job id: %s" %response)
150
    def is_synced(self):
151
        
170

  
171
    def is_synced(self):      
152 172
        found = False
153 173
        get_device = PR.Retriever()
154 174
        device = get_device.fetch_device()
155 175
        try:
156 176
            routes = device.routing_options[0].routes
157 177
        except Exception as e:
158
            self.is_online = False
178
            self.status = "EXPIRED"
159 179
            self.save()
160 180
            logger.error("No routing options on device. Exception: %s" %e)
161 181
            return False
......
230 250
                        logger.info('Protocol fields do not match')
231 251
                except:
232 252
                    pass
233
                if found and not self.is_online:
253
                if found and self.status != "ACTIVE":
234 254
                     logger.error('Rule is applied on device but appears as offline')
235 255
                     found = False
236 256
        
......
252 272
    def get_match(self):
253 273
        ret = ''
254 274
        if self.destination:
255
            ret = '%s Destination Address:<strong>%s</strong><br/>' %(ret, self.destination)
275
            ret = '%s Dst Addr:<strong>%s</strong><br/>' %(ret, self.destination)
256 276
        if self.fragmenttype:
257 277
            ret = "%s Fragment Type:<strong>%s</strong><br/>" %(ret, self.fragmenttype)
258 278
        if self.icmpcode:
......
264 284
        if self.protocol:
265 285
            ret = "%s Protocol:<strong>%s</strong><br/>" %(ret, self.protocol)
266 286
        if self.source:
267
            ret = "%s Source Address:<strong>%s</strong><br/>" %(ret, self.source)
287
            ret = "%s Src Addr:<strong>%s</strong><br/>" %(ret, self.source)
268 288
        if self.tcpflag:
269 289
            ret = "%s TCP flag:<strong>%s</strong><br/>" %(ret, self.tcpflag)
270 290
        if self.port:
......
272 292
                    ret = ret + "Port:<strong>%s</strong><br/>" %(port)
273 293
        if self.destinationport:
274 294
            for port in self.destinationport.all():
275
                    ret = ret + "Destination Port:<strong>%s</strong><br/>" %(port)
295
                    ret = ret + "Dst Port:<strong>%s</strong><br/>" %(port)
276 296
        if self.sourceport:
277 297
            for port in self.sourceport.all():
278
                    ret = ret +"Source Port:<strong>%s</strong><br/>" %(port)
298
                    ret = ret +"Src Port:<strong>%s</strong><br/>" %(port)
279 299
        if self.dscp:
280 300
            for dscp in self.dscp.all():
281 301
                    ret = ret + "%s Port:<strong>%s</strong><br/>" %(ret, dscp)
......
285 305
    get_match.allow_tags = True
286 306

  
287 307
def send_message(msg, user):
288
    username = user.username
308
#    username = user.username
309
    peer = user
289 310
    b = beanstalkc.Connection()
290 311
    b.use(settings.POLLS_TUBE)
291
    tube_message = json.dumps({'message': str(msg), 'username':username})
312
    tube_message = json.dumps({'message': str(msg), 'username':peer})
292 313
    b.put(tube_message)
293 314
    b.close()
b/flowspec/sql/then_action.sql
1
INSERT INTO `then_action` (`id`, `action`, `action_value`) VALUES
2
(1, 'accept', ''),
3
(2, 'discard', '');
b/flowspec/tasks.py
18 18
    applier = PR.Applier(route_object=route)
19 19
    commit, response = applier.apply()
20 20
    if commit:
21
        is_online = True
22
        is_active = True
21
        status = "ACTIVE"
23 22
    else:
24
        is_online = False
25
        is_active = True
26
    route.is_online = is_online
27
    route.is_active = is_active
23
        status = "ERROR"
24
    route.status = status
28 25
    route.response = response
29
    subtask(announce).delay("Route add: %s - Result: %s" %(route.name, response), route.applier)
26
    subtask(announce).delay("[%s] Route add: %s - Result: %s" %(route.applier, route.name, response), route.applier)
30 27
    route.save()
31 28

  
32 29
@task
......
34 31
    applier = PR.Applier(route_object=route)
35 32
    commit, response = applier.apply(operation="replace")
36 33
    if commit:
37
        is_online = True
34
        status = "ACTIVE"
38 35
    else:
39
        is_online = False
40
    route.is_active = True
41
    route.is_online = is_online
36
        status = "ERROR"
37
    route.status = status
42 38
    route.response = response
43 39
    route.save()
44
    subtask(announce).delay("Route edit: %s - Result: %s" %(route.name, response), route.applier)
40
    subtask(announce).delay("[%s] Route edit: %s - Result: %s"%(route.applier, route.name, response), route.applier)
45 41

  
46 42

  
47 43

  
......
50 46
    applier = PR.Applier(route_object=route)
51 47
    commit, response = applier.apply(operation="delete")
52 48
    if commit:
53
        is_online = False
54
        is_active = False
49
        status = "INACTIVE"
55 50
    else:
56
        is_online = route.is_online
57
        is_active = route.is_active
58
    route.is_online = is_online
59
    route.is_active = is_active
51
        status = "ERROR"
52
    route.status = status
60 53
    route.response = response
61 54
    route.save()
62
    subtask(announce).delay("Route delete: %s - Result %s" %(route.name, response), route.applier)
55
    subtask(announce).delay("[%s] Route delete: %s - Result %s" %(route.applier, route.name, response), route.applier)
63 56

  
64 57

  
65 58

  
66 59
@task
67 60
def announce(messg, user):
68 61
    messg = str(messg)
69
    username = user.username
62
#    username = user.username
63
    username = user.get_profile().peer.domain_name
70 64
    b = beanstalkc.Connection()
71 65
    b.use(settings.POLLS_TUBE)
72 66
    tube_message = json.dumps({'message': messg, 'username':username})
73 67
    b.put(tube_message)
74 68
    b.close()
75 69

  
76

  
70
@task
71
def check_sync(route_name=None, selected_routes = []):
72
    if not selected_routes:
73
        routes = Route.objects.all()
74
    else:
75
        routes = selected_routes
76
    if route_name:
77
        routes = routes.filter(name=route_name)
78
    for route in roures:
79
        if route.is_synced():
80
            logger.info("Route %s is synced" %route.name)
81
        else:
82
            logger.warn("Route %s is out of sync" %route.name)
77 83
#def delete(route):
78 84
#    
79 85
#    applier = PR.Applier(route_object=route)
b/flowspec/views.py
2 2
import urllib2
3 3
import re
4 4
import socket
5
import json
5 6
from django import forms
6 7
from django.views.decorators.csrf import csrf_exempt
7 8
from django.core import urlresolvers
9
from django.core import serializers
8 10
from django.contrib.auth.decorators import login_required
9 11
from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse
10 12
from django.shortcuts import get_object_or_404, render_to_response
......
16 18
from django.contrib import messages
17 19
from flowspy.accounts.models import *
18 20

  
21
from django.contrib.auth import authenticate, login
22

  
19 23
from django.forms.models import model_to_dict
20 24

  
21 25
from flowspy.flowspec.forms import * 
22 26
from flowspy.flowspec.models import *
23 27

  
24 28
from copy import deepcopy
29
from flowspy.utils.decorators import shib_required
25 30

  
26 31
def days_offset(): return datetime.now() + timedelta(days = settings.EXPIRATION_DAYS_OFFSET)
27 32

  
28 33
@login_required
29 34
def user_routes(request):
30
    if request.user.is_anonymous():
31
        return HttpResponseRedirect(reverse('login'))
32 35
    user_routes = Route.objects.filter(applier=request.user)
33 36
    return render_to_response('user_routes.html', {'routes': user_routes},
34 37
                              context_instance=RequestContext(request))
35 38

  
36 39
@login_required
37 40
def group_routes(request):
38
    if request.user.is_anonymous():
39
        return HttpResponseRedirect(reverse('login'))
41
    group_routes = []
40 42
    peer = request.user.get_profile().peer
41 43
    if peer:
42 44
       peer_members = UserProfile.objects.filter(peer=peer)
......
48 50

  
49 51
@login_required
50 52
def add_route(request):
53
    applier = request.user.pk
51 54
    if request.method == "GET":
52 55
        form = RouteForm()
53
        return render_to_response('apply.html', {'form': form},
56
        return render_to_response('apply.html', {'form': form, 'applier': applier},
54 57
                                  context_instance=RequestContext(request))
55 58

  
56 59
    else:
......
59 62
            route=form.save(commit=False)
60 63
            route.applier = request.user
61 64
            route.expires = days_offset()
65
            route.status = "PENDING"
62 66
            route.save()
63 67
            form.save_m2m()
64 68
            route.commit_add()
65
            return HttpResponseRedirect(urlresolvers.reverse("user-routes"))
69
            return HttpResponseRedirect(reverse("group-routes"))
66 70
        else:
67
            return render_to_response('apply.html', {'form': form},
71
            return render_to_response('apply.html', {'form': form, 'applier':applier},
68 72
                                      context_instance=RequestContext(request))
73

  
74
@login_required
75
def add_then(request):
76
    applier = request.user.pk
77
    if request.method == "GET":
78
        form = RouteForm()
79
        return render_to_response('apply.html', {'form': form, 'applier': applier},
80
                                  context_instance=RequestContext(request))
81

  
82
    else:
83
        form = RouteForm(request.POST)
84
        if form.is_valid():
85
            route=form.save(commit=False)
86
            route.applier = request.user
87
            route.expires = days_offset()
88
            route.save()
89
            form.save_m2m()
90
            route.commit_add()
91
            return HttpResponseRedirect(reverse("group-routes"))
92
        else:
93
            return render_to_response('apply.html', {'form': form, 'applier':applier},
94
                                      context_instance=RequestContext(request))
95

  
69 96
@login_required
70 97
def edit_route(request, route_slug):
98
    applier = request.user.pk
71 99
    route_edit = get_object_or_404(Route, name=route_slug)
72 100
    route_original = deepcopy(route_edit)
73 101
    if request.POST:
......
75 103
        if form.is_valid():
76 104
            route=form.save(commit=False)
77 105
            route.name = route_original.name
78
            route.applier = route_original.applier
106
            route.applier = request.user
79 107
            route.expires = route_original.expires
80
            route.is_active = route_original.is_active
108
            route.status = "PENDING"
81 109
            route.save()
82 110
            form.save_m2m()
83 111
            route.commit_edit()
84
            return HttpResponseRedirect(urlresolvers.reverse("user-routes"))
112
            return HttpResponseRedirect(reverse("group-routes"))
85 113
        else:
86
            return render_to_response('apply.html', {'form': form, 'edit':True},
114
            return render_to_response('apply.html', {'form': form, 'edit':True, 'applier': applier},
87 115
                                      context_instance=RequestContext(request))
88 116
    else:
89 117
        dictionary = model_to_dict(route_edit, fields=[], exclude=[])
118
        #form = RouteForm(instance=route_edit)
90 119
        form = RouteForm(dictionary)
91
        return render_to_response('apply.html', {'form': form, 'edit':True},
120
        return render_to_response('apply.html', {'form': form, 'edit':True, 'applier': applier},
92 121
                                  context_instance=RequestContext(request))
93 122

  
94 123
@login_required
95 124
def delete_route(request, route_slug):
96 125
    if request.is_ajax():
97 126
        route = get_object_or_404(Route, name=route_slug)
98
        if route.applier == request.user:
127
        applier_peer = route.applier.get_profile().peer
128
        requester_peer = request.user.get_profile().peer
129
        if applier_peer == requester_peer:
99 130
            route.deactivate()
100 131
            route.commit_delete()
101
    return HttpResponseRedirect(urlresolvers.reverse("user-routes"))
132
        html = "<html><body>Done</body></html>"
133
        return HttpResponse(html)
134
    else:
135
        return HttpResponseRedirect(reverse("group-routes"))
136

  
137
@login_required
138
def user_profile(request):
139
    user = request.user
140
    peer = request.user.get_profile().peer
141
    
142
    return render_to_response('profile.html', {'user': user, 'peer':peer},
143
                                  context_instance=RequestContext(request))
144

  
145

  
146
def user_login(request):
147
    try:
148
        error_username = None
149
        error_orgname = None
150
        username = request.META['HTTP_EPPN']
151
        if not username:
152
            error_username = True
153
        firstname = request.META['HTTP_SHIB_INETORGPERSON_GIVENNAME']
154
        lastname = request.META['HTTP_SHIB_PERSON_SURNAME']
155
        mail = request.META['HTTP_SHIB_INETORGPERSON_MAIL']
156
        organization = request.META['HTTP_SHIB_HOMEORGANIZATION']
157
        if not organization:
158
            error_orgname = True
159

  
160
        if error_orgname or error_username:
161
            error = "Your idP should release the HTTP_EPPN, HTTP_SHIB_HOMEORGANIZATION attributes towards this service" 
162
            return render_to_response('error.html', {'error': error,},
163
                                  context_instance=RequestContext(request))
164
        user = authenticate(username=username, firstname=firstname, lastname=lastname, mail=mail, organization=organization, affiliation=None)
165
        if user is not None:
166
            login(request, user)
167
            return HttpResponseRedirect(reverse("group-routes"))
168
                # Redirect to a success page.
169
                # Return a 'disabled account' error message
170
        else:
171
            html = "<html><body>Invalid User</body></html>"
172
            return HttpResponse(html)
173
    except Exception as e:
174
        html = "<html><body>Invalid Login Procedure %s </body></html>" %e
175
        return HttpResponse(html)
176
        # Return an 'invalid login' error message.
177
#    return HttpResponseRedirect(reverse("user-routes"))
178

  
179
@login_required
180
def add_rate_limit(request):
181
    if request.method == "GET":
182
        form = ThenPlainForm()
183
        return render_to_response('add_rate_limit.html', {'form': form,},
184
                                  context_instance=RequestContext(request))
185

  
186
    else:
187
        form = ThenPlainForm(request.POST)
188
        if form.is_valid():
189
            then=form.save(commit=False)
190
            then.action_value = "%sk"%then.action_value
191
            then.save()
192
            response_data = {}
193
            response_data['pk'] = "%s" %then.pk
194
            response_data['value'] = "%s:%s" %(then.action, then.action_value)
195
            return HttpResponse(simplejson.dumps(response_data), mimetype='application/json')
196
        else:
197
            return render_to_response('add_rate_limit.html', {'form': form,},
198
                                      context_instance=RequestContext(request))
199

  
200
@login_required
201
def add_port(request):
202
    if request.method == "GET":
203
        form = PortPlainForm()
204
        return render_to_response('add_port.html', {'form': form,},
205
                                  context_instance=RequestContext(request))
206

  
207
    else:
208
        form = PortPlainForm(request.POST)
209
        if form.is_valid():
210
            port=form.save()
211
            response_data = {}
212
            response_data['value'] = "%s" %port.pk
213
            response_data['text'] = "%s" %port.port
214
            return HttpResponse(simplejson.dumps(response_data), mimetype='application/json')
215
        else:
216
            return render_to_response('add_port.html', {'form': form,},
217
                                      context_instance=RequestContext(request))
218

  
219
@login_required
220
def user_logout(request):
221
    return HttpResponseRedirect(settings.SHIB_LOGOUT_URL)
222
    
223
    
224
def load_jscript(request, file):
225
    return render_to_response('%s.js' % file, context_instance=RequestContext(request), mimetype="text/javascript")
b/monkey_patch/models.py
1
from django.contrib.auth.models import User
2
from django.core.validators import MaxLengthValidator
3

  
4
NEW_USERNAME_LENGTH = 255
5

  
6
def monkey_patch_username():
7
    username = User._meta.get_field("username")
8
    username.max_length = NEW_USERNAME_LENGTH
9
    for v in username.validators:
10
        if isinstance(v, MaxLengthValidator):
11
            v.limit_value = NEW_USERNAME_LENGTH
12

  
13
monkey_patch_username()
b/poller/urls.py
3 3

  
4 4
urlpatterns = patterns('flowspy.poller.views',
5 5
                       ('^$', 'main'),
6
                       ('^a/message/existing$', 'message_existing'),
7
                       ('^a/message/new$', 'message_new'),
8
                       ('^a/message/updates$', 'message_updates'))
6
                       url('^a/message/existing$', 'message_existing', name='fetch-existing'),
7
                       url('^a/message/new$', 'message_new',name='fetch-new'),
8
                       url('^a/message/updates$', 'message_updates', name='fetch-updates'))
9 9

  
10 10
urlpatterns += patterns('',
11 11
    (r'^static/(?P<path>.*)', 'django.views.static.serve',\
b/poller/views.py
12 12
from gevent.event import Event
13 13
from django.conf import settings
14 14
from django.views.decorators.csrf import csrf_exempt
15
from django.http import HttpResponseRedirect
16
from django.core.urlresolvers import reverse
17

  
15 18

  
16 19
from flowspy.utils import beanstalkc
17 20

  
......
37 40
    cache_size = 200
38 41

  
39 42
    def __init__(self):
43
        self.user = None
40 44
        self.user_cache = {}
41 45
        self.user_cursor = {}
42 46
        self.cache = []
......
47 51
        if self.user_cache:
48 52
            request.session['cursor'] = self.user_cache[-1]['id']
49 53
        return render_to_response('poll.html', {'messages': self.user_cache})
50
    
54

  
51 55
    @csrf_exempt
52 56
    def message_existing(self, request):
53
        
54
        try:
55
            user = request.user.username
56
        except:
57
            user = None
58
        self.new_message_user_event[user] = Event()
59
        try:
60
            if self.user_cache[user]:
61
                self.user_cursor[user] = self.user_cache[user][-1]['id']
62
        except:
63
            self.user_cache[user] = []
64
            self.user_cursor[user] = ''
65
        return json_response({'messages': self.user_cache[user]})
57
        if request.is_ajax():
58
            try:
59
                user = request.user.get_profile().peer.domain_name
60
            except:
61
                user = None
62
                return False
63
            try:
64
                assert(self.new_message_user_event[user])
65
            except:
66
                self.new_message_user_event[user] = Event()
67
    #        self.new_message_user_event[user] = Event()
68
            try:
69
                if self.user_cache[user]:
70
                    self.user_cursor[user] = self.user_cache[user][-1]['id']
71
            except:
72
                self.user_cache[user] = []
73
                self.user_cursor[user] = ''
74
            return json_response({'messages': self.user_cache[user]})
75
        return HttpResponseRedirect(reverse('login'))
66 76
    
67 77
    @csrf_exempt
68 78
    def message_new(self, mesg=None):
......
89 99
    
90 100
    @csrf_exempt
91 101
    def message_updates(self, request):
92
        cursor = {}
93
        try:
94
            user = request.user.username
95
        except:
96
            user = None
97

  
98
        cursor[user] = self.user_cursor[user]
99
            
100
        try:
101
            if not isinstance(self.user_cache[user], list):
102
        if request.is_ajax():
103
            cursor = {}
104
            try:
105
    #            user = request.user.username
106
                user = request.user.get_profile().peer.domain_name
107
            except:
108
                user = None
109
                return False
110
            cursor[user] = self.user_cursor[user]
111
                
112
            try:
113
                if not isinstance(self.user_cache[user], list):
114
                    self.user_cache[user] = []
115
            except:
102 116
                self.user_cache[user] = []
103
        except:
104
            self.user_cache[user] = []
105
        if not self.user_cache[user] or cursor[user] == self.user_cache[user][-1]['id']:
106
            self.new_message_user_event[user].wait()
107
#            self.new_message_event.wait()
108
#        assert cursor[user] != self.user_cache[user][-1]['id'], cursor[user]
109
        try:
110
            for index, m in enumerate(self.user_cache[user]):
111
                if m['id'] == cursor[user]:
112
                    return json_response({'messages': self.user_cache[user][index + 1:]})
113
            return json_response({'messages': self.user_cache[user]})
114
        finally:
115
            if self.user_cache[user]:
116
                self.user_cursor[user] = self.user_cache[user][-1]['id']
117
#            else:
118
#                request.session.pop('cursor', None)
117
            if not self.user_cache[user] or cursor[user] == self.user_cache[user][-1]['id']:
118
                self.new_message_user_event[user].wait()
119
    #            self.new_message_event.wait()
120
    #        assert cursor[user] != self.user_cache[user][-1]['id'], cursor[user]
121
            try:
122
                for index, m in enumerate(self.user_cache[user]):
123
                    if m['id'] == cursor[user]:
124
                        return json_response({'messages': self.user_cache[user][index + 1:]})
125
                return json_response({'messages': self.user_cache[user]})
126
            finally:
127
                if self.user_cache[user]:
128
                    self.user_cursor[user] = self.user_cache[user][-1]['id']
129
        return HttpResponseRedirect(reverse('login'))
130
    #            else:
131
    #                request.session.pop('cursor', None)
119 132

  
120 133
    def monitor_polls(self, polls=None):
121 134
        b = beanstalkc.Connection()
122 135
        b.watch(settings.POLLS_TUBE)
123 136
        while True:
124 137
            job = b.reserve()
125
            print job.body
126 138
            msg = json.loads(job.body)
127 139
            job.bury()
128 140
            self.message_new(msg)
b/run_poller.py
1 1
#!/usr/bin/python
2 2
from gevent.wsgi import WSGIServer
3 3
from poller.application import application
4
print 'Serving on 8000...'
5
WSGIServer(('netdev.grnet.gr', 9090), application).serve_forever()
4
server="localhost"
5
port=8081
6
print 'Serving on port %s...' % port
7
WSGIServer((server,port ), application).serve_forever()
b/static/css/base.css
455 455
#cluster_overview_table_wrapper{
456 456
	width: 100%;
457 457
}
458

  
458
/*
459
 * jQuery UI specific styling
460
 */
459 461

  
460 462
.paging_two_button .ui-button {
461 463
	float: left;
......
518 520
}
519 521

  
520 522

  
523

  
524

  
525
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
526
 *
527
 * Everything below this line is the same as demo_table.css. This file is
528
 * required for 'cleanliness' of the markup
529
 *
530
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
531

  
532

  
533

  
534
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
535
 * DataTables features
536
 */
537

  
521 538
.dataTables_wrapper {
522 539
	position: relative;
523 540
	min-height: 302px;
524
	float: left;
525
	clear: both;
526 541
	_height: 302px;
527
	width: 100%;
528
	zoom: 1; /* Feeling sorry for IE */
542
	clear: both;
529 543
}
530 544

  
531 545
.dataTables_processing {
532 546
	position: absolute;
533
	top: 50%;
547
	top: 0px;
534 548
	left: 50%;
535 549
	width: 250px;
536
	height: 30px;
537 550
	margin-left: -125px;
538
	margin-top: -15px;
539
	padding: 14px 0 2px 0;
540 551
	border: 1px solid #ddd;
541 552
	text-align: center;
542 553
	color: #999;
543
	font-size: 14px;
544
	background-color: white;
554
	font-size: 11px;
555
	padding: 2px 0;
545 556
}
546 557

  
547 558
.dataTables_length {
......
556 567
}
557 568

  
558 569
.dataTables_info {
559
	width: 60%;
570
	width: 50%;
560 571
	float: left;
561 572
}
562 573

  
563 574
.dataTables_paginate {
564
	width: 44px;
565
	* width: 50px;
566 575
	float: right;
567 576
	text-align: right;
568 577
}
......
593 602

  
594 603

  
595 604

  
596

  
605
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
606
 * DataTables display
607
 */
597 608
table.display {
598 609
	margin: 0 auto;
599
	clear: both;
600 610
	width: 100%;
611
	clear: both;
601 612
	border-collapse: collapse;
602
	
603
}
604

  
605
table.display thead th {
606
	padding: 3px 18px 3px 10px;
607
	font-weight: normal;
608
	cursor: pointer;
609
	* cursor: hand;
610 613
}
611 614

  
612 615
table.display tfoot th {
613
	padding: 3px 18px 3px 10px;
614
	border-top: 1px solid black;
616
	padding: 3px 0px 3px 10px;
615 617
	font-weight: bold;
618
	font-weight: normal;
616 619
}
617 620

  
618 621
table.display tr.heading2 td {
......
623 626
	padding: 3px 10px;
624 627
}
625 628

  
626
table.display td a {
627
	font-weight: normal;
628
}
629

  
630 629
table.display td.center {
631 630
	text-align: center;
632 631
}
633 632

  
634
table.display tfoot th {
635
	padding: 3px 0px 3px 10px;
636
	font-weight: bold;
637
	font-weight: normal;
638
}
639 633

  
640
table.display tr.heading2 td {
641
	border-bottom: 1px solid #aaa;
642
}
643

  
644
table.display td {
645
	padding: 3px 10px;
646
}
647 634

  
648
table.display td.center {
649
	text-align: center;
650
}
635
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
636
 * DataTables sorting
637
 */
651 638

  
652 639
.sorting_asc {
653 640
	background: url('../images/sort_asc.png') no-repeat center right;
......
669 656
	background: url('../images/sort_desc_disabled.png') no-repeat center right;
670 657
}
671 658

  
659

  
660

  
661

  
662
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
663
 * DataTables row classes
664
 */
672 665
table.display tr.odd.gradeA {
673
	background-color: #EEEEEE;
666
	background-color: #ddffdd;
674 667
}
675 668

  
676 669
table.display tr.even.gradeA {
677
	background-color: #F6F6F6;
670
	background-color: #eeffee;
671
}
672

  
673

  
674

  
675

  
676
table.display tr.odd.gradeA {
677
	background-color: #ddffdd;
678
}
679

  
680
table.display tr.even.gradeA {
681
	background-color: #eeffee;
678 682
}
679 683

  
680 684
table.display tr.odd.gradeC {
......
710 714
	background-color: white;
711 715
}
712 716

  
717

  
718

  
719

  
720

  
721
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
722
 * Misc
723
 */
713 724
.dataTables_scroll {
714 725
	clear: both;
715 726
}
716 727

  
717
.dataTables_scrollBody {
718
	*margin-top: -1px;
719
}
720

  
721 728
.top, .bottom {
722 729
	padding: 15px;
723 730
	background-color: #F5F5F5;
......
762 769
	width: 40%;
763 770
}
764 771

  
765
.paging_full_numbers {
766
	width: 400px;
767
	height: 22px;
768
	line-height: 22px;
769
}
770

  
771 772
.paging_full_numbers span.paginate_button,
772 773
 	.paging_full_numbers span.paginate_active {
773 774
	border: 1px solid #aaa;
......
799 800
	background-color: #9FAFD1;
800 801
}
801 802

  
803

  
804
/*
805
 * Sorting classes for columns
806
 */
807
/* For the standard odd/even */
802 808
tr.odd td.sorting_1 {
803 809
	background-color: #D3D6FF;
804 810
}
......
823 829
	background-color: #F9F9FF;
824 830
}
825 831

  
832

  
833
/* For the Conditional-CSS grading rows */
834
/*
835
 	Colour calculations (based off the main row colours)
836
  Level 1:
837
		dd > c4
838
		ee > d5
839
	Level 2:
840
	  dd > d1
841
	  ee > e2
842
 */
826 843
tr.odd.gradeA td.sorting_1 {
827
	background-color: #E1E1E1;
844
	background-color: #c4ffc4;
828 845
}
829 846

  
830 847
tr.odd.gradeA td.sorting_2 {
831
	background-color: #E8E8E8;
848
	background-color: #d1ffd1;
832 849
}
833 850

  
834 851
tr.odd.gradeA td.sorting_3 {
835
	background-color: #E8E8E8;
852
	background-color: #d1ffd1;
836 853
}
837 854

  
838 855
tr.even.gradeA td.sorting_1 {
839
	background-color: #EAEAEA;
856
	background-color: #d5ffd5;
840 857
}
841 858

  
842 859
tr.even.gradeA td.sorting_2 {
843
	background-color: #F0F0F0;
860
	background-color: #e2ffe2;
844 861
}
845 862

  
846 863
tr.even.gradeA td.sorting_3 {
847
	background-color: #F0F0F0;
864
	background-color: #e2ffe2;
848 865
}
849 866

  
850 867
tr.odd.gradeC td.sorting_1 {
......
919 936
	background-color: #e2e2e2;
920 937
}
921 938

  
939

  
940
/*
941
 * Row highlighting example
942
 */
922 943
.ex_highlight #example tbody tr.even:hover, #example tbody tr.even td.highlighted {
923 944
	background-color: #ECFFB3;
924 945
}
......
927 948
	background-color: #E6FF99;
928 949
}
929 950

  
930
.ex_highlight_row #example tr.even:hover {
931
	background-color: #ECFFB3;
932
}
933

  
934
.ex_highlight_row #example tr.even:hover td.sorting_1 {
935
	background-color: #DDFF75;
936
}
937

  
938
.ex_highlight_row #example tr.even:hover td.sorting_2 {
939
	background-color: #E7FF9E;
940
}
941

  
942
.ex_highlight_row #example tr.even:hover td.sorting_3 {
943
	background-color: #E2FF89;
944
}
945

  
946
.ex_highlight_row #example tr.odd:hover {
947
	background-color: #E6FF99;
948
}
949

  
950
.ex_highlight_row #example tr.odd:hover td.sorting_1 {
951
	background-color: #D6FF5C;
952
}
953

  
954
.ex_highlight_row #example tr.odd:hover td.sorting_2 {
955
	background-color: #E0FF84;
956
}
957

  
958
.ex_highlight_row #example tr.odd:hover td.sorting_3 {
959
	background-color: #DBFF70;
960
}
961

  
962
table.KeyTable td {
963
	border: 3px solid transparent;
964
}
965

  
966
table.KeyTable td.focus {
967
	border: 3px solid #3366FF;
968
}
969

  
970
table.display tr.gradeA {
971
	background-color: #eeffee;
972
}
973

  
974
table.display tr.gradeC {
975
	background-color: #ddddff;
976
}
977

  
978
table.display tr.gradeX {
979
	background-color: #ffdddd;
980
}
981

  
982
table.display tr.gradeU {
983
	background-color: #ddd;
984
}
985

  
986 951
div.box {
987 952
	height: 100px;
988 953
	padding: 10px;
b/templates/add_port.html
1
<form id="add_port_form" method="POST">
2
	{% csrf_token %}
3
<table>
4
<tr><th>Port</th><td>{{ form.port }}<span class="error" id='rl_error'>{{ form.port.errors|join:", " }}</span></td></tr>
5
</table>
6
</form>
b/templates/add_rate_limit.html
1
<form id="add_rl_form" method="POST">
2
	{% csrf_token %}
3
<table>
4
<input type="hidden" id="id_action" name="action" value="rate-limit"/>
5
<tr><th>Value</th><td>{{ form.action_value }}Kbps<span class="error" id='rl_error'>{{ form.action_value.errors|join:", " }}</span></td></tr>
6
<tr class="help"><td></td><td>Value should be >50Kbps</td></tr>
7
</table>
8
</form>
b/templates/apply.html
1 1
{% extends "base.html" %}
2 2
{% load i18n %}
3
	
3

  
4 4
{% block title %}
5 5
	{% if edit %}
6 6
		{% trans "Edit Route" %} {{form.data.name}}
......
16 16
	{% trans "Create route" %}
17 17
	{% endif %}
18 18
		{% endblock %}
19

  
19
{% block extrahead %}
20
<script>
21
	$(document).ready( function(){
22
		
23
		$("#id_sourceport").css('width', '100px').attr('size', '5');
24
		$("#id_port").css('width', '100px').attr('size', '5');
25
		$("#id_destinationport").css('width', '100px').attr('size', '5');
26
		$('#id_then').attr("multiple", "");
27
		$('#then_diag').dialog({
28
			height: 220,
29
            width: 340,
30
			modal: true,
31
			autoOpen: false,
32
			buttons: {
33
		'Add': function() {
34
			console.log($("#add_rl_form").serialize());
35
			$.ajax({
36
			url:"{% url add-rate-limit %}", 
37
			data:$("#add_rl_form").serialize(),
38
			type: "POST",
39
			cache: false,
40
			success:function(data){
41
					try {
42
						value = data.pk;
43
						text = data.value;
44
						$('#id_then').append($("<option></option>").attr("value",value).text(text));
45
						$('#then_diag').dialog('close');
46
					}
47
					catch (exception) {
48
						$('#then_diag').html(data);
49
					}					
50
				}
51
				});
52
		},
53
		Cancel: function() {
54
			$('#then_diag').dialog('close');
55
		}
56
	}
57
		});
58
		
59
		$('#port_diag').dialog({
60
			height: 220,
61
            width: 340,
62
			modal: true,
63
			autoOpen: false,
64
			buttons: {
65
		'Add': function() {
66
			console.log($("#add_port_form").serialize());
67
			$.ajax({
68
			url:"{% url add-port %}", 
69
			data:$("#add_port_form").serialize(),
70
			type: "POST",
71
			cache: false,
72
			success:function(data){
73
					try {
74
						value = data.value;
75
						text = data.text;
76
						$('#id_port').append($("<option></option>").attr("value",value).text(text));
77
						$('#id_destinationport').append($("<option></option>").attr("value",value).text(text));
78
						$('#id_sourceport').append($("<option></option>").attr("value",value).text(text));
79
						$('#port_diag').dialog('close');
80
					}
81
					catch (exception) {
82
						$('#port_diag').html(data);
83
					}					
84
				}
85
				});
86
		},
87
		Cancel: function() {
88
			$('#port_diag').dialog('close');
89
		}
90
	}
91
		});
92
		
93
		
94
		$("#new_then_actions").button({
95
            icons: {
96
                primary: "ui-icon-plusthick"
97
            },
98
			})
99
			.click(function(){
100
				$.ajax({
101
					url: "{% url add-rate-limit %}",
102
					cache: false,
103
					success: function(data){
104
						$("#then_diag").html(data);
105
					}
106
				});
107
				$('#then_diag').dialog('open');
108
				return false;
109
			});
110
			
111
			
112
			$(".new_port").button({
113
            icons: {
114
                primary: "ui-icon-plusthick"
115
            },
116
			})
117
			.click(function(){
118
				$.ajax({
119
					url: "{% url add-port %}",
120
					cache: false,
121
					success: function(data){
122
						$("#port_diag").html(data);
123
					}
124
				});
125
				$('#port_diag').dialog('open');
126
				return false;
127
			});
128
		});
129
		
130
</script>
131
{% endblock %}
20 132
{% block content %}
21 133
<style type="text/css">
22 134
th {
......
30 142

  
31 143
}
32 144
</style>
33

  
34 145
<div align="center">
35 146
	{% if edit %}
36 147
	<h3>{% trans "Edit route" %}: {{form.data.name}}</h3>
......
47 158
	<legend>{% trans "Route Basic Info" %}</legend>
48 159
<table>
49 160
<tr><th>{{ form.name.label_tag }}</th><td>{{ form.name }}<span class="error">{{ form.name.errors|join:", " }}</span></td></tr>
50
<tr class="help"><td></td><td>{{ form.name.help_text }}</td></tr>
161
<tr class="help"><td></td><td>A unique identifier will be added as a name_suffix</td></tr>
51 162
</table>
52 163
</fieldset>
53 164

  
54 165
<fieldset>
55 166
<legend>{% trans "Route Match Conditions" %}</legend>
56 167
<table>
168
<input type="hidden" id="id_applier" name="applier" value="{{applier}}"/>
57 169
<tr><th>{{ form.source.label_tag }}</th><td>{{ form.source }}<span class="error">{{ form.source.errors|join:", " }}</span></td></tr>
58 170
<tr class="help"><td></td><td>{{ form.source.help_text }}</td></tr>
59
<tr><th>{{ form.sourceport.label_tag }}</th><td>{{ form.sourceport }}<span class="error">{{ form.sourceport.errors|join:", " }}</span></td></tr>
171
<tr><th>{{ form.sourceport.label_tag }}</th><td>{{ form.sourceport }}&nbsp;&nbsp;<button class="new_port">Port</button><span class="error">{{ form.sourceport.errors|join:", " }}</span></td></tr>
60 172
<tr class="help"><td></td><td>{{ form.sourceport.help_text }}</td></tr>
61 173
<tr><th>{{ form.destination.label_tag }}</th><td>{{ form.destination }}<span class="error">{{ form.destination.errors|join:", " }}</span></td></tr>
62 174
<tr class="help"><td></td><td>{{ form.destination.help_text }}</td></tr>
63
<tr><th>{{ form.destinationport.label_tag }}</th><td>{{ form.destinationport }}<span class="error">{{ form.destinationport.errors|join:", " }}</span></td></tr>
175
<tr><th>{{ form.destinationport.label_tag }}</th><td>{{ form.destinationport }}&nbsp;&nbsp;<button class="new_port">Port</button><span class="error">{{ form.destinationport.errors|join:", " }}</span></td></tr>
64 176
<tr class="help"><td></td><td>{{ form.destinationport.help_text }}</td></tr>
65
<tr><th>{{ form.port.label_tag }}</th><td>{{ form.port }}<span class="error">{{ form.port.errors|join:", " }}</span></td></tr>
177
<tr><th>{{ form.port.label_tag }}</th><td>{{ form.port }}&nbsp;&nbsp;<button class="new_port">Port</button><span class="error">{{ form.port.errors|join:", " }}</span></td></tr>
66 178
<tr class="help"><td></td><td>{{ form.port.help_text }}</td></tr>
67 179
</table>
68 180
</fieldset>
69 181
<fieldset>
70 182
<legend>{% trans "Route Actions" %}</legend>
71 183
<table>
72
<tr><th>{{ form.then.label_tag }}</th><td>{{ form.then }}<span class="error">{{ form.then.errors|join:", " }}</span></td></tr>
73
<tr class="help"><td></td><td>{{ form.then.help_text }}</td></tr>
184
<tr><th>{{ form.then.label_tag }}</th><td>{{ form.then }}&nbsp;&nbsp;<button id="new_then_actions">Rate-limit</button><span class="error">{{ form.then.errors|join:", " }}</span></td></tr>
74 185
</table>
75 186
</fieldset>
76 187
<fieldset>
......
87 198
</form>
88 199
</div>
89 200

  
201
<div id="then_diag" title="Add new rate-limit value">
202
</div>
203

  
204
<div id="port_diag" title="Add new port">
205
</div>
206

  
90 207
{% endblock %}
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff