diff --git a/nova/exception.py b/nova/exception.py index 9ffb4965e7be..4025f9a93573 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -843,6 +843,12 @@ class PortNotUsable(Invalid): msg_fmt = _("Port %(port_id)s not usable for instance %(instance)s.") +class PortNotUsableDNS(Invalid): + msg_fmt = _("Port %(port_id)s not usable for instance %(instance)s. " + "Value %(value)s assigned to dns_name attribute does not " + "match instance's hostname %(hostname)s") + + class PortNotFree(Invalid): msg_fmt = _("No free port available for instance %(instance)s.") diff --git a/nova/network/neutronv2/api.py b/nova/network/neutronv2/api.py index e58b0718f7c8..ac50035551bf 100644 --- a/nova/network/neutronv2/api.py +++ b/nova/network/neutronv2/api.py @@ -378,6 +378,8 @@ class API(base_api.NetworkAPI): port's MAC address is not in that set. :raises nova.exception.PortInUse: If a requested port is already attached to another instance. + :raises nova.exception.PortNotUsableDNS: If a requested port has a + value assigned to its dns_name attribute. """ available_macs = None @@ -408,6 +410,15 @@ class API(base_api.NetworkAPI): if port.get('device_id'): raise exception.PortInUse(port_id=request.port_id) + # Make sure the user didn't assign a value to the port's + # dns_name attribute. + if port.get('dns_name'): + if port['dns_name'] != instance.hostname: + raise exception.PortNotUsableDNS( + port_id=request.port_id, + instance=instance.uuid, value=port['dns_name'], + hostname=instance.hostname) + # Make sure the port is usable if (port.get('binding:vif_type') == network_model.VIF_TYPE_BINDING_FAILED): @@ -626,7 +637,8 @@ class API(base_api.NetworkAPI): try: self._populate_neutron_extension_values( context, instance, request.pci_request_id, port_req_body, - neutron=neutron, bind_host_id=bind_host_id) + network=network, neutron=neutron, + bind_host_id=bind_host_id) if request.port_id: port = ports[request.port_id] port_client.update_port(port['id'], port_req_body) @@ -639,6 +651,9 @@ class API(base_api.NetworkAPI): security_group_ids, available_macs, dhcp_opts) created_port_ids.append(created_port) ports_in_requested_order.append(created_port) + self._update_port_dns_name(context, instance, network, + ports_in_requested_order[-1], + neutron) except Exception: with excutils.save_and_reraise_exception(): self._unbind_ports(context, @@ -699,7 +714,8 @@ class API(base_api.NetworkAPI): def _populate_neutron_extension_values(self, context, instance, pci_request_id, port_req_body, - neutron=None, bind_host_id=None): + network=None, neutron=None, + bind_host_id=None): """Populate neutron extension values for the instance. If the extensions loaded contain QOS_QUEUE then pass the rxtx_factor. @@ -709,11 +725,53 @@ class API(base_api.NetworkAPI): flavor = instance.get_flavor() rxtx_factor = flavor.get('rxtx_factor') port_req_body['port']['rxtx_factor'] = rxtx_factor - if self._has_port_binding_extension(context, neutron=neutron): + has_port_binding_extension = ( + self._has_port_binding_extension(context, neutron=neutron)) + if has_port_binding_extension: port_req_body['port']['binding:host_id'] = bind_host_id self._populate_neutron_binding_profile(instance, pci_request_id, port_req_body) + if constants.DNS_INTEGRATION in self.extensions: + # If the DNS integration extension is enabled in Neutron, most + # ports will get their dns_name attribute set in the port create or + # update requests in allocate_for_instance. So we just add the + # dns_name attribute to the payload of those requests. The + # exception is when the port binding extension is enabled in + # Neutron and the port is on a network that has a non-blank + # dns_domain attribute. This case requires to be processed by + # method _update_port_dns_name + if (not has_port_binding_extension + or not network.get('dns_domain')): + port_req_body['port']['dns_name'] = instance.hostname + + def _update_port_dns_name(self, context, instance, network, port_id, + neutron): + """Update an instance port dns_name attribute with instance.hostname. + + The dns_name attribute of a port on a network with a non-blank + dns_domain attribute will be sent to the external DNS service + (Designate) if DNS integration is enabled in Neutron. This requires the + assignment of the dns_name to the port to be done with a Neutron client + using the user's context. allocate_for_instance uses a port with admin + context if the port binding extensions is enabled in Neutron. In this + case, we assign in this method the dns_name attribute to the port with + an additional update request. Only a very small fraction of ports will + require this additional update request. + """ + if (constants.DNS_INTEGRATION in self.extensions and + self._has_port_binding_extension(context) and + network.get('dns_domain')): + try: + port_req_body = {'port': {'dns_name': instance.hostname}} + neutron.update_port(port_id, port_req_body) + except neutron_client_exc.BadRequest: + LOG.warning(_LW('Neutron error: Instance hostname ' + '%(hostname)s is not a valid DNS name'), + {'hostname': instance.hostname}, instance=instance) + msg = (_('Instance hostname %(hostname)s is not a valid DNS ' + 'name') % {'hostname': instance.hostname}) + raise exception.InvalidInput(reason=msg) def _delete_ports(self, neutron, instance, ports, raise_if_fail=False): exceptions = [] diff --git a/nova/network/neutronv2/constants.py b/nova/network/neutronv2/constants.py index c0df396e6716..ca400c8a5760 100644 --- a/nova/network/neutronv2/constants.py +++ b/nova/network/neutronv2/constants.py @@ -17,3 +17,4 @@ QOS_QUEUE = 'QoS Queue' NET_EXTERNAL = 'router:external' PORTBINDING_EXT = 'Port Binding' VNIC_INDEX_EXT = 'VNIC Index' +DNS_INTEGRATION = 'DNS Integration' diff --git a/nova/tests/unit/network/test_neutronv2.py b/nova/tests/unit/network/test_neutronv2.py index b4623a160ad6..b38bcd10e408 100644 --- a/nova/tests/unit/network/test_neutronv2.py +++ b/nova/tests/unit/network/test_neutronv2.py @@ -511,7 +511,8 @@ class TestNeutronv2Base(test.TestCase): if not has_portbinding: api._populate_neutron_extension_values(mox.IgnoreArg(), self.instance, mox.IgnoreArg(), - mox.IgnoreArg(), neutron=self.moxed_client, + mox.IgnoreArg(), network=network, + neutron=self.moxed_client, bind_host_id=None).AndReturn(None) else: # since _populate_neutron_extension_values() will call @@ -1140,7 +1141,7 @@ class TestNeutronv2(TestNeutronv2Base): port = {'id': 'portid_' + network['id']} api._populate_neutron_extension_values(self.context, - self.instance, None, binding_port_req_body, + self.instance, None, binding_port_req_body, network=network, neutron=self.moxed_client, bind_host_id=None).AndReturn(None) if index == 0: self.moxed_client.create_port( @@ -1195,7 +1196,8 @@ class TestNeutronv2(TestNeutronv2Base): } api._populate_neutron_extension_values(self.context, self.instance, None, binding_port_req_body, - neutron=self.moxed_client, bind_host_id=None).AndReturn(None) + network=self.nets2[0], neutron=self.moxed_client, + bind_host_id=None).AndReturn(None) self.moxed_client.create_port( MyComparator(port_req_body)).AndRaise( Exception("fail to create port"))