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:
parent
75d0e2356c
commit
c1f5214613
@ -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):
|
||||
|
131
neutron/db/extradhcpopt_db.py
Normal file
131
neutron/db/extradhcpopt_db.py
Normal 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])
|
@ -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 ###
|
89
neutron/extensions/extra_dhcp_opt.py
Normal file
89
neutron/extensions/extra_dhcp_opt.py
Normal 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 {}
|
@ -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']:
|
||||
|
172
neutron/tests/unit/test_extension_extradhcpopts.py
Normal file
172
neutron/tests/unit/test_extension_extradhcpopts.py
Normal 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])
|
@ -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,'
|
||||
|
Loading…
Reference in New Issue
Block a user