Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
Gelencsér Szabolcs
/
circlestack
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
0bfdb2a3
authored
Oct 13, 2014
by
Bach Dániel
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'feature-offline-migration' into 'master'
Feature offline migration See merge request !239
parents
a7e9781e
860300db
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
78 additions
and
45 deletions
+78
-45
circle/dashboard/forms.py
+14
-0
circle/dashboard/templates/dashboard/_vm-mass-migrate.html
+5
-3
circle/dashboard/templates/dashboard/_vm-migrate.html
+5
-3
circle/dashboard/tests/test_mockedviews.py
+30
-15
circle/dashboard/views/vm.py
+17
-18
circle/vm/operations.py
+5
-5
circle/vm/tests/test_operations.py
+2
-1
No files found.
circle/dashboard/forms.py
View file @
0bfdb2a3
...
...
@@ -752,6 +752,20 @@ class VmRenewForm(forms.Form):
return
helper
class
VmMigrateForm
(
forms
.
Form
):
live_migration
=
forms
.
BooleanField
(
required
=
False
,
initial
=
True
,
label
=
_
(
"live migration"
))
def
__init__
(
self
,
*
args
,
**
kwargs
):
choices
=
kwargs
.
pop
(
'choices'
)
default
=
kwargs
.
pop
(
'default'
)
super
(
VmMigrateForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
.
insert
(
0
,
'to_node'
,
forms
.
ModelChoiceField
(
queryset
=
choices
,
initial
=
default
,
required
=
False
,
widget
=
forms
.
RadioSelect
(),
label
=
_
(
"Node"
)))
class
VmStateChangeForm
(
forms
.
Form
):
interrupt
=
forms
.
BooleanField
(
required
=
False
,
label
=
_
(
...
...
circle/dashboard/templates/dashboard/_vm-mass-migrate.html
View file @
0bfdb2a3
{% extends "dashboard/mass-operate.html" %}
{% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block formfields %}
...
...
@@ -11,20 +12,20 @@
<label
for=
"migrate-to-none"
>
<strong>
{% trans "Reschedule" %}
</strong>
</label>
<input
id=
"migrate-to-none"
type=
"radio"
name=
"node"
value=
""
style=
"float: right;"
checked=
"checked"
>
<input
id=
"migrate-to-none"
type=
"radio"
name=
"
to_
node"
value=
""
style=
"float: right;"
checked=
"checked"
>
<span
class=
"vm-migrate-node-property"
>
{% trans "This option will reschedule each virtual machine to the optimal node." %}
</span>
<div
style=
"clear: both;"
></div>
</div>
</li>
{% for n in
nodes
%}
{% for n in
form.fields.to_node.queryset.all
%}
<li
class=
"panel panel-default mass-migrate-node"
>
<div
class=
"panel-body"
>
<label
for=
"migrate-to-{{n.pk}}"
>
<strong>
{{ n }}
</strong>
</label>
<input
id=
"migrate-to-{{n.pk}}"
type=
"radio"
name=
"node"
value=
"{{ n.pk }}"
style=
"float: right;"
/>
<input
id=
"migrate-to-{{n.pk}}"
type=
"radio"
name=
"
to_
node"
value=
"{{ n.pk }}"
style=
"float: right;"
/>
<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>
...
...
@@ -32,5 +33,6 @@
</li>
{% endfor %}
</ul>
{{ form.live_migration|as_crispy_field }}
<hr
/>
{% endblock %}
circle/dashboard/templates/dashboard/_vm-migrate.html
View file @
0bfdb2a3
{% extends "dashboard/operate.html" %}
{% load i18n %}
{% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block question %}
<p>
...
...
@@ -13,8 +14,8 @@ Choose a compute node to migrate {{obj}} to.
{% block formfields %}
<ul
id=
"vm-migrate-node-list"
class=
"list-unstyled"
>
{% with current=object.node.pk %}
{% for n in
nodes
%}
{% with current=object.node.pk
recommended=form.fields.to_node.initial.pk
%}
{% for n in
form.fields.to_node.queryset.all
%}
<li
class=
"panel panel-default"
><div
class=
"panel-body"
>
<label
for=
"migrate-to-{{n.pk}}"
>
<strong>
{{ n }}
</strong>
...
...
@@ -23,7 +24,7 @@ Choose a compute node to migrate {{obj}} to.
{% if current == n.pk %}
<div
class=
"label label-info"
>
{% trans "current" %}
</div>
{% endif %}
{% if recommended == 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;"
<input
id=
"migrate-to-{{n.pk}}"
type=
"radio"
name=
"
to_
node"
value=
"{{ n.pk }}"
style=
"float: right;"
{%
if
current =
=
n
.
pk
%}
disabled=
"disabled"
{%
endif
%}
{%
if
recommended =
=
n
.
pk
%}
checked=
"checked"
{%
endif
%}
/>
...
...
@@ -35,4 +36,5 @@ Choose a compute node to migrate {{obj}} to.
{% endfor %}
{% endwith %}
</ul>
{{ form.live_migration|as_crispy_field }}
{% endblock %}
circle/dashboard/tests/test_mockedviews.py
View file @
0bfdb2a3
...
...
@@ -34,6 +34,13 @@ from ..views import AclUpdateView
from
..
import
views
class
QuerySet
(
list
):
model
=
MagicMock
()
def
get
(
self
,
*
args
,
**
kwargs
):
return
self
.
pop
()
class
ViewUserTestCase
(
unittest
.
TestCase
):
def
test_404
(
self
):
...
...
@@ -145,58 +152,66 @@ class VmOperationViewTestCase(unittest.TestCase):
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
def
test_migrate
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
},
superuser
=
True
)
request
=
FakeRequestFactory
(
POST
=
{
'to_node'
:
1
,
'live_migration'
:
True
},
superuser
=
True
)
view
=
vm_ops
[
'migrate'
]
node
=
MagicMock
(
pk
=
1
,
name
=
'node1'
)
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.util.messages'
)
as
msg
,
\
patch
(
'dashboard.views.vm.get_object_or_404'
)
as
go4
:
patch
.
object
(
view
,
'get_form_kwargs'
)
as
form_kwargs
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
migrate
=
Instance
.
_ops
[
'migrate'
](
inst
)
inst
.
migrate
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
True
form_kwargs
.
return_value
=
{
'default'
:
100
,
'choices'
:
QuerySet
([
node
])}
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
assert
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
]
assert
not
msg
.
error
.
called
assert
go4
.
called
inst
.
migrate
.
async
.
assert_called_once_with
(
to_node
=
node
,
live_migration
=
True
,
user
=
request
.
user
)
def
test_migrate_failed
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
},
superuser
=
True
)
request
=
FakeRequestFactory
(
POST
=
{
'
to_
node'
:
1
},
superuser
=
True
)
view
=
vm_ops
[
'migrate'
]
node
=
MagicMock
(
pk
=
1
,
name
=
'node1'
)
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.util.messages'
)
as
msg
,
\
patch
(
'dashboard.views.vm.get_object_or_404'
)
as
go4
:
patch
.
object
(
view
,
'get_form_kwargs'
)
as
form_kwargs
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
migrate
=
Instance
.
_ops
[
'migrate'
](
inst
)
inst
.
migrate
.
async
=
MagicMock
()
inst
.
migrate
.
async
.
side_effect
=
Exception
inst
.
has_level
.
return_value
=
True
form_kwargs
.
return_value
=
{
'default'
:
100
,
'choices'
:
QuerySet
([
node
])}
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
assert
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
]
assert
inst
.
migrate
.
async
.
called
assert
msg
.
error
.
called
assert
go4
.
called
def
test_migrate_wo_permission
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
},
superuser
=
False
)
request
=
FakeRequestFactory
(
POST
=
{
'
to_
node'
:
1
},
superuser
=
False
)
view
=
vm_ops
[
'migrate'
]
node
=
MagicMock
(
pk
=
1
,
name
=
'node1'
)
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.vm.get_object_or_404'
)
as
go4
:
patch
.
object
(
view
,
'get_form_kwargs'
)
as
form_kwargs
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
migrate
=
Instance
.
_ops
[
'migrate'
](
inst
)
inst
.
migrate
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
True
form_kwargs
.
return_value
=
{
'default'
:
100
,
'choices'
:
QuerySet
([
node
])}
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
with
self
.
assertRaises
(
PermissionDenied
):
assert
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
]
assert
go4
.
called
assert
not
inst
.
migrate
.
async
.
called
def
test_migrate_template
(
self
):
"""check if GET dialog's template can be rendered"""
...
...
@@ -303,7 +318,7 @@ class VmMassOperationViewTestCase(unittest.TestCase):
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
def
test_migrate
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
},
superuser
=
True
)
request
=
FakeRequestFactory
(
POST
=
{
'
to_
node'
:
1
},
superuser
=
True
)
view
=
vm_mass_ops
[
'migrate'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
...
...
@@ -320,7 +335,7 @@ class VmMassOperationViewTestCase(unittest.TestCase):
assert
not
msg2
.
error
.
called
def
test_migrate_failed
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
},
superuser
=
True
)
request
=
FakeRequestFactory
(
POST
=
{
'
to_
node'
:
1
},
superuser
=
True
)
view
=
vm_mass_ops
[
'migrate'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
...
...
@@ -336,7 +351,7 @@ class VmMassOperationViewTestCase(unittest.TestCase):
assert
msg
.
error
.
called
def
test_migrate_wo_permission
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
},
superuser
=
False
)
request
=
FakeRequestFactory
(
POST
=
{
'
to_
node'
:
1
},
superuser
=
False
)
view
=
vm_mass_ops
[
'migrate'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
:
...
...
circle/dashboard/views/vm.py
View file @
0bfdb2a3
...
...
@@ -60,6 +60,7 @@ from ..forms import (
VmAddInterfaceForm
,
VmCreateDiskForm
,
VmDownloadDiskForm
,
VmSaveForm
,
VmRenewForm
,
VmStateChangeForm
,
VmListSearchForm
,
VmCustomizeForm
,
TransferOwnershipForm
,
VmDiskResizeForm
,
RedeployForm
,
VmDiskRemoveForm
,
VmMigrateForm
,
)
from
..models
import
Favourite
,
Profile
...
...
@@ -421,36 +422,28 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
with_reload
=
True
class
VmMigrateView
(
VmOperationView
):
class
VmMigrateView
(
FormOperationMixin
,
VmOperationView
):
op
=
'migrate'
icon
=
'truck'
effect
=
'info'
template_name
=
'dashboard/_vm-migrate.html'
form_class
=
VmMigrateForm
def
get_context_data
(
self
,
**
kwargs
):
ctx
=
super
(
VmMigrateView
,
self
)
.
get_context_data
(
**
kwargs
)
ctx
[
'nodes'
]
=
[
n
for
n
in
Node
.
objects
.
filter
(
enabled
=
True
)
if
n
.
online
]
def
get_form_kwargs
(
self
):
online
=
(
n
.
pk
for
n
in
Node
.
objects
.
filter
(
enabled
=
True
)
if
n
.
online
)
choices
=
Node
.
objects
.
filter
(
pk__in
=
online
)
default
=
None
inst
=
self
.
get_object
()
ctx
[
"recommended"
]
=
None
try
:
if
isinstance
(
inst
,
Instance
):
ctx
[
"recommended"
]
=
inst
.
select_node
()
.
pk
default
=
inst
.
select_node
()
except
SchedulerError
:
logger
.
exception
(
"scheduler error:"
)
return
ctx
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
if
extra
is
None
:
extra
=
{}
node
=
self
.
request
.
POST
.
get
(
"node"
)
if
node
:
node
=
get_object_or_404
(
Node
,
pk
=
node
)
extra
[
"to_node"
]
=
node
return
super
(
VmMigrateView
,
self
)
.
post
(
request
,
extra
,
*
args
,
**
kwargs
)
val
=
super
(
VmMigrateView
,
self
)
.
get_form_kwargs
()
val
.
update
({
'choices'
:
choices
,
'default'
:
default
})
return
val
class
VmSaveView
(
FormOperationMixin
,
VmOperationView
):
...
...
@@ -769,6 +762,12 @@ class MassOperationView(OperationView):
self
.
check_auth
()
if
extra
is
None
:
extra
=
{}
if
hasattr
(
self
,
'form_class'
):
form
=
self
.
form_class
(
self
.
request
.
POST
,
**
self
.
get_form_kwargs
())
if
form
.
is_valid
():
extra
.
update
(
form
.
cleaned_data
)
self
.
_call_operations
(
extra
)
if
request
.
is_ajax
():
store
=
messages
.
get_messages
(
request
)
...
...
circle/vm/operations.py
View file @
0bfdb2a3
...
...
@@ -473,10 +473,9 @@ class MigrateOperation(RemoteInstanceOperation):
remote_queue
=
(
"vm"
,
"slow"
)
remote_timeout
=
1000
def
_get_remote_args
(
self
,
to_node
,
**
kwargs
):
def
_get_remote_args
(
self
,
to_node
,
live_migration
,
**
kwargs
):
return
(
super
(
MigrateOperation
,
self
)
.
_get_remote_args
(
**
kwargs
)
+
[
to_node
.
host
.
hostname
,
True
])
# TODO handle non-live migration
+
[
to_node
.
host
.
hostname
,
live_migration
])
def
rollback
(
self
,
activity
):
with
activity
.
sub_activity
(
...
...
@@ -484,7 +483,7 @@ class MigrateOperation(RemoteInstanceOperation):
"redeploy network (rollback)"
)):
self
.
instance
.
deploy_net
()
def
_operation
(
self
,
activity
,
to_node
=
None
):
def
_operation
(
self
,
activity
,
to_node
=
None
,
live_migration
=
True
):
if
not
to_node
:
with
activity
.
sub_activity
(
'scheduling'
,
readable_name
=
ugettext_noop
(
...
...
@@ -496,7 +495,8 @@ class MigrateOperation(RemoteInstanceOperation):
with
activity
.
sub_activity
(
'migrate_vm'
,
readable_name
=
create_readable
(
ugettext_noop
(
"migrate to
%(node)
s"
),
node
=
to_node
)):
super
(
MigrateOperation
,
self
)
.
_operation
(
to_node
=
to_node
)
super
(
MigrateOperation
,
self
)
.
_operation
(
to_node
=
to_node
,
live_migration
=
live_migration
)
except
Exception
as
e
:
if
hasattr
(
e
,
'libvirtError'
):
self
.
rollback
(
activity
)
...
...
circle/vm/tests/test_operations.py
View file @
0bfdb2a3
...
...
@@ -59,7 +59,8 @@ class MigrateOperationTestCase(TestCase):
MigrateException
,
op
.
_operation
,
act
,
to_node
=
None
)
assert
inst
.
select_node
.
called
op
.
_get_remote_args
.
assert_called_once_with
(
to_node
=
'test'
)
op
.
_get_remote_args
.
assert_called_once_with
(
to_node
=
'test'
,
live_migration
=
True
)
class
RebootOperationTestCase
(
TestCase
):
...
...
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