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