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
d0dc1ed8
authored
Jul 14, 2014
by
Őry Máté
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into issue-139
Conflicts: circle/vm/models/activity.py
parents
06663c9e
2ca7eb56
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
777 additions
and
392 deletions
+777
-392
circle/acl/management/__init__.py
+3
-3
circle/circle/settings/test.py
+3
-2
circle/common/operations.py
+2
-0
circle/dashboard/forms.py
+24
-0
circle/dashboard/templates/dashboard/lease-edit.html
+80
-1
circle/dashboard/templates/dashboard/template-list.html
+2
-0
circle/dashboard/templates/dashboard/vm-detail/home.html
+6
-1
circle/dashboard/tests/test_mockedviews.py
+208
-10
circle/dashboard/tests/test_views.py
+1
-99
circle/dashboard/urls.py
+4
-3
circle/dashboard/views.py
+183
-180
circle/vm/models/activity.py
+2
-0
circle/vm/models/common.py
+16
-2
circle/vm/models/instance.py
+6
-31
circle/vm/operations.py
+21
-2
docs/deploy.rst
+145
-2
docs/install.rst
+70
-55
requirements/base.txt
+1
-1
No files found.
circle/acl/management/__init__.py
View file @
d0dc1ed8
...
@@ -59,10 +59,10 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
...
@@ -59,10 +59,10 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
]
]
Level
.
objects
.
using
(
db
)
.
bulk_create
(
levels
)
Level
.
objects
.
using
(
db
)
.
bulk_create
(
levels
)
if
verbosity
>=
2
:
if
verbosity
>=
2
:
print
(
"Adding levels [
%
s]."
%
", "
.
join
(
levels
))
print
(
"Adding levels [
%
s]."
%
", "
.
join
(
unicode
(
l
)
for
l
in
levels
))
print
(
"Searched: [
%
s]."
%
", "
.
join
(
print
(
"Searched: [
%
s]."
%
", "
.
join
(
[
unicode
(
l
)
for
l
in
searched_levels
]
))
unicode
(
l
)
for
l
in
searched_levels
))
print
(
"All: [
%
s]."
%
", "
.
join
(
[
unicode
(
l
)
for
l
in
all_levels
]
))
print
(
"All: [
%
s]."
%
", "
.
join
(
unicode
(
l
)
for
l
in
all_levels
))
# set weights
# set weights
for
ctype
,
codename
,
weight
in
level_weights
:
for
ctype
,
codename
,
weight
in
level_weights
:
...
...
circle/circle/settings/test.py
View file @
d0dc1ed8
...
@@ -46,8 +46,9 @@ CACHES = {
...
@@ -46,8 +46,9 @@ CACHES = {
LOGGING
[
'loggers'
][
'djangosaml2'
]
=
{
'handlers'
:
[
'console'
],
LOGGING
[
'loggers'
][
'djangosaml2'
]
=
{
'handlers'
:
[
'console'
],
'level'
:
'CRITICAL'
}
'level'
:
'CRITICAL'
}
LOGGING
[
'handlers'
][
'console'
]
=
{
'level'
:
'WARNING'
,
level
=
environ
.
get
(
'LOGLEVEL'
,
'CRITICAL'
)
LOGGING
[
'handlers'
][
'console'
]
=
{
'level'
:
level
,
'class'
:
'logging.StreamHandler'
,
'class'
:
'logging.StreamHandler'
,
'formatter'
:
'simple'
}
'formatter'
:
'simple'
}
for
i
in
LOCAL_APPS
:
for
i
in
LOCAL_APPS
:
LOGGING
[
'loggers'
][
i
]
=
{
'handlers'
:
[
'console'
],
'level'
:
'CRITICAL'
}
LOGGING
[
'loggers'
][
i
]
=
{
'handlers'
:
[
'console'
],
'level'
:
level
}
circle/common/operations.py
View file @
d0dc1ed8
...
@@ -59,6 +59,8 @@ class Operation(object):
...
@@ -59,6 +59,8 @@ class Operation(object):
skip_auth_check
=
auxargs
.
pop
(
'system'
)
skip_auth_check
=
auxargs
.
pop
(
'system'
)
user
=
auxargs
.
pop
(
'user'
)
user
=
auxargs
.
pop
(
'user'
)
parent_activity
=
auxargs
.
pop
(
'parent_activity'
)
parent_activity
=
auxargs
.
pop
(
'parent_activity'
)
if
parent_activity
and
user
is
None
and
not
skip_auth_check
:
user
=
parent_activity
.
user
# check for unexpected keyword arguments
# check for unexpected keyword arguments
argspec
=
getargspec
(
self
.
_operation
)
argspec
=
getargspec
(
self
.
_operation
)
...
...
circle/dashboard/forms.py
View file @
d0dc1ed8
...
@@ -612,6 +612,9 @@ class TemplateForm(forms.ModelForm):
...
@@ -612,6 +612,9 @@ class TemplateForm(forms.ModelForm):
self
.
instance
.
ram_size
=
512
self
.
instance
.
ram_size
=
512
self
.
instance
.
num_cores
=
2
self
.
instance
.
num_cores
=
2
self
.
fields
[
"lease"
]
.
queryset
=
Lease
.
get_objects_with_level
(
"operator"
,
self
.
user
)
def
clean_owner
(
self
):
def
clean_owner
(
self
):
if
self
.
instance
.
pk
is
not
None
:
if
self
.
instance
.
pk
is
not
None
:
return
User
.
objects
.
get
(
pk
=
self
.
instance
.
owner
.
pk
)
return
User
.
objects
.
get
(
pk
=
self
.
instance
.
owner
.
pk
)
...
@@ -888,6 +891,27 @@ class LeaseForm(forms.ModelForm):
...
@@ -888,6 +891,27 @@ class LeaseForm(forms.ModelForm):
model
=
Lease
model
=
Lease
class
VmRenewForm
(
forms
.
Form
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
choices
=
kwargs
.
pop
(
'choices'
)
default
=
kwargs
.
pop
(
'default'
)
super
(
VmRenewForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
'lease'
]
=
forms
.
ModelChoiceField
(
queryset
=
choices
,
initial
=
default
,
required
=
True
,
label
=
_
(
'Length'
))
if
len
(
choices
)
<
2
:
self
.
fields
[
'lease'
]
.
widget
=
HiddenInput
()
@property
def
helper
(
self
):
helper
=
FormHelper
(
self
)
helper
.
form_tag
=
False
return
helper
class
VmCreateDiskForm
(
forms
.
Form
):
class
VmCreateDiskForm
(
forms
.
Form
):
name
=
forms
.
CharField
(
max_length
=
100
,
label
=
_
(
"Name"
))
name
=
forms
.
CharField
(
max_length
=
100
,
label
=
_
(
"Name"
))
size
=
forms
.
CharField
(
size
=
forms
.
CharField
(
...
...
circle/dashboard/templates/dashboard/lease-edit.html
View file @
d0dc1ed8
...
@@ -6,7 +6,7 @@
...
@@ -6,7 +6,7 @@
{% block content %}
{% block content %}
<div
class=
"row"
>
<div
class=
"row"
>
<div
class=
"col-md-
12
"
>
<div
class=
"col-md-
7
"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<div
class=
"panel-heading"
>
<a
class=
"pull-right btn btn-default btn-xs"
href=
"{% url "
dashboard
.
views
.
template-list
"
%}"
>
{% trans "Back" %}
</a>
<a
class=
"pull-right btn btn-default btn-xs"
href=
"{% url "
dashboard
.
views
.
template-list
"
%}"
>
{% trans "Back" %}
</a>
...
@@ -20,6 +20,85 @@
...
@@ -20,6 +20,85 @@
</div>
</div>
</div>
</div>
</div>
</div>
<div
class=
"col-md-5"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<h4
class=
"no-margin"
><i
class=
"icon-group"
></i>
{% trans "Manage access" %}
</h4>
</div>
<div
class=
"panel-body"
>
<form
action=
"{% url "
dashboard
.
views
.
lease-acl
"
pk=
object.pk
%}"
method=
"post"
>
{% csrf_token %}
<table
class=
"table table-striped table-with-form-fields"
id=
"template-access-table"
>
<thead>
<tr>
<th></th>
<th>
{% trans "Who" %}
</th>
<th>
{% trans "What" %}
</th>
<th><i
class=
"icon-remove"
></i></th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i
class=
"icon-user"
></i>
</td>
<td>
<a
href=
"{% url "
dashboard
.
views
.
profile
"
username=
i.user.username
%}"
title=
"{{ i.user.username }}"
>
{% include "dashboard/_display-name.html" with user=i.user show_org=True %}
</a>
</td>
<td>
<select
class=
"form-control"
name=
"perm-u-{{i.user.id}}"
>
{% for id, name in acl.levels %}
<option
{%
if
id =
i.level%}
selected=
"selected"
{%
endif
%}
value=
"{{id}}"
>
{{name}}
</option>
{% endfor %}
</select>
</td>
<td>
<input
type=
"checkbox"
name=
"remove-u-{{i.user.id}}"
title=
"{% trans "
Remove
"
%}"
/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i
class=
"icon-group"
></i></td>
<td>
<a
href=
"{% url "
dashboard
.
views
.
group-detail
"
pk=
i.group.pk
%}"
>
{{i.group}}
</a>
</td>
<td>
<select
class=
"form-control"
name=
"perm-g-{{i.group.id}}"
>
{% for id, name in acl.levels %}
<option
{%
if
id =
i.level%}
selected=
"selected"
{%
endif
%}
value=
"{{id}}"
>
{{name}}
</option>
{% endfor %}
</select>
</td>
<td>
<input
type=
"checkbox"
name=
"remove-g-{{i.group.id}}"
title=
"{% trans "
Remove
"
%}"
/>
</td>
</tr>
{% endfor %}
<tr><td><i
class=
"icon-plus"
></i></td>
<td><input
type=
"text"
class=
"form-control"
name=
"perm-new-name"
placeholder=
"{% trans "
Name
of
group
or
user
"
%}"
></td>
<td><select
class=
"form-control"
name=
"perm-new"
>
{% for id, name in acl.levels %}
<option
value=
"{{id}}"
>
{{name}}
</option>
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div
class=
"form-actions"
>
<button
type=
"submit"
class=
"btn btn-success"
>
{% trans "Save" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}
circle/dashboard/templates/dashboard/template-list.html
View file @
d0dc1ed8
...
@@ -26,9 +26,11 @@
...
@@ -26,9 +26,11 @@
<div
class=
"col-md-6"
>
<div
class=
"col-md-6"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<div
class=
"panel-heading"
>
{% if perms.vm.create_leases %}
<a
href=
"{% url "
dashboard
.
views
.
lease-create
"
%}"
class=
"pull-right btn btn-success btn-xs"
style=
"margin-right: 10px;"
>
<a
href=
"{% url "
dashboard
.
views
.
lease-create
"
%}"
class=
"pull-right btn btn-success btn-xs"
style=
"margin-right: 10px;"
>
<i
class=
"icon-plus"
></i>
{% trans "new lease" %}
<i
class=
"icon-plus"
></i>
{% trans "new lease" %}
</a>
</a>
{% endif %}
<h3
class=
"no-margin"
><i
class=
"icon-time"
></i>
{% trans "Leases" %}
</h3>
<h3
class=
"no-margin"
><i
class=
"icon-time"
></i>
{% trans "Leases" %}
</h3>
</div>
</div>
<div
class=
"panel-body"
>
<div
class=
"panel-body"
>
...
...
circle/dashboard/templates/dashboard/vm-detail/home.html
View file @
d0dc1ed8
...
@@ -47,7 +47,12 @@
...
@@ -47,7 +47,12 @@
</dl>
</dl>
<h4>
{% trans "Expiration" %} {% if instance.is_expiring %}
<i
class=
"icon-warning-sign text-danger"
></i>
{% endif %}
<h4>
{% trans "Expiration" %} {% if instance.is_expiring %}
<i
class=
"icon-warning-sign text-danger"
></i>
{% endif %}
<a
href=
"{% url "
dashboard
.
views
.
vm-renew
"
instance
.
pk
""
%}"
class=
"btn btn-success btn-xs pull-right"
>
{% trans "renew" %}
</a>
{% with op=op.renew %}
<a
href=
"{{op.get_url}}"
class=
"btn btn-success btn-xs
operation operation-{{op.op}} btn btn-default"
>
<i
class=
"icon-{{op.icon}}"
></i>
{{op.name}}
</a>
{% endwith %}
</h4>
</h4>
<dl>
<dl>
<dt>
{% trans "Suspended at:" %}
</dt>
<dt>
{% trans "Suspended at:" %}
</dt>
...
...
circle/dashboard/tests/test_mockedviews.py
View file @
d0dc1ed8
...
@@ -16,13 +16,15 @@
...
@@ -16,13 +16,15 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import
unittest
import
unittest
import
warnings
from
factory
import
Factory
,
Sequence
from
factory
import
Factory
,
Sequence
from
mock
import
patch
,
MagicMock
from
mock
import
patch
,
MagicMock
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.core.exceptions
import
PermissionDenied
from
django.core.exceptions
import
PermissionDenied
from
django.core.signing
import
TimestampSigner
,
JSONSerializer
,
b64_encode
from
django.core.signing
import
TimestampSigner
,
JSONSerializer
,
b64_encode
from
django.http
import
HttpRequest
,
Http404
from
django.http
import
HttpRequest
,
Http404
,
QueryDict
from
django.utils
import
baseconv
from
django.utils
import
baseconv
from
..models
import
Profile
from
..models
import
Profile
...
@@ -142,7 +144,7 @@ class VmOperationViewTestCase(unittest.TestCase):
...
@@ -142,7 +144,7 @@ class VmOperationViewTestCase(unittest.TestCase):
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
def
test_migrate
(
self
):
def
test_migrate
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
})
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
}
,
superuser
=
True
)
view
=
vm_ops
[
'migrate'
]
view
=
vm_ops
[
'migrate'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
...
@@ -176,7 +178,24 @@ class VmOperationViewTestCase(unittest.TestCase):
...
@@ -176,7 +178,24 @@ class VmOperationViewTestCase(unittest.TestCase):
assert
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
]
assert
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
]
assert
msg
.
error
.
called
assert
msg
.
error
.
called
def
test_migrate_wo_permission
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'node'
:
1
},
superuser
=
False
)
view
=
vm_ops
[
'migrate'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.get_object_or_404'
)
as
go4
:
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
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
with
self
.
assertRaises
(
PermissionDenied
):
assert
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
]
def
test_migrate_template
(
self
):
def
test_migrate_template
(
self
):
"""check if GET dialog's template can be rendered"""
request
=
FakeRequestFactory
(
superuser
=
True
)
request
=
FakeRequestFactory
(
superuser
=
True
)
view
=
vm_ops
[
'migrate'
]
view
=
vm_ops
[
'migrate'
]
...
@@ -207,7 +226,8 @@ class VmOperationViewTestCase(unittest.TestCase):
...
@@ -207,7 +226,8 @@ class VmOperationViewTestCase(unittest.TestCase):
assert
not
msg
.
error
.
called
assert
not
msg
.
error
.
called
def
test_save_as_w_name
(
self
):
def
test_save_as_w_name
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'name'
:
'foobar'
})
request
=
FakeRequestFactory
(
POST
=
{
'name'
:
'foobar'
},
has_perms_mock
=
True
)
view
=
vm_ops
[
'save_as_template'
]
view
=
vm_ops
[
'save_as_template'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
...
@@ -238,26 +258,204 @@ class VmOperationViewTestCase(unittest.TestCase):
...
@@ -238,26 +258,204 @@ class VmOperationViewTestCase(unittest.TestCase):
self
.
assertEquals
(
rend
.
status_code
,
200
)
self
.
assertEquals
(
rend
.
status_code
,
200
)
def
FakeRequestFactory
(
*
args
,
**
kwargs
):
class
RenewViewTest
(
unittest
.
TestCase
):
def
test_renew_template
(
self
):
request
=
FakeRequestFactory
(
has_perms_mock
=
True
)
view
=
vm_ops
[
'renew'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
name
=
'foo'
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
has_level
.
return_value
=
True
go
.
return_value
=
inst
rend
=
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
self
.
assertEquals
(
rend
.
status_code
,
200
)
def
test_renew_by_owner_wo_param
(
self
):
request
=
FakeRequestFactory
(
POST
=
{},
has_perms_mock
=
True
)
view
=
vm_ops
[
'renew'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.get_object_or_404'
)
as
go4
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
True
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
assert
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
.
status_code
==
200
# success would redirect
def
test_renew_by_owner_w_param
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'length'
:
1
},
has_perms_mock
=
True
)
view
=
vm_ops
[
'renew'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.messages'
)
as
msg
,
\
patch
(
'dashboard.views.get_object_or_404'
)
as
go4
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
True
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
assert
view
.
as_view
()(
request
,
pk
=
1234
)
assert
not
msg
.
error
.
called
def
test_renew_get_by_anon_wo_key
(
self
):
request
=
FakeRequestFactory
(
authenticated
=
False
)
view
=
vm_ops
[
'renew'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.get_object_or_404'
)
as
go4
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
False
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
self
.
assertIn
(
'login'
,
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
])
def
test_renew_get_by_nonowner_wo_key
(
self
):
request
=
FakeRequestFactory
(
has_perms_mock
=
True
)
view
=
vm_ops
[
'renew'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.get_object_or_404'
)
as
go4
:
inst
=
MagicMock
(
spec
=
Instance
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
False
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
with
self
.
assertRaises
(
PermissionDenied
):
assert
view
.
as_view
()(
request
,
pk
=
1234
)
def
test_renew_post_by_nonowner_wo_key
(
self
):
request
=
FakeRequestFactory
(
POST
=
{
'length'
:
1
},
has_perms_mock
=
True
)
view
=
vm_ops
[
'renew'
]
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.get_object_or_404'
)
as
go4
:
inst
=
MagicMock
(
spec
=
Instance
,
pk
=
11
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
False
go
.
return_value
=
inst
go4
.
return_value
=
MagicMock
()
with
self
.
assertRaises
(
PermissionDenied
):
assert
view
.
as_view
()(
request
,
pk
=
1234
)
def
test_renew_get_by_nonowner_w_key
(
self
):
user
=
FakeRequestFactory
(
superuser
=
True
)
.
user
view
=
vm_ops
[
'renew'
]
inst
=
MagicMock
(
spec
=
Instance
,
pk
=
11
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
key
=
view
.
get_token_url
(
inst
,
user
)
.
split
(
'?'
)[
1
]
.
split
(
'='
)[
1
]
request
=
FakeRequestFactory
(
GET
=
{
'k'
:
key
})
# other user!
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.User.objects'
)
as
gu
,
\
patch
(
'dashboard.views.Lease.get_objects_with_level'
)
as
gol
:
gol
.
return_value
=
views
.
Lease
.
objects
.
all
()
gu
.
get
.
return_value
=
user
go
.
return_value
=
inst
assert
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
.
status_code
==
200
def
test_renew_post_by_anon_w_key
(
self
):
user
=
FakeRequestFactory
(
authenticated
=
True
)
.
user
view
=
vm_ops
[
'renew'
]
inst
=
MagicMock
(
spec
=
Instance
,
pk
=
11
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
=
lambda
user
,
level
:
user
.
is_authenticated
()
key
=
view
.
get_token_url
(
inst
,
user
)
.
split
(
'?'
)[
1
]
.
split
(
'='
)[
1
]
request
=
FakeRequestFactory
(
GET
=
{
'k'
:
key
},
authenticated
=
False
)
with
patch
.
object
(
view
,
'get_object'
)
as
go
,
\
patch
(
'dashboard.views.Lease.get_objects_with_level'
)
as
gol
:
go
.
return_value
=
inst
gol
.
return_value
=
views
.
Lease
.
objects
.
all
()
assert
view
.
as_view
()(
request
,
pk
=
1234
)
.
render
()
.
status_code
==
200
def
test_renew_post_by_anon_w_invalid_key
(
self
):
view
=
vm_ops
[
'renew'
]
key
=
"invalid"
inst
=
MagicMock
(
spec
=
Instance
,
pk
=
11
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
False
request
=
FakeRequestFactory
(
GET
=
{
'k'
:
key
},
authenticated
=
False
)
with
patch
.
object
(
view
,
'get_object'
)
as
go
:
go
.
return_value
=
inst
self
.
assertIn
(
'login'
,
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
])
def
test_renew_post_by_anon_w_expired_key
(
self
):
def
side
(
max_age
=
None
,
*
args
,
**
kwargs
):
if
max_age
:
raise
views
.
signing
.
BadSignature
user
=
FakeRequestFactory
(
authenticated
=
False
)
.
user
view
=
vm_ops
[
'renew'
]
inst
=
MagicMock
(
spec
=
Instance
,
pk
=
11
)
inst
.
_meta
.
object_name
=
"Instance"
inst
.
renew
=
Instance
.
_ops
[
'renew'
](
inst
)
inst
.
renew
.
async
=
MagicMock
()
inst
.
has_level
.
return_value
=
False
key
=
view
.
get_token_url
(
inst
,
user
)
.
split
(
'?'
)[
1
]
.
split
(
'='
)[
1
]
with
patch
(
'dashboard.views.signing.loads'
)
as
loader
,
\
patch
.
object
(
view
,
'get_object'
)
as
go
:
loader
.
return_value
=
(
inst
.
pk
,
user
.
pk
)
loader
.
side_effect
=
side
request
=
FakeRequestFactory
(
GET
=
{
'k'
:
key
},
user
=
user
)
go
.
return_value
=
inst
self
.
assertIn
(
'login'
,
view
.
as_view
()(
request
,
pk
=
1234
)[
'location'
])
def
FakeRequestFactory
(
user
=
None
,
**
kwargs
):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client.
mocking out django views; they are MUCH faster than the Django test client.
'''
'''
if
user
is
None
:
user
=
UserFactory
()
user
=
UserFactory
()
user
.
is_authenticated
=
lambda
:
kwargs
.
get
(
'authenticated'
,
True
)
user
.
is_authenticated
=
lambda
:
kwargs
.
pop
(
'authenticated'
,
True
)
user
.
is_superuser
=
kwargs
.
get
(
'superuser'
,
False
)
user
.
is_superuser
=
kwargs
.
pop
(
'superuser'
,
False
)
if
kwargs
.
get
(
'has_perms_mock'
,
False
):
if
kwargs
.
pop
(
'has_perms_mock'
,
False
):
user
.
has_perms
=
MagicMock
(
return_value
=
True
)
user
.
has_perms
=
MagicMock
(
return_value
=
True
)
user
.
save
()
request
=
HttpRequest
()
request
=
HttpRequest
()
request
.
user
=
user
request
.
user
=
user
request
.
session
=
kwargs
.
get
(
'session'
,
{})
request
.
session
=
kwargs
.
pop
(
'session'
,
{})
if
kwargs
.
get
(
'POST'
)
is
not
None
:
if
kwargs
.
get
(
'POST'
)
is
not
None
:
request
.
method
=
'POST'
request
.
method
=
'POST'
request
.
POST
=
kwargs
.
get
(
'POST'
)
request
.
POST
=
QueryDict
(
''
,
mutable
=
True
)
request
.
POST
.
update
(
kwargs
.
pop
(
'POST'
))
else
:
else
:
request
.
method
=
'GET'
request
.
method
=
'GET'
request
.
GET
=
kwargs
.
get
(
'GET'
,
{})
request
.
GET
=
QueryDict
(
''
,
mutable
=
True
)
request
.
GET
.
update
(
kwargs
.
pop
(
'GET'
,
{}))
if
len
(
kwargs
):
warnings
.
warn
(
"FakeRequestFactory kwargs unused: "
+
unicode
(
kwargs
))
return
request
return
request
...
...
circle/dashboard/tests/test_views.py
View file @
d0dc1ed8
...
@@ -21,14 +21,12 @@ import json
...
@@ -21,14 +21,12 @@ import json
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.client
import
Client
from
django.test.client
import
Client
from
django.contrib.auth.models
import
User
,
Group
from
django.contrib.auth.models
import
User
,
Group
from
django.core.urlresolvers
import
reverse
from
django.contrib.auth.models
import
Permission
from
django.contrib.auth.models
import
Permission
from
django.contrib.auth
import
authenticate
from
django.contrib.auth
import
authenticate
from
vm.models
import
Instance
,
InstanceTemplate
,
Lease
,
Node
,
Trait
from
vm.models
import
Instance
,
InstanceTemplate
,
Lease
,
Node
,
Trait
from
vm.operations
import
WakeUpOperation
from
vm.operations
import
WakeUpOperation
from
..models
import
Profile
from
..models
import
Profile
from
..views
import
VmRenewView
from
storage.models
import
Disk
from
storage.models
import
Disk
from
firewall.models
import
Vlan
,
Host
,
VlanGroup
from
firewall.models
import
Vlan
,
Host
,
VlanGroup
from
mock
import
Mock
,
patch
from
mock
import
Mock
,
patch
...
@@ -568,10 +566,8 @@ class VmDetailTest(LoginMixin, TestCase):
...
@@ -568,10 +566,8 @@ class VmDetailTest(LoginMixin, TestCase):
inst
=
Instance
.
objects
.
get
(
pk
=
1
)
inst
=
Instance
.
objects
.
get
(
pk
=
1
)
inst
.
manual_state_change
(
'SUSPENDED'
)
inst
.
manual_state_change
(
'SUSPENDED'
)
inst
.
set_level
(
self
.
u2
,
'user'
)
inst
.
set_level
(
self
.
u2
,
'user'
)
with
patch
(
'dashboard.views.messages'
)
as
msg
:
response
=
c
.
post
(
"/dashboard/vm/1/op/wake_up/"
)
response
=
c
.
post
(
"/dashboard/vm/1/op/wake_up/"
)
assert
msg
.
error
.
called
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
302
)
inst
=
Instance
.
objects
.
get
(
pk
=
1
)
inst
=
Instance
.
objects
.
get
(
pk
=
1
)
self
.
assertEqual
(
inst
.
status
,
'SUSPENDED'
)
self
.
assertEqual
(
inst
.
status
,
'SUSPENDED'
)
...
@@ -1631,100 +1627,6 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
...
@@ -1631,100 +1627,6 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
self
.
assertEquals
(
Instance
.
objects
.
get
(
pk
=
1
)
.
owner
.
pk
,
self
.
u2
.
pk
)
self
.
assertEquals
(
Instance
.
objects
.
get
(
pk
=
1
)
.
owner
.
pk
,
self
.
u2
.
pk
)
class
RenewViewTest
(
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
()
Profile
.
objects
.
create
(
user
=
self
.
u1
)
self
.
u2
=
User
.
objects
.
create
(
username
=
'user2'
,
is_staff
=
True
)
self
.
u2
.
set_password
(
'password'
)
self
.
u2
.
save
()
Profile
.
objects
.
create
(
user
=
self
.
u2
)
self
.
us
=
User
.
objects
.
create
(
username
=
'superuser'
,
is_superuser
=
True
)
self
.
us
.
set_password
(
'password'
)
self
.
us
.
save
()
Profile
.
objects
.
create
(
user
=
self
.
us
)
inst
=
Instance
.
objects
.
get
(
pk
=
1
)
inst
.
owner
=
self
.
u1
inst
.
save
()
def
test_renew_by_owner
(
self
):
c
=
Client
()
ct
=
Instance
.
objects
.
get
(
pk
=
1
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
self
.
login
(
c
,
'user1'
)
response
=
c
.
get
(
'/dashboard/vm/1/renew/'
)
self
.
assertEquals
(
response
.
status_code
,
200
)
response
=
c
.
post
(
'/dashboard/vm/1/renew/'
)
self
.
assertEquals
(
response
.
status_code
,
302
)
ct2
=
Instance
.
objects
.
get
(
pk
=
1
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
self
.
assertEquals
(
ct
+
1
,
ct2
)
def
test_renew_get_by_nonowner_wo_key
(
self
):
c
=
Client
()
self
.
login
(
c
,
'user2'
)
response
=
c
.
get
(
'/dashboard/vm/1/renew/'
)
self
.
assertEquals
(
response
.
status_code
,
403
)
def
test_renew_post_by_nonowner_wo_key
(
self
):
c
=
Client
()
self
.
login
(
c
,
'user2'
)
response
=
c
.
post
(
'/dashboard/vm/1/renew/'
)
self
.
assertEquals
(
response
.
status_code
,
403
)
def
test_renew_get_by_nonowner_w_key
(
self
):
key
=
VmRenewView
.
get_token_url
(
Instance
.
objects
.
get
(
pk
=
1
),
self
.
u2
)
c
=
Client
()
response
=
c
.
get
(
key
)
self
.
assertEquals
(
response
.
status_code
,
200
)
def
test_renew_post_by_anon_w_key
(
self
):
key
=
VmRenewView
.
get_token_url
(
Instance
.
objects
.
get
(
pk
=
1
),
self
.
u2
)
ct
=
Instance
.
objects
.
get
(
pk
=
1
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
c
=
Client
()
response
=
c
.
post
(
key
)
self
.
assertEquals
(
response
.
status_code
,
302
)
ct2
=
Instance
.
objects
.
get
(
pk
=
1
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
self
.
assertEquals
(
ct
+
1
,
ct2
)
def
test_renew_post_by_anon_w_invalid_key
(
self
):
class
Mockinst
(
object
):
pk
=
2
key
=
VmRenewView
.
get_token_url
(
Mockinst
(),
self
.
u2
)
ct
=
Instance
.
objects
.
get
(
pk
=
1
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
c
=
Client
()
self
.
login
(
c
,
'user2'
)
response
=
c
.
get
(
key
)
self
.
assertEquals
(
response
.
status_code
,
404
)
response
=
c
.
post
(
key
)
self
.
assertEquals
(
response
.
status_code
,
404
)
ct2
=
Instance
.
objects
.
get
(
pk
=
1
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
self
.
assertEquals
(
ct
,
ct2
)
def
test_renew_post_by_anon_w_expired_key
(
self
):
key
=
reverse
(
VmRenewView
.
url_name
,
args
=
(
12
,
'WzEyLDFd:1WLbSi:2zIb8SUNAIRIOMTmSmKSSit2gpY'
))
ct
=
Instance
.
objects
.
get
(
pk
=
12
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
c
=
Client
()
self
.
login
(
c
,
'user2'
)
response
=
c
.
get
(
key
)
self
.
assertEquals
(
response
.
status_code
,
302
)
response
=
c
.
post
(
key
)
self
.
assertEquals
(
response
.
status_code
,
403
)
ct2
=
Instance
.
objects
.
get
(
pk
=
12
)
.
activity_log
.
\
filter
(
activity_code__endswith
=
'renew'
)
.
count
()
self
.
assertEquals
(
ct
,
ct2
)
class
IndexViewTest
(
LoginMixin
,
TestCase
):
class
IndexViewTest
(
LoginMixin
,
TestCase
):
fixtures
=
[
'test-vm-fixture.json'
,
'node.json'
]
fixtures
=
[
'test-vm-fixture.json'
,
'node.json'
]
...
...
circle/dashboard/urls.py
View file @
d0dc1ed8
...
@@ -29,7 +29,7 @@ from .views import (
...
@@ -29,7 +29,7 @@ from .views import (
TemplateDelete
,
TemplateDetail
,
TemplateList
,
TransferOwnershipConfirmView
,
TemplateDelete
,
TemplateDetail
,
TemplateList
,
TransferOwnershipConfirmView
,
TransferOwnershipView
,
vm_activity
,
VmCreate
,
VmDelete
,
VmDetailView
,
TransferOwnershipView
,
vm_activity
,
VmCreate
,
VmDelete
,
VmDetailView
,
VmDetailVncTokenView
,
VmGraphView
,
VmList
,
VmMassDelete
,
VmDetailVncTokenView
,
VmGraphView
,
VmList
,
VmMassDelete
,
VmRenewView
,
DiskRemoveView
,
get_disk_download_status
,
InterfaceDeleteView
,
DiskRemoveView
,
get_disk_download_status
,
InterfaceDeleteView
,
GroupRemoveAclUserView
,
GroupRemoveAclGroupView
,
GroupRemoveUserView
,
GroupRemoveAclUserView
,
GroupRemoveAclGroupView
,
GroupRemoveUserView
,
GroupRemoveFutureUserView
,
GroupRemoveFutureUserView
,
GroupCreate
,
GroupProfileUpdate
,
GroupCreate
,
GroupProfileUpdate
,
...
@@ -40,6 +40,7 @@ from .views import (
...
@@ -40,6 +40,7 @@ from .views import (
UserKeyDelete
,
UserKeyDetail
,
UserKeyCreate
,
UserKeyDelete
,
UserKeyDetail
,
UserKeyCreate
,
VmTraitsUpdate
,
VmRawDataUpdate
,
VmTraitsUpdate
,
VmRawDataUpdate
,
GroupPermissionsView
,
GroupPermissionsView
,
LeaseAclUpdateView
,
)
)
urlpatterns
=
patterns
(
urlpatterns
=
patterns
(
...
@@ -51,6 +52,8 @@ urlpatterns = patterns(
...
@@ -51,6 +52,8 @@ urlpatterns = patterns(
name
=
"dashboard.views.lease-create"
),
name
=
"dashboard.views.lease-create"
),
url
(
r'^lease/delete/(?P<pk>\d+)/$'
,
LeaseDelete
.
as_view
(),
url
(
r'^lease/delete/(?P<pk>\d+)/$'
,
LeaseDelete
.
as_view
(),
name
=
"dashboard.views.lease-delete"
),
name
=
"dashboard.views.lease-delete"
),
url
(
r'^lease/(?P<pk>\d+)/acl/$'
,
LeaseAclUpdateView
.
as_view
(),
name
=
"dashboard.views.lease-acl"
),
url
(
r'^template/create/$'
,
TemplateCreate
.
as_view
(),
url
(
r'^template/create/$'
,
TemplateCreate
.
as_view
(),
name
=
"dashboard.views.template-create"
),
name
=
"dashboard.views.template-create"
),
...
@@ -84,8 +87,6 @@ urlpatterns = patterns(
...
@@ -84,8 +87,6 @@ urlpatterns = patterns(
url
(
r'^vm/mass-delete/'
,
VmMassDelete
.
as_view
(),
url
(
r'^vm/mass-delete/'
,
VmMassDelete
.
as_view
(),
name
=
'dashboard.view.mass-delete-vm'
),
name
=
'dashboard.view.mass-delete-vm'
),
url
(
r'^vm/(?P<pk>\d+)/activity/$'
,
vm_activity
),
url
(
r'^vm/(?P<pk>\d+)/activity/$'
,
vm_activity
),
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
(),
url
(
r'^vm/activity/(?P<pk>\d+)/$'
,
InstanceActivityDetail
.
as_view
(),
name
=
'dashboard.views.vm-activity'
),
name
=
'dashboard.views.vm-activity'
),
url
(
r'^vm/(?P<pk>\d+)/screenshot/$'
,
get_vm_screenshot
,
url
(
r'^vm/(?P<pk>\d+)/screenshot/$'
,
get_vm_screenshot
,
...
...
circle/dashboard/views.py
View file @
d0dc1ed8
...
@@ -60,7 +60,7 @@ from .forms import (
...
@@ -60,7 +60,7 @@ from .forms import (
CircleAuthenticationForm
,
HostForm
,
LeaseForm
,
MyProfileForm
,
CircleAuthenticationForm
,
HostForm
,
LeaseForm
,
MyProfileForm
,
NodeForm
,
TemplateForm
,
TraitForm
,
VmCustomizeForm
,
GroupCreateForm
,
NodeForm
,
TemplateForm
,
TraitForm
,
VmCustomizeForm
,
GroupCreateForm
,
UserCreationForm
,
GroupProfileUpdateForm
,
UnsubscribeForm
,
UserCreationForm
,
GroupProfileUpdateForm
,
UnsubscribeForm
,
VmSaveForm
,
UserKeyForm
,
VmSaveForm
,
UserKeyForm
,
VmRenewForm
,
CirclePasswordChangeForm
,
VmCreateDiskForm
,
VmDownloadDiskForm
,
CirclePasswordChangeForm
,
VmCreateDiskForm
,
VmDownloadDiskForm
,
TraitsForm
,
RawDataForm
,
GroupPermissionForm
TraitsForm
,
RawDataForm
,
GroupPermissionForm
)
)
...
@@ -91,6 +91,23 @@ def search_user(keyword):
...
@@ -91,6 +91,23 @@ def search_user(keyword):
return
User
.
objects
.
get
(
email
=
keyword
)
return
User
.
objects
.
get
(
email
=
keyword
)
class
RedirectToLoginMixin
(
AccessMixin
):
redirect_exception_classes
=
(
PermissionDenied
,
)
def
dispatch
(
self
,
request
,
*
args
,
**
kwargs
):
try
:
return
super
(
RedirectToLoginMixin
,
self
)
.
dispatch
(
request
,
*
args
,
**
kwargs
)
except
self
.
redirect_exception_classes
:
if
not
request
.
user
.
is_authenticated
():
return
redirect_to_login
(
request
.
get_full_path
(),
self
.
get_login_url
(),
self
.
get_redirect_field_name
())
else
:
raise
class
GroupCodeMixin
(
object
):
class
GroupCodeMixin
(
object
):
@classmethod
@classmethod
...
@@ -498,7 +515,7 @@ class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
...
@@ -498,7 +515,7 @@ class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
return
self
.
get_object
()
.
get_absolute_url
()
+
"#resources"
return
self
.
get_object
()
.
get_absolute_url
()
+
"#resources"
class
OperationView
(
DetailView
):
class
OperationView
(
RedirectToLoginMixin
,
DetailView
):
template_name
=
'dashboard/operate.html'
template_name
=
'dashboard/operate.html'
show_in_toolbar
=
True
show_in_toolbar
=
True
...
@@ -519,8 +536,16 @@ class OperationView(DetailView):
...
@@ -519,8 +536,16 @@ class OperationView(DetailView):
def
get_urlname
(
cls
):
def
get_urlname
(
cls
):
return
'dashboard.vm.op.
%
s'
%
cls
.
op
return
'dashboard.vm.op.
%
s'
%
cls
.
op
def
get_url
(
self
):
@classmethod
return
reverse
(
self
.
get_urlname
(),
args
=
(
self
.
get_object
()
.
pk
,
))
def
get_instance_url
(
cls
,
pk
,
key
=
None
,
*
args
,
**
kwargs
):
url
=
reverse
(
cls
.
get_urlname
(),
args
=
(
pk
,
)
+
args
,
kwargs
=
kwargs
)
if
key
is
None
:
return
url
else
:
return
"
%
s?k=
%
s"
%
(
url
,
key
)
def
get_url
(
self
,
**
kwargs
):
return
self
.
get_instance_url
(
self
.
get_object
()
.
pk
,
**
kwargs
)
def
get_template_names
(
self
):
def
get_template_names
(
self
):
if
self
.
request
.
is_ajax
():
if
self
.
request
.
is_ajax
():
...
@@ -541,15 +566,23 @@ class OperationView(DetailView):
...
@@ -541,15 +566,23 @@ class OperationView(DetailView):
ctx
=
super
(
OperationView
,
self
)
.
get_context_data
(
**
kwargs
)
ctx
=
super
(
OperationView
,
self
)
.
get_context_data
(
**
kwargs
)
ctx
[
'op'
]
=
self
.
get_op
()
ctx
[
'op'
]
=
self
.
get_op
()
ctx
[
'opview'
]
=
self
ctx
[
'opview'
]
=
self
ctx
[
'url'
]
=
self
.
request
.
path
url
=
self
.
request
.
path
if
self
.
request
.
GET
:
url
+=
'?'
+
self
.
request
.
GET
.
urlencode
()
ctx
[
'url'
]
=
url
ctx
[
'template'
]
=
super
(
OperationView
,
self
)
.
get_template_names
()[
0
]
ctx
[
'template'
]
=
super
(
OperationView
,
self
)
.
get_template_names
()[
0
]
return
ctx
return
ctx
def
check_auth
(
self
):
logger
.
debug
(
"OperationView.check_auth(
%
s)"
,
unicode
(
self
))
self
.
get_op
()
.
check_auth
(
self
.
request
.
user
)
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
self
.
get_op
()
.
check_auth
(
request
.
user
)
self
.
check_auth
(
)
return
super
(
OperationView
,
self
)
.
get
(
request
,
*
args
,
**
kwargs
)
return
super
(
OperationView
,
self
)
.
get
(
request
,
*
args
,
**
kwargs
)
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
self
.
check_auth
()
self
.
object
=
self
.
get_object
()
self
.
object
=
self
.
get_object
()
if
extra
is
None
:
if
extra
is
None
:
extra
=
{}
extra
=
{}
...
@@ -557,31 +590,31 @@ class OperationView(DetailView):
...
@@ -557,31 +590,31 @@ class OperationView(DetailView):
self
.
get_op
()
.
async
(
user
=
request
.
user
,
**
extra
)
self
.
get_op
()
.
async
(
user
=
request
.
user
,
**
extra
)
except
Exception
as
e
:
except
Exception
as
e
:
messages
.
error
(
request
,
_
(
'Could not start operation.'
))
messages
.
error
(
request
,
_
(
'Could not start operation.'
))
logger
.
error
(
e
)
logger
.
exception
(
e
)
else
:
messages
.
success
(
request
,
_
(
'Operation is started.'
))
return
redirect
(
"
%
s#activity"
%
self
.
object
.
get_absolute_url
())
return
redirect
(
"
%
s#activity"
%
self
.
object
.
get_absolute_url
())
@classmethod
@classmethod
def
factory
(
cls
,
op
,
icon
=
'cog'
,
effect
=
'info'
):
def
factory
(
cls
,
op
,
icon
=
'cog'
,
effect
=
'info'
,
extra_bases
=
(),
**
kwargs
):
kwargs
.
update
({
'op'
:
op
,
'icon'
:
icon
,
'effect'
:
effect
})
return
type
(
str
(
cls
.
__name__
+
op
),
return
type
(
str
(
cls
.
__name__
+
op
),
(
cls
,
),
{
'op'
:
op
,
'icon'
:
icon
,
'effect'
:
effect
}
)
tuple
(
list
(
extra_bases
)
+
[
cls
]),
kwargs
)
@classmethod
@classmethod
def
bind_to_object
(
cls
,
instance
,
**
kwargs
):
def
bind_to_object
(
cls
,
instance
,
**
kwargs
):
v
=
cls
()
me
=
cls
()
v
.
get_object
=
lambda
:
instance
me
.
get_object
=
lambda
:
instance
for
key
,
value
in
kwargs
.
iteritems
():
for
key
,
value
in
kwargs
.
iteritems
():
setattr
(
v
,
key
,
value
)
setattr
(
me
,
key
,
value
)
return
v
return
me
class
VmOperationView
(
OperationView
):
model
=
Instance
class
AjaxOperationMixin
(
object
):
context_object_name
=
'instance'
# much simpler to mock object
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
resp
=
super
(
VmOperationView
,
self
)
.
post
(
request
,
extra
,
*
args
,
resp
=
super
(
AjaxOperationMixin
,
self
)
.
post
(
**
kwargs
)
request
,
extra
,
*
args
,
**
kwargs
)
if
request
.
is_ajax
():
if
request
.
is_ajax
():
store
=
messages
.
get_messages
(
request
)
store
=
messages
.
get_messages
(
request
)
store
.
used
=
True
store
.
used
=
True
...
@@ -594,22 +627,32 @@ class VmOperationView(OperationView):
...
@@ -594,22 +627,32 @@ class VmOperationView(OperationView):
return
resp
return
resp
class
VmOperationView
(
AjaxOperationMixin
,
OperationView
):
model
=
Instance
context_object_name
=
'instance'
# much simpler to mock object
class
FormOperationMixin
(
object
):
class
FormOperationMixin
(
object
):
form_class
=
None
form_class
=
None
def
get_form_kwargs
(
self
):
return
{}
def
get_context_data
(
self
,
**
kwargs
):
def
get_context_data
(
self
,
**
kwargs
):
ctx
=
super
(
FormOperationMixin
,
self
)
.
get_context_data
(
**
kwargs
)
ctx
=
super
(
FormOperationMixin
,
self
)
.
get_context_data
(
**
kwargs
)
if
self
.
request
.
method
==
'POST'
:
if
self
.
request
.
method
==
'POST'
:
ctx
[
'form'
]
=
self
.
form_class
(
self
.
request
.
POST
)
ctx
[
'form'
]
=
self
.
form_class
(
self
.
request
.
POST
,
**
self
.
get_form_kwargs
())
else
:
else
:
ctx
[
'form'
]
=
self
.
form_class
()
ctx
[
'form'
]
=
self
.
form_class
(
**
self
.
get_form_kwargs
()
)
return
ctx
return
ctx
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
def
post
(
self
,
request
,
extra
=
None
,
*
args
,
**
kwargs
):
if
extra
is
None
:
if
extra
is
None
:
extra
=
{}
extra
=
{}
form
=
self
.
form_class
(
self
.
request
.
POST
)
form
=
self
.
form_class
(
self
.
request
.
POST
,
**
self
.
get_form_kwargs
()
)
if
form
.
is_valid
():
if
form
.
is_valid
():
extra
.
update
(
form
.
cleaned_data
)
extra
.
update
(
form
.
cleaned_data
)
resp
=
super
(
FormOperationMixin
,
self
)
.
post
(
resp
=
super
(
FormOperationMixin
,
self
)
.
post
(
...
@@ -625,6 +668,14 @@ class FormOperationMixin(object):
...
@@ -625,6 +668,14 @@ class FormOperationMixin(object):
return
self
.
get
(
request
)
return
self
.
get
(
request
)
class
RequestFormOperationMixin
(
FormOperationMixin
):
def
get_form_kwargs
(
self
):
val
=
super
(
FormOperationMixin
,
self
)
.
get_form_kwargs
()
val
.
update
({
'request'
:
self
.
request
})
return
val
class
VmCreateDiskView
(
FormOperationMixin
,
VmOperationView
):
class
VmCreateDiskView
(
FormOperationMixin
,
VmOperationView
):
op
=
'create_disk'
op
=
'create_disk'
...
@@ -696,12 +747,108 @@ class VmResourcesChangeView(VmOperationView):
...
@@ -696,12 +747,108 @@ class VmResourcesChangeView(VmOperationView):
*
args
,
**
kwargs
)
*
args
,
**
kwargs
)
class
TokenOperationView
(
OperationView
):
"""Abstract operation view with token support.
User can do the action with a valid token instead of logging in.
"""
token_max_age
=
3
*
24
*
3600
redirect_exception_classes
=
(
PermissionDenied
,
SuspiciousOperation
,
)
@classmethod
def
get_salt
(
cls
):
return
unicode
(
cls
)
@classmethod
def
get_token
(
cls
,
instance
,
user
):
t
=
tuple
([
getattr
(
i
,
'pk'
,
i
)
for
i
in
[
instance
,
user
]])
return
signing
.
dumps
(
t
,
salt
=
cls
.
get_salt
(),
compress
=
True
)
@classmethod
def
get_token_url
(
cls
,
instance
,
user
):
key
=
cls
.
get_token
(
instance
,
user
)
return
cls
.
get_instance_url
(
instance
.
pk
,
key
)
def
check_auth
(
self
):
if
'k'
in
self
.
request
.
GET
:
try
:
# check if token is needed at all
return
super
(
TokenOperationView
,
self
)
.
check_auth
()
except
Exception
:
op
=
self
.
get_op
()
pk
=
op
.
instance
.
pk
key
=
self
.
request
.
GET
.
get
(
'k'
)
logger
.
debug
(
"checking token supplied to
%
s"
,
self
.
request
.
get_full_path
())
try
:
user
=
self
.
validate_key
(
pk
,
key
)
except
signing
.
SignatureExpired
:
messages
.
error
(
self
.
request
,
_
(
'The token has expired.'
))
else
:
logger
.
info
(
"Request user changed to
%
s at
%
s"
,
user
,
self
.
request
.
get_full_path
())
self
.
request
.
user
=
user
else
:
logger
.
debug
(
"no token supplied to
%
s"
,
self
.
request
.
get_full_path
())
return
super
(
TokenOperationView
,
self
)
.
check_auth
()
def
validate_key
(
self
,
pk
,
key
):
"""Get object based on signed token.
"""
try
:
data
=
signing
.
loads
(
key
,
salt
=
self
.
get_salt
())
logger
.
debug
(
'Token data:
%
s'
,
unicode
(
data
))
instance
,
user
=
data
logger
.
debug
(
'Extracted token data: instance:
%
s, user:
%
s'
,
unicode
(
instance
),
unicode
(
user
))
except
(
signing
.
BadSignature
,
ValueError
,
TypeError
)
as
e
:
logger
.
warning
(
'Tried invalid token. Token:
%
s, user:
%
s.
%
s'
,
key
,
unicode
(
self
.
request
.
user
),
unicode
(
e
))
raise
SuspiciousOperation
()
try
:
instance
,
user
=
signing
.
loads
(
key
,
max_age
=
self
.
token_max_age
,
salt
=
self
.
get_salt
())
logger
.
debug
(
'Extracted non-expired token data:
%
s,
%
s'
,
unicode
(
instance
),
unicode
(
user
))
except
signing
.
BadSignature
as
e
:
raise
signing
.
SignatureExpired
()
if
pk
!=
instance
:
logger
.
debug
(
'pk (
%
d) != instance (
%
d)'
,
pk
,
instance
)
raise
SuspiciousOperation
()
user
=
User
.
objects
.
get
(
pk
=
user
)
return
user
class
VmRenewView
(
FormOperationMixin
,
TokenOperationView
,
VmOperationView
):
op
=
'renew'
icon
=
'calendar'
effect
=
'info'
show_in_toolbar
=
False
form_class
=
VmRenewForm
def
get_form_kwargs
(
self
):
choices
=
Lease
.
get_objects_with_level
(
"user"
,
self
.
request
.
user
)
default
=
self
.
get_op
()
.
instance
.
lease
if
default
and
default
not
in
choices
:
choices
=
list
(
choices
)
+
[
default
]
val
=
super
(
VmRenewView
,
self
)
.
get_form_kwargs
()
val
.
update
({
'choices'
:
choices
,
'default'
:
default
})
return
val
vm_ops
=
OrderedDict
([
vm_ops
=
OrderedDict
([
(
'deploy'
,
VmOperationView
.
factory
(
(
'deploy'
,
VmOperationView
.
factory
(
op
=
'deploy'
,
icon
=
'play'
,
effect
=
'success'
)),
op
=
'deploy'
,
icon
=
'play'
,
effect
=
'success'
)),
(
'wake_up'
,
VmOperationView
.
factory
(
(
'wake_up'
,
VmOperationView
.
factory
(
op
=
'wake_up'
,
icon
=
'sun'
,
effect
=
'success'
)),
op
=
'wake_up'
,
icon
=
'sun'
,
effect
=
'success'
)),
(
'sleep'
,
VmOperationView
.
factory
(
(
'sleep'
,
VmOperationView
.
factory
(
extra_bases
=
[
TokenOperationView
],
op
=
'sleep'
,
icon
=
'moon'
,
effect
=
'info'
)),
op
=
'sleep'
,
icon
=
'moon'
,
effect
=
'info'
)),
(
'migrate'
,
VmMigrateView
),
(
'migrate'
,
VmMigrateView
),
(
'save_as_template'
,
VmSaveView
),
(
'save_as_template'
,
VmSaveView
),
...
@@ -716,9 +863,11 @@ vm_ops = OrderedDict([
...
@@ -716,9 +863,11 @@ vm_ops = OrderedDict([
(
'recover'
,
VmOperationView
.
factory
(
(
'recover'
,
VmOperationView
.
factory
(
op
=
'recover'
,
icon
=
'medkit'
,
effect
=
'warning'
)),
op
=
'recover'
,
icon
=
'medkit'
,
effect
=
'warning'
)),
(
'destroy'
,
VmOperationView
.
factory
(
(
'destroy'
,
VmOperationView
.
factory
(
extra_bases
=
[
TokenOperationView
],
op
=
'destroy'
,
icon
=
'remove'
,
effect
=
'danger'
)),
op
=
'destroy'
,
icon
=
'remove'
,
effect
=
'danger'
)),
(
'create_disk'
,
VmCreateDiskView
),
(
'create_disk'
,
VmCreateDiskView
),
(
'download_disk'
,
VmDownloadDiskView
),
(
'download_disk'
,
VmDownloadDiskView
),
(
'renew'
,
VmRenewView
),
])
])
...
@@ -2136,10 +2285,11 @@ class VmMassDelete(LoginRequiredMixin, View):
...
@@ -2136,10 +2285,11 @@ class VmMassDelete(LoginRequiredMixin, View):
return
redirect
(
next
if
next
else
reverse_lazy
(
'dashboard.index'
))
return
redirect
(
next
if
next
else
reverse_lazy
(
'dashboard.index'
))
class
LeaseCreate
(
LoginRequiredMixin
,
Superuser
RequiredMixin
,
class
LeaseCreate
(
LoginRequiredMixin
,
Permission
RequiredMixin
,
SuccessMessageMixin
,
CreateView
):
SuccessMessageMixin
,
CreateView
):
model
=
Lease
model
=
Lease
form_class
=
LeaseForm
form_class
=
LeaseForm
permission_required
=
'vm.create_leases'
template_name
=
"dashboard/lease-create.html"
template_name
=
"dashboard/lease-create.html"
success_message
=
_
(
"Successfully created a new lease."
)
success_message
=
_
(
"Successfully created a new lease."
)
...
@@ -2147,6 +2297,10 @@ class LeaseCreate(LoginRequiredMixin, SuperuserRequiredMixin,
...
@@ -2147,6 +2297,10 @@ class LeaseCreate(LoginRequiredMixin, SuperuserRequiredMixin,
return
reverse_lazy
(
"dashboard.views.template-list"
)
return
reverse_lazy
(
"dashboard.views.template-list"
)
class
LeaseAclUpdateView
(
AclUpdateView
):
model
=
Lease
class
LeaseDetail
(
LoginRequiredMixin
,
SuperuserRequiredMixin
,
class
LeaseDetail
(
LoginRequiredMixin
,
SuperuserRequiredMixin
,
SuccessMessageMixin
,
UpdateView
):
SuccessMessageMixin
,
UpdateView
):
model
=
Lease
model
=
Lease
...
@@ -2154,6 +2308,12 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
...
@@ -2154,6 +2308,12 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
template_name
=
"dashboard/lease-edit.html"
template_name
=
"dashboard/lease-edit.html"
success_message
=
_
(
"Successfully modified lease."
)
success_message
=
_
(
"Successfully modified lease."
)
def
get_context_data
(
self
,
*
args
,
**
kwargs
):
obj
=
self
.
get_object
()
context
=
super
(
LeaseDetail
,
self
)
.
get_context_data
(
*
args
,
**
kwargs
)
context
[
'acl'
]
=
get_vm_acl_data
(
obj
)
return
context
def
get_success_url
(
self
):
def
get_success_url
(
self
):
return
reverse_lazy
(
"dashboard.views.lease-detail"
,
kwargs
=
self
.
kwargs
)
return
reverse_lazy
(
"dashboard.views.lease-detail"
,
kwargs
=
self
.
kwargs
)
...
@@ -2295,163 +2455,6 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
...
@@ -2295,163 +2455,6 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
kwargs
=
{
'pk'
:
obj
.
pk
}))
kwargs
=
{
'pk'
:
obj
.
pk
}))
class
AbstractVmFunctionView
(
AccessMixin
,
View
):
"""Abstract instance-action view.
User can do the action with a valid token or if has at least required_level
ACL level for the instance.
Children should at least implement/add template_name, success_message,
url_name, and do_action().
"""
token_max_age
=
3
*
24
*
3600
required_level
=
'owner'
success_message
=
_
(
"Failed to perform requested action."
)
@classmethod
def
check_acl
(
cls
,
instance
,
user
):
if
not
instance
.
has_level
(
user
,
cls
.
required_level
):
raise
PermissionDenied
()
@classmethod
def
get_salt
(
cls
):
return
unicode
(
cls
)
@classmethod
def
get_token
(
cls
,
instance
,
user
,
*
args
):
t
=
tuple
([
getattr
(
i
,
'pk'
,
i
)
for
i
in
[
instance
,
user
]
+
list
(
args
)])
return
signing
.
dumps
(
t
,
salt
=
cls
.
get_salt
(),
compress
=
True
)
@classmethod
def
get_token_url
(
cls
,
instance
,
user
,
*
args
):
key
=
cls
.
get_token
(
instance
,
user
,
*
args
)
args
=
(
instance
.
pk
,
key
)
+
args
return
reverse
(
cls
.
url_name
,
args
=
args
)
# this wont work, CBVs suck: reverse(cls.as_view(), args=args)
def
get_template_names
(
self
):
return
[
self
.
template_name
]
def
get
(
self
,
request
,
pk
,
key
=
None
,
*
args
,
**
kwargs
):
class
LoginNeeded
(
Exception
):
pass
pk
=
int
(
pk
)
instance
=
get_object_or_404
(
Instance
,
pk
=
pk
)
try
:
if
key
:
logger
.
debug
(
'Confirm dialog for token
%
s.'
,
key
)
try
:
self
.
validate_key
(
pk
,
key
)
except
signing
.
SignatureExpired
:
messages
.
error
(
request
,
_
(
'The token has expired, please log in.'
))
raise
LoginNeeded
()
self
.
key
=
key
else
:
if
not
request
.
user
.
is_authenticated
():
raise
LoginNeeded
()
self
.
check_acl
(
instance
,
request
.
user
)
except
LoginNeeded
:
return
redirect_to_login
(
request
.
get_full_path
(),
self
.
get_login_url
(),
self
.
get_redirect_field_name
())
except
SuspiciousOperation
as
e
:
messages
.
error
(
request
,
_
(
'This token is invalid.'
))
logger
.
warning
(
'This token
%
s is invalid.
%
s'
,
key
,
unicode
(
e
))
raise
PermissionDenied
()
return
render
(
request
,
self
.
get_template_names
(),
self
.
get_context
(
instance
))
def
post
(
self
,
request
,
pk
,
key
=
None
,
*
args
,
**
kwargs
):
class
LoginNeeded
(
Exception
):
pass
pk
=
int
(
pk
)
instance
=
get_object_or_404
(
Instance
,
pk
=
pk
)
try
:
if
not
request
.
user
.
is_authenticated
()
and
key
:
try
:
user
=
self
.
validate_key
(
pk
,
key
)
except
signing
.
SignatureExpired
:
messages
.
error
(
request
,
_
(
'The token has expired, please log in.'
))
raise
LoginNeeded
()
self
.
key
=
key
else
:
user
=
request
.
user
self
.
check_acl
(
instance
,
request
.
user
)
except
LoginNeeded
:
return
redirect_to_login
(
request
.
get_full_path
(),
self
.
get_login_url
(),
self
.
get_redirect_field_name
())
except
SuspiciousOperation
as
e
:
messages
.
error
(
request
,
_
(
'This token is invalid.'
))
logger
.
warning
(
'This token
%
s is invalid.
%
s'
,
key
,
unicode
(
e
))
raise
PermissionDenied
()
if
self
.
do_action
(
instance
,
user
):
messages
.
success
(
request
,
self
.
success_message
)
else
:
messages
.
error
(
request
,
self
.
fail_message
)
return
HttpResponseRedirect
(
instance
.
get_absolute_url
())
def
validate_key
(
self
,
pk
,
key
):
"""Get object based on signed token.
"""
try
:
data
=
signing
.
loads
(
key
,
salt
=
self
.
get_salt
())
logger
.
debug
(
'Token data:
%
s'
,
unicode
(
data
))
instance
,
user
=
data
logger
.
debug
(
'Extracted token data: instance:
%
s, user:
%
s'
,
unicode
(
instance
),
unicode
(
user
))
except
(
signing
.
BadSignature
,
ValueError
,
TypeError
)
as
e
:
logger
.
warning
(
'Tried invalid token. Token:
%
s, user:
%
s.
%
s'
,
key
,
unicode
(
self
.
request
.
user
),
unicode
(
e
))
raise
SuspiciousOperation
()
try
:
instance
,
user
=
signing
.
loads
(
key
,
max_age
=
self
.
token_max_age
,
salt
=
self
.
get_salt
())
logger
.
debug
(
'Extracted non-expired token data:
%
s,
%
s'
,
unicode
(
instance
),
unicode
(
user
))
except
signing
.
BadSignature
as
e
:
raise
signing
.
SignatureExpired
()
if
pk
!=
instance
:
logger
.
debug
(
'pk (
%
d) != instance (
%
d)'
,
pk
,
instance
)
raise
SuspiciousOperation
()
user
=
User
.
objects
.
get
(
pk
=
user
)
return
user
def
do_action
(
self
,
instance
,
user
):
# noqa
raise
NotImplementedError
(
'Please override do_action(instance, user)'
)
def
get_context
(
self
,
instance
):
context
=
{
'instance'
:
instance
}
if
getattr
(
self
,
'key'
,
None
)
is
not
None
:
context
[
'key'
]
=
self
.
key
return
context
class
VmRenewView
(
AbstractVmFunctionView
):
"""User can renew an instance."""
template_name
=
'dashboard/confirm/base-renew.html'
success_message
=
_
(
"Virtual machine is successfully renewed."
)
url_name
=
'dashboard.views.vm-renew'
def
get_context
(
self
,
instance
):
context
=
super
(
VmRenewView
,
self
)
.
get_context
(
instance
)
(
context
[
'time_of_suspend'
],
context
[
'time_of_delete'
])
=
instance
.
get_renew_times
()
return
context
def
do_action
(
self
,
instance
,
user
):
instance
.
renew
(
user
=
user
)
logger
.
info
(
'Instance
%
s renewed by
%
s.'
,
unicode
(
instance
),
unicode
(
user
))
return
True
class
TransferOwnershipConfirmView
(
LoginRequiredMixin
,
View
):
class
TransferOwnershipConfirmView
(
LoginRequiredMixin
,
View
):
"""User can accept an ownership offer."""
"""User can accept an ownership offer."""
...
...
circle/vm/models/activity.py
View file @
d0dc1ed8
...
@@ -225,6 +225,8 @@ def node_activity(code_suffix, node, task_uuid=None, user=None):
...
@@ -225,6 +225,8 @@ def node_activity(code_suffix, node, task_uuid=None, user=None):
@worker_ready.connect
()
@worker_ready.connect
()
def
cleanup
(
conf
=
None
,
**
kwargs
):
def
cleanup
(
conf
=
None
,
**
kwargs
):
# TODO check if other manager workers are running
# TODO check if other manager workers are running
from
celery.task.control
import
discard_all
discard_all
()
msg_txt
=
ugettext_noop
(
"Manager is restarted, activity is cleaned up. "
msg_txt
=
ugettext_noop
(
"Manager is restarted, activity is cleaned up. "
"You can try again now."
)
"You can try again now."
)
message
=
create_readable
(
msg_txt
,
msg_txt
)
message
=
create_readable
(
msg_txt
,
msg_txt
)
...
...
circle/vm/models/common.py
View file @
d0dc1ed8
...
@@ -18,12 +18,14 @@
...
@@ -18,12 +18,14 @@
from
__future__
import
absolute_import
,
unicode_literals
from
__future__
import
absolute_import
,
unicode_literals
from
datetime
import
timedelta
,
datetime
from
datetime
import
timedelta
,
datetime
from
django.db.models
import
Model
,
CharField
,
IntegerField
from
django.db.models
import
Model
,
CharField
,
IntegerField
,
permalink
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils.timesince
import
timeuntil
from
django.utils.timesince
import
timeuntil
from
model_utils.models
import
TimeStampedModel
from
model_utils.models
import
TimeStampedModel
from
acl.models
import
AclBase
ARCHITECTURES
=
((
'x86_64'
,
'x86-64 (64 bit)'
),
ARCHITECTURES
=
((
'x86_64'
,
'x86-64 (64 bit)'
),
(
'i686'
,
'x86 (32 bit)'
))
(
'i686'
,
'x86 (32 bit)'
))
...
@@ -66,13 +68,18 @@ class NamedBaseResourceConfig(BaseResourceConfigModel, TimeStampedModel):
...
@@ -66,13 +68,18 @@ class NamedBaseResourceConfig(BaseResourceConfigModel, TimeStampedModel):
return
self
.
name
return
self
.
name
class
Lease
(
Model
):
class
Lease
(
AclBase
):
"""Lease times for VM instances.
"""Lease times for VM instances.
Specifies a time duration until suspension and deletion of a VM
Specifies a time duration until suspension and deletion of a VM
instance.
instance.
"""
"""
ACL_LEVELS
=
(
(
'user'
,
_
(
'user'
)),
# use this lease
(
'operator'
,
_
(
'operator'
)),
# share this lease
(
'owner'
,
_
(
'owner'
)),
# change this lease
)
name
=
CharField
(
max_length
=
100
,
unique
=
True
,
name
=
CharField
(
max_length
=
100
,
unique
=
True
,
verbose_name
=
_
(
'name'
))
verbose_name
=
_
(
'name'
))
suspend_interval_seconds
=
IntegerField
(
suspend_interval_seconds
=
IntegerField
(
...
@@ -88,6 +95,9 @@ class Lease(Model):
...
@@ -88,6 +95,9 @@ class Lease(Model):
app_label
=
'vm'
app_label
=
'vm'
db_table
=
'vm_lease'
db_table
=
'vm_lease'
ordering
=
[
'name'
,
]
ordering
=
[
'name'
,
]
permissions
=
(
(
'create_leases'
,
_
(
'Can create new leases.'
)),
)
@property
@property
def
suspend_interval
(
self
):
def
suspend_interval
(
self
):
...
@@ -141,6 +151,10 @@ class Lease(Model):
...
@@ -141,6 +151,10 @@ class Lease(Model):
's'
:
self
.
get_readable_suspend_time
(),
's'
:
self
.
get_readable_suspend_time
(),
'r'
:
self
.
get_readable_delete_time
()}
'r'
:
self
.
get_readable_delete_time
()}
@permalink
def
get_absolute_url
(
self
):
return
(
'dashboard.views.lease-detail'
,
None
,
{
'pk'
:
self
.
pk
})
class
Trait
(
Model
):
class
Trait
(
Model
):
name
=
CharField
(
max_length
=
50
,
verbose_name
=
_
(
'name'
))
name
=
CharField
(
max_length
=
50
,
verbose_name
=
_
(
'name'
))
...
...
circle/vm/models/instance.py
View file @
d0dc1ed8
...
@@ -439,10 +439,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
...
@@ -439,10 +439,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
for
cps
in
customized_params
]
for
cps
in
customized_params
]
def
clean
(
self
,
*
args
,
**
kwargs
):
def
clean
(
self
,
*
args
,
**
kwargs
):
if
self
.
time_of_suspend
is
None
:
self
.
time_of_suspend
,
self
.
time_of_delete
=
self
.
get_renew_times
()
self
.
_do_renew
(
which
=
'suspend'
)
if
self
.
time_of_delete
is
None
:
self
.
_do_renew
(
which
=
'delete'
)
super
(
Instance
,
self
)
.
clean
(
*
args
,
**
kwargs
)
super
(
Instance
,
self
)
.
clean
(
*
args
,
**
kwargs
)
def
manual_state_change
(
self
,
new_state
=
"NOSTATE"
,
reason
=
None
,
user
=
None
):
def
manual_state_change
(
self
,
new_state
=
"NOSTATE"
,
reason
=
None
,
user
=
None
):
...
@@ -715,36 +712,14 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
...
@@ -715,36 +712,14 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
else
:
else
:
return
False
return
False
def
get_renew_times
(
self
):
def
get_renew_times
(
self
,
lease
=
None
):
"""Returns new suspend and delete times if renew would be called.
"""Returns new suspend and delete times if renew would be called.
"""
"""
if
lease
is
None
:
lease
=
self
.
lease
return
(
return
(
timezone
.
now
()
+
self
.
lease
.
suspend_interval
,
timezone
.
now
()
+
lease
.
suspend_interval
,
timezone
.
now
()
+
self
.
lease
.
delete_interval
)
timezone
.
now
()
+
lease
.
delete_interval
)
def
_do_renew
(
self
,
which
=
'both'
):
"""Set expiration times to renewed values.
"""
time_of_suspend
,
time_of_delete
=
self
.
get_renew_times
()
if
which
in
(
'suspend'
,
'both'
):
self
.
time_of_suspend
=
time_of_suspend
if
which
in
(
'delete'
,
'both'
):
self
.
time_of_delete
=
time_of_delete
def
renew
(
self
,
which
=
'both'
,
base_activity
=
None
,
user
=
None
):
"""Renew virtual machine instance leases.
"""
if
base_activity
is
None
:
act_ctx
=
instance_activity
(
code_suffix
=
'renew'
,
instance
=
self
,
user
=
user
)
else
:
act_ctx
=
base_activity
.
sub_activity
(
'renew'
)
with
act_ctx
:
if
which
not
in
(
'suspend'
,
'delete'
,
'both'
):
raise
ValueError
(
'No such expiration type.'
)
self
.
_do_renew
(
which
)
self
.
save
()
def
change_password
(
self
,
user
=
None
):
def
change_password
(
self
,
user
=
None
):
"""Generate new password for the vm
"""Generate new password for the vm
...
...
circle/vm/operations.py
View file @
d0dc1ed8
...
@@ -189,6 +189,7 @@ class DeployOperation(InstanceOperation):
...
@@ -189,6 +189,7 @@ class DeployOperation(InstanceOperation):
self
.
instance
.
deploy_disks
()
self
.
instance
.
deploy_disks
()
# Deploy VM on remote machine
# Deploy VM on remote machine
if
self
.
instance
.
state
not
in
[
'PAUSED'
]:
with
activity
.
sub_activity
(
'deploying_vm'
)
as
deploy_act
:
with
activity
.
sub_activity
(
'deploying_vm'
)
as
deploy_act
:
deploy_act
.
result
=
self
.
instance
.
deploy_vm
(
timeout
=
timeout
)
deploy_act
.
result
=
self
.
instance
.
deploy_vm
(
timeout
=
timeout
)
...
@@ -200,7 +201,7 @@ class DeployOperation(InstanceOperation):
...
@@ -200,7 +201,7 @@ class DeployOperation(InstanceOperation):
with
activity
.
sub_activity
(
'booting'
):
with
activity
.
sub_activity
(
'booting'
):
self
.
instance
.
resume_vm
(
timeout
=
timeout
)
self
.
instance
.
resume_vm
(
timeout
=
timeout
)
self
.
instance
.
renew
(
which
=
'both'
,
base
_activity
=
activity
)
self
.
instance
.
renew
(
parent
_activity
=
activity
)
register_operation
(
DeployOperation
)
register_operation
(
DeployOperation
)
...
@@ -613,12 +614,30 @@ class WakeUpOperation(InstanceOperation):
...
@@ -613,12 +614,30 @@ class WakeUpOperation(InstanceOperation):
self
.
instance
.
deploy_net
()
self
.
instance
.
deploy_net
()
# Renew vm
# Renew vm
self
.
instance
.
renew
(
which
=
'both'
,
base
_activity
=
activity
)
self
.
instance
.
renew
(
parent
_activity
=
activity
)
register_operation
(
WakeUpOperation
)
register_operation
(
WakeUpOperation
)
class
RenewOperation
(
InstanceOperation
):
activity_code_suffix
=
'renew'
id
=
'renew'
name
=
_
(
"renew"
)
description
=
_
(
"Renew expiration times"
)
acl_level
=
"operator"
required_perms
=
()
concurrency_check
=
False
def
_operation
(
self
,
lease
=
None
):
(
self
.
instance
.
time_of_suspend
,
self
.
instance
.
time_of_delete
)
=
self
.
instance
.
get_renew_times
(
lease
)
self
.
instance
.
save
()
register_operation
(
RenewOperation
)
class
NodeOperation
(
Operation
):
class
NodeOperation
(
Operation
):
async_operation
=
abortable_async_node_operation
async_operation
=
abortable_async_node_operation
host_cls
=
Node
host_cls
=
Node
...
...
docs/deploy.rst
View file @
d0dc1ed8
Deploy
Deploy
======
==
======
This is where you describe how the project is deployed in production.
This tutorial describes the installation of a production environment. To
have a fully working environment, you have to set up the other components
as well. The full procedure is included in the :doc:`Puppet recipes
<puppet>` available for CIRCLE Cloud.
This component should normally deployed to a single head node.
This is the web-based entry point to the end users, and also the manager of
the compute and storage nodes.
Preparation
-----------
To get the project running, launch a new Ubuntu 14.04 machine, and
log in to it over SSH.
.. warning::
If the first character of the hostname should not be a digit, because
RabbitMQ won't work with it.
The machine should have an :abbr:`fqdn (fully qualified domain name)`,
which shoud be correctly printed by :kbd:`hostname -f`. You can achieve
this with an IP address (e.g. 127.0.1.1) in :file:`/etc/hosts` having the
short hostname as first, and the fqdn as second alias).
Setting up required software
----------------------------
Update the package lists, and install the required system software::
sudo apt-get update
sudo apt-get install --yes virtualenvwrapper postgresql git \
python-pip rabbitmq-server libpq-dev python-dev ntp memcached \
libmemcached-dev gettext wget pwgen nginx
Set up *PostgreSQL* to listen on localhost and restart it::
sudo sed -i /etc/postgresql/9.1/main/postgresql.conf -e '/#listen_addresses/ s/^#//'
sudo /etc/init.d/postgresql restart
Also, create a new database and user::
pwgen 12 >pgpw
sudo -u postgres createuser -S -D -R circle
sudo -u postgres psql <<<"ALTER USER circle WITH PASSWORD '$(cat pgpw)';"
sudo -u postgres createdb circle -O circle
Configure RabbitMQ: remove the guest user, add virtual host and user with
proper permissions::
pwgen 12 >rmqpw
sudo rabbitmqctl delete_user guest
sudo rabbitmqctl add_vhost circle
sudo rabbitmqctl add_user cloud $(cat rmqpw)
sudo rabbitmqctl set_permissions -p circle cloud '.*' '.*' '.*'
Set up nginx to serve the CIRCLE portal. ::
sudo tee /etc/nginx/conf.d/default.conf <<END
ignore_invalid_headers on;
server {
listen 443 ssl default;
ssl on;
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
location /static {
alias ${PWD}/circle/static_collected; # your Django project's static files
}
location / {
uwsgi_pass unix:///tmp/uwsgi.sock;
include /etc/nginx/uwsgi_params; # or the uwsgi_params you installed manually
}
location /vnc/ {
proxy_pass http://localhost:9999;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Host \$host;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
# WebSocket support (nginx 1.4)
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 80 default;
rewrite ^ https://\$host/; # permanent;
}
END
sudo /etc/init.d/nginx restart
.. warning::
For a production deployment, you should use certificates issued by a
recognized certificate authority. Until you get it, you can use a
self-signed one automatically generated by the package.
Setting up Circle itself
------------------------
Clone the git repository::
git clone https://git.ik.bme.hu/circle/cloud.git circle
Set up *virtualenvwrapper* and the *virtual Python environment* for the
project::
source /etc/bash_completion.d/virtualenvwrapper
mkvirtualenv circle
Set up default Circle configuration and activate the virtual environment::
cat >>/home/cloud/.virtualenvs/circle/bin/postactivate <<END
export DJANGO_SETTINGS_MODULE=circle.settings.production
export DJANGO_DB_HOST=localhost
export DJANGO_DB_PASSWORD=$(cat pgpw)
export DJANGO_FIREWALL_SETTINGS='{"dns_ip": "152.66.243.60", "dns_hostname":
"localhost", "dns_ttl": "300", "reload_sleep": "10",
"rdns_ip": "152.66.243.60", "default_vlangroup": "publikus"}'
export AMQP_URI='amqp://cloud:$(cat rmqpw)@localhost:5672/circle'
export CACHE_URI='pylibmc://127.0.0.1:11211/'
END
workon circle
cd ~/circle
You should change DJANGO_FIREWALL_SETTINGS to your needs.
Install the required Python libraries to the virtual environment::
pip install -r requirements.txt
Sync the database and create a superuser::
circle/manage.py syncdb --all --noinput
circle/manage.py migrate --fake
circle/manage.py createsuperuser
Copy the init files to Upstart's config directory and start the manager and
the portal application server::
sudo cp miscellaneous/mancelery.conf /etc/init/
sudo start mancelery
sudo cp miscellaneous/portal-uwsgi.conf /etc/init/
sudo start portal-uwsgi
docs/install.rst
View file @
d0dc1ed8
...
@@ -3,31 +3,40 @@ Installation of a development machine
...
@@ -3,31 +3,40 @@ Installation of a development machine
.. highlight:: bash
.. highlight:: bash
This tutorial describes the installation of a development environment. To
have a fully working environment, you have to set up the other components
as well. The full procedure is included in the :doc:`Puppet recipes
</puppet>` available for CIRCLE Cloud.
Preparation
Preparation
-----------
-----------
To get the project running on a development machine,
create a new Ubuntu 12.04
To get the project running on a development machine,
launch a new Ubuntu
instanc
e, and log in to it over SSH.
14.04 machin
e, and log in to it over SSH.
To use *git* over *SSH*, we advise enabling SSH *agent forwarding*.
.. info::
On your personal computer check if *ssh-agent* is running (the command should
To use *git* over *SSH*, we advise enabling SSH *agent forwarding*.
print a process id)::
On your terminal computer check if *ssh-agent* is running (the command
should print a process id)::
$ echo $SSH_AGENT_PID
$ echo $SSH_AGENT_PID
1234
1234
If it is not running, you should set up your login manager or some other
If it is not running, you can configure your dektop environment to
solution to automatically launch it.
automatically launch it.
Add your private key to the agent (if it is not added by your desktop
environment)::
Add your private key to the agent (if it is not added by your desktop
ssh-add [~/.ssh/path_to_id_rsa]
environment)::
$ ssh-add [~/.ssh/path_to_id_rsa]
You can read and write all repositories over https, but you will have to
provide username and password for every push command.
Log in to the new vm. The :kbd:`-A` switch enables agent forwarding::
Log in to the new vm. The :kbd:`-A` switch enables agent forwarding::
$
ssh -A cloud@host
ssh -A cloud@host
You can check agent forwarding on the vm::
You can check agent forwarding on the vm::
...
@@ -38,55 +47,55 @@ You can check agent forwarding on the vm::
...
@@ -38,55 +47,55 @@ You can check agent forwarding on the vm::
If the first character of the hostname of the vm is a digit, you have to
If the first character of the hostname of the vm is a digit, you have to
change it, because RabbitMQ won't work with it. ::
change it, because RabbitMQ won't work with it. ::
$
old=$(hostname)
old=$(hostname)
$
new=c-${old}
new=c-${old}
$
sudo tee /etc/hostname <<<$new
sudo tee /etc/hostname <<<$new
$
sudo hostname $new
sudo hostname $new
$
sudo sed -i /etc/hosts -e "s/$old/$new/g"
sudo sed -i /etc/hosts -e "s/$old/$new/g"
Setting up required software
Setting up required software
----------------------------
----------------------------
Update the package lists, and install the required system software::
Update the package lists, and install the required system software::
$
sudo apt-get update
sudo apt-get update
$
sudo apt-get install --yes virtualenvwrapper postgresql git \
sudo apt-get install --yes virtualenvwrapper postgresql git \
python-pip rabbitmq-server libpq-dev python-dev ntp memcached \
python-pip rabbitmq-server libpq-dev python-dev ntp memcached \
libmemcached-dev
libmemcached-dev
Set up *PostgreSQL* to listen on localhost and restart it::
Set up *PostgreSQL* to listen on localhost and restart it::
$
sudo sed -i /etc/postgresql/9.1/main/postgresql.conf -e '/#listen_addresses/ s/^#//'
sudo sed -i /etc/postgresql/9.1/main/postgresql.conf -e '/#listen_addresses/ s/^#//'
$
sudo /etc/init.d/postgresql restart
sudo /etc/init.d/postgresql restart
Also, create a new database and user::
Also, create a new database and user::
$
sudo -u postgres createuser -S -D -R circle
sudo -u postgres createuser -S -D -R circle
$
sudo -u postgres psql <<<"ALTER USER circle WITH PASSWORD 'circle';"
sudo -u postgres psql <<<"ALTER USER circle WITH PASSWORD 'circle';"
$
sudo -u postgres createdb circle -O circle
sudo -u postgres createdb circle -O circle
Configure RabbitMQ: remove the guest user, add virtual host and user with
Configure RabbitMQ: remove the guest user, add virtual host and user with
proper permissions::
proper permissions::
$
sudo rabbitmqctl delete_user guest
sudo rabbitmqctl delete_user guest
$
sudo rabbitmqctl add_vhost circle
sudo rabbitmqctl add_vhost circle
$
sudo rabbitmqctl add_user cloud password
sudo rabbitmqctl add_user cloud password
$
sudo rabbitmqctl set_permissions -p circle cloud '.*' '.*' '.*'
sudo rabbitmqctl set_permissions -p circle cloud '.*' '.*' '.*'
Enable SSH server to accept your name and address from your environment::
Enable SSH server to accept your name and address from your environment::
$
sudo sed -i /etc/ssh/sshd_config -e '$ a AcceptEnv GIT_*'
sudo sed -i /etc/ssh/sshd_config -e '$ a AcceptEnv GIT_*'
$
sudo /etc/init.d/ssh reload
sudo /etc/init.d/ssh reload
You should set these vars in your **local** profile::
You should set these vars in your **local** profile::
$
cat >>~/.profile <<'END'
cat >>~/.profile <<'END'
export GIT_AUTHOR_NAME="Your Name"
export GIT_AUTHOR_NAME="Your Name"
export GIT_AUTHOR_EMAIL="your.address@example.org"
export GIT_AUTHOR_EMAIL="your.address@example.org"
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
END
END
$
source ~/.profile
source ~/.profile
Allow sending it in your **local** ssh configuration::
Allow sending it in your **local** ssh configuration::
...
@@ -100,17 +109,23 @@ Setting up Circle itself
...
@@ -100,17 +109,23 @@ Setting up Circle itself
Clone the git repository::
Clone the git repository::
$ git clone git@git.cloud.ik.bme.hu:circle/cloud.git circle
git clone https://git.ik.bme.hu/circle/cloud.git circle
If you want to push back any modifications, it is possible to set SSH as the
push protocol::
cd circle
git remote set-url --push origin git@git.ik.bme.hu:circle/cloud.git
Set up *virtualenvwrapper* and the *virtual Python environment* for the
Set up *virtualenvwrapper* and the *virtual Python environment* for the
project::
project::
$
source /etc/bash_completion.d/virtualenvwrapper
source /etc/bash_completion.d/virtualenvwrapper
$
mkvirtualenv circle
mkvirtualenv circle
Set up default Circle configuration and activate the virtual environment::
Set up default Circle configuration and activate the virtual environment::
$
cat >>/home/cloud/.virtualenvs/circle/bin/postactivate <<END
cat >>/home/cloud/.virtualenvs/circle/bin/postactivate <<END
export DJANGO_SETTINGS_MODULE=circle.settings.local
export DJANGO_SETTINGS_MODULE=circle.settings.local
export DJANGO_DB_HOST=localhost
export DJANGO_DB_HOST=localhost
export DJANGO_DB_PASSWORD=circle
export DJANGO_DB_PASSWORD=circle
...
@@ -120,32 +135,32 @@ Set up default Circle configuration and activate the virtual environment::
...
@@ -120,32 +135,32 @@ Set up default Circle configuration and activate the virtual environment::
export AMQP_URI='amqp://cloud:password@localhost:5672/circle'
export AMQP_URI='amqp://cloud:password@localhost:5672/circle'
export CACHE_URI='pylibmc://127.0.0.1:11211/'
export CACHE_URI='pylibmc://127.0.0.1:11211/'
END
END
$
workon circle
workon circle
$
cd ~/circle
cd ~/circle
Install the required Python libraries to the virtual environment::
Install the required Python libraries to the virtual environment::
$
pip install -r requirements/local.txt
pip install -r requirements/local.txt
Sync the database and create a superuser::
Sync the database and create a superuser::
$
circle/manage.py syncdb --all --noinput
circle/manage.py syncdb --all --noinput
$
circle/manage.py migrate --fake
circle/manage.py migrate --fake
$ circle/manage.py createsuperuser --username=test --email=test@example.org
circle/manage.py createsuperuser --username=test --email=test@example.org
You can now start the development server::
You can now start the development server::
$
circle/manage.py runserver '[::]:8080'
circle/manage.py runserver '[::]:8080'
You will also need to run a local Celery worker::
You will also need to run a local Celery worker::
$
circle/manage.py celery worker -A manager.mancelery
circle/manage.py celery worker -A manager.mancelery
.. note::
.. note::
You might run the Celery worker (and also the development server) in GNU
You might run the Celery worker (and also the development server) in GNU
Screen, or use Upstart::
Screen, or use Upstart::
$
sudo cp miscellaneous/mancelery.conf /etc/init/
sudo cp miscellaneous/mancelery.conf /etc/init/
$
sudo start mancelery
sudo start mancelery
Building documentation
Building documentation
----------------------
----------------------
...
@@ -153,14 +168,14 @@ Building documentation
...
@@ -153,14 +168,14 @@ Building documentation
To build the *docs*, install *make*, go to the docs folder, and run the building
To build the *docs*, install *make*, go to the docs folder, and run the building
process. ::
process. ::
$
sudo apt-get install make
sudo apt-get install make
$
cd ~/circle/docs/
cd ~/circle/docs/
$
make html
make html
You might also want to serve the generated docs with Python's development
You might also want to serve the generated docs with Python's development
server::
server::
$
(cd _build/html && python -m SimpleHTTPServer 8080)
(cd _build/html && python -m SimpleHTTPServer 8080)
Configuring vim
Configuring vim
---------------
---------------
...
@@ -168,16 +183,16 @@ Configuring vim
...
@@ -168,16 +183,16 @@ Configuring vim
To follow the coding style of the project more easily, you might want to
To follow the coding style of the project more easily, you might want to
configure vim like we do::
configure vim like we do::
$
mkdir -p ~/.vim/autoload ~/.vim/bundle
mkdir -p ~/.vim/autoload ~/.vim/bundle
$
curl -Sso ~/.vim/autoload/pathogen.vim \
curl -Sso ~/.vim/autoload/pathogen.vim \
https://raw.githubusercontent
.com/tpope/vim-pathogen/master/autoload/pathogen.vim
https://raw.github
.com/tpope/vim-pathogen/master/autoload/pathogen.vim
$
cd ~/.vim; mkdir -p bundle; cd bundle && git clone \
cd ~/.vim; mkdir -p bundle; cd bundle && git clone \
git://github.com/klen/python-mode.git
git://github.com/klen/python-mode.git
$
cat >>~/.vimrc <<END
cat >>~/.vimrc <<END
filetype off
filetype off
call pathogen#infect()
call pathogen#infect()
call pathogen#helptags()
call pathogen#helptags()
filetype plugin indent on
filetype plugin indent on
syntax on
syntax on
END
END
$
sudo pip install pyflakes rope pep8 mccabe
sudo pip install pyflakes rope pep8 mccabe
requirements/base.txt
View file @
d0dc1ed8
...
@@ -8,7 +8,7 @@ django-braces==1.4.0
...
@@ -8,7 +8,7 @@ django-braces==1.4.0
django-celery==3.1.10
django-celery==3.1.10
django-crispy-forms==1.4.0
django-crispy-forms==1.4.0
django-model-utils==2.0.3
django-model-utils==2.0.3
django-sizefield==0.
4
django-sizefield==0.
5
django-sshkey==2.2.0
django-sshkey==2.2.0
django-statici18n==1.1
django-statici18n==1.1
django-tables2==0.15.0
django-tables2==0.15.0
...
...
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