diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py index 060ed3b1892..5257ca92f90 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 00000000000..abbccbac871 --- /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 /opts files, and each option's + tag will be referenced in the /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 00000000000..e44d7daeea4 --- /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 00000000000..39995ccbff1 --- /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 baeef5ef548..31312bf4ea7 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 00000000000..fbe583663a0 --- /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 6be5c954c14..34f16267fa5 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,'