Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
CIRCLE
/
cloud
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
94
Merge Requests
10
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
ca137eda
authored
Nov 17, 2014
by
Bach Dániel
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/master' into feature-management-commands
parents
bad4afd4
1ca7edbb
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
421 additions
and
66 deletions
+421
-66
circle/dashboard/static/dashboard/dashboard.js
+3
-3
circle/dashboard/templates/dashboard/base.html
+1
-0
circle/dashboard/templates/dashboard/index.html
+7
-0
circle/dashboard/templates/dashboard/vm-opensearch.xml
+6
-0
circle/dashboard/urls.py
+3
-0
circle/dashboard/views/index.py
+13
-0
circle/firewall/fields.py
+55
-19
circle/firewall/models.py
+133
-11
circle/firewall/tasks/remote_tasks.py
+2
-1
circle/firewall/tests/test_firewall.py
+4
-6
circle/locale/hu/LC_MESSAGES/django.po
+0
-0
circle/locale/hu/LC_MESSAGES/djangojs.po
+0
-0
circle/network/forms.py
+14
-5
circle/network/static/js/network.js
+43
-0
circle/network/tables.py
+32
-4
circle/network/templates/network/columns/host-register.html
+3
-0
circle/network/templates/network/columns/mac.html
+0
-3
circle/network/templates/network/vlan-edit.html
+7
-0
circle/network/views.py
+94
-14
circle/vm/tests/test_models.py
+1
-0
No files found.
circle/dashboard/static/dashboard/dashboard.js
View file @
ca137eda
...
...
@@ -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
()
{
...
...
circle/dashboard/templates/dashboard/base.html
View file @
ca137eda
...
...
@@ -8,6 +8,7 @@
<link
rel=
"stylesheet"
href=
"{{ STATIC_URL }}dashboard/loopj-jquery-simple-slider/css/simple-slider.css"
/>
<link
rel=
"stylesheet"
href=
"{{ STATIC_URL }}dashboard/introjs/introjs.min.css"
>
<link
href=
"{{ STATIC_URL }}dashboard/dashboard.css"
rel=
"stylesheet"
>
{% block extra_link_2 %}{% endblock %}
{% endblock %}
...
...
circle/dashboard/templates/dashboard/index.html
View file @
ca137eda
...
...
@@ -3,6 +3,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"
>
...
...
circle/dashboard/templates/dashboard/vm-opensearch.xml
0 → 100644
View file @
ca137eda
{% 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}&stype=shared"
/>
</OpenSearchDescription>
circle/dashboard/urls.py
View file @
ca137eda
...
...
@@ -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
(
...
...
circle/dashboard/views/index.py
View file @
ca137eda
...
...
@@ -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
circle/firewall/fields.py
View file @
ca137eda
...
...
@@ -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)02
x
%(b)02
x:
%(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
])})
circle/firewall/models.py
View file @
ca137eda
...
...
@@ -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)02
x 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)02
x00::".'
),
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)02
x00::', 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
)
...
...
@@ -835,7 +943,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.
...
...
@@ -848,6 +956,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
],
...
...
circle/firewall/tasks/remote_tasks.py
View file @
ca137eda
...
...
@@ -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
circle/firewall/tests/test_firewall.py
View file @
ca137eda
...
...
@@ -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
...
...
circle/locale/hu/LC_MESSAGES/django.po
View file @
ca137eda
This source diff could not be displayed because it is too large. You can
view the blob
instead.
circle/locale/hu/LC_MESSAGES/djangojs.po
View file @
ca137eda
This diff is collapsed.
Click to expand it.
circle/network/forms.py
View file @
ca137eda
...
...
@@ -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
):
...
...
circle/network/static/js/network.js
View file @
ca137eda
...
...
@@ -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
);
}});
});
});
circle/network/tables.py
View file @
ca137eda
...
...
@@ -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
...
...
@@ -108,6 +122,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
):
...
...
circle/network/templates/network/columns/host-register.html
0 → 100644
View file @
ca137eda
{% 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>
circle/network/templates/network/columns/mac.html
deleted
100644 → 0
View file @
bad4afd4
{% load i18n %}
<span
title=
"{% blocktrans with vendor=record.hw_vendor|default:"
n
/
a
"
%}
Vendor:
{{
vendor
}}{%
endblocktrans
%}"
>
{{ record.mac }}
</span>
circle/network/templates/network/vlan-edit.html
View file @
ca137eda
...
...
@@ -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>
...
...
circle/network/views.py
View file @
ca137eda
...
...
@@ -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
(