Commit f08c1e86 by Őry Máté

Merge branch 'feature-network-gui-improvements' into 'master'

Feature network gui improvements

* closes #358 Automatically select a free ipv4 address based on vlan (host creation)
* closes #357 Automatically generate ipv6 address from ipv4 (host creation/edit)
* closes #354 MAC address autocomplete
* closes #360 Add sensible default and help_text to Vlan.ipv6_template

See merge request !273
parents 8a96b4f8 6694020e
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from string import ascii_letters
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -22,7 +23,7 @@ from django.utils.ipv6 import is_valid_ipv6_address ...@@ -22,7 +23,7 @@ from django.utils.ipv6 import is_valid_ipv6_address
from south.modelsinspector import add_introspection_rules from south.modelsinspector import add_introspection_rules
from django import forms from django import forms
from netaddr import (IPAddress, IPNetwork, AddrFormatError, ZEROFILL, from netaddr import (IPAddress, IPNetwork, AddrFormatError, ZEROFILL,
EUI, mac_unix) EUI, mac_unix, AddrConversionError)
import re import re
...@@ -31,7 +32,6 @@ domain_re = re.compile(r'^([A-Za-z0-9_-]\.?)+$') ...@@ -31,7 +32,6 @@ domain_re = re.compile(r'^([A-Za-z0-9_-]\.?)+$')
domain_wildcard_re = re.compile(r'^(\*\.)?([A-Za-z0-9_-]\.?)+$') domain_wildcard_re = re.compile(r'^(\*\.)?([A-Za-z0-9_-]\.?)+$')
ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$') ipv4_re = re.compile('^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$')
reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$') reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
class mac_custom(mac_unix): class mac_custom(mac_unix):
...@@ -246,15 +246,60 @@ def val_reverse_domain(value): ...@@ -246,15 +246,60 @@ def val_reverse_domain(value):
raise ValidationError(u'%s - invalid reverse domain name' % value) raise ValidationError(u'%s - invalid reverse domain name' % value)
def is_valid_ipv6_template(value):
"""Check whether the parameter is a valid ipv6 template."""
return ipv6_template_re.match(value) is not None
def val_ipv6_template(value): def val_ipv6_template(value):
"""Validate whether the parameter is a valid ipv6 template.""" """Validate whether the parameter is a valid ipv6 template.
if not is_valid_ipv6_template(value):
raise ValidationError(u'%s - invalid reverse ipv6 template' % value) Normal use:
>>> val_ipv6_template("123::%(a)d:%(b)d:%(c)d:%(d)d")
>>> val_ipv6_template("::%(a)x:%(b)x:%(c)d:%(d)d")
Don't have to use all bytes from the left (no a):
>>> val_ipv6_template("::%(b)x:%(c)d:%(d)d")
But have to use all ones to the right (a, but no b):
>>> val_ipv6_template("::%(a)x:%(c)d:%(d)d")
Traceback (most recent call last):
...
ValidationError: [u"template doesn't use parameter b"]
Detects valid templates building invalid ips:
>>> val_ipv6_template("xxx::%(a)d:%(b)d:%(c)d:%(d)d")
Traceback (most recent call last):
...
ValidationError: [u'template renders invalid IPv6 address']
Also IPv4-compatible addresses are invalid:
>>> val_ipv6_template("::%(a)02x%(b)02x:%(c)d:%(d)d")
Traceback (most recent call last):
...
ValidationError: [u'template results in IPv4 address']
"""
tpl = {ascii_letters[i]: 255 for i in range(4)}
try:
v6 = value % tpl
except:
raise ValidationError(_('%s: invalid template') % value)
used = False
for i in ascii_letters[:4]:
try:
value % {k: tpl[k] for k in tpl if k != i}
except KeyError:
used = True # ok, it misses this key
else:
if used:
raise ValidationError(
_("template doesn't use parameter %s") % i)
try:
v6 = IPAddress(v6, 6)
except:
raise ValidationError(_('template renders invalid IPv6 address'))
try:
v6.ipv4()
except (AddrConversionError, AddrFormatError):
pass # can't converted to ipv4 == it's real ipv6
else:
raise ValidationError(_('template results in IPv4 address'))
def is_valid_ipv4_address(value): def is_valid_ipv4_address(value):
...@@ -284,12 +329,3 @@ def val_mx(value): ...@@ -284,12 +329,3 @@ def val_mx(value):
domain_re.match(mx[1])): domain_re.match(mx[1])):
raise ValidationError(_("Bad MX address format. " raise ValidationError(_("Bad MX address format. "
"Should be: <priority>:<hostname>")) "Should be: <priority>:<hostname>"))
def convert_ipv4_to_ipv6(ipv6_template, ipv4):
"""Convert IPv4 address string to IPv6 address string."""
m = ipv4.words
return IPAddress(ipv6_template % {'a': int(m[0]),
'b': int(m[1]),
'c': int(m[2]),
'd': int(m[3])})
...@@ -17,9 +17,11 @@ ...@@ -17,9 +17,11 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from itertools import islice, ifilter from string import ascii_letters
from itertools import islice, ifilter, chain
from math import ceil
import logging import logging
from netaddr import IPSet, EUI, IPNetwork import random
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
...@@ -28,15 +30,17 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -28,15 +30,17 @@ from django.utils.translation import ugettext_lazy as _
from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain, from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain,
val_ipv6_template, val_domain, val_ipv4, val_ipv6_template, val_domain, val_ipv4,
val_domain_wildcard, val_domain_wildcard,
val_ipv6, val_mx, convert_ipv4_to_ipv6, val_ipv6, val_mx,
IPNetworkField, IPAddressField) IPNetworkField, IPAddressField)
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
import django.conf import django.conf
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
import random from celery.exceptions import TimeoutError
from netaddr import IPSet, EUI, IPNetwork, IPAddress, ipv6_full
from common.models import method_cache, WorkerNotFound, HumanSortField from common.models import method_cache, WorkerNotFound, HumanSortField
from firewall.tasks.local_tasks import reloadtask from firewall.tasks.local_tasks import reloadtask
from firewall.tasks.remote_tasks import get_dhcp_clients
from .iptables import IptRule from .iptables import IptRule
from acl.models import AclBase from acl.models import AclBase
...@@ -360,9 +364,19 @@ class Vlan(AclBase, models.Model): ...@@ -360,9 +364,19 @@ class Vlan(AclBase, models.Model):
'address is: "%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa".'), 'address is: "%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa".'),
default="%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa") default="%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa")
ipv6_template = models.TextField( ipv6_template = models.TextField(
validators=[val_ipv6_template], blank=True,
verbose_name=_('ipv6 template'), help_text=_('Template for translating IPv4 addresses to IPv6. '
default="2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0") 'Automatically generated hosts in dual-stack networks '
'will get this address. The template '
'can contain four tokens: "%(a)d", "%(b)d", '
'"%(c)d", and "%(d)d", representing the four bytes '
'of the IPv4 address, respectively, in decimal notation. '
'Moreover you can use any standard printf format '
'specification like %(a)02x to get the first byte as two '
'hexadecimal digits. Usual choices for mapping '
'198.51.100.0/24 to 2001:0DB8:1:1::/64 would be '
'"2001:db8:1:1:%(d)d::" and "2001:db8:1:1:%(d)02x00::".'),
validators=[val_ipv6_template], verbose_name=_('ipv6 template'))
dhcp_pool = models.TextField(blank=True, verbose_name=_('DHCP pool'), dhcp_pool = models.TextField(blank=True, verbose_name=_('DHCP pool'),
help_text=_( help_text=_(
'The address range of the DHCP pool: ' 'The address range of the DHCP pool: '
...@@ -377,6 +391,87 @@ class Vlan(AclBase, models.Model): ...@@ -377,6 +391,87 @@ class Vlan(AclBase, models.Model):
modified_at = models.DateTimeField(auto_now=True, modified_at = models.DateTimeField(auto_now=True,
verbose_name=_('modified at')) verbose_name=_('modified at'))
def clean(self):
super(Vlan, self).clean()
if self.ipv6_template:
if not self.network6:
raise ValidationError(
_("You cannot specify an IPv6 template if there is no "
"IPv6 network set."))
for i in (self.network4[1], self.network4[-1]):
i6 = self.convert_ipv4_to_ipv6(i)
if i6 not in self.network6:
raise ValidationError(
_("%(ip6)s (translated from %(ip4)s) is outside of "
"the IPv6 network.") % {"ip4": i, "ip6": i6})
if self.network6:
tpl, prefixlen = self._magic_ipv6_template(self.network4,
self.network6)
if not self.ipv6_template:
self.ipv6_template = tpl
if not self.host_ipv6_prefixlen:
self.host_ipv6_prefixlen = prefixlen
@staticmethod
def _host_bytes(prefixlen, maxbytes):
return int(ceil((maxbytes - prefixlen / 8.0)))
@staticmethod
def _append_hexa(s, v, lasthalf):
if lasthalf: # can use last half word
assert s[-1] == "0" or s[-1].endswith("00")
if s[-1].endswith("00"):
s[-1] = s[-1][:-2]
s[-1] += "%({})02x".format(v)
s[-1].lstrip("0")
else:
s.append("%({})02x00".format(v))
@classmethod
def _magic_ipv6_template(cls, network4, network6, verbose=None):
"""Offer a sensible ipv6_template value.
Based on prefix lengths the method magically selects verbose (decimal)
format:
>>> Vlan._magic_ipv6_template(IPNetwork("198.51.100.0/24"),
... IPNetwork("2001:0DB8:1:1::/64"))
('2001:db8:1:1:%(d)d::', 80)
However you can explicitly select non-verbose, i.e. hexa format:
>>> Vlan._magic_ipv6_template(IPNetwork("198.51.100.0/24"),
... IPNetwork("2001:0DB8:1:1::/64"), False)
('2001:db8:1:1:%(d)02x00::', 72)
"""
host4_bytes = cls._host_bytes(network4.prefixlen, 4)
host6_bytes = cls._host_bytes(network6.prefixlen, 16)
if host4_bytes > host6_bytes:
raise ValidationError(
_("IPv6 network is too small to map IPv4 addresses to it."))
letters = ascii_letters[4-host4_bytes:4]
remove = host6_bytes // 2
ipstr = network6.network.format(ipv6_full)
s = ipstr.split(":")[0:-remove]
if verbose is None: # use verbose format if net6 much wider
verbose = 2 * (host4_bytes + 1) < host6_bytes
if verbose:
for i in letters:
s.append("%({})d".format(i))
else:
remain = host6_bytes
for i in letters:
cls._append_hexa(s, i, remain % 2 == 1)
remain -= 1
if host6_bytes > host4_bytes:
s.append(":")
tpl = ":".join(s)
# compute prefix length
mask = int(IPAddress(tpl % {"a": 1, "b": 1, "c": 1, "d": 1}))
prefixlen = 128
while mask % 2 == 0:
mask /= 2
prefixlen -= 1
return (tpl, prefixlen)
def __unicode__(self): def __unicode__(self):
return "%s - %s" % ("managed" if self.managed else "unmanaged", return "%s - %s" % ("managed" if self.managed else "unmanaged",
self.name) self.name)
...@@ -402,7 +497,7 @@ class Vlan(AclBase, models.Model): ...@@ -402,7 +497,7 @@ class Vlan(AclBase, models.Model):
logger.debug("Found unused IPv4 address %s.", ipv4) logger.debug("Found unused IPv4 address %s.", ipv4)
ipv6 = None ipv6 = None
if self.network6 is not None: if self.network6 is not None:
ipv6 = convert_ipv4_to_ipv6(self.ipv6_template, ipv4) ipv6 = self.convert_ipv4_to_ipv6(ipv4)
if ipv6 in used_v6: if ipv6 in used_v6:
continue continue
else: else:
...@@ -411,6 +506,20 @@ class Vlan(AclBase, models.Model): ...@@ -411,6 +506,20 @@ class Vlan(AclBase, models.Model):
else: else:
raise ValidationError(_("All IP addresses are already in use.")) raise ValidationError(_("All IP addresses are already in use."))
def convert_ipv4_to_ipv6(self, ipv4):
"""Convert IPv4 address string to IPv6 address string."""
if isinstance(ipv4, basestring):
ipv4 = IPAddress(ipv4, 4)
nums = {ascii_letters[i]: int(ipv4.words[i]) for i in range(4)}
return IPAddress(self.ipv6_template % nums)
def get_dhcp_clients(self):
macs = set(i.mac for i in self.host_set.all())
return [{"mac": k, "ip": v["ip"], "hostname": v["hostname"]}
for k, v in chain(*(fw.get_dhcp_clients().iteritems()
for fw in Firewall.objects.all() if fw))
if v["interface"] == self.name and EUI(k) not in macs]
class VlanGroup(models.Model): class VlanGroup(models.Model):
""" """
...@@ -581,8 +690,7 @@ class Host(models.Model): ...@@ -581,8 +690,7 @@ class Host(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.id and self.ipv6 == "auto": if not self.id and self.ipv6 == "auto":
self.ipv6 = convert_ipv4_to_ipv6(self.vlan.ipv6_template, self.ipv6 = self.vlan.convert_ipv4_to_ipv6(self.ipv4)
self.ipv4)
self.full_clean() self.full_clean()
super(Host, self).save(*args, **kwargs) super(Host, self).save(*args, **kwargs)
...@@ -838,7 +946,7 @@ class Firewall(models.Model): ...@@ -838,7 +946,7 @@ class Firewall(models.Model):
return self.name return self.name
@method_cache(30) @method_cache(30)
def get_remote_queue_name(self, queue_id): def get_remote_queue_name(self, queue_id="firewall"):
"""Returns the name of the remote celery queue for this node. """Returns the name of the remote celery queue for this node.
Throws Exception if there is no worker on the queue. Throws Exception if there is no worker on the queue.
...@@ -851,6 +959,20 @@ class Firewall(models.Model): ...@@ -851,6 +959,20 @@ class Firewall(models.Model):
else: else:
raise WorkerNotFound() raise WorkerNotFound()
@method_cache(20)
def get_dhcp_clients(self):
try:
return get_dhcp_clients.apply_async(
queue=self.get_remote_queue_name(), expires=60).get(timeout=2)
except TimeoutError:
logger.info("get_dhcp_clients task timed out")
except IOError:
logger.exception("get_dhcp_clients failed. "
"maybe syslog isn't readble by firewall worker")
except:
logger.exception("get_dhcp_clients failed")
return None
class Domain(models.Model): class Domain(models.Model):
name = models.CharField(max_length=40, validators=[val_domain], name = models.CharField(max_length=40, validators=[val_domain],
......
...@@ -62,5 +62,6 @@ def reload_blacklist(data): ...@@ -62,5 +62,6 @@ def reload_blacklist(data):
@celery.task(name='firewall.get_dhcp_clients') @celery.task(name='firewall.get_dhcp_clients')
def get_dhcp_clients(data): def get_dhcp_clients():
# {'00:21:5a:73:72:cd': {'interface': 'OFF', 'ip': None, 'hostname': None}}
pass pass
...@@ -78,6 +78,7 @@ class GetNewAddressTestCase(TestCase): ...@@ -78,6 +78,7 @@ class GetNewAddressTestCase(TestCase):
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29', self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29',
network6='2001:738:2001:4031::/80', domain=d, network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1) owner=self.u1)
self.vlan.clean()
self.vlan.save() self.vlan.save()
self.vlan.host_set.all().delete() self.vlan.host_set.all().delete()
for i in [1] + range(3, 6): for i in [1] + range(3, 6):
...@@ -85,6 +86,9 @@ class GetNewAddressTestCase(TestCase): ...@@ -85,6 +86,9 @@ class GetNewAddressTestCase(TestCase):
ipv4='10.0.0.%d' % i, vlan=self.vlan, ipv4='10.0.0.%d' % i, vlan=self.vlan,
owner=self.u1).save() owner=self.u1).save()
def tearDown(self):
self.vlan.delete()
def test_new_addr_w_empty_vlan(self): def test_new_addr_w_empty_vlan(self):
self.vlan.host_set.all().delete() self.vlan.host_set.all().delete()
self.vlan.get_new_address() self.vlan.get_new_address()
...@@ -96,12 +100,6 @@ class GetNewAddressTestCase(TestCase): ...@@ -96,12 +100,6 @@ class GetNewAddressTestCase(TestCase):
owner=self.u1).save() owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address) self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_all_addr_in_use_w_ipv6(self):
Host(hostname='h-x', mac='01:02:03:04:05:06',
ipv4='10.0.0.6', ipv6='2001:738:2001:4031:0:0:2:0',
vlan=self.vlan, owner=self.u1).save()
self.assertRaises(ValidationError, self.vlan.get_new_address)
def test_new_addr(self): def test_new_addr(self):
used_v4 = IPSet(self.vlan.host_set.values_list('ipv4', flat=True)) used_v4 = IPSet(self.vlan.host_set.values_list('ipv4', flat=True))
assert self.vlan.get_new_address()['ipv4'] not in used_v4 assert self.vlan.get_new_address()['ipv4'] not in used_v4
......
...@@ -15,13 +15,13 @@ ...@@ -15,13 +15,13 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.forms import ModelForm from django.forms import ModelForm, widgets
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput
from crispy_forms.bootstrap import FormActions from crispy_forms.bootstrap import FormActions, FieldWithButtons, StrictButton
from firewall.models import (Host, Vlan, Domain, Group, Record, BlacklistItem, from firewall.models import (Host, Vlan, Domain, Group, Record, BlacklistItem,
Rule, VlanGroup, SwitchPort) Rule, VlanGroup, SwitchPort)
...@@ -122,8 +122,12 @@ class HostForm(ModelForm): ...@@ -122,8 +122,12 @@ class HostForm(ModelForm):
Fieldset( Fieldset(
_('Network'), _('Network'),
'vlan', 'vlan',
'ipv4', FieldWithButtons('ipv4', StrictButton(
'ipv6', '<i class="fa fa-magic"></i>', css_id="ipv4-magic",
title=_("Generate random address."))),
FieldWithButtons('ipv6', StrictButton(
'<i class="fa fa-magic"></i>', css_id="ipv6-magic",
title=_("Generate IPv6 pair of IPv4 address."))),
'shared_ip', 'shared_ip',
'external_ipv4', 'external_ipv4',
), ),
...@@ -252,7 +256,9 @@ class VlanForm(ModelForm): ...@@ -252,7 +256,9 @@ class VlanForm(ModelForm):
Fieldset( Fieldset(
_('IPv6'), _('IPv6'),
'network6', 'network6',
'ipv6_template', FieldWithButtons('ipv6_template', StrictButton(
'<i class="fa fa-magic"></i>', css_id="ipv6-tpl-magic",
title=_("Generate sensible template."))),
'host_ipv6_prefixlen', 'host_ipv6_prefixlen',
), ),
Fieldset( Fieldset(
...@@ -277,6 +283,9 @@ class VlanForm(ModelForm): ...@@ -277,6 +283,9 @@ class VlanForm(ModelForm):
class Meta: class Meta:
model = Vlan model = Vlan
widgets = {
'ipv6_template': widgets.TextInput,
}
class VlanGroupForm(ModelForm): class VlanGroupForm(ModelForm):
......
...@@ -28,6 +28,49 @@ function getURLParameter(name) { ...@@ -28,6 +28,49 @@ function getURLParameter(name) {
); );
} }
function doBlink(id, count) {
if (count > 0) {
$(id).parent().delay(200).queue(function() {
$(this).delay(200).queue(function() {
$(this).removeClass("has-warning").dequeue();
doBlink(id, count-1);});
$(this).addClass("has-warning").dequeue()
});
}
}
$(function() { $(function() {
$("[title]").tooltip(); $("[title]").tooltip();
$("#ipv6-magic").click(function() {
$.ajax({url: window.location,
data: {ipv4: $("[name=ipv4]").val(),
vlan: $("[name=vlan]").val()},
success: function(data) {
$("[name=ipv6]").val(data.ipv6);
}});
});
$("#ipv4-magic").click(function() {
$.ajax({url: window.location,
data: {vlan: $("[name=vlan]").val()},
success: function(data) {
$("[name=ipv4]").val(data.ipv4);
if ($("[name=ipv6]").val() != data.ipv6) {
doBlink("[name=ipv6]", 3);
}
$("[name=ipv6]").val(data.ipv6);
}});
});
$("#ipv6-tpl-magic").click(function() {
$.ajax({url: window.location,
data: {network4: $("[name=network4]").val(),
network6: $("[name=network6]").val()},
success: function(data) {
$("[name=ipv6_template]").val(data.ipv6_template);
if ($("[name=host_ipv6_prefixlen]").val() != data.host_ipv6_prefixlen) {
doBlink("[name=host_ipv6_prefixlen]", 3);
}
$("[name=host_ipv6_prefixlen]").val(data.host_ipv6_prefixlen);
}});
});
}); });
...@@ -15,14 +15,30 @@ ...@@ -15,14 +15,30 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from netaddr import EUI
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.html import format_html
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import LinkColumn, TemplateColumn from django_tables2.columns import LinkColumn, TemplateColumn, Column
from firewall.models import Host, Vlan, Domain, Group, Record, Rule, SwitchPort from firewall.models import Host, Vlan, Domain, Group, Record, Rule, SwitchPort
class MACColumn(Column):
def render(self, value):
if isinstance(value, basestring):
try:
value = EUI(value)
except:
return value
try:
return format_html('<abbr title="{0}">{1}</abbr>',
value.oui.registration().org, value)
except:
return value
class BlacklistItemTable(Table): class BlacklistItemTable(Table):
ipv4 = LinkColumn('network.blacklist', args=[A('pk')]) ipv4 = LinkColumn('network.blacklist', args=[A('pk')])
...@@ -55,9 +71,7 @@ class GroupTable(Table): ...@@ -55,9 +71,7 @@ class GroupTable(Table):
class HostTable(Table): class HostTable(Table):
hostname = LinkColumn('network.host', args=[A('pk')]) hostname = LinkColumn('network.host', args=[A('pk')])
mac = TemplateColumn( mac = MACColumn()
template_name="network/columns/mac.html"
)
class Meta: class Meta:
model = Host model = Host
...@@ -108,6 +122,20 @@ class SmallHostTable(Table): ...@@ -108,6 +122,20 @@ class SmallHostTable(Table):
attrs = {'class': 'table table-striped table-condensed'} attrs = {'class': 'table table-striped table-condensed'}
fields = ('hostname', 'ipv4') fields = ('hostname', 'ipv4')
order_by = ('vlan', 'hostname', ) order_by = ('vlan', 'hostname', )
empty_text = _("No hosts.")
class SmallDhcpTable(Table):
mac = MACColumn(verbose_name=_("MAC address"))
hostname = Column(verbose_name=_("hostname"))
ip = Column(verbose_name=_("requested IP"))
register = TemplateColumn(
template_name="network/columns/host-register.html",
attrs={"th": {"style": "display: none;"}})
class Meta:
attrs = {'class': 'table table-striped table-condensed'}
empty_text = _("No hosts.")
class RecordTable(Table): class RecordTable(Table):
......
{% load i18n %}
<a href="{% url "network.host_create" %}?vlan={{ object.pk }}&amp;mac={{ record.mac }}&amp;hostname={{ record.hostname }}&amp;ipv4={{ record.ip }}"
title="{% trans "register host" %}"><i class="fa fa-plus-circle"></i></a>
{% load i18n %}
<span title="{% blocktrans with vendor=record.hw_vendor|default:"n/a" %}Vendor: {{vendor}}{% endblocktrans %}">{{ record.mac }}</span>
...@@ -19,9 +19,16 @@ ...@@ -19,9 +19,16 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="page-header"> <div class="page-header">
<a href="{% url "network.host_create" %}?vlan={{vlan.pk}}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new host" %}</a>
<h3>{% trans "Host list" %}</h3> <h3>{% trans "Host list" %}</h3>
</div> </div>
{% render_table host_list %} {% render_table host_list %}
<div class="page-header">
<h3>{% trans "Unregistered hosts" %}</h3>
</div>
{% render_table dhcp_list %}
<div class="page-header"> <div class="page-header">
<h3>{% trans "Manage access" %}</h3> <h3>{% trans "Manage access" %}</h3>
</div> </div>
......
...@@ -15,11 +15,13 @@ ...@@ -15,11 +15,13 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from netaddr import IPNetwork
from django.views.generic import (TemplateView, UpdateView, DeleteView, from django.views.generic import (TemplateView, UpdateView, DeleteView,
CreateView) CreateView)
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.shortcuts import render, redirect from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse from django.http import HttpResponse, Http404
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
...@@ -29,7 +31,7 @@ from vm.models import Interface ...@@ -29,7 +31,7 @@ from vm.models import Interface
from .tables import (HostTable, VlanTable, SmallHostTable, DomainTable, from .tables import (HostTable, VlanTable, SmallHostTable, DomainTable,
GroupTable, RecordTable, BlacklistItemTable, RuleTable, GroupTable, RecordTable, BlacklistItemTable, RuleTable,
VlanGroupTable, SmallRuleTable, SmallGroupRuleTable, VlanGroupTable, SmallRuleTable, SmallGroupRuleTable,
SmallRecordTable, SwitchPortTable) SmallRecordTable, SwitchPortTable, SmallDhcpTable, )
from .forms import (HostForm, VlanForm, DomainForm, GroupForm, RecordForm, from .forms import (HostForm, VlanForm, DomainForm, GroupForm, RecordForm,
BlacklistItemForm, RuleForm, VlanGroupForm, SwitchPortForm) BlacklistItemForm, RuleForm, VlanGroupForm, SwitchPortForm)
...@@ -41,10 +43,36 @@ from braces.views import LoginRequiredMixin, SuperuserRequiredMixin ...@@ -41,10 +43,36 @@ from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
# from django.db.models import Q # from django.db.models import Q
from operator import itemgetter from operator import itemgetter
from itertools import chain from itertools import chain
import json
from dashboard.views import AclUpdateView from dashboard.views import AclUpdateView
from dashboard.forms import AclUserOrGroupAddForm from dashboard.forms import AclUserOrGroupAddForm
from django.utils import simplejson
try:
from django.http import JsonResponse
except ImportError:
class JsonResponse(HttpResponse):
"""JSON response for Django < 1.7
https://gist.github.com/philippeowagner/3179eb475fe1795d6515
"""
def __init__(self, content, mimetype='application/json',
status=None, content_type=None):
super(JsonResponse, self).__init__(
content=simplejson.dumps(content),
mimetype=mimetype,
status=status,
content_type=content_type)
class MagicMixin(object):
def get(self, *args, **kwargs):
if self.request.is_ajax():
result = self._get_ajax(*args, **kwargs)
return JsonResponse({k: unicode(result[k] or "") for k in result})
else:
return super(MagicMixin, self).get(*args, **kwargs)
class InitialOwnerMixin(FormMixin): class InitialOwnerMixin(FormMixin):
def get_initial(self): def get_initial(self):
...@@ -310,6 +338,25 @@ class GroupDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): ...@@ -310,6 +338,25 @@ class GroupDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
return context return context
class HostMagicMixin(MagicMixin):
def _get_ajax(self, *args, **kwargs):
GET = self.request.GET
result = {}
vlan = get_object_or_404(Vlan.objects, pk=GET.get("vlan", ""))
if "ipv4" in GET:
try:
result["ipv6"] = vlan.convert_ipv4_to_ipv6(GET["ipv4"]) or ""
except:
result["ipv6"] = ""
else:
try:
result.update(vlan.get_new_address())
except ValidationError:
result["ipv4"] = ""
result["ipv6"] = ""
return result
class HostList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView): class HostList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
model = Host model = Host
table_class = HostTable table_class = HostTable
...@@ -331,15 +378,15 @@ class HostList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView): ...@@ -331,15 +378,15 @@ class HostList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
return data return data
class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin, class HostDetail(HostMagicMixin, LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView): SuccessMessageMixin, UpdateView):
model = Host model = Host
template_name = "network/host-edit.html" template_name = "network/host-edit.html"
form_class = HostForm form_class = HostForm
success_message = _(u'Successfully modified host %(hostname)s!') success_message = _(u'Successfully modified host %(hostname)s!')
def get(self, request, *args, **kwargs): def _get_ajax(self, *args, **kwargs):
if request.is_ajax(): if "vlan" not in self.request.GET:
host = Host.objects.get(pk=kwargs['pk']) host = Host.objects.get(pk=kwargs['pk'])
host = { host = {
'hostname': host.hostname, 'hostname': host.hostname,
...@@ -347,9 +394,11 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -347,9 +394,11 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
'ipv6': str(host.ipv6), 'ipv6': str(host.ipv6),
'fqdn': host.get_fqdn() 'fqdn': host.get_fqdn()
} }
return HttpResponse(json.dumps(host), return host
content_type="application/json")
else: else:
return super(HostDetail, self)._get_ajax(*args, **kwargs)
def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
return super(HostDetail, self).get(request, *args, **kwargs) return super(HostDetail, self).get(request, *args, **kwargs)
...@@ -402,13 +451,29 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -402,13 +451,29 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
return reverse_lazy('network.host', kwargs=self.kwargs) return reverse_lazy('network.host', kwargs=self.kwargs)
class HostCreate(LoginRequiredMixin, SuperuserRequiredMixin, class HostCreate(HostMagicMixin, LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, InitialOwnerMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Host model = Host
template_name = "network/host-create.html" template_name = "network/host-create.html"
form_class = HostForm form_class = HostForm
success_message = _(u'Successfully created host %(hostname)s!') success_message = _(u'Successfully created host %(hostname)s!')
def get_initial(self):
initial = super(HostCreate, self).get_initial()
for i in ("vlan", "mac", "hostname"):
if i in self.request.GET and i not in self.request.POST:
initial[i] = self.request.GET[i]
if "vlan" in initial:
if not initial['vlan'].isnumeric():
raise Http404()
vlan = get_object_or_404(Vlan.objects, pk=initial['vlan'])
try:
initial.update(vlan.get_new_address())
except ValidationError as e:
messages.error(self.request, e.message)
return initial
class HostDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): class HostDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
model = Host model = Host
...@@ -644,7 +709,21 @@ class VlanAclUpdateView(AclUpdateView): ...@@ -644,7 +709,21 @@ class VlanAclUpdateView(AclUpdateView):
model = Vlan model = Vlan
class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, class VlanMagicMixin(MagicMixin):
def _get_ajax(self, *args, **kwargs):
GET = self.request.GET
result = {}
if "network4" in GET and "network6" in GET:
try:
result["ipv6_template"], result["host_ipv6_prefixlen"] = (
Vlan._magic_ipv6_template(IPNetwork(GET['network4']),
IPNetwork(GET['network6'])))
except:
result["ipv6_template"] = result["host_ipv6_prefixlen"] = ""
return result
class VlanDetail(VlanMagicMixin, LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView): SuccessMessageMixin, UpdateView):
model = Vlan model = Vlan
template_name = "network/vlan-edit.html" template_name = "network/vlan-edit.html"
...@@ -656,6 +735,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -656,6 +735,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(VlanDetail, self).get_context_data(**kwargs) context = super(VlanDetail, self).get_context_data(**kwargs)
context['host_list'] = SmallHostTable(self.object.host_set.all()) context['host_list'] = SmallHostTable(self.object.host_set.all())
context['dhcp_list'] = SmallDhcpTable(self.object.get_dhcp_clients())
context['vlan_vid'] = self.kwargs.get('vid') context['vlan_vid'] = self.kwargs.get('vid')
context['acl'] = AclUpdateView.get_acl_data( context['acl'] = AclUpdateView.get_acl_data(
self.object, self.request.user, 'network.vlan-acl') self.object, self.request.user, 'network.vlan-acl')
...@@ -665,7 +745,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -665,7 +745,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
success_url = reverse_lazy('network.vlan_list') success_url = reverse_lazy('network.vlan_list')
class VlanCreate(LoginRequiredMixin, SuperuserRequiredMixin, class VlanCreate(VlanMagicMixin, LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, InitialOwnerMixin, CreateView): SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Vlan model = Vlan
template_name = "network/vlan-create.html" template_name = "network/vlan-create.html"
......
...@@ -177,6 +177,7 @@ class InterfaceTestCase(TestCase): ...@@ -177,6 +177,7 @@ class InterfaceTestCase(TestCase):
d.save() d.save()
v = Vlan(vid=55, network4='127.0.0.1/8', v = Vlan(vid=55, network4='127.0.0.1/8',
network6='2001::1/32', domain=d) network6='2001::1/32', domain=d)
v.clean()
v.save() v.save()
Interface.create(i, v, managed=True, owner=owner) Interface.create(i, v, managed=True, owner=owner)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment