Add API and DB change for Blueprint extra-dhcp-opts-ipv4-ipv6.
Add unit tests for this change.
The validation of input extra dhcp options is not included
in this commit. A follow-up commit will be added for
validation.
DocImpact
APIImpact
Change-Id: I346334568929e50e51dd577cde6a257f4bce8e77
Partially-implements: Blueprint extra-dhcp-opts-ipv4-ipv6
from neutron.common import constants
from neutron.common import exceptions
from neutron.common import utils as commonutils
-from neutron.i18n import _LE
+from neutron.i18n import _LE, _LI
from neutron.openstack.common import log as logging
from neutron.openstack.common import uuidutils
def _output_opts_file(self):
"""Write a dnsmasq compatible options file."""
+ options, subnet_index_map = self._generate_opts_per_subnet()
+ options += self._generate_opts_per_port(subnet_index_map)
- if self.conf.enable_isolated_metadata:
- subnet_to_interface_ip = self._make_subnet_interface_ip_map()
+ name = self.get_conf_file_name('opts')
+ utils.replace_file(name, '\n'.join(options))
+ return name
+ def _generate_opts_per_subnet(self):
options = []
-
+ subnet_index_map = {}
+ if self.conf.enable_isolated_metadata:
+ subnet_to_interface_ip = self._make_subnet_interface_ip_map()
isolated_subnets = self.get_isolated_subnets(self.network)
- dhcp_ips = collections.defaultdict(list)
- subnet_idx_map = {}
for i, subnet in enumerate(self.network.subnets):
if (not subnet.enable_dhcp or
(subnet.ip_version == 6 and
else:
# use the dnsmasq ip as nameservers only if there is no
# dns-server submitted by the server
- subnet_idx_map[subnet.id] = i
+ subnet_index_map[subnet.id] = i
if self.conf.dhcp_domain and subnet.ip_version == 6:
options.append('tag:tag%s,option6:domain-search,%s' %
else:
options.append(self._format_option(subnet.ip_version,
i, 'router'))
+ return options, subnet_index_map
+ def _generate_opts_per_port(self, subnet_index_map):
+ options = []
+ dhcp_ips = collections.defaultdict(list)
for port in self.network.ports:
if getattr(port, 'extra_dhcp_opts', False):
- for ip_version in (4, 6):
- if any(
- netaddr.IPAddress(ip.ip_address).version == ip_version
- for ip in port.fixed_ips):
- options.extend(
- # TODO(xuhanp):Instead of applying extra_dhcp_opts
- # to both DHCPv4 and DHCPv6, we need to find a new
- # way to specify options for v4 and v6
- # respectively. We also need to validate the option
- # before applying it.
- self._format_option(ip_version, port.id,
- opt.opt_name, opt.opt_value)
- for opt in port.extra_dhcp_opts)
+ port_ip_versions = set(
+ [netaddr.IPAddress(ip.ip_address).version
+ for ip in port.fixed_ips])
+ for opt in port.extra_dhcp_opts:
+ opt_ip_version = opt.ip_version
+ if opt_ip_version in port_ip_versions:
+ options.append(
+ self._format_option(opt_ip_version, port.id,
+ opt.opt_name, opt.opt_value))
+ else:
+ LOG.info(_LI("Cannot apply dhcp option %(opt)s "
+ "because it's ip_version %(version)d "
+ "is not in port's address IP versions"),
+ {'opt': opt.opt_name,
+ 'version': opt_ip_version})
# provides all dnsmasq ip as dns-server if there is more than
# one dnsmasq for a subnet and there is no dns-server submitted
# by the server
if port.device_owner == constants.DEVICE_OWNER_DHCP:
for ip in port.fixed_ips:
- i = subnet_idx_map.get(ip.subnet_id)
+ i = subnet_index_map.get(ip.subnet_id)
if i is None:
continue
dhcp_ips[i].append(ip.ip_address)
','.join(
Dnsmasq._convert_to_literal_addrs(ip_version,
vx_ips))))
-
- name = self.get_conf_file_name('opts')
- utils.replace_file(name, '\n'.join(options))
- return name
+ return options
def _make_subnet_interface_ip_map(self):
ip_dev = ip_lib.IPDevice(
nullable=False)
opt_name = sa.Column(sa.String(64), nullable=False)
opt_value = sa.Column(sa.String(255), nullable=False)
- __table_args__ = (sa.UniqueConstraint('port_id',
- 'opt_name',
- name='uidx_portid_optname'),
+ ip_version = sa.Column(sa.Integer, server_default='4', nullable=False)
+ __table_args__ = (sa.UniqueConstraint(
+ 'port_id',
+ 'opt_name',
+ 'ip_version',
+ name='uniq_extradhcpopts0portid0optname0ipversion'),
model_base.BASEV2.__table_args__,)
# Add a relationship to the Port model in order to instruct SQLAlchemy to
with context.session.begin(subtransactions=True):
for dopt in extra_dhcp_opts:
if dopt['opt_value']:
+ ip_version = dopt.get('ip_version', 4)
db = ExtraDhcpOpt(
port_id=port['id'],
opt_name=dopt['opt_name'],
- opt_value=dopt['opt_value'])
+ opt_value=dopt['opt_value'],
+ ip_version=ip_version)
context.session.add(db)
return self._extend_port_extra_dhcp_opts_dict(context, port)
def _get_port_extra_dhcp_opts_binding(self, context, port_id):
query = self._model_query(context, ExtraDhcpOpt)
binding = query.filter(ExtraDhcpOpt.port_id == port_id)
- return [{'opt_name': r.opt_name, 'opt_value': r.opt_value}
+ return [{'opt_name': r.opt_name, 'opt_value': r.opt_value,
+ 'ip_version': r.ip_version}
for r in binding]
def _update_extra_dhcp_opts_on_port(self, context, id, port,
with context.session.begin(subtransactions=True):
for upd_rec in dopts:
for opt in opt_db:
- if opt['opt_name'] == upd_rec['opt_name']:
+ if (opt['opt_name'] == upd_rec['opt_name']
+ and opt['ip_version'] == upd_rec.get(
+ 'ip_version', 4)):
# to handle deleting of a opt from the port.
if upd_rec['opt_value'] is None:
context.session.delete(opt)
- elif opt['opt_value'] != upd_rec['opt_value']:
- opt.update(
- {'opt_value': upd_rec['opt_value']})
+ else:
+ if opt['opt_value'] != upd_rec['opt_value']:
+ opt.update(
+ {'opt_value': upd_rec['opt_value']})
break
else:
if upd_rec['opt_value'] is not None:
+ ip_version = upd_rec.get('ip_version', 4)
db = ExtraDhcpOpt(
port_id=id,
opt_name=upd_rec['opt_name'],
- opt_value=upd_rec['opt_value'])
+ opt_value=upd_rec['opt_value'],
+ ip_version=ip_version)
context.session.add(db)
if updated_port:
def _extend_port_dict_extra_dhcp_opt(self, res, port):
res[edo_ext.EXTRADHCPOPTS] = [{'opt_name': dho.opt_name,
- 'opt_value': dho.opt_value}
+ 'opt_value': dho.opt_value,
+ 'ip_version': dho.ip_version}
for dho in port.dhcp_opts]
return res
--- /dev/null
+# Copyright 2015 OpenStack Foundation
+#
+# 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.
+#
+
+"""extra_dhcp_options IPv6 support
+
+Revision ID: 16cdf118d31d
+Revises: 14be42f3d0a5
+Create Date: 2014-10-23 17:04:19.796731
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '16cdf118d31d'
+down_revision = '14be42f3d0a5'
+
+from alembic import op
+import sqlalchemy as sa
+
+from neutron.db import migration
+
+CONSTRAINT_NAME_OLD = 'uidx_portid_optname'
+CONSTRAINT_NAME_NEW = 'uniq_extradhcpopts0portid0optname0ipversion'
+TABLE_NAME = 'extradhcpopts'
+
+
+def upgrade():
+ with migration.remove_fks_from_table(TABLE_NAME):
+ op.drop_constraint(
+ name=CONSTRAINT_NAME_OLD,
+ table_name=TABLE_NAME,
+ type_='unique'
+ )
+
+ op.add_column('extradhcpopts', sa.Column('ip_version', sa.Integer(),
+ server_default='4', nullable=False))
+ op.execute("UPDATE extradhcpopts SET ip_version = 4")
+
+ op.create_unique_constraint(
+ name=CONSTRAINT_NAME_NEW,
+ source='extradhcpopts',
+ local_cols=['port_id', 'opt_name', 'ip_version']
+ )
+
+
+def downgrade():
+ with migration.remove_fks_from_table(TABLE_NAME):
+ op.drop_constraint(
+ name=CONSTRAINT_NAME_NEW,
+ table_name='extradhcpopts',
+ type_='unique'
+ )
+ op.drop_column('extradhcpopts', 'ip_version')
+
+ op.create_unique_constraint(
+ name=CONSTRAINT_NAME_OLD,
+ source='extradhcpopts',
+ local_cols=['port_id', 'opt_name']
+ )
-14be42f3d0a5
\ No newline at end of file
+16cdf118d31d
'opt_name': {'type:not_empty_string': None,
'required': True},
'opt_value': {'type:not_empty_string_or_none': None,
- 'required': True}}}}}}
+ 'required': True},
+ 'ip_version': {'convert_to': attr.convert_to_int,
+ 'type:values': [4, 6],
+ 'required': False}}}}}}
class Extra_dhcp_opt(extensions.ExtensionDescriptor):
for opt in returned:
name = opt['opt_name']
for exp in expected:
- if name == exp['opt_name']:
+ if (name == exp['opt_name']
+ and opt['ip_version'] == exp.get(
+ 'ip_version', 4)):
val = exp['opt_value']
break
- self.assertEqual(opt['opt_value'], val)
+ self.assertEqual(val, opt['opt_value'])
def test_create_port_with_extradhcpopts(self):
opt_list = [{'opt_name': 'bootfile-name',
self._check_opts(expected,
port['port'][edo_ext.EXTRADHCPOPTS])
- def test_update_port_with_extradhcpopts_with_same(self):
- opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'},
- {'opt_name': 'tftp-server',
- 'opt_value': '123.123.123.123'},
+ def test_create_port_with_extradhcpopts_ipv4_opt_version(self):
+ opt_list = [{'opt_name': 'bootfile-name',
+ 'opt_value': 'pxelinux.0',
+ 'ip_version': 4},
{'opt_name': 'server-ip-address',
- 'opt_value': '123.123.123.456'}]
- upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'changeme.0'}]
- expected_opts = opt_list[:]
- for i in expected_opts:
- if i['opt_name'] == upd_opts[0]['opt_name']:
- i['opt_value'] = upd_opts[0]['opt_value']
- break
+ 'opt_value': '123.123.123.456',
+ 'ip_version': 4},
+ {'opt_name': 'tftp-server',
+ 'opt_value': '123.123.123.123',
+ 'ip_version': 4}]
+
+ params = {edo_ext.EXTRADHCPOPTS: opt_list,
+ 'arg_list': (edo_ext.EXTRADHCPOPTS,)}
+
+ with self.port(**params) as port:
+ self._check_opts(opt_list,
+ port['port'][edo_ext.EXTRADHCPOPTS])
+
+ def test_create_port_with_extradhcpopts_ipv6_opt_version(self):
+ opt_list = [{'opt_name': 'bootfile-name',
+ 'opt_value': 'pxelinux.0',
+ 'ip_version': 6},
+ {'opt_name': 'tftp-server',
+ 'opt_value': '2001:192:168::1',
+ 'ip_version': 6}]
+
+ params = {edo_ext.EXTRADHCPOPTS: opt_list,
+ 'arg_list': (edo_ext.EXTRADHCPOPTS,)}
+ with self.port(**params) as port:
+ self._check_opts(opt_list,
+ port['port'][edo_ext.EXTRADHCPOPTS])
+
+ def _test_update_port_with_extradhcpopts(self, opt_list, upd_opts,
+ expected_opts):
params = {edo_ext.EXTRADHCPOPTS: opt_list,
'arg_list': (edo_ext.EXTRADHCPOPTS,)}
req = self.new_update_request('ports', update_port,
port['port']['id'])
- port = self.deserialize('json', req.get_response(self.api))
+ res = req.get_response(self.api)
+ self.assertEqual(res.status_int, webob.exc.HTTPOk.code)
+ port = self.deserialize('json', res)
self._check_opts(expected_opts,
port['port'][edo_ext.EXTRADHCPOPTS])
+ def test_update_port_with_extradhcpopts_with_same(self):
+ opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'},
+ {'opt_name': 'tftp-server',
+ 'opt_value': '123.123.123.123'},
+ {'opt_name': 'server-ip-address',
+ 'opt_value': '123.123.123.456'}]
+ upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'changeme.0'}]
+ expected_opts = opt_list[:]
+ for i in expected_opts:
+ if i['opt_name'] == upd_opts[0]['opt_name']:
+ i['opt_value'] = upd_opts[0]['opt_value']
+ break
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
+
def test_update_port_with_additional_extradhcpopt(self):
opt_list = [{'opt_name': 'tftp-server',
'opt_value': '123.123.123.123'},
upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'changeme.0'}]
expected_opts = copy.deepcopy(opt_list)
expected_opts.append(upd_opts[0])
- params = {edo_ext.EXTRADHCPOPTS: opt_list,
- 'arg_list': (edo_ext.EXTRADHCPOPTS,)}
-
- with self.port(**params) as port:
- update_port = {'port': {edo_ext.EXTRADHCPOPTS: upd_opts}}
-
- req = self.new_update_request('ports', update_port,
- port['port']['id'])
- port = self.deserialize('json', req.get_response(self.api))
- self._check_opts(expected_opts,
- port['port'][edo_ext.EXTRADHCPOPTS])
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
def test_update_port_with_extradhcpopts(self):
opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'},
if i['opt_name'] == upd_opts[0]['opt_name']:
i['opt_value'] = upd_opts[0]['opt_value']
break
-
- params = {edo_ext.EXTRADHCPOPTS: opt_list,
- 'arg_list': (edo_ext.EXTRADHCPOPTS,)}
-
- with self.port(**params) as port:
- update_port = {'port': {edo_ext.EXTRADHCPOPTS: upd_opts}}
-
- req = self.new_update_request('ports', update_port,
- port['port']['id'])
- port = self.deserialize('json', req.get_response(self.api))
- self._check_opts(expected_opts,
- port['port'][edo_ext.EXTRADHCPOPTS])
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
def test_update_port_with_extradhcpopt_delete(self):
opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'},
expected_opts = [opt for opt in opt_list
if opt['opt_name'] != 'bootfile-name']
-
- params = {edo_ext.EXTRADHCPOPTS: opt_list,
- 'arg_list': (edo_ext.EXTRADHCPOPTS,)}
-
- with self.port(**params) as port:
- update_port = {'port': {edo_ext.EXTRADHCPOPTS: upd_opts}}
-
- req = self.new_update_request('ports', update_port,
- port['port']['id'])
- port = self.deserialize('json', req.get_response(self.api))
- self._check_opts(expected_opts,
- port['port'][edo_ext.EXTRADHCPOPTS])
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
def test_update_port_without_extradhcpopt_delete(self):
+ opt_list = []
upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': None}]
-
- with self.port() as port:
- update_port = {'port': {edo_ext.EXTRADHCPOPTS: upd_opts}}
-
- req = self.new_update_request('ports', update_port,
- port['port']['id'])
- port = self.deserialize('json', req.get_response(self.api))
- edo_attr = port['port'].get(edo_ext.EXTRADHCPOPTS)
- self.assertEqual(edo_attr, [])
+ expected_opts = []
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
def test_update_port_adding_extradhcpopts(self):
- opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'},
+ opt_list = []
+ upd_opts = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'},
{'opt_name': 'tftp-server',
'opt_value': '123.123.123.123'},
{'opt_name': 'server-ip-address',
'opt_value': '123.123.123.456'}]
- with self.port() as port:
- update_port = {'port': {edo_ext.EXTRADHCPOPTS: opt_list}}
-
- req = self.new_update_request('ports', update_port,
- port['port']['id'])
- port = self.deserialize('json', req.get_response(self.api))
- self._check_opts(opt_list,
- port['port'][edo_ext.EXTRADHCPOPTS])
+ expected_opts = copy.deepcopy(upd_opts)
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
def test_update_port_with_blank_string_extradhcpopt(self):
opt_list = [{'opt_name': 'bootfile-name', 'opt_value': 'pxelinux.0'},
port['port']['id'])
res = req.get_response(self.api)
self.assertEqual(res.status_int, webob.exc.HTTPBadRequest.code)
+
+ def test_update_port_with_extradhcpopts_ipv6_change_value(self):
+ opt_list = [{'opt_name': 'bootfile-name',
+ 'opt_value': 'pxelinux.0',
+ 'ip_version': 6},
+ {'opt_name': 'tftp-server',
+ 'opt_value': '2001:192:168::1',
+ 'ip_version': 6}]
+ upd_opts = [{'opt_name': 'tftp-server',
+ 'opt_value': '2001:192:168::2',
+ 'ip_version': 6}]
+ expected_opts = copy.deepcopy(opt_list)
+ for i in expected_opts:
+ if i['opt_name'] == upd_opts[0]['opt_name']:
+ i['opt_value'] = upd_opts[0]['opt_value']
+ break
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
+
+ def test_update_port_with_extradhcpopts_add_another_ver_opt(self):
+ opt_list = [{'opt_name': 'bootfile-name',
+ 'opt_value': 'pxelinux.0',
+ 'ip_version': 6},
+ {'opt_name': 'tftp-server',
+ 'opt_value': '2001:192:168::1',
+ 'ip_version': 6}]
+ upd_opts = [{'opt_name': 'tftp-server',
+ 'opt_value': '123.123.123.123',
+ 'ip_version': 4}]
+ expected_opts = copy.deepcopy(opt_list)
+ expected_opts.extend(upd_opts)
+ self._test_update_port_with_extradhcpopts(opt_list, upd_opts,
+ expected_opts)
class DhcpOpt(object):
def __init__(self, **kwargs):
+ self.__dict__.update(ip_version=4)
self.__dict__.update(kwargs)
def __str__(self):
DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux3.0')]
+class FakeV6NetworkPxePort(object):
+ id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
+ subnets = [FakeV6SubnetDHCPStateful()]
+ ports = [FakeV6Port()]
+ namespace = 'qdhcp-ns'
+
+ def __init__(self):
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='2001:192:168::1',
+ ip_version=6),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0',
+ ip_version=6)]
+
+
+class FakeV6NetworkPxePortWrongOptVersion(object):
+ id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
+ subnets = [FakeV6SubnetDHCPStateful()]
+ ports = [FakeV6Port()]
+ namespace = 'qdhcp-ns'
+
+ def __init__(self):
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.7',
+ ip_version=4),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0',
+ ip_version=6)]
+
+
class FakeDualStackNetworkSingleDHCP(object):
id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
self.safe.assert_called_once_with('/foo/opts', expected)
+ @mock.patch('neutron.agent.linux.dhcp.Dnsmasq.get_conf_file_name',
+ return_value='/foo/opts')
+ def test_output_opts_file_pxe_ipv6_port_with_ipv6_opt(self,
+ mock_get_conf_fn):
+ expected = (
+ 'tag:tag0,option6:dns-server,[2001:0200:feed:7ac0::1]\n'
+ 'tag:tag0,option6:domain-search,openstacklocal\n'
+ 'tag:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh,'
+ 'option6:tftp-server,2001:192:168::1\n'
+ 'tag:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh,'
+ 'option6:bootfile-name,pxelinux.0')
+ expected = expected.lstrip()
+
+ dm = self._get_dnsmasq(FakeV6NetworkPxePort())
+ dm._output_opts_file()
+
+ self.safe.assert_called_once_with('/foo/opts', expected)
+
+ @mock.patch('neutron.agent.linux.dhcp.Dnsmasq.get_conf_file_name',
+ return_value='/foo/opts')
+ def test_output_opts_file_pxe_ipv6_port_with_ipv4_opt(self,
+ mock_get_conf_fn):
+ expected = (
+ 'tag:tag0,option6:dns-server,[2001:0200:feed:7ac0::1]\n'
+ 'tag:tag0,option6:domain-search,openstacklocal\n'
+ 'tag:hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh,'
+ 'option6:bootfile-name,pxelinux.0')
+ expected = expected.lstrip()
+
+ dm = self._get_dnsmasq(FakeV6NetworkPxePortWrongOptVersion())
+ dm._output_opts_file()
+
+ self.safe.assert_called_once_with('/foo/opts', expected)
+
@property
def _test_no_dhcp_domain_alloc_data(self):
exp_host_name = '/dhcp/cccccccc-cccc-cccc-cccc-cccccccccccc/host'