Rodolfo Alonso Hernandez bc0741e689 [eventlet-removal] Remove eventlet in the Metadata Agent
This patch removes the usage of eventlet in the Metadata Agent. The new
metadata proxy is based on the implementation done in [1]. This new
new implementation mimics the implementation of the
``UnixDomainWSGIServer`` class. The server listens to a file socket
that is populated by the HAProxy instance running inside the
network metadata namespace. The HAProxy writes the requests of the
virtual machines on the Unix file socket and the metadata proxy
sends the requests to the Nova metadata server.

This implementation doesn't use ``oslo.services`` to spawn the
processes or the local threads depending on the ``metadata_workers``
configuration variable. Right now only the embedded form (local thread)
is implemented (metadata_workers=0, the default value). Future
implementations will enable again this configuration variable.

[1]https://review.opendev.org/c/openstack/neutron/+/937545

Closes-Bug: #2099896
Change-Id: Ifadef192ef766a4f291643868686a98b591ddb40
2025-02-28 12:47:00 +00:00

462 lines
18 KiB
Python

# 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.
from unittest import mock
import ddt
import netaddr
from neutron_lib import constants as n_const
import testtools
import webob
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_utils import fileutils
from oslo_utils import netutils
from neutron.agent.metadata import agent
from neutron.agent.metadata import proxy_base
from neutron.agent import metadata_agent
from neutron.common import cache_utils as cache
from neutron.common import utils
from neutron.conf.agent.metadata import config as meta_conf
from neutron.tests import base
class ConfFixture(config_fixture.Config):
def setUp(self):
super().setUp()
cache.register_oslo_configs(self.conf)
class TestMetadataProxyHandlerBase(base.BaseTestCase):
fake_conf = cfg.CONF
fake_conf_fixture = ConfFixture(fake_conf)
def setUp(self):
super().setUp()
self.useFixture(self.fake_conf_fixture)
self.log_p = mock.patch.object(proxy_base, 'LOG')
self.log = self.log_p.start()
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()
class TestMetadataProxyHandlerRpc(TestMetadataProxyHandlerBase):
def test_get_port_filters(self):
router_id = 'test_router_id'
ip = '1.2.3.4'
networks = ('net_id1', 'net_id2')
expected = {'device_id': [router_id],
'device_owner': n_const.ROUTER_INTERFACE_OWNERS,
'network_id': networks,
'fixed_ips': {'ip_address': [ip]}}
actual = self.handler._get_port_filters(router_id, ip, networks)
self.assertEqual(expected, actual)
def test_get_port_filters_mac(self):
router_id = 'test_router_id'
networks = ('net_id1', 'net_id2')
mac = '11:22:33:44:55:66'
expected = {'device_id': [router_id],
'device_owner': n_const.ROUTER_INTERFACE_OWNERS,
'network_id': networks,
'mac_address': [mac]}
actual = self.handler._get_port_filters(
router_id=router_id, networks=networks, mac_address=mac)
self.assertEqual(expected, actual)
def test_get_router_networks(self):
router_id = 'router-id'
expected = ('network_id1', 'network_id2')
ports = [{'network_id': 'network_id1', 'something': 42},
{'network_id': 'network_id2', 'something_else': 32}]
self.handler.plugin_rpc.get_ports.return_value = ports
networks = self.handler._get_router_networks(router_id)
self.assertEqual(expected, networks)
def test_get_ports_for_remote_address(self):
ip = '1.1.1.1'
networks = ('network_id1', 'network_id2')
expected = [{'port_id': 'port_id1'},
{'port_id': 'port_id2'}]
self.handler.plugin_rpc.get_ports.return_value = expected
ports = self.handler._get_ports_for_remote_address(ip, networks)
self.assertEqual(expected, ports)
@ddt.ddt
class _TestMetadataProxyHandlerCacheMixin:
def test_call(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 = 'value'
retval = self.handler(req)
self.assertEqual('value', retval)
def test_call_no_instance_match(self):
req = mock.Mock()
with mock.patch.object(self.handler,
'_get_instance_and_project_id') as get_ids:
get_ids.return_value = None, None
retval = self.handler(req)
self.assertIsInstance(retval, webob.exc.HTTPNotFound)
def test_call_internal_server_error(self):
req = mock.Mock()
with mock.patch.object(self.handler,
'_get_instance_and_project_id') as get_ids:
get_ids.side_effect = Exception
retval = self.handler(req)
self.assertIsInstance(retval, webob.exc.HTTPInternalServerError)
self.assertEqual(2, len(self.log.mock_calls))
def test_get_router_networks(self):
router_id = 'router-id'
expected = ('network_id1', 'network_id2')
ports = [{'network_id': 'network_id1', 'something': 42},
{'network_id': 'network_id2', 'something_else': 32}]
mock_get_ports = self.handler.plugin_rpc.get_ports
mock_get_ports.return_value = ports
networks = self.handler._get_router_networks(router_id)
mock_get_ports.assert_called_once_with(
mock.ANY,
{'device_id': [router_id],
'device_owner': n_const.ROUTER_INTERFACE_OWNERS})
self.assertEqual(expected, networks)
def _test_get_router_networks_twice_helper(self):
router_id = 'router-id'
ports = [{'network_id': 'network_id1', 'something': 42}]
expected_networks = ('network_id1',)
with mock.patch('oslo_utils.timeutils.utcnow_ts', return_value=0):
mock_get_ports = self.handler.plugin_rpc.get_ports
mock_get_ports.return_value = ports
networks = self.handler._get_router_networks(router_id)
mock_get_ports.assert_called_once_with(
mock.ANY,
{'device_id': [router_id],
'device_owner': n_const.ROUTER_INTERFACE_OWNERS})
self.assertEqual(expected_networks, networks)
networks = self.handler._get_router_networks(router_id)
def test_get_router_networks_twice(self):
self._test_get_router_networks_twice_helper()
self.assertEqual(
1, self.handler.plugin_rpc.get_ports.call_count)
def _get_ports_for_remote_address_cache_hit_helper(self):
remote_address = 'remote_address'
networks = ('net1', 'net2')
mock_get_ports = self.handler.plugin_rpc.get_ports
mock_get_ports.return_value = [{'network_id': 'net1', 'something': 42}]
self.handler._get_ports_for_remote_address(remote_address, networks)
mock_get_ports.assert_called_once_with(
mock.ANY,
{'network_id': networks,
'fixed_ips': {'ip_address': [remote_address]}}
)
self.assertEqual(1, mock_get_ports.call_count)
self.handler._get_ports_for_remote_address(remote_address,
networks)
def test_get_ports_for_remote_address_cache_hit(self):
self._get_ports_for_remote_address_cache_hit_helper()
self.assertEqual(
1, self.handler.plugin_rpc.get_ports.call_count)
def test_get_port_network_id(self):
network_id = 'network-id'
router_id = 'router-id'
remote_address = 'remote-address'
expected = ('device1', 'tenant1')
ports = [
{'device_id': 'device1', 'tenant_id': 'tenant1',
'network_id': 'network1'}
]
networks = (network_id,)
with mock.patch.object(self.handler,
'_get_ports_for_remote_address',
return_value=ports
) as mock_get_ip_addr,\
mock.patch.object(self.handler,
'_get_router_networks'
) as mock_get_router_networks:
port = self.handler.get_port(remote_address, network_id,
router_id=router_id)
mock_get_ip_addr.assert_called_once_with(remote_address,
networks,
remote_mac=None,
skip_cache=False)
self.assertFalse(mock_get_router_networks.called)
self.assertEqual(expected, port)
def test_get_port_router_id(self):
router_id = 'router-id'
remote_address = 'remote-address'
expected = ('device1', 'tenant1')
ports = [
{'device_id': 'device1', 'tenant_id': 'tenant1',
'network_id': 'network1'}
]
networks = ('network1', 'network2')
with mock.patch.object(self.handler,
'_get_ports_for_remote_address',
return_value=ports
) as mock_get_ip_addr,\
mock.patch.object(self.handler,
'_get_router_networks',
return_value=networks
) as mock_get_router_networks:
port = self.handler.get_port(remote_address, router_id=router_id)
mock_get_router_networks.assert_called_once_with(
router_id, skip_cache=False)
mock_get_ip_addr.assert_called_once_with(
remote_address, networks, remote_mac=None, skip_cache=False)
self.assertEqual(expected, port)
def test_get_port_no_id(self):
self.assertRaises(TypeError, self.handler.get_port, 'remote_address')
def _get_instance_and_tenant_id_helper(self, headers, list_ports_retval,
networks=None, router_id=None,
remote_address='192.168.1.1'):
headers['X-Forwarded-For'] = remote_address
req = mock.Mock(headers=headers)
def mock_get_ports(*args, **kwargs):
return list_ports_retval.pop(0)
self.handler.plugin_rpc.get_ports.side_effect = mock_get_ports
instance_id, tenant_id = self.handler._get_instance_and_project_id(req)
expected = []
if networks and router_id:
return (instance_id, tenant_id)
if router_id:
expected.append(
mock.call(
mock.ANY,
{'device_id': [router_id],
'device_owner': n_const.ROUTER_INTERFACE_OWNERS}
)
)
remote_ip = netaddr.IPAddress(remote_address)
if remote_ip.is_link_local():
expected.append(
mock.call(
mock.ANY,
{'network_id': networks,
'mac_address': [netutils.get_mac_addr_by_ipv6(remote_ip)]}
)
)
else:
expected.append(
mock.call(
mock.ANY,
{'network_id': networks,
'fixed_ips': {'ip_address': ['192.168.1.1']}}
)
)
self.handler.plugin_rpc.get_ports.assert_has_calls(expected)
return (instance_id, tenant_id)
@ddt.data('192.168.1.1', '::ffff:192.168.1.1', 'fe80::5054:ff:fede:5bbf')
def test_get_instance_id_router_id(self, remote_address):
router_id = 'the_id'
headers = {
'X-Neutron-Router-ID': router_id
}
networks = ('net1', 'net2')
ports = [
[{'network_id': 'net1'}, {'network_id': 'net2'}],
[{'device_id': 'device_id', 'tenant_id': 'tenant_id',
'network_id': 'net1'}]
]
self.assertEqual(
('device_id', 'tenant_id'),
self._get_instance_and_tenant_id_helper(
headers, ports, networks=networks, router_id=router_id,
remote_address=remote_address)
)
@ddt.data('192.168.1.1', '::ffff:192.168.1.1', 'fe80::5054:ff:fede:5bbf')
def test_get_instance_id_router_id_no_match(self, remote_address):
router_id = 'the_id'
headers = {
'X-Neutron-Router-ID': router_id
}
networks = ('net1', 'net2')
ports = [
[{'network_id': 'net1'}, {'network_id': 'net2'}],
[]
]
self.assertEqual(
(None, None),
self._get_instance_and_tenant_id_helper(
headers, ports, networks=networks, router_id=router_id,
remote_address=remote_address)
)
@ddt.data('192.168.1.1', '::ffff:192.168.1.1', 'fe80::5054:ff:fede:5bbf')
def test_get_instance_id_network_id(self, remote_address):
network_id = 'the_id'
headers = {
'X-Neutron-Network-ID': network_id
}
ports = [
[{'device_id': 'device_id',
'tenant_id': 'tenant_id',
'network_id': 'the_id'}]
]
self.assertEqual(
('device_id', 'tenant_id'),
self._get_instance_and_tenant_id_helper(
headers, ports, networks=('the_id',),
remote_address=remote_address)
)
@ddt.data('192.168.1.1', '::ffff:192.168.1.1', 'fe80::5054:ff:fede:5bbf')
def test_get_instance_id_network_id_no_match(self, remote_address):
network_id = 'the_id'
headers = {
'X-Neutron-Network-ID': network_id
}
ports = [[]]
self.assertEqual(
(None, None),
self._get_instance_and_tenant_id_helper(
headers, ports, networks=('the_id',),
remote_address=remote_address)
)
@ddt.data('192.168.1.1', '::ffff:192.168.1.1', 'fe80::5054:ff:fede:5bbf')
def test_get_instance_id_network_id_and_router_id_invalid(
self, remote_address):
network_id = 'the_nid'
router_id = 'the_rid'
headers = {
'X-Neutron-Network-ID': network_id,
'X-Neutron-Router-ID': router_id
}
# The call should never do a port lookup, but mock it to verify
ports = [
[{'device_id': 'device_id',
'tenant_id': 'tenant_id',
'network_id': network_id}]
]
self.assertEqual(
(None, None),
self._get_instance_and_tenant_id_helper(
headers, ports, networks=(network_id,), router_id=router_id,
remote_address=remote_address)
)
class TestUnixDomainMetadataProxy(base.BaseTestCase):
def setUp(self):
super().setUp()
self.cfg_p = mock.patch.object(agent, 'cfg')
self.cfg = self.cfg_p.start()
looping_call_p = mock.patch(
'oslo_service.loopingcall.FixedIntervalLoopingCall')
self.looping_mock = looping_call_p.start()
self.cfg.CONF.metadata_proxy_socket = '/the/path'
self.cfg.CONF.metadata_workers = 0
self.cfg.CONF.metadata_backlog = 128
self.cfg.CONF.metadata_proxy_socket_mode = meta_conf.USER_MODE
@mock.patch.object(fileutils, 'ensure_tree')
def test_init_doesnot_exists(self, ensure_dir):
agent.UnixDomainMetadataProxy(mock.Mock())
ensure_dir.assert_called_once_with('/the', mode=0o755)
def test_init_exists(self):
with mock.patch('os.path.isdir') as isdir:
with mock.patch('os.unlink') as unlink:
isdir.return_value = True
agent.UnixDomainMetadataProxy(mock.Mock())
unlink.assert_called_once_with('/the/path')
def test_init_exists_unlink_no_file(self):
with mock.patch('os.path.isdir') as isdir:
with mock.patch('os.unlink') as unlink:
with mock.patch('os.path.exists') as exists:
isdir.return_value = True
exists.return_value = False
unlink.side_effect = OSError
agent.UnixDomainMetadataProxy(mock.Mock())
unlink.assert_called_once_with('/the/path')
def test_init_exists_unlink_fails_file_still_exists(self):
with mock.patch('os.path.isdir') as isdir:
with mock.patch('os.unlink') as unlink:
with mock.patch('os.path.exists') as exists:
isdir.return_value = True
exists.return_value = True
unlink.side_effect = OSError
with testtools.ExpectedException(OSError):
agent.UnixDomainMetadataProxy(mock.Mock())
unlink.assert_called_once_with('/the/path')
def test_main(self):
with mock.patch.object(agent, 'UnixDomainMetadataProxy') as proxy:
with mock.patch.object(metadata_agent, 'config') as config:
with mock.patch.object(metadata_agent, 'cfg') as cfg:
with mock.patch.object(utils, 'cfg'):
metadata_agent.main()
self.assertTrue(config.setup_logging.called)
proxy.assert_has_calls([
mock.call(cfg.CONF),
mock.call().run()]
)
def test_report_state(self):
with mock.patch('neutron.agent.rpc.PluginReportStateAPI') as state_api:
with mock.patch('os.makedirs'):
proxy = agent.UnixDomainMetadataProxy(self.cfg.CONF)
proxy._init_state_reporting()
self.assertTrue(proxy.agent_state['start_flag'])
proxy._report_state()
self.assertNotIn('start_flag', proxy.agent_state)
state_api_inst = state_api.return_value
state_api_inst.report_state.assert_called_once_with(
proxy.context, proxy.agent_state, use_call=True)