diff --git a/.zuul.yaml b/.zuul.yaml index 5a377c17..ee6cf2c5 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -29,4 +29,5 @@ - openstack/stx-config - openstack/stx-fault - openstack/stx-update - - openstack/stx-nfv \ No newline at end of file + - openstack/stx-nfv + - openstack/stx-ha \ No newline at end of file diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/api/iservice.py b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/api/iservice.py new file mode 100755 index 00000000..7e7b582a --- /dev/null +++ b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/api/iservice.py @@ -0,0 +1,49 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from __future__ import absolute_import + +import logging + +from django.conf import settings +from openstack_dashboard.api import base +import sm_client as smc + +# Swap out with SM API +LOG = logging.getLogger(__name__) +SM_API_SERVICENAME = "smapi" + + +def sm_client(request): + insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + + sm_api_path = base.url_for(request, SM_API_SERVICENAME) + return smc.Client('1', sm_api_path, + token=request.user.token.id, + insecure=insecure) + + +def sm_sda_list(request): + sdas = sm_client(request).sm_sda.list() + + return sdas + + +def sm_nodes_list(request): + nodes = sm_client(request).sm_nodes.list() + + return nodes diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/__init__.py b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/tables.py b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/tables.py new file mode 100644 index 00000000..c9f58ff9 --- /dev/null +++ b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/tables.py @@ -0,0 +1,56 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +from django import template +from django.utils.translation import ugettext_lazy as _ + +from horizon import tables + + +class ControllerServiceFilterAction(tables.FilterAction): + def filter(self, table, services, filter_string): + q = filter_string.lower() + + def comp(service): + if q in service.type.lower(): + return True + return False + + return filter(comp, services) + + +def cs_get_c0(iservice): + template_name = 'controller_services/_services_c0.html' + context = {"iservice": iservice} + return template.loader.render_to_string(template_name, context) + + +def cs_get_c1(iservice): + template_name = 'controller_services/_services_c1.html' + context = {"iservice": iservice} + return template.loader.render_to_string(template_name, context) + + +class ControllerServicesTable(tables.DataTable): + servicename = tables.Column("servicename", verbose_name=_('Name')) + c0 = tables.Column(cs_get_c0, verbose_name=_('controller-0')) + c1 = tables.Column(cs_get_c1, verbose_name=_('controller-1')) + + class Meta(object): + name = "controller_services" + verbose_name = _("Controller Services") + table_actions = (ControllerServiceFilterAction,) + multi_select = False diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/tabs.py b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/tabs.py new file mode 100644 index 00000000..e5eec4be --- /dev/null +++ b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/tabs.py @@ -0,0 +1,142 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tabs + +from starlingx_dashboard.api import iservice +from starlingx_dashboard.api import sysinv as sysinv_api +from starlingx_dashboard.dashboards.admin.controller_services import tables +from starlingx_dashboard.utils import objectify + +import logging + +LOG = logging.getLogger(__name__) + + +class ControllerServicesTab(tabs.TableTab): + table_classes = (tables.ControllerServicesTable,) + name = _("Controller Services") + slug = "controller_services" + template_name = ("horizon/common/_detail_table.html") + + def _find_service_group_names(self, sdas): + service_group_names_set = set() + for sda in sdas: + service_group_names_set.add(sda.service_group_name) + + service_group_names_list = list(service_group_names_set) + + return service_group_names_list + + def _update_service_group_states(self, service_group_name, sdas, nodes): + entry = {} + + for sda in sdas: + for n in nodes: + if n.name == sda.node_name: + if n.administrative_state.lower() == "locked": + dstate = "locked" + elif n.operational_state.lower() == "enabled": + dstate = "standby" + else: + dstate = n.operational_state.lower() + + if sda.service_group_name == service_group_name: + state_str = sda.state + if sda.status != "": + state_str += '-' + sda.status + if sda.condition != "": + state_str += ' [' + sda.condition + ']' + + if sda.state == "active": + if sda.node_name == "controller-0": + entry.update({'c0_activity': 'active'}) + entry.update({'c0_hostname': sda.node_name}) + entry.update({'c0_state': state_str}) + elif sda.node_name == "controller-1": + entry.update({'c1_activity': 'active'}) + entry.update({'c1_hostname': sda.node_name}) + entry.update({'c1_state': state_str}) + else: + if dstate == "standby": + dstate = state_str + + if sda.node_name == "controller-0": + entry.update({'c0_activity': sda.state}) + entry.update({'c0_hostname': sda.node_name}) + entry.update({'c0_state': dstate}) + elif sda.node_name == "controller-1": + entry.update({'c1_activity': sda.state}) + entry.update({'c1_hostname': sda.node_name}) + entry.update({'c1_state': dstate}) + + return entry + + def get_controller_services_data(self): + """Populate the data for the controller services tab""" + + # Here we filter the controller-1 column if we're a simplex system + # We should make this data driven in the future. This would allow us to + # more easily support n controllers + if sysinv_api.is_system_mode_simplex(self.tab_group.request): + controller1_col = self._tables['controller_services'].columns['c1'] + controller1_col.classes.append("hide") + + try: + nodes = iservice.sm_nodes_list(self.tab_group.request) + + sdas = iservice.sm_sda_list(self.tab_group.request) + + services = [] + + sgs = self._find_service_group_names(sdas) + + sdaid = 0 + for sg in sgs: + sdaid += 1 + entry = {} + entry.update({'id': sdaid}) + entry.update({'servicename': sg}) + sg_states = self._update_service_group_states(sg, sdas, nodes) + entry.update(sg_states) + + # Need to latch if any sg is enabled + if 'c0_activity' in entry.keys(): + sgstate = entry['c0_activity'] + if sgstate == "active": + entry.update({'sgstate': sgstate}) + elif 'c1_activity' in entry.keys(): + sgstate = entry['c1_activity'] + if sgstate == "active": + entry.update({'sgstate': sgstate}) + + if sgstate != "active": + entry.update({'sgstate': sgstate}) + + if entry != {}: + entry_object = objectify.objectify(entry) + services.append(entry_object) + + except Exception: + msg = _('Unable to get controller services list.') + exceptions.check_message(["Connection", "refused"], msg) + raise + + return services diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/templates/controller_services/_services_c0.html b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/templates/controller_services/_services_c0.html new file mode 100755 index 00000000..cf7d3a03 --- /dev/null +++ b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/templates/controller_services/_services_c0.html @@ -0,0 +1,3 @@ +{% if iservice.c0_hostname == "controller-0" %} +{{ iservice.c0_state|linebreaksbr }} +{% endif %} diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/templates/controller_services/_services_c1.html b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/templates/controller_services/_services_c1.html new file mode 100755 index 00000000..2a72cd68 --- /dev/null +++ b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/dashboards/admin/controller_services/templates/controller_services/_services_c1.html @@ -0,0 +1,3 @@ +{% if iservice.c1_hostname == "controller-1" %} +{{ iservice.c1_state|linebreaksbr }} +{% endif %} diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/enabled/_2841_starlingx_admin_info_panel_tabs.py b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/enabled/_2841_starlingx_admin_info_panel_tabs.py new file mode 100644 index 00000000..57e1d711 --- /dev/null +++ b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/enabled/_2841_starlingx_admin_info_panel_tabs.py @@ -0,0 +1,16 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'info' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'admin' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'admin' + +ADD_INSTALLED_APPS = \ + ['starlingx_dashboard.dashboards.admin.controller_services', ] + +EXTRA_TABS = { + 'openstack_dashboard.dashboards.admin.info.tabs.SystemInfoTabs': ( + 'starlingx_dashboard.dashboards.admin.controller_services.tabs.' + 'ControllerServicesTab', + ), +} diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/utils/__init__.py b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/utils/objectify.py b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/utils/objectify.py new file mode 100644 index 00000000..a0fc8e1b --- /dev/null +++ b/starlingx-dashboard/starlingx-dashboard/starlingx_dashboard/utils/objectify.py @@ -0,0 +1,80 @@ +# 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. +# +# Copyright (c) 2019 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + + +from functools import wraps # noqa + + +def objectify(func): + """Mimic an object given a dictionary. + + Given a dictionary, create an object and make sure that each of its + keys are accessible via attributes. + Ignore everything if the given value is not a dictionary. + :param value: A dictionary or another kind of object. + :returns: Either the created object or the given value. + + >>> obj = {'old_key': 'old_value'} + >>> oobj = objectify(obj) + >>> oobj['new_key'] = 'new_value' + >>> print oobj['old_key'], oobj['new_key'], oobj.old_key, oobj.new_key + + >>> @objectify + ... def func(): + ... return {'old_key': 'old_value'} + >>> obj = func() + >>> obj['new_key'] = 'new_value' + >>> print obj['old_key'], obj['new_key'], obj.old_key, obj.new_key + + + """ + + def create_object(value): + if isinstance(value, dict): + # Build a simple generic object. + class Object(dict): + def __setitem__(self, key, val): + setattr(self, key, val) + return super(Object, self).__setitem__(key, val) + + # Create that simple generic object. + ret_obj = Object() + # Assign the attributes given the dictionary keys. + for key, val in value.iteritems(): + ret_obj[key] = val + setattr(ret_obj, key, val) + return ret_obj + else: + return value + + # If func is a function, wrap around and act like a decorator. + if hasattr(func, '__call__'): + @wraps(func) + def wrapper(*args, **kwargs): + """Wrapper function for the decorator. + + :returns: The return value of the decorated function. + + """ + value = func(*args, **kwargs) + return create_object(value) + + return wrapper + + # Else just try to objectify the value given. + else: + return create_object(func) diff --git a/tox.ini b/tox.ini index 13781d7a..4a3d1a70 100644 --- a/tox.ini +++ b/tox.ini @@ -57,6 +57,7 @@ deps = {[testenv]deps} -e{[tox]stxdir}/stx-fault/python-fmclient/fmclient -e{[tox]stxdir}/stx-update/cgcs-patch/cgcs-patch -e{[tox]stxdir}/stx-nfv/nfv/nfv-client + -e{[tox]stxdir}/stx-ha/service-mgmt-client/sm-client requests-toolbelt pylint commands =