Commit 5c743bdc by Czémán Arnold

dashboard: add delete and restore feature for Storage, small rework on Endpoint deletition

parent f7b7edeb
...@@ -460,6 +460,9 @@ class NodeForm(forms.ModelForm): ...@@ -460,6 +460,9 @@ class NodeForm(forms.ModelForm):
class TemplateForm(forms.ModelForm): class TemplateForm(forms.ModelForm):
networks = forms.ModelMultipleChoiceField( networks = forms.ModelMultipleChoiceField(
queryset=None, required=False, label=_("Networks")) queryset=None, required=False, label=_("Networks"))
datastore = forms.ModelChoiceField(
queryset=DataStore.objects.filter(destroyed__isnull=True),
empty_label=None)
num_cores = forms.IntegerField(widget=forms.NumberInput(attrs={ num_cores = forms.IntegerField(widget=forms.NumberInput(attrs={
'class': "form-control input-tags cpu-count-input", 'class': "form-control input-tags cpu-count-input",
...@@ -1451,7 +1454,8 @@ class RawDataForm(forms.ModelForm): ...@@ -1451,7 +1454,8 @@ class RawDataForm(forms.ModelForm):
class VmDataStoreForm(forms.ModelForm): class VmDataStoreForm(forms.ModelForm):
datastore = forms.ModelChoiceField(queryset=DataStore.objects.all(), datastore = forms.ModelChoiceField(
queryset=DataStore.objects.filter(destroyed__isnull=True),
empty_label=None) empty_label=None)
class Meta: class Meta:
...@@ -1671,13 +1675,29 @@ class CephDataStoreForm(DataStoreForm): ...@@ -1671,13 +1675,29 @@ class CephDataStoreForm(DataStoreForm):
class StorageListSearchForm(forms.Form): class StorageListSearchForm(forms.Form):
CHOICES = (
("active", _("active")),
("destroyed", _("destroyed")),
(("all"), _("all")),
)
s = forms.CharField(widget=forms.TextInput(attrs={ s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags", 'class': "form-control input-tags",
'placeholder': _("Search...") 'placeholder': _("Search...")
})) }))
stype = forms.ChoiceField(CHOICES, widget=forms.Select(attrs={
'class': "btn btn-default input-tags",
}))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(StorageListSearchForm, self).__init__(*args, **kwargs) super(StorageListSearchForm, self).__init__(*args, **kwargs)
# set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"):
data = self.data.copy()
data['stype'] = "active"
self.data = data
class EndpointForm(ModelForm): class EndpointForm(ModelForm):
......
...@@ -29,7 +29,9 @@ $(function () { ...@@ -29,7 +29,9 @@ $(function () {
return false; return false;
}); });
$('.group-create, .node-create, .tx-tpl-ownership, .group-delete, .node-delete, .disk-remove, .template-delete, .delete-from-group, .lease-delete, .endpoint-delete').click(function(e) { $('.group-create, .node-create, .tx-tpl-ownership, .group-delete, .node-delete, ' +
'.disk-remove, .template-delete, .delete-from-group, .lease-delete, .endpoint-delete, ' +
'.storage-delete, .storage-restore').click(function(e) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: $(this).prop('href'), url: $(this).prop('href'),
......
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{% blocktrans with object=object %}
Are you sure you want to restore <strong>{{ object }}</strong>?
{% endblocktrans %}
<br />
<div class="pull-right" style="margin-top: 15px;">
<form action="{{ request.path }}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="next" value="{{ request.GET.next }}"/>
<button class="btn btn-warning modal-accept"
{% if disable_submit %}disabled{% endif %}
>{% trans "Restore" %}</button>
</form>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
{% trans "Restore confirmation" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
{% if text %}
{{ text|safe }}
{% else %}
{% blocktrans with object=object %}
Are you sure you want to restore <strong>{{ object }}</strong>?
{% endblocktrans %}
{% endif %}
<div class="pull-right">
<form action="{{ request.path }}" method="POST">
{% csrf_token %}
<a class="btn btn-default">{% trans "Cancel" %}</a>
<input type="hidden" name="next" value="{{ request.GET.next }}"/>
<button class="btn btn-warning"
{% if disable_submit %}disabled{% endif %}
>{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
<div class="input-group"> <div class="input-group">
{{ search_form.s }} {{ search_form.s }}
<div class="input-group-btn"> <div class="input-group-btn">
{{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags"> <button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-database"></i> {% trans "Datastore" %}</h3> <h3 class="no-margin"><i class="fa fa-database"></i> {% trans "Data store" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form id="storage-create-form" action="" method="POST"> <form id="storage-create-form" action="" method="POST">
...@@ -23,6 +23,27 @@ ...@@ -23,6 +23,27 @@
</div><!-- .panel-body --> </div><!-- .panel-body -->
</div> </div>
</div> </div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
{% if object.destroyed %}
<a href="{% url "dashboard.views.storage-restore" pk=object.pk %}"
class="btn btn-xs btn-warning pull-right storage-restore">
{% trans "Restore" %}
</a>
<h4 class="no-margin"><i class="fa fa-medkit"></i> {% trans "Restore data store" %}</h4>
{% else %}
<a href="{% url "dashboard.views.storage-delete" pk=object.pk %}"
class="btn btn-xs btn-danger pull-right storage-delete">
{% trans "Delete" %}
</a>
<h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete data store" %}</h4>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
......
...@@ -54,6 +54,7 @@ from .views import ( ...@@ -54,6 +54,7 @@ from .views import (
NodeActivityView, NodeActivityView,
UserList, UserList,
StorageDetail, StorageList, StorageChoose, StorageCreate, DiskDetail, StorageDetail, StorageList, StorageChoose, StorageCreate, DiskDetail,
StorageDelete, StorageRestore,
EndpointCreate, EndpointList, EndpointEdit, EndpointDelete, EndpointCreate, EndpointList, EndpointEdit, EndpointDelete,
MessageList, MessageDetail, MessageCreate, MessageDelete, MessageList, MessageDetail, MessageCreate, MessageDelete,
) )
...@@ -244,6 +245,10 @@ urlpatterns = patterns( ...@@ -244,6 +245,10 @@ urlpatterns = patterns(
name="dashboard.views.storage-list"), name="dashboard.views.storage-list"),
url(r'^storage/choose/$', StorageChoose.as_view(), url(r'^storage/choose/$', StorageChoose.as_view(),
name="dashboard.views.storage-choose"), name="dashboard.views.storage-choose"),
url(r"^storage/delete/(?P<pk>\d+)/$", StorageDelete.as_view(),
name="dashboard.views.storage-delete"),
url(r"^storage/restore/(?P<pk>\d+)/$", StorageRestore.as_view(),
name="dashboard.views.storage-restore"),
url(r'^storage/endpoint/create/$', EndpointCreate.as_view(), url(r'^storage/endpoint/create/$', EndpointCreate.as_view(),
name="dashboard.views.storage-endpoint-create"), name="dashboard.views.storage-endpoint-create"),
......
...@@ -172,7 +172,9 @@ class StorageList(SuperuserRequiredMixin, FilterMixin, SingleTableView): ...@@ -172,7 +172,9 @@ class StorageList(SuperuserRequiredMixin, FilterMixin, SingleTableView):
def get_queryset(self): def get_queryset(self):
logger.debug('StorageList.get_queryset() called. User: %s', logger.debug('StorageList.get_queryset() called. User: %s',
unicode(self.request.user)) unicode(self.request.user))
qs = DataStore.get_all() cleaned_data = self.search_form.cleaned_data
stype = cleaned_data.get('stype', "all")
qs = self.get_queryset_by_stype(stype)
self.create_fake_get() self.create_fake_get()
try: try:
...@@ -183,6 +185,14 @@ class StorageList(SuperuserRequiredMixin, FilterMixin, SingleTableView): ...@@ -183,6 +185,14 @@ class StorageList(SuperuserRequiredMixin, FilterMixin, SingleTableView):
return qs return qs
def get_queryset_by_stype(self, stype):
if stype == "all":
return DataStore.get_all()
elif stype == "destroyed":
return DataStore.objects.filter(destroyed__isnull=False)
else:
return DataStore.objects.filter(destroyed__isnull=True)
class StorageDetail(SuperuserRequiredMixin, UpdateView): class StorageDetail(SuperuserRequiredMixin, UpdateView):
model = DataStore model = DataStore
...@@ -273,6 +283,84 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView): ...@@ -273,6 +283,84 @@ class StorageDetail(SuperuserRequiredMixin, UpdateView):
return reverse("dashboard.views.storage-detail", kwargs={"pk": ds.id}) return reverse("dashboard.views.storage-detail", kwargs={"pk": ds.id})
class StorageDelete(SuperuserRequiredMixin, DeleteView):
model = DataStore
success_message = _("Endpoint successfully deleted.")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def check_destroyable(self):
object = self.get_object()
if not object.is_destroyable:
raise PermissionDenied()
def get(self, request, *args, **kwargs):
try:
self.check_destroyable()
except PermissionDenied:
message = ugettext("Another object references"
" to the selected object.")
if request.is_ajax():
return JsonResponse({"error": message})
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(StorageDelete, self).get(request, *args, **kwargs)
def get_success_url(self):
return reverse_lazy("dashboard.views.storage-list")
def delete_obj(self, request, *args, **kwargs):
self.get_object().destroy()
def delete(self, request, *args, **kwargs):
self.check_destroyable()
self.delete_obj(request, *args, **kwargs)
if request.is_ajax():
return JsonResponse(
json.dumps({'message': self.success_message}),
)
else:
messages.success(request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
class StorageRestore(SuperuserRequiredMixin, UpdateView):
model = DataStore
fields = ("destroyed",)
success_message = _("Data store successfully restored.")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-restore.html']
else:
return ['dashboard/confirm/base-restore.html']
def form_valid(self, form):
object = self.get_object()
object.destroyed = None
object.save()
if self.request.is_ajax():
return JsonResponse(
json.dumps({'message': self.success_message}),
)
else:
messages.success(self.request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
ds = self.get_object()
return reverse_lazy("dashboard.views.storage-detail",
kwargs={"pk": ds.id})
class DiskDetail(SuperuserRequiredMixin, UpdateView): class DiskDetail(SuperuserRequiredMixin, UpdateView):
model = Disk model = Disk
form_class = DiskForm form_class = DiskForm
...@@ -407,14 +495,14 @@ class EndpointDelete(SuperuserRequiredMixin, DeleteView): ...@@ -407,14 +495,14 @@ class EndpointDelete(SuperuserRequiredMixin, DeleteView):
else: else:
return ['dashboard/confirm/base-delete.html'] return ['dashboard/confirm/base-delete.html']
def check_reference(self): def check_deletable(self):
object = self.get_object() object = self.get_object()
if object.datastore_set.count() != 0: if not object.is_deletable:
raise PermissionDenied() raise PermissionDenied()
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
try: try:
self.check_reference() self.check_deletable()
except PermissionDenied: except PermissionDenied:
message = ugettext("Another object references" message = ugettext("Another object references"
" to the selected object.") " to the selected object.")
...@@ -432,13 +520,12 @@ class EndpointDelete(SuperuserRequiredMixin, DeleteView): ...@@ -432,13 +520,12 @@ class EndpointDelete(SuperuserRequiredMixin, DeleteView):
self.get_object().delete() self.get_object().delete()
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
self.check_reference() self.check_deletable()
self.delete_obj(request, *args, **kwargs) self.delete_obj(request, *args, **kwargs)
if request.is_ajax(): if request.is_ajax():
return HttpResponse( return JsonResponse(
json.dumps({'message': self.success_message}), json.dumps({'message': self.success_message}),
content_type="application/json",
) )
else: else:
messages.success(request, self.success_message) messages.success(request, self.success_message)
......
...@@ -437,7 +437,8 @@ class VmCreateDiskView(FormOperationMixin, VmOperationView): ...@@ -437,7 +437,8 @@ class VmCreateDiskView(FormOperationMixin, VmOperationView):
val = super(VmCreateDiskView, self).get_form_kwargs() val = super(VmCreateDiskView, self).get_form_kwargs()
num = op.instance.disks.count() + 1 num = op.instance.disks.count() + 1
val['default'] = "%s %d" % (op.instance.name, num) val['default'] = "%s %d" % (op.instance.name, num)
val['datastore_choices'] = DataStore.get_all() val['datastore_choices'] = DataStore.objects.filter(
destroyed__isnull=True)
return val return val
...@@ -453,7 +454,8 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView): ...@@ -453,7 +454,8 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
def get_form_kwargs(self): def get_form_kwargs(self):
val = super(VmDownloadDiskView, self).get_form_kwargs() val = super(VmDownloadDiskView, self).get_form_kwargs()
val['datastore_choices'] = DataStore.get_all() val['datastore_choices'] = DataStore.objects.filter(
destroyed__isnull=True)
return val return val
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('storage', '0006_auto_20160505_0824'),
]
operations = [
migrations.AddField(
model_name='datastore',
name='destroyed',
field=models.DateTimeField(default=None, null=True, blank=True),
),
]
...@@ -63,6 +63,10 @@ class Endpoint(Model): ...@@ -63,6 +63,10 @@ class Endpoint(Model):
def __unicode__(self): def __unicode__(self):
return u"%s | %s:%d" % (self.name, self.address, self.port) return u"%s | %s:%d" % (self.name, self.address, self.port)
@property
def is_deletable(self):
return self.datastore_set.filter(destroyed__isnull=True).count() == 0
class DataStore(Model): class DataStore(Model):
...@@ -86,6 +90,7 @@ class DataStore(Model): ...@@ -86,6 +90,7 @@ class DataStore(Model):
verbose_name=_('Ceph username')) verbose_name=_('Ceph username'))
secret_uuid = CharField(max_length=255, null=True, blank=True, secret_uuid = CharField(max_length=255, null=True, blank=True,
verbose_name=_('uuid of secret key')) verbose_name=_('uuid of secret key'))
destroyed = DateTimeField(blank=True, default=None, null=True)
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
...@@ -120,6 +125,14 @@ class DataStore(Model): ...@@ -120,6 +125,14 @@ class DataStore(Model):
return [(ep.address, ep.port) for ep in self.endpoints.all()] return [(ep.address, ep.port) for ep in self.endpoints.all()]
def destroy(self):
if self.destroyed:
return False
self.destroyed = timezone.now()
self.save()
return True
@property @property
def used_percent(self): def used_percent(self):
stats = self.get_statistics() stats = self.get_statistics()
...@@ -127,6 +140,13 @@ class DataStore(Model): ...@@ -127,6 +140,13 @@ class DataStore(Model):
return int(100 - free_percent) return int(100 - free_percent)
@property
def is_destroyable(self):
disk_count = self.disk_set.filter(destroyed__isnull=True).count()
template_count = self.instancetemplate_set.count()
vm_count = self.instance_set.filter(destroyed_at__isnull=True).count()
return 0 == disk_count + vm_count + template_count
@method_cache(30) @method_cache(30)
def get_statistics(self, timeout=15): def get_statistics(self, timeout=15):
q = self.get_remote_queue_name("storage", priority="fast") q = self.get_remote_queue_name("storage", priority="fast")
......
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