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:
parent
6a22df54a7
commit
c6f970673b
@ -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.
|
||||
|
@ -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
1
actions/show-dhcp-networks
Symbolic link
@ -0,0 +1 @@
|
||||
actions.py
|
1
actions/show-loadbalancers
Symbolic link
1
actions/show-loadbalancers
Symbolic link
@ -0,0 +1 @@
|
||||
actions.py
|
1
actions/show-routers
Symbolic link
1
actions/show-routers
Symbolic link
@ -0,0 +1 @@
|
||||
actions.py
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user