diff options
Diffstat (limited to 'tuskar_ui/infrastructure/flavors')
-rw-r--r-- | tuskar_ui/infrastructure/flavors/__init__.py | 0 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/panel.py | 27 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/tables.py | 114 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/tabs.py | 140 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/templates/flavors/create.html | 11 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/templates/flavors/details.html | 35 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/templates/flavors/index.html | 16 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/tests.py | 219 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/urls.py | 28 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/views.py | 87 | ||||
-rw-r--r-- | tuskar_ui/infrastructure/flavors/workflows.py | 106 |
11 files changed, 783 insertions, 0 deletions
diff --git a/tuskar_ui/infrastructure/flavors/__init__.py b/tuskar_ui/infrastructure/flavors/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/__init__.py diff --git a/tuskar_ui/infrastructure/flavors/panel.py b/tuskar_ui/infrastructure/flavors/panel.py new file mode 100644 index 00000000..ffdfdc7f --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/panel.py @@ -0,0 +1,27 @@ +# -*- coding: utf8 -*- +# +# 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 _ + +import horizon + +from tuskar_ui.infrastructure import dashboard + + +class Flavors(horizon.Panel): + name = _("Flavors") + slug = "flavors" + + +dashboard.Infrastructure.register(Flavors) diff --git a/tuskar_ui/infrastructure/flavors/tables.py b/tuskar_ui/infrastructure/flavors/tables.py new file mode 100644 index 00000000..29b9e709 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/tables.py @@ -0,0 +1,114 @@ +# -*- coding: utf8 -*- +# +# 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 tables + +from openstack_dashboard.dashboards.admin.flavors \ + import tables as flavor_tables + +from tuskar_ui import api + + +class CreateFlavor(flavor_tables.CreateFlavor): + verbose_name = _("New Flavor") + url = "horizon:infrastructure:flavors:create" + + +class CreateSuggestedFlavor(CreateFlavor): + verbose_name = _("Create") + + +class DeleteFlavor(flavor_tables.DeleteFlavor): + + def __init__(self, **kwargs): + super(DeleteFlavor, self).__init__(**kwargs) + # NOTE(dtantsur): setting class attributes doesn't work + # probably due to metaclass magic in actions + self.data_type_singular = _("Flavor") + self.data_type_plural = _("Flavors") + + def allowed(self, request, datum=None): + """Check that action is allowed on flavor + + This is overridden method from horizon.tables.BaseAction. + + :param datum: flavor we're operating on + :type datum: tuskar_ui.api.Flavor + """ + if datum is not None: + deployed_flavors = api.Flavor.list_deployed_ids( + request, _error_default=None) + if deployed_flavors is None or datum.id in deployed_flavors: + return False + return super(DeleteFlavor, self).allowed(request, datum) + + +class FlavorsTable(tables.DataTable): + name = tables.Column('name', verbose_name=_('Flavor'), + link="horizon:infrastructure:flavors:details") + arch = tables.Column('cpu_arch', verbose_name=_('Architecture')) + vcpus = tables.Column('vcpus', verbose_name=_('CPUs')) + ram = tables.Column(flavor_tables.get_size, + verbose_name=_('Memory'), + attrs={'data-type': 'size'}) + disk = tables.Column(flavor_tables.get_disk_size, + verbose_name=_('Disk'), + attrs={'data-type': 'size'}) + # FIXME(dtantsur): would be much better to have names here + kernel_image_id = tables.Column('kernel_image_id', + verbose_name=_('Deploy Kernel Image ID')) + ramdisk_image_id = tables.Column('ramdisk_image_id', + verbose_name=_('Deploy Ramdisk Image ID')) + + class Meta: + name = "flavors" + verbose_name = _("Flavors") + table_actions = (CreateFlavor, + DeleteFlavor, + flavor_tables.FlavorFilterAction) + row_actions = (DeleteFlavor,) + + +class FlavorRolesTable(tables.DataTable): + name = tables.Column('name', verbose_name=_('Role Name')) + + def __init__(self, request, *args, **kwargs): + # TODO(dtantsur): support multiple overclouds + try: + overcloud = api.Overcloud.get_the_overcloud(request) + except Exception: + count_getter = lambda role: _("Not deployed") + else: + count_getter = overcloud.resources_count + self._columns['count'] = tables.Column( + count_getter, + verbose_name=_("Instances Count") + ) + super(FlavorRolesTable, self).__init__(request, *args, **kwargs) + + +class FlavorSuggestionsTable(tables.DataTable): + arch = tables.Column('cpu_arch', verbose_name=_('Architecture')) + vcpus = tables.Column('vcpus', verbose_name=_('CPUs')) + ram = tables.Column(flavor_tables.get_size, verbose_name=_('Memory'), + attrs={'data-type': 'size'}) + disk = tables.Column(flavor_tables.get_disk_size, + verbose_name=_('Disk'), attrs={'data-type': 'size'}) + + class Meta: + name = "flavor_suggestions" + verbose_name = _("Flavor Suggestions") + table_actions = () + row_actions = (CreateSuggestedFlavor,) diff --git a/tuskar_ui/infrastructure/flavors/tabs.py b/tuskar_ui/infrastructure/flavors/tabs.py new file mode 100644 index 00000000..772c40a3 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/tabs.py @@ -0,0 +1,140 @@ +# -*- coding: utf8 -*- +# +# 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 _ +import horizon.tabs + +from tuskar_ui import api +from tuskar_ui.infrastructure.flavors import tables + + +def _get_unmatched_suggestions(request): + unmatched_suggestions = [] + flavor_suggestions = [FlavorSuggestion.from_flavor(flavor) + for flavor in api.Flavor.list(request)] + for node in api.Node.list(request): + node_suggestion = FlavorSuggestion.from_node(node) + for flavor_suggestion in flavor_suggestions: + if flavor_suggestion == node_suggestion: + break + else: + unmatched_suggestions.append(node_suggestion) + return unmatched_suggestions + + +def get_flavor_suggestions(request): + return set(_get_unmatched_suggestions(request)) + + +class FlavorsTab(horizon.tabs.TableTab): + name = _("Flavors") + slug = 'flavors' + table_classes = (tables.FlavorsTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def get_flavors_data(self): + flavors = api.Flavor.list(self.request) + flavors.sort(key=lambda np: (np.vcpus, np.ram, np.disk)) + return flavors + + +class FlavorSuggestion(object): + """Describe node parameters in a way that is easy to compare.""" + + def __init__(self, vcpus=None, ram=None, disk=None, cpu_arch=None, + ram_bytes=None, disk_bytes=None, node_id=None): + self.vcpus = vcpus + self.ram_bytes = ram_bytes or ram * 1024 * 1024 or 0 + self.disk_bytes = disk_bytes or disk * 1024 * 1024 * 1024 or 0 + self.cpu_arch = cpu_arch + self.id = node_id + + @classmethod + def from_node(cls, node): + return cls( + node_id=node.id, + vcpus=int(node.properties['cpu']), + ram_bytes=int(node.properties['ram']), + disk_bytes=int(node.properties['local_disk']), + # TODO(rdopieralski) Add architecture when available. + ) + + @classmethod + def from_flavor(cls, flavor): + return cls( + vcpus=flavor.vcpus, + ram_bytes=flavor.ram_bytes, + disk_bytes=flavor.disk_bytes, + # TODO(rdopieralski) Add architecture when available. + ) + + @property + def name(self): + return '%s-%s-%s-%s' % ( + self.vcpus or '0', + self.cpu_arch or '', + self.ram or '0', + self.disk or '0', + ) + + @property + def ram(self): + return self.ram_bytes / 1024 / 1024 + + @property + def disk(self): + return self.disk_bytes / 1024 / 1024 / 1024 + + def __hash__(self): + return self.name.__hash__() + + def __eq__(self, other): + return self.name == other.name + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return ( + '%s(vcpus=%r, ram_bytes=%r, disk_bytes=%r, ' + 'cpu_arch=%r, node_id=%r)' % ( + self.__class__.__name__, + self.vcpus, + self.ram_bytes, + self.disk_bytes, + self.cpu_arch, + self.id, + ) + ) + + +class FlavorSuggestionsTab(horizon.tabs.TableTab): + name = _("Flavor Suggestions") + slug = 'flavor_suggestions' + table_classes = (tables.FlavorSuggestionsTable,) + template_name = ("horizon/common/_detail_table.html") + preload = False + + def get_flavor_suggestions_data(self): + return list(get_flavor_suggestions(self.request)) + + +class FlavorTabs(horizon.tabs.TabGroup): + slug = 'flavor_tabs' + tabs = ( + FlavorsTab, + FlavorSuggestionsTab, + ) + sticky = True diff --git a/tuskar_ui/infrastructure/flavors/templates/flavors/create.html b/tuskar_ui/infrastructure/flavors/templates/flavors/create.html new file mode 100644 index 00000000..4fedb05a --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/templates/flavors/create.html @@ -0,0 +1,11 @@ +{% extends 'infrastructure/base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Flavor" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create Flavor") %} +{% endblock page_header %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} diff --git a/tuskar_ui/infrastructure/flavors/templates/flavors/details.html b/tuskar_ui/infrastructure/flavors/templates/flavors/details.html new file mode 100644 index 00000000..a6331c18 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/templates/flavors/details.html @@ -0,0 +1,35 @@ +{% extends 'infrastructure/base.html' %} +{% load i18n %} +{% block title %}{% trans 'Flavor Details' %}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_page_header.html' with title=_('Flavor Details') %} +{% endblock page_header %} + +{% block main %} +<div class="row-fluid"> + <div class="span12"> + <h4>{% trans "Hardware Info" %}</h4> + <dl class="clearfix"> + <dt>{% trans "Architecture" %}</dt> + <dd>{{ flavor.cpu_arch|default:"—" }}</dd> + <dt>{% trans "CPUs" %}</dt> + <dd>{{ flavor.vcpus|default:"—" }}</dd> + <dt>{% trans "Memory" %}</dt> + <dd>{{ flavor.ram_bytes|filesizeformat|default:"—" }}</dd> + <dt>{% trans "Disk" %}</dt> + <dd>{{ flavor.disk_bytes|filesizeformat|default:"—" }}</dd> + </dl> + <h4>{% trans "Deploy Images" %}</h4> + <dl class="clearfix"> + <dt>{% trans "Kernel" %}</dt> + <dd>{{ kernel_image.name|default:"—" }}</dd> + <dt>{% trans "Ramdisk" %}</dt> + <dd>{{ ramdisk_image.name|default:"—" }}</dd> + </dl> + <h4>{% trans "Overcloud Roles" %}</h4> + {{ table.render }} + </div> +</div> + +{% endblock %} diff --git a/tuskar_ui/infrastructure/flavors/templates/flavors/index.html b/tuskar_ui/infrastructure/flavors/templates/flavors/index.html new file mode 100644 index 00000000..663a6237 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/templates/flavors/index.html @@ -0,0 +1,16 @@ +{% extends 'infrastructure/base.html' %} +{% load i18n %} +{% load url from future %} +{% block title %}{% trans 'Flavors' %}{% endblock %} + +{% block page_header %} + {% include 'horizon/common/_domain_page_header.html' with title=_('Flavors') %} +{% endblock page_header %} + +{% block main %} +<div class="row-fluid"> + <div class="span12"> + {{ tab_group.render }} + </div> +</div> +{% endblock %} diff --git a/tuskar_ui/infrastructure/flavors/tests.py b/tuskar_ui/infrastructure/flavors/tests.py new file mode 100644 index 00000000..1b43d125 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/tests.py @@ -0,0 +1,219 @@ +# -*- coding: utf8 -*- +# +# 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 contextlib + +from django.core import urlresolvers + +from mock import patch, call # noqa + +from novaclient.v1_1 import servers + +from horizon import exceptions +from openstack_dashboard.test.test_data import utils +from tuskar_ui import api +from tuskar_ui.test import helpers as test +from tuskar_ui.test.test_data import tuskar_data + + +TEST_DATA = utils.TestDataContainer() +tuskar_data.data(TEST_DATA) +INDEX_URL = urlresolvers.reverse( + 'horizon:infrastructure:flavors:index') +CREATE_URL = urlresolvers.reverse( + 'horizon:infrastructure:flavors:create') +DETAILS_VIEW = 'horizon:infrastructure:flavors:details' + + +@contextlib.contextmanager +def _prepare_create(): + flavor = TEST_DATA.novaclient_flavors.first() + all_flavors = TEST_DATA.novaclient_flavors.list() + images = TEST_DATA.glanceclient_images.list() + data = {'name': 'foobar', + 'vcpus': 3, + 'memory_mb': 1024, + 'disk_gb': 40, + 'arch': 'amd64', + 'kernel_image_id': images[0].id, + 'ramdisk_image_id': images[1].id} + with contextlib.nested( + patch('tuskar_ui.api.Flavor.create', + return_value=flavor), + patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=(TEST_DATA.glanceclient_images.list(), False)), + # Inherited code calls this directly + patch('openstack_dashboard.api.nova.flavor_list', + return_value=all_flavors), + ) as mocks: + yield mocks[0], data + + +class FlavorsTest(test.BaseAdminViewTests): + + def test_index(self): + with contextlib.nested( + patch('openstack_dashboard.api.nova.flavor_list', + return_value=TEST_DATA.novaclient_flavors.list()), + patch('openstack_dashboard.api.nova.server_list', + return_value=([], False)), + ) as (flavors_mock, servers_mock): + res = self.client.get(INDEX_URL) + self.assertEqual(flavors_mock.call_count, 1) + self.assertEqual(servers_mock.call_count, 1) + + self.assertTemplateUsed(res, + 'infrastructure/flavors/index.html') + + def test_index_recoverable_failure(self): + with patch('openstack_dashboard.api.nova.flavor_list', + side_effect=exceptions.Conflict): + self.client.get(INDEX_URL) + # FIXME(dtantsur): I expected the following to work: + # self.assertMessageCount(error=1, warning=0) + + def test_create_get(self): + with patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=([], False)) as mock: + res = self.client.get(CREATE_URL) + self.assertEqual(mock.call_count, 2) + self.assertTemplateUsed(res, + 'infrastructure/flavors/create.html') + + def test_create_get_recoverable_failure(self): + with patch('openstack_dashboard.api.glance.image_list_detailed', + side_effect=exceptions.Conflict): + self.client.get(CREATE_URL) + self.assertMessageCount(error=1, warning=0) + + def test_create_post_ok(self): + images = TEST_DATA.glanceclient_images.list() + with _prepare_create() as (create_mock, data): + res = self.client.post(CREATE_URL, data) + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + request = create_mock.call_args_list[0][0][0] + self.assertListEqual(create_mock.call_args_list, [ + call(request, name=u'foobar', memory=1024, vcpus=3, disk=40, + cpu_arch='amd64', kernel_image_id=images[0].id, + ramdisk_image_id=images[1].id) + ]) + + def test_create_post_name_exists(self): + flavor = TEST_DATA.novaclient_flavors.first() + with _prepare_create() as (create_mock, data): + data['name'] = flavor.name + res = self.client.post(CREATE_URL, data) + self.assertFormErrors(res) + + def test_delete_ok(self): + flavors = TEST_DATA.novaclient_flavors.list() + data = {'action': 'flavors__delete', + 'object_ids': [flavors[0].id, flavors[1].id]} + with contextlib.nested( + patch('openstack_dashboard.api.nova.flavor_delete'), + patch('openstack_dashboard.api.nova.server_list', + return_value=([], False)), + patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=([], False)), + patch('openstack_dashboard.api.nova.flavor_list', + return_value=TEST_DATA.novaclient_flavors.list()) + ) as (delete_mock, server_list_mock, glance_mock, flavors_mock): + res = self.client.post(INDEX_URL, data) + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertEqual(delete_mock.call_count, 2) + self.assertEqual(server_list_mock.call_count, 1) + + def test_delete_deployed(self): + flavors = TEST_DATA.novaclient_flavors.list() + server = servers.Server( + servers.ServerManager(None), + {'id': 'aa', + 'name': 'Compute', + 'image': {'id': 1}, + 'status': 'ACTIVE', + 'flavor': {'id': flavors[0].id}} + ) + data = {'action': 'flavors__delete', + 'object_ids': [flavors[0].id, flavors[1].id]} + with contextlib.nested( + patch('openstack_dashboard.api.nova.flavor_delete'), + patch('openstack_dashboard.api.nova.server_list', + return_value=([server], False)), + patch('openstack_dashboard.api.glance.image_list_detailed', + return_value=([], False)), + patch('openstack_dashboard.api.nova.flavor_list', + return_value=TEST_DATA.novaclient_flavors.list()) + ) as (delete_mock, server_list_mock, glance_mock, flavors_mock): + res = self.client.post(INDEX_URL, data) + self.assertMessageCount(error=1, warning=0) + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + self.assertEqual(delete_mock.call_count, 1) + self.assertEqual(server_list_mock.call_count, 1) + + def test_details_no_overcloud(self): + flavor = api.Flavor(TEST_DATA.novaclient_flavors.first()) + images = TEST_DATA.glanceclient_images.list()[:2] + roles = TEST_DATA.tuskarclient_overcloud_roles.list() + roles[0].flavor_id = flavor.id + with contextlib.nested( + patch('openstack_dashboard.api.glance.image_get', + side_effect=images), + patch('tuskar_ui.api.Flavor.get', + return_value=flavor), + patch('tuskar_ui.api.OvercloudRole.list', + return_value=roles), + patch('tuskar_ui.api.Overcloud.get_the_overcloud', + side_effect=Exception) + ) as (image_mock, get_mock, roles_mock, overcloud_mock): + res = self.client.get(urlresolvers.reverse(DETAILS_VIEW, + args=(flavor.id,))) + self.assertEqual(image_mock.call_count, 1) # memoized + self.assertEqual(get_mock.call_count, 1) + self.assertEqual(roles_mock.call_count, 1) + self.assertEqual(overcloud_mock.call_count, 1) + self.assertTemplateUsed(res, + 'infrastructure/flavors/details.html') + + def test_details(self): + flavor = api.Flavor(TEST_DATA.novaclient_flavors.first()) + images = TEST_DATA.glanceclient_images.list()[:2] + roles = TEST_DATA.tuskarclient_overcloud_roles.list() + roles[0].flavor_id = flavor.id + overcloud = api.Overcloud(TEST_DATA.tuskarclient_overclouds.first()) + with contextlib.nested( + patch('openstack_dashboard.api.glance.image_get', + side_effect=images), + patch('tuskar_ui.api.Flavor.get', + return_value=flavor), + patch('tuskar_ui.api.OvercloudRole.list', + return_value=roles), + patch('tuskar_ui.api.Overcloud.get_the_overcloud', + return_value=overcloud), + # __name__ is required for horizon.tables + patch('tuskar_ui.api.Overcloud.resources_count', + return_value=42, __name__='') + ) as (image_mock, get_mock, roles_mock, overcloud_mock, count_mock): + res = self.client.get(urlresolvers.reverse(DETAILS_VIEW, + args=(flavor.id,))) + self.assertEqual(image_mock.call_count, 1) # memoized + self.assertEqual(get_mock.call_count, 1) + self.assertEqual(roles_mock.call_count, 1) + self.assertEqual(overcloud_mock.call_count, 1) + self.assertEqual(count_mock.call_count, 1) + self.assertListEqual(count_mock.call_args_list, [call(roles[0])]) + self.assertTemplateUsed(res, + 'infrastructure/flavors/details.html') diff --git a/tuskar_ui/infrastructure/flavors/urls.py b/tuskar_ui/infrastructure/flavors/urls.py new file mode 100644 index 00000000..7f78dc45 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/urls.py @@ -0,0 +1,28 @@ +# -*- coding: utf8 -*- +# +# 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.conf import urls + +from tuskar_ui.infrastructure.flavors import views + + +urlpatterns = urls.patterns( + 'tuskar_ui.infrastructure.flavors.views', + urls.url(r'^$', views.IndexView.as_view(), name='index'), + urls.url(r'^create/(?P<suggestion_id>[^/]+)$', views.CreateView.as_view(), + name='create'), + urls.url(r'^create/$', views.CreateView.as_view(), name='create'), + urls.url(r'^(?P<flavor_id>[^/]+)/$', views.DetailView.as_view(), + name='details'), +) diff --git a/tuskar_ui/infrastructure/flavors/views.py b/tuskar_ui/infrastructure/flavors/views.py new file mode 100644 index 00000000..ed8dc3d5 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/views.py @@ -0,0 +1,87 @@ +# -*- coding: utf8 -*- +# +# 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_lazy +from django.utils.translation import ugettext_lazy as _ + +import horizon.exceptions +import horizon.tables +import horizon.tabs +import horizon.workflows + +import tuskar_ui.api +from tuskar_ui.infrastructure.flavors import tables +from tuskar_ui.infrastructure.flavors import tabs +from tuskar_ui.infrastructure.flavors import workflows + + +def image_get(request, image_id, error_message): + # TODO(dtantsur): there should be generic way to handle exceptions + try: + return tuskar_ui.api.image_get(request, image_id) + except Exception: + horizon.exceptions.handle(request, error_message) + + +class IndexView(horizon.tabs.TabbedTableView): + tab_group_class = tabs.FlavorTabs + template_name = 'infrastructure/flavors/index.html' + + +class CreateView(horizon.workflows.WorkflowView): + workflow_class = workflows.CreateFlavor + template_name = 'infrastructure/flavors/create.html' + + def get_initial(self): + suggestion_id = self.kwargs.get('suggestion_id') + if not suggestion_id: + return super(CreateView, self).get_initial() + node = tuskar_ui.api.Node.get(self.request, suggestion_id) + suggestion = tabs.FlavorSuggestion.from_node(node) + return { + 'name': suggestion.name, + 'vcpus': suggestion.vcpus, + 'memory_mb': suggestion.ram, + 'disk_gb': suggestion.disk, + 'arch': suggestion.cpu_arch, + } + + +class DetailView(horizon.tables.DataTableView): + table_class = tables.FlavorRolesTable + template_name = 'infrastructure/flavors/details.html' + error_redirect = reverse_lazy('horizon:infrastructure:flavors:index') + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + context['flavor'] = tuskar_ui.api.Flavor.get( + self.request, + kwargs.get('flavor_id'), + _error_redirect=self.error_redirect + ) + context['kernel_image'] = image_get( + self.request, + context['flavor'].kernel_image_id, + error_message=_("Cannot get kernel image details") + ) + context['ramdisk_image'] = image_get( + self.request, + context['flavor'].ramdisk_image_id, + error_message=_("Cannot get ramdisk image details") + ) + return context + + def get_data(self): + return [role for role in tuskar_ui.api.OvercloudRole.list(self.request) + if role.flavor_id == str(self.kwargs.get('flavor_id'))] diff --git a/tuskar_ui/infrastructure/flavors/workflows.py b/tuskar_ui/infrastructure/flavors/workflows.py new file mode 100644 index 00000000..0ea91c04 --- /dev/null +++ b/tuskar_ui/infrastructure/flavors/workflows.py @@ -0,0 +1,106 @@ +# -*- coding: utf8 -*- +# +# 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.forms import fields +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import workflows + +from openstack_dashboard.api import glance +from openstack_dashboard.dashboards.admin.flavors \ + import workflows as flavor_workflows +from tuskar_ui import api + + +class CreateFlavorAction(flavor_workflows.CreateFlavorInfoAction): + arch = fields.ChoiceField(choices=(('i386', 'i386'), ('amd64', 'amd64')), + label=_("Architecture")) + kernel_image_id = fields.ChoiceField(choices=(), + label=_("Deploy Kernel Image")) + ramdisk_image_id = fields.ChoiceField(choices=(), + label=_("Deploy Ramdisk Image")) + + def __init__(self, *args, **kwrds): + super(CreateFlavorAction, self).__init__(*args, **kwrds) + try: + kernel_images = glance.image_list_detailed( + self.request, + filters={'disk_format': 'aki'} + )[0] + ramdisk_images = glance.image_list_detailed( + self.request, + filters={'disk_format': 'ari'} + )[0] + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve images list.')) + kernel_images = [] + ramdisk_images = [] + self.fields['kernel_image_id'].choices = [(img.id, img.name) + for img in kernel_images] + self.fields['ramdisk_image_id'].choices = [(img.id, img.name) + for img in ramdisk_images] + # Delete what is not applicable to hardware + del self.fields['eph_gb'] + del self.fields['swap_mb'] + # Alter user-visible strings + self.fields['vcpus'].label = _("CPUs") + self.fields['disk_gb'].label = _("Disk GB") + # No idea why Horizon exposes this database detail + del self.fields['flavor_id'] + + class Meta: + name = _("Flavor") + # FIXME(dtantsur): maybe better help text? + help_text = _("From here you can create a new " + "flavor to organize instance resources.") + + +class CreateFlavorStep(workflows.Step): + action_class = CreateFlavorAction + contributes = ("name", + "vcpus", + "memory_mb", + "disk_gb", + "arch", + "kernel_image_id", + "ramdisk_image_id") + + +class CreateFlavor(flavor_workflows.CreateFlavor): + slug = "create_flavor" + name = _("Create Flavor") + finalize_button_name = _("Create Flavor") + success_message = _('Created new flavor "%s".') + failure_message = _('Unable to create flavor "%s".') + success_url = "horizon:infrastructure:flavors:index" + default_steps = (CreateFlavorStep,) + + def handle(self, request, data): + try: + self.object = api.Flavor.create( + request, + name=data['name'], + memory=data['memory_mb'], + vcpus=data['vcpus'], + disk=data['disk_gb'], + cpu_arch=data['arch'], + kernel_image_id=data['kernel_image_id'], + ramdisk_image_id=data['ramdisk_image_id'] + ) + except Exception: + exceptions.handle(request, _("Unable to create flavor")) + return False + return True |