Merge "add metadata proxy support for Quantum Networks"
This commit is contained in:
commit
59e0cdce22
20
bin/quantum-metadata-agent
Executable file
20
bin/quantum-metadata-agent
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 Openstack, LLC.
|
||||
# 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 quantum.agent.metadata.agent import main
|
||||
main()
|
20
bin/quantum-ns-metadata-proxy
Executable file
20
bin/quantum-ns-metadata-proxy
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 Openstack, LLC.
|
||||
# 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 quantum.agent.metadata.namespace_proxy import main
|
||||
main()
|
@ -27,6 +27,12 @@ root_helper = sudo
|
||||
# use_namespaces = True
|
||||
|
||||
# If use_namespaces is set as False then the agent can only configure one router.
|
||||
|
||||
# Where to store metadata state files. This directory must be writable by the
|
||||
# user executing the agent. The value below is compatible with a default
|
||||
# devstack installation.
|
||||
state_path = /opt/stack/data/quantum
|
||||
|
||||
# This is done by setting the specific router_id.
|
||||
# router_id =
|
||||
|
||||
@ -46,11 +52,8 @@ root_helper = sudo
|
||||
# empty value for the linux bridge
|
||||
# external_network_bridge = br-ex
|
||||
|
||||
# IP address used by Nova metadata server
|
||||
# metadata_ip =
|
||||
|
||||
# TCP Port used by Nova metadata server
|
||||
# metadata_port = 8775
|
||||
# TCP Port used by Quantum metadata server
|
||||
# metadata_port = 9697
|
||||
|
||||
# The time in seconds between state poll requests
|
||||
# polling_interval = 3
|
||||
|
32
etc/metadata_agent.ini
Normal file
32
etc/metadata_agent.ini
Normal file
@ -0,0 +1,32 @@
|
||||
[DEFAULT]
|
||||
# Show debugging output in log (sets DEBUG log level output)
|
||||
# debug = True
|
||||
|
||||
# The Quantum user information for accessing the Quantum API.
|
||||
auth_url = http://localhost:35357/v2.0
|
||||
auth_region = RegionOne
|
||||
admin_tenant_name = %SERVICE_TENANT_NAME%
|
||||
admin_user = %SERVICE_USER%
|
||||
admin_password = %SERVICE_PASSWORD%
|
||||
|
||||
# Use "sudo quantum-rootwrap /etc/quantum/rootwrap.conf" to use the real
|
||||
# root filter facility.
|
||||
# Change to "sudo" to skip the filtering and just run the comand directly
|
||||
root_helper = sudo
|
||||
|
||||
# Where to store metadata state files. This directory must be writable by the
|
||||
# user executing the agent. The value below is compatible with a default
|
||||
# devstack installation.
|
||||
state_path = /opt/stack/data/quantum
|
||||
|
||||
# IP address used by Nova metadata server
|
||||
# nova_metadata_ip = 127.0.0.1
|
||||
|
||||
# TCP Port used by Nova metadata server
|
||||
# nova_metadata_port = 8775
|
||||
|
||||
# When proxying metadata requests, Quantum signs the Instance-ID header with a
|
||||
# shared secret to prevent spoofing. You may select any string for a secret,
|
||||
# but it must match here and in the configuration used by the Nova Metadata
|
||||
# Server. NOTE: Nova uses a different key: quantum_metadata_proxy_shared_secret
|
||||
# metadata_proxy_shared_secret =
|
@ -16,6 +16,11 @@ arping_sbin: CommandFilter, /sbin/arping, root
|
||||
sysctl: CommandFilter, /sbin/sysctl, root
|
||||
route: CommandFilter, /sbin/route, root
|
||||
|
||||
# metadata proxy
|
||||
metadata_proxy: CommandFilter, /usr/local/bin/quantum-ns-metadata-proxy, root
|
||||
kill_metadata7: KillFilter, root, /usr/bin/python2.7, -9
|
||||
kill_metadata6: KillFilter, root, /usr/bin/python2.6, -9
|
||||
|
||||
# ip_lib
|
||||
ip: IpFilter, /sbin/ip, root
|
||||
ip_usr: IpFilter, /usr/sbin/ip, root
|
||||
|
@ -25,6 +25,7 @@ import time
|
||||
import netaddr
|
||||
|
||||
from quantum.agent.common import config
|
||||
from quantum.agent.linux import external_process
|
||||
from quantum.agent.linux import interface
|
||||
from quantum.agent.linux import ip_lib
|
||||
from quantum.agent.linux import iptables_manager
|
||||
@ -78,11 +79,9 @@ class L3NATAgent(object):
|
||||
cfg.IntOpt('polling_interval',
|
||||
default=3,
|
||||
help="The time in seconds between state poll requests."),
|
||||
cfg.StrOpt('metadata_ip', default='',
|
||||
help="IP address used by Nova metadata server."),
|
||||
cfg.IntOpt('metadata_port',
|
||||
default=8775,
|
||||
help="TCP Port used by Nova metadata server."),
|
||||
default=9697,
|
||||
help="TCP Port used by Quantum metadata namespace proxy."),
|
||||
cfg.IntOpt('send_arp_for_ha',
|
||||
default=3,
|
||||
help="Send this many gratuitous ARPs for HA setup, "
|
||||
@ -244,6 +243,7 @@ class L3NATAgent(object):
|
||||
for c, r in self.metadata_nat_rules():
|
||||
ri.iptables_manager.ipv4['nat'].add_rule(c, r)
|
||||
ri.iptables_manager.apply()
|
||||
self._spawn_metadata_agent(ri)
|
||||
|
||||
def _router_removed(self, router_id):
|
||||
ri = self.router_info[router_id]
|
||||
@ -252,9 +252,32 @@ class L3NATAgent(object):
|
||||
for c, r in self.metadata_nat_rules():
|
||||
ri.iptables_manager.ipv4['nat'].remove_rule(c, r)
|
||||
ri.iptables_manager.apply()
|
||||
self._destroy_metadata_agent(ri)
|
||||
del self.router_info[router_id]
|
||||
self._destroy_router_namespace(ri.ns_name())
|
||||
|
||||
def _spawn_metadata_agent(self, router_info):
|
||||
def callback(pid_file):
|
||||
return ['quantum-ns-metadata-proxy',
|
||||
'--pid_file=%s' % pid_file,
|
||||
'--router_id=%s' % router_info.router_id,
|
||||
'--state_path=%s' % self.conf.state_path]
|
||||
|
||||
pm = external_process.ProcessManager(
|
||||
self.conf,
|
||||
router_info.router_id,
|
||||
self.conf.root_helper,
|
||||
router_info.ns_name())
|
||||
pm.enable(callback)
|
||||
|
||||
def _destroy_metadata_agent(self, router_info):
|
||||
pm = external_process.ProcessManager(
|
||||
self.conf,
|
||||
router_info.router_id,
|
||||
self.conf.root_helper,
|
||||
router_info.ns_name())
|
||||
pm.disable()
|
||||
|
||||
def _set_subnet_info(self, port):
|
||||
ips = port['fixed_ips']
|
||||
if not ips:
|
||||
@ -440,20 +463,16 @@ class L3NATAgent(object):
|
||||
|
||||
def metadata_filter_rules(self):
|
||||
rules = []
|
||||
if self.conf.metadata_ip:
|
||||
rules.append(('INPUT', '-s 0.0.0.0/0 -d %s '
|
||||
'-p tcp -m tcp --dport %s '
|
||||
'-j ACCEPT' %
|
||||
(self.conf.metadata_ip, self.conf.metadata_port)))
|
||||
rules.append(('INPUT', '-s 0.0.0.0/0 -d 127.0.0.1 '
|
||||
'-p tcp -m tcp --dport %s '
|
||||
'-j ACCEPT' % self.conf.metadata_port))
|
||||
return rules
|
||||
|
||||
def metadata_nat_rules(self):
|
||||
rules = []
|
||||
if self.conf.metadata_ip:
|
||||
rules.append(('PREROUTING', '-s 0.0.0.0/0 -d 169.254.169.254/32 '
|
||||
'-p tcp -m tcp --dport 80 -j DNAT '
|
||||
'--to-destination %s:%s' %
|
||||
(self.conf.metadata_ip, self.conf.metadata_port)))
|
||||
rules.append(('PREROUTING', '-s 0.0.0.0/0 -d 169.254.169.254/32 '
|
||||
'-p tcp -m tcp --dport 80 -j REDIRECT '
|
||||
'--to-port %s' % self.conf.metadata_port))
|
||||
return rules
|
||||
|
||||
def external_gateway_nat_rules(self, ex_gw_ip, internal_cidrs,
|
||||
@ -505,9 +524,6 @@ class L3NATAgent(object):
|
||||
def internal_network_nat_rules(self, ex_gw_ip, internal_cidr):
|
||||
rules = [('snat', '-s %s -j SNAT --to-source %s' %
|
||||
(internal_cidr, ex_gw_ip))]
|
||||
if self.conf.metadata_ip:
|
||||
rules.append(('POSTROUTING', '-s %s -d %s/32 -j ACCEPT' %
|
||||
(internal_cidr, self.conf.metadata_ip)))
|
||||
return rules
|
||||
|
||||
def floating_ip_added(self, ri, ex_gw_port, floating_ip, fixed_ip):
|
||||
@ -551,6 +567,7 @@ def main():
|
||||
conf = config.setup_conf()
|
||||
conf.register_opts(L3NATAgent.OPTS)
|
||||
conf.register_opts(interface.OPTS)
|
||||
conf.register_opts(external_process.OPTS)
|
||||
conf(sys.argv)
|
||||
config.setup_logging(conf)
|
||||
|
||||
|
148
quantum/agent/linux/daemon.py
Normal file
148
quantum/agent/linux/daemon.py
Normal file
@ -0,0 +1,148 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import atexit
|
||||
import fcntl
|
||||
import os
|
||||
from signal import SIGTERM
|
||||
import sys
|
||||
import time
|
||||
|
||||
from quantum.agent.linux import utils
|
||||
from quantum.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pidfile(object):
|
||||
def __init__(self, pidfile, procname, root_helper='sudo'):
|
||||
try:
|
||||
self.fd = os.open(pidfile, os.O_CREAT | os.O_RDWR)
|
||||
except IOError, e:
|
||||
LOG.exception(_("Failed to open pidfile: %s") % pidfile)
|
||||
sys.exit(1)
|
||||
self.pidfile = pidfile
|
||||
self.procname = procname
|
||||
self.root_helper = root_helper
|
||||
if not not fcntl.flock(self.fd, fcntl.LOCK_EX):
|
||||
raise IOError(_('Unable to lock pid file'))
|
||||
|
||||
def __str__(self):
|
||||
return self.pidfile
|
||||
|
||||
def unlock(self):
|
||||
if not not fcntl.flock(self.fd, fcntl.LOCK_UN):
|
||||
raise IOError(_('Unable to unlock pid file'))
|
||||
|
||||
def write(self, pid):
|
||||
os.ftruncate(self.fd, 0)
|
||||
os.write(self.fd, "%d" % pid)
|
||||
os.fsync(self.fd)
|
||||
|
||||
def read(self):
|
||||
try:
|
||||
pid = int(os.read(self.fd, 128))
|
||||
os.lseek(self.fd, 0, os.SEEK_SET)
|
||||
return pid
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
def is_running(self):
|
||||
pid = self.read()
|
||||
if not pid:
|
||||
return False
|
||||
|
||||
cmd = ['cat', '/proc/%s/cmdline' % pid]
|
||||
try:
|
||||
return self.procname in utils.execute(cmd, self.root_helper)
|
||||
except RuntimeError, e:
|
||||
return False
|
||||
|
||||
|
||||
class Daemon(object):
|
||||
"""
|
||||
A generic daemon class.
|
||||
|
||||
Usage: subclass the Daemon class and override the run() method
|
||||
"""
|
||||
def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null',
|
||||
stderr='/dev/null', procname='python', root_helper='sudo'):
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.procname = procname
|
||||
self.pidfile = Pidfile(pidfile, procname, root_helper)
|
||||
|
||||
def _fork(self):
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError, e:
|
||||
LOG.exception(_('Fork failed'))
|
||||
sys.exit(1)
|
||||
|
||||
def daemonize(self):
|
||||
"""Daemonize process by doing Stevens double fork."""
|
||||
# fork first time
|
||||
self._fork()
|
||||
|
||||
# decouple from parent environment
|
||||
os.chdir("/")
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
# fork second time
|
||||
self._fork()
|
||||
|
||||
# redirect standard file descriptors
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
stdin = open(self.stdin, 'r')
|
||||
stdout = open(self.stdout, 'a+')
|
||||
stderr = open(self.stderr, 'a+', 0)
|
||||
os.dup2(stdin.fileno(), sys.stdin.fileno())
|
||||
os.dup2(stdout.fileno(), sys.stdout.fileno())
|
||||
os.dup2(stderr.fileno(), sys.stderr.fileno())
|
||||
|
||||
# write pidfile
|
||||
atexit.register(self.delete_pid)
|
||||
self.pidfile.write(os.getpid())
|
||||
|
||||
def delete_pid(self):
|
||||
os.remove(str(self.pidfile))
|
||||
|
||||
def start(self):
|
||||
""" Start the daemon """
|
||||
|
||||
if self.pidfile.is_running():
|
||||
self.pidfile.unlock()
|
||||
message = _('pidfile %s already exist. Daemon already running?\n')
|
||||
LOG.error(message % self.pidfile)
|
||||
sys.exit(1)
|
||||
|
||||
# Start the daemon
|
||||
self.daemonize()
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
"""Override this method when subclassing Daemon.
|
||||
|
||||
start() will call this method after the process has daemonized.
|
||||
"""
|
||||
pass
|
112
quantum/agent/linux/external_process.py
Normal file
112
quantum/agent/linux/external_process.py
Normal file
@ -0,0 +1,112 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from quantum.agent.linux import ip_lib
|
||||
from quantum.agent.linux import utils
|
||||
from quantum.openstack.common import cfg
|
||||
from quantum.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
OPTS = [
|
||||
cfg.StrOpt('external_pids',
|
||||
default='$state_path/external/pids',
|
||||
help='Location to store child pid files'),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(OPTS)
|
||||
|
||||
|
||||
class ProcessManager(object):
|
||||
"""An external process manager for Quantum spawned processes.
|
||||
|
||||
Note: The manager expects uuid to be in cmdline.
|
||||
"""
|
||||
def __init__(self, conf, uuid, root_helper='sudo', namespace=None):
|
||||
self.conf = conf
|
||||
self.uuid = uuid
|
||||
self.root_helper = root_helper
|
||||
self.namespace = namespace
|
||||
|
||||
def enable(self, cmd_callback):
|
||||
if not self.active:
|
||||
cmd = cmd_callback(self.get_pid_file_name(ensure_pids_dir=True))
|
||||
|
||||
if self.namespace:
|
||||
ip_wrapper = ip_lib.IPWrapper(self.root_helper, self.namespace)
|
||||
ip_wrapper.netns.execute(cmd)
|
||||
else:
|
||||
# For normal sudo prepend the env vars before command
|
||||
utils.execute(cmd, self.root_helper)
|
||||
|
||||
def disable(self):
|
||||
pid = self.pid
|
||||
|
||||
if self.active:
|
||||
cmd = ['kill', '-9', pid]
|
||||
if self.namespace:
|
||||
ip_wrapper = ip_lib.IPWrapper(self.root_helper, self.namespace)
|
||||
ip_wrapper.netns.execute(cmd)
|
||||
else:
|
||||
utils.execute(cmd, self.root_helper)
|
||||
|
||||
elif pid:
|
||||
LOG.debug(_('Process for %(uuid)s pid %(pid)d is stale, ignoring '
|
||||
'command') % {'uuid': self.uuid, 'pid': pid})
|
||||
else:
|
||||
LOG.debug(_('No process started for %s') % self.uuid)
|
||||
|
||||
def get_pid_file_name(self, ensure_pids_dir=False):
|
||||
"""Returns the file name for a given kind of config file."""
|
||||
pids_dir = os.path.abspath(os.path.normpath(self.conf.external_pids))
|
||||
if ensure_pids_dir and not os.path.isdir(pids_dir):
|
||||
os.makedirs(pids_dir, 0755)
|
||||
|
||||
return os.path.join(pids_dir, self.uuid + '.pid')
|
||||
|
||||
@property
|
||||
def pid(self):
|
||||
"""Last known pid for this external process spawned for this uuid."""
|
||||
file_name = self.get_pid_file_name()
|
||||
msg = _('Error while reading %s')
|
||||
|
||||
try:
|
||||
with open(file_name, 'r') as f:
|
||||
return int(f.read())
|
||||
except IOError, e:
|
||||
msg = _('Unable to access %s')
|
||||
except ValueError, e:
|
||||
msg = _('Unable to convert value in %s')
|
||||
|
||||
LOG.debug(msg % file_name)
|
||||
return None
|
||||
|
||||
@property
|
||||
def active(self):
|
||||
pid = self.pid
|
||||
if pid is None:
|
||||
return False
|
||||
|
||||
cmd = ['cat', '/proc/%s/cmdline' % pid]
|
||||
try:
|
||||
return self.uuid in utils.execute(cmd, self.root_helper)
|
||||
except RuntimeError, e:
|
||||
return False
|
17
quantum/agent/metadata/__init__.py
Normal file
17
quantum/agent/metadata/__init__.py
Normal file
@ -0,0 +1,17 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
214
quantum/agent/metadata/agent.py
Normal file
214
quantum/agent/metadata/agent.py
Normal file
@ -0,0 +1,214 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import urlparse
|
||||
|
||||
import eventlet
|
||||
import httplib2
|
||||
from quantumclient.v2_0 import client
|
||||
import webob
|
||||
|
||||
from quantum.common import config
|
||||
from quantum.openstack.common import cfg
|
||||
from quantum.openstack.common import log as logging
|
||||
from quantum import wsgi
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
DEVICE_OWNER_ROUTER_INTF = "network:router_interface"
|
||||
|
||||
|
||||
class MetadataProxyHandler(object):
|
||||
OPTS = [
|
||||
cfg.StrOpt('admin_user'),
|
||||
cfg.StrOpt('admin_password'),
|
||||
cfg.StrOpt('admin_tenant_name'),
|
||||
cfg.StrOpt('auth_url'),
|
||||
cfg.StrOpt('auth_strategy', default='keystone'),
|
||||
cfg.StrOpt('auth_region'),
|
||||
cfg.StrOpt('nova_metadata_ip', default='127.0.0.1',
|
||||
help="IP address used by Nova metadata server."),
|
||||
cfg.IntOpt('nova_metadata_port',
|
||||
default=8775,
|
||||
help="TCP Port used by Nova metadata server."),
|
||||
cfg.StrOpt('metadata_proxy_shared_secret',
|
||||
default='',
|
||||
help='Shared secret to sign instance-id request')
|
||||
]
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
|
||||
self.qclient = client.Client(
|
||||
username=self.conf.admin_user,
|
||||
password=self.conf.admin_password,
|
||||
tenant_name=self.conf.admin_tenant_name,
|
||||
auth_url=self.conf.auth_url,
|
||||
auth_strategy=self.conf.auth_strategy,
|
||||
region_name=self.conf.auth_region
|
||||
)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
try:
|
||||
LOG.debug(_("Request: %s"), req)
|
||||
|
||||
instance_id = self._get_instance_id(req)
|
||||
if instance_id:
|
||||
return self._proxy_request(instance_id, req)
|
||||
else:
|
||||
return webob.exc.HTTPNotFound()
|
||||
|
||||
except Exception, e:
|
||||
LOG.exception(_("Unexpected error."))
|
||||
msg = _('An unknown error has occurred. '
|
||||
'Please try your request again.')
|
||||
return webob.exc.HTTPInternalServerError(explanation=unicode(msg))
|
||||
|
||||
def _get_instance_id(self, req):
|
||||
remote_address = req.headers.get('X-Forwarded-For')
|
||||
network_id = req.headers.get('X-Quantum-Network-ID')
|
||||
router_id = req.headers.get('X-Quantum-Router-ID')
|
||||
|
||||
if network_id:
|
||||
networks = [network_id]
|
||||
else:
|
||||
internal_ports = self.qclient.list_ports(
|
||||
device_id=router_id,
|
||||
device_owner=DEVICE_OWNER_ROUTER_INTF)['ports']
|
||||
|
||||
networks = [p['network_id'] for p in internal_ports]
|
||||
|
||||
ports = self.qclient.list_ports(
|
||||
network_id=networks,
|
||||
fixed_ips=['ip_address=%s' % remote_address])['ports']
|
||||
|
||||
if len(ports) == 1:
|
||||
return ports[0]['device_id']
|
||||
|
||||
def _proxy_request(self, instance_id, req):
|
||||
headers = {
|
||||
'X-Instance-ID': instance_id,
|
||||
'X-Instance-ID-Signature': self._sign_instance_id(instance_id)
|
||||
}
|
||||
|
||||
url = urlparse.urlunsplit((
|
||||
'http',
|
||||
'%s:%s' % (self.conf.nova_metadata_ip,
|
||||
self.conf.nova_metadata_port),
|
||||
req.path_info,
|
||||
req.query_string,
|
||||
''))
|
||||
|
||||
h = httplib2.Http()
|
||||
resp, content = h.request(url, headers=headers)
|
||||
|
||||
if resp.status == 200:
|
||||
LOG.debug(str(resp))
|
||||
return content
|
||||
elif resp.status == 403:
|
||||
msg = _(
|
||||
'The remote metadata server responded with Forbidden. This '
|
||||
'response usually occurs when shared secrets do not match.'
|
||||
)
|
||||
LOG.warn(msg)
|
||||
return webob.exc.HTTPForbidden()
|
||||
elif resp.status == 404:
|
||||
return webob.exc.HTTPNotFound()
|
||||
elif resp.status == 500:
|
||||
msg = _(
|
||||
'Remote metadata server experienced an internal server error.'
|
||||
)
|
||||
LOG.warn(msg)
|
||||
return webob.exc.HTTPInternalServerError(explanation=unicode(msg))
|
||||
else:
|
||||
raise Exception(_('Unexpected response code: %s') % resp.status)
|
||||
|
||||
def _sign_instance_id(self, instance_id):
|
||||
return hmac.new(self.conf.metadata_proxy_shared_secret,
|
||||
instance_id,
|
||||
hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
class UnixDomainHttpProtocol(eventlet.wsgi.HttpProtocol):
|
||||
def __init__(self, request, client_address, server):
|
||||
if client_address == '':
|
||||
client_address = ('<local>', 0)
|
||||
# base class is old-style, no super does not work properly
|
||||
eventlet.wsgi.HttpProtocol.__init__(self, request, client_address,
|
||||
server)
|
||||
|
||||
|
||||
class UnixDomainWSGIServer(wsgi.Server):
|
||||
def start(self, application, file_socket, backlog=128):
|
||||
sock = eventlet.listen(file_socket,
|
||||
family=socket.AF_UNIX,
|
||||
backlog=backlog)
|
||||
self.pool.spawn_n(self._run, application, sock)
|
||||
|
||||
def _run(self, application, socket):
|
||||
"""Start a WSGI service in a new green thread."""
|
||||
logger = logging.getLogger('eventlet.wsgi.server')
|
||||
eventlet.wsgi.server(socket,
|
||||
application,
|
||||
custom_pool=self.pool,
|
||||
protocol=UnixDomainHttpProtocol,
|
||||
log=logging.WritableLogger(logger))
|
||||
|
||||
|
||||
class UnixDomainMetadataProxy(object):
|
||||
OPTS = [
|
||||
cfg.StrOpt('metadata_proxy_socket',
|
||||
default='$state_path/metadata_proxy',
|
||||
help='Location for Metadata Proxy UNIX domain socket')
|
||||
]
|
||||
|
||||
def __init__(self, conf):
|
||||
self.conf = conf
|
||||
|
||||
dirname = os.path.dirname(cfg.CONF.metadata_proxy_socket)
|
||||
if os.path.isdir(dirname):
|
||||
try:
|
||||
os.unlink(cfg.CONF.metadata_proxy_socket)
|
||||
except OSError:
|
||||
if os.path.exists(cfg.CONF.metadata_proxy_socket):
|
||||
raise
|
||||
else:
|
||||
os.makedirs(dirname, 0755)
|
||||
|
||||
def run(self):
|
||||
server = UnixDomainWSGIServer('quantum-metadata-agent')
|
||||
server.start(MetadataProxyHandler(self.conf),
|
||||
self.conf.metadata_proxy_socket)
|
||||
server.wait()
|
||||
|
||||
|
||||
def main():
|
||||
eventlet.monkey_patch()
|
||||
cfg.CONF.register_opts(UnixDomainMetadataProxy.OPTS)
|
||||
cfg.CONF.register_opts(MetadataProxyHandler.OPTS)
|
||||
cfg.CONF(args=sys.argv, project='quantum')
|
||||
config.setup_logging(cfg.CONF)
|
||||
|
||||
proxy = UnixDomainMetadataProxy(cfg.CONF)
|
||||
proxy.run()
|
165
quantum/agent/metadata/namespace_proxy.py
Normal file
165
quantum/agent/metadata/namespace_proxy.py
Normal file
@ -0,0 +1,165 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import httplib
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import urlparse
|
||||
|
||||
import eventlet
|
||||
import httplib2
|
||||
import webob
|
||||
|
||||
from quantum.agent.linux import daemon
|
||||
from quantum.common import config
|
||||
from quantum.openstack.common import cfg
|
||||
from quantum.openstack.common import log as logging
|
||||
from quantum import wsgi
|
||||
|
||||
proxy_socket = cfg.StrOpt('metadata_proxy_socket',
|
||||
default='$state_path/metadata_proxy',
|
||||
help='Location of Metadata Proxy UNIX domain socket')
|
||||
|
||||
cfg.CONF.register_opt(proxy_socket)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UnixDomainHTTPConnection(httplib.HTTPConnection):
|
||||
"""Connection class for HTTP over UNIX domain socket."""
|
||||
def __init__(self, host, port=None, strict=None, timeout=None,
|
||||
proxy_info=None):
|
||||
httplib.HTTPConnection.__init__(self, host, port, strict)
|
||||
self.timeout = timeout
|
||||
|
||||
def connect(self):
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
if self.timeout:
|
||||
self.sock.settimeout(self.timeout)
|
||||
self.sock.connect(cfg.CONF.metadata_proxy_socket)
|
||||
|
||||
|
||||
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:
|
||||
msg = _('network_id and router_id are None. One must be provided.')
|
||||
raise ValueError(msg)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
LOG.debug(_("Request: %s"), req)
|
||||
try:
|
||||
return self._proxy_request(req.remote_addr,
|
||||
req.path_info,
|
||||
req.query_string)
|
||||
except Exception, e:
|
||||
LOG.exception(_("Unexpected error."))
|
||||
msg = _('An unknown error has occurred. '
|
||||
'Please try your request again.')
|
||||
return webob.exc.HTTPInternalServerError(explanation=unicode(msg))
|
||||
|
||||
def _proxy_request(self, remote_address, path_info, query_string):
|
||||
headers = {
|
||||
'X-Forwarded-For': remote_address,
|
||||
}
|
||||
|
||||
if self.router_id:
|
||||
headers['X-Quantum-Router-ID'] = self.router_id
|
||||
else:
|
||||
headers['X-Quantum-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,
|
||||
headers=headers,
|
||||
connection_type=UnixDomainHTTPConnection)
|
||||
|
||||
if resp.status == 200:
|
||||
LOG.debug(resp)
|
||||
LOG.debug(content)
|
||||
return content
|
||||
elif resp.status == 404:
|
||||
return webob.exc.HTTPNotFound()
|
||||
elif resp.status == 500:
|
||||
msg = _(
|
||||
'Remote metadata server experienced an internal server error.'
|
||||
)
|
||||
LOG.debug(msg)
|
||||
return webob.exc.HTTPInternalServerError(explanation=unicode(msg))
|
||||
else:
|
||||
raise Exception(_('Unexpected response code: %s') % resp.status)
|
||||
|
||||
|
||||
class ProxyDaemon(daemon.Daemon):
|
||||
def __init__(self, pidfile, port, network_id=None, router_id=None):
|
||||
super(ProxyDaemon, self).__init__(pidfile)
|
||||
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('quantum-network-metadata-proxy')
|
||||
proxy.start(handler, self.port)
|
||||
proxy.wait()
|
||||
|
||||
|
||||
def main():
|
||||
eventlet.monkey_patch()
|
||||
opts = [
|
||||
cfg.StrOpt('network_id'),
|
||||
cfg.StrOpt('router_id'),
|
||||
cfg.StrOpt('pid_file'),
|
||||
cfg.BoolOpt('daemonize', default=True),
|
||||
cfg.IntOpt('metadata_port',
|
||||
default=9697,
|
||||
help="TCP Port to listen for metadata server requests."),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(opts)
|
||||
cfg.CONF(args=sys.argv, project='quantum')
|
||||
config.setup_logging(cfg.CONF)
|
||||
|
||||
proxy = ProxyDaemon(cfg.CONF.pid_file,
|
||||
cfg.CONF.metadata_port,
|
||||
network_id=cfg.CONF.network_id,
|
||||
router_id=cfg.CONF.router_id)
|
||||
|
||||
if cfg.CONF.daemonize:
|
||||
proxy.start()
|
||||
else:
|
||||
proxy.run()
|
@ -49,6 +49,10 @@ class TestBasicRouterOperations(unittest.TestCase):
|
||||
'quantum.agent.linux.utils.execute')
|
||||
self.utils_exec = self.utils_exec_p.start()
|
||||
|
||||
self.external_process_p = mock.patch(
|
||||
'quantum.agent.linux.external_process.ProcessManager')
|
||||
self.external_process = self.external_process_p.start()
|
||||
|
||||
self.dvr_cls_p = mock.patch('quantum.agent.linux.interface.NullDriver')
|
||||
driver_cls = self.dvr_cls_p.start()
|
||||
self.mock_driver = mock.MagicMock()
|
||||
@ -72,6 +76,7 @@ class TestBasicRouterOperations(unittest.TestCase):
|
||||
self.ip_cls_p.stop()
|
||||
self.dvr_cls_p.stop()
|
||||
self.utils_exec_p.stop()
|
||||
self.external_process_p.stop()
|
||||
|
||||
def testRouterInfoCreate(self):
|
||||
id = _uuid()
|
||||
@ -254,19 +259,25 @@ class TestBasicRouterOperations(unittest.TestCase):
|
||||
|
||||
def testSingleLoopRouterRemoval(self):
|
||||
agent = l3_agent.L3NATAgent(self.conf)
|
||||
router_id = _uuid()
|
||||
|
||||
self.client_inst.list_ports.return_value = {'ports': []}
|
||||
|
||||
self.client_inst.list_networks.return_value = {'networks': []}
|
||||
|
||||
self.client_inst.list_routers.return_value = {'routers': [
|
||||
{'id': _uuid(),
|
||||
{'id': router_id,
|
||||
'admin_state_up': True,
|
||||
'external_gateway_info': {}}]}
|
||||
agent.do_single_loop()
|
||||
|
||||
self.client_inst.list_routers.return_value = {'routers': []}
|
||||
agent.do_single_loop()
|
||||
self.external_process.assert_has_calls(
|
||||
[mock.call(agent.conf, router_id, 'sudo', 'qrouter-' + router_id),
|
||||
mock.call().enable(mock.ANY),
|
||||
mock.call(agent.conf, router_id, 'sudo', 'qrouter-' + router_id),
|
||||
mock.call().disable()])
|
||||
|
||||
# verify that remove is called
|
||||
self.assertEquals(self.mock_ip.get_devices.call_count, 1)
|
||||
|
179
quantum/tests/unit/test_linux_daemon.py
Normal file
179
quantum/tests/unit/test_linux_daemon.py
Normal file
@ -0,0 +1,179 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import os
|
||||
|
||||
import mock
|
||||
import unittest2 as unittest
|
||||
|
||||
from quantum.agent.linux import daemon
|
||||
|
||||
FAKE_FD = 8
|
||||
|
||||
|
||||
class TestPidfile(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.os_p = mock.patch.object(daemon, 'os')
|
||||
self.os = self.os_p.start()
|
||||
self.os.open.return_value = FAKE_FD
|
||||
|
||||
self.fcntl_p = mock.patch.object(daemon, 'fcntl')
|
||||
self.fcntl = self.fcntl_p.start()
|
||||
self.fcntl.flock.return_value = 0
|
||||
|
||||
def tearDown(self):
|
||||
self.fcntl_p.stop()
|
||||
self.os_p.stop()
|
||||
|
||||
def test_init(self):
|
||||
self.os.O_CREAT = os.O_CREAT
|
||||
self.os.O_RDWR = os.O_RDWR
|
||||
|
||||
p = daemon.Pidfile('thefile', 'python')
|
||||
self.os.open.assert_called_once_with('thefile', os.O_CREAT | os.O_RDWR)
|
||||
self.fcntl.flock.assert_called_once_with(FAKE_FD, self.fcntl.LOCK_EX)
|
||||
|
||||
def test_init_open_fail(self):
|
||||
self.os.open.side_effect = IOError
|
||||
|
||||
with mock.patch.object(daemon.sys, 'stderr') as stderr:
|
||||
with self.assertRaises(SystemExit):
|
||||
p = daemon.Pidfile('thefile', 'python')
|
||||
sys.assert_has_calls([
|
||||
mock.call.stderr.write(mock.ANY),
|
||||
mock.call.exit(1)]
|
||||
)
|
||||
|
||||
def test_unlock(self):
|
||||
p = daemon.Pidfile('thefile', 'python')
|
||||
p.unlock()
|
||||
self.fcntl.flock.assert_has_calls([
|
||||
mock.call(FAKE_FD, self.fcntl.LOCK_EX),
|
||||
mock.call(FAKE_FD, self.fcntl.LOCK_UN)]
|
||||
)
|
||||
|
||||
def test_write(self):
|
||||
p = daemon.Pidfile('thefile', 'python')
|
||||
p.write(34)
|
||||
|
||||
self.os.assert_has_calls([
|
||||
mock.call.ftruncate(FAKE_FD, 0),
|
||||
mock.call.write(FAKE_FD, '34'),
|
||||
mock.call.fsync(FAKE_FD)]
|
||||
)
|
||||
|
||||
def test_read(self):
|
||||
self.os.read.return_value = '34'
|
||||
p = daemon.Pidfile('thefile', 'python')
|
||||
self.assertEqual(34, p.read())
|
||||
|
||||
def test_is_running(self):
|
||||
with mock.patch('quantum.agent.linux.utils.execute') as execute:
|
||||
execute.return_value = 'python'
|
||||
p = daemon.Pidfile('thefile', 'python')
|
||||
|
||||
with mock.patch.object(p, 'read') as read:
|
||||
read.return_value = 34
|
||||
self.assertTrue(p.is_running())
|
||||
|
||||
execute.assert_called_once_with(
|
||||
['cat', '/proc/34/cmdline'], 'sudo')
|
||||
|
||||
|
||||
class TestDaemon(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.os_p = mock.patch.object(daemon, 'os')
|
||||
self.os = self.os_p.start()
|
||||
|
||||
self.pidfile_p = mock.patch.object(daemon, 'Pidfile')
|
||||
self.pidfile = self.pidfile_p.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.pidfile_p.stop()
|
||||
self.os_p.stop()
|
||||
|
||||
def test_init(self):
|
||||
d = daemon.Daemon('pidfile')
|
||||
self.assertEqual(d.procname, 'python')
|
||||
|
||||
def test_fork_parent(self):
|
||||
self.os.fork.return_value = 1
|
||||
with self.assertRaises(SystemExit):
|
||||
d = daemon.Daemon('pidfile')
|
||||
d._fork()
|
||||
|
||||
def test_fork_child(self):
|
||||
self.os.fork.return_value = 0
|
||||
d = daemon.Daemon('pidfile')
|
||||
self.assertIsNone(d._fork())
|
||||
|
||||
def test_fork_error(self):
|
||||
self.os.fork.side_effect = lambda: OSError(1)
|
||||
with mock.patch.object(daemon.sys, 'stderr') as stderr:
|
||||
with self.assertRaises(SystemExit):
|
||||
d = daemon.Daemon('pidfile', 'stdin')
|
||||
d._fork()
|
||||
|
||||
def test_daemonize(self):
|
||||
d = daemon.Daemon('pidfile')
|
||||
with mock.patch.object(d, '_fork') as fork:
|
||||
with mock.patch.object(daemon, 'atexit') as atexit:
|
||||
with mock.patch.object(daemon, 'sys') as sys:
|
||||
sys.stdin.fileno.return_value = 0
|
||||
sys.stdout.fileno.return_value = 1
|
||||
sys.stderr.fileno.return_value = 2
|
||||
d.daemonize()
|
||||
atexit.register.assert_called_once_with(d.delete_pid)
|
||||
|
||||
fork.assert_has_calls([mock.call(), mock.call()])
|
||||
|
||||
self.os.assert_has_calls([
|
||||
mock.call.chdir('/'),
|
||||
mock.call.setsid(),
|
||||
mock.call.umask(0),
|
||||
mock.call.dup2(mock.ANY, 0),
|
||||
mock.call.dup2(mock.ANY, 1),
|
||||
mock.call.dup2(mock.ANY, 2),
|
||||
mock.call.getpid()]
|
||||
)
|
||||
|
||||
def test_delete_pid(self):
|
||||
self.pidfile.return_value.__str__.return_value = 'pidfile'
|
||||
d = daemon.Daemon('pidfile')
|
||||
d.delete_pid()
|
||||
self.os.remove.assert_called_once_with('pidfile')
|
||||
|
||||
def test_start(self):
|
||||
self.pidfile.return_value.is_running.return_value = False
|
||||
d = daemon.Daemon('pidfile')
|
||||
|
||||
with mock.patch.object(d, 'daemonize') as daemonize:
|
||||
with mock.patch.object(d, 'run') as run:
|
||||
d.start()
|
||||
run.assert_called_once_with()
|
||||
daemonize.assert_called_once_with()
|
||||
|
||||
def test_start_running(self):
|
||||
self.pidfile.return_value.is_running.return_value = True
|
||||
d = daemon.Daemon('pidfile')
|
||||
|
||||
with mock.patch.object(daemon.sys, 'stderr') as stderr:
|
||||
with mock.patch.object(d, 'daemonize') as daemonize:
|
||||
with self.assertRaises(SystemExit):
|
||||
d.start()
|
||||
self.assertFalse(daemonize.called)
|
200
quantum/tests/unit/test_linux_external_process.py
Normal file
200
quantum/tests/unit/test_linux_external_process.py
Normal file
@ -0,0 +1,200 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import os
|
||||
|
||||
import mock
|
||||
import unittest2 as unittest
|
||||
|
||||
from quantum.agent.linux import external_process as ep
|
||||
|
||||
|
||||
class TestProcessManager(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.execute_p = mock.patch('quantum.agent.linux.utils.execute')
|
||||
self.execute = self.execute_p.start()
|
||||
self.conf = mock.Mock()
|
||||
self.conf.external_pids = '/var/path'
|
||||
|
||||
def tearDown(self):
|
||||
self.execute_p.stop()
|
||||
|
||||
def test_enable_no_namespace(self):
|
||||
callback = mock.Mock()
|
||||
callback.return_value = ['the', 'cmd']
|
||||
|
||||
with mock.patch.object(ep.ProcessManager, 'get_pid_file_name') as name:
|
||||
name.return_value = 'pidfile'
|
||||
with mock.patch.object(ep.ProcessManager, 'active') as active:
|
||||
active.__get__ = mock.Mock(return_value=False)
|
||||
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
manager.enable(callback)
|
||||
callback.assert_called_once_with('pidfile')
|
||||
name.assert_called_once_with(ensure_pids_dir=True)
|
||||
self.execute.assert_called_once_with(['the', 'cmd'], 'sudo')
|
||||
|
||||
def test_enable_with_namespace(self):
|
||||
callback = mock.Mock()
|
||||
callback.return_value = ['the', 'cmd']
|
||||
|
||||
with mock.patch.object(ep.ProcessManager, 'get_pid_file_name') as name:
|
||||
name.return_value = 'pidfile'
|
||||
with mock.patch.object(ep.ProcessManager, 'active') as active:
|
||||
active.__get__ = mock.Mock(return_value=False)
|
||||
|
||||
manager = ep.ProcessManager(self.conf, 'uuid', namespace='ns')
|
||||
with mock.patch.object(ep, 'ip_lib') as ip_lib:
|
||||
manager.enable(callback)
|
||||
callback.assert_called_once_with('pidfile')
|
||||
name.assert_called_once_with(ensure_pids_dir=True)
|
||||
ip_lib.assert_has_calls([
|
||||
mock.call.IPWrapper('sudo', 'ns'),
|
||||
mock.call.IPWrapper().netns.execute(['the', 'cmd'])]
|
||||
)
|
||||
|
||||
def test_enable_with_namespace_process_active(self):
|
||||
callback = mock.Mock()
|
||||
callback.return_value = ['the', 'cmd']
|
||||
|
||||
with mock.patch.object(ep.ProcessManager, 'active') as active:
|
||||
active.__get__ = mock.Mock(return_value=True)
|
||||
|
||||
manager = ep.ProcessManager(self.conf, 'uuid', namespace='ns')
|
||||
with mock.patch.object(ep, 'ip_lib') as ip_lib:
|
||||
manager.enable(callback)
|
||||
self.assertFalse(callback.called)
|
||||
|
||||
def test_disable_no_namespace(self):
|
||||
with mock.patch.object(ep.ProcessManager, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=4)
|
||||
with mock.patch.object(ep.ProcessManager, 'active') as active:
|
||||
active.__get__ = mock.Mock(return_value=True)
|
||||
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
manager.disable()
|
||||
self.execute(['kill', '-9', 4], 'sudo')
|
||||
|
||||
def test_disable_namespace(self):
|
||||
with mock.patch.object(ep.ProcessManager, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=4)
|
||||
with mock.patch.object(ep.ProcessManager, 'active') as active:
|
||||
active.__get__ = mock.Mock(return_value=True)
|
||||
|
||||
manager = ep.ProcessManager(self.conf, 'uuid', namespace='ns')
|
||||
|
||||
with mock.patch.object(ep, 'ip_lib') as ip_lib:
|
||||
manager.disable()
|
||||
ip_lib.assert_has_calls([
|
||||
mock.call.IPWrapper('sudo', 'ns'),
|
||||
mock.call.IPWrapper().netns.execute(['kill', '-9', 4])]
|
||||
)
|
||||
|
||||
def test_disable_not_active(self):
|
||||
with mock.patch.object(ep.ProcessManager, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=4)
|
||||
with mock.patch.object(ep.ProcessManager, 'active') as active:
|
||||
active.__get__ = mock.Mock(return_value=False)
|
||||
with mock.patch.object(ep.LOG, 'debug') as debug:
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
manager.disable()
|
||||
debug.assert_called_once_with(mock.ANY)
|
||||
|
||||
def test_disable_no_pid(self):
|
||||
with mock.patch.object(ep.ProcessManager, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=None)
|
||||
with mock.patch.object(ep.ProcessManager, 'active') as active:
|
||||
active.__get__ = mock.Mock(return_value=False)
|
||||
with mock.patch.object(ep.LOG, 'debug') as debug:
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
manager.disable()
|
||||
debug.assert_called_once_with(mock.ANY)
|
||||
|
||||
def test_get_pid_file_name_existing(self):
|
||||
with mock.patch.object(ep.os.path, 'isdir') as isdir:
|
||||
isdir.return_value = True
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
retval = manager.get_pid_file_name(ensure_pids_dir=True)
|
||||
self.assertEqual(retval, '/var/path/uuid.pid')
|
||||
|
||||
def test_get_pid_file_name_not_existing(self):
|
||||
with mock.patch.object(ep.os.path, 'isdir') as isdir:
|
||||
with mock.patch.object(ep.os, 'makedirs') as makedirs:
|
||||
isdir.return_value = False
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
retval = manager.get_pid_file_name(ensure_pids_dir=True)
|
||||
self.assertEqual(retval, '/var/path/uuid.pid')
|
||||
makedirs.assert_called_once_with('/var/path', 0755)
|
||||
|
||||
def test_get_pid_file_name_default(self):
|
||||
with mock.patch.object(ep.os.path, 'isdir') as isdir:
|
||||
isdir.return_value = True
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
retval = manager.get_pid_file_name(ensure_pids_dir=False)
|
||||
self.assertEqual(retval, '/var/path/uuid.pid')
|
||||
self.assertFalse(isdir.called)
|
||||
|
||||
def test_pid(self):
|
||||
with mock.patch('__builtin__.open') as mock_open:
|
||||
mock_open.return_value.__enter__ = lambda s: s
|
||||
mock_open.return_value.__exit__ = mock.Mock()
|
||||
mock_open.return_value.read.return_value = '5'
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
self.assertEqual(manager.pid, 5)
|
||||
|
||||
def test_pid_no_an_int(self):
|
||||
with mock.patch('__builtin__.open') as mock_open:
|
||||
mock_open.return_value.__enter__ = lambda s: s
|
||||
mock_open.return_value.__exit__ = mock.Mock()
|
||||
mock_open.return_value.read.return_value = 'foo'
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
self.assertIsNone(manager.pid, 5)
|
||||
|
||||
def test_pid_invalid_file(self):
|
||||
with mock.patch.object(ep.ProcessManager, 'get_pid_file_name') as name:
|
||||
name.return_value = '.doesnotexist/pid'
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
self.assertIsNone(manager.pid)
|
||||
|
||||
def test_active(self):
|
||||
dummy_cmd_line = 'python foo --router_id=uuid'
|
||||
self.execute.return_value = dummy_cmd_line
|
||||
with mock.patch.object(ep.ProcessManager, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=4)
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
self.assertTrue(manager.active)
|
||||
self.execute.assert_called_once_with(['cat', '/proc/4/cmdline'],
|
||||
'sudo')
|
||||
|
||||
def test_active_none(self):
|
||||
dummy_cmd_line = 'python foo --router_id=uuid'
|
||||
self.execute.return_value = dummy_cmd_line
|
||||
with mock.patch.object(ep.ProcessManager, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=None)
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
self.assertFalse(manager.active)
|
||||
|
||||
def test_active_cmd_mismatch(self):
|
||||
dummy_cmd_line = 'python foo --router_id=anotherid'
|
||||
self.execute.return_value = dummy_cmd_line
|
||||
with mock.patch.object(ep.ProcessManager, 'pid') as pid:
|
||||
pid.__get__ = mock.Mock(return_value=4)
|
||||
manager = ep.ProcessManager(self.conf, 'uuid')
|
||||
self.assertFalse(manager.active)
|
||||
self.execute.assert_called_once_with(['cat', '/proc/4/cmdline'],
|
||||
'sudo')
|
432
quantum/tests/unit/test_metadata_agent.py
Normal file
432
quantum/tests/unit/test_metadata_agent.py
Normal file
@ -0,0 +1,432 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import socket
|
||||
|
||||
import mock
|
||||
import unittest2 as unittest
|
||||
import webob
|
||||
|
||||
from quantum.agent.metadata import agent
|
||||
|
||||
|
||||
class FakeConf(object):
|
||||
admin_user = 'quantum'
|
||||
admin_password = 'password'
|
||||
admin_tenant_name = 'tenant'
|
||||
auth_url = 'http://127.0.0.1'
|
||||
auth_strategy = 'keystone'
|
||||
auth_region = 'region'
|
||||
nova_metadata_ip = '9.9.9.9'
|
||||
nova_metadata_port = 8775
|
||||
metadata_proxy_shared_secret = 'secret'
|
||||
|
||||
|
||||
class TestMetadataProxyHandler(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.qclient_p = mock.patch('quantumclient.v2_0.client.Client')
|
||||
self.qclient = self.qclient_p.start()
|
||||
|
||||
self.log_p = mock.patch.object(agent, 'LOG')
|
||||
self.log = self.log_p.start()
|
||||
|
||||
self.handler = agent.MetadataProxyHandler(FakeConf)
|
||||
|
||||
def tearDown(self):
|
||||
self.log_p.stop()
|
||||
self.qclient_p.stop()
|
||||
|
||||
def test_call(self):
|
||||
req = mock.Mock()
|
||||
with mock.patch.object(self.handler, '_get_instance_id') as get_id:
|
||||
get_id.return_value = 'id'
|
||||
with mock.patch.object(self.handler, '_proxy_request') as proxy:
|
||||
proxy.return_value = 'value'
|
||||
|
||||
retval = self.handler(req)
|
||||
self.assertEqual(retval, 'value')
|
||||
|
||||
def test_call_no_instance_match(self):
|
||||
req = mock.Mock()
|
||||
with mock.patch.object(self.handler, '_get_instance_id') as get_id:
|
||||
get_id.return_value = 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_id') as get_id:
|
||||
get_id.side_effect = Exception
|
||||
retval = self.handler(req)
|
||||
self.assertIsInstance(retval, webob.exc.HTTPInternalServerError)
|
||||
self.assertEqual(len(self.log.mock_calls), 2)
|
||||
|
||||
def _get_instance_id_helper(self, headers, list_ports_retval,
|
||||
networks=None, router_id=None):
|
||||
headers['X-Forwarded-For'] = '192.168.1.1'
|
||||
req = mock.Mock(headers=headers)
|
||||
|
||||
def mock_list_ports(*args, **kwargs):
|
||||
return {'ports': list_ports_retval.pop(0)}
|
||||
|
||||
self.qclient.return_value.list_ports.side_effect = mock_list_ports
|
||||
retval = self.handler._get_instance_id(req)
|
||||
|
||||
expected = [
|
||||
mock.call(
|
||||
username=FakeConf.admin_user,
|
||||
tenant_name=FakeConf.admin_tenant_name,
|
||||
region_name=FakeConf.auth_region,
|
||||
auth_url=FakeConf.auth_url,
|
||||
password=FakeConf.admin_password,
|
||||
auth_strategy=FakeConf.auth_strategy)
|
||||
]
|
||||
|
||||
if router_id:
|
||||
expected.append(
|
||||
mock.call().list_ports(
|
||||
device_id=router_id,
|
||||
device_owner='network:router_interface'
|
||||
)
|
||||
)
|
||||
|
||||
expected.append(
|
||||
mock.call().list_ports(
|
||||
network_id=networks or [],
|
||||
fixed_ips=['ip_address=192.168.1.1'])
|
||||
)
|
||||
|
||||
self.qclient.assert_has_calls(expected)
|
||||
|
||||
return retval
|
||||
|
||||
def test_get_instance_id_router_id(self):
|
||||
router_id = 'the_id'
|
||||
headers = {
|
||||
'X-Quantum-Router-ID': router_id
|
||||
}
|
||||
|
||||
networks = ['net1', 'net2']
|
||||
ports = [
|
||||
[{'network_id': 'net1'}, {'network_id': 'net2'}],
|
||||
[{'device_id': 'device_id'}]
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
self._get_instance_id_helper(headers, ports, networks=networks,
|
||||
router_id=router_id),
|
||||
'device_id'
|
||||
)
|
||||
|
||||
def test_get_instance_id_router_id_no_match(self):
|
||||
router_id = 'the_id'
|
||||
headers = {
|
||||
'X-Quantum-Router-ID': router_id
|
||||
}
|
||||
|
||||
networks = ['net1', 'net2']
|
||||
ports = [
|
||||
[{'network_id': 'net1'}, {'network_id': 'net2'}],
|
||||
[]
|
||||
]
|
||||
|
||||
self.assertIsNone(
|
||||
self._get_instance_id_helper(headers, ports, networks=networks,
|
||||
router_id=router_id),
|
||||
)
|
||||
|
||||
def test_get_instance_id_network_id(self):
|
||||
network_id = 'the_id'
|
||||
headers = {
|
||||
'X-Quantum-Network-ID': network_id
|
||||
}
|
||||
|
||||
ports = [
|
||||
[{'device_id': 'device_id'}]
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
self._get_instance_id_helper(headers, ports, networks=['the_id']),
|
||||
'device_id'
|
||||
)
|
||||
|
||||
def test_get_instance_id_network_id_no_match(self):
|
||||
network_id = 'the_id'
|
||||
headers = {
|
||||
'X-Quantum-Network-ID': network_id
|
||||
}
|
||||
|
||||
ports = [[]]
|
||||
|
||||
self.assertIsNone(
|
||||
self._get_instance_id_helper(headers, ports, networks=['the_id'])
|
||||
)
|
||||
|
||||
def test_proxy_request_200(self):
|
||||
req = mock.Mock(path_info='/the_path', query_string='')
|
||||
resp = mock.Mock(status=200)
|
||||
with mock.patch.object(self.handler, '_sign_instance_id') as sign:
|
||||
sign.return_value = 'signed'
|
||||
with mock.patch('httplib2.Http') as mock_http:
|
||||
mock_http.return_value.request.return_value = (resp, 'content')
|
||||
|
||||
retval = self.handler._proxy_request('the_id', req)
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://9.9.9.9:8775/the_path',
|
||||
headers={
|
||||
'X-Instance-ID-Signature': 'signed',
|
||||
'X-Instance-ID': 'the_id'
|
||||
}
|
||||
)]
|
||||
)
|
||||
|
||||
self.assertEqual(retval, 'content')
|
||||
|
||||
def test_proxy_request_403(self):
|
||||
req = mock.Mock(path_info='/the_path', query_string='')
|
||||
resp = mock.Mock(status=403)
|
||||
with mock.patch.object(self.handler, '_sign_instance_id') as sign:
|
||||
sign.return_value = 'signed'
|
||||
with mock.patch('httplib2.Http') as mock_http:
|
||||
mock_http.return_value.request.return_value = (resp, 'content')
|
||||
|
||||
retval = self.handler._proxy_request('the_id', req)
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://9.9.9.9:8775/the_path',
|
||||
headers={
|
||||
'X-Instance-ID-Signature': 'signed',
|
||||
'X-Instance-ID': 'the_id'
|
||||
}
|
||||
)]
|
||||
)
|
||||
|
||||
self.assertIsInstance(retval, webob.exc.HTTPForbidden)
|
||||
|
||||
def test_proxy_request_404(self):
|
||||
req = mock.Mock(path_info='/the_path', query_string='')
|
||||
resp = mock.Mock(status=404)
|
||||
with mock.patch.object(self.handler, '_sign_instance_id') as sign:
|
||||
sign.return_value = 'signed'
|
||||
with mock.patch('httplib2.Http') as mock_http:
|
||||
mock_http.return_value.request.return_value = (resp, 'content')
|
||||
|
||||
retval = self.handler._proxy_request('the_id', req)
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://9.9.9.9:8775/the_path',
|
||||
headers={
|
||||
'X-Instance-ID-Signature': 'signed',
|
||||
'X-Instance-ID': 'the_id'
|
||||
}
|
||||
)]
|
||||
)
|
||||
|
||||
self.assertIsInstance(retval, webob.exc.HTTPNotFound)
|
||||
|
||||
def test_proxy_request_500(self):
|
||||
req = mock.Mock(path_info='/the_path', query_string='')
|
||||
resp = mock.Mock(status=500)
|
||||
with mock.patch.object(self.handler, '_sign_instance_id') as sign:
|
||||
sign.return_value = 'signed'
|
||||
with mock.patch('httplib2.Http') as mock_http:
|
||||
mock_http.return_value.request.return_value = (resp, 'content')
|
||||
|
||||
retval = self.handler._proxy_request('the_id', req)
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://9.9.9.9:8775/the_path',
|
||||
headers={
|
||||
'X-Instance-ID-Signature': 'signed',
|
||||
'X-Instance-ID': 'the_id'
|
||||
}
|
||||
)]
|
||||
)
|
||||
|
||||
self.assertIsInstance(
|
||||
retval,
|
||||
webob.exc.HTTPInternalServerError)
|
||||
|
||||
def test_proxy_request_other_code(self):
|
||||
req = mock.Mock(path_info='/the_path', query_string='')
|
||||
resp = mock.Mock(status=302)
|
||||
with mock.patch.object(self.handler, '_sign_instance_id') as sign:
|
||||
sign.return_value = 'signed'
|
||||
with mock.patch('httplib2.Http') as mock_http:
|
||||
mock_http.return_value.request.return_value = (resp, 'content')
|
||||
|
||||
with self.assertRaises(Exception) as e:
|
||||
self.handler._proxy_request('the_id', req)
|
||||
self.assertIn('302', str(e))
|
||||
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://9.9.9.9:8775/the_path',
|
||||
headers={
|
||||
'X-Instance-ID-Signature': 'signed',
|
||||
'X-Instance-ID': 'the_id'
|
||||
}
|
||||
)]
|
||||
)
|
||||
|
||||
def test_sign_instance_id(self):
|
||||
self.assertEqual(
|
||||
self.handler._sign_instance_id('foo'),
|
||||
'773ba44693c7553d6ee20f61ea5d2757a9a4f4a44d2841ae4e95b52e4cd62db4'
|
||||
)
|
||||
|
||||
|
||||
class TestUnixDomainHttpProtocol(unittest.TestCase):
|
||||
def test_init_empty_client(self):
|
||||
u = agent.UnixDomainHttpProtocol(mock.Mock(), '', mock.Mock())
|
||||
self.assertEqual(u.client_address, ('<local>', 0))
|
||||
|
||||
def test_init_with_client(self):
|
||||
u = agent.UnixDomainHttpProtocol(mock.Mock(), 'foo', mock.Mock())
|
||||
self.assertEqual(u.client_address, 'foo')
|
||||
|
||||
|
||||
class TestUnixDomainWSGIServer(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.eventlet_p = mock.patch.object(agent, 'eventlet')
|
||||
self.eventlet = self.eventlet_p.start()
|
||||
self.server = agent.UnixDomainWSGIServer('test')
|
||||
|
||||
def tearDown(self):
|
||||
self.eventlet_p.stop()
|
||||
|
||||
def test_start(self):
|
||||
mock_app = mock.Mock()
|
||||
with mock.patch.object(self.server, 'pool') as pool:
|
||||
self.server.start(mock_app, '/the/path')
|
||||
self.eventlet.assert_has_calls([
|
||||
mock.call.listen(
|
||||
'/the/path',
|
||||
family=socket.AF_UNIX,
|
||||
backlog=128
|
||||
)]
|
||||
)
|
||||
pool.spawn_n.assert_called_once_with(
|
||||
self.server._run,
|
||||
mock_app,
|
||||
self.eventlet.listen.return_value
|
||||
)
|
||||
|
||||
def test_run(self):
|
||||
with mock.patch.object(agent, 'logging') as logging:
|
||||
self.server._run('app', 'sock')
|
||||
|
||||
self.eventlet.wsgi.server.called_once_with(
|
||||
'sock',
|
||||
'app',
|
||||
self.server.pool,
|
||||
agent.UnixDomainHttpProtocol,
|
||||
mock.ANY
|
||||
)
|
||||
self.assertTrue(len(logging.mock_calls))
|
||||
|
||||
|
||||
class TestUnixDomainMetadataProxy(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cfg_p = mock.patch.object(agent, 'cfg')
|
||||
self.cfg = self.cfg_p.start()
|
||||
self.cfg.CONF.metadata_proxy_socket = '/the/path'
|
||||
|
||||
def tearDown(self):
|
||||
self.cfg_p.stop()
|
||||
|
||||
def test_init_doesnot_exists(self):
|
||||
with mock.patch('os.path.isdir') as isdir:
|
||||
with mock.patch('os.makedirs') as makedirs:
|
||||
isdir.return_value = False
|
||||
p = agent.UnixDomainMetadataProxy(mock.Mock())
|
||||
|
||||
isdir.assert_called_once_with('/the')
|
||||
makedirs.assert_called_once_with('/the', 0755)
|
||||
|
||||
def test_init_exists(self):
|
||||
with mock.patch('os.path.isdir') as isdir:
|
||||
with mock.patch('os.unlink') as unlink:
|
||||
isdir.return_value = True
|
||||
p = agent.UnixDomainMetadataProxy(mock.Mock())
|
||||
|
||||
isdir.assert_called_once_with('/the')
|
||||
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
|
||||
|
||||
p = agent.UnixDomainMetadataProxy(mock.Mock())
|
||||
|
||||
isdir.assert_called_once_with('/the')
|
||||
unlink.assert_called_once_with('/the/path')
|
||||
exists.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 self.assertRaises(OSError):
|
||||
p = agent.UnixDomainMetadataProxy(mock.Mock())
|
||||
|
||||
isdir.assert_called_once_with('/the')
|
||||
unlink.assert_called_once_with('/the/path')
|
||||
exists.assert_called_once_with('/the/path')
|
||||
|
||||
def test_run(self):
|
||||
with mock.patch.object(agent, 'MetadataProxyHandler') as handler:
|
||||
with mock.patch.object(agent, 'UnixDomainWSGIServer') as server:
|
||||
with mock.patch('os.path.isdir') as isdir:
|
||||
with mock.patch('os.makedirs') as makedirs:
|
||||
isdir.return_value = False
|
||||
|
||||
p = agent.UnixDomainMetadataProxy(self.cfg.CONF)
|
||||
p.run()
|
||||
|
||||
isdir.assert_called_once_with('/the')
|
||||
makedirs.assert_called_once_with('/the', 0755)
|
||||
server.assert_has_calls([
|
||||
mock.call('quantum-metadata-agent'),
|
||||
mock.call().start(handler.return_value,
|
||||
'/the/path'),
|
||||
mock.call().wait()]
|
||||
)
|
||||
|
||||
def test_main(self):
|
||||
with mock.patch.object(agent, 'UnixDomainMetadataProxy') as proxy:
|
||||
with mock.patch('eventlet.monkey_patch') as eventlet:
|
||||
with mock.patch.object(agent, 'config') as config:
|
||||
with mock.patch.object(agent, 'cfg') as cfg:
|
||||
agent.main()
|
||||
|
||||
self.assertTrue(eventlet.called)
|
||||
self.assertTrue(config.setup_logging.called)
|
||||
proxy.assert_has_calls([
|
||||
mock.call(cfg.CONF),
|
||||
mock.call().run()]
|
||||
)
|
292
quantum/tests/unit/test_metadata_namespace_proxy.py
Normal file
292
quantum/tests/unit/test_metadata_namespace_proxy.py
Normal file
@ -0,0 +1,292 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# @author: Mark McClain, DreamHost
|
||||
|
||||
import socket
|
||||
|
||||
import mock
|
||||
import unittest2 as unittest
|
||||
import webob
|
||||
|
||||
from quantum.agent.metadata import namespace_proxy as ns_proxy
|
||||
|
||||
|
||||
class FakeConf(object):
|
||||
admin_user = 'quantum'
|
||||
admin_password = 'password'
|
||||
admin_tenant_name = 'tenant'
|
||||
auth_url = 'http://127.0.0.1'
|
||||
auth_strategy = 'keystone'
|
||||
auth_region = 'region'
|
||||
nova_metadata_ip = '9.9.9.9'
|
||||
nova_metadata_port = 8775
|
||||
metadata_proxy_shared_secret = 'secret'
|
||||
|
||||
|
||||
class TestUnixDomainHttpConnection(unittest.TestCase):
|
||||
def test_connect(self):
|
||||
with mock.patch.object(ns_proxy, 'cfg') as cfg:
|
||||
cfg.CONF.metadata_proxy_socket = '/the/path'
|
||||
with mock.patch('socket.socket') as socket_create:
|
||||
conn = ns_proxy.UnixDomainHTTPConnection('169.254.169.254',
|
||||
timeout=3)
|
||||
|
||||
conn.connect()
|
||||
|
||||
socket_create.assert_has_calls([
|
||||
mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
||||
mock.call().settimeout(3),
|
||||
mock.call().connect('/the/path')]
|
||||
)
|
||||
self.assertEqual(conn.timeout, 3)
|
||||
|
||||
|
||||
class TestNetworkMetadataProxyHandler(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.log_p = mock.patch.object(ns_proxy, 'LOG')
|
||||
self.log = self.log_p.start()
|
||||
|
||||
self.handler = ns_proxy.NetworkMetadataProxyHandler('router_id')
|
||||
|
||||
def tearDown(self):
|
||||
self.log_p.stop()
|
||||
|
||||
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.path_info,
|
||||
req.query_string)
|
||||
|
||||
def test_no_argument_passed_to_init(self):
|
||||
with self.assertRaises(ValueError):
|
||||
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.assertEqual(len(self.log.mock_calls), 2)
|
||||
self.assertTrue(proxy_req.called)
|
||||
|
||||
def test_proxy_request_router_200(self):
|
||||
self.handler.router_id = 'router_id'
|
||||
|
||||
resp = mock.Mock(status=200)
|
||||
with mock.patch('httplib2.Http') as mock_http:
|
||||
mock_http.return_value.request.return_value = (resp, 'content')
|
||||
|
||||
retval = self.handler._proxy_request('192.168.1.1',
|
||||
'/latest/meta-data',
|
||||
'')
|
||||
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://169.254.169.254/latest/meta-data',
|
||||
headers={
|
||||
'X-Forwarded-For': '192.168.1.1',
|
||||
'X-Quantum-Router-ID': 'router_id'
|
||||
},
|
||||
connection_type=ns_proxy.UnixDomainHTTPConnection
|
||||
)]
|
||||
)
|
||||
|
||||
self.assertEqual(retval, 'content')
|
||||
|
||||
def test_proxy_request_network_200(self):
|
||||
self.handler.network_id = 'network_id'
|
||||
|
||||
resp = mock.Mock(status=200)
|
||||
with mock.patch('httplib2.Http') as mock_http:
|
||||
mock_http.return_value.request.return_value = (resp, 'content')
|
||||
|
||||
retval = self.handler._proxy_request('192.168.1.1',
|
||||
'/latest/meta-data',
|
||||
'')
|
||||
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://169.254.169.254/latest/meta-data',
|
||||
headers={
|
||||
'X-Forwarded-For': '192.168.1.1',
|
||||
'X-Quantum-Network-ID': 'network_id'
|
||||
},
|
||||
connection_type=ns_proxy.UnixDomainHTTPConnection
|
||||
)]
|
||||
)
|
||||
|
||||
self.assertEqual(retval, 'content')
|
||||
|
||||
def test_proxy_request_network_404(self):
|
||||
self.handler.network_id = 'network_id'
|
||||
|
||||
resp = mock.Mock(status=404)
|
||||
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',
|
||||
'/latest/meta-data',
|
||||
'')
|
||||
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://169.254.169.254/latest/meta-data',
|
||||
headers={
|
||||
'X-Forwarded-For': '192.168.1.1',
|
||||
'X-Quantum-Network-ID': 'network_id'
|
||||
},
|
||||
connection_type=ns_proxy.UnixDomainHTTPConnection
|
||||
)]
|
||||
)
|
||||
|
||||
self.assertIsInstance(retval, webob.exc.HTTPNotFound)
|
||||
|
||||
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',
|
||||
'/latest/meta-data',
|
||||
'')
|
||||
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://169.254.169.254/latest/meta-data',
|
||||
headers={
|
||||
'X-Forwarded-For': '192.168.1.1',
|
||||
'X-Quantum-Network-ID': 'network_id'
|
||||
},
|
||||
connection_type=ns_proxy.UnixDomainHTTPConnection
|
||||
)]
|
||||
)
|
||||
|
||||
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 self.assertRaises(Exception):
|
||||
self.handler._proxy_request('192.168.1.1',
|
||||
'/latest/meta-data',
|
||||
'')
|
||||
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://169.254.169.254/latest/meta-data',
|
||||
headers={
|
||||
'X-Forwarded-For': '192.168.1.1',
|
||||
'X-Quantum-Network-ID': 'network_id'
|
||||
},
|
||||
connection_type=ns_proxy.UnixDomainHTTPConnection
|
||||
)]
|
||||
)
|
||||
|
||||
def test_proxy_request_network_exception(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.side_effect = Exception
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
self.handler._proxy_request('192.168.1.1',
|
||||
'/latest/meta-data',
|
||||
'')
|
||||
|
||||
mock_http.assert_has_calls([
|
||||
mock.call().request(
|
||||
'http://169.254.169.254/latest/meta-data',
|
||||
headers={
|
||||
'X-Forwarded-For': '192.168.1.1',
|
||||
'X-Quantum-Network-ID': 'network_id'
|
||||
},
|
||||
connection_type=ns_proxy.UnixDomainHTTPConnection
|
||||
)]
|
||||
)
|
||||
|
||||
|
||||
class TestProxyDaemon(unittest.TestCase):
|
||||
def test_init(self):
|
||||
with mock.patch('quantum.agent.linux.daemon.Pidfile') as pf:
|
||||
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('quantum.agent.linux.daemon.Pidfile') as pf:
|
||||
with mock.patch('quantum.wsgi.Server') as Server:
|
||||
pd = ns_proxy.ProxyDaemon('pidfile', 9697, 'net_id',
|
||||
'router_id')
|
||||
pd.run()
|
||||
Server.assert_has_calls([
|
||||
mock.call('quantum-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('eventlet.monkey_patch') as eventlet:
|
||||
with mock.patch.object(ns_proxy, 'config') as config:
|
||||
with mock.patch.object(ns_proxy, 'cfg') as 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
|
||||
ns_proxy.main()
|
||||
|
||||
self.assertTrue(eventlet.called)
|
||||
self.assertTrue(config.setup_logging.called)
|
||||
daemon.assert_has_calls([
|
||||
mock.call('pidfile', 9697, router_id='router_id',
|
||||
network_id=None),
|
||||
mock.call().start()]
|
||||
)
|
||||
|
||||
def test_main_dont_fork(self):
|
||||
with mock.patch.object(ns_proxy, 'ProxyDaemon') as daemon:
|
||||
with mock.patch('eventlet.monkey_patch') as eventlet:
|
||||
with mock.patch.object(ns_proxy, 'config') as config:
|
||||
with mock.patch.object(ns_proxy, 'cfg') as 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
|
||||
ns_proxy.main()
|
||||
|
||||
self.assertTrue(eventlet.called)
|
||||
self.assertTrue(config.setup_logging.called)
|
||||
daemon.assert_has_calls([
|
||||
mock.call('pidfile', 9697, router_id='router_id',
|
||||
network_id=None),
|
||||
mock.call().run()]
|
||||
)
|
4
setup.py
4
setup.py
@ -123,6 +123,10 @@ setuptools.setup(
|
||||
'quantum-l3-agent = quantum.agent.l3_agent:main',
|
||||
'quantum-linuxbridge-agent ='
|
||||
'quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent:main',
|
||||
'quantum-metadata-agent ='
|
||||
'quantum.agent.metadata.agent:main',
|
||||
'quantum-ns-metadata-proxy ='
|
||||
'quantum.agent.metadata.namespace_proxy:main',
|
||||
'quantum-openvswitch-agent ='
|
||||
'quantum.plugins.openvswitch.agent.ovs_quantum_agent:main',
|
||||
'quantum-ryu-agent = '
|
||||
|
Loading…
Reference in New Issue
Block a user