From 6ab330c51411395912386cfc2c71a3eaa9d183bf Mon Sep 17 00:00:00 2001 From: Hiroaki Kobayashi Date: Wed, 26 Jul 2017 17:37:30 +0900 Subject: [PATCH] Support lease creation This patch adds a lease creation feature to the Blazar dashboard. Change-Id: Id078c570122e3de4d4569023f85a94af7ccaa05b Partially Implements: blueprint climate-dashboard --- README.rst | 9 +- blazar_dashboard/content/leases/forms.py | 141 ++++++++++++++++++ blazar_dashboard/content/leases/tables.py | 10 +- .../leases/templates/leases/_create.html | 7 + .../leases/templates/leases/create.html | 11 ++ blazar_dashboard/content/leases/tests.py | 93 ++++++++++++ blazar_dashboard/content/leases/urls.py | 1 + blazar_dashboard/content/leases/views.py | 10 ++ doc/source/index.rst | 1 + .../dashboard-support-1b429d36f395d93a.yaml | 1 + 10 files changed, 276 insertions(+), 8 deletions(-) create mode 100644 blazar_dashboard/content/leases/templates/leases/_create.html create mode 100644 blazar_dashboard/content/leases/templates/leases/create.html diff --git a/README.rst b/README.rst index 8af7877..4977b8e 100644 --- a/README.rst +++ b/README.rst @@ -11,17 +11,12 @@ Horizon plugin for the Blazar Reservation Service for OpenStack Features -------- -The following features are currently supported: - -* Show a list of leases -* Show details of a lease -* Update a lease -* Delete lease(s) +See doc/source/index.rst Enabling in DevStack -------------------- -* Not yet supported +Not yet supported Manual Installation ------------------- diff --git a/blazar_dashboard/content/leases/forms.py b/blazar_dashboard/content/leases/forms.py index f6dc1e2..333b78f 100644 --- a/blazar_dashboard/content/leases/forms.py +++ b/blazar_dashboard/content/leases/forms.py @@ -13,18 +13,159 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime import logging from django.utils.translation import ugettext_lazy as _ from horizon import exceptions from horizon import forms from horizon import messages +from pytz import timezone from blazar_dashboard import api 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 Meta(object): diff --git a/blazar_dashboard/content/leases/tables.py b/blazar_dashboard/content/leases/tables.py index db9891e..d6b447e 100644 --- a/blazar_dashboard/content/leases/tables.py +++ b/blazar_dashboard/content/leases/tables.py @@ -26,6 +26,14 @@ import pytz 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): name = "update" verbose_name = _("Update Lease") @@ -83,5 +91,5 @@ class LeasesTable(tables.DataTable): class Meta(object): name = "leases" verbose_name = _("Leases") - table_actions = (DeleteLease, ) + table_actions = (CreateLease, DeleteLease, ) row_actions = (UpdateLease, DeleteLease, ) diff --git a/blazar_dashboard/content/leases/templates/leases/_create.html b/blazar_dashboard/content/leases/templates/leases/_create.html new file mode 100644 index 0000000..96311ef --- /dev/null +++ b/blazar_dashboard/content/leases/templates/leases/_create.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description" %}:

+

{% trans "Create a lease with the provided values." %}

+{% endblock %} diff --git a/blazar_dashboard/content/leases/templates/leases/create.html b/blazar_dashboard/content/leases/templates/leases/create.html new file mode 100644 index 0000000..cfd608f --- /dev/null +++ b/blazar_dashboard/content/leases/templates/leases/create.html @@ -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 %} diff --git a/blazar_dashboard/content/leases/tests.py b/blazar_dashboard/content/leases/tests.py index 93e74c0..9d612c2 100644 --- a/blazar_dashboard/content/leases/tests.py +++ b/blazar_dashboard/content/leases/tests.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime + from django.core.urlresolvers import reverse from django import http from mox3.mox import IsA +import pytz from blazar_dashboard import api 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') DETAIL_TEMPLATE = 'project/leases/detail.html' 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_TEMPLATE = 'project/leases/update.html' @@ -87,6 +92,94 @@ class LeasesTests(test.TestCase): self.assertMessageCount(error=1) 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')}) def test_update_lease(self): lease = self.leases.get(name='lease-1') diff --git a/blazar_dashboard/content/leases/urls.py b/blazar_dashboard/content/leases/urls.py index b099515..93a8422 100644 --- a/blazar_dashboard/content/leases/urls.py +++ b/blazar_dashboard/content/leases/urls.py @@ -20,6 +20,7 @@ from blazar_dashboard.content.leases import views as leases_views urlpatterns = [ url(r'^$', leases_views.IndexView.as_view(), name='index'), + url(r'^create/$', leases_views.CreateView.as_view(), name='create'), url(r'^(?P[^/]+)/$', leases_views.DetailView.as_view(), name='detail'), url(r'^(?P[^/]+)/update$', leases_views.UpdateView.as_view(), diff --git a/blazar_dashboard/content/leases/views.py b/blazar_dashboard/content/leases/views.py index 67781f5..fdf676f 100644 --- a/blazar_dashboard/content/leases/views.py +++ b/blazar_dashboard/content/leases/views.py @@ -47,6 +47,16 @@ class DetailView(tabs.TabView): 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): form_class = project_forms.UpdateForm template_name = 'project/leases/update.html' diff --git a/doc/source/index.rst b/doc/source/index.rst index 78e226b..b047dea 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -15,6 +15,7 @@ The following features are currently supported: * Show a list of leases * Show details of a lease +* Create a lease * Update a lease * Delete lease(s) diff --git a/releasenotes/notes/dashboard-support-1b429d36f395d93a.yaml b/releasenotes/notes/dashboard-support-1b429d36f395d93a.yaml index d669b4f..8eca683 100644 --- a/releasenotes/notes/dashboard-support-1b429d36f395d93a.yaml +++ b/releasenotes/notes/dashboard-support-1b429d36f395d93a.yaml @@ -3,5 +3,6 @@ features: The following features are currently supported: - Show a list of leases - Show details of a lease + - Create a lease - Update a lease - Delete lease(s)