From: Xu Han Peng Date: Thu, 23 Oct 2014 08:24:26 +0000 (+0800) Subject: Support Extra DHCP Options for IPv4 and IPv6 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=a42928780cec66446999d9a76804b2ed8c1d570a;p=openstack-build%2Fneutron-build.git Support Extra DHCP Options for IPv4 and IPv6 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 --- diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py index 6d186f222..5906aad62 100644 --- a/neutron/agent/linux/dhcp.py +++ b/neutron/agent/linux/dhcp.py @@ -31,7 +31,7 @@ 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.i18n import _LE +from neutron.i18n import _LE, _LI from neutron.openstack.common import log as logging from neutron.openstack.common import uuidutils @@ -549,15 +549,19 @@ class Dnsmasq(DhcpLocalProcess): 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 @@ -574,7 +578,7 @@ class Dnsmasq(DhcpLocalProcess): 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' % @@ -619,29 +623,35 @@ class Dnsmasq(DhcpLocalProcess): 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) @@ -657,10 +667,7 @@ class Dnsmasq(DhcpLocalProcess): ','.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( diff --git a/neutron/db/extradhcpopt_db.py b/neutron/db/extradhcpopt_db.py index 6e7b23f43..fc58027c3 100644 --- a/neutron/db/extradhcpopt_db.py +++ b/neutron/db/extradhcpopt_db.py @@ -39,9 +39,12 @@ class ExtraDhcpOpt(model_base.BASEV2, models_v2.HasId): 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 @@ -62,10 +65,12 @@ class ExtraDhcpOptMixin(object): 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) @@ -76,7 +81,8 @@ class ExtraDhcpOptMixin(object): 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, @@ -93,20 +99,25 @@ class ExtraDhcpOptMixin(object): 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: @@ -117,7 +128,8 @@ class ExtraDhcpOptMixin(object): 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 diff --git a/neutron/db/migration/alembic_migrations/versions/16cdf118d31d_extra_dhcp_options_ipv6_support.py b/neutron/db/migration/alembic_migrations/versions/16cdf118d31d_extra_dhcp_options_ipv6_support.py new file mode 100644 index 000000000..e64a714b3 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/16cdf118d31d_extra_dhcp_options_ipv6_support.py @@ -0,0 +1,70 @@ +# 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'] + ) diff --git a/neutron/db/migration/alembic_migrations/versions/HEAD b/neutron/db/migration/alembic_migrations/versions/HEAD index abd8db33b..f55008a4e 100644 --- a/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -14be42f3d0a5 \ No newline at end of file +16cdf118d31d diff --git a/neutron/extensions/extra_dhcp_opt.py b/neutron/extensions/extra_dhcp_opt.py index 33d2ed709..20370de1d 100644 --- a/neutron/extensions/extra_dhcp_opt.py +++ b/neutron/extensions/extra_dhcp_opt.py @@ -55,7 +55,10 @@ EXTENDED_ATTRIBUTES_2_0 = { '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): diff --git a/neutron/tests/unit/test_extension_extradhcpopts.py b/neutron/tests/unit/test_extension_extradhcpopts.py index 103a7939f..ea9b60d58 100644 --- a/neutron/tests/unit/test_extension_extradhcpopts.py +++ b/neutron/tests/unit/test_extension_extradhcpopts.py @@ -64,10 +64,12 @@ class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase): 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', @@ -103,19 +105,41 @@ class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase): 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,)} @@ -124,10 +148,27 @@ class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase): 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'}, @@ -136,17 +177,8 @@ class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase): 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'}, @@ -160,18 +192,8 @@ class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase): 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'}, @@ -184,45 +206,26 @@ class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase): 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'}, @@ -261,3 +264,36 @@ class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase): 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) diff --git a/neutron/tests/unit/test_linux_dhcp.py b/neutron/tests/unit/test_linux_dhcp.py index 4b55d9798..84f66bed5 100644 --- a/neutron/tests/unit/test_linux_dhcp.py +++ b/neutron/tests/unit/test_linux_dhcp.py @@ -39,6 +39,7 @@ class FakeIPAllocation(object): class DhcpOpt(object): def __init__(self, **kwargs): + self.__dict__.update(ip_version=4) self.__dict__.update(kwargs) def __str__(self): @@ -465,6 +466,34 @@ class FakeV4NetworkPxe3Ports(object): 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' @@ -1036,6 +1065,40 @@ class TestDnsmasq(TestBase): 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'