From e4b9888a6be74c498bc2bfea981e27fbab13763a Mon Sep 17 00:00:00 2001 From: Hiroaki Kobayashi Date: Wed, 4 Oct 2017 15:42:45 +0900 Subject: [PATCH] Support a host create operation This patch adds a create host workflow to the blazar-dashboard. Partially Implements: blueprint host-operation-with-dashboard Change-Id: Idaf142a947963d3cccaf94e3e611fe7c9bcebf94 --- blazar_dashboard/api/client.py | 6 + blazar_dashboard/content/hosts/tables.py | 10 +- .../content/hosts/templates/hosts/create.html | 7 ++ blazar_dashboard/content/hosts/tests.py | 67 +++++++--- blazar_dashboard/content/hosts/urls.py | 1 + blazar_dashboard/content/hosts/views.py | 8 ++ blazar_dashboard/content/hosts/workflows.py | 118 ++++++++++++++++++ .../test/test_data/blazar_data.py | 14 +++ 8 files changed, 210 insertions(+), 21 deletions(-) create mode 100644 blazar_dashboard/content/hosts/templates/hosts/create.html create mode 100644 blazar_dashboard/content/hosts/workflows.py diff --git a/blazar_dashboard/api/client.py b/blazar_dashboard/api/client.py index 828812c..dad3ccf 100644 --- a/blazar_dashboard/api/client.py +++ b/blazar_dashboard/api/client.py @@ -118,6 +118,12 @@ def host_get(request, host_id): return Host(host) +def host_create(request, name, **kwargs): + """Create a host.""" + host = blazarclient(request).host.create(name, **kwargs) + return Host(host) + + def host_delete(request, host_id): """Delete a host.""" blazarclient(request).host.delete(host_id) diff --git a/blazar_dashboard/content/hosts/tables.py b/blazar_dashboard/content/hosts/tables.py index 85285cd..0ee1be9 100644 --- a/blazar_dashboard/content/hosts/tables.py +++ b/blazar_dashboard/content/hosts/tables.py @@ -18,6 +18,14 @@ from horizon.templatetags import sizeformat from blazar_dashboard import api +class CreateHosts(tables.LinkAction): + name = "create" + verbose_name = _("Create Hosts") + url = "horizon:admin:hosts:create" + classes = ("ajax-modal",) + icon = "plus" + + class DeleteHost(tables.DeleteAction): name = "delete" data_type_singular = _("Host") @@ -57,5 +65,5 @@ class HostsTable(tables.DataTable): class Meta(object): name = "hosts" verbose_name = _("Hosts") - table_actions = (DeleteHost,) + table_actions = (CreateHosts, DeleteHost,) row_actions = (DeleteHost,) diff --git a/blazar_dashboard/content/hosts/templates/hosts/create.html b/blazar_dashboard/content/hosts/templates/hosts/create.html new file mode 100644 index 0000000..6e540d7 --- /dev/null +++ b/blazar_dashboard/content/hosts/templates/hosts/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Hosts" %}{% endblock %} + +{% block main %} + {% include 'horizon/common/_workflow.html' %} +{% endblock %} \ No newline at end of file diff --git a/blazar_dashboard/content/hosts/tests.py b/blazar_dashboard/content/hosts/tests.py index d44796b..5c04af2 100644 --- a/blazar_dashboard/content/hosts/tests.py +++ b/blazar_dashboard/content/hosts/tests.py @@ -13,8 +13,9 @@ from django.core.urlresolvers import reverse from django import http from mox3.mox import IsA +from openstack_dashboard import api -from blazar_dashboard import api +from blazar_dashboard import api as blazar_api from blazar_dashboard.test import helpers as test import logging @@ -24,13 +25,15 @@ INDEX_TEMPLATE = 'admin/hosts/index.html' INDEX_URL = reverse('horizon:admin:hosts:index') DETAIL_TEMPLATE = 'admin/hosts/detail.html' DETAIL_URL_BASE = 'horizon:admin:hosts:detail' +CREATE_URL = reverse('horizon:admin:hosts:create') +CREATE_TEMPLATE = 'admin/hosts/create.html' class HostsTests(test.BaseAdminViewTests): - @test.create_stubs({api.client: ('host_list',)}) + @test.create_stubs({blazar_api.client: ('host_list',)}) def test_index(self): hosts = self.hosts.list() - api.client.host_list(IsA(http.HttpRequest)).AndReturn(hosts) + blazar_api.client.host_list(IsA(http.HttpRequest)).AndReturn(hosts) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -39,9 +42,9 @@ class HostsTests(test.BaseAdminViewTests): self.assertContains(res, 'compute-1') self.assertContains(res, 'compute-2') - @test.create_stubs({api.client: ('host_list',)}) + @test.create_stubs({blazar_api.client: ('host_list',)}) def test_index_no_hosts(self): - api.client.host_list(IsA(http.HttpRequest)).AndReturn(()) + blazar_api.client.host_list(IsA(http.HttpRequest)).AndReturn(()) self.mox.ReplayAll() res = self.client.get(INDEX_URL) @@ -49,9 +52,9 @@ class HostsTests(test.BaseAdminViewTests): self.assertNoMessages(res) self.assertContains(res, 'No items to display') - @test.create_stubs({api.client: ('host_list',)}) + @test.create_stubs({blazar_api.client: ('host_list',)}) def test_index_error(self): - api.client.host_list( + blazar_api.client.host_list( IsA(http.HttpRequest) ).AndRaise(self.exceptions.blazar) self.mox.ReplayAll() @@ -60,11 +63,11 @@ class HostsTests(test.BaseAdminViewTests): self.assertTemplateUsed(res, INDEX_TEMPLATE) self.assertMessageCount(res, error=1) - @test.create_stubs({api.client: ('host_get',)}) + @test.create_stubs({blazar_api.client: ('host_get',)}) def test_host_detail(self): host = self.hosts.get(hypervisor_hostname='compute-1') - api.client.host_get(IsA(http.HttpRequest), - host['id']).AndReturn(host) + blazar_api.client.host_get(IsA(http.HttpRequest), + host['id']).AndReturn(host) self.mox.ReplayAll() res = self.client.get(reverse(DETAIL_URL_BASE, args=[host['id']])) @@ -72,10 +75,10 @@ class HostsTests(test.BaseAdminViewTests): self.assertContains(res, 'compute-1') self.assertContains(res, 'ex1') - @test.create_stubs({api.client: ('host_get',)}) + @test.create_stubs({blazar_api.client: ('host_get',)}) def test_host_detail_error(self): - api.client.host_get(IsA(http.HttpRequest), - 'invalid').AndRaise(self.exceptions.blazar) + blazar_api.client.host_get(IsA(http.HttpRequest), + 'invalid').AndRaise(self.exceptions.blazar) self.mox.ReplayAll() res = self.client.get(reverse(DETAIL_URL_BASE, args=['invalid'])) @@ -83,12 +86,35 @@ class HostsTests(test.BaseAdminViewTests): self.assertMessageCount(error=1) self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api.client: ('host_list', 'host_delete')}) + @test.create_stubs({blazar_api.client: ('host_list', 'host_create',), + api.nova: ('host_list',)}) + def test_create_hosts(self): + blazar_api.client.host_list(IsA(http.HttpRequest) + ).AndReturn([]) + api.nova.host_list(IsA(http.HttpRequest) + ).AndReturn(self.novahosts.list()) + host_names = [h.host_name for h in self.novahosts.list()] + for host_name in host_names: + blazar_api.client.host_create( + IsA(http.HttpRequest), + name=host_name, + ).AndReturn([]) + self.mox.ReplayAll() + form_data = { + 'select_hosts_role_member': host_names + } + + res = self.client.post(CREATE_URL, form_data) + self.assertNoFormErrors(res) + self.assertMessageCount(success=(len(host_names) + 1)) + self.assertRedirectsNoFollow(res, INDEX_URL) + + @test.create_stubs({blazar_api.client: ('host_list', 'host_delete')}) def test_delete_host(self): hosts = self.hosts.list() host = self.hosts.get(hypervisor_hostname='compute-1') - api.client.host_list(IsA(http.HttpRequest)).AndReturn(hosts) - api.client.host_delete(IsA(http.HttpRequest), host['id']) + blazar_api.client.host_list(IsA(http.HttpRequest)).AndReturn(hosts) + blazar_api.client.host_delete(IsA(http.HttpRequest), host['id']) self.mox.ReplayAll() action = 'hosts__delete__%s' % host['id'] @@ -97,13 +123,14 @@ class HostsTests(test.BaseAdminViewTests): self.assertMessageCount(success=1) self.assertRedirectsNoFollow(res, INDEX_URL) - @test.create_stubs({api.client: ('host_list', 'host_delete')}) + @test.create_stubs({blazar_api.client: ('host_list', 'host_delete')}) def test_delete_host_error(self): hosts = self.hosts.list() host = self.hosts.get(hypervisor_hostname='compute-1') - api.client.host_list(IsA(http.HttpRequest)).AndReturn(hosts) - api.client.host_delete(IsA(http.HttpRequest), - host['id']).AndRaise(self.exceptions.blazar) + blazar_api.client.host_list(IsA(http.HttpRequest)).AndReturn(hosts) + blazar_api.client.host_delete( + IsA(http.HttpRequest), + host['id']).AndRaise(self.exceptions.blazar) self.mox.ReplayAll() action = 'hosts__delete__%s' % host['id'] diff --git a/blazar_dashboard/content/hosts/urls.py b/blazar_dashboard/content/hosts/urls.py index f7886cf..34d7a60 100644 --- a/blazar_dashboard/content/hosts/urls.py +++ b/blazar_dashboard/content/hosts/urls.py @@ -17,5 +17,6 @@ from blazar_dashboard.content.hosts import views urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), url(r'^(?P[^/]+)/$', views.DetailView.as_view(), name='detail') ] diff --git a/blazar_dashboard/content/hosts/views.py b/blazar_dashboard/content/hosts/views.py index f8b6989..0b1d46d 100644 --- a/blazar_dashboard/content/hosts/views.py +++ b/blazar_dashboard/content/hosts/views.py @@ -14,10 +14,12 @@ from django.utils.translation import ugettext_lazy as _ from horizon import exceptions from horizon import tables from horizon import tabs +from horizon import workflows from blazar_dashboard import api from blazar_dashboard.content.hosts import tables as project_tables from blazar_dashboard.content.hosts import tabs as project_tabs +from blazar_dashboard.content.hosts import workflows as project_workflows class IndexView(tables.DataTableView): @@ -37,3 +39,9 @@ class IndexView(tables.DataTableView): class DetailView(tabs.TabView): tab_group_class = project_tabs.HostDetailTabs template_name = 'admin/hosts/detail.html' + + +class CreateView(workflows.WorkflowView): + workflow_class = project_workflows.CreateHostsWorkflow + template_name = 'admin/hosts/create.html' + page_title = _("Create Hosts") diff --git a/blazar_dashboard/content/hosts/workflows.py b/blazar_dashboard/content/hosts/workflows.py new file mode 100644 index 0000000..1f56724 --- /dev/null +++ b/blazar_dashboard/content/hosts/workflows.py @@ -0,0 +1,118 @@ +# 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.utils.translation import ugettext_lazy as _ +from horizon import exceptions +from horizon import forms +from horizon import messages +from horizon import workflows +from openstack_dashboard import api + +from blazar_dashboard import api as blazar_api + +LOG = logging.getLogger(__name__) + + +class SelectHostsAction(workflows.MembershipAction): + def __init__(self, request, *args, **kwargs): + super(SelectHostsAction, self).__init__(request, *args, **kwargs) + err_msg = _('Unable to get the available hosts') + + default_role_field_name = self.get_default_role_field_name() + self.fields[default_role_field_name] = forms.CharField(required=False) + self.fields[default_role_field_name].initial = 'member' + + field_name = self.get_member_field_name('member') + self.fields[field_name] = forms.MultipleChoiceField(required=False) + + try: + nova_hosts = api.nova.host_list(request) + blazar_hosts = blazar_api.client.host_list(request) + except Exception: + exceptions.handle(request, err_msg) + + nova_hostnames = [] + for host in nova_hosts: + if (host.host_name not in nova_hostnames + and host.service == u'compute'): + nova_hostnames.append(host.host_name) + + blazar_hostnames = [] + for host in blazar_hosts: + if host.hypervisor_hostname not in blazar_hostnames: + blazar_hostnames.append(host.hypervisor_hostname) + + host_names = list(set(nova_hostnames) - set(blazar_hostnames)) + host_names.sort() + + self.fields[field_name].choices = \ + [(host_name, host_name) for host_name in host_names] + + self.fields[field_name].initial = None + + class Meta(object): + name = _("Select Hosts") + slug = "select_hosts" + + +class AddExtraCapsAction(workflows.Action): + # TODO(hiro-kobayashi): Implement this class + class Meta(object): + name = _("Extra Capabilities") + help_text = _("Not supported yet.") + slug = "add_extra_caps" + + +class SelectHostsStep(workflows.UpdateMembersStep): + action_class = SelectHostsAction + help_text = _("Select hosts to create") + available_list_title = _("All available hosts") + members_list_title = _("Selected hosts") + no_available_text = _("No host found.") + no_members_text = _("No host selected.") + show_roles = False + contributes = ("names",) + + def contribute(self, data, context): + if data: + member_field_name = self.get_member_field_name('member') + context['names'] = data.get(member_field_name, []) + return context + + +class AddExtraCapsStep(workflows.Step): + # TODO(hiro-kobayashi): Implement this class + action_class = AddExtraCapsAction + help_text = _("Add extra capabilities") + show_roles = False + + +class CreateHostsWorkflow(workflows.Workflow): + slug = "create_hosts" + name = _("Create Hosts") + finalize_button_name = _("Create Hosts") + success_url = 'horizon:admin:hosts:index' + default_steps = (SelectHostsStep, AddExtraCapsStep) + + def handle(self, request, context): + try: + for name in context['names']: + blazar_api.client.host_create(request, name=name) + messages.success(request, _('Host %s was successfully ' + 'created.') % name) + except Exception: + exceptions.handle(request, _('Unable to create host.')) + return False + + return True diff --git a/blazar_dashboard/test/test_data/blazar_data.py b/blazar_dashboard/test/test_data/blazar_data.py index bc32d82..b4043b0 100644 --- a/blazar_dashboard/test/test_data/blazar_data.py +++ b/blazar_dashboard/test/test_data/blazar_data.py @@ -168,6 +168,15 @@ host_sample2 = { } +class DummyNovaHost(object): + def __init__(self, host_name, service): + self.host_name = host_name + self.service = service + +novahost_sample1 = DummyNovaHost('compute-1', 'compute') +novahost_sample2 = DummyNovaHost('compute-2', 'compute') + + def data(TEST): TEST.leases = utils.TestDataContainer() @@ -178,3 +187,8 @@ def data(TEST): TEST.hosts.add(api.client.Host(host_sample1)) TEST.hosts.add(api.client.Host(host_sample2)) + + TEST.novahosts = utils.TestDataContainer() + + TEST.novahosts.add(novahost_sample1) + TEST.novahosts.add(novahost_sample2)