summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2014-10-09 09:48:01 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2014-11-10 17:59:58 +0000
commit00f5bb8550a7e852b544817d00671be68936137c (patch)
tree2d780e734e622d56f4fe4e43b90bc90f7f7dfcff
parentd2ec7e9f3b30af999637c3f918621fe2b5385bc8 (diff)
downloaddefinitions-baserock/richardmaw/pxeboot-write.tar.gz
Add pxeboot write extension.baserock/richardmaw/pxeboot-write
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`.
-rwxr-xr-xpxeboot.check69
-rwxr-xr-xpxeboot.write698
-rw-r--r--pxeboot.write.help123
3 files changed, 890 insertions, 0 deletions
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..b5d7cc80
--- /dev/null
+++ b/pxeboot.write
@@ -0,0 +1,698 @@
+#!/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
+
+
+try:
+ socket.SO_BINDTODEVICE
+except AttributeError:
+ import platform
+ if platform.machine() not in ('parisc', 'sparc'):
+ socket.SO_BINDTODEVICE = 25
+
+
+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)
+ #try:
+ # sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE,
+ # interface)
+ #except socket.error as e:
+ # # Best effort to force dev in case same IP used on another dev
+ # pass
+ 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)
+ #try:
+ # s.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, interface)
+ #except socket.error as e:
+ # # Best effort to force dev in case same IP used on another dev
+ # pass
+ 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':
+ scope = tokens.popleft()
+ 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'
+ elif vlan is None:
+ 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.