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 }