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
This commit is contained in:
Brian Haley 2023-10-21 19:34:45 -04:00 committed by Slawek Kaplonski
parent 2f7f7c2fc2
commit 63f690e6fd
8 changed files with 359 additions and 522 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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