From d5c0a37999f9e3a611a322baacabebc06b13283b Mon Sep 17 00:00:00 2001 From: Ed Bak Date: Fri, 7 Mar 2014 17:16:15 +0000 Subject: [PATCH] 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 --- neutron/agent/linux/dhcp.py | 19 +++++++++++---- neutron/common/constants.py | 2 ++ neutron/common/utils.py | 10 ++++++++ neutron/db/agentschedulers_db.py | 11 +++++++++ .../unit/openvswitch/test_agent_scheduler.py | 24 +++++++++++++++++++ neutron/tests/unit/test_db_plugin.py | 8 +++++++ neutron/tests/unit/test_dhcp_agent.py | 14 +++++------ 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py index 59a2d7ad1..432f0f602 100644 --- a/neutron/agent/linux/dhcp.py +++ b/neutron/agent/linux/dhcp.py @@ -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' diff --git a/neutron/common/constants.py b/neutron/common/constants.py index 14178970f..cf3fb6025 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -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' diff --git a/neutron/common/utils.py b/neutron/common/utils.py index 5b0d38a1d..317f35432 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -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) diff --git a/neutron/db/agentschedulers_db.py b/neutron/db/agentschedulers_db.py index 1e46a94fa..9ba1b2d1f 100644 --- a/neutron/db/agentschedulers_db.py +++ b/neutron/db/agentschedulers_db.py @@ -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: diff --git a/neutron/tests/unit/openvswitch/test_agent_scheduler.py b/neutron/tests/unit/openvswitch/test_agent_scheduler.py index 72d57568a..ddc1cee8b 100644 --- a/neutron/tests/unit/openvswitch/test_agent_scheduler.py +++ b/neutron/tests/unit/openvswitch/test_agent_scheduler.py @@ -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() diff --git a/neutron/tests/unit/test_db_plugin.py b/neutron/tests/unit/test_db_plugin.py index 7f764e332..1b7059bc9 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -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 diff --git a/neutron/tests/unit/test_dhcp_agent.py b/neutron/tests/unit/test_dhcp_agent.py index 5978874cf..cd7c41985 100644 --- a/neutron/tests/unit/test_dhcp_agent.py +++ b/neutron/tests/unit/test_dhcp_agent.py @@ -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' - - 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') + 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) + 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 -- 2.45.2