iotronic/iotronic/common/pxe_utils.py

286 lines
9.9 KiB
Python

#
# 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 oslo_log import log as logging
from iotronic.common import dhcp_factory
from iotronic.common import exception
from iotronic.common.i18n import _
from iotronic.common import utils
from iotronic.drivers.modules import deploy_utils
from iotronic.drivers import utils as driver_utils
from iotronic.openstack.common import fileutils
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
PXE_CFG_DIR_NAME = 'pxelinux.cfg'
def get_root_dir():
"""Returns the directory where the config files and images will live."""
if CONF.pxe.ipxe_enabled:
return CONF.pxe.http_root
else:
return CONF.pxe.tftp_root
def _ensure_config_dirs_exist(node_uuid):
"""Ensure that the node's and PXE configuration directories exist.
:param node_uuid: the UUID of the node.
"""
root_dir = get_root_dir()
fileutils.ensure_tree(os.path.join(root_dir, node_uuid))
fileutils.ensure_tree(os.path.join(root_dir, PXE_CFG_DIR_NAME))
def _build_pxe_config(pxe_options, template):
"""Build the PXE boot configuration file.
This method builds the PXE boot configuration file by rendering the
template with the given parameters.
:param pxe_options: A dict of values to set on the configuration file.
:param template: The PXE configuration template.
:returns: A formatted string with the file content.
"""
tmpl_path, tmpl_file = os.path.split(template)
env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
template = env.get_template(tmpl_file)
return template.render({'pxe_options': pxe_options,
'ROOT': '{{ ROOT }}',
'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}',
})
def _link_mac_pxe_configs(task):
"""Link each MAC address with the PXE configuration file.
:param task: A TaskManager instance.
"""
def create_link(mac_path):
utils.unlink_without_raise(mac_path)
utils.create_link_without_raise(pxe_config_file_path, mac_path)
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
for mac in driver_utils.get_node_mac_addresses(task):
create_link(_get_pxe_mac_path(mac))
# TODO(lucasagomes): Backward compatibility with :hexraw,
# to be removed in M.
# see: https://bugs.launchpad.net/iotronic/+bug/1441710
if CONF.pxe.ipxe_enabled:
create_link(_get_pxe_mac_path(mac, delimiter=''))
def _link_ip_address_pxe_configs(task):
"""Link each IP address with the PXE configuration file.
:param task: A TaskManager instance.
:raises: FailedToGetIPAddressOnPort
:raises: InvalidIPv4Address
"""
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
api = dhcp_factory.DHCPFactory().provider
ip_addrs = api.get_ip_addresses(task)
if not ip_addrs:
raise exception.FailedToGetIPAddressOnPort(_(
"Failed to get IP address for any port on node %s.") %
task.node.uuid)
for port_ip_address in ip_addrs:
ip_address_path = _get_pxe_ip_address_path(port_ip_address)
utils.unlink_without_raise(ip_address_path)
utils.create_link_without_raise(pxe_config_file_path,
ip_address_path)
def _get_pxe_mac_path(mac, delimiter=None):
"""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.
:param delimiter: The MAC address delimiter. Defaults to dash ('-').
:returns: the path to the config file.
"""
if delimiter is None:
delimiter = '-'
mac_file_name = mac.replace(':', delimiter).lower()
if not CONF.pxe.ipxe_enabled:
mac_file_name = '01-' + mac_file_name
return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name)
def _get_pxe_ip_address_path(ip_address):
"""Convert an ipv4 address into a PXE config file name.
:param ip_address: A valid IPv4 address string in the format 'n.n.n.n'.
:returns: the path to the config file.
"""
ip = ip_address.split('.')
hex_ip = '{0:02X}{1:02X}{2:02X}{3:02X}'.format(*map(int, ip))
return os.path.join(
CONF.pxe.tftp_root, hex_ip + ".conf"
)
def get_deploy_kr_info(node_uuid, driver_info):
"""Get href and tftp path for deploy kernel and ramdisk.
Note: driver_info should be validated outside of this method.
"""
root_dir = get_root_dir()
image_info = {}
for label in ('deploy_kernel', 'deploy_ramdisk'):
image_info[label] = (
str(driver_info[label]),
os.path.join(root_dir, node_uuid, label)
)
return image_info
def get_pxe_config_file_path(node_uuid):
"""Generate the path for the node's PXE configuration file.
:param node_uuid: the UUID of the node.
:returns: The path to the node's PXE configuration file.
"""
return os.path.join(get_root_dir(), node_uuid, 'config')
def create_pxe_config(task, pxe_options, template=None):
"""Generate PXE configuration file and MAC address links for it.
This method will generate the PXE configuration file for the task's
node under a directory named with the UUID of that node. For each
MAC address (port) of that node, a symlink for the configuration file
will be created under the PXE configuration directory, so regardless
of which port boots first they'll get the same PXE configuration.
:param task: A TaskManager instance.
:param pxe_options: A dictionary with the PXE configuration
parameters.
:param template: The PXE configuration template. If no template is
given the CONF.pxe.pxe_config_template will be used.
"""
LOG.debug("Building PXE config for node %s", task.node.uuid)
if template is None:
template = CONF.pxe.pxe_config_template
_ensure_config_dirs_exist(task.node.uuid)
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
pxe_config = _build_pxe_config(pxe_options, template)
utils.write_to_file(pxe_config_file_path, pxe_config)
if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
_link_ip_address_pxe_configs(task)
else:
_link_mac_pxe_configs(task)
def clean_up_pxe_config(task):
"""Clean up the TFTP environment for the task's node.
:param task: A TaskManager instance.
"""
LOG.debug("Cleaning up PXE config for node %s", task.node.uuid)
if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
api = dhcp_factory.DHCPFactory().provider
ip_addresses = api.get_ip_addresses(task)
if not ip_addresses:
return
for port_ip_address in ip_addresses:
try:
ip_address_path = _get_pxe_ip_address_path(port_ip_address)
except exception.InvalidIPv4Address:
continue
utils.unlink_without_raise(ip_address_path)
else:
for mac in driver_utils.get_node_mac_addresses(task):
utils.unlink_without_raise(_get_pxe_mac_path(mac))
# TODO(lucasagomes): Backward compatibility with :hexraw,
# to be removed in M.
# see: https://bugs.launchpad.net/iotronic/+bug/1441710
if CONF.pxe.ipxe_enabled:
utils.unlink_without_raise(_get_pxe_mac_path(mac,
delimiter=''))
utils.rmtree_without_raise(os.path.join(get_root_dir(),
task.node.uuid))
def dhcp_options_for_instance(task):
"""Retrieves the DHCP PXE boot options.
:param task: A TaskManager instance.
"""
dhcp_opts = []
if CONF.pxe.ipxe_enabled:
script_name = os.path.basename(CONF.pxe.ipxe_boot_script)
ipxe_script_url = '/'.join([CONF.pxe.http_url, script_name])
dhcp_provider_name = dhcp_factory.CONF.dhcp.dhcp_provider
# if the request comes from dumb firmware send them the iPXE
# boot image.
if dhcp_provider_name == 'neutron':
# Neutron use dnsmasq as default DHCP agent, add extra config
# to neutron "dhcp-match=set:ipxe,175" and use below option
dhcp_opts.append({'opt_name': 'tag:!ipxe,bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name})
dhcp_opts.append({'opt_name': 'tag:ipxe,bootfile-name',
'opt_value': ipxe_script_url})
else:
# !175 == non-iPXE.
# http://ipxe.org/howto/dhcpd#ipxe-specific_options
dhcp_opts.append({'opt_name': '!175,bootfile-name',
'opt_value': CONF.pxe.pxe_bootfile_name})
dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': ipxe_script_url})
else:
if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
boot_file = CONF.pxe.uefi_pxe_bootfile_name
else:
boot_file = CONF.pxe.pxe_bootfile_name
dhcp_opts.append({'opt_name': 'bootfile-name',
'opt_value': boot_file})
dhcp_opts.append({'opt_name': 'server-ip-address',
'opt_value': CONF.pxe.tftp_server})
dhcp_opts.append({'opt_name': 'tftp-server',
'opt_value': CONF.pxe.tftp_server})
return dhcp_opts