Skip to content

Commit

Permalink
Factoring out PXE and TFTP functions
Browse files Browse the repository at this point in the history
Multiple drivers need to support neutron and Ironic's PXE and TFTP
server. Factoring them out into a common file will make that
transition possible. This moves all the generic calls into neutron
and reconfigures the PXE driver to work with the new functions.

Change-Id: I181ffabe673636b628329592e623e6425da54d6d
  • Loading branch information
Josh Gachnang authored and jimrollenhagen committed Jun 14, 2014
1 parent 3a2582a commit 5f5ec56
Show file tree
Hide file tree
Showing 7 changed files with 526 additions and 291 deletions.
21 changes: 14 additions & 7 deletions etc/ironic/ironic.conf.sample
Expand Up @@ -904,13 +904,6 @@
# is created. (string value)
#default_ephemeral_format=ext4

# IP address of Ironic compute node's tftp server. (string
# value)
#tftp_server=$my_ip

# Ironic compute node's tftp root path. (string value)
#tftp_root=/tftpboot

# Directory where images are stored on disk. (string value)
#images_path=/var/lib/ironic/images/

Expand Down Expand Up @@ -958,3 +951,17 @@
#libvirt_uri=qemu:///system


[tftp]

#
# Options defined in ironic.common.tftp
#

# IP address of Ironic compute node's tftp server. (string
# value)
#tftp_server=$my_ip

# Ironic compute node's tftp root path. (string value)
#tftp_root=/tftpboot


58 changes: 56 additions & 2 deletions ironic/common/neutron.py
Expand Up @@ -21,6 +21,7 @@
from ironic.api import acl
from ironic.common import exception
from ironic.common import keystone
from ironic.common import tftp
from ironic.openstack.common import log as logging


Expand All @@ -30,10 +31,12 @@
help='URL for connecting to neutron.'),
cfg.IntOpt('url_timeout',
default=30,
help='Timeout value for connecting to neutron in seconds.'),
]
help='Timeout value for connecting to neutron in seconds.')
]


CONF = cfg.CONF
CONF.import_opt('my_ip', 'ironic.netconf')
CONF.register_opts(neutron_opts, group='neutron')
acl.register_opts(CONF)
LOG = logging.getLogger(__name__)
Expand Down Expand Up @@ -105,3 +108,54 @@ def update_port_address(self, port_id, address):
LOG.exception(_("Failed to update MAC address on Neutron port %s."
), port_id)
raise exception.FailedToUpdateMacOnPort(port_id=port_id)


def get_node_vif_ids(task):
"""Get all Neutron VIF ids for a node.
This function does not handle multi node operations.
:param task: a TaskManager instance.
:returns: A dict of the Node's port UUIDs and their associated VIFs
"""
port_vifs = {}
for port in task.ports:
vif = port.extra.get('vif_port_id')
if vif:
port_vifs[port.uuid] = vif
return port_vifs


def update_neutron(task, pxe_bootfile_name):
"""Send or update the DHCP BOOT options to Neutron for this node."""
options = tftp.dhcp_options_for_instance(pxe_bootfile_name)
vifs = get_node_vif_ids(task)
if not vifs:
LOG.warning(_("No VIFs found for node %(node)s when attempting to "
"update Neutron DHCP BOOT options."),
{'node': task.node.uuid})
return

# TODO(deva): decouple instantiation of NeutronAPI from task.context.
# Try to use the user's task.context.auth_token, but if it
# is not present, fall back to a server-generated context.
# We don't need to recreate this in every method call.
api = NeutronAPI(task.context)
failures = []
for port_id, port_vif in vifs.iteritems():
try:
api.update_port_dhcp_opts(port_vif, options)
except exception.FailedToUpdateDHCPOptOnPort:
failures.append(port_id)

