diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py index 749048da8bc..051bcc06ee9 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 @@ -697,9 +697,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. @@ -776,6 +774,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 13f1ca939ce..8898e817368 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 f2b5b53d962..47eeb9d0c44 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -23,6 +23,7 @@ import logging as std_logging import os import signal import socket +import uuid from eventlet.green import subprocess from oslo.config import cfg @@ -264,3 +265,12 @@ def log_opt_values(log): def is_valid_vlan_tag(vlan): return q_const.MIN_VLAN_TAG <= vlan <= q_const.MAX_VLAN_TAG + + +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 4965d133873..cfd3a97fa0c 100644 --- a/neutron/db/agentschedulers_db.py +++ b/neutron/db/agentschedulers_db.py @@ -22,6 +22,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 @@ -157,6 +158,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 230d99e13ed..8ab6d480960 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 685866a376e..e7257b96d5e 100644 --- a/neutron/tests/unit/test_db_plugin.py +++ b/neutron/tests/unit/test_db_plugin.py @@ -35,6 +35,7 @@ from neutron.common import config from neutron.common import constants from neutron.common import exceptions as n_exc from neutron.common.test_lib import test_config +from neutron.common import utils from neutron import context from neutron.db import api as db from neutron.db import db_base_plugin_v2 @@ -357,6 +358,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 e54f303db5f..a446ebc2135 100644 --- a/neutron/tests/unit/test_dhcp_agent.py +++ b/neutron/tests/unit/test_dhcp_agent.py @@ -1300,14 +1300,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