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: I5a75cc582dca48defafb440207d10e2f7b4f218bchanges/91/431691/34
parent
67da6a3122
commit
3b22541a2a
@ -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()
|
@ -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()
|
@ -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)
|
@ -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()]
|
||||
)
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
features:
|
||||