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