charm-neutron-gateway/actions/actions.py
Edin Sarajlic c6f970673b 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
2021-09-12 17:47:38 +02:00

322 lines
11 KiB
Python
Executable File

#!/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"))
def _add_path(path):
if path not in sys.path:
sys.path.insert(1, 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 (
assess_status,
pause_unit_helper,
resume_unit_helper,
register_configs,
)
def pause(args):
"""Pause the Ceilometer services.
@raises Exception should the service fail to stop.
"""
pause_unit_helper(register_configs())
def resume(args):
"""Resume the Ceilometer services.
@raises Exception should the service fail to start."""
resume_unit_helper(register_configs())
def restart(args):
"""Restart services.
:param args: Unused
:type args: List[str]
"""
deferred_only = action_get("deferred-only")
services = action_get("services").split()
# Check input
if deferred_only and services:
action_fail("Cannot set deferred-only and services")
return
if not (deferred_only or services):
action_fail("Please specify deferred-only or services")
return
if action_get('run-hooks'):
log("Charm does not defer any hooks at present", DEBUG)
if deferred_only:
os_utils.restart_services_action(deferred_only=True)
else:
os_utils.restart_services_action(services=services)
assess_status(register_configs())
def run_deferred_hooks(args):
"""Run deferred hooks.
:param args: Unused
:type args: List[str]
"""
# Charm defers restarts on a case-by-case basis so no full
# hook deferalls are needed.
action_fail("Charm does not defer any hooks at present")
def show_deferred_events(args):
"""Show the deferred events.
:param args: Unused
:type args: List[str]
"""
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,
"show-deferred-events": show_deferred_events,
"run-deferred-hooks": run_deferred_hooks,
"show-routers": get_routers,
"show-dhcp-networks": get_dhcp_networks,
"show-loadbalancers": get_lbaasv2_lb,
}
def main(args):
action_name = os.path.basename(args[0])
try:
action = ACTIONS[action_name]
except KeyError:
s = "Action {} undefined".format(action_name)
action_fail(s)
return s
else:
try:
action(args)
except Exception as e:
action_fail("Action {} failed: {}".format(action_name, str(e)))
if __name__ == "__main__":
sys.exit(main(sys.argv))