add metadata proxy support for Quantum Networks

blueprint metadata-overlapping-networks

Adds Metadata for guest VMs running on Qunatum networks.  This requires
a companion patchset for Nova to test.

Change-Id: I524c6fdcd6a44e46da08395fd84c1288052a69ea
This commit is contained in:
Mark McClain 2012-11-15 23:08:52 -05:00
parent f19d4bada1
commit 0c3dd5af4c
17 changed files with 1894 additions and 23 deletions

20
bin/quantum-metadata-agent Executable file
View 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
View 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()

View File

@ -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
View 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 =

View File

@ -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

View File

@ -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:
@ -437,20 +460,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,
@ -502,9 +521,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):
@ -548,6 +564,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)

View 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

View 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

View 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

View 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()

View 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()

View File

@ -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)

View 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)

View 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')

View 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()]
)

View 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()]
)

View File

@ -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 = '