Support lease creation

This patch adds a lease creation feature to the Blazar dashboard.

Change-Id: Id078c570122e3de4d4569023f85a94af7ccaa05b
Partially Implements: blueprint climate-dashboard
This commit is contained in:
Hiroaki Kobayashi 2017-07-26 17:37:30 +09:00
parent c4afe8b438
commit 6ab330c514
10 changed files with 276 additions and 8 deletions

View File

@ -11,17 +11,12 @@ Horizon plugin for the Blazar Reservation Service for OpenStack
Features Features
-------- --------
The following features are currently supported: See doc/source/index.rst
* Show a list of leases
* Show details of a lease
* Update a lease
* Delete lease(s)
Enabling in DevStack Enabling in DevStack
-------------------- --------------------
* Not yet supported Not yet supported
Manual Installation Manual Installation
------------------- -------------------

View File

@ -13,18 +13,159 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import datetime
import logging import logging
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import messages from horizon import messages
from pytz import timezone
from blazar_dashboard import api from blazar_dashboard import api
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class CreateForm(forms.SelfHandlingForm):
# General fields
name = forms.CharField(
label=_("Lease Name"),
required=True,
max_length=80
)
start_date = forms.DateTimeField(
label=_("Start Date"),
required=False,
help_text=_('Enter YYYY-MM-DD HH:MM or blank for now'),
input_formats=['%Y-%m-%d %H:%M'],
widget=forms.DateTimeInput(
attrs={'placeholder': 'YYYY-MM-DD HH:MM (blank for now)'})
)
end_date = forms.DateTimeField(
label=_("End Date"),
required=False,
help_text=_('Enter YYYY-MM-DD HH:MM or blank for Start Date + 24h'),
input_formats=['%Y-%m-%d %H:%M'],
widget=forms.DateTimeInput(
attrs={'placeholder': 'YYYY-MM-DD HH:MM (blank for Start Date + '
'24h)'})
)
resource_type = forms.ChoiceField(
label=_("Resource Type"),
required=True,
choices=(
('host', _('Physical Host')),
('instance', _('Virtual Instance (Not yet supported in GUI)'))
),
widget=forms.ThemableSelectWidget(attrs={
'class': 'switchable',
'data-slug': 'source'}))
# Fields for host reservation
min_hosts = forms.IntegerField(
label=_('Minimum Number of Hosts'),
required=False,
help_text=_('Enter the minimum number of hosts to reserve.'),
min_value=1,
initial=1,
widget=forms.NumberInput(attrs={
'class': 'switched',
'data-switch-on': 'source',
'data-source-host': _('Minimum Number of Hosts')})
)
max_hosts = forms.IntegerField(
label=_('Maximum Number of Hosts'),
required=False,
help_text=_('Enter the maximum number of hosts to reserve.'),
min_value=1,
initial=1,
widget=forms.NumberInput(attrs={
'class': 'switched',
'data-switch-on': 'source',
'data-source-host': _('Maximum Number of Hosts')})
)
hypervisor_properties = forms.CharField(
label=_("Hypervisor Properties"),
required=False,
help_text=_('Enter properties of a hypervisor to reserve.'),
max_length=255,
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'source',
'data-source-host': _('Hypervisor Properties'),
'placeholder': 'e.g. [">=", "$vcpus", "2"]'})
)
resource_properties = forms.CharField(
label=_("Resource Properties"),
required=False,
help_text=_('Enter properties of a resource to reserve.'),
max_length=255,
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'source',
'data-source-host': _('Resource Properties'),
'placeholder': 'e.g. ["==", "$extra_key", "extra_value"]'})
)
def handle(self, request, data):
if data['resource_type'] == 'host':
reservations = [
{
'resource_type': 'physical:host',
'min': data['min_hosts'],
'max': data['max_hosts'],
'hypervisor_properties': (data['hypervisor_properties']
or ''),
'resource_properties': data['resource_properties'] or ''
}
]
elif data['resource_type'] == 'instance':
raise forms.ValidationError('Virtual instance is not yet '
'supported in GUI')
events = []
try:
api.client.lease_create(
request, data['name'],
data['start_date'].strftime('%Y-%m-%d %H:%M'),
data['end_date'].strftime('%Y-%m-%d %H:%M'),
reservations, events)
messages.success(request, _('Lease %s was successfully '
'created.') % data['name'])
return True
except Exception as e:
LOG.error('Error submitting lease: %s', e)
exceptions.handle(request,
message='An error occurred while creating this '
'lease: %s. Please try again.' % e)
def clean(self):
cleaned_data = super(CreateForm, self).clean()
local = timezone(self.request.session.get(
'django_timezone',
self.request.COOKIES.get('django_timezone', 'UTC')))
if cleaned_data['start_date']:
cleaned_data['start_date'] = local.localize(
cleaned_data['start_date'].replace(tzinfo=None)
).astimezone(timezone('UTC'))
else:
cleaned_data['start_date'] = datetime.datetime.utcnow()
if cleaned_data['end_date']:
cleaned_data['end_date'] = local.localize(
cleaned_data['end_date'].replace(tzinfo=None)
).astimezone(timezone('UTC'))
else:
cleaned_data['end_date'] = (cleaned_data['start_date']
+ datetime.timedelta(days=1))
if cleaned_data['resource_type'] == 'instance':
raise forms.ValidationError('Resource type "virtual instance" is '
'not yet supported in GUI')
class UpdateForm(forms.SelfHandlingForm): class UpdateForm(forms.SelfHandlingForm):
class Meta(object): class Meta(object):

