Commit 4cbd33d4 by Czémán Arnold

dashboard, vm, storage: add disk snapshoting feature

parent a8272957
Pipeline #181 passed with stage
in 0 seconds
...@@ -19,6 +19,7 @@ from __future__ import absolute_import ...@@ -19,6 +19,7 @@ from __future__ import absolute_import
from datetime import timedelta from datetime import timedelta
from urlparse import urlparse from urlparse import urlparse
import re
from django.forms import ModelForm from django.forms import ModelForm
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
...@@ -897,6 +898,88 @@ class VmDiskRemoveForm(OperationForm): ...@@ -897,6 +898,88 @@ class VmDiskRemoveForm(OperationForm):
return helper 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): class VmDownloadDiskForm(OperationForm):
name = forms.CharField(max_length=100, label=_("Name"), required=False) name = forms.CharField(max_length=100, label=_("Name"), required=False)
url = forms.CharField(label=_('URL'), validators=[URLValidator(), ]) url = forms.CharField(label=_('URL'), validators=[URLValidator(), ])
......
...@@ -1533,3 +1533,21 @@ textarea[name="new_members"] { ...@@ -1533,3 +1533,21 @@ textarea[name="new_members"] {
#manage-access-select-all { #manage-access-select-all {
cursor: pointer; 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 @@ ...@@ -6,6 +6,14 @@
<span class="operation-wrapper pull-right"> <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 d.is_resizable %}
{% if op.resize_disk %} {% if op.resize_disk %}
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}" <a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
...@@ -30,10 +38,47 @@ ...@@ -30,10 +38,47 @@
<i class="fa fa-{{ op.remove_disk.icon }} fa-fw-12"></i> {% trans "Remove" %} <i class="fa fa-{{ op.remove_disk.icon }} fa-fw-12"></i> {% trans "Remove" %}
</a> </a>
{% endif %} {% 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> </span>
<div style="clear: both;"></div> <br />
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<small>{% trans "File name" %}: {{ d.filename }}</small><br/> <small>{% trans "File name" %}: {{ d.filename }}</small><br />
<small>{% trans "Bus" %}: {{ d.device_bus }}</small> <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 %} {% endif %}
...@@ -65,6 +65,7 @@ from ..forms import ( ...@@ -65,6 +65,7 @@ from ..forms import (
VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm, VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm, VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmSnapshotDiskForm, VmCommonSnapshotDiskForm,
VmMigrateForm, VmDeployForm, VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm, VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm, VmRemoveInterfaceForm,
...@@ -743,6 +744,18 @@ class VmDeployView(FormOperationMixin, VmOperationView): ...@@ -743,6 +744,18 @@ class VmDeployView(FormOperationMixin, VmOperationView):
return kwargs 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([ vm_ops = OrderedDict([
('deploy', VmDeployView), ('deploy', VmDeployView),
('wake_up', VmOperationView.factory( ('wake_up', VmOperationView.factory(
...@@ -792,6 +805,13 @@ vm_ops = OrderedDict([ ...@@ -792,6 +805,13 @@ vm_ops = OrderedDict([
op='install_keys', icon='key', effect='info', op='install_keys', icon='key', effect='info',
show_in_toolbar=False, 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 ...@@ -24,6 +24,8 @@ from os.path import join
import uuid import uuid
import re import re
import arrow
from celery.contrib.abortable import AbortableAsyncResult from celery.contrib.abortable import AbortableAsyncResult
from django.db.models import (Model, BooleanField, CharField, DateTimeField, from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey) ForeignKey)
...@@ -142,7 +144,11 @@ class Disk(TimeStampedModel): ...@@ -142,7 +144,11 @@ class Disk(TimeStampedModel):
permissions = ( permissions = (
('create_empty_disk', _('Can create an empty disk.')), ('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a 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): class DiskError(HumanReadableException):
...@@ -391,9 +397,9 @@ class Disk(TimeStampedModel): ...@@ -391,9 +397,9 @@ class Disk(TimeStampedModel):
queue_name = self.get_remote_queue_name('storage', priority="fast") queue_name = self.get_remote_queue_name('storage', priority="fast")
disk_desc = self.get_disk_desc() disk_desc = self.get_disk_desc()
if self.base is not None: 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 queue=queue_name
).get(timeout=timeout) ).get(timeout=timeout)
else: else:
storage_tasks.create.apply_async(args=[disk_desc], storage_tasks.create.apply_async(args=[disk_desc],
queue=queue_name queue=queue_name
...@@ -403,6 +409,26 @@ class Disk(TimeStampedModel): ...@@ -403,6 +409,26 @@ class Disk(TimeStampedModel):
self.save() self.save()
return True 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 @classmethod
def create(cls, user=None, **params): def create(cls, user=None, **params):
disk = cls.__create(user, params) disk = cls.__create(user, params)
......
...@@ -48,8 +48,28 @@ def delete_dump(path): ...@@ -48,8 +48,28 @@ def delete_dump(path):
pass pass
@celery.task(name='storagedriver.snapshot_from_base')
def snapshot_from_base(disk_desc):
pass
@celery.task(name='storagedriver.snapshot') @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 pass
......
...@@ -283,6 +283,88 @@ class CreateDiskOperation(InstanceOperation): ...@@ -283,6 +283,88 @@ class CreateDiskOperation(InstanceOperation):
size=filesizeformat(kwargs['size']), name=kwargs['name']) 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')
Please register or sign in to reply
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 @register_operation
class ResizeDiskOperation(RemoteInstanceOperation): 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