From 63f690e6fd1b029938746f77a224e1f1b987c6c0 Mon Sep 17 00:00:00 2001 From: Brian Haley Date: Sat, 21 Oct 2023 19:34:45 -0400 Subject: [PATCH] Make common Metadata Driver classes The ML2 and OVN metadata agents have almost identical code, as the former was copied to the latter and modified. Instead, combine all the common parts and just have each do any driver-specific operations separately. Change-Id: Iff8bc8de16a8afc7c0195bf301d1b0643e17d7c6 --- neutron/agent/metadata/driver.py | 306 ++---------------- neutron/agent/metadata/driver_base.py | 283 ++++++++++++++++ neutron/agent/ovn/metadata/driver.py | 232 +------------ .../tests/functional/agent/l3/framework.py | 4 +- .../tests/functional/agent/test_dhcp_agent.py | 4 +- .../tests/unit/agent/metadata/test_driver.py | 18 +- .../unit/agent/ovn/metadata/test_agent.py | 4 + .../unit/agent/ovn/metadata/test_driver.py | 30 +- 8 files changed, 359 insertions(+), 522 deletions(-) create mode 100644 neutron/agent/metadata/driver_base.py diff --git a/neutron/agent/metadata/driver.py b/neutron/agent/metadata/driver.py index e20693b1cd5..6389ca3ffd3 100644 --- a/neutron/agent/metadata/driver.py +++ b/neutron/agent/metadata/driver.py @@ -13,164 +13,36 @@ # License for the specific language governing permissions and limitations # under the License. -import grp -import os -import pwd -import signal - from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants -from neutron_lib import exceptions -from oslo_config import cfg from oslo_log import log as logging from oslo_utils import netutils -from neutron._i18n import _ from neutron.agent.l3 import ha_router from neutron.agent.l3 import namespaces -from neutron.agent.linux import external_process -from neutron.agent.linux import ip_lib -from neutron.agent.linux import utils as linux_utils +from neutron.agent.metadata import driver_base from neutron.common import coordination -from neutron.common import metadata as comm_meta -from neutron.common import utils as common_utils LOG = logging.getLogger(__name__) -SIGTERM_TIMEOUT = 5 - -METADATA_SERVICE_NAME = 'metadata-proxy' -HAPROXY_SERVICE = 'haproxy' - -PROXY_CONFIG_DIR = "ns-metadata-proxy" - _HEADER_CONFIG_TEMPLATE = """ http-request del-header X-Neutron-%(res_type_del)s-ID http-request set-header X-Neutron-%(res_type)s-ID %(res_id)s """ -_UNLIMITED_CONFIG_TEMPLATE = """ -listen listener - bind %(host)s:%(port)s - %(bind_v6_line)s - server metadata %(unix_socket_path)s -""" + +class HaproxyConfigurator(driver_base.HaproxyConfiguratorBase): + PROXY_CONFIG_DIR = "ns-metadata-proxy" + HEADER_CONFIG_TEMPLATE = _HEADER_CONFIG_TEMPLATE -class HaproxyConfigurator(object): - def __init__(self, network_id, router_id, unix_socket_path, host, port, - user, group, state_path, pid_file, rate_limiting_config, - host_v6=None, bind_interface=None): - self.network_id = network_id - self.router_id = router_id - if network_id is None and router_id is None: - raise exceptions.NetworkIdOrRouterIdRequiredError() - - self.host = host - self.host_v6 = host_v6 - self.bind_interface = bind_interface - self.port = port - self.user = user - self.group = group - self.state_path = state_path - self.unix_socket_path = unix_socket_path - self.pidfile = pid_file - self.rate_limiting_config = rate_limiting_config - self.log_level = ( - 'debug' if logging.is_debug_enabled(cfg.CONF) else 'info') - # log-tag will cause entries to have the string pre-pended, so use - # the uuid haproxy will be started with. Additionally, if it - # starts with "haproxy" then things will get logged to - # /var/log/haproxy.log on Debian distros, instead of to syslog. - uuid = network_id or router_id - self.log_tag = "haproxy-" + METADATA_SERVICE_NAME + "-" + uuid - - def create_config_file(self): - """Create the config file for haproxy.""" - # Need to convert uid/gid into username/group - try: - username = pwd.getpwuid(int(self.user)).pw_name - except (ValueError, KeyError): - try: - username = pwd.getpwnam(self.user).pw_name - except KeyError: - raise comm_meta.InvalidUserOrGroupException( - _("Invalid user/uid: '%s'") % self.user) - - try: - groupname = grp.getgrgid(int(self.group)).gr_name - except (ValueError, KeyError): - try: - groupname = grp.getgrnam(self.group).gr_name - except KeyError: - raise comm_meta.InvalidUserOrGroupException( - _("Invalid group/gid: '%s'") % self.group) - - cfg_info = { - 'host': self.host, - 'port': self.port, - 'unix_socket_path': self.unix_socket_path, - 'user': username, - 'group': groupname, - 'pidfile': self.pidfile, - 'log_level': self.log_level, - 'log_tag': self.log_tag, - 'bind_v6_line': '', - } - if self.host_v6 and self.bind_interface: - cfg_info['bind_v6_line'] = ( - 'bind %s:%s interface %s' % ( - self.host_v6, self.port, self.bind_interface) - ) - # If using the network ID, delete any spurious router ID that might - # have been in the request, same for network ID when using router ID. - if self.network_id: - cfg_info['res_type'] = 'Network' - cfg_info['res_id'] = self.network_id - cfg_info['res_type_del'] = 'Router' - else: - cfg_info['res_type'] = 'Router' - cfg_info['res_id'] = self.router_id - cfg_info['res_type_del'] = 'Network' - - haproxy_cfg = comm_meta.get_haproxy_config(cfg_info, - self.rate_limiting_config, - _HEADER_CONFIG_TEMPLATE, - _UNLIMITED_CONFIG_TEMPLATE) - - LOG.debug("haproxy_cfg = %s", haproxy_cfg) - cfg_dir = self.get_config_path(self.state_path) - # uuid has to be included somewhere in the command line so that it can - # be tracked by process_monitor. - self.cfg_path = os.path.join(cfg_dir, "%s.conf" % cfg_info['res_id']) - if not os.path.exists(cfg_dir): - os.makedirs(cfg_dir) - with open(self.cfg_path, "w") as cfg_file: - cfg_file.write(haproxy_cfg) - - @staticmethod - def get_config_path(state_path): - return os.path.join(state_path or cfg.CONF.state_path, - PROXY_CONFIG_DIR) - - @staticmethod - def cleanup_config_file(uuid, state_path): - """Delete config file created when metadata proxy was spawned.""" - # Delete config file if it exists - cfg_path = os.path.join( - HaproxyConfigurator.get_config_path(state_path), - "%s.conf" % uuid) - linux_utils.delete_if_exists(cfg_path, run_as_root=True) - - -class MetadataDriver(object): - - monitors = {} - - def __init__(self, l3_agent): +class MetadataDriver(driver_base.MetadataDriverBase): + def __init__(self, l3_agent=None): + if not l3_agent: + return self.metadata_port = l3_agent.conf.metadata_port self.metadata_access_mark = l3_agent.conf.metadata_access_mark registry.subscribe( @@ -180,142 +52,26 @@ class MetadataDriver(object): registry.subscribe( before_router_removed, resources.ROUTER, events.BEFORE_DELETE) - @classmethod - def metadata_filter_rules(cls, port, mark): - return [('INPUT', '-m mark --mark %s/%s -j ACCEPT' % - (mark, constants.ROUTER_MARK_MASK)), - ('INPUT', '-p tcp -m tcp --dport %s ' - '-j DROP' % port)] + @staticmethod + def haproxy_configurator(): + return HaproxyConfigurator - @classmethod - def metadata_nat_rules( - cls, port, metadata_address=constants.METADATA_V4_CIDR): - return [('PREROUTING', '-d %(metadata_address)s ' - '-i %(interface_name)s ' - '-p tcp -m tcp --dport 80 -j REDIRECT ' - '--to-ports %(port)s' % - {'metadata_address': metadata_address, - 'interface_name': namespaces.INTERNAL_DEV_PREFIX + '+', - 'port': port})] - @classmethod - def _get_metadata_proxy_user_group(cls, conf): - user = conf.metadata_proxy_user or str(os.geteuid()) - group = conf.metadata_proxy_group or str(os.getegid()) +def metadata_filter_rules(port, mark): + return [('INPUT', '-m mark --mark %s/%s -j ACCEPT' % + (mark, constants.ROUTER_MARK_MASK)), + ('INPUT', '-p tcp -m tcp --dport %s ' + '-j DROP' % port)] - return user, group - @classmethod - def _get_metadata_proxy_callback(cls, bind_address, port, conf, - network_id=None, router_id=None, - bind_address_v6=None, - bind_interface=None): - def callback(pid_file): - metadata_proxy_socket = conf.metadata_proxy_socket - user, group = ( - cls._get_metadata_proxy_user_group(conf)) - haproxy = HaproxyConfigurator(network_id, - router_id, - metadata_proxy_socket, - bind_address, - port, - user, - group, - conf.state_path, - pid_file, - conf.metadata_rate_limiting, - bind_address_v6, - bind_interface) - haproxy.create_config_file() - proxy_cmd = [HAPROXY_SERVICE, - '-f', haproxy.cfg_path] - return proxy_cmd - - return callback - - @classmethod - def spawn_monitored_metadata_proxy(cls, monitor, ns_name, port, conf, - bind_address="0.0.0.0", network_id=None, - router_id=None, bind_address_v6=None, - bind_interface=None): - if bind_interface is not None and bind_address_v6 is not None: - # HAProxy cannot bind() until IPv6 Duplicate Address Detection - # completes. We must wait until the address leaves its 'tentative' - # state. - try: - ip_lib.IpAddrCommand( - parent=ip_lib.IPDevice(name=bind_interface, - namespace=ns_name) - ).wait_until_address_ready(address=bind_address_v6) - except ip_lib.DADFailed as exc: - # This failure means that another DHCP agent has already - # configured this metadata address, so all requests will - # be via that single agent. - LOG.info('DAD failed for address %(address)s on interface ' - '%(interface)s in namespace %(namespace)s on network ' - '%(network)s, deleting it. Exception: %(exception)s', - {'address': bind_address_v6, - 'interface': bind_interface, - 'namespace': ns_name, - 'network': network_id, - 'exception': str(exc)}) - try: - ip_lib.delete_ip_address(bind_address_v6, bind_interface, - namespace=ns_name) - except Exception as exc: - # do not re-raise a delete failure, just log - LOG.info('Address deletion failure: %s', str(exc)) - - # Do not use the address or interface when DAD fails - bind_address_v6 = bind_interface = None - - uuid = network_id or router_id - callback = cls._get_metadata_proxy_callback( - bind_address, port, conf, - network_id=network_id, router_id=router_id, - bind_address_v6=bind_address_v6, bind_interface=bind_interface) - pm = cls._get_metadata_proxy_process_manager(uuid, conf, - ns_name=ns_name, - callback=callback) - try: - pm.enable(ensure_active=True) - except exceptions.ProcessExecutionError as exec_err: - LOG.error("Encountered process execution error %(err)s while " - "starting process in namespace %(ns)s", - {"err": exec_err, "ns": ns_name}) - return - monitor.register(uuid, METADATA_SERVICE_NAME, pm) - cls.monitors[router_id] = pm - - @classmethod - def destroy_monitored_metadata_proxy(cls, monitor, uuid, conf, ns_name): - monitor.unregister(uuid, METADATA_SERVICE_NAME) - pm = cls._get_metadata_proxy_process_manager(uuid, conf, - ns_name=ns_name) - pm.disable(sig=str(int(signal.SIGTERM))) - try: - common_utils.wait_until_true(lambda: not pm.active, - timeout=SIGTERM_TIMEOUT) - except common_utils.WaitTimeout: - LOG.warning('Metadata process %s did not finish after SIGTERM ' - 'signal in %s seconds, sending SIGKILL signal', - pm.pid, SIGTERM_TIMEOUT) - pm.disable(sig=str(int(signal.SIGKILL))) - - # Delete metadata proxy config. - HaproxyConfigurator.cleanup_config_file(uuid, cfg.CONF.state_path) - - cls.monitors.pop(uuid, None) - - @classmethod - def _get_metadata_proxy_process_manager(cls, router_id, conf, ns_name=None, - callback=None): - return external_process.ProcessManager( - conf=conf, - uuid=router_id, - namespace=ns_name, - service=HAPROXY_SERVICE, - default_cmd_callback=callback) +def metadata_nat_rules(port, metadata_address=constants.METADATA_V4_CIDR): + return [('PREROUTING', '-d %(metadata_address)s ' + '-i %(interface_name)s ' + '-p tcp -m tcp --dport 80 -j REDIRECT ' + '--to-ports %(port)s' % + {'metadata_address': metadata_address, + 'interface_name': namespaces.INTERNAL_DEV_PREFIX + '+', + 'port': port})] def after_router_added(resource, event, l3_agent, payload): @@ -364,17 +120,17 @@ def before_router_removed(resource, event, l3_agent, payload=None): @coordination.synchronized('router-lock-ns-{router.ns_name}') def apply_metadata_nat_rules(router, proxy): - for c, r in proxy.metadata_filter_rules(proxy.metadata_port, - proxy.metadata_access_mark): + for c, r in metadata_filter_rules(proxy.metadata_port, + proxy.metadata_access_mark): router.iptables_manager.ipv4['filter'].add_rule(c, r) if netutils.is_ipv6_enabled(): - for c, r in proxy.metadata_filter_rules(proxy.metadata_port, - proxy.metadata_access_mark): + for c, r in metadata_filter_rules(proxy.metadata_port, + proxy.metadata_access_mark): router.iptables_manager.ipv6['filter'].add_rule(c, r) - for c, r in proxy.metadata_nat_rules(proxy.metadata_port): + for c, r in metadata_nat_rules(proxy.metadata_port): router.iptables_manager.ipv4['nat'].add_rule(c, r) if netutils.is_ipv6_enabled(): - for c, r in proxy.metadata_nat_rules( + for c, r in metadata_nat_rules( proxy.metadata_port, metadata_address=(constants.METADATA_V6_CIDR)): router.iptables_manager.ipv6['nat'].add_rule(c, r) diff --git a/neutron/agent/metadata/driver_base.py b/neutron/agent/metadata/driver_base.py new file mode 100644 index 00000000000..8fb3952dde1 --- /dev/null +++ b/neutron/agent/metadata/driver_base.py @@ -0,0 +1,283 @@ +# Copyright 2014 OpenStack Foundation. +# All Rights Reserved. +# +# 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. + +import abc +import grp +import os +import pwd +import signal + +from neutron_lib import exceptions +from oslo_config import cfg +from oslo_log import log as logging + +from neutron._i18n import _ +from neutron.agent.linux import external_process +from neutron.agent.linux import ip_lib +from neutron.agent.linux import utils as linux_utils +from neutron.common import metadata as comm_meta +from neutron.common import utils as common_utils + + +LOG = logging.getLogger(__name__) + +SIGTERM_TIMEOUT = 5 + +METADATA_SERVICE_NAME = 'metadata-proxy' +HAPROXY_SERVICE = 'haproxy' + +_UNLIMITED_CONFIG_TEMPLATE = """ +listen listener + bind %(host)s:%(port)s + %(bind_v6_line)s + server metadata %(unix_socket_path)s +""" + + +class HaproxyConfiguratorBase(object): + PROXY_CONFIG_DIR = None + HEADER_CONFIG_TEMPLATE = None + + def __init__(self, network_id, router_id, unix_socket_path, host, port, + user, group, state_path, pid_file, rate_limiting_config, + host_v6=None, bind_interface=None): + self.network_id = network_id + self.router_id = router_id + if network_id is None and router_id is None: + raise exceptions.NetworkIdOrRouterIdRequiredError() + + self.host = host + self.host_v6 = host_v6 + self.bind_interface = bind_interface + self.port = port + self.user = user + self.group = group + self.state_path = state_path + self.unix_socket_path = unix_socket_path + self.pidfile = pid_file + self.rate_limiting_config = rate_limiting_config + self.log_level = ( + 'debug' if logging.is_debug_enabled(cfg.CONF) else 'info') + # log-tag will cause entries to have the string pre-pended, so use + # the uuid haproxy will be started with. Additionally, if it + # starts with "haproxy" then things will get logged to + # /var/log/haproxy.log on Debian distros, instead of to syslog. + uuid = network_id or router_id + self.log_tag = "haproxy-{}-{}".format(METADATA_SERVICE_NAME, uuid) + + def create_config_file(self): + """Create the config file for haproxy.""" + # Need to convert uid/gid into username/group + try: + username = pwd.getpwuid(int(self.user)).pw_name + except (ValueError, KeyError): + try: + username = pwd.getpwnam(self.user).pw_name + except KeyError: + raise comm_meta.InvalidUserOrGroupException( + _("Invalid user/uid: '%s'") % self.user) + + try: + groupname = grp.getgrgid(int(self.group)).gr_name + except (ValueError, KeyError): + try: + groupname = grp.getgrnam(self.group).gr_name + except KeyError: + raise comm_meta.InvalidUserOrGroupException( + _("Invalid group/gid: '%s'") % self.group) + + cfg_info = { + 'host': self.host, + 'port': self.port, + 'unix_socket_path': self.unix_socket_path, + 'user': username, + 'group': groupname, + 'pidfile': self.pidfile, + 'log_level': self.log_level, + 'log_tag': self.log_tag, + 'bind_v6_line': '', + } + if self.host_v6 and self.bind_interface: + cfg_info['bind_v6_line'] = ( + 'bind %s:%s interface %s' % ( + self.host_v6, self.port, self.bind_interface) + ) + # If using the network ID, delete any spurious router ID that might + # have been in the request, same for network ID when using router ID. + # This is to prevent someone from spoofing a metadata request using + # the proxy via an external network. See LP #1865036 for more info. + # This only applies to the non-OVN driver. + if self.network_id: + cfg_info['res_type'] = 'Network' + cfg_info['res_id'] = self.network_id + cfg_info['res_type_del'] = 'Router' + else: + cfg_info['res_type'] = 'Router' + cfg_info['res_id'] = self.router_id + cfg_info['res_type_del'] = 'Network' + + haproxy_cfg = comm_meta.get_haproxy_config(cfg_info, + self.rate_limiting_config, + self.HEADER_CONFIG_TEMPLATE, + _UNLIMITED_CONFIG_TEMPLATE) + + LOG.debug("haproxy_cfg = %s", haproxy_cfg) + cfg_dir = self.get_config_path(self.state_path) + # uuid has to be included somewhere in the command line so that it can + # be tracked by process_monitor. + self.cfg_path = os.path.join(cfg_dir, "%s.conf" % cfg_info['res_id']) + if not os.path.exists(cfg_dir): + os.makedirs(cfg_dir) + with open(self.cfg_path, "w") as cfg_file: + cfg_file.write(haproxy_cfg) + + @classmethod + def get_config_path(cls, state_path): + return os.path.join(state_path or cfg.CONF.state_path, + cls.PROXY_CONFIG_DIR) + + @classmethod + def cleanup_config_file(cls, uuid, state_path): + """Delete config file created when metadata proxy was spawned.""" + # Delete config file if it exists + cfg_path = os.path.join( + cls.get_config_path(state_path), + "%s.conf" % uuid) + linux_utils.delete_if_exists(cfg_path, run_as_root=True) + + +class MetadataDriverBase(object, metaclass=abc.ABCMeta): + @staticmethod + @abc.abstractmethod + def haproxy_configurator(): + """Returns the HaproxyConfigurator for the class.""" + pass + + @classmethod + def _get_metadata_proxy_user_group(cls, conf): + user = conf.metadata_proxy_user or str(os.geteuid()) + group = conf.metadata_proxy_group or str(os.getegid()) + + return user, group + + @classmethod + def _get_metadata_proxy_callback(cls, bind_address, port, conf, + network_id=None, router_id=None, + bind_address_v6=None, + bind_interface=None): + def callback(pid_file): + metadata_proxy_socket = conf.metadata_proxy_socket + user, group = cls._get_metadata_proxy_user_group(conf) + configurator = cls.haproxy_configurator() + haproxy = configurator(network_id, + router_id, + metadata_proxy_socket, + bind_address, + port, + user, + group, + conf.state_path, + pid_file, + conf.metadata_rate_limiting, + bind_address_v6, + bind_interface) + haproxy.create_config_file() + proxy_cmd = [HAPROXY_SERVICE, '-f', haproxy.cfg_path] + + return proxy_cmd + + return callback + + @classmethod + def spawn_monitored_metadata_proxy(cls, monitor, ns_name, port, conf, + bind_address="0.0.0.0", network_id=None, + router_id=None, bind_address_v6=None, + bind_interface=None): + if bind_interface is not None and bind_address_v6 is not None: + # HAProxy cannot bind() until IPv6 Duplicate Address Detection + # completes. We must wait until the address leaves its 'tentative' + # state. + try: + ip_lib.IpAddrCommand( + parent=ip_lib.IPDevice(name=bind_interface, + namespace=ns_name) + ).wait_until_address_ready(address=bind_address_v6) + except ip_lib.DADFailed as exc: + # This failure means that another DHCP agent has already + # configured this metadata address, so all requests will + # be via that single agent. + LOG.info('DAD failed for address %(address)s on interface ' + '%(interface)s in namespace %(namespace)s on network ' + '%(network)s, deleting it. Exception: %(exception)s', + {'address': bind_address_v6, + 'interface': bind_interface, + 'namespace': ns_name, + 'network': network_id, + 'exception': str(exc)}) + try: + ip_lib.delete_ip_address(bind_address_v6, bind_interface, + namespace=ns_name) + except Exception as exc: + # do not re-raise a delete failure, just log + LOG.info('Address deletion failure: %s', str(exc)) + + # Do not use the address or interface when DAD fails + bind_address_v6 = bind_interface = None + + uuid = network_id or router_id + callback = cls._get_metadata_proxy_callback( + bind_address, port, conf, + network_id=network_id, router_id=router_id, + bind_address_v6=bind_address_v6, bind_interface=bind_interface) + pm = cls._get_metadata_proxy_process_manager(uuid, conf, + ns_name=ns_name, + callback=callback) + try: + pm.enable(ensure_active=True) + except exceptions.ProcessExecutionError as exec_err: + LOG.error("Encountered process execution error %(err)s while " + "starting process in namespace %(ns)s", + {"err": exec_err, "ns": ns_name}) + return + monitor.register(uuid, METADATA_SERVICE_NAME, pm) + + @classmethod + def destroy_monitored_metadata_proxy(cls, monitor, uuid, conf, ns_name): + monitor.unregister(uuid, METADATA_SERVICE_NAME) + pm = cls._get_metadata_proxy_process_manager(uuid, conf, + ns_name=ns_name) + pm.disable(sig=str(int(signal.SIGTERM))) + try: + common_utils.wait_until_true(lambda: not pm.active, + timeout=SIGTERM_TIMEOUT) + except common_utils.WaitTimeout: + LOG.warning('Metadata process %s did not finish after SIGTERM ' + 'signal in %s seconds, sending SIGKILL signal', + pm.pid, SIGTERM_TIMEOUT) + pm.disable(sig=str(int(signal.SIGKILL))) + + # Delete metadata proxy config. + configurator = cls.haproxy_configurator() + configurator.cleanup_config_file(uuid, cfg.CONF.state_path) + + @classmethod + def _get_metadata_proxy_process_manager(cls, router_id, conf, ns_name=None, + callback=None): + return external_process.ProcessManager( + conf=conf, + uuid=router_id, + namespace=ns_name, + service=HAPROXY_SERVICE, + default_cmd_callback=callback) diff --git a/neutron/agent/ovn/metadata/driver.py b/neutron/agent/ovn/metadata/driver.py index 76b87077430..f965bedae1e 100644 --- a/neutron/agent/ovn/metadata/driver.py +++ b/neutron/agent/ovn/metadata/driver.py @@ -13,235 +13,19 @@ # License for the specific language governing permissions and limitations # under the License. -import errno -import grp -import os -import pwd - -from neutron.agent.linux import external_process -from neutron.agent.linux import ip_lib -from neutron_lib import exceptions -from oslo_config import cfg -from oslo_log import log as logging - -from neutron._i18n import _ -from neutron.common import metadata as comm_meta - -LOG = logging.getLogger(__name__) - -METADATA_SERVICE_NAME = 'metadata-proxy' -HAPROXY_SERVICE = 'haproxy' - -PROXY_CONFIG_DIR = "ovn-metadata-proxy" +from neutron.agent.metadata import driver_base _HEADER_CONFIG_TEMPLATE = """ http-request add-header X-OVN-%(res_type)s-ID %(res_id)s """ -_UNLIMITED_CONFIG_TEMPLATE = """ -listen listener - bind %(host)s:%(port)s - %(bind_v6_line)s - server metadata %(unix_socket_path)s -""" + +class HaproxyConfigurator(driver_base.HaproxyConfiguratorBase): + PROXY_CONFIG_DIR = "ovn-metadata-proxy" + HEADER_CONFIG_TEMPLATE = _HEADER_CONFIG_TEMPLATE -class HaproxyConfigurator(object): - def __init__(self, network_id, router_id, unix_socket_path, host, - port, user, group, state_path, pid_file, - rate_limiting_config, host_v6=None, - bind_interface=None): - self.network_id = network_id - self.router_id = router_id - if network_id is None and router_id is None: - raise exceptions.NetworkIdOrRouterIdRequiredError() - - self.host = host - self.host_v6 = host_v6 - self.bind_interface = bind_interface - self.port = port - self.user = user - self.group = group - self.state_path = state_path - self.unix_socket_path = unix_socket_path - self.pidfile = pid_file - self.rate_limiting_config = rate_limiting_config - self.log_level = ( - 'debug' if logging.is_debug_enabled(cfg.CONF) else 'info') - # log-tag will cause entries to have the string pre-pended, so use - # the uuid haproxy will be started with. Additionally, if it - # starts with "haproxy" then things will get logged to - # /var/log/haproxy.log on Debian distros, instead of to syslog. - uuid = network_id or router_id - self.log_tag = 'haproxy-{}-{}'.format(METADATA_SERVICE_NAME, uuid) - - def create_config_file(self): - """Create the config file for haproxy.""" - # Need to convert uid/gid into username/group - try: - username = pwd.getpwuid(int(self.user)).pw_name - except (ValueError, KeyError): - try: - username = pwd.getpwnam(self.user).pw_name - except KeyError: - raise comm_meta.InvalidUserOrGroupException( - _("Invalid user/uid: '%s'") % self.user) - - try: - groupname = grp.getgrgid(int(self.group)).gr_name - except (ValueError, KeyError): - try: - groupname = grp.getgrnam(self.group).gr_name - except KeyError: - raise comm_meta.InvalidUserOrGroupException( - _("Invalid group/gid: '%s'") % self.group) - - cfg_info = { - 'host': self.host, - 'port': self.port, - 'unix_socket_path': self.unix_socket_path, - 'user': username, - 'group': groupname, - 'pidfile': self.pidfile, - 'log_level': self.log_level, - 'log_tag': self.log_tag, - 'bind_v6_line': '', - } - if self.host_v6 and self.bind_interface: - cfg_info['bind_v6_line'] = ( - 'bind %s:%s interface %s' % ( - self.host_v6, self.port, self.bind_interface) - ) - if self.network_id: - cfg_info['res_type'] = 'Network' - cfg_info['res_id'] = self.network_id - else: - cfg_info['res_type'] = 'Router' - cfg_info['res_id'] = self.router_id - - haproxy_cfg = comm_meta.get_haproxy_config(cfg_info, - self.rate_limiting_config, - _HEADER_CONFIG_TEMPLATE, - _UNLIMITED_CONFIG_TEMPLATE) - - LOG.debug("haproxy_cfg = %s", haproxy_cfg) - cfg_dir = self.get_config_path(self.state_path) - # uuid has to be included somewhere in the command line so that it can - # be tracked by process_monitor. - self.cfg_path = os.path.join(cfg_dir, "%s.conf" % cfg_info['res_id']) - if not os.path.exists(cfg_dir): - os.makedirs(cfg_dir) - with open(self.cfg_path, "w") as cfg_file: - cfg_file.write(haproxy_cfg) - +class MetadataDriver(driver_base.MetadataDriverBase): @staticmethod - def get_config_path(state_path): - return os.path.join(state_path or cfg.CONF.state_path, - PROXY_CONFIG_DIR) - - @staticmethod - def cleanup_config_file(uuid, state_path): - """Delete config file created when metadata proxy was spawned.""" - # Delete config file if it exists - cfg_path = os.path.join( - HaproxyConfigurator.get_config_path(state_path), - "%s.conf" % uuid) - try: - os.unlink(cfg_path) - except OSError as ex: - # It can happen that this function is called but metadata proxy - # was never spawned so its config file won't exist - if ex.errno != errno.ENOENT: - raise - - -class MetadataDriver(object): - - monitors = {} - - @classmethod - def _get_metadata_proxy_user_group(cls, conf): - user = conf.metadata_proxy_user or str(os.geteuid()) - group = conf.metadata_proxy_group or str(os.getegid()) - - return user, group - - @classmethod - def _get_metadata_proxy_callback(cls, bind_address, port, conf, - network_id=None, router_id=None, - bind_address_v6=None, - bind_interface=None): - def callback(pid_file): - metadata_proxy_socket = conf.metadata_proxy_socket - user, group = ( - cls._get_metadata_proxy_user_group(conf)) - haproxy = HaproxyConfigurator(network_id, - router_id, - metadata_proxy_socket, - bind_address, - port, - user, - group, - conf.state_path, - pid_file, - conf.metadata_rate_limiting, - bind_address_v6, - bind_interface) - haproxy.create_config_file() - proxy_cmd = [HAPROXY_SERVICE, - '-f', haproxy.cfg_path] - return proxy_cmd - - return callback - - @classmethod - def spawn_monitored_metadata_proxy(cls, monitor, ns_name, port, conf, - bind_address="0.0.0.0", network_id=None, - router_id=None, bind_address_v6=None, - bind_interface=None): - uuid = network_id or router_id - callback = cls._get_metadata_proxy_callback( - bind_address, port, conf, network_id=network_id, - router_id=router_id, bind_address_v6=bind_address_v6, - bind_interface=bind_interface) - pm = cls._get_metadata_proxy_process_manager(uuid, conf, - ns_name=ns_name, - callback=callback) - if bind_interface is not None and bind_address_v6 is not None: - # HAProxy cannot bind() until IPv6 Duplicate Address Detection - # completes. We must wait until the address leaves its 'tentative' - # state. - ip_lib.IpAddrCommand( - parent=ip_lib.IPDevice(name=bind_interface, namespace=ns_name) - ).wait_until_address_ready(address=bind_address_v6) - try: - pm.enable(ensure_active=True) - except exceptions.ProcessExecutionError as exec_err: - LOG.error("Encountered process execution error %(err)s while " - "starting process in namespace %(ns)s", - {"err": exec_err, "ns": ns_name}) - return - monitor.register(uuid, METADATA_SERVICE_NAME, pm) - cls.monitors[router_id] = pm - - @classmethod - def destroy_monitored_metadata_proxy(cls, monitor, uuid, conf, ns_name): - monitor.unregister(uuid, METADATA_SERVICE_NAME) - pm = cls._get_metadata_proxy_process_manager(uuid, conf, - ns_name=ns_name) - pm.disable() - - # Delete metadata proxy config file - HaproxyConfigurator.cleanup_config_file(uuid, cfg.CONF.state_path) - - cls.monitors.pop(uuid, None) - - @classmethod - def _get_metadata_proxy_process_manager(cls, router_id, conf, ns_name=None, - callback=None): - return external_process.ProcessManager( - conf=conf, - uuid=router_id, - namespace=ns_name, - service=HAPROXY_SERVICE, - default_cmd_callback=callback) + def haproxy_configurator(): + return HaproxyConfigurator diff --git a/neutron/tests/functional/agent/l3/framework.py b/neutron/tests/functional/agent/l3/framework.py index 2829583a976..005cd0fc733 100644 --- a/neutron/tests/functional/agent/l3/framework.py +++ b/neutron/tests/functional/agent/l3/framework.py @@ -34,7 +34,7 @@ from neutron.agent import l3_agent as l3_agent_main from neutron.agent.linux import external_process from neutron.agent.linux import ip_lib from neutron.agent.linux import keepalived -from neutron.agent.metadata import driver as metadata_driver +from neutron.agent.metadata import driver_base from neutron.common import utils as common_utils from neutron.conf.agent import common as agent_config from neutron.conf.agent.l3 import config as l3_config @@ -443,7 +443,7 @@ class L3AgentTestFramework(base.BaseSudoTestCase): conf, router.router_id, router.ns_name, - service=metadata_driver.HAPROXY_SERVICE) + service=driver_base.HAPROXY_SERVICE) def _metadata_proxy_exists(self, conf, router): pm = self._metadata_proxy(conf, router) diff --git a/neutron/tests/functional/agent/test_dhcp_agent.py b/neutron/tests/functional/agent/test_dhcp_agent.py index 47f38115e3f..717ae861e06 100644 --- a/neutron/tests/functional/agent/test_dhcp_agent.py +++ b/neutron/tests/functional/agent/test_dhcp_agent.py @@ -34,7 +34,7 @@ from neutron.agent.linux import external_process from neutron.agent.linux import interface from neutron.agent.linux import ip_lib from neutron.agent.linux import utils -from neutron.agent.metadata import driver as metadata_driver +from neutron.agent.metadata import driver_base from neutron.common import utils as common_utils from neutron.conf.agent import common as config from neutron.tests.common import net_helpers @@ -272,7 +272,7 @@ class DHCPAgentOVSTestFramework(base.BaseSudoTestCase): self.conf, network.id, network.namespace, - service=metadata_driver.HAPROXY_SERVICE) + service=driver_base.HAPROXY_SERVICE) class DHCPAgentOVSTestCase(DHCPAgentOVSTestFramework): diff --git a/neutron/tests/unit/agent/metadata/test_driver.py b/neutron/tests/unit/agent/metadata/test_driver.py index 5847b9047e2..707d7f702d5 100644 --- a/neutron/tests/unit/agent/metadata/test_driver.py +++ b/neutron/tests/unit/agent/metadata/test_driver.py @@ -30,6 +30,7 @@ from neutron.agent.linux import ip_lib from neutron.agent.linux import iptables_manager from neutron.agent.linux import utils as linux_utils from neutron.agent.metadata import driver as metadata_driver +from neutron.agent.metadata import driver_base from neutron.common import metadata as comm_meta from neutron.conf.agent import common as agent_config from neutron.conf.agent.l3 import config as l3_config @@ -54,14 +55,14 @@ class TestMetadataDriverRules(base.BaseTestCase): '-p tcp -m tcp --dport 80 -j REDIRECT --to-ports 9697') self.assertEqual( [rules], - metadata_driver.MetadataDriver.metadata_nat_rules(9697)) + metadata_driver.metadata_nat_rules(9697)) def test_metadata_nat_rules_ipv6(self): rules = ('PREROUTING', '-d fe80::a9fe:a9fe/128 -i qr-+ ' '-p tcp -m tcp --dport 80 -j REDIRECT --to-ports 9697') self.assertEqual( [rules], - metadata_driver.MetadataDriver.metadata_nat_rules( + metadata_driver.metadata_nat_rules( 9697, metadata_address='fe80::a9fe:a9fe/128')) def test_metadata_filter_rules(self): @@ -70,7 +71,7 @@ class TestMetadataDriverRules(base.BaseTestCase): ('INPUT', '-p tcp -m tcp --dport 9697 -j DROP')] self.assertEqual( rules, - metadata_driver.MetadataDriver.metadata_filter_rules(9697, '0x1')) + metadata_driver.metadata_filter_rules(9697, '0x1')) class TestMetadataDriverProcess(base.BaseTestCase): @@ -216,7 +217,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): service_name, '-f', cfg_file] - log_tag = ("haproxy-" + metadata_driver.METADATA_SERVICE_NAME + + log_tag = ("haproxy-" + driver_base.METADATA_SERVICE_NAME + "-" + router_id) expected_params = { @@ -251,7 +252,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): else: expected_config_template = ( comm_meta.METADATA_HAPROXY_GLOBAL + - metadata_driver._UNLIMITED_CONFIG_TEMPLATE + + driver_base._UNLIMITED_CONFIG_TEMPLATE + metadata_driver._HEADER_CONFIG_TEMPLATE) mock_open.assert_has_calls([ @@ -267,7 +268,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): ]) agent.process_monitor.register.assert_called_once_with( - router_id, metadata_driver.METADATA_SERVICE_NAME, + router_id, driver_base.METADATA_SERVICE_NAME, mock.ANY) self.delete_if_exists.assert_called_once_with( @@ -293,7 +294,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): def test_spawn_metadata_proxy_dad_failed(self): self._test_spawn_metadata_proxy(dad_failed=True) - @mock.patch.object(metadata_driver.LOG, 'error') + @mock.patch.object(driver_base.LOG, 'error') def test_spawn_metadata_proxy_handles_process_exception(self, error_log): process_instance = mock.Mock(active=False) process_instance.enable.side_effect = ( @@ -311,7 +312,6 @@ class TestMetadataDriverProcess(base.BaseTestCase): network_id=network_id) error_log.assert_called_once() process_monitor.register.assert_not_called() - self.assertNotIn(network_id, metadata_driver.MetadataDriver.monitors) def test_create_config_file_wrong_user(self): with mock.patch('pwd.getpwnam', side_effect=KeyError): @@ -356,7 +356,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): mock_get_process = self.mock_get_process.start() mock_get_process.return_value = mproxy_process driver = metadata_driver.MetadataDriver(FakeL3NATAgent()) - with mock.patch.object(metadata_driver, 'SIGTERM_TIMEOUT', 0): + with mock.patch.object(driver_base, 'SIGTERM_TIMEOUT', 0): driver.destroy_monitored_metadata_proxy(mock.Mock(), 'uuid', 'conf', 'ns_name') mproxy_process.disable.assert_has_calls([ diff --git a/neutron/tests/unit/agent/ovn/metadata/test_agent.py b/neutron/tests/unit/agent/ovn/metadata/test_agent.py index cb6ee43a5b6..0ed5bbb0a1d 100644 --- a/neutron/tests/unit/agent/ovn/metadata/test_agent.py +++ b/neutron/tests/unit/agent/ovn/metadata/test_agent.py @@ -26,6 +26,7 @@ from neutron.agent.linux.ip_lib import IpAddrCommand as ip_addr from neutron.agent.linux.ip_lib import IpLinkCommand as ip_link from neutron.agent.linux.ip_lib import IpNetnsCommand as ip_netns from neutron.agent.linux.ip_lib import IPWrapper as ip_wrap +from neutron.agent.linux import utils as linux_utils from neutron.agent.ovn.metadata import agent from neutron.agent.ovn.metadata import driver from neutron.common.ovn import constants as ovn_const @@ -451,6 +452,8 @@ class TestMetadataAgent(base.BaseTestCase): ip_wrap, 'add_veth', return_value=[ip_lib.IPDevice('ip1'), ip_lib.IPDevice('ip2')]) as add_veth,\ + mock.patch.object( + linux_utils, 'delete_if_exists') as mock_delete,\ mock.patch.object( driver.MetadataDriver, 'spawn_monitored_metadata_proxy') as spawn_mdp, \ @@ -486,6 +489,7 @@ class TestMetadataAgent(base.BaseTestCase): self.assertCountEqual(expected_call, ip_addr_add_multiple.call_args.args[0]) # Check that metadata proxy has been spawned + mock_delete.assert_called_once_with(mock.ANY, run_as_root=True) spawn_mdp.assert_called_once_with( mock.ANY, nemaspace_name, 80, mock.ANY, bind_address=n_const.METADATA_V4_IP, network_id=net_name, diff --git a/neutron/tests/unit/agent/ovn/metadata/test_driver.py b/neutron/tests/unit/agent/ovn/metadata/test_driver.py index 799ee2b297a..190ae0ac3de 100644 --- a/neutron/tests/unit/agent/ovn/metadata/test_driver.py +++ b/neutron/tests/unit/agent/ovn/metadata/test_driver.py @@ -23,6 +23,7 @@ from oslo_utils import uuidutils from neutron.agent.linux import external_process as ep from neutron.agent.linux import utils as linux_utils +from neutron.agent.metadata import driver_base from neutron.agent.ovn.metadata import agent as metadata_agent from neutron.agent.ovn.metadata import driver as metadata_driver from neutron.common import metadata as comm_meta @@ -40,6 +41,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): EUNAME = 'neutron' EGNAME = 'neutron' METADATA_DEFAULT_IP = '169.254.169.254' + METADATA_DEFAULT_IPV6 = 'fe80::a9fe:a9fe' METADATA_PORT = 8080 METADATA_SOCKET = '/socket/path' PIDFILE = 'pidfile' @@ -99,27 +101,36 @@ class TestMetadataDriverProcess(base.BaseTestCase): 'neutron.agent.linux.external_process.' 'ProcessManager.active', new_callable=mock.PropertyMock, - side_effect=[False, True]): + side_effect=[False, True]),\ + mock.patch( + 'neutron.agent.linux.ip_lib.' + 'IpAddrCommand.wait_until_address_ready', + return_value=True): cfg_file = os.path.join( metadata_driver.HaproxyConfigurator.get_config_path( - cfg.CONF.state_path), + agent.conf.state_path), "%s.conf" % datapath_id) mock_open = self.useFixture( lib_fixtures.OpenFixture(cfg_file)).mock_open - metadata_driver.MetadataDriver.spawn_monitored_metadata_proxy( + bind_v6_line = 'bind %s:%s interface %s' % ( + self.METADATA_DEFAULT_IPV6, self.METADATA_PORT, 'fake-if') + proxy = metadata_driver.MetadataDriver() + proxy.spawn_monitored_metadata_proxy( agent._process_monitor, metadata_ns, self.METADATA_PORT, - cfg.CONF, + agent.conf, bind_address=self.METADATA_DEFAULT_IP, - network_id=datapath_id) + network_id=datapath_id, + bind_address_v6=self.METADATA_DEFAULT_IPV6, + bind_interface='fake-if') netns_execute_args = [ service_name, '-f', cfg_file] log_tag = '{}-{}-{}'.format( - service_name, metadata_driver.METADATA_SERVICE_NAME, + service_name, driver_base.METADATA_SERVICE_NAME, datapath_id) expected_params = { @@ -133,7 +144,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): 'pidfile': self.PIDFILE, 'log_level': 'debug', 'log_tag': log_tag, - 'bind_v6_line': ''} + 'bind_v6_line': bind_v6_line} if rate_limited: expected_params.update(self.RATE_LIMIT_CONFIG, @@ -146,7 +157,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): else: expected_config_template = ( comm_meta.METADATA_HAPROXY_GLOBAL + - metadata_driver._UNLIMITED_CONFIG_TEMPLATE + + driver_base._UNLIMITED_CONFIG_TEMPLATE + metadata_driver._HEADER_CONFIG_TEMPLATE) cfg_contents = expected_config_template % expected_params @@ -165,7 +176,7 @@ class TestMetadataDriverProcess(base.BaseTestCase): self.delete_if_exists.assert_called_once_with( mock.ANY, run_as_root=True) - @mock.patch.object(metadata_driver.LOG, 'error') + @mock.patch.object(driver_base.LOG, 'error') def test_spawn_metadata_proxy_handles_process_exception(self, error_log): process_instance = mock.Mock(active=False) process_instance.enable.side_effect = ( @@ -186,7 +197,6 @@ class TestMetadataDriverProcess(base.BaseTestCase): error_log.assert_called_once() process_monitor.register.assert_not_called() - self.assertNotIn(network_id, metadata_driver.MetadataDriver.monitors) def test_create_config_file_wrong_user(self): with mock.patch('pwd.getpwnam', side_effect=KeyError):