345 lines
13 KiB
Python
345 lines
13 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
|
|
|
|
from ironic_lib import utils as ironic_utils
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import fileutils
|
|
|
|
from ironic.common import dhcp_factory
|
|
from ironic.common import exception
|
|
from ironic.common.i18n import _
|
|
from ironic.common import utils
|
|
from ironic.drivers.modules import deploy_utils
|
|
|
|
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.deploy.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()
|
|
node_dir = os.path.join(root_dir, node_uuid)
|
|
pxe_dir = os.path.join(root_dir, PXE_CFG_DIR_NAME)
|
|
# NOTE: We should only change the permissions if the folder
|
|
# does not exist. i.e. if defined, an operator could have
|
|
# already created it and placed specific ACLs upon the folder
|
|
# which may not recurse downward.
|
|
for directory in (node_dir, pxe_dir):
|
|
if not os.path.isdir(directory):
|
|
fileutils.ensure_tree(directory)
|
|
if CONF.pxe.dir_permission:
|
|
os.chmod(directory, CONF.pxe.dir_permission)
|
|
|
|
|
|
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):
|
|
ironic_utils.unlink_without_raise(mac_path)
|
|
relative_source_path = os.path.relpath(
|
|
pxe_config_file_path, os.path.dirname(mac_path))
|
|
utils.create_link_without_raise(relative_source_path, mac_path)
|
|
|
|
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
|
|
for port in task.ports:
|
|
client_id = port.extra.get('client-id')
|
|
create_link(_get_pxe_mac_path(port.address, client_id=client_id))
|
|
|
|
|
|
def _link_ip_address_pxe_configs(task, hex_form):
|
|
"""Link each IP address with the PXE configuration file.
|
|
|
|
:param task: A TaskManager instance.
|
|
:param hex_form: Boolean value indicating if the conf file name should be
|
|
hexadecimal equivalent of supplied ipv4 address.
|
|
: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,
|
|
hex_form)
|
|
ironic_utils.unlink_without_raise(ip_address_path)
|
|
relative_source_path = os.path.relpath(
|
|
pxe_config_file_path, os.path.dirname(ip_address_path))
|
|
utils.create_link_without_raise(relative_source_path,
|
|
ip_address_path)
|
|
|
|
|
|
def _get_pxe_mac_path(mac, delimiter='-', client_id=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 ('-').
|
|
:param client_id: client_id indicate InfiniBand port.
|
|
Defaults is None (Ethernet)
|
|
:returns: the path to the config file.
|
|
|
|
"""
|
|
mac_file_name = mac.replace(':', delimiter).lower()
|
|
if not CONF.pxe.ipxe_enabled:
|
|
hw_type = '01-'
|
|
if client_id:
|
|
hw_type = '20-'
|
|
mac_file_name = hw_type + 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, hex_form):
|
|
"""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'.
|
|
:param hex_form: Boolean value indicating if the conf file name should be
|
|
hexadecimal equivalent of supplied ipv4 address.
|
|
:returns: the path to the config file.
|
|
|
|
"""
|
|
# elilo bootloader needs hex based config file name.
|
|
if hex_form:
|
|
ip = ip_address.split('.')
|
|
ip_address = '{0:02X}{1:02X}{2:02X}{3:02X}'.format(*map(int, ip))
|
|
|
|
# grub2 bootloader needs ip based config file name.
|
|
return os.path.join(
|
|
CONF.pxe.tftp_root, ip_address + ".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 or DHCP IP 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.
|
|
If elilo is the bootloader in use, then its configuration file will
|
|
be created based on hex form of DHCP IP address.
|
|
If grub2 bootloader is in use, then its configuration will be created
|
|
based on DHCP IP address in the form nn.nn.nn.nn.
|
|
|
|
: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 node specific template will be used.
|
|
|
|
"""
|
|
LOG.debug("Building PXE config for node %s", task.node.uuid)
|
|
|
|
if template is None:
|
|
template = deploy_utils.get_pxe_config_template(task.node)
|
|
|
|
_ensure_config_dirs_exist(task.node.uuid)
|
|
|
|
pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
|
|
is_uefi_boot_mode = (deploy_utils.get_boot_mode_for_deploy(task.node) ==
|
|
'uefi')
|
|
|
|
# grub bootloader panics with '{}' around any of its tags in its
|
|
# config file. To overcome that 'ROOT' and 'DISK_IDENTIFIER' are enclosed
|
|
# with '(' and ')' in uefi boot mode.
|
|
# These changes do not have any impact on elilo bootloader.
|
|
hex_form = True
|
|
if is_uefi_boot_mode and utils.is_regex_string_in_file(template,
|
|
'^menuentry'):
|
|
hex_form = False
|
|
pxe_config_root_tag = '(( ROOT ))'
|
|
pxe_config_disk_ident = '(( DISK_IDENTIFIER ))'
|
|
else:
|
|
# TODO(stendulker): We should use '(' ')' as the delimiters for all our
|
|
# config files so that we do not need special handling for each of the
|
|
# bootloaders. Should be removed once the Mitaka release starts.
|
|
pxe_config_root_tag = '{{ ROOT }}'
|
|
pxe_config_disk_ident = '{{ DISK_IDENTIFIER }}'
|
|
|
|
params = {'pxe_options': pxe_options,
|
|
'ROOT': pxe_config_root_tag,
|
|
'DISK_IDENTIFIER': pxe_config_disk_ident}
|
|
|
|
pxe_config = utils.render_template(template, params)
|
|
utils.write_to_file(pxe_config_file_path, pxe_config)
|
|
|
|
if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled:
|
|
_link_ip_address_pxe_configs(task, hex_form)
|
|
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)
|
|
|
|
is_uefi_boot_mode = (deploy_utils.get_boot_mode_for_deploy(task.node) ==
|
|
'uefi')
|
|
if is_uefi_boot_mode and not CONF.pxe.ipxe_enabled:
|
|
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:
|
|
# Get xx.xx.xx.xx based grub config file
|
|
ip_address_path = _get_pxe_ip_address_path(port_ip_address,
|
|
False)
|
|
# Get 0AOAOAOA based elilo config file
|
|
hex_ip_path = _get_pxe_ip_address_path(port_ip_address,
|
|
True)
|
|
except exception.InvalidIPv4Address:
|
|
continue
|
|
# Cleaning up config files created for grub2.
|
|
ironic_utils.unlink_without_raise(ip_address_path)
|
|
# Cleaning up config files created for elilo.
|
|
ironic_utils.unlink_without_raise(hex_ip_path)
|
|
else:
|
|
for port in task.ports:
|
|
client_id = port.extra.get('client-id')
|
|
ironic_utils.unlink_without_raise(
|
|
_get_pxe_mac_path(port.address, client_id=client_id))
|
|
|
|
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 = []
|
|
|
|
boot_file = deploy_utils.get_pxe_boot_file(task.node)
|
|
|
|
if CONF.pxe.ipxe_enabled:
|
|
script_name = os.path.basename(CONF.pxe.ipxe_boot_script)
|
|
ipxe_script_url = '/'.join([CONF.deploy.http_url, script_name])
|
|
dhcp_provider_name = 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': boot_file})
|
|
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': boot_file})
|
|
dhcp_opts.append({'opt_name': 'bootfile-name',
|
|
'opt_value': ipxe_script_url})
|
|
else:
|
|
dhcp_opts.append({'opt_name': 'bootfile-name',
|
|
'opt_value': boot_file})
|
|
# 210 == tftp server path-prefix or tftp root, will be used to find
|
|
# pxelinux.cfg directory. The pxelinux.0 loader infers this information
|
|
# from it's own path, but Petitboot needs it to be specified by this
|
|
# option since it doesn't use pxelinux.0 loader.
|
|
dhcp_opts.append({'opt_name': '210',
|
|
'opt_value': get_tftp_path_prefix()})
|
|
|
|
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})
|
|
|
|
# Append the IP version for all the configuration options
|
|
for opt in dhcp_opts:
|
|
opt.update({'ip_version': int(CONF.pxe.ip_version)})
|
|
|
|
return dhcp_opts
|
|
|
|
|
|
def get_tftp_path_prefix():
|
|
"""Adds trailing slash (if needed) necessary for path-prefix
|
|
|
|
:return: CONF.pxe.tftp_root ensured to have a trailing slash
|
|
"""
|
|
return os.path.join(CONF.pxe.tftp_root, '')
|
|
|
|
|
|
def get_path_relative_to_tftp_root(file_path):
|
|
"""Return file relative path to CONF.pxe.tftp_root
|
|
|
|
:param file_path: full file path to be made relative path.
|
|
:returns: The path relative to CONF.pxe.tftp_root
|
|
"""
|
|
return os.path.relpath(file_path, get_tftp_path_prefix())
|