From a83abbd2b5ac8ac00b0d5bdc2a9474c6d3f0093a Mon Sep 17 00:00:00 2001 From: nirajsingh Date: Tue, 30 Jan 2018 17:09:37 +0530 Subject: [PATCH] Add host panel Added host panel and implemented add and list host functionality. Also added test cases that actually not covering the line of code but tested the add and list host functionally. Partial-Implements: blueprint masakari-dashboard Change-Id: I756ab397bd9c84a4520c4ed12a576832784ab41f --- masakaridashboard/api/api.py | 23 ++++++ masakaridashboard/dashboard.py | 2 +- masakaridashboard/hosts/__init__.py | 0 masakaridashboard/hosts/panel.py | 27 +++++++ masakaridashboard/hosts/tables.py | 53 ++++++++++++++ .../hosts/templates/hosts/index.html | 7 ++ masakaridashboard/hosts/tests.py | 72 +++++++++++++++++++ masakaridashboard/hosts/urls.py | 22 ++++++ masakaridashboard/hosts/views.py | 47 ++++++++++++ masakaridashboard/segments/forms.py | 67 +++++++++++++++++ masakaridashboard/segments/tables.py | 14 +++- .../segments/templates/segments/_addhost.html | 8 +++ .../segments/templates/segments/addhost.html | 7 ++ masakaridashboard/segments/urls.py | 1 + masakaridashboard/segments/views.py | 57 +++++++++++++++ .../test/test_data/masakari_data.py | 21 ++++++ 16 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 masakaridashboard/hosts/__init__.py create mode 100644 masakaridashboard/hosts/panel.py create mode 100644 masakaridashboard/hosts/tables.py create mode 100644 masakaridashboard/hosts/templates/hosts/index.html create mode 100644 masakaridashboard/hosts/tests.py create mode 100644 masakaridashboard/hosts/urls.py create mode 100644 masakaridashboard/hosts/views.py create mode 100644 masakaridashboard/segments/templates/segments/_addhost.html create mode 100644 masakaridashboard/segments/templates/segments/addhost.html diff --git a/masakaridashboard/api/api.py b/masakaridashboard/api/api.py index fb031db..bba4094 100644 --- a/masakaridashboard/api/api.py +++ b/masakaridashboard/api/api.py @@ -23,6 +23,7 @@ from horizon.utils import memoized from keystoneauth1.identity.generic import token from keystoneauth1 import session as ks_session from openstack import connection +from openstack_dashboard.api import nova as nova_api from masakaridashboard.handle_errors import handle_errors @@ -40,6 +41,10 @@ def openstack_connection(request): return conn.instance_ha +def get_hypervisor_list(request): + return nova_api.hypervisor_list(request) + + @handle_errors(_("Unable to retrieve segments"), []) def get_segment_list(request, marker='', paginate=False, filters=None): """Returns segments as per page size.""" @@ -123,3 +128,21 @@ def segment_update(request, segment_id, fields_to_update): """Update segment.""" return openstack_connection(request).update_segment( segment_id, **fields_to_update) + + +def create_host(request, data): + """Create Host.""" + attrs = {'name': data['name'], + 'reserved': data['reserved'], + 'type': data['type'], + 'control_attributes': data['control_attributes'], + 'on_maintenance': data['on_maintenance']} + + return openstack_connection(request).create_host( + data['segment_id'], **attrs) + + +@handle_errors(_("Unable to get host list"), []) +def get_host_list(request, segment_id, filters): + """Returns host list.""" + return openstack_connection(request).hosts(segment_id, **filters) diff --git a/masakaridashboard/dashboard.py b/masakaridashboard/dashboard.py index 3148c8b..9142a88 100644 --- a/masakaridashboard/dashboard.py +++ b/masakaridashboard/dashboard.py @@ -23,7 +23,7 @@ from masakaridashboard.default import panel class MasakariDashboard(horizon.Dashboard): slug = "masakaridashboard" name = _("Instance-ha") - panels = ('default', 'segments') + panels = ('default', 'segments', 'hosts') default_panel = 'default' policy_rules = (('instance-ha', 'context_is_admin'),) diff --git a/masakaridashboard/hosts/__init__.py b/masakaridashboard/hosts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/masakaridashboard/hosts/panel.py b/masakaridashboard/hosts/panel.py new file mode 100644 index 0000000..0ed426e --- /dev/null +++ b/masakaridashboard/hosts/panel.py @@ -0,0 +1,27 @@ +# Copyright (c) 2018 NTT DATA +# +# 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 masakaridashboard import dashboard + + +class Hosts(horizon.Panel): + name = _("Hosts") + slug = 'hosts' + + +dashboard.MasakariDashboard.register(Hosts) diff --git a/masakaridashboard/hosts/tables.py b/masakaridashboard/hosts/tables.py new file mode 100644 index 0000000..0f8bcd8 --- /dev/null +++ b/masakaridashboard/hosts/tables.py @@ -0,0 +1,53 @@ +# Copyright (c) 2018 NTT DATA +# +# 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 + +HOST_FILTER_CHOICES = ( + ('failover_segment_id', _("Segment Id ="), True), + ('type', _("Type ="), True), + ('on_maintenance', _("On Maintenance ="), True), + ('reserved', _("Reserved ="), True), +) + + +class HostFilterAction(tables.FilterAction): + filter_type = "server" + filter_choices = HOST_FILTER_CHOICES + + +class HostTable(tables.DataTable): + + name = tables.Column('name', verbose_name=_("Name")) + uuid = tables.Column('uuid', verbose_name=_("UUID")) + reserved = tables.Column( + 'reserved', verbose_name=_("Reserved")) + type = tables.Column( + 'type', verbose_name=_("Type")) + control_attributes = tables.Column( + 'control_attributes', verbose_name=_( + "Control Attribute"), truncate=40) + on_maintenance = tables.Column( + 'on_maintenance', verbose_name=_("On Maintenance")) + failover_segment_id = tables.Column( + 'failover_segment_id', verbose_name=_("Failover Segment")) + + def get_object_id(self, datum): + return datum.uuid + ',' + datum.failover_segment_id + + class Meta(object): + name = "host" + verbose_name = _("Host") + table_actions = (HostFilterAction,) diff --git a/masakaridashboard/hosts/templates/hosts/index.html b/masakaridashboard/hosts/templates/hosts/index.html new file mode 100644 index 0000000..ed8a0bc --- /dev/null +++ b/masakaridashboard/hosts/templates/hosts/index.html @@ -0,0 +1,7 @@ +{% extends 'masakaridashboard/default/table.html' %} +{% load i18n %} +{% block title %}{% trans "Hosts" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Hosts") %} +{% endblock page_header %} diff --git a/masakaridashboard/hosts/tests.py b/masakaridashboard/hosts/tests.py new file mode 100644 index 0000000..5784097 --- /dev/null +++ b/masakaridashboard/hosts/tests.py @@ -0,0 +1,72 @@ +# Copyright (C) 2018 NTT DATA +# 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. + +from django.core.urlresolvers import reverse +import mock + +from masakaridashboard.test import helpers as test + + +INDEX_URL = reverse('horizon:masakaridashboard:hosts:index') + + +class HostTest(test.TestCase): + + def test_index(self): + hosts = self.masakari_host.list() + segments = self.masakari_segment.list() + with mock.patch('masakaridashboard.api.api.segment_list', + return_value=segments), mock.patch( + 'masakaridashboard.api.api.get_segment', + return_value=segments[0]), mock.patch( + 'masakaridashboard.api.api.get_host_list', + return_value=hosts): + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, 'masakaridashboard/hosts/index.html') + + def test_create_post(self): + segment = self.masakari_segment.list() + host = self.masakari_host.list()[0] + hypervisors = self.hypervisors.list() + create_url = reverse('horizon:masakaridashboard:segments:addhost', + args=[segment[0].uuid]) + form_data = { + 'segment_id': host.failover_segment_id, + 'segment_name': segment[0].name, + 'name': host.name, + 'type': host.type, + 'reserved': '1', + 'control_attributes': host.control_attributes, + 'on_maintenance': '0' + } + with mock.patch('masakaridashboard.api.api.segment_list', + return_value=segment), mock.patch( + 'masakaridashboard.api.api.get_host_list', + return_value=[]), mock.patch( + 'masakaridashboard.api.api.get_hypervisor_list', + return_value=hypervisors), mock.patch( + 'masakaridashboard.api.api.get_segment', + return_value=segment[0]), mock.patch( + 'masakaridashboard.api.api.create_host', + return_value=host) as mocked_create: + res = self.client.post(create_url, form_data) + self.assertNoFormErrors(res) + self.assertEqual(res.status_code, 302) + self.assertRedirectsNoFollow(res, INDEX_URL) + + mocked_create.assert_called_once_with( + mock.ANY, + form_data + ) diff --git a/masakaridashboard/hosts/urls.py b/masakaridashboard/hosts/urls.py new file mode 100644 index 0000000..3ba7aa3 --- /dev/null +++ b/masakaridashboard/hosts/urls.py @@ -0,0 +1,22 @@ +# Copyright (c) 2018 NTT DATA +# +# 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.urls import url + +from masakaridashboard.hosts import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), +] diff --git a/masakaridashboard/hosts/views.py b/masakaridashboard/hosts/views.py new file mode 100644 index 0000000..c2e7b28 --- /dev/null +++ b/masakaridashboard/hosts/views.py @@ -0,0 +1,47 @@ +# Copyright (c) 2018 NTT DATA +# +# 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 settings +from horizon import tables + +from masakaridashboard.api import api +from masakaridashboard.hosts import tables as masakari_tab + + +class IndexView(tables.DataTableView): + table_class = masakari_tab.HostTable + template_name = 'masakaridashboard/hosts/index.html' + + def needs_filter_first(self, table): + return self._needs_filter_first + + def get_data(self): + segments = api.segment_list(self.request) + host_list = [] + filters = self.get_filters() + self._needs_filter_first = True + + filter_first = getattr(settings, 'FILTER_DATA_FIRST', {}) + if filter_first.get('masakaridashboard.hosts', False) and len( + filters) == 0: + self._needs_filter_first = True + self._more = False + return host_list + + for segment in segments: + host_gen = api.get_host_list(self.request, segment.uuid, filters) + for item in host_gen: + host_list.append(item) + + return host_list diff --git a/masakaridashboard/segments/forms.py b/masakaridashboard/segments/forms.py index 876a8f3..07e848d 100644 --- a/masakaridashboard/segments/forms.py +++ b/masakaridashboard/segments/forms.py @@ -106,3 +106,70 @@ class UpdateForm(forms.SelfHandlingForm): exceptions.handle(request, msg, redirect=redirect) return True + + +class AddHostForm(forms.SelfHandlingForm): + + segment_id = forms.CharField(widget=forms.HiddenInput()) + segment_name = forms.CharField( + label=_('Segment Name'), widget=forms.TextInput( + attrs={'readonly': 'readonly'}), required=False) + name = forms.ChoiceField(label=_('Host Name'), + choices=[]) + reserved = forms.ChoiceField( + label=_('Reserved'), + choices=[('0', 'False'), + ('1', 'True')], + widget=forms.Select( + attrs={'class': 'switchable', + 'data-slug': 'available host'}), + required=False, + help_text=_("A boolean indicating whether this host is reserved or" + " not. Default value is set to False.")) + type = forms.CharField( + label=_('Type'), + widget=forms.TextInput(attrs={'maxlength': 255}), + help_text=_("Type of host.")) + control_attributes = forms.CharField( + label=_('Control Attribute'), + widget=forms.TextInput(), + help_text=_("Attributes to control host.")) + on_maintenance = forms.ChoiceField( + label=_('On Maintenance'), + choices=[('0', 'False'), + ('1', 'True')], + widget=forms.Select( + attrs={'class': 'switchable', + 'data-slug': 'available host'}), + required=False, + help_text=_("A boolean indicating whether this host is on maintenance" + " or not. Default value is set to False.")) + + def __init__(self, *args, **kwargs): + super(AddHostForm, self).__init__(*args, **kwargs) + + # Populate hypervisor name choices + hypervisor_list = kwargs.get('initial', {}).get("hypervisor_list", []) + hypervisor_name_list = [] + for hypervisor in hypervisor_list: + hypervisor_name_list.append( + (hypervisor.hypervisor_hostname, '%(name)s (%(id)s)' + % {"name": hypervisor.hypervisor_hostname, + "id": hypervisor.id})) + if hypervisor_name_list: + hypervisor_name_list.insert(0, ("", _("Select a host"))) + else: + hypervisor_name_list.insert(0, ("", _("No host available"))) + self.fields['name'].choices = hypervisor_name_list + + def handle(self, request, data): + try: + api.create_host(request, data) + msg = _('Host created successfully.') + messages.success(request, msg) + except Exception: + msg = _('Failed to create host.') + redirect = reverse('horizon:masakaridashboard:segments:index') + exceptions.handle(request, msg, redirect=redirect) + + return True diff --git a/masakaridashboard/segments/tables.py b/masakaridashboard/segments/tables.py index 5aaa76e..21074ec 100644 --- a/masakaridashboard/segments/tables.py +++ b/masakaridashboard/segments/tables.py @@ -22,6 +22,18 @@ from masakaridashboard.api import api from horizon import tables +class AddHost(tables.LinkAction): + name = "add_host" + verbose_name = _("Add Host") + classes = ("ajax-modal",) + + def get_link_url(self, datum): + obj_id = datum.uuid + url = "horizon:masakaridashboard:segments:addhost" + + return reverse(url, args=[obj_id]) + + class CreateSegment(tables.LinkAction): name = "create" verbose_name = _("Create Segment") @@ -96,4 +108,4 @@ class FailoverSegmentTable(tables.DataTable): name = "failover_segment" verbose_name = _("FailoverSegment") table_actions = (DeleteSegment, CreateSegment, SegmentFilterAction) - row_actions = (UpdateSegment,) + row_actions = (UpdateSegment, AddHost) diff --git a/masakaridashboard/segments/templates/segments/_addhost.html b/masakaridashboard/segments/templates/segments/_addhost.html new file mode 100644 index 0000000..1411bfc --- /dev/null +++ b/masakaridashboard/segments/templates/segments/_addhost.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Create a Host under a given segment with name, type and control_attributes."%}

