github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/lib/filesystem/util/writeRaw.go (about) 1 package util 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "sync" 15 "syscall" 16 "text/template" 17 "time" 18 "unsafe" 19 20 "github.com/Cloud-Foundations/Dominator/lib/constants" 21 "github.com/Cloud-Foundations/Dominator/lib/filesystem" 22 "github.com/Cloud-Foundations/Dominator/lib/format" 23 "github.com/Cloud-Foundations/Dominator/lib/fsutil" 24 "github.com/Cloud-Foundations/Dominator/lib/hash" 25 "github.com/Cloud-Foundations/Dominator/lib/log" 26 "github.com/Cloud-Foundations/Dominator/lib/mbr" 27 "github.com/Cloud-Foundations/Dominator/lib/objectserver" 28 "github.com/Cloud-Foundations/Dominator/lib/wsyscall" 29 ) 30 31 const ( 32 BLKGETSIZE = 0x00001260 33 createFlags = os.O_CREATE | os.O_TRUNC | os.O_RDWR 34 ) 35 36 var ( 37 mutex sync.Mutex 38 defaultMkfsFeatures map[string]struct{} // Key: feature name. 39 grubTemplate = template.Must(template.New("grub").Parse( 40 grubTemplateString)) 41 ) 42 43 func checkIfPartition(device string) (bool, error) { 44 if isBlock, err := checkIsBlock(device); err != nil { 45 if !os.IsNotExist(err) { 46 return false, err 47 } 48 return false, nil 49 } else if !isBlock { 50 return false, fmt.Errorf("%s is not a block device", device) 51 } else { 52 return true, nil 53 } 54 } 55 56 func checkIsBlock(filename string) (bool, error) { 57 if fi, err := os.Stat(filename); err != nil { 58 if !os.IsNotExist(err) { 59 return false, fmt.Errorf("error stating: %s: %s", filename, err) 60 } 61 return false, err 62 } else { 63 return fi.Mode()&os.ModeDevice == os.ModeDevice, nil 64 } 65 } 66 67 func findExecutable(rootDir, file string) error { 68 if d, err := os.Stat(filepath.Join(rootDir, file)); err != nil { 69 return err 70 } else { 71 if m := d.Mode(); !m.IsDir() && m&0111 != 0 { 72 return nil 73 } 74 return os.ErrPermission 75 } 76 } 77 78 func getBootDirectory(fs *filesystem.FileSystem) ( 79 *filesystem.DirectoryInode, error) { 80 if fs.EntriesByName == nil { 81 fs.BuildEntryMap() 82 } 83 dirent, ok := fs.EntriesByName["boot"] 84 if !ok { 85 return nil, errors.New("missing /boot directory") 86 } 87 bootDirectory, ok := dirent.Inode().(*filesystem.DirectoryInode) 88 if !ok { 89 return nil, errors.New("/boot is not a directory") 90 } 91 return bootDirectory, nil 92 } 93 94 func getDefaultMkfsFeatures(device, size string, logger log.Logger) ( 95 map[string]struct{}, error) { 96 mutex.Lock() 97 defer mutex.Unlock() 98 if defaultMkfsFeatures == nil { 99 startTime := time.Now() 100 logger.Println("Making calibration file-system") 101 cmd := exec.Command("mkfs.ext4", "-L", "calibration-fs", "-i", "65536", 102 device, size) 103 if output, err := cmd.CombinedOutput(); err != nil { 104 return nil, fmt.Errorf( 105 "error making calibration file-system on: %s: %s: %s", 106 device, err, output) 107 } 108 logger.Printf("Made calibration file-system in %s\n", 109 format.Duration(time.Since(startTime))) 110 cmd = exec.Command("dumpe2fs", "-h", device) 111 output, err := cmd.CombinedOutput() 112 if err != nil { 113 return nil, fmt.Errorf("error dumping file-system info: %s: %s", 114 err, output) 115 } 116 defaultMkfsFeatures = make(map[string]struct{}) 117 for _, line := range strings.Split(string(output), "\n") { 118 fields := strings.Fields(line) 119 if len(fields) < 3 { 120 continue 121 } 122 if fields[0] != "Filesystem" || fields[1] != "features:" { 123 continue 124 } 125 for _, field := range fields[2:] { 126 defaultMkfsFeatures[field] = struct{}{} 127 } 128 break 129 } 130 // Scrub out the calibration file-system. 131 buffer := make([]byte, 65536) 132 if file, err := os.OpenFile(device, os.O_WRONLY, 0); err == nil { 133 file.Write(buffer) 134 file.Close() 135 } 136 } 137 return defaultMkfsFeatures, nil 138 } 139 140 func getUnsupportedOptions(fs *filesystem.FileSystem, 141 objectsGetter objectserver.ObjectsGetter) ([]string, error) { 142 bootDirectory, err := getBootDirectory(fs) 143 if err != nil { 144 return nil, err 145 } 146 dirent, ok := bootDirectory.EntriesByName["ext4.unsupported-features"] 147 var unsupportedOptions []string 148 if ok { 149 if inode, ok := dirent.Inode().(*filesystem.RegularInode); ok { 150 hashes := []hash.Hash{inode.Hash} 151 objectsReader, err := objectsGetter.GetObjects(hashes) 152 if err != nil { 153 return nil, err 154 } 155 defer objectsReader.Close() 156 size, reader, err := objectsReader.NextObject() 157 if err != nil { 158 return nil, err 159 } 160 defer reader.Close() 161 if size > 1024 { 162 return nil, 163 errors.New("/boot/ext4.unsupported-features is too large") 164 } 165 for { 166 var option string 167 _, err := fmt.Fscanf(reader, "%s\n", &option) 168 if err != nil { 169 if err == io.EOF { 170 break 171 } 172 return nil, err 173 } else { 174 unsupportedOptions = append(unsupportedOptions, 175 strings.Map(sanitiseInput, option)) 176 } 177 } 178 } 179 } 180 return unsupportedOptions, nil 181 } 182 183 func getRootPartition(bootDevice string) (string, error) { 184 if isPartition, err := checkIfPartition(bootDevice + "p1"); err != nil { 185 return "", err 186 } else if isPartition { 187 return bootDevice + "p1", nil 188 } 189 if isPartition, err := checkIfPartition(bootDevice + "1"); err != nil { 190 return "", err 191 } else if !isPartition { 192 return "", errors.New("no root partition found") 193 } else { 194 return bootDevice + "1", nil 195 } 196 } 197 198 func getDeviceSize(device string) (uint64, error) { 199 fd, err := syscall.Open(device, os.O_RDONLY|syscall.O_CLOEXEC, 0666) 200 if err != nil { 201 return 0, fmt.Errorf("error opening: %s: %s", device, err) 202 } 203 defer syscall.Close(fd) 204 var statbuf syscall.Stat_t 205 if err := syscall.Fstat(fd, &statbuf); err != nil { 206 return 0, fmt.Errorf("error stating: %s: %s\n", device, err) 207 } else if statbuf.Mode&syscall.S_IFMT != syscall.S_IFBLK { 208 return 0, fmt.Errorf("%s is not a block device, mode: %0o", 209 device, statbuf.Mode) 210 } 211 var blk uint64 212 err = wsyscall.Ioctl(fd, BLKGETSIZE, uintptr(unsafe.Pointer(&blk))) 213 if err != nil { 214 return 0, fmt.Errorf("error geting device size: %s: %s", device, err) 215 } 216 return blk << 9, nil 217 } 218 219 func lookPath(rootDir, file string) (string, error) { 220 if strings.Contains(file, "/") { 221 if err := findExecutable(rootDir, file); err != nil { 222 return "", err 223 } 224 return file, nil 225 } 226 path := os.Getenv("PATH") 227 for _, dir := range filepath.SplitList(path) { 228 if dir == "" { 229 dir = "." // Unix shell semantics: path element "" means "." 230 } 231 path := filepath.Join(dir, file) 232 if err := findExecutable(rootDir, path); err == nil { 233 return path, nil 234 } 235 } 236 return "", fmt.Errorf("(chroot=%s) %s not found in PATH", rootDir, file) 237 } 238 239 func makeAndWriteRoot(fs *filesystem.FileSystem, 240 objectsGetter objectserver.ObjectsGetter, bootDevice, rootDevice string, 241 options WriteRawOptions, logger log.DebugLogger) error { 242 unsupportedOptions, err := getUnsupportedOptions(fs, objectsGetter) 243 if err != nil { 244 return err 245 } 246 var bootInfo *BootInfoType 247 if options.RootLabel == "" { 248 options.RootLabel = fmt.Sprintf("rootfs@%x", time.Now().Unix()) 249 } 250 if options.InstallBootloader { 251 var err error 252 bootInfo, err = getBootInfo(fs, options.RootLabel, "net.ifnames=0") 253 if err != nil { 254 return err 255 } 256 } 257 err = makeExt4fs(rootDevice, options.RootLabel, unsupportedOptions, 8192, 258 logger) 259 if err != nil { 260 return err 261 } 262 mountPoint, err := ioutil.TempDir("", "write-raw-image") 263 if err != nil { 264 return err 265 } 266 defer os.RemoveAll(mountPoint) 267 err = wsyscall.Mount(rootDevice, mountPoint, "ext4", 0, "") 268 if err != nil { 269 return fmt.Errorf("error mounting: %s", rootDevice) 270 } 271 defer syscall.Unmount(mountPoint, 0) 272 os.RemoveAll(filepath.Join(mountPoint, "lost+found")) 273 if err := Unpack(fs, objectsGetter, mountPoint, logger); err != nil { 274 return err 275 } 276 if err := writeImageName(mountPoint, options.InitialImageName); err != nil { 277 return err 278 } 279 if options.WriteFstab { 280 err := writeRootFstabEntry(mountPoint, options.RootLabel) 281 if err != nil { 282 return err 283 } 284 } 285 if options.InstallBootloader { 286 err := bootInfo.installBootloader(bootDevice, mountPoint, 287 options.RootLabel, options.DoChroot, logger) 288 if err != nil { 289 return err 290 } 291 } 292 return nil 293 } 294 295 func makeBootable(fs *filesystem.FileSystem, 296 deviceName, rootLabel, rootDir, kernelOptions string, 297 doChroot bool, logger log.DebugLogger) error { 298 if err := writeRootFstabEntry(rootDir, rootLabel); err != nil { 299 return err 300 } 301 if bootInfo, err := getBootInfo(fs, rootLabel, kernelOptions); err != nil { 302 return err 303 } else { 304 return bootInfo.installBootloader(deviceName, rootDir, rootLabel, 305 doChroot, logger) 306 } 307 } 308 309 func makeExt4fs(deviceName, label string, unsupportedOptions []string, 310 bytesPerInode uint64, logger log.Logger) error { 311 size, err := getDeviceSize(deviceName) 312 if err != nil { 313 return err 314 } 315 sizeString := strconv.FormatUint(size>>10, 10) 316 var options []string 317 if len(unsupportedOptions) > 0 { 318 defaultFeatures, err := getDefaultMkfsFeatures(deviceName, sizeString, 319 logger) 320 if err != nil { 321 return err 322 } 323 for _, option := range unsupportedOptions { 324 if _, ok := defaultFeatures[option]; ok { 325 options = append(options, "^"+option) 326 } 327 } 328 } 329 cmd := exec.Command("mkfs.ext4") 330 if bytesPerInode != 0 { 331 cmd.Args = append(cmd.Args, "-i", strconv.FormatUint(bytesPerInode, 10)) 332 } 333 if label != "" { 334 cmd.Args = append(cmd.Args, "-L", label) 335 } 336 if len(options) > 0 { 337 cmd.Args = append(cmd.Args, "-O", strings.Join(options, ",")) 338 } 339 cmd.Args = append(cmd.Args, deviceName, sizeString) 340 startTime := time.Now() 341 if output, err := cmd.CombinedOutput(); err != nil { 342 return fmt.Errorf("error making file-system on: %s: %s: %s", 343 deviceName, err, output) 344 } 345 logger.Printf("Made %s file-system on: %s in %s\n", 346 format.FormatBytes(size), deviceName, 347 format.Duration(time.Since(startTime))) 348 return nil 349 } 350 351 func sanitiseInput(ch rune) rune { 352 if 'a' <= ch && ch <= 'z' { 353 return ch 354 } else if '0' <= ch && ch <= '9' { 355 return ch 356 } else if ch == '_' { 357 return ch 358 } else { 359 return -1 360 } 361 } 362 363 func getBootInfo(fs *filesystem.FileSystem, rootLabel string, 364 extraKernelOptions string) (*BootInfoType, error) { 365 bootDirectory, err := getBootDirectory(fs) 366 if err != nil { 367 return nil, err 368 } 369 bootInfo := &BootInfoType{ 370 BootDirectory: bootDirectory, 371 KernelOptions: MakeKernelOptions("LABEL="+rootLabel, 372 extraKernelOptions), 373 } 374 for _, dirent := range bootDirectory.EntryList { 375 if strings.HasPrefix(dirent.Name, "initrd.img-") || 376 strings.HasPrefix(dirent.Name, "initramfs-") { 377 if bootInfo.InitrdImageFile != "" { 378 return nil, errors.New("multiple initrd images") 379 } 380 bootInfo.InitrdImageDirent = dirent 381 bootInfo.InitrdImageFile = "/boot/" + dirent.Name 382 } 383 if strings.HasPrefix(dirent.Name, "vmlinuz-") { 384 if bootInfo.KernelImageFile != "" { 385 return nil, errors.New("multiple kernel images") 386 } 387 bootInfo.KernelImageDirent = dirent 388 bootInfo.KernelImageFile = "/boot/" + dirent.Name 389 } 390 } 391 return bootInfo, nil 392 } 393 394 func (bootInfo *BootInfoType) installBootloader(deviceName string, 395 rootDir, rootLabel string, doChroot bool, logger log.DebugLogger) error { 396 startTime := time.Now() 397 var bootDir, chrootDir string 398 if doChroot { 399 bootDir = "/boot" 400 chrootDir = rootDir 401 } else { 402 bootDir = filepath.Join(rootDir, "boot") 403 } 404 grubConfigFile := filepath.Join(rootDir, "boot", "grub", "grub.cfg") 405 grubInstaller, err := lookPath(chrootDir, "grub-install") 406 if err != nil { 407 grubInstaller, err = lookPath(chrootDir, "grub2-install") 408 if err != nil { 409 return fmt.Errorf("cannot find GRUB installer: %s", err) 410 } 411 grubConfigFile = filepath.Join(rootDir, "boot", "grub2", "grub.cfg") 412 } 413 cmd := exec.Command(grubInstaller, "--boot-directory="+bootDir, deviceName) 414 if doChroot { 415 cmd.Dir = "/" 416 cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: chrootDir} 417 logger.Debugf(0, "running(chroot=%s): %s %s\n", 418 chrootDir, cmd.Path, strings.Join(cmd.Args[1:], " ")) 419 } else { 420 logger.Debugf(0, "running: %s %s\n", 421 cmd.Path, strings.Join(cmd.Args[1:], " ")) 422 } 423 if output, err := cmd.CombinedOutput(); err != nil { 424 return fmt.Errorf("error installing GRUB on: %s: %s: %s", 425 deviceName, err, output) 426 } 427 logger.Printf("installed GRUB in %s\n", 428 format.Duration(time.Since(startTime))) 429 if err := bootInfo.writeGrubConfig(grubConfigFile); err != nil { 430 return err 431 } 432 return bootInfo.writeGrubTemplate(grubConfigFile + ".template") 433 } 434 435 func (bootInfo *BootInfoType) writeGrubConfig(filename string) error { 436 file, err := os.Create(filename) 437 if err != nil { 438 return fmt.Errorf("error creating GRUB config file: %s", err) 439 } 440 defer file.Close() 441 if err := grubTemplate.Execute(file, bootInfo); err != nil { 442 return err 443 } 444 return file.Close() 445 } 446 447 func (bootInfo *BootInfoType) writeGrubTemplate(filename string) error { 448 file, err := os.Create(filename) 449 if err != nil { 450 return fmt.Errorf("error creating GRUB config file template: %s", err) 451 } 452 defer file.Close() 453 if _, err := file.Write([]byte(grubTemplateString)); err != nil { 454 return err 455 } 456 return file.Close() 457 } 458 459 func (bootInfo *BootInfoType) writeBootloaderConfig(rootDir string, 460 logger log.Logger) error { 461 grubConfigFile := filepath.Join(rootDir, "boot", "grub", "grub.cfg") 462 _, err := lookPath("", "grub-install") 463 if err != nil { 464 _, err = lookPath("", "grub2-install") 465 if err != nil { 466 return fmt.Errorf("cannot find GRUB installer: %s", err) 467 } 468 grubConfigFile = filepath.Join(rootDir, "boot", "grub2", "grub.cfg") 469 } 470 if err := bootInfo.writeGrubConfig(grubConfigFile); err != nil { 471 return err 472 } 473 return bootInfo.writeGrubTemplate(grubConfigFile + ".template") 474 } 475 476 func writeFstabEntry(writer io.Writer, 477 source, mountPoint, fileSystemType, flags string, 478 dumpFrequency, checkOrder uint) error { 479 if flags == "" { 480 flags = "defaults" 481 } 482 _, err := fmt.Fprintf(writer, "%-22s %-10s %-5s %-10s %d %d\n", 483 source, mountPoint, fileSystemType, flags, dumpFrequency, checkOrder) 484 return err 485 } 486 487 func writeImageName(mountPoint, imageName string) error { 488 pathname := filepath.Join(mountPoint, constants.InitialImageNameFile) 489 if imageName == "" { 490 if err := os.Remove(pathname); err != nil { 491 if os.IsNotExist(err) { 492 return nil 493 } 494 return err 495 } 496 } 497 if err := os.MkdirAll(filepath.Dir(pathname), fsutil.DirPerms); err != nil { 498 return err 499 } 500 buffer := &bytes.Buffer{} 501 fmt.Fprintln(buffer, imageName) 502 return fsutil.CopyToFile(pathname, fsutil.PublicFilePerms, buffer, 0) 503 } 504 505 func writeToBlock(fs *filesystem.FileSystem, 506 objectsGetter objectserver.ObjectsGetter, bootDevice string, 507 tableType mbr.TableType, options WriteRawOptions, 508 logger log.DebugLogger) error { 509 if err := mbr.WriteDefault(bootDevice, tableType); err != nil { 510 return err 511 } 512 if rootDevice, err := getRootPartition(bootDevice); err != nil { 513 return err 514 } else { 515 return makeAndWriteRoot(fs, objectsGetter, bootDevice, rootDevice, 516 options, logger) 517 } 518 } 519 520 func writeToFile(fs *filesystem.FileSystem, 521 objectsGetter objectserver.ObjectsGetter, rawFilename string, 522 perm os.FileMode, tableType mbr.TableType, options WriteRawOptions, 523 logger log.DebugLogger) error { 524 tmpFilename := rawFilename + "~" 525 if file, err := os.OpenFile(tmpFilename, createFlags, perm); err != nil { 526 return err 527 } else { 528 file.Close() 529 defer os.Remove(tmpFilename) 530 } 531 usageEstimate := fs.EstimateUsage(0) 532 minBytes := usageEstimate + usageEstimate>>3 // 12% extra for good luck. 533 minBytes += options.MinimumFreeBytes 534 if options.RoundupPower < 24 { 535 options.RoundupPower = 24 // 16 MiB. 536 } 537 imageUnits := minBytes >> options.RoundupPower 538 if imageUnits<<options.RoundupPower < minBytes { 539 imageUnits++ 540 } 541 imageSize := imageUnits << options.RoundupPower 542 if err := os.Truncate(tmpFilename, int64(imageSize)); err != nil { 543 return err 544 } 545 if err := mbr.WriteDefault(tmpFilename, tableType); err != nil { 546 return err 547 } 548 loopDevice, err := fsutil.LoopbackSetup(tmpFilename) 549 if err != nil { 550 return err 551 } 552 defer fsutil.LoopbackDelete(loopDevice) 553 err = makeAndWriteRoot(fs, objectsGetter, loopDevice, loopDevice+"p1", 554 options, logger) 555 if options.AllocateBlocks { // mkfs discards blocks, so do this after. 556 if err := fsutil.Fallocate(tmpFilename, imageSize); err != nil { 557 return err 558 } 559 } 560 if err != nil { 561 return err 562 } 563 return os.Rename(tmpFilename, rawFilename) 564 } 565 566 func writeRaw(fs *filesystem.FileSystem, 567 objectsGetter objectserver.ObjectsGetter, rawFilename string, 568 perm os.FileMode, tableType mbr.TableType, options WriteRawOptions, 569 logger log.DebugLogger) error { 570 if isBlock, err := checkIsBlock(rawFilename); err != nil { 571 if !os.IsNotExist(err) { 572 return err 573 } 574 } else if isBlock { 575 return writeToBlock(fs, objectsGetter, rawFilename, tableType, 576 options, logger) 577 } 578 return writeToFile(fs, objectsGetter, rawFilename, perm, tableType, 579 options, logger) 580 } 581 582 func writeRootFstabEntry(rootDir, rootLabel string) error { 583 file, err := os.Create(filepath.Join(rootDir, "etc", "fstab")) 584 if err != nil { 585 return err 586 } else { 587 defer file.Close() 588 return writeFstabEntry(file, "LABEL="+rootLabel, "/", "ext4", 589 "", 0, 1) 590 } 591 } 592 593 const grubTemplateString string = `# Generated from simple template. 594 insmod serial 595 serial --unit=0 --speed=115200 596 terminal_output serial 597 set timeout=0 598 599 menuentry 'Linux' 'Solitary Linux' { 600 insmod gzio 601 insmod part_msdos 602 insmod ext2 603 echo 'Loading Linux {{.KernelImageFile}} ...' 604 linux {{.KernelImageFile}} {{.KernelOptions}} 605 echo 'Loading initial ramdisk ...' 606 initrd {{.InitrdImageFile}} 607 } 608 `