github.com/rck/u-root@v0.0.0-20180106144920-7eb602e381bb/pkg/pxe/pxe.go (about) 1 // Package pxe aims to implement the PXE specification. 2 // 3 // See http://www.pix.net/software/pxeboot/archive/pxespec.pdf 4 package pxe 5 6 import ( 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "net" 13 "net/url" 14 "path" 15 "strings" 16 ) 17 18 var ( 19 // ErrConfigNotFound is returned when no suitable configuration file 20 // was found by AppendFile. 21 ErrConfigNotFound = errors.New("configuration file was not found") 22 23 // ErrDefaultEntryNotFound is returned when the configuration file 24 // names a default label that is not part of the configuration. 25 ErrDefaultEntryNotFound = errors.New("default label not found in configuration") 26 ) 27 28 // Entry encapsulates a Syslinux "label" config directive. 29 type Entry struct { 30 // Kernel is the kernel for this label. 31 Kernel io.Reader 32 33 // Initrd is the initial ramdisk for this label. 34 Initrd io.Reader 35 36 // Cmdline is the list of kernel command line parameters. 37 Cmdline string 38 } 39 40 // Config encapsulates a parsed Syslinux configuration file. 41 // 42 // See http://www.syslinux.org/wiki/index.php?title=Config for the 43 // configuration file specification. 44 // 45 // TODO: Tear apart parser internals from Config. 46 type Config struct { 47 // Entries is a map of label name -> label configuration. 48 Entries map[string]*Entry 49 50 // DefaultEntry is the default label key to use. 51 // 52 // If DefaultEntry is non-empty, the label is guaranteed to exist in 53 // `Entries`. 54 DefaultEntry string 55 56 // Parser internals. 57 globalAppend string 58 scope scope 59 curEntry string 60 wd *url.URL 61 schemes Schemes 62 } 63 64 type scope uint8 65 66 const ( 67 scopeGlobal scope = iota 68 scopeEntry 69 ) 70 71 // NewConfig returns a new PXE parser using working directory `wd` and default 72 // schemes. 73 // 74 // See NewConfigWithSchemes for more details. 75 func NewConfig(wd *url.URL) *Config { 76 return NewConfigWithSchemes(wd, DefaultSchemes) 77 } 78 79 // NewConfigWithSchemes returns a new PXE parser using working directory `wd` 80 // and schemes `s`. 81 // 82 // If a path encountered in a configuration file is relative instead of a full 83 // URI, `wd` is used as the "working directory" of that relative path; the 84 // resulting URI is roughly `wd.String()/path`. 85 // 86 // `s` is used to get files referred to by URIs. 87 func NewConfigWithSchemes(wd *url.URL, s Schemes) *Config { 88 return &Config{ 89 Entries: make(map[string]*Entry), 90 scope: scopeGlobal, 91 wd: wd, 92 schemes: s, 93 } 94 } 95 96 // FindConfigFile probes for config files based on the Mac and IP given. 97 func (c *Config) FindConfigFile(mac net.HardwareAddr, ip net.IP) error { 98 for _, relname := range probeFiles(mac, ip) { 99 if err := c.AppendFile(relname); err != ErrConfigNotFound { 100 return err 101 } 102 } 103 return fmt.Errorf("no valid pxelinux config found") 104 } 105 106 // ParseConfigFile parses a PXE/Syslinux configuration as specified in 107 // http://www.syslinux.org/wiki/index.php?title=Config 108 // 109 // Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD 110 // directives are partially supported. 111 // 112 // `wd` is the default scheme, host, and path for any files named as a 113 // relative path. The default path for config files is assumed to be 114 // `wd.Path`/pxelinux.cfg/. 115 func ParseConfigFile(uri string, wd *url.URL) (*Config, error) { 116 c := NewConfig(wd) 117 if err := c.AppendFile(uri); err != nil { 118 return nil, err 119 } 120 return c, nil 121 } 122 123 // AppendFile parses the config file downloaded from `uri` and adds it to `c`. 124 func (c *Config) AppendFile(uri string) error { 125 cfgWd := *c.wd 126 // The default location for looking for configuration is 127 // CWD/pxelinux.cfg/, so if `uri` is just a relative path, this will be 128 // the default directory. 129 cfgWd.Path = path.Join(cfgWd.Path, "pxelinux.cfg") 130 131 r, err := c.schemes.GetFile(uri, &cfgWd) 132 if err != nil { 133 return ErrConfigNotFound 134 } 135 config, err := ioutil.ReadAll(r) 136 if err != nil { 137 return err 138 } 139 return c.Append(string(config)) 140 } 141 142 // Append parses `config` and adds the respective configuration to `c`. 143 func (c *Config) Append(config string) error { 144 // Here's a shitty parser. 145 for _, line := range strings.Split(config, "\n") { 146 // This is stupid. There should be a FieldsN(...). 147 kv := strings.Fields(line) 148 if len(kv) <= 1 { 149 continue 150 } 151 directive := strings.ToLower(kv[0]) 152 var arg string 153 if len(kv) == 2 { 154 arg = kv[1] 155 } else { 156 arg = strings.Join(kv[1:], " ") 157 } 158 159 switch directive { 160 case "default": 161 c.DefaultEntry = arg 162 163 case "include": 164 if err := c.AppendFile(arg); err == ErrConfigNotFound { 165 // TODO: What to do in this case? 166 // Return for now. Test this with pxelinux. 167 return err 168 } else if err != nil { 169 return err 170 } 171 172 case "label": 173 // We forever enter label scope. 174 c.scope = scopeEntry 175 c.curEntry = arg 176 c.Entries[c.curEntry] = &Entry{} 177 c.Entries[c.curEntry].Cmdline = c.globalAppend 178 179 case "kernel": 180 k, err := c.schemes.LazyGetFile(arg, c.wd) 181 if err != nil { 182 return err 183 } 184 c.Entries[c.curEntry].Kernel = k 185 186 case "initrd": 187 i, err := c.schemes.LazyGetFile(arg, c.wd) 188 if err != nil { 189 return err 190 } 191 c.Entries[c.curEntry].Initrd = i 192 193 case "append": 194 switch c.scope { 195 case scopeGlobal: 196 c.globalAppend = arg 197 198 case scopeEntry: 199 if arg == "-" { 200 c.Entries[c.curEntry].Cmdline = "" 201 } else { 202 c.Entries[c.curEntry].Cmdline = arg 203 } 204 } 205 } 206 } 207 208 // Go through all labels and download the initrds. 209 for _, label := range c.Entries { 210 // If the initrd was set via the INITRD directive, don't 211 // overwrite that. 212 // 213 // TODO(hugelgupf): Is this really what syslinux does? Does 214 // INITRD trump cmdline? Does it trump global? What if both the 215 // directive and cmdline initrd= are set? Does it depend on the 216 // order in the config file? (My current best guess: order.) 217 if label.Initrd != nil { 218 continue 219 } 220 221 for _, opt := range strings.Fields(label.Cmdline) { 222 optkv := strings.Split(opt, "=") 223 if optkv[0] != "initrd" { 224 continue 225 } 226 227 i, err := c.schemes.LazyGetFile(optkv[1], c.wd) 228 if err != nil { 229 return err 230 } 231 label.Initrd = i 232 } 233 } 234 235 if len(c.DefaultEntry) > 0 { 236 if _, ok := c.Entries[c.DefaultEntry]; !ok { 237 return ErrDefaultEntryNotFound 238 } 239 } 240 return nil 241 242 } 243 244 func probeFiles(ethernetMac net.HardwareAddr, ip net.IP) []string { 245 files := make([]string, 0, 10) 246 // Skipping client UUID. Figure that out later. 247 248 // MAC address. 249 files = append(files, fmt.Sprintf("01-%s", strings.ToLower(strings.Replace(ethernetMac.String(), ":", "-", -1)))) 250 251 // IP address in upper case hex, chopping one letter off at a time. 252 ipf := strings.ToUpper(hex.EncodeToString(ip)) 253 for n := len(ipf); n >= 1; n-- { 254 files = append(files, ipf[:n]) 255 } 256 files = append(files, "default") 257 return files 258 }