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
a8fb5a86
authored
Jul 07, 2014
by
Bach Dániel
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'feature-fix-acls'
Conflicts: circle/vm/operations.py
parents
f7488817
f32bb524
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
283 additions
and
72 deletions
+283
-72
circle/circle/__init__.py
+20
-0
circle/common/operations.py
+5
-2
circle/dashboard/fixtures/test-vm-fixture.json
+18
-0
circle/dashboard/forms.py
+44
-13
circle/dashboard/static/dashboard/bootstrap-slider/bootstrap-slider.js
+3
-0
circle/dashboard/static/dashboard/vm-details.js
+12
-3
circle/dashboard/templates/dashboard/_template-choose.html
+2
-0
circle/dashboard/templates/dashboard/vm-detail/console.html
+6
-0
circle/dashboard/templates/dashboard/vm-detail/resources.html
+10
-1
circle/dashboard/templates/dashboard/vm-list/column-admin.html
+1
-1
circle/dashboard/tests/test_mockedviews.py
+6
-4
circle/dashboard/tests/test_views.py
+6
-0
circle/dashboard/urls.py
+1
-3
circle/dashboard/views.py
+45
-44
circle/storage/models.py
+3
-0
circle/vm/models/instance.py
+5
-0
circle/vm/operations.py
+91
-1
circle/vm/tests/test_models.py
+5
-0
No files found.
circle/circle/__init__.py
View file @
a8fb5a86
# register a signal do update permissions every migration.
# This is based on app django_extensions update_permissions command
from
south.signals
import
post_migrate
def
update_permissions_after_migration
(
app
,
**
kwargs
):
"""
Update app permission just after every migration.
This is based on app django_extensions update_permissions
management command.
"""
from
django.conf
import
settings
from
django.db.models
import
get_app
,
get_models
from
django.contrib.auth.management
import
create_permissions
create_permissions
(
get_app
(
app
),
get_models
(),
2
if
settings
.
DEBUG
else
0
)
post_migrate
.
connect
(
update_permissions_after_migration
)
circle/common/operations.py
View file @
a8fb5a86
...
...
@@ -20,7 +20,7 @@ from logging import getLogger
from
.models
import
activity_context
,
has_suffix
from
django.core.exceptions
import
PermissionDenied
from
django.core.exceptions
import
PermissionDenied
,
ImproperlyConfigured
logger
=
getLogger
(
__name__
)
...
...
@@ -30,7 +30,7 @@ class Operation(object):
"""Base class for VM operations.
"""
async_queue
=
'localhost.man'
required_perms
=
()
required_perms
=
None
do_not_call_in_templates
=
True
abortable
=
False
has_percentage
=
False
...
...
@@ -141,6 +141,9 @@ class Operation(object):
pass
def
check_auth
(
self
,
user
):
if
self
.
required_perms
is
None
:
raise
ImproperlyConfigured
(
"Set required_perms to () if none needed."
)
if
not
user
.
has_perms
(
self
.
required_perms
):
raise
PermissionDenied
(
"
%
s doesn't have the required permissions."
%
user
)
...
...
circle/dashboard/fixtures/test-vm-fixture.json
View file @
a8fb5a86
...
...
@@ -1240,6 +1240,24 @@
}
},
{
"pk"
:
1367
,
"model"
:
"auth.permission"
,
"fields"
:
{
"codename"
:
"create_vm"
,
"name"
:
"Can create a new VM."
,
"content_type"
:
28
}
},
{
"pk"
:
1368
,
"model"
:
"auth.permission"
,
"fields"
:
{
"codename"
:
"access_console"
,
"name"
:
"Can access the graphical console of a VM."
,
"content_type"
:
28
}
},
{
"pk"
:
1
,
"model"
:
"auth.group"
,
"fields"
:
{
...
...
circle/dashboard/forms.py
View file @
a8fb5a86
...
...
@@ -25,6 +25,7 @@ from django.contrib.auth.forms import (
)
from
django.contrib.auth.models
import
User
,
Group
from
django.core.validators
import
URLValidator
from
django.core.exceptions
import
PermissionDenied
,
ValidationError
from
crispy_forms.helper
import
FormHelper
from
crispy_forms.layout
import
(
...
...
@@ -593,6 +594,17 @@ class TemplateForm(forms.ModelForm):
n
=
self
.
instance
.
interface_set
.
values_list
(
"vlan"
,
flat
=
True
)
self
.
initial
[
'networks'
]
=
n
self
.
allowed_fields
=
(
'name'
,
'access_method'
,
'description'
,
'system'
,
'tags'
)
if
self
.
user
.
has_perm
(
'vm.change_template_resources'
):
self
.
allowed_fields
+=
tuple
(
set
(
self
.
fields
.
keys
())
-
set
([
'raw_data'
]))
if
self
.
user
.
is_superuser
:
self
.
allowed_fields
+=
(
'raw_data'
,
)
for
name
,
field
in
self
.
fields
.
items
():
if
name
not
in
self
.
allowed_fields
:
field
.
widget
.
attrs
[
'disabled'
]
=
'disabled'
if
not
self
.
instance
.
pk
and
len
(
self
.
errors
)
<
1
:
self
.
instance
.
priority
=
20
self
.
instance
.
ram_size
=
512
...
...
@@ -603,14 +615,35 @@ class TemplateForm(forms.ModelForm):
return
User
.
objects
.
get
(
pk
=
self
.
instance
.
owner
.
pk
)
return
self
.
user
def
clean_raw_data
(
self
):
# if raw_data has changed and the user is not superuser
if
"raw_data"
in
self
.
changed_data
and
not
self
.
user
.
is_superuser
:
old_raw_data
=
InstanceTemplate
.
objects
.
get
(
pk
=
self
.
instance
.
pk
)
.
raw_data
return
old_raw_data
else
:
return
self
.
cleaned_data
[
'raw_data'
]
def
_clean_fields
(
self
):
try
:
old
=
InstanceTemplate
.
objects
.
get
(
pk
=
self
.
instance
.
pk
)
except
InstanceTemplate
.
DoesNotExist
:
old
=
None
for
name
,
field
in
self
.
fields
.
items
():
if
name
in
self
.
allowed_fields
:
value
=
field
.
widget
.
value_from_datadict
(
self
.
data
,
self
.
files
,
self
.
add_prefix
(
name
))
try
:
if
isinstance
(
field
,
forms
.
FileField
):
initial
=
self
.
initial
.
get
(
name
,
field
.
initial
)
value
=
field
.
clean
(
value
,
initial
)
else
:
value
=
field
.
clean
(
value
)
self
.
cleaned_data
[
name
]
=
value
if
hasattr
(
self
,
'clean_
%
s'
%
name
):
value
=
getattr
(
self
,
'clean_
%
s'
%
name
)()
self
.
cleaned_data
[
name
]
=
value
except
ValidationError
as
e
:
self
.
_errors
[
name
]
=
self
.
error_class
(
e
.
messages
)
if
name
in
self
.
cleaned_data
:
del
self
.
cleaned_data
[
name
]
elif
old
:
if
name
==
'networks'
:
self
.
cleaned_data
[
name
]
=
[
i
.
vlan
for
i
in
self
.
instance
.
interface_set
.
all
()]
else
:
self
.
cleaned_data
[
name
]
=
getattr
(
old
,
name
)
def
save
(
self
,
commit
=
True
):
data
=
self
.
cleaned_data
...
...
@@ -624,6 +657,8 @@ class TemplateForm(forms.ModelForm):
networks
=
InterfaceTemplate
.
objects
.
filter
(
template
=
self
.
instance
)
.
values_list
(
"vlan"
,
flat
=
True
)
for
m
in
data
[
'networks'
]:
if
not
m
.
has_level
(
self
.
user
,
"user"
):
raise
PermissionDenied
()
if
m
.
pk
not
in
networks
:
InterfaceTemplate
(
vlan
=
m
,
managed
=
m
.
managed
,
template
=
self
.
instance
)
.
save
()
...
...
@@ -635,10 +670,6 @@ class TemplateForm(forms.ModelForm):
@property
def
helper
(
self
):
kwargs_raw_data
=
{}
if
not
self
.
user
.
is_superuser
:
kwargs_raw_data
[
'readonly'
]
=
None
helper
=
FormHelper
()
helper
.
layout
=
Layout
(
Field
(
"name"
),
...
...
@@ -690,7 +721,7 @@ class TemplateForm(forms.ModelForm):
_
(
"Virtual machine settings"
),
Field
(
'access_method'
),
Field
(
'boot_menu'
),
Field
(
'raw_data'
,
**
kwargs_raw_data
),
Field
(
'raw_data'
),
Field
(
'req_traits'
),
Field
(
'description'
),
Field
(
"parent"
,
type
=
"hidden"
),
...
...
circle/dashboard/static/dashboard/bootstrap-slider/bootstrap-slider.js
View file @
a8fb5a86
...
...
@@ -192,6 +192,9 @@
},
mousedown
:
function
(
ev
)
{
if
(
this
.
element
[
0
].
disabled
)
{
return
false
;
}
// Touch: Get the original event:
if
(
this
.
touchCapable
&&
ev
.
type
===
'touchstart'
)
{
...
...
circle/dashboard/static/dashboard/vm-details.js
View file @
a8fb5a86
...
...
@@ -11,13 +11,14 @@ $(function() {
/* save resources */
$
(
'#vm-details-resources-save'
).
click
(
function
()
{
$
(
'i.icon-save'
,
this
).
removeClass
(
"icon-save"
).
addClass
(
"icon-refresh icon-spin"
);
var
vm
=
$
(
this
).
data
(
"vm"
);
$
.
ajax
({
type
:
'POST'
,
url
:
location
.
href
,
url
:
"/dashboard/vm/"
+
vm
+
"/op/resources_change/"
,
data
:
$
(
'#vm-details-resources-form'
).
serialize
(),
success
:
function
(
data
,
textStatus
,
xhr
)
{
addMessage
(
data
[
'message'
],
'success'
);
$
(
"#vm-details-resources-save i"
).
removeClass
(
'icon-refresh icon-spin'
).
addClass
(
"icon-save"
);
$
(
'a[href="#activity"]'
).
trigger
(
"click"
);
},
error
:
function
(
xhr
,
textStatus
,
error
)
{
$
(
"#vm-details-resources-save i"
).
removeClass
(
'icon-refresh icon-spin'
).
addClass
(
"icon-save"
);
...
...
@@ -330,7 +331,7 @@ function decideActivityRefresh() {
/* unescapes html got via the request, also removes whitespaces and replaces all ' with " */
function
unescapeHTML
(
html
)
{
return
html
.
replace
(
/</g
,
'<'
).
replace
(
/>/g
,
'>'
).
replace
(
/&/g
,
'&'
).
replace
(
/–/g
,
"–"
).
replace
(
/
\/
/g
,
""
).
replace
(
/'/g
,
'"'
).
replace
(
/ /g
,
''
);
return
html
.
replace
(
/</g
,
'<'
).
replace
(
/>/g
,
'>'
).
replace
(
/&/g
,
'&'
).
replace
(
/–/g
,
"–"
).
replace
(
/
\/
/g
,
""
).
replace
(
/'/g
,
'"'
).
replace
(
/
'/g
,
"'"
).
replace
(
/
/g
,
''
);
}
/* the html page contains some tags that were modified via js (titles for example), we delete these
...
...
@@ -367,6 +368,14 @@ function checkNewActivity(only_status, runs) {
$
(
"[data-target=#_console]"
).
attr
(
"data-toggle"
,
"_pill"
).
attr
(
"href"
,
"#"
).
parent
(
"li"
).
addClass
(
"disabled"
);
}
if
(
data
[
'status'
]
==
"STOPPED"
)
{
$
(
".enabled-when-stopped"
).
prop
(
"disabled"
,
false
);
$
(
".hide-when-stopped"
).
hide
();
}
else
{
$
(
".enabled-when-stopped"
).
prop
(
"disabled"
,
true
);
$
(
".hide-when-stopped"
).
show
();
}
if
(
runs
>
0
&&
decideActivityRefresh
())
{
setTimeout
(
function
()
{
checkNewActivity
(
only_status
,
runs
+
1
)},
...
...
circle/dashboard/templates/dashboard/_template-choose.html
View file @
a8fb5a86
...
...
@@ -16,10 +16,12 @@
<div
class=
"clearfix"
></div>
</div>
{% endfor %}
{% if perms.vm.create_base_template %}
<div
class=
"panel panel-default template-choose-list-element"
>
<input
type=
"radio"
name=
"parent"
value=
"base_vm"
/>
{% trans "Create a new base VM without disk" %}
</div>
{% endif %}
<button
type=
"submit"
id=
"template-choose-next-button"
class=
"btn btn-success pull-right"
>
{% trans "Next" %}
</button>
<div
class=
"clearfix"
></div>
</div>
...
...
circle/dashboard/templates/dashboard/vm-detail/console.html
View file @
a8fb5a86
{% load i18n %}
<div
class=
"btn-toolbar"
>
{% if perms.vm.access_console %}
<button
id=
"sendCtrlAltDelButton"
class=
"btn btn-danger btn-sm"
>
{% trans "Send Ctrl+Alt+Del" %}
</button>
<button
id=
"sendPasswordButton"
class=
"btn btn-default btn-sm"
>
{% trans "Type password" %}
</button>
{% endif %}
<button
id=
"getScreenshotButton"
class=
"btn btn-info btn-sm pull-right"
data-vm-pk=
"{{ instance.pk }}"
><i
class=
"icon-picture"
></i>
{% trans "Screenshot" %}
</button>
</div>
{% if perms.vm.access_console %}
<div
class=
"alert alert-info"
id=
"noVNC_status"
>
</div>
{% endif %}
<div
id=
"vm-console-screenshot"
>
<button
class=
"btn btn-danger btn-sm pull-right"
>
{% trans "Close" %}
</button>
...
...
@@ -14,6 +18,7 @@
<hr
/>
</div>
{% if perms.vm.access_console %}
<canvas
id=
"noVNC_canvas"
width=
"640px"
height=
"20px"
>
Canvas not supported.
</canvas>
...
...
@@ -22,3 +27,4 @@
var
INCLUDE_URI
=
'{{ STATIC_URL }}dashboard/novnc/'
;
var
VNC_URL
=
"{{ vnc_url }}"
;
</script>
{% endif %}
circle/dashboard/templates/dashboard/vm-detail/resources.html
View file @
a8fb5a86
...
...
@@ -33,11 +33,20 @@
</div>
</p>
{% if can_change_resources %}
<p
class=
"row"
>
<div
class=
"col-sm-12"
>
<button
type=
"submit"
class=
"btn btn-success btn-sm"
id=
"vm-details-resources-save"
><i
class=
"icon-save"
></i>
{% trans "Save resources" %}
</button>
<button
type=
"submit"
class=
"btn btn-success btn-sm enabled-when-stopped"
id=
"vm-details-resources-save"
data-vm=
"{{ instance.pk }}"
{%
if
not
op
.
resources_change
%}
disabled
{%
endif
%}
>
<i
class=
"icon-save"
></i>
{% trans "Save resources" %}
</button>
<span
class=
"hide-when-stopped"
{%
if
op
.
resources_change
%}
style=
"display: none;"
{%
endif
%}
>
{% trans "Stop your VM to change resources." %}
</span>
</div>
</p>
{% endif %}
</form>
<hr
/>
...
...
circle/dashboard/templates/dashboard/vm-list/column-admin.html
View file @
a8fb5a86
<a
href=
"{% url "
dashboard
.
v
iews
.
vm-
migrate
"
pk=
record.pk
%}"
class=
"btn btn-default btn-xs vm-migrate"
data-vm-pk=
"{{ record.pk }}"
title
data-original-title=
"Migrate"
>
<a
href=
"{% url "
dashboard
.
v
m
.
op
.
migrate
"
pk=
record.pk
%}"
class=
"btn btn-default btn-xs vm-migrate"
data-vm-pk=
"{{ record.pk }}"
title
data-original-title=
"Migrate"
>
<i
class=
"icon-truck"
></i>
</a>
<a
id=
"vm-list-rename-button"
class=
"btn btn-default btn-xs"
title
data-original-title=
"Rename"
>
...
...
circle/dashboard/tests/test_mockedviews.py
View file @
a8fb5a86
...
...
@@ -159,7 +159,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert
not
msg
.
error
.
called
def
test_migrate_failed
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
})
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
}
,
superuser
=
True
)
view
=
vm_ops
[
'migrate'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
...
...
@@ -177,7 +177,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert
msg
.
error
.
called
def
test_migrate_template
(
self
):
request
=
FakeRequestFactory
()
request
=
FakeRequestFactory
(
superuser
=
True
)
view
=
vm_ops
[
'migrate'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
:
...
...
@@ -190,7 +190,7 @@ class VmOperationViewTestCase(unittest.TestCase):
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
.
status_code
,
200
)
def
test_save_as_wo_name
(
self
):
request
=
FakeRequestFactory
(
POST
=
{})
request
=
FakeRequestFactory
(
POST
=
{}
,
has_perms_mock
=
True
)
view
=
vm_ops
[
'save_as_template'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
...
...
@@ -224,7 +224,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert
not
msg
.
error
.
called
def
test_save_as_template
(
self
):
request
=
FakeRequestFactory
()
request
=
FakeRequestFactory
(
has_perms_mock
=
True
)
view
=
vm_ops
[
'save_as_template'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
:
...
...
@@ -246,6 +246,8 @@ def FakeRequestFactory(*args, **kwargs):
user
=
UserFactory
()
user
.
is_authenticated
=
lambda
:
kwargs
.
get
(
'authenticated'
,
True
)
user
.
is_superuser
=
kwargs
.
get
(
'superuser'
,
False
)
if
kwargs
.
get
(
'has_perms_mock'
,
False
):
user
.
has_perms
=
MagicMock
(
return_value
=
True
)
request
=
HttpRequest
()
request
.
user
=
user
...
...
circle/dashboard/tests/test_views.py
View file @
a8fb5a86
...
...
@@ -63,6 +63,8 @@ class VmDetailTest(LoginMixin, TestCase):
self
.
g1
.
user_set
.
add
(
self
.
u1
)
self
.
g1
.
user_set
.
add
(
self
.
u2
)
self
.
g1
.
save
()
self
.
u1
.
user_permissions
.
add
(
Permission
.
objects
.
get
(
codename
=
'create_vm'
))
settings
[
"default_vlangroup"
]
=
'public'
VlanGroup
.
objects
.
create
(
name
=
'public'
)
...
...
@@ -1544,6 +1546,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst
.
node
=
Node
.
objects
.
all
()[
0
]
inst
.
save
()
inst
.
set_level
(
self
.
u1
,
'operator'
)
self
.
u1
.
user_permissions
.
add
(
Permission
.
objects
.
get
(
codename
=
'access_console'
))
response
=
c
.
get
(
'/dashboard/vm/1/vnctoken/'
)
self
.
assertEqual
(
response
.
status_code
,
200
)
...
...
@@ -1554,6 +1558,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst
.
node
=
Node
.
objects
.
all
()[
0
]
inst
.
save
()
inst
.
set_level
(
self
.
u1
,
'user'
)
self
.
u1
.
user_permissions
.
add
(
Permission
.
objects
.
get
(
codename
=
'access_console'
))
response
=
c
.
get
(
'/dashboard/vm/1/vnctoken/'
)
self
.
assertEqual
(
response
.
status_code
,
403
)
...
...
circle/dashboard/urls.py
View file @
a8fb5a86
...
...
@@ -28,7 +28,7 @@ from .views import (
NotificationView
,
PortDelete
,
TemplateAclUpdateView
,
TemplateCreate
,
TemplateDelete
,
TemplateDetail
,
TemplateList
,
TransferOwnershipConfirmView
,
TransferOwnershipView
,
vm_activity
,
VmCreate
,
VmDelete
,
VmDetailView
,
VmDetailVncTokenView
,
VmGraphView
,
VmList
,
VmMassDelete
,
VmMigrateView
,
VmDetailVncTokenView
,
VmGraphView
,
VmList
,
VmMassDelete
,
VmRenewView
,
DiskRemoveView
,
get_disk_download_status
,
InterfaceDeleteView
,
GroupRemoveAclUserView
,
GroupRemoveAclGroupView
,
GroupRemoveUserView
,
GroupRemoveFutureUserView
,
...
...
@@ -83,8 +83,6 @@ urlpatterns = patterns(
url
(
r'^vm/mass-delete/'
,
VmMassDelete
.
as_view
(),
name
=
'dashboard.view.mass-delete-vm'
),
url
(
r'^vm/(?P<pk>\d+)/activity/$'
,
vm_activity
),
url
(
r'^vm/(?P<pk>\d+)/migrate/$'
,
VmMigrateView
.
as_view
(),
name
=
'dashboard.views.vm-migrate'
),
url
(
r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$'
,
VmRenewView
.
as_view
(),
name
=
'dashboard.views.vm-renew'
),
url
(
r'^vm/activity/(?P<pk>\d+)/$'
,
InstanceActivityDetail
.
as_view
(),
...
...
circle/dashboard/views.py
View file @
a8fb5a86
...
...
@@ -244,6 +244,8 @@ class VmDetailVncTokenView(CheckedDetailView):
self
.
object
=
self
.
get_object
()
if
not
self
.
object
.
has_level
(
request
.
user
,
'operator'
):
raise
PermissionDenied
()
if
not
request
.
user
.
has_perm
(
'vm.access_console'
):
raise
PermissionDenied
()
if
self
.
object
.
node
:
with
instance_activity
(
code_suffix
=
'console-accessed'
,
instance
=
self
.
object
,
user
=
request
.
user
,
...
...
@@ -294,13 +296,14 @@ class VmDetailView(CheckedDetailView):
if
self
.
request
.
user
.
is_superuser
:
context
[
'traits_form'
]
=
TraitsForm
(
instance
=
instance
)
context
[
'raw_data_form'
]
=
RawDataForm
(
instance
=
instance
)
# resources change perm
context
[
'can_change_resources'
]
=
self
.
request
.
user
.
has_perm
(
"vm.change_resources"
)
return
context
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
if
(
request
.
POST
.
get
(
'ram-size'
)
and
request
.
POST
.
get
(
'cpu-count'
)
and
request
.
POST
.
get
(
'cpu-priority'
)):
return
self
.
__set_resources
(
request
)
options
=
{
'change_password'
:
self
.
__change_password
,
'new_name'
:
self
.
__set_name
,
...
...
@@ -328,33 +331,6 @@ class VmDetailView(CheckedDetailView):
return
redirect
(
reverse_lazy
(
"dashboard.views.detail"
,
kwargs
=
{
'pk'
:
self
.
object
.
pk
}))
def
__set_resources
(
self
,
request
):
self
.
object
=
self
.
get_object
()
if
not
self
.
object
.
has_level
(
request
.
user
,
'owner'
):
raise
PermissionDenied
()
if
not
request
.
user
.
has_perm
(
'vm.change_resources'
):
raise
PermissionDenied
()
resources
=
{
'num_cores'
:
request
.
POST
.
get
(
'cpu-count'
),
'ram_size'
:
request
.
POST
.
get
(
'ram-size'
),
'max_ram_size'
:
request
.
POST
.
get
(
'ram-size'
),
# TODO: max_ram
'priority'
:
request
.
POST
.
get
(
'cpu-priority'
)
}
Instance
.
objects
.
filter
(
pk
=
self
.
object
.
pk
)
.
update
(
**
resources
)
success_message
=
_
(
"Resources successfully updated."
)
if
request
.
is_ajax
():
response
=
{
'message'
:
success_message
}
return
HttpResponse
(
json
.
dumps
(
response
),
content_type
=
"application/json"
)
else
:
messages
.
success
(
request
,
success_message
)
return
redirect
(
reverse_lazy
(
"dashboard.views.detail"
,
kwargs
=
{
'pk'
:
self
.
object
.
pk
}))
def
__set_name
(
self
,
request
):
self
.
object
=
self
.
get_object
()
if
not
self
.
object
.
has_level
(
request
.
user
,
'owner'
):
...
...
@@ -606,8 +582,9 @@ class VmOperationView(OperationView):
model
=
Instance
context_object_name
=
'instance'
# much simpler to mock object
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
resp
=
super
(
VmOperationView
,
self
)
.
post
(
request
,
*
args
,
**
kwargs
)
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
resp
=
super
(
VmOperationView
,
self
)
.
post
(
request
,
extra
,
*
args
,
**
kwargs
)
if
request
.
is_ajax
():
store
=
messages
.
get_messages
(
request
)
store
.
used
=
True
...
...
@@ -699,6 +676,29 @@ class VmSaveView(FormOperationMixin, VmOperationView):
effect
=
'info'
form_class
=
VmSaveForm
class
VmResourcesChangeView
(
VmOperationView
):
op
=
'resources_change'
icon
=
"save"
show_in_toolbar
=
False
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
if
extra
is
None
:
extra
=
{}
resources
=
{
'num_cores'
:
"cpu-count"
,
'priority'
:
"cpu-priority"
,
'ram_size'
:
"ram-size"
,
"max_ram_size"
:
"ram-size"
,
# TODO
}
for
k
,
v
in
resources
.
iteritems
():
extra
[
k
]
=
request
.
POST
.
get
(
v
)
return
super
(
VmResourcesChangeView
,
self
)
.
post
(
request
,
extra
,
*
args
,
**
kwargs
)
vm_ops
=
OrderedDict
([
(
'deploy'
,
VmOperationView
.
factory
(
op
=
'deploy'
,
icon
=
'play'
,
effect
=
'success'
)),
...
...
@@ -1012,7 +1012,7 @@ class GroupAclUpdateView(AclUpdateView):
kwargs
=
self
.
kwargs
))
class
TemplateChoose
(
TemplateView
):
class
TemplateChoose
(
LoginRequiredMixin
,
TemplateView
):
def
get_template_names
(
self
):
if
self
.
request
.
is_ajax
():
...
...
@@ -1045,6 +1045,9 @@ class TemplateChoose(TemplateView):
else
:
template
=
get_object_or_404
(
InstanceTemplate
,
pk
=
template
)
if
not
template
.
has_level
(
request
.
user
,
"user"
):
raise
PermissionDenied
()
instance
=
Instance
.
create_from_template
(
template
=
template
,
owner
=
request
.
user
,
is_base
=
True
)
...
...
@@ -1072,7 +1075,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return
context
def
get
(
self
,
*
args
,
**
kwargs
):
if
not
self
.
request
.
user
.
has_perm
(
'vm.create_template'
):
if
not
self
.
request
.
user
.
has_perm
(
'vm.create_
base_
template'
):
raise
PermissionDenied
()
return
super
(
TemplateCreate
,
self
)
.
get
(
*
args
,
**
kwargs
)
...
...
@@ -1083,7 +1086,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return
kwargs
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
if
not
self
.
request
.
user
.
has_perm
(
'vm.create_template'
):
if
not
self
.
request
.
user
.
has_perm
(
'vm.create_
base_
template'
):
raise
PermissionDenied
()
form
=
self
.
form_class
(
request
.
POST
,
user
=
request
.
user
)
...
...
@@ -1105,8 +1108,6 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return
redirect
(
"
%
s#resources"
%
inst
.
get_absolute_url
())
return
super
(
TemplateCreate
,
self
)
.
post
(
self
,
request
,
args
,
kwargs
)
def
__create_networks
(
self
,
vlans
,
user
):
networks
=
[]
for
v
in
vlans
:
...
...
@@ -1167,12 +1168,6 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
template
=
self
.
get_object
()
if
not
template
.
has_level
(
request
.
user
,
'owner'
):
raise
PermissionDenied
()
for
disk
in
self
.
get_object
()
.
disks
.
all
():
if
not
disk
.
has_level
(
request
.
user
,
'user'
):
raise
PermissionDenied
()
for
network
in
self
.
get_object
()
.
interface_set
.
all
():
if
not
network
.
vlan
.
has_level
(
request
.
user
,
"user"
):
raise
PermissionDenied
()
return
super
(
TemplateDetail
,
self
)
.
post
(
self
,
request
,
args
,
kwargs
)
def
get_form_kwargs
(
self
):
...
...
@@ -1546,6 +1541,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
return
[
'dashboard/nojs-wrapper.html'
]
def
get
(
self
,
request
,
form
=
None
,
*
args
,
**
kwargs
):
if
not
request
.
user
.
has_perm
(
'vm.create_vm'
):
raise
PermissionDenied
()
form_error
=
form
is
not
None
template
=
(
form
.
template
.
pk
if
form_error
else
request
.
GET
.
get
(
"template"
))
...
...
@@ -1651,6 +1649,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
user
=
request
.
user
if
not
request
.
user
.
has_perm
(
'vm.create_vm'
):
raise
PermissionDenied
()
# limit chekcs
try
:
limit
=
user
.
profile
.
instance_limit
...
...
circle/storage/models.py
View file @
a8fb5a86
...
...
@@ -106,6 +106,9 @@ class Disk(AclBase, TimeStampedModel):
ordering
=
[
'name'
]
verbose_name
=
_
(
'disk'
)
verbose_name_plural
=
_
(
'disks'
)
permissions
=
(
(
'create_empty_disk'
,
_
(
'Can create an empty disk.'
)),
(
'download_disk'
,
_
(
'Can download a disk.'
)))
class
WrongDiskTypeError
(
Exception
):
...
...
circle/vm/models/instance.py
View file @
a8fb5a86
...
...
@@ -151,6 +151,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
ordering
=
(
'name'
,
)
permissions
=
(
(
'create_template'
,
_
(
'Can create an instance template.'
)),
(
'create_base_template'
,
_
(
'Can create an instance template (base).'
)),
(
'change_template_resources'
,
_
(
'Can change resources of a template.'
)),
)
verbose_name
=
_
(
'template'
)
verbose_name_plural
=
_
(
'templates'
)
...
...
@@ -263,6 +267,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
(
'access_console'
,
_
(
'Can access the graphical console of a VM.'
)),
(
'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.'
)),
(
'config_ports'
,
_
(
'Can configure port forwards.'
)),
(
'recover'
,
_
(
'Can recover a destroyed VM.'
)),
)
...
...
circle/vm/operations.py
View file @
a8fb5a86
...
...
@@ -42,6 +42,7 @@ class InstanceOperation(Operation):
acl_level
=
'owner'
async_operation
=
abortable_async_instance_operation
host_cls
=
Instance
concurrency_check
=
True
def
__init__
(
self
,
instance
):
super
(
InstanceOperation
,
self
)
.
__init__
(
subject
=
instance
)
...
...
@@ -73,7 +74,7 @@ class InstanceOperation(Operation):
else
:
return
InstanceActivity
.
create
(
code_suffix
=
self
.
activity_code_suffix
,
instance
=
self
.
instance
,
user
=
user
)
user
=
user
,
concurrency_check
=
self
.
concurrency_check
)
def
is_preferred
(
self
):
"""If this is the recommended op in the current state of the instance.
...
...
@@ -87,6 +88,7 @@ class AddInterfaceOperation(InstanceOperation):
name
=
_
(
"add interface"
)
description
=
_
(
"Add a new network interface for the specified VLAN to "
"the VM."
)
required_perms
=
()
def
_operation
(
self
,
activity
,
user
,
system
,
vlan
,
managed
=
None
):
if
managed
is
None
:
...
...
@@ -109,6 +111,7 @@ class CreateDiskOperation(InstanceOperation):
id
=
'create_disk'
name
=
_
(
"create disk"
)
description
=
_
(
"Create empty disk for the VM."
)
required_perms
=
(
'storage.create_empty_disk'
,
)
def
check_precond
(
self
):
super
(
CreateDiskOperation
,
self
)
.
check_precond
()
...
...
@@ -123,6 +126,7 @@ class CreateDiskOperation(InstanceOperation):
if
not
name
:
name
=
"new disk"
disk
=
Disk
.
create
(
size
=
size
,
name
=
name
,
type
=
"qcow2-norm"
)
disk
.
full_clean
()
self
.
instance
.
disks
.
add
(
disk
)
register_operation
(
CreateDiskOperation
)
...
...
@@ -135,6 +139,7 @@ class DownloadDiskOperation(InstanceOperation):
description
=
_
(
"Download disk for the VM."
)
abortable
=
True
has_percentage
=
True
required_perms
=
(
'storage.download_disk'
,
)
def
check_precond
(
self
):
super
(
DownloadDiskOperation
,
self
)
.
check_precond
()
...
...
@@ -148,6 +153,7 @@ class DownloadDiskOperation(InstanceOperation):
from
storage.models
import
Disk
disk
=
Disk
.
download
(
url
=
url
,
name
=
name
,
task
=
task
)
disk
.
full_clean
()
self
.
instance
.
disks
.
add
(
disk
)
register_operation
(
DownloadDiskOperation
)
...
...
@@ -158,6 +164,12 @@ class DeployOperation(InstanceOperation):
id
=
'deploy'
name
=
_
(
"deploy"
)
description
=
_
(
"Deploy new virtual machine with network."
)
required_perms
=
()
def
check_precond
(
self
):
super
(
DeployOperation
,
self
)
.
check_precond
()
if
self
.
instance
.
status
in
[
'RUNNING'
,
'SUSPENDED'
]:
raise
self
.
instance
.
WrongStateError
(
self
.
instance
)
def
is_preferred
(
self
):
return
self
.
instance
.
status
in
(
self
.
instance
.
STATUS
.
STOPPED
,
...
...
@@ -198,6 +210,7 @@ class DestroyOperation(InstanceOperation):
id
=
'destroy'
name
=
_
(
"destroy"
)
description
=
_
(
"Destroy virtual machine and its networks."
)
required_perms
=
()
def
on_commit
(
self
,
activity
):
activity
.
resultant_state
=
'DESTROYED'
...
...
@@ -239,11 +252,23 @@ class MigrateOperation(InstanceOperation):
id
=
'migrate'
name
=
_
(
"migrate"
)
description
=
_
(
"Live migrate running VM to another node."
)
required_perms
=
()
def
rollback
(
self
,
activity
):
with
activity
.
sub_activity
(
'rollback_net'
):
self
.
instance
.
deploy_net
()
def
check_precond
(
self
):
super
(
MigrateOperation
,
self
)
.
check_precond
()
if
self
.
instance
.
status
not
in
[
'RUNNING'
]:
raise
self
.
instance
.
WrongStateError
(
self
.
instance
)
def
check_auth
(
self
,
user
):
if
not
user
.
is_superuser
:
raise
PermissionDenied
()
super
(
MigrateOperation
,
self
)
.
check_auth
(
user
=
user
)
def
_operation
(
self
,
activity
,
to_node
=
None
,
timeout
=
120
):
if
not
to_node
:
with
activity
.
sub_activity
(
'scheduling'
)
as
sa
:
...
...
@@ -278,6 +303,12 @@ class RebootOperation(InstanceOperation):
id
=
'reboot'
name
=
_
(
"reboot"
)
description
=
_
(
"Reboot virtual machine with Ctrl+Alt+Del signal."
)
required_perms
=
()
def
check_precond
(
self
):
super
(
RebootOperation
,
self
)
.
check_precond
()
if
self
.
instance
.
status
not
in
[
'RUNNING'
]:
raise
self
.
instance
.
WrongStateError
(
self
.
instance
)
def
_operation
(
self
,
timeout
=
5
):
self
.
instance
.
reboot_vm
(
timeout
=
timeout
)
...
...
@@ -291,6 +322,7 @@ class RemoveInterfaceOperation(InstanceOperation):
id
=
'remove_interface'
name
=
_
(
"remove interface"
)
description
=
_
(
"Remove the specified network interface from the VM."
)
required_perms
=
()
def
_operation
(
self
,
activity
,
user
,
system
,
interface
):
if
self
.
instance
.
is_running
:
...
...
@@ -308,6 +340,7 @@ class RemoveDiskOperation(InstanceOperation):
id
=
'remove_disk'
name
=
_
(
"remove disk"
)
description
=
_
(
"Remove the specified disk from the VM."
)
required_perms
=
()
def
check_precond
(
self
):
super
(
RemoveDiskOperation
,
self
)
.
check_precond
()
...
...
@@ -328,6 +361,12 @@ class ResetOperation(InstanceOperation):
id
=
'reset'
name
=
_
(
"reset"
)
description
=
_
(
"Reset virtual machine (reset button)."
)
required_perms
=
()
def
check_precond
(
self
):
super
(
ResetOperation
,
self
)
.
check_precond
()
if
self
.
instance
.
status
not
in
[
'RUNNING'
]:
raise
self
.
instance
.
WrongStateError
(
self
.
instance
)
def
_operation
(
self
,
timeout
=
5
):
self
.
instance
.
reset_vm
(
timeout
=
timeout
)
...
...
@@ -345,6 +384,7 @@ class SaveAsTemplateOperation(InstanceOperation):
Users can instantiate Virtual Machines from Templates.
"""
)
abortable
=
True
required_perms
=
(
'vm.create_template'
,
)
def
is_preferred
(
self
):
return
(
self
.
instance
.
is_base
and
...
...
@@ -365,6 +405,11 @@ class SaveAsTemplateOperation(InstanceOperation):
for
disk
in
self
.
disks
:
disk
.
destroy
()
def
check_precond
(
self
):
super
(
SaveAsTemplateOperation
,
self
)
.
check_precond
()
if
self
.
instance
.
status
not
in
[
'RUNNING'
,
'PENDING'
,
'STOPPED'
]:
raise
self
.
instance
.
WrongStateError
(
self
.
instance
)
def
_operation
(
self
,
activity
,
user
,
system
,
timeout
=
300
,
name
=
None
,
with_shutdown
=
True
,
task
=
None
,
**
kwargs
):
if
with_shutdown
:
...
...
@@ -435,6 +480,7 @@ class ShutdownOperation(InstanceOperation):
name
=
_
(
"shutdown"
)
description
=
_
(
"Shutdown virtual machine with ACPI signal."
)
abortable
=
True
required_perms
=
()
def
check_precond
(
self
):
super
(
ShutdownOperation
,
self
)
.
check_precond
()
...
...
@@ -458,6 +504,12 @@ class ShutOffOperation(InstanceOperation):
id
=
'shut_off'
name
=
_
(
"shut off"
)
description
=
_
(
"Shut off VM (plug-out)."
)
required_perms
=
()
def
check_precond
(
self
):
super
(
ShutOffOperation
,
self
)
.
check_precond
()
if
self
.
instance
.
status
not
in
[
'RUNNING'
]:
raise
self
.
instance
.
WrongStateError
(
self
.
instance
)
def
on_commit
(
self
,
activity
):
activity
.
resultant_state
=
'STOPPED'
...
...
@@ -484,6 +536,7 @@ class SleepOperation(InstanceOperation):
id
=
'sleep'
name
=
_
(
"sleep"
)
description
=
_
(
"Suspend virtual machine with memory dump."
)
required_perms
=
()
def
is_preferred
(
self
):
return
(
not
self
.
instance
.
is_base
and
...
...
@@ -527,6 +580,7 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump.
"""
)
required_perms
=
()
def
is_preferred
(
self
):
return
(
self
.
instance
.
is_base
and
...
...
@@ -593,6 +647,7 @@ class FlushOperation(NodeOperation):
id
=
'flush'
name
=
_
(
"flush"
)
description
=
_
(
"Disable node and move all instances to other ones."
)
required_perms
=
()
def
on_abort
(
self
,
activity
,
error
):
from
manager.scheduler
import
TraitsUnsatisfiableException
...
...
@@ -600,6 +655,12 @@ class FlushOperation(NodeOperation):
if
self
.
node_enabled
:
self
.
node
.
enable
(
activity
.
user
,
activity
)
def
check_auth
(
self
,
user
):
if
not
user
.
is_superuser
:
raise
PermissionDenied
()
super
(
FlushOperation
,
self
)
.
check_auth
(
user
=
user
)
def
_operation
(
self
,
activity
,
user
):
self
.
node_enabled
=
self
.
node
.
enabled
self
.
node
.
disable
(
user
,
activity
)
...
...
@@ -617,6 +678,7 @@ class ScreenshotOperation(InstanceOperation):
name
=
_
(
"screenshot"
)
description
=
_
(
"Get screenshot"
)
acl_level
=
"owner"
required_perms
=
()
def
check_precond
(
self
):
super
(
ScreenshotOperation
,
self
)
.
check_precond
()
...
...
@@ -655,3 +717,31 @@ class RecoverOperation(InstanceOperation):
register_operation
(
RecoverOperation
)
class
ResourcesOperation
(
InstanceOperation
):
activity_code_suffix
=
'Resources change'
id
=
'resources_change'
name
=
_
(
"resources change"
)
description
=
_
(
"Change resources"
)
acl_level
=
"owner"
concurrency_check
=
False
required_perms
=
(
'vm.change_resources'
,
)
def
check_precond
(
self
):
super
(
ResourcesOperation
,
self
)
.
check_precond
()
if
self
.
instance
.
status
not
in
[
"STOPPED"
,
"PENDING"
]:
raise
self
.
instance
.
WrongStateError
(
self
.
instance
)
def
_operation
(
self
,
user
,
num_cores
,
ram_size
,
max_ram_size
,
priority
):
self
.
instance
.
num_cores
=
num_cores
self
.
instance
.
ram_size
=
ram_size
self
.
instance
.
max_ram_size
=
max_ram_size
self
.
instance
.
priority
=
priority
self
.
instance
.
full_clean
()
self
.
instance
.
save
()
register_operation
(
ResourcesOperation
)
circle/vm/tests/test_models.py
View file @
a8fb5a86
...
...
@@ -103,6 +103,7 @@ class InstanceTestCase(TestCase):
inst
=
Mock
(
destroyed_at
=
None
,
spec
=
Instance
)
inst
.
interface_set
.
all
.
return_value
=
[]
inst
.
node
=
MagicMock
(
spec
=
Node
)
inst
.
status
=
'RUNNING'
migrate_op
=
MigrateOperation
(
inst
)
with
patch
(
'vm.models.instance.vm_tasks.migrate'
)
as
migr
:
act
=
MagicMock
()
...
...
@@ -118,6 +119,7 @@ class InstanceTestCase(TestCase):
inst
=
MagicMock
(
destroyed_at
=
None
,
spec
=
Instance
)
inst
.
interface_set
.
all
.
return_value
=
[]
inst
.
node
=
MagicMock
(
spec
=
Node
)
inst
.
status
=
'RUNNING'
migrate_op
=
MigrateOperation
(
inst
)
with
patch
(
'vm.models.instance.vm_tasks.migrate'
)
as
migr
:
inst
.
select_node
.
side_effect
=
AssertionError
...
...
@@ -133,6 +135,7 @@ class InstanceTestCase(TestCase):
inst
=
Mock
(
destroyed_at
=
None
,
spec
=
Instance
)
inst
.
interface_set
.
all
.
return_value
=
[]
inst
.
node
=
MagicMock
(
spec
=
Node
)
inst
.
status
=
'RUNNING'
e
=
Exception
(
'abc'
)
setattr
(
e
,
'libvirtError'
,
''
)
inst
.
migrate_vm
.
side_effect
=
e
...
...
@@ -372,6 +375,7 @@ class InstanceActivityTestCase(TestCase):
node
=
MagicMock
(
spec
=
Node
,
enabled
=
True
)
node
.
instance_set
.
all
.
return_value
=
insts
user
=
MagicMock
(
spec
=
User
)
user
.
is_superuser
=
MagicMock
(
return_value
=
True
)
flush_op
=
FlushOperation
(
node
)
with
patch
.
object
(
FlushOperation
,
'create_activity'
)
as
create_act
:
...
...
@@ -383,6 +387,7 @@ class InstanceActivityTestCase(TestCase):
node
.
disable
.
assert_called_with
(
user
,
act
)
for
i
in
insts
:
i
.
migrate
.
assert_called
()
user
.
is_superuser
.
assert_called
()
def
test_flush_disabled_wo_user
(
self
):
insts
=
[
MagicMock
(
spec
=
Instance
,
migrate
=
MagicMock
()),
...
...
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