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:
parent
c4afe8b438
commit
6ab330c514
@ -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
|
||||
-------------------
|
||||
|
@ -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):
|
||||
|
@ -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, )
|
||||
|
@ -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 %}
|
11
blazar_dashboard/content/leases/templates/leases/create.html
Normal file
11
blazar_dashboard/content/leases/templates/leases/create.html
Normal 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 %}
|
@ -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')
|
||||
|
@ -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(),
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user