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
--------
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
-------------------

View File

@ -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):

View File

@ -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, )

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
# 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')

View File

@ -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<lease_id>[^/]+)/$', leases_views.DetailView.as_view(),
name='detail'),
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'
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'

View File

@ -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)

View File

@ -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)