if failures:
if len(failures) == len(vifs):
raise exception.FailedToUpdateDHCPOptOnPort(_(
"Failed to set DHCP BOOT options for any port on node %s.") %
task.node.uuid)
else:
LOG.warning(_("Some errors were encountered when updating the "
"DHCP BOOT options for node %(node)s on the "
"following ports: %(ports)s."),
{'node': task.node.uuid, 'ports': failures})
125 changes: 125 additions & 0 deletions ironic/common/tftp.py
@@ -0,0 +1,125 @@
#
# Copyright 2014 Rackspace, Inc
# 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.

import os

import jinja2
from oslo.config import cfg

from ironic.common import utils
from ironic.drivers import utils as driver_utils
from ironic.openstack.common import fileutils
from ironic.openstack.common import log as logging


tftp_opts = [
cfg.StrOpt('tftp_server',
default='$my_ip',
help='IP address of Ironic compute node\'s tftp server.',
deprecated_group='pxe'),
cfg.StrOpt('tftp_root',
default='/tftpboot',
help='Ironic compute node\'s tftp root path.',
deprecated_group='pxe')
]

CONF = cfg.CONF
CONF.register_opts(tftp_opts, group='tftp')

LOG = logging.getLogger(__name__)


def create_pxe_config(task, pxe_options, pxe_config_template):
"""Generate PXE configuration file and MAC symlinks for it."""
node = task.node
fileutils.ensure_tree(os.path.join(CONF.tftp.tftp_root,
node.uuid))
fileutils.ensure_tree(os.path.join(CONF.tftp.tftp_root,
'pxelinux.cfg'))

pxe_config_file_path = get_pxe_config_file_path(node.uuid)
pxe_config = build_pxe_config(node, pxe_options, pxe_config_template)
utils.write_to_file(pxe_config_file_path, pxe_config)
_write_mac_pxe_configs(task)


def clean_up_pxe_config(task):
"""Clean up the TFTP environment for the task's node."""
node = task.node

utils.unlink_without_raise(get_pxe_config_file_path(node.uuid))
for port in driver_utils.get_node_mac_addresses(task):
utils.unlink_without_raise(get_pxe_mac_path(port))

utils.rmtree_without_raise(os.path.join(CONF.tftp.tftp_root, node.uuid))


def _write_mac_pxe_configs(task):
"""Create a file in the PXE config directory for each MAC so regardless
of which port boots first, they'll get the same PXE config.
"""
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
for port in driver_utils.get_node_mac_addresses(task):
mac_path = get_pxe_mac_path(port)
utils.unlink_without_raise(mac_path)
utils.create_link_without_raise(pxe_config_file_path, mac_path)


def build_pxe_config(node, pxe_options, pxe_config_template):
"""Build the PXE config file for a node
This method builds the PXE boot configuration file for a node,
given all the required parameters.
:param pxe_options: A dict of values to set on the configuration file
:returns: A formatted string with the file content.
"""
LOG.debug("Building PXE config for deployment %s."), node['id']

tmpl_path, tmpl_file = os.path.split(pxe_config_template)
env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
template = env.get_template(tmpl_file)
return template.render({'pxe_options': pxe_options,
'ROOT': '{{ ROOT }}'})


def get_pxe_mac_path(mac):
"""Convert a MAC address into a PXE config file name.
:param mac: A mac address string in the format xx:xx:xx:xx:xx:xx.
:returns: the path to the config file.
"""
return os.path.join(
CONF.tftp.tftp_root,
'pxelinux.cfg',
"01-" + mac.replace(":", "-").lower()
)


def get_pxe_config_file_path(node_uuid):
"""Generate the path for an instances PXE config file."""
return os.path.join(CONF.tftp.tftp_root, node_uuid, 'config')


def dhcp_options_for_instance(pxe_bootfile_name):
"""Retrives the DHCP PXE boot options."""
return [{'opt_name': 'bootfile-name',
'opt_value': pxe_bootfile_name},
{'opt_name': 'server-ip-address',
'opt_value': CONF.tftp.tftp_server},
{'opt_name': 'tftp-server',
'opt_value': CONF.tftp.tftp_server}
]

0 comments on commit 5f5ec56

Please sign in to comment.