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 }