Commit 6d44029c by Őry Máté

Merge branch 'master' into feature-pipeline

Conflicts:
	circle/dashboard/templates/dashboard/base.html
parents 43de3dcb 1ca7edbb
......@@ -108,9 +108,9 @@ $(function () {
e.stopImmediatePropagation();
return false;
});
$('[title]:not(.title-favourite)').tooltip();
$('.title-favourite').tooltip({'placement': 'right'});
$(':input[title]').tooltip({trigger: 'focus', placement: 'auto right'});
$('body [title]:not(.title-favourite)').tooltip();
$('body .title-favourite').tooltip({'placement': 'right'});
$('body :input[title]').tooltip({trigger: 'focus', placement: 'auto right'});
$(".knob").knob();
$('[data-toggle="pill"]').click(function() {
......@@ -132,7 +132,7 @@ $(function () {
$('.js-hidden').hide();
/* favourite star */
$("#dashboard-vm-list").on('click', '.dashboard-vm-favourite', function(e) {
$("#dashboard-vm-list, .page-header").on('click', '.dashboard-vm-favourite', function(e) {
var star = $(this).children("i");
var pk = $(this).data("vm");
if(star.hasClass("fa-star-o")) {
......
......@@ -5,6 +5,11 @@
{% block title-site %}Dashboard | CIRCLE{% endblock %}
{% block extra_link %}
{% block extra_link_2 %}{% endblock %}
{% endblock %}
{% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
{% include "branding.html" %}
......
......@@ -4,6 +4,13 @@
{% block title-page %}{% trans "Index" %}{% endblock %}
{% block extra_link_2 %}
<link rel="search"
type="application/opensearchdescription+xml"
href="{% url "dashboard.views.vm-opensearch" %}"
title="{% blocktrans with name=COMPANY_NAME %}{{name}} virtual machines{% endblocktrans %}" />
{% endblock %}
{% block content %}
<div class="body-content dashboard-index">
<div class="row">
......
......@@ -50,7 +50,7 @@
<dd>{{ object.task_uuid|default:'n/a' }}</dd>
<dt>{% trans "status" %}</dt>
<dd>{{ object.get_status_id }}</dd>
<dd id="activity_status">{{ object.get_status_id }}</dd>
<dt>{% trans "result" %}</dt>
......
......@@ -72,6 +72,13 @@
{{ instance.name }}
</div>
<small>{{ instance.primary_host.get_fqdn }}</small>
<small class="dashboard-vm-favourite" style="line-height: 39.6px;" data-vm="{{ instance.pk }}">
{% if fav %}
<i class="fa fa-star text-primary title-favourite" title="{% trans "Unfavourite" %}"></i>
{% else %}
<i class="fa fa-star-o text-primary title-favourite" title="{% trans "Mark as favorite" %}"></i>
{% endif %}
</small>
</h1>
<div style="clear: both;"></div>
</div>
......
{% load i18n %}<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>{% blocktrans with name=COMPANY_NAME %}{{name}} virtual machines{% endblocktrans %}</ShortName>
<Url type="text/html"
template="{{ url }}?s={searchTerms}&amp;stype=shared" />
</OpenSearchDescription>
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from os import listdir
from os.path import isfile, isdir, join
import unittest
from django.conf import settings
from django.template import Template, Context, VariableDoesNotExist
from django.template.loader import find_template_loader
from django.core.urlresolvers import NoReverseMatch
class TemplateSyntaxTestCase(unittest.TestCase):
def test_templates(self):
"""Test all templates for syntax errors."""
for loader_name in settings.TEMPLATE_LOADERS:
print loader_name
loader = find_template_loader(loader_name)
self._test_dir(loader.get_template_sources(''))
def _test_dir(self, dir, path="/"):
for i in dir:
i = join(path, i)
if isfile(i):
self._test_template(join(path, i))
elif isdir(i):
print "%s:" % i
self._test_dir(listdir(i), i)
def _test_template(self, path):
print path
try:
Template(open(path).read()).render(Context({}))
except (NoReverseMatch, VariableDoesNotExist, KeyError, AttributeError,
ValueError, ) as e:
print e
......@@ -50,6 +50,7 @@ from .views import (
VmGraphView, NodeGraphView, NodeListGraphView,
TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView,
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
OpenSearchDescriptionView,
)
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
......@@ -221,6 +222,8 @@ urlpatterns = patterns(
name="dashboard.views.client-check"),
url(r'^token-login/(?P<token>.*)/$', TokenLogin.as_view(),
name="dashboard.views.token-login"),
url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(),
name="dashboard.views.vm-opensearch"),
)
urlpatterns += patterns(
......
......@@ -19,6 +19,7 @@ from __future__ import unicode_literals, absolute_import
import logging
from django.core.cache import get_cache
from django.core.urlresolvers import reverse
from django.conf import settings
from django.contrib.auth.models import Group
from django.views.generic import TemplateView
......@@ -121,3 +122,15 @@ class HelpView(TemplateView):
ctx.update({"saml": hasattr(settings, "SAML_CONFIG"),
"store": settings.STORE_URL})
return ctx
class OpenSearchDescriptionView(TemplateView):
template_name = "dashboard/vm-opensearch.xml"
content_type = "application/opensearchdescription+xml"
def get_context_data(self, **kwargs):
context = super(OpenSearchDescriptionView, self).get_context_data(
**kwargs)
context['url'] = self.request.build_absolute_uri(
reverse("dashboard.views.vm-list"))
return context
......@@ -115,6 +115,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
'op': {i.op: i for i in ops},
'connect_commands': user.profile.get_connect_commands(instance),
'hide_tutorial': hide_tutorial,
'fav': instance.favourite_set.filter(user=user).exists(),
})
# activity data
......
......@@ -112,10 +112,12 @@ def pull(dir="~/circle/circle"):
@roles('portal')
def update_portal(test=False):
def update_portal(test=False, git=True):
"Update and restart portal+manager"
with _stopped("portal", "manager"):
pull()
if git:
pull()
cleanup()
pip("circle", "~/circle/requirements.txt")
bower()
migrate()
......@@ -125,6 +127,12 @@ def update_portal(test=False):
@roles('portal')
def build_portal():
"Update portal without pulling from git"
return update_portal(False, False)
@roles('portal')
def stop_portal(test=False):
"Stop portal and manager"
_stop_services("portal", "manager")
......@@ -136,10 +144,15 @@ def update_node():
with _stopped("node", "agentdriver", "monitor-client"):
pull("~/vmdriver")
pip("vmdriver", "~/vmdriver/requirements/production.txt")
_cleanup("~/vmdriver")
pull("~/agentdriver")
pip("agentdriver", "~/agentdriver/requirements.txt")
_cleanup("~/agentdriver")
pull("~/monitor-client")
pip("monitor-client", "~/monitor-client/requirements.txt")
_cleanup("~/monitor-client")
@parallel
......@@ -161,6 +174,18 @@ def checkout(vmdriver="master", agent="master"):
run("git checkout %s" % agent)
@roles('portal')
def cleanup():
"Clean pyc files of portal"
_cleanup()
def _cleanup(dir="~/circle/circle"):
"Clean pyc files"
with cd("~/circle/circle"):
run("find -name '*.py[co]' -exec rm -f {} +")
def _stop_services(*services):
"Stop given services (warn only if not running)"
with settings(warn_only=True):
......@@ -189,3 +214,12 @@ def _stopped(*services):
def _workon(name):
return prefix("source ~/.virtualenvs/%s/bin/activate && "
"source ~/.virtualenvs/%s/bin/postactivate" % (name, name))
@roles('portal')
def install_bash_completion_script():
sudo("wget https://raw.githubusercontent.com/marcelor/fabric-bash-"
"autocompletion/48baf5735bafbb2be5be8787d2c2c04a44b6cdb0/fab "
"-O /etc/bash_completion.d/fab")
print("To have bash completion instantly, run\n"
" source /etc/bash_completion.d/fab")
......@@ -15,6 +15,7 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from string import ascii_letters
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _
......@@ -22,7 +23,7 @@ from django.utils.ipv6 import is_valid_ipv6_address
from south.modelsinspector import add_introspection_rules
from django import forms
from netaddr import (IPAddress, IPNetwork, AddrFormatError, ZEROFILL,
EUI, mac_unix)
EUI, mac_unix, AddrConversionError)
import re
......@@ -31,7 +32,6 @@ domain_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]+)$')
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):
......@@ -246,15 +246,60 @@ def val_reverse_domain(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):
"""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)
"""Validate whether the parameter is a valid ipv6 template.
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):
......@@ -284,12 +329,3 @@ def val_mx(value):
domain_re.match(mx[1])):
raise ValidationError(_("Bad MX address format. "
"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 @@
# You should have received a copy of the GNU General Public License along
# 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
from netaddr import IPSet, EUI, IPNetwork
import random
from django.contrib.auth.models import User
from django.db import models
......@@ -28,15 +30,17 @@ from django.utils.translation import ugettext_lazy as _
from firewall.fields import (MACAddressField, val_alfanum, val_reverse_domain,
val_ipv6_template, val_domain, val_ipv4,
val_domain_wildcard,
val_ipv6, val_mx, convert_ipv4_to_ipv6,
val_ipv6, val_mx,
IPNetworkField, IPAddressField)
from django.core.validators import MinValueValidator, MaxValueValidator
import django.conf
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 firewall.tasks.local_tasks import reloadtask
from firewall.tasks.remote_tasks import get_dhcp_clients
from .iptables import IptRule
from acl.models import AclBase
......@@ -360,9 +364,19 @@ class Vlan(AclBase, models.Model):
'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")
ipv6_template = models.TextField(
validators=[val_ipv6_template],
verbose_name=_('ipv6 template'),
default="2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0")
blank=True,
help_text=_('Template for translating IPv4 addresses to IPv6. '
'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'),
help_text=_(
'The address range of the DHCP pool: '
......@@ -377,6 +391,87 @@ class Vlan(AclBase, models.Model):
modified_at = models.DateTimeField(auto_now=True,
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):
return "%s - %s" % ("managed" if self.managed else "unmanaged",
self.name)
......@@ -402,7 +497,7 @@ class Vlan(AclBase, models.Model):
logger.debug("Found unused IPv4 address %s.", ipv4)
ipv6 = 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:
continue
else:
......@@ -411,6 +506,20 @@ class Vlan(AclBase, models.Model):
else:
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):
"""
......@@ -581,8 +690,7 @@ class Host(models.Model):
def save(self, *args, **kwargs):
if not self.id and self.ipv6 == "auto":
self.ipv6 = convert_ipv4_to_ipv6(self.vlan.ipv6_template,
self.ipv4)
self.ipv6 = self.vlan.convert_ipv4_to_ipv6(self.ipv4)
self.full_clean()
super(Host, self).save(*args, **kwargs)
......@@ -838,7 +946,7 @@ class Firewall(models.Model):
return self.name
@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.
Throws Exception if there is no worker on the queue.
......@@ -851,6 +959,20 @@ class Firewall(models.Model):
else:
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 {}
class Domain(models.Model):
name = models.CharField(max_length=40, validators=[val_domain],
......
......@@ -62,5 +62,6 @@ def reload_blacklist(data):
@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
......@@ -78,6 +78,7 @@ class GetNewAddressTestCase(TestCase):
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.0/29',
network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1)
self.vlan.clean()
self.vlan.save()
self.vlan.host_set.all().delete()
for i in [1] + range(3, 6):
......@@ -85,6 +86,9 @@ class GetNewAddressTestCase(TestCase):
ipv4='10.0.0.%d' % i, vlan=self.vlan,
owner=self.u1).save()
def tearDown(self):
self.vlan.delete()
def test_new_addr_w_empty_vlan(self):
self.vlan.host_set.all().delete()
self.vlan.get_new_address()
......@@ -96,12 +100,6 @@ class GetNewAddressTestCase(TestCase):
owner=self.u1).save()
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):
used_v4 = IPSet(self.vlan.host_set.values_list('ipv4', flat=True))
assert self.vlan.get_new_address()['ipv4'] not in used_v4
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -15,13 +15,13 @@
# You should have received a copy of the GNU General Public License along
# 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.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper
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,
Rule, VlanGroup, SwitchPort)
......@@ -122,8 +122,12 @@ class HostForm(ModelForm):
Fieldset(
_('Network'),
'vlan',
'ipv4',
'ipv6',
FieldWithButtons('ipv4', StrictButton(
'<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',
'external_ipv4',
),
......@@ -252,7 +256,9 @@ class VlanForm(ModelForm):
Fieldset(
_('IPv6'),
'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',
),
Fieldset(
......@@ -277,6 +283,9 @@ class VlanForm(ModelForm):
class Meta:
model = Vlan
widgets = {
'ipv6_template': widgets.TextInput,
}
class VlanGroupForm(ModelForm):
......
......@@ -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() {
$("[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 @@
# You should have received a copy of the GNU General Public License along
# 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.html import format_html
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
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):
ipv4 = LinkColumn('network.blacklist', args=[A('pk')])
......@@ -55,9 +71,7 @@ class GroupTable(Table):
class HostTable(Table):
hostname = LinkColumn('network.host', args=[A('pk')])
mac = TemplateColumn(
template_name="network/columns/mac.html"
)
mac = MACColumn()
class Meta:
model = Host
......@@ -109,6 +123,20 @@ class SmallHostTable(Table):
attrs = {'class': 'table table-striped table-condensed'}
fields = ('hostname', 'ipv4')
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):
......
{% 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 @@
</div>
<div class="col-sm-6">
<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>
</div>
{% render_table host_list %}
<div class="page-header">
<h3>{% trans "Unregistered hosts" %}</h3>
</div>
{% render_table dhcp_list %}
<div class="page-header">
<h3>{% trans "Manage access" %}</h3>
</div>
......
......@@ -15,11 +15,13 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from netaddr import IPNetwork
from django.views.generic import (TemplateView, UpdateView, DeleteView,
CreateView)
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import render, redirect
from django.http import HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.http import HttpResponse, Http404
from django_tables2 import SingleTableView
......@@ -29,7 +31,7 @@ from vm.models import Interface
from .tables import (HostTable, VlanTable, SmallHostTable, DomainTable,
GroupTable, RecordTable, BlacklistItemTable, RuleTable,
VlanGroupTable, SmallRuleTable, SmallGroupRuleTable,
SmallRecordTable, SwitchPortTable)
SmallRecordTable, SwitchPortTable, SmallDhcpTable, )
from .forms import (HostForm, VlanForm, DomainForm, GroupForm, RecordForm,
BlacklistItemForm, RuleForm, VlanGroupForm, SwitchPortForm)
......@@ -41,10 +43,36 @@ from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
# from django.db.models import Q
from operator import itemgetter
from itertools import chain
import json
from dashboard.views import AclUpdateView
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):
def get_initial(self):
......@@ -310,6 +338,25 @@ class GroupDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
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):
model = Host
table_class = HostTable
......@@ -331,15 +378,15 @@ class HostList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
return data
class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
class HostDetail(HostMagicMixin, LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView):
model = Host
template_name = "network/host-edit.html"
form_class = HostForm
success_message = _(u'Successfully modified host %(hostname)s!')
def get(self, request, *args, **kwargs):
if request.is_ajax():
def _get_ajax(self, *args, **kwargs):
if "vlan" not in self.request.GET:
host = Host.objects.get(pk=kwargs['pk'])
host = {
'hostname': host.hostname,
......@@ -347,11 +394,13 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
'ipv6': str(host.ipv6),
'fqdn': host.get_fqdn()
}
return HttpResponse(json.dumps(host),
content_type="application/json")
return host
else:
self.object = self.get_object()
return super(HostDetail, self).get(request, *args, **kwargs)
return super(HostDetail, self)._get_ajax(*args, **kwargs)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super(HostDetail, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
......@@ -402,13 +451,29 @@ class HostDetail(LoginRequiredMixin, SuperuserRequiredMixin,
return reverse_lazy('network.host', kwargs=self.kwargs)
class HostCreate(LoginRequiredMixin, SuperuserRequiredMixin,
class HostCreate(HostMagicMixin, LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Host
template_name = "network/host-create.html"
form_class = HostForm
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):
model = Host
......@@ -644,7 +709,21 @@ class VlanAclUpdateView(AclUpdateView):
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):
model = Vlan
template_name = "network/vlan-edit.html"
......@@ -655,12 +734,8 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
def get_context_data(self, **kwargs):
context = super(VlanDetail, self).get_context_data(**kwargs)
q = Host.objects.filter(interface__in=Interface.objects.filter(
vlan=self.object
))
context['host_list'] = SmallHostTable(q)
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['acl'] = AclUpdateView.get_acl_data(
self.object, self.request.user, 'network.vlan-acl')
......@@ -670,7 +745,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
success_url = reverse_lazy('network.vlan_list')
class VlanCreate(LoginRequiredMixin, SuperuserRequiredMixin,
class VlanCreate(VlanMagicMixin, LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, InitialOwnerMixin, CreateView):
model = Vlan
template_name = "network/vlan-create.html"
......
......@@ -769,8 +769,13 @@ class SaveAsTemplateOperation(InstanceOperation):
tmpl = InstanceTemplate(**params)
tmpl.full_clean() # Avoiding database errors.
tmpl.save()
# Copy traits from the VM instance
tmpl.req_traits.add(*self.instance.req_traits.all())
if clone:
tmpl.clone_acl(self.instance.template)
# Add permission for the original owner of the template
tmpl.set_level(self.instance.template.owner, 'owner')
tmpl.set_level(user, 'owner')
try:
tmpl.disks.add(*self.disks)
# create interface templates
......
......@@ -177,6 +177,7 @@ class InterfaceTestCase(TestCase):
d.save()
v = Vlan(vid=55, network4='127.0.0.1/8',
network6='2001::1/32', domain=d)
v.clean()
v.save()
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