+

{% trans "Reserved : User can set specific host as reserved by checking on reserved parameter. On this particular host, compute service must be disabled. Default value is set to False." %}

+

{% trans "On Maintenance: Boolean parameter indicating whether this host is under maintenance or not. Default value is set to False." %}

+{% endblock %} diff --git a/masakaridashboard/segments/templates/segments/addhost.html b/masakaridashboard/segments/templates/segments/addhost.html new file mode 100644 index 0000000..7ed47af --- /dev/null +++ b/masakaridashboard/segments/templates/segments/addhost.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Add Host" %}{% endblock %} + +{% block main %} + {% include 'masakaridashboard/segment/_addhost.html' %} +{% endblock %} diff --git a/masakaridashboard/segments/urls.py b/masakaridashboard/segments/urls.py index 35b3306..349a0c2 100644 --- a/masakaridashboard/segments/urls.py +++ b/masakaridashboard/segments/urls.py @@ -25,4 +25,5 @@ urlpatterns = [ name='create_segment'), url(SEGMENT % 'detail', views.DetailView.as_view(), name='detail'), url(SEGMENT % 'update', views.UpdateView.as_view(), name='update'), + url(SEGMENT % 'addhost', views.AddHostView.as_view(), name='addhost'), ] diff --git a/masakaridashboard/segments/views.py b/masakaridashboard/segments/views.py index 8c08167..9bf7f94 100644 --- a/masakaridashboard/segments/views.py +++ b/masakaridashboard/segments/views.py @@ -170,3 +170,60 @@ class UpdateView(forms.ModalFormView): 'name': segment.name, 'recovery_method': segment.recovery_method, 'description': segment.description} + + +class AddHostView(forms.ModalFormView): + template_name = 'masakaridashboard/segments/addhost.html' + modal_header = _("Add Host") + form_id = "add_host" + form_class = segment_forms.AddHostForm + submit_label = _("Add Host") + submit_url = "horizon:masakaridashboard:segments:addhost" + success_url = reverse_lazy("horizon:masakaridashboard:hosts:index") + page_title = _("Add Host") + + @memoized.memoized_method + def get_object(self): + + segments = api.segment_list(self.request) + host_list = [] + for segment in segments: + host_gen = api.get_host_list( + self.request, segment.uuid, filters={}) + for item in host_gen: + host_list.append(item.name) + try: + available_host_list = [] + hypervisor_list = api.get_hypervisor_list(self.request) + for hypervisor in hypervisor_list: + if hypervisor.hypervisor_hostname not in host_list: + available_host_list.append(hypervisor) + return available_host_list + except Exception: + msg = _('Unable to retrieve host list.') + redirect = reverse('horizon:masakaridashboard:segments:index') + exceptions.handle(self.request, msg, redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(AddHostView, self).get_context_data(**kwargs) + context['submit_url'] = reverse( + self.submit_url, + args=[self.kwargs["segment_id"]] + ) + + return context + + def get_initial(self): + hypervisor_list = self.get_object() + segment_name = api.get_segment( + self.request, self.kwargs['segment_id']).name + initial = {'segment_id': self.kwargs['segment_id'], + 'segment_name': segment_name, + 'hypervisor_list': hypervisor_list, + 'reserved': self.kwargs.get('reserved'), + 'type': self.kwargs.get('service_type'), + 'control_attributes': self.kwargs.get('control_attributes'), + 'on_maintenance': self.kwargs.get('on_maintenance') + } + + return initial diff --git a/masakaridashboard/test/test_data/masakari_data.py b/masakaridashboard/test/test_data/masakari_data.py index d27823c..8d9fad0 100644 --- a/masakaridashboard/test/test_data/masakari_data.py +++ b/masakaridashboard/test/test_data/masakari_data.py @@ -13,14 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.instance_ha.v1 import host from openstack.instance_ha.v1 import segment from openstack_dashboard.test.test_data import utils as test_data_utils from masakaridashboard.test import uuidsentinel +from novaclient.v2.hypervisors import Hypervisor +from novaclient.v2.hypervisors import HypervisorManager def data(TEST): + TEST.masakari_segment = test_data_utils.TestDataContainer() segment1 = segment.Segment(uuid=uuidsentinel.segment1, name='test', @@ -36,3 +40,20 @@ def data(TEST): TEST.masakari_segment.add(segment1) TEST.masakari_segment.add(segment2) TEST.masakari_segment.add(segment3) + + TEST.masakari_host = test_data_utils.TestDataContainer() + + host1 = host.Host(uuid=uuidsentinel.host1, name="test", + reserved=True, type='service', + control_attributes='test', + failover_segment_id=uuidsentinel.segment1, + on_maintenance=False) + + TEST.masakari_host.add(host1) + + TEST.hypervisors = test_data_utils.TestDataContainer() + + hypervisor1 = Hypervisor( + HypervisorManager, {'id': '1', 'hypervisor_hostname': "test"}) + + TEST.hypervisors.add(hypervisor1)