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
01f0294e
authored
Jul 14, 2016
by
Czémán Arnold
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into issue_439
parents
d989daf2
3cff165b
Show whitespace changes
Inline
Side-by-side
Showing
35 changed files
with
410 additions
and
74 deletions
+410
-74
circle/bower.json
+1
-1
circle/common/models.py
+8
-0
circle/dashboard/forms.py
+1
-1
circle/dashboard/migrations/0004_profile_desktop_notifications.py
+19
-0
circle/dashboard/models.py
+4
-0
circle/dashboard/static/dashboard/activity.js
+36
-2
circle/dashboard/static/dashboard/dashboard.js
+8
-0
circle/dashboard/static/dashboard/dashboard.less
+13
-2
circle/dashboard/templates/dashboard/_display-name.html
+0
-1
circle/dashboard/templates/dashboard/_manage_access.html
+1
-1
circle/dashboard/templates/dashboard/_vm-renew.html
+1
-1
circle/dashboard/templates/dashboard/base.html
+3
-0
circle/dashboard/templates/dashboard/node-detail.html
+5
-0
circle/dashboard/templates/dashboard/node-detail/_activity-timeline.html
+5
-3
circle/dashboard/templates/dashboard/nodeactivity_detail.html
+83
-0
circle/dashboard/templates/dashboard/vm-detail.html
+4
-4
circle/dashboard/templates/dashboard/vm-detail/home.html
+2
-2
circle/dashboard/tests/test_views.py
+86
-27
circle/dashboard/urls.py
+3
-1
circle/dashboard/views/node.py
+40
-3
circle/dashboard/views/user.py
+2
-0
circle/dashboard/views/vm.py
+13
-0
circle/firewall/models.py
+5
-1
circle/firewall/tests/test_firewall.py
+14
-5
circle/request/forms.py
+4
-1
circle/request/tables.py
+2
-1
circle/request/templates/request/detail.html
+3
-0
circle/request/views.py
+6
-0
circle/vm/models/activity.py
+7
-8
circle/vm/models/instance.py
+9
-4
circle/vm/models/node.py
+2
-0
circle/vm/operations.py
+3
-1
circle/vm/tasks/vm_tasks.py
+1
-1
miscellaneous/portal-uwsgi.service
+13
-0
requirements/base.txt
+3
-3
No files found.
circle/bower.json
View file @
01f0294e
...
...
@@ -14,7 +14,7 @@
"bootstrap"
:
"~3.2.0"
,
"fontawesome"
:
"~4.3.0"
,
"jquery"
:
"~2.1.1"
,
"no-vnc"
:
"
*
"
,
"no-vnc"
:
"
0.5.1
"
,
"jquery-knob"
:
"~1.2.9"
,
"jquery-simple-slider"
:
"https://github.com/BME-IK/jquery-simple-slider.git"
,
"bootbox"
:
"~4.3.0"
,
...
...
circle/common/models.py
View file @
01f0294e
...
...
@@ -232,6 +232,14 @@ class ActivityModel(TimeStampedModel):
else
:
return
code
def
get_status_id
(
self
):
if
self
.
succeeded
is
None
:
return
'wait'
elif
self
.
succeeded
:
return
'success'
else
:
return
'failed'
@celery.task
()
def
compute_cached
(
method
,
instance
,
memcached_seconds
,
...
...
circle/dashboard/forms.py
View file @
01f0294e
...
...
@@ -1223,7 +1223,7 @@ class MyProfileForm(forms.ModelForm):
class
Meta
:
fields
=
(
'preferred_language'
,
'email_notifications'
,
'use_gravatar'
,
)
'
desktop_notifications'
,
'
use_gravatar'
,
)
model
=
Profile
@property
...
...
circle/dashboard/migrations/0004_profile_desktop_notifications.py
0 → 100644
View file @
01f0294e
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
models
,
migrations
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'dashboard'
,
'0003_message'
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
'profile'
,
name
=
'desktop_notifications'
,
field
=
models
.
BooleanField
(
default
=
False
,
help_text
=
'Whether user wants to get desktop notification when an activity has finished and the window is not in focus.'
,
verbose_name
=
'Desktop notifications'
),
),
]
circle/dashboard/models.py
View file @
01f0294e
...
...
@@ -184,6 +184,10 @@ class Profile(Model):
email_notifications
=
BooleanField
(
verbose_name
=
_
(
"Email notifications"
),
default
=
True
,
help_text
=
_
(
'Whether user wants to get digested email notifications.'
))
desktop_notifications
=
BooleanField
(
verbose_name
=
_
(
"Desktop notifications"
),
default
=
False
,
help_text
=
_
(
'Whether user wants to get desktop notification when an '
'activity has finished and the window is not in focus.'
))
smb_password
=
CharField
(
max_length
=
20
,
verbose_name
=
_
(
'Samba password'
),
...
...
circle/dashboard/static/dashboard/activity.js
View file @
01f0294e
...
...
@@ -169,6 +169,9 @@ $(function() {
);
}
else
{
in_progress
=
false
;
if
(
document
.
hasFocus
()
===
false
&&
userWantNotifications
()){
sendNotification
(
generateMessageFromLastActivity
());
}
if
(
reload_vm_detail
)
location
.
reload
();
if
(
runs
>
1
)
addConnectText
();
}
...
...
@@ -181,18 +184,49 @@ $(function() {
}
});
// Notification init
$
(
function
(){
if
(
userWantNotifications
())
Notification
.
requestPermission
();
});
function
generateMessageFromLastActivity
(){
var
ac
=
$
(
"div.activity"
).
first
();
var
error
=
ac
.
children
(
".timeline-icon-failed"
).
length
;
var
sign
=
(
error
===
1
)
?
"❌ "
:
"✓ "
;
var
msg
=
ac
.
children
(
"strong"
).
text
().
replace
(
/
\s
+/g
,
" "
);
return
sign
+
msg
;
}
function
sendNotification
(
message
)
{
var
options
=
{
icon
:
"/static/dashboard/img/favicon.png"
};
if
(
Notification
.
permission
===
"granted"
)
{
var
notification
=
new
Notification
(
message
,
options
);
}
else
if
(
Notification
.
permission
!==
"denied"
)
{
Notification
.
requestPermission
(
function
(
permission
)
{
if
(
permission
===
"granted"
)
{
var
notification
=
new
Notification
(
message
,
options
);
}
});
}
}
function
userWantNotifications
(){
var
dn
=
$
(
"#user-options"
).
data
(
"desktop_notifications"
);
return
dn
===
"True"
;
}
function
addConnectText
()
{
var
activities
=
$
(
".timeline .activity"
);
if
(
activities
.
length
>
1
)
{
if
(
activities
.
eq
(
0
).
data
(
"activity-code"
)
==
"vm.Instance.wake_up"
||
activities
.
eq
(
0
).
data
(
"activity-code"
)
==
"vm.Instance.agent"
)
{
$
(
"#vm-detail-successful
l
-boot"
).
slideDown
(
500
);
$
(
"#vm-detail-successful-boot"
).
slideDown
(
500
);
}
}
}
String
.
prototype
.
hashCode
=
function
()
{
var
hash
=
0
,
i
,
chr
,
len
;
if
(
this
.
length
===
0
)
return
hash
;
...
...
circle/dashboard/static/dashboard/dashboard.js
View file @
01f0294e
...
...
@@ -557,3 +557,11 @@ $(function () {
"alert-"
+
$
(
this
).
val
());
});
});
/* select all in template list */
$
(
function
()
{
$
(
"#manage-access-select-all"
).
click
(
function
(
e
)
{
var
inputs
=
$
(
this
).
closest
(
"table"
).
find
(
'input[type="checkbox"]'
);
inputs
.
prop
(
"checked"
,
!
inputs
.
prop
(
"checked"
));
});
});
circle/dashboard/static/dashboard/dashboard.less
View file @
01f0294e
...
...
@@ -284,7 +284,7 @@ a.hover-black {
}
.hover-black:hover {
color: black
/*#d9534f*/;
color: black
; /*#d9534f*/
text-decoration: none;
}
...
...
@@ -1285,9 +1285,16 @@ textarea[name="new_members"] {
}
}
#vm-detail-successful
l
-boot {
#vm-detail-successful-boot {
margin-bottom: 20px;
display: none;
.label {
width: 100%;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
#vm-detail-access-help {
...
...
@@ -1523,3 +1530,7 @@ textarea[name="new_members"] {
text-align: center;
width: 100%;
}
#manage-access-select-all {
cursor: pointer;
}
circle/dashboard/templates/dashboard/_display-name.html
View file @
01f0294e
...
...
@@ -14,5 +14,4 @@
({% trans "username" %}: {{ user.username }})
{% endif %}
{% endif %}
{% endif %}
circle/dashboard/templates/dashboard/_manage_access.html
View file @
01f0294e
...
...
@@ -6,7 +6,7 @@
<th></th>
<th>
{% trans "Who" %}
</th>
<th>
{% trans "What" %}
</th>
<th><i
class=
"fa fa-times"
></i></th>
<th><i
id=
"manage-access-select-all"
class=
"fa fa-times"
></i></th>
</tr>
</thead>
<tbody>
...
...
circle/dashboard/templates/dashboard/_vm-renew.html
View file @
01f0294e
...
...
@@ -8,7 +8,7 @@
<a
class=
"btn btn-default"
href=
"{{object.get_absolute_url}}"
data-dismiss=
"modal"
>
{% trans "Cancel" %}
</a>
{% if lease_types and not request.token_user %}
{% if
object.active and
lease_types and not request.token_user %}
<a
class=
"btn btn-primary"
id=
"vm-renew-request-lease-button"
href=
"{% url "
request
.
views
.
request-lease
"
vm_pk=
object.pk
%}"
>
<i
class=
"fa fa-forward"
></i>
...
...
circle/dashboard/templates/dashboard/base.html
View file @
01f0294e
...
...
@@ -12,6 +12,9 @@
{% block navbar %}
{% if request.user.is_authenticated and request.user.pk and not request.token_user %}
<span
id=
"user-options"
data-desktop_notifications=
"{{ request.user.profile.desktop_notifications }}"
><span>
<ul
class=
"nav navbar-nav navbar-right"
id=
"dashboard-menu"
>
{% if request.user.is_superuser %}
{% if ADMIN_ENABLED %}
...
...
circle/dashboard/templates/dashboard/node-detail.html
View file @
01f0294e
...
...
@@ -56,6 +56,11 @@
<span
class=
"label label-warning"
>
{% trans "Offline" %}
</span>
{% endif %}
</div>
<div>
{% for k, v in queues.iteritems %}
<span
class=
"label label-{% if v %}success{% else %}danger{% endif %}"
>
{{ k }}
</span>
{% endfor %}
</div>
</div>
<div
class=
"col-md-10"
id=
"node-detail-pane"
>
<div
class=
"panel panel-default"
id=
"node-detail-panel"
>
...
...
circle/dashboard/templates/dashboard/node-detail/_activity-timeline.html
View file @
01f0294e
...
...
@@ -5,10 +5,11 @@
{% for a in activities %}
<div
class=
"activity"
data-activity-id=
"{{ a.pk }}"
>
<span
class=
"timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"
>
<i
class=
"fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-
plus
{% endif %}"
></i>
<i
class=
"fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-
{{a.icon}}
{% endif %}"
></i>
</span>
<strong
title=
"{{ a.result.get_admin_text }}"
>
{{ a.readable_name.get_admin_text|capfirst }}
<a
href=
"{{ a.get_absolute_url }}"
>
{{ a.readable_name.get_admin_text|capfirst }}
</a>
</strong>
<span
title=
"{{ a.started }}"
>
{{ a.started|arrowfilter:LANGUAGE_CODE }}
</span>
{% if a.user %}, {{ a.user }}{% endif %}
...
...
@@ -19,7 +20,8 @@
<div
data-activity-id=
"{{ s.pk }}"
class=
"sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"
>
<span
title=
"{{ s.result.get_admin_text }}"
>
{{ s.readable_name|get_text:user }}
<a
href=
"{{ s.get_absolute_url }}"
>
{{ s.readable_name|get_text:user }}
</a>
</span>
–
{% if s.finished %}
...
...
circle/dashboard/templates/dashboard/nodeactivity_detail.html
0 → 100644
View file @
01f0294e
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load hro %}
{% block content %}
<div
class=
"body-content"
>
<div
class=
"page-header"
>
<h1>
<div
class=
"pull-right"
id=
"vm-activity-state"
>
<span
class=
"label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}danger{% endif %}{% endif %}"
>
<span>
{{ object.get_status_id|upper }}
</span>
</span>
</div>
<i
class=
"fa fa-{{icon}}"
></i>
{{ object.node.name }}: {{object.readable_name|get_text:user}}
</h1>
</div>
<div
class=
"row"
>
<div
class=
"col-md-6"
id=
"vm-info-pane"
>
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
<div
class=
"col-md-6"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-body"
>
<dl>
<dt>
{% trans "activity code" %}
</dt>
<dd>
{{object.activity_code}}
</dd>
<dt>
{% trans "node" %}
</dt>
<dd><a
href=
"{{object.node.get_absolute_url}}"
>
{{object.node}}
</a></dd>
<dt>
{% trans "time" %}
</dt>
<dd>
{{object.started|default:'n/a'}} → {{object.finished|default:'n/a'}}
</dd>
<dt>
{% trans "user" %}
</dt>
<dd>
<a
href=
"{{ object.user.profile.get_absolute_url }}"
>
{{object.user|default:'(system)'}}
</a></dd>
<dt>
{% trans "type" %}
</dt>
<dd>
{% if object.parent %}
{% blocktrans with url=object.parent.get_absolute_url name=object.parent %}
subactivity of
<a
href=
"{{url}}"
>
{{name}}
</a>
{% endblocktrans %}
{% else %}{% trans "top level activity" %}{% endif %}
</dd>
<dt>
{% trans "task uuid" %}
</dt>
<dd>
{{ object.task_uuid|default:'n/a' }}
</dd>
<dt>
{% trans "status" %}
</dt>
<dd
id=
"activity_status"
>
{{ object.get_status_id }}
</dd>
<dt>
{% trans "result" %}
</dt>
<dd><textarea
class=
"form-control"
id=
"activity_result_text"
>
{{object.result|get_text:user}}
</textarea></dd>
<dt>
{% trans "subactivities" %}
</dt>
{% for s in object.children.all %}
<dd>
<span
{%
if
s
.
result
%}
title=
"{{ s.result|get_text:user }}"
{%
endif
%}
>
<a
href=
"{{ s.get_absolute_url }}"
>
{{ s.readable_name|get_text:user|capfirst }}
</a></span>
–
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
<i
class=
"fa fa-refresh fa-spin"
class=
"sub-activity-loading-icon"
></i>
{% endif %}
{% if s.has_failed %}
<div
class=
"label label-danger"
>
{% trans "failed" %}
</div>
{% endif %}
</dd>
{% empty %}
<dd>
{% trans "none" %}
</dd>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
circle/dashboard/templates/dashboard/vm-detail.html
View file @
01f0294e
...
...
@@ -132,7 +132,7 @@
<dd>
<div
class=
"input-group"
>
<input
type=
"text"
id=
"vm-details-pw-input"
class=
"form-control input-sm input-tags"
value=
"{{ instance.pw }}"
spellcheck=
"false"
/>
value=
"{{ instance.pw }}"
spellcheck=
"false"
autocomplete=
"new-password"
/>
<span
class=
"input-group-addon input-tags"
id=
"vm-details-pw-show"
title=
"{% trans "
Show
password
"
%}"
data-container=
"body"
>
<i
class=
"fa fa-eye"
id=
"vm-details-pw-eye"
></i>
...
...
@@ -192,11 +192,11 @@
{% endif %}
</div>
<div
class=
"col-md-8"
id=
"vm-detail-pane"
>
<div
class=
"big"
id=
"vm-detail-successful
l
-boot"
>
<
span
class=
"label label-info"
data-status=
"{{ instance.status }}"
>
<div
class=
"big"
id=
"vm-detail-successful-boot"
>
<
div
class=
"label label-info"
data-status=
"{{ instance.status }}"
>
<i
class=
"fa fa-check"
></i>
{% trans "The virtual machine successfully started, you can connect now." %}
</
span
>
</
div
>
</div>
<div
class=
"panel panel-default"
id=
"vm-detail-panel"
>
<ul
class=
"nav nav-pills panel-heading"
>
...
...
circle/dashboard/templates/dashboard/vm-detail/home.html
View file @
01f0294e
...
...
@@ -59,8 +59,8 @@
{% if instance.is_expiring %}
<i
class=
"fa fa-warning-sign text-danger"
></i>
{% endif %}
<span
id=
"vm-details-renew-op"
>
{% with op=op.renew %}{% if op %}
<a
href=
"{{op.get_url}}"
class=
"btn btn-
{{op.effect}} btn-xs
operation operation-{{op.op}
}"
>
<a
href=
"{{op.get_url}}"
class=
"btn btn-
xs operation operation-{{ op.op }}
{% if op.disabled %}btn-default disabled{% else %}btn-{{op.effect}}{% endif %
}"
>
<i
class=
"fa fa-{{op.icon}}"
></i>
{{op.name}}
</a>
...
...
circle/dashboard/tests/test_views.py
View file @
01f0294e
...
...
@@ -270,33 +270,6 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase):
self
.
assertEqual
(
InstanceTemplate
.
objects
.
get
(
id
=
1
)
.
raw_data
,
"<devices></devices>"
)
def
test_permitted_lease_delete_w_template_using_it
(
self
):
c
=
Client
()
self
.
login
(
c
,
'superuser'
)
leases
=
Lease
.
objects
.
count
()
response
=
c
.
post
(
"/dashboard/lease/delete/1/"
)
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
leases
,
Lease
.
objects
.
count
())
def
test_permitted_lease_delete_w_template_not_using_it
(
self
):
c
=
Client
()
self
.
login
(
c
,
'superuser'
)
lease
=
Lease
.
objects
.
create
(
name
=
"yay"
)
leases
=
Lease
.
objects
.
count
()
response
=
c
.
post
(
"/dashboard/lease/delete/
%
d/"
%
lease
.
pk
)
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertEqual
(
leases
-
1
,
Lease
.
objects
.
count
())
def
test_unpermitted_lease_delete
(
self
):
c
=
Client
()
self
.
login
(
c
,
'user1'
)
leases
=
Lease
.
objects
.
count
()
response
=
c
.
post
(
"/dashboard/lease/delete/1/"
)
# redirect to the login page
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
leases
,
Lease
.
objects
.
count
())
def
test_notification_read
(
self
):
c
=
Client
()
self
.
login
(
c
,
"user1"
)
...
...
@@ -615,6 +588,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
node
=
Node
.
objects
.
get
(
pk
=
1
)
trait
,
created
=
Trait
.
objects
.
get_or_create
(
name
=
'testtrait'
)
node
.
traits
.
add
(
trait
)
self
.
patcher
=
patch
(
"vm.tasks.vm_tasks.get_queues"
,
return_value
=
{
'x'
:
[{
'name'
:
"devenv.vm.fast"
}],
'y'
:
[{
'name'
:
"devenv.vm.slow"
}],
'z'
:
[{
'name'
:
"devenv.net.fast"
}],
})
self
.
patcher
.
start
()
def
tearDown
(
self
):
super
(
NodeDetailTest
,
self
)
.
tearDown
()
...
...
@@ -622,6 +601,7 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
self
.
u2
.
delete
()
self
.
us
.
delete
()
self
.
g1
.
delete
()
self
.
patcher
.
stop
()
def
test_404_superuser_node_page
(
self
):
c
=
Client
()
...
...
@@ -629,6 +609,12 @@ class NodeDetailTest(LoginMixin, MockCeleryMixin, TestCase):
response
=
c
.
get
(
'/dashboard/node/25555/'
)
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_200_superuser_node_page
(
self
):
c
=
Client
()
self
.
login
(
c
,
'superuser'
)
response
=
c
.
get
(
'/dashboard/node/1/'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_302_user_node_page
(
self
):
c
=
Client
()
self
.
login
(
c
,
'user1'
)
...
...
@@ -1758,3 +1744,76 @@ class SshKeyTest(LoginMixin, TestCase):
resp
=
c
.
post
(
"/dashboard/sshkey/delete/1/"
)
self
.
assertEqual
(
403
,
resp
.
status_code
)
class
LeaseDetailTest
(
LoginMixin
,
TestCase
):
fixtures
=
[
'test-vm-fixture.json'
,
]
def
setUp
(
self
):
self
.
u1
=
User
.
objects
.
create
(
username
=
'user1'
)
self
.
u1
.
set_password
(
'password'
)
self
.
u1
.
save
()
self
.
u2
=
User
.
objects
.
create
(
username
=
'user2'
,
is_staff
=
True
)
self
.
u2
.
set_password
(
'password'
)
self
.
u2
.
save
()
self
.
us
=
User
.
objects
.
create
(
username
=
'superuser'
,
is_superuser
=
True
)
self
.
us
.
set_password
(
'password'
)
self
.
us
.
save
()
def
tearDown
(
self
):
super
(
LeaseDetailTest
,
self
)
.
tearDown
()
self
.
u1
.
delete
()
self
.
u2
.
delete
()
self
.
us
.
delete
()
def
test_anon_view
(
self
):
c
=
Client
()
response
=
c
.
get
(
"/dashboard/lease/1/"
)
self
.
assertEqual
(
response
.
status_code
,
302
)
def
test_unpermitted_view
(
self
):
c
=
Client
()
self
.
login
(
c
,
'user1'
)
response
=
c
.
get
(
"/dashboard/lease/1/"
)
self
.
assertEqual
(
response
.
status_code
,
302
)
def
test_operator_view
(
self
):
c
=
Client
()
self
.
login
(
c
,
'user2'
)
lease
=
Lease
.
objects
.
get
()
lease
.
set_level
(
self
.
u2
,
"owner"
)
response
=
c
.
get
(
"/dashboard/lease/1/"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_superuser_view
(
self
):
c
=
Client
()
self
.
login
(
c
,
'superuser'
)
response
=
c
.
get
(
"/dashboard/lease/1/"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
def
test_permitted_lease_delete_w_template_using_it
(
self
):
c
=
Client
()
self
.
login
(
c
,
'superuser'
)
leases
=
Lease
.
objects
.
count
()
response
=
c
.
post
(
"/dashboard/lease/delete/1/"
)
self
.
assertEqual
(
response
.
status_code
,
400
)
self
.
assertEqual
(
leases
,
Lease
.
objects
.
count
())
def
test_permitted_lease_delete_w_template_not_using_it
(
self
):
c
=
Client
()
self
.
login
(
c
,
'superuser'
)
lease
=
Lease
.
objects
.
create
(
name
=
"yay"
)
leases
=
Lease
.
objects
.
count
()
response
=
c
.
post
(
"/dashboard/lease/delete/
%
d/"
%
lease
.
pk
)
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertEqual
(
leases
-
1
,
Lease
.
objects
.
count
())
def
test_unpermitted_lease_delete
(
self
):
c
=
Client
()
self
.
login
(
c
,
'user1'
)
leases
=
Lease
.
objects
.
count
()
response
=
c
.
post
(
"/dashboard/lease/delete/1/"
)
# redirect to the login page
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
leases
,
Lease
.
objects
.
count
())
circle/dashboard/urls.py
View file @
01f0294e
...
...
@@ -25,7 +25,7 @@ from .views import (
GroupDetailView
,
GroupList
,
IndexView
,
InstanceActivityDetail
,
LeaseCreate
,
LeaseDelete
,
LeaseDetail
,
MyPreferencesView
,
NodeAddTraitView
,
NodeCreate
,
NodeDelete
,
NodeDetailView
,
NodeList
,
NodeDetailView
,
NodeList
,
NodeActivityDetail
,
NotificationView
,
TemplateAclUpdateView
,
TemplateCreate
,
TemplateDelete
,
TemplateDetail
,
TemplateList
,
vm_activity
,
VmCreate
,
VmDetailView
,
...
...
@@ -136,6 +136,8 @@ urlpatterns = patterns(
name
=
'dashboard.views.node-activity-list'
),
url
(
r'^node/create/$'
,
NodeCreate
.
as_view
(),
name
=
'dashboard.views.node-create'
),
url
(
r'^node/activity/(?P<pk>\d+)/$'
,
NodeActivityDetail
.
as_view
(),
name
=
'dashboard.views.node-activity'
),
url
(
r'^favourite/$'
,
FavouriteView
.
as_view
(),
name
=
'dashboard.views.favourite'
),
...
...
circle/dashboard/views/node.py
View file @
01f0294e
...
...
@@ -37,6 +37,7 @@ from django_tables2 import SingleTableView
from
firewall.models
import
Host
from
vm.models
import
Node
,
NodeActivity
,
Trait
from
vm.tasks.vm_tasks
import
check_queue
from
..forms
import
TraitForm
,
HostForm
,
NodeForm
from
..tables
import
NodeListTable
...
...
@@ -81,6 +82,20 @@ node_ops = OrderedDict([
])
def
_get_activity_icon
(
act
):
op
=
act
.
get_operation
()
if
op
and
op
.
id
in
node_ops
:
return
node_ops
[
op
.
id
]
.
icon
else
:
return
"cog"
def
_format_activities
(
acts
):
for
i
in
acts
:
i
.
icon
=
_get_activity_icon
(
i
)
return
acts
class
NodeDetailView
(
LoginRequiredMixin
,
GraphMixin
,
DetailView
):
template_name
=
"dashboard/node-detail.html"
...
...
@@ -103,10 +118,17 @@ class NodeDetailView(LoginRequiredMixin,
context
[
'ops'
]
=
get_operations
(
self
.
object
,
self
.
request
.
user
)
context
[
'op'
]
=
{
i
.
op
:
i
for
i
in
context
[
'ops'
]}
context
[
'show_show_all'
]
=
len
(
na
)
>
10
context
[
'activities'
]
=
na
[:
10
]
context
[
'activities'
]
=
_format_activities
(
na
[:
10
])
context
[
'trait_form'
]
=
form
context
[
'graphite_enabled'
]
=
(
settings
.
GRAPHITE_URL
is
not
None
)
node_hostname
=
self
.
object
.
host
.
hostname
context
[
'queues'
]
=
{
'vmcelery.fast'
:
check_queue
(
node_hostname
,
"vm"
,
"fast"
),
'vmcelery.slow'
:
check_queue
(
node_hostname
,
"vm"
,
"slow"
),
'netcelery.fast'
:
check_queue
(
node_hostname
,
"net"
,
"fast"
),
}
return
context
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
...
...
@@ -298,8 +320,8 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
show_all
=
request
.
GET
.
get
(
"show_all"
,
"false"
)
==
"true"
node
=
Node
.
objects
.
get
(
pk
=
pk
)
activities
=
NodeActivity
.
objects
.
filter
(
node
=
node
,
parent
=
None
)
.
order_by
(
'-started'
)
.
select_related
()
activities
=
_format_activities
(
NodeActivity
.
objects
.
filter
(
node
=
node
,
parent
=
None
)
.
order_by
(
'-started'
)
.
select_related
()
)
show_show_all
=
len
(
activities
)
>
10
if
not
show_all
:
...
...
@@ -316,3 +338,18 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
json
.
dumps
(
response
),
content_type
=
"application/json"
)
class
NodeActivityDetail
(
LoginRequiredMixin
,
SuperuserRequiredMixin
,
DetailView
):
model
=
NodeActivity
context_object_name
=
'nodeactivity'
# much simpler to mock object
template_name
=
'dashboard/nodeactivity_detail.html'
def
get_context_data
(
self
,
**
kwargs
):
ctx
=
super
(
NodeActivityDetail
,
self
)
.
get_context_data
(
**
kwargs
)
ctx
[
'activities'
]
=
_format_activities
(
NodeActivity
.
objects
.
filter
(
node
=
self
.
object
.
node
,
parent
=
None
)
.
order_by
(
'-started'
)
.
select_related
())
ctx
[
'icon'
]
=
_get_activity_icon
(
self
.
object
)
return
ctx
circle/dashboard/views/user.py
View file @
01f0294e
...
...
@@ -200,6 +200,8 @@ class MyPreferencesView(UpdateView):
data
=
request
.
POST
)
if
form
.
is_valid
():
form
.
save
()
messages
.
success
(
self
.
request
,
_
(
"Password successfully changed."
))
if
form
.
is_valid
():
return
redirect_response
...
...
circle/dashboard/views/vm.py
View file @
01f0294e
...
...
@@ -105,6 +105,19 @@ class VmDetailView(GraphMixin, CheckedDetailView):
template_name
=
"dashboard/vm-detail.html"
model
=
Instance
def
get
(
self
,
*
args
,
**
kwargs
):
if
self
.
request
.
is_ajax
():
return
JsonResponse
(
self
.
get_json_data
())
else
:
return
super
(
VmDetailView
,
self
)
.
get
(
*
args
,
**
kwargs
)
def
get_json_data
(
self
):
instance
=
self
.
get_object
()
return
{
"status"
:
instance
.
status
,
"host"
:
instance
.
get_connect_host
(),
"port"
:
instance
.
get_connect_port
(),
"password"
:
instance
.
pw
}
def
get_context_data
(
self
,
**
kwargs
):
context
=
super
(
VmDetailView
,
self
)
.
get_context_data
(
**
kwargs
)
instance
=
context
[
'instance'
]
...
...
circle/firewall/models.py
View file @
01f0294e
...
...
@@ -499,7 +499,11 @@ class Vlan(AclBase, models.Model):
def
get_new_address
(
self
):
hosts
=
self
.
host_set
used_v4
=
IPSet
(
hosts
.
values_list
(
'ipv4'
,
flat
=
True
))
used_ext_addrs
=
Host
.
objects
.
filter
(
external_ipv4__isnull
=
False
)
.
values_list
(
'external_ipv4'
,
flat
=
True
)
used_v4
=
IPSet
(
hosts
.
values_list
(
'ipv4'
,
flat
=
True
))
.
union
(
used_ext_addrs
)
.
union
([
self
.
network4
.
ip
])
used_v6
=
IPSet
(
hosts
.
exclude
(
ipv6__isnull
=
True
)
.
values_list
(
'ipv6'
,
flat
=
True
))
...
...
circle/firewall/tests/test_firewall.py
View file @
01f0294e
...
...
@@ -77,13 +77,13 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
d
=
Domain
(
name
=
'example.org'
,
owner
=
self
.
u1
)
d
.
save
()
# /29 = .1-.6 = 6 hosts/subnet + broadcast + network id
self
.
vlan
=
Vlan
(
vid
=
1
,
name
=
'test'
,
network4
=
'10.0.0.
0
/29'
,
self
.
vlan
=
Vlan
(
vid
=
1
,
name
=
'test'
,
network4
=
'10.0.0.
1
/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
):
for
i
in
range
(
3
,
6
):
Host
(
hostname
=
'h-
%
d'
%
i
,
mac
=
'01:02:03:04:05:
%02
d'
%
i
,
ipv4
=
'10.0.0.
%
d'
%
i
,
vlan
=
self
.
vlan
,
owner
=
self
.
u1
)
.
save
()
...
...
@@ -102,6 +102,15 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
owner
=
self
.
u1
)
.
save
()
self
.
assertRaises
(
ValidationError
,
self
.
vlan
.
get_new_address
)
def
test_all_addr_in_use2
(
self
):
Host
(
hostname
=
'h-xd'
,
mac
=
'01:02:03:04:05:06'
,
ipv4
=
'10.0.0.6'
,
vlan
=
self
.
vlan
,
owner
=
self
.
u1
)
.
save
()
Host
(
hostname
=
'h-arni'
,
mac
=
'01:02:03:04:05:02'
,
ipv4
=
'100.0.0.1'
,
vlan
=
self
.
vlan
,
external_ipv4
=
'10.0.0.2'
,
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
...
...
@@ -114,7 +123,7 @@ class HostGetHostnameTestCase(MockCeleryMixin, TestCase):
self
.
d
=
Domain
(
name
=
'example.org'
,
owner
=
self
.
u1
)
self
.
d
.
save
()
Record
.
objects
.
all
()
.
delete
()
self
.
vlan
=
Vlan
(
vid
=
1
,
name
=
'test'
,
network4
=
'10.0.0.
0
/24'
,
self
.
vlan
=
Vlan
(
vid
=
1
,
name
=
'test'
,
network4
=
'10.0.0.
1
/24'
,
network6
=
'2001:738:2001:4031::/80'
,
domain
=
self
.
d
,
owner
=
self
.
u1
,
network_type
=
'portforward'
,
snat_ip
=
'10.1.1.1'
)
...
...
@@ -194,13 +203,13 @@ class ReloadTestCase(MockCeleryMixin, TestCase):
self
.
u1
=
User
.
objects
.
create
(
username
=
'user1'
)
self
.
u1
.
save
()
d
=
Domain
.
objects
.
create
(
name
=
'example.org'
,
owner
=
self
.
u1
)
self
.
vlan
=
Vlan
(
vid
=
1
,
name
=
'test'
,
network4
=
'10.0.0.
0
/29'
,
self
.
vlan
=
Vlan
(
vid
=
1
,
name
=
'test'
,
network4
=
'10.0.0.
1
/29'
,
snat_ip
=
'152.66.243.99'
,
network6
=
'2001:738:2001:4031::/80'
,
domain
=
d
,
owner
=
self
.
u1
,
network_type
=
'portforward'
,
dhcp_pool
=
'manual'
)
self
.
vlan
.
save
()
self
.
vlan2
=
Vlan
(
vid
=
2
,
name
=
'pub'
,
network4
=
'10.1.0.
0
/29'
,
self
.
vlan2
=
Vlan
(
vid
=
2
,
name
=
'pub'
,
network4
=
'10.1.0.
1
/29'
,
network6
=
'2001:738:2001:4032::/80'
,
domain
=
d
,
owner
=
self
.
u1
,
network_type
=
'public'
)
self
.
vlan2
.
save
()
...
...
circle/request/forms.py
View file @
01f0294e
...
...
@@ -73,8 +73,11 @@ class InitialFromFileMixin(object):
)
def
clean_message
(
self
):
def
comp
(
x
):
return
""
.
join
(
x
.
strip
()
.
splitlines
())
message
=
self
.
cleaned_data
[
'message'
]
if
message
.
strip
()
==
self
.
initial
[
'message'
]
.
strip
(
):
if
comp
(
message
)
==
comp
(
self
.
initial
[
'message'
]
):
raise
ValidationError
(
_
(
"Fill in the message."
),
code
=
"invalid"
)
return
message
.
strip
()
...
...
circle/request/tables.py
View file @
01f0294e
...
...
@@ -38,6 +38,7 @@ class RequestTable(Table):
template_name
=
"request/columns/user.html"
,
verbose_name
=
_
(
"User"
),
)
created
=
Column
(
verbose_name
=
_
(
"Date"
))
type
=
TemplateColumn
(
template_name
=
"request/columns/type.html"
,
verbose_name
=
_
(
"Type"
),
...
...
@@ -48,7 +49,7 @@ class RequestTable(Table):
template
=
"django_tables2/with_pagination.html"
attrs
=
{
'class'
:
(
'table table-bordered table-striped table-hover'
),
'id'
:
"request-list-table"
}
fields
=
(
"pk"
,
"status"
,
"type"
,
"user"
,
)
fields
=
(
"pk"
,
"status"
,
"type"
,
"
created"
,
"
user"
,
)
order_by
=
(
"-pk"
,
)
empty_text
=
_
(
"No more requests."
)
per_page
=
10
...
...
circle/request/templates/request/detail.html
View file @
01f0294e
...
...
@@ -38,6 +38,9 @@
<pre>
{{ object.message }}
</pre>
</p>
<hr
/>
<div
class=
"pull-right"
>
<strong>
{% trans "Submitted" %}:
</strong>
{{ object.created }}
</div>
{% if object.type == "lease" %}
<dl>
<dt>
{% trans "VM name" %}
</dt>
...
...
circle/request/views.py
View file @
01f0294e
...
...
@@ -208,6 +208,12 @@ class VmRequestMixin(LoginRequiredMixin, object):
user
=
self
.
request
.
user
if
not
vm
.
has_level
(
user
,
self
.
user_level
):
raise
PermissionDenied
()
if
vm
.
destroyed_at
:
message
=
_
(
"Instance
%(instance)
s has already been destroyed."
)
messages
.
error
(
self
.
request
,
message
%
{
'instance'
:
vm
.
name
})
return
redirect
(
vm
.
get_absolute_url
())
return
super
(
VmRequestMixin
,
self
)
.
dispatch
(
*
args
,
**
kwargs
)
def
get_context_data
(
self
,
**
kwargs
):
...
...
circle/vm/models/activity.py
View file @
01f0294e
...
...
@@ -135,14 +135,6 @@ class InstanceActivity(ActivityModel):
def
get_absolute_url
(
self
):
return
reverse
(
'dashboard.views.vm-activity'
,
args
=
[
self
.
pk
])
def
get_status_id
(
self
):
if
self
.
succeeded
is
None
:
return
'wait'
elif
self
.
succeeded
:
return
'success'
else
:
return
'failed'
def
has_percentage
(
self
):
op
=
self
.
instance
.
get_operation_from_activity_code
(
self
.
activity_code
)
return
(
self
.
task_uuid
and
op
and
op
.
has_percentage
and
...
...
@@ -215,6 +207,13 @@ class NodeActivity(ActivityModel):
app_label
=
'vm'
db_table
=
'vm_nodeactivity'
def
get_operation
(
self
):
return
self
.
node
.
get_operation_from_activity_code
(
self
.
activity_code
)
def
get_absolute_url
(
self
):
return
reverse
(
'dashboard.views.node-activity'
,
args
=
[
self
.
pk
])
def
__unicode__
(
self
):
if
self
.
parent
:
return
'{}({})->{}'
.
format
(
self
.
parent
.
activity_code
,
...
...
circle/vm/models/instance.py
View file @
01f0294e
...
...
@@ -448,12 +448,17 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
if
new_node
is
False
:
# None would be a valid value
new_node
=
self
.
node
# log state change
if
new_node
:
msg
=
ugettext_noop
(
"vm state changed to
%(state)
s on
%(node)
s"
)
else
:
msg
=
ugettext_noop
(
"vm state changed to
%(state)
s"
)
try
:
act
=
InstanceActivity
.
create
(
code_suffix
=
'vm_state_changed'
,
readable_name
=
create_readable
(
ugettext_noop
(
"vm state changed to
%(state)
s on
%(node)
s"
),
state
=
new_state
,
node
=
new_node
),
readable_name
=
create_readable
(
msg
,
state
=
new_state
,
node
=
new_node
),
instance
=
self
)
except
ActivityInProgressError
:
pass
# discard state change if another activity is in progress.
...
...
@@ -676,7 +681,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
with
self
.
activity
(
'notification_about_expiration'
,
readable_name
=
ugettext_noop
(
"notify owner about expiration"
),
on_commit
=
on_commit
):
on_commit
=
on_commit
,
concurrency_check
=
False
):
from
dashboard.views
import
VmRenewView
,
absolute_url
level
=
self
.
get_level_object
(
"owner"
)
for
u
,
ulevel
in
self
.
get_users_with_level
(
level__pk
=
level
.
pk
):
...
...
circle/vm/models/node.py
View file @
01f0294e
...
...
@@ -160,6 +160,8 @@ class Node(OperatedMixin, TimeStampedModel):
"""
try
:
self
.
get_remote_queue_name
(
"vm"
,
"fast"
)
self
.
get_remote_queue_name
(
"vm"
,
"slow"
)
self
.
get_remote_queue_name
(
"net"
,
"fast"
)
except
:
return
False
else
:
...
...
circle/vm/operations.py
View file @
01f0294e
...
...
@@ -861,7 +861,9 @@ class ShutOffOperation(InstanceOperation):
def
_operation
(
self
,
activity
):
# Shutdown networks
with
activity
.
sub_activity
(
'shutdown_net'
):
with
activity
.
sub_activity
(
'shutdown_net'
,
readable_name
=
ugettext_noop
(
"shutdown network"
)):
self
.
instance
.
shutdown_net
()
self
.
instance
.
_delete_vm
(
parent_activity
=
activity
)
...
...
circle/vm/tasks/vm_tasks.py
View file @
01f0294e
...
...
@@ -57,7 +57,7 @@ def get_queues():
inspect
=
celery
.
control
.
inspect
()
inspect
.
timeout
=
0.5
result
=
inspect
.
active_queues
()
logger
.
debug
(
'Queue list of length
%
d cached.'
,
len
(
result
))
logger
.
debug
(
'Queue list of length
%
d cached.'
,
result
and
len
(
result
))
cache
.
set
(
key
,
result
,
10
)
return
result
...
...
miscellaneous/portal-uwsgi.service
0 → 100644
View file @
01f0294e
[Unit]
Description=CIRCLE portal
After=network.target
[Service]
User=cloud
Group=cloud
WorkingDirectory=/home/cloud/circle/circle
ExecStart=/bin/bash -c "source /etc/profile; workon circle; exec /home/cloud/.virtualenvs/circle/bin/uwsgi --chdir=/home/cloud/circle/circle -H /home/cloud/.virtualenvs/circle --socket /tmp/uwsgi.sock --wsgi-file circle/wsgi.py --chmod-socket=666"
Restart=always
[Install]
WantedBy=multi-user.target
requirements/base.txt
View file @
01f0294e
amqp==1.4.6
anyjson==0.3.3
arrow==0.
6
.0
arrow==0.
7
.0
billiard==3.3.0.20
bpython==0.14.1
celery==3.1.18
Django==1.8.2
Django==1.8.
1
2
django-appconf==1.0.1
django-autocomplete-light==2.1.1
django-braces==1.8.0
django-crispy-forms==1.
4
.0
django-crispy-forms==1.
6
.0
django-model-utils==2.2
djangosaml2==0.13.0
django-sizefield==0.7
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment