Move Controller services tab to stx-gui

Add a new tab to the system information panel. The new tab displays the
state of the controller services. This code was previously included in
the stx-upstream horizon repo.

Story: 2004552
Task: 30242
Change-Id: I223c60c18de782c637d0b67bad9b8dcfe71fd184
Signed-off-by: David Sullivan <david.sullivan@windriver.com>
This commit is contained in:
David Sullivan 2019-03-28 11:14:24 -04:00
parent 88d5b2e3ba
commit c75f3bb737
11 changed files with 352 additions and 1 deletions

View File

@ -29,4 +29,5 @@
- openstack/stx-config
- openstack/stx-fault
- openstack/stx-update
- openstack/stx-nfv
- openstack/stx-nfv
- openstack/stx-ha

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
{% if iservice.c0_hostname == "controller-0" %}
{{ iservice.c0_state|linebreaksbr }}
{% endif %}

View File

@ -0,0 +1,3 @@
{% if iservice.c1_hostname == "controller-1" %}
{{ iservice.c1_state|linebreaksbr }}
{% endif %}

View File

@ -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',
),
}

View File

@ -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)

View File

@ -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 =