Commit c5c309fa by Czémán Arnold

storage: add create views and forms for storage, small changes on model

parent 1e70ee0c
......@@ -54,7 +54,7 @@ from firewall.models import Vlan, Host
from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
)
from storage.models import DataStore, Disk
from storage.models import DataStore, Disk, DataStoreHost
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.contrib.auth.models import Permission
from .models import Profile, GroupProfile, Message
......@@ -1614,7 +1614,74 @@ class DataStoreForm(ModelForm):
class Meta:
model = DataStore
fields = ("name", "path", "hostname", )
fields = ("type", "name", "path", "hostname", )
class CephDataStoreForm(DataStoreForm):
hostnames = forms.ModelMultipleChoiceField(
queryset=None, required=False, label=_("Hostnames"))
other_hostnames = forms.MultipleChoiceField(
required=False, label=_("Other hostnames"))
type = forms.CharField(widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
super(DataStoreForm, self).__init__(*args, **kwargs)
hostnames = self.fields["hosts"].queryset.all()
other_hostnames = set(DataStoreHost.objects.all()) - set(hostnames)
self.fields['hostnames'].queryset = hostnames
self.fields['other_hostnames'].initial = other_hostnames
@property
def helper(self):
helper = FormHelper()
helper.layout = Layout(
Fieldset(
'',
'ceph_user',
'secret_uuid',
),
FormActions(
Submit('submit', _('Save')),
)
)
return helper
class Meta:
model = DataStore
fields = ("type", "name", "path", "hostname",
"ceph_user", "secret_uuid", "hosts")
class DataStoreHostForm(ModelForm):
@property
def helper(self):
helper = FormHelper()
helper.layout = Layout(
Fieldset(
'',
'name',
'address',
'port',
),
FormActions(
Submit('submit', _('Save')),
)
)
return helper
def __init__(self, *args, **kwargs):
super(DataStoreHostForm, self).__init__(*args, **kwargs)
self.fields['port'].initial = 6789
class Meta:
model = DataStoreHost
fields = ("name", "address", "port")
class StorageListSearchForm(forms.Form):
......
......@@ -52,7 +52,7 @@ $(function () {
return false;
});
$('.template-choose').click(function(e) {
$('.template-choose, .storage-choose').click(function(e) {
$.ajax({
type: 'GET',
url: $(this).prop('href'),
......@@ -73,11 +73,66 @@ $(function () {
}
return true;
});
$("#storage-choose-next-button").click(function() {
var radio = $('input[type="radio"]:checked', "#storage-choose-form").val();
if(!radio) {
$("#storage-choose-alert").addClass("alert-warning")
.text(gettext("Select an option to proceed!"));
return false;
}
return true;
});
}
});
return false;
});
$('.data_store_host-create').click(function(e) {
$.ajax({
type: 'GET',
url: $(this).prop('href'),
success: function(data) {
$('body').append(data);
var modal = $('#confirmation-modal');
modal.modal('show');
modal.on('hidden.bs.modal', function() {
modal.remove();
});
$("#data_store_host_host-create-btn").click(function(){
var form = $("#data_store_host_form")
$.post(form.attr("action"), form.serialize(), function(data){
if(data.status===true){
$('#id_other_hostnames')
.append($('<option>')
.text(data.response.text)
.attr('value', data.response.val));
modal.modal("hide");
$('body').removeClass('modal-open');
$('.modal-backdrop').remove();
}
else{
var error_msg = $("#data_store_host-create-alert");
error_msg.empty();
error_msg.append(data.response);
error_msg.show();
}
}, "json");
return false;
});
}
});
return false;
});
$('#storage-create-form').submit(function(){
$('#id_hostnames option').prop('selected', true);
});
$('[href=#index-graph-view]').click(function (e) {
var box = $(this).data('index-box');
$("#" + box + "-list-view").hide();
......
......@@ -471,7 +471,7 @@ footer a, footer a:hover, footer a:visited {
margin-bottom: 20px;
}
.template-choose-list {
.template-choose-list, .storage-choose-list {
max-width: 600px;
}
......@@ -481,13 +481,13 @@ footer a, footer a:hover, footer a:visited {
padding-right: 50px;
}
.template-choose-list-element {
.template-choose-list-element, .storage-choose-list-element {
padding: 6px 10px;
cursor: pointer;
margin-bottom: 15px; /* bootstrap panel default is 20px */
}
.template-choose-list input[type="radio"] {
.template-choose-list input[type="radio"], .storage-choose-list input[type="radio"] {
float: right;
}
......
{% load i18n %}
{% load crispy_forms_tags %}
<form id="data_store_host_form" action="{% url "dashboard.views.storage-host-create" %}" method="POST">
<div class="alert alert-danger" style="display: none;" id="data_store_host-create-alert">
</div>
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% csrf_token %}
<div class="row">
<div class="col-xs-12">{{ form.name|as_crispy_field }}</div>
</div>
<div class="row">
<div class="col-xs-9">
{{ form.address|as_crispy_field }}
</div>
<div class="col-xs-3">
{{ form.port|as_crispy_field }}
</div>
</div>
<input type="submit" value="{% trans "Create new host" %}" class="btn btn-success" id="data_store_host_host-create-btn">
</form>
<style>
fieldset {
margin-top: 40px;
}
fieldset legend {
font-weight: bold;
}
</style>
......@@ -26,5 +26,6 @@
{% if request.user.is_superuser %}
<small>{% trans "File name" %}: {{ d.filename }}</small><br/>
<small>{% trans "Bus" %}: {{ d.device_bus }}</small>
<small>{% trans "Bus" %}: {{ d.device_bus }}</small><br/>
<small>{% trans "Data store" %}: {{ d.datastore }}</small>
{% endif %}
{% load i18n %}
<div class="alert alert-info" id="storage-choose-alert">
{% trans "Choose the type of the data store that you want to create." %}
</div>
<form action="{% url "dashboard.views.storage-choose" %}" method="POST"
id="storage-choose-form">
{% csrf_token %}
<div class="storage-choose-list">
{% for t in types %}
<div class="panel panel-default storage-choose-list-element">
<input type="radio" name="type" value="{{ t.0 }}"/>
{{ t.1 }}
<div class="clearfix"></div>
</div>
{% endfor %}
<button type="submit" id="storage-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></div>
</div>
</form>
<script>
$(function() {
$(".storage-choose-list-element").click(function() {
$("input", $(this)).prop("checked", true);
});
$(".storage-choose-list-element").hover(
function() {
$("small", $(this)).stop().fadeIn(200);
},
function() {
$("small", $(this)).stop().fadeOut(200);
}
);
});
</script>
{% load i18n %}
{% load crispy_forms_tags %}
<form id="storage-create-form" action="" method="POST">
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% csrf_token %}
{{ form.type }}
<fieldset>
<legend>{% trans "General settings" %}</legend>
{{ form.name|as_crispy_field }}
{{ form.path|as_crispy_field }}
{{ form.hostname|as_crispy_field }}
</fieldset>
{% if form.type.value == "ceph_block" %}
<fieldset>
<legend>{% trans "Ceph block storage authentication settings" %}</legend>
{{ form.ceph_user|as_crispy_field }}
{{ form.secret_uuid|as_crispy_field }}
</fieldset>
<fieldset>
<legend>{% trans "Select or add new Ceph monitor hostname(s)" %}</legend>
{% include 'dashboard/storage/hostname_selector.html' %}
</fieldset>
{% endif %}
<fieldset>
<input type="submit" value="{% trans "Create new data store" %}" class="btn btn-success">
</fieldset>
</form>
<style>
fieldset {
margin-top: 40px;
}
fieldset legend {
font-weight: bold;
}
</style>
......@@ -23,7 +23,6 @@
<div class="input-group">
{{ search_form.s }}
<div class="input-group-btn">
{{ search_form.stype }}
<button type="submit" class="btn btn-primary input-tags">
<i class="fa fa-search"></i>
</button>
......
<div class="row">
<div class="col-xs-5">
<div class="controls">
<select multiple="multiple" class="selectmultiple form-control" id="id_hostnames" name="hostnames">
{% for hostname in hostnames %}
<option value="{{ hostname.id }}">{{ hostname }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-xs-1">
<div class="row text-center">
<div class="btn" onclick="move('#id_hostnames', '#id_other_hostnames')">
<i class="fa fa-arrow-left fa-2x"></i>
</div>
</div>
<div class="row text-center">
<div class="btn" onclick="move('#id_other_hostnames', '#id_hostnames')">
<i class="fa fa-arrow-right fa-2x"></i>
</div>
</div>
</div>
<div class="col-xs-5">
<div class="controls">
<select multiple="multiple" class="selectmultiple form-control" id="id_other_hostnames" name="other_hostnames">
{% for hostname in other_hostnames %}
<option value="{{ hostname.id }}">{{ hostname }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col-xs-1 text-left">
<a href="{% url "dashboard.views.storage-host-create" %}" class="btn btn-success btn-xs data_store_host-create">
<i class="fa fa-plus fa-2x"></i>
</a>
</div>
</div>
<script type="text/javascript">
function move(to, from){
var selected_items = $(from).find(":selected");
$(to).append(selected_items);
}
</script>
......@@ -53,7 +53,8 @@ from .views import (
OpenSearchDescriptionView,
NodeActivityView,
UserList,
StorageDetail, StorageList, DiskDetail,
StorageDetail, StorageList, StorageChoose, StorageCreate, DiskDetail,
DataStoreHostCreate,
MessageList, MessageDetail, MessageCreate, MessageDelete,
)
from .views.vm import vm_ops, vm_mass_ops
......@@ -233,13 +234,17 @@ urlpatterns = patterns(
url(r'^vm/opensearch.xml$', OpenSearchDescriptionView.as_view(),
name="dashboard.views.vm-opensearch"),
url(r'^storage/create/(?P<type>.+)$', StorageCreate.as_view(),
name="dashboard.views.storage-create"),
url(r'^storage/(?P<pk>\d+)/$', StorageDetail.as_view(),
name='dashboard.views.storage-detail'),
url(r'^storage/list/$', StorageList.as_view(),
name="dashboard.views.storage-list"),
url(r'^storage/choose/$', StorageDetail.as_view(),
url(r'^storage/choose/$', StorageChoose.as_view(),
name="dashboard.views.storage-choose"),
url(r'^storage/host/create/$', DataStoreHostCreate.as_view(),
name="dashboard.views.storage-host-create"),
url(r'^disk/(?P<pk>\d+)/$', DiskDetail.as_view(),
name="dashboard.views.disk-detail"),
......
......@@ -19,25 +19,130 @@ from __future__ import unicode_literals, absolute_import
import logging
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.views.generic import UpdateView
from django.views.generic import UpdateView, TemplateView, CreateView
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import redirect
from django_tables2 import SingleTableView
from django.http import Http404, HttpResponse
from django.core.exceptions import PermissionDenied
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from sizefield.utils import filesizeformat
from common.models import WorkerNotFound
from storage.models import DataStore, Disk
from storage.models import DataStore, Disk, DataStoreHost
from ..tables import DiskListTable, StorageListTable
from ..forms import DataStoreForm, DiskForm, StorageListSearchForm
from ..forms import (
DataStoreForm, CephDataStoreForm, DiskForm, StorageListSearchForm,
DataStoreHostForm
)
from .util import FilterMixin
import json
logger = logging.getLogger(__name__)
class StorageChoose(LoginRequiredMixin, TemplateView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(StorageChoose, self).get_context_data(*args, **kwargs)
types = DataStore.TYPES
context.update({
'box_title': _('Choose data store type'),
'ajax_title': True,
'template': "dashboard/_storage-choose.html",
'types': types,
})
return context
def post(self, request, *args, **kwargs):
if not request.user.has_perm('storage.add_datastore'):
raise PermissionDenied()
type = request.POST.get("type")
if any(type in t for t in DataStore.TYPES):
return redirect(reverse("dashboard.views.storage-create",
kwargs={"type": type}))
else:
messages.warning(request, _("Select an option to proceed."))
return redirect(reverse("dashboard.views.storage-choose"))
class StorageCreate(SuccessMessageMixin, CreateView):
model = DataStore
form = None
def get_template_names(self):
if self.request.is_ajax():
pass
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(StorageCreate, self).get_context_data(*args, **kwargs)
other_hostnames = DataStoreHost.objects.all()
context["hostnames_of_datastore"] = []
context["other_hostnames"] = other_hostnames
context.update({
'box_title': _("Create a new data store"),
'template': "dashboard/_storage-create.html",
})
return context
def get(self, *args, **kwargs):
if not self.request.user.has_perm('storage.add_datastore'):
raise PermissionDenied()
return super(StorageCreate, self).get(*args, **kwargs)
def post(self, request, *args, **kwargs):
if not self.request.user.has_perm('storage.add_datastore'):
raise PermissionDenied()
self.form = self.form_class(request.POST)
if not self.form.is_valid():
logger.debug("invalid form")
return self.get(request, self.form, *args, **kwargs)
else:
self.form.save()
return redirect(self.get_success_url())
def get_success_url(self):
return reverse_lazy("dashboard.views.storage-list")
def get_form(self):
if self.form is not None:
return self.form
else:
type = self.kwargs.get("type")
fc = self.form_class
f = fc(initial={"type": type})
return f
@property
def form_class(self):
type = self.kwargs.get("type")
if type == "file":
fc = DataStoreForm
elif type == "ceph_block":
fc = CephDataStoreForm
else:
raise Http404(_("Invalid creation type"))
return fc
class StorageList(LoginRequiredMixin, FilterMixin, SingleTableView):
template_name = "dashboard/storage-list.html"
model = DataStore
......@@ -161,3 +266,70 @@ class DiskDetail(SuperuserRequiredMixin, UpdateView):
def form_valid(self, form):
pass
class DataStoreHostCreate(SuccessMessageMixin, CreateView):
model = DataStoreHost
form_class = DataStoreHostForm
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(DataStoreHostCreate, self).get_context_data(
*args, **kwargs)
context.update({
'box_title': _("Create a new hostname"),
'ajax_title': True,
'template': "dashboard/_data_store_host-create.html",
})
return context
def get(self, *args, **kwargs):
if not self.request.user.has_perm('vm.add_datastorehost'):
raise PermissionDenied()
return super(DataStoreHostCreate, self).get(*args, **kwargs)
def post(self, request, *args, **kwargs):
if not self.request.user.has_perm('vm.add_datastorehost'):
raise PermissionDenied()
form = self.form_class(request.POST)
if not form.is_valid():
if self.request.is_ajax():
errors = self.errors_to_string(form)
return self.json_response(False, errors)
else:
return self.get(request, form, *args, **kwargs)
else:
instance = form.save()
if self.request.is_ajax():
resp = {"val": instance.id, "text": unicode(instance)}
return self.json_response(True, resp)
else:
return redirect(self.get_success_url())
def json_response(self, status, response):
resp = {
"status": status,
"response": response
}
return HttpResponse(json.dumps(resp), content_type="application/json")
def errors_to_string(self, form):
error_str = ""
if form.errors:
for field, error in form.errors.iteritems():
error_str += "%s: %s<br />" % (field, error)
for error in form.non_field_errors():
error_str += "%s<br />" % error
return error_str
def get_success_url(self):
return reverse_lazy("dashboard.views.storage-list") # TODO
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import storage.models
class Migration(migrations.Migration):
dependencies = [
('storage', '0003_auto_20151122_2104'),
]
operations = [
migrations.AddField(
model_name='datastorehost',
name='name',
field=models.CharField(default='Monitor1', unique=True, max_length=255, verbose_name='name'),
preserve_default=False,
),
migrations.AlterField(
model_name='datastore',
name='path',
field=models.CharField(unique=True, max_length=200, verbose_name='path', validators=[storage.models.validate_ascii]),
),
migrations.AlterField(
model_name='disk',
name='filename',
field=models.CharField(unique=True, max_length=256, verbose_name='filename', validators=[storage.models.validate_ascii]),
),
]
......@@ -55,9 +55,13 @@ class DataStoreHost(Model):
""" Address and port of a data store.
"""
name = CharField(max_length=255, unique=True, verbose_name=_('name'))
address = CharField(max_length=1024, verbose_name=_('address'))
port = IntegerField(null=True, blank=True, verbose_name=_('port'))
def __unicode__(self):
return u"%s | %s:%d" % (self.name, self.address, self.port)
class DataStore(Model):
......@@ -133,7 +137,8 @@ class DataStore(Model):
"""
queue_name = self.get_remote_queue_name('storage', "slow")
files = set(storage_tasks.list_files.apply_async(
args=[self.type, self.path], queue=queue_name).get(timeout=timeout))
args=[self.type, self.path], queue=queue_name).get(
timeout=timeout))
disks = set([disk.filename for disk in self.disk_set.all()])
orphans = []
......@@ -148,7 +153,8 @@ class DataStore(Model):
"""
queue_name = self.get_remote_queue_name('storage', "slow")
files = set(storage_tasks.list_files.apply_async(
args=[self.type, self.path], queue=queue_name).get(timeout=timeout))
args=[self.type, self.path], queue=queue_name).get(
timeout=timeout))
disks = Disk.objects.filter(destroyed__isnull=True, is_ready=True,
datastore=self)
return disks.exclude(filename__in=files)
......
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