vmware-nsx/quantum/agent/dhcp_agent.py
Aaron Rosen 314134bdcb Make dhcp agent configurable for namespace
Bug #1035769

Change-Id: I9f0d48a73f059a4985d629c7f0209675f5b01bec
2012-08-12 14:14:07 -07:00

391 lines
14 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 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.
import collections
import logging
import socket
import sys
import time
import uuid
import netaddr
from sqlalchemy.ext import sqlsoup
from quantum.agent.common import config
from quantum.agent.linux import dhcp
from quantum.agent.linux import interface
from quantum.agent.linux import ip_lib
from quantum.common import exceptions
from quantum.openstack.common import cfg
from quantum.openstack.common import importutils
from quantum.version import version_string
from quantumclient.v2_0 import client
LOG = logging.getLogger(__name__)
State = collections.namedtuple('State',
['networks', 'subnet_hashes', 'ipalloc_hashes'])
class DhcpAgent(object):
OPTS = [
cfg.StrOpt('db_connection', default=''),
cfg.StrOpt('root_helper', default='sudo'),
cfg.StrOpt('dhcp_driver',
default='quantum.agent.linux.dhcp.Dnsmasq',
help="The driver used to manage the DHCP server."),
cfg.IntOpt('polling_interval',
default=3,
help="The time in seconds between state poll requests."),
cfg.IntOpt('reconnect_interval',
default=5,
help="The time in seconds between db reconnect attempts."),
cfg.BoolOpt('use_namespaces', default=True,
help="Allow overlapping IP.")
]
def __init__(self, conf):
self.conf = conf
self.dhcp_driver_cls = importutils.import_class(conf.dhcp_driver)
self.db = None
self.polling_interval = conf.polling_interval
self.reconnect_interval = conf.reconnect_interval
self._run = True
self.prev_state = State(set(), set(), set())
def daemon_loop(self):
while self._run:
delta = self.get_network_state_delta()
if delta is None:
continue
for network in delta.get('new', []):
self.call_driver('enable', network)
for network in delta.get('updated', []):
self.call_driver('reload_allocations', network)
for network in delta.get('deleted', []):
self.call_driver('disable', network)
time.sleep(self.polling_interval)
def _state_builder(self):
"""Polls the Quantum database and returns a represenation
of the network state.
The value returned is a State tuple that contains three sets:
networks, subnet_hashes, and ipalloc_hashes.
The hash sets are a tuple that contains the computed signature of the
obejct's metadata and the network that owns it. Signatures are used
because the objects metadata can change. Python's built-in hash
function is used on the string repr to compute the metadata signature.
"""
try:
if self.db is None:
time.sleep(self.reconnect_interval)
self.db = sqlsoup.SqlSoup(self.conf.db_connection)
LOG.info("Connecting to database \"%s\" on %s" %
(self.db.engine.url.database,
self.db.engine.url.host))
else:
# we have to commit to get the latest view
self.db.commit()
subnets = {}
subnet_hashes = set()
network_admin_up = {}
for network in self.db.networks.all():
network_admin_up[network.id] = network.admin_state_up
for subnet in self.db.subnets.all():
if (not subnet.enable_dhcp or
not network_admin_up[subnet.network_id]):
continue
subnet_hashes.add((hash(str(subnet)), subnet.network_id))
subnets[subnet.id] = subnet.network_id
ipalloc_hashes = set([(hash(str(a)), subnets[a.subnet_id])
for a in self.db.ipallocations.all()
if a.subnet_id in subnets])
networks = set(subnets.itervalues())
return State(networks, subnet_hashes, ipalloc_hashes)
except Exception, e:
LOG.warn('Unable to get network state delta. Exception: %s' % e)
self.db = None
return None
def get_network_state_delta(self):
"""Return a dict containing the sets of networks that are new,
updated, and deleted."""
delta = {}
state = self._state_builder()
if state is None:
return None
# determine the new/deleted networks
delta['deleted'] = self.prev_state.networks - state.networks
delta['new'] = state.networks - self.prev_state.networks
# Get the networks that have subnets added or deleted.
# The change candidates are the net_id portion of the symmetric diff
# between the sets of (subnet_hash,net_id)
candidates = set(
[h[1] for h in
(state.subnet_hashes ^ self.prev_state.subnet_hashes)]
)
# Update with the networks that have had allocations added/deleted.
# change candidates are the net_id portion of the symmetric diff
# between the sets of (alloc_hash,net_id)
candidates.update(
[h[1] for h in
(state.ipalloc_hashes ^ self.prev_state.ipalloc_hashes)]
)
# the updated set will contain new and deleted networks, so remove them
delta['updated'] = candidates - delta['new'] - delta['deleted']
self.prev_state = state
return delta
def call_driver(self, action, network_id):
"""Invoke an action on a DHCP driver instance."""
try:
# the Driver expects something that is duck typed similar to
# the base models. Augmenting will add support to the SqlSoup
# result, so that the Driver does have to concern itself with our
# db schema.
network = AugmentingWrapper(
self.db.networks.filter_by(id=network_id).one(),
self.db
)
driver = self.dhcp_driver_cls(self.conf,
network,
self.conf.root_helper,
DeviceManager(self.conf, self.db))
getattr(driver, action)()
except Exception, e:
LOG.warn('Unable to %s dhcp. Exception: %s' % (action, e))
# Manipulate the state so the action will be attempted on next
# loop iteration.
if action == 'disable':
# adding to prev state means we'll try to delete it next time
self.prev_state.networks.add(network_id)
else:
# removing means it will look like new next time
self.prev_state.networks.remove(network_id)
class DeviceManager(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('interface_driver',
help="The driver used to manage the virtual interface.")
]
def __init__(self, conf, db):
self.conf = conf
self.db = db
if not conf.interface_driver:
LOG.error(_('You must specify an interface driver'))
self.driver = importutils.import_object(conf.interface_driver, conf)
def get_interface_name(self, network, port=None):
if not port:
port = self._get_or_create_port(network)
return self.driver.get_device_name(port)
def get_device_id(self, network):
# There could be more than one dhcp server per network, so create
# a device id that combines host and network ids
host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, socket.gethostname())
return 'dhcp%s-%s' % (host_uuid, network.id)
def setup(self, network, reuse_existing=False):
port = self._get_or_create_port(network)
interface_name = self.get_interface_name(network, port)
if self.conf.use_namespaces:
namespace = network.id
else:
namespace = None
if ip_lib.device_exists(interface_name,
self.conf.root_helper,
namespace):
if not reuse_existing:
raise exceptions.PreexistingDeviceFailure(
dev_name=interface_name)
LOG.debug(_('Reusing existing device: %s.') % interface_name)
else:
self.driver.plug(network.id,
port.id,
interface_name,
port.mac_address,
namespace=namespace)
ip_cidrs = []
for fixed_ip in port.fixed_ips:
subnet = fixed_ip.subnet
net = netaddr.IPNetwork(subnet.cidr)
ip_cidr = '%s/%s' % (fixed_ip.ip_address, net.prefixlen)
ip_cidrs.append(ip_cidr)
self.driver.init_l3(interface_name, ip_cidrs,
namespace=namespace)
def destroy(self, network):
self.driver.unplug(self.get_interface_name(network))
def _get_or_create_port(self, network):
# todo (mark): reimplement using RPC
# Usage of client lib is a temporary measure.
try:
device_id = self.get_device_id(network)
port_obj = self.db.ports.filter_by(device_id=device_id).one()
port = AugmentingWrapper(port_obj, self.db)
except sqlsoup.SQLAlchemyError, e:
port = self._create_port(network)
return port
def _create_port(self, network):
# todo (mark): reimplement using RPC
# Usage of client lib is a temporary measure.
quantum = 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,
auth_region=self.conf.auth_region
)
body = dict(port=dict(
admin_state_up=True,
device_id=self.get_device_id(network),
network_id=network.id,
tenant_id=network.tenant_id,
fixed_ips=[dict(subnet_id=s.id) for s in network.subnets]))
port_dict = quantum.create_port(body)['port']
# we have to call commit since the port was created in outside of
# our current transaction
self.db.commit()
port = AugmentingWrapper(
self.db.ports.filter_by(id=port_dict['id']).one(),
self.db)
return port
class PortModel(object):
def __init__(self, port_dict):
self.__dict__.update(port_dict)
class AugmentingWrapper(object):
"""A wrapper that augments Sqlsoup results so that they look like the
base v2 db model.
"""
MAPPING = {
'networks': {'subnets': 'subnets', 'ports': 'ports'},
'subnets': {'allocations': 'ipallocations'},
'ports': {'fixed_ips': 'ipallocations'},
}
def __init__(self, obj, db):
self.obj = obj
self.db = db
def __repr__(self):
return repr(self.obj)
def __getattr__(self, name):
"""Executes a dynamic lookup of attributes to make SqlSoup results
mimic the same structure as the v2 db models.
The actual models could not be used because they're dependent on the
plugin and the agent is not tied to any plugin structure.
If .subnet, is accessed, the wrapper will return a subnet
object if this instance has a subnet_id attribute.
If the _id attribute does not exists then wrapper will check MAPPING
to see if a reverse relationship exists. If so, a wrapped result set
will be returned.
"""
try:
return getattr(self.obj, name)
except:
pass
id_attr = '%s_id' % name
if hasattr(self.obj, id_attr):
args = {'id': getattr(self.obj, id_attr)}
return AugmentingWrapper(
getattr(self.db, '%ss' % name).filter_by(**args).one(),
self.db
)
try:
attr_name = self.MAPPING[self.obj._table.name][name]
arg_name = '%s_id' % self.obj._table.name[:-1]
args = {arg_name: self.obj.id}
return [AugmentingWrapper(o, self.db) for o in
getattr(self.db, attr_name).filter_by(**args).all()]
except KeyError:
pass
raise AttributeError
def main():
conf = config.setup_conf()
conf.register_opts(DhcpAgent.OPTS)
conf.register_opts(DeviceManager.OPTS)
conf.register_opts(dhcp.OPTS)
conf.register_opts(interface.OPTS)
conf(sys.argv)
config.setup_logging(conf)
mgr = DhcpAgent(conf)
mgr.daemon_loop()
if __name__ == '__main__':
main()