As an Operator, I want to define a resource class through the UI

Tab and Table with resource classes added(index view)
Create Workflow for Resource Class, info is editable.

Deleting is now working. Thanks to jprovaznik
Basic unit tests written.

Change-Id: I9f50a0dac26759f8c3fecbccbc4a822024939155
This commit is contained in:
Ladislav Smola
2013-06-12 12:47:02 +02:00
committed by Tomas Sedovic
parent 949779e62a
commit 4eaa37ef0b
16 changed files with 578 additions and 61 deletions

View File

@@ -1,15 +1,20 @@
[
{"pk": 1, "model": "infrastructure.flavor", "fields": {"name": "flavor1"}},
{"pk": 1, "model": "infrastructure.flavor", "fields": {"name": "flavor1"}},
{"pk": 2, "model": "infrastructure.flavor", "fields": {"name": "flavor2"}},
{"pk": 3, "model": "infrastructure.flavor", "fields": {"name": "flavor3"}},
{"pk": 4, "model": "infrastructure.flavor", "fields": {"name": "flavor4"}},
{"pk": 5, "model": "infrastructure.flavor", "fields": {"name": "flavor5"}},
{"pk": 6, "model": "infrastructure.flavor", "fields": {"name": "flavor6"}},
{"pk": 1, "model": "infrastructure.host", "fields": {"name": "host1", "rack": 1}},
{"pk": 2, "model": "infrastructure.host", "fields": {"name": "host2", "rack": 1}},
{"pk": 3, "model": "infrastructure.host", "fields": {"name": "host3", "rack": 2}},
{"pk": 4, "model": "infrastructure.host", "fields": {"name": "host4", "rack": 2}},
{"pk": 1, "model": "infrastructure.host", "fields": {"name": "host1", "rack": 1}},
{"pk": 2, "model": "infrastructure.host", "fields": {"name": "host2", "rack": 1}},
{"pk": 3, "model": "infrastructure.host", "fields": {"name": "host3", "rack": 2}},
{"pk": 4, "model": "infrastructure.host", "fields": {"name": "host4", "rack": 2}},
{"pk": 1, "model": "infrastructure.rack", "fields": {"name": "rack1", "resource_class": 1}},
{"pk": 2, "model": "infrastructure.rack", "fields": {"name": "rack2", "resource_class": 1}},
{"pk": 1, "model": "infrastructure.rack", "fields": {"name": "rack1", "resource_class": 1}},
{"pk": 2, "model": "infrastructure.rack", "fields": {"name": "rack2", "resource_class": 1}},
{"pk": 1, "model": "infrastructure.resourceclass", "fields": {"service_type": "compute", "flavors": [1], "name": "rclass1"}},
{"pk": 2, "model": "infrastructure.resourceclass", "fields": {"service_type": "compute", "flavors": [], "name": "rclass2"}},
{"pk": 3, "model": "infrastructure.resourceclass", "fields": {"service_type": "storage", "flavors": [], "name": "rclass3"}}
{"pk": 1, "model": "infrastructure.resourceclass", "fields": {"service_type": "compute", "flavors": [1], "name": "rclass1"}},
{"pk": 2, "model": "infrastructure.resourceclass", "fields": {"service_type": "compute", "flavors": [], "name": "rclass2"}},
{"pk": 3, "model": "infrastructure.resourceclass", "fields": {"service_type": "storage", "flavors": [], "name": "rclass3"}}
]

View File

@@ -24,7 +24,7 @@ class CreateRack(forms.SelfHandlingForm):
def __init__(self, request, *args, **kwargs):
super(CreateRack, self).__init__(request, *args, **kwargs)
resource_class_id_choices = [('', _("Select a Resource Class"))]
for rc in api.management.resource_class_list(request):
for rc in api.management.ResourceClass.list(request):
resource_class_id_choices.append((rc.id, rc.name))
self.fields['resource_class_id'].choices = resource_class_id_choices

View File

