Commit 013ed92b by Kálmán Viktor

Merge branch 'feature-resize-help' into 'master'

Disk resize help and request

![Kijelölés_053](https://git.ik.bme.hu/circle/cloud/uploads/04c1c2c4264db0e93ad9243f22c79d8e/Kijelölés_053.png)

See merge request !341
parents 9d2184d8 fb293c07
......@@ -25,7 +25,7 @@ from django.shortcuts import redirect
from circle.settings.base import get_env_variable
from dashboard.views import circle_login, HelpView
from dashboard.views import circle_login, HelpView, ResizeHelpView
from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm
from firewall.views import add_blacklist_item
......@@ -65,6 +65,8 @@ urlpatterns = patterns(
url(r'^info/support/$',
TemplateView.as_view(template_name="info/support.html"),
name="info.support"),
url(r'^info/resize-how-to/$', ResizeHelpView.as_view(),
name="info.resize"),
)
......
......@@ -1488,3 +1488,38 @@ textarea[name="new_members"] {
.acl-table td:first-child {
text-align: center;
}
#resize-help {
table {
background-color: #f5f5f5;
}
.panel {
padding: 2px 20px;
background-color: #f5f5f5;
margin: 20px 0px;
}
ol li {
margin-top: 15px;
}
img {
display: block;
margin: 15px 0 5px 0;
}
pre {
margin-top: 5px;
}
hr {
margin: 50px 0;
}
}
#vm-details-resize-how-to {
font-size: 1.5em;
text-align: center;
width: 100%;
}
......@@ -4,24 +4,29 @@
<i class="fa fa-file"></i>
{{ d.name }} (#{{ d.id }}) - {{ d.size|filesize }}
{% if op.remove_disk %}
<span class="operation-wrapper">
<a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn
{% if op.remove_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.remove_disk.icon }} fa-fw-12"></i> {% trans "Remove" %}
</a>
</span>
{% endif %}
{% if op.resize_disk %}
<span class="operation-wrapper">
<span class="operation-wrapper pull-right">
{% if d.is_resizable %}
{% if op.resize_disk %}
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn
class="btn btn-xs btn-{{ op.resize_disk.effect }} operation disk-resize-btn
{% if op.resize_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.resize_disk.icon }} fa-fw-12"></i> {% trans "Resize" %}
</a>
</span>
{% endif %}
{% else %}
<a href="{% url "request.views.request-resize" vm_pk=instance.pk disk_pk=d.pk %}" class="btn btn-xs btn-primary operation">
<i class="fa fa-arrows-alt fa-fw-12"></i> {% trans "Request resize" %}
</a>
{% endif %}
{% endif %}
{% if op.remove_disk %}
<a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.remove_disk.effect}} operation disk-remove-btn
{% if op.remove_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.remove_disk.icon }} fa-fw-12"></i> {% trans "Remove" %}
</a>
{% endif %}
</span>
<div style="clear: both;"></div>
{% if request.user.is_superuser %}
......
......@@ -66,6 +66,17 @@
{% endfor %}
</div>
<hr />
{% if instance.disks.all %}
<div id="vm-details-resize-how-to">
<i class="fa fa-question"></i>
{% url "info.resize" as resize_url %}
{% blocktrans with url=resize_url %}
If you need help resizing the disks check out our <a href="{{ url }}">resize how-to.</a>
{% endblocktrans %}
</div>
{% endif %}
{% if user.is_superuser %}
<hr/>
......
......@@ -136,6 +136,10 @@ class HelpView(TemplateView):
return ctx
class ResizeHelpView(TemplateView):
template_name = "info/resize.html"
class OpenSearchDescriptionView(TemplateView):
template_name = "dashboard/vm-opensearch.xml"
content_type = "application/opensearchdescription+xml"
......
......@@ -22,6 +22,8 @@ from django.utils.translation import ugettext_lazy as _
from django.template import RequestContext
from django.template.loader import render_to_string
from sizefield.widgets import FileSizeWidget
from sizefield.utils import filesizeformat
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
......@@ -70,34 +72,32 @@ class InitialFromFileMixin(object):
RequestContext(request, {}),
)
def clean(self):
cleaned_data = super(InitialFromFileMixin, self).clean()
if cleaned_data['message'].strip() == self.initial['message'].strip():
raise ValidationError(
_("Fill in the message."),
code="invalid")
return cleaned_data
def clean_message(self):
message = self.cleaned_data['message']
if message.strip() == self.initial['message'].strip():
raise ValidationError(_("Fill in the message."), code="invalid")
return message.strip()
class TemplateRequestForm(InitialFromFileMixin, Form):
message = CharField(widget=Textarea, label=_("Message"))
template = ModelChoiceField(TemplateAccessType.objects.all(),
label=_("Template share"))
level = ChoiceField(TemplateAccessAction.LEVELS, widget=RadioSelect,
initial=TemplateAccessAction.LEVELS.user)
message = CharField(widget=Textarea, label=_("Message"))
initial_template = "request/initials/template.html"
class LeaseRequestForm(InitialFromFileMixin, Form):
lease = ModelChoiceField(LeaseType.objects.all(), label=_("Lease"))
message = CharField(widget=Textarea)
message = CharField(widget=Textarea, label=_("Message"))
initial_template = "request/initials/lease.html"
class ResourceRequestForm(InitialFromFileMixin, VmResourcesForm):
message = CharField(widget=Textarea)
message = CharField(widget=Textarea, label=_("Message"))
initial_template = "request/initials/resources.html"
......@@ -110,3 +110,28 @@ class ResourceRequestForm(InitialFromFileMixin, VmResourcesForm):
raise ValidationError(
_("You haven't changed any of the resources."),
code="invalid")
class ResizeRequestForm(InitialFromFileMixin, Form):
message = CharField(widget=Textarea, label=_("Message"))
size = CharField(widget=FileSizeWidget, label=_('Size'),
help_text=_('Size to resize the disk in bytes or with'
' units like MB or GB.'))
initial_template = "request/initials/resize.html"
def __init__(self, *args, **kwargs):
self.disk = kwargs.pop("disk")
super(ResizeRequestForm, self).__init__(*args, **kwargs)
def clean_size(self):
cleaned_data = super(ResizeRequestForm, self).clean()
disk = self.disk
size_in_bytes = cleaned_data.get("size")
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
raise ValidationError(_("Invalid format, you can use GB or MB!"))
if int(size_in_bytes) < int(disk.size):
raise ValidationError(_("Disk size must be greater than the actual"
"size (%s).") % filesizeformat(disk.size))
return size_in_bytes
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import sizefield.models
class Migration(migrations.Migration):
dependencies = [
('vm', '0002_interface_model'),
('storage', '0002_disk_bus'),
('request', '0003_auto_20150410_1917'),
]
operations = [
migrations.CreateModel(
name='DiskResizeAction',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('size', sizefield.models.FileSizeField(default=None, null=True)),
('disk', models.ForeignKey(to='storage.Disk')),
('instance', models.ForeignKey(to='vm.Instance')),
],
options={
'abstract': False,
},
),
migrations.AlterField(
model_name='request',
name='type',
field=models.CharField(max_length=10, choices=[(b'resource', 'resource request'), (b'lease', 'lease request'), (b'template', 'template access request'), (b'resize', 'disk resize request')]),
),
]
......@@ -32,10 +32,14 @@ from django.utils.translation import (
from django.core.urlresolvers import reverse
import requests
from sizefield.models import FileSizeField
from model_utils.models import TimeStampedModel
from model_utils import Choices
from sizefield.utils import filesizeformat
from vm.models import Instance, InstanceTemplate, Lease
from vm.operations import ResourcesOperation, ResizeDiskOperation
from storage.models import Disk
logger = logging.getLogger(__name__)
......@@ -49,6 +53,9 @@ class RequestAction(Model):
def accept_msg(self):
raise NotImplementedError
def is_acceptable(self):
return True
class Meta:
abstract = True
......@@ -77,6 +84,7 @@ class Request(TimeStampedModel):
('resource', _('resource request')),
('lease', _("lease request")),
('template', _("template access request")),
('resize', _("disk resize request")),
)
type = CharField(choices=TYPES, max_length=10)
message = TextField(verbose_name=_("Message"))
......@@ -99,7 +107,8 @@ class Request(TimeStampedModel):
return {
'resource': "tasks",
'lease': "clock-o",
'template': "puzzle-piece"
'template': "puzzle-piece",
'resize': "arrows-alt",
}.get(self.type)
def get_effect(self):
......@@ -143,6 +152,10 @@ class Request(TimeStampedModel):
decline_msg, url=self.get_absolute_url(), reason=self.reason,
)
@property
def is_acceptable(self):
return self.action.is_acceptable()
class LeaseType(RequestType):
lease = ForeignKey(Lease, verbose_name=_("Lease"))
......@@ -200,6 +213,9 @@ class ResourceChangeAction(RequestAction):
'priority': self.priority,
}
def is_acceptable(self):
return self.instance.status in ResourcesOperation.accept_states
class ExtendLeaseAction(RequestAction):
instance = ForeignKey(Instance)
......@@ -246,6 +262,30 @@ class TemplateAccessAction(RequestAction):
) % ", ".join([x.name for x in self.template_type.templates.all()])
class DiskResizeAction(RequestAction):
instance = ForeignKey(Instance)
disk = ForeignKey(Disk)
size = FileSizeField(null=True, default=None)
def accept(self, user):
self.instance.resize_disk(disk=self.disk, size=self.size, user=user)
@property
def accept_msg(self):
return _(
'The disk <em class="text-muted">%(disk_name)s (#%(id)d)</em> of '
'<a href="%(url)s">%(vm_name)s</a> got resized. '
'The new size is: %(bytes)d bytes (%(size)s).'
) % {'disk_name': self.disk.name, 'id': self.disk.id,
'url': self.instance.get_absolute_url(),
'vm_name': self.instance.name,
'bytes': self.size, 'size': filesizeformat(self.size),
}
def is_acceptable(self):
return self.instance.status in ResizeDiskOperation.accept_states
def send_notifications(sender, instance, created, **kwargs):
if not created:
return
......
{% load i18n %}
{% load crispy_forms_tags %}
{% load sizefieldtags %}
<dl>
<dt>{% trans "Virtual machine" %}</dt>
<dd><a href="{{ vm.get_absolute_url }}">{{ vm.name }}</a></dd>
<dt>{% trans "Disk" %}</dt>
<dd>
{% if request.user.is_superuser %}
<a href="{{ disk.get_absolute_url }}">{{ disk.name }} (#{{ disk.id }})</a>
{% else %}
{{ disk.name }} (#{{ disk.id }})
{% endif %}
- {{ disk.size|filesize }}
</dd>
</dl>
<form action="{% url "request.views.request-resize" vm_pk=vm.pk disk_pk=disk.pk %}" method="POST">
{% include "display-form-errors.html" %}
{% csrf_token %}
{{ form.size|as_crispy_field }}
{{ form.message|as_crispy_field }}
<input type="submit" class="btn btn-primary" id="op-form-send"/>
</form>
......@@ -3,6 +3,7 @@
{% load i18n %}
{% load render_table from django_tables2 %}
{% load arrowfilter %}
{% load sizefieldtags %}
{% block title-page %}{% trans "Request" %}{% endblock %}
......@@ -65,6 +66,15 @@
<dd>{{ action.get_readable_level }}</dd>
</dl>
{% elif object.type == "resource" %}
{% if not is_acceptable %}
<div class="alert alert-warning">
{% blocktrans %}
To change the resources the virtual machine must be in one of the following states:
STOPPED, PENDING, RUNNING. If the virtual machine is running it will be
automatically stopped when accepting the request.
{% endblocktrans %}
</div>
{% endif %}
<dl>
<dt>{% trans "VM name" %}</dt>
<dd><a href="{{ action.instance.get_absolute_url }}">{{ action.instance.name }}</a></dd>
......@@ -74,7 +84,7 @@
{{ action.instance.get_status_display|upper }}
</dd>
<dt>{% trans "VM description" %}</dt>
<dd>{{ action.instance.description }}</dd>
<dd>{{ action.instance.description|default:"-" }}</dd>
<dt>
{% trans "Priority" %}
<span class="text-muted" style="font-weight: normal;">{% trans "(old values in parentheses)" %}</span>
......@@ -85,8 +95,39 @@
<dt>{% trans "Ram size" %}</dt>
<dd>{{ action.ram_size }} ({{ action.instance.ram_size }}) MiB</dd>
</dl>
{% elif object.type == "resize" %}
{% if not is_acceptable %}
<div class="alert alert-warning">
{% trans "To resize the disk the virtual machine must be in RUNNING state." %}
</div>
{% endif %}
<dl>
<dt>{% trans "VM name" %}</dt>
<dd><a href="{{ action.instance.get_absolute_url }}">{{ action.instance.name }}</a></dd>
<dt>{% trans "Status" %}</dt>
<dd>
<i class="fa {{ action.instance.get_status_icon }}"></i>
{{ action.instance.get_status_display|upper }}
</dd>
<dt>{% trans "VM description" %}</dt>
<dd>{{ action.instance.description|default:"-" }}</dd>
<dt>{% trans "Disk" %}</dt>
<dd>
{% if request.user.is_superuser %}
<a href="{{ action.disk.get_absolute_url }}">
{{ action.disk.name }} (#{{ action.disk.id}})
</a>
{% else %}
{{ action.disk.name }} (#{{ action.disk.id}})</dd>
{% endif %}
</dd>
<dt>{% trans "Current size" %}</dt>
<dd>{{ action.disk.size|filesize}} ({{ action.disk.size }} bytes)</dd>
<dt>{% trans "Requested size" %}</dt>
<dd>{{ action.size|filesize}} ({{ action.size }} bytes)</dd>
</dl>
{% else %}
hacks!!!
Are you adding a new action type?
{% endif %}
{% if object.status == "PENDING" and request.user.is_superuser %}
......@@ -103,7 +144,7 @@
{% trans "Decline" %}
</button>
</form>
{% if object.type == "resource" and action.instance.status not in accept_states %}
{% if not is_acceptable %}
{% trans "You can't accept this request because of the VM's state." %}
{% else %}
<form method="POST">
......
{% spaceless %}
{% if LANGUAGE_CODE == "en" %}
Why do you need a bigger disk?
{% else %} {# place your translations here #}
Why do you need a bigger disk?
{% endif %}
{% endspaceless %}
......@@ -23,7 +23,7 @@ from .views import (
LeaseTypeCreate, LeaseTypeDetail,
TemplateAccessTypeCreate, TemplateAccessTypeDetail,
TemplateRequestView, LeaseRequestView, ResourceRequestView,
LeaseTypeDelete, TemplateAccessTypeDelete,
LeaseTypeDelete, TemplateAccessTypeDelete, ResizeRequestView,
)
urlpatterns = patterns(
......@@ -60,4 +60,6 @@ urlpatterns = patterns(
name="request.views.request-lease"),
url(r'resource/(?P<vm_pk>\d+)/$', ResourceRequestView.as_view(),
name="request.views.request-resource"),
url(r'resize/(?P<vm_pk>\d+)/(?P<disk_pk>\d+)/$',
ResizeRequestView.as_view(), name="request.views.request-resize"),
)
......@@ -19,27 +19,29 @@ from __future__ import unicode_literals, absolute_import
from django.views.generic import (
UpdateView, TemplateView, DetailView, CreateView, FormView, DeleteView,
)
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import redirect, get_object_or_404
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.http import JsonResponse
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django_tables2 import SingleTableView
from request.models import (
Request, TemplateAccessType, LeaseType, TemplateAccessAction,
ExtendLeaseAction, ResourceChangeAction,
ExtendLeaseAction, ResourceChangeAction, DiskResizeAction
)
from storage.models import Disk
from vm.models import Instance
from vm.operations import ResourcesOperation
from request.tables import (
RequestTable, TemplateAccessTypeTable, LeaseTypeTable,
)
from request.forms import (
LeaseTypeForm, TemplateAccessTypeForm, TemplateRequestForm,
LeaseRequestForm, ResourceRequestForm,
LeaseRequestForm, ResourceRequestForm, ResizeRequestForm,
)
......@@ -93,7 +95,7 @@ class RequestDetail(LoginRequiredMixin, DetailView):
context = super(RequestDetail, self).get_context_data(**kwargs)
context['action'] = request.action
context['accept_states'] = ResourcesOperation.accept_states
context['is_acceptable'] = request.is_acceptable
# workaround for http://git.io/vIIYi
context['request'] = self.request
......@@ -167,6 +169,7 @@ class RequestTypeList(LoginRequiredMixin, SuperuserRequiredMixin,
class TemplateRequestView(LoginRequiredMixin, FormView):
form_class = TemplateRequestForm
template_name = "request/request-template.html"
success_message = _("Request successfully sent.")
def get_form_kwargs(self):
kwargs = super(TemplateRequestView, self).get_form_kwargs()
......@@ -192,7 +195,8 @@ class TemplateRequestView(LoginRequiredMixin, FormView):
)
req.save()
return redirect("/")
messages.success(self.request, self.success_message)
return redirect(reverse("dashboard.index"))
class VmRequestMixin(LoginRequiredMixin, object):
......@@ -224,6 +228,7 @@ class LeaseRequestView(VmRequestMixin, FormView):
form_class = LeaseRequestForm
template_name = "request/request-lease.html"
user_level = "operator"
success_message = _("Request successfully sent.")
def form_valid(self, form):
data = form.cleaned_data
......@@ -244,6 +249,7 @@ class LeaseRequestView(VmRequestMixin, FormView):
)
req.save()
messages.success(self.request, self.success_message)
return redirect(vm.get_absolute_url())
......@@ -251,6 +257,7 @@ class ResourceRequestView(VmRequestMixin, FormView):
form_class = ResourceRequestForm
template_name = "request/request-resource.html"
user_level = "user"
success_message = _("Request successfully sent.")
def get_form_kwargs(self):
kwargs = super(ResourceRequestView, self).get_form_kwargs()
......@@ -287,4 +294,60 @@ class ResourceRequestView(VmRequestMixin, FormView):
)
req.save()
messages.success(self.request, self.success_message)
return redirect(vm.get_absolute_url())
class ResizeRequestView(VmRequestMixin, FormView):
form_class = ResizeRequestForm
template_name = "request/_request-resize-form.html"
user_level = "owner"
success_message = _("Request successfully sent.")
def get_disk(self, *args, **kwargs):
disk = get_object_or_404(Disk, pk=self.kwargs['disk_pk'])
if disk not in self.get_vm().disks.all():
raise SuspiciousOperation
return disk
def get_form_kwargs(self):
kwargs = super(ResizeRequestView, self).get_form_kwargs()
kwargs['disk'] = self.get_disk()
return kwargs
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/_base.html']
def get_context_data(self, **kwargs):
context = super(ResizeRequestView, self).get_context_data(**kwargs)
context['disk'] = self.get_disk()
context['template'] = self.template_name
context['box_title'] = context['title'] = _("Disk resize request")
context['ajax_title'] = True
return context
def form_valid(self, form):
disk = self.get_disk()
if not disk.is_resizable:
raise SuspiciousOperation
vm = self.get_vm()
data = form.cleaned_data
user = self.request.user
dra = DiskResizeAction(instance=vm, disk=disk, size=data['size'])
dra.save()
req = Request(user=user, message=data['message'], action=dra,
type=Request.TYPES.resize)
req.save()
if self.request.is_ajax():
return JsonResponse({'success': True,
'messages': [self.success_message]})
else:
messages.success(self.request, self.success_message)
return redirect(vm.get_absolute_url())
......@@ -28,6 +28,7 @@ from celery.contrib.abortable import AbortableAsyncResult
from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey)
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
from model_utils.models import TimeStampedModel
......@@ -535,3 +536,10 @@ class Disk(TimeStampedModel):
disk.is_ready = True
disk.save()
return disk
def get_absolute_url(self):
return reverse('dashboard.views.disk-detail', kwargs={'pk': self.pk})
@property
def is_resizable(self):
return self.type in ('qcow2-norm', 'raw-rw')
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% block title-page %}{% trans "Resize how-to" %}{% endblock %}
{% block content %}
<div class="row" id="resize-help">
<div class="col-lg-12">
<div class="page-header">
<h1 id="disk-linux">
<i class="fa fa-linux"></i>
{% trans "Expanding disk on Linux" %}
</h1>
</div>
<p>
{% blocktrans %}
If you don't have enogh space on your virtual machine you can ask for more.
After a request has been made an administrator can extend your HDD.
If the request is granted you have to manually rescan
and extend a logical volume on your machine to acquire the extra space.
To do so you need root access/administrator rights.
{% endblocktrans %}
</p>
<ol>
<li>
{% trans "Ask the administrator for more space. After it has been granted do the following steps." %}
</li>
<li>
{% blocktrans %}
You can check how much free space is left on your machine
(on Debian based distributions like Ubuntu) with the
<strong><code>df -h</code></strong> command.
As you can see below we need more space on
<strong>/</strong> so we will extend
<strong>/dev/mapper/cloud–x–vg-root</strong>.
{% endblocktrans %}
<div class="panel panel-default table-responsive">
<table class="table">
<thead>
<tr>
<th>Filesystem</th>
<th>Size</th>
<th>Used</th>
<th>Avail</th>
<th>Use%</th>
<th>Mounted on</th>
</tr>
</thead>
<tbody><tr>
<td><strong>/dev/mapper/cloud–x–vg-root</strong></td>
<td>39G</td>
<td>37G</td>
<td>65M</td>
<td>100%</td>
<td><strong>/</strong></td>
</tr>
<tr>
<td>none</td>
<td>4.0K</td>
<td>0</td>
<td>4.0K</td>
<td>0%</td>
<td>/sys/fs/cgroup</td>
</tr>
<tr>
<td>udev</td>
<td>487M</td>
<td>4.0K</td>
<td>487M</td>
<td>1%</td>
<td>/dev</td>
</tr>
<tr>
<td>tmpfs</td>
<td>100M</td>
<td>368K</td>
<td>100M</td>
<td>1%</td>
<td>/run</td>
</tr>
<tr>
<td>none</td>
<td>5.0M</td>
<td>0</td>
<td>5.0M</td>
<td>0%</td>
<td>/run/lock</td>
</tr>
<tr>
<td>none</td>
<td>498M</td>
<td>0</td>
<td>498M</td>
<td>0%</td>
<td>/run/shm</td>
</tr>
<tr>