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 }