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  `