Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
Gyuricska Milán
/
cloud
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
4cbd33d4
authored
Aug 29, 2016
by
Czémán Arnold
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
dashboard, vm, storage: add disk snapshoting feature
parent
a8272957
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
319 additions
and
7 deletions
+319
-7
circle/dashboard/forms.py
+83
-0
circle/dashboard/static/dashboard/dashboard.less
+18
-0
circle/dashboard/templates/dashboard/_disk-list-element.html
+49
-4
circle/dashboard/views/vm.py
+20
-0
circle/storage/migrations/0003_auto_20160826_1619.py
+18
-0
circle/storage/models.py
+28
-2
circle/storage/tasks/storage_tasks.py
+21
-1
circle/vm/operations.py
+82
-0
No files found.
circle/dashboard/forms.py
View file @
4cbd33d4
...
...
@@ -19,6 +19,7 @@ from __future__ import absolute_import
from
datetime
import
timedelta
from
urlparse
import
urlparse
import
re
from
django.forms
import
ModelForm
from
django.contrib.auth.forms
import
(
...
...
@@ -897,6 +898,88 @@ class VmDiskRemoveForm(OperationForm):
return
helper
def
snapshot_name_validator
(
name
):
number
=
re
.
compile
(
r'^\d+$'
)
if
number
.
match
(
name
):
raise
forms
.
ValidationError
(
_
(
'The name shall not be a number.'
),
code
=
'invalid'
)
class
VmCommonSnapshotDiskForm
(
OperationForm
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
choices
=
kwargs
.
pop
(
'choices'
)
self
.
disk
=
kwargs
.
pop
(
'default'
)
self
.
snap_id
=
kwargs
.
pop
(
'snap_id'
)
self
.
snap_name
=
kwargs
.
pop
(
'snap_name'
)
super
(
VmCommonSnapshotDiskForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
'disk'
]
=
forms
.
ModelChoiceField
(
queryset
=
choices
,
initial
=
self
.
disk
,
required
=
True
,
empty_label
=
None
,
label
=
_
(
'Disk'
))
if
self
.
disk
:
self
.
fields
[
'disk'
]
.
widget
=
HiddenInput
()
self
.
fields
[
'snap_id'
]
=
forms
.
IntegerField
(
initial
=
self
.
snap_id
,
widget
=
HiddenInput
())
self
.
fields
[
'snap_name'
]
=
forms
.
CharField
(
initial
=
self
.
snap_name
,
widget
=
HiddenInput
(),
validators
=
[
snapshot_name_validator
])
@property
def
helper
(
self
):
helper
=
super
(
VmCommonSnapshotDiskForm
,
self
)
.
helper
if
self
.
disk
:
helper
.
layout
=
Layout
(
AnyTag
(
'div'
,
HTML
(
_
(
'<label>Disk:</label>
%
s<br />'
'<label>Snapshot:</label>
%
s (#
%
s)'
)
%
(
escape
(
self
.
disk
),
escape
(
self
.
snap_name
),
escape
(
self
.
snap_id
))),
css_class
=
'form-group'
,
),
Field
(
'disk'
),
Field
(
'snap_id'
),
Field
(
'snap_name'
),
)
return
helper
class
VmSnapshotDiskForm
(
OperationForm
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
choices
=
kwargs
.
pop
(
'choices'
)
self
.
disk
=
kwargs
.
pop
(
'default'
)
super
(
VmSnapshotDiskForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
'disk'
]
=
forms
.
ModelChoiceField
(
queryset
=
choices
,
initial
=
self
.
disk
,
required
=
True
,
empty_label
=
None
,
label
=
_
(
'Disk'
))
if
self
.
disk
:
self
.
fields
[
'disk'
]
.
widget
=
HiddenInput
()
self
.
fields
[
'snap_name'
]
=
forms
.
CharField
(
validators
=
[
snapshot_name_validator
])
@property
def
helper
(
self
):
helper
=
super
(
VmSnapshotDiskForm
,
self
)
.
helper
if
self
.
disk
:
helper
.
layout
=
Layout
(
AnyTag
(
'div'
,
HTML
(
_
(
'<label>Disk:</label>
%
s'
)
%
escape
(
self
.
disk
)),
css_class
=
'form-group'
,
),
Field
(
'disk'
),
Field
(
'snap_name'
),
)
return
helper
class
VmDownloadDiskForm
(
OperationForm
):
name
=
forms
.
CharField
(
max_length
=
100
,
label
=
_
(
"Name"
),
required
=
False
)
url
=
forms
.
CharField
(
label
=
_
(
'URL'
),
validators
=
[
URLValidator
(),
])
...
...
circle/dashboard/static/dashboard/dashboard.less
View file @
4cbd33d4
...
...
@@ -1533,3 +1533,21 @@ textarea[name="new_members"] {
#manage-access-select-all {
cursor: pointer;
}
.snapshot-list {
margin-top: 5px;
padding-left: 5px;
background: gray;
}
.show-snapshot-btn {
margin-top: 10px;
}
.disk-create_snapshot-btn {
margin-right: 5px;
}
.snapshot-table {
background: white;
}
circle/dashboard/templates/dashboard/_disk-list-element.html
View file @
4cbd33d4
...
...
@@ -6,6 +6,14 @@
<span
class=
"operation-wrapper pull-right"
>
<div>
{% if op.create_snapshot %}
<a
href=
"{{ op.create_snapshot.get_url }}?disk={{d.pk}}"
class=
"btn btn-xs btn-{{ op.create_snapshot.effect }} operation disk-create_snapshot-btn
{% if op.create_snapshot.disabled %}disabled{% endif %}"
>
<i
class=
"fa fa-{{ op.create_snapshot.icon }} fa-fw-12"
></i>
{% trans "Snapshot" %}
</a>
{% endif %}
{% if d.is_resizable %}
{% if op.resize_disk %}
<a
href=
"{{ op.resize_disk.get_url }}?disk={{d.pk}}"
...
...
@@ -30,10 +38,47 @@
<i
class=
"fa fa-{{ op.remove_disk.icon }} fa-fw-12"
></i>
{% trans "Remove" %}
</a>
{% endif %}
</div>
<div
class=
"pull-right"
>
{% if perms.view_snapshot and d.list_snapshots %}
<input
type=
"button"
class=
"btn btn-default btn-xs show-snapshot-btn"
data-toggle=
"collapse"
data-target=
"#snapshots-{{ d.pk }}"
value=
"{% trans "
Show
snapshots
"
%}"
/>
{% endif %}
</div>
</span>
<div
style=
"clear: both;"
></div>
<br
/>
{% if request.user.is_superuser %}
<small>
{% trans "File name" %}: {{ d.filename }}
</small><br/>
<small>
{% trans "Bus" %}: {{ d.device_bus }}
</small>
<small>
{% trans "File name" %}: {{ d.filename }}
</small><br
/>
<small>
{% trans "Bus" %}: {{ d.device_bus }}
</small><br
/>
{% endif %}
<div
style=
"clear: both;"
></div>
{% if perms.storage.view_snapshot %}
<div
id=
"snapshots-{{ d.pk }}"
class=
"collapse out snapshot-list"
>
<table
class=
"table table-striped info-panel small snapshot-table"
>
{% for snap in d.list_snapshots %}
<tr>
<td>
{{ snap.id }}
</td>
<td>
{{ snap.name }}
</td>
<td><span
title=
"{{ snap.date }}"
>
{{ snap.date_human }}
</span></td>
<td>
<div
class=
"pull-right"
>
<a
href=
"{{ op.revert_snapshot.get_url }}?disk={{d.pk}}&snap_name={{ snap.name }}&snap_id={{ snap.id }}"
class=
"btn btn-xs btn-{{ op.revert_snapshot.effect }} operation disk-revert_snapshot-btn
{% if op.revert_snapshot.disabled %}disabled{% endif %}"
title=
"{% trans "
Revert
"
%}"
>
<i
class=
"fa fa-{{ op.revert_snapshot.icon }} fa-fw-12"
></i>
</a>
<a
href=
"{{ op.remove_snapshot.get_url }}?disk={{d.pk}}&snap_name={{ snap.name }}&snap_id={{ snap.id }}"
class=
"btn btn-xs btn-{{ op.remove_snapshot.effect }} operation disk-remove_snapshot-btn
{% if op.remove_snapshot.disabled %}disabled{% endif %}"
title=
"{% trans "
Remove
"
%}"
>
<i
class=
"fa fa-{{ op.remove_snapshot.icon }} fa-fw-12"
></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
circle/dashboard/views/vm.py
View file @
4cbd33d4
...
...
@@ -65,6 +65,7 @@ from ..forms import (
VmAddInterfaceForm
,
VmCreateDiskForm
,
VmDownloadDiskForm
,
VmSaveForm
,
VmRenewForm
,
VmStateChangeForm
,
VmListSearchForm
,
VmCustomizeForm
,
VmDiskResizeForm
,
RedeployForm
,
VmDiskRemoveForm
,
VmSnapshotDiskForm
,
VmCommonSnapshotDiskForm
,
VmMigrateForm
,
VmDeployForm
,
VmPortRemoveForm
,
VmPortAddForm
,
VmRemoveInterfaceForm
,
...
...
@@ -743,6 +744,18 @@ class VmDeployView(FormOperationMixin, VmOperationView):
return
kwargs
class
VmCommonSnapshotDiskView
(
VmDiskModifyView
):
form_class
=
VmCommonSnapshotDiskForm
def
get_form_kwargs
(
self
):
snap_id
=
self
.
request
.
GET
.
get
(
'snap_id'
)
snap_name
=
self
.
request
.
GET
.
get
(
'snap_name'
)
val
=
super
(
VmCommonSnapshotDiskView
,
self
)
.
get_form_kwargs
()
val
.
update
({
'snap_id'
:
snap_id
,
'snap_name'
:
snap_name
})
return
val
vm_ops
=
OrderedDict
([
(
'deploy'
,
VmDeployView
),
(
'wake_up'
,
VmOperationView
.
factory
(
...
...
@@ -792,6 +805,13 @@ vm_ops = OrderedDict([
op
=
'install_keys'
,
icon
=
'key'
,
effect
=
'info'
,
show_in_toolbar
=
False
,
)),
(
'create_snapshot'
,
VmDiskModifyView
.
factory
(
op
=
'create_snapshot'
,
icon
=
'camera'
,
effect
=
'success'
,
form_class
=
VmSnapshotDiskForm
)),
(
'remove_snapshot'
,
VmCommonSnapshotDiskView
.
factory
(
op
=
'remove_snapshot'
,
icon
=
'times'
,
effect
=
'danger'
)),
(
'revert_snapshot'
,
VmCommonSnapshotDiskView
.
factory
(
op
=
'revert_snapshot'
,
icon
=
'backward'
,
effect
=
'warning'
)),
])
...
...
circle/storage/migrations/0003_auto_20160826_1619.py
0 → 100644
View file @
4cbd33d4
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
'storage'
,
'0002_disk_bus'
),
]
operations
=
[
migrations
.
AlterModelOptions
(
name
=
'disk'
,
options
=
{
'ordering'
:
[
'name'
],
'verbose_name'
:
'disk'
,
'verbose_name_plural'
:
'disks'
,
'permissions'
:
((
'create_empty_disk'
,
'Can create an empty disk.'
),
(
'download_disk'
,
'Can download a disk.'
),
(
'resize_disk'
,
'Can resize a disk.'
),
(
'create_snapshot'
,
'Can create snapshot'
),
(
'remove_snapshot'
,
'Can remove snapshot'
),
(
'revert_snapshot'
,
'Can revert snapshot'
),
(
'view_snapshot'
,
'Can view snapshot'
))},
),
]
circle/storage/models.py
View file @
4cbd33d4
...
...
@@ -24,6 +24,8 @@ from os.path import join
import
uuid
import
re
import
arrow
from
celery.contrib.abortable
import
AbortableAsyncResult
from
django.db.models
import
(
Model
,
BooleanField
,
CharField
,
DateTimeField
,
ForeignKey
)
...
...
@@ -142,7 +144,11 @@ class Disk(TimeStampedModel):
permissions
=
(
(
'create_empty_disk'
,
_
(
'Can create an empty disk.'
)),
(
'download_disk'
,
_
(
'Can download a disk.'
)),
(
'resize_disk'
,
_
(
'Can resize a disk.'
))
(
'resize_disk'
,
_
(
'Can resize a disk.'
)),
(
'create_snapshot'
,
_
(
'Can create snapshot'
)),
(
'remove_snapshot'
,
_
(
'Can remove snapshot'
)),
(
'revert_snapshot'
,
_
(
'Can revert snapshot'
)),
(
'view_snapshot'
,
_
(
'Can view snapshot'
)),
)
class
DiskError
(
HumanReadableException
):
...
...
@@ -391,7 +397,7 @@ class Disk(TimeStampedModel):
queue_name
=
self
.
get_remote_queue_name
(
'storage'
,
priority
=
"fast"
)
disk_desc
=
self
.
get_disk_desc
()
if
self
.
base
is
not
None
:
storage_tasks
.
snapshot
.
apply_async
(
args
=
[
disk_desc
],
storage_tasks
.
snapshot
_from_base
.
apply_async
(
args
=
[
disk_desc
],
queue
=
queue_name
)
.
get
(
timeout
=
timeout
)
else
:
...
...
@@ -403,6 +409,26 @@ class Disk(TimeStampedModel):
self
.
save
()
return
True
def
repack_snapshot_info
(
self
,
snap
):
date
=
arrow
.
get
(
snap
[
'date-sec'
])
return
{
'id'
:
snap
[
'id'
],
'name'
:
snap
[
'name'
],
'date'
:
date
.
format
(
'YYYY.DD.MM. hh:mm:ss'
),
'date_human'
:
date
.
humanize
(),
}
@method_cache
(
30
)
def
list_snapshots
(
self
,
timeout
=
15
):
if
not
self
.
is_ready
:
return
[]
queue_name
=
self
.
get_remote_queue_name
(
'storage'
,
priority
=
'fast'
)
disk_desc
=
self
.
get_disk_desc
()
snaps
=
storage_tasks
.
list_snapshots
.
apply_async
(
args
=
[
disk_desc
],
queue
=
queue_name
)
.
get
(
timeout
=
timeout
)
return
[
self
.
repack_snapshot_info
(
snap
)
for
snap
in
snaps
]
@classmethod
def
create
(
cls
,
user
=
None
,
**
params
):
disk
=
cls
.
__create
(
user
,
params
)
...
...
circle/storage/tasks/storage_tasks.py
View file @
4cbd33d4
...
...
@@ -48,8 +48,28 @@ def delete_dump(path):
pass
@celery.task
(
name
=
'storagedriver.snapshot_from_base'
)
def
snapshot_from_base
(
disk_desc
):
pass
@celery.task
(
name
=
'storagedriver.snapshot'
)
def
snapshot
(
disk_desc
):
def
snapshot
(
disk_desc
,
name
):
pass
@celery.task
(
name
=
'storagedriver.list_snapshots'
)
def
list_snapshots
(
disk_desc
):
pass
@celery.task
(
name
=
'storagedriver.remove_snapshot'
)
def
remove_snapshot
(
disk_desc
,
id
):
pass
@celery.task
(
name
=
'storagedriver.revert_snapshot'
)
def
revert_snapshot
(
disk_desc
,
id
):
pass
...
...
circle/vm/operations.py
View file @
4cbd33d4
...
...
@@ -283,6 +283,88 @@ class CreateDiskOperation(InstanceOperation):
size
=
filesizeformat
(
kwargs
[
'size'
]),
name
=
kwargs
[
'name'
])
class
RemoteSnapshotDiskOperation
(
InstanceOperation
):
remote_queue
=
(
'storage'
,
'slow'
)
remote_timeout
=
30
def
_operation
(
self
,
disk
,
**
kwargs
):
if
disk
:
if
not
disk
.
is_ready
:
raise
disk
.
DiskIsNotReady
(
disk
)
disk_desc
=
disk
.
get_disk_desc
()
args
=
[
disk_desc
]
+
self
.
_get_remote_args
(
**
kwargs
)
return
self
.
task
.
apply_async
(
args
=
args
,
queue
=
disk
.
get_remote_queue_name
(
*
self
.
remote_queue
)
)
.
get
(
timeout
=
self
.
remote_timeout
)
@register_operation
class
CreateSnapshotDiskOperation
(
RemoteSnapshotDiskOperation
):
id
=
'create_snapshot'
name
=
_
(
'create snapshot'
)
description
=
_
(
'Create snapshot from disk.'
)
required_perms
=
(
'storage.create_snapshot'
,
)
accept_states
=
(
'STOPPED'
)
task
=
storage_tasks
.
snapshot
def
_get_remote_args
(
self
,
**
kwargs
):
snap_name
=
kwargs
.
get
(
'snap_name'
)
if
not
snap_name
:
snap_name
=
'new snapshot'
return
[
snap_name
]
def
get_activity_name
(
self
,
kwargs
):
return
create_readable
(
ugettext_noop
(
'Created snapshot
%(snap_name)
s'
' from disk
%(disk_name)
s'
),
disk_name
=
kwargs
[
'disk'
]
.
name
,
snap_name
=
kwargs
[
'snap_name'
])
@register_operation
class
RemoveSnapshotDiskOperation
(
RemoteSnapshotDiskOperation
):
id
=
'remove_snapshot'
name
=
_
(
'remove snapshot'
)
description
=
_
(
'Remove snapshot from disk.'
)
required_perms
=
(
'storage.remove_snapshot'
,
)
task
=
storage_tasks
.
remove_snapshot
def
_get_remote_args
(
self
,
**
kwargs
):
return
[
kwargs
.
get
(
'snap_id'
)]
def
get_activity_name
(
self
,
kwargs
):
return
create_readable
(
ugettext_noop
(
'Removed snapshot
%(snap_name)
s'
' from disk
%(disk_name)
s'
),
disk_name
=
kwargs
[
'disk'
]
.
name
,
snap_name
=
kwargs
[
'snap_name'
])
@register_operation
class
RevertSnapshotDiskOperation
(
RemoteSnapshotDiskOperation
):
id
=
'revert_snapshot'
name
=
_
(
'revert snapshot'
)
description
=
_
(
'Revert snapshot on disk.'
)
required_perms
=
(
'storage.revert_snapshot'
,
)
accept_states
=
(
'STOPPED'
)
task
=
storage_tasks
.
revert_snapshot
def
_get_remote_args
(
self
,
**
kwargs
):
return
[
kwargs
.
get
(
'snap_id'
)]
def
get_activity_name
(
self
,
kwargs
):
return
create_readable
(
ugettext_noop
(
'Revert snapshot
%(snap_name)
s'
' on disk
%(disk_name)
s'
),
disk_name
=
kwargs
[
'disk'
]
.
name
,
snap_name
=
kwargs
[
'snap_name'
])
@register_operation
class
ResizeDiskOperation
(
RemoteInstanceOperation
):
...
...
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