github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/image/image_linux.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2021 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  	"github.com/snapcore/snapd/asserts"
    33  	"github.com/snapcore/snapd/asserts/sysdb"
    34  	"github.com/snapcore/snapd/boot"
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/gadget"
    37  	"github.com/snapcore/snapd/osutil"
    38  
    39  	// to set sysconfig.ApplyFilesystemOnlyDefaults hook
    40  	_ "github.com/snapcore/snapd/overlord/configstate/configcore"
    41  	"github.com/snapcore/snapd/release"
    42  	"github.com/snapcore/snapd/seed/seedwriter"
    43  	"github.com/snapcore/snapd/snap"
    44  	"github.com/snapcore/snapd/snap/snapfile"
    45  	"github.com/snapcore/snapd/snap/squashfs"
    46  	"github.com/snapcore/snapd/strutil"
    47  	"github.com/snapcore/snapd/sysconfig"
    48  )
    49  
    50  var (
    51  	Stdout io.Writer = os.Stdout
    52  	Stderr io.Writer = os.Stderr
    53  )
    54  
    55  func (custo *Customizations) validate(model *asserts.Model) error {
    56  	core20 := model.Grade() != asserts.ModelGradeUnset
    57  	var unsupported []string
    58  	unsupportedConsoleConfDisable := func() {
    59  		if custo.ConsoleConf == "disabled" {
    60  			unsupported = append(unsupported, "console-conf disable")
    61  		}
    62  	}
    63  	unsupportedBootFlags := func() {
    64  		if len(custo.BootFlags) != 0 {
    65  			unsupported = append(unsupported, fmt.Sprintf("boot flags (%s)", strings.Join(custo.BootFlags, " ")))
    66  		}
    67  	}
    68  
    69  	kind := "UC16/18"
    70  	switch {
    71  	case core20:
    72  		kind = "UC20"
    73  		// TODO:UC20: consider supporting these with grade dangerous?
    74  		unsupportedConsoleConfDisable()
    75  		if custo.CloudInitUserData != "" {
    76  			unsupported = append(unsupported, "cloud-init user-data")
    77  		}
    78  	case model.Classic():
    79  		kind = "classic"
    80  		unsupportedConsoleConfDisable()
    81  		unsupportedBootFlags()
    82  	default:
    83  		// UC16/18
    84  		unsupportedBootFlags()
    85  	}
    86  	if len(unsupported) != 0 {
    87  		return fmt.Errorf("cannot support with %s model requested customizations: %s", kind, strings.Join(unsupported, ", "))
    88  	}
    89  	return nil
    90  }
    91  
    92  // classicHasSnaps returns whether the model or options specify any snaps for the classic case
    93  func classicHasSnaps(model *asserts.Model, opts *Options) bool {
    94  	return model.Gadget() != "" || len(model.RequiredNoEssentialSnaps()) != 0 || len(opts.Snaps) != 0
    95  }
    96  
    97  func Prepare(opts *Options) error {
    98  	model, err := decodeModelAssertion(opts)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	if model.Architecture() != "" && opts.Architecture != "" && model.Architecture() != opts.Architecture {
   104  		return fmt.Errorf("cannot override model architecture: %s", model.Architecture())
   105  	}
   106  
   107  	if !opts.Classic {
   108  		if model.Classic() {
   109  			return fmt.Errorf("--classic mode is required to prepare the image for a classic model")
   110  		}
   111  	} else {
   112  		if !model.Classic() {
   113  			return fmt.Errorf("cannot prepare the image for a core model with --classic mode specified")
   114  		}
   115  		if model.Architecture() == "" && classicHasSnaps(model, opts) && opts.Architecture == "" {
   116  			return fmt.Errorf("cannot have snaps for a classic image without an architecture in the model or from --arch")
   117  		}
   118  	}
   119  
   120  	tsto, err := NewToolingStoreFromModel(model, opts.Architecture)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	// FIXME: limitation until we can pass series parametrized much more
   126  	if model.Series() != release.Series {
   127  		return fmt.Errorf("model with series %q != %q unsupported", model.Series(), release.Series)
   128  	}
   129  
   130  	if err := opts.Customizations.validate(model); err != nil {
   131  		return err
   132  	}
   133  
   134  	return setupSeed(tsto, model, opts)
   135  }
   136  
   137  // these are postponed, not implemented or abandoned, not finalized,
   138  // don't let them sneak in into a used model assertion
   139  var reserved = []string{"core", "os", "class", "allowed-modes"}
   140  
   141  func decodeModelAssertion(opts *Options) (*asserts.Model, error) {
   142  	fn := opts.ModelFile
   143  
   144  	rawAssert, err := ioutil.ReadFile(fn)
   145  	if err != nil {
   146  		return nil, fmt.Errorf("cannot read model assertion: %s", err)
   147  	}
   148  
   149  	ass, err := asserts.Decode(rawAssert)
   150  	if err != nil {
   151  		return nil, fmt.Errorf("cannot decode model assertion %q: %s", fn, err)
   152  	}
   153  	modela, ok := ass.(*asserts.Model)
   154  	if !ok {
   155  		return nil, fmt.Errorf("assertion in %q is not a model assertion", fn)
   156  	}
   157  
   158  	for _, rsvd := range reserved {
   159  		if modela.Header(rsvd) != nil {
   160  			return nil, fmt.Errorf("model assertion cannot have reserved/unsupported header %q set", rsvd)
   161  		}
   162  	}
   163  
   164  	return modela, nil
   165  }
   166  
   167  func unpackSnap(gadgetFname, gadgetUnpackDir string) error {
   168  	// FIXME: jumping through layers here, we need to make
   169  	//        unpack part of the container interface (again)
   170  	snap := squashfs.New(gadgetFname)
   171  	return snap.Unpack("*", gadgetUnpackDir)
   172  }
   173  
   174  func installCloudConfig(rootDir, gadgetDir string) error {
   175  	cloudConfig := filepath.Join(gadgetDir, "cloud.conf")
   176  	if !osutil.FileExists(cloudConfig) {
   177  		return nil
   178  	}
   179  
   180  	cloudDir := filepath.Join(rootDir, "/etc/cloud")
   181  	if err := os.MkdirAll(cloudDir, 0755); err != nil {
   182  		return err
   183  	}
   184  	dst := filepath.Join(cloudDir, "cloud.cfg")
   185  	return osutil.CopyFile(cloudConfig, dst, osutil.CopyFlagOverwrite)
   186  }
   187  
   188  func customizeImage(rootDir, defaultsDir string, custo *Customizations) error {
   189  	// customize with cloud-init user-data
   190  	if custo.CloudInitUserData != "" {
   191  		// See
   192  		// https://cloudinit.readthedocs.io/en/latest/topics/dir_layout.html
   193  		// https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html
   194  		varCloudDir := filepath.Join(rootDir, "/var/lib/cloud/seed/nocloud-net")
   195  		if err := os.MkdirAll(varCloudDir, 0755); err != nil {
   196  			return err
   197  		}
   198  		if err := ioutil.WriteFile(filepath.Join(varCloudDir, "meta-data"), []byte("instance-id: nocloud-static\n"), 0644); err != nil {
   199  			return err
   200  		}
   201  		dst := filepath.Join(varCloudDir, "user-data")
   202  		if err := osutil.CopyFile(custo.CloudInitUserData, dst, osutil.CopyFlagOverwrite); err != nil {
   203  			return err
   204  		}
   205  	}
   206  
   207  	if custo.ConsoleConf == "disabled" {
   208  		// TODO: maybe share code with configcore somehow
   209  		consoleConfDisabled := filepath.Join(defaultsDir, "/var/lib/console-conf/complete")
   210  		if err := os.MkdirAll(filepath.Dir(consoleConfDisabled), 0755); err != nil {
   211  			return err
   212  		}
   213  		if err := ioutil.WriteFile(consoleConfDisabled, []byte("console-conf has been disabled by image customization\n"), 0644); err != nil {
   214  			return err
   215  		}
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  var trusted = sysdb.Trusted()
   222  
   223  func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) {
   224  	prevTrusted := trusted
   225  	trusted = mockTrusted
   226  	return func() {
   227  		trusted = prevTrusted
   228  	}
   229  }
   230  
   231  func makeLabel(now time.Time) string {
   232  	return now.UTC().Format("20060102")
   233  }
   234  
   235  func setupSeed(tsto *ToolingStore, model *asserts.Model, opts *Options) error {
   236  	if model.Classic() != opts.Classic {
   237  		return fmt.Errorf("internal error: classic model but classic mode not set")
   238  	}
   239  
   240  	core20 := model.Grade() != asserts.ModelGradeUnset
   241  	var rootDir string
   242  	var bootRootDir string
   243  	var seedDir string
   244  	var label string
   245  	if !core20 {
   246  		if opts.Classic {
   247  			// Classic, PrepareDir is the root dir itself
   248  			rootDir = opts.PrepareDir
   249  		} else {
   250  			// Core 16/18,  writing for the writeable partition
   251  			rootDir = filepath.Join(opts.PrepareDir, "image")
   252  			bootRootDir = rootDir
   253  		}
   254  		seedDir = dirs.SnapSeedDirUnder(rootDir)
   255  
   256  		// sanity check target
   257  		if osutil.FileExists(dirs.SnapStateFileUnder(rootDir)) {
   258  			return fmt.Errorf("cannot prepare seed over existing system or an already booted image, detected state file %s", dirs.SnapStateFileUnder(rootDir))
   259  		}
   260  		if snaps, _ := filepath.Glob(filepath.Join(dirs.SnapBlobDirUnder(rootDir), "*.snap")); len(snaps) > 0 {
   261  			return fmt.Errorf("expected empty snap dir in rootdir, got: %v", snaps)
   262  		}
   263  
   264  	} else {
   265  		// Core 20, writing for the system-seed partition
   266  		seedDir = filepath.Join(opts.PrepareDir, "system-seed")
   267  		label = makeLabel(time.Now())
   268  		bootRootDir = seedDir
   269  
   270  		// sanity check target
   271  		if systems, _ := filepath.Glob(filepath.Join(seedDir, "systems", "*")); len(systems) > 0 {
   272  			return fmt.Errorf("expected empty systems dir in system-seed, got: %v", systems)
   273  		}
   274  	}
   275  
   276  	// TODO: developer database in home or use snapd (but need
   277  	// a bit more API there, potential issues when crossing stores/series)
   278  	db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
   279  		Backstore: asserts.NewMemoryBackstore(),
   280  		Trusted:   trusted,
   281  	})
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	wOpts := &seedwriter.Options{
   287  		SeedDir:        seedDir,
   288  		Label:          label,
   289  		DefaultChannel: opts.Channel,
   290  
   291  		TestSkipCopyUnverifiedModel: osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL"),
   292  	}
   293  
   294  	w, err := seedwriter.New(model, wOpts)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	optSnaps := make([]*seedwriter.OptionsSnap, 0, len(opts.Snaps))
   300  	for _, snapName := range opts.Snaps {
   301  		var optSnap seedwriter.OptionsSnap
   302  		if strings.HasSuffix(snapName, ".snap") {
   303  			// local
   304  			optSnap.Path = snapName
   305  		} else {
   306  			optSnap.Name = snapName
   307  		}
   308  		optSnap.Channel = opts.SnapChannels[snapName]
   309  		optSnaps = append(optSnaps, &optSnap)
   310  	}
   311  
   312  	if err := w.SetOptionsSnaps(optSnaps); err != nil {
   313  		return err
   314  	}
   315  
   316  	var gadgetUnpackDir, kernelUnpackDir string
   317  	// create directory for later unpacking the gadget in
   318  	if !opts.Classic {
   319  		gadgetUnpackDir = filepath.Join(opts.PrepareDir, "gadget")
   320  		kernelUnpackDir = filepath.Join(opts.PrepareDir, "kernel")
   321  		for _, unpackDir := range []string{gadgetUnpackDir, kernelUnpackDir} {
   322  			if err := os.MkdirAll(unpackDir, 0755); err != nil {
   323  				return fmt.Errorf("cannot create unpack dir %q: %s", unpackDir, err)
   324  			}
   325  		}
   326  	}
   327  
   328  	newFetcher := func(save func(asserts.Assertion) error) asserts.Fetcher {
   329  		return tsto.AssertionFetcher(db, save)
   330  	}
   331  	f, err := w.Start(db, newFetcher)
   332  	if err != nil {
   333  		return err
   334  	}
   335  
   336  	localSnaps, err := w.LocalSnaps()
   337  	if err != nil {
   338  		return err
   339  	}
   340  
   341  	for _, sn := range localSnaps {
   342  		si, aRefs, err := seedwriter.DeriveSideInfo(sn.Path, f, db)
   343  		if err != nil && !asserts.IsNotFound(err) {
   344  			return err
   345  		}
   346  
   347  		snapFile, err := snapfile.Open(sn.Path)
   348  		if err != nil {
   349  			return err
   350  		}
   351  		info, err := snap.ReadInfoFromSnapFile(snapFile, si)
   352  		if err != nil {
   353  			return err
   354  		}
   355  
   356  		if err := w.SetInfo(sn, info); err != nil {
   357  			return err
   358  		}
   359  		sn.ARefs = aRefs
   360  	}
   361  
   362  	if err := w.InfoDerived(); err != nil {
   363  		return err
   364  	}
   365  
   366  	for {
   367  		toDownload, err := w.SnapsToDownload()
   368  		if err != nil {
   369  			return err
   370  		}
   371  
   372  		for _, sn := range toDownload {
   373  			fmt.Fprintf(Stdout, "Fetching %s\n", sn.SnapName())
   374  
   375  			targetPathFunc := func(info *snap.Info) (string, error) {
   376  				if err := w.SetInfo(sn, info); err != nil {
   377  					return "", err
   378  				}
   379  				return sn.Path, nil
   380  			}
   381  
   382  			dlOpts := DownloadOptions{
   383  				TargetPathFunc: targetPathFunc,
   384  				Channel:        sn.Channel,
   385  				CohortKey:      opts.WideCohortKey,
   386  			}
   387  			fn, info, redirectChannel, err := tsto.DownloadSnap(sn.SnapName(), dlOpts) // TODO|XXX make this take the SnapRef really
   388  			if err != nil {
   389  				return err
   390  			}
   391  			if err := w.SetRedirectChannel(sn, redirectChannel); err != nil {
   392  				return err
   393  			}
   394  
   395  			// fetch snap assertions
   396  			prev := len(f.Refs())
   397  			if _, err = FetchAndCheckSnapAssertions(fn, info, f, db); err != nil {
   398  				return err
   399  			}
   400  			aRefs := f.Refs()[prev:]
   401  			sn.ARefs = aRefs
   402  		}
   403  
   404  		complete, err := w.Downloaded()
   405  		if err != nil {
   406  			return err
   407  		}
   408  		if complete {
   409  			break
   410  		}
   411  	}
   412  
   413  	for _, warn := range w.Warnings() {
   414  		fmt.Fprintf(Stderr, "WARNING: %s\n", warn)
   415  	}
   416  
   417  	unassertedSnaps, err := w.UnassertedSnaps()
   418  	if err != nil {
   419  		return err
   420  	}
   421  	if len(unassertedSnaps) > 0 {
   422  		locals := make([]string, len(unassertedSnaps))
   423  		for i, sn := range unassertedSnaps {
   424  			locals[i] = sn.SnapName()
   425  		}
   426  		fmt.Fprintf(Stderr, "WARNING: %s installed from local snaps disconnected from a store cannot be refreshed subsequently!\n", strutil.Quoted(locals))
   427  	}
   428  
   429  	copySnap := func(name, src, dst string) error {
   430  		fmt.Fprintf(Stdout, "Copying %q (%s)\n", src, name)
   431  		return osutil.CopyFile(src, dst, 0)
   432  	}
   433  	if err := w.SeedSnaps(copySnap); err != nil {
   434  		return err
   435  	}
   436  
   437  	if err := w.WriteMeta(); err != nil {
   438  		return err
   439  	}
   440  
   441  	if opts.Classic {
   442  		// TODO:UC20: consider Core 20 extended models vs classic
   443  		seedFn := filepath.Join(seedDir, "seed.yaml")
   444  		// warn about ownership if not root:root
   445  		fi, err := os.Stat(seedFn)
   446  		if err != nil {
   447  			return fmt.Errorf("cannot stat seed.yaml: %s", err)
   448  		}
   449  		if st, ok := fi.Sys().(*syscall.Stat_t); ok {
   450  			if st.Uid != 0 || st.Gid != 0 {
   451  				fmt.Fprintf(Stderr, "WARNING: ensure that the contents under %s are owned by root:root in the (final) image", seedDir)
   452  			}
   453  		}
   454  		// done already
   455  		return nil
   456  	}
   457  
   458  	bootSnaps, err := w.BootSnaps()
   459  	if err != nil {
   460  		return err
   461  	}
   462  
   463  	bootWith := &boot.BootableSet{
   464  		UnpackedGadgetDir: gadgetUnpackDir,
   465  		Recovery:          core20,
   466  	}
   467  	if label != "" {
   468  		bootWith.RecoverySystemDir = filepath.Join("/systems/", label)
   469  		bootWith.RecoverySystemLabel = label
   470  	}
   471  
   472  	// find the gadget file
   473  	// find the snap.Info/path for kernel/os/base so
   474  	// that boot.MakeBootable can DTRT
   475  	gadgetFname := ""
   476  	kernelFname := ""
   477  	for _, sn := range bootSnaps {
   478  		switch sn.Info.Type() {
   479  		case snap.TypeGadget:
   480  			gadgetFname = sn.Path
   481  		case snap.TypeOS, snap.TypeBase:
   482  			bootWith.Base = sn.Info
   483  			bootWith.BasePath = sn.Path
   484  		case snap.TypeKernel:
   485  			bootWith.Kernel = sn.Info
   486  			bootWith.KernelPath = sn.Path
   487  			kernelFname = sn.Path
   488  		}
   489  	}
   490  
   491  	// unpacking the gadget for core models
   492  	if err := unpackSnap(gadgetFname, gadgetUnpackDir); err != nil {
   493  		return err
   494  	}
   495  	if err := unpackSnap(kernelFname, kernelUnpackDir); err != nil {
   496  		return err
   497  	}
   498  
   499  	if err := boot.MakeBootableImage(model, bootRootDir, bootWith, opts.Customizations.BootFlags); err != nil {
   500  		return err
   501  	}
   502  
   503  	gadgetInfo, err := gadget.ReadInfoAndValidate(gadgetUnpackDir, model, nil)
   504  	if err != nil {
   505  		return err
   506  	}
   507  	// validate content against the kernel as well
   508  	if err := gadget.ValidateContent(gadgetInfo, gadgetUnpackDir, kernelUnpackDir); err != nil {
   509  		return err
   510  	}
   511  
   512  	// write resolved content to structure root
   513  	if err := writeResolvedContent(opts.PrepareDir, gadgetInfo, gadgetUnpackDir, kernelUnpackDir); err != nil {
   514  		return err
   515  	}
   516  
   517  	// early config & cloud-init config (done at install for Core 20)
   518  	if !core20 {
   519  		// and the cloud-init things
   520  		if err := installCloudConfig(rootDir, gadgetUnpackDir); err != nil {
   521  			return err
   522  		}
   523  
   524  		defaultsDir := sysconfig.WritableDefaultsDir(rootDir)
   525  		defaults := gadget.SystemDefaults(gadgetInfo.Defaults)
   526  		if len(defaults) > 0 {
   527  			if err := os.MkdirAll(sysconfig.WritableDefaultsDir(rootDir, "/etc"), 0755); err != nil {
   528  				return err
   529  			}
   530  			return sysconfig.ApplyFilesystemOnlyDefaults(model, defaultsDir, defaults)
   531  		}
   532  
   533  		customizeImage(rootDir, defaultsDir, &opts.Customizations)
   534  	}
   535  
   536  	return nil
   537  }