github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/image/image_linux.go (about)

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