gitee.com/mysnapcore/mysnapd@v0.1.0/image/image_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2022 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package image
    21  
    22  import (
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"syscall"
    30  	"time"
    31  
    32  	"gitee.com/mysnapcore/mysnapd/asserts"
    33  	"gitee.com/mysnapcore/mysnapd/asserts/sysdb"
    34  	"gitee.com/mysnapcore/mysnapd/boot"
    35  	"gitee.com/mysnapcore/mysnapd/dirs"
    36  	"gitee.com/mysnapcore/mysnapd/gadget"
    37  	"gitee.com/mysnapcore/mysnapd/store/tooling"
    38  
    39  	// to set sysconfig.ApplyFilesystemOnlyDefaults hook
    40  	"gitee.com/mysnapcore/mysnapd/image/preseed"
    41  	"gitee.com/mysnapcore/mysnapd/osutil"
    42  	_ "gitee.com/mysnapcore/mysnapd/overlord/configstate/configcore"
    43  	"gitee.com/mysnapcore/mysnapd/release"
    44  	"gitee.com/mysnapcore/mysnapd/seed/seedwriter"
    45  	"gitee.com/mysnapcore/mysnapd/snap"
    46  	"gitee.com/mysnapcore/mysnapd/snap/snapfile"
    47  	"gitee.com/mysnapcore/mysnapd/snap/squashfs"
    48  	"gitee.com/mysnapcore/mysnapd/strutil"
    49  	"gitee.com/mysnapcore/mysnapd/sysconfig"
    50  )
    51  
    52  var (
    53  	Stdout io.Writer = os.Stdout
    54  	Stderr io.Writer = os.Stderr
    55  
    56  	preseedCore20 = preseed.Core20
    57  )
    58  
    59  func (custo *Customizations) validate(model *asserts.Model) error {
    60  	core20 := model.Grade() != asserts.ModelGradeUnset
    61  	var unsupported []string
    62  	unsupportedConsoleConfDisable := func() {
    63  		if custo.ConsoleConf == "disabled" {
    64  			unsupported = append(unsupported, "console-conf disable")
    65  		}
    66  	}
    67  	unsupportedBootFlags := func() {
    68  		if len(custo.BootFlags) != 0 {
    69  			unsupported = append(unsupported, fmt.Sprintf("boot flags (%s)", strings.Join(custo.BootFlags, " ")))
    70  		}
    71  	}
    72  
    73  	kind := "UC16/18"
    74  	switch {
    75  	case core20:
    76  		kind = "UC20+"
    77  		// TODO:UC20: consider supporting these with grade dangerous?
    78  		unsupportedConsoleConfDisable()
    79  		if custo.CloudInitUserData != "" {
    80  			unsupported = append(unsupported, "cloud-init user-data")
    81  		}
    82  	case model.Classic():
    83  		kind = "classic"
    84  		unsupportedConsoleConfDisable()
    85  		unsupportedBootFlags()
    86  	default:
    87  		// UC16/18
    88  		unsupportedBootFlags()
    89  	}
    90  	if len(unsupported) != 0 {
    91  		return fmt.Errorf("cannot support with %s model requested customizations: %s", kind, strings.Join(unsupported, ", "))
    92  	}
    93  	return nil
    94  }
    95  
    96  // classicHasSnaps returns whether the model or options specify any snaps for the classic case
    97  func classicHasSnaps(model *asserts.Model, opts *Options) bool {
    98  	return model.Gadget() != "" || len(model.RequiredNoEssentialSnaps()) != 0 || len(opts.Snaps) != 0
    99  }
   100  
   101  var newToolingStoreFromModel = tooling.NewToolingStoreFromModel
   102  
   103  func Prepare(opts *Options) error {
   104  	var model *asserts.Model
   105  	var err error
   106  	if opts.Classic && opts.ModelFile == "" {
   107  		// ubuntu-image has a use case for preseeding snaps in an arbitrary rootfs
   108  		// using its --filesystem flag. This rootfs may or may not already have
   109  		// snaps preseeded in it. In the case where the provided rootfs has no
   110  		// snaps seeded image.Prepare will be called with no model assertion,
   111  		// and we then use the GenericClassicModel.
   112  		model = sysdb.GenericClassicModel()
   113  	} else {
   114  		model, err = decodeModelAssertion(opts)
   115  		if err != nil {
   116  			return err
   117  		}
   118  	}
   119  
   120  	if model.Architecture() != "" && opts.Architecture != "" && model.Architecture() != opts.Architecture {
   121  		return fmt.Errorf("cannot override model architecture: %s", model.Architecture())
   122  	}
   123  
   124  	if !opts.Classic {
   125  		if model.Classic() {
   126  			return fmt.Errorf("--classic mode is required to prepare the image for a classic model")
   127  		}
   128  	} else {
   129  		if !model.Classic() {
   130  			return fmt.Errorf("cannot prepare the image for a core model with --classic mode specified")
   131  		}
   132  		if model.Architecture() == "" && classicHasSnaps(model, opts) && opts.Architecture == "" {
   133  			return fmt.Errorf("cannot have snaps for a classic image without an architecture in the model or from --arch")
   134  		}
   135  	}
   136  
   137  	tsto, err := newToolingStoreFromModel(model, opts.Architecture)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	tsto.Stdout = Stdout
   142  
   143  	// FIXME: limitation until we can pass series parametrized much more
   144  	if model.Series() != release.Series {
   145  		return fmt.Errorf("model with series %q != %q unsupported", model.Series(), release.Series)
   146  	}
   147  
   148  	if err := opts.Customizations.validate(model); err != nil {
   149  		return err
   150  	}
   151  
   152  	if err := setupSeed(tsto, model, opts); err != nil {
   153  		return err
   154  	}
   155  
   156  	if opts.Preseed {
   157  		// TODO: support UC22
   158  		if model.Classic() {
   159  			return fmt.Errorf("cannot preseed the image for a classic model")
   160  		}
   161  		if model.Base() != "core20" {
   162  			return fmt.Errorf("cannot preseed the image for a model other than core20")
   163  		}
   164  		return preseedCore20(opts.PrepareDir, opts.PreseedSignKey, opts.AppArmorKernelFeaturesDir)
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  // these are postponed, not implemented or abandoned, not finalized,
   171  // don't let them sneak in into a used model assertion
   172  var reserved = []string{"core", "os", "class", "allowed-modes"}
   173  
   174  func decodeModelAssertion(opts *Options) (*asserts.Model, error) {
   175  	fn := opts.ModelFile
   176  
   177  	rawAssert, err := ioutil.ReadFile(fn)
   178  	if err != nil {
   179  		return nil, fmt.Errorf("cannot read model assertion: %s", err)
   180  	}
   181  
   182  	ass, err := asserts.Decode(rawAssert)
   183  	if err != nil {
   184  		return nil, fmt.Errorf("cannot decode model assertion %q: %s", fn, err)
   185  	}
   186  	modela, ok := ass.(*asserts.Model)
   187  	if !ok {
   188  		return nil, fmt.Errorf("assertion in %q is not a model assertion", fn)
   189  	}
   190  
   191  	for _, rsvd := range reserved {
   192  		if modela.Header(rsvd) != nil {
   193  			return nil, fmt.Errorf("model assertion cannot have reserved/unsupported header %q set", rsvd)
   194  		}
   195  	}
   196  
   197  	return modela, nil
   198  }
   199  
   200  func unpackSnap(gadgetFname, gadgetUnpackDir string) error {
   201  	// FIXME: jumping through layers here, we need to make
   202  	//        unpack part of the container interface (again)
   203  	snap := squashfs.New(gadgetFname)
   204  	return snap.Unpack("*", gadgetUnpackDir)
   205  }
   206  
   207  func installCloudConfig(rootDir, gadgetDir string) error {
   208  	cloudConfig := filepath.Join(gadgetDir, "cloud.conf")
   209  	if !osutil.FileExists(cloudConfig) {
   210  		return nil
   211  	}
   212  
   213  	cloudDir := filepath.Join(rootDir, "/etc/cloud")
   214  	if err := os.MkdirAll(cloudDir, 0755); err != nil {
   215  		return err
   216  	}
   217  	dst := filepath.Join(cloudDir, "cloud.cfg")
   218  	return osutil.CopyFile(cloudConfig, dst, osutil.CopyFlagOverwrite)
   219  }
   220  
   221  func customizeImage(rootDir, defaultsDir string, custo *Customizations) error {
   222  	// customize with cloud-init user-data
   223  	if custo.CloudInitUserData != "" {
   224  		// See
   225  		// https://cloudinit.readthedocs.io/en/latest/topics/dir_layout.html
   226  		// https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html
   227  		varCloudDir := filepath.Join(rootDir, "/var/lib/cloud/seed/nocloud-net")
   228  		if err := os.MkdirAll(varCloudDir, 0755); err != nil {
   229  			return err
   230  		}
   231  		if err := ioutil.WriteFile(filepath.Join(varCloudDir, "meta-data"), []byte("instance-id: nocloud-static\n"), 0644); err != nil {
   232  			return err
   233  		}
   234  		dst := filepath.Join(varCloudDir, "user-data")
   235  		if err := osutil.CopyFile(custo.CloudInitUserData, dst, osutil.CopyFlagOverwrite); err != nil {
   236  			return err
   237  		}
   238  	}
   239  
   240  	if custo.ConsoleConf == "disabled" {
   241  		// TODO: maybe share code with configcore somehow
   242  		consoleConfDisabled := filepath.Join(defaultsDir, "/var/lib/console-conf/complete")
   243  		if err := os.MkdirAll(filepath.Dir(consoleConfDisabled), 0755); err != nil {
   244  			return err
   245  		}
   246  		if err := ioutil.WriteFile(consoleConfDisabled, []byte("console-conf has been disabled by image customization\n"), 0644); err != nil {
   247  			return err
   248  		}
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  var trusted = sysdb.Trusted()
   255  
   256  func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) {
   257  	prevTrusted := trusted
   258  	trusted = mockTrusted
   259  	return func() {
   260  		trusted = prevTrusted
   261  	}
   262  }
   263  
   264  func makeLabel(now time.Time) string {
   265  	return now.UTC().Format("20060102")
   266  }
   267  
   268  var setupSeed = func(tsto *tooling.ToolingStore, model *asserts.Model, opts *Options) error {
   269  	if model.Classic() != opts.Classic {
   270  		return fmt.Errorf("internal error: classic model but classic mode not set")
   271  	}
   272  
   273  	core20 := model.Grade() != asserts.ModelGradeUnset
   274  	var rootDir string
   275  	var bootRootDir string
   276  	var seedDir string
   277  	var label string
   278  	if !core20 {
   279  		if opts.Classic {
   280  			// Classic, PrepareDir is the root dir itself
   281  			rootDir = opts.PrepareDir
   282  		} else {
   283  			// Core 16/18,  writing for the writeable partition
   284  			rootDir = filepath.Join(opts.PrepareDir, "image")
   285  			bootRootDir = rootDir
   286  		}
   287  		seedDir = dirs.SnapSeedDirUnder(rootDir)
   288  
   289  		// validity check target
   290  		if osutil.FileExists(dirs.SnapStateFileUnder(rootDir)) {
   291  			return fmt.Errorf("cannot prepare seed over existing system or an already booted image, detected state file %s", dirs.SnapStateFileUnder(rootDir))
   292  		}
   293  		if snaps, _ := filepath.Glob(filepath.Join(dirs.SnapBlobDirUnder(rootDir), "*.snap")); len(snaps) > 0 {
   294  			return fmt.Errorf("expected empty snap dir in rootdir, got: %v", snaps)
   295  		}
   296  
   297  	} else {
   298  		// Core 20, writing for the system-seed partition
   299  		seedDir = filepath.Join(opts.PrepareDir, "system-seed")
   300  		label = makeLabel(time.Now())
   301  		bootRootDir = seedDir
   302  
   303  		// validity check target
   304  		if systems, _ := filepath.Glob(filepath.Join(seedDir, "systems", "*")); len(systems) > 0 {
   305  			return fmt.Errorf("expected empty systems dir in system-seed, got: %v", systems)
   306  		}
   307  	}
   308  
   309  	// TODO: developer database in home or use snapd (but need
   310  	// a bit more API there, potential issues when crossing stores/series)
   311  	db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
   312  		Backstore: asserts.NewMemoryBackstore(),
   313  		Trusted:   trusted,
   314  	})
   315  	if err != nil {
   316  		return err
   317  	}
   318  
   319  	wOpts := &seedwriter.Options{
   320  		SeedDir:        seedDir,
   321  		Label:          label,
   322  		DefaultChannel: opts.Channel,
   323  
   324  		TestSkipCopyUnverifiedModel: osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL"),
   325  	}
   326  
   327  	w, err := seedwriter.New(model, wOpts)
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	optSnaps := make([]*seedwriter.OptionsSnap, 0, len(opts.Snaps))
   333  	for _, snapName := range opts.Snaps {
   334  		var optSnap seedwriter.OptionsSnap
   335  		if strings.HasSuffix(snapName, ".snap") {
   336  			// local
   337  			optSnap.Path = snapName
   338  		} else {
   339  			optSnap.Name = snapName
   340  		}
   341  		optSnap.Channel = opts.SnapChannels[snapName]
   342  		optSnaps = append(optSnaps, &optSnap)
   343  	}
   344  
   345  	if err := w.SetOptionsSnaps(optSnaps); err != nil {
   346  		return err
   347  	}
   348  
   349  	var gadgetUnpackDir, kernelUnpackDir string
   350  	// create directory for later unpacking the gadget in
   351  	if !opts.Classic {
   352  		gadgetUnpackDir = filepath.Join(opts.PrepareDir, "gadget")
   353  		kernelUnpackDir = filepath.Join(opts.PrepareDir, "kernel")
   354  		for _, unpackDir := range []string{gadgetUnpackDir, kernelUnpackDir} {
   355  			if err := os.MkdirAll(unpackDir, 0755); err != nil {
   356  				return fmt.Errorf("cannot create unpack dir %q: %s", unpackDir, err)
   357  			}
   358  		}
   359  	}
   360  
   361  	newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher {
   362  		return tsto.AssertionFetcher(db, save)
   363  	}
   364  	f, err := w.Start(db, newFetcher)
   365  	if err != nil {
   366  		return err
   367  	}
   368  
   369  	if opts.Customizations.Validation == "" && !opts.Classic {
   370  		fmt.Fprintf(Stderr, "WARNING: proceeding to download snaps ignoring validations, this default will change in the future. For now use --validation=enforce for validations to be taken into account, pass instead --validation=ignore to preserve current behavior going forward\n")
   371  	}
   372  	if opts.Customizations.Validation == "" {
   373  		opts.Customizations.Validation = "ignore"
   374  	}
   375  
   376  	localSnaps, err := w.LocalSnaps()
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	var curSnaps []*tooling.CurrentSnap
   382  	for _, sn := range localSnaps {
   383  		si, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, model, f, db)
   384  		if err != nil && !asserts.IsNotFound(err) {
   385  			return err
   386  		}
   387  
   388  		snapFile, err := snapfile.Open(sn.Path)
   389  		if err != nil {
   390  			return err
   391  		}
   392  		info, err := snap.ReadInfoFromSnapFile(snapFile, si)
   393  		if err != nil {
   394  			return err
   395  		}
   396  
   397  		if err := w.SetInfo(sn, info); err != nil {
   398  			return err
   399  		}
   400  		sn.ARefs = aRefs
   401  
   402  		if info.ID() != "" {
   403  			curSnaps = append(curSnaps, &tooling.CurrentSnap{
   404  				SnapName: info.SnapName(),
   405  				SnapID:   info.ID(),
   406  				Revision: info.Revision,
   407  				Epoch:    info.Epoch,
   408  			})
   409  		}
   410  	}
   411  
   412  	if err := w.InfoDerived(); err != nil {
   413  		return err
   414  	}
   415  
   416  	for {
   417  		toDownload, err := w.SnapsToDownload()
   418  		if err != nil {
   419  			return err
   420  		}
   421  
   422  		byName := make(map[string]*seedwriter.SeedSnap, len(toDownload))
   423  		beforeDownload := func(info *snap.Info) (string, error) {
   424  			sn := byName[info.SnapName()]
   425  			if sn == nil {
   426  				return "", fmt.Errorf("internal error: downloading unexpected snap %q", info.SnapName())
   427  			}
   428  			fmt.Fprintf(Stdout, "Fetching %s\n", sn.SnapName())
   429  			if err := w.SetInfo(sn, info); err != nil {
   430  				return "", err
   431  			}
   432  			return sn.Path, nil
   433  		}
   434  		snapToDownloadOptions := make([]tooling.SnapToDownload, len(toDownload))
   435  		for i, sn := range toDownload {
   436  			byName[sn.SnapName()] = sn
   437  			snapToDownloadOptions[i].Snap = sn
   438  			snapToDownloadOptions[i].Channel = sn.Channel
   439  			snapToDownloadOptions[i].CohortKey = opts.WideCohortKey
   440  		}
   441  		downloadedSnaps, err := tsto.DownloadMany(snapToDownloadOptions, curSnaps, tooling.DownloadManyOptions{
   442  			BeforeDownloadFunc: beforeDownload,
   443  			EnforceValidation:  opts.Customizations.Validation == "enforce",
   444  		})
   445  		if err != nil {
   446  			return err
   447  		}
   448  
   449  		for _, sn := range toDownload {
   450  			dlsn := downloadedSnaps[sn.SnapName()]
   451  
   452  			if err := w.SetRedirectChannel(sn, dlsn.RedirectChannel); err != nil {
   453  				return err
   454  			}
   455  
   456  			// fetch snap assertions
   457  			prev := len(f.Refs())
   458  			if _, err = FetchAndCheckSnapAssertions(dlsn.Path, dlsn.Info, model, f, db); err != nil {
   459  				return err
   460  			}
   461  			aRefs := f.Refs()[prev:]
   462  			sn.ARefs = aRefs
   463  
   464  			curSnaps = append(curSnaps, &tooling.CurrentSnap{
   465  				SnapName: sn.Info.SnapName(),
   466  				SnapID:   sn.Info.ID(),
   467  				Revision: sn.Info.Revision,
   468  				Epoch:    sn.Info.Epoch,
   469  				Channel:  sn.Channel,
   470  			})
   471  		}
   472  
   473  		complete, err := w.Downloaded()
   474  		if err != nil {
   475  			return err
   476  		}
   477  		if complete {
   478  			break
   479  		}
   480  	}
   481  
   482  	for _, warn := range w.Warnings() {
   483  		fmt.Fprintf(Stderr, "WARNING: %s\n", warn)
   484  	}
   485  
   486  	unassertedSnaps, err := w.UnassertedSnaps()
   487  	if err != nil {
   488  		return err
   489  	}
   490  	if len(unassertedSnaps) > 0 {
   491  		locals := make([]string, len(unassertedSnaps))
   492  		for i, sn := range unassertedSnaps {
   493  			locals[i] = sn.SnapName()
   494  		}
   495  		fmt.Fprintf(Stderr, "WARNING: %s installed from local snaps disconnected from a store cannot be refreshed subsequently!\n", strutil.Quoted(locals))
   496  	}
   497  
   498  	copySnap := func(name, src, dst string) error {
   499  		fmt.Fprintf(Stdout, "Copying %q (%s)\n", src, name)
   500  		return osutil.CopyFile(src, dst, 0)
   501  	}
   502  	if err := w.SeedSnaps(copySnap); err != nil {
   503  		return err
   504  	}
   505  
   506  	if err := w.WriteMeta(); err != nil {
   507  		return err
   508  	}
   509  
   510  	// TODO: There will be classic UC20+ model based systems
   511  	//       that will have a bootable  ubuntu-seed partition.
   512  	//       This will need to be handled here eventually too.
   513  	if opts.Classic {
   514  		var fpath string
   515  		if core20 {
   516  			fpath = filepath.Join(seedDir, "systems")
   517  		} else {
   518  			fpath = filepath.Join(seedDir, "seed.yaml")
   519  		}
   520  		// warn about ownership if not root:root
   521  		fi, err := os.Stat(fpath)
   522  		if err != nil {
   523  			return fmt.Errorf("cannot stat %q: %s", fpath, err)
   524  		}
   525  		if st, ok := fi.Sys().(*syscall.Stat_t); ok {
   526  			if st.Uid != 0 || st.Gid != 0 {
   527  				fmt.Fprintf(Stderr, "WARNING: ensure that the contents under %s are owned by root:root in the (final) image\n", seedDir)
   528  			}
   529  		}
   530  		// done already
   531  		return nil
   532  	}
   533  
   534  	bootSnaps, err := w.BootSnaps()
   535  	if err != nil {
   536  		return err
   537  	}
   538  
   539  	bootWith := &boot.BootableSet{
   540  		UnpackedGadgetDir: gadgetUnpackDir,
   541  		Recovery:          core20,
   542  	}
   543  	if label != "" {
   544  		bootWith.RecoverySystemDir = filepath.Join("/systems/", label)
   545  		bootWith.RecoverySystemLabel = label
   546  	}
   547  
   548  	// find the snap.Info/path for kernel/os/base/gadget so
   549  	// that boot.MakeBootable can DTRT
   550  	kernelFname := ""
   551  	for _, sn := range bootSnaps {
   552  		switch sn.Info.Type() {
   553  		case snap.TypeGadget:
   554  			bootWith.Gadget = sn.Info
   555  			bootWith.GadgetPath = sn.Path
   556  		case snap.TypeOS, snap.TypeBase:
   557  			bootWith.Base = sn.Info
   558  			bootWith.BasePath = sn.Path
   559  		case snap.TypeKernel:
   560  			bootWith.Kernel = sn.Info
   561  			bootWith.KernelPath = sn.Path
   562  			kernelFname = sn.Path
   563  		}
   564  	}
   565  
   566  	// unpacking the gadget for core models
   567  	if err := unpackSnap(bootWith.GadgetPath, gadgetUnpackDir); err != nil {
   568  		return err
   569  	}
   570  	if err := unpackSnap(kernelFname, kernelUnpackDir); err != nil {
   571  		return err
   572  	}
   573  
   574  	gadgetInfo, err := gadget.ReadInfoAndValidate(gadgetUnpackDir, model, nil)
   575  	if err != nil {
   576  		return err
   577  	}
   578  	// validate content against the kernel as well
   579  	if err := gadget.ValidateContent(gadgetInfo, gadgetUnpackDir, kernelUnpackDir); err != nil {
   580  		return err
   581  	}
   582  
   583  	// write resolved content to structure root
   584  	if err := writeResolvedContent(opts.PrepareDir, gadgetInfo, gadgetUnpackDir, kernelUnpackDir); err != nil {
   585  		return err
   586  	}
   587  
   588  	if err := boot.MakeBootableImage(model, bootRootDir, bootWith, opts.Customizations.BootFlags); err != nil {
   589  		return err
   590  	}
   591  
   592  	// early config & cloud-init config (done at install for Core 20)
   593  	if !core20 {
   594  		// and the cloud-init things
   595  		if err := installCloudConfig(rootDir, gadgetUnpackDir); err != nil {
   596  			return err
   597  		}
   598  
   599  		defaultsDir := sysconfig.WritableDefaultsDir(rootDir)
   600  		defaults := gadget.SystemDefaults(gadgetInfo.Defaults)
   601  		if len(defaults) > 0 {
   602  			if err := os.MkdirAll(sysconfig.WritableDefaultsDir(rootDir, "/etc"), 0755); err != nil {
   603  				return err
   604  			}
   605  			return sysconfig.ApplyFilesystemOnlyDefaults(model, defaultsDir, defaults)
   606  		}
   607  
   608  		customizeImage(rootDir, defaultsDir, &opts.Customizations)
   609  	}
   610  
   611  	return nil
   612  }