--- /dev/null
+#!/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()
--- /dev/null
+#!/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()
# 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 =
# 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
--- /dev/null
+[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 =
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
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
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, "
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]
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:
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,
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):
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)
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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()
--- /dev/null
+# 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()
'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()
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()
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)
--- /dev/null
+# 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)
--- /dev/null
+# 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')
--- /dev/null
+# 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()]
+ )
--- /dev/null
+# 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()]
+ )
'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 = '