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
e11fffa5
authored
Oct 01, 2014
by
Bach Dániel
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/master' into feature-vm-tasks
Conflicts: circle/vm/models/instance.py circle/vm/tests/test_models.py
parents
19cdda94
9f91f3a7
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
251 additions
and
64 deletions
+251
-64
circle/dashboard/forms.py
+12
-0
circle/dashboard/static/dashboard/dashboard.css
+10
-1
circle/dashboard/static/dashboard/dashboard.js
+1
-1
circle/dashboard/templates/dashboard/_disk-list-element.html
+2
-1
circle/dashboard/templates/dashboard/_vm-migrate.html
+3
-3
circle/dashboard/templates/dashboard/vm-detail.html
+4
-1
circle/dashboard/templates/dashboard/vm-detail/_network-port-add.html
+2
-1
circle/dashboard/templates/dashboard/vm-detail/home.html
+41
-10
circle/dashboard/templates/dashboard/vm-detail/network.html
+7
-5
circle/dashboard/templates/dashboard/vm-list.html
+13
-2
circle/dashboard/tests/test_views.py
+8
-8
circle/dashboard/views/group.py
+1
-4
circle/dashboard/views/node.py
+2
-0
circle/dashboard/views/user.py
+19
-0
circle/dashboard/views/vm.py
+34
-10
circle/network/views.py
+1
-1
circle/vm/models/instance.py
+2
-0
circle/vm/models/node.py
+2
-2
circle/vm/operations.py
+66
-9
circle/vm/tests/test_models.py
+2
-3
circle/vm/tests/test_operations.py
+18
-1
requirements/base.txt
+1
-1
No files found.
circle/dashboard/forms.py
View file @
e11fffa5
...
...
@@ -752,6 +752,7 @@ class VmStateChangeForm(forms.Form):
"but don't interrupt any tasks."
))
new_state
=
forms
.
ChoiceField
(
Instance
.
STATUS
,
label
=
_
(
"New status"
))
reset_node
=
forms
.
BooleanField
(
required
=
False
,
label
=
_
(
"Reset node"
))
def
__init__
(
self
,
*
args
,
**
kwargs
):
show_interrupt
=
kwargs
.
pop
(
'show_interrupt'
)
...
...
@@ -769,6 +770,17 @@ class VmStateChangeForm(forms.Form):
return
helper
class
RedeployForm
(
forms
.
Form
):
with_emergency_change_state
=
forms
.
BooleanField
(
required
=
False
,
initial
=
True
,
label
=
_
(
"use emergency state change"
))
@property
def
helper
(
self
):
helper
=
FormHelper
(
self
)
helper
.
form_tag
=
False
return
helper
class
VmCreateDiskForm
(
forms
.
Form
):
name
=
forms
.
CharField
(
max_length
=
100
,
label
=
_
(
"Name"
))
size
=
forms
.
CharField
(
...
...
circle/dashboard/static/dashboard/dashboard.css
View file @
e11fffa5
...
...
@@ -974,6 +974,10 @@ textarea[name="new_members"] {
color
:
orange
;
}
#vm-info-pane
{
margin-bottom
:
20px
;
}
.node-list-table
tbody
>
tr
>
td
,
.node-list-table
thead
>
tr
>
th
{
vertical-align
:
middle
;
}
...
...
@@ -996,10 +1000,15 @@ textarea[name="new_members"] {
max-width
:
100%
;
}
#vm-list-table
tbody
td
:nth-child
(
3
)
{
#vm-list-table
td
.state
,
#vm-list-table
td
.memory
{
white-space
:
nowrap
;
}
#vm-list-table
td
{
vertical-align
:
middle
;
}
.disk-resize-btn
{
margin-right
:
5px
;
}
circle/dashboard/static/dashboard/dashboard.js
View file @
e11fffa5
...
...
@@ -618,7 +618,7 @@ function addModalConfirmation(func, data) {
}
function
clientInstalledAction
(
location
)
{
setCookie
(
'downloaded_client'
,
true
,
365
*
24
*
60
*
60
,
"/"
);
setCookie
(
'downloaded_client'
,
true
,
365
*
24
*
60
*
60
*
1000
,
"/"
);
window
.
location
.
href
=
location
;
$
(
'#confirmation-modal'
).
modal
(
"hide"
);
}
...
...
circle/dashboard/templates/dashboard/_disk-list-element.html
View file @
e11fffa5
...
...
@@ -18,7 +18,8 @@
</a>
{% if op.resize_disk %}
<span
class=
"operation-wrapper"
>
<a
href=
"{{ op.resize_disk.get_url }}?disk={{d.pk}}"
class=
"btn btn-xs btn-warning pull-right operation"
>
<a
href=
"{{ op.resize_disk.get_url }}?disk={{d.pk}}"
class=
"btn btn-xs btn-warning pull-right operation disk-resize-btn"
>
<i
class=
"fa fa-arrows-alt"
></i>
{% trans "Resize" %}
</a>
</span>
...
...
circle/dashboard/templates/dashboard/_vm-migrate.html
View file @
e11fffa5
...
...
@@ -13,7 +13,7 @@ Choose a compute node to migrate {{obj}} to.
{% block formfields %}
<ul
id=
"vm-migrate-node-list"
class=
"list-unstyled"
>
{% with current=object.node.pk
selected=object.select_node.pk
%}
{% with current=object.node.pk %}
{% for n in nodes %}
<li
class=
"panel panel-default"
><div
class=
"panel-body"
>
<label
for=
"migrate-to-{{n.pk}}"
>
...
...
@@ -21,11 +21,11 @@ Choose a compute node to migrate {{obj}} to.
<div
class=
"label label-primary"
><i
class=
"fa {{n.get_status_icon}}"
></i>
{{n.get_status_display}}
</div>
{% if current == n.pk %}
<div
class=
"label label-info"
>
{% trans "current" %}
</div>
{% endif %}
{% if
select
ed == n.pk %}
<div
class=
"label label-success"
>
{% trans "recommended" %}
</div>
{% endif %}
{% if
recommend
ed == n.pk %}
<div
class=
"label label-success"
>
{% trans "recommended" %}
</div>
{% endif %}
</label>
<input
id=
"migrate-to-{{n.pk}}"
type=
"radio"
name=
"node"
value=
"{{ n.pk }}"
style=
"float: right;"
{%
if
current =
=
n
.
pk
%}
disabled=
"disabled"
{%
endif
%}
{%
if
select
ed =
=
n
.
pk
%}
checked=
"checked"
{%
endif
%}
/>
{%
if
recommend
ed =
=
n
.
pk
%}
checked=
"checked"
{%
endif
%}
/>
<span
class=
"vm-migrate-node-property"
>
{% trans "CPU load" %}: {{ n.cpu_usage }}
</span>
<span
class=
"vm-migrate-node-property"
>
{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}
</span>
<div
style=
"clear: both;"
></div>
...
...
circle/dashboard/templates/dashboard/vm-detail.html
View file @
e11fffa5
...
...
@@ -47,7 +47,10 @@
<div
class=
"input-group vm-details-home-name"
>
<input
id=
"vm-details-rename-name"
class=
"form-control input-sm"
name=
"new_name"
type=
"text"
value=
"{{ instance.name }}"
/>
<span
class=
"input-group-btn"
>
<button
type=
"submit"
class=
"btn btn-sm vm-details-rename-submit"
>
{% trans "Rename" %}
</button>
<button
type=
"submit"
class=
"btn btn-sm vm-details-rename-submit
{% if not is_operator %}disabled{% endif %}"
>
{% trans "Rename" %}
</button>
</span>
</div>
</form>
...
...
circle/dashboard/templates/dashboard/vm-detail/_network-port-add.html
View file @
e11fffa5
...
...
@@ -11,7 +11,8 @@
<span
class=
"input-group-addon"
>
/
</span>
<select
class=
"form-control"
name=
"proto"
style=
"width: 70px;"
><option>
tcp
</option><option>
udp
</option></select>
<div
class=
"input-group-btn"
>
<button
type=
"submit"
class=
"btn btn-success btn-sm"
>
{% trans "Add" %}
</button>
<button
type=
"submit"
class=
"btn btn-success btn-sm
{% if not is_operator %}disabled{% endif %}"
>
{% trans "Add" %}
</button>
</div>
</div>
</form>
...
...
circle/dashboard/templates/dashboard/vm-detail/home.html
View file @
e11fffa5
...
...
@@ -6,7 +6,9 @@
<dd><i
class=
"fa fa-{{ os_type_icon }}"
></i>
{{ instance.system }}
</dd>
<dt
style=
"margin-top: 5px;"
>
{% trans "Name" %}:
<a
href=
"#"
class=
"vm-details-home-edit-name-click"
><i
class=
"fa fa-pencil"
></i></a>
{% if is_operator %}
<a
href=
"#"
class=
"vm-details-home-edit-name-click"
><i
class=
"fa fa-pencil"
></i></a>
{% endif %}
</dt>
<dd>
<div
class=
"vm-details-home-edit-name-click"
>
...
...
@@ -18,8 +20,9 @@
<div
class=
"input-group"
>
<input
type=
"text"
name=
"new_name"
value=
"{{ instance.name }}"
class=
"form-control input-sm"
/>
<span
class=
"input-group-btn"
>
<button
type=
"submit"
class=
"btn btn-success btn-sm vm-details-rename-submit"
>
<i
class=
"fa fa-pencil"
></i>
{% trans "Rename" %}
<button
type=
"submit"
class=
"btn btn-success btn-sm vm-details-rename-submit
{% if not is_operator %}disabled{% endif %}"
title=
"{% trans "
Rename
"
%}"
>
<i
class=
"fa fa-pencil"
></i>
</button>
</span>
</div>
...
...
@@ -28,7 +31,9 @@
</dd>
<dt
style=
"margin-top: 5px;"
>
{% trans "Description" %}:
<a
href=
"#"
class=
"vm-details-home-edit-description-click"
><i
class=
"fa fa-pencil"
></i></a>
{% if is_operator %}
<a
href=
"#"
class=
"vm-details-home-edit-description-click"
><i
class=
"fa fa-pencil"
></i></a>
{% endif %}
</dt>
<dd>
{% csrf_token %}
...
...
@@ -38,7 +43,8 @@
<div
id=
"vm-details-home-description"
class=
"js-hidden"
>
<form
method=
"POST"
>
<textarea
name=
"new_description"
class=
"form-control"
>
{{ instance.description }}
</textarea>
<button
type=
"submit"
class=
"btn btn-xs btn-success vm-details-description-submit"
>
<button
type=
"submit"
class=
"btn btn-xs btn-success vm-details-description-submit
{% if not is_operator %}disabled{% endif %}"
>
<i
class=
"fa fa-pencil"
></i>
{% trans "Update" %}
</button>
</form>
...
...
@@ -58,9 +64,17 @@
</h4>
<dl>
<dt>
{% trans "Suspended at:" %}
</dt>
<dd><i
class=
"fa fa-moon-o"
></i>
{{ instance.time_of_suspend|timeuntil }}
</dd>
<dd>
<span
title=
"{{ instance.time_of_suspend }}"
>
<i
class=
"fa fa-moon-o"
></i>
{{ instance.time_of_suspend|timeuntil }}
</span>
</dd>
<dt>
{% trans "Destroyed at:" %}
</dt>
<dd><i
class=
"fa fa-times"
></i>
{{ instance.time_of_delete|timeuntil }}
</dd>
<dd>
<span
title=
"{{ instance.time_of_delete }}"
>
<i
class=
"fa fa-times"
></i>
{{ instance.time_of_delete|timeuntil }}
</span>
</dd>
</dl>
<div
style=
"font-weight: bold;"
>
{% trans "Tags" %}
</div>
...
...
@@ -70,11 +84,13 @@
{% for t in instance.tags.all %}
<div
class=
"label label-primary label-tag"
style=
"display: inline-block"
>
{{ t }}
<a
href=
"#"
class=
"vm-details-remove-tag"
><i
class=
"fa fa-times"
></i></a>
{% if is_operator %}
<a
href=
"#"
class=
"vm-details-remove-tag"
><i
class=
"fa fa-times"
></i></a>
{% endif %}
</div>
{% endfor %}
{% else %}
<small>
{% trans "No tag added
!
" %}
</small>
<small>
{% trans "No tag added
.
" %}
</small>
{% endif %}
</div>
<form
action=
""
method=
"POST"
>
...
...
@@ -85,11 +101,26 @@
<i class="fa fa-question"></i>
</div>-->
<div
class=
"input-group-btn"
>
<input
type=
"submit"
class=
"btn btn-default btn-sm input-tags"
value=
"{% trans "
Add
tag
"
%}"
/>
<input
type=
"submit"
class=
"btn btn-default btn-sm input-tags
{% if not is_operator %}disabled{% endif %}"
value=
"{% trans "
Add
tag
"
%}"
/>
</div>
</div>
</form>
</div>
<!-- id:vm-details-tags -->
{% if request.user.is_superuser %}
<dl>
<dt>
{% trans "Node" %}:
</dt>
<dd>
{% if instance.node %}
<a
href=
"{{ instance.node.get_absolute_url }}"
>
{{ instance.node.name }}
</a>
{% else %}
-
{% endif %}
</dd>
{% endif %}
</dl>
<dl>
<dt>
{% trans "Template" %}:
</dt>
<dd>
...
...
circle/dashboard/templates/dashboard/vm-detail/network.html
View file @
e11fffa5
...
...
@@ -21,11 +21,13 @@
<a
href=
"{{ i.host.get_absolute_url }}"
class=
"btn btn-default btn-xs"
>
{% trans "edit" %}
</a>
{% endif %}
<a
href=
"{% url "
dashboard
.
views
.
interface-delete
"
pk=
i.pk
%}?
next=
{{
request
.
path
}}"
class=
"btn btn-danger btn-xs interface-remove"
data-interface-pk=
"{{ i.pk }}"
>
{% trans "remove" %}
</a>
{% if is_owner %}
<a
href=
"{% url "
dashboard
.
views
.
interface-delete
"
pk=
i.pk
%}?
next=
{{
request
.
path
}}"
class=
"btn btn-danger btn-xs interface-remove"
data-interface-pk=
"{{ i.pk }}"
>
{% trans "remove" %}
</a>
{% endif %}
</h3>
{% if i.host %}
<div
class=
"row"
>
...
...
circle/dashboard/templates/dashboard/vm-list.html
View file @
e11fffa5
...
...
@@ -72,6 +72,10 @@
{% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</th>
<th
data-sort=
"string"
class=
"orderable sortable"
>
{% trans "Memory" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="ram_size" %}
</th>
{% if user.is_superuser %}
<th
data-sort=
"string"
class=
"orderable sortable"
>
{% trans "IP address" as t %}
...
...
@@ -86,7 +90,9 @@
{% for i in object_list %}
<tr
class=
"{% cycle 'odd' 'even' %}"
data-vm-pk=
"{{ i.pk }}"
>
<td
class=
"pk"
><div
id=
"vm-{{i.pk}}"
>
{{i.pk}}
</div>
</td>
<td
class=
"name"
><a
class=
"real-link"
href=
"{% url "
dashboard
.
views
.
detail
"
i
.
pk
%}"
>
{{ i.name }}
</a>
</td>
<td
class=
"name"
><a
class=
"real-link"
href=
"{% url "
dashboard
.
views
.
detail
"
i
.
pk
%}"
>
{{ i.name }}
</a>
</td>
<td
class=
"state"
>
<i
class=
"fa fa-fw
{% if show_acts_in_progress and i.is_in_status_change %}
...
...
@@ -104,7 +110,12 @@
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td>
<td
class=
"lease "
data-sort-value=
"{{ i.lease.name }}"
>
{{ i.lease.name }}
<span
title=
"{{ i.time_of_suspend|timeuntil }} | {{ i.time_of_delete|timeuntil }}"
>
{{ i.lease.name }}
</span>
</td>
<td
class=
"memory "
data-sort-value=
"{{ i.ram_size }}"
>
{{ i.ram_size }} MiB
</td>
{% if user.is_superuser %}
<td
class=
"ip_addr "
data-sort-value=
"{{ i.ipv4 }}"
>
...
...
circle/dashboard/tests/test_views.py
View file @
e11fffa5
...
...
@@ -1210,7 +1210,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp
=
self
.
g1
.
profile
acl_users
=
len
(
gp
.
get_users_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'user3'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_users
,
len
(
gp
.
get_users_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -1221,7 +1221,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp
=
self
.
g1
.
profile
acl_users
=
len
(
gp
.
get_users_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'user3'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_users
,
len
(
gp
.
get_users_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -1232,7 +1232,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self
.
login
(
c
,
'superuser'
)
acl_users
=
len
(
gp
.
get_users_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'user3'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_users
+
1
,
len
(
gp
.
get_users_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -1243,7 +1243,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self
.
login
(
c
,
'user0'
)
acl_users
=
len
(
gp
.
get_users_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'user3'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_users
+
1
,
len
(
gp
.
get_users_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -1253,7 +1253,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp
=
self
.
g1
.
profile
acl_groups
=
len
(
gp
.
get_groups_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'group2'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_groups
,
len
(
gp
.
get_groups_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -1264,7 +1264,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self
.
login
(
c
,
'user3'
)
acl_groups
=
len
(
gp
.
get_groups_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'group2'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_groups
,
len
(
gp
.
get_groups_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -1275,7 +1275,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self
.
login
(
c
,
'superuser'
)
acl_groups
=
len
(
gp
.
get_groups_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'group2'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_groups
+
1
,
len
(
gp
.
get_groups_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
@@ -1286,7 +1286,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self
.
login
(
c
,
'user0'
)
acl_groups
=
len
(
gp
.
get_groups_with_level
())
response
=
c
.
post
(
'/dashboard/group/'
+
str
(
self
.
g1
.
pk
)
+
'/acl/'
,
str
(
gp
.
pk
)
+
'/acl/'
,
{
'name'
:
'group2'
,
'level'
:
'owner'
})
self
.
assertEqual
(
acl_groups
+
1
,
len
(
gp
.
get_groups_with_level
()))
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
circle/dashboard/views/group.py
View file @
e11fffa5
...
...
@@ -180,10 +180,7 @@ class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
class
GroupAclUpdateView
(
AclUpdateView
):
model
=
Group
def
get_object
(
self
):
return
super
(
GroupAclUpdateView
,
self
)
.
get_object
()
.
profile
model
=
GroupProfile
class
GroupList
(
LoginRequiredMixin
,
SingleTableView
):
...
...
circle/dashboard/views/node.py
View file @
e11fffa5
...
...
@@ -68,6 +68,8 @@ node_ops = OrderedDict([
op
=
'passivate'
,
icon
=
'play-circle-o'
,
effect
=
'info'
)),
(
'disable'
,
NodeOperationView
.
factory
(
op
=
'disable'
,
icon
=
'times-circle-o'
,
effect
=
'danger'
)),
(
'reset'
,
NodeOperationView
.
factory
(
op
=
'reset'
,
icon
=
'stethoscope'
,
effect
=
'danger'
)),
(
'flush'
,
NodeOperationView
.
factory
(
op
=
'flush'
,
icon
=
'paint-brush'
,
effect
=
'danger'
)),
])
...
...
circle/dashboard/views/user.py
View file @
e11fffa5
...
...
@@ -285,6 +285,25 @@ class ProfileView(LoginRequiredMixin, DetailView):
slug_field
=
"username"
slug_url_kwarg
=
"username"
def
get
(
self
,
*
args
,
**
kwargs
):
user
=
self
.
request
.
user
target
=
self
.
get_object
()
# get the list of groups where the user is operator
user_g_w_op
=
GroupProfile
.
get_objects_with_level
(
"operator"
,
user
)
# get the list of groups the "target" (the profile) is member of
target_groups
=
GroupProfile
.
objects
.
filter
(
group__in
=
target
.
groups
.
all
())
intersection
=
set
(
user_g_w_op
)
.
intersection
(
target_groups
)
# if the intersection of the 2 lists is empty the logged in user
# has no permission to check the target's profile
# (except if the user want to see his own profile)
if
len
(
intersection
)
<
1
and
target
!=
user
:
raise
PermissionDenied
return
super
(
ProfileView
,
self
)
.
get
(
*
args
,
**
kwargs
)
def
get_context_data
(
self
,
**
kwargs
):
context
=
super
(
ProfileView
,
self
)
.
get_context_data
(
**
kwargs
)
user
=
self
.
get_object
()
...
...
circle/dashboard/views/vm.py
View file @
e11fffa5
...
...
@@ -45,6 +45,7 @@ from common.models import (
create_readable
,
HumanReadableException
,
fetch_human_exception
,
)
from
firewall.models
import
Vlan
,
Host
,
Rule
from
manager.scheduler
import
SchedulerError
from
storage.models
import
Disk
from
vm.models
import
(
Instance
,
instance_activity
,
InstanceActivity
,
Node
,
Lease
,
...
...
@@ -58,7 +59,7 @@ from ..forms import (
AclUserOrGroupAddForm
,
VmResourcesForm
,
TraitsForm
,
RawDataForm
,
VmAddInterfaceForm
,
VmCreateDiskForm
,
VmDownloadDiskForm
,
VmSaveForm
,
VmRenewForm
,
VmStateChangeForm
,
VmListSearchForm
,
VmCustomizeForm
,
TransferOwnershipForm
,
VmDiskResizeForm
,
TransferOwnershipForm
,
VmDiskResizeForm
,
RedeployForm
,
)
from
..models
import
Favourite
,
Profile
...
...
@@ -97,6 +98,8 @@ class VmDetailView(GraphMixin, CheckedDetailView):
context
=
super
(
VmDetailView
,
self
)
.
get_context_data
(
**
kwargs
)
instance
=
context
[
'instance'
]
user
=
self
.
request
.
user
is_operator
=
instance
.
has_level
(
user
,
"operator"
)
is_owner
=
instance
.
has_level
(
user
,
"owner"
)
ops
=
get_operations
(
instance
,
user
)
context
.
update
({
'graphite_enabled'
:
settings
.
GRAPHITE_URL
is
not
None
,
...
...
@@ -152,9 +155,11 @@ class VmDetailView(GraphMixin, CheckedDetailView):
context
[
'client_download'
]
=
self
.
request
.
COOKIES
.
get
(
'downloaded_client'
)
# can link template
context
[
'can_link_template'
]
=
(
instance
.
template
and
instance
.
template
.
has_level
(
user
,
"operator"
)
)
context
[
'can_link_template'
]
=
instance
.
template
and
is_operator
# is operator/owner
context
[
'is_operator'
]
=
is_operator
context
[
'is_owner'
]
=
is_owner
return
context
...
...
@@ -174,7 +179,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def
__set_name
(
self
,
request
):
self
.
object
=
self
.
get_object
()
if
not
self
.
object
.
has_level
(
request
.
user
,
'owner'
):
if
not
self
.
object
.
has_level
(
request
.
user
,
"operator"
):
raise
PermissionDenied
()
new_name
=
request
.
POST
.
get
(
"new_name"
)
Instance
.
objects
.
filter
(
pk
=
self
.
object
.
pk
)
.
update
(
...
...
@@ -197,7 +202,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def
__set_description
(
self
,
request
):
self
.
object
=
self
.
get_object
()
if
not
self
.
object
.
has_level
(
request
.
user
,
'owner'
):
if
not
self
.
object
.
has_level
(
request
.
user
,
"operator"
):
raise
PermissionDenied
()
new_description
=
request
.
POST
.
get
(
"new_description"
)
...
...
@@ -221,7 +226,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def
__add_tag
(
self
,
request
):
new_tag
=
request
.
POST
.
get
(
'new_tag'
)
self
.
object
=
self
.
get_object
()
if
not
self
.
object
.
has_level
(
request
.
user
,
'owner'
):
if
not
self
.
object
.
has_level
(
request
.
user
,
"operator"
):
raise
PermissionDenied
()
if
len
(
new_tag
)
<
1
:
...
...
@@ -243,7 +248,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
try
:
to_remove
=
request
.
POST
.
get
(
'to_remove'
)
self
.
object
=
self
.
get_object
()
if
not
self
.
object
.
has_level
(
request
.
user
,
'owner'
):
if
not
self
.
object
.
has_level
(
request
.
user
,
"operator"
):
raise
PermissionDenied
()
self
.
object
.
tags
.
remove
(
to_remove
)
...
...
@@ -262,8 +267,8 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def
__add_port
(
self
,
request
):
object
=
self
.
get_object
()
if
(
not
object
.
has_level
(
request
.
user
,
'owner'
)
or
not
request
.
user
.
has_perm
(
'vm.config_ports'
)):
if
not
(
object
.
has_level
(
request
.
user
,
"operator"
)
and
request
.
user
.
has_perm
(
'vm.config_ports'
)):
raise
PermissionDenied
()
port
=
request
.
POST
.
get
(
"port"
)
...
...
@@ -420,6 +425,15 @@ class VmMigrateView(VmOperationView):
ctx
=
super
(
VmMigrateView
,
self
)
.
get_context_data
(
**
kwargs
)
ctx
[
'nodes'
]
=
[
n
for
n
in
Node
.
objects
.
filter
(
enabled
=
True
)
if
n
.
online
]
inst
=
self
.
get_object
()
ctx
[
"recommended"
]
=
None
try
:
if
isinstance
(
inst
,
Instance
):
ctx
[
"recommended"
]
=
inst
.
select_node
()
.
pk
except
SchedulerError
:
logger
.
exception
(
"scheduler error:"
)
return
ctx
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
...
...
@@ -599,6 +613,15 @@ class VmStateChangeView(FormOperationMixin, VmOperationView):
return
val
class
RedeployView
(
FormOperationMixin
,
VmOperationView
):
op
=
'redeploy'
icon
=
'stethoscope'
effect
=
'danger'
show_in_toolbar
=
True
form_class
=
RedeployForm
wait_for_result
=
0.5
vm_ops
=
OrderedDict
([
(
'deploy'
,
VmOperationView
.
factory
(
op
=
'deploy'
,
icon
=
'play'
,
effect
=
'success'
)),
...
...
@@ -620,6 +643,7 @@ vm_ops = OrderedDict([
(
'recover'
,
VmOperationView
.
factory
(
op
=
'recover'
,
icon
=
'medkit'
,
effect
=
'warning'
)),
(
'nostate'
,
VmStateChangeView
),
(
'redeploy'
,
RedeployView
),
(
'destroy'
,
VmOperationView
.
factory
(
extra_bases
=
[
TokenOperationView
],
op
=
'destroy'
,
icon
=
'times'
,
effect
=
'danger'
)),
...
...
circle/network/views.py
View file @
e11fffa5
...
...
@@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context
=
super
(
VlanDetail
,
self
)
.
get_context_data
(
**
kwargs
)
q
=
Host
.
objects
.
filter
(
interface__in
=
Interface
.
objects
.
filter
(
vlan
=
self
.
object
,
instance__destroyed_at
=
None
vlan
=
self
.
object
))
context
[
'host_list'
]
=
SmallHostTable
(
q
)
...
...
circle/vm/models/instance.py
View file @
e11fffa5
...
...
@@ -266,6 +266,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
(
'change_resources'
,
_
(
'Can change resources of a running VM.'
)),
(
'set_resources'
,
_
(
'Can change resources of a new VM.'
)),
(
'create_vm'
,
_
(
'Can create a new VM.'
)),
(
'redeploy'
,
_
(
'Can redeploy a VM.'
)),
(
'config_ports'
,
_
(
'Can configure port forwards.'
)),
(
'recover'
,
_
(
'Can recover a destroyed VM.'
)),
(
'emergency_change_state'
,
_
(
'Can change VM state to NOSTATE.'
)),
...
...
@@ -763,6 +764,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
if
self
.
node
is
None
:
self
.
node
=
self
.
select_node
()
self
.
save
()
return
self
.
node
def
yield_node
(
self
):
if
self
.
node
is
not
None
:
...
...
circle/vm/models/node.py
View file @
e11fffa5
...
...
@@ -114,8 +114,8 @@ class Node(OperatedMixin, TimeStampedModel):
def
get_info
(
self
):
return
self
.
remote_query
(
vm_tasks
.
get_info
,
priority
=
'fast'
,
default
=
{
'core_num'
:
''
,
'ram_size'
:
'0'
,
default
=
{
'core_num'
:
0
,
'ram_size'
:
0
,
'architecture'
:
''
})
info
=
property
(
get_info
)
...
...
circle/vm/operations.py
View file @
e11fffa5
...
...
@@ -330,7 +330,7 @@ class DeployOperation(InstanceOperation):
def
_operation
(
self
,
activity
,
timeout
=
15
):
# Allocate VNC port and host node
self
.
instance
.
allocate_vnc_port
()
self
.
instance
.
allocate_node
()
self
.
instance
.
allocate_node
(
activity
)
# Deploy virtual images
self
.
instance
.
_deploy_disks
(
parent_activity
=
activity
)
...
...
@@ -487,12 +487,7 @@ class MigrateOperation(RemoteInstanceOperation):
def
_operation
(
self
,
activity
,
to_node
=
None
):
if
not
to_node
:
with
activity
.
sub_activity
(
'scheduling'
,
readable_name
=
ugettext_noop
(
"schedule"
))
as
sa
:
to_node
=
self
.
instance
.
select_node
()
sa
.
result
=
to_node
to_node
=
self
.
instance
.
reallocate_node
(
activity
)
try
:
with
activity
.
sub_activity
(
'migrate_vm'
,
readable_name
=
create_readable
(
...
...
@@ -512,6 +507,7 @@ class MigrateOperation(RemoteInstanceOperation):
# Refresh node information
self
.
instance
.
node
=
to_node
self
.
instance
.
save
()
# Estabilish network connection (vmdriver)
with
activity
.
sub_activity
(
'deploying_net'
,
readable_name
=
ugettext_noop
(
...
...
@@ -824,7 +820,7 @@ class WakeUpOperation(InstanceOperation):
def
_operation
(
self
,
activity
):
# Schedule vm
self
.
instance
.
allocate_vnc_port
()
self
.
instance
.
allocate_node
()
self
.
instance
.
allocate_node
(
activity
)
# Resume vm
self
.
instance
.
_wake_up_vm
(
parent_activity
=
activity
)
...
...
@@ -901,7 +897,8 @@ class ChangeStateOperation(InstanceOperation):
required_perms
=
(
'vm.emergency_change_state'
,
)
concurrency_check
=
False
def
_operation
(
self
,
user
,
activity
,
new_state
=
"NOSTATE"
,
interrupt
=
False
):
def
_operation
(
self
,
user
,
activity
,
new_state
=
"NOSTATE"
,
interrupt
=
False
,
reset_node
=
False
):
activity
.
resultant_state
=
new_state
if
interrupt
:
msg_txt
=
ugettext_noop
(
"Activity is forcibly interrupted."
)
...
...
@@ -911,6 +908,37 @@ class ChangeStateOperation(InstanceOperation):
i
.
finish
(
False
,
result
=
message
)
logger
.
error
(
'Forced finishing activity
%
s'
,
i
)
if
reset_node
:
self
.
instance
.
node
=
None
self
.
instance
.
save
()
@register_operation
class
RedeployOperation
(
InstanceOperation
):
activity_code_suffix
=
'redeploy'
id
=
'redeploy'
name
=
_
(
"redeploy"
)
description
=
_
(
"Change the virtual machine state to NOSTATE "
"and redeploy the VM. This operation allows starting "
"machines formerly running on a failed node."
)
acl_level
=
"owner"
required_perms
=
(
'vm.redeploy'
,
)
concurrency_check
=
False
def
_operation
(
self
,
user
,
activity
,
with_emergency_change_state
=
True
):
if
with_emergency_change_state
:
ChangeStateOperation
(
self
.
instance
)
.
call
(
parent_activity
=
activity
,
user
=
user
,
new_state
=
'NOSTATE'
,
interrupt
=
False
,
reset_node
=
True
)
else
:
ShutOffOperation
(
self
.
instance
)
.
call
(
parent_activity
=
activity
,
user
=
user
)
self
.
instance
.
_update_status
()
DeployOperation
(
self
.
instance
)
.
call
(
parent_activity
=
activity
,
user
=
user
)
class
NodeOperation
(
Operation
):
async_operation
=
abortable_async_node_operation
...
...
@@ -950,6 +978,35 @@ class NodeOperation(Operation):
@register_operation
class
ResetNodeOperation
(
NodeOperation
):
activity_code_suffix
=
'reset'
id
=
'reset'
name
=
_
(
"reset"
)
description
=
_
(
"Disable missing node and redeploy all instances "
"on other ones."
)
required_perms
=
()
online_required
=
False
async_queue
=
"localhost.man.slow"
def
check_precond
(
self
):
super
(
ResetNodeOperation
,
self
)
.
check_precond
()
if
not
self
.
node
.
enabled
or
self
.
node
.
online
:
raise
humanize_exception
(
ugettext_noop
(
"You cannot reset a disabled or online node."
),
Exception
())
def
_operation
(
self
,
activity
,
user
):
if
self
.
node
.
enabled
:
DisableOperation
(
self
.
node
)
.
call
(
parent_activity
=
activity
,
user
=
user
)
for
i
in
self
.
node
.
instance_set
.
all
():
name
=
create_readable
(
ugettext_noop
(
"migrate
%(instance)
s (
%(pk)
s)"
),
instance
=
i
.
name
,
pk
=
i
.
pk
)
with
activity
.
sub_activity
(
'migrate_instance_
%
d'
%
i
.
pk
,
readable_name
=
name
):
i
.
redeploy
(
user
=
user
)
@register_operation
class
FlushOperation
(
NodeOperation
):
id
=
'flush'
name
=
_
(
"flush"
)
...
...
circle/vm/tests/test_models.py
View file @
e11fffa5
...
...
@@ -114,8 +114,7 @@ class InstanceTestCase(TestCase):
migrate_op
(
system
=
True
)
migr
.
apply_async
.
assert_called
()
self
.
assertIn
(
call
.
sub_activity
(
u'scheduling'
,
readable_name
=
u'schedule'
),
act
.
mock_calls
)
inst
.
allocate_node
.
assert_called
()
inst
.
select_node
.
assert_called
()
def
test_migrate_wo_scheduling
(
self
):
...
...
@@ -133,7 +132,7 @@ class InstanceTestCase(TestCase):
migrate_op
(
to_node
=
inst
.
node
,
system
=
True
)
migr
.
apply_async
.
assert_called
()
self
.
assertNotIn
(
call
.
sub_activity
(
u'scheduling'
),
act
.
mock_calls
)
inst
.
allocate_node
.
assert_called
(
)
def
test_migrate_with_error
(
self
):
inst
=
Mock
(
destroyed_at
=
None
,
spec
=
Instance
)
...
...
circle/vm/tests/test_operations.py
View file @
e11fffa5
...
...
@@ -16,9 +16,10 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from
django.test
import
TestCase
from
mock
import
MagicMock
from
common.operations
import
operation_registry_name
as
op_reg_name
from
vm.models
import
Instance
,
Node
from
vm.models
import
Instance
,
InstanceActivity
,
Node
from
vm.operations
import
(
DeployOperation
,
DestroyOperation
,
FlushOperation
,
MigrateOperation
,
RebootOperation
,
ResetOperation
,
SaveAsTemplateOperation
,
...
...
@@ -45,6 +46,22 @@ class MigrateOperationTestCase(TestCase):
def
test_operation_registered
(
self
):
assert
MigrateOperation
.
id
in
getattr
(
Instance
,
op_reg_name
)
def
test_operation_wo_to_node_param
(
self
):
class
MigrateException
(
Exception
):
pass
inst
=
MagicMock
(
spec
=
Instance
)
act
=
MagicMock
(
spec
=
InstanceActivity
)
inst
.
migrate_vm
=
MagicMock
(
side_effect
=
MigrateException
())
inst
.
select_node
=
MagicMock
(
return_value
=
'test'
)
inst
.
reallocate_node
=
(
lambda
act
:
Instance
.
reallocate_node
(
inst
,
act
))
self
.