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