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

     1  package statemachine
     2  
     3  import (
     4  	"fmt"
     5  	"math"
     6  	"os"
     7  	"path/filepath"
     8  	"strconv"
     9  
    10  	diskfs "github.com/diskfs/go-diskfs"
    11  	diskutils "github.com/diskfs/go-diskfs/disk"
    12  	"github.com/snapcore/snapd/gadget"
    13  	"github.com/snapcore/snapd/gadget/quantity"
    14  	"github.com/snapcore/snapd/osutil"
    15  
    16  	"github.com/canonical/ubuntu-image/internal/helper"
    17  )
    18  
    19  var setArtifactNamesState = stateFunc{"set_artifact_names", (*StateMachine).setArtifactNames}
    20  
    21  // for snap/core image builds, the image name is always <volume-name>.img for
    22  // each volume in the gadget. This function stores that info in the struct
    23  func (stateMachine *StateMachine) setArtifactNames() error {
    24  	stateMachine.VolumeNames = make(map[string]string)
    25  	for volumeName := range stateMachine.GadgetInfo.Volumes {
    26  		stateMachine.VolumeNames[volumeName] = volumeName + ".img"
    27  	}
    28  	return nil
    29  }
    30  
    31  var loadGadgetYamlState = stateFunc{"load_gadget_yaml", (*StateMachine).loadGadgetYaml}
    32  
    33  // Load gadget.yaml, do some validation, and store the relevant info in the StateMachine struct
    34  func (stateMachine *StateMachine) loadGadgetYaml() error {
    35  	gadgetYamlDst := filepath.Join(stateMachine.stateMachineFlags.WorkDir, "gadget.yaml")
    36  	if err := osutilCopyFile(stateMachine.YamlFilePath,
    37  		gadgetYamlDst, osutil.CopyFlagOverwrite); err != nil {
    38  		return fmt.Errorf(`Error copying gadget.yaml to %s: %s
    39  The gadget.yaml file is expected to be located in a "meta" subdirectory of the provided built gadget directory.
    40  `, gadgetYamlDst, err.Error())
    41  	}
    42  
    43  	// read in the gadget.yaml as bytes, because snapd expects it that way
    44  	gadgetYamlBytes, err := osReadFile(stateMachine.YamlFilePath)
    45  	if err != nil {
    46  		return fmt.Errorf("Error reading gadget.yaml bytes: %s", err.Error())
    47  	}
    48  
    49  	stateMachine.GadgetInfo, err = gadget.InfoFromGadgetYaml(gadgetYamlBytes, nil)
    50  	if err != nil {
    51  		return fmt.Errorf("Error running InfoFromGadgetYaml: %s", err.Error())
    52  	}
    53  
    54  	// check if the unpack dir should be preserved
    55  	err = preserveUnpack(stateMachine.tempDirs.unpack)
    56  	if err != nil {
    57  		return err
    58  	}
    59  
    60  	// for the --image-size argument, the order of the volumes specified in gadget.yaml
    61  	// must be preserved. However, since gadget.Info stores the volumes as a map, the
    62  	// order is not preserved. We use the already read-in gadget.yaml file to store the
    63  	// order of the volumes as an array in the StateMachine struct
    64  	stateMachine.saveVolumeOrder(string(gadgetYamlBytes))
    65  
    66  	if err := stateMachine.postProcessGadgetYaml(); err != nil {
    67  		return err
    68  	}
    69  
    70  	if err := stateMachine.parseImageSizes(); err != nil {
    71  		return err
    72  	}
    73  
    74  	// pre-parse the sector size argument here as it's a string and we will be using it
    75  	// in various places
    76  	stateMachine.SectorSize, err = quantity.ParseSize(stateMachine.commonFlags.SectorSize)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	return nil
    82  }
    83  
    84  // preserveUnpack checks if and does preserve the gadget unpack directory
    85  func preserveUnpack(unpackDir string) error {
    86  	preserveUnpackDir := os.Getenv("UBUNTU_IMAGE_PRESERVE_UNPACK")
    87  	if len(preserveUnpackDir) == 0 {
    88  		return nil
    89  	}
    90  	err := osMkdirAll(preserveUnpackDir, 0755)
    91  	if err != nil && !os.IsExist(err) {
    92  		return fmt.Errorf("Error creating preserve unpack directory: %s", err.Error())
    93  	}
    94  	if err := osutilCopySpecialFile(unpackDir, preserveUnpackDir); err != nil {
    95  		return fmt.Errorf("Error preserving unpack dir: %s", err.Error())
    96  	}
    97  	return nil
    98  }
    99  
   100  var generateDiskInfoState = stateFunc{"generate_disk_info", (*StateMachine).generateDiskInfo}
   101  
   102  // If --disk-info was used, copy the provided file to the correct location
   103  func (stateMachine *StateMachine) generateDiskInfo() error {
   104  	if stateMachine.commonFlags.DiskInfo != "" {
   105  		diskInfoDir := filepath.Join(stateMachine.tempDirs.rootfs, ".disk")
   106  		if err := osMkdir(diskInfoDir, 0755); err != nil {
   107  			return fmt.Errorf("Failed to create disk info directory: %s", err.Error())
   108  		}
   109  		diskInfoFile := filepath.Join(diskInfoDir, "info")
   110  		err := osutilCopyFile(stateMachine.commonFlags.DiskInfo, diskInfoFile, osutil.CopyFlagDefault)
   111  		if err != nil {
   112  			return fmt.Errorf("Failed to copy Disk Info file: %s", err.Error())
   113  		}
   114  	}
   115  	return nil
   116  }
   117  
   118  var calculateRootfsSizeState = stateFunc{"calculate_rootfs_size", (*StateMachine).calculateRootfsSize}
   119  
   120  // calculateRootfsSize calculates the size of the root filesystem.
   121  // On a 100MiB filesystem, ext4 takes a little over 7MiB for the
   122  // metadata, so use 8MB as a minimum padding.
   123  func (stateMachine *StateMachine) calculateRootfsSize() error {
   124  	rootfsSize, err := helper.Du(stateMachine.tempDirs.rootfs)
   125  	if err != nil {
   126  		return fmt.Errorf("Error getting rootfs size: %s", err.Error())
   127  	}
   128  
   129  	// fudge factor for incidentals
   130  	rootfsPadding := 8 * quantity.SizeMiB
   131  	rootfsSize = quantity.Size(math.Ceil(float64(rootfsSize) * 1.5))
   132  	rootfsSize += rootfsPadding
   133  
   134  	stateMachine.RootfsSize = stateMachine.alignToSectorSize(rootfsSize)
   135  
   136  	if stateMachine.commonFlags.Size != "" {
   137  		rootfsVolume, rootfsVolumeName := stateMachine.findRootfsVolume()
   138  		desiredSize := stateMachine.getSuggestedImageSize(rootfsVolumeName)
   139  
   140  		// subtract the size and offsets of the existing volumes
   141  		if rootfsVolume != nil {
   142  			for _, structure := range rootfsVolume.Structure {
   143  				desiredSize = helper.SafeQuantitySubtraction(desiredSize, structure.Size)
   144  				if structure.Offset != nil {
   145  					desiredSize = helper.SafeQuantitySubtraction(desiredSize,
   146  						quantity.Size(*structure.Offset))
   147  				}
   148  			}
   149  
   150  			desiredSize = stateMachine.alignToSectorSize(desiredSize)
   151  
   152  			if desiredSize < stateMachine.RootfsSize {
   153  				return fmt.Errorf("Error: calculated rootfs partition size %d is smaller "+
   154  					"than actual rootfs contents (%d). Try using a larger value of "+
   155  					"--image-size",
   156  					desiredSize, stateMachine.RootfsSize,
   157  				)
   158  			}
   159  
   160  			stateMachine.RootfsSize = desiredSize
   161  		}
   162  	}
   163  
   164  	stateMachine.syncGadgetStructureRootfsSize()
   165  	return nil
   166  }
   167  
   168  // findRootfsVolume finds the volume associated to the rootfs
   169  func (stateMachine *StateMachine) findRootfsVolume() (*gadget.Volume, string) {
   170  	for volumeName, volume := range stateMachine.GadgetInfo.Volumes {
   171  		for _, structure := range volume.Structure {
   172  			if structure.Size == 0 {
   173  				return volume, volumeName
   174  			}
   175  		}
   176  	}
   177  	return nil, ""
   178  }
   179  
   180  // alignToSectorSize align the given size to the SectorSize of the stateMachine
   181  func (stateMachine *StateMachine) alignToSectorSize(size quantity.Size) quantity.Size {
   182  	return quantity.Size(math.Ceil(float64(size)/float64(stateMachine.SectorSize))) *
   183  		quantity.Size(stateMachine.SectorSize)
   184  }
   185  
   186  // syncGadgetStructureRootfsSize synchronizes size of the gadget.Structure that
   187  // represents the rootfs with the RootfsSize value of the statemachine
   188  // This functions assumes stateMachine.RootfsSize was previously correctly updated.
   189  func (stateMachine *StateMachine) syncGadgetStructureRootfsSize() {
   190  	for _, volume := range stateMachine.GadgetInfo.Volumes {
   191  		for structIndex, structure := range volume.Structure {
   192  			if structure.Size == 0 {
   193  				structure.Size = stateMachine.RootfsSize
   194  			}
   195  			volume.Structure[structIndex] = structure
   196  		}
   197  	}
   198  }
   199  
   200  var populateBootfsContentsState = stateFunc{"populate_bootfs_contents", (*StateMachine).populateBootfsContents}
   201  
   202  // Populate the Bootfs Contents by using snapd's MountedFilesystemWriter
   203  func (stateMachine *StateMachine) populateBootfsContents() error {
   204  	var preserve []string
   205  	for _, volumeName := range stateMachine.VolumeOrder {
   206  		volume := stateMachine.GadgetInfo.Volumes[volumeName]
   207  		// piboot modifies the original config.txt from the gadget,
   208  		// avoid overwriting with the one coming from the gadget
   209  		if volume.Bootloader == "piboot" {
   210  			preserve = append(preserve, "config.txt")
   211  		}
   212  
   213  		// Get a LaidOutVolume we can use with a mountedFilesystemWriter
   214  		laidOutVolume, err := stateMachine.layoutVolume(volume)
   215  		if err != nil {
   216  			return err
   217  		}
   218  
   219  		for i, laidOutStructure := range laidOutVolume.LaidOutStructure {
   220  			err = stateMachine.populateBootfsLayoutStructure(laidOutStructure, laidOutVolume, i, volume, volumeName, preserve)
   221  			if err != nil {
   222  				return err
   223  			}
   224  		}
   225  	}
   226  	return nil
   227  }
   228  
   229  // layoutVolume generates a LaidOutVolume to be used with a gadget.NewMountedFilesystemWriter
   230  func (stateMachine *StateMachine) layoutVolume(volume *gadget.Volume) (*gadget.LaidOutVolume, error) {
   231  	layoutOptions := &gadget.LayoutOptions{
   232  		SkipResolveContent: false,
   233  		IgnoreContent:      false,
   234  		GadgetRootDir:      filepath.Join(stateMachine.tempDirs.unpack, "gadget"),
   235  		KernelRootDir:      filepath.Join(stateMachine.tempDirs.unpack, "kernel"),
   236  	}
   237  	laidOutVolume, err := gadgetLayoutVolume(volume,
   238  		gadget.OnDiskStructsFromGadget(volume), layoutOptions)
   239  	if err != nil {
   240  		return nil, fmt.Errorf("Error laying out bootfs contents: %s", err.Error())
   241  	}
   242  
   243  	return laidOutVolume, nil
   244  }
   245  
   246  // populateBootfsLayoutStructure write a laidOutStructure to the associated target directory
   247  func (stateMachine *StateMachine) populateBootfsLayoutStructure(laidOutStructure gadget.LaidOutStructure, laidOutVolume *gadget.LaidOutVolume, index int, volume *gadget.Volume, volumeName string, preserve []string) error {
   248  	var targetDir string
   249  	if laidOutStructure.Role() == gadget.SystemSeed {
   250  		targetDir = stateMachine.tempDirs.rootfs
   251  	} else {
   252  		targetDir = filepath.Join(stateMachine.tempDirs.volumes,
   253  			volumeName,
   254  			"part"+strconv.Itoa(index))
   255  	}
   256  	// Bad special-casing.  snapd's image.Prepare currently
   257  	// installs to /boot/grub, but we need to map this to
   258  	// /EFI/ubuntu.  This is because we are using a SecureBoot
   259  	// signed bootloader image which has this path embedded, so
   260  	// we need to install our files to there.
   261  	if !stateMachine.IsSeeded &&
   262  		(laidOutStructure.Role() == gadget.SystemBoot ||
   263  			laidOutStructure.Label() == gadget.SystemBoot) {
   264  		if err := stateMachine.handleSecureBoot(volume, targetDir); err != nil {
   265  			return err
   266  		}
   267  	}
   268  	if laidOutStructure.HasFilesystem() {
   269  		mountedFilesystemWriter, err := gadgetNewMountedFilesystemWriter(nil, &laidOutVolume.LaidOutStructure[index], nil)
   270  		if err != nil {
   271  			return fmt.Errorf("Error creating NewMountedFilesystemWriter: %s", err.Error())
   272  		}
   273  
   274  		err = mountedFilesystemWriter.Write(targetDir, preserve)
   275  		if err != nil {
   276  			return fmt.Errorf("Error in mountedFilesystem.Write(): %s", err.Error())
   277  		}
   278  	}
   279  	return nil
   280  }
   281  
   282  var populatePreparePartitionsState = stateFunc{"populate_prepare_partitions", (*StateMachine).populatePreparePartitions}
   283  
   284  // Populate and prepare the partitions. For partitions without "filesystem:" specified in
   285  // gadget.yaml, this involves using dd to copy the content blobs into a .img file. For
   286  // partitions that do have "filesystem:" specified, we use the Mkfs functions from snapd.
   287  // Throughout this process, the offset is tracked to ensure partitions are not overlapping.
   288  func (stateMachine *StateMachine) populatePreparePartitions() error {
   289  	for _, volumeName := range stateMachine.VolumeOrder {
   290  		volume := stateMachine.GadgetInfo.Volumes[volumeName]
   291  		if err := stateMachine.handleLkBootloader(volume); err != nil {
   292  			return err
   293  		}
   294  		for structIndex, structure := range volume.Structure {
   295  			var contentRoot string
   296  			if structure.Role == gadget.SystemData || structure.Role == gadget.SystemSeed {
   297  				contentRoot = stateMachine.tempDirs.rootfs
   298  			} else {
   299  				contentRoot = filepath.Join(stateMachine.tempDirs.volumes, volumeName,
   300  					"part"+strconv.Itoa(structIndex))
   301  			}
   302  			if shouldSkipStructure(structure, stateMachine.IsSeeded) {
   303  				continue
   304  			}
   305  
   306  			// copy the data
   307  			partImg := filepath.Join(stateMachine.tempDirs.volumes, volumeName,
   308  				"part"+strconv.Itoa(structIndex)+".img")
   309  			if err := stateMachine.copyStructureContent(volume, structure,
   310  				structIndex, contentRoot, partImg); err != nil {
   311  				return err
   312  			}
   313  		}
   314  		// Set the image size values to be used by make_disk, by using
   315  		// the minimum size that would be valid according to gadget.yaml.
   316  		stateMachine.handleContentSizes(quantity.Offset(volume.MinSize()), volumeName)
   317  	}
   318  	return nil
   319  }
   320  
   321  var makeDiskState = stateFunc{"make_disk", (*StateMachine).makeDisk}
   322  
   323  // Make the disk
   324  func (stateMachine *StateMachine) makeDisk() error {
   325  	for volumeName, volume := range stateMachine.GadgetInfo.Volumes {
   326  		_, found := stateMachine.VolumeNames[volumeName]
   327  		if !found {
   328  			continue
   329  		}
   330  		imgName := filepath.Join(stateMachine.commonFlags.OutputDir, stateMachine.VolumeNames[volumeName])
   331  
   332  		diskImg, imgSize, err := stateMachine.createDiskImage(volumeName, volume, imgName)
   333  		if err != nil {
   334  			return err
   335  		}
   336  
   337  		partitionTable, rootfsPartitionNumber := generatePartitionTable(volume, uint64(stateMachine.SectorSize), stateMachine.IsSeeded)
   338  
   339  		// Save the rootfs partition number, if found, for later use
   340  		if rootfsPartitionNumber != -1 {
   341  			stateMachine.RootfsVolName = volumeName
   342  			stateMachine.RootfsPartNum = rootfsPartitionNumber
   343  		}
   344  
   345  		if err := diskImg.Partition(*partitionTable); err != nil {
   346  			return fmt.Errorf("Error partitioning image file: %s", err.Error())
   347  		}
   348  
   349  		// TODO: go-diskfs doesn't set the disk ID when using an MBR partition table.
   350  		// this function is a temporary workaround, but we should change upstream go-diskfs
   351  		if volume.Schema == schemaMBR {
   352  			err = fixDiskIDOnMBR(imgName)
   353  			if err != nil {
   354  				return err
   355  			}
   356  		}
   357  
   358  		// After the partitions have been created, copy the data into the correct locations
   359  		if err := stateMachine.copyDataToImage(volumeName, volume, diskImg); err != nil {
   360  			return err
   361  		}
   362  
   363  		// Open the file and write any OffsetWrite values
   364  		if err := writeOffsetValues(volume, imgName, uint64(stateMachine.SectorSize), uint64(imgSize)); err != nil {
   365  			return err
   366  		}
   367  	}
   368  	return nil
   369  }
   370  
   371  // createDiskImage creates a disk image and making sure the size respects the configuration and
   372  // the SectorSize
   373  func (stateMachine *StateMachine) createDiskImage(volumeName string, volume *gadget.Volume, imgName string) (*diskutils.Disk, quantity.Size, error) {
   374  	imgSize, found := stateMachine.ImageSizes[volumeName]
   375  	if !found {
   376  		// Calculate the minimum size that would be
   377  		// valid according to gadget.yaml.
   378  		imgSize = volume.MinSize()
   379  	}
   380  	if err := osRemoveAll(imgName); err != nil {
   381  		return nil, 0, fmt.Errorf("Error removing old disk image: %s", err.Error())
   382  	}
   383  	sectorSizeFlag := diskfs.SectorSize(int(stateMachine.SectorSize))
   384  	diskImg, err := diskfsCreate(imgName, int64(imgSize), diskfs.Raw, sectorSizeFlag)
   385  	if err != nil {
   386  		return nil, 0, fmt.Errorf("Error creating disk image: %s", err.Error())
   387  	}
   388  
   389  	imgSize = stateMachine.alignToSectorSize(imgSize)
   390  	if err := osTruncate(diskImg.File.Name(), int64(imgSize)); err != nil {
   391  		return nil, 0, fmt.Errorf("Error resizing disk image to a multiple of its block size: %s",
   392  			err.Error())
   393  	}
   394  
   395  	return diskImg, imgSize, nil
   396  }