@@ -0,0 +1,86 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django import shortcuts
from django.core import urlresolvers
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import messages
from horizon import tables
from horizon import forms
from openstack_dashboard import api
LOG = logging.getLogger(__name__)
class CreateResourceClass(tables.LinkAction):
name = "create_class"
verbose_name = _("Create Class")
url = "horizon:infrastructure:resource_management:resource_classes:create"
classes = ("ajax-modal", "btn-create")
class UpdateResourceClass(tables.LinkAction):
name = "edit_class"
verbose_name = _("Edit Class")
url = "horizon:infrastructure:resource_management:resource_classes:update"
classes = ("ajax-modal", "btn-edit")
class DeleteResourceClass(tables.DeleteAction):
data_type_singular = _("Resource Class")
data_type_plural = _("Resource Classes")
def delete(self, request, obj_id):
try:
api.management.ResourceClass.get(request, obj_id).delete(request)
except:
msg = _('Failed to delete resource class %s') % obj_id
LOG.info(msg)
redirect = urlresolvers.reverse(
"horizon:infrastructure:resource_management:index")
exceptions.handle(request, msg, redirect=redirect)
class ResourcesClassFilterAction(tables.FilterAction):
def filter(self, table, instances, filter_string):
pass
class ResourceClassesTable(tables.DataTable):
name = tables.Column("name",
link=('horizon:infrastructure:'
'resource_management:resource_classes:detail'),
verbose_name=_("Class Name"))
service_type = tables.Column("service_type",
verbose_name=_("Class Type"))
racks_count = tables.Column("racks_count",
verbose_name=_("Racks"),
empty_value="0")
hosts_count = tables.Column("hosts_count",
verbose_name=_("Hosts"),
empty_value="0")
class Meta:
name = "resource_classes"
verbose_name = ("Classes")
table_actions = (ResourcesClassFilterAction, CreateResourceClass,
DeleteResourceClass)
row_actions = (UpdateResourceClass, DeleteResourceClass)

View File

@@ -1,3 +1,17 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import tabs

View File

