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/<pid>/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
This commit is contained in:
Daniel Alvarez 2017-02-09 18:30:23 +00:00
parent 67da6a3122
commit 3b22541a2a
21 changed files with 376 additions and 680 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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