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
536b16d6
authored
May 07, 2014
by
Kálmán Viktor
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/master' into feature-template-wizard
Conflicts: circle/dashboard/static/dashboard/dashboard.css
parents
7af77c17
be367e47
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
643 additions
and
119 deletions
+643
-119
circle/common/models.py
+41
-0
circle/common/operations.py
+58
-17
circle/common/tests/test_operations.py
+38
-0
circle/dashboard/static/dashboard/dashboard.css
+55
-0
circle/dashboard/static/dashboard/img/logo.png
+0
-0
circle/dashboard/static/dashboard/vm-details.js
+7
-0
circle/dashboard/templates/base.html
+3
-1
circle/dashboard/templates/dashboard/template-edit.html
+36
-18
circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html
+8
-0
circle/dashboard/templates/dashboard/vm-detail/access.html
+32
-15
circle/dashboard/tests/test_views.py
+118
-0
circle/dashboard/views.py
+37
-15
circle/storage/models.py
+10
-0
circle/vm/models/activity.py
+52
-19
circle/vm/models/instance.py
+24
-4
circle/vm/operations.py
+20
-22
circle/vm/tasks/local_tasks.py
+17
-8
circle/vm/tests/test_models.py
+87
-0
No files found.
circle/common/models.py
View file @
536b16d6
...
...
@@ -18,6 +18,7 @@
from
collections
import
deque
from
contextlib
import
contextmanager
from
hashlib
import
sha224
from
itertools
import
chain
,
imap
from
logging
import
getLogger
from
time
import
time
...
...
@@ -56,12 +57,52 @@ activity_context = contextmanager(activitycontextimpl)
activity_code_separator
=
'.'
def
has_prefix
(
activity_code
,
*
prefixes
):
"""Determine whether the activity code has the specified prefix.
E.g.: has_prefix('foo.bar.buz', 'foo.bar') == True
has_prefix('foo.bar.buz', 'foo', 'bar') == True
has_prefix('foo.bar.buz', 'foo.bar', 'buz') == True
has_prefix('foo.bar.buz', 'foo', 'bar', 'buz') == True
has_prefix('foo.bar.buz', 'foo', 'buz') == False
"""
equal
=
lambda
a
,
b
:
a
==
b
act_code_parts
=
split_activity_code
(
activity_code
)
prefixes
=
chain
(
*
imap
(
split_activity_code
,
prefixes
))
return
all
(
imap
(
equal
,
act_code_parts
,
prefixes
))
def
has_suffix
(
activity_code
,
*
suffixes
):
"""Determine whether the activity code has the specified suffix.
E.g.: has_suffix('foo.bar.buz', 'bar.buz') == True
has_suffix('foo.bar.buz', 'bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo.bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo', 'bar', 'buz') == True
has_suffix('foo.bar.buz', 'foo', 'buz') == False
"""
equal
=
lambda
a
,
b
:
a
==
b
act_code_parts
=
split_activity_code
(
activity_code
)
suffixes
=
list
(
chain
(
*
imap
(
split_activity_code
,
suffixes
)))
return
all
(
imap
(
equal
,
reversed
(
act_code_parts
),
reversed
(
suffixes
)))
def
join_activity_code
(
*
args
):
"""Join the specified parts into an activity code.
:returns: Activity code string.
"""
return
activity_code_separator
.
join
(
args
)
def
split_activity_code
(
activity_code
):
"""Split the specified activity code into its parts.
:returns: A list of activity code parts.
"""
return
activity_code
.
split
(
activity_code_separator
)
class
ActivityModel
(
TimeStampedModel
):
activity_code
=
CharField
(
max_length
=
100
,
verbose_name
=
_
(
'activity code'
))
parent
=
ForeignKey
(
'self'
,
blank
=
True
,
null
=
True
,
related_name
=
'children'
)
...
...
circle/common/operations.py
View file @
536b16d6
...
...
@@ -15,9 +15,10 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from
inspect
import
getargspec
from
logging
import
getLogger
from
.models
import
activity_context
from
.models
import
activity_context
,
has_suffix
from
django.core.exceptions
import
PermissionDenied
...
...
@@ -31,6 +32,7 @@ class Operation(object):
async_queue
=
'localhost.man'
required_perms
=
()
do_not_call_in_templates
=
True
abortable
=
False
def
__call__
(
self
,
**
kwargs
):
return
self
.
call
(
**
kwargs
)
...
...
@@ -46,23 +48,50 @@ class Operation(object):
def
__prelude
(
self
,
kwargs
):
"""This method contains the shared prelude of call and async.
"""
skip_auth_check
=
kwargs
.
setdefault
(
'system'
,
False
)
user
=
kwargs
.
setdefault
(
'user'
,
None
)
parent_activity
=
kwargs
.
pop
(
'parent_activity'
,
None
)
defaults
=
{
'parent_activity'
:
None
,
'system'
:
False
,
'user'
:
None
}
allargs
=
dict
(
defaults
,
**
kwargs
)
# all arguments
auxargs
=
allargs
.
copy
()
# auxiliary (i.e. only for _operation) args
# NOTE: consumed items should be removed from auxargs, and no new items
# should be added to it
skip_auth_check
=
auxargs
.
pop
(
'system'
)
user
=
auxargs
.
pop
(
'user'
)
parent_activity
=
auxargs
.
pop
(
'parent_activity'
)
# check for unexpected keyword arguments
argspec
=
getargspec
(
self
.
_operation
)
if
argspec
.
keywords
is
None
:
# _operation doesn't take ** args
unexpected_kwargs
=
set
(
auxargs
)
-
set
(
argspec
.
args
)
if
unexpected_kwargs
:
raise
TypeError
(
"Operation got unexpected keyword arguments: "
"
%
s"
%
", "
.
join
(
unexpected_kwargs
))
if
not
skip_auth_check
:
self
.
check_auth
(
user
)
self
.
check_precond
()
return
self
.
create_activity
(
parent
=
parent_activity
,
user
=
user
)
def
_exec_op
(
self
,
activity
,
user
,
**
kwargs
):
activity
=
self
.
create_activity
(
parent
=
parent_activity
,
user
=
user
)
return
activity
,
allargs
,
auxargs
def
_exec_op
(
self
,
allargs
,
auxargs
):
"""Execute the operation inside the specified activity's context.
"""
with
activity_context
(
activity
,
on_abort
=
self
.
on_abort
,
# compile arguments for _operation
argspec
=
getargspec
(
self
.
_operation
)
if
argspec
.
keywords
is
not
None
:
# _operation takes ** args
arguments
=
allargs
.
copy
()
else
:
# _operation doesn't take ** args
arguments
=
{
k
:
v
for
(
k
,
v
)
in
allargs
.
iteritems
()
if
k
in
argspec
.
args
}
arguments
.
update
(
auxargs
)
with
activity_context
(
allargs
[
'activity'
],
on_abort
=
self
.
on_abort
,
on_commit
=
self
.
on_commit
):
return
self
.
_operation
(
activity
=
activity
,
user
=
user
,
**
kwarg
s
)
return
self
.
_operation
(
**
argument
s
)
def
_operation
(
self
,
activity
,
user
,
system
,
**
kwargs
):
def
_operation
(
self
,
**
kwargs
):
"""This method is the operation's particular implementation.
Deriving classes should implement this method.
...
...
@@ -82,12 +111,10 @@ class Operation(object):
logger
.
info
(
"
%
s called asynchronously on
%
s with the following "
"parameters:
%
r"
,
self
.
__class__
.
__name__
,
self
.
subject
,
kwargs
)
activity
=
self
.
__prelude
(
kwargs
)
return
self
.
async_operation
.
apply_async
(
args
=
(
self
.
id
,
self
.
subject
.
pk
,
activity
.
pk
),
kwargs
=
kwargs
,
queue
=
self
.
async_queue
)
activity
,
allargs
,
auxargs
=
self
.
__prelude
(
kwargs
)
return
self
.
async_operation
.
apply_async
(
args
=
(
self
.
id
,
self
.
subject
.
pk
,
activity
.
pk
,
allargs
,
auxargs
,
),
queue
=
self
.
async_queue
)
def
call
(
self
,
**
kwargs
):
"""Execute the operation (synchronously).
...
...
@@ -105,8 +132,9 @@ class Operation(object):
logger
.
info
(
"
%
s called (synchronously) on
%
s with the following "
"parameters:
%
r"
,
self
.
__class__
.
__name__
,
self
.
subject
,
kwargs
)
activity
=
self
.
__prelude
(
kwargs
)
return
self
.
_exec_op
(
activity
=
activity
,
**
kwargs
)
activity
,
allargs
,
auxargs
=
self
.
__prelude
(
kwargs
)
allargs
[
'activity'
]
=
activity
return
self
.
_exec_op
(
allargs
,
auxargs
)
def
check_precond
(
self
):
pass
...
...
@@ -160,6 +188,19 @@ class OperatedMixin(object):
else
:
yield
op
def
get_operation_from_activity_code
(
self
,
activity_code
):
"""Get an instance of the Operation corresponding to the specified
activity code.
:returns: A bound instance of an operation, or None if no matching
operation could be found.
"""
for
op
in
getattr
(
self
,
operation_registry_name
,
{})
.
itervalues
():
if
has_suffix
(
activity_code
,
op
.
activity_code_suffix
):
return
op
(
self
)
else
:
return
None
def
register_operation
(
op_cls
,
op_id
=
None
,
target_cls
=
None
):
"""Register the specified operation with the target class.
...
...
circle/common/tests/test_operations.py
View file @
536b16d6
...
...
@@ -75,3 +75,41 @@ class OperationTestCase(TestCase):
patch
.
object
(
Operation
,
'create_activity'
),
\
patch
.
object
(
Operation
,
'_exec_op'
):
op
.
call
(
system
=
True
)
def
test_no_exception_for_more_arguments_when_operation_takes_kwargs
(
self
):
class
KwargOp
(
Operation
):
activity_code_suffix
=
'test'
id
=
'test'
def
_operation
(
self
,
**
kwargs
):
pass
op
=
KwargOp
(
MagicMock
())
with
patch
.
object
(
KwargOp
,
'create_activity'
),
\
patch
.
object
(
KwargOp
,
'_exec_op'
):
op
.
call
(
system
=
True
,
foo
=
42
)
def
test_exception_for_unexpected_arguments
(
self
):
class
TestOp
(
Operation
):
activity_code_suffix
=
'test'
id
=
'test'
def
_operation
(
self
):
pass
op
=
TestOp
(
MagicMock
())
with
patch
.
object
(
TestOp
,
'create_activity'
),
\
patch
.
object
(
TestOp
,
'_exec_op'
):
self
.
assertRaises
(
TypeError
,
op
.
call
,
system
=
True
,
foo
=
42
)
def
test_exception_for_missing_arguments
(
self
):
class
TestOp
(
Operation
):
activity_code_suffix
=
'test'
id
=
'test'
def
_operation
(
self
,
foo
):
pass
op
=
TestOp
(
MagicMock
())
with
patch
.
object
(
TestOp
,
'create_activity'
):
self
.
assertRaises
(
TypeError
,
op
.
call
,
system
=
True
)
circle/dashboard/static/dashboard/dashboard.css
View file @
536b16d6
...
...
@@ -23,6 +23,46 @@ html {
padding-right
:
15px
;
}
/* values for 45px tall navbar */
.navbar
{
min-height
:
45px
;
}
.navbar-brand
{
height
:
45px
;
padding
:
12.5px
12.5px
;
}
.navbar-toggle
{
margin-top
:
5.5px
;
margin-bottom
:
5.5px
;
}
.navbar-form
{
margin-top
:
5.5px
;
margin-bottom
:
5.5px
;
}
.navbar-btn
{
margin-top
:
5.5px
;
margin-bottom
:
5.5px
;
}
.navbar-btn.btn-sm
{
margin-top
:
7.5px
;
margin-bottom
:
7.5px
;
}
.navbar-btn.btn-xs
{
margin-top
:
11.5px
;
margin-bottom
:
11.5px
;
}
.navbar-text
{
margin-top
:
12.5px
;
margin-bottom
:
12.5px
;
}
/* --- */
/* Responsive: Portrait tablets and up */
@media
screen
and
(
min-width
:
768px
)
{
/* Let the jumbotron breathe */
...
...
@@ -33,6 +73,12 @@ html {
.body-content
{
padding
:
0
;
}
.navbar-nav
>
li
>
a
{
padding-top
:
12.5px
;
padding-bottom
:
12.5px
;
}
}
.no-margin
{
margin
:
0
!important
;
...
...
@@ -552,3 +598,12 @@ footer a, footer a:hover, footer a:visited {
#ops
{
padding
:
15px
0
15px
15px
;
}
#vm-access-table
th
:last-child
,
#vm-access-table
td
:last-child
,
#template-access-table
th
:last-child
,
#template-access-table
td
:last-child
{
text-align
:
center
;
}
#notifications-button
{
margin
:
0
;
}
circle/dashboard/static/dashboard/img/logo.png
0 → 100644
View file @
536b16d6
6 KB
circle/dashboard/static/dashboard/vm-details.js
View file @
536b16d6
...
...
@@ -183,6 +183,7 @@ $(function() {
$
(
"#vm-details-h1-name"
).
hide
();
$
(
"#vm-details-rename"
).
css
(
'display'
,
'inline'
);
$
(
"#vm-details-rename-name"
).
focus
();
return
false
;
});
/* rename in home tab */
...
...
@@ -190,6 +191,7 @@ $(function() {
$
(
".vm-details-home-edit-name-click"
).
hide
();
$
(
"#vm-details-home-rename"
).
show
();
$
(
"input"
,
$
(
"#vm-details-home-rename"
)).
focus
();
return
false
;
});
/* rename ajax */
...
...
@@ -219,6 +221,11 @@ $(function() {
$
(
".vm-details-home-edit-description-click"
).
click
(
function
()
{
$
(
".vm-details-home-edit-description-click"
).
hide
();
$
(
"#vm-details-home-description"
).
show
();
var
ta
=
$
(
"#vm-details-home-description textarea"
);
var
tmp
=
ta
.
val
();
ta
.
val
(
""
);
ta
.
focus
();
ta
.
val
(
tmp
)
return
false
;
});
...
...
circle/dashboard/templates/base.html
View file @
536b16d6
...
...
@@ -26,7 +26,9 @@
<body>
<div
class=
"navbar navbar-inverse navbar-fixed-top"
>
<div
class=
"navbar-header"
>
<a
class=
"navbar-brand"
href=
"{% url "
dashboard
.
index
"
%}"
>
CIRCLE
</a>
<a
class=
"navbar-brand"
href=
"{% url "
dashboard
.
index
"
%}"
style=
"padding: 10px 15px;"
>
<img
src=
"{{ STATIC_URL}}dashboard/img/logo.png"
style=
"height: 25px;"
/>
</a>
<button
type=
"button"
class=
"navbar-toggle"
data-toggle=
"collapse"
data-target=
".navbar-collapse"
>
<span
class=
"icon-bar"
></span>
<span
class=
"icon-bar"
></span>
...
...
circle/dashboard/templates/dashboard/template-edit.html
View file @
536b16d6
...
...
@@ -8,7 +8,7 @@
{% block content %}
<div
class=
"row"
>
<div
class=
"col-md-
8
"
>
<div
class=
"col-md-
7
"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<a
class=
"pull-right btn btn-default btn-xs"
href=
"{% url "
dashboard
.
views
.
template-list
"
%}"
>
{% trans "Back" %}
</a>
...
...
@@ -23,33 +23,51 @@
</div>
</div>
<div
class=
"col-md-
4
"
>
<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
.
template-acl
"
pk=
object.pk
%}"
method=
"post"
>
{% csrf_token %}
<table
class=
"table table-striped table-with-form-fields"
>
<thead><tr><th></th><th>
{% trans "Who" %}
</th><th>
{% trans "What" %}
</th><th></th></tr></thead>
<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>
{{i.user}}
</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><a
href=
"#"
class=
"btn btn-link btn-xs"
><i
class=
"icon-remove"
><span
class=
"sr-only"
>
{% trans "remove" %}
</span></i></a></td></tr>
<tr>
<td><i
class=
"icon-user"
></i></td><td>
{{i.user}}
</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>
{{i.group}}
</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><a
href=
"#"
class=
"btn btn-link btn-xs"
><i
class=
"icon-remove"
><span
class=
"sr-only"
>
{% trans "remove" %}
</span></i></a></td></tr>
<tr>
<td><i
class=
"icon-group"
></i></td><td>
{{i.group}}
</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"
...
...
circle/dashboard/templates/dashboard/vm-detail/_activity-timeline.html
View file @
536b16d6
...
...
@@ -9,6 +9,14 @@
{{ a.get_readable_name }}{% if user.is_superuser %}
</a>
{% endif %}
</strong>
{{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.is_abortable_for_user %}
<form
action=
"{{ a.instance.get_absolute_url }}"
method=
"POST"
class=
"pull-right"
>
{% csrf_token %}
<input
type=
"hidden"
name=
"abort_operation"
/>
<input
type=
"hidden"
name=
"activity"
value=
"{{ a.pk }}"
/>
<button
class=
"btn btn-danger btn-xs"
><i
class=
"icon-bolt"
></i>
{% trans "Abort" %}
</button>
</form>
{% endif %}
{% if a.children.count > 0 %}
<div
class=
"sub-timeline"
>
{% for s in a.children.all %}
...
...
circle/dashboard/templates/dashboard/vm-detail/access.html
View file @
536b16d6
...
...
@@ -15,26 +15,43 @@
</p>
<h3>
{% trans "Permissions"|capfirst %}
</h3>
<form
action=
"{{acl.url}}"
method=
"post"
>
{% csrf_token %}
<table
class=
"table table-striped table-with-form-fields"
>
<thead><tr><th></th><th>
{% trans "Who" %}
</th><th>
{% trans "What" %}
</th><th></th></tr></thead>
<table
class=
"table table-striped table-with-form-fields"
id=
"vm-access-table"
>
<thead><tr>
<th></th>
<th>
{% trans "Who" %}
</th>
<th>
{% trans "What" %}
</th>
<th>
{% trans "Remove" %}
</th>
</tr></thead>
<tbody>
{% for i in acl.users %}
<tr><td><i
class=
"icon-user"
></i></td><td>
{{i.user}}
</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><a
href=
"#"
class=
"btn btn-link btn-xs"
><i
class=
"icon-remove"
><span
class=
"sr-only"
>
{% trans "remove" %}
</span></i></a></td></tr>
<tr>
<td><i
class=
"icon-user"
></i></td>
<td>
{{i.user}}
</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}}"
/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr><td><i
class=
"icon-group"
></i></td><td>
{{i.group}}
</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 %}
<tr>
<td><i
class=
"icon-group"
></i></td><td>
{{i.group}}
</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><a
href=
"#"
class=
"btn btn-link btn-xs"
><i
class=
"icon-remove"
><span
class=
"sr-only"
>
{% trans "remove" %}
</span></i></a></td></tr>
<td>
<input
type=
"checkbox"
name=
"remove-g-{{i.group.id}}"
/>
</td>
</tr>
{% endfor %}
<tr><td><i
class=
"icon-plus"
></i></td>
<td><input
type=
"text"
class=
"form-control"
name=
"perm-new-name"
...
...
circle/dashboard/tests/test_views.py
View file @
536b16d6
...
...
@@ -1161,3 +1161,121 @@ class ProfileViewTest(LoginMixin, TestCase):
self
.
assertIsNotNone
(
authenticate
(
username
=
"user1"
,
password
=
"password"
))
self
.
assertIsNone
(
authenticate
(
username
=
"user1"
,
password
=
"asd"
))
class
AclViewTest
(
LoginMixin
,
TestCase
):
fixtures
=
[
'test-vm-fixture.json'
,
'node.json'
]
def
setUp
(
self
):
Instance
.
get_remote_queue_name
=
Mock
(
return_value
=
'test'
)
self
.
u1
=
User
.
objects
.
create
(
username
=
'user1'
)
self
.
u1
.
set_password
(
'password'
)
self
.
u1
.
save
()
self
.
u2
=
User
.
objects
.
create
(
username
=
'user2'
,
is_staff
=
True
)
self
.
u2
.
set_password
(
'password'
)
self
.
u2
.
save
()
self
.
us
=
User
.
objects
.
create
(
username
=
'superuser'
,
is_superuser
=
True
)
self
.
us
.
set_password
(
'password'
)
self
.
us
.
save
()
self
.
ut
=
User
.
objects
.
get
(
username
=
"test"
)
self
.
g1
=
Group
.
objects
.
create
(
name
=
'group1'
)
self
.
g1
.
user_set
.
add
(
self
.
u1
)
self
.
g1
.
user_set
.
add
(
self
.
u2
)
self
.
g1
.
save
()
settings
[
"default_vlangroup"
]
=
'public'
VlanGroup
.
objects
.
create
(
name
=
'public'
)
def
tearDown
(
self
):
super
(
AclViewTest
,
self
)
.
tearDown
()
self
.
u1
.
delete
()
self
.
u2
.
delete
()
self
.
us
.
delete
()
self
.
g1
.
delete
()
def
test_permitted_instance_access_revoke
(
self
):
c
=
Client
()
# this is from the fixtures
self
.
login
(
c
,
"test"
,
"test"
)
inst
=
Instance
.
objects
.
get
(
id
=
1
)
inst
.
set_level
(
self
.
u1
,
"user"
)
resp
=
c
.
post
(
"/dashboard/vm/1/acl/"
,
{
'remove-u-
%
d'
%
self
.
u1
.
pk
:
""
,
'perm-new-name'
:
""
,
'perm-new'
:
""
,
})
self
.
assertFalse
((
self
.
u1
,
"user"
)
in
inst
.
get_users_with_level
())
self
.
assertEqual
(
resp
.
status_code
,
302
)
def
test_unpermitted_instance_access_revoke
(
self
):
c
=
Client
()
self
.
login
(
c
,
self
.
u2
)
inst
=
Instance
.
objects
.
get
(
id
=
1
)
inst
.
set_level
(
self
.
u1
,
"user"
)
resp
=
c
.
post
(
"/dashboard/vm/1/acl/"
,
{
'remove-u-
%
d'
%
self
.
u1
.
pk
:
""
,
'perm-new-name'
:
""
,
'perm-new'
:
""
,
})
self
.
assertTrue
((
self
.
u1
,
"user"
)
in
inst
.
get_users_with_level
())
self
.
assertEqual
(
resp
.
status_code
,
403
)
def
test_instance_original_owner_access_revoke
(
self
):
c
=
Client
()
self
.
login
(
c
,
self
.
u1
)
inst
=
Instance
.
objects
.
get
(
id
=
1
)
inst
.
set_level
(
self
.
u1
,
"owner"
)
inst
.
set_level
(
self
.
ut
,
"owner"
)
resp
=
c
.
post
(
"/dashboard/vm/1/acl/"
,
{
'remove-u-
%
d'
%
self
.
ut
.
pk
:
""
,
'perm-new-name'
:
""
,
'perm-new'
:
""
,
})
self
.
assertEqual
(
self
.
ut
,
Instance
.
objects
.
get
(
id
=
1
)
.
owner
)
self
.
assertTrue
((
self
.
ut
,
"owner"
)
in
inst
.
get_users_with_level
())
self
.
assertEqual
(
resp
.
status_code
,
302
)
def
test_permitted_template_access_revoke
(
self
):
c
=
Client
()
# this is from the fixtures
self
.
login
(
c
,
"test"
,
"test"
)
tmpl
=
InstanceTemplate
.
objects
.
get
(
id
=
1
)
tmpl
.
set_level
(
self
.
u1
,
"user"
)
resp
=
c
.
post
(
"/dashboard/template/1/acl/"
,
{
'remove-u-
%
d'
%
self
.
u1
.
pk
:
""
,
'perm-new-name'
:
""
,
'perm-new'
:
""
,
})
self
.
assertFalse
((
self
.
u1
,
"user"
)
in
tmpl
.
get_users_with_level
())
self
.
assertEqual
(
resp
.
status_code
,
302
)
def
test_unpermitted_template_access_revoke
(
self
):
c
=
Client
()
self
.
login
(
c
,
self
.
u2
)
tmpl
=
InstanceTemplate
.
objects
.
get
(
id
=
1
)
tmpl
.
set_level
(
self
.
u1
,
"user"
)
resp
=
c
.
post
(
"/dashboard/template/1/acl/"
,
{
'remove-u-
%
d'
%
self
.
u1
.
pk
:
""
,
'perm-new-name'
:
""
,
'perm-new'
:
""
,
})
self
.
assertTrue
((
self
.
u1
,
"user"
)
in
tmpl
.
get_users_with_level
())
self
.
assertEqual
(
resp
.
status_code
,
403
)
def
test_template_original_owner_access_revoke
(
self
):
c
=
Client
()
self
.
login
(
c
,
self
.
u1
)
tmpl
=
InstanceTemplate
.
objects
.
get
(
id
=
1
)
tmpl
.
set_level
(
self
.
u1
,
"owner"
)
tmpl
.
set_level
(
self
.
ut
,
"owner"
)
resp
=
c
.
post
(
"/dashboard/template/1/acl/"
,
{
'remove-u-
%
d'
%
self
.
ut
.
pk
:
""
,
'perm-new-name'
:
""
,
'perm-new'
:
""
,
})
self
.
assertEqual
(
self
.
ut
,
InstanceTemplate
.
objects
.
get
(
id
=
1
)
.
owner
)
self
.
assertTrue
((
self
.
ut
,
"owner"
)
in
tmpl
.
get_users_with_level
())
self
.
assertEqual
(
resp
.
status_code
,
302
)
circle/dashboard/views.py
View file @
536b16d6
...
...
@@ -224,11 +224,7 @@ class VmDetailView(CheckedDetailView):
})
# activity data
context
[
'activities'
]
=
(
InstanceActivity
.
objects
.
filter
(
instance
=
self
.
object
,
parent
=
None
)
.
order_by
(
'-started'
)
.
select_related
(
'user'
)
.
prefetch_related
(
'children'
))
context
[
'activities'
]
=
self
.
object
.
get_activities
(
self
.
request
.
user
)
context
[
'vlans'
]
=
Vlan
.
get_objects_with_level
(
'user'
,
self
.
request
.
user
...
...
@@ -260,6 +256,7 @@ class VmDetailView(CheckedDetailView):
'to_remove'
:
self
.
__remove_tag
,
'port'
:
self
.
__add_port
,
'new_network_vlan'
:
self
.
__new_network
,
'abort_operation'
:
self
.
__abort_operation
,
}
for
k
,
v
in
options
.
iteritems
():
if
request
.
POST
.
get
(
k
)
is
not
None
:
...
...
@@ -445,6 +442,16 @@ class VmDetailView(CheckedDetailView):
return
redirect
(
"
%
s#network"
%
reverse_lazy
(
"dashboard.views.detail"
,
kwargs
=
{
'pk'
:
self
.
object
.
pk
}))
def
__abort_operation
(
self
,
request
):
self
.
object
=
self
.
get_object
()
activity
=
get_object_or_404
(
InstanceActivity
,
pk
=
request
.
POST
.
get
(
"activity"
))
if
not
activity
.
is_abortable_for
(
request
.
user
):
raise
PermissionDenied
()
activity
.
abort
()
return
redirect
(
"
%
s#activity"
%
self
.
object
.
get_absolute_url
())
class
OperationView
(
DetailView
):
...
...
@@ -714,8 +721,9 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
unicode
(
instance
),
unicode
(
request
.
user
))
raise
PermissionDenied
()
self
.
set_levels
(
request
,
instance
)
self
.
remove_levels
(
request
,
instance
)
self
.
add_levels
(
request
,
instance
)
return
redirect
(
instance
)
return
redirect
(
"
%
s#access"
%
instance
.
get_absolute_url
()
)
def
set_levels
(
self
,
request
,
instance
):
for
key
,
value
in
request
.
POST
.
items
():
...
...
@@ -732,6 +740,24 @@ class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
unicode
(
entity
),
unicode
(
instance
),
value
,
unicode
(
request
.
user
))
def
remove_levels
(
self
,
request
,
instance
):
for
key
,
value
in
request
.
POST
.
items
():
if
key
.
startswith
(
"remove"
):
typ
=
key
[
7
:
8
]
# len("remove-")
id
=
key
[
9
:]
# len("remove-x-")
entity
=
{
'u'
:
User
,
'g'
:
Group
}[
typ
]
.
objects
.
get
(
id
=
id
)
if
getattr
(
instance
,
"owner"
,
None
)
==
entity
:
logger
.
info
(
"Tried to remove owner from
%
s by
%
s."
,
unicode
(
instance
),
unicode
(
request
.
user
))
msg
=
_
(
"The original owner cannot be removed, however "
"you can transfer ownership!"
)
messages
.
warning
(
request
,
msg
)
continue
instance
.
set_level
(
entity
,
None
)
logger
.
info
(
"Revoked
%
s's access to
%
s by
%
s."
,
unicode
(
entity
),
unicode
(
instance
),
unicode
(
request
.
user
))
def
add_levels
(
self
,
request
,
instance
):
name
=
request
.
POST
[
'perm-new-name'
]
value
=
request
.
POST
[
'perm-new'
]
...
...
@@ -772,6 +798,7 @@ class TemplateAclUpdateView(AclUpdateView):
else
:
self
.
set_levels
(
request
,
template
)
self
.
add_levels
(
request
,
template
)
self
.
remove_levels
(
request
,
template
)
post_for_disk
=
request
.
POST
.
copy
()
post_for_disk
[
'perm-new'
]
=
'user'
...
...
@@ -779,8 +806,7 @@ class TemplateAclUpdateView(AclUpdateView):
for
d
in
template
.
disks
.
all
():
self
.
add_levels
(
request
,
d
)
return
redirect
(
reverse
(
"dashboard.views.template-detail"
,
kwargs
=
self
.
kwargs
))
return
redirect
(
template
)
class
GroupAclUpdateView
(
AclUpdateView
):
...
...
@@ -1791,9 +1817,7 @@ def vm_activity(request, pk):
if
only_status
==
"false"
:
# instance activity
context
=
{
'instance'
:
instance
,
'activities'
:
InstanceActivity
.
objects
.
filter
(
instance
=
instance
,
parent
=
None
)
.
order_by
(
'-started'
)
.
select_related
(),
'activities'
:
instance
.
get_activities
(
request
.
user
),
'ops'
:
get_operations
(
instance
,
request
.
user
),
}
...
...
@@ -2398,10 +2422,8 @@ class InstanceActivityDetail(SuperuserRequiredMixin, DetailView):
def
get_context_data
(
self
,
**
kwargs
):
ctx
=
super
(
InstanceActivityDetail
,
self
)
.
get_context_data
(
**
kwargs
)
ctx
[
'activities'
]
=
(
self
.
object
.
instance
.
activity_log
.
filter
(
parent
=
None
)
.
order_by
(
'-started'
)
.
select_related
(
'user'
)
.
prefetch_related
(
'children'
))
ctx
[
'activities'
]
=
self
.
object
.
instance
.
get_activities
(