github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/pkg/esxi/esxi.go (about)

     1  // Copyright 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 esxi contains an ESXi boot config parser for disks and CDROMs.
     6  //
     7  // For CDROMs, it parses the boot.cfg found in the root directory and tries to
     8  // boot from it.
     9  //
    10  // For disks, there may be multiple boot partitions:
    11  //
    12  // - Locates both <device>5/boot.cfg and <device>6/boot.cfg.
    13  //
    14  // - If parsable, chooses partition with bootstate=(0|2|empty) and greater
    15  // updated=N.
    16  //
    17  // Sometimes, an ESXi partition can contain a valid boot.cfg, but not actually
    18  // any of the named modules. Hence it is important to try fully loading ESXi
    19  // into memory, and only then falling back to the other partition.
    20  //
    21  // Only boots partitions with bootstate=0, bootstate=2, bootstate=(empty) will
    22  // boot at all.
    23  //
    24  // Most of the parsing logic in this package comes from
    25  // https://github.com/vmware/esx-boot/blob/master/safeboot/bootbank.c
    26  package esxi
    27  
    28  import (
    29  	"bufio"
    30  	"encoding/hex"
    31  	"fmt"
    32  	"io"
    33  	"io/ioutil"
    34  	"os"
    35  	"path/filepath"
    36  	"strconv"
    37  	"strings"
    38  
    39  	"golang.org/x/sys/unix"
    40  
    41  	"github.com/u-root/u-root/pkg/boot"
    42  	"github.com/u-root/u-root/pkg/gpt"
    43  	"github.com/u-root/u-root/pkg/mount"
    44  )
    45  
    46  // LoadDisk loads the right ESXi multiboot kernel from partitions 5 or 6 of the
    47  // given device.
    48  //
    49  // The kernels are returned in the priority order according to the bootstate
    50  // and updated values in their boot configurations.
    51  //
    52  // The caller should try loading all returned images in order, as some of them
    53  // may not be valid.
    54  //
    55  // device5 and device6 will be mounted at temporary directories.
    56  func LoadDisk(device string) ([]*boot.MultibootImage, error) {
    57  	opts5, err5 := mountPartition(fmt.Sprintf("%s5", device))
    58  	opts6, err6 := mountPartition(fmt.Sprintf("%s6", device))
    59  	if err5 != nil && err6 != nil {
    60  		return nil, fmt.Errorf("could not mount or read either partition 5 (%v) or partition 6 (%v)", err5, err6)
    61  	}
    62  	return getImages(device, opts5, opts6)
    63  }
    64  
    65  func getImages(device string, opts5, opts6 *options) ([]*boot.MultibootImage, error) {
    66  	var (
    67  		img5, img6 *boot.MultibootImage
    68  		err5, err6 error
    69  	)
    70  	if opts5 != nil {
    71  		img5, err5 = getBootImage(*opts5, device, 5)
    72  	}
    73  	if opts6 != nil {
    74  		img6, err6 = getBootImage(*opts6, device, 6)
    75  	}
    76  	if img5 == nil && img6 == nil {
    77  		return nil, fmt.Errorf("could not read boot configs on partition 5 (%v) or partition 6 (%v)", err5, err6)
    78  	}
    79  
    80  	if img5 != nil && img6 != nil {
    81  		if opts6.updated > opts5.updated {
    82  			return []*boot.MultibootImage{img6, img5}, nil
    83  		}
    84  		return []*boot.MultibootImage{img5, img6}, nil
    85  	} else if img5 != nil {
    86  		return []*boot.MultibootImage{img5}, nil
    87  	}
    88  	return []*boot.MultibootImage{img6}, nil
    89  }
    90  
    91  // LoadCDROM loads an ESXi multiboot kernel from a CDROM at device.
    92  //
    93  // device will be mounted at mountPoint.
    94  func LoadCDROM(device string) (*boot.MultibootImage, error) {
    95  	mountPoint, err := ioutil.TempDir("", "esxi-mount-")
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	if err := mount.Mount(device, mountPoint, "iso9660", "", unix.MS_RDONLY|unix.MS_NOATIME); err != nil {
   100  		return nil, err
   101  	}
   102  	// Don't pass the device to ESXi. It doesn't need it.
   103  	return LoadConfig(filepath.Join(mountPoint, "boot.cfg"))
   104  }
   105  
   106  // LoadConfig loads an ESXi configuration from configFile.
   107  func LoadConfig(configFile string) (*boot.MultibootImage, error) {
   108  	opts, err := parse(configFile)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("cannot parse config at %s: %v", configFile, err)
   111  	}
   112  	return getBootImage(opts, "", 0)
   113  }
   114  
   115  func mountPartition(dev string) (*options, error) {
   116  	base := filepath.Base(dev)
   117  	mountPoint, err := ioutil.TempDir("", fmt.Sprintf("%s-", base))
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	if err := mount.Mount(dev, mountPoint, "vfat", "", unix.MS_RDONLY|unix.MS_NOATIME); err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	configFile := filepath.Join(mountPoint, "boot.cfg")
   126  	opts, err := parse(configFile)
   127  	if err != nil {
   128  		return nil, fmt.Errorf("cannot parse config at %s: %v", configFile, err)
   129  	}
   130  	return &opts, nil
   131  }
   132  
   133  func getBootImage(opts options, device string, partition int) (*boot.MultibootImage, error) {
   134  	// Only valid and upgrading are bootable partitions.
   135  	//
   136  	// We are supposed to support the following two state transitions (only
   137  	// one transition every boot!):
   138  	//
   139  	// upgrading -> dirty
   140  	// dirty -> invalid
   141  	//
   142  	// A validly booted system will set its own bootstate to "valid" from
   143  	// "dirty".
   144  	//
   145  	// We currently don't support writing the state back to disk, which is
   146  	// fine in our manual testing.
   147  	if opts.bootstate != bootValid && opts.bootstate != bootUpgrading {
   148  		return nil, fmt.Errorf("boot state %d invalid", opts.bootstate)
   149  	}
   150  
   151  	if len(device) > 0 {
   152  		if err := opts.addUUID(device, partition); err != nil {
   153  			return nil, fmt.Errorf("cannot add boot uuid of %s: %v", device, err)
   154  		}
   155  	}
   156  	return &boot.MultibootImage{
   157  		Path:    opts.kernel,
   158  		Cmdline: opts.args,
   159  		Modules: opts.modules,
   160  	}, nil
   161  }
   162  
   163  type options struct {
   164  	kernel    string
   165  	args      string
   166  	modules   []string
   167  	updated   int
   168  	bootstate bootstate
   169  }
   170  
   171  type bootstate int
   172  
   173  // From safeboot.c
   174  const (
   175  	bootValid     bootstate = 0
   176  	bootUpgrading bootstate = 1
   177  	bootDirty     bootstate = 2
   178  	bootInvalid   bootstate = 3
   179  )
   180  
   181  // So tests can replace this and don't have to have actual block devices.
   182  var getBlockSize = gpt.GetBlockSize
   183  
   184  func getUUID(device string, partition int) (string, error) {
   185  	device = strings.TrimRight(device, "/")
   186  	blockSize, err := getBlockSize(device)
   187  	if err != nil {
   188  		return "", err
   189  	}
   190  
   191  	f, err := os.Open(fmt.Sprintf("%s%d", device, partition))
   192  	if err != nil {
   193  		return "", err
   194  	}
   195  
   196  	// Boot uuid is stored in the second block of the disk
   197  	// in the following format:
   198  	//
   199  	// VMWARE FAT16    <uuid>
   200  	// <---128 bit----><128 bit>
   201  	data := make([]byte, uuidSize)
   202  	n, err := f.ReadAt(data, int64(blockSize))
   203  	if err != nil {
   204  		return "", err
   205  	}
   206  	if n != uuidSize {
   207  		return "", io.ErrUnexpectedEOF
   208  	}
   209  
   210  	if magic := string(data[:len(uuidMagic)]); magic != uuidMagic {
   211  		return "", fmt.Errorf("bad uuid magic %q, want %q", magic, uuidMagic)
   212  	}
   213  
   214  	uuid := hex.EncodeToString(data[len(uuidMagic):])
   215  	return fmt.Sprintf("bootUUID=%s", uuid), nil
   216  }
   217  
   218  func (o *options) addUUID(device string, partition int) error {
   219  	uuid, err := getUUID(device, partition)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	o.args += " " + uuid
   224  	return nil
   225  }
   226  
   227  const (
   228  	comment = '#'
   229  	sep     = "---"
   230  
   231  	uuidMagic = "VMWARE FAT16    "
   232  	uuidSize  = 32
   233  )
   234  
   235  func parse(configFile string) (options, error) {
   236  	dir := filepath.Dir(configFile)
   237  
   238  	f, err := os.Open(configFile)
   239  	if err != nil {
   240  		return options{}, err
   241  	}
   242  	defer f.Close()
   243  
   244  	// An empty or missing updated value is always 0, so we can let the
   245  	// ints be initialized to 0.
   246  	//
   247  	// see esx-boot/bootlib/parse.c:parse_config_file.
   248  	opt := options{
   249  		// Default value taken from
   250  		// esx-boot/safeboot/bootbank.c:bank_scan.
   251  		bootstate: bootInvalid,
   252  	}
   253  
   254  	scanner := bufio.NewScanner(f)
   255  	for scanner.Scan() {
   256  		line := scanner.Text()
   257  		line = strings.TrimSpace(line)
   258  
   259  		if len(line) == 0 || line[0] == comment {
   260  			continue
   261  		}
   262  
   263  		tokens := strings.SplitN(line, "=", 2)
   264  		if len(tokens) != 2 {
   265  			return opt, fmt.Errorf("bad line %q", line)
   266  		}
   267  		key := strings.TrimSpace(tokens[0])
   268  		val := strings.TrimSpace(tokens[1])
   269  		switch key {
   270  		case "kernel":
   271  			opt.kernel = filepath.Join(dir, val)
   272  		case "kernelopt":
   273  			opt.args = val
   274  		case "updated":
   275  			if len(val) == 0 {
   276  				// Explicitly setting to 0, as in
   277  				// esx-boot/bootlib/parse.c:parse_config_file,
   278  				// in case this value is specified twice.
   279  				opt.updated = 0
   280  			} else {
   281  				n, err := strconv.Atoi(val)
   282  				if err != nil {
   283  					return options{}, err
   284  				}
   285  				opt.updated = n
   286  			}
   287  		case "bootstate":
   288  			if len(val) == 0 {
   289  				// Explicitly setting to valid, as in
   290  				// esx-boot/bootlib/parse.c:parse_config_file,
   291  				// in case this value is specified twice.
   292  				opt.bootstate = bootValid
   293  			} else {
   294  				n, err := strconv.Atoi(val)
   295  				if err != nil {
   296  					return options{}, err
   297  				}
   298  				if n < 0 || n > 3 {
   299  					opt.bootstate = bootInvalid
   300  				} else {
   301  					opt.bootstate = bootstate(n)
   302  				}
   303  			}
   304  		case "modules":
   305  			for _, tok := range strings.Split(val, sep) {
   306  				// Each module is "filename arg0 arg1 arg2" and
   307  				// the filename is relative to the directory
   308  				// the module is in.
   309  				tok = strings.TrimSpace(tok)
   310  				if len(tok) > 0 {
   311  					entry := strings.Fields(tok)
   312  					entry[0] = filepath.Join(dir, entry[0])
   313  					opt.modules = append(opt.modules, strings.Join(entry, " "))
   314  				}
   315  			}
   316  		}
   317  	}
   318  
   319  	err = scanner.Err()
   320  	return opt, err
   321  }