github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/boot/netboot/ipxe/ipxe.go (about) 1 // Copyright 2017-2019 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package ipxe implements a trivial IPXE config file parser. 6 package ipxe 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "io" 13 "net/url" 14 "path" 15 "strings" 16 17 "github.com/mvdan/u-root-coreutils/pkg/boot" 18 "github.com/mvdan/u-root-coreutils/pkg/curl" 19 "github.com/mvdan/u-root-coreutils/pkg/uio" 20 "github.com/mvdan/u-root-coreutils/pkg/ulog" 21 ) 22 23 // ErrNotIpxeScript is returned when the config file is not an 24 // ipxe script. 25 var ErrNotIpxeScript = errors.New("config file is not ipxe as it does not start with #!ipxe") 26 27 // parser encapsulates a parsed ipxe configuration file. 28 // 29 // We currently only support kernel and initrd commands. 30 type parser struct { 31 bootImage *boot.LinuxImage 32 33 // wd is the current working directory. 34 // 35 // Relative file paths are interpreted relative to this URL. 36 wd *url.URL 37 38 log ulog.Logger 39 40 schemes curl.Schemes 41 } 42 43 // ParseConfig returns a new configuration with the file at URL and default 44 // schemes. 45 // 46 // `s` is used to get files referred to by URLs in the configuration. 47 func ParseConfig(ctx context.Context, l ulog.Logger, configURL *url.URL, s curl.Schemes) (*boot.LinuxImage, error) { 48 c := &parser{ 49 schemes: s, 50 log: l, 51 } 52 if err := c.getAndParseFile(ctx, configURL); err != nil { 53 return nil, err 54 } 55 return c.bootImage, nil 56 } 57 58 // getAndParse parses the config file downloaded from `url` and fills in `c`. 59 func (c *parser) getAndParseFile(ctx context.Context, u *url.URL) error { 60 r, err := c.schemes.Fetch(ctx, u) 61 if err != nil { 62 return err 63 } 64 data, err := uio.ReadAll(r) 65 if err != nil { 66 return err 67 } 68 config := string(data) 69 if !strings.HasPrefix(config, "#!ipxe") { 70 return ErrNotIpxeScript 71 } 72 c.log.Printf("Got ipxe config file %s:\n%s\n", r, config) 73 74 // Parent dir of the config file. 75 c.wd = &url.URL{ 76 Scheme: u.Scheme, 77 Host: u.Host, 78 Path: path.Dir(u.Path), 79 } 80 return c.parseIpxe(config) 81 } 82 83 // getFile parses `surl` and returns an io.Reader for the requested url. 84 func (c *parser) getFile(surl string) (io.ReaderAt, error) { 85 u, err := parseURL(surl, c.wd) 86 if err != nil { 87 return nil, fmt.Errorf("could not parse URL %q: %v", surl, err) 88 } 89 return c.schemes.LazyFetch(u) 90 } 91 92 func parseURL(name string, wd *url.URL) (*url.URL, error) { 93 u, err := url.Parse(name) 94 if err != nil { 95 return nil, fmt.Errorf("could not parse URL %q: %v", name, err) 96 } 97 98 // If it parsed, but it didn't have a Scheme or Host, use the working 99 // directory's values. 100 if len(u.Scheme) == 0 && wd != nil { 101 u.Scheme = wd.Scheme 102 103 if len(u.Host) == 0 { 104 // If this is not there, it was likely just a path. 105 u.Host = wd.Host 106 107 // Absolute file names don't get the parent 108 // directories, just the host and scheme. 109 if !path.IsAbs(name) { 110 u.Path = path.Join(wd.Path, path.Clean(u.Path)) 111 } 112 } 113 } 114 return u, nil 115 } 116 117 func (c *parser) createInitrd(initrds []io.ReaderAt) { 118 if len(initrds) > 0 { 119 c.bootImage.Initrd = boot.CatInitrds(initrds...) 120 } 121 } 122 123 // parseIpxe parses `config` and constructs a BootImage for `c`. 124 func (c *parser) parseIpxe(config string) error { 125 // A trivial ipxe script parser. 126 // Currently only supports kernel and initrd commands. 127 c.bootImage = &boot.LinuxImage{} 128 129 var initrds []io.ReaderAt 130 for _, line := range strings.Split(config, "\n") { 131 // Skip blank lines and comment lines. 132 line = strings.TrimSpace(line) 133 if line == "" || line[0] == '#' { 134 continue 135 } 136 137 args := strings.Fields(line) 138 if len(args) == 0 { 139 continue 140 } 141 cmd := strings.ToLower(args[0]) 142 143 switch cmd { 144 case "kernel": 145 if len(args) > 1 { 146 k, err := c.getFile(args[1]) 147 if err != nil { 148 return err 149 } 150 c.bootImage.Kernel = k 151 } 152 153 // Add cmdline if there are any. 154 if len(args) > 2 { 155 c.bootImage.Cmdline = strings.Join(args[2:], " ") 156 } 157 158 case "initrd": 159 if len(args) > 1 { 160 for _, f := range strings.Split(args[1], ",") { 161 i, err := c.getFile(f) 162 if err != nil { 163 return err 164 } 165 initrds = append(initrds, i) 166 } 167 } 168 169 case "boot": 170 // Stop parsing at this point, we should go ahead and 171 // boot. 172 c.createInitrd(initrds) 173 return nil 174 175 default: 176 c.log.Printf("Ignoring unsupported ipxe cmd: %s", line) 177 } 178 } 179 180 // EOF - we should go ahead and boot. 181 c.createInitrd(initrds) 182 return nil 183 }