Actions that expose various neutron resources

New actions:
  * show-routers
  * show-dhcp-networks
  * show-loadbalancers

Partial-Bug: #1916231
Closes-Bug: #1917401
Closes-Bug: #1917403
Closes-Bug: #1917405

Change-Id: Ie59c2a7d5c1ee9c51a0f7db4e8f38229812ac84a
func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/611
This commit is contained in:
Edin Sarajlic 2021-03-25 04:17:03 +00:00 committed by Martin Kalcok
parent 6a22df54a7
commit c6f970673b
8 changed files with 289 additions and 3 deletions

View File

@ -49,5 +49,11 @@ run-deferred-hooks:
.
NOTE: Service will be restarted as needed irrespective of enable-auto-restarts
show-deferred-events:
descrpition: |
description: |
Show the outstanding restarts
show-routers:
description: Shows a list of routers hosted on the neutron-gateway unit.
show-dhcp-networks:
description: Shows a list of DHCP networks hosted on the neutron-gateway unit.
show-loadbalancers:
description: Shows a list of LBaasV2 load-balancers hosted on the neutron-gateway unit.

View File

@ -1,8 +1,14 @@
#!/usr/bin/env python3
import os
import socket
import sys
from keystoneauth1 import identity
from keystoneauth1 import session
from neutronclient.v2_0 import client
import yaml
_path = os.path.dirname(os.path.realpath(__file__))
_hooks_dir = os.path.abspath(os.path.join(_path, "..", "hooks"))
@ -14,12 +20,18 @@ def _add_path(path):
_add_path(_hooks_dir)
from charmhelpers.core.hookenv import (
relation_get,
relation_ids,
related_units,
)
import charmhelpers.contrib.openstack.utils as os_utils
from charmhelpers.core.hookenv import (
DEBUG,
action_get,
action_fail,
function_set,
log,
)
from neutron_utils import (
@ -87,11 +99,207 @@ def show_deferred_events(args):
os_utils.show_deferred_events_action_helper()
def get_neutron():
"""Return authenticated neutron client.
:return: neutron client
:rtype: neutronclient.v2_0.client.Client
:raises RuntimeError: Exception is raised if authentication of neutron
client fails. This can be either because this unit does not have a
"neutron-plugin-api" relation which should contain credentials or
the credentials are wrong or keystone service is not available.
"""
rel_name = "neutron-plugin-api"
neutron = None
for rid in relation_ids(rel_name):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
if rdata is None:
continue
protocol = rdata.get("auth_protocol")
host = rdata.get("auth_host")
port = rdata.get("auth_port")
username = rdata.get("service_username")
password = rdata.get("service_password")
project = rdata.get("service_tenant")
project_domain = rdata.get("service_domain", "default")
user_domain_name = rdata.get("service_domain", "default")
if protocol and host and port \
and username and password and project:
auth_url = "{}://{}:{}/".format(protocol,
host,
port)
auth = identity.Password(auth_url=auth_url,
username=username,
password=password,
project_name=project,
project_domain_name=project_domain,
user_domain_name=user_domain_name)
sess = session.Session(auth=auth)
neutron = client.Client(session=sess)
break
if neutron is not None:
break
if neutron is None:
raise RuntimeError("Relation '{}' is either missing or does not "
"contain neutron credentials".format(rel_name))
return neutron
def get_network_agents_on_host(hostname, neutron, agent_type=None):
"""Fetch list of neutron agents on specified host.
:param hostname: name of host on which the agents are running
:param neutron: authenticated neutron client
:param agent_type: If provided, filter only agents of selected type
:return: List of agents matching given criteria
:rtype: list[dict]
"""
params = {'host': hostname}
if agent_type is not None:
params['agent_type'] = agent_type
agent_list = neutron.list_agents(**params)["agents"]
return agent_list
def get_resource_list_on_agents(agent_list,
list_resource_function,
resource_name):
"""Fetch resources hosted on neutron agents combined into single list.
:param agent_list: List of agents on which to search for resources
:type agent_list: list[dict]
:param list_resource_function: function that takes agent ID and returns
resources present on that agent.
:type list_resource_function: Callable
:param resource_name: filter only resources with given name (e.g.:
"networks", "routers", ...)
:type resource_name: str
:return: List of neutron resources.
:rtype: list[dict]
"""
agent_id_list = [agent["id"] for agent in agent_list]
resource_list = []
for agent_id in agent_id_list:
resource_list.extend(list_resource_function(agent_id)[resource_name])
return resource_list
def clean_resource_list(resource_list, allowed_keys=None):
"""Strip resources of all fields except those in 'allowed_keys.'
resource_list is a list where each resources is represented as a dict with
many attributes. This function strips all but those defined in
'allowed_keys'
:param resource_list: List of resources to strip
:param allowed_keys: keys allowed in the resulting dictionary for each
resource
:return: List of stripped resources
:rtype: list[dict]
"""
if allowed_keys is None:
allowed_keys = ["id", "status"]
clean_data_list = []
for resource in resource_list:
clean_data = {key: value for key, value in resource.items()
if key in allowed_keys}
clean_data_list.append(clean_data)
return clean_data_list
def format_status_output(data, key_attribute="id"):
"""Reformat data from the neutron api into human-readable yaml.
Input data are expected to be list of dictionaries (as returned by neutron
api). This list is transformed into dict where "id" of a resource is key
and rest of the resource attributes are value (in form of dictionary). The
resulting structure is dumped as yaml text.
:param data: List of dictionaires representing neutron resources
:param key_attribute: attribute that will be used as a key in the
result. (default=id)
:return: yaml string representing input data.
"""
output = {}
for entry in data:
header = entry.pop(key_attribute)
output[header] = {}
for attribute, value in entry.items():
output[header][attribute] = value
return yaml.dump(output)
def get_routers(args):
"""Implementation of 'show-routers' action."""
neutron = get_neutron()
agent_list = get_network_agents_on_host(socket.gethostname(), neutron,
"L3 agent")
router_list = get_resource_list_on_agents(agent_list,
neutron.list_routers_on_l3_agent,
"routers")
clean_data = clean_resource_list(router_list,
allowed_keys=["id",
"status",
"ha",
"name"])
function_set({"router-list": format_status_output(clean_data)})
def get_dhcp_networks(args):
"""Implementation of 'show-dhcp-networks' action."""
neutron = get_neutron()
agent_list = get_network_agents_on_host(socket.gethostname(), neutron,
"DHCP agent")
list_func = neutron.list_networks_on_dhcp_agent
dhcp_network_list = get_resource_list_on_agents(agent_list,
list_func,
"networks")
clean_data = clean_resource_list(dhcp_network_list,
allowed_keys=["id", "status", "name"])
function_set({"dhcp-networks": format_status_output(clean_data)})
def get_lbaasv2_lb(args):
"""Implementation of 'show-loadbalancers' action."""
neutron = get_neutron()
agent_list = get_network_agents_on_host(socket.gethostname(),
neutron,
"Loadbalancerv2 agent")
list_func = neutron.list_loadbalancers_on_lbaas_agent
lb_list = get_resource_list_on_agents(agent_list,
list_func,
"loadbalancers")
clean_data = clean_resource_list(lb_list, allowed_keys=["id",
"status",
"name"])
function_set({"load-balancers": format_status_output(clean_data)})
# A dictionary of all the defined actions to callables (which take
# parsed arguments).
ACTIONS = {"pause": pause, "resume": resume, "restart-services": restart,
ACTIONS = {"pause": pause,
"resume": resume,
"restart-services": restart,
"show-deferred-events": show_deferred_events,
"run-deferred-hooks": run_deferred_hooks}
"run-deferred-hooks": run_deferred_hooks,
"show-routers": get_routers,
"show-dhcp-networks": get_dhcp_networks,
"show-loadbalancers": get_lbaasv2_lb,
}
def main(args):

1
actions/show-dhcp-networks Symbolic link
View File

@ -0,0 +1 @@
actions.py

1
actions/show-loadbalancers Symbolic link
View File

@ -0,0 +1 @@
actions.py

1
actions/show-routers Symbolic link
View File

@ -0,0 +1 @@
actions.py

View File

@ -115,6 +115,7 @@ def install():
status_set('maintenance', 'Installing apt packages')
apt_update(fatal=True)
apt_install('python-six', fatal=True) # Force upgrade
apt_install('python3-neutronclient')
if valid_plugin():
apt_install(filter_installed_packages(get_early_packages()),
fatal=True)

View File

@ -68,6 +68,7 @@ configure_options:
tests:
- zaza.openstack.charm_tests.neutron.tests.NeutronGatewayDeferredRestartTest
- zaza.openstack.charm_tests.neutron.tests.NeutronGatewayTest
- zaza.openstack.charm_tests.neutron.tests.NeutronGatewayShowActionsTest
- zaza.openstack.charm_tests.neutron.tests.SecurityTest
- zaza.openstack.charm_tests.neutron.tests.NeutronNetworkingTest
- zaza.openstack.charm_tests.neutron.tests.NeutronOvsVsctlTest
@ -75,6 +76,7 @@ tests:
- migrate-ovn:
- zaza.openstack.charm_tests.neutron.tests.NeutronGatewayDeferredRestartTest
- zaza.openstack.charm_tests.neutron.tests.NeutronGatewayTest
- zaza.openstack.charm_tests.neutron.tests.NeutronGatewayShowActionsTest
- zaza.openstack.charm_tests.neutron.tests.SecurityTest
- zaza.openstack.charm_tests.neutron.tests.NeutronOvsVsctlTest
- zaza.openstack.charm_tests.neutron.tests.NeutronBridgePortMappingTest

View File

@ -46,6 +46,72 @@ class ResumeTestCase(CharmTestCase):
self.resume_unit_helper.assert_called_once_with('test-config')
class GetStatusTestCase(CharmTestCase):
def setUp(self):
super(GetStatusTestCase, self).setUp(actions, [])
def test_clean_resource_list(self):
data = [{"id": 1, "x": "data", "z": "data"},
{"id": 2, "y": "data", "z": "data"}]
clean_data = actions.clean_resource_list(data)
for resource in clean_data:
self.assertTrue("id" in resource)
self.assertTrue("x" not in resource)
self.assertTrue("y" not in resource)
self.assertTrue("z" not in resource)
# test allowed keys
clean_data = actions.clean_resource_list(data,
allowed_keys=["id", "z"])
for resource in clean_data:
self.assertTrue("id" in resource)
self.assertTrue("x" not in resource)
self.assertTrue("y" not in resource)
self.assertTrue("z" in resource)
def test_get_resource_list_on_agents(self):
list_function = MagicMock()
agent_list = [{"id": 1}, {"id": 2}]
list_results = [{"results": ["a", "b"]}, {"results": ["c"], "x": [""]}]
expected_resource_length = 0
for r in list_results:
expected_resource_length += len(r.get("results", []))
list_function.side_effect = list_results
resource_list = actions.get_resource_list_on_agents(agent_list,
list_function,
"results")
assert list_function.call_count > 0
self.assertEqual(len(resource_list), expected_resource_length)
@mock.patch("actions.function_set")
@mock.patch("actions.get_resource_list_on_agents")
@mock.patch("actions.get_neutron")
def test_action_get_status(self, mock_get_neutron,
mock_get_resource_list_on_agents,
mock_function_set):
data = [{"id": 1, "x": "data", "z": "data"},
{"id": 2, "y": "data", "z": "data"}]
mock_get_resource_list_on_agents.return_value = data
clean_data = actions.clean_resource_list(data)
yaml_clean_data = actions.format_status_output(clean_data)
actions.get_routers(None)
mock_function_set.assert_called_with({"router-list": yaml_clean_data})
actions.get_dhcp_networks(None)
mock_function_set.assert_called_with({"dhcp-networks":
yaml_clean_data})
actions.get_lbaasv2_lb(None)
mock_function_set.assert_called_with({"load-balancers":
yaml_clean_data})
class MainTestCase(CharmTestCase):
def setUp(self):