View File

@ -26,6 +26,14 @@ import pytz
from blazar_dashboard import api from blazar_dashboard import api
class CreateLease(tables.LinkAction):
name = "create"
verbose_name = _("Create Lease")
url = "horizon:project:leases:create"
classes = ("ajax-modal",)
icon = "plus"
class UpdateLease(tables.LinkAction): class UpdateLease(tables.LinkAction):
name = "update" name = "update"
verbose_name = _("Update Lease") verbose_name = _("Update Lease")
@ -83,5 +91,5 @@ class LeasesTable(tables.DataTable):
class Meta(object): class Meta(object):
name = "leases" name = "leases"
verbose_name = _("Leases") verbose_name = _("Leases")
table_actions = (DeleteLease, ) table_actions = (CreateLease, DeleteLease, )
row_actions = (UpdateLease, DeleteLease, ) row_actions = (UpdateLease, DeleteLease, )

View File

@ -0,0 +1,7 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description" %}:</h3>
<p>{% trans "Create a lease with the provided values." %}</p>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Lease" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Create Lease") %}
{% endblock page_header %}
{% block main %}
{% include 'project/leases/_create.html' %}
{% endblock %}

View File

@ -10,9 +10,12 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from datetime import datetime
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
from mox3.mox import IsA from mox3.mox import IsA
import pytz
from blazar_dashboard import api from blazar_dashboard import api
from blazar_dashboard.test import helpers as test from blazar_dashboard.test import helpers as test
@ -24,6 +27,8 @@ INDEX_TEMPLATE = 'project/leases/index.html'
INDEX_URL = reverse('horizon:project:leases:index') INDEX_URL = reverse('horizon:project:leases:index')
DETAIL_TEMPLATE = 'project/leases/detail.html' DETAIL_TEMPLATE = 'project/leases/detail.html'
DETAIL_URL_BASE = 'horizon:project:leases:detail' DETAIL_URL_BASE = 'horizon:project:leases:detail'
CREATE_URL = reverse('horizon:project:leases:create')
CREATE_TEMPLATE = 'project/leases/create.html'
UPDATE_URL_BASE = 'horizon:project:leases:update' UPDATE_URL_BASE = 'horizon:project:leases:update'
UPDATE_TEMPLATE = 'project/leases/update.html' UPDATE_TEMPLATE = 'project/leases/update.html'
@ -87,6 +92,94 @@ class LeasesTests(test.TestCase):
self.assertMessageCount(error=1) self.assertMessageCount(error=1)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.client: ('lease_create', )})
def test_create_lease_host_reservation(self):
start_date = datetime(2030, 6, 27, 18, 0, tzinfo=pytz.utc)
end_date = datetime(2030, 6, 30, 18, 0, tzinfo=pytz.utc)
new_lease = self.leases.get(name='lease-1')
api.client.lease_create(
IsA(http.HttpRequest),
'lease-1',
start_date.strftime('%Y-%m-%d %H:%M'),
end_date.strftime('%Y-%m-%d %H:%M'),
[
{
'min': 1,
'max': 1,
'hypervisor_properties': '[">=", "$vcpus", "2"]',
'resource_properties': '',
'resource_type': 'physical:host',
}
],
[]
).AndReturn(new_lease)
self.mox.ReplayAll()
form_data = {
'name': 'lease-1',
'start_date': start_date.strftime('%Y-%m-%d %H:%M'),
'end_date': end_date.strftime('%Y-%m-%d %H:%M'),
'resource_type': 'host',
'min_hosts': 1,
'max_hosts': 1,
'hypervisor_properties': '[">=", "$vcpus", "2"]'
}
res = self.client.post(CREATE_URL, form_data)
self.assertNoFormErrors(res)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.client: ('lease_create', )})
def test_create_lease_instance_reservation(self):
start_date = datetime(2030, 6, 27, 18, 0, tzinfo=pytz.utc)
end_date = datetime(2030, 6, 30, 18, 0, tzinfo=pytz.utc)
form_data = {
'name': 'lease-1',
'start_date': start_date.strftime('%Y-%m-%d %H:%M'),
'end_date': end_date.strftime('%Y-%m-%d %H:%M'),
'resource_type': 'instance',
}
res = self.client.post(CREATE_URL, form_data)
self.assertTemplateUsed(res, CREATE_TEMPLATE)
self.assertFormErrors(res, 1)
self.assertContains(res, 'not yet supported')
@test.create_stubs({api.client: ('lease_create', )})
def test_create_lease_client_error(self):
start_date = datetime(2030, 6, 27, 18, 0, tzinfo=pytz.utc)
end_date = datetime(2030, 6, 30, 18, 0, tzinfo=pytz.utc)
api.client.lease_create(
IsA(http.HttpRequest),
'lease-1',
start_date.strftime('%Y-%m-%d %H:%M'),
end_date.strftime('%Y-%m-%d %H:%M'),
[
{
'min': 1,
'max': 1,
'hypervisor_properties': '',
'resource_properties': '',
'resource_type': 'physical:host',
}
],
[]
).AndRaise(self.exceptions.blazar)
self.mox.ReplayAll()
form_data = {
'name': 'lease-1',
'start_date': start_date.strftime('%Y-%m-%d %H:%M'),
'end_date': end_date.strftime('%Y-%m-%d %H:%M'),
'resource_type': 'host',
'min_hosts': 1,
'max_hosts': 1,
}
res = self.client.post(CREATE_URL, form_data)
self.assertTemplateUsed(res, CREATE_TEMPLATE)
self.assertNoFormErrors(res)
self.assertContains(res, 'An error occurred while creating')
@test.create_stubs({api.client: ('lease_get', 'lease_update')}) @test.create_stubs({api.client: ('lease_get', 'lease_update')})
def test_update_lease(self): def test_update_lease(self):
lease = self.leases.get(name='lease-1') lease = self.leases.get(name='lease-1')

