summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--neutron/agent/linux/dhcp.py30
-rw-r--r--neutron/db/extradhcpopt_db.py131
-rw-r--r--neutron/db/migration/alembic_migrations/versions/53bbd27ec841_extra_dhcp_opts_supp.py64
-rw-r--r--neutron/extensions/extra_dhcp_opt.py89
-rw-r--r--neutron/plugins/openvswitch/ovs_neutron_plugin.py14
-rw-r--r--neutron/tests/unit/test_extension_extradhcpopts.py172
-rw-r--r--neutron/tests/unit/test_linux_dhcp.py212
7 files changed, 705 insertions, 7 deletions
diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py
index 060ed3b189..5257ca92f9 100644
--- a/neutron/agent/linux/dhcp.py
+++ b/neutron/agent/linux/dhcp.py
@@ -397,8 +397,17 @@ class Dnsmasq(DhcpLocalProcess):
for alloc in port.fixed_ips:
name = 'host-%s.%s' % (r.sub('-', alloc.ip_address),
self.conf.dhcp_domain)
- buf.write('%s,%s,%s\n' %
- (port.mac_address, name, alloc.ip_address))
+ set_tag = ''
+ if port.extra_dhcp_opts:
+ if self.version >= self.MINIMUM_VERSION:
+ set_tag = 'set:'
+
+ buf.write('%s,%s,%s,%s%s\n' %
+ (port.mac_address, name, alloc.ip_address,
+ set_tag, port.id))
+ else:
+ buf.write('%s,%s,%s\n' %
+ (port.mac_address, name, alloc.ip_address))
name = self.get_conf_file_name('host')
utils.replace_file(name, buf.getvalue())
@@ -453,6 +462,12 @@ class Dnsmasq(DhcpLocalProcess):
else:
options.append(self._format_option(i, 'router'))
+ for port in self.network.ports:
+ if port.extra_dhcp_opts:
+ options.extend(
+ self._format_option(port.id, opt.opt_name, opt.opt_value)
+ for opt in port.extra_dhcp_opts)
+
name = self.get_conf_file_name('opts')
utils.replace_file(name, '\n'.join(options))
return name
@@ -479,17 +494,22 @@ class Dnsmasq(DhcpLocalProcess):
return retval
- def _format_option(self, index, option, *args):
+ def _format_option(self, tag, option, *args):
"""Format DHCP option by option name or code."""
if self.version >= self.MINIMUM_VERSION:
set_tag = 'tag:'
else:
set_tag = ''
+
option = str(option)
+
+ if isinstance(tag, int):
+ tag = self._TAG_PREFIX % tag
+
if not option.isdigit():
option = 'option:%s' % option
- return ','.join((set_tag + self._TAG_PREFIX % index,
- option) + args)
+
+ return ','.join((set_tag + tag, '%s' % option) + args)
@classmethod
def lease_update(cls):
diff --git a/neutron/db/extradhcpopt_db.py b/neutron/db/extradhcpopt_db.py
new file mode 100644
index 0000000000..abbccbac87
--- /dev/null
+++ b/neutron/db/extradhcpopt_db.py
@@ -0,0 +1,131 @@
+# Copyright (c) 2013 OpenStack Foundation.
+# 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.
+#
+# @author: Don Kehn, dekehn@gmail.com
+#
+import sqlalchemy as sa
+from sqlalchemy import orm
+
+from neutron.api.v2 import attributes
+from neutron.db import db_base_plugin_v2
+from neutron.db import model_base
+from neutron.db import models_v2
+from neutron.extensions import extra_dhcp_opt as edo_ext
+from neutron.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+class ExtraDhcpOpt(model_base.BASEV2, models_v2.HasId):
+ """Represent a generic concept of extra options associated to a port.
+
+ Each port may have none to many dhcp opts associated to it that can
+ define specifically different or extra options to DHCP clients.
+ These will be written to the <network_id>/opts files, and each option's
+ tag will be referenced in the <network_id>/host file.
+ """
+ port_id = sa.Column(sa.String(36),
+ sa.ForeignKey('ports.id', ondelete="CASCADE"),
+ 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'),)
+
+ # Add a relationship to the Port model in order to instruct SQLAlchemy to
+ # eagerly load extra_dhcp_opts bindings
+ ports = orm.relationship(
+ models_v2.Port,
+ backref=orm.backref("dhcp_opts", lazy='joined', cascade='delete'))
+
+
+class ExtraDhcpOptMixin(object):
+ """Mixin class to add extra options to the DHCP opts file
+ and associate them to a port.
+ """
+ def _process_port_create_extra_dhcp_opts(self, context, port,
+ extra_dhcp_opts):
+ if not extra_dhcp_opts:
+ return port
+ with context.session.begin(subtransactions=True):
+ for dopt in extra_dhcp_opts:
+ db = ExtraDhcpOpt(
+ port_id=port['id'],
+ opt_name=dopt['opt_name'],
+ opt_value=dopt['opt_value'])
+ context.session.add(db)
+ return self._extend_port_extra_dhcp_opts_dict(context, port)
+
+ def _extend_port_extra_dhcp_opts_dict(self, context, port):
+ port[edo_ext.EXTRADHCPOPTS] = self._get_port_extra_dhcp_opts_binding(
+ context, port['id'])
+
+ 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}
+ for r in binding]
+
+ def _update_extra_dhcp_opts_on_port(self, context, id, port,
+ updated_port=None):
+ # It is not necessary to update in a transaction, because
+ # its called from within one from ovs_neutron_plugin.
+ dopts = port['port'].get(edo_ext.EXTRADHCPOPTS)
+
+ if dopts:
+ opt_db = self._model_query(
+ context, ExtraDhcpOpt).filter_by(port_id=id).all()
+ # if there are currently no dhcp_options associated to
+ # this port, Then just insert the new ones and be done.
+ if not opt_db:
+ with context.session.begin(subtransactions=True):
+ for dopt in dopts:
+ db = ExtraDhcpOpt(
+ port_id=id,
+ opt_name=dopt['opt_name'],
+ opt_value=dopt['opt_value'])
+ context.session.add(db)
+ else:
+ for upd_rec in dopts:
+ with context.session.begin(subtransactions=True):
+ for opt in opt_db:
+ if opt['opt_name'] == upd_rec['opt_name']:
+ if opt['opt_value'] != upd_rec['opt_value']:
+ opt.update(
+ {'opt_value': upd_rec['opt_value']})
+ break
+ # this handles the adding an option that didn't exist.
+ else:
+ db = ExtraDhcpOpt(
+ port_id=id,
+ opt_name=upd_rec['opt_name'],
+ opt_value=upd_rec['opt_value'])
+ context.session.add(db)
+
+ if updated_port:
+ edolist = self._get_port_extra_dhcp_opts_binding(context, id)
+ updated_port[edo_ext.EXTRADHCPOPTS] = edolist
+
+ return bool(dopts)
+
+ def _extend_port_dict_extra_dhcp_opt(self, res, port):
+ res[edo_ext.EXTRADHCPOPTS] = [{'opt_name': dho.opt_name,
+ 'opt_value': dho.opt_value}
+ for dho in port.dhcp_opts]
+ return res
+
+ db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
+ attributes.PORTS, [_extend_port_dict_extra_dhcp_opt])
diff --git a/neutron/db/migration/alembic_migrations/versions/53bbd27ec841_extra_dhcp_opts_supp.py b/neutron/db/migration/alembic_migrations/versions/53bbd27ec841_extra_dhcp_opts_supp.py
new file mode 100644
index 0000000000..e44d7daeea
--- /dev/null
+++ b/neutron/db/migration/alembic_migrations/versions/53bbd27ec841_extra_dhcp_opts_supp.py
@@ -0,0 +1,64 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 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 opts support
+
+Revision ID: 53bbd27ec841
+Revises: 40dffbf4b549
+Create Date: 2013-05-09 15:36:50.485036
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '53bbd27ec841'
+down_revision = '40dffbf4b549'
+
+# Change to ['*'] if this migration applies to all plugins
+
+migration_for_plugins = [
+ 'neutron.plugins.openvswitch.ovs_neutron_plugin.OVSNeutronPluginV2'
+]
+
+from alembic import op
+import sqlalchemy as sa
+
+
+from neutron.db import migration
+
+
+def upgrade(active_plugins=None, options=None):
+ if not migration.should_run(active_plugins, migration_for_plugins):
+ return
+
+ op.create_table(
+ 'extradhcpopts',
+ sa.Column('id', sa.String(length=36), nullable=False),
+ sa.Column('port_id', sa.String(length=36), nullable=False),
+ sa.Column('opt_name', sa.String(length=64), nullable=False),
+ sa.Column('opt_value', sa.String(length=255), nullable=False),
+ sa.ForeignKeyConstraint(['port_id'], ['ports.id'], ondelete='CASCADE'),
+ sa.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('port_id', 'opt_name', name='uidx_portid_optname'))
+
+
+def downgrade(active_plugins=None, options=None):
+ if not migration.should_run(active_plugins, migration_for_plugins):
+ return
+
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('extradhcpopts')
+ ### end Alembic commands ###
diff --git a/neutron/extensions/extra_dhcp_opt.py b/neutron/extensions/extra_dhcp_opt.py
new file mode 100644
index 0000000000..39995ccbff
--- /dev/null
+++ b/neutron/extensions/extra_dhcp_opt.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2013 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.
+#
+# @Author Don Kehn, dekehn@gmail.com
+
+from neutron.api import extensions
+from neutron.api.v2 import attributes as attr
+from neutron.common import exceptions
+
+
+# ExtraDHcpOpts Exceptions
+class ExtraDhcpOptNotFound(exceptions.NotFound):
+ message = _("ExtraDhcpOpt %(id)s could not be found")
+
+
+class ExtraDhcpOptBadData(exceptions.InvalidInput):
+ message = _("Invalid data format for extra-dhcp-opt, "
+ "provide a list of dicts: %(data)s")
+
+
+def _validate_list_of_dict_or_none(data, key_specs=None):
+ if data is not None:
+ if not isinstance(data, list):
+ raise ExtraDhcpOptBadData(data=data)
+ for d in data:
+ msg = attr._validate_dict(d, key_specs)
+ if msg:
+ raise ExtraDhcpOptBadData(data=msg)
+
+attr.validators['type:list_of_dict_or_none'] = _validate_list_of_dict_or_none
+
+# Attribute Map
+EXTRADHCPOPTS = 'extra_dhcp_opts'
+
+EXTENDED_ATTRIBUTES_2_0 = {
+ 'ports': {
+ EXTRADHCPOPTS:
+ {'allow_post': True,
+ 'allow_put': True,
+ 'is_visible': True,
+ 'default': None,
+ 'validate': {
+ 'type:list_of_dict_or_none': {
+ 'id': {'type:uuid': None, 'required': False},
+ 'opt_name': {'type:string': None, 'required': True},
+ 'opt_value': {'type:string': None, 'required': True}}}}}}
+
+
+class Extra_dhcp_opt(extensions.ExtensionDescriptor):
+ @classmethod
+ def get_name(cls):
+ return "Neutron Extra DHCP opts"
+
+ @classmethod
+ def get_alias(cls):
+ return "extra_dhcp_opt"
+
+ @classmethod
+ def get_description(cls):
+ return ("Extra options configuration for DHCP. "
+ "For example PXE boot options to DHCP clients can "
+ "be specified (e.g. tftp-server, server-ip-address, "
+ "bootfile-name)")
+
+ @classmethod
+ def get_namespace(cls):
+ return "http://docs.openstack.org/ext/neutron/extra_dhcp_opt/api/v1.0"
+
+ @classmethod
+ def get_updated(cls):
+ return "2013-03-17T12:00:00-00:00"
+
+ def get_extended_resources(self, version):
+ if version == "2.0":
+ return EXTENDED_ATTRIBUTES_2_0
+ else:
+ return {}
diff --git a/neutron/plugins/openvswitch/ovs_neutron_plugin.py b/neutron/plugins/openvswitch/ovs_neutron_plugin.py
index baeef5ef54..31312bf4ea 100644
--- a/neutron/plugins/openvswitch/ovs_neutron_plugin.py
+++ b/neutron/plugins/openvswitch/ovs_neutron_plugin.py
@@ -38,12 +38,14 @@ from neutron.db import agents_db
from neutron.db import agentschedulers_db
from neutron.db import db_base_plugin_v2
from neutron.db import dhcp_rpc_base
+from neutron.db import extradhcpopt_db
from neutron.db import extraroute_db
from neutron.db import l3_gwmode_db
from neutron.db import l3_rpc_base
from neutron.db import portbindings_db
from neutron.db import quota_db # noqa
from neutron.db import securitygroups_rpc_base as sg_db_rpc
+from neutron.extensions import extra_dhcp_opt as edo_ext
from neutron.extensions import portbindings
from neutron.extensions import providernet as provider
from neutron.openstack.common import importutils
@@ -222,7 +224,8 @@ class OVSNeutronPluginV2(db_base_plugin_v2.NeutronDbPluginV2,
sg_db_rpc.SecurityGroupServerRpcMixin,
agentschedulers_db.L3AgentSchedulerDbMixin,
agentschedulers_db.DhcpAgentSchedulerDbMixin,
- portbindings_db.PortBindingMixin):
+ portbindings_db.PortBindingMixin,
+ extradhcpopt_db.ExtraDhcpOptMixin):
"""Implement the Neutron abstractions using Open vSwitch.
@@ -252,7 +255,8 @@ class OVSNeutronPluginV2(db_base_plugin_v2.NeutronDbPluginV2,
"binding", "quotas", "security-group",
"agent", "extraroute",
"l3_agent_scheduler",
- "dhcp_agent_scheduler"]
+ "dhcp_agent_scheduler",
+ "extra_dhcp_opt"]
@property
def supported_extension_aliases(self):
@@ -536,10 +540,13 @@ class OVSNeutronPluginV2(db_base_plugin_v2.NeutronDbPluginV2,
with session.begin(subtransactions=True):
self._ensure_default_security_group_on_port(context, port)
sgids = self._get_security_groups_on_port(context, port)
+ dhcp_opts = port['port'].get(edo_ext.EXTRADHCPOPTS, [])
port = super(OVSNeutronPluginV2, self).create_port(context, port)
self._process_portbindings_create_and_update(context,
port_data, port)
self._process_port_create_security_group(context, port, sgids)
+ self._process_port_create_extra_dhcp_opts(context, port,
+ dhcp_opts)
self.notify_security_groups_member_updated(context, port)
return port
@@ -556,6 +563,9 @@ class OVSNeutronPluginV2(db_base_plugin_v2.NeutronDbPluginV2,
self._process_portbindings_create_and_update(context,
port['port'],
updated_port)
+ need_port_update_notify |= self._update_extra_dhcp_opts_on_port(
+ context, id, port, updated_port)
+
need_port_update_notify |= self.is_security_group_member_updated(
context, original_port, updated_port)
if original_port['admin_state_up'] != updated_port['admin_state_up']:
diff --git a/neutron/tests/unit/test_extension_extradhcpopts.py b/neutron/tests/unit/test_extension_extradhcpopts.py
new file mode 100644
index 0000000000..fbe583663a
--- /dev/null
+++ b/neutron/tests/unit/test_extension_extradhcpopts.py
@@ -0,0 +1,172 @@
+# Copyright (c) 2013 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.
+#
+# @author: D.E. Kehn, dekehn@gmail.com
+#
+
+import copy
+
+from neutron.db import db_base_plugin_v2
+from neutron.db import extradhcpopt_db as edo_db
+from neutron.extensions import extra_dhcp_opt as edo_ext
+from neutron.openstack.common import log as logging
+from neutron.tests.unit import test_db_plugin
+
+LOG = logging.getLogger(__name__)
+
+DB_PLUGIN_KLASS = (
+ 'neutron.tests.unit.test_extension_extradhcpopts.ExtraDhcpOptTestPlugin')
+
+
+class ExtraDhcpOptTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
+ edo_db.ExtraDhcpOptMixin):
+ """Test plugin that implements necessary calls on create/delete port for
+ associating ports with extra dhcp options.
+ """
+
+ supported_extension_aliases = ["extra_dhcp_opt"]
+
+ def create_port(self, context, port):
+ with context.session.begin(subtransactions=True):
+ edos = port['port'].get(edo_ext.EXTRADHCPOPTS, [])
+ new_port = super(ExtraDhcpOptTestPlugin, self).create_port(
+ context, port)
+ self._process_port_create_extra_dhcp_opts(context, new_port, edos)
+ return new_port
+
+ def update_port(self, context, id, port):
+ with context.session.begin(subtransactions=True):
+ rtn_port = super(ExtraDhcpOptTestPlugin, self).update_port(
+ context, id, port)
+ self._update_extra_dhcp_opts_on_port(context, id, port, rtn_port)
+ return rtn_port
+
+
+class ExtraDhcpOptDBTestCase(test_db_plugin.NeutronDbPluginV2TestCase):
+ def setUp(self, plugin=None):
+ super(ExtraDhcpOptDBTestCase, self).setUp(plugin=DB_PLUGIN_KLASS)
+
+
+class TestExtraDhcpOpt(ExtraDhcpOptDBTestCase):
+ def _check_opts(self, expected, returned):
+ self.assertEqual(len(expected), len(returned))
+ for opt in returned:
+ name = opt['opt_name']
+ for exp in expected:
+ if name == exp['opt_name']:
+ val = exp['opt_value']
+ break
+ self.assertEqual(opt['opt_value'], val)
+
+ def test_create_port_with_extradhcpopts(self):
+ opt_dict = [{'opt_name': 'bootfile-name',
+ 'opt_value': 'pxelinux.0'},
+ {'opt_name': 'server-ip-address',
+ 'opt_value': '123.123.123.456'},
+ {'opt_name': 'tftp-server',
+ 'opt_value': '123.123.123.123'}]
+
+ params = {edo_ext.EXTRADHCPOPTS: opt_dict,
+ 'arg_list': (edo_ext.EXTRADHCPOPTS,)}
+
+ with self.port(**params) as port:
+ self._check_opts(opt_dict,
+ port['port'][edo_ext.EXTRADHCPOPTS])
+
+ def test_update_port_with_extradhcpopts_with_same(self):
+ opt_dict = [{'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'}]
+ new_opts = opt_dict[:]
+ for i in new_opts:
+ if i['opt_name'] == upd_opts[0]['opt_name']:
+ i['opt_value'] = upd_opts[0]['opt_value']
+ break
+
+ params = {edo_ext.EXTRADHCPOPTS: opt_dict,
+ '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(new_opts,
+ port['port'][edo_ext.EXTRADHCPOPTS])
+
+ def test_update_port_with_extradhcpopts(self):
+ opt_dict = [{'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'}]
+ new_opts = copy.deepcopy(opt_dict)
+ for i in new_opts:
+ if i['opt_name'] == upd_opts[0]['opt_name']:
+ i['opt_value'] = upd_opts[0]['opt_value']
+ break
+
+ params = {edo_ext.EXTRADHCPOPTS: opt_dict,
+ '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(new_opts,
+ port['port'][edo_ext.EXTRADHCPOPTS])
+
+ def test_update_port_with_extradhcpopt1(self):
+ opt_dict = [{'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'}]
+ new_opts = copy.deepcopy(opt_dict)
+ new_opts.append(upd_opts[0])
+
+ params = {edo_ext.EXTRADHCPOPTS: opt_dict,
+ '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(new_opts,
+ port['port'][edo_ext.EXTRADHCPOPTS])
+
+ def test_update_port_adding_extradhcpopts(self):
+ opt_dict = [{'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_dict}}
+
+ req = self.new_update_request('ports', update_port,
+ port['port']['id'])
+ port = self.deserialize('json', req.get_response(self.api))
+ self._check_opts(opt_dict,
+ port['port'][edo_ext.EXTRADHCPOPTS])
diff --git a/neutron/tests/unit/test_linux_dhcp.py b/neutron/tests/unit/test_linux_dhcp.py
index 6be5c954c1..34f16267fa 100644
--- a/neutron/tests/unit/test_linux_dhcp.py
+++ b/neutron/tests/unit/test_linux_dhcp.py
@@ -23,20 +23,34 @@ from oslo.config import cfg
from neutron.agent.common import config
from neutron.agent.linux import dhcp
from neutron.common import config as base_config
+from neutron.openstack.common import log as logging
from neutron.tests import base
+LOG = logging.getLogger(__name__)
+
class FakeIPAllocation:
def __init__(self, address):
self.ip_address = address
+class DhcpOpt(object):
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
+
+ def __str__(self):
+ return str(self.__dict__)
+
+
class FakePort1:
id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
admin_state_up = True
fixed_ips = [FakeIPAllocation('192.168.0.2')]
mac_address = '00:00:80:aa:bb:cc'
+ def __init__(self):
+ self.extra_dhcp_opts = []
+
class FakePort2:
id = 'ffffffff-ffff-ffff-ffff-ffffffffffff'
@@ -44,6 +58,9 @@ class FakePort2:
fixed_ips = [FakeIPAllocation('fdca:3ba5:a17a:4ba3::2')]
mac_address = '00:00:f3:aa:bb:cc'
+ def __init__(self):
+ self.extra_dhcp_opts = []
+
class FakePort3:
id = '44444444-4444-4444-4444-444444444444'
@@ -52,6 +69,9 @@ class FakePort3:
FakeIPAllocation('fdca:3ba5:a17a:4ba3::3')]
mac_address = '00:00:0f:aa:bb:cc'
+ def __init__(self):
+ self.extra_dhcp_opts = []
+
class FakeV4HostRoute:
destination = '20.0.0.1/24'
@@ -157,6 +177,103 @@ class FakeV4NoGatewayNetwork:
ports = [FakePort1()]
+class FakeDualV4Pxe3Ports:
+ id = 'cccccccc-cccc-cccc-cccc-cccccccccccc'
+ subnets = [FakeV4Subnet(), FakeV4SubnetNoDHCP()]
+ ports = [FakePort1(), FakePort2(), FakePort3()]
+ namespace = 'qdhcp-ns'
+
+ def __init__(self, port_detail="portsSame"):
+ if port_detail == "portsSame":
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ self.ports[1].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.1.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.1.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux2.0')]
+ self.ports[2].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.1.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.1.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux3.0')]
+ else:
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ self.ports[1].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.5'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.5'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux2.0')]
+ self.ports[2].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.7'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.7'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux3.0')]
+
+
+class FakeV4NetworkPxe2Ports:
+ id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
+ subnets = [FakeV4Subnet()]
+ ports = [FakePort1(), FakePort2()]
+ namespace = 'qdhcp-ns'
+
+ def __init__(self, port_detail="portsSame"):
+ if port_detail == "portsSame":
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ self.ports[1].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ else:
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ self.ports[1].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.5'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.5'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+
+
+class FakeV4NetworkPxe3Ports:
+ id = 'dddddddd-dddd-dddd-dddd-dddddddddddd'
+ subnets = [FakeV4Subnet()]
+ ports = [FakePort1(), FakePort2(), FakePort3()]
+ namespace = 'qdhcp-ns'
+
+ def __init__(self, port_detail="portsSame"):
+ if port_detail == "portsSame":
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ self.ports[1].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.1.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.1.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ self.ports[2].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.1.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.1.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ else:
+ self.ports[0].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.3'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.2'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux.0')]
+ self.ports[1].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.5'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.5'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux2.0')]
+ self.ports[2].extra_dhcp_opts = [
+ DhcpOpt(opt_name='tftp-server', opt_value='192.168.0.7'),
+ DhcpOpt(opt_name='server-ip-address', opt_value='192.168.0.7'),
+ DhcpOpt(opt_name='bootfile-name', opt_value='pxelinux3.0')]
+
+
class LocalChild(dhcp.DhcpLocalProcess):
PORTS = {4: [4], 6: [6]}
@@ -588,6 +705,101 @@ tag:tag0,option:router""".lstrip()
self.execute.assert_called_once_with(exp_args, root_helper='sudo',
check_exit_code=True)
+ def test_output_opts_file_pxe_2port_1net(self):
+ expected = """
+tag:tag0,option:dns-server,8.8.8.8
+tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1
+tag:tag0,249,20.0.0.1/24,20.0.0.1
+tag:tag0,option:router,192.168.0.1
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:tftp-server,192.168.0.3
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:server-ip-address,192.168.0.2
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:bootfile-name,pxelinux.0
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:tftp-server,192.168.0.3
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:server-ip-address,192.168.0.2
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:bootfile-name,pxelinux.0"""
+ expected = expected.lstrip()
+
+ with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
+ conf_fn.return_value = '/foo/opts'
+ fp = FakeV4NetworkPxe2Ports()
+ dm = dhcp.Dnsmasq(self.conf, fp, version=float(2.59))
+ dm._output_opts_file()
+
+ self.safe.assert_called_once_with('/foo/opts', expected)
+
+ def test_output_opts_file_pxe_2port_1net_diff_details(self):
+ expected = """
+tag:tag0,option:dns-server,8.8.8.8
+tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1
+tag:tag0,249,20.0.0.1/24,20.0.0.1
+tag:tag0,option:router,192.168.0.1
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:tftp-server,192.168.0.3
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:server-ip-address,192.168.0.2
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:bootfile-name,pxelinux.0
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:tftp-server,192.168.0.5
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:server-ip-address,192.168.0.5
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:bootfile-name,pxelinux.0"""
+ expected = expected.lstrip()
+
+ with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
+ conf_fn.return_value = '/foo/opts'
+ dm = dhcp.Dnsmasq(self.conf, FakeV4NetworkPxe2Ports("portsDiff"),
+ version=float(2.59))
+ dm._output_opts_file()
+
+ self.safe.assert_called_once_with('/foo/opts', expected)
+
+ def test_output_opts_file_pxe_3port_1net_diff_details(self):
+ expected = """
+tag:tag0,option:dns-server,8.8.8.8
+tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1
+tag:tag0,249,20.0.0.1/24,20.0.0.1
+tag:tag0,option:router,192.168.0.1
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:tftp-server,192.168.0.3
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:server-ip-address,192.168.0.2
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:bootfile-name,pxelinux.0
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:tftp-server,192.168.0.5
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:server-ip-address,192.168.0.5
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:bootfile-name,pxelinux2.0
+tag:44444444-4444-4444-4444-444444444444,option:tftp-server,192.168.0.7
+tag:44444444-4444-4444-4444-444444444444,option:server-ip-address,192.168.0.7
+tag:44444444-4444-4444-4444-444444444444,option:bootfile-name,pxelinux3.0"""
+ expected = expected.lstrip()
+
+ with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
+ conf_fn.return_value = '/foo/opts'
+ dm = dhcp.Dnsmasq(self.conf,
+ FakeV4NetworkPxe3Ports("portsDifferent"),
+ version=float(2.59))
+ dm._output_opts_file()
+
+ self.safe.assert_called_once_with('/foo/opts', expected)
+
+ def test_output_opts_file_pxe_3port_2net(self):
+ expected = """
+tag:tag0,option:dns-server,8.8.8.8
+tag:tag0,option:classless-static-route,20.0.0.1/24,20.0.0.1
+tag:tag0,249,20.0.0.1/24,20.0.0.1
+tag:tag0,option:router,192.168.0.1
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:tftp-server,192.168.0.3
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:server-ip-address,192.168.0.2
+tag:eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee,option:bootfile-name,pxelinux.0
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:tftp-server,192.168.1.3
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:server-ip-address,192.168.1.2
+tag:ffffffff-ffff-ffff-ffff-ffffffffffff,option:bootfile-name,pxelinux2.0
+tag:44444444-4444-4444-4444-444444444444,option:tftp-server,192.168.1.3
+tag:44444444-4444-4444-4444-444444444444,option:server-ip-address,192.168.1.2
+tag:44444444-4444-4444-4444-444444444444,option:bootfile-name,pxelinux3.0"""
+ expected = expected.lstrip()
+
+ with mock.patch.object(dhcp.Dnsmasq, 'get_conf_file_name') as conf_fn:
+ conf_fn.return_value = '/foo/opts'
+ dm = dhcp.Dnsmasq(self.conf, FakeDualV4Pxe3Ports(),
+ version=float(2.59))
+ dm._output_opts_file()
+
+ self.safe.assert_called_once_with('/foo/opts', expected)
+
def test_reload_allocations(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,'