From 3b22541a2aa9a5b06e2bff256701dbe24554c17c Mon Sep 17 00:00:00 2001 From: Daniel Alvarez Date: Thu, 9 Feb 2017 18:30:23 +0000 Subject: [PATCH] Switch ns-metadata-proxy to haproxy Due to the high memory footprint of current Python ns-metadata-proxy, it has to be replaced with a lighter process to avoid OOM conditions in large environments. This patch spawns haproxy through a process monitor using a pidfile. This allows tracking the process and respawn it if necessary as it was done before. Also, it implements an upgrade path which consists of detecting any running Python instance of ns-metadata-proxy and replacing them by haproxy. Therefore, upgrades will take place by simply restarting neutron-l3-agent and neutron-dhcp-agent. According to /proc//smaps, memory footprint goes down from ~50MB to ~1.5MB. Also, haproxy is added to bindep in order to ensure that it's installed. UpgradeImpact Depends-On: I36a5531cacc21c0d4bb7f20d4bec6da65d04c262 Depends-On: Ia37368a7ff38ea48c683a7bad76f87697e194b04 Closes-Bug: #1524916 Change-Id: I5a75cc582dca48defafb440207d10e2f7b4f218b --- bindep.txt | 1 + etc/neutron/rootwrap.d/dhcp.filters | 8 +- etc/neutron/rootwrap.d/l3.filters | 8 +- neutron/agent/common/config.py | 2 - neutron/agent/dhcp_agent.py | 1 - neutron/agent/l3_agent.py | 1 - neutron/agent/linux/external_process.py | 13 +- neutron/agent/metadata/driver.py | 198 +++++++++-- neutron/agent/metadata/namespace_proxy.py | 159 --------- neutron/cmd/eventlet/agents/metadata_proxy.py | 17 - neutron/conf/agent/metadata/config.py | 14 - .../conf/agent/metadata/namespace_proxy.py | 54 --- neutron/opts.py | 3 +- .../tests/functional/agent/l3/framework.py | 2 - .../agent/l3/test_metadata_proxy.py | 59 +++- neutron/tests/unit/agent/dhcp/test_agent.py | 8 +- .../unit/agent/linux/test_external_process.py | 41 ++- .../tests/unit/agent/metadata/test_driver.py | 141 +++++--- .../agent/metadata/test_namespace_proxy.py | 313 ------------------ ...y-for-metadata-proxy-9d8f7549fadf9182.yaml | 12 + setup.cfg | 1 - 21 files changed, 376 insertions(+), 680 deletions(-) delete mode 100644 neutron/agent/metadata/namespace_proxy.py delete mode 100644 neutron/cmd/eventlet/agents/metadata_proxy.py delete mode 100644 neutron/conf/agent/metadata/namespace_proxy.py delete mode 100644 neutron/tests/unit/agent/metadata/test_namespace_proxy.py create mode 100644 releasenotes/notes/switching-to-haproxy-for-metadata-proxy-9d8f7549fadf9182.yaml diff --git a/bindep.txt b/bindep.txt index 7e1f8dd536b..fd9b2172eff 100644 --- a/bindep.txt +++ b/bindep.txt @@ -8,6 +8,7 @@ gettext [test] # OpenStack infra that need these like # periodic-neutron-py27-with-oslo-master and # periodic-neutron-py35-with-neutron-lib-master. +haproxy libmysqlclient-dev [platform:dpkg test] mysql [platform:rpm test] mysql-client [platform:dpkg test] diff --git a/etc/neutron/rootwrap.d/dhcp.filters b/etc/neutron/rootwrap.d/dhcp.filters index 24404f59975..d48d2eac2be 100644 --- a/etc/neutron/rootwrap.d/dhcp.filters +++ b/etc/neutron/rootwrap.d/dhcp.filters @@ -22,9 +22,13 @@ mm-ctl: CommandFilter, mm-ctl, root dhcp_release: CommandFilter, dhcp_release, root dhcp_release6: CommandFilter, dhcp_release6, root -# metadata proxy -metadata_proxy: CommandFilter, neutron-ns-metadata-proxy, root +# haproxy +haproxy: RegExpFilter, haproxy, root, haproxy, -f, .* +kill_haproxy: KillFilter, root, haproxy, -15, -9, -HUP # RHEL invocation of the metadata proxy will report /usr/bin/python +# TODO(dalvarez): Remove kill_metadata* filters in Q release since +# neutron-ns-metadata-proxy is now replaced by haproxy. We keep them for now +# for the migration process kill_metadata: KillFilter, root, python, -9 kill_metadata7: KillFilter, root, python2.7, -9 kill_metadata35: KillFilter, root, python3.5, -9 diff --git a/etc/neutron/rootwrap.d/l3.filters b/etc/neutron/rootwrap.d/l3.filters index de4590ecf71..a0a86e600c4 100644 --- a/etc/neutron/rootwrap.d/l3.filters +++ b/etc/neutron/rootwrap.d/l3.filters @@ -16,9 +16,13 @@ sysctl: CommandFilter, sysctl, root route: CommandFilter, route, root radvd: CommandFilter, radvd, root -# metadata proxy -metadata_proxy: CommandFilter, neutron-ns-metadata-proxy, root +# haproxy +haproxy: RegExpFilter, haproxy, root, haproxy, -f, .* +kill_haproxy: KillFilter, root, haproxy, -15, -9, -HUP # RHEL invocation of the metadata proxy will report /usr/bin/python +# TODO(dalvarez): Remove kill_metadata* filters in Q release since +# neutron-ns-metadata-proxy is now replaced by haproxy. We keep them for now +# for the migration process kill_metadata: KillFilter, root, python, -15, -9 kill_metadata7: KillFilter, root, python2.7, -15, -9 kill_metadata35: KillFilter, root, python3.5, -15, -9 diff --git a/neutron/agent/common/config.py b/neutron/agent/common/config.py index 54f606168f5..9bffe7a76b5 100644 --- a/neutron/agent/common/config.py +++ b/neutron/agent/common/config.py @@ -122,8 +122,6 @@ def get_log_args(conf, log_file_name, **kwargs): log_dir = os.path.dirname(conf.log_file) if log_dir: cmd_args.append('--log-dir=%s' % log_dir) - if kwargs.get('metadata_proxy_watch_log') is False: - cmd_args.append('--nometadata_proxy_watch_log') else: if conf.use_syslog: cmd_args.append('--use-syslog') diff --git a/neutron/agent/dhcp_agent.py b/neutron/agent/dhcp_agent.py index a8269d8ac55..ff34405019e 100644 --- a/neutron/agent/dhcp_agent.py +++ b/neutron/agent/dhcp_agent.py @@ -33,7 +33,6 @@ def register_options(conf): config.register_agent_state_opts_helper(conf) config.register_availability_zone_opts_helper(conf) dhcp_config.register_agent_dhcp_opts(conf) - meta_conf.register_meta_conf_opts(meta_conf.DRIVER_OPTS, conf) meta_conf.register_meta_conf_opts(meta_conf.SHARED_OPTS, conf) conf.register_opts(interface.OPTS) diff --git a/neutron/agent/l3_agent.py b/neutron/agent/l3_agent.py index eadf490389b..810bae56f94 100644 --- a/neutron/agent/l3_agent.py +++ b/neutron/agent/l3_agent.py @@ -35,7 +35,6 @@ from neutron import service as neutron_service def register_opts(conf): l3_config.register_l3_agent_config_opts(l3_config.OPTS, conf) ha_conf.register_l3_agent_ha_opts(conf) - meta_conf.register_meta_conf_opts(meta_conf.DRIVER_OPTS, conf) meta_conf.register_meta_conf_opts(meta_conf.SHARED_OPTS, conf) config.register_interface_driver_opts_helper(conf) config.register_agent_state_opts_helper(conf) diff --git a/neutron/agent/linux/external_process.py b/neutron/agent/linux/external_process.py index 4dc3e642f56..4e4946228d9 100644 --- a/neutron/agent/linux/external_process.py +++ b/neutron/agent/linux/external_process.py @@ -138,16 +138,21 @@ class ProcessManager(MonitoredProcess): @property def active(self): + cmdline = self.cmdline + return self.uuid in cmdline if cmdline else False + + @property + def cmdline(self): pid = self.pid - if pid is None: - return False + if not pid: + return cmdline = '/proc/%s/cmdline' % pid try: with open(cmdline, "r") as f: - return self.uuid in f.readline() + return f.readline() except IOError: - return False + return ServiceId = collections.namedtuple('ServiceId', ['uuid', 'service']) diff --git a/neutron/agent/metadata/driver.py b/neutron/agent/metadata/driver.py index 92e4ba287be..5dd963eb360 100644 --- a/neutron/agent/metadata/driver.py +++ b/neutron/agent/metadata/driver.py @@ -13,23 +13,147 @@ # License for the specific language governing permissions and limitations # under the License. +import errno +import grp import os +import pwd -from neutron.agent.common import config +from oslo_config import cfg +from oslo_log import log as logging + +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 utils from neutron.callbacks import events from neutron.callbacks import registry from neutron.callbacks import resources from neutron.common import constants from neutron.common import exceptions +LOG = logging.getLogger(__name__) # Access with redirection to metadata proxy iptables mark mask METADATA_SERVICE_NAME = 'metadata-proxy' +PROXY_CONFIG_DIR = "ns-metadata-proxy" +_HAPROXY_CONFIG_TEMPLATE = """ +global + log /dev/log local0 %(log_level)s + user %(user)s + group %(group)s + maxconn 1024 + pidfile %(pidfile)s + daemon + +defaults + log global + mode http + option httplog + option dontlognull + option http-server-close + option forwardfor + retries 3 + timeout http-request 30s + timeout connect 30s + timeout client 32s + timeout server 32s + timeout http-keep-alive 30s + +listen listener + bind 0.0.0.0:%(port)s + server metadata %(unix_socket_path)s + http-request add-header X-Neutron-%(res_type)s-ID %(res_id)s +""" + + +class InvalidUserOrGroupException(Exception): + pass + + +class HaproxyConfigurator(object): + def __init__(self, network_id, router_id, unix_socket_path, port, user, + group, state_path, pid_file): + self.network_id = network_id + self.router_id = router_id + if network_id is None and router_id is None: + raise exceptions.NetworkIdOrRouterIdRequiredError() + + 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.log_level = 'debug' if cfg.CONF.debug else 'info' + + 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 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 InvalidUserOrGroupException( + _("Invalid group/gid: '%s'") % self.group) + + cfg_info = { + 'port': self.port, + 'unix_socket_path': self.unix_socket_path, + 'user': username, + 'group': groupname, + 'pidfile': self.pidfile, + 'log_level': self.log_level + } + 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 = _HAPROXY_CONFIG_TEMPLATE % cfg_info + 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) + 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): @@ -72,45 +196,30 @@ class MetadataDriver(object): 'port': port})] @classmethod - def _get_metadata_proxy_user_group_watchlog(cls, conf): + 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()) - watch_log = conf.metadata_proxy_watch_log - if watch_log is None: - # NOTE(cbrandily): Commonly, log watching can be enabled only - # when metadata proxy user is agent effective user (id/name). - watch_log = utils.is_effective_user(user) - - return user, group, watch_log + return user, group @classmethod def _get_metadata_proxy_callback(cls, port, conf, network_id=None, router_id=None): - uuid = network_id or router_id - if uuid is None: - raise exceptions.NetworkIdOrRouterIdRequiredError() - - if network_id: - lookup_param = '--network_id=%s' % network_id - else: - lookup_param = '--router_id=%s' % router_id - def callback(pid_file): metadata_proxy_socket = conf.metadata_proxy_socket - user, group, watch_log = ( - cls._get_metadata_proxy_user_group_watchlog(conf)) - proxy_cmd = ['neutron-ns-metadata-proxy', - '--pid_file=%s' % pid_file, - '--metadata_proxy_socket=%s' % metadata_proxy_socket, - lookup_param, - '--state_path=%s' % conf.state_path, - '--metadata_port=%s' % port, - '--metadata_proxy_user=%s' % user, - '--metadata_proxy_group=%s' % group] - proxy_cmd.extend(config.get_log_args( - conf, 'neutron-ns-metadata-proxy-%s.log' % uuid, - metadata_proxy_watch_log=watch_log)) + user, group = ( + cls._get_metadata_proxy_user_group(conf)) + haproxy = HaproxyConfigurator(network_id, + router_id, + metadata_proxy_socket, + port, + user, + group, + conf.state_path, + pid_file) + haproxy.create_config_file() + proxy_cmd = ['haproxy', + '-f', haproxy.cfg_path] return proxy_cmd return callback @@ -124,16 +233,41 @@ class MetadataDriver(object): pm = cls._get_metadata_proxy_process_manager(uuid, conf, ns_name=ns_name, callback=callback) + # TODO(dalvarez): Remove in Q cycle. This will kill running instances + # of old ns-metadata-proxy Python version in order to be replaced by + # haproxy. This will help with upgrading and shall be removed in next + # cycle. + cls._migrate_python_ns_metadata_proxy_if_needed(pm) + pm.enable() monitor.register(uuid, METADATA_SERVICE_NAME, pm) cls.monitors[router_id] = pm + @staticmethod + def _migrate_python_ns_metadata_proxy_if_needed(pm): + """Kill running Python version of ns-metadata-proxy. + + This function will detect if the current metadata proxy process is + running the old Python version and kill it so that the new haproxy + version is spawned instead. + """ + # Read cmdline to a local var to avoid reading twice from /proc file + cmdline = pm.cmdline + if cmdline and 'haproxy' not in cmdline: + LOG.debug("Migrating old instance of python ns-metadata proxy to " + "new one based on haproxy (%s)", cmdline) + pm.disable() + @classmethod def destroy_monitored_metadata_proxy(cls, monitor, uuid, conf): monitor.unregister(uuid, METADATA_SERVICE_NAME) # No need to pass ns name as it's not needed for disable() pm = cls._get_metadata_proxy_process_manager(uuid, conf) pm.disable() + + # Delete metadata proxy config file + HaproxyConfigurator.cleanup_config_file(uuid, cfg.CONF.state_path) + cls.monitors.pop(uuid, None) @classmethod diff --git a/neutron/agent/metadata/namespace_proxy.py b/neutron/agent/metadata/namespace_proxy.py deleted file mode 100644 index 7a484de0f48..00000000000 --- a/neutron/agent/metadata/namespace_proxy.py +++ /dev/null @@ -1,159 +0,0 @@ -# Copyright 2012 New Dream Network, LLC (DreamHost) -# -# 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 httplib2 -from oslo_config import cfg -from oslo_log import log as logging -from oslo_service import wsgi as base_wsgi -from oslo_utils import encodeutils -import six -import six.moves.urllib.parse as urlparse -import webob - -from neutron._i18n import _, _LE -from neutron.agent.linux import daemon -from neutron.agent.linux import utils as agent_utils -from neutron.common import config -from neutron.common import exceptions -from neutron.common import utils -from neutron.conf.agent.metadata import namespace_proxy as namespace -from neutron import wsgi - -LOG = logging.getLogger(__name__) - - -class NetworkMetadataProxyHandler(object): - """Proxy AF_INET metadata request through Unix Domain socket. - - The Unix domain socket allows the proxy access resource that are not - accessible within the isolated tenant context. - """ - - def __init__(self, network_id=None, router_id=None): - self.network_id = network_id - self.router_id = router_id - - if network_id is None and router_id is None: - raise exceptions.NetworkIdOrRouterIdRequiredError() - - @webob.dec.wsgify(RequestClass=base_wsgi.Request) - def __call__(self, req): - LOG.debug("Request: %s", req) - try: - return self._proxy_request(req.remote_addr, - req.method, - req.path_info, - req.query_string, - req.body) - except Exception: - LOG.exception(_LE("Unexpected error.")) - msg = _('An unknown error has occurred. ' - 'Please try your request again.') - explanation = six.text_type(msg) - return webob.exc.HTTPInternalServerError(explanation=explanation) - - def _proxy_request(self, remote_address, method, path_info, - query_string, body): - headers = { - 'X-Forwarded-For': remote_address, - } - - if self.router_id: - headers['X-Neutron-Router-ID'] = self.router_id - else: - headers['X-Neutron-Network-ID'] = self.network_id - - url = urlparse.urlunsplit(( - 'http', - '169.254.169.254', # a dummy value to make the request proper - path_info, - query_string, - '')) - - h = httplib2.Http() - resp, content = h.request( - url, - method=method, - headers=headers, - body=body, - connection_type=agent_utils.UnixDomainHTTPConnection) - - if resp.status == 200: - LOG.debug(resp) - LOG.debug(encodeutils.safe_decode(content, errors='replace')) - response = webob.Response() - response.status = resp.status - response.headers['Content-Type'] = resp['content-type'] - response.body = wsgi.encode_body(content) - return response - elif resp.status == 400: - return webob.exc.HTTPBadRequest() - elif resp.status == 404: - return webob.exc.HTTPNotFound() - elif resp.status == 409: - return webob.exc.HTTPConflict() - elif resp.status == 500: - msg = _( - 'Remote metadata server experienced an internal server error.' - ) - LOG.debug(msg) - explanation = six.text_type(msg) - return webob.exc.HTTPInternalServerError(explanation=explanation) - else: - raise Exception(_('Unexpected response code: %s') % resp.status) - - -class ProxyDaemon(daemon.Daemon): - def __init__(self, pidfile, port, network_id=None, router_id=None, - user=None, group=None, watch_log=True): - uuid = network_id or router_id - super(ProxyDaemon, self).__init__(pidfile, uuid=uuid, user=user, - group=group, watch_log=watch_log) - self.network_id = network_id - self.router_id = router_id - self.port = port - - def run(self): - handler = NetworkMetadataProxyHandler( - self.network_id, - self.router_id) - proxy = wsgi.Server('neutron-network-metadata-proxy') - proxy.start(handler, self.port) - - # Drop privileges after port bind - super(ProxyDaemon, self).run() - - proxy.wait() - - -def main(): - namespace.register_namespace_proxy_opts(cfg.CONF) - # Don't read any default configuration file, just handle cmdline opts - cfg.CONF(project='neutron', - default_config_files=[], default_config_dirs=[]) - config.setup_logging() - utils.log_opt_values(LOG) - - proxy = ProxyDaemon(cfg.CONF.pid_file, - cfg.CONF.metadata_port, - network_id=cfg.CONF.network_id, - router_id=cfg.CONF.router_id, - user=cfg.CONF.metadata_proxy_user, - group=cfg.CONF.metadata_proxy_group, - watch_log=cfg.CONF.metadata_proxy_watch_log) - - if cfg.CONF.daemonize: - proxy.start() - else: - proxy.run() diff --git a/neutron/cmd/eventlet/agents/metadata_proxy.py b/neutron/cmd/eventlet/agents/metadata_proxy.py deleted file mode 100644 index dc61c144528..00000000000 --- a/neutron/cmd/eventlet/agents/metadata_proxy.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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. - -from neutron.agent.metadata import namespace_proxy - - -def main(): - namespace_proxy.main() diff --git a/neutron/conf/agent/metadata/config.py b/neutron/conf/agent/metadata/config.py index 5f521f4e7e0..f72d56d40b7 100644 --- a/neutron/conf/agent/metadata/config.py +++ b/neutron/conf/agent/metadata/config.py @@ -40,20 +40,6 @@ SHARED_OPTS = [ ] -DRIVER_OPTS = [ - cfg.BoolOpt('metadata_proxy_watch_log', - help=_("Enable/Disable log watch by metadata proxy. It " - "should be disabled when metadata_proxy_user/group " - "is not allowed to read/write its log file and " - "copytruncate logrotate option must be used if " - "logrotate is enabled on metadata proxy log " - "files. Option default value is deduced from " - "metadata_proxy_user: watch log is enabled if " - "metadata_proxy_user is agent effective user " - "id/name.")), -] - - METADATA_PROXY_HANDLER_OPTS = [ cfg.StrOpt('auth_ca_cert', help=_("Certificate Authority public key (CA cert) " diff --git a/neutron/conf/agent/metadata/namespace_proxy.py b/neutron/conf/agent/metadata/namespace_proxy.py deleted file mode 100644 index 66160c2cc93..00000000000 --- a/neutron/conf/agent/metadata/namespace_proxy.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2016 New Dream Network, LLC (DreamHost) -# -# 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. - -from oslo_config import cfg - -from neutron._i18n import _ - -OPTS = [ - cfg.StrOpt('network_id', - help=_('Network that will have instance metadata ' - 'proxied.')), - cfg.StrOpt('router_id', - help=_('Router that will have connected instances\' ' - 'metadata proxied.')), - cfg.StrOpt('pid_file', - help=_('Location of pid file of this process.')), - cfg.BoolOpt('daemonize', - default=True, - help=_('Run as daemon.')), - cfg.PortOpt('metadata_port', - default=9697, - help=_('TCP Port to listen for metadata server' - 'requests.')), - cfg.StrOpt('metadata_proxy_socket', - default='$state_path/metadata_proxy', - help=_('Location of Metadata Proxy UNIX domain ' - 'socket')), - cfg.StrOpt('metadata_proxy_user', - help=_('User (uid or name) running metadata proxy after ' - 'its initialization')), - cfg.StrOpt('metadata_proxy_group', - help=_('Group (gid or name) running metadata proxy after ' - 'its initialization')), - cfg.BoolOpt('metadata_proxy_watch_log', - default=True, - help=_('Watch file log. Log watch should be disabled when ' - 'metadata_proxy_user/group has no read/write ' - 'permissions on metadata proxy log file.')), -] - - -def register_namespace_proxy_opts(cfg=cfg.CONF): - cfg.register_cli_opts(OPTS) diff --git a/neutron/opts.py b/neutron/opts.py index 6ea48134d55..3cd9caf4548 100644 --- a/neutron/opts.py +++ b/neutron/opts.py @@ -89,8 +89,7 @@ def list_agent_opts(): ('DEFAULT', itertools.chain( neutron.agent.common.config.INTERFACE_DRIVER_OPTS, - neutron.conf.agent.metadata.config.SHARED_OPTS, - neutron.conf.agent.metadata.config.DRIVER_OPTS) + neutron.conf.agent.metadata.config.SHARED_OPTS) ) ] diff --git a/neutron/tests/functional/agent/l3/framework.py b/neutron/tests/functional/agent/l3/framework.py index 35a2b259778..27e8b8af2ec 100644 --- a/neutron/tests/functional/agent/l3/framework.py +++ b/neutron/tests/functional/agent/l3/framework.py @@ -83,8 +83,6 @@ class L3AgentTestFramework(base.BaseSudoTestCase): get_temp_file_path = functools.partial(self.get_temp_file_path, root=temp_dir) conf.set_override('state_path', temp_dir.path) - # NOTE(cbrandily): log_file or log_dir must be set otherwise - # metadata_proxy_watch_log has no effect conf.set_override('log_file', get_temp_file_path('log_file')) conf.set_override('metadata_proxy_socket', diff --git a/neutron/tests/functional/agent/l3/test_metadata_proxy.py b/neutron/tests/functional/agent/l3/test_metadata_proxy.py index fb0aa973fae..cd4e9dd3aeb 100644 --- a/neutron/tests/functional/agent/l3/test_metadata_proxy.py +++ b/neutron/tests/functional/agent/l3/test_metadata_proxy.py @@ -12,20 +12,24 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import os.path import time +import fixtures +from oslo_config import cfg import webob import webob.dec import webob.exc from neutron.agent.linux import dhcp +from neutron.agent.linux import external_process from neutron.agent.linux import utils from neutron.tests.common import machine_fixtures from neutron.tests.common import net_helpers from neutron.tests.functional.agent.l3 import framework from neutron.tests.functional.agent.linux import helpers - +from neutron.tests.functional.agent.linux import simple_daemon METADATA_REQUEST_TIMEOUT = 60 METADATA_REQUEST_SLEEP = 5 @@ -118,6 +122,57 @@ class MetadataL3AgentTestCase(framework.L3AgentTestFramework): # Check status code self.assertIn(str(webob.exc.HTTPOk.code), firstline.split()) + @staticmethod + def _make_cmdline_callback(uuid): + def _cmdline_callback(pidfile): + cmdline = ["python", simple_daemon.__file__, + "--uuid=%s" % uuid, + "--pid_file=%s" % pidfile] + return cmdline + return _cmdline_callback + + def test_haproxy_migration_path(self): + """Test the migration path for haproxy. + + This test will launch the simple_daemon Python process before spawning + haproxy. When launching haproxy, it will be detected and killed, as + it's running on the same pidfile and with the router uuid in its + cmdline. + """ + # Make sure that external_pids configuration option is the same for + # simple_daemon and haproxy so that both work on the same pid_file. + get_temp_file_path = functools.partial( + self.get_temp_file_path, + root=self.useFixture(fixtures.TempDir())) + cfg.CONF.set_override('external_pids', + get_temp_file_path('external/pids')) + self.agent.conf.set_override('external_pids', + get_temp_file_path('external/pids')) + + router_info = self.generate_router_info(enable_ha=False) + + # Spawn the simple_daemon process in the background using the generated + # router uuid. We are not registering it within ProcessMonitor so that + # it doesn't get respawned once killed. + _callback = self._make_cmdline_callback(router_info['id']) + pm = external_process.ProcessManager( + conf=cfg.CONF, + uuid=router_info['id'], + default_cmd_callback=_callback) + pm.enable() + self.addCleanup(pm.disable) + + # Make sure that simple_daemon is running + self.assertIn('simple_daemon', pm.cmdline) + + # Create the router. This is expected to launch haproxy after killing + # the simple_daemon process. + self.manage_router(self.agent, router_info) + + # Make sure that it was killed and replaced by haproxy + self.assertNotIn('simple_daemon', pm.cmdline) + self.assertIn('haproxy', pm.cmdline) + class UnprivilegedUserMetadataL3AgentTestCase(MetadataL3AgentTestCase): """Test metadata proxy with least privileged user. @@ -131,7 +186,6 @@ class UnprivilegedUserMetadataL3AgentTestCase(MetadataL3AgentTestCase): def setUp(self): super(UnprivilegedUserMetadataL3AgentTestCase, self).setUp() self.agent.conf.set_override('metadata_proxy_user', '65534') - self.agent.conf.set_override('metadata_proxy_watch_log', False) class UnprivilegedUserGroupMetadataL3AgentTestCase(MetadataL3AgentTestCase): @@ -149,4 +203,3 @@ class UnprivilegedUserGroupMetadataL3AgentTestCase(MetadataL3AgentTestCase): super(UnprivilegedUserGroupMetadataL3AgentTestCase, self).setUp() self.agent.conf.set_override('metadata_proxy_user', '65534') self.agent.conf.set_override('metadata_proxy_group', '65534') - self.agent.conf.set_override('metadata_proxy_watch_log', False) diff --git a/neutron/tests/unit/agent/dhcp/test_agent.py b/neutron/tests/unit/agent/dhcp/test_agent.py index 35cc7d12b21..b70e593ae2b 100644 --- a/neutron/tests/unit/agent/dhcp/test_agent.py +++ b/neutron/tests/unit/agent/dhcp/test_agent.py @@ -234,7 +234,9 @@ class TestDhcpAgent(base.BaseTestCase): self.driver_cls.return_value = self.driver self.mock_makedirs_p = mock.patch("os.makedirs") self.mock_makedirs = self.mock_makedirs_p.start() - + self.mock_create_metadata_proxy_cfg = mock.patch( + "neutron.agent.metadata.driver.HaproxyConfigurator") + self.mock_create_metadata_proxy_cfg.start() self.mock_ip_wrapper_p = mock.patch("neutron.agent.linux.ip_lib." "IPWrapper") self.mock_ip_wrapper = self.mock_ip_wrapper_p.start() @@ -676,7 +678,7 @@ class TestDhcpAgentEventHandler(base.BaseTestCase): if is_isolated_network and enable_isolated_metadata: self.external_process.assert_has_calls([ self._process_manager_constructor_call(), - mock.call().enable()]) + mock.call().enable()], any_order=True) else: self.external_process.assert_has_calls([ self._process_manager_constructor_call(ns=None), @@ -842,7 +844,7 @@ class TestDhcpAgentEventHandler(base.BaseTestCase): self.external_process.assert_has_calls([ self._process_manager_constructor_call(), mock.call().enable() - ]) + ], any_order=True) def test_disable_isolated_metadata_proxy(self): method_path = ('neutron.agent.metadata.driver.MetadataDriver' diff --git a/neutron/tests/unit/agent/linux/test_external_process.py b/neutron/tests/unit/agent/linux/test_external_process.py index e746bf83e2d..962a5c360cf 100644 --- a/neutron/tests/unit/agent/linux/test_external_process.py +++ b/neutron/tests/unit/agent/linux/test_external_process.py @@ -25,6 +25,7 @@ from neutron.tests import tools TEST_UUID = 'test-uuid' TEST_SERVICE = 'testsvc' TEST_PID = 1234 +TEST_CMDLINE = 'python foo --router_id=%s' class BaseTestProcessMonitor(base.BaseTestCase): @@ -264,32 +265,42 @@ class TestProcessManager(base.BaseTestCase): self.assertIsNone(manager.pid) def test_active(self): - mock_open = self.useFixture( - tools.OpenFixture('/proc/4/cmdline', 'python foo --router_id=uuid') - ).mock_open - with mock.patch.object(ep.ProcessManager, 'pid') as pid: - pid.__get__ = mock.Mock(return_value=4) + with mock.patch.object(ep.ProcessManager, 'cmdline') as cmdline: + cmdline.__get__ = mock.Mock( + return_value=TEST_CMDLINE % 'uuid') manager = ep.ProcessManager(self.conf, 'uuid') self.assertTrue(manager.active) - mock_open.assert_called_once_with('/proc/4/cmdline', 'r') - def test_active_none(self): - dummy_cmd_line = 'python foo --router_id=uuid' - self.execute.return_value = dummy_cmd_line - with mock.patch.object(ep.ProcessManager, 'pid') as pid: - pid.__get__ = mock.Mock(return_value=None) + with mock.patch.object(ep.ProcessManager, 'cmdline') as cmdline: + cmdline.__get__ = mock.Mock(return_value=None) manager = ep.ProcessManager(self.conf, 'uuid') self.assertFalse(manager.active) def test_active_cmd_mismatch(self): + with mock.patch.object(ep.ProcessManager, 'cmdline') as cmdline: + cmdline.__get__ = mock.Mock( + return_value=TEST_CMDLINE % 'anotherid') + manager = ep.ProcessManager(self.conf, 'uuid') + self.assertFalse(manager.active) + + def test_cmdline(self): mock_open = self.useFixture( - tools.OpenFixture('/proc/4/cmdline', - 'python foo --router_id=anotherid') + tools.OpenFixture('/proc/4/cmdline', TEST_CMDLINE % 'uuid') ).mock_open with mock.patch.object(ep.ProcessManager, 'pid') as pid: pid.__get__ = mock.Mock(return_value=4) manager = ep.ProcessManager(self.conf, 'uuid') - self.assertFalse(manager.active) - + self.assertEqual(TEST_CMDLINE % 'uuid', manager.cmdline) + mock_open.assert_called_once_with('/proc/4/cmdline', 'r') + + def test_cmdline_none(self): + mock_open = self.useFixture( + tools.OpenFixture('/proc/4/cmdline', TEST_CMDLINE % 'uuid') + ).mock_open + mock_open.side_effect = IOError() + with mock.patch.object(ep.ProcessManager, 'pid') as pid: + pid.__get__ = mock.Mock(return_value=4) + manager = ep.ProcessManager(self.conf, 'uuid') + self.assertIsNone(manager.cmdline) mock_open.assert_called_once_with('/proc/4/cmdline', 'r') diff --git a/neutron/tests/unit/agent/metadata/test_driver.py b/neutron/tests/unit/agent/metadata/test_driver.py index 35b951e4b5c..05e587d302a 100644 --- a/neutron/tests/unit/agent/metadata/test_driver.py +++ b/neutron/tests/unit/agent/metadata/test_driver.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +import os + import mock from oslo_config import cfg from oslo_utils import uuidutils @@ -26,7 +28,8 @@ from neutron.conf.agent.l3 import config as l3_config from neutron.conf.agent.l3 import ha as ha_conf from neutron.conf.agent.metadata import config as meta_conf from neutron.tests import base - +from neutron.tests import tools +from neutron.tests.unit.agent.linux import test_utils _uuid = uuidutils.generate_uuid @@ -60,9 +63,11 @@ class TestMetadataDriverRules(base.BaseTestCase): class TestMetadataDriverProcess(base.BaseTestCase): - EUID = 123 - EGID = 456 EUNAME = 'neutron' + EGNAME = 'neutron' + METADATA_PORT = 8080 + METADATA_SOCKET = '/socket/path' + PIDFILE = 'pidfile' def setUp(self): super(TestMetadataDriverProcess, self).setUp() @@ -78,7 +83,6 @@ class TestMetadataDriverProcess(base.BaseTestCase): l3_config.register_l3_agent_config_opts(l3_config.OPTS, cfg.CONF) ha_conf.register_l3_agent_ha_opts() meta_conf.register_meta_conf_opts(meta_conf.SHARED_OPTS, cfg.CONF) - meta_conf.register_meta_conf_opts(meta_conf.DRIVER_OPTS, cfg.CONF) def test_after_router_updated_called_on_agent_process_update(self): with mock.patch.object(metadata_driver, 'after_router_updated') as f,\ @@ -93,74 +97,105 @@ class TestMetadataDriverProcess(base.BaseTestCase): f.assert_called_once_with( 'router', 'after_update', agent, router=ri) - def _test_spawn_metadata_proxy(self, expected_user, expected_group, - user='', group='', watch_log=True): + def test_spawn_metadata_proxy(self): router_id = _uuid() router_ns = 'qrouter-%s' % router_id - metadata_port = 8080 ip_class_path = 'neutron.agent.linux.ip_lib.IPWrapper' - is_effective_user = 'neutron.agent.linux.utils.is_effective_user' - fake_is_effective_user = lambda x: x in [self.EUNAME, str(self.EUID)] - cfg.CONF.set_override('metadata_proxy_user', user) - cfg.CONF.set_override('metadata_proxy_group', group) - cfg.CONF.set_override('log_file', 'test.log') + cfg.CONF.set_override('metadata_proxy_user', self.EUNAME) + cfg.CONF.set_override('metadata_proxy_group', self.EGNAME) + cfg.CONF.set_override('metadata_proxy_socket', self.METADATA_SOCKET) cfg.CONF.set_override('debug', True) agent = l3_agent.L3NATAgent('localhost') - with mock.patch('os.geteuid', return_value=self.EUID),\ - mock.patch('os.getegid', return_value=self.EGID),\ - mock.patch(is_effective_user, - side_effect=fake_is_effective_user),\ - mock.patch(ip_class_path) as ip_mock: + with mock.patch(ip_class_path) as ip_mock,\ + mock.patch( + 'neutron.agent.linux.external_process.' + 'ProcessManager.get_pid_file_name', + return_value=self.PIDFILE),\ + mock.patch('pwd.getpwnam', + return_value=test_utils.FakeUser(self.EUNAME)),\ + mock.patch('grp.getgrnam', + return_value=test_utils.FakeGroup(self.EGNAME)),\ + mock.patch('os.makedirs'): + cfg_file = os.path.join( + metadata_driver.HaproxyConfigurator.get_config_path( + agent.conf.state_path), + "%s.conf" % router_id) + mock_open = self.useFixture( + tools.OpenFixture(cfg_file)).mock_open agent.metadata_driver.spawn_monitored_metadata_proxy( agent.process_monitor, router_ns, - metadata_port, + self.METADATA_PORT, agent.conf, router_id=router_id) + netns_execute_args = [ - 'neutron-ns-metadata-proxy', - mock.ANY, - mock.ANY, - '--router_id=%s' % router_id, - mock.ANY, - '--metadata_port=%s' % metadata_port, - '--metadata_proxy_user=%s' % expected_user, - '--metadata_proxy_group=%s' % expected_group, - '--debug', - '--log-file=neutron-ns-metadata-proxy-%s.log' % - router_id] - if not watch_log: - netns_execute_args.append( - '--nometadata_proxy_watch_log') + 'haproxy', + '-f', cfg_file] + + cfg_contents = metadata_driver._HAPROXY_CONFIG_TEMPLATE % { + 'user': self.EUNAME, + 'group': self.EGNAME, + 'port': self.METADATA_PORT, + 'unix_socket_path': self.METADATA_SOCKET, + 'res_type': 'Router', + 'res_id': router_id, + 'pidfile': self.PIDFILE, + 'log_level': 'debug'} + + mock_open.assert_has_calls([ + mock.call(cfg_file, 'w'), + mock.call().write(cfg_contents)], + any_order=True) + ip_mock.assert_has_calls([ mock.call(namespace=router_ns), mock.call().netns.execute(netns_execute_args, addl_env=None, run_as_root=False) ]) - def test_spawn_metadata_proxy_with_agent_user(self): - self._test_spawn_metadata_proxy( - self.EUNAME, str(self.EGID), user=self.EUNAME) + def test_create_config_file_wrong_user(self): + with mock.patch('pwd.getpwnam', side_effect=KeyError): + config = metadata_driver.HaproxyConfigurator(mock.ANY, mock.ANY, + mock.ANY, mock.ANY, + self.EUNAME, + self.EGNAME, + mock.ANY, mock.ANY) + self.assertRaises(metadata_driver.InvalidUserOrGroupException, + config.create_config_file) - def test_spawn_metadata_proxy_with_nonagent_user(self): - self._test_spawn_metadata_proxy( - 'notneutron', str(self.EGID), user='notneutron', watch_log=False) + def test_create_config_file_wrong_group(self): + with mock.patch('grp.getgrnam', side_effect=KeyError),\ + mock.patch('pwd.getpwnam', + return_value=test_utils.FakeUser(self.EUNAME)): + config = metadata_driver.HaproxyConfigurator(mock.ANY, mock.ANY, + mock.ANY, mock.ANY, + self.EUNAME, + self.EGNAME, + mock.ANY, mock.ANY) + self.assertRaises(metadata_driver.InvalidUserOrGroupException, + config.create_config_file) - def test_spawn_metadata_proxy_with_agent_uid(self): - self._test_spawn_metadata_proxy( - str(self.EUID), str(self.EGID), user=str(self.EUID)) + def test__migrate_python_ns_metadata_proxy_if_needed(self): + agent = l3_agent.L3NATAgent('localhost') + with mock.patch( + 'neutron.agent.linux.external_process.ProcessManager')\ + as mock_pm: + mock_pm.cmdline = ( + 'python neutron-ns-metadata-proxy') + (agent.metadata_driver + ._migrate_python_ns_metadata_proxy_if_needed(mock_pm)) + mock_pm.disable.assert_called_once_with() - def test_spawn_metadata_proxy_with_nonagent_uid(self): - self._test_spawn_metadata_proxy( - '321', str(self.EGID), user='321', watch_log=False) - - def test_spawn_metadata_proxy_with_group(self): - self._test_spawn_metadata_proxy(str(self.EUID), 'group', group='group') - - def test_spawn_metadata_proxy_with_gid(self): - self._test_spawn_metadata_proxy(str(self.EUID), '654', group='654') - - def test_spawn_metadata_proxy(self): - self._test_spawn_metadata_proxy(str(self.EUID), str(self.EGID)) + def test__migrate_python_ns_metadata_proxy_if_needed_not_called(self): + agent = l3_agent.L3NATAgent('localhost') + with mock.patch( + 'neutron.agent.linux.external_process.ProcessManager')\ + as mock_pm: + mock_pm.cmdline = ( + 'haproxy -f foo.cfg') + (agent.metadata_driver + ._migrate_python_ns_metadata_proxy_if_needed(mock_pm)) + mock_pm.disable.assert_not_called() diff --git a/neutron/tests/unit/agent/metadata/test_namespace_proxy.py b/neutron/tests/unit/agent/metadata/test_namespace_proxy.py deleted file mode 100644 index fda4e02ca32..00000000000 --- a/neutron/tests/unit/agent/metadata/test_namespace_proxy.py +++ /dev/null @@ -1,313 +0,0 @@ -# Copyright 2012 New Dream Network, LLC (DreamHost) -# -# 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 mock -import testtools -import webob - -from neutron.agent.linux import utils as agent_utils -from neutron.agent.metadata import namespace_proxy as ns_proxy -from neutron.common import exceptions -from neutron.common import utils -from neutron.tests import base -from neutron import wsgi - - -class TestNetworkMetadataProxyHandler(base.BaseTestCase): - def setUp(self): - super(TestNetworkMetadataProxyHandler, self).setUp() - self.handler = ns_proxy.NetworkMetadataProxyHandler('router_id') - - def test_call(self): - req = mock.Mock(headers={}) - with mock.patch.object(self.handler, '_proxy_request') as proxy_req: - proxy_req.return_value = 'value' - - retval = self.handler(req) - self.assertEqual(retval, 'value') - proxy_req.assert_called_once_with(req.remote_addr, - req.method, - req.path_info, - req.query_string, - req.body) - - def test_no_argument_passed_to_init(self): - with testtools.ExpectedException( - exceptions.NetworkIdOrRouterIdRequiredError): - ns_proxy.NetworkMetadataProxyHandler() - - def test_call_internal_server_error(self): - req = mock.Mock(headers={}) - with mock.patch.object(self.handler, '_proxy_request') as proxy_req: - proxy_req.side_effect = Exception - retval = self.handler(req) - self.assertIsInstance(retval, webob.exc.HTTPInternalServerError) - self.assertTrue(proxy_req.called) - - def test_proxy_request_router_200(self): - self.handler.router_id = 'router_id' - - resp = mock.MagicMock(status=200) - with mock.patch('httplib2.Http') as mock_http: - resp.__getitem__.return_value = "text/plain" - mock_http.return_value.request.return_value = (resp, 'content') - - retval = self.handler._proxy_request('192.168.1.1', - 'GET', - '/latest/meta-data', - '', - '') - - mock_http.assert_has_calls([ - mock.call().request( - 'http://169.254.169.254/latest/meta-data', - method='GET', - headers={ - 'X-Forwarded-For': '192.168.1.1', - 'X-Neutron-Router-ID': 'router_id' - }, - connection_type=agent_utils.UnixDomainHTTPConnection, - body='' - )] - ) - - self.assertEqual(retval.headers['Content-Type'], 'text/plain') - self.assertEqual(b'content', retval.body) - - def _test_proxy_request_network_200(self, content): - self.handler.network_id = 'network_id' - - resp = mock.MagicMock(status=200) - with mock.patch('httplib2.Http') as mock_http: - resp.__getitem__.return_value = "application/json" - mock_http.return_value.request.return_value = (resp, content) - - retval = self.handler._proxy_request('192.168.1.1', - 'GET', - '/latest/meta-data', - '', - '') - - mock_http.assert_has_calls([ - mock.call().request( - 'http://169.254.169.254/latest/meta-data', - method='GET', - headers={ - 'X-Forwarded-For': '192.168.1.1', - 'X-Neutron-Network-ID': 'network_id' - }, - connection_type=agent_utils.UnixDomainHTTPConnection, - body='' - )] - ) - - self.assertEqual(retval.headers['Content-Type'], - 'application/json') - self.assertEqual(wsgi.encode_body(content), retval.body) - - def test_proxy_request_network_200(self): - self._test_proxy_request_network_200('{}') - - def test_proxy_request_network_200_unicode_in_content(self): - self._test_proxy_request_network_200('Gl\xfcck') - - def _test_proxy_request_network_4xx(self, status, method, expected): - self.handler.network_id = 'network_id' - - resp = mock.Mock(status=status) - with mock.patch('httplib2.Http') as mock_http: - mock_http.return_value.request.return_value = (resp, '') - - retval = self.handler._proxy_request('192.168.1.1', - method, - '/latest/meta-data', - '', - '') - - mock_http.assert_has_calls([ - mock.call().request( - 'http://169.254.169.254/latest/meta-data', - method=method, - headers={ - 'X-Forwarded-For': '192.168.1.1', - 'X-Neutron-Network-ID': 'network_id' - }, - connection_type=agent_utils.UnixDomainHTTPConnection, - body='' - )] - ) - - self.assertIsInstance(retval, expected) - - def test_proxy_request_network_400(self): - self._test_proxy_request_network_4xx( - 400, 'GET', webob.exc.HTTPBadRequest) - - def test_proxy_request_network_404(self): - self._test_proxy_request_network_4xx( - 404, 'GET', webob.exc.HTTPNotFound) - - def test_proxy_request_network_409(self): - self._test_proxy_request_network_4xx( - 409, 'POST', webob.exc.HTTPConflict) - - def test_proxy_request_network_500(self): - self.handler.network_id = 'network_id' - - resp = mock.Mock(status=500) - with mock.patch('httplib2.Http') as mock_http: - mock_http.return_value.request.return_value = (resp, '') - - retval = self.handler._proxy_request('192.168.1.1', - 'GET', - '/latest/meta-data', - '', - '') - - mock_http.assert_has_calls([ - mock.call().request( - 'http://169.254.169.254/latest/meta-data', - method='GET', - headers={ - 'X-Forwarded-For': '192.168.1.1', - 'X-Neutron-Network-ID': 'network_id' - }, - connection_type=agent_utils.UnixDomainHTTPConnection, - body='' - )] - ) - - self.assertIsInstance(retval, webob.exc.HTTPInternalServerError) - - def test_proxy_request_network_418(self): - self.handler.network_id = 'network_id' - - resp = mock.Mock(status=418) - with mock.patch('httplib2.Http') as mock_http: - mock_http.return_value.request.return_value = (resp, '') - - with testtools.ExpectedException(Exception): - self.handler._proxy_request('192.168.1.1', - 'GET', - '/latest/meta-data', - '', - '') - - mock_http.assert_has_calls([ - mock.call().request( - 'http://169.254.169.254/latest/meta-data', - method='GET', - headers={ - 'X-Forwarded-For': '192.168.1.1', - 'X-Neutron-Network-ID': 'network_id' - }, - connection_type=agent_utils.UnixDomainHTTPConnection, - body='' - )] - ) - - def test_proxy_request_network_exception(self): - self.handler.network_id = 'network_id' - - mock.Mock(status=500) - with mock.patch('httplib2.Http') as mock_http: - mock_http.return_value.request.side_effect = Exception - - with testtools.ExpectedException(Exception): - self.handler._proxy_request('192.168.1.1', - 'GET', - '/latest/meta-data', - '', - '') - - mock_http.assert_has_calls([ - mock.call().request( - 'http://169.254.169.254/latest/meta-data', - method='GET', - headers={ - 'X-Forwarded-For': '192.168.1.1', - 'X-Neutron-Network-ID': 'network_id' - }, - connection_type=agent_utils.UnixDomainHTTPConnection, - body='' - )] - ) - - -class TestProxyDaemon(base.BaseTestCase): - def test_init(self): - with mock.patch('neutron.agent.linux.daemon.Pidfile'): - pd = ns_proxy.ProxyDaemon('pidfile', 9697, 'net_id', 'router_id') - self.assertEqual(pd.router_id, 'router_id') - self.assertEqual(pd.network_id, 'net_id') - - def test_run(self): - with mock.patch('neutron.agent.linux.daemon.Pidfile'): - with mock.patch('neutron.wsgi.Server') as Server: - pd = ns_proxy.ProxyDaemon('pidfile', 9697, 'net_id', - 'router_id') - pd.run() - Server.assert_has_calls([ - mock.call('neutron-network-metadata-proxy'), - mock.call().start(mock.ANY, 9697), - mock.call().wait()] - ) - - def test_main(self): - with mock.patch.object(ns_proxy, 'ProxyDaemon') as daemon: - with mock.patch.object(ns_proxy, 'config') as config: - with mock.patch.object(ns_proxy, 'cfg') as cfg: - with mock.patch.object(utils, 'cfg') as utils_cfg: - cfg.CONF.router_id = 'router_id' - cfg.CONF.network_id = None - cfg.CONF.metadata_port = 9697 - cfg.CONF.pid_file = 'pidfile' - cfg.CONF.daemonize = True - utils_cfg.CONF.log_opt_values.return_value = None - ns_proxy.main() - - self.assertTrue(config.setup_logging.called) - daemon.assert_has_calls([ - mock.call('pidfile', 9697, - router_id='router_id', - network_id=None, - user=mock.ANY, - group=mock.ANY, - watch_log=mock.ANY), - mock.call().start()] - ) - - def test_main_dont_fork(self): - with mock.patch.object(ns_proxy, 'ProxyDaemon') as daemon: - with mock.patch.object(ns_proxy, 'config') as config: - with mock.patch.object(ns_proxy, 'cfg') as cfg: - with mock.patch.object(utils, 'cfg') as utils_cfg: - cfg.CONF.router_id = 'router_id' - cfg.CONF.network_id = None - cfg.CONF.metadata_port = 9697 - cfg.CONF.pid_file = 'pidfile' - cfg.CONF.daemonize = False - utils_cfg.CONF.log_opt_values.return_value = None - ns_proxy.main() - - self.assertTrue(config.setup_logging.called) - daemon.assert_has_calls([ - mock.call('pidfile', 9697, - router_id='router_id', - network_id=None, - user=mock.ANY, - group=mock.ANY, - watch_log=mock.ANY), - mock.call().run()] - ) diff --git a/releasenotes/notes/switching-to-haproxy-for-metadata-proxy-9d8f7549fadf9182.yaml b/releasenotes/notes/switching-to-haproxy-for-metadata-proxy-9d8f7549fadf9182.yaml new file mode 100644 index 00000000000..dbc8aded631 --- /dev/null +++ b/releasenotes/notes/switching-to-haproxy-for-metadata-proxy-9d8f7549fadf9182.yaml @@ -0,0 +1,12 @@ +--- +features: + - In order to reduce metadata proxy memory footprint, ``haproxy`` is now used + as a replacement for ``neutron-ns-metadata-proxy`` Python implementation. +upgrade: + - Since ``haproxy`` was not used before by ``neutron-l3-agent`` and + ``neutron-dhcp-agent``, rootwrap filters for both agents have to be copied + over when upgrading. + - To upgrade to the ``haproxy`` based metadata proxy, ``neutron-l3-agent`` + and ``neutron-dhcp-agent`` have to be restarted. On startup, old proxy + processes will be detected and replaced with ``haproxy``. + diff --git a/setup.cfg b/setup.cfg index e6c2fdab9b2..cfa10e8947a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,6 @@ console_scripts = neutron-macvtap-agent = neutron.cmd.eventlet.plugins.macvtap_neutron_agent:main neutron-metadata-agent = neutron.cmd.eventlet.agents.metadata:main neutron-netns-cleanup = neutron.cmd.netns_cleanup:main - neutron-ns-metadata-proxy = neutron.cmd.eventlet.agents.metadata_proxy:main neutron-openvswitch-agent = neutron.cmd.eventlet.plugins.ovs_neutron_agent:main neutron-ovs-cleanup = neutron.cmd.ovs_cleanup:main neutron-pd-notify = neutron.cmd.pd_notify:main