github.com/tompreston/snapd@v0.0.0-20210817193607-954edfcb9611/image/helpers.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  // TODO: put these in appropriate package(s) once they are clarified a bit more
    23  
    24  import (
    25  	"bytes"
    26  	"context"
    27  	"crypto"
    28  	"encoding/json"
    29  	"errors"
    30  	"fmt"
    31  	"io/ioutil"
    32  	"net/url"
    33  	"os"
    34  	"os/signal"
    35  	"path/filepath"
    36  	"strings"
    37  	"syscall"
    38  
    39  	"github.com/mvo5/goconfigparser"
    40  
    41  	"github.com/snapcore/snapd/asserts"
    42  	"github.com/snapcore/snapd/asserts/snapasserts"
    43  	"github.com/snapcore/snapd/gadget"
    44  	"github.com/snapcore/snapd/logger"
    45  	"github.com/snapcore/snapd/osutil"
    46  	"github.com/snapcore/snapd/overlord/auth"
    47  	"github.com/snapcore/snapd/progress"
    48  	"github.com/snapcore/snapd/release"
    49  	"github.com/snapcore/snapd/snap"
    50  	"github.com/snapcore/snapd/snapdenv"
    51  	"github.com/snapcore/snapd/store"
    52  	"github.com/snapcore/snapd/strutil"
    53  )
    54  
    55  // A Store can find metadata on snaps, download snaps and fetch assertions.
    56  type Store interface {
    57  	SnapAction(context.Context, []*store.CurrentSnap, []*store.SnapAction, store.AssertionQuery, *auth.UserState, *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error)
    58  	Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error
    59  
    60  	Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error)
    61  }
    62  
    63  // ToolingStore wraps access to the store for tools.
    64  type ToolingStore struct {
    65  	sto  Store
    66  	user *auth.UserState
    67  }
    68  
    69  func newToolingStore(arch, storeID string) (*ToolingStore, error) {
    70  	cfg := store.DefaultConfig()
    71  	cfg.Architecture = arch
    72  	cfg.StoreID = storeID
    73  	var user *auth.UserState
    74  	if authFn := os.Getenv("UBUNTU_STORE_AUTH_DATA_FILENAME"); authFn != "" {
    75  		var err error
    76  		user, err = readAuthFile(authFn)
    77  		if err != nil {
    78  			return nil, err
    79  		}
    80  	}
    81  	sto := store.New(cfg, toolingStoreContext{})
    82  	return &ToolingStore{
    83  		sto:  sto,
    84  		user: user,
    85  	}, nil
    86  }
    87  
    88  type authData struct {
    89  	Macaroon   string   `json:"macaroon"`
    90  	Discharges []string `json:"discharges"`
    91  }
    92  
    93  func readAuthFile(authFn string) (*auth.UserState, error) {
    94  	data, err := ioutil.ReadFile(authFn)
    95  	if err != nil {
    96  		return nil, fmt.Errorf("cannot read auth file %q: %v", authFn, err)
    97  	}
    98  
    99  	creds, err := parseAuthFile(authFn, data)
   100  	if err != nil {
   101  		// try snapcraft login format instead
   102  		var err2 error
   103  		creds, err2 = parseSnapcraftLoginFile(authFn, data)
   104  		if err2 != nil {
   105  			trimmed := bytes.TrimSpace(data)
   106  			if len(trimmed) > 0 && trimmed[0] == '[' {
   107  				return nil, err2
   108  			}
   109  			return nil, err
   110  		}
   111  	}
   112  
   113  	return &auth.UserState{
   114  		StoreMacaroon:   creds.Macaroon,
   115  		StoreDischarges: creds.Discharges,
   116  	}, nil
   117  }
   118  
   119  func parseAuthFile(authFn string, data []byte) (*authData, error) {
   120  	var creds authData
   121  	err := json.Unmarshal(data, &creds)
   122  	if err != nil {
   123  		return nil, fmt.Errorf("cannot decode auth file %q: %v", authFn, err)
   124  	}
   125  	if creds.Macaroon == "" || len(creds.Discharges) == 0 {
   126  		return nil, fmt.Errorf("invalid auth file %q: missing fields", authFn)
   127  	}
   128  	return &creds, nil
   129  }
   130  
   131  func snapcraftLoginSection() string {
   132  	if snapdenv.UseStagingStore() {
   133  		return "login.staging.ubuntu.com"
   134  	}
   135  	return "login.ubuntu.com"
   136  }
   137  
   138  func parseSnapcraftLoginFile(authFn string, data []byte) (*authData, error) {
   139  	errPrefix := fmt.Sprintf("invalid snapcraft login file %q", authFn)
   140  
   141  	cfg := goconfigparser.New()
   142  	if err := cfg.ReadString(string(data)); err != nil {
   143  		return nil, fmt.Errorf("%s: %v", errPrefix, err)
   144  	}
   145  	sec := snapcraftLoginSection()
   146  	macaroon, err := cfg.Get(sec, "macaroon")
   147  	if err != nil {
   148  		return nil, fmt.Errorf("%s: %s", errPrefix, err)
   149  	}
   150  	unboundDischarge, err := cfg.Get(sec, "unbound_discharge")
   151  	if err != nil {
   152  		return nil, fmt.Errorf("%s: %v", errPrefix, err)
   153  	}
   154  	if macaroon == "" || unboundDischarge == "" {
   155  		return nil, fmt.Errorf("invalid snapcraft login file %q: empty fields", authFn)
   156  	}
   157  	return &authData{
   158  		Macaroon:   macaroon,
   159  		Discharges: []string{unboundDischarge},
   160  	}, nil
   161  }
   162  
   163  // toolingStoreContext implements trivially store.DeviceAndAuthContext
   164  // except implementing UpdateUserAuth properly to be used to refresh a
   165  // soft-expired user macaroon.
   166  type toolingStoreContext struct{}
   167  
   168  func (tac toolingStoreContext) CloudInfo() (*auth.CloudInfo, error) {
   169  	return nil, nil
   170  }
   171  
   172  func (tac toolingStoreContext) Device() (*auth.DeviceState, error) {
   173  	return &auth.DeviceState{}, nil
   174  }
   175  
   176  func (tac toolingStoreContext) DeviceSessionRequestParams(_ string) (*store.DeviceSessionRequestParams, error) {
   177  	return nil, store.ErrNoSerial
   178  }
   179  
   180  func (tac toolingStoreContext) ProxyStoreParams(defaultURL *url.URL) (proxyStoreID string, proxySroreURL *url.URL, err error) {
   181  	return "", defaultURL, nil
   182  }
   183  
   184  func (tac toolingStoreContext) StoreID(fallback string) (string, error) {
   185  	return fallback, nil
   186  }
   187  
   188  func (tac toolingStoreContext) UpdateDeviceAuth(_ *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) {
   189  	return nil, fmt.Errorf("internal error: no device state in tools")
   190  }
   191  
   192  func (tac toolingStoreContext) UpdateUserAuth(user *auth.UserState, discharges []string) (*auth.UserState, error) {
   193  	user.StoreDischarges = discharges
   194  	return user, nil
   195  }
   196  
   197  func NewToolingStoreFromModel(model *asserts.Model, fallbackArchitecture string) (*ToolingStore, error) {
   198  	architecture := model.Architecture()
   199  	// can happen on classic
   200  	if architecture == "" {
   201  		architecture = fallbackArchitecture
   202  	}
   203  	return newToolingStore(architecture, model.Store())
   204  }
   205  
   206  func NewToolingStore() (*ToolingStore, error) {
   207  	arch := os.Getenv("UBUNTU_STORE_ARCH")
   208  	storeID := os.Getenv("UBUNTU_STORE_ID")
   209  	return newToolingStore(arch, storeID)
   210  }
   211  
   212  // DownloadOptions carries options for downloading snaps plus assertions.
   213  type DownloadOptions struct {
   214  	TargetDir string
   215  	// if TargetPathFunc is not nil it will be invoked
   216  	// to compute the target path for the download and TargetDir is
   217  	// ignored
   218  	TargetPathFunc func(*snap.Info) (string, error)
   219  
   220  	Revision  snap.Revision
   221  	Channel   string
   222  	CohortKey string
   223  	Basename  string
   224  
   225  	LeavePartialOnError bool
   226  }
   227  
   228  var (
   229  	errRevisionAndCohort = errors.New("cannot specify both revision and cohort")
   230  	errPathInBase        = errors.New("cannot specify a path in basename (use target dir for that)")
   231  )
   232  
   233  func (opts *DownloadOptions) validate() error {
   234  	if strings.ContainsRune(opts.Basename, filepath.Separator) {
   235  		return errPathInBase
   236  	}
   237  	if !(opts.Revision.Unset() || opts.CohortKey == "") {
   238  		return errRevisionAndCohort
   239  	}
   240  	return nil
   241  }
   242  
   243  func (opts *DownloadOptions) String() string {
   244  	spec := make([]string, 0, 5)
   245  	if !opts.Revision.Unset() {
   246  		spec = append(spec, fmt.Sprintf("(%s)", opts.Revision))
   247  	}
   248  	if opts.Channel != "" {
   249  		spec = append(spec, fmt.Sprintf("from channel %q", opts.Channel))
   250  	}
   251  	if opts.CohortKey != "" {
   252  		// cohort keys are really long, and the rightmost bit being the
   253  		// interesting bit, so ellipt the rest
   254  		spec = append(spec, fmt.Sprintf(`from cohort %q`, strutil.ElliptLeft(opts.CohortKey, 10)))
   255  	}
   256  	if opts.Basename != "" {
   257  		spec = append(spec, fmt.Sprintf("to %q", opts.Basename+".snap"))
   258  	}
   259  	if opts.TargetDir != "" {
   260  		spec = append(spec, fmt.Sprintf("in %q", opts.TargetDir))
   261  	}
   262  	return strings.Join(spec, " ")
   263  }
   264  
   265  // DownloadSnap downloads the snap with the given name and optionally
   266  // revision using the provided store and options. It returns the final
   267  // full path of the snap and a snap.Info for it and optionally a
   268  // channel the snap got redirected to.
   269  func (tsto *ToolingStore) DownloadSnap(name string, opts DownloadOptions) (targetFn string, info *snap.Info, redirectChannel string, err error) {
   270  	if err := opts.validate(); err != nil {
   271  		return "", nil, "", err
   272  	}
   273  	sto := tsto.sto
   274  
   275  	if opts.TargetPathFunc == nil && opts.TargetDir == "" {
   276  		pwd, err := os.Getwd()
   277  		if err != nil {
   278  			return "", nil, "", err
   279  		}
   280  		opts.TargetDir = pwd
   281  	}
   282  
   283  	if !opts.Revision.Unset() {
   284  		// XXX: is this really necessary (and, if it is, shoudn't we error out instead)
   285  		opts.Channel = ""
   286  	}
   287  
   288  	logger.Debugf("Going to download snap %q %s.", name, &opts)
   289  
   290  	actions := []*store.SnapAction{{
   291  		Action:       "download",
   292  		InstanceName: name,
   293  		Revision:     opts.Revision,
   294  		CohortKey:    opts.CohortKey,
   295  		Channel:      opts.Channel,
   296  	}}
   297  
   298  	sars, _, err := sto.SnapAction(context.TODO(), nil, actions, nil, tsto.user, nil)
   299  	if err != nil {
   300  		// err will be 'cannot download snap "foo": <reasons>'
   301  		return "", nil, "", err
   302  	}
   303  	snap := sars[0].Info
   304  	redirectChannel = sars[0].RedirectChannel
   305  
   306  	if opts.TargetPathFunc == nil {
   307  		baseName := opts.Basename
   308  		if baseName == "" {
   309  			baseName = snap.Filename()
   310  		} else {
   311  			baseName += ".snap"
   312  		}
   313  		targetFn = filepath.Join(opts.TargetDir, baseName)
   314  	} else {
   315  		var err error
   316  		targetFn, err = opts.TargetPathFunc(snap)
   317  		if err != nil {
   318  			return "", nil, "", err
   319  		}
   320  	}
   321  
   322  	// check if we already have the right file
   323  	if osutil.FileExists(targetFn) {
   324  		sha3_384Dgst, size, err := osutil.FileDigest(targetFn, crypto.SHA3_384)
   325  		if err == nil && size == uint64(snap.DownloadInfo.Size) && fmt.Sprintf("%x", sha3_384Dgst) == snap.DownloadInfo.Sha3_384 {
   326  			logger.Debugf("not downloading, using existing file %s", targetFn)
   327  			return targetFn, snap, redirectChannel, nil
   328  		}
   329  		logger.Debugf("File exists but has wrong hash, ignoring (here).")
   330  	}
   331  
   332  	pb := progress.MakeProgressBar()
   333  	defer pb.Finished()
   334  
   335  	// Intercept sigint
   336  	c := make(chan os.Signal, 3)
   337  	signal.Notify(c, syscall.SIGINT)
   338  	go func() {
   339  		<-c
   340  		pb.Finished()
   341  		os.Exit(1)
   342  	}()
   343  
   344  	dlOpts := &store.DownloadOptions{LeavePartialOnError: opts.LeavePartialOnError}
   345  	if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, tsto.user, dlOpts); err != nil {
   346  		return "", nil, "", err
   347  	}
   348  
   349  	signal.Reset(syscall.SIGINT)
   350  
   351  	return targetFn, snap, redirectChannel, nil
   352  }
   353  
   354  // AssertionFetcher creates an asserts.Fetcher for assertions against the given store using dlOpts for authorization, the fetcher will add assertions in the given database and after that also call save for each of them.
   355  func (tsto *ToolingStore) AssertionFetcher(db *asserts.Database, save func(asserts.Assertion) error) asserts.Fetcher {
   356  	retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
   357  		return tsto.sto.Assertion(ref.Type, ref.PrimaryKey, tsto.user)
   358  	}
   359  	save2 := func(a asserts.Assertion) error {
   360  		// for checking
   361  		err := db.Add(a)
   362  		if err != nil {
   363  			if _, ok := err.(*asserts.RevisionError); ok {
   364  				return nil
   365  			}
   366  			return fmt.Errorf("cannot add assertion %v: %v", a.Ref(), err)
   367  		}
   368  		return save(a)
   369  	}
   370  	return asserts.NewFetcher(db, retrieve, save2)
   371  }
   372  
   373  // FetchAndCheckSnapAssertions fetches and cross checks the snap assertions matching the given snap file using the provided asserts.Fetcher and assertion database.
   374  func FetchAndCheckSnapAssertions(snapPath string, info *snap.Info, f asserts.Fetcher, db asserts.RODatabase) (*asserts.SnapDeclaration, error) {
   375  	sha3_384, size, err := asserts.SnapFileSHA3_384(snapPath)
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	// this assumes series "16"
   381  	if err := snapasserts.FetchSnapAssertions(f, sha3_384); err != nil {
   382  		return nil, fmt.Errorf("cannot fetch snap signatures/assertions: %v", err)
   383  	}
   384  
   385  	// cross checks
   386  	if err := snapasserts.CrossCheck(info.InstanceName(), sha3_384, size, &info.SideInfo, db); err != nil {
   387  		return nil, err
   388  	}
   389  
   390  	a, err := db.Find(asserts.SnapDeclarationType, map[string]string{
   391  		"series":  release.Series,
   392  		"snap-id": info.SnapID,
   393  	})
   394  	if err != nil {
   395  		return nil, fmt.Errorf("internal error: lost snap declaration for %q: %v", info.InstanceName(), err)
   396  	}
   397  	return a.(*asserts.SnapDeclaration), nil
   398  }
   399  
   400  // Find provides the snapsserts.Finder interface for snapasserts.DerviceSideInfo
   401  func (tsto *ToolingStore) Find(at *asserts.AssertionType, headers map[string]string) (asserts.Assertion, error) {
   402  	pk, err := asserts.PrimaryKeyFromHeaders(at, headers)
   403  	if err != nil {
   404  		return nil, err
   405  	}
   406  	return tsto.sto.Assertion(at, pk, tsto.user)
   407  }
   408  
   409  // var so that it can be mocked for tests
   410  var writeResolvedContent = writeResolvedContentImpl
   411  
   412  // writeResolvedContent takes gadget.Info and the unpacked
   413  // gadget/kernel snaps and outputs the resolved content from the
   414  // {gadget,kernel}.yaml into a filesystem tree with the structure:
   415  // <prepareImageDir>/resolved-content/<volume-name>/part<structure-nr>/...
   416  //
   417  // E.g.
   418  // /tmp/prep-img/resolved-content/pi/part0/{config.txt,bootcode.bin,...}
   419  func writeResolvedContentImpl(prepareDir string, info *gadget.Info, gadgetUnpackDir, kernelUnpackDir string) error {
   420  	fullPrepareDir, err := filepath.Abs(prepareDir)
   421  	if err != nil {
   422  		return err
   423  	}
   424  	targetDir := filepath.Join(fullPrepareDir, "resolved-content")
   425  
   426  	for volName, vol := range info.Volumes {
   427  		pvol, err := gadget.LayoutVolume(gadgetUnpackDir, kernelUnpackDir, vol, gadget.DefaultConstraints)
   428  		if err != nil {
   429  			return err
   430  		}
   431  		for i, ps := range pvol.LaidOutStructure {
   432  			if !ps.HasFilesystem() {
   433  				continue
   434  			}
   435  			mw, err := gadget.NewMountedFilesystemWriter(&ps, nil)
   436  			if err != nil {
   437  				return err
   438  			}
   439  			// ubuntu-image uses the "part{}" nomenclature
   440  			dst := filepath.Join(targetDir, volName, fmt.Sprintf("part%d", i))
   441  			// on UC20, ensure system-seed links back to the
   442  			// <PrepareDir>/system-seed
   443  			if ps.Role == gadget.SystemSeed {
   444  				uc20systemSeedDir := filepath.Join(fullPrepareDir, "system-seed")
   445  				if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
   446  					return err
   447  				}
   448  				if err := os.Symlink(uc20systemSeedDir, dst); err != nil {
   449  					return err
   450  				}
   451  			}
   452  			if err := mw.Write(dst, nil); err != nil {
   453  				return err
   454  			}
   455  		}
   456  	}
   457  
   458  	return nil
   459  }