]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Support Extra DHCP Options for IPv4 and IPv6
authorXu Han Peng <xuhanp@cn.ibm.com>
Thu, 23 Oct 2014 08:24:26 +0000 (16:24 +0800)
committerXu Han Peng <xuhanp@cn.ibm.com>
Thu, 29 Jan 2015 10:07:09 +0000 (18:07 +0800)
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

neutron/agent/linux/dhcp.py
neutron/db/extradhcpopt_db.py
neutron/db/migration/alembic_migrations/versions/16cdf118d31d_extra_dhcp_options_ipv6_support.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/HEAD
neutron/extensions/extra_dhcp_opt.py
neutron/tests/unit/test_extension_extradhcpopts.py
neutron/tests/unit/test_linux_dhcp.py

index 6d186f22237e380fab6541193f1fa11928454fa2..5906aad6208725fd5c7f306a7fec8bfbf135e4d0 100644 (file)
@@ -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(
index 6e7b23f43584a21483a3b846c0d3cb63ecfd5f40..fc58027c3d52eaf578020637f2d238869033102c 100644 (file)
@@ -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 (file)
index 0000000..e64a714
--- /dev/null
@@ -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']
+    )
index abd8db33b37db893d8372f3dec8822526c04fafb..f55008a4e107883169ba24bf7d4fd21589019cb8 100644 (file)
@@ -1 +1 @@
-14be42f3d0a5
\ No newline at end of file
+16cdf118d31d
index 33d2ed709e097476e697ef5df087a82bc40fd254..20370de1d676c9c7775091bff1769a1c9a1fc594 100644 (file)
@@ -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):
index 103a7939f638fe72d29bc6457543a6ee4d410a13..ea9b60d58688107ff8c829e28f51495fe63f7707 100644 (file)
@@ -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)
index 4b55d9798e7966e3fd291106a8d26c87c7c17111..84f66bed589615abea5bc7c291ac6474f41186e4 100644 (file)
@@ -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'