Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
Gelencsér Szabolcs
/
circlestack
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
f469fe6a
authored
Apr 02, 2014
by
Dudás Ádám
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
move generic operations to common module
parent
7756a844
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
225 additions
and
180 deletions
+225
-180
circle/common/operations.py
+133
-0
circle/common/tests/test_operations.py
+60
-0
circle/vm/models/instance.py
+2
-11
circle/vm/operations.py
+29
-111
circle/vm/tests/test_operations.py
+1
-58
No files found.
circle/common/operations.py
0 → 100644
View file @
f469fe6a
from
.models
import
activity_context
from
django.core.exceptions
import
PermissionDenied
class
Operation
(
object
):
"""Base class for VM operations.
"""
async_queue
=
'localhost.man'
required_perms
=
()
def
__call__
(
self
,
**
kwargs
):
return
self
.
call
(
**
kwargs
)
def
__init__
(
self
,
subject
):
"""Initialize a new operation bound to the specified subject.
"""
self
.
subject
=
subject
def
__unicode__
(
self
):
return
self
.
name
def
__prelude
(
self
,
kwargs
):
"""This method contains the shared prelude of call and async.
"""
skip_checks
=
kwargs
.
setdefault
(
'system'
,
False
)
user
=
kwargs
.
setdefault
(
'user'
,
None
)
if
not
skip_checks
:
self
.
check_auth
(
user
)
self
.
check_precond
()
return
self
.
create_activity
(
user
=
user
)
def
_exec_op
(
self
,
activity
,
user
,
**
kwargs
):
"""Execute the operation inside the specified activity's context.
"""
with
activity_context
(
activity
,
on_abort
=
self
.
on_abort
,
on_commit
=
self
.
on_commit
):
return
self
.
_operation
(
activity
,
user
,
**
kwargs
)
def
_operation
(
self
,
activity
,
user
,
system
,
**
kwargs
):
"""This method is the operation's particular implementation.
Deriving classes should implement this method.
"""
raise
NotImplementedError
def
async
(
self
,
**
kwargs
):
"""Execute the operation asynchronously.
Only a quick, preliminary check is ran before creating the associated
activity and queuing the job.
The returned value is the handle for the asynchronous job.
For more information, check the synchronous call's documentation.
"""
activity
=
self
.
__prelude
(
kwargs
)
return
self
.
async_operation
.
apply_async
(
args
=
(
self
.
id
,
self
.
subject
.
pk
,
activity
.
pk
),
kwargs
=
kwargs
,
queue
=
self
.
async_queue
)
def
call
(
self
,
**
kwargs
):
"""Execute the operation (synchronously).
Anticipated keyword arguments:
* user: The User invoking the operation. If this argument is not
present, it'll be provided with a default value of None.
* system: Indicates that the operation is invoked by the system, not a
User. If this argument is present and has a value of True,
then authorization checks are skipped.
"""
activity
=
self
.
__prelude
(
kwargs
)
return
self
.
_exec_op
(
activity
=
activity
,
**
kwargs
)
def
check_precond
(
self
):
pass
def
check_auth
(
self
,
user
):
if
not
user
.
has_perms
(
self
.
required_perms
):
raise
PermissionDenied
(
"
%
s doesn't have the required permissions."
%
user
)
def
create_activity
(
self
,
user
):
raise
NotImplementedError
def
on_abort
(
self
,
activity
,
error
):
"""This method is called when the operation aborts (i.e. raises an
exception).
"""
pass
def
on_commit
(
self
,
activity
):
"""This method is called when the operation executes successfully.
"""
pass
operation_registry_name
=
'_ops'
class
OperatedMixin
(
object
):
def
__getattr__
(
self
,
name
):
# NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__
cls
=
self
.
__class__
ops
=
getattr
(
cls
,
operation_registry_name
,
{})
op
=
ops
.
get
(
name
)
if
op
:
return
op
(
self
)
else
:
raise
AttributeError
(
"
%
r object has no attribute
%
r"
%
(
self
.
__class__
.
__name__
,
name
))
def
register_operation
(
target_cls
,
op_cls
,
op_id
=
None
):
"""Register the specified operation with the target class.
You can optionally specify an ID to be used for the registration;
otherwise, the operation class' 'id' attribute will be used.
"""
if
op_id
is
None
:
op_id
=
op_cls
.
id
if
not
issubclass
(
target_cls
,
OperatedMixin
):
raise
TypeError
(
"
%
r is not a subclass of
%
r"
%
(
target_cls
.
__name__
,
OperatedMixin
.
__name__
))
if
not
hasattr
(
target_cls
,
operation_registry_name
):
setattr
(
target_cls
,
operation_registry_name
,
dict
())
getattr
(
target_cls
,
operation_registry_name
)[
op_id
]
=
op_cls
circle/common/tests/test_operations.py
0 → 100644
View file @
f469fe6a
from
mock
import
MagicMock
,
patch
from
django.test
import
TestCase
from
..operations
import
Operation
class
OperationTestCase
(
TestCase
):
def
test_activity_created_before_async_job
(
self
):
class
AbortEx
(
Exception
):
pass
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
op
.
async_operation
=
MagicMock
(
apply_async
=
MagicMock
(
side_effect
=
AbortEx
))
with
patch
.
object
(
Operation
,
'check_precond'
):
with
patch
.
object
(
Operation
,
'create_activity'
)
as
create_act
:
try
:
op
.
async
(
system
=
True
)
except
AbortEx
:
self
.
assertTrue
(
create_act
.
called
)
def
test_check_precond_called_before_create_activity
(
self
):
class
AbortEx
(
Exception
):
pass
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
with
patch
.
object
(
Operation
,
'create_activity'
,
side_effect
=
AbortEx
):
with
patch
.
object
(
Operation
,
'check_precond'
)
as
chk_pre
:
try
:
op
.
call
(
system
=
True
)
except
AbortEx
:
self
.
assertTrue
(
chk_pre
.
called
)
def
test_auth_check_on_non_system_call
(
self
):
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
user
=
MagicMock
()
with
patch
.
object
(
Operation
,
'check_auth'
)
as
check_auth
:
with
patch
.
object
(
Operation
,
'check_precond'
),
\
patch
.
object
(
Operation
,
'create_activity'
),
\
patch
.
object
(
Operation
,
'_exec_op'
):
op
.
call
(
user
=
user
)
check_auth
.
assert_called_with
(
user
)
def
test_no_auth_check_on_system_call
(
self
):
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
with
patch
.
object
(
Operation
,
'check_auth'
,
side_effect
=
AssertionError
):
with
patch
.
object
(
Operation
,
'check_precond'
),
\
patch
.
object
(
Operation
,
'create_activity'
),
\
patch
.
object
(
Operation
,
'_exec_op'
):
op
.
call
(
system
=
True
)
circle/vm/models/instance.py
View file @
f469fe6a
...
...
@@ -20,6 +20,7 @@ from model_utils.models import TimeStampedModel, StatusModel
from
taggit.managers
import
TaggableManager
from
acl.models
import
AclBase
from
common.operations
import
OperatedMixin
from
storage.models
import
Disk
from
..tasks
import
vm_tasks
,
agent_tasks
from
.activity
import
(
ActivityInProgressError
,
instance_activity
,
...
...
@@ -160,7 +161,7 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
return
(
'dashboard.views.template-detail'
,
None
,
{
'pk'
:
self
.
pk
})
class
Instance
(
AclBase
,
VirtualMachineDescModel
,
StatusModel
,
class
Instance
(
AclBase
,
VirtualMachineDescModel
,
StatusModel
,
OperatedMixin
,
TimeStampedModel
):
"""Virtual machine instance.
...
...
@@ -216,7 +217,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
"destruction."
))
objects
=
Manager
()
active
=
InstanceActiveManager
()
_ops
=
{}
# operation factory registry
class
Meta
:
app_label
=
'vm'
...
...
@@ -254,15 +254,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel,
self
.
instance
=
instance
def
__getattr__
(
self
,
name
):
# NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__
if
name
in
self
.
_ops
:
return
self
.
_ops
[
name
](
self
)
else
:
raise
AttributeError
(
"
%
s object has no attribute '
%
s'"
%
(
self
.
__class__
.
__name__
,
name
))
def
__unicode__
(
self
):
parts
=
(
self
.
name
,
"("
+
str
(
self
.
id
)
+
")"
)
return
" "
.
join
(
s
for
s
in
parts
if
s
!=
""
)
...
...
circle/vm/operations.py
View file @
f469fe6a
...
...
@@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _
from
celery.exceptions
import
TimeLimitExceeded
from
common.
models
import
activity_context
from
common.
operations
import
Operation
,
register_operation
from
storage.models
import
Disk
from
.tasks
import
vm_tasks
from
.tasks.local_tasks
import
async_instance_operation
...
...
@@ -18,78 +18,14 @@ from .models import Instance, InstanceActivity, InstanceTemplate
logger
=
getLogger
(
__name__
)
class
Operation
(
object
):
"""Base class for VM operations.
"""
class
InstanceOperation
(
Operation
):
acl_level
=
'owner'
async_queue
=
'localhost.man'
required_perms
=
()
def
__call__
(
self
,
**
kwargs
):
return
self
.
call
(
**
kwargs
)
async_operation
=
async_instance_operation
def
__init__
(
self
,
instance
):
"""Initialize a new operation bound to the specified VM instance.
"""
super
(
InstanceOperation
,
self
)
.
__init__
(
subject
=
instance
)
self
.
instance
=
instance
def
__unicode__
(
self
):
return
self
.
name
def
__prelude
(
self
,
kwargs
):
"""This method contains the shared prelude of call and async.
"""
skip_checks
=
kwargs
.
setdefault
(
'system'
,
False
)
user
=
kwargs
.
setdefault
(
'user'
,
None
)
if
not
skip_checks
:
self
.
check_auth
(
user
)
self
.
check_precond
()
return
self
.
create_activity
(
user
=
user
)
def
_exec_op
(
self
,
activity
,
user
,
**
kwargs
):
"""Execute the operation inside the specified activity's context.
"""
with
activity_context
(
activity
,
on_abort
=
self
.
on_abort
,
on_commit
=
self
.
on_commit
):
return
self
.
_operation
(
activity
,
user
,
**
kwargs
)
def
_operation
(
self
,
activity
,
user
,
system
,
**
kwargs
):
"""This method is the operation's particular implementation.
Deriving classes should implement this method.
"""
raise
NotImplementedError
def
async
(
self
,
**
kwargs
):
"""Execute the operation asynchronously.
Only a quick, preliminary check is ran before creating the associated
activity and queuing the job.
The returned value is the handle for the asynchronous job.
For more information, check the synchronous call's documentation.
"""
activity
=
self
.
__prelude
(
kwargs
)
return
async_instance_operation
.
apply_async
(
args
=
(
self
.
id
,
self
.
instance
.
pk
,
activity
.
pk
),
kwargs
=
kwargs
,
queue
=
self
.
async_queue
)
def
call
(
self
,
**
kwargs
):
"""Execute the operation (synchronously).
Anticipated keyword arguments:
* user: The User invoking the operation. If this argument is not
present, it'll be provided with a default value of None.
* system: Indicates that the operation is invoked by the system, not a
User. If this argument is present and has a value of True,
then authorization checks are skipped.
"""
activity
=
self
.
__prelude
(
kwargs
)
return
self
.
_exec_op
(
activity
=
activity
,
**
kwargs
)
def
check_precond
(
self
):
if
self
.
instance
.
destroyed_at
:
raise
self
.
instance
.
InstanceDestroyedError
(
self
.
instance
)
...
...
@@ -99,36 +35,18 @@ class Operation(object):
raise
PermissionDenied
(
"
%
s doesn't have the required ACL level."
%
user
)
if
not
user
.
has_perms
(
self
.
required_perms
):
raise
PermissionDenied
(
"
%
s doesn't have the required permissions."
%
user
)
super
(
InstanceOperation
,
self
)
.
check_auth
(
user
=
user
)
def
create_activity
(
self
,
user
):
return
InstanceActivity
.
create
(
code_suffix
=
self
.
activity_code_suffix
,
instance
=
self
.
instance
,
user
=
user
)
def
on_abort
(
self
,
activity
,
error
):
"""This method is called when the operation aborts (i.e. raises an
exception).
"""
pass
def
on_commit
(
self
,
activity
):
"""This method is called when the operation executes successfully.
"""
pass
def
register_operation
(
op_cls
,
op_id
=
None
):
"""Register the specified operation with Instance.
"""
if
op_id
is
None
:
op_id
=
op_cls
.
id
Instance
.
_ops
[
op_id
]
=
lambda
inst
:
op_cls
(
inst
)
def
register_instance_operation
(
op_cls
,
op_id
=
None
):
return
register_operation
(
Instance
,
op_cls
,
op_id
)
class
DeployOperation
(
Operation
):
class
DeployOperation
(
Instance
Operation
):
activity_code_suffix
=
'deploy'
id
=
'deploy'
name
=
_
(
"deploy"
)
...
...
@@ -167,10 +85,10 @@ class DeployOperation(Operation):
self
.
instance
.
_deploy_vm
(
activity
)
register_operation
(
DeployOperation
)
register_
instance_
operation
(
DeployOperation
)
class
DestroyOperation
(
Operation
):
class
DestroyOperation
(
Instance
Operation
):
activity_code_suffix
=
'destroy'
id
=
'destroy'
name
=
_
(
"destroy"
)
...
...
@@ -205,10 +123,10 @@ class DestroyOperation(Operation):
self
.
instance
.
save
()
register_operation
(
DestroyOperation
)
register_
instance_
operation
(
DestroyOperation
)
class
MigrateOperation
(
Operation
):
class
MigrateOperation
(
Instance
Operation
):
activity_code_suffix
=
'migrate'
id
=
'migrate'
name
=
_
(
"migrate"
)
...
...
@@ -239,10 +157,10 @@ class MigrateOperation(Operation):
net
.
deploy
()
register_operation
(
MigrateOperation
)
register_
instance_
operation
(
MigrateOperation
)
class
RebootOperation
(
Operation
):
class
RebootOperation
(
Instance
Operation
):
activity_code_suffix
=
'reboot'
id
=
'reboot'
name
=
_
(
"reboot"
)
...
...
@@ -254,10 +172,10 @@ class RebootOperation(Operation):
queue
=
queue_name
)
.
get
(
timeout
=
timeout
)
register_operation
(
RebootOperation
)
register_
instance_
operation
(
RebootOperation
)
class
RedeployOperation
(
Operation
):
class
RedeployOperation
(
Instance
Operation
):
activity_code_suffix
=
'redeploy'
id
=
'redeploy'
name
=
_
(
"redeploy"
)
...
...
@@ -286,10 +204,10 @@ class RedeployOperation(Operation):
self
.
instance
.
_deploy_vm
(
activity
)
register_operation
(
RedeployOperation
)
register_
instance_
operation
(
RedeployOperation
)
class
ResetOperation
(
Operation
):
class
ResetOperation
(
Instance
Operation
):
activity_code_suffix
=
'reset'
id
=
'reset'
name
=
_
(
"reset"
)
...
...
@@ -301,10 +219,10 @@ class ResetOperation(Operation):
queue
=
queue_name
)
.
get
(
timeout
=
timeout
)
register_operation
(
ResetOperation
)
register_
instance_
operation
(
ResetOperation
)
class
SaveAsTemplateOperation
(
Operation
):
class
SaveAsTemplateOperation
(
Instance
Operation
):
activity_code_suffix
=
'save_as_template'
id
=
'save_as_template'
name
=
_
(
"save as template"
)
...
...
@@ -358,10 +276,10 @@ class SaveAsTemplateOperation(Operation):
return
tmpl
register_operation
(
SaveAsTemplateOperation
)
register_
instance_
operation
(
SaveAsTemplateOperation
)
class
ShutdownOperation
(
Operation
):
class
ShutdownOperation
(
Instance
Operation
):
activity_code_suffix
=
'shutdown'
id
=
'shutdown'
name
=
_
(
"shutdown"
)
...
...
@@ -387,10 +305,10 @@ class ShutdownOperation(Operation):
self
.
instance
.
save
()
register_operation
(
ShutdownOperation
)
register_
instance_
operation
(
ShutdownOperation
)
class
ShutOffOperation
(
Operation
):
class
ShutOffOperation
(
Instance
Operation
):
activity_code_suffix
=
'shut_off'
id
=
'shut_off'
name
=
_
(
"shut off"
)
...
...
@@ -408,10 +326,10 @@ class ShutOffOperation(Operation):
self
.
instance
.
save
()
register_operation
(
ShutOffOperation
)
register_
instance_
operation
(
ShutOffOperation
)
class
SleepOperation
(
Operation
):
class
SleepOperation
(
Instance
Operation
):
activity_code_suffix
=
'sleep'
id
=
'sleep'
name
=
_
(
"sleep"
)
...
...
@@ -447,10 +365,10 @@ class SleepOperation(Operation):
self
.
instance
.
save
()
register_operation
(
SleepOperation
)
register_
instance_
operation
(
SleepOperation
)
class
WakeUpOperation
(
Operation
):
class
WakeUpOperation
(
Instance
Operation
):
activity_code_suffix
=
'wake_up'
id
=
'wake_up'
name
=
_
(
"wake up"
)
...
...
@@ -490,4 +408,4 @@ class WakeUpOperation(Operation):
self
.
instance
.
renew
(
which
=
'both'
,
base_activity
=
activity
)
register_operation
(
WakeUpOperation
)
register_
instance_
operation
(
WakeUpOperation
)
circle/vm/tests/test_operations.py
View file @
f469fe6a
from
mock
import
MagicMock
,
patch
from
django.test
import
TestCase
from
vm.models
import
Instance
from
vm.operations
import
(
Operation
,
DeployOperation
,
DestroyOperation
,
MigrateOperation
,
DeployOperation
,
DestroyOperation
,
MigrateOperation
,
RebootOperation
,
RedeployOperation
,
ResetOperation
,
SaveAsTemplateOperation
,
ShutdownOperation
,
ShutOffOperation
,
SleepOperation
,
WakeUpOperation
,
)
from
vm.tasks.local_tasks
import
async_instance_operation
class
OperationTestCase
(
TestCase
):
def
test_activity_created_before_async_job
(
self
):
class
AbortEx
(
Exception
):
pass
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
with
patch
.
object
(
async_instance_operation
,
'apply_async'
,
side_effect
=
AbortEx
):
with
patch
.
object
(
Operation
,
'check_precond'
):
with
patch
.
object
(
Operation
,
'create_activity'
)
as
create_act
:
try
:
op
.
async
(
system
=
True
)
except
AbortEx
:
self
.
assertTrue
(
create_act
.
called
)
def
test_check_precond_called_before_create_activity
(
self
):
class
AbortEx
(
Exception
):
pass
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
with
patch
.
object
(
Operation
,
'create_activity'
,
side_effect
=
AbortEx
):
with
patch
.
object
(
Operation
,
'check_precond'
)
as
chk_pre
:
try
:
op
.
call
(
system
=
True
)
except
AbortEx
:
self
.
assertTrue
(
chk_pre
.
called
)
def
test_auth_check_on_non_system_call
(
self
):
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
user
=
MagicMock
()
with
patch
.
object
(
Operation
,
'check_auth'
)
as
check_auth
:
with
patch
.
object
(
Operation
,
'check_precond'
),
\
patch
.
object
(
Operation
,
'create_activity'
),
\
patch
.
object
(
Operation
,
'_exec_op'
):
op
.
call
(
user
=
user
)
check_auth
.
assert_called_with
(
user
)
def
test_no_auth_check_on_system_call
(
self
):
op
=
Operation
(
MagicMock
())
op
.
activity_code_suffix
=
'test'
op
.
id
=
'test'
with
patch
.
object
(
Operation
,
'check_auth'
,
side_effect
=
AssertionError
):
with
patch
.
object
(
Operation
,
'check_precond'
),
\
patch
.
object
(
Operation
,
'create_activity'
),
\
patch
.
object
(
Operation
,
'_exec_op'
):
op
.
call
(
system
=
True
)
class
DeployOperationTestCase
(
TestCase
):
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment