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  }