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  }