diff --git a/neutron/agent/l2/extensions/metadata/__init__.py b/neutron/agent/l2/extensions/metadata/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/agent/l2/extensions/metadata/host_metadata_proxy.py b/neutron/agent/l2/extensions/metadata/host_metadata_proxy.py new file mode 100644 index 00000000000..aa89782b85b --- /dev/null +++ b/neutron/agent/l2/extensions/metadata/host_metadata_proxy.py @@ -0,0 +1,200 @@ +# Copyright (c) 2022 China Unicom Cloud Data Co.,Ltd. +# All Rights Reserved. +# +# 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 grp +import io +import os +import pwd + +import jinja2 +from neutron_lib.utils import file as file_utils +from oslo_config import cfg +from oslo_log import log as logging + +from neutron._i18n import _ +from neutron.agent.linux import external_process +from neutron.agent.linux import utils +from neutron.common import metadata as common_meta +from neutron.common import utils as common_utils + +LOG = logging.getLogger(__name__) + +PROXY_SERVICE_NAME = common_meta.PROXY_SERVICE_NAME +PROXY_SERVICE_CMD = common_meta.PROXY_SERVICE_CMD + +_HOST_PATH_PROXY_TEMPLATE = jinja2.Template(""" +global + log /dev/log local0 {{ log_level }} + log-tag {{ log_tag }} + user {{ user }} + group {{ group }} + maxconn {{ maxconn }} + daemon + +frontend public + bind *:80 name clear + mode http + log global + option httplog + option dontlognull + maxconn {{ maxconn }} + timeout http-request 30s + timeout connect 30s + timeout client 32s + timeout server 32s + timeout http-keep-alive 30s + + monitor-uri /monitoruri + stats uri /admin/stats +{% for instance in instance_list %} + acl instance_{{ instance.uuid }}_{{ instance.provider_ip + }} src {{ instance.provider_ip }} +{% endfor %} + +{% for instance in instance_list %} + use_backend backend_{{ instance.uuid }}_{{ + instance.provider_ip }} if instance_{{ instance.uuid }}_{{ + instance.provider_ip }} +{% endfor %} + +{% for instance in instance_list %} +backend backend_{{ instance.uuid }}_{{ instance.provider_ip }} + mode http + balance roundrobin + retries 3 + option redispatch + timeout http-request 30s + timeout connect 30s + timeout server 30s + + http-request set-header X-Instance-ID {{ instance.uuid }} + http-request set-header X-Tenant-ID {{ instance.project_id }} + http-request set-header X-Instance-ID-Signature {{ instance.signature }} + + server metasrv {{ meta_api }} + +{% endfor %} +""") + + +class ProxyInstance(object): + def __init__(self, instance_id, provider_ip, project_id): + self.uuid = instance_id + self.provider_ip = provider_ip + self.project_id = project_id + self.signature = common_utils.sign_instance_id( + cfg.CONF.METADATA, self.uuid) + + +class HostMedataHAProxyDaemonMonitor(object): + """Manage the data and state of a host metadata haproxy process.""" + + def __init__(self, process_monitor, uuid=None, + user=None, group=None): + self._host_id = uuid or "host_metadata_proxy" + self._process_monitor = process_monitor + self.haproxy_conf = None + self.user = user or str(os.geteuid()) + self.group = group or str(os.getegid()) + + def _generate_proxy_conf(self, instance_infos): + haproxy_conf = utils.get_conf_file_name( + cfg.CONF.state_path, self._host_id, + 'haproxy.conf', True) + buf = io.StringIO() + meta_api = "%s:%s" % ( + cfg.CONF.METADATA.nova_metadata_host, + cfg.CONF.METADATA.nova_metadata_port) + + try: + username = pwd.getpwuid(int(self.user)).pw_name + except (ValueError, KeyError): + try: + username = pwd.getpwnam(self.user).pw_name + except KeyError: + raise common_meta.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 common_meta.InvalidUserOrGroupException( + _("Invalid group/gid: '%s'") % self.group) + + buf.write('%s' % _HOST_PATH_PROXY_TEMPLATE.render( + log_level='debug', + log_tag="%s-%s" % (PROXY_SERVICE_NAME, self._host_id), + user=username, + group=groupname, + maxconn=1024, + instance_list=instance_infos, + meta_api=meta_api)) + + contents = buf.getvalue() + LOG.debug("Host metadata haproxy config = %s", contents) + file_utils.replace_file(haproxy_conf, contents) + return haproxy_conf + + def _get_proxy_process_manager(self, callback=None): + return external_process.ProcessManager( + conf=cfg.CONF, + uuid=self._host_id, + service=PROXY_SERVICE_NAME, + default_cmd_callback=callback, + run_as_root=True) + + def _spawn_proxy(self, haproxy_conf): + def callback(pid_file): + proxy_cmd = [PROXY_SERVICE_NAME, '-f', '%s' % haproxy_conf, + '-p', '%s' % pid_file] + return proxy_cmd + + pm = self._get_proxy_process_manager(callback) + pm.enable(reload_cfg=True) + self._process_monitor.register(uuid=self._host_id, + service_name=PROXY_SERVICE_NAME, + monitored_process=pm) + LOG.debug("Host metadata proxy enabled for host %s", self._host_id) + + def config(self, instance_infos): + infos = [] + for info in instance_infos: + infos.append(ProxyInstance(info['instance_id'], + info['provider_ip'], + info['project_id'])) + if infos: + self.haproxy_conf = self._generate_proxy_conf(infos) + + def enable(self): + if self.haproxy_conf: + self._spawn_proxy(self.haproxy_conf) + return + + self.disable() + + def disable(self): + self._process_monitor.unregister(uuid=self._host_id, + service_name=PROXY_SERVICE_NAME) + pm = self._get_proxy_process_manager() + pm.disable() + utils.remove_conf_files(cfg.CONF.state_path, self._host_id) + LOG.debug("Host metadata proxy disabled for host %s", self._host_id) + + @property + def enabled(self): + return self._get_proxy_process_manager().active diff --git a/neutron/conf/plugins/ml2/drivers/ovs_conf.py b/neutron/conf/plugins/ml2/drivers/ovs_conf.py index 7bd43dc52dc..e895e0098b5 100644 --- a/neutron/conf/plugins/ml2/drivers/ovs_conf.py +++ b/neutron/conf/plugins/ml2/drivers/ovs_conf.py @@ -18,6 +18,7 @@ from oslo_config import cfg from neutron._i18n import _ from neutron.conf.agent import common +from neutron.conf.agent.metadata import config as meta_conf DEFAULT_BRIDGE_MAPPINGS = [] @@ -260,6 +261,7 @@ def register_ovs_agent_opts(cfg=cfg.CONF): cfg.register_opts(dhcp_opts, "DHCP") cfg.register_opts(common.DHCP_PROTOCOL_OPTS, "DHCP") cfg.register_opts(local_ip_opts, "LOCAL_IP") + cfg.register_opts(meta_conf.METADATA_PROXY_HANDLER_OPTS, "METADATA") def register_ovs_opts(cfg=cfg.CONF): diff --git a/neutron/opts.py b/neutron/opts.py index b6117c9cc78..528126e71e1 100644 --- a/neutron/opts.py +++ b/neutron/opts.py @@ -355,7 +355,10 @@ def list_ovs_opts(): ('dhcp', itertools.chain( neutron.conf.plugins.ml2.drivers.ovs_conf.dhcp_opts, - neutron.conf.agent.common.DHCP_PROTOCOL_OPTS)) + neutron.conf.agent.common.DHCP_PROTOCOL_OPTS)), + ('metadata', + itertools.chain( + meta_conf.METADATA_PROXY_HANDLER_OPTS)) ] diff --git a/neutron/tests/unit/agent/l2/extensions/metadata/__init__.py b/neutron/tests/unit/agent/l2/extensions/metadata/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/agent/l2/extensions/metadata/test_host_metadata_proxy.py b/neutron/tests/unit/agent/l2/extensions/metadata/test_host_metadata_proxy.py new file mode 100644 index 00000000000..c6a57d9846d --- /dev/null +++ b/neutron/tests/unit/agent/l2/extensions/metadata/test_host_metadata_proxy.py @@ -0,0 +1,104 @@ +# Copyright (c) 2022 China Unicom Cloud Data Co.,Ltd. +# All Rights Reserved. +# +# 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 + +from oslo_config import cfg + +from neutron.agent.l2.extensions.metadata import host_metadata_proxy +from neutron.agent.linux import external_process +from neutron.tests import base + + +class TestHostMedataHAProxyDaemonMonitor(base.BaseTestCase): + + def setUp(self): + super(TestHostMedataHAProxyDaemonMonitor, self).setUp() + + self.ensure_dir = mock.patch( + 'oslo_utils.fileutils.ensure_tree').start() + + self.utils_exec_p = mock.patch( + 'neutron.agent.linux.utils.execute') + self.utils_exec = self.utils_exec_p.start() + + self.utils_replace_file_p = mock.patch( + 'neutron_lib.utils.file.replace_file') + self.utils_replace_file = self.utils_replace_file_p.start() + + def test_spawn_host_metadata_haproxy(self): + cfg.CONF.set_override('metadata_proxy_shared_secret', + 'secret', group='METADATA') + conffile = '/fake/host_metadata_proxy.haproxy.conf' + pidfile = '/fake/host_metadata_proxy.pid.haproxy' + process_monitor = external_process.ProcessMonitor( + config=cfg.CONF, + resource_type='MetadataPath') + + get_conf_file_name = 'neutron.agent.linux.utils.get_conf_file_name' + get_pid_file_name = ('neutron.agent.linux.external_process.' + 'ProcessManager.get_pid_file_name') + utils_execute = 'neutron.agent.common.utils.execute' + + mock.patch(get_conf_file_name).start().return_value = conffile + mock.patch(get_pid_file_name).start().return_value = pidfile + execute = mock.patch(utils_execute).start() + + host_meta = host_metadata_proxy.HostMedataHAProxyDaemonMonitor( + process_monitor) + instance_infos = [ + {"instance_id": "uuid1", + "provider_ip": "1.1.1.1", + "project_id": "project1"}] + host_meta.config(instance_infos) + host_meta.enable() + cmd = execute.call_args[0][0] + _join = lambda *args: ' '.join(args) + cmd = _join(*cmd) + self.assertIn('haproxy', cmd) + self.assertIn(_join('-f', conffile), cmd) + self.assertIn(_join('-p', pidfile), cmd) + + def test_generate_host_metadata_haproxy_config(self): + cfg.CONF.set_override('metadata_proxy_shared_secret', + 'secret', group='METADATA') + sig = ( + "3b5421875d7ba0fc910202f5ce448d9419597e7b66f702b53335116fee60e81e") + cfg.CONF.set_override('nova_metadata_host', + '2.2.2.2', + group='METADATA') + cfg.CONF.set_override('nova_metadata_port', + '8775', + group='METADATA') + process_monitor = external_process.ProcessMonitor( + config=cfg.CONF, + resource_type='MetadataPath') + host_meta = host_metadata_proxy.HostMedataHAProxyDaemonMonitor( + process_monitor) + instance_infos = [ + host_metadata_proxy.ProxyInstance('uuid1', '1.1.1.1', 'project1')] + host_meta._generate_proxy_conf(instance_infos) + acl = "acl instance_uuid1_1.1.1.1 src 1.1.1.1" + use_acl = "use_backend backend_uuid1_1.1.1.1 if instance_uuid1_1.1.1.1" + backend = "backend backend_uuid1_1.1.1.1" + http_hd_ins_id = "http-request set-header X-Instance-ID uuid1" + http_hd_pj = "http-request set-header X-Tenant-ID project1" + http_hd_sig = ( + "http-request set-header X-Instance-ID-Signature %s" % sig) + meta_real_srv = "server metasrv 2.2.2.2:8775" + expects = [acl, use_acl, backend, http_hd_ins_id, http_hd_pj, + http_hd_sig, meta_real_srv] + for exp in expects: + self.assertIn(exp, self.utils_replace_file.call_args[0][1])