Provide way to reserve dhcp port during failovers

This change provides a way to save the dhcp port when failing
over a network from one dhcp agent to another.  When a
dhcp-agent-network-remove is issued, the dhcp port device_id is
marked as reserved which causes it to not be deleted. When a
subsequent dhcp-agent-network-add is issued, the reserved port
is used and the device_id is corrected.  This is desirable
in order to maintain the dhcp port ip address so that dns doesn't
get impacted. Unit test added.

Change-Id: I531d7ffab074b01adfe186d2c3df43ca978359cd
Closes-Bug: #1288923
This commit is contained in:
Ed Bak 2014-03-07 17:16:15 +00:00
parent f341b9ec88
commit d5c0a37999
7 changed files with 75 additions and 11 deletions

View File

@ -22,7 +22,6 @@ import re
import shutil
import socket
import sys
import uuid
import netaddr
from oslo.config import cfg
@ -32,6 +31,7 @@ from neutron.agent.linux import ip_lib
from neutron.agent.linux import utils
from neutron.common import constants
from neutron.common import exceptions
from neutron.common import utils as commonutils
from neutron.openstack.common import importutils
from neutron.openstack.common import jsonutils
from neutron.openstack.common import log as logging
@ -696,9 +696,7 @@ class DeviceManager(object):
"""Return a unique DHCP device ID for this host on the 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)
return commonutils.get_dhcp_agent_device_id(network.id, self.conf.host)
def _set_default_route(self, network, device_name):
"""Sets the default gateway for this dhcp namespace.
@ -775,6 +773,19 @@ class DeviceManager(object):
# break since we found port that matches device_id
break
# check for a reserved DHCP port
if dhcp_port is None:
LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'
' does not yet exist. Checking for a reserved port.'),
{'device_id': device_id, 'network_id': network.id})
for port in network.ports:
port_device_id = getattr(port, 'device_id', None)
if port_device_id == constants.DEVICE_ID_RESERVED_DHCP_PORT:
dhcp_port = self.plugin.update_dhcp_port(
port.id, {'port': {'device_id': device_id}})
if dhcp_port:
break
# DHCP port has not yet been created.
if dhcp_port is None:
LOG.debug(_('DHCP port %(device_id)s on network %(network_id)s'

View File

@ -34,6 +34,8 @@ DEVICE_OWNER_ROUTER_GW = "network:router_gateway"
DEVICE_OWNER_FLOATINGIP = "network:floatingip"
DEVICE_OWNER_DHCP = "network:dhcp"
DEVICE_ID_RESERVED_DHCP_PORT = "reserved_dhcp_port"
FLOATINGIP_KEY = '_floatingips'
INTERFACE_KEY = '_interfaces'
METERING_LABEL_KEY = '_metering_labels'

View File

@ -25,6 +25,7 @@ import os
import random
import signal
import socket
import uuid
from eventlet.green import subprocess
from oslo.config import cfg
@ -216,3 +217,12 @@ def get_random_string(length):
rndstr += hashlib.sha224(str(random.random())).hexdigest()
return rndstr[0:length]
def get_dhcp_agent_device_id(network_id, host):
# Split host so as to always use only the hostname and
# not the domain name. This will guarantee consistentcy
# whether a local hostname or an fqdn is passed in.
local_hostname = host.split('.')[0]
host_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, str(local_hostname))
return 'dhcp%s-%s' % (host_uuid, network_id)

View File

@ -20,6 +20,7 @@ from sqlalchemy.orm import exc
from sqlalchemy.orm import joinedload
from neutron.common import constants
from neutron.common import utils
from neutron.db import agents_db
from neutron.db import model_base
from neutron.extensions import dhcpagentscheduler
@ -155,6 +156,16 @@ class DhcpAgentSchedulerDbMixin(dhcpagentscheduler
except exc.NoResultFound:
raise dhcpagentscheduler.NetworkNotHostedByDhcpAgent(
network_id=network_id, agent_id=id)
# reserve the port, so the ip is reused on a subsequent add
device_id = utils.get_dhcp_agent_device_id(network_id,
agent['host'])
filters = dict(device_id=[device_id])
ports = self.get_ports(context, filters=filters)
for port in ports:
port['device_id'] = constants.DEVICE_ID_RESERVED_DHCP_PORT
self.update_port(context, port['id'], dict(port=port))
context.session.delete(binding)
dhcp_notifier = self.agent_notifiers.get(constants.AGENT_TYPE_DHCP)
if dhcp_notifier:

View File

@ -576,6 +576,30 @@ class OvsAgentSchedulerTestCase(OvsAgentSchedulerTestCaseBase):
self.assertEqual(1, num_before_remove)
self.assertEqual(0, num_after_remove)
def test_reserved_port_after_network_remove_from_dhcp_agent(self):
dhcp_hosta = {
'binary': 'neutron-dhcp-agent',
'host': DHCP_HOSTA,
'topic': 'DHCP_AGENT',
'configurations': {'dhcp_driver': 'dhcp_driver',
'use_namespaces': True,
},
'agent_type': constants.AGENT_TYPE_DHCP}
self._register_one_agent_state(dhcp_hosta)
hosta_id = self._get_agent_id(constants.AGENT_TYPE_DHCP,
DHCP_HOSTA)
with self.port(device_owner=constants.DEVICE_OWNER_DHCP,
host=DHCP_HOSTA) as port1:
self._remove_network_from_dhcp_agent(hosta_id,
port1['port']['network_id'])
port_res = self._list_ports(
'json',
200,
network_id=port1['port']['network_id'])
port_list = self.deserialize('json', port_res)
self.assertEqual(port_list['ports'][0]['device_id'],
constants.DEVICE_ID_RESERVED_DHCP_PORT)
def test_router_auto_schedule_with_invalid_router(self):
with self.router() as router:
l3_rpc = l3_rpc_base.L3RpcCallbackMixin()

View File

@ -31,6 +31,7 @@ from neutron.api.v2 import router
from neutron.common import constants
from neutron.common import exceptions as n_exc
from neutron.common import test_lib
from neutron.common import utils
from neutron import context
from neutron.db import api as db
from neutron.db import db_base_plugin_v2
@ -347,6 +348,13 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
# Arg must be present
if arg in kwargs:
data['port'][arg] = kwargs[arg]
# create a dhcp port device id if one hasn't been supplied
if ('device_owner' in kwargs and
kwargs['device_owner'] == constants.DEVICE_OWNER_DHCP and
'host' in kwargs and
not 'device_id' in kwargs):
device_id = utils.get_dhcp_agent_device_id(net_id, kwargs['host'])
data['port']['device_id'] = device_id
port_req = self.new_create_request('ports', data, fmt)
if (kwargs.get('set_context') and 'tenant_id' in kwargs):
# create a specific auth context for this request

View File

@ -1268,14 +1268,12 @@ class TestDeviceManager(base.BaseTestCase):
expected = ('dhcp1ae5f96c-c527-5079-82ea-371a01645457-12345678-1234-'
'5678-1234567890ab')
with mock.patch('socket.gethostname') as get_host:
with mock.patch('uuid.uuid5') as uuid5:
uuid5.return_value = '1ae5f96c-c527-5079-82ea-371a01645457'
get_host.return_value = 'localhost'
with mock.patch('uuid.uuid5') as uuid5:
uuid5.return_value = '1ae5f96c-c527-5079-82ea-371a01645457'
dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, None)
self.assertEqual(dh.get_device_id(fake_net), expected)
uuid5.assert_called_once_with(uuid.NAMESPACE_DNS, 'localhost')
dh = dhcp.DeviceManager(cfg.CONF, cfg.CONF.root_helper, None)
uuid5.called_once_with(uuid.NAMESPACE_DNS, cfg.CONF.host)
self.assertEqual(dh.get_device_id(fake_net), expected)
def test_update(self):
# Try with namespaces and no metadata network