github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/helper.go (about)

     1  package statemachine
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"fmt"
     7  	"io/fs"
     8  	"math"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/diskfs/go-diskfs/disk"
    16  	"github.com/diskfs/go-diskfs/partition"
    17  	"github.com/diskfs/go-diskfs/partition/gpt"
    18  	"github.com/diskfs/go-diskfs/partition/mbr"
    19  	"github.com/go-git/go-git/v5"
    20  	"github.com/go-git/go-git/v5/plumbing"
    21  	"github.com/snapcore/snapd/gadget"
    22  	"github.com/snapcore/snapd/gadget/quantity"
    23  	"github.com/snapcore/snapd/seed"
    24  	"github.com/snapcore/snapd/timings"
    25  
    26  	"github.com/canonical/ubuntu-image/internal/helper"
    27  	"github.com/canonical/ubuntu-image/internal/imagedefinition"
    28  )
    29  
    30  const (
    31  	// schemaMBR identifies a Master Boot Record partitioning schema, or an
    32  	// MBR like role
    33  	schemaMBR = "mbr"
    34  	// schemaGPT identifies a GUID Partition Table partitioning schema
    35  	schemaGPT = "gpt"
    36  
    37  	bareStructure = "bare"
    38  )
    39  
    40  var runCmd = helper.RunCmd
    41  var blockSize string = "1"
    42  
    43  // validateInput ensures that command line flags for the state machine are valid. These
    44  // flags are applicable to all image types
    45  func (stateMachine *StateMachine) validateInput() error {
    46  	// Validate command line options
    47  	if stateMachine.stateMachineFlags.Thru != "" && stateMachine.stateMachineFlags.Until != "" {
    48  		return fmt.Errorf("cannot specify both --until and --thru")
    49  	}
    50  	if stateMachine.stateMachineFlags.WorkDir == "" && stateMachine.stateMachineFlags.Resume {
    51  		return fmt.Errorf("must specify workdir when using --resume flag")
    52  	}
    53  
    54  	logLevelFlags := []bool{stateMachine.commonFlags.Debug,
    55  		stateMachine.commonFlags.Verbose,
    56  		stateMachine.commonFlags.Quiet,
    57  	}
    58  
    59  	logLevels := 0
    60  	for _, logLevelFlag := range logLevelFlags {
    61  		if logLevelFlag {
    62  			logLevels++
    63  		}
    64  	}
    65  
    66  	if logLevels > 1 {
    67  		return fmt.Errorf("--quiet, --verbose, and --debug flags are mutually exclusive")
    68  	}
    69  
    70  	return nil
    71  }
    72  
    73  func (stateMachine *StateMachine) setConfDefDir(confFileArg string) error {
    74  	path, err := filepath.Abs(filepath.Dir(confFileArg))
    75  	if err != nil {
    76  		return fmt.Errorf("unable to determine the configuration definition directory: %w", err)
    77  	}
    78  	stateMachine.ConfDefPath = path
    79  
    80  	return nil
    81  }
    82  
    83  // validateUntilThru validates that the the state passed as --until
    84  // or --thru exists in the state machine's list of states
    85  func (stateMachine *StateMachine) validateUntilThru() error {
    86  	// if --until or --thru was given, make sure the specified state exists
    87  	var searchState string
    88  	var stateFound bool = false
    89  	if stateMachine.stateMachineFlags.Until != "" {
    90  		searchState = stateMachine.stateMachineFlags.Until
    91  	}
    92  	if stateMachine.stateMachineFlags.Thru != "" {
    93  		searchState = stateMachine.stateMachineFlags.Thru
    94  	}
    95  
    96  	if searchState != "" {
    97  		for _, state := range stateMachine.states {
    98  			if state.name == searchState {
    99  				stateFound = true
   100  				break
   101  			}
   102  		}
   103  		if !stateFound {
   104  			return fmt.Errorf("state %s is not a valid state name", searchState)
   105  		}
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  // cleanup cleans the workdir. For now this is just deleting the temporary directory if necessary
   112  // but will have more functionality added to it later
   113  func (stateMachine *StateMachine) cleanup() error {
   114  	if stateMachine.cleanWorkDir {
   115  		if err := osRemoveAll(stateMachine.stateMachineFlags.WorkDir); err != nil {
   116  			return fmt.Errorf("Error cleaning up workDir: %s", err.Error())
   117  		}
   118  	}
   119  	return nil
   120  }
   121  
   122  // handleLkBootloader handles the special "lk" bootloader case where some extra
   123  // files need to be added to the bootfs
   124  func (stateMachine *StateMachine) handleLkBootloader(volume *gadget.Volume) error {
   125  	if volume.Bootloader != "lk" {
   126  		return nil
   127  	}
   128  	// For the LK bootloader we need to copy boot.img and snapbootsel.bin to
   129  	// the gadget folder so they can be used as partition content. The first
   130  	// one comes from the kernel snap, while the second one is modified by
   131  	// the prepare_image step to set the right core and kernel for the kernel
   132  	// command line.
   133  	bootDir := filepath.Join(stateMachine.tempDirs.unpack,
   134  		"image", "boot", "lk")
   135  	gadgetDir := filepath.Join(stateMachine.tempDirs.unpack, "gadget")
   136  	if _, err := os.Stat(bootDir); err != nil {
   137  		return fmt.Errorf("got lk bootloader but directory %s does not exist", bootDir)
   138  	}
   139  	err := osMkdir(gadgetDir, 0755)
   140  	if err != nil && !os.IsExist(err) {
   141  		return fmt.Errorf("Failed to create gadget dir: %s", err.Error())
   142  	}
   143  	files, err := osReadDir(bootDir)
   144  	if err != nil {
   145  		return fmt.Errorf("Error reading lk bootloader dir: %s", err.Error())
   146  	}
   147  	for _, lkFile := range files {
   148  		srcFile := filepath.Join(bootDir, lkFile.Name())
   149  		if err := osutilCopySpecialFile(srcFile, gadgetDir); err != nil {
   150  			return fmt.Errorf("Error copying lk bootloader dir: %s", err.Error())
   151  		}
   152  	}
   153  	return nil
   154  }
   155  
   156  // shouldSkipStructure returns whether a structure should be skipped during certain processing
   157  func shouldSkipStructure(structure gadget.VolumeStructure, isSeeded bool) bool {
   158  	if isSeeded &&
   159  		(structure.Role == gadget.SystemBoot ||
   160  			structure.Role == gadget.SystemData ||
   161  			structure.Role == gadget.SystemSave ||
   162  			structure.Label == gadget.SystemBoot) {
   163  		return true
   164  	}
   165  	return false
   166  }
   167  
   168  // copyStructureContent handles copying raw blobs or creating formatted filesystems
   169  func (stateMachine *StateMachine) copyStructureContent(volume *gadget.Volume,
   170  	structure gadget.VolumeStructure, structIndex int,
   171  	contentRoot, partImg string) error {
   172  
   173  	if structure.Filesystem == "" {
   174  		err := copyStructureNoFS(stateMachine.tempDirs.unpack, structure, partImg)
   175  		if err != nil {
   176  			return err
   177  		}
   178  	} else {
   179  		err := stateMachine.prepareAndCreateFS(volume, structure, structIndex, contentRoot, partImg)
   180  		if err != nil {
   181  			return err
   182  		}
   183  	}
   184  	return nil
   185  }
   186  
   187  // copyStructureNoFS copies the contents to the new location.
   188  // It first zeros it out. Structures without filesystem specified in the gadget
   189  // yaml must have the size specified, so the bs= argument below is valid
   190  func copyStructureNoFS(unpackDir string, structure gadget.VolumeStructure, partImg string) error {
   191  	ddArgs := []string{"if=/dev/zero", "of=" + partImg, "count=0",
   192  		"bs=" + strconv.FormatUint(uint64(structure.Size), 10),
   193  		"seek=1"}
   194  	if err := helperCopyBlob(ddArgs); err != nil {
   195  		return fmt.Errorf("Error zeroing partition: %s",
   196  			err.Error())
   197  	}
   198  	var runningOffset quantity.Offset = 0
   199  	for _, content := range structure.Content {
   200  		if content.Offset != nil {
   201  			runningOffset = *content.Offset
   202  		}
   203  		// now copy the raw content file specified in gadget.yaml
   204  		inFile := filepath.Join(unpackDir, "gadget", content.Image)
   205  		ddArgs = []string{"if=" + inFile, "of=" + partImg, "bs=" + blockSize,
   206  			"seek=" + strconv.FormatUint(uint64(runningOffset), 10),
   207  			"conv=sparse,notrunc"}
   208  		if err := helperCopyBlob(ddArgs); err != nil {
   209  			return fmt.Errorf("Error copying image blob: %s",
   210  				err.Error())
   211  		}
   212  		runningOffset += quantity.Offset(content.Size)
   213  	}
   214  
   215  	return nil
   216  }
   217  
   218  // prepareAndCreateFS prepares and creates a filesystem for the given structure
   219  func (stateMachine *StateMachine) prepareAndCreateFS(volume *gadget.Volume, structure gadget.VolumeStructure, structIndex int, contentRoot, partImg string) error {
   220  	blockSize := structure.Size
   221  	if (structure.Role == gadget.SystemData || structure.Role == gadget.SystemSeed) && structure.Size < stateMachine.RootfsSize {
   222  		// system-data and system-seed structures are not required to have
   223  		// an explicit size set in the yaml file
   224  		if !stateMachine.commonFlags.Quiet {
   225  			fmt.Printf("WARNING: rootfs structure size %s smaller "+
   226  				"than actual rootfs contents %s\n",
   227  				structure.Size.IECString(),
   228  				stateMachine.RootfsSize.IECString())
   229  		}
   230  		blockSize = stateMachine.RootfsSize
   231  		structure.Size = stateMachine.RootfsSize
   232  		volume.Structure[structIndex] = structure
   233  	}
   234  
   235  	err := prepareDiskImg(structure, partImg, blockSize, stateMachine.RootfsSize)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	return makeFS(structure, contentRoot, partImg, stateMachine.SectorSize)
   241  }
   242  
   243  // prepareDiskImg prepares a raw image
   244  func prepareDiskImg(structure gadget.VolumeStructure, partImg string, blockSize quantity.Size, rootfsSize quantity.Size) error {
   245  	if structure.Role == gadget.SystemData {
   246  		_, err := os.Create(partImg)
   247  		if err != nil {
   248  			return fmt.Errorf("unable to create partImg file: %w", err)
   249  		}
   250  		err = os.Truncate(partImg, int64(rootfsSize))
   251  		if err != nil {
   252  			return fmt.Errorf("unable to truncate partImg file: %w", err)
   253  		}
   254  	} else {
   255  		// zero out the .img file
   256  		ddArgs := []string{"if=/dev/zero", "of=" + partImg, "count=0",
   257  			"bs=" + strconv.FormatUint(uint64(blockSize), 10), "seek=1"}
   258  		if err := helperCopyBlob(ddArgs); err != nil {
   259  			return fmt.Errorf("Error zeroing image file %s: %s",
   260  				partImg, err.Error())
   261  		}
   262  	}
   263  	return nil
   264  }
   265  
   266  // makeFS actually creates the filesystem for the given structure
   267  func makeFS(structure gadget.VolumeStructure, contentRoot string, partImg string, sectorSize quantity.Size) error {
   268  	hasC, err := hasContent(structure, contentRoot)
   269  	if err != nil {
   270  		return err
   271  	}
   272  
   273  	if hasC {
   274  		err := mkfsMakeWithContent(structure.Filesystem, partImg, structure.Label,
   275  			contentRoot, structure.Size, sectorSize)
   276  		if err != nil {
   277  			return fmt.Errorf("Error running mkfs with content: %s", err.Error())
   278  		}
   279  		return nil
   280  	}
   281  	err = mkfsMake(structure.Filesystem, partImg, structure.Label,
   282  		structure.Size, sectorSize)
   283  	if err != nil {
   284  		return fmt.Errorf("Error running mkfs: %s", err.Error())
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  // hasContent checks if the structure or the contentRoot dir contains anything
   291  func hasContent(structure gadget.VolumeStructure, contentRoot string) (bool, error) {
   292  	contentFiles, err := osReadDir(contentRoot)
   293  	if err != nil && !os.IsNotExist(err) {
   294  		return false, fmt.Errorf("Error listing contents of volume \"%s\": %s",
   295  			contentRoot, err.Error())
   296  	}
   297  
   298  	return structure.Content != nil || len(contentFiles) > 0, nil
   299  }
   300  
   301  func fixDiskIDOnMBR(imgName string) error {
   302  	var existingDiskIds [][]byte
   303  	randomBytes, err := generateUniqueDiskID(&existingDiskIds)
   304  	if err != nil {
   305  		return fmt.Errorf("Error generating disk ID: %s", err.Error())
   306  	}
   307  	diskFile, err := osOpenFile(imgName, os.O_RDWR, 0755)
   308  	if err != nil {
   309  		return fmt.Errorf("Error opening disk to write MBR disk identifier: %s",
   310  			err.Error())
   311  	}
   312  	defer diskFile.Close()
   313  	_, err = diskFile.WriteAt(randomBytes, 440)
   314  	if err != nil {
   315  		return fmt.Errorf("Error writing MBR disk identifier: %s", err.Error())
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  // handleSecureBoot handles a special case where files need to be moved from /boot/ to
   322  // /EFI/ubuntu/ so that SecureBoot can still be used
   323  func (stateMachine *StateMachine) handleSecureBoot(volume *gadget.Volume, targetDir string) error {
   324  	var bootDir, ubuntuDir string
   325  	if volume.Bootloader == "u-boot" {
   326  		bootDir = filepath.Join(stateMachine.tempDirs.unpack,
   327  			"image", "boot", "uboot")
   328  		ubuntuDir = targetDir
   329  	} else if volume.Bootloader == "piboot" {
   330  		bootDir = filepath.Join(stateMachine.tempDirs.unpack,
   331  			"image", "boot", "piboot")
   332  		ubuntuDir = targetDir
   333  	} else if volume.Bootloader == "grub" {
   334  		bootDir = filepath.Join(stateMachine.tempDirs.unpack,
   335  			"image", "boot", "grub")
   336  		ubuntuDir = filepath.Join(targetDir, "EFI", "ubuntu")
   337  	}
   338  
   339  	if _, err := os.Stat(bootDir); err != nil {
   340  		// this won't always exist, and that's fine
   341  		return nil
   342  	}
   343  
   344  	// copy the files from bootDir to ubuntuDir
   345  	if err := osMkdirAll(ubuntuDir, 0755); err != nil {
   346  		return fmt.Errorf("Error creating ubuntu dir: %s", err.Error())
   347  	}
   348  
   349  	files, err := osReadDir(bootDir)
   350  	if err != nil {
   351  		return fmt.Errorf("Error reading boot dir: %s", err.Error())
   352  	}
   353  	for _, bootFile := range files {
   354  		srcFile := filepath.Join(bootDir, bootFile.Name())
   355  		dstFile := filepath.Join(ubuntuDir, bootFile.Name())
   356  		if err := osRename(srcFile, dstFile); err != nil {
   357  			return fmt.Errorf("Error copying boot dir: %s", err.Error())
   358  		}
   359  	}
   360  
   361  	return nil
   362  }
   363  
   364  // WriteSnapManifest generates a snap manifest based on the contents of the selected snapsDir
   365  func WriteSnapManifest(snapsDir string, outputPath string) error {
   366  	files, err := osReadDir(snapsDir)
   367  	if err != nil {
   368  		// As per previous ubuntu-image manifest generation, we skip generating
   369  		// manifests for non-existent/invalid paths
   370  		return nil
   371  	}
   372  
   373  	manifest, err := osCreate(outputPath)
   374  	if err != nil {
   375  		return fmt.Errorf("Error creating manifest file: %s", err.Error())
   376  	}
   377  	defer manifest.Close()
   378  
   379  	for _, file := range files {
   380  		if strings.HasSuffix(file.Name(), ".snap") {
   381  			split := strings.SplitN(file.Name(), "_", 2)
   382  			fmt.Fprintf(manifest, "%s %s\n", split[0], strings.TrimSuffix(split[1], ".snap"))
   383  		}
   384  	}
   385  	return nil
   386  }
   387  
   388  // getHostArch uses dpkg to return the host architecture of the current system
   389  func getHostArch() string {
   390  	cmd := exec.Command("dpkg", "--print-architecture")
   391  	outputBytes, _ := cmd.Output() // nolint: errcheck
   392  	return strings.TrimSpace(string(outputBytes))
   393  }
   394  
   395  // getHostSuite checks the release name of the host system to use as a default if --suite is not passed
   396  func getHostSuite() string {
   397  	cmd := exec.Command("lsb_release", "-c", "-s")
   398  	outputBytes, _ := cmd.Output() // nolint: errcheck
   399  	return strings.TrimSpace(string(outputBytes))
   400  }
   401  
   402  // getQemuStaticForArch returns the name of the qemu binary for the specified arch
   403  func getQemuStaticForArch(arch string) string {
   404  	archs := map[string]string{
   405  		"armhf":   "qemu-arm-static",
   406  		"arm64":   "qemu-aarch64-static",
   407  		"ppc64el": "qemu-ppc64le-static",
   408  	}
   409  	if static, exists := archs[arch]; exists {
   410  		return static
   411  	}
   412  	return ""
   413  }
   414  
   415  // maxOffset returns the maximum of two quantity.Offset types
   416  func maxOffset(offset1, offset2 quantity.Offset) quantity.Offset {
   417  	if offset1 > offset2 {
   418  		return offset1
   419  	}
   420  	return offset2
   421  }
   422  
   423  // generatePartitionTable prepares the partition table for a structures in a volume and
   424  // returns it with the partition number of the root partition.
   425  func generatePartitionTable(volume *gadget.Volume, sectorSize uint64, isSeeded bool) (*partition.Table, int) {
   426  	var gptPartitions = make([]*gpt.Partition, 0)
   427  	var mbrPartitions = make([]*mbr.Partition, 0)
   428  	var partitionTable partition.Table
   429  	partitionNumber, rootfsPartitionNumber := 1, -1
   430  
   431  	for _, structure := range volume.Structure {
   432  		if structure.Role == schemaMBR || structure.Type == bareStructure ||
   433  			shouldSkipStructure(structure, isSeeded) {
   434  			continue
   435  		}
   436  
   437  		// Record the actual partition number of the root partition, as it
   438  		// might be useful for certain operations (like updating the bootloader)
   439  		if structure.Role == gadget.SystemData {
   440  			rootfsPartitionNumber = partitionNumber
   441  		}
   442  
   443  		structureType := getStructureType(structure, volume.Schema)
   444  
   445  		if volume.Schema == schemaMBR {
   446  			mbrPartition := mbrPartitionFromStruct(structure, sectorSize, structureType)
   447  			mbrPartitions = append(mbrPartitions, mbrPartition)
   448  		} else {
   449  			gptPartition := gptPartitionFromStruct(structure, sectorSize, structureType)
   450  			gptPartitions = append(gptPartitions, gptPartition)
   451  		}
   452  
   453  		partitionNumber++
   454  	}
   455  
   456  	if volume.Schema == schemaMBR {
   457  		partitionTable = &mbr.Table{
   458  			Partitions:         mbrPartitions,
   459  			LogicalSectorSize:  int(sectorSize),
   460  			PhysicalSectorSize: int(sectorSize),
   461  		}
   462  	} else {
   463  		partitionTable = &gpt.Table{
   464  			Partitions:         gptPartitions,
   465  			LogicalSectorSize:  int(sectorSize),
   466  			PhysicalSectorSize: int(sectorSize),
   467  			ProtectiveMBR:      true,
   468  		}
   469  	}
   470  
   471  	return &partitionTable, rootfsPartitionNumber
   472  }
   473  
   474  // getStructureType extracts the structure type from the structure.Type considering
   475  // the schema
   476  func getStructureType(structure gadget.VolumeStructure, schema string) string {
   477  	structureType := structure.Type
   478  	// Check for hybrid MBR/GPT
   479  	if !strings.Contains(structure.Type, ",") {
   480  		return structureType
   481  	}
   482  
   483  	types := strings.Split(structure.Type, ",")
   484  	structureType = types[0]
   485  
   486  	if schema == schemaGPT {
   487  		structureType = types[1]
   488  	}
   489  
   490  	return structureType
   491  }
   492  
   493  // mbrPartitionFromStruct prepares a mbr.Partition object from a gadget.VolumeStructure
   494  func mbrPartitionFromStruct(structure gadget.VolumeStructure, sectorSize uint64, structureType string) *mbr.Partition {
   495  	bootable := false
   496  	if structure.Role == gadget.SystemBoot || structure.Label == gadget.SystemBoot {
   497  		bootable = true
   498  	}
   499  	// mbr.Type is a byte. snapd has already verified that this string
   500  	// is exactly two chars, so we can safely parse those two chars to a byte
   501  	partitionType, _ := strconv.ParseUint(structureType, 16, 8) // nolint: errcheck
   502  
   503  	return &mbr.Partition{
   504  		Start:    uint32(math.Ceil(float64(*structure.Offset) / float64(sectorSize))),
   505  		Size:     uint32(math.Ceil(float64(structure.Size) / float64(sectorSize))),
   506  		Type:     mbr.Type(partitionType),
   507  		Bootable: bootable,
   508  	}
   509  }
   510  
   511  // gptPartitionFromStruct prepares a gpt.Partition object from a gadget.VolumeStructure
   512  func gptPartitionFromStruct(structure gadget.VolumeStructure, sectorSize uint64, structureType string) *gpt.Partition {
   513  	partitionName := structure.Name
   514  	if structure.Role == gadget.SystemData && structure.Name == "" {
   515  		partitionName = "writable"
   516  	}
   517  
   518  	return &gpt.Partition{
   519  		Start: uint64(math.Ceil(float64(*structure.Offset) / float64(sectorSize))),
   520  		Size:  uint64(structure.Size),
   521  		Type:  gpt.Type(structureType),
   522  		Name:  partitionName,
   523  	}
   524  }
   525  
   526  // copyDataToImage runs dd commands to copy the raw data to the final image with appropriate offsets
   527  func (stateMachine *StateMachine) copyDataToImage(volumeName string, volume *gadget.Volume, diskImg *disk.Disk) error {
   528  	// Resolve gadget information to on disk volume
   529  	onDisk := gadget.OnDiskStructsFromGadget(volume)
   530  	for structureNumber, structure := range volume.Structure {
   531  		if shouldSkipStructure(structure, stateMachine.IsSeeded) {
   532  			continue
   533  		}
   534  		sectorSize := diskImg.LogicalBlocksize
   535  		// set up the arguments to dd the structures into an image
   536  		partImg := filepath.Join(stateMachine.tempDirs.volumes, volumeName,
   537  			"part"+strconv.Itoa(structureNumber)+".img")
   538  		onDiskStruct := onDisk[structure.YamlIndex]
   539  		seek := strconv.FormatInt(int64(onDiskStruct.StartOffset)/sectorSize, 10)
   540  		count := strconv.FormatFloat(math.Ceil(float64(onDiskStruct.Size)/float64(sectorSize)), 'f', 0, 64)
   541  		ddArgs := []string{
   542  			"if=" + partImg,
   543  			"of=" + diskImg.File.Name(),
   544  			"bs=" + strconv.FormatInt(sectorSize, 10),
   545  			"seek=" + seek,
   546  			"count=" + count,
   547  			"conv=notrunc",
   548  			"conv=sparse",
   549  		}
   550  		if err := helperCopyBlob(ddArgs); err != nil {
   551  			return fmt.Errorf("Error writing disk image: %s",
   552  				err.Error())
   553  		}
   554  	}
   555  	return nil
   556  }
   557  
   558  // writeOffsetValues handles any OffsetWrite values present in the volume structures.
   559  func writeOffsetValues(volume *gadget.Volume, imgName string, sectorSize, imgSize uint64) error {
   560  	imgFile, err := osOpenFile(imgName, os.O_RDWR, 0755)
   561  	if err != nil {
   562  		return fmt.Errorf("Error opening image file to write offsets: %s", err.Error())
   563  	}
   564  	defer imgFile.Close()
   565  	for _, structure := range volume.Structure {
   566  		if structure.OffsetWrite != nil {
   567  			offset := uint64(*structure.Offset) / sectorSize
   568  			if imgSize-4 < offset {
   569  				return fmt.Errorf("write offset beyond end of file")
   570  			}
   571  			offsetBytes := make([]byte, 4)
   572  			binary.LittleEndian.PutUint32(offsetBytes, uint32(offset))
   573  			_, err := imgFile.WriteAt(offsetBytes, int64(structure.OffsetWrite.Offset))
   574  			if err != nil {
   575  				return fmt.Errorf("Failed to write offset to disk at %d: %s",
   576  					structure.OffsetWrite.Offset, err.Error())
   577  			}
   578  		}
   579  	}
   580  	return nil
   581  }
   582  
   583  // generateUniqueDiskID returns a random 4-byte long disk ID, unique per the list of existing IDs
   584  func generateUniqueDiskID(existing *[][]byte) ([]byte, error) {
   585  	var retry bool
   586  	randomBytes := make([]byte, 4)
   587  	// we'll try 10 times, not to loop into infinity in case the RNG is broken (no entropy?)
   588  	for i := 0; i < 10; i++ {
   589  		retry = false
   590  		_, err := randRead(randomBytes)
   591  		if err != nil {
   592  			retry = true
   593  			continue
   594  		}
   595  		for _, id := range *existing {
   596  			if bytes.Equal(randomBytes, id) {
   597  				retry = true
   598  				break
   599  			}
   600  		}
   601  
   602  		if !retry {
   603  			break
   604  		}
   605  	}
   606  	if retry {
   607  		// this means for some weird reason we didn't get an unique ID after many retries
   608  		return nil, fmt.Errorf("Failed to generate unique disk ID. Random generator failure?")
   609  	}
   610  	*existing = append(*existing, randomBytes)
   611  	return randomBytes, nil
   612  }
   613  
   614  // parseSnapsAndChannels converts the command line arguments to a format that is expected
   615  // by snapd's image.Prepare()
   616  func parseSnapsAndChannels(snaps []string) (snapNames []string, snapChannels map[string]string, err error) {
   617  	snapNames = make([]string, len(snaps))
   618  	snapChannels = make(map[string]string)
   619  	for ii, snap := range snaps {
   620  		if strings.Contains(snap, "=") {
   621  			splitSnap := strings.Split(snap, "=")
   622  			if len(splitSnap) != 2 {
   623  				return snapNames, snapChannels,
   624  					fmt.Errorf("Invalid syntax passed to --snap: %s. "+
   625  						"Argument must be in the form --snap=name or "+
   626  						"--snap=name=channel", snap)
   627  			}
   628  			snapNames[ii] = splitSnap[0]
   629  			snapChannels[splitSnap[0]] = splitSnap[1]
   630  		} else {
   631  			snapNames[ii] = snap
   632  		}
   633  	}
   634  	return snapNames, snapChannels, nil
   635  }
   636  
   637  // generateGerminateCmd creates the appropriate germinate command for the
   638  // values configured in the image definition yaml file
   639  func generateGerminateCmd(imageDefinition imagedefinition.ImageDefinition) *exec.Cmd {
   640  	// determine the value for the seed-dist in the form of <archive>.<series>
   641  	seedDist := imageDefinition.Rootfs.Flavor
   642  	if imageDefinition.Rootfs.Seed.SeedBranch != "" {
   643  		seedDist = seedDist + "." + imageDefinition.Rootfs.Seed.SeedBranch
   644  	}
   645  
   646  	seedSource := strings.Join(imageDefinition.Rootfs.Seed.SeedURLs, ",")
   647  
   648  	germinateCmd := execCommand(
   649  		"germinate",
   650  		"--mirror", imageDefinition.Rootfs.Mirror,
   651  		"--arch", imageDefinition.Architecture,
   652  		"--dist", imageDefinition.Series,
   653  		"--seed-source", seedSource,
   654  		"--seed-dist", seedDist,
   655  		"--no-rdepends",
   656  	)
   657  
   658  	if *imageDefinition.Rootfs.Seed.Vcs {
   659  		germinateCmd.Args = append(germinateCmd.Args, "--vcs=auto")
   660  	}
   661  
   662  	if len(imageDefinition.Rootfs.Components) > 0 {
   663  		components := strings.Join(imageDefinition.Rootfs.Components, ",")
   664  		germinateCmd.Args = append(germinateCmd.Args, "--components="+components)
   665  	}
   666  
   667  	return germinateCmd
   668  }
   669  
   670  // cloneGitRepo takes options from the image definition and clones the git
   671  // repo with the corresponding options
   672  func cloneGitRepo(imageDefinition imagedefinition.ImageDefinition, workDir string) error {
   673  	// clone the repo
   674  	cloneOptions := &git.CloneOptions{
   675  		URL:          imageDefinition.Gadget.GadgetURL,
   676  		SingleBranch: true,
   677  		Depth:        1,
   678  	}
   679  	if imageDefinition.Gadget.GadgetBranch != "" {
   680  		cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(imageDefinition.Gadget.GadgetBranch)
   681  	}
   682  
   683  	err := cloneOptions.Validate()
   684  	if err != nil {
   685  		return err
   686  	}
   687  
   688  	_, err = git.PlainClone(workDir, false, cloneOptions)
   689  	return err
   690  }
   691  
   692  // generateDebootstrapCmd generates the debootstrap command used to create a chroot
   693  // environment that will eventually become the rootfs of the resulting image
   694  func generateDebootstrapCmd(imageDefinition imagedefinition.ImageDefinition, targetDir string) *exec.Cmd {
   695  	debootstrapCmd := execCommand("debootstrap",
   696  		"--arch", imageDefinition.Architecture,
   697  		"--variant=minbase",
   698  	)
   699  
   700  	if imageDefinition.Customization != nil && len(imageDefinition.Customization.ExtraPPAs) > 0 {
   701  		// ca-certificates is needed to use PPAs
   702  		debootstrapCmd.Args = append(debootstrapCmd.Args, "--include=ca-certificates")
   703  	}
   704  
   705  	if len(imageDefinition.Rootfs.Components) > 0 {
   706  		components := strings.Join(imageDefinition.Rootfs.Components, ",")
   707  		debootstrapCmd.Args = append(debootstrapCmd.Args, "--components="+components)
   708  	}
   709  
   710  	// add the SUITE TARGET and MIRROR arguments
   711  	debootstrapCmd.Args = append(debootstrapCmd.Args, []string{
   712  		imageDefinition.Series,
   713  		targetDir,
   714  		imageDefinition.Rootfs.Mirror,
   715  	}...)
   716  
   717  	return debootstrapCmd
   718  }
   719  
   720  // generateAptCmd generates the apt command used to create a chroot
   721  // environment that will eventually become the rootfs of the resulting image
   722  func generateAptCmds(targetDir string, packageList []string) []*exec.Cmd {
   723  	updateCmd := execCommand("chroot", targetDir, "apt", "update")
   724  
   725  	installCmd := execCommand("chroot", targetDir, "apt", "install",
   726  		"--assume-yes",
   727  		"--quiet",
   728  		"--option=Dpkg::options::=--force-unsafe-io",
   729  		"--option=Dpkg::Options::=--force-confold",
   730  	)
   731  
   732  	installCmd.Args = append(installCmd.Args, packageList...)
   733  
   734  	// Env is sometimes used for mocking command calls in tests,
   735  	// so only overwrite env if it is nil
   736  	if installCmd.Env == nil {
   737  		installCmd.Env = os.Environ()
   738  	}
   739  	installCmd.Env = append(installCmd.Env, "DEBIAN_FRONTEND=noninteractive")
   740  
   741  	return []*exec.Cmd{updateCmd, installCmd}
   742  }
   743  
   744  func setDenyingPolicyRcD(path string) (func(error) error, error) {
   745  	const policyRcDDisableAll = `#!/bin/sh
   746  echo "All runlevel operations denied by policy" >&2
   747  exit 101
   748  `
   749  	err := osMkdirAll(filepath.Dir(path), 0755)
   750  	if err != nil {
   751  		return nil, fmt.Errorf("Error creating policy-rc.d dir: %s", err.Error())
   752  	}
   753  
   754  	err = osWriteFile(path, []byte(policyRcDDisableAll), 0755)
   755  	if err != nil {
   756  		return nil, fmt.Errorf("Error writing to policy-rc.d: %s", err.Error())
   757  	}
   758  
   759  	return func(err error) error {
   760  		tmpErr := osRemove(path)
   761  		if tmpErr != nil {
   762  			err = fmt.Errorf("%s\n%s", err, tmpErr)
   763  		}
   764  		return err
   765  	}, nil
   766  }
   767  
   768  // divertPolicyRcD dpkg-diverts policy-rc.d to keep it if it already exists
   769  func divertPolicyRcD(targetDir string) (*exec.Cmd, *exec.Cmd) {
   770  	return dpkgDivert(targetDir, "/usr/sbin/policy-rc.d")
   771  }
   772  
   773  // dpkgDivert dpkg-diverts the given file in the given baseDir
   774  func dpkgDivert(baseDir string, target string) (*exec.Cmd, *exec.Cmd) {
   775  	dpkgDivert := "dpkg-divert"
   776  	targetDiverted := target + ".dpkg-divert"
   777  
   778  	commonArgs := []string{
   779  		"--local",
   780  		"--divert",
   781  		targetDiverted,
   782  		"--rename",
   783  		target,
   784  	}
   785  
   786  	divert := append([]string{baseDir, dpkgDivert}, commonArgs...)
   787  	undivert := append([]string{baseDir, dpkgDivert, "--remove"}, commonArgs...)
   788  
   789  	return execCommand("chroot", divert...), execCommand("chroot", undivert...)
   790  }
   791  
   792  // backupReplaceStartStopDaemon backup start-stop-daemon and replace it with a fake one
   793  // Returns a restore function to put the original one in place
   794  func backupReplaceStartStopDaemon(baseDir string) (func(error) error, error) {
   795  	const startStopDaemonContent = `#!/bin/sh
   796  echo
   797  echo "Warning: Fake start-stop-daemon called, doing nothing"
   798  `
   799  
   800  	startStopDaemon := filepath.Join(baseDir, "sbin", "start-stop-daemon")
   801  	return helper.BackupReplace(startStopDaemon, startStopDaemonContent)
   802  }
   803  
   804  // backupReplaceInitctl backup initctl and replace it with a fake one
   805  // Returns a restore function to put the original one in place
   806  func backupReplaceInitctl(baseDir string) (func(error) error, error) {
   807  	const initctlContent = `#!/bin/sh
   808  if [ "$1" = version ]; then exec /sbin/initctl.REAL "$@"; fi
   809  echo
   810  echo "Warning: Fake initctl called, doing nothing"
   811  `
   812  
   813  	initctl := filepath.Join(baseDir, "sbin", "initctl")
   814  	return helper.BackupReplace(initctl, initctlContent)
   815  }
   816  
   817  // execTeardownCmds executes given commands and collects error to join them with an existing error.
   818  // Failure to execute one command will not stop from executing following ones.
   819  func execTeardownCmds(teardownCmds []*exec.Cmd, debug bool, prevErr error) (err error) {
   820  	err = prevErr
   821  	errs := make([]string, 0)
   822  	for _, teardownCmd := range teardownCmds {
   823  		cmdOutput := helper.SetCommandOutput(teardownCmd, debug)
   824  		teardownErr := teardownCmd.Run()
   825  		if teardownErr != nil {
   826  			errs = append(errs, fmt.Sprintf("teardown command  \"%s\" failed. Output: \n%s",
   827  				teardownCmd.String(), cmdOutput.String()))
   828  		}
   829  	}
   830  
   831  	if len(errs) > 0 {
   832  		err = fmt.Errorf("teardown failed: %s", strings.Join(errs, "\n"))
   833  		if prevErr != nil {
   834  			errs := append([]string{prevErr.Error()}, errs...)
   835  			err = fmt.Errorf(strings.Join(errs, "\n"))
   836  		}
   837  	}
   838  
   839  	return err
   840  }
   841  
   842  // manualMakeDirs creates a directory (and intermediate directories) into the chroot
   843  func manualMakeDirs(customizations []*imagedefinition.MakeDirs, targetDir string, debug bool) error {
   844  	for _, c := range customizations {
   845  		path := filepath.Join(targetDir, c.Path)
   846  		if debug {
   847  			fmt.Printf("Creating directory \"%s\"\n", path)
   848  		}
   849  		if err := osMkdirAll(path, fs.FileMode(c.Permissions)); err != nil {
   850  			return fmt.Errorf("Error creating directory \"%s\" into chroot: %s",
   851  				path, err.Error())
   852  		}
   853  	}
   854  	return nil
   855  }
   856  
   857  // manualCopyFile copies a file into the chroot
   858  func manualCopyFile(customizations []*imagedefinition.CopyFile, confDefPath string, targetDir string, debug bool) error {
   859  	for _, c := range customizations {
   860  		source := filepath.Join(confDefPath, c.Source)
   861  		dest := filepath.Join(targetDir, c.Dest)
   862  		if debug {
   863  			fmt.Printf("Copying file \"%s\" to \"%s\"\n", source, dest)
   864  		}
   865  		if err := osutilCopySpecialFile(source, dest); err != nil {
   866  			return fmt.Errorf("Error copying file \"%s\" into chroot: %s",
   867  				source, err.Error())
   868  		}
   869  	}
   870  	return nil
   871  }
   872  
   873  // manualExecute executes executable files in the chroot
   874  func manualExecute(customizations []*imagedefinition.Execute, targetDir string, debug bool) error {
   875  	for _, c := range customizations {
   876  		executeCmd := execCommand("chroot", targetDir, c.ExecutePath)
   877  		if debug {
   878  			fmt.Printf("Executing command \"%s\"\n", executeCmd.String())
   879  		}
   880  		executeOutput := helper.SetCommandOutput(executeCmd, debug)
   881  		err := executeCmd.Run()
   882  		if err != nil {
   883  			return fmt.Errorf("Error running script \"%s\". Error is %s. Full output below:\n%s",
   884  				executeCmd.String(), err.Error(), executeOutput.String())
   885  		}
   886  	}
   887  	return nil
   888  }
   889  
   890  // manualTouchFile touches files in the chroot
   891  func manualTouchFile(customizations []*imagedefinition.TouchFile, targetDir string, debug bool) error {
   892  	for _, c := range customizations {
   893  		fullPath := filepath.Join(targetDir, c.TouchPath)
   894  		if debug {
   895  			fmt.Printf("Creating empty file \"%s\"\n", fullPath)
   896  		}
   897  		_, err := osCreate(fullPath)
   898  		if err != nil {
   899  			return fmt.Errorf("Error creating file in chroot: %s", err.Error())
   900  		}
   901  	}
   902  	return nil
   903  }
   904  
   905  // manualAddGroup adds groups in the chroot
   906  func manualAddGroup(customizations []*imagedefinition.AddGroup, targetDir string, debug bool) error {
   907  	for _, c := range customizations {
   908  		addGroupCmd := execCommand("chroot", targetDir, "groupadd", c.GroupName)
   909  		debugStatement := fmt.Sprintf("Adding group \"%s\"\n", c.GroupName)
   910  		if c.GroupID != "" {
   911  			addGroupCmd.Args = append(addGroupCmd.Args, []string{"--gid", c.GroupID}...)
   912  			debugStatement = fmt.Sprintf("%s with GID %s\n", strings.TrimSpace(debugStatement), c.GroupID)
   913  		}
   914  		if debug {
   915  			fmt.Print(debugStatement)
   916  		}
   917  		addGroupOutput := helper.SetCommandOutput(addGroupCmd, debug)
   918  		err := addGroupCmd.Run()
   919  		if err != nil {
   920  			return fmt.Errorf("Error adding group. Command used is \"%s\". Error is %s. Full output below:\n%s",
   921  				addGroupCmd.String(), err.Error(), addGroupOutput.String())
   922  		}
   923  	}
   924  	return nil
   925  }
   926  
   927  // manualAddUser adds users in the chroot
   928  func manualAddUser(customizations []*imagedefinition.AddUser, targetDir string, debug bool) error {
   929  	for _, c := range customizations {
   930  		debugStatement := fmt.Sprintf("Adding user \"%s\"\n", c.UserName)
   931  		var addUserCmds []*exec.Cmd
   932  
   933  		addUserCmd := execCommand("chroot", targetDir, "useradd", c.UserName)
   934  		if c.UserID != "" {
   935  			addUserCmd.Args = append(addUserCmd.Args, []string{"--uid", c.UserID}...)
   936  			debugStatement = fmt.Sprintf("%s with UID %s\n", strings.TrimSpace(debugStatement), c.UserID)
   937  		}
   938  
   939  		addUserCmds = append(addUserCmds, addUserCmd)
   940  
   941  		if c.Password != "" {
   942  			chPasswordCmd := execCommand("chroot", targetDir, "chpasswd")
   943  
   944  			if c.PasswordType == "hash" {
   945  				chPasswordCmd.Args = append(chPasswordCmd.Args, "-e")
   946  			}
   947  
   948  			chPasswordCmd.Stdin = strings.NewReader(fmt.Sprintf("%s:%s", c.UserName, c.Password))
   949  
   950  			debugStatement = fmt.Sprintf("%s, setting a password\n", strings.TrimSpace(debugStatement))
   951  			addUserCmds = append(addUserCmds, chPasswordCmd)
   952  		}
   953  
   954  		debugStatement = fmt.Sprintf("%s, forcing reseting the password at first login\n", strings.TrimSpace(debugStatement))
   955  		addUserCmds = append(addUserCmds,
   956  			execCommand("chroot", targetDir, "passwd", "--expire", c.UserName),
   957  		)
   958  
   959  		if debug {
   960  			fmt.Print(debugStatement)
   961  		}
   962  
   963  		for _, cmd := range addUserCmds {
   964  			err := runCmd(cmd, debug)
   965  			if err != nil {
   966  				return err
   967  			}
   968  		}
   969  	}
   970  
   971  	return nil
   972  }
   973  
   974  // getPreseedsnaps returns a slice of the snaps that were preseeded in a chroot
   975  // and their channels
   976  func getPreseededSnaps(rootfs string) (seededSnaps map[string]string, err error) {
   977  	// seededSnaps maps the snap name and channel that was seeded
   978  	seededSnaps = make(map[string]string)
   979  
   980  	// open the seed and run LoadAssertions and LoadMeta to get a list of snaps
   981  	snapdDir := filepath.Join(rootfs, "var", "lib", "snapd")
   982  	seedDir := filepath.Join(snapdDir, "seed")
   983  	preseed, err := seedOpen(seedDir, "")
   984  	if err != nil {
   985  		return seededSnaps, err
   986  	}
   987  	measurer := timings.New(nil)
   988  	if err := preseed.LoadAssertions(nil, nil); err != nil {
   989  		return seededSnaps, err
   990  	}
   991  	if err := preseed.LoadMeta(seed.AllModes, nil, measurer); err != nil {
   992  		return seededSnaps, err
   993  	}
   994  
   995  	// iterate over the snaps in the seed and add them to the list
   996  	err = preseed.Iter(func(sn *seed.Snap) error {
   997  		seededSnaps[sn.SnapName()] = sn.Channel
   998  		return nil
   999  	})
  1000  	if err != nil {
  1001  		return seededSnaps, err
  1002  	}
  1003  
  1004  	return seededSnaps, nil
  1005  }
  1006  
  1007  // associateLoopDevice associates a file to a loop device and returns the loop device number
  1008  // Also returns the command to detach the loop device during teardown
  1009  func (stateMachine *StateMachine) associateLoopDevice(path string) (string, *exec.Cmd, error) {
  1010  	// run the losetup command and read the output to determine which loopback was used
  1011  	losetupCmd := execCommand("losetup",
  1012  		"--find",
  1013  		"--show",
  1014  		"--partscan",
  1015  		"--sector-size",
  1016  		stateMachine.SectorSize.String(),
  1017  		path,
  1018  	)
  1019  	var losetupOutput []byte
  1020  	losetupOutput, err := losetupCmd.Output()
  1021  	if err != nil {
  1022  		err = fmt.Errorf("Error running losetup command \"%s\". Error is %s",
  1023  			losetupCmd.String(),
  1024  			err.Error(),
  1025  		)
  1026  		return "", nil, err
  1027  	}
  1028  
  1029  	loopUsed := strings.TrimSpace(string(losetupOutput))
  1030  
  1031  	//nolint:gosec,G204
  1032  	losetupDetachCmd := execCommand("losetup", "--detach", loopUsed)
  1033  
  1034  	return loopUsed, losetupDetachCmd, nil
  1035  }
  1036  
  1037  // divertOSProber divert GRUB's os-prober as we don't want to scan for other OSes on
  1038  // the build system
  1039  func divertOSProber(mountDir string) (*exec.Cmd, *exec.Cmd) {
  1040  	return dpkgDivert(mountDir, "/etc/grub.d/30_os-prober")
  1041  }
  1042  
  1043  // updateGrub mounts the resulting image and runs update-grub
  1044  func (stateMachine *StateMachine) updateGrub(rootfsVolName string, rootfsPartNum int) (err error) {
  1045  	// create a directory in which to mount the rootfs
  1046  	mountDir := filepath.Join(stateMachine.tempDirs.scratch, "loopback")
  1047  	err = osMkdir(mountDir, 0755)
  1048  	if err != nil && !os.IsExist(err) {
  1049  		return fmt.Errorf("Error creating scratch/loopback directory: %s", err.Error())
  1050  	}
  1051  
  1052  	// Slice used to store all the commands that need to be run
  1053  	// to properly update grub.cfg in the chroot
  1054  	// updateGrubCmds should be filled as a FIFO list
  1055  	var updateGrubCmds []*exec.Cmd
  1056  	// Slice used to store all the commands that need to be run
  1057  	// to properly cleanup everything after the update of grub.cfg
  1058  	// updateGrubCmds should be filled as a LIFO list (so new entries should added at the start of the slice)
  1059  	var teardownCmds []*exec.Cmd
  1060  
  1061  	defer func() {
  1062  		err = execTeardownCmds(teardownCmds, stateMachine.commonFlags.Debug, err)
  1063  	}()
  1064  
  1065  	imgPath := filepath.Join(stateMachine.commonFlags.OutputDir, stateMachine.VolumeNames[rootfsVolName])
  1066  
  1067  	loopUsed, losetupDetachCmd, err := stateMachine.associateLoopDevice(imgPath)
  1068  	if err != nil {
  1069  		return err
  1070  	}
  1071  
  1072  	// detach the loopback device
  1073  	teardownCmds = append(teardownCmds, losetupDetachCmd)
  1074  
  1075  	updateGrubCmds = append(updateGrubCmds,
  1076  		// mount the rootfs partition in which to run update-grub
  1077  		//nolint:gosec,G204
  1078  		execCommand("mount",
  1079  			fmt.Sprintf("%sp%d", loopUsed, rootfsPartNum),
  1080  			mountDir,
  1081  		),
  1082  	)
  1083  
  1084  	teardownCmds = append([]*exec.Cmd{execCommand("umount", mountDir)}, teardownCmds...)
  1085  
  1086  	// set up the mountpoints
  1087  	mountPoints := []mountPoint{
  1088  		{
  1089  			src:      "devtmpfs-build",
  1090  			basePath: mountDir,
  1091  			relpath:  "/dev",
  1092  			typ:      "devtmpfs",
  1093  		},
  1094  		{
  1095  			src:      "devpts-build",
  1096  			basePath: mountDir,
  1097  			relpath:  "/dev/pts",
  1098  			typ:      "devpts",
  1099  			opts:     []string{"nodev", "nosuid"},
  1100  		},
  1101  		{
  1102  			src:      "proc-build",
  1103  			basePath: mountDir,
  1104  			relpath:  "/proc",
  1105  			typ:      "proc",
  1106  		},
  1107  		{
  1108  			src:      "sysfs-build",
  1109  			basePath: mountDir,
  1110  			relpath:  "/sys",
  1111  			typ:      "sysfs",
  1112  		},
  1113  	}
  1114  
  1115  	for _, mp := range mountPoints {
  1116  		mountCmds, umountCmds, err := mp.getMountCmd()
  1117  		if err != nil {
  1118  			return fmt.Errorf("Error preparing mountpoint \"%s\": \"%s\"",
  1119  				mp.relpath,
  1120  				err.Error(),
  1121  			)
  1122  		}
  1123  		updateGrubCmds = append(updateGrubCmds, mountCmds...)
  1124  		teardownCmds = append(umountCmds, teardownCmds...)
  1125  	}
  1126  
  1127  	teardownCmds = append([]*exec.Cmd{
  1128  		execCommand("udevadm", "settle"),
  1129  	}, teardownCmds...)
  1130  
  1131  	divert, undivert := divertOSProber(mountDir)
  1132  
  1133  	updateGrubCmds = append(updateGrubCmds, divert)
  1134  	teardownCmds = append([]*exec.Cmd{undivert}, teardownCmds...)
  1135  
  1136  	// actually run update-grub
  1137  	updateGrubCmds = append(updateGrubCmds,
  1138  		execCommand("chroot",
  1139  			mountDir,
  1140  			"update-grub",
  1141  		),
  1142  	)
  1143  
  1144  	// now run all the commands
  1145  	err = helper.RunCmds(updateGrubCmds, stateMachine.commonFlags.Debug)
  1146  	if err != nil {
  1147  		return err
  1148  	}
  1149  
  1150  	return nil
  1151  }