@@ -1,15 +1,83 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from collections import namedtuple
from django import http
from django.core.urlresolvers import reverse
from mox import IsA
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
class ResourceClassesTests(test.BaseAdminViewTests):
def test_create_resource_class(self):
ResourceClass = namedtuple('ResourceClass', 'id, name, service_type')
resource_class = ResourceClass(1, 'test', 'compute')
self.mox.ReplayAll()
url = reverse(
'horizon:infrastructure:resource_management:'
'resource_classes:create')
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
data = {'name': resource_class.name,
'service_type': resource_class.service_type}
resp = self.client.post(url, data)
self.assertRedirectsNoFollow(
resp, reverse('horizon:infrastructure:resource_management:index'))
def test_edit_resource_class(self):
ResourceClass = namedtuple('ResourceClass', 'id, name, service_type')
resource_class = ResourceClass(1, 'test', 'compute')
self.mox.ReplayAll()
# get_test
url = reverse(
'horizon:infrastructure:resource_management:'
'resource_classes:update',
args=[resource_class.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
# post test
data = {'resource_class_id': resource_class.id,
'name': resource_class.name,
'service_type': resource_class.service_type}
resp = self.client.post(url, data)
self.assertNoFormErrors(resp)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(
resp, reverse('horizon:infrastructure:resource_management:index'))
""" #I don't have update yet, it's not suported by API """
def test_delete_resource_class(self):
ResourceClass = namedtuple('ResourceClass', 'id, name, service_type')
resource_class = ResourceClass(1, 'test', 'compute')
self.mox.ReplayAll()
form_data = {'action':
'resource_classes__delete__%s' % resource_class.id}
res = self.client.post(
reverse('horizon:infrastructure:resource_management:index'),
form_data)
self.assertRedirectsNoFollow(
res, reverse('horizon:infrastructure:resource_management:index'))
class ResourceClassViewTests(test.BaseAdminViewTests):
def test_detail_get(self):
ResourceClass = namedtuple('ResourceClass', 'id, name')
resource_class = ResourceClass('1', 'test')
ResourceClass = namedtuple('ResourceClass', 'id, name, service_type')
resource_class = ResourceClass('1', 'test', 'compute')
url = reverse('horizon:infrastructure:resource_management:'
'resource_classes:detail', args=[resource_class.id])

View File

@@ -1,9 +1,26 @@
from django.conf.urls.defaults import patterns, url
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from .views import DetailView
from django.conf.urls.defaults import patterns, url, include
from .views import CreateView, UpdateView, DetailView
urlpatterns = patterns('',
RESOURCE_CLASS = r'^(?P<resource_class_id>[^/]+)/%s$'
urlpatterns = patterns(
'',
url(r'^create$', CreateView.as_view(), name='create'),
url(r'^(?P<resource_class_id>[^/]+)/$',
DetailView.as_view(), name='detail'),
)
url(RESOURCE_CLASS % 'update', UpdateView.as_view(), name='update'))

View File

@@ -1,13 +1,79 @@
from django.core.urlresolvers import reverse
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Views for managing resource classes
"""
import logging
from django.core.urlresolvers import reverse_lazy, reverse
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from horizon import exceptions
from horizon import forms
from horizon import workflows
from openstack_dashboard import api
from .workflows import CreateResourceClass, UpdateResourceClass
from .tables import ResourceClassesTable
from .tabs import ResourceClassDetailTabs
LOG = logging.getLogger(__name__)
class CreateView(workflows.WorkflowView):
workflow_class = CreateResourceClass
def get_initial(self):
pass
class UpdateView(workflows.WorkflowView):
workflow_class = UpdateResourceClass
def get_context_data(self, **kwargs):
context = super(UpdateView, self).get_context_data(**kwargs)
context["resource_class_id"] = self.kwargs['resource_class_id']
return context
def _get_object(self, *args, **kwargs):
if not hasattr(self, "_object"):
resource_class_id = self.kwargs['resource_class_id']
try:
self._object = \
api.management.ResourceClass.get(self.request,
resource_class_id)
except:
redirect = self.success_url
msg = _('Unable to retrieve resource class details.')
exceptions.handle(self.request, msg, redirect=redirect)
return self._object
def get_initial(self):
resource_class = self._get_object()
return {'resource_class_id': resource_class.id,
'name': resource_class.name,
'service_type': resource_class.service_type}
class DetailView(tabs.TabView):
tab_group_class = ResourceClassDetailTabs
@@ -24,12 +90,11 @@ class DetailView(tabs.TabView):
try:
resource_class_id = self.kwargs['resource_class_id']
resource_class = api.management.\
resource_class_get(self.request,
resource_class_id)
ResourceClass.get(self.request,
resource_class_id)
except:
redirect = reverse('horizon:infrastructure:'
'resource_management:resource_classes:'
'index')
'resource_management:index')
exceptions.handle(self.request,
_('Unable to retrieve details for '
'resource class "%s".')

View File

@@ -0,0 +1,159 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import workflows
from horizon import forms
from openstack_dashboard import api
INDEX_URL = "horizon:infrastructure:resource_management:index"
class ResourceClassInfoAndFlavorsAction(workflows.Action):
name = forms.CharField(max_length=255,
label=_("Class Name"),
help_text="",
required=True)
service_type = forms.ChoiceField(label=_('Class Type'),
required=True,
choices=[('', ''),
('compute',
('Compute')),
('storage',
('Storage')),
],
widget=forms.Select(
attrs={'class': 'switchable'})
)
class Meta:
name = _("Class Settings")
help_text = _("From here you can fill the class "
"settings and add flavors to class.")
class CreateResourceClassInfoAndFlavors(workflows.Step):
action_class = ResourceClassInfoAndFlavorsAction
template_name = 'infrastructure/resource_management/resource_classes/'\
'_resource_class_info_and_flavors_step.html'
contributes = ("name", "service_type", "flavors_object_ids",
'flavors_object_ids_max_vms')
class ResourcesAction(workflows.Action):
class Meta:
name = _("Resources")
class CreateResources(workflows.Step):
action_class = ResourcesAction
contributes = ("resources_object_ids")
template_name = 'infrastructure/resource_management/'\
'resource_classes/_resources_step.html'
class ResourceClassWorkflowMixin:
def get_success_url(self):
return reverse(INDEX_URL)
def get_failure_url(self):
return reverse(INDEX_URL)
def format_status_message(self, message):
name = self.context.get('name')
return message % name
def _add_flavors(self, request, data, resource_class):
pass
def _add_resources(self, request, data, resource_class):
pass
class CreateResourceClass(ResourceClassWorkflowMixin, workflows.Workflow):
default_steps = (CreateResourceClassInfoAndFlavors,
CreateResources)
slug = "create_resource_class"
name = _("Create Class")
finalize_button_name = _("Create Class")
success_message = _('Created class "%s".')
failure_message = _('Unable to create class "%s".')
def _create_resource_class_info(self, request, data):
try:
return api.management.ResourceClass.create(
request,
name=data['name'],
service_type=data['service_type'])
except:
redirect = reverse(INDEX_URL)
exceptions.handle(request,
_('Unable to create resource class.'),
redirect=redirect)
return None
def handle(self, request, data):
resource_class = self._create_resource_class_info(request, data)
self._add_resources(request, data, resource_class)
self._add_flavors(request, data, resource_class)
return True
class UpdateResourceClassInfoAndFlavors(CreateResourceClassInfoAndFlavors):
depends_on = ("resource_class_id",)
class UpdateResources(CreateResources):
depends_on = ("resource_class_id",)
class UpdateResourceClass(ResourceClassWorkflowMixin, workflows.Workflow):
default_steps = (UpdateResourceClassInfoAndFlavors,
UpdateResources)
slug = "update_resource_class"
name = _("Update Class")
finalize_button_name = _("Update Class")
success_message = _('Updated class "%s".')
failure_message = _('Unable to update class "%s".')
def _update_resource_class_info(self, request, data):
try:
resource_class = api.management.ResourceClass.get(
request,
data['resource_class_id'])
resource_class.update_attributes(
request,
name=data['name'],
service_type=data['service_type'])
return resource_class
except:
redirect = reverse(INDEX_URL)
exceptions.handle(request,
_('Unable to create resource class.'),
redirect=redirect)
return None
def handle(self, request, data):
resource_class = self._update_resource_class_info(request, data)
self._add_resources(request, data, resource_class)
self._add_flavors(request, data, resource_class)
return True

View File

@@ -26,6 +26,7 @@ from openstack_dashboard.api import management
from .flavors.tables import FlavorsTable
from .racks.tables import RacksTable
from .resource_classes.tables import ResourceClassesTable
class RacksTab(tabs.TableTab):
@@ -64,7 +65,24 @@ class FlavorsTab(tabs.TableTab):
return flavors
class ResourceClassesTab(tabs.TableTab):
table_classes = (ResourceClassesTable,)
name = _("Classes")
slug = "resource_classes_tab"
template_name = "horizon/common/_detail_table.html"
#preload = False buggy, checkboxes doesn't work wit table actions
def get_resource_classes_data(self):
try:
resource_classes = management.ResourceClass.list(self.request)
except:
resource_classes = []
exceptions.handle(self.request,
_('Unable to retrieve resource classes list.'))
return resource_classes
class ResourceManagementTabs(tabs.TabGroup):
slug = "resource_management_tabs"
tabs = (FlavorsTab, RacksTab,)
tabs = (FlavorsTab, RacksTab, ResourceClassesTab, )
sticky = True

View File

@@ -6,7 +6,7 @@
{% include "horizon/common/_page_header.html" with title=_("Resource Management") %}
{% endblock page_header %}
{% block infrastructure_main %}
{% block main %}
<div class="row-fluid">
<div class="span12">
{{ tab_group.render }}
@@ -14,4 +14,3 @@
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
<noscript><h3>{{ step }}</h3></noscript>
<table class="table-fixed">
<tbody>
<tr>
<td class="actions">
{% include "horizon/common/_form_fields.html" %}
</td>
<td class="help_text">
{{ step.get_help_text }}
</td>
</tr>
</tbody>
</table>
TODO: flavors_table.render

View File

@@ -0,0 +1,3 @@
<noscript><h3>{{ step }}</h3></noscript>
TODO: resources_table.render

View File

@@ -19,13 +19,13 @@ from django.conf.urls.defaults import patterns, url, include
from .flavors import urls as flavor_urls
from .resource_classes import urls as resource_classes_urls
from .racks import urls as rack_urls
from .views import IndexView
from .views import IndexView
urlpatterns = patterns('',
url(r'^$', IndexView.as_view(), name='index'),
url(r'flavors/', include(flavor_urls, namespace='flavors')),
url(r'resource_classes/', include(resource_classes_urls,
namespace='resource_classes')),
url(r'racks/', include(rack_urls, namespace='racks')),
url(r'resource_classes/',
include(resource_classes_urls, namespace='resource_classes')),
)