5af046fd4e
If a user specifies a header in their request for metadata, it could override what the proxy would have inserted on their behalf. Make sure to remove any headers we don't want, and override something that might be present in the request. If the agent somehow gets a request with both headers it will silently drop it. Change-Id: Id6c103b7bcebe441c27c6049d349d84ba7fd15a6 Closes-bug: #1865036
313 lines
11 KiB
Python
313 lines
11 KiB
Python
# Copyright 2014 OpenStack Foundation.
|
|
# 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 errno
|
|
import grp
|
|
import os
|
|
import pwd
|
|
|
|
from neutron_lib.callbacks import events
|
|
from neutron_lib.callbacks import registry
|
|
from neutron_lib.callbacks import resources
|
|
from neutron_lib import constants
|
|
from neutron_lib import exceptions
|
|
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
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
METADATA_SERVICE_NAME = 'metadata-proxy'
|
|
HAPROXY_SERVICE = 'haproxy'
|
|
|
|
PROXY_CONFIG_DIR = "ns-metadata-proxy"
|
|
_HAPROXY_CONFIG_TEMPLATE = """
|
|
global
|
|
log /dev/log local0 %(log_level)s
|
|
log-tag %(log_tag)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 %(host)s:%(port)s
|
|
server metadata %(unix_socket_path)s
|
|
http-request del-header X-Neutron-%(res_type_del)s-ID
|
|
http-request set-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, host, 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.host = host
|
|
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 logging.is_debug_enabled(cfg.CONF) else 'info')
|
|
# log-tag will cause entries to have the string pre-pended, so use
|
|
# the uuid haproxy will be started with. Additionally, if it
|
|
# starts with "haproxy" then things will get logged to
|
|
# /var/log/haproxy.log on Debian distros, instead of to syslog.
|
|
uuid = network_id or router_id
|
|
self.log_tag = "haproxy-" + METADATA_SERVICE_NAME + "-" + uuid
|
|
|
|
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 = {
|
|
'host': self.host,
|
|
'port': self.port,
|
|
'unix_socket_path': self.unix_socket_path,
|
|
'user': username,
|
|
'group': groupname,
|
|
'pidfile': self.pidfile,
|
|
'log_level': self.log_level,
|
|
'log_tag': self.log_tag
|
|
}
|
|
# If using the network ID, delete any spurious router ID that might
|
|
# have been in the request, same for network ID when using router ID.
|
|
if self.network_id:
|
|
cfg_info['res_type'] = 'Network'
|
|
cfg_info['res_id'] = self.network_id
|
|
cfg_info['res_type_del'] = 'Router'
|
|
else:
|
|
cfg_info['res_type'] = 'Router'
|
|
cfg_info['res_id'] = self.router_id
|
|
cfg_info['res_type_del'] = 'Network'
|
|
|
|
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):
|
|
|
|
monitors = {}
|
|
|
|
def __init__(self, l3_agent):
|
|
self.metadata_port = l3_agent.conf.metadata_port
|
|
self.metadata_access_mark = l3_agent.conf.metadata_access_mark
|
|
registry.subscribe(
|
|
after_router_added, resources.ROUTER, events.AFTER_CREATE)
|
|
registry.subscribe(
|
|
after_router_updated, resources.ROUTER, events.AFTER_UPDATE)
|
|
registry.subscribe(
|
|
before_router_removed, resources.ROUTER, events.BEFORE_DELETE)
|
|
|
|
@classmethod
|
|
def metadata_filter_rules(cls, port, mark):
|
|
return [('INPUT', '-m mark --mark %s/%s -j ACCEPT' %
|
|
(mark, constants.ROUTER_MARK_MASK)),
|
|
('INPUT', '-p tcp -m tcp --dport %s '
|
|
'-j DROP' % port)]
|
|
|
|
@classmethod
|
|
def metadata_nat_rules(cls, port):
|
|
return [('PREROUTING', '-d 169.254.169.254/32 '
|
|
'-i %(interface_name)s '
|
|
'-p tcp -m tcp --dport 80 -j REDIRECT '
|
|
'--to-ports %(port)s' %
|
|
{'interface_name': namespaces.INTERNAL_DEV_PREFIX + '+',
|
|
'port': port})]
|
|
|
|
@classmethod
|
|
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())
|
|
|
|
return user, group
|
|
|
|
@classmethod
|
|
def _get_metadata_proxy_callback(cls, bind_address, port, conf,
|
|
network_id=None, router_id=None):
|
|
def callback(pid_file):
|
|
metadata_proxy_socket = conf.metadata_proxy_socket
|
|
user, group = (
|
|
cls._get_metadata_proxy_user_group(conf))
|
|
haproxy = HaproxyConfigurator(network_id,
|
|
router_id,
|
|
metadata_proxy_socket,
|
|
bind_address,
|
|
port,
|
|
user,
|
|
group,
|
|
conf.state_path,
|
|
pid_file)
|
|
haproxy.create_config_file()
|
|
proxy_cmd = [HAPROXY_SERVICE,
|
|
'-f', haproxy.cfg_path]
|
|
return proxy_cmd
|
|
|
|
return callback
|
|
|
|
@classmethod
|
|
def spawn_monitored_metadata_proxy(cls, monitor, ns_name, port, conf,
|
|
bind_address="0.0.0.0", network_id=None,
|
|
router_id=None):
|
|
uuid = network_id or router_id
|
|
callback = cls._get_metadata_proxy_callback(
|
|
bind_address, port, conf, network_id=network_id,
|
|
router_id=router_id)
|
|
pm = cls._get_metadata_proxy_process_manager(uuid, conf,
|
|
ns_name=ns_name,
|
|
callback=callback)
|
|
pm.enable()
|
|
monitor.register(uuid, METADATA_SERVICE_NAME, pm)
|
|
cls.monitors[router_id] = pm
|
|
|
|
@classmethod
|
|
def destroy_monitored_metadata_proxy(cls, monitor, uuid, conf, ns_name):
|
|
monitor.unregister(uuid, METADATA_SERVICE_NAME)
|
|
pm = cls._get_metadata_proxy_process_manager(uuid, conf,
|
|
ns_name=ns_name)
|
|
pm.disable()
|
|
|
|
# Delete metadata proxy config file
|
|
HaproxyConfigurator.cleanup_config_file(uuid, cfg.CONF.state_path)
|
|
|
|
cls.monitors.pop(uuid, None)
|
|
|
|
@classmethod
|
|
def _get_metadata_proxy_process_manager(cls, router_id, conf, ns_name=None,
|
|
callback=None):
|
|
return external_process.ProcessManager(
|
|
conf=conf,
|
|
uuid=router_id,
|
|
namespace=ns_name,
|
|
service=HAPROXY_SERVICE,
|
|
default_cmd_callback=callback)
|
|
|
|
|
|
def after_router_added(resource, event, l3_agent, **kwargs):
|
|
router = kwargs['router']
|
|
proxy = l3_agent.metadata_driver
|
|
for c, r in proxy.metadata_filter_rules(proxy.metadata_port,
|
|
proxy.metadata_access_mark):
|
|
router.iptables_manager.ipv4['filter'].add_rule(c, r)
|
|
for c, r in proxy.metadata_nat_rules(proxy.metadata_port):
|
|
router.iptables_manager.ipv4['nat'].add_rule(c, r)
|
|
router.iptables_manager.apply()
|
|
|
|
if not isinstance(router, ha_router.HaRouter):
|
|
proxy.spawn_monitored_metadata_proxy(
|
|
l3_agent.process_monitor,
|
|
router.ns_name,
|
|
proxy.metadata_port,
|
|
l3_agent.conf,
|
|
router_id=router.router_id)
|
|
|
|
|
|
def after_router_updated(resource, event, l3_agent, **kwargs):
|
|
router = kwargs['router']
|
|
proxy = l3_agent.metadata_driver
|
|
if (not proxy.monitors.get(router.router_id) and
|
|
not isinstance(router, ha_router.HaRouter)):
|
|
proxy.spawn_monitored_metadata_proxy(
|
|
l3_agent.process_monitor,
|
|
router.ns_name,
|
|
proxy.metadata_port,
|
|
l3_agent.conf,
|
|
router_id=router.router_id)
|
|
|
|
|
|
def before_router_removed(resource, event, l3_agent, payload=None):
|
|
router = payload.latest_state
|
|
proxy = l3_agent.metadata_driver
|
|
|
|
proxy.destroy_monitored_metadata_proxy(l3_agent.process_monitor,
|
|
router.router['id'],
|
|
l3_agent.conf,
|
|
router.ns_name)
|