View File

@ -20,6 +20,7 @@ from blazar_dashboard.content.leases import views as leases_views
urlpatterns = [ urlpatterns = [
url(r'^$', leases_views.IndexView.as_view(), name='index'), url(r'^$', leases_views.IndexView.as_view(), name='index'),
url(r'^create/$', leases_views.CreateView.as_view(), name='create'),
url(r'^(?P<lease_id>[^/]+)/$', leases_views.DetailView.as_view(), url(r'^(?P<lease_id>[^/]+)/$', leases_views.DetailView.as_view(),
name='detail'), name='detail'),
url(r'^(?P<lease_id>[^/]+)/update$', leases_views.UpdateView.as_view(), url(r'^(?P<lease_id>[^/]+)/update$', leases_views.UpdateView.as_view(),

View File

@ -47,6 +47,16 @@ class DetailView(tabs.TabView):
template_name = 'project/leases/detail.html' template_name = 'project/leases/detail.html'
class CreateView(forms.ModalFormView):
form_class = project_forms.CreateForm
template_name = 'project/leases/create.html'
success_url = reverse_lazy('horizon:project:leases:index')
modal_id = "create_lease_modal"
modal_header = _("Create Lease")
submit_label = _("Create Lease")
submit_url = reverse_lazy('horizon:project:leases:create')
class UpdateView(forms.ModalFormView): class UpdateView(forms.ModalFormView):
form_class = project_forms.UpdateForm form_class = project_forms.UpdateForm
template_name = 'project/leases/update.html' template_name = 'project/leases/update.html'

View File

@ -15,6 +15,7 @@ The following features are currently supported:
* Show a list of leases * Show a list of leases
* Show details of a lease * Show details of a lease
* Create a lease
* Update a lease * Update a lease
* Delete lease(s) * Delete lease(s)

View File

@ -3,5 +3,6 @@ features:
The following features are currently supported: The following features are currently supported:
- Show a list of leases - Show a list of leases
- Show details of a lease - Show details of a lease
- Create a lease
- Update a lease - Update a lease
- Delete lease(s) - Delete lease(s)