mirror of
https://github.com/danderson/netboot.git
synced 2025-08-07 23:27:16 +02:00
275 lines
7.9 KiB
Go
275 lines
7.9 KiB
Go
// Copyright 2016 Google Inc.
|
|
//
|
|
// 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.
|
|
|
|
package pixiecore // import "go.universe.tf/netboot/pixiecore"
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"text/template"
|
|
|
|
"go.universe.tf/netboot/dhcp4"
|
|
)
|
|
|
|
const (
|
|
portDHCP = 67
|
|
portTFTP = 69
|
|
portHTTP = 80
|
|
portPXE = 4011
|
|
)
|
|
|
|
// An ID is an identifier used by Booters to reference files.
|
|
type ID string
|
|
|
|
// Architecture describes a kind of CPU architecture.
|
|
type Architecture int
|
|
|
|
// Architecture types that Pixiecore knows how to boot.
|
|
//
|
|
// These architectures are self-reported by the booting machine. The
|
|
// machine may support additional execution modes. For example, legacy
|
|
// PC BIOS reports itself as an ArchIA32, but may also support ArchX64
|
|
// execution.
|
|
const (
|
|
// ArchIA32 is a 32-bit x86 machine. It _may_ also support X64
|
|
// execution, but Pixiecore has no way of knowing.
|
|
ArchIA32 Architecture = iota
|
|
// ArchX64 is a 64-bit x86 machine (aka amd64 aka X64).
|
|
ArchX64
|
|
)
|
|
|
|
func (a Architecture) String() string {
|
|
switch a {
|
|
case ArchIA32:
|
|
return "IA32"
|
|
case ArchX64:
|
|
return "X64"
|
|
default:
|
|
return "Unknown architecture"
|
|
}
|
|
}
|
|
|
|
// A Machine describes a machine that is attempting to boot.
|
|
type Machine struct {
|
|
MAC net.HardwareAddr
|
|
Arch Architecture
|
|
}
|
|
|
|
// A Spec describes a kernel and associated configuration.
|
|
type Spec struct {
|
|
// The kernel to boot
|
|
Kernel ID
|
|
// Optional init ramdisks for linux kernels
|
|
Initrd []ID
|
|
// Optional kernel commandline. This string is evaluated as a
|
|
// text/template template, in which "ID(x)" function is
|
|
// available. Invoking ID(x) returns a URL that will call
|
|
// Booter.ReadBootFile(x) when fetched.
|
|
Cmdline string
|
|
// Message to print on the client machine before booting.
|
|
Message string
|
|
|
|
// A raw iPXE script to run. Overrides all of the above.
|
|
//
|
|
// THIS IS NOT A STABLE INTERFACE. This will only work for
|
|
// machines that get booted via iPXE. Currently, that is all of
|
|
// them, but there is no guarantee that this will remain
|
|
// true. When passing a custom iPXE script, it is your
|
|
// responsibility to make the boot succeed, Pixiecore's
|
|
// involvement ends when it serves your script.
|
|
IpxeScript string
|
|
}
|
|
|
|
func expandCmdline(tpl string, funcs template.FuncMap) (string, error) {
|
|
tmpl, err := template.New("cmdline").Option("missingkey=error").Funcs(funcs).Parse(tpl)
|
|
if err != nil {
|
|
return "", fmt.Errorf("parsing cmdline %q: %s", tpl, err)
|
|
}
|
|
var out bytes.Buffer
|
|
if err = tmpl.Execute(&out, nil); err != nil {
|
|
return "", fmt.Errorf("expanding cmdline template %q: %s", tpl, err)
|
|
}
|
|
cmdline := strings.TrimSpace(out.String())
|
|
if strings.Contains(cmdline, "\n") {
|
|
return "", fmt.Errorf("cmdline %q contains a newline", cmdline)
|
|
}
|
|
return cmdline, nil
|
|
}
|
|
|
|
// A Booter provides boot instructions and files for machines.
|
|
//
|
|
// Due to the stateless nature of various boot protocols, BootSpec()
|
|
// will be called multiple times in the course of a single boot
|
|
// attempt.
|
|
type Booter interface {
|
|
// The given MAC address wants to know what it should
|
|
// boot. What should Pixiecore make it boot?
|
|
//
|
|
// Returning an error or a nil BootSpec will make Pixiecore ignore
|
|
// the client machine's request.
|
|
BootSpec(m Machine) (*Spec, error)
|
|
// Get the bytes corresponding to an ID given in Spec.
|
|
//
|
|
// Additionally returns the total number of bytes in the
|
|
// ReadCloser, or -1 if the size is unknown. Be warned, returning
|
|
// -1 will make the boot process orders of magnitude slower due to
|
|
// poor ipxe behavior.
|
|
ReadBootFile(id ID) (io.ReadCloser, int64, error)
|
|
// Write the given Reader to an ID given in Spec.
|
|
WriteBootFile(id ID, body io.Reader) error
|
|
}
|
|
|
|
// Firmware describes a kind of firmware attempting to boot.
|
|
//
|
|
// This should only be used for selecting the right bootloader within
|
|
// Pixiecore, kernel selection should key off the more generic
|
|
// Architecture.
|
|
type Firmware int
|
|
|
|
// The bootloaders that Pixiecore knows how to handle.
|
|
const (
|
|
FirmwareX86PC Firmware = iota // "Classic" x86 BIOS with PXE/UNDI support
|
|
FirmwareEFI32 // 32-bit x86 processor running EFI
|
|
FirmwareEFI64 // 64-bit x86 processor running EFI
|
|
FirmwareEFIBC // 64-bit x86 processor running EFI
|
|
FirmwareX86Ipxe // "Classic" x86 BIOS running iPXE (no UNDI support)
|
|
FirmwarePixiecoreIpxe // Pixiecore's iPXE, which has replaced the underlying firmware
|
|
)
|
|
|
|
// A Server boots machines using a Booter.
|
|
type Server struct {
|
|
Booter Booter
|
|
|
|
// Address to listen on, or empty for all interfaces.
|
|
Address string
|
|
// HTTP port for boot services.
|
|
HTTPPort int
|
|
// HTTP port for human-readable information. Can be the same as
|
|
// HTTPPort.
|
|
HTTPStatusPort int
|
|
|
|
// Ipxe lists the supported bootable Firmwares, and their
|
|
// associated ipxe binary.
|
|
Ipxe map[Firmware][]byte
|
|
|
|
// Log receives logs on Pixiecore's operation. If nil, logging
|
|
// is suppressed.
|
|
Log func(subsystem, msg string)
|
|
// Debug receives extensive logging on Pixiecore's internals. Very
|
|
// useful for debugging, but very verbose.
|
|
Debug func(subsystem, msg string)
|
|
|
|
// These ports can technically be set for testing, but the
|
|
// protocols burned in firmware on the client side hardcode these,
|
|
// so if you change them in production, nothing will work.
|
|
DHCPPort int
|
|
TFTPPort int
|
|
PXEPort int
|
|
|
|
// Listen for DHCP traffic without binding to the DHCP port. This
|
|
// enables coexistence of Pixiecore with another DHCP server.
|
|
//
|
|
// Currently only supported on Linux.
|
|
DHCPNoBind bool
|
|
|
|
// Read UI assets from this path, rather than use the builtin UI
|
|
// assets. Used for development of Pixiecore.
|
|
UIAssetsDir string
|
|
|
|
errs chan error
|
|
|
|
eventsMu sync.Mutex
|
|
events map[string][]machineEvent
|
|
}
|
|
|
|
// Serve listens for machines attempting to boot, and uses Booter to
|
|
// help them.
|
|
func (s *Server) Serve() error {
|
|
if s.DHCPPort == 0 {
|
|
s.DHCPPort = portDHCP
|
|
}
|
|
if s.TFTPPort == 0 {
|
|
s.TFTPPort = portTFTP
|
|
}
|
|
if s.PXEPort == 0 {
|
|
s.PXEPort = portPXE
|
|
}
|
|
if s.HTTPPort == 0 {
|
|
s.HTTPPort = portHTTP
|
|
}
|
|
|
|
newDHCP := dhcp4.NewConn
|
|
if s.DHCPNoBind {
|
|
newDHCP = dhcp4.NewSnooperConn
|
|
}
|
|
|
|
dhcp, err := newDHCP(fmt.Sprintf("%s:%d", s.Address, s.DHCPPort))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tftp, err := net.ListenPacket("udp", fmt.Sprintf("%s:%d", s.Address, s.TFTPPort))
|
|
if err != nil {
|
|
dhcp.Close()
|
|
return err
|
|
}
|
|
pxe, err := net.ListenPacket("udp4", fmt.Sprintf("%s:%d", s.Address, s.PXEPort))
|
|
if err != nil {
|
|
dhcp.Close()
|
|
tftp.Close()
|
|
return err
|
|
}
|
|
http, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Address, s.HTTPPort))
|
|
if err != nil {
|
|
dhcp.Close()
|
|
tftp.Close()
|
|
pxe.Close()
|
|
return err
|
|
}
|
|
|
|
s.events = make(map[string][]machineEvent)
|
|
// 5 buffer slots, one for each goroutine, plus one for
|
|
// Shutdown(). We only ever pull the first error out, but shutdown
|
|
// will likely generate some spurious errors from the other
|
|
// goroutines, and we want them to be able to dump them without
|
|
// blocking.
|
|
s.errs = make(chan error, 6)
|
|
|
|
s.debug("Init", "Starting Pixiecore goroutines")
|
|
|
|
go func() { s.errs <- s.serveDHCP(dhcp) }()
|
|
go func() { s.errs <- s.servePXE(pxe) }()
|
|
go func() { s.errs <- s.serveTFTP(tftp) }()
|
|
go func() { s.errs <- serveHTTP(http, s.serveHTTP) }()
|
|
|
|
// Wait for either a fatal error, or Shutdown().
|
|
err = <-s.errs
|
|
dhcp.Close()
|
|
tftp.Close()
|
|
pxe.Close()
|
|
http.Close()
|
|
return err
|
|
}
|
|
|
|
// Shutdown causes Serve() to exit, cleaning up behind itself.
|
|
func (s *Server) Shutdown() {
|
|
select {
|
|
case s.errs <- nil:
|
|
default:
|
|
}
|
|
}
|