Add dns_label processing for Ports
Functionallity is added to enable users to specify a dns_label field during port creation and update. This dns_label field will be used for DNS resolution of the hostname in dnsmasq and also will be used when Neutron can integrate with external DNS systems. Change-Id: I6beab336dfd9b70b1af6e975939c602047faa651 DocImpact APIImpact Closes-Bug: #1459030 Implements: blueprint internal-dns-resolution
This commit is contained in:
parent
1c19e898c0
commit
3d28fc0bfc
|
@ -66,7 +66,8 @@
|
|||
# Location to store DHCP server config files
|
||||
# dhcp_confs = $state_path/dhcp
|
||||
|
||||
# Domain to use for building the hostnames
|
||||
# Domain to use for building the hostnames. This option will be deprecated in
|
||||
# a future release. It is being replaced by dns_domain in neutron.conf
|
||||
# dhcp_domain = openstacklocal
|
||||
|
||||
# Override the default dnsmasq settings with this file
|
||||
|
|
|
@ -114,6 +114,9 @@
|
|||
# tell dnsmasq to use infinite lease times.
|
||||
# dhcp_lease_duration = 86400
|
||||
|
||||
# Domain to use for building the hostnames
|
||||
# dns_domain = openstacklocal
|
||||
|
||||
# Allow sending resource operation notification to DHCP agent
|
||||
# dhcp_agent_notification = True
|
||||
|
||||
|
|
|
@ -40,7 +40,11 @@ DHCP_OPTS = [
|
|||
help=_('Location to store DHCP server config files')),
|
||||
cfg.StrOpt('dhcp_domain',
|
||||
default='openstacklocal',
|
||||
help=_('Domain to use for building the hostnames')),
|
||||
help=_('Domain to use for building the hostnames.'
|
||||
'This option is deprecated. It has been moved to '
|
||||
'neutron.conf as dns_domain. It will removed from here '
|
||||
'in a future release'),
|
||||
deprecated_for_removal=True),
|
||||
]
|
||||
|
||||
DNSMASQ_OPTS = [
|
||||
|
|
|
@ -510,6 +510,11 @@ class Dnsmasq(DhcpLocalProcess):
|
|||
for port in self.network.ports:
|
||||
fixed_ips = self._sort_fixed_ips_for_dnsmasq(port.fixed_ips,
|
||||
v6_nets)
|
||||
# Confirm whether Neutron server supports dns_name attribute in the
|
||||
# ports API
|
||||
dns_assignment = getattr(port, 'dns_assignment', None)
|
||||
if dns_assignment:
|
||||
dns_ip_map = {d.ip_address: d for d in dns_assignment}
|
||||
for alloc in fixed_ips:
|
||||
# Note(scollins) Only create entries that are
|
||||
# associated with the subnet being managed by this
|
||||
|
@ -523,11 +528,18 @@ class Dnsmasq(DhcpLocalProcess):
|
|||
yield (port, alloc, hostname, fqdn)
|
||||
continue
|
||||
|
||||
hostname = 'host-%s' % alloc.ip_address.replace(
|
||||
'.', '-').replace(':', '-')
|
||||
fqdn = hostname
|
||||
if self.conf.dhcp_domain:
|
||||
fqdn = '%s.%s' % (fqdn, self.conf.dhcp_domain)
|
||||
# If dns_name attribute is supported by ports API, return the
|
||||
# dns_assignment generated by the Neutron server. Otherwise,
|
||||
# generate hostname and fqdn locally (previous behaviour)
|
||||
if dns_assignment:
|
||||
hostname = dns_ip_map[alloc.ip_address].hostname
|
||||
fqdn = dns_ip_map[alloc.ip_address].fqdn
|
||||
else:
|
||||
hostname = 'host-%s' % alloc.ip_address.replace(
|
||||
'.', '-').replace(':', '-')
|
||||
fqdn = hostname
|
||||
if self.conf.dhcp_domain:
|
||||
fqdn = '%s.%s' % (fqdn, self.conf.dhcp_domain)
|
||||
yield (port, alloc, hostname, fqdn)
|
||||
|
||||
def _get_port_extra_dhcp_opts(self, port):
|
||||
|
|
|
@ -82,6 +82,9 @@ core_opts = [
|
|||
deprecated_name='dhcp_lease_time',
|
||||
help=_("DHCP lease duration (in seconds). Use -1 to tell "
|
||||
"dnsmasq to use infinite lease times.")),
|
||||
cfg.StrOpt('dns_domain',
|
||||
default='openstacklocal',
|
||||
help=_('Domain to use for building the hostnames')),
|
||||
cfg.BoolOpt('dhcp_agent_notification', default=True,
|
||||
help=_("Allow sending resource operation"
|
||||
" notification to DHCP agent")),
|
||||
|
|
|
@ -168,6 +168,13 @@ class DbBasePluginCommon(common_db_mixin.CommonDbMixin):
|
|||
for ip in port["fixed_ips"]],
|
||||
"device_id": port["device_id"],
|
||||
"device_owner": port["device_owner"]}
|
||||
if "dns_name" in port:
|
||||
res["dns_name"] = port["dns_name"]
|
||||
if "dns_assignment" in port:
|
||||
res["dns_assignment"] = [{"ip_address": a["ip_address"],
|
||||
"hostname": a["hostname"],
|
||||
"fqdn": a["fqdn"]}
|
||||
for a in port["dns_assignment"]]
|
||||
# Call auxiliary extend functions, if any
|
||||
if process_extensions:
|
||||
self._apply_dict_extend_functions(
|
||||
|
|
|
@ -63,6 +63,9 @@ LOG = logging.getLogger(__name__)
|
|||
# IP allocations being cleaned up by cascade.
|
||||
AUTO_DELETE_PORT_OWNERS = [constants.DEVICE_OWNER_DHCP]
|
||||
|
||||
DNS_DOMAIN_DEFAULT = 'openstacklocal.'
|
||||
FQDN_MAX_LEN = 255
|
||||
|
||||
|
||||
def _check_subnet_not_used(context, subnet_id):
|
||||
try:
|
||||
|
@ -1034,6 +1037,54 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||
def create_port_bulk(self, context, ports):
|
||||
return self._create_bulk('port', context, ports)
|
||||
|
||||
def _get_dns_domain(self):
|
||||
if not cfg.CONF.dns_domain:
|
||||
return ''
|
||||
if cfg.CONF.dns_domain.endswith('.'):
|
||||
return cfg.CONF.dns_domain
|
||||
return '%s.' % cfg.CONF.dns_domain
|
||||
|
||||
def _get_request_dns_name(self, port):
|
||||
dns_domain = self._get_dns_domain()
|
||||
if ((dns_domain and dns_domain != DNS_DOMAIN_DEFAULT)):
|
||||
return port.get('dns_name', '')
|
||||
return ''
|
||||
|
||||
def _get_dns_names_for_port(self, context, network_id, ips,
|
||||
request_dns_name):
|
||||
filter = {'network_id': [network_id]}
|
||||
subnets = self._get_subnets(context, filters=filter)
|
||||
v6_subnets = {subnet['id']: subnet for subnet in subnets
|
||||
if subnet['ip_version'] == 6}
|
||||
dns_assignment = []
|
||||
dns_domain = self._get_dns_domain()
|
||||
if request_dns_name:
|
||||
request_fqdn = request_dns_name
|
||||
if not request_dns_name.endswith('.'):
|
||||
request_fqdn = '%s.%s' % (request_dns_name, dns_domain)
|
||||
|
||||
for ip in ips:
|
||||
subnet_id = ip['subnet_id']
|
||||
is_auto_address_subnet = (
|
||||
subnet_id in v6_subnets and
|
||||
ipv6_utils.is_auto_address_subnet(v6_subnets[subnet_id]))
|
||||
if is_auto_address_subnet:
|
||||
continue
|
||||
if request_dns_name:
|
||||
hostname = request_dns_name
|
||||
fqdn = request_fqdn
|
||||
else:
|
||||
hostname = 'host-%s' % ip['ip_address'].replace(
|
||||
'.', '-').replace(':', '-')
|
||||
fqdn = hostname
|
||||
if dns_domain:
|
||||
fqdn = '%s.%s' % (hostname, dns_domain)
|
||||
dns_assignment.append({'ip_address': ip['ip_address'],
|
||||
'hostname': hostname,
|
||||
'fqdn': fqdn})
|
||||
|
||||
return dns_assignment
|
||||
|
||||
def _create_port_with_mac(self, context, network_id, port_data,
|
||||
mac_address):
|
||||
try:
|
||||
|
@ -1081,6 +1132,9 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||
status=p.get('status', constants.PORT_STATUS_ACTIVE),
|
||||
device_id=p['device_id'],
|
||||
device_owner=p['device_owner'])
|
||||
if 'dns_name' in p:
|
||||
request_dns_name = self._get_request_dns_name(p)
|
||||
port_data['dns_name'] = request_dns_name
|
||||
|
||||
with context.session.begin(subtransactions=True):
|
||||
# Ensure that the network exists.
|
||||
|
@ -1094,8 +1148,16 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||
db_port = self._create_port_with_mac(
|
||||
context, network_id, port_data, p['mac_address'])
|
||||
|
||||
self.ipam.allocate_ips_for_port_and_store(context, port, port_id)
|
||||
ips = self.ipam.allocate_ips_for_port_and_store(context, port,
|
||||
port_id)
|
||||
if 'dns_name' in p:
|
||||
dns_assignment = []
|
||||
if ips:
|
||||
dns_assignment = self._get_dns_names_for_port(
|
||||
context, network_id, ips, request_dns_name)
|
||||
|
||||
if 'dns_name' in p:
|
||||
db_port['dns_assignment'] = dns_assignment
|
||||
return self._make_port_dict(db_port, process_extensions=False)
|
||||
|
||||
def _validate_port_for_update(self, context, db_port, new_port, new_mac):
|
||||
|
@ -1114,20 +1176,45 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||
self._check_mac_addr_update(context, db_port,
|
||||
new_mac, current_owner)
|
||||
|
||||
def _get_dns_names_for_updated_port(self, context, db_port,
|
||||
original_ips, original_dns_name,
|
||||
request_dns_name, changes):
|
||||
if changes.original or changes.add or changes.remove:
|
||||
return self._get_dns_names_for_port(
|
||||
context, db_port['network_id'], changes.original + changes.add,
|
||||
request_dns_name or original_dns_name)
|
||||
if original_ips:
|
||||
return self._get_dns_names_for_port(
|
||||
context, db_port['network_id'], original_ips,
|
||||
request_dns_name or original_dns_name)
|
||||
return []
|
||||
|
||||
def update_port(self, context, id, port):
|
||||
new_port = port['port']
|
||||
|
||||
with context.session.begin(subtransactions=True):
|
||||
port = self._get_port(context, id)
|
||||
if 'dns-integration' in self.supported_extension_aliases:
|
||||
original_ips = self._make_fixed_ip_dict(port['fixed_ips'])
|
||||
original_dns_name = port.get('dns_name', '')
|
||||
request_dns_name = self._get_request_dns_name(new_port)
|
||||
if not request_dns_name:
|
||||
new_port['dns_name'] = ''
|
||||
new_mac = new_port.get('mac_address')
|
||||
self._validate_port_for_update(context, port, new_port, new_mac)
|
||||
changes = self.ipam.update_port_with_ips(context, port,
|
||||
new_port, new_mac)
|
||||
if 'dns-integration' in self.supported_extension_aliases:
|
||||
dns_assignment = self._get_dns_names_for_updated_port(
|
||||
context, port, original_ips, original_dns_name,
|
||||
request_dns_name, changes)
|
||||
result = self._make_port_dict(port)
|
||||
# Keep up with fields that changed
|
||||
if changes.original or changes.add or changes.remove:
|
||||
result['fixed_ips'] = self._make_fixed_ip_dict(
|
||||
changes.original + changes.add)
|
||||
if 'dns-integration' in self.supported_extension_aliases:
|
||||
result['dns_assignment'] = dns_assignment
|
||||
return result
|
||||
|
||||
def delete_port(self, context, id):
|
||||
|
@ -1150,8 +1237,19 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||
"The port has already been deleted.",
|
||||
port_id)
|
||||
|
||||
def _get_dns_name_for_port_get(self, context, port):
|
||||
if port['fixed_ips']:
|
||||
return self._get_dns_names_for_port(
|
||||
context, port['network_id'], port['fixed_ips'],
|
||||
port['dns_name'])
|
||||
return []
|
||||
|
||||
def get_port(self, context, id, fields=None):
|
||||
port = self._get_port(context, id)
|
||||
if (('dns-integration' in self.supported_extension_aliases and
|
||||
'dns_name' in port)):
|
||||
port['dns_assignment'] = self._get_dns_name_for_port_get(context,
|
||||
port)
|
||||
return self._make_port_dict(port, fields)
|
||||
|
||||
def _get_ports_query(self, context, filters=None, sorts=None, limit=None,
|
||||
|
@ -1189,7 +1287,13 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
|||
sorts=sorts, limit=limit,
|
||||
marker_obj=marker_obj,
|
||||
page_reverse=page_reverse)
|
||||
items = [self._make_port_dict(c, fields) for c in query]
|
||||
items = []
|
||||
for c in query:
|
||||
if (('dns-integration' in self.supported_extension_aliases and
|
||||
'dns_name' in c)):
|
||||
c['dns_assignment'] = self._get_dns_name_for_port_get(context,
|
||||
c)
|
||||
items.append(self._make_port_dict(c, fields))
|
||||
if limit and page_reverse:
|
||||
items.reverse()
|
||||
return items
|
||||
|
|
|
@ -207,6 +207,7 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
subnet_id = ip['subnet_id']
|
||||
self._store_ip_allocation(context, ip_address, network_id,
|
||||
subnet_id, port_id)
|
||||
return ips
|
||||
|
||||
def update_port_with_ips(self, context, db_port, new_port, new_mac):
|
||||
changes = self.Changes(add=[], original=[], remove=[])
|
||||
|
|
|
@ -160,6 +160,7 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
IpamPluggableBackend._store_ip_allocation(
|
||||
context, ip_address, network_id,
|
||||
subnet_id, port_id)
|
||||
return ips
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
if ips:
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
2e5352a0ad4d
|
||||
9859ac9c136
|
||||
34af2b5c5a59
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# Copyright 2015 Rackspace
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Add dns_name to Port
|
||||
|
||||
Revision ID: 34af2b5c5a59
|
||||
Revises: 9859ac9c136
|
||||
Create Date: 2015-08-23 00:22:47.618593
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '34af2b5c5a59'
|
||||
down_revision = '9859ac9c136'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from neutron.extensions import dns
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('ports',
|
||||
sa.Column('dns_name',
|
||||
sa.String(length=dns.FQDN_MAX_LEN),
|
||||
nullable=True))
|
|
@ -141,6 +141,7 @@ class Port(model_base.BASEV2, HasId, HasTenant):
|
|||
device_id = sa.Column(sa.String(attr.DEVICE_ID_MAX_LEN), nullable=False)
|
||||
device_owner = sa.Column(sa.String(attr.DEVICE_OWNER_MAX_LEN),
|
||||
nullable=False)
|
||||
dns_name = sa.Column(sa.String(255), nullable=True)
|
||||
__table_args__ = (
|
||||
sa.Index(
|
||||
'ix_ports_network_id_mac_address', 'network_id', 'mac_address'),
|
||||
|
@ -154,7 +155,8 @@ class Port(model_base.BASEV2, HasId, HasTenant):
|
|||
|
||||
def __init__(self, id=None, tenant_id=None, name=None, network_id=None,
|
||||
mac_address=None, admin_state_up=None, status=None,
|
||||
device_id=None, device_owner=None, fixed_ips=None):
|
||||
device_id=None, device_owner=None, fixed_ips=None,
|
||||
dns_name=None):
|
||||
self.id = id
|
||||
self.tenant_id = tenant_id
|
||||
self.name = name
|
||||
|
@ -163,6 +165,7 @@ class Port(model_base.BASEV2, HasId, HasTenant):
|
|||
self.admin_state_up = admin_state_up
|
||||
self.device_owner = device_owner
|
||||
self.device_id = device_id
|
||||
self.dns_name = dns_name
|
||||
# Since this is a relationship only set it if one is passed in.
|
||||
if fixed_ips:
|
||||
self.fixed_ips = fixed_ips
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
# Copyright (c) 2015 Rackspace
|
||||
# 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 re
|
||||
import six
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from neutron.api import extensions
|
||||
from neutron.api.v2 import attributes as attr
|
||||
from neutron.common import exceptions as n_exc
|
||||
|
||||
DNS_LABEL_MAX_LEN = 63
|
||||
DNS_LABEL_REGEX = "[a-z0-9-]{1,%d}$" % DNS_LABEL_MAX_LEN
|
||||
FQDN_MAX_LEN = 255
|
||||
DNS_DOMAIN_DEFAULT = 'openstacklocal.'
|
||||
|
||||
|
||||
def _validate_dns_name(data, max_len=FQDN_MAX_LEN):
|
||||
msg = _validate_dns_format(data, max_len)
|
||||
if msg:
|
||||
return msg
|
||||
request_dns_name = _get_request_dns_name(data)
|
||||
if request_dns_name:
|
||||
msg = _validate_dns_name_with_dns_domain(request_dns_name)
|
||||
if msg:
|
||||
return msg
|
||||
|
||||
|
||||
def _validate_dns_format(data, max_len=FQDN_MAX_LEN):
|
||||
# NOTE: An individual name regex instead of an entire FQDN was used
|
||||
# because its easier to make correct. The logic should validate that the
|
||||
# dns_name matches RFC 1123 (section 2.1) and RFC 952.
|
||||
if not data:
|
||||
return
|
||||
try:
|
||||
# Trailing periods are allowed to indicate that a name is fully
|
||||
# qualified per RFC 1034 (page 7).
|
||||
trimmed = data if not data.endswith('.') else data[:-1]
|
||||
if len(trimmed) > 255:
|
||||
raise TypeError(
|
||||
_("'%s' exceeds the 255 character FQDN limit") % trimmed)
|
||||
names = trimmed.split('.')
|
||||
for name in names:
|
||||
if not name:
|
||||
raise TypeError(_("Encountered an empty component."))
|
||||
if name.endswith('-') or name[0] == '-':
|
||||
raise TypeError(
|
||||
_("Name '%s' must not start or end with a hyphen.") % name)
|
||||
if not re.match(DNS_LABEL_REGEX, name):
|
||||
raise TypeError(
|
||||
_("Name '%s' must be 1-63 characters long, each of "
|
||||
"which can only be alphanumeric or a hyphen.") % name)
|
||||
# RFC 1123 hints that a TLD can't be all numeric. last is a TLD if
|
||||
# it's an FQDN.
|
||||
if len(names) > 1 and re.match("^[0-9]+$", names[-1]):
|
||||
raise TypeError(_("TLD '%s' must not be all numeric") % names[-1])
|
||||
except TypeError as e:
|
||||
msg = _("'%(data)s' not a valid PQDN or FQDN. Reason: %(reason)s") % {
|
||||
'data': data, 'reason': e.message}
|
||||
return msg
|
||||
|
||||
|
||||
def _validate_dns_name_with_dns_domain(request_dns_name):
|
||||
# If a PQDN was passed, make sure the FQDN that will be generated is of
|
||||
# legal size
|
||||
dns_domain = _get_dns_domain()
|
||||
higher_labels = dns_domain
|
||||
if dns_domain:
|
||||
higher_labels = '.%s' % dns_domain
|
||||
higher_labels_len = len(higher_labels)
|
||||
dns_name_len = len(request_dns_name)
|
||||
if not request_dns_name.endswith('.'):
|
||||
if dns_name_len + higher_labels_len > FQDN_MAX_LEN:
|
||||
msg = _("The dns_name passed is a PQDN and its size is "
|
||||
"'%(dns_name_len)s'. The dns_domain option in "
|
||||
"neutron.conf is set to %(dns_domain)s, with a "
|
||||
"length of '%(higher_labels_len)s'. When the two are "
|
||||
"concatenated to form a FQDN (with a '.' at the end), "
|
||||
"the resulting length exceeds the maximum size "
|
||||
"of '%(fqdn_max_len)s'"
|
||||
) % {'dns_name_len': dns_name_len,
|
||||
'dns_domain': cfg.CONF.dns_domain,
|
||||
'higher_labels_len': higher_labels_len,
|
||||
'fqdn_max_len': FQDN_MAX_LEN}
|
||||
return msg
|
||||
return
|
||||
|
||||
# A FQDN was passed
|
||||
if (dns_name_len <= higher_labels_len or not
|
||||
request_dns_name.endswith(higher_labels)):
|
||||
msg = _("The dns_name passed is a FQDN. Its higher level labels "
|
||||
"must be equal to the dns_domain option in neutron.conf, "
|
||||
"that has been set to '%(dns_domain)s'. It must also "
|
||||
"include one or more valid DNS labels to the left "
|
||||
"of '%(dns_domain)s'") % {'dns_domain':
|
||||
cfg.CONF.dns_domain}
|
||||
return msg
|
||||
|
||||
|
||||
def _get_dns_domain():
|
||||
if not cfg.CONF.dns_domain:
|
||||
return ''
|
||||
if cfg.CONF.dns_domain.endswith('.'):
|
||||
return cfg.CONF.dns_domain
|
||||
return '%s.' % cfg.CONF.dns_domain
|
||||
|
||||
|
||||
def _get_request_dns_name(data):
|
||||
dns_domain = _get_dns_domain()
|
||||
if ((dns_domain and dns_domain != DNS_DOMAIN_DEFAULT)):
|
||||
return data
|
||||
return ''
|
||||
|
||||
|
||||
def convert_to_lowercase(data):
|
||||
if isinstance(data, six.string_types):
|
||||
return data.lower()
|
||||
msg = _("'%s' cannot be converted to lowercase string") % data
|
||||
raise n_exc.InvalidInput(error_message=msg)
|
||||
|
||||
|
||||
attr.validators['type:dns_name'] = (
|
||||
_validate_dns_name)
|
||||
|
||||
|
||||
DNSNAME = 'dns_name'
|
||||
DNSASSIGNMENT = 'dns_assignment'
|
||||
EXTENDED_ATTRIBUTES_2_0 = {
|
||||
'ports': {
|
||||
DNSNAME: {'allow_post': True, 'allow_put': True,
|
||||
'default': '',
|
||||
'convert_to': convert_to_lowercase,
|
||||
'validate': {'type:dns_name': FQDN_MAX_LEN},
|
||||
'is_visible': True},
|
||||
DNSASSIGNMENT: {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Dns(extensions.ExtensionDescriptor):
|
||||
"""Extension class supporting DNS Integration."""
|
||||
|
||||
@classmethod
|
||||
def get_name(cls):
|
||||
return "DNS Integration"
|
||||
|
||||
@classmethod
|
||||
def get_alias(cls):
|
||||
return "dns-integration"
|
||||
|
||||
@classmethod
|
||||
def get_description(cls):
|
||||
return "Provides integration with internal DNS."
|
||||
|
||||
@classmethod
|
||||
def get_updated(cls):
|
||||
return "2015-08-15T18:00:00-00:00"
|
||||
|
||||
def get_extended_resources(self, version):
|
||||
if version == "2.0":
|
||||
return EXTENDED_ATTRIBUTES_2_0
|
||||
else:
|
||||
return {}
|
|
@ -118,7 +118,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
|||
"multi-provider", "allowed-address-pairs",
|
||||
"extra_dhcp_opt", "subnet_allocation",
|
||||
"net-mtu", "vlan-transparent",
|
||||
"address-scope"]
|
||||
"address-scope", "dns-integration"]
|
||||
|
||||
@property
|
||||
def supported_extension_aliases(self):
|
||||
|
|
|
@ -39,6 +39,19 @@ class FakeIPAllocation(object):
|
|||
self.subnet_id = subnet_id
|
||||
|
||||
|
||||
class FakeDNSAssignment(object):
|
||||
def __init__(self, ip_address, dns_name='', domain='openstacklocal'):
|
||||
if dns_name:
|
||||
self.hostname = dns_name
|
||||
else:
|
||||
self.hostname = 'host-%s' % ip_address.replace(
|
||||
'.', '-').replace(':', '-')
|
||||
self.ip_address = ip_address
|
||||
self.fqdn = self.hostname
|
||||
if domain:
|
||||
self.fqdn = '%s.%s.' % (self.hostname, domain)
|
||||
|
||||
|
||||
class DhcpOpt(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(ip_version=4)
|
||||
|
@ -90,8 +103,9 @@ class FakePort1(object):
|
|||
mac_address = '00:00:80:aa:bb:cc'
|
||||
device_id = 'fake_port1'
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, domain='openstacklocal'):
|
||||
self.extra_dhcp_opts = []
|
||||
self.dns_assignment = [FakeDNSAssignment('192.168.0.2', domain=domain)]
|
||||
|
||||
|
||||
class FakePort2(object):
|
||||
|
@ -102,6 +116,7 @@ class FakePort2(object):
|
|||
'dddddddd-dddd-dddd-dddd-dddddddddddd')]
|
||||
mac_address = '00:00:f3:aa:bb:cc'
|
||||
device_id = 'fake_port2'
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.3')]
|
||||
|
||||
def __init__(self):
|
||||
self.extra_dhcp_opts = []
|
||||
|
@ -115,6 +130,8 @@ class FakePort3(object):
|
|||
'dddddddd-dddd-dddd-dddd-dddddddddddd'),
|
||||
FakeIPAllocation('192.168.1.2',
|
||||
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.4'),
|
||||
FakeDNSAssignment('192.168.1.2')]
|
||||
mac_address = '00:00:0f:aa:bb:cc'
|
||||
device_id = 'fake_port3'
|
||||
|
||||
|
@ -131,6 +148,7 @@ class FakePort4(object):
|
|||
'dddddddd-dddd-dddd-dddd-dddddddddddd'),
|
||||
FakeIPAllocation('ffda:3ba5:a17a:4ba3:0216:3eff:fec2:771d',
|
||||
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.4')]
|
||||
mac_address = '00:16:3E:C2:77:1D'
|
||||
device_id = 'fake_port4'
|
||||
|
||||
|
@ -144,6 +162,7 @@ class FakePort5(object):
|
|||
device_owner = 'foo5'
|
||||
fixed_ips = [FakeIPAllocation('192.168.0.5',
|
||||
'dddddddd-dddd-dddd-dddd-dddddddddddd')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.5')]
|
||||
mac_address = '00:00:0f:aa:bb:55'
|
||||
device_id = 'fake_port5'
|
||||
|
||||
|
@ -159,6 +178,7 @@ class FakePort6(object):
|
|||
device_owner = 'foo6'
|
||||
fixed_ips = [FakeIPAllocation('192.168.0.6',
|
||||
'dddddddd-dddd-dddd-dddd-dddddddddddd')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.6')]
|
||||
mac_address = '00:00:0f:aa:bb:66'
|
||||
device_id = 'fake_port6'
|
||||
|
||||
|
@ -181,8 +201,10 @@ class FakeV6Port(object):
|
|||
mac_address = '00:00:f3:aa:bb:cc'
|
||||
device_id = 'fake_port6'
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, domain='openstacklocal'):
|
||||
self.extra_dhcp_opts = []
|
||||
self.dns_assignment = [FakeDNSAssignment('fdca:3ba5:a17a:4ba3::2',
|
||||
domain=domain)]
|
||||
|
||||
|
||||
class FakeV6PortExtraOpt(object):
|
||||
|
@ -191,6 +213,7 @@ class FakeV6PortExtraOpt(object):
|
|||
device_owner = 'foo3'
|
||||
fixed_ips = [FakeIPAllocation('ffea:3ba5:a17a:4ba3:0216:3eff:fec2:771d',
|
||||
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee')]
|
||||
dns_assignment = []
|
||||
mac_address = '00:16:3e:c2:77:1d'
|
||||
device_id = 'fake_port6'
|
||||
|
||||
|
@ -209,6 +232,7 @@ class FakeDualPortWithV6ExtraOpt(object):
|
|||
'dddddddd-dddd-dddd-dddd-dddddddddddd'),
|
||||
FakeIPAllocation('ffea:3ba5:a17a:4ba3:0216:3eff:fec2:771d',
|
||||
'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.3')]
|
||||
mac_address = '00:16:3e:c2:77:1d'
|
||||
device_id = 'fake_port6'
|
||||
|
||||
|
@ -230,8 +254,11 @@ class FakeDualPort(object):
|
|||
mac_address = '00:00:0f:aa:bb:cc'
|
||||
device_id = 'fake_dual_port'
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, domain='openstacklocal'):
|
||||
self.extra_dhcp_opts = []
|
||||
self.dns_assignment = [FakeDNSAssignment('192.168.0.3', domain=domain),
|
||||
FakeDNSAssignment('fdca:3ba5:a17a:4ba3::3',
|
||||
domain=domain)]
|
||||
|
||||
|
||||
class FakeRouterPort(object):
|
||||
|
@ -240,13 +267,16 @@ class FakeRouterPort(object):
|
|||
device_owner = constants.DEVICE_OWNER_ROUTER_INTF
|
||||
mac_address = '00:00:0f:rr:rr:rr'
|
||||
device_id = 'fake_router_port'
|
||||
dns_assignment = []
|
||||
|
||||
def __init__(self, dev_owner=constants.DEVICE_OWNER_ROUTER_INTF,
|
||||
ip_address='192.168.0.1'):
|
||||
ip_address='192.168.0.1', domain='openstacklocal'):
|
||||
self.extra_dhcp_opts = []
|
||||
self.device_owner = dev_owner
|
||||
self.fixed_ips = [FakeIPAllocation(
|
||||
ip_address, 'dddddddd-dddd-dddd-dddd-dddddddddddd')]
|
||||
self.dns_assignment = [FakeDNSAssignment(ip.ip_address, domain=domain)
|
||||
for ip in self.fixed_ips]
|
||||
|
||||
|
||||
class FakeRouterPort2(object):
|
||||
|
@ -255,6 +285,7 @@ class FakeRouterPort2(object):
|
|||
device_owner = constants.DEVICE_OWNER_ROUTER_INTF
|
||||
fixed_ips = [FakeIPAllocation('192.168.1.1',
|
||||
'dddddddd-dddd-dddd-dddd-dddddddddddd')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.1.1')]
|
||||
mac_address = '00:00:0f:rr:rr:r2'
|
||||
device_id = 'fake_router_port2'
|
||||
|
||||
|
@ -268,6 +299,7 @@ class FakePortMultipleAgents1(object):
|
|||
device_owner = constants.DEVICE_OWNER_DHCP
|
||||
fixed_ips = [FakeIPAllocation('192.168.0.5',
|
||||
'dddddddd-dddd-dddd-dddd-dddddddddddd')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.5')]
|
||||
mac_address = '00:00:0f:dd:dd:dd'
|
||||
device_id = 'fake_multiple_agents_port'
|
||||
|
||||
|
@ -281,6 +313,7 @@ class FakePortMultipleAgents2(object):
|
|||
device_owner = constants.DEVICE_OWNER_DHCP
|
||||
fixed_ips = [FakeIPAllocation('192.168.0.6',
|
||||
'dddddddd-dddd-dddd-dddd-dddddddddddd')]
|
||||
dns_assignment = [FakeDNSAssignment('192.168.0.6')]
|
||||
mac_address = '00:00:0f:ee:ee:ee'
|
||||
device_id = 'fake_multiple_agents_port2'
|
||||
|
||||
|
@ -499,9 +532,14 @@ class FakeV6Network(object):
|
|||
class FakeDualNetwork(object):
|
||||
id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
|
||||
subnets = [FakeV4Subnet(), FakeV6SubnetDHCPStateful()]
|
||||
ports = [FakePort1(), FakeV6Port(), FakeDualPort(), FakeRouterPort()]
|
||||
# ports = [FakePort1(), FakeV6Port(), FakeDualPort(), FakeRouterPort()]
|
||||
namespace = 'qdhcp-ns'
|
||||
|
||||
def __init__(self, domain='openstacklocal'):
|
||||
self.ports = [FakePort1(domain=domain), FakeV6Port(domain=domain),
|
||||
FakeDualPort(domain=domain),
|
||||
FakeRouterPort(domain=domain)]
|
||||
|
||||
|
||||
class FakeDeviceManagerNetwork(object):
|
||||
id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
|
||||
|
@ -1079,7 +1117,8 @@ class TestDnsmasq(TestBase):
|
|||
(exp_host_name, exp_host_data,
|
||||
exp_addn_name, exp_addn_data) = self._test_no_dhcp_domain_alloc_data
|
||||
self.conf.set_override('dhcp_domain', '')
|
||||
self._test_spawn(['--conf-file='])
|
||||
network = FakeDualNetwork(domain=self.conf.dhcp_domain)
|
||||
self._test_spawn(['--conf-file='], network=network)
|
||||
self.safe.assert_has_calls([mock.call(exp_host_name, exp_host_data),
|
||||
mock.call(exp_addn_name, exp_addn_data)])
|
||||
|
||||
|
@ -1475,30 +1514,30 @@ class TestDnsmasq(TestBase):
|
|||
@property
|
||||
def _test_reload_allocation_data(self):
|
||||
exp_host_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal,'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal.,'
|
||||
'192.168.0.2\n'
|
||||
'00:00:f3:aa:bb:cc,host-fdca-3ba5-a17a-4ba3--2.'
|
||||
'openstacklocal,[fdca:3ba5:a17a:4ba3::2]\n'
|
||||
'00:00:0f:aa:bb:cc,host-192-168-0-3.openstacklocal,'
|
||||
'openstacklocal.,[fdca:3ba5:a17a:4ba3::2]\n'
|
||||
'00:00:0f:aa:bb:cc,host-192-168-0-3.openstacklocal.,'
|
||||
'192.168.0.3\n'
|
||||
'00:00:0f:aa:bb:cc,host-fdca-3ba5-a17a-4ba3--3.'
|
||||
'openstacklocal,[fdca:3ba5:a17a:4ba3::3]\n'
|
||||
'00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal,'
|
||||
'openstacklocal.,[fdca:3ba5:a17a:4ba3::3]\n'
|
||||
'00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal.,'
|
||||
'192.168.0.1\n').lstrip()
|
||||
exp_addn_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/addn_hosts'
|
||||
exp_addn_data = (
|
||||
'192.168.0.2\t'
|
||||
'host-192-168-0-2.openstacklocal host-192-168-0-2\n'
|
||||
'host-192-168-0-2.openstacklocal. host-192-168-0-2\n'
|
||||
'fdca:3ba5:a17a:4ba3::2\t'
|
||||
'host-fdca-3ba5-a17a-4ba3--2.openstacklocal '
|
||||
'host-fdca-3ba5-a17a-4ba3--2.openstacklocal. '
|
||||
'host-fdca-3ba5-a17a-4ba3--2\n'
|
||||
'192.168.0.3\thost-192-168-0-3.openstacklocal '
|
||||
'192.168.0.3\thost-192-168-0-3.openstacklocal. '
|
||||
'host-192-168-0-3\n'
|
||||
'fdca:3ba5:a17a:4ba3::3\t'
|
||||
'host-fdca-3ba5-a17a-4ba3--3.openstacklocal '
|
||||
'host-fdca-3ba5-a17a-4ba3--3.openstacklocal. '
|
||||
'host-fdca-3ba5-a17a-4ba3--3\n'
|
||||
'192.168.0.1\t'
|
||||
'host-192-168-0-1.openstacklocal '
|
||||
'host-192-168-0-1.openstacklocal. '
|
||||
'host-192-168-0-1\n'
|
||||
).lstrip()
|
||||
exp_opt_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/opts'
|
||||
|
@ -1774,11 +1813,11 @@ class TestDnsmasq(TestBase):
|
|||
|
||||
def test_only_populates_dhcp_enabled_subnets(self):
|
||||
exp_host_name = '/dhcp/eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee/host'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal,'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal.,'
|
||||
'192.168.0.2\n'
|
||||
'00:16:3E:C2:77:1D,host-192-168-0-4.openstacklocal,'
|
||||
'00:16:3E:C2:77:1D,host-192-168-0-4.openstacklocal.,'
|
||||
'192.168.0.4\n'
|
||||
'00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal,'
|
||||
'00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal.,'
|
||||
'192.168.0.1\n').lstrip()
|
||||
dm = self._get_dnsmasq(FakeDualStackNetworkSingleDHCP())
|
||||
dm._output_hosts_file()
|
||||
|
@ -1787,13 +1826,13 @@ class TestDnsmasq(TestBase):
|
|||
|
||||
def test_only_populates_dhcp_client_id(self):
|
||||
exp_host_name = '/dhcp/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/host'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal,'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal.,'
|
||||
'192.168.0.2\n'
|
||||
'00:00:0f:aa:bb:55,id:test5,'
|
||||
'host-192-168-0-5.openstacklocal,'
|
||||
'host-192-168-0-5.openstacklocal.,'
|
||||
'192.168.0.5\n'
|
||||
'00:00:0f:aa:bb:66,id:test6,'
|
||||
'host-192-168-0-6.openstacklocal,192.168.0.6,'
|
||||
'host-192-168-0-6.openstacklocal.,192.168.0.6,'
|
||||
'set:ccccccccc-cccc-cccc-cccc-ccccccccc\n').lstrip()
|
||||
|
||||
dm = self._get_dnsmasq(FakeV4NetworkClientId)
|
||||
|
@ -1803,13 +1842,13 @@ class TestDnsmasq(TestBase):
|
|||
|
||||
def test_only_populates_dhcp_enabled_subnet_on_a_network(self):
|
||||
exp_host_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal,'
|
||||
exp_host_data = ('00:00:80:aa:bb:cc,host-192-168-0-2.openstacklocal.,'
|
||||
'192.168.0.2\n'
|
||||
'00:00:f3:aa:bb:cc,host-192-168-0-3.openstacklocal,'
|
||||
'00:00:f3:aa:bb:cc,host-192-168-0-3.openstacklocal.,'
|
||||
'192.168.0.3\n'
|
||||
'00:00:0f:aa:bb:cc,host-192-168-0-4.openstacklocal,'
|
||||
'00:00:0f:aa:bb:cc,host-192-168-0-4.openstacklocal.,'
|
||||
'192.168.0.4\n'
|
||||
'00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal,'
|
||||
'00:00:0f:rr:rr:rr,host-192-168-0-1.openstacklocal.,'
|
||||
'192.168.0.1\n').lstrip()
|
||||
dm = self._get_dnsmasq(FakeDualNetworkSingleDHCP())
|
||||
dm._output_hosts_file()
|
||||
|
@ -1835,10 +1874,10 @@ class TestDnsmasq(TestBase):
|
|||
exp_host_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/host'
|
||||
exp_host_data = (
|
||||
'00:16:3e:c2:77:1d,set:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh\n'
|
||||
'00:16:3e:c2:77:1d,host-192-168-0-3.openstacklocal,'
|
||||
'00:16:3e:c2:77:1d,host-192-168-0-3.openstacklocal.,'
|
||||
'192.168.0.3,set:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh\n'
|
||||
'00:00:0f:rr:rr:rr,'
|
||||
'host-192-168-0-1.openstacklocal,192.168.0.1\n').lstrip()
|
||||
'host-192-168-0-1.openstacklocal.,192.168.0.1\n').lstrip()
|
||||
exp_opt_name = '/dhcp/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/opts'
|
||||
exp_opt_data = (
|
||||
'tag:tag0,option6:domain-search,openstacklocal\n'
|
||||
|
|
|
@ -0,0 +1,469 @@
|
|||
# Copyright 2015 Rackspace
|
||||
#
|
||||
# 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 math
|
||||
import netaddr
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from neutron.common import constants
|
||||
from neutron.common import utils
|
||||
from neutron import context
|
||||
from neutron.db import db_base_plugin_v2
|
||||
from neutron.extensions import dns
|
||||
from neutron.tests.unit.db import test_db_base_plugin_v2
|
||||
|
||||
|
||||
class DnsExtensionManager(object):
|
||||
|
||||
def get_resources(self):
|
||||
return []
|
||||
|
||||
def get_actions(self):
|
||||
return []
|
||||
|
||||
def get_request_extensions(self):
|
||||
return []
|
||||
|
||||
def get_extended_resources(self, version):
|
||||
return dns.get_extended_resources(version)
|
||||
|
||||
|
||||
class DnsExtensionTestPlugin(db_base_plugin_v2.NeutronDbPluginV2):
|
||||
"""Test plugin to mixin the DNS Integration extensions.
|
||||
"""
|
||||
|
||||
supported_extension_aliases = ["dns-integration"]
|
||||
|
||||
|
||||
class DnsExtensionTestCase(test_db_base_plugin_v2.TestNetworksV2):
|
||||
"""Test API extension dns attributes.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
plugin = ('neutron.tests.unit.extensions.test_dns.' +
|
||||
'DnsExtensionTestPlugin')
|
||||
ext_mgr = DnsExtensionManager()
|
||||
super(DnsExtensionTestCase, self).setUp(plugin=plugin, ext_mgr=ext_mgr)
|
||||
|
||||
def _create_port(self, fmt, net_id, expected_res_status=None,
|
||||
arg_list=None, **kwargs):
|
||||
data = {'port': {'network_id': net_id,
|
||||
'tenant_id': self._tenant_id}}
|
||||
|
||||
for arg in (('admin_state_up', 'device_id',
|
||||
'mac_address', 'name', 'fixed_ips',
|
||||
'tenant_id', 'device_owner', 'security_groups',
|
||||
'dns_name') + (arg_list or ())):
|
||||
# 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
|
||||
'device_id' not 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
|
||||
port_req.environ['neutron.context'] = context.Context(
|
||||
'', kwargs['tenant_id'])
|
||||
|
||||
port_res = port_req.get_response(self.api)
|
||||
if expected_res_status:
|
||||
self.assertEqual(port_res.status_int, expected_res_status)
|
||||
return port_res
|
||||
|
||||
def _test_list_resources(self, resource, items, neutron_context=None,
|
||||
query_params=None):
|
||||
res = self._list('%ss' % resource,
|
||||
neutron_context=neutron_context,
|
||||
query_params=query_params)
|
||||
resource = resource.replace('-', '_')
|
||||
self.assertItemsEqual([i['id'] for i in res['%ss' % resource]],
|
||||
[i[resource]['id'] for i in items])
|
||||
return res
|
||||
|
||||
def test_create_port_json(self):
|
||||
keys = [('admin_state_up', True), ('status', self.port_create_status)]
|
||||
with self.port(name='myname') as port:
|
||||
for k, v in keys:
|
||||
self.assertEqual(port['port'][k], v)
|
||||
self.assertIn('mac_address', port['port'])
|
||||
ips = port['port']['fixed_ips']
|
||||
self.assertEqual(len(ips), 1)
|
||||
self.assertEqual(ips[0]['ip_address'], '10.0.0.2')
|
||||
self.assertEqual('myname', port['port']['name'])
|
||||
self._verify_dns_assigment(port['port'],
|
||||
ips_list=['10.0.0.2'])
|
||||
|
||||
def test_list_ports(self):
|
||||
# for this test we need to enable overlapping ips
|
||||
cfg.CONF.set_default('allow_overlapping_ips', True)
|
||||
with self.port() as v1, self.port() as v2, self.port() as v3:
|
||||
ports = (v1, v2, v3)
|
||||
res = self._test_list_resources('port', ports)
|
||||
for port in res['ports']:
|
||||
self._verify_dns_assigment(
|
||||
port, ips_list=[port['fixed_ips'][0]['ip_address']])
|
||||
|
||||
def test_show_port(self):
|
||||
with self.port() as port:
|
||||
req = self.new_show_request('ports', port['port']['id'], self.fmt)
|
||||
sport = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
self.assertEqual(port['port']['id'], sport['port']['id'])
|
||||
self._verify_dns_assigment(
|
||||
sport['port'],
|
||||
ips_list=[sport['port']['fixed_ips'][0]['ip_address']])
|
||||
|
||||
def test_update_port_non_default_dns_domain_with_dns_name(self):
|
||||
with self.port() as port:
|
||||
cfg.CONF.set_override('dns_domain', 'example.com')
|
||||
data = {'port': {'admin_state_up': False, 'dns_name': 'vm1'}}
|
||||
req = self.new_update_request('ports', data, port['port']['id'])
|
||||
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
self.assertEqual(res['port']['admin_state_up'],
|
||||
data['port']['admin_state_up'])
|
||||
self._verify_dns_assigment(res['port'],
|
||||
ips_list=['10.0.0.2'],
|
||||
dns_name='vm1')
|
||||
|
||||
def test_update_port_default_dns_domain_with_dns_name(self):
|
||||
with self.port() as port:
|
||||
data = {'port': {'admin_state_up': False, 'dns_name': 'vm1'}}
|
||||
req = self.new_update_request('ports', data, port['port']['id'])
|
||||
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
self.assertEqual(res['port']['admin_state_up'],
|
||||
data['port']['admin_state_up'])
|
||||
self._verify_dns_assigment(res['port'],
|
||||
ips_list=['10.0.0.2'])
|
||||
|
||||
def _verify_dns_assigment(self, port, ips_list=[], exp_ips_ipv4=0,
|
||||
exp_ips_ipv6=0, ipv4_cidrs=[], ipv6_cidrs=[],
|
||||
dns_name=''):
|
||||
self.assertEqual(port['dns_name'], dns_name)
|
||||
dns_assignment = port['dns_assignment']
|
||||
if ips_list:
|
||||
self.assertEqual(len(dns_assignment), len(ips_list))
|
||||
ips_set = set(ips_list)
|
||||
else:
|
||||
self.assertEqual(len(dns_assignment), exp_ips_ipv4 + exp_ips_ipv6)
|
||||
ipv4_count = 0
|
||||
ipv6_count = 0
|
||||
subnets_v4 = [netaddr.IPNetwork(cidr) for cidr in ipv4_cidrs]
|
||||
subnets_v6 = [netaddr.IPNetwork(cidr) for cidr in ipv6_cidrs]
|
||||
|
||||
request_dns_name, request_fqdn = self._get_request_hostname_and_fqdn(
|
||||
dns_name)
|
||||
for assignment in dns_assignment:
|
||||
if ips_list:
|
||||
self.assertIn(assignment['ip_address'], ips_set)
|
||||
ips_set.remove(assignment['ip_address'])
|
||||
else:
|
||||
ip = netaddr.IPAddress(assignment['ip_address'])
|
||||
if ip.version == 4:
|
||||
self.assertTrue(self._verify_ip_in_subnet(ip, subnets_v4))
|
||||
ipv4_count += 1
|
||||
else:
|
||||
self.assertTrue(self._verify_ip_in_subnet(ip, subnets_v6))
|
||||
ipv6_count += 1
|
||||
hostname, fqdn = self._get_hostname_and_fqdn(request_dns_name,
|
||||
request_fqdn,
|
||||
assignment)
|
||||
self.assertEqual(assignment['hostname'], hostname)
|
||||
self.assertEqual(assignment['fqdn'], fqdn)
|
||||
if ips_list:
|
||||
self.assertFalse(ips_set)
|
||||
else:
|
||||
self.assertEqual(ipv4_count, exp_ips_ipv4)
|
||||
self.assertEqual(ipv6_count, exp_ips_ipv6)
|
||||
|
||||
def _get_dns_domain(self):
|
||||
if not cfg.CONF.dns_domain:
|
||||
return ''
|
||||
if cfg.CONF.dns_domain.endswith('.'):
|
||||
return cfg.CONF.dns_domain
|
||||
return '%s.' % cfg.CONF.dns_domain
|
||||
|
||||
def _get_request_hostname_and_fqdn(self, dns_name):
|
||||
request_dns_name = ''
|
||||
request_fqdn = ''
|
||||
dns_domain = self._get_dns_domain()
|
||||
if dns_name and dns_domain and dns_domain != 'openstacklocal.':
|
||||
request_dns_name = dns_name
|
||||
request_fqdn = request_dns_name
|
||||
if not request_dns_name.endswith('.'):
|
||||
request_fqdn = '%s.%s' % (dns_name, dns_domain)
|
||||
return request_dns_name, request_fqdn
|
||||
|
||||
def _get_hostname_and_fqdn(self, request_dns_name, request_fqdn,
|
||||
assignment):
|
||||
dns_domain = self._get_dns_domain()
|
||||
if request_dns_name:
|
||||
hostname = request_dns_name
|
||||
fqdn = request_fqdn
|
||||
else:
|
||||
hostname = 'host-%s' % assignment['ip_address'].replace(
|
||||
'.', '-').replace(':', '-')
|
||||
fqdn = hostname
|
||||
if dns_domain:
|
||||
fqdn = '%s.%s' % (hostname, dns_domain)
|
||||
return hostname, fqdn
|
||||
|
||||
def _verify_ip_in_subnet(self, ip, subnets_list):
|
||||
for subnet in subnets_list:
|
||||
if ip in subnet:
|
||||
return True
|
||||
return False
|
||||
|
||||
def test_update_port_update_ip(self):
|
||||
"""Test update of port IP.
|
||||
|
||||
Check that a configured IP 10.0.0.2 is replaced by 10.0.0.10.
|
||||
"""
|
||||
with self.subnet() as subnet:
|
||||
with self.port(subnet=subnet) as port:
|
||||
ips = port['port']['fixed_ips']
|
||||
self.assertEqual(len(ips), 1)
|
||||
self.assertEqual(ips[0]['ip_address'], '10.0.0.2')
|
||||
self.assertEqual(ips[0]['subnet_id'], subnet['subnet']['id'])
|
||||
data = {'port': {'fixed_ips': [{'subnet_id':
|
||||
subnet['subnet']['id'],
|
||||
'ip_address': "10.0.0.10"}]}}
|
||||
req = self.new_update_request('ports', data,
|
||||
port['port']['id'])
|
||||
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
ips = res['port']['fixed_ips']
|
||||
self.assertEqual(len(ips), 1)
|
||||
self.assertEqual(ips[0]['ip_address'], '10.0.0.10')
|
||||
self.assertEqual(ips[0]['subnet_id'], subnet['subnet']['id'])
|
||||
self._verify_dns_assigment(res['port'], ips_list=['10.0.0.10'])
|
||||
|
||||
def test_update_port_update_ip_address_only(self):
|
||||
with self.subnet() as subnet:
|
||||
with self.port(subnet=subnet) as port:
|
||||
ips = port['port']['fixed_ips']
|
||||
self.assertEqual(len(ips), 1)
|
||||
self.assertEqual(ips[0]['ip_address'], '10.0.0.2')
|
||||
self.assertEqual(ips[0]['subnet_id'], subnet['subnet']['id'])
|
||||
data = {'port': {'fixed_ips': [{'subnet_id':
|
||||
subnet['subnet']['id'],
|
||||
'ip_address': "10.0.0.10"},
|
||||
{'ip_address': "10.0.0.2"}]}}
|
||||
req = self.new_update_request('ports', data,
|
||||
port['port']['id'])
|
||||
res = self.deserialize(self.fmt, req.get_response(self.api))
|
||||
ips = res['port']['fixed_ips']
|
||||
self.assertEqual(len(ips), 2)
|
||||
self.assertIn({'ip_address': '10.0.0.2',
|
||||
'subnet_id': subnet['subnet']['id']}, ips)
|
||||
self.assertIn({'ip_address': '10.0.0.10',
|
||||
'subnet_id': subnet['subnet']['id']}, ips)
|
||||
self._verify_dns_assigment(res['port'],
|
||||
ips_list=['10.0.0.10',
|
||||
'10.0.0.2'])
|
||||
|
||||
def test_create_port_with_multiple_ipv4_and_ipv6_subnets(self):
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets()
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_pqdn_and_dns_domain_no_period(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', 'example.com')
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets(
|
||||
dns_name='vm1')
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_pqdn_and_dns_domain_period(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', 'example.com.')
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets(
|
||||
dns_name='vm1')
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_pqdn_and_no_dns_domain(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', '')
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets()
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_fqdn_and_dns_domain_no_period(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', 'example.com')
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets(
|
||||
dns_name='vm1.example.com.')
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_fqdn_and_dns_domain_period(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', 'example.com.')
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets(
|
||||
dns_name='vm1.example.com.')
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_fqdn_default_domain_period(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', 'openstacklocal.')
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets()
|
||||
self.assertEqual(res.status_code, 201)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_bad_fqdn_and_dns_domain(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', 'example.com')
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets(
|
||||
dns_name='vm1.bad-domain.com.')
|
||||
self.assertEqual(res.status_code, 400)
|
||||
expected_error = ('The dns_name passed is a FQDN. Its higher level '
|
||||
'labels must be equal to the dns_domain option in '
|
||||
'neutron.conf')
|
||||
self.assertIn(expected_error, res.text)
|
||||
|
||||
def test_create_port_multiple_v4_v6_subnets_bad_pqdn_and_dns_domain(
|
||||
self):
|
||||
cfg.CONF.set_override('dns_domain', 'example.com')
|
||||
num_labels = int(
|
||||
math.floor(dns.FQDN_MAX_LEN / dns.DNS_LABEL_MAX_LEN))
|
||||
filler_len = int(
|
||||
math.floor(dns.FQDN_MAX_LEN % dns.DNS_LABEL_MAX_LEN))
|
||||
dns_name = (('a' * (dns.DNS_LABEL_MAX_LEN - 1) + '.') *
|
||||
num_labels + 'a' * filler_len)
|
||||
res = self._test_create_port_with_multiple_ipv4_and_ipv6_subnets(
|
||||
dns_name=dns_name)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
expected_error = ("When the two are concatenated to form a FQDN "
|
||||
"(with a '.' at the end), the resulting length "
|
||||
"exceeds the maximum size")
|
||||
self.assertIn(expected_error, res.text)
|
||||
|
||||
def _test_create_port_with_multiple_ipv4_and_ipv6_subnets(self,
|
||||
dns_name=''):
|
||||
"""Test port create with multiple IPv4, IPv6 DHCP/SLAAC subnets."""
|
||||
res = self._create_network(fmt=self.fmt, name='net',
|
||||
admin_state_up=True)
|
||||
network = self.deserialize(self.fmt, res)
|
||||
sub_dicts = [
|
||||
{'gateway': '10.0.0.1', 'cidr': '10.0.0.0/24',
|
||||
'ip_version': 4, 'ra_addr_mode': None},
|
||||
{'gateway': '10.0.1.1', 'cidr': '10.0.1.0/24',
|
||||
'ip_version': 4, 'ra_addr_mode': None},
|
||||
{'gateway': 'fe80::1', 'cidr': 'fe80::/64',
|
||||
'ip_version': 6, 'ra_addr_mode': constants.IPV6_SLAAC},
|
||||
{'gateway': 'fe81::1', 'cidr': 'fe81::/64',
|
||||
'ip_version': 6, 'ra_addr_mode': constants.IPV6_SLAAC},
|
||||
{'gateway': 'fe82::1', 'cidr': 'fe82::/64',
|
||||
'ip_version': 6, 'ra_addr_mode': constants.DHCPV6_STATEFUL},
|
||||
{'gateway': 'fe83::1', 'cidr': 'fe83::/64',
|
||||
'ip_version': 6, 'ra_addr_mode': constants.DHCPV6_STATEFUL}]
|
||||
subnets = {}
|
||||
for sub_dict in sub_dicts:
|
||||
subnet = self._make_subnet(
|
||||
self.fmt, network,
|
||||
gateway=sub_dict['gateway'],
|
||||
cidr=sub_dict['cidr'],
|
||||
ip_version=sub_dict['ip_version'],
|
||||
ipv6_ra_mode=sub_dict['ra_addr_mode'],
|
||||
ipv6_address_mode=sub_dict['ra_addr_mode'])
|
||||
subnets[subnet['subnet']['id']] = sub_dict
|
||||
res = self._create_port(self.fmt, net_id=network['network']['id'],
|
||||
dns_name=dns_name)
|
||||
if res.status_code != 201:
|
||||
return res
|
||||
port = self.deserialize(self.fmt, res)
|
||||
# Since the create port request was made without a list of fixed IPs,
|
||||
# the port should be associated with addresses for one of the
|
||||
# IPv4 subnets, one of the DHCPv6 subnets, and both of the IPv6
|
||||
# SLAAC subnets.
|
||||
self.assertEqual(4, len(port['port']['fixed_ips']))
|
||||
addr_mode_count = {None: 0, constants.DHCPV6_STATEFUL: 0,
|
||||
constants.IPV6_SLAAC: 0}
|
||||
for fixed_ip in port['port']['fixed_ips']:
|
||||
subnet_id = fixed_ip['subnet_id']
|
||||
if subnet_id in subnets:
|
||||
addr_mode_count[subnets[subnet_id]['ra_addr_mode']] += 1
|
||||
self.assertEqual(1, addr_mode_count[None])
|
||||
self.assertEqual(1, addr_mode_count[constants.DHCPV6_STATEFUL])
|
||||
self.assertEqual(2, addr_mode_count[constants.IPV6_SLAAC])
|
||||
self._verify_dns_assigment(port['port'], exp_ips_ipv4=1,
|
||||
exp_ips_ipv6=1,
|
||||
ipv4_cidrs=[sub_dicts[0]['cidr'],
|
||||
sub_dicts[1]['cidr']],
|
||||
ipv6_cidrs=[sub_dicts[4]['cidr'],
|
||||
sub_dicts[5]['cidr']],
|
||||
dns_name=dns_name)
|
||||
return res
|
||||
|
||||
def test_api_extension_validation_with_bad_dns_names(self):
|
||||
num_labels = int(
|
||||
math.floor(dns.FQDN_MAX_LEN / dns.DNS_LABEL_MAX_LEN))
|
||||
filler_len = int(
|
||||
math.floor(dns.FQDN_MAX_LEN % dns.DNS_LABEL_MAX_LEN))
|
||||
dns_names = [555, '\f\n\r', '.', '-vm01', '_vm01', 'vm01-',
|
||||
'-vm01.test1', 'vm01.-test1', 'vm01._test1',
|
||||
'vm01.test1-', 'vm01.te$t1', 'vm0#1.test1.',
|
||||
'vm01.123.', '-' + 'a' * dns.DNS_LABEL_MAX_LEN,
|
||||
'a' * (dns.DNS_LABEL_MAX_LEN + 1),
|
||||
('a' * (dns.DNS_LABEL_MAX_LEN - 1) + '.') *
|
||||
num_labels + 'a' * (filler_len + 1)]
|
||||
res = self._create_network(fmt=self.fmt, name='net',
|
||||
admin_state_up=True)
|
||||
network = self.deserialize(self.fmt, res)
|
||||
sub_dict = {'gateway': '10.0.0.1', 'cidr': '10.0.0.0/24',
|
||||
'ip_version': 4, 'ra_addr_mode': None}
|
||||
self._make_subnet(self.fmt, network, gateway=sub_dict['gateway'],
|
||||
cidr=sub_dict['cidr'],
|
||||
ip_version=sub_dict['ip_version'],
|
||||
ipv6_ra_mode=sub_dict['ra_addr_mode'],
|
||||
ipv6_address_mode=sub_dict['ra_addr_mode'])
|
||||
for dns_name in dns_names:
|
||||
res = self._create_port(self.fmt, net_id=network['network']['id'],
|
||||
dns_name=dns_name)
|
||||
self.assertEqual(res.status_code, 400)
|
||||
is_expected_message = (
|
||||
'cannot be converted to lowercase string' in res.text or
|
||||
'not a valid PQDN or FQDN. Reason:' in res.text)
|
||||
self.assertTrue(is_expected_message)
|
||||
|
||||
def test_api_extension_validation_with_good_dns_names(self):
|
||||
cfg.CONF.set_override('dns_domain', 'example.com')
|
||||
higher_labels_len = len('example.com.')
|
||||
num_labels = int(
|
||||
math.floor((dns.FQDN_MAX_LEN - higher_labels_len) /
|
||||
dns.DNS_LABEL_MAX_LEN))
|
||||
filler_len = int(
|
||||
math.floor((dns.FQDN_MAX_LEN - higher_labels_len) %
|
||||
dns.DNS_LABEL_MAX_LEN))
|
||||
dns_names = ['', 'www.1000.com', 'vM01', 'vm01.example.com.',
|
||||
'8vm01', 'vm-01.example.com.', 'vm01.test',
|
||||
'vm01.test.example.com.', 'vm01.test-100',
|
||||
'vm01.test-100.example.com.',
|
||||
'a' * dns.DNS_LABEL_MAX_LEN,
|
||||
('a' * dns.DNS_LABEL_MAX_LEN) + '.example.com.',
|
||||
('a' * (dns.DNS_LABEL_MAX_LEN - 1) + '.') *
|
||||
num_labels + 'a' * (filler_len - 1)]
|
||||
res = self._create_network(fmt=self.fmt, name='net',
|
||||
admin_state_up=True)
|
||||
network = self.deserialize(self.fmt, res)
|
||||
sub_dict = {'gateway': '10.0.0.1', 'cidr': '10.0.0.0/24',
|
||||
'ip_version': 4, 'ra_addr_mode': None}
|
||||
self._make_subnet(self.fmt, network, gateway=sub_dict['gateway'],
|
||||
cidr=sub_dict['cidr'],
|
||||
ip_version=sub_dict['ip_version'],
|
||||
ipv6_ra_mode=sub_dict['ra_addr_mode'],
|
||||
ipv6_address_mode=sub_dict['ra_addr_mode'])
|
||||
for dns_name in dns_names:
|
||||
res = self._create_port(self.fmt, net_id=network['network']['id'],
|
||||
dns_name=dns_name)
|
||||
self.assertEqual(res.status_code, 201)
|
Loading…
Reference in New Issue