Commit 4cbd33d4 by Czémán Arnold

dashboard, vm, storage: add disk snapshoting feature

parent a8272957
......@@ -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(), ])
......
......@@ -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;
}
......@@ -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 %}
......@@ -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')),
])
......
# -*- 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'))},
),
]
......@@ -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)
......
......@@ -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
......
......@@ -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):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment