github.com/canonical/ubuntu-image@v0.0.0-20240430122802-2202fe98b290/internal/statemachine/classic_states.go (about) 1 package statemachine 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "path" 11 "path/filepath" 12 "reflect" 13 "regexp" 14 "strings" 15 16 "github.com/snapcore/snapd/image" 17 "github.com/snapcore/snapd/image/preseed" 18 "github.com/snapcore/snapd/interfaces/builtin" 19 "github.com/snapcore/snapd/osutil" 20 "github.com/snapcore/snapd/seed/seedwriter" 21 "github.com/snapcore/snapd/snap" 22 "github.com/snapcore/snapd/store" 23 24 "github.com/canonical/ubuntu-image/internal/helper" 25 "github.com/canonical/ubuntu-image/internal/imagedefinition" 26 "github.com/canonical/ubuntu-image/internal/ppa" 27 ) 28 29 var ( 30 seedVersionRegex = regexp.MustCompile(`^[a-z0-9].*`) 31 localePresentRegex = regexp.MustCompile(`(?m)^LANG=|LC_[A-Z_]+=`) 32 ) 33 34 var buildGadgetTreeState = stateFunc{"build_gadget_tree", (*StateMachine).buildGadgetTree} 35 36 // Build the gadget tree 37 func (stateMachine *StateMachine) buildGadgetTree() error { 38 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 39 40 // make the gadget directory under scratch 41 gadgetDir := filepath.Join(stateMachine.tempDirs.scratch, "gadget") 42 43 err := classicStateMachine.prepareGadgetDir(gadgetDir) 44 if err != nil { 45 return err 46 } 47 48 makeCmd := execCommand("make") 49 50 // if a make target was specified then add it to the command 51 if classicStateMachine.ImageDef.Gadget.GadgetTarget != "" { 52 makeCmd.Args = append(makeCmd.Args, classicStateMachine.ImageDef.Gadget.GadgetTarget) 53 } 54 55 // add ARCH and SERIES environment variables for making the gadget tree 56 makeCmd.Env = append(makeCmd.Env, []string{ 57 fmt.Sprintf("ARCH=%s", classicStateMachine.ImageDef.Architecture), 58 fmt.Sprintf("SERIES=%s", classicStateMachine.ImageDef.Series), 59 }...) 60 // add the current ENV to the command 61 makeCmd.Env = append(makeCmd.Env, os.Environ()...) 62 makeCmd.Dir = gadgetDir 63 64 makeOutput := helper.SetCommandOutput(makeCmd, classicStateMachine.commonFlags.Debug) 65 66 if err := makeCmd.Run(); err != nil { 67 return fmt.Errorf("Error running \"make\" in gadget source. "+ 68 "Error is \"%s\". Full output below:\n%s", 69 err.Error(), makeOutput.String()) 70 } 71 72 return nil 73 } 74 75 // prepareGadgetDir prepares the gadget directory prior to running the make command 76 func (classicStateMachine *ClassicStateMachine) prepareGadgetDir(gadgetDir string) error { 77 err := osMkdir(gadgetDir, 0755) 78 if err != nil && !os.IsExist(err) { 79 return fmt.Errorf("Error creating scratch/gadget directory: %s", err.Error()) 80 } 81 82 switch classicStateMachine.ImageDef.Gadget.GadgetType { 83 case "git": 84 err := cloneGitRepo(classicStateMachine.ImageDef, gadgetDir) 85 if err != nil { 86 return fmt.Errorf("Error cloning gadget repository: \"%s\"", err.Error()) 87 } 88 case "directory": 89 gadgetTreePath := strings.TrimPrefix(classicStateMachine.ImageDef.Gadget.GadgetURL, "file://") 90 if !filepath.IsAbs(gadgetTreePath) { 91 gadgetTreePath = filepath.Join(classicStateMachine.ConfDefPath, gadgetTreePath) 92 } 93 94 // copy the source tree to the workdir 95 files, err := osReadDir(gadgetTreePath) 96 if err != nil { 97 return fmt.Errorf("Error reading gadget tree: %s", err.Error()) 98 } 99 for _, gadgetFile := range files { 100 srcFile := filepath.Join(gadgetTreePath, gadgetFile.Name()) 101 if err := osutilCopySpecialFile(srcFile, gadgetDir); err != nil { 102 return fmt.Errorf("Error copying gadget source: %s", err.Error()) 103 } 104 } 105 } 106 return nil 107 } 108 109 var prepareGadgetTreeState = stateFunc{"prepare_gadget_tree", (*StateMachine).prepareGadgetTree} 110 111 // Prepare the gadget tree 112 func (stateMachine *StateMachine) prepareGadgetTree() error { 113 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 114 gadgetDir := filepath.Join(classicStateMachine.tempDirs.unpack, "gadget") 115 err := osMkdirAll(gadgetDir, 0755) 116 if err != nil && !os.IsExist(err) { 117 return fmt.Errorf("Error creating unpack directory: %s", err.Error()) 118 } 119 // recursively copy the gadget tree to unpack/gadget 120 var gadgetTree string 121 if classicStateMachine.ImageDef.Gadget.GadgetType == "prebuilt" { 122 gadgetTree = strings.TrimPrefix(classicStateMachine.ImageDef.Gadget.GadgetURL, "file://") 123 if !filepath.IsAbs(gadgetTree) { 124 gadgetTree, err = filepath.Abs(gadgetTree) 125 if err != nil { 126 return fmt.Errorf("Error finding the absolute path of the gadget tree: %s", err.Error()) 127 } 128 } 129 } else { 130 gadgetTree = filepath.Join(classicStateMachine.tempDirs.scratch, "gadget", "install") 131 } 132 entries, err := osReadDir(gadgetTree) 133 if err != nil { 134 return fmt.Errorf("Error reading gadget tree: %s", err.Error()) 135 } 136 for _, gadgetEntry := range entries { 137 srcFile := filepath.Join(gadgetTree, gadgetEntry.Name()) 138 if err := osutilCopySpecialFile(srcFile, gadgetDir); err != nil { 139 return fmt.Errorf("Error copying gadget tree entry: %s", err.Error()) 140 } 141 } 142 143 classicStateMachine.YamlFilePath = filepath.Join(gadgetDir, gadgetYamlPathInTree) 144 145 return nil 146 } 147 148 // fixHostname set fresh hostname since debootstrap copies /etc/hostname from build environment 149 func (stateMachine *StateMachine) fixHostname() error { 150 hostname := filepath.Join(stateMachine.tempDirs.chroot, "etc", "hostname") 151 hostnameFile, err := osOpenFile(hostname, os.O_TRUNC|os.O_WRONLY, 0644) 152 if err != nil { 153 return fmt.Errorf("unable to open hostname file: %w", err) 154 } 155 defer hostnameFile.Close() 156 _, err = hostnameFile.WriteString("ubuntu\n") 157 if err != nil { 158 return fmt.Errorf("unable to write hostname: %w", err) 159 } 160 return nil 161 } 162 163 var createChrootState = stateFunc{"create_chroot", (*StateMachine).createChroot} 164 165 // Bootstrap a chroot environment to install packages in. It will eventually 166 // become the rootfs of the image 167 func (stateMachine *StateMachine) createChroot() error { 168 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 169 170 if err := osMkdir(stateMachine.tempDirs.chroot, 0755); err != nil { 171 return fmt.Errorf("Failed to create chroot directory %s : %s", stateMachine.tempDirs.chroot, err.Error()) 172 } 173 174 debootstrapCmd := generateDebootstrapCmd(classicStateMachine.ImageDef, 175 stateMachine.tempDirs.chroot, 176 ) 177 178 debootstrapOutput := helper.SetCommandOutput(debootstrapCmd, classicStateMachine.commonFlags.Debug) 179 180 if err := debootstrapCmd.Run(); err != nil { 181 return fmt.Errorf("Error running debootstrap command \"%s\". Error is \"%s\". Output is: \n%s", 182 debootstrapCmd.String(), err.Error(), debootstrapOutput.String()) 183 } 184 185 err := stateMachine.fixHostname() 186 if err != nil { 187 return err 188 } 189 190 // debootstrap also copies /etc/resolv.conf from build environment; truncate it 191 // as to not leak the host files into the built image 192 resolvConf := filepath.Join(stateMachine.tempDirs.chroot, "etc", "resolv.conf") 193 if err = osTruncate(resolvConf, 0); err != nil { 194 return fmt.Errorf("Error truncating resolv.conf: %s", err.Error()) 195 } 196 197 if *classicStateMachine.ImageDef.Rootfs.SourcesListDeb822 { 198 err := stateMachine.setDeb822SourcesList(classicStateMachine.ImageDef.Deb822BuildSourcesList()) 199 if err != nil { 200 return err 201 } 202 return stateMachine.setLegacySourcesList(imagedefinition.LegacySourcesListComment) 203 } 204 205 return stateMachine.setLegacySourcesList(classicStateMachine.ImageDef.LegacyBuildSourcesList()) 206 } 207 208 var addExtraPPAsState = stateFunc{"add_extra_ppas", (*StateMachine).addExtraPPAs} 209 210 // addExtraPPAs adds PPAs to the /etc/apt/sources.list.d directory 211 func (stateMachine *StateMachine) addExtraPPAs() (err error) { 212 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 213 214 for _, extraPPA := range classicStateMachine.ImageDef.Customization.ExtraPPAs { 215 p := ppa.New(extraPPA, *classicStateMachine.ImageDef.Rootfs.SourcesListDeb822, classicStateMachine.ImageDef.Series) 216 err := p.Add(classicStateMachine.tempDirs.chroot, classicStateMachine.commonFlags.Debug) 217 if err != nil { 218 return err 219 } 220 } 221 222 return nil 223 } 224 225 var cleanExtraPPAsState = stateFunc{"clean_extra_ppas", (*StateMachine).cleanExtraPPAs} 226 227 // cleanExtraPPAs cleans previously added PPA to the source list 228 func (stateMachine *StateMachine) cleanExtraPPAs() (err error) { 229 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 230 231 for _, extraPPA := range classicStateMachine.ImageDef.Customization.ExtraPPAs { 232 p := ppa.New(extraPPA, *classicStateMachine.ImageDef.Rootfs.SourcesListDeb822, classicStateMachine.ImageDef.Series) 233 err := p.Remove(stateMachine.tempDirs.chroot) 234 if err != nil { 235 return err 236 } 237 } 238 239 return nil 240 } 241 242 var installPackagesState = stateFunc{"install_packages", (*StateMachine).installPackages} 243 244 // Install packages in the chroot environment 245 func (stateMachine *StateMachine) installPackages() error { 246 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 247 248 err := helperBackupAndCopyResolvConf(classicStateMachine.tempDirs.chroot) 249 if err != nil { 250 return fmt.Errorf("Error setting up /etc/resolv.conf in the chroot: \"%s\"", err.Error()) 251 } 252 253 stateMachine.gatherPackages(&classicStateMachine.ImageDef) 254 255 // setupCmds should be filled as a FIFO list 256 var setupCmds []*exec.Cmd 257 258 // teardownCmds should be filled as a LIFO list 259 var teardownCmds []*exec.Cmd 260 261 mountPoints := []*mountPoint{} 262 263 // Make sure we left the system as clean as possible if something has gone wrong 264 defer func() { 265 err = teardownMount(stateMachine.tempDirs.chroot, mountPoints, teardownCmds, err, stateMachine.commonFlags.Debug) 266 }() 267 268 // mount some necessary partitions in the chroot 269 mountPoints = append(mountPoints, 270 &mountPoint{ 271 src: "devtmpfs-build", 272 basePath: stateMachine.tempDirs.chroot, 273 relpath: "/dev", 274 typ: "devtmpfs", 275 }, 276 &mountPoint{ 277 src: "devpts-build", 278 basePath: stateMachine.tempDirs.chroot, 279 relpath: "/dev/pts", 280 typ: "devpts", 281 opts: []string{"nodev", "nosuid"}, 282 }, 283 &mountPoint{ 284 src: "proc-build", 285 basePath: stateMachine.tempDirs.chroot, 286 relpath: "/proc", 287 typ: "proc", 288 }, 289 &mountPoint{ 290 src: "sysfs-build", 291 basePath: stateMachine.tempDirs.chroot, 292 relpath: "/sys", 293 typ: "sysfs", 294 }, 295 &mountPoint{ 296 basePath: stateMachine.tempDirs.chroot, 297 relpath: "/run", 298 bind: true, 299 }, 300 ) 301 302 mountCmds, umountCmds, err := generateMountPointCmds(mountPoints, stateMachine.tempDirs.scratch) 303 if err != nil { 304 return err 305 } 306 setupCmds = append(setupCmds, mountCmds...) 307 teardownCmds = append(umountCmds, teardownCmds...) 308 309 teardownCmds = append([]*exec.Cmd{ 310 execCommand("udevadm", "settle"), 311 }, teardownCmds...) 312 313 policyRcDPath := filepath.Join(classicStateMachine.tempDirs.chroot, "usr", "sbin", "policy-rc.d") 314 315 if osutil.FileExists(policyRcDPath) { 316 divertCmd, undivertCmd := divertPolicyRcD(stateMachine.tempDirs.chroot) 317 setupCmds = append(setupCmds, divertCmd) 318 teardownCmds = append([]*exec.Cmd{undivertCmd}, teardownCmds...) 319 } 320 321 err = helper.RunCmds(setupCmds, classicStateMachine.commonFlags.Debug) 322 if err != nil { 323 return err 324 } 325 326 unsetDenyingPolicyRcD, err := setDenyingPolicyRcD(policyRcDPath) 327 if err != nil { 328 return err 329 } 330 331 defer func() { 332 err = unsetDenyingPolicyRcD(err) 333 }() 334 335 restoreStartStopDaemon, err := backupReplaceStartStopDaemon(classicStateMachine.tempDirs.chroot) 336 if err != nil { 337 return err 338 } 339 340 defer func() { 341 err = restoreStartStopDaemon(err) 342 }() 343 344 initctlPath := filepath.Join(classicStateMachine.tempDirs.chroot, "sbin", "initctl") 345 346 if osutil.FileExists(initctlPath) { 347 restoreInitctl, err := backupReplaceInitctl(classicStateMachine.tempDirs.chroot) 348 if err != nil { 349 return err 350 } 351 352 defer func() { 353 err = restoreInitctl(err) 354 }() 355 } 356 357 installPackagesCmds := generateAptCmds(stateMachine.tempDirs.chroot, classicStateMachine.Packages) 358 359 err = helper.RunCmds(installPackagesCmds, classicStateMachine.commonFlags.Debug) 360 if err != nil { 361 return err 362 } 363 364 return nil 365 } 366 367 func (stateMachine *StateMachine) gatherPackages(imageDef *imagedefinition.ImageDefinition) { 368 if imageDef.Customization != nil { 369 for _, packageInfo := range imageDef.Customization.ExtraPackages { 370 stateMachine.Packages = append(stateMachine.Packages, 371 packageInfo.PackageName) 372 } 373 } 374 375 // Make sure to install the extra kernel if it is specified 376 if imageDef.Kernel != "" { 377 stateMachine.Packages = append(stateMachine.Packages, 378 imageDef.Kernel) 379 } 380 } 381 382 // generateMountPointCmds generate lists of mount/umount commands for a list of mountpoints 383 func generateMountPointCmds(mountPoints []*mountPoint, scratchDir string) (allMountCmds []*exec.Cmd, allUmountCmds []*exec.Cmd, err error) { 384 for _, mp := range mountPoints { 385 var mountCmds, umountCmds []*exec.Cmd 386 var err error 387 if mp.bind { 388 mp.src, err = osMkdirTemp(scratchDir, strings.Trim(mp.relpath, "/")) 389 if err != nil { 390 return nil, nil, fmt.Errorf("Error making temporary directory for mountpoint \"%s\": \"%s\"", 391 mp.relpath, 392 err.Error(), 393 ) 394 } 395 } 396 397 mountCmds, umountCmds, err = mp.getMountCmd() 398 if err != nil { 399 return nil, nil, fmt.Errorf("Error preparing mountpoint \"%s\": \"%s\"", 400 mp.relpath, 401 err.Error(), 402 ) 403 } 404 405 allMountCmds = append(allMountCmds, mountCmds...) 406 allUmountCmds = append(umountCmds, allUmountCmds...) 407 } 408 return allMountCmds, allUmountCmds, err 409 } 410 411 var verifyArtifactNamesState = stateFunc{"verify_artifact_names", (*StateMachine).verifyArtifactNames} 412 413 // Verify artifact names have volumes listed for multi-volume gadgets and set 414 // the volume names in the struct 415 func (stateMachine *StateMachine) verifyArtifactNames() error { 416 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 417 418 if classicStateMachine.ImageDef.Artifacts == nil { 419 return nil 420 } 421 422 stateMachine.VolumeNames = make(map[string]string) 423 424 if len(stateMachine.GadgetInfo.Volumes) > 1 { 425 err := stateMachine.prepareImgArtifactsMultipleVolumes(classicStateMachine.ImageDef.Artifacts) 426 if err != nil { 427 return err 428 } 429 err = stateMachine.prepareQcow2ArtifactsMultipleVolumes(classicStateMachine.ImageDef.Artifacts) 430 if err != nil { 431 return err 432 } 433 } else { 434 stateMachine.prepareImgArtifactOneVolume(classicStateMachine.ImageDef.Artifacts) 435 stateMachine.prepareQcow2ArtifactOneVolume(classicStateMachine.ImageDef.Artifacts) 436 } 437 return nil 438 } 439 440 func (stateMachine *StateMachine) prepareImgArtifactsMultipleVolumes(artifacts *imagedefinition.Artifact) error { 441 if artifacts.Img == nil { 442 return nil 443 } 444 for _, img := range *artifacts.Img { 445 if img.ImgVolume == "" { 446 return fmt.Errorf("Volume names must be specified for each image when using a gadget with more than one volume") 447 } 448 stateMachine.VolumeNames[img.ImgVolume] = img.ImgName 449 } 450 return nil 451 } 452 453 // qcow2 img logic is complicated. If .img artifacts are already specified 454 // in the image definition for corresponding volumes, we will re-use those and 455 // convert them to a qcow2 image. Otherwise, we will create a raw .img file to 456 // use as an input file for the conversion. 457 // The names of these images are placed in the VolumeNames map, which is used 458 // as an input file for an eventual `qemu-convert` operation. 459 func (stateMachine *StateMachine) prepareQcow2ArtifactsMultipleVolumes(artifacts *imagedefinition.Artifact) error { 460 if artifacts.Qcow2 != nil { 461 for _, qcow2 := range *artifacts.Qcow2 { 462 if qcow2.Qcow2Volume == "" { 463 return fmt.Errorf("Volume names must be specified for each image when using a gadget with more than one volume") 464 } 465 // We can save a whole lot of disk I/O here if the volume is 466 // already specified as a .img file 467 if artifacts.Img != nil { 468 found := false 469 for _, img := range *artifacts.Img { 470 if img.ImgVolume == qcow2.Qcow2Volume { 471 found = true 472 } 473 } 474 if !found { 475 // if a .img artifact for this volume isn't explicitly stated in 476 // the image definition, then create one 477 stateMachine.VolumeNames[qcow2.Qcow2Volume] = fmt.Sprintf("%s.img", qcow2.Qcow2Name) 478 } 479 } else { 480 // no .img artifacts exist in the image definition, 481 // but we still need to create one to convert to qcow2 482 stateMachine.VolumeNames[qcow2.Qcow2Volume] = fmt.Sprintf("%s.img", qcow2.Qcow2Name) 483 } 484 } 485 } 486 return nil 487 } 488 489 func (stateMachine *StateMachine) prepareImgArtifactOneVolume(artifacts *imagedefinition.Artifact) { 490 if artifacts.Img == nil { 491 return 492 } 493 img := (*artifacts.Img)[0] 494 if img.ImgVolume == "" { 495 // there is only one volume, so get it from the map 496 volName := reflect.ValueOf(stateMachine.GadgetInfo.Volumes).MapKeys()[0].String() 497 stateMachine.VolumeNames[volName] = img.ImgName 498 } else { 499 stateMachine.VolumeNames[img.ImgVolume] = img.ImgName 500 } 501 } 502 503 // qcow2 img logic is complicated. If .img artifacts are already specified 504 // in the image definition for corresponding volumes, we will re-use those and 505 // convert them to a qcow2 image. Otherwise, we will create a raw .img file to 506 // use as an input file for the conversion. 507 // The names of these images are placed in the VolumeNames map, which is used 508 // as an input file for an eventual `qemu-convert` operation. 509 func (stateMachine *StateMachine) prepareQcow2ArtifactOneVolume(artifacts *imagedefinition.Artifact) { 510 if artifacts.Qcow2 == nil { 511 return 512 } 513 qcow2 := (*artifacts.Qcow2)[0] 514 if qcow2.Qcow2Volume == "" { 515 volName := reflect.ValueOf(stateMachine.GadgetInfo.Volumes).MapKeys()[0].String() 516 if artifacts.Img != nil { 517 qcow2.Qcow2Volume = volName 518 (*artifacts.Qcow2)[0] = qcow2 519 return // We will re-use the .img file in this case 520 } 521 // there is only one volume, so get it from the map 522 stateMachine.VolumeNames[volName] = fmt.Sprintf("%s.img", qcow2.Qcow2Name) 523 qcow2.Qcow2Volume = volName 524 (*artifacts.Qcow2)[0] = qcow2 525 } else { 526 if artifacts.Img != nil { 527 return // We will re-use the .img file in this case 528 } 529 stateMachine.VolumeNames[qcow2.Qcow2Volume] = fmt.Sprintf("%s.img", qcow2.Qcow2Name) 530 } 531 } 532 533 var buildRootfsFromTasksState = stateFunc{"build_rootfs_from_tasks", (*StateMachine).buildRootfsFromTasks} 534 535 // Build a rootfs from a list of archive tasks 536 func (stateMachine *StateMachine) buildRootfsFromTasks() error { 537 // currently a no-op pending implementation of the classic image redesign 538 return nil 539 } 540 541 var extractRootfsTarState = stateFunc{"extract_rootfs_tar", (*StateMachine).extractRootfsTar} 542 543 // Extract the rootfs from a tar archive 544 func (stateMachine *StateMachine) extractRootfsTar() error { 545 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 546 547 // make the chroot directory to which we will extract the tar 548 if err := osMkdir(stateMachine.tempDirs.chroot, 0755); err != nil { 549 return fmt.Errorf("Failed to create chroot directory: %s", err.Error()) 550 } 551 552 // convert the URL to a file path 553 // no need to check error here as the validity of the URL 554 // has been confirmed by the schema validation 555 tarPath := strings.TrimPrefix(classicStateMachine.ImageDef.Rootfs.Tarball.TarballURL, "file://") 556 if !filepath.IsAbs(tarPath) { 557 tarPath = filepath.Join(stateMachine.ConfDefPath, tarPath) 558 } 559 560 // if the sha256 sum of the tarball is provided, make sure it matches 561 if classicStateMachine.ImageDef.Rootfs.Tarball.SHA256sum != "" { 562 tarSHA256, err := helper.CalculateSHA256(tarPath) 563 if err != nil { 564 return err 565 } 566 if tarSHA256 != classicStateMachine.ImageDef.Rootfs.Tarball.SHA256sum { 567 return fmt.Errorf("Calculated SHA256 sum of rootfs tarball \"%s\" does not match "+ 568 "the expected value specified in the image definition: \"%s\"", 569 tarSHA256, classicStateMachine.ImageDef.Rootfs.Tarball.SHA256sum) 570 } 571 } 572 573 // now extract the archive 574 return helper.ExtractTarArchive(tarPath, stateMachine.tempDirs.chroot, 575 stateMachine.commonFlags.Verbose, stateMachine.commonFlags.Debug) 576 } 577 578 var germinateState = stateFunc{"germinate", (*StateMachine).germinate} 579 580 // germinate runs the germinate binary and parses the output to create 581 // a list of packages from the seed section of the image definition 582 func (stateMachine *StateMachine) germinate() error { 583 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 584 585 // create a scratch directory to run germinate in 586 germinateDir := filepath.Join(classicStateMachine.stateMachineFlags.WorkDir, "germinate") 587 err := osMkdir(germinateDir, 0755) 588 if err != nil && !os.IsExist(err) { 589 return fmt.Errorf("Error creating germinate directory: \"%s\"", err.Error()) 590 } 591 592 germinateCmd := generateGerminateCmd(classicStateMachine.ImageDef) 593 germinateCmd.Dir = germinateDir 594 595 germinateOutput := helper.SetCommandOutput(germinateCmd, classicStateMachine.commonFlags.Debug) 596 597 if err := germinateCmd.Run(); err != nil { 598 return fmt.Errorf("Error running germinate command \"%s\". Error is \"%s\". Output is: \n%s", 599 germinateCmd.String(), err.Error(), germinateOutput.String()) 600 } 601 602 packageMap := make(map[string]*[]string) 603 packageMap[".seed"] = &classicStateMachine.Packages 604 packageMap[".snaps"] = &classicStateMachine.Snaps 605 for fileExtension, packageList := range packageMap { 606 for _, fileName := range classicStateMachine.ImageDef.Rootfs.Seed.Names { 607 seedFilePath := filepath.Join(germinateDir, fileName+fileExtension) 608 seedFile, err := osOpen(seedFilePath) 609 if err != nil { 610 return fmt.Errorf("Error opening seed file %s: \"%s\"", seedFilePath, err.Error()) 611 } 612 defer seedFile.Close() 613 614 seedScanner := bufio.NewScanner(seedFile) 615 for seedScanner.Scan() { 616 seedLine := seedScanner.Bytes() 617 if seedVersionRegex.Match(seedLine) { 618 packageName := strings.Split(string(seedLine), " ")[0] 619 *packageList = append(*packageList, packageName) 620 } 621 } 622 } 623 } 624 625 return nil 626 } 627 628 // customizeCloudInitFile customizes a cloud-init data file with the given content 629 func customizeCloudInitFile(customData string, seedPath string, fileName string, requireHeader bool) error { 630 if customData == "" { 631 return nil 632 } 633 f, err := osCreate(path.Join(seedPath, fileName)) 634 if err != nil { 635 return err 636 } 637 defer f.Close() 638 639 if requireHeader && !strings.HasPrefix(customData, "#cloud-config\n") { 640 return fmt.Errorf("provided cloud-init customization for %s is missing proper header", fileName) 641 } 642 643 _, err = f.WriteString(customData) 644 if err != nil { 645 return err 646 } 647 648 return nil 649 } 650 651 var customizeCloudInitState = stateFunc{"customize_cloud_init", (*StateMachine).customizeCloudInit} 652 653 // Customize Cloud init with the values in the image definition YAML 654 func (stateMachine *StateMachine) customizeCloudInit() error { 655 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 656 657 cloudInitCustomization := classicStateMachine.ImageDef.Customization.CloudInit 658 659 seedPath := path.Join(classicStateMachine.tempDirs.chroot, "var/lib/cloud/seed/nocloud") 660 err := osMkdirAll(seedPath, 0755) 661 if err != nil { 662 return err 663 } 664 665 err = customizeCloudInitFile(cloudInitCustomization.MetaData, seedPath, "meta-data", false) 666 if err != nil { 667 return err 668 } 669 670 err = customizeCloudInitFile(cloudInitCustomization.UserData, seedPath, "user-data", true) 671 if err != nil { 672 return err 673 } 674 675 err = customizeCloudInitFile(cloudInitCustomization.NetworkConfig, seedPath, "network-config", false) 676 if err != nil { 677 return err 678 } 679 680 datasourceConfig := "# to update this file, run dpkg-reconfigure cloud-init\ndatasource_list: [ NoCloud ]\n" 681 682 dpkgConfigPath := path.Join(classicStateMachine.tempDirs.chroot, "etc/cloud/cloud.cfg.d/90_dpkg.cfg") 683 dpkgConfigFile, err := osCreate(dpkgConfigPath) 684 if err != nil { 685 return err 686 } 687 defer dpkgConfigFile.Close() 688 689 _, err = dpkgConfigFile.WriteString(datasourceConfig) 690 691 return err 692 } 693 694 var customizeFstabState = stateFunc{"customize_fstab", (*StateMachine).customizeFstab} 695 696 // Customize /etc/fstab based on values in the image definition 697 func (stateMachine *StateMachine) customizeFstab() error { 698 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 699 700 fstabPath := filepath.Join(stateMachine.tempDirs.chroot, "etc", "fstab") 701 702 fstabIO, err := osOpenFile(fstabPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 703 if err != nil { 704 return fmt.Errorf("Error opening fstab: %s", err.Error()) 705 } 706 defer fstabIO.Close() 707 708 var fstabEntries []string 709 for _, fstab := range classicStateMachine.ImageDef.Customization.Fstab { 710 var dumpString string 711 if fstab.Dump { 712 dumpString = "1" 713 } else { 714 dumpString = "0" 715 } 716 fstabEntry := fmt.Sprintf("LABEL=%s\t%s\t%s\t%s\t%s\t%d", 717 fstab.Label, 718 fstab.Mountpoint, 719 fstab.FSType, 720 fstab.MountOptions, 721 dumpString, 722 fstab.FsckOrder, 723 ) 724 fstabEntries = append(fstabEntries, fstabEntry) 725 } 726 727 _, err = fstabIO.Write([]byte(strings.Join(fstabEntries, "\n") + "\n")) 728 729 return err 730 } 731 732 var manualCustomizationState = stateFunc{"perform_manual_customization", (*StateMachine).manualCustomization} 733 734 // Handle any manual customizations specified in the image definition 735 func (stateMachine *StateMachine) manualCustomization() error { 736 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 737 738 // copy /etc/resolv.conf from the host system into the chroot if it hasn't already been done 739 err := helperBackupAndCopyResolvConf(classicStateMachine.tempDirs.chroot) 740 if err != nil { 741 return fmt.Errorf("Error setting up /etc/resolv.conf in the chroot: \"%s\"", err.Error()) 742 } 743 744 err = manualMakeDirs(classicStateMachine.ImageDef.Customization.Manual.MakeDirs, stateMachine.tempDirs.chroot, stateMachine.commonFlags.Debug) 745 if err != nil { 746 return err 747 } 748 749 err = manualCopyFile(classicStateMachine.ImageDef.Customization.Manual.CopyFile, classicStateMachine.ConfDefPath, stateMachine.tempDirs.chroot, stateMachine.commonFlags.Debug) 750 if err != nil { 751 return err 752 } 753 754 err = manualExecute(classicStateMachine.ImageDef.Customization.Manual.Execute, stateMachine.tempDirs.chroot, stateMachine.commonFlags.Debug) 755 if err != nil { 756 return err 757 } 758 759 err = manualTouchFile(classicStateMachine.ImageDef.Customization.Manual.TouchFile, stateMachine.tempDirs.chroot, stateMachine.commonFlags.Debug) 760 if err != nil { 761 return err 762 } 763 764 err = manualAddGroup(classicStateMachine.ImageDef.Customization.Manual.AddGroup, stateMachine.tempDirs.chroot, stateMachine.commonFlags.Debug) 765 if err != nil { 766 return err 767 } 768 769 err = manualAddUser(classicStateMachine.ImageDef.Customization.Manual.AddUser, stateMachine.tempDirs.chroot, stateMachine.commonFlags.Debug) 770 if err != nil { 771 return err 772 } 773 774 return nil 775 } 776 777 var prepareClassicImageState = stateFunc{"prepare_image", (*StateMachine).prepareClassicImage} 778 779 // prepareClassicImage calls image.Prepare to stage snaps in classic images 780 func (stateMachine *StateMachine) prepareClassicImage() error { 781 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 782 imageOpts := &image.Options{} 783 var err error 784 785 imageOpts.Snaps, imageOpts.SnapChannels, err = parseSnapsAndChannels(classicStateMachine.Snaps) 786 if err != nil { 787 return err 788 } 789 if stateMachine.commonFlags.Channel != "" { 790 imageOpts.Channel = stateMachine.commonFlags.Channel 791 } 792 793 // plug/slot sanitization needed by provider handling 794 snap.SanitizePlugsSlots = builtin.SanitizePlugsSlots 795 796 err = resetPreseeding(imageOpts, classicStateMachine.tempDirs.chroot, stateMachine.commonFlags.Debug, stateMachine.commonFlags.Verbose) 797 if err != nil { 798 return err 799 } 800 801 err = ensureSnapBasesInstalled(imageOpts) 802 if err != nil { 803 return err 804 } 805 806 err = addExtraSnaps(imageOpts, &classicStateMachine.ImageDef) 807 if err != nil { 808 return err 809 } 810 811 setModelFile(imageOpts, classicStateMachine.ImageDef.ModelAssertion, stateMachine.ConfDefPath) 812 813 imageOpts.Classic = true 814 imageOpts.Architecture = classicStateMachine.ImageDef.Architecture 815 imageOpts.PrepareDir = classicStateMachine.tempDirs.chroot 816 imageOpts.Customizations = *new(image.Customizations) 817 imageOpts.Customizations.Validation = stateMachine.commonFlags.Validation 818 819 // image.Prepare automatically has some output that we only want for 820 // verbose or greater logging 821 if !stateMachine.commonFlags.Debug && !stateMachine.commonFlags.Verbose { 822 oldImageStdout := image.Stdout 823 image.Stdout = io.Discard 824 defer func() { 825 image.Stdout = oldImageStdout 826 }() 827 } 828 829 if err := imagePrepare(imageOpts); err != nil { 830 return fmt.Errorf("Error preparing image: %s", err.Error()) 831 } 832 833 return nil 834 } 835 836 // resetPreseeding checks if the rootfs is already preseeded and reset if necessary. 837 // This can happen when building from a rootfs tarball 838 func resetPreseeding(imageOpts *image.Options, chroot string, debug, verbose bool) error { 839 if !osutil.FileExists(filepath.Join(chroot, "var", "lib", "snapd", "state.json")) { 840 return nil 841 } 842 // first get a list of all preseeded snaps 843 // seededSnaps maps the snap name and channel that was seeded 844 preseededSnaps, err := getPreseededSnaps(chroot) 845 if err != nil { 846 return fmt.Errorf("Error getting list of preseeded snaps from existing rootfs: %s", 847 err.Error()) 848 } 849 for snap, channel := range preseededSnaps { 850 // if a channel is specified on the command line for a snap that was already 851 // preseeded, use the channel from the command line instead of the channel 852 // that was originally used for the preseeding 853 if !helper.SliceHasElement(imageOpts.Snaps, snap) { 854 imageOpts.Snaps = append(imageOpts.Snaps, snap) 855 imageOpts.SnapChannels[snap] = channel 856 } 857 } 858 // preseed.ClassicReset automatically has some output that we only want for 859 // verbose or greater logging 860 if !debug && !verbose { 861 oldPreseedStdout := preseed.Stdout 862 preseed.Stdout = io.Discard 863 defer func() { 864 preseed.Stdout = oldPreseedStdout 865 }() 866 } 867 // We need to use the snap-preseed binary for the reset as well, as using 868 // preseed.ClassicReset() might leave us in a chroot jail 869 cmd := execCommand("/usr/lib/snapd/snap-preseed", "--reset", chroot) 870 err = cmd.Run() 871 if err != nil { 872 return fmt.Errorf("Error resetting preseeding in the chroot. Error is \"%s\"", err.Error()) 873 } 874 875 return nil 876 } 877 878 // ensureSnapBasesInstalled iterates through the list of snaps and ensure that all 879 // of their bases are also set to be installed. Note we only do this for snaps that 880 // are seeded. Users are expected to specify all base and content provider snaps 881 // in the image definition. 882 func ensureSnapBasesInstalled(imageOpts *image.Options) error { 883 snapStore := store.New(nil, nil) 884 snapContext := context.Background() 885 for _, seededSnap := range imageOpts.Snaps { 886 snapSpec := store.SnapSpec{Name: seededSnap} 887 snapInfo, err := snapStore.SnapInfo(snapContext, snapSpec, nil) 888 if err != nil { 889 return fmt.Errorf("Error getting info for snap %s: \"%s\"", 890 seededSnap, err.Error()) 891 } 892 if snapInfo.Base != "" && !helper.SliceHasElement(imageOpts.Snaps, snapInfo.Base) { 893 imageOpts.Snaps = append(imageOpts.Snaps, snapInfo.Base) 894 } 895 } 896 return nil 897 } 898 899 // addExtraSnaps adds any extra snaps from the image definition to the list 900 // This should be done last to ensure the correct channels are being used 901 func addExtraSnaps(imageOpts *image.Options, imageDefinition *imagedefinition.ImageDefinition) error { 902 if imageDefinition.Customization == nil || len(imageDefinition.Customization.ExtraSnaps) == 0 { 903 return nil 904 } 905 906 imageOpts.SeedManifest = seedwriter.NewManifest() 907 for _, extraSnap := range imageDefinition.Customization.ExtraSnaps { 908 if !helper.SliceHasElement(imageOpts.Snaps, extraSnap.SnapName) { 909 imageOpts.Snaps = append(imageOpts.Snaps, extraSnap.SnapName) 910 } 911 if extraSnap.Channel != "" { 912 imageOpts.SnapChannels[extraSnap.SnapName] = extraSnap.Channel 913 } 914 if extraSnap.SnapRevision != 0 { 915 fmt.Printf("WARNING: revision %d for snap %s may not be the latest available version!\n", 916 extraSnap.SnapRevision, 917 extraSnap.SnapName, 918 ) 919 err := imageOpts.SeedManifest.SetAllowedSnapRevision(extraSnap.SnapName, snap.R(extraSnap.SnapRevision)) 920 if err != nil { 921 return fmt.Errorf("error dealing with the extra snap %s: %w", extraSnap.SnapName, err) 922 } 923 } 924 } 925 926 return nil 927 } 928 929 // setModelFile sets the ModelFile based on the given ModelAssertion 930 func setModelFile(imageOpts *image.Options, modelAssertion string, confDefPath string) { 931 modelAssertionPath := strings.TrimPrefix(modelAssertion, "file://") 932 // if no explicit model assertion was given, keep empty ModelFile to let snapd fallback to default 933 // model assertion 934 if len(modelAssertionPath) != 0 { 935 if !filepath.IsAbs(modelAssertionPath) { 936 imageOpts.ModelFile = filepath.Join(confDefPath, modelAssertionPath) 937 } else { 938 imageOpts.ModelFile = modelAssertionPath 939 } 940 } 941 } 942 943 var preseedClassicImageState = stateFunc{"preseed_image", (*StateMachine).preseedClassicImage} 944 945 // preseedClassicImage preseeds the snaps that have already been staged in the chroot 946 func (stateMachine *StateMachine) preseedClassicImage() (err error) { 947 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 948 949 // preseedCmds should be filled as a FIFO list 950 var preseedCmds []*exec.Cmd 951 // teardownCmds should be filled as a LIFO list to unmount first what was mounted last 952 var teardownCmds []*exec.Cmd 953 954 // set up the mount commands 955 mountPoints := []*mountPoint{ 956 { 957 src: "devtmpfs-build", 958 basePath: stateMachine.tempDirs.chroot, 959 relpath: "/dev", 960 typ: "devtmpfs", 961 }, 962 { 963 src: "devpts-build", 964 basePath: stateMachine.tempDirs.chroot, 965 relpath: "/dev/pts", 966 typ: "devpts", 967 opts: []string{"nodev", "nosuid"}, 968 }, 969 { 970 src: "proc-build", 971 basePath: stateMachine.tempDirs.chroot, 972 relpath: "/proc", 973 typ: "proc", 974 }, 975 { 976 src: "none", 977 basePath: stateMachine.tempDirs.chroot, 978 relpath: "/sys/kernel/security", 979 typ: "securityfs", 980 }, 981 { 982 src: "none", 983 basePath: stateMachine.tempDirs.chroot, 984 relpath: "/sys/fs/cgroup", 985 typ: "cgroup2", 986 }, 987 } 988 989 // Make sure we left the system as clean as possible if something has gone wrong 990 defer func() { 991 err = teardownMount(stateMachine.tempDirs.chroot, mountPoints, teardownCmds, err, stateMachine.commonFlags.Debug) 992 }() 993 994 for _, mp := range mountPoints { 995 mountCmds, umountCmds, err := mp.getMountCmd() 996 if err != nil { 997 return fmt.Errorf("Error preparing mountpoint \"%s\": \"%s\"", 998 mp.relpath, 999 err.Error(), 1000 ) 1001 } 1002 preseedCmds = append(preseedCmds, mountCmds...) 1003 teardownCmds = append(umountCmds, teardownCmds...) 1004 } 1005 1006 teardownCmds = append([]*exec.Cmd{ 1007 execCommand("udevadm", "settle"), 1008 }, teardownCmds...) 1009 1010 preseedCmds = append(preseedCmds, 1011 //nolint:gosec,G204 1012 exec.Command("/usr/lib/snapd/snap-preseed", stateMachine.tempDirs.chroot), 1013 ) 1014 1015 err = helper.RunCmds(preseedCmds, classicStateMachine.commonFlags.Debug) 1016 if err != nil { 1017 return err 1018 } 1019 1020 return nil 1021 } 1022 1023 var populateClassicRootfsContentsState = stateFunc{"populate_rootfs_contents", (*StateMachine).populateClassicRootfsContents} 1024 1025 // populateClassicRootfsContents copies over the staged rootfs 1026 // to rootfs. It also changes fstab and handles the --cloud-init flag 1027 func (stateMachine *StateMachine) populateClassicRootfsContents() error { 1028 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1029 1030 // if we backed up resolv.conf then restore it here 1031 err := helperRestoreResolvConf(classicStateMachine.tempDirs.chroot) 1032 if err != nil { 1033 return fmt.Errorf("Error restoring /etc/resolv.conf in the chroot: \"%s\"", err.Error()) 1034 } 1035 1036 files, err := osReadDir(stateMachine.tempDirs.chroot) 1037 if err != nil { 1038 return fmt.Errorf("Error reading chroot dir: %s", err.Error()) 1039 } 1040 1041 for _, srcFile := range files { 1042 srcFile := filepath.Join(stateMachine.tempDirs.chroot, srcFile.Name()) 1043 if err := osutilCopySpecialFile(srcFile, classicStateMachine.tempDirs.rootfs); err != nil { 1044 return fmt.Errorf("Error copying rootfs: %s", err.Error()) 1045 } 1046 } 1047 1048 if classicStateMachine.ImageDef.Customization == nil { 1049 return nil 1050 } 1051 1052 return classicStateMachine.fixFstab() 1053 } 1054 1055 var customizeSourcesListState = stateFunc{"customize_sources_list", (*StateMachine).customizeSourcesList} 1056 1057 // customizeSourcesList customize the /etc/apt/sources.list file for the 1058 // resulting image. This state must be executed once packages installation 1059 // is done, and before other manual customization to let users modify it. 1060 func (stateMachine *StateMachine) customizeSourcesList() error { 1061 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1062 1063 if *classicStateMachine.ImageDef.Rootfs.SourcesListDeb822 { 1064 err := stateMachine.setDeb822SourcesList(classicStateMachine.ImageDef.Deb822TargetSourcesList()) 1065 if err != nil { 1066 return err 1067 } 1068 return stateMachine.setLegacySourcesList(imagedefinition.LegacySourcesListComment) 1069 } 1070 1071 return stateMachine.setLegacySourcesList(classicStateMachine.ImageDef.LegacyTargetSourcesList()) 1072 } 1073 1074 // setLegacySourcesList replaces /etc/apt/sources.list with the given list of entries 1075 // This function will truncate the existing file. 1076 func (stateMachine *StateMachine) setLegacySourcesList(aptSources string) error { 1077 sourcesList := filepath.Join(stateMachine.tempDirs.chroot, "etc", "apt", "sources.list") 1078 sourcesListFile, err := osOpenFile(sourcesList, os.O_TRUNC|os.O_WRONLY, 0644) 1079 if err != nil { 1080 return fmt.Errorf("unable to open sources.list file: %w", err) 1081 } 1082 defer sourcesListFile.Close() 1083 _, err = sourcesListFile.WriteString(aptSources) 1084 if err != nil { 1085 return fmt.Errorf("unable to write apt sources: %w", err) 1086 } 1087 return nil 1088 } 1089 1090 // setDeb822SourcesList replaces /etc/apt/sources.list.d/ubuntu.sources with the given content 1091 // This function will truncate the existing file if any 1092 func (stateMachine *StateMachine) setDeb822SourcesList(sourcesListContent string) error { 1093 sourcesListDir := filepath.Join(stateMachine.tempDirs.chroot, "etc", "apt", "sources.list.d") 1094 err := osMkdirAll(sourcesListDir, 0755) 1095 if err != nil && !os.IsExist(err) { 1096 return fmt.Errorf("Error /etc/apt/sources.list.d directory: %s", err.Error()) 1097 } 1098 1099 sourcesList := filepath.Join(sourcesListDir, "ubuntu.sources") 1100 f, err := osOpenFile(sourcesList, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) 1101 if err != nil { 1102 return fmt.Errorf("unable to open ubuntu.sources file: %w", err) 1103 } 1104 defer f.Close() 1105 1106 _, err = f.WriteString(sourcesListContent) 1107 if err != nil { 1108 return fmt.Errorf("unable to write apt sources: %w", err) 1109 } 1110 1111 return nil 1112 } 1113 1114 // fixFstab makes sure the fstab contains a valid entry for the root mount point 1115 func (stateMachine *StateMachine) fixFstab() error { 1116 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1117 1118 if len(classicStateMachine.ImageDef.Customization.Fstab) != 0 { 1119 return nil 1120 } 1121 1122 fstabPath := filepath.Join(classicStateMachine.tempDirs.rootfs, "etc", "fstab") 1123 fstabBytes, err := osReadFile(fstabPath) 1124 if err != nil { 1125 return fmt.Errorf("Error reading fstab: %s", err.Error()) 1126 } 1127 1128 lines := strings.Split(string(fstabBytes), "\n") 1129 newLines := generateFstabLines(lines) 1130 1131 err = osWriteFile(fstabPath, []byte(strings.Join(newLines, "\n")+"\n"), 0644) 1132 if err != nil { 1133 return fmt.Errorf("Error writing to fstab: %s", err.Error()) 1134 } 1135 return nil 1136 } 1137 1138 // generateFstabLines generates new fstab lines from current ones 1139 func generateFstabLines(lines []string) []string { 1140 rootMountFound := false 1141 newLines := make([]string, 0) 1142 rootFSLabel := "writable" 1143 rootFSOptions := "discard,errors=remount-ro" 1144 fsckOrder := "1" 1145 1146 for _, l := range lines { 1147 if l == "# UNCONFIGURED FSTAB" { 1148 // omit this line if still present 1149 continue 1150 } 1151 1152 if strings.HasPrefix(l, "#") { 1153 newLines = append(newLines, l) 1154 continue 1155 } 1156 1157 entry := strings.Fields(l) 1158 if len(entry) < 6 { 1159 // ignore invalid fstab entry 1160 continue 1161 } 1162 1163 if entry[1] == "/" && !rootMountFound { 1164 entry[0] = "LABEL=" + rootFSLabel 1165 entry[3] = rootFSOptions 1166 entry[5] = fsckOrder 1167 1168 rootMountFound = true 1169 } 1170 newLines = append(newLines, strings.Join(entry, "\t")) 1171 } 1172 1173 if !rootMountFound { 1174 newLines = append(newLines, fmt.Sprintf("LABEL=%s / ext4 %s 0 %s", rootFSLabel, rootFSOptions, fsckOrder)) 1175 } 1176 1177 return newLines 1178 } 1179 1180 var setDefaultLocaleState = stateFunc{"set_default_locale", (*StateMachine).setDefaultLocale} 1181 1182 // Set a default locale if one is not configured beforehand by other customizations 1183 func (stateMachine *StateMachine) setDefaultLocale() error { 1184 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1185 1186 defaultPath := filepath.Join(classicStateMachine.tempDirs.chroot, "etc", "default") 1187 localePath := filepath.Join(defaultPath, "locale") 1188 localeBytes, err := osReadFile(localePath) 1189 if err == nil && localePresentRegex.Find(localeBytes) != nil { 1190 return nil 1191 } 1192 1193 err = osMkdirAll(defaultPath, 0755) 1194 if err != nil { 1195 return fmt.Errorf("Error creating default directory: %s", err.Error()) 1196 } 1197 1198 err = osWriteFile(localePath, []byte("# Default Ubuntu locale\nLANG=C.UTF-8\n"), 0644) 1199 if err != nil { 1200 return fmt.Errorf("Error writing to locale file: %s", err.Error()) 1201 } 1202 return nil 1203 } 1204 1205 var generatePackageManifestState = stateFunc{"generate_package_manifest", (*StateMachine).generatePackageManifest} 1206 1207 // Generate the manifest 1208 func (stateMachine *StateMachine) generatePackageManifest() error { 1209 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1210 1211 // This is basically just a wrapper around dpkg-query 1212 outputPath := filepath.Join(stateMachine.commonFlags.OutputDir, 1213 classicStateMachine.ImageDef.Artifacts.Manifest.ManifestName) 1214 cmd := execCommand("chroot", stateMachine.tempDirs.rootfs, "dpkg-query", "-W", "--showformat=${Package} ${Version}\n") 1215 cmdOutput := helper.SetCommandOutput(cmd, classicStateMachine.commonFlags.Debug) 1216 1217 if err := cmd.Run(); err != nil { 1218 return fmt.Errorf("Error generating package manifest with command \"%s\". "+ 1219 "Error is \"%s\". Full output below:\n%s", 1220 cmd.String(), err.Error(), cmdOutput.String()) 1221 } 1222 1223 // write the output to a file on successful executions 1224 manifest, err := osCreate(outputPath) 1225 if err != nil { 1226 return fmt.Errorf("Error creating manifest file: %s", err.Error()) 1227 } 1228 defer manifest.Close() 1229 _, err = manifest.Write(cmdOutput.Bytes()) 1230 if err != nil { 1231 return fmt.Errorf("error writing the manifest file: %w", err) 1232 } 1233 return nil 1234 } 1235 1236 var generateFilelistState = stateFunc{"generate_filelist", (*StateMachine).generateFilelist} 1237 1238 // Generate the manifest 1239 func (stateMachine *StateMachine) generateFilelist() error { 1240 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1241 1242 // This is basically just a wrapper around find (similar to what we do in livecd-rootfs) 1243 outputPath := filepath.Join(stateMachine.commonFlags.OutputDir, 1244 classicStateMachine.ImageDef.Artifacts.Filelist.FilelistName) 1245 cmd := execCommand("chroot", stateMachine.tempDirs.rootfs, "find", "-xdev") 1246 cmdOutput := helper.SetCommandOutput(cmd, classicStateMachine.commonFlags.Debug) 1247 1248 if err := cmd.Run(); err != nil { 1249 return fmt.Errorf("Error generating file list with command \"%s\". "+ 1250 "Error is \"%s\". Full output below:\n%s", 1251 cmd.String(), err.Error(), cmdOutput.String()) 1252 } 1253 1254 // write the output to a file on successful executions 1255 filelist, err := osCreate(outputPath) 1256 if err != nil { 1257 return fmt.Errorf("Error creating filelist file: %s", err.Error()) 1258 } 1259 defer filelist.Close() 1260 _, err = filelist.Write(cmdOutput.Bytes()) 1261 if err != nil { 1262 return fmt.Errorf("error writing the filelist file: %w", err) 1263 } 1264 return nil 1265 } 1266 1267 var generateRootfsTarballState = stateFunc{"generate_rootfs_tarball", (*StateMachine).generateRootfsTarball} 1268 1269 // Generate the rootfs tarball 1270 func (stateMachine *StateMachine) generateRootfsTarball() error { 1271 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1272 1273 // first create a vanilla uncompressed tar archive 1274 rootfsSrc := filepath.Join(stateMachine.stateMachineFlags.WorkDir, "root") 1275 rootfsDst := filepath.Join(stateMachine.commonFlags.OutputDir, 1276 classicStateMachine.ImageDef.Artifacts.RootfsTar.RootfsTarName) 1277 return helper.CreateTarArchive(rootfsSrc, rootfsDst, 1278 classicStateMachine.ImageDef.Artifacts.RootfsTar.Compression, 1279 stateMachine.commonFlags.Verbose, stateMachine.commonFlags.Debug) 1280 } 1281 1282 var makeQcow2ImgState = stateFunc{"make_qcow2_image", (*StateMachine).makeQcow2Img} 1283 1284 // makeQcow2Img converts raw .img artifacts into qcow2 artifacts 1285 func (stateMachine *StateMachine) makeQcow2Img() error { 1286 classicStateMachine := stateMachine.parent.(*ClassicStateMachine) 1287 1288 for _, qcow2 := range *classicStateMachine.ImageDef.Artifacts.Qcow2 { 1289 backingFile := filepath.Join(stateMachine.commonFlags.OutputDir, stateMachine.VolumeNames[qcow2.Qcow2Volume]) 1290 resultingFile := filepath.Join(stateMachine.commonFlags.OutputDir, qcow2.Qcow2Name) 1291 qemuImgCommand := execCommand("qemu-img", 1292 "convert", 1293 "-c", 1294 "-O", 1295 "qcow2", 1296 "-o", 1297 "compat=0.10", 1298 backingFile, 1299 resultingFile, 1300 ) 1301 qemuOutput := helper.SetCommandOutput(qemuImgCommand, classicStateMachine.commonFlags.Debug) 1302 if err := qemuImgCommand.Run(); err != nil { 1303 return fmt.Errorf("Error creating qcow2 artifact with command \"%s\". "+ 1304 "Error is \"%s\". Full output below:\n%s", 1305 qemuImgCommand.String(), err.Error(), qemuOutput.String()) 1306 } 1307 } 1308 return nil 1309 } 1310 1311 var updateBootloaderState = stateFunc{"update_bootloader", (*StateMachine).updateBootloader} 1312 1313 // updateBootloader determines the bootloader for each volume 1314 // and runs the correct helper function to update the bootloader 1315 func (stateMachine *StateMachine) updateBootloader() error { 1316 if stateMachine.RootfsPartNum == -1 || stateMachine.RootfsVolName == "" { 1317 return fmt.Errorf("Error: could not determine partition number of the root filesystem") 1318 } 1319 volume := stateMachine.GadgetInfo.Volumes[stateMachine.RootfsVolName] 1320 switch volume.Bootloader { 1321 case "grub": 1322 err := stateMachine.updateGrub(stateMachine.RootfsVolName, stateMachine.RootfsPartNum) 1323 if err != nil { 1324 return err 1325 } 1326 default: 1327 fmt.Printf("WARNING: updating bootloader %s not yet supported\n", 1328 volume.Bootloader, 1329 ) 1330 } 1331 return nil 1332 } 1333 1334 var cleanRootfsState = stateFunc{"clean_rootfs", (*StateMachine).cleanRootfs} 1335 1336 // cleanRootfs cleans the created chroot from secrets/values generated 1337 // during the various preceding install steps 1338 func (stateMachine *StateMachine) cleanRootfs() error { 1339 toDelete := []string{ 1340 filepath.Join(stateMachine.tempDirs.chroot, "var", "lib", "dbus", "machine-id"), 1341 } 1342 1343 toTruncate := []string{ 1344 filepath.Join(stateMachine.tempDirs.chroot, "etc", "machine-id"), 1345 } 1346 1347 toCleanFromPattern, err := listWithPatterns(stateMachine.tempDirs.chroot, 1348 []string{ 1349 filepath.Join("etc", "ssh", "ssh_host_*_key.pub"), 1350 filepath.Join("etc", "ssh", "ssh_host_*_key"), 1351 filepath.Join("var", "cache", "debconf", "*-old"), 1352 filepath.Join("var", "lib", "dpkg", "*-old"), 1353 }) 1354 if err != nil { 1355 return err 1356 } 1357 1358 toDelete = append(toDelete, toCleanFromPattern...) 1359 1360 err = doDeleteFiles(toDelete) 1361 if err != nil { 1362 return err 1363 } 1364 1365 toTruncateFromPattern, err := listWithPatterns(stateMachine.tempDirs.chroot, 1366 []string{ 1367 // udev persistent rules 1368 filepath.Join("etc", "udev", "rules.d", "*persistent-net.rules"), 1369 }) 1370 if err != nil { 1371 return err 1372 } 1373 1374 toTruncate = append(toTruncate, toTruncateFromPattern...) 1375 1376 return doTruncateFiles(toTruncate) 1377 } 1378 1379 func listWithPatterns(chroot string, patterns []string) ([]string, error) { 1380 files := make([]string, 0) 1381 for _, pattern := range patterns { 1382 matches, err := filepath.Glob(filepath.Join(chroot, pattern)) 1383 if err != nil { 1384 return nil, fmt.Errorf("unable to list files for pattern %s: %s", pattern, err.Error()) 1385 } 1386 1387 files = append(files, matches...) 1388 } 1389 return files, nil 1390 } 1391 1392 // doDeleteFiles deletes the given list of files 1393 func doDeleteFiles(toDelete []string) error { 1394 for _, f := range toDelete { 1395 err := osRemove(f) 1396 if err != nil && !os.IsNotExist(err) { 1397 return fmt.Errorf("Error removing %s: %s", f, err.Error()) 1398 } 1399 } 1400 return nil 1401 } 1402 1403 // doTruncateFiles truncates content in the given list of files 1404 func doTruncateFiles(toTruncate []string) error { 1405 for _, f := range toTruncate { 1406 err := osTruncate(f, 0) 1407 if err != nil && !os.IsNotExist(err) { 1408 return fmt.Errorf("Error truncating %s: %s", f, err.Error()) 1409 } 1410 } 1411 return nil 1412 }