diff --git a/doc/source/eventlet_deprecation/index.rst b/doc/source/eventlet_deprecation/index.rst index b75543bf61c..8d50c71197b 100644 --- a/doc/source/eventlet_deprecation/index.rst +++ b/doc/source/eventlet_deprecation/index.rst @@ -74,6 +74,17 @@ The OVN metadata agent uses the same implementation as the OVN agent. The same limitations apply. +Metadata agent +-------------- + +The Metadata agent uses the same implementation as the OVN agent and the same +limitations apply. The ``MetadataProxyHandler`` class is now instantiated every +time a new request is done; after the call, the instance is destroyed. The +cache used to store the previous RPC calls results is no longer relevant and +has been removed. In order to implement an RPC cache, it should be implemented +outside the mentioned class. + + Neutron API ----------- diff --git a/neutron/agent/metadata/agent.py b/neutron/agent/metadata/agent.py index b9a35d31293..fdb9852a2d5 100644 --- a/neutron/agent/metadata/agent.py +++ b/neutron/agent/metadata/agent.py @@ -12,23 +12,51 @@ # License for the specific language governing permissions and limitations # under the License. +import io +import socketserver +import urllib + +import jinja2 from neutron_lib.agent import topics from neutron_lib import constants from neutron_lib import context -from neutron_lib.utils import host from oslo_config import cfg from oslo_log import log as logging from oslo_service import loopingcall +from oslo_utils import encodeutils +import requests +import webob +from webob import exc as webob_exc from neutron._i18n import _ from neutron.agent.common import base_agent_rpc from neutron.agent.linux import utils as agent_utils from neutron.agent.metadata import proxy_base from neutron.agent import rpc as agent_rpc -from neutron.common import cache_utils as cache +from neutron.common import ipv6_utils +from neutron.common import utils as common_utils + LOG = logging.getLogger(__name__) +RESPONSE = jinja2.Template("""HTTP/1.1 {{ http_code }} +Content-Type: text/plain; charset=UTF-8 +Connection: close +Content-Length: {{ len }} + + + + {{ title }} + + +

{{ body_title }}

+ {{ body }}

+ + + +""") +RESPONSE_LENGHT = 40 + class MetadataPluginAPI(base_agent_rpc.BasePluginApi): """Agent-side RPC for metadata agent-to-plugin interaction. @@ -43,7 +71,6 @@ class MetadataPluginAPI(base_agent_rpc.BasePluginApi): API version history: 1.0 - Initial version. """ - def __init__(self, topic): super().__init__( topic=topic, @@ -51,25 +78,128 @@ class MetadataPluginAPI(base_agent_rpc.BasePluginApi): version='1.0') -class MetadataProxyHandler(proxy_base.MetadataProxyHandlerBase): +class MetadataProxyHandlerBaseSocketServer( + proxy_base.MetadataProxyHandlerBase): + @staticmethod + def _http_response(http_response, request): + _res = webob.Response( + body=http_response.content, + status=http_response.status_code, + content_type=http_response.headers['content-type'], + charset=http_response.encoding) + # NOTE(ralonsoh): there should be a better way to format the HTTP + # response, adding the HTTP version to the ``webob.Response`` + # output string. + out = request.http_version + ' ' + str(_res) + if (int(_res.headers['content-length']) == 0 and + _res.status_code == 200): + # Add 2 extra \r\n to the result. HAProxy is also expecting + # it even when the body is empty. + out += '\r\n\r\n' + return out.encode(http_response.encoding) + + def _proxy_request(self, instance_id, project_id, req): + headers = { + 'X-Forwarded-For': req.headers.get('X-Forwarded-For'), + 'X-Instance-ID': instance_id, + 'X-Tenant-ID': project_id, + 'X-Instance-ID-Signature': common_utils.sign_instance_id( + self.conf, instance_id) + } + + nova_host_port = ipv6_utils.valid_ipv6_url( + self.conf.nova_metadata_host, + self.conf.nova_metadata_port) + + url = urllib.parse.urlunsplit(( + self.conf.nova_metadata_protocol, + nova_host_port, + req.path_info, + req.query_string, + '')) + + disable_ssl_certificate_validation = self.conf.nova_metadata_insecure + if self.conf.auth_ca_cert and not disable_ssl_certificate_validation: + verify_cert = self.conf.auth_ca_cert + else: + verify_cert = not disable_ssl_certificate_validation + + client_cert = None + if self.conf.nova_client_cert and self.conf.nova_client_priv_key: + client_cert = (self.conf.nova_client_cert, + self.conf.nova_client_priv_key) + + try: + resp = requests.request(method=req.method, url=url, + headers=headers, + data=req.body, + cert=client_cert, + verify=verify_cert, + timeout=60) + except requests.ConnectionError: + msg = _('The remote metadata server is temporarily unavailable. ' + 'Please try again later.') + LOG.warning(msg) + title = '503 Service Unavailable' + length = RESPONSE_LENGHT + len(title) * 2 + len(msg) + reponse = RESPONSE.render(http_code=title, title=title, + body_title=title, body=title, len=length) + return encodeutils.to_utf8(reponse) + + if resp.status_code == 200: + return self._http_response(resp, req) + if resp.status_code == 403: + LOG.warning( + 'The remote metadata server responded with Forbidden. This ' + 'response usually occurs when shared secrets do not match.' + ) + # TODO(ralonsoh): add info in the returned HTTP message to the VM. + return self._http_response(resp, req) + if resp.status_code == 500: + msg = _( + 'Remote metadata server experienced an internal server error.' + ) + LOG.warning(msg) + # TODO(ralonsoh): add info in the returned HTTP message to the VM. + return self._http_response(resp, req) + if resp.status_code in (400, 404, 409, 502, 503, 504): + # TODO(ralonsoh): add info in the returned HTTP message to the VM. + return self._http_response(resp, req) + raise Exception(_('Unexpected response code: %s') % resp.status_code) + + +class MetadataProxyHandler(MetadataProxyHandlerBaseSocketServer, + socketserver.StreamRequestHandler): NETWORK_ID_HEADER = 'X-Neutron-Network-ID' ROUTER_ID_HEADER = 'X-Neutron-Router-ID' + _conf = None - def __init__(self, conf): - self._cache = cache.get_cache(conf) - super().__init__(conf, has_cache=True) - + def __init__(self, request, client_address, server): self.plugin_rpc = MetadataPluginAPI(topics.PLUGIN) self.context = context.get_admin_context_without_session() + super().__init__(self._conf, has_cache=False, request=request, + client_address=client_address, server=server) - def _get_ports_from_server(self, router_id=None, ip_address=None, - networks=None, mac_address=None): - """Get ports from server.""" - filters = self._get_port_filters( - router_id, ip_address, networks, mac_address) - return self.plugin_rpc.get_ports(self.context, filters) + def handle(self): + try: + request = self.request.recv(4096) + LOG.debug('Request: %s', request.decode('utf-8')) + f_request = io.BytesIO(request) + req = webob.Request.from_file(f_request) + instance_id, project_id = self._get_instance_and_project_id(req) + if instance_id: + res = self._proxy_request(instance_id, project_id, req) + self.wfile.write(res) + return + # TODO(ralonsoh): change this return to be a formatted Request + # and added to self.wfile + return webob_exc.HTTPNotFound() + except Exception as exc: + LOG.exception('Error while receiving data.') + raise exc - def _get_port_filters(self, router_id=None, ip_address=None, + @staticmethod + def _get_port_filters(router_id=None, ip_address=None, networks=None, mac_address=None): filters = {} if router_id: @@ -89,13 +219,18 @@ class MetadataProxyHandler(proxy_base.MetadataProxyHandlerBase): return filters - @cache.cache_method_results + def _get_ports_from_server(self, router_id=None, ip_address=None, + networks=None, mac_address=None): + """Get ports from server.""" + filters = self._get_port_filters( + router_id, ip_address, networks, mac_address) + return self.plugin_rpc.get_ports(self.context, filters) + def _get_router_networks(self, router_id, skip_cache=False): """Find all networks connected to given router.""" internal_ports = self._get_ports_from_server(router_id=router_id) return tuple(p['network_id'] for p in internal_ports) - @cache.cache_method_results def _get_ports_for_remote_address(self, remote_address, networks, remote_mac=None, skip_cache=False): @@ -196,16 +331,8 @@ class UnixDomainMetadataProxy(proxy_base.UnixDomainMetadataProxyBase): self.agent_state.pop('start_flag', None) def run(self): - server = agent_utils.UnixDomainWSGIServer( - constants.AGENT_PROCESS_METADATA) - # Set the default metadata_workers if not yet set in the config file - md_workers = self.conf.metadata_workers - if md_workers is None: - md_workers = host.cpu_count() // 2 - server.start(MetadataProxyHandler(self.conf), - self.conf.metadata_proxy_socket, - workers=md_workers, - backlog=self.conf.metadata_backlog, - mode=self._get_socket_mode()) - self._init_state_reporting() - server.wait() + file_socket = cfg.CONF.metadata_proxy_socket + self._server = socketserver.ThreadingUnixStreamServer( + file_socket, MetadataProxyHandler) + MetadataProxyHandler._conf = self.conf + self._server.serve_forever() diff --git a/neutron/cmd/agents/metadata.py b/neutron/cmd/agents/metadata.py new file mode 100644 index 00000000000..de3d89f7375 --- /dev/null +++ b/neutron/cmd/agents/metadata.py @@ -0,0 +1,24 @@ +# 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 setproctitle + +from neutron.agent import metadata_agent +from neutron_lib import constants + + +def main(): + proctitle = "{} ({})".format( + constants.AGENT_PROCESS_METADATA, setproctitle.getproctitle()) + setproctitle.setproctitle(proctitle) + + metadata_agent.main() diff --git a/neutron/tests/unit/agent/metadata/test_agent.py b/neutron/tests/unit/agent/metadata/test_agent.py index dd910894b81..b7cad506d3c 100644 --- a/neutron/tests/unit/agent/metadata/test_agent.py +++ b/neutron/tests/unit/agent/metadata/test_agent.py @@ -25,7 +25,6 @@ from oslo_config import fixture as config_fixture from oslo_utils import fileutils from oslo_utils import netutils -from neutron.agent.linux import utils as agent_utils from neutron.agent.metadata import agent from neutron.agent.metadata import proxy_base from neutron.agent import metadata_agent @@ -41,16 +40,6 @@ class ConfFixture(config_fixture.Config): cache.register_oslo_configs(self.conf) -class NewCacheConfFixture(ConfFixture): - def setUp(self): - super().setUp() - self.config( - group='cache', - enabled=True, - backend='oslo_cache.dict', - expiration_time=5) - - class TestMetadataProxyHandlerBase(base.BaseTestCase): fake_conf = cfg.CONF fake_conf_fixture = ConfFixture(fake_conf) @@ -60,7 +49,10 @@ class TestMetadataProxyHandlerBase(base.BaseTestCase): self.useFixture(self.fake_conf_fixture) self.log_p = mock.patch.object(proxy_base, 'LOG') self.log = self.log_p.start() - self.handler = agent.MetadataProxyHandler(self.fake_conf) + agent.MetadataProxyHandler._conf = self.fake_conf + with mock.patch.object(agent.MetadataProxyHandler, 'handle'): + self.handler = agent.MetadataProxyHandler( + mock.Mock(), mock.Mock(), mock.Mock()) self.handler.plugin_rpc = mock.Mock() self.handler.context = mock.Mock() @@ -122,16 +114,6 @@ class _TestMetadataProxyHandlerCacheMixin: retval = self.handler(req) self.assertEqual('value', retval) - def test_call_skip_cache(self): - req = mock.Mock() - with mock.patch.object(self.handler, - '_get_instance_and_project_id') as get_ids: - get_ids.return_value = ('instance_id', 'tenant_id') - with mock.patch.object(self.handler, '_proxy_request') as proxy: - proxy.return_value = webob.exc.HTTPNotFound() - self.handler(req) - get_ids.assert_called_with(req, skip_cache=True) - def test_call_no_instance_match(self): req = mock.Mock() with mock.patch.object(self.handler, @@ -405,12 +387,6 @@ class _TestMetadataProxyHandlerCacheMixin: ) -class TestMetadataProxyHandlerNewCache(TestMetadataProxyHandlerBase, - _TestMetadataProxyHandlerCacheMixin): - fake_conf = cfg.CONF - fake_conf_fixture = NewCacheConfFixture(fake_conf) - - class TestUnixDomainMetadataProxy(base.BaseTestCase): def setUp(self): super().setUp() @@ -459,25 +435,6 @@ class TestUnixDomainMetadataProxy(base.BaseTestCase): agent.UnixDomainMetadataProxy(mock.Mock()) unlink.assert_called_once_with('/the/path') - @mock.patch.object(agent, 'MetadataProxyHandler') - @mock.patch.object(agent_utils, 'UnixDomainWSGIServer') - @mock.patch.object(fileutils, 'ensure_tree') - def test_run(self, ensure_dir, server, handler): - p = agent.UnixDomainMetadataProxy(self.cfg.CONF) - p.run() - - ensure_dir.assert_called_once_with('/the', mode=0o755) - server.assert_has_calls([ - mock.call(n_const.AGENT_PROCESS_METADATA), - mock.call().start(handler.return_value, - '/the/path', workers=0, - backlog=128, mode=0o644), - mock.call().wait()] - ) - self.looping_mock.assert_called_once_with(p._report_state) - self.looping_mock.return_value.start.assert_called_once_with( - interval=mock.ANY) - def test_main(self): with mock.patch.object(agent, 'UnixDomainMetadataProxy') as proxy: with mock.patch.object(metadata_agent, 'config') as config: diff --git a/setup.cfg b/setup.cfg index 73d4b71c269..45bae6bd152 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ console_scripts = neutron-ipset-cleanup = neutron.cmd.ipset_cleanup:main neutron-l3-agent = neutron.cmd.eventlet.agents.l3:main neutron-macvtap-agent = neutron.cmd.eventlet.plugins.macvtap_neutron_agent:main - neutron-metadata-agent = neutron.cmd.eventlet.agents.metadata:main + neutron-metadata-agent = neutron.cmd.agents.metadata:main neutron-netns-cleanup = neutron.cmd.netns_cleanup:main neutron-openvswitch-agent = neutron.cmd.eventlet.plugins.ovs_neutron_agent:main neutron-ovs-cleanup = neutron.cmd.ovs_cleanup:main