-
Notifications
You must be signed in to change notification settings - Fork 181
/
pixiecore.go
274 lines (242 loc) · 7.87 KB
/
pixiecore.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// 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:
}
}