From 16229d7c01a4c9b6db28e7ad27eed878be66096e Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Tue, 23 Sep 2014 10:06:41 +0000 Subject: Add nfs userland tools to devel systems It's handy to be able to start an NFS server when you want to. --- systems/devel-system-armv7-highbank.morph | 2 ++ systems/devel-system-armv7-versatile.morph | 2 ++ systems/devel-system-armv7-wandboard.morph | 2 ++ systems/devel-system-armv7b-highbank.morph | 2 ++ systems/devel-system-armv7lhf-highbank.morph | 2 ++ systems/devel-system-armv7lhf-jetson.morph | 2 ++ systems/devel-system-armv7lhf-wandboard.morph | 2 ++ systems/devel-system-ppc64-generic.morph | 2 ++ systems/devel-system-x86_32-generic.morph | 2 ++ systems/devel-system-x86_64-generic.morph | 2 ++ 10 files changed, 20 insertions(+) diff --git a/systems/devel-system-armv7-highbank.morph b/systems/devel-system-armv7-highbank.morph index 919c800e..91716fc8 100644 --- a/systems/devel-system-armv7-highbank.morph +++ b/systems/devel-system-armv7-highbank.morph @@ -24,6 +24,8 @@ strata: morph: strata/nodejs.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-armv7-versatile.morph b/systems/devel-system-armv7-versatile.morph index 984e0b78..8e911703 100644 --- a/systems/devel-system-armv7-versatile.morph +++ b/systems/devel-system-armv7-versatile.morph @@ -24,6 +24,8 @@ strata: morph: strata/nodejs.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-armv7-wandboard.morph b/systems/devel-system-armv7-wandboard.morph index 5bc5e449..973f05ea 100644 --- a/systems/devel-system-armv7-wandboard.morph +++ b/systems/devel-system-armv7-wandboard.morph @@ -24,6 +24,8 @@ strata: morph: strata/nodejs.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-armv7b-highbank.morph b/systems/devel-system-armv7b-highbank.morph index 0ffcd0c6..4f3944c2 100644 --- a/systems/devel-system-armv7b-highbank.morph +++ b/systems/devel-system-armv7b-highbank.morph @@ -20,6 +20,8 @@ strata: morph: strata/morph-utils.morph - name: openstack-clients morph: strata/openstack-clients.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-armv7lhf-highbank.morph b/systems/devel-system-armv7lhf-highbank.morph index 5fbae184..7049b1b0 100644 --- a/systems/devel-system-armv7lhf-highbank.morph +++ b/systems/devel-system-armv7lhf-highbank.morph @@ -25,6 +25,8 @@ strata: morph: strata/nodejs.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-armv7lhf-jetson.morph b/systems/devel-system-armv7lhf-jetson.morph index 7892056d..35976a70 100644 --- a/systems/devel-system-armv7lhf-jetson.morph +++ b/systems/devel-system-armv7lhf-jetson.morph @@ -19,6 +19,8 @@ strata: morph: strata/morph-utils.morph - name: openstack-clients morph: strata/openstack-clients.morph +- name: nfs + morph: strata/nfs.morph - name: bsp-jetson-devel morph: strata/bsp-jetson-devel.morph - name: nodejs diff --git a/systems/devel-system-armv7lhf-wandboard.morph b/systems/devel-system-armv7lhf-wandboard.morph index fb52e2ea..7c27bcdf 100644 --- a/systems/devel-system-armv7lhf-wandboard.morph +++ b/systems/devel-system-armv7lhf-wandboard.morph @@ -25,6 +25,8 @@ strata: morph: strata/nodejs.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-ppc64-generic.morph b/systems/devel-system-ppc64-generic.morph index 326ea8d4..16bc7fc3 100644 --- a/systems/devel-system-ppc64-generic.morph +++ b/systems/devel-system-ppc64-generic.morph @@ -22,6 +22,8 @@ strata: morph: strata/openstack-clients.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-x86_32-generic.morph b/systems/devel-system-x86_32-generic.morph index c8bb4e92..5afb6f72 100644 --- a/systems/devel-system-x86_32-generic.morph +++ b/systems/devel-system-x86_32-generic.morph @@ -26,6 +26,8 @@ strata: morph: strata/nodejs.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files diff --git a/systems/devel-system-x86_64-generic.morph b/systems/devel-system-x86_64-generic.morph index dcc39efb..969f32b6 100644 --- a/systems/devel-system-x86_64-generic.morph +++ b/systems/devel-system-x86_64-generic.morph @@ -26,6 +26,8 @@ strata: morph: strata/nodejs.morph - name: ruby morph: strata/ruby.morph +- name: nfs + morph: strata/nfs.morph configuration-extensions: - set-hostname - add-config-files -- cgit v1.2.1 From 57a83a083622b7cd4453878dc3b6fba9025f2512 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Mon, 6 Oct 2014 13:06:53 +0000 Subject: Make nfsboot-server architecture independent --- nfsboot-server.configure | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/nfsboot-server.configure b/nfsboot-server.configure index 9779c053..43df0bea 100755 --- a/nfsboot-server.configure +++ b/nfsboot-server.configure @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (C) 2013 Codethink Limited +# Copyright (C) 2013-2014 Codethink Limited # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,21 +24,30 @@ ROOT="$1" ########################################################################## -chroot "$ROOT" systemctl enable nfs-server.service +nfsboot_root=/srv/nfsboot +tftp_root="$nfsboot_root"/tftp +nfs_root="$nfsboot_root"/nfs +mkdir -p "$ROOT$tftp_root" "$ROOT$nfs_root" -mkdir -p "$ROOT/srv/nfsboot/tftp" "$ROOT/srv/nfsboot/nfs" - -cat >"$ROOT/usr/lib/systemd/system/nfsboot-tftp.service" < Date: Mon, 6 Oct 2014 13:07:10 +0000 Subject: Make nfsboot-server.configure install pxelinux.0 --- nfsboot-server.configure | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nfsboot-server.configure b/nfsboot-server.configure index 43df0bea..9fb48096 100755 --- a/nfsboot-server.configure +++ b/nfsboot-server.configure @@ -51,3 +51,8 @@ for prefix in / /usr; do fi done done + +pxelinux_file="$ROOT/usr/share/syslinux/pxelinux.0" +if [ -e "$pxelinux_file" ]; then + cp "$pxelinux_file" "$ROOT$tftp_root/pxelinux.0" +fi -- cgit v1.2.1 From 88b9119b4a9abee7402b0164e2ec8f287a4cda81 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Thu, 9 Oct 2014 12:49:46 +0000 Subject: Add VLAN configuration to kernels With VLANs it's possible to PXE boot hardware without needing to fiddle with the DHCP server, by starting your own PXE server on a VLAN, and configure the NIC of the target to start in that VLAN. --- strata/bsp-armv7-highbank/linux-armv7-highbank.morph | 2 ++ strata/bsp-armv7-versatile/linux-armv7-versatile.morph | 2 ++ strata/bsp-armv7b-highbank/linux-armv7b-highbank.morph | 2 ++ strata/bsp-armv7b-vexpress-tc2/linux-armv7b-vexpress-tc2.morph | 2 ++ strata/bsp-jetson-devel/linux-jetson-tk1.morph | 2 ++ strata/bsp-jetson-genivi/linux-jetson-tk1-genivi.morph | 2 ++ strata/bsp-wandboard/linux-armv7-wandboard.morph | 2 ++ strata/bsp-x86_32-generic/linux-x86-32-generic.morph | 2 ++ strata/bsp-x86_64-generic/linux-x86-64-generic.morph | 2 ++ 9 files changed, 18 insertions(+) diff --git a/strata/bsp-armv7-highbank/linux-armv7-highbank.morph b/strata/bsp-armv7-highbank/linux-armv7-highbank.morph index 58ad90b4..dfebf69c 100644 --- a/strata/bsp-armv7-highbank/linux-armv7-highbank.morph +++ b/strata/bsp-armv7-highbank/linux-armv7-highbank.morph @@ -37,6 +37,8 @@ configure-commands: - scripts/config -e TMPFS - scripts/config -e TMPFS_POSIX_ACL - scripts/config -e VFAT_FS +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make oldconfig build-commands: - make zImage $MAKEFLAGS diff --git a/strata/bsp-armv7-versatile/linux-armv7-versatile.morph b/strata/bsp-armv7-versatile/linux-armv7-versatile.morph index 9b03991b..1da640e7 100644 --- a/strata/bsp-armv7-versatile/linux-armv7-versatile.morph +++ b/strata/bsp-armv7-versatile/linux-armv7-versatile.morph @@ -17,6 +17,8 @@ configure-commands: - scripts/config -e SECCOMP - scripts/config -d DEBUG_STACK_TRACE - scripts/config -e FB_VESA +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make oldconfig build-commands: - make ARCH=arm zImage $MAKEFLAGS diff --git a/strata/bsp-armv7b-highbank/linux-armv7b-highbank.morph b/strata/bsp-armv7b-highbank/linux-armv7b-highbank.morph index e3601dc4..ea1a2d5f 100644 --- a/strata/bsp-armv7b-highbank/linux-armv7b-highbank.morph +++ b/strata/bsp-armv7b-highbank/linux-armv7b-highbank.morph @@ -39,6 +39,8 @@ configure-commands: - scripts/config -e EXT4_USE_FOR_EXT23 - scripts/config -e MSDOS_FS - scripts/config -e VFAT_FS +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make oldconfig build-commands: - make zImage $MAKEFLAGS diff --git a/strata/bsp-armv7b-vexpress-tc2/linux-armv7b-vexpress-tc2.morph b/strata/bsp-armv7b-vexpress-tc2/linux-armv7b-vexpress-tc2.morph index e187716d..ecf6b9bc 100644 --- a/strata/bsp-armv7b-vexpress-tc2/linux-armv7b-vexpress-tc2.morph +++ b/strata/bsp-armv7b-vexpress-tc2/linux-armv7b-vexpress-tc2.morph @@ -39,6 +39,8 @@ configure-commands: - scripts/config -e EXT4_USE_FOR_EXT23 - scripts/config -e MSDOS_FS - scripts/config -e VFAT_FS +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make oldconfig build-commands: - make zImage vexpress-v2p-ca15_a7.dtb diff --git a/strata/bsp-jetson-devel/linux-jetson-tk1.morph b/strata/bsp-jetson-devel/linux-jetson-tk1.morph index 3494f62b..31cb393f 100644 --- a/strata/bsp-jetson-devel/linux-jetson-tk1.morph +++ b/strata/bsp-jetson-devel/linux-jetson-tk1.morph @@ -44,6 +44,8 @@ configure-commands: - scripts/config -d DEBUG_STACK_TRACE - scripts/config -e NFSD - scripts/config -e NFSD_V3 +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make ARCH=arm oldconfig build-commands: - make $MAKEFLAGS ARCH=arm LOADADDR=0x80200000 zImage dtbs diff --git a/strata/bsp-jetson-genivi/linux-jetson-tk1-genivi.morph b/strata/bsp-jetson-genivi/linux-jetson-tk1-genivi.morph index e1c99473..c3dfe0f1 100644 --- a/strata/bsp-jetson-genivi/linux-jetson-tk1-genivi.morph +++ b/strata/bsp-jetson-genivi/linux-jetson-tk1-genivi.morph @@ -46,6 +46,8 @@ configure-commands: - scripts/config -e NFSD_V3 - scripts/config -e DRM_TEGRA_STAGING - scripts/config -m DRM_NOUVEAU +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make ARCH=arm oldconfig build-commands: - make $MAKEFLAGS ARCH=arm LOADADDR=0x80200000 zImage dtbs diff --git a/strata/bsp-wandboard/linux-armv7-wandboard.morph b/strata/bsp-wandboard/linux-armv7-wandboard.morph index 16929c9d..15317978 100644 --- a/strata/bsp-wandboard/linux-armv7-wandboard.morph +++ b/strata/bsp-wandboard/linux-armv7-wandboard.morph @@ -44,6 +44,8 @@ configure-commands: - scripts/config -d DEBUG_STACK_TRACE - scripts/config -e NFSD - scripts/config -e NFSD_V3 +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make oldconfig build-commands: - make $MAKEFLAGS ARCH=arm LOADADDR=0x10800000 uImage dtbs diff --git a/strata/bsp-x86_32-generic/linux-x86-32-generic.morph b/strata/bsp-x86_32-generic/linux-x86-32-generic.morph index 10bac0cd..593d9a65 100644 --- a/strata/bsp-x86_32-generic/linux-x86-32-generic.morph +++ b/strata/bsp-x86_32-generic/linux-x86-32-generic.morph @@ -78,6 +78,8 @@ configure-commands: - scripts/config -e FB_VESA - scripts/config -e HOTPLUG_PCI - scripts/config -e HOTPLUG_PCI_ACPI +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make oldconfig build-commands: - make $MAKEFLAGS diff --git a/strata/bsp-x86_64-generic/linux-x86-64-generic.morph b/strata/bsp-x86_64-generic/linux-x86-64-generic.morph index 4b18b537..fe0721cc 100644 --- a/strata/bsp-x86_64-generic/linux-x86-64-generic.morph +++ b/strata/bsp-x86_64-generic/linux-x86-64-generic.morph @@ -78,6 +78,8 @@ configure-commands: - scripts/config -e FB_VESA - scripts/config -e HOTPLUG_PCI - scripts/config -e HOTPLUG_PCI_ACPI +- scripts/config -e VLAN_8021Q +- scripts/config -e BRIDGE_VLAN_FILTERING - yes '' | make oldconfig build-commands: - make $MAKEFLAGS -- cgit v1.2.1 From f9e02689f1d9cdaaa1478187f490ca051a607fe4 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Thu, 9 Oct 2014 09:48:01 +0000 Subject: Add pxeboot write extension. It has 4 modes. 1. Specify PXEBOOT_DEPLOYER_INTERFACE and PXEBOOT_VLAN to configure the target to pxeboot on a vlan and spawn a dhcp, nfs and tftp server. This is potentially the fastest, since it doesn't need to copy data to other servers. 2. Specify PXEBOOT_DEPLOYER_INTERFACE without PXEBOOT_VLAN to configure do 1, but without creating the vlan interface. This assumes that you have exclusive access to the interface, such as if you're plugged in to the device directly, or your interface is vlanned by your infrastructure team. This is required if you are serving from a VM and bridging it to the correct network via macvtap. For this to work, you need to macvtap bridge to a pre-vlanned interface on your host machine. 3. Specify PXEBOOT_DEPLOYER_INTERFACE and PXEBOOT_CONFIG_TFTP_ADDRESS to put config on an existing tftp server, already configured by the dhcp server. This spawns a tftp server and configures the local nfs server, but doesn't do a dhcp server. This is useful if you have already got a dhcp server that serves PXE images. 4. Specify at least PXEBOOT_CONFIG_TFTP_ADDRESS and PXEBOOT_ROOTFS_RSYNC_ADDRESS to specify existing servers to copy config, kernels and the rootfs to. The mode detection can be overridden by specifying PXEBOOT_MODE. Mode 1 is `spawn-vlan`. Mode 2 is `spawn-novlan`, Mode 3 is `existing-dhcp` and Mode 4 is `existing-server`. --- pxeboot.check | 69 ++++++ pxeboot.write | 679 +++++++++++++++++++++++++++++++++++++++++++++++++++++ pxeboot.write.help | 123 ++++++++++ 3 files changed, 871 insertions(+) create mode 100755 pxeboot.check create mode 100755 pxeboot.write create mode 100644 pxeboot.write.help diff --git a/pxeboot.check b/pxeboot.check new file mode 100755 index 00000000..d7eb9b5c --- /dev/null +++ b/pxeboot.check @@ -0,0 +1,69 @@ +#!/usr/bin/python + +import itertools +import os +import sys +flatten = itertools.chain.from_iterable + +def powerset(iterable): + "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" + s = list(iterable) + return flatten(itertools.combinations(s, r) for r in range(len(s)+1)) + +valid_option_sets = frozenset(( + ('spawn-novlan', frozenset(('PXEBOOT_DEPLOYER_INTERFACE',))), + ('spawn-vlan', frozenset(('PXEBOOT_DEPLOYER_INTERFACE', 'PXEBOOT_VLAN'))), + ('existing-dhcp', frozenset(('PXEBOOT_DEPLOYER_INTERFACE', + 'PXEBOOT_CONFIG_TFTP_ADDRESS'))), + ('existing-server', frozenset(('PXEBOOT_CONFIG_TFTP_ADDRESS', + 'PXEBOOT_ROOTFS_RSYNC_ADDRESS'))), +)) +valid_modes = frozenset(mode for (mode, opt_set in valid_option_sets)) + + +def compute_matches(env): + complete_matches = set() + for mode, opt_set in valid_option_sets: + if all(k in env for k in opt_set): + complete_matches.add(opt_set) + return complete_matches +complete_matches = compute_matches(os.environ) + +def word_separate_options(options): + assert options + s = options.pop(-1) + if options: + s = '%s and %s' % (', '.join(options), s) + return s + + +valid_options = frozenset(flatten(opt_set for (mode, opt_set) + in valid_option_sets)) +matched_options = frozenset(o for o in valid_options) + if o in os.environ) +if not complete_matches: + addable_sets = frozenset(frozenset(os) - matched_options for os in + valid_options + if frozenset(os) - matched_options) + print('Please provide %s' % ' or '.join( + word_separate_options(list(opt_set)) + for opt_set in addable_sets if opt_set)) + sys.exit(1) +elif len(complete_matches) > 1: + removable_sets = frozenset(matched_options - frozenset(os) for os in + powerset(matched_options) + if len(compute_matches(os)) == 1) + print('Please unset %s' % ' or '.join( + word_separate_options(list(opt_set)) + for opt_set in removable_sets if opt_set)) + sys.exit(1) + +if 'PXEBOOT_MODE' in os.environ: + mode = os.environ['PXEBOOT_MODE'] +else: + mode, = (mode for (mode, opt_set) in valid_option_sets + if all(o in os.environ for o in opt_set)) + +if mode not in valid_modes: + print('%s is not a valid PXEBOOT_MODE' % mode) + sys.exit(1) diff --git a/pxeboot.write b/pxeboot.write new file mode 100755 index 00000000..e33da527 --- /dev/null +++ b/pxeboot.write @@ -0,0 +1,679 @@ +#!/usr/bin/env python + + +import collections +import contextlib +import errno +import itertools +import logging +import os +import select +import signal +import shutil +import socket +import string +import StringIO +import subprocess +import sys +import tempfile +import textwrap +import urlparse + +import cliapp + +import morphlib + + +def _int_to_quad_dot(i): + return '.'.join(( + str(i >> 24 & 0xff), + str(i >> 16 & 0xff), + str(i >> 8 & 0xff), + str(i & 0xff))) + + +def _quad_dot_to_int(s): + i = 0 + for octet in s.split('.'): + i <<= 8 + i += int(octet, 10) + return i + + +def _netmask_to_prefixlen(mask): + bs = '{:032b}'.format(mask) + prefix = bs.rstrip('0') + if '0' in prefix: + raise ValueError('abnormal netmask: %s' % + _int_to_quad_dot(mask)) + return len(prefix) + + +def _get_routes(): + routes = [] + with open('/proc/net/route', 'r') as f: + for line in list(f)[1:]: + fields = line.split() + destination, flags, mask = fields[1], fields[3], fields[7] + flags = int(flags, 16) + if flags & 2: + # default route, ignore + continue + destination = socket.ntohl(int(destination, 16)) + mask = socket.ntohl(int(mask, 16)) + prefixlen = _netmask_to_prefixlen(mask) + routes.append((destination, prefixlen)) + return routes + + +class IPRange(object): + def __init__(self, prefix, prefixlen): + self.prefixlen = prefixlen + mask = (1 << prefixlen) - 1 + self.mask = mask << (32 - prefixlen) + self.prefix = prefix & self.mask + @property + def bitstring(self): + return ('{:08b}' * 4).format( + self.prefix >> 24 & 0xff, + self.prefix >> 16 & 0xff, + self.prefix >> 8 & 0xff, + self.prefix & 0xff + )[:self.prefixlen] + def startswith(self, other_range): + return self.bitstring.startswith(other_range.bitstring) + + +def find_subnet(valid_ranges, invalid_ranges): + for vr in valid_ranges: + known_subnets = set(ir for ir in invalid_ranges if ir.startswith(vr)) + prefixlens = set(r.prefixlen for r in known_subnets) + prefixlens.add(32 - 2) # need at least 4 addresses in subnet + prefixlen = min(prefixlens) + if prefixlen <= vr.prefixlen: + # valid subnet is full, move on to next + continue + subnetlen = prefixlen - vr.prefixlen + for prefix in (subnetid + vr.prefix + for subnetid in xrange(1 << subnetlen)): + if any(subnet.prefix == prefix for subnet in known_subnets): + continue + return prefix, prefixlen + + +def _normalise_macaddr(macaddr): + '''pxelinux.0 wants the mac address to be lowercase and - separated''' + digits = (c for c in macaddr.lower() if c in string.hexdigits) + nibble_pairs = grouper(digits, 2) + return '-'.join(''.join(byte) for byte in nibble_pairs) + + +@contextlib.contextmanager +def executor(target_pid): + 'Kills a process if its parent dies' + read_fd, write_fd = os.pipe() + helper_pid = os.fork() + if helper_pid == 0: + try: + os.close(write_fd) + while True: + rlist, _, _ = select.select([read_fd], [], []) + if read_fd in rlist: + d = os.read(read_fd, 1) + if not d: + os.kill(target_pid, signal.SIGKILL) + if d in ('', 'Q'): + os._exit(0) + else: + os._exit(1) + except BaseException as e: + import traceback + traceback.print_exc() + os._exit(1) + os.close(read_fd) + yield + os.write(write_fd, 'Q') + os.close(write_fd) + + +def grouper(iterable, n, fillvalue=None): + "Collect data into fixed-length chunks or blocks" + # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx" + args = [iter(iterable)] * n + return itertools.izip_longest(*args, fillvalue=fillvalue) + + +class PXEBoot(morphlib.writeexts.WriteExtension): + @contextlib.contextmanager + def _vlan(self, interface, vlan): + viface = '%s.%s' % (interface, vlan) + self.status(msg='Creating vlan %(viface)s', viface=viface) + subprocess.check_call(['vconfig', 'add', interface, str(vlan)]) + try: + yield viface + finally: + self.status(msg='Destroying vlan %(viface)s', viface=viface) + subprocess.call(['vconfig', 'rem', viface]) + + @contextlib.contextmanager + def _static_ip(self, iface): + valid_ranges = set(( + IPRange(_quad_dot_to_int('192.168.0.0'), 16), + IPRange(_quad_dot_to_int('172.16.0.0'), 12), + IPRange(_quad_dot_to_int('10.0.0.0'), 8), + )) + invalid_ranges = set(IPRange(prefix, prefixlen) + for (prefix, prefixlen) in _get_routes()) + prefix, prefixlen = find_subnet(valid_ranges, invalid_ranges) + netaddr = prefix + dhcp_server_ip = netaddr + 1 + client_ip = netaddr + 2 + broadcast_ip = prefix | ((1 << (32 - prefixlen)) - 1) + self.status(msg='Assigning ip address %(ip)s/%(prefixlen)d to ' + 'iface %(iface)s', + ip=_int_to_quad_dot(dhcp_server_ip), prefixlen=prefixlen, + iface=iface) + subprocess.check_call(['ip', 'addr', 'add', + '{}/{}'.format(_int_to_quad_dot(dhcp_server_ip), + prefixlen), + 'broadcast', _int_to_quad_dot(broadcast_ip), + 'scope', 'global', + 'dev', iface]) + try: + yield (dhcp_server_ip, client_ip, broadcast_ip) + finally: + self.status(msg='Removing ip addresses from iface %(iface)s', + iface=iface) + subprocess.call(['ip', 'addr', 'flush', 'dev', iface]) + + @contextlib.contextmanager + def _up_interface(self, iface): + self.status(msg='Bringing interface %(iface)s up', iface=iface) + subprocess.check_call(['ip', 'link', 'set', iface, 'up']) + try: + yield + finally: + self.status(msg='Bringing interface %(iface)s down', iface=iface) + subprocess.call(['ip', 'link', 'set', iface, 'down']) + + @contextlib.contextmanager + def static_ip(self, interface): + with self._static_ip(iface=interface) as (host_ip, client_ip, + broadcast_ip), \ + self._up_interface(iface=interface): + yield (_int_to_quad_dot(host_ip), + _int_to_quad_dot(client_ip), + _int_to_quad_dot(broadcast_ip)) + + @contextlib.contextmanager + def vlan(self, interface, vlan): + with self._vlan(interface=interface, vlan=vlan) as viface, \ + self.static_ip(interface=viface) \ + as (host_ip, client_ip, broadcast_ip): + yield host_ip, client_ip, broadcast_ip + + @contextlib.contextmanager + def _tempdir(self): + td = tempfile.mkdtemp() + print 'Created tempdir:', td + try: + yield td + finally: + shutil.rmtree(td, ignore_errors=True) + + @contextlib.contextmanager + def _remote_tempdir(self, hostname, template): + td = cliapp.ssh_runcmd(hostname, ['mktemp', '-d', template]).strip() + try: + yield td + finally: + cliapp.ssh_runcmd(hostname, ['find', td, '-delete']) + + def _serve_tftpd(self, sock, host, port, interface, tftproot): + self.settings.progname = 'tftp server' + self._set_process_name() + while True: + logging.debug('tftpd waiting for connections') + # recvfrom with MSG_PEEK is how you accept UDP connections + _, peer = sock.recvfrom(0, socket.MSG_PEEK) + conn = sock + logging.debug('Connecting socket to peer: ' + repr(peer)) + conn.connect(peer) + # The existing socket is now only serving that peer, so we need to + # bind a new UDP socket to the wildcard address, which needs the + # port to be in REUSEADDR mode. + conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + logging.debug('Binding replacement socket to ' + repr((host, port))) + sock.bind((host, port)) + + logging.debug('tftpd server handing connection to tftpd') + tftpd_serve = ['tftpd', '-rl', tftproot] + ret = subprocess.call(args=tftpd_serve, stdin=conn, + stdout=conn, stderr=None, close_fds=True) + # It's handy to turn off REUSEADDR after the rebinding, + # so we can protect against future bind attempts on this port. + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 0) + logging.debug('tftpd exited %d' % ret) + os._exit(0) + + @contextlib.contextmanager + def _spawned_tftp_server(self, tftproot, host_ip, interface, tftp_port=0): + # inetd-style launchers tend to bind UDP ports with SO_REUSEADDR, + # because they need to have multiple ports bound, one for recieving + # all connection attempts on that port, and one for each concurrent + # connection with a peer + # this makes detecting whether there's a tftpd running difficult, so + # we'll instead use an ephemeral port and configure the PXE boot to + # use that tftp server for the kernel + s = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM) + s.bind((host_ip, tftp_port)) + host, port = s.getsockname() + self.status(msg='Bound listen socket to %(host)s, %(port)s', + host=host, port=port) + pid = os.fork() + if pid == 0: + try: + self._serve_tftpd(sock=s, host=host, port=port, + interface=interface, tftproot=tftproot) + except BaseException as e: + import traceback + traceback.print_exc() + os._exit(1) + s.close() + with executor(pid): + try: + yield port + finally: + self.status(msg='Killing tftpd listener pid=%(pid)d', + pid=pid) + os.kill(pid, signal.SIGKILL) + + @contextlib.contextmanager + def tftp_server(self, host_ip, interface, tftp_port=0): + with self._tempdir() as tftproot, \ + self._spawned_tftp_server(tftproot=tftproot, host_ip=host_ip, + interface=interface, + tftp_port=tftp_port) as tftp_port: + self.status(msg='Serving tftp root %(tftproot)s, on port %(port)d', + port=tftp_port, tftproot=tftproot) + yield tftp_port, tftproot + + @contextlib.contextmanager + def _local_copy(self, src, dst): + self.status(msg='Installing %(src)s to %(dst)s', + src=src, dst=dst) + shutil.copy2(src=src, dst=dst) + try: + yield + finally: + self.status(msg='Removing %(dst)s', dst=dst) + os.unlink(dst) + + def local_pxelinux(self, tftproot): + return self._local_copy('/usr/share/syslinux/pxelinux.0', + os.path.join(tftproot, 'pxelinux.0')) + + def local_kernel(self, rootfs, tftproot): + return self._local_copy(os.path.join(rootfs, 'boot/vmlinuz'), + os.path.join(tftproot, 'kernel')) + + @contextlib.contextmanager + def _remote_copy(self, hostname, src, dst): + with open(src, 'r') as f: + cliapp.ssh_runcmd(hostname, + ['install', '-D', '-m644', '/proc/self/fd/0', + dst], stdin=f, stdout=None, stderr=None) + try: + yield + finally: + cliapp.ssh_runcmd(hostname, ['rm', dst]) + + @contextlib.contextmanager + def remote_kernel(self, rootfs, tftp_url, macaddr): + for name in ('vmlinuz', 'zImage', 'uImage'): + kernel_path = os.path.join(rootfs, 'boot', name) + if os.path.exists(kernel_path): + break + else: + raise cliapp.AppException('Failed to locate kernel') + url = urlparse.urlsplit(tftp_url) + basename = '{}-kernel'.format(_normalise_macaddr(macaddr)) + target_path = os.path.join(url.path, basename) + with self._remote_copy(hostname=url.hostname, src=kernel_path, + dst=target_path): + yield basename + + @contextlib.contextmanager + def local_nfsroot(self, rootfs, target_ip): + nfsroot = target_ip + ':' + rootfs + self.status(msg='Exporting %(nfsroot)s as local nfsroot', + nfsroot=nfsroot) + cliapp.runcmd(['exportfs', '-o', 'ro,insecure,no_root_squash', + nfsroot]) + try: + yield + finally: + self.status(msg='Removing %(nfsroot)s from local nfsroots', + nfsroot=nfsroot) + cliapp.runcmd(['exportfs', '-u', nfsroot]) + + @contextlib.contextmanager + def remote_nfsroot(self, rootfs, rsync_url, macaddr): + url = urlparse.urlsplit(rsync_url) + template = os.path.join(url.path, 'nfsroot.XXXXXXXXXX') + with self._remote_tempdir(hostname=url.hostname, template=template) \ + as tempdir: + nfsroot = urlparse.urlunsplit(url.scheme, url.netloc, tempdir, + url.query, url.fragment) + cliapp.runcmd(['rsync', '-asXSPH', '--delete', rootfs, nfsroot], + stdin=None, stdout=None, stderr=None) + yield basename + + @staticmethod + def _write_pxe_config(fh, kernel_tftp_url, rootfs_nfs_url, extra_args=''): + fh.write(textwrap.dedent('''\ + DEFAULT default + LABEL default + LINUX {kernel_url} + APPEND root=/dev/nfs ip=dhcp nfsroot={rootfs_nfs_url} {extra_args} + ''').format(kernel_url=kernel_tftp_url, + rootfs_nfs_url=rootfs_nfs_url, extra_args=extra_args)) + fh.flush() + + @contextlib.contextmanager + def local_pxeboot_config(self, tftproot, macaddr, ip, tftp_port, + nfsroot_dir): + kernel_tftp_url = 'tftp://{}:{}/kernel'.format(ip, tftp_port) + rootfs_nfs_url = '{}:{}'.format(ip, nfsroot_dir) + pxe_cfg_filename = _normalise_macaddr(macaddr) + pxe_cfg_path = os.path.join(tftproot, 'pxelinux.cfg', pxe_cfg_filename) + os.makedirs(os.path.dirname(pxe_cfg_path)) + with open(pxe_cfg_path, 'w') as f: + self._write_pxe_config(fh=f, kernel_tftp_url=kernel_tftp_url, + rootfs_nfs_url=rootfs_nfs_url, + extra_args=os.environ.get('KERNEL_ARGS','')) + try: + yield + finally: + os.unlink(pxe_cfg_path) + + @contextlib.contextmanager + def remote_pxeboot_config(self, tftproot, kernel_tftproot, kernel_subpath, + rootfs_nfsroot, rootfs_subpath, macaddr): + rootfs_nfs_url = '{}:{}/{}'.format(ip, rootfs_nfsroot, rootfs_subpath) + kernel_tftp_url = '{}/{}'.format(kernel_tftproot, kernel_subpath) + pxe_cfg_filename = _normalise_macaddr(macaddr) + url = urlparse.urlsplit(tftproot) + inst_cfg_path = os.path.join(url.path, 'pxelinux.cfg', + pxe_cfg_filename) + with tempfile.NamedTemporaryFile() as f: + self._write_pxe_config(fh=f, kernel_tftp_url=kernel_tftp_url, + rootfs_nfs_url=rootfs_nfs_url, + extra_args=os.environ.get('KERNEL_ARGS','')) + with self._remote_copy(hostname=url.hostname, src=f.name, + dst=inst_cfg_path): + yield + + @contextlib.contextmanager + def dhcp_server(self, interface, host_ip, target_ip, broadcast_ip): + with self._tempdir() as td: + leases_path = os.path.join(td, 'leases') + config_path = os.path.join(td, 'config') + stdout_path = os.path.join(td, 'stdout') + stderr_path = os.path.join(td, 'stderr') + pidfile_path = os.path.join(td, 'pid') + with open(config_path, 'w') as f: + f.write(textwrap.dedent('''\ + start {target_ip} + end {target_ip} + interface {interface} + max_leases 1 + lease_file {leases_path} + pidfile {pidfile_path} + boot_file pxelinux.0 + option dns {host_ip} + option broadcast {broadcast_ip} + ''').format(**locals())) + with open(stdout_path, 'w') as stdout, \ + open(stderr_path, 'w') as stderr: + sp = subprocess.Popen(['udhcpd', '-f', config_path], cwd=td, + stdin=open(os.devnull), stdout=stdout, + stderr=stderr) + try: + with executor(sp.pid): + yield + finally: + sp.terminate() + + def get_interface_ip(self, interface): + ip_addresses = [] + info = cliapp.runcmd(['ip', '-o', '-f', 'inet', + 'addr', 'show', interface]).rstrip('\n') + if info: + tokens = collections.deque(info.split()[1:]) + ifname = tokens.popleft() + while tokens: + tok = tokens.popleft() + if tok == 'inet': + address = tokens.popleft() + address, netmask = address.split('/') + ip_addresses.append(address) + elif tok == 'brd': + tokens.popleft() # not interested in broadcast address + elif tok == 'scope': + tokens.popleft() # not interested in scope tag + else: + continue + if not ip_addresses: + raise cliapp.AppException('Interface %s has no addresses' + % interface) + if len(ip_addresses) > 1: + warnings.warn('Interface %s has multiple addresses, ' + 'using first (%s)' % (interface, ip_addresses[0])) + return ip_addresses[0] + + def ipmi_set_target_vlan(self): + if any(env_var.startswith('IPMI_') for env_var in os.environ): + # Needs IPMI_USER, IPMI_PASSWORD, IPMI_HOST and PXEBOOT_VLAN + default = textwrap.dedent('''\ + ipmitool -I lanplus -U "$IPMI_USER" -E -H "$IPMI_HOST" \\ + lan set 1 vlan id "$PXEBOOT_VLAN" + ''') + else: + default = textwrap.dedent('''\ + while true; do + echo Please set the target\\'s vlan to $PXEBOOT_VLAN, \\ + then enter \\"vlanned\\" + read + if [ "$REPLY" = vlanned ]; then + break + fi + done + ''') + command = os.environ.get('PXEBOOT_SET_VLAN_COMMAND', default) + subprocess.check_call(['sh', '-euc', command, '-']) + + def ipmi_pxe_reboot_target(self): + if any(env_var.startswith('IPMI_') for env_var in os.environ): + # Needs IPMI_USER, IPMI_PASSWORD, IPMI_HOST and PXEBOOT_VLAN + default = textwrap.dedent('''\ + set -- ipmitool -I lanplus -U "$IPMI_USER" -E -H "$IPMI_HOST" + "$@" chassis bootdev pxe + "$@" chassis power reset + ''') + else: + default = textwrap.dedent('''\ + while true; do + echo Please reboot the target in PXE mode, then\\ + enter \\"pxe-booted\\" + read + if [ "$REPLY" = pxe-booted ]; then + break + fi + done + ''') + command = os.environ.get('PXEBOOT_PXE_REBOOT_COMMAND', default) + subprocess.check_call(['sh', '-euc', command, '-']) + + def wait_for_target_to_install(self): + command = os.environ.get( + 'PXEBOOT_WAIT_INSTALL_COMMAND', + textwrap.dedent('''\ + while true; do + echo Please wait for the system to install, then \\ + enter \\"installed\\" + read + if [ "$REPLY" = installed ]; then + break + fi + done + ''')) + subprocess.check_call(['sh', '-euc', command, '-']) + + def ipmi_unset_target_vlan(self): + if any(env_var.startswith('IPMI_') for env_var in os.environ): + # Needs IPMI_USER, IPMI_PASSWORD, IPMI_HOST + default = textwrap.dedent('''\ + ipmitool -I lanplus -U "$IPMI_USER" -E -H "$IPMI_HOST" \\ + lan set 1 vlan id off + ''') + else: + default = textwrap.dedent('''\ + while true; do + echo Please reset the target\\'s vlan, \\ + then enter \\"unvlanned\\" + read + if [ "$REPLY" = unvlanned ]; then + break + fi + done + ''') + command = os.environ.get('PXEBOOT_UNSET_VLAN_COMMAND', default) + subprocess.check_call(['sh', '-euc', command, '-']) + + def ipmi_reboot_target(self): + if any(env_var.startswith('IPMI_') for env_var in os.environ): + # Needs IPMI_USER, IPMI_PASSWORD, IPMI_HOST + default = textwrap.dedent('''\ + ipmitool -I lanplus -U "$IPMI_USER" -E -H "$IPMI_HOST" \\ + chassis power reset + ''') + else: + default = textwrap.dedent('''\ + while true; do + echo Please reboot the target, then\\ + enter \\"rebooted\\" + read + if [ "$REPLY" = rebooted ]; then + break + fi + done + ''') + command = os.environ.get('PXEBOOT_REBOOT_COMMAND', default) + subprocess.check_call(['sh', '-euc', command, '-']) + + def process_args(self, (temp_root, macaddr)): + interface = os.environ.get('PXEBOOT_DEPLOYER_INTERFACE', None) + vlan = os.environ.get('PXEBOOT_VLAN') + if vlan is not None: vlan = int(vlan) + mode = os.environ.get('PXEBOOT_MODE') + if mode is None: + if interface: + if vlan is not None: + mode = 'spawn-vlan' + else: + if 'PXEBOOT_CONFIG_TFTP_ADDRESS' in os.environ: + mode = 'existing-dhcp' + else: + mode = 'spawn-novlan' + else: + mode = 'existing-server' + assert mode in ('spawn-vlan', 'spawn-novlan', 'existing-dhcp', + 'existing-server') + if mode == 'spawn-vlan': + with self.vlan(interface=interface, vlan=vlan) \ + as (host_ip, target_ip, broadcast_ip), \ + self.tftp_server(host_ip=host_ip, interface=interface) \ + as (tftp_port, tftproot), \ + self.local_pxelinux(tftproot=tftproot), \ + self.local_kernel(rootfs=temp_root, tftproot=tftproot), \ + self.local_nfsroot(rootfs=temp_root, target_ip=target_ip), \ + self.local_pxeboot_config(tftproot=tftproot, macaddr=macaddr, + ip=host_ip, tftp_port=tftp_port, + nfsroot_dir=temp_root), \ + self.dhcp_server(interface=interface, host_ip=host_ip, + target_ip=target_ip, + broadcast_ip=broadcast_ip): + self.ipmi_set_target_vlan() + self.ipmi_pxe_reboot_target() + self.wait_for_target_to_install() + self.ipmi_unset_target_vlan() + self.ipmi_reboot_target() + elif mode == 'spawn-novlan': + with self.static_ip(interface=interface) as (host_ip, target_ip, + broadcast_ip), \ + self.tftp_server(host_ip=host_ip, interface=interface, + tftp_port=69) \ + as (tftp_port, tftproot), \ + self.local_pxelinux(tftproot=tftproot), \ + self.local_kernel(rootfs=temp_root, tftproot=tftproot), \ + self.local_nfsroot(rootfs=temp_root, target_ip=target_ip), \ + self.local_pxeboot_config(tftproot=tftproot, macaddr=macaddr, + ip=host_ip, tftp_port=tftp_port, + nfsroot_dir=temp_root), \ + self.dhcp_server(interface=interface, host_ip=host_ip, + target_ip=target_ip, + broadcast_ip=broadcast_ip): + self.ipmi_pxe_reboot_target() + self.wait_for_target_to_install() + self.ipmi_reboot_target() + elif mode == 'existing-dhcp': + ip = self.get_interface_ip(interface) + config_tftpaddr = os.environ['PXEBOOT_CONFIG_TFTP_ADDRESS'] + with self.tftp_server(ip=ip, interface=interface, tftp_port=69) \ + as (tftp_port, tftproot), \ + self.local_kernel(rootfs=temp_root, tftproot=tftproot), \ + self.local_nfsroot(rootfs=temp_root, client_ip=''): + kernel_tftproot = 'tftp://{}:{}/'.format(ip, tftp_port) + rootfs_nfsroot = '{}:{}'.format(ip, temp_root) + with self.remote_pxeboot_config( + tftproot=config_tftpaddr, + kernel_tftproot=kernel_tftproot, + kernel_subpath='kernel', + rootfs_nfsroot=nfsroot, + rootfs_subpath='', + macaddr=macaddr): + self.ipmi_pxe_reboot_target() + self.wait_for_target_to_install() + self.ipmi_reboot_target() + elif mode == 'existing-server': + config_tftpaddr = os.environ[ 'PXEBOOT_CONFIG_TFTP_ADDRESS'] + kernel_tftpaddr = os.environ.get('PXEBOOT_KERNEL_TFTP_ADDRESS', + config_tftpaddr) + url = urlparse.urlsplit(kernel_tftpaddr) + kernel_tftproot = os.environ.get('PXEBOOT_KERNEL_TFTP_ROOT', + 'tftp://%s/%s' % (url.hostname, + url.path)) + rootfs_rsync = os.environ['PXEBOOT_ROOTFS_RSYNC_ADDRESS'] + url = urlparse.urlsplit(rootfs_rsync) + nfsroot = os.environ.get('PXEBOOT_ROOTFS_NFSROOT', + '%s:%s' % (url.hostname, url.path)) + with self.remote_kernel(rootfs=temp_root, url=kernel_tftpaddr, + macaddr=macaddr) as kernel_subpath, \ + self.remote_nfsroot(rootfs=temp_root, rsync_url=rootfs_rsync)\ + as rootfs_subpath, \ + self.remote_pxeboot_config(tftproot=config_tftpaddr, + kernel_tftproot=kernel_tftproot, + kernel_subpath=kernel_subpath, + rootfs_nfsroot=nfsroot, + rootfs_subpath=rootfs_subpath, + macaddr=macaddr): + self.ipmi_pxe_reboot_target() + self.wait_for_target_to_install() + self.ipmi_reboot_target() + else: + cliapp.AppException('Invalid PXEBOOT_MODE: %s' % mode) + +PXEBoot().run() diff --git a/pxeboot.write.help b/pxeboot.write.help new file mode 100644 index 00000000..58a86957 --- /dev/null +++ b/pxeboot.write.help @@ -0,0 +1,123 @@ +help: > + pxeboot.write extension. + + + This write extension will serve your generated system over NFS to + the target system. + + In all modes `location` is the mac address of the interface that + the target will PXE boot from. This is used so that the target will + load the configuration file appropriate to it. + + + # `PXEBOOT_MODE` + + + It has 4 modes, which can be specified with PXEBOOT_MODE, or inferred + from which parameters are passed: + + + ## spawn-vlan + + + Specify PXEBOOT_DEPLOYER_INTERFACE and PXEBOOT_VLAN to configure + the target to pxeboot on a vlan and spawn a dhcp, nfs and tftp + server. This is potentially the fastest, since it doesn't need to + copy data to other servers. + + This will create a vlan interface for the interface specified in + PXEBOOT_DEPLOYER_INTERFACE and spawn a dhcp server which serves + pxelinux.0, a configuration file and a kernel image from itself. + + The configuration file informs the target to boot with a kernel + command-line that uses an NFS root served from the deployment host. + + + ## spawn-novlan + + + Specify PXEBOOT_DEPLOYER_INTERFACE without PXEBOOT_VLAN to configure + like `spawn-vlan`, but without creating the vlan interface. + + This assumes that you have exclusive access to the interface, such + as if you're plugged in to the device directly, or your interface + is vlanned by your infrastructure team. + + This is required if you are serving from a VM and bridging it to the + correct network via macvtap. For this to work, you need to macvtap + bridge to a pre-vlanned interface on your host machine. + + + ## existing-dhcp + + + Specify PXEBOOT_DEPLOYER_INTERFACE and PXEBOOT_CONFIG_TFTP_ADDRESS + to put config on an existing tftp server, already configured by the + dhcp server. + + This spawns a tftp server and configures the local nfs server, but + doesn't spawn a dhcp server. This is useful if you have already got a + dhcp server that serves PXE images. + + PXEBOOT_CONFIG_TFTP_ADDRESS is a URL in the form `sftp://$HOST/$PATH`. + The configuration file is copied to `$PATH/pxelinux.cfg/` on the + target identified by `$HOST`. + + + ## existing-server + + + Specify at least PXEBOOT_CONFIG_TFTP_ADDRESS and + PXEBOOT_ROOTFS_RSYNC_ADDRESS to specify existing servers to copy + config, kernels and the rootfs to. + + Configuration is copied to the target as `existing-dhcp`. + + Specify PXEBOOT_KERNEL_TFTP_ADDRESS if the tftp server that the + kernel must be downloaded from is different to that of the pxelinux + configuration file. + + PXEBOOT_ROOTFS_RSYNC_ADDRESS is a rsync URL describing where to copy + nfsroots to where they will be exported by the NFS server. + + Specify PXEBOOT_ROOTFS_NFSROOT if the nfsroot appears as a different + address from the target's perspective. + + + # IPMI commands + + + After the PXE boot has been set up, the target needs to be rebooted + in PXE mode. + + If the target is IPMI enabled, you can set `IPMI_USER`, `IPMI_HOST` + and `IPMI_PASSWORD` to make it reboot the target into netboot mode + automatically. + + If they are not specified, then instructions will be displayed, and + `pxeboot.write` will wait for you to finish. + + If there are command-line automation tools for rebooting the target + in netboot mode, then appropriate commands can be defined in the + following variables. + + + ## PXEBOOT_PXE_REBOOT_COMMAND + + + This command will be used to reboot the target device with its boot + device set to PXE boot. + + + ## PXEBOOT_REBOOT_COMMAND + + + This command will be used to reboot the target device in its default + boot mode. + + + ## PXEBOOT_WAIT_INSTALL_COMMAND + + + If it is possible for the target to notify you that it has finished + installing, you can put a command in here to wait for the event. -- cgit v1.2.1