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.
......@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-10-20 12:09+0200\n"
"POT-Creation-Date: 2014-11-14 13:43+0100\n"
"PO-Revision-Date: 2014-10-20 12:21+0200\n"
"Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n"
......@@ -17,7 +17,7 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Lokalize 1.5\n"
#: dashboard/static/dashboard/dashboard.js:68
#: dashboard/static/dashboard/dashboard.js:83
#: static_collected/all.047675ebf594.js:3443
#: static_collected/all.0aecd87e873a.js:3443
#: static_collected/all.0db607331718.js:3443
......@@ -34,13 +34,13 @@ msgstr ""
#: static_collected/dashboard/dashboard.be8725cd91bf.js:68
#: static_collected/dashboard/dashboard.e740d80401b2.js:68
#: static_collected/dashboard/dashboard.fe0a2f126346.js:68
#: static_collected/dashboard/dashboard.js:68
#: static_collected/dashboard/dashboard.js:83
msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:259
#: dashboard/static/dashboard/dashboard.js:307
#: dashboard/static/dashboard/dashboard.js:317
#: dashboard/static/dashboard/dashboard.js:274
#: dashboard/static/dashboard/dashboard.js:322
#: dashboard/static/dashboard/dashboard.js:332
#: static_collected/all.047675ebf594.js:3633
#: static_collected/all.047675ebf594.js:3681
#: static_collected/all.047675ebf594.js:3691
......@@ -88,9 +88,9 @@ msgstr "Válasszon a folytatáshoz."
#: static_collected/dashboard/dashboard.fe0a2f126346.js:258
#: static_collected/dashboard/dashboard.fe0a2f126346.js:306
#: static_collected/dashboard/dashboard.fe0a2f126346.js:316
#: static_collected/dashboard/dashboard.js:259
#: static_collected/dashboard/dashboard.js:307
#: static_collected/dashboard/dashboard.js:317
#: static_collected/dashboard/dashboard.js:274
#: static_collected/dashboard/dashboard.js:322
#: static_collected/dashboard/dashboard.js:332
msgid "No result"
msgstr "Nincs eredmény"
......@@ -129,10 +129,12 @@ msgid "Unknown error."
msgstr "Ismeretlen hiba."
#: dashboard/static/dashboard/template-list.js:103
#: static_collected/dashboard/template-list.js:103
msgid "Only the owners can delete the selected object."
msgstr "Csak a tulajdonos törölheti a kiválasztott elemet."
#: dashboard/static/dashboard/template-list.js:105
#: static_collected/dashboard/template-list.js:105
msgid "An error occurred. ("
msgstr "Hiba történt. ("
......@@ -206,11 +208,12 @@ msgstr "Jelszó megjelenítése"
#: static_collected/vm-detail.js:6391
#: static_collected/dashboard/vm-tour.1562cc89a659.js:22
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:22
#: static_collected/dashboard/vm-tour.js:22
#: static_collected/dashboard/vm-tour.js:6
msgid "Next"
msgstr "Tovább"
#: dashboard/static/dashboard/vm-tour.js:7
#: static_collected/dashboard/vm-tour.js:7
msgid "Previous"
msgstr "Vissza"
......@@ -226,15 +229,17 @@ msgstr "Vissza"
#: static_collected/vm-detail.js:6395
#: static_collected/dashboard/vm-tour.1562cc89a659.js:26
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:26
#: static_collected/dashboard/vm-tour.js:26
#: static_collected/dashboard/vm-tour.js:8
msgid "End tour"
msgstr "Befejezés"
#: dashboard/static/dashboard/vm-tour.js:9
#: static_collected/dashboard/vm-tour.js:9
msgid "Done"
msgstr "Kész"
#: dashboard/static/dashboard/vm-tour.js:56
#: static_collected/dashboard/vm-tour.js:56
msgid ""
"Welcome to the template tutorial. In this quick tour, we are going to show "
"you how to do the steps described above."
......@@ -254,7 +259,7 @@ msgstr ""
#: static_collected/vm-detail.js:6404
#: static_collected/dashboard/vm-tour.1562cc89a659.js:35
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:35
#: static_collected/dashboard/vm-tour.js:35
#: static_collected/dashboard/vm-tour.js:57
msgid ""
"For the next tour step press the \"Next\" button or the right arrow (or "
"\"Back\" button/left arrow for the previous step)."
......@@ -263,6 +268,7 @@ msgstr ""
"nyílbillentyűket."
#: dashboard/static/dashboard/vm-tour.js:61
#: static_collected/dashboard/vm-tour.js:61
msgid ""
"In this tab you can extend the expiration date of your virtual machine, add "
"tags and modify the name and description."
......@@ -271,24 +277,26 @@ msgstr ""
"vagy módosíthatja a nevét, leírását."
#: dashboard/static/dashboard/vm-tour.js:65
#: static_collected/dashboard/vm-tour.js:65
msgid ""
"Please add a meaningful description to the virtual machine. Changing the "
"name is also recommended, however you can choose a new name when saving the "
"template."
msgstr ""
"Kérjük, adjon meg egy informatív leírást. A név megváltoztatása is "
"ajánlott, azonban a mentéskor is van a sablon nevének "
"megválasztására."
"Kérjük, adjon meg egy informatív leírást. A név megváltoztatása is ajánlott, "
"azonban a mentéskor is van a sablon nevének megválasztására."
#: dashboard/static/dashboard/vm-tour.js:69
#: static_collected/dashboard/vm-tour.js:69
msgid ""
"You can change the lease to extend the expiration date. This will be the "
"lease of the new template."
msgstr ""
"Megváltoztathatja a bérleti módot is a lejárat bővítéséhez. Az gép "
"bérleti módját örökli majd a sablon is."
"Megváltoztathatja a bérleti módot is a lejárat bővítéséhez. Az gép bérleti "
"módját örökli majd a sablon is."
#: dashboard/static/dashboard/vm-tour.js:73
#: static_collected/dashboard/vm-tour.js:73
msgid ""
"On the resources tab you can edit the CPU/RAM options and add/remove disks "
"if you have required permissions."
......@@ -308,7 +316,7 @@ msgstr ""
#: static_collected/vm-detail.js:6438
#: static_collected/dashboard/vm-tour.1562cc89a659.js:69
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:69
#: static_collected/dashboard/vm-tour.js:69
#: static_collected/dashboard/vm-tour.js:81
msgid "CPU priority"
msgstr "CPU prioritás"
......@@ -324,7 +332,7 @@ msgstr "CPU prioritás"
#: static_collected/vm-detail.js:6438
#: static_collected/dashboard/vm-tour.1562cc89a659.js:69
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:69
#: static_collected/dashboard/vm-tour.js:69
#: static_collected/dashboard/vm-tour.js:82
msgid "higher is better"
msgstr "a nagyobb érték a jobb"
......@@ -340,7 +348,7 @@ msgstr "a nagyobb érték a jobb"
#: static_collected/vm-detail.js:6439
#: static_collected/dashboard/vm-tour.1562cc89a659.js:70
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:70
#: static_collected/dashboard/vm-tour.js:70
#: static_collected/dashboard/vm-tour.js:83
msgid "CPU count"
msgstr "CPU-k száma"
......@@ -356,7 +364,7 @@ msgstr "CPU-k száma"
#: static_collected/vm-detail.js:6439
#: static_collected/dashboard/vm-tour.1562cc89a659.js:70
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:70
#: static_collected/dashboard/vm-tour.js:70
#: static_collected/dashboard/vm-tour.js:84
msgid "number of CPU cores."
msgstr "A CPU-magok száma."
......@@ -372,7 +380,7 @@ msgstr "A CPU-magok száma."
#: static_collected/vm-detail.js:6440
#: static_collected/dashboard/vm-tour.1562cc89a659.js:71
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:71
#: static_collected/dashboard/vm-tour.js:71
#: static_collected/dashboard/vm-tour.js:85
msgid "RAM amount"
msgstr "RAM mennyiség"
......@@ -388,7 +396,7 @@ msgstr "RAM mennyiség"
#: static_collected/vm-detail.js:6440
#: static_collected/dashboard/vm-tour.1562cc89a659.js:71
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:71
#: static_collected/dashboard/vm-tour.js:71
#: static_collected/dashboard/vm-tour.js:86
msgid "amount of RAM."
msgstr "a memória mennyisége."
......@@ -404,7 +412,7 @@ msgstr "a memória mennyisége."
#: static_collected/vm-detail.js:6451
#: static_collected/dashboard/vm-tour.1562cc89a659.js:82
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:82
#: static_collected/dashboard/vm-tour.js:82
#: static_collected/dashboard/vm-tour.js:96
msgid ""
"You can add empty disks, download new ones and remove existing ones here."
msgstr ""
......@@ -423,7 +431,7 @@ msgstr ""
#: static_collected/vm-detail.js:6462
#: static_collected/dashboard/vm-tour.1562cc89a659.js:93
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:93
#: static_collected/dashboard/vm-tour.js:93
#: static_collected/dashboard/vm-tour.js:105
msgid "You can add new network interfaces or remove existing ones here."
msgstr "Hozzáadhat új hálózati interfészeket, vagy törölheti a meglévőket."
......@@ -439,17 +447,18 @@ msgstr "Hozzáadhat új hálózati interfészeket, vagy törölheti a meglévők
#: static_collected/vm-detail.js:6474
#: static_collected/dashboard/vm-tour.1562cc89a659.js:105
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:105
#: static_collected/dashboard/vm-tour.js:105
#: static_collected/dashboard/vm-tour.js:109
msgid "Deploy the virtual machine."
msgstr "A virtuális gép elindítása."
#: dashboard/static/dashboard/vm-tour.js:113
#: static_collected/dashboard/vm-tour.js:113
msgid ""
"Use the CIRCLE client or the connection string to connect to the virtual "
"machine."
msgstr ""
"Használja a CIRCLE klienst vagy a kapcsolódási adatokat a "
"virtuális géphez való csatlakozáshoz."
"Használja a CIRCLE klienst vagy a kapcsolódási adatokat a virtuális géphez "
"való csatlakozáshoz."
#: dashboard/static/dashboard/vm-tour.js:117
#: static_collected/vm-detail.09737c69abc3.js:5954
......@@ -463,7 +472,7 @@ msgstr ""
#: static_collected/vm-detail.js:6490
#: static_collected/dashboard/vm-tour.1562cc89a659.js:121
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:121
#: static_collected/dashboard/vm-tour.js:121
#: static_collected/dashboard/vm-tour.js:117
msgid ""
"After you have connected to the virtual machine do your modifications then "
"log off."
......@@ -483,7 +492,7 @@ msgstr ""
#: static_collected/vm-detail.js:6498
#: static_collected/dashboard/vm-tour.1562cc89a659.js:129
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:129
#: static_collected/dashboard/vm-tour.js:129
#: static_collected/dashboard/vm-tour.js:121
msgid ""
"Press the \"Save as template\" button and wait until the activity finishes."
msgstr ""
......@@ -491,12 +500,11 @@ msgstr ""
"elkészül."
#: dashboard/static/dashboard/vm-tour.js:125
#: static_collected/dashboard/vm-tour.js:125
msgid ""
"This is the last message, if something is not clear you can do the the tour "
"again."
msgstr ""
"A túra véget ért. Ha valami nem érthető, újrakezdheti az "
"útmutatót."
msgstr "A túra véget ért. Ha valami nem érthető, újrakezdheti az útmutatót."
#: network/static/js/host.js:10 static_collected/all.047675ebf594.js:5239
#: static_collected/all.0aecd87e873a.js:5309
......@@ -617,7 +625,6 @@ msgstr "Biztosan törli ezt az eszközt?"
#: static_collected/vm-detail.js:6389
#: static_collected/dashboard/vm-tour.1562cc89a659.js:20
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:20
#: static_collected/dashboard/vm-tour.js:20
msgid "Prev"
msgstr "Vissza"
......@@ -632,7 +639,6 @@ msgstr "Vissza"
#: static_collected/vm-detail.js:6402
#: static_collected/dashboard/vm-tour.1562cc89a659.js:33
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:33
#: static_collected/dashboard/vm-tour.js:33
msgid "Template Tutorial Tour"
msgstr "Sablon-kalauz"
......@@ -647,7 +653,6 @@ msgstr "Sablon-kalauz"
#: static_collected/vm-detail.js:6403
#: static_collected/dashboard/vm-tour.1562cc89a659.js:34
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:34
#: static_collected/dashboard/vm-tour.js:34
msgid ""
"Welcome to the template tutorial. In this quick tour, we gonna show you how "
"to do the steps described above."
......@@ -665,7 +670,6 @@ msgstr ""
#: static_collected/vm-detail.js:6405
#: static_collected/dashboard/vm-tour.1562cc89a659.js:36
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:36
#: static_collected/dashboard/vm-tour.js:36
msgid ""
"During the tour please don't try the functions because it may lead to "
"graphical glitches, however "
......@@ -682,7 +686,6 @@ msgstr "A túra során még ne próbálja ki a bemutatott funkciókat."
#: static_collected/vm-detail.js:6414
#: static_collected/dashboard/vm-tour.1562cc89a659.js:45
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:45
#: static_collected/dashboard/vm-tour.js:45
msgid "Home tab"
msgstr "Kezdőoldal"
......@@ -697,7 +700,6 @@ msgstr "Kezdőoldal"
#: static_collected/vm-detail.js:6415
#: static_collected/dashboard/vm-tour.1562cc89a659.js:46
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:46
#: static_collected/dashboard/vm-tour.js:46
msgid ""
"In this tab you can tag your virtual machine and modify the name and "
"description."
......@@ -716,7 +718,6 @@ msgstr ""
#: static_collected/vm-detail.js:6424
#: static_collected/dashboard/vm-tour.1562cc89a659.js:55
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:55
#: static_collected/dashboard/vm-tour.js:55
msgid "Resources tab"
msgstr "Erőforrások lap"
......@@ -731,7 +732,6 @@ msgstr "Erőforrások lap"
#: static_collected/vm-detail.js:6427
#: static_collected/dashboard/vm-tour.1562cc89a659.js:58
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:58
#: static_collected/dashboard/vm-tour.js:58
msgid ""
"On the resources tab you can edit the CPU/RAM options and add/remove disks!"
msgstr ""
......@@ -749,7 +749,6 @@ msgstr ""
#: static_collected/vm-detail.js:6437
#: static_collected/dashboard/vm-tour.1562cc89a659.js:68
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:68
#: static_collected/dashboard/vm-tour.js:68
msgid "Resources"
msgstr "Erőforrások"
......@@ -764,7 +763,6 @@ msgstr "Erőforrások"
#: static_collected/vm-detail.js:6450
#: static_collected/dashboard/vm-tour.1562cc89a659.js:81
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:81
#: static_collected/dashboard/vm-tour.js:81
msgid "Disks"
msgstr "Lemezek"
......@@ -779,7 +777,6 @@ msgstr "Lemezek"
#: static_collected/vm-detail.js:6461
#: static_collected/dashboard/vm-tour.1562cc89a659.js:92
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:92
#: static_collected/dashboard/vm-tour.js:92
msgid "Network tab"
msgstr "Hálózat lap"
......@@ -794,7 +791,6 @@ msgstr "Hálózat lap"
#: static_collected/vm-detail.js:6471
#: static_collected/dashboard/vm-tour.1562cc89a659.js:102
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:102
#: static_collected/dashboard/vm-tour.js:102
msgid "Deploy"
msgstr "Indítás"
......@@ -809,7 +805,6 @@ msgstr "Indítás"
#: static_collected/vm-detail.js:6479
#: static_collected/dashboard/vm-tour.1562cc89a659.js:110
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:110
#: static_collected/dashboard/vm-tour.js:110
msgid "Connect"
msgstr "Csatlakozás"
......@@ -824,7 +819,6 @@ msgstr "Csatlakozás"
#: static_collected/vm-detail.js:6482
#: static_collected/dashboard/vm-tour.1562cc89a659.js:113
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:113
#: static_collected/dashboard/vm-tour.js:113
msgid "Use the connection string or connect with your choice of client!"
msgstr "Használja a megadott parancsot, vagy kedvenc kliensét."
......@@ -839,7 +833,6 @@ msgstr "Használja a megadott parancsot, vagy kedvenc kliensét."
#: static_collected/vm-detail.js:6489
#: static_collected/dashboard/vm-tour.1562cc89a659.js:120
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:120
#: static_collected/dashboard/vm-tour.js:120
msgid "Customize the virtual machine"
msgstr "Szabja testre a gépet"
......@@ -854,7 +847,6 @@ msgstr "Szabja testre a gépet"
#: static_collected/vm-detail.js:6495
#: static_collected/dashboard/vm-tour.1562cc89a659.js:126
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:126
#: static_collected/dashboard/vm-tour.js:126
msgid "Save as"
msgstr "Mentés sablonként"
......@@ -869,7 +861,6 @@ msgstr "Mentés sablonként"
#: static_collected/vm-detail.js:6504
#: static_collected/dashboard/vm-tour.1562cc89a659.js:135
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:135
#: static_collected/dashboard/vm-tour.js:135
msgid "Finish"
msgstr "Befejezés"
......@@ -884,7 +875,6 @@ msgstr "Befejezés"
#: static_collected/vm-detail.js:6507
#: static_collected/dashboard/vm-tour.1562cc89a659.js:138
#: static_collected/dashboard/vm-tour.7b4cf596f543.js:138
#: static_collected/dashboard/vm-tour.js:138
msgid ""
"This is the last message, if something is not clear you can do the the tour "
"again!"
......
......@@ -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