bp: pxeboot-port, provide pxeboot on ports

Teach neutron how to manage PXE boot.

Allow pxe boot parameters to be specified when creating a network port.

Implements bp:pxeboot-ports

Change-Id: I45fe7a16bc6c5975a765dd6a065558b9ba702e5b
This commit is contained in:
dekehn 2013-08-30 20:15:12 -06:00
parent 75d0e2356c
commit c1f5214613
7 changed files with 705 additions and 7 deletions

View File

@ -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):

View File

@ -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])

View File

@ -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 ###

View File

@ -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 {}

View File

@ -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']:

View File

@ -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])

View File

@ -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,'