github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/overlord/snapshotstate/backend/backend.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 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 backend
    21  
    22  import (
    23  	"archive/tar"
    24  	"archive/zip"
    25  	"context"
    26  	"crypto"
    27  	"encoding/json"
    28  	"errors"
    29  	"fmt"
    30  	"io"
    31  	"os"
    32  	"path"
    33  	"path/filepath"
    34  	"runtime"
    35  	"sort"
    36  	"strconv"
    37  	"strings"
    38  	"syscall"
    39  	"time"
    40  
    41  	"github.com/snapcore/snapd/client"
    42  	"github.com/snapcore/snapd/dirs"
    43  	"github.com/snapcore/snapd/logger"
    44  	"github.com/snapcore/snapd/osutil"
    45  	"github.com/snapcore/snapd/snap"
    46  	"github.com/snapcore/snapd/snapdenv"
    47  	"github.com/snapcore/snapd/strutil"
    48  )
    49  
    50  const (
    51  	archiveName  = "archive.tgz"
    52  	metadataName = "meta.json"
    53  	metaHashName = "meta.sha3_384"
    54  
    55  	userArchivePrefix = "user/"
    56  	userArchiveSuffix = ".tgz"
    57  )
    58  
    59  var (
    60  	// Stop is used to ask Iter to stop iteration, without it being an error.
    61  	Stop = errors.New("stop iteration")
    62  
    63  	osOpen      = os.Open
    64  	dirNames    = (*os.File).Readdirnames
    65  	backendOpen = Open
    66  	timeNow     = time.Now
    67  
    68  	usersForUsernames = usersForUsernamesImpl
    69  )
    70  
    71  // Flags encompasses extra flags for snapshots backend Save.
    72  type Flags struct {
    73  	Auto bool
    74  }
    75  
    76  // LastSnapshotSetID returns the highest set id number for the snapshots stored
    77  // in snapshots directory; set ids are inferred from the filenames.
    78  func LastSnapshotSetID() (uint64, error) {
    79  	dir, err := osOpen(dirs.SnapshotsDir)
    80  	if err != nil {
    81  		if osutil.IsDirNotExist(err) {
    82  			// no snapshots
    83  			return 0, nil
    84  		}
    85  		return 0, fmt.Errorf("cannot open snapshots directory: %v", err)
    86  	}
    87  	defer dir.Close()
    88  
    89  	var maxSetID uint64
    90  
    91  	var readErr error
    92  	for readErr == nil {
    93  		var names []string
    94  		names, readErr = dirNames(dir, 100)
    95  		for _, name := range names {
    96  			if ok, setID := isSnapshotFilename(name); ok {
    97  				if setID > maxSetID {
    98  					maxSetID = setID
    99  				}
   100  			}
   101  		}
   102  	}
   103  	if readErr != nil && readErr != io.EOF {
   104  		return 0, readErr
   105  	}
   106  	return maxSetID, nil
   107  }
   108  
   109  // Iter loops over all snapshots in the snapshots directory, applying the given
   110  // function to each. The snapshot will be closed after the function returns. If
   111  // the function returns an error, iteration is stopped (and if the error isn't
   112  // Stop, it's returned as the error of the iterator).
   113  func Iter(ctx context.Context, f func(*Reader) error) error {
   114  	if err := ctx.Err(); err != nil {
   115  		return err
   116  	}
   117  
   118  	dir, err := osOpen(dirs.SnapshotsDir)
   119  	if err != nil {
   120  		if osutil.IsDirNotExist(err) {
   121  			// no dir -> no snapshots
   122  			return nil
   123  		}
   124  		return fmt.Errorf("cannot open snapshots directory: %v", err)
   125  	}
   126  	defer dir.Close()
   127  
   128  	var names []string
   129  	var readErr error
   130  	for readErr == nil && err == nil {
   131  		names, readErr = dirNames(dir, 100)
   132  		// note os.Readdirnames can return a non-empty names and a non-nil err
   133  		for _, name := range names {
   134  			if err = ctx.Err(); err != nil {
   135  				break
   136  			}
   137  
   138  			// filter out non-snapshot directory entries
   139  			ok, setID := isSnapshotFilename(name)
   140  			if !ok {
   141  				continue
   142  			}
   143  			filename := filepath.Join(dirs.SnapshotsDir, name)
   144  			reader, openError := backendOpen(filename, setID)
   145  			// reader can be non-nil even when openError is not nil (in
   146  			// which case reader.Broken will have a reason). f can
   147  			// check and either ignore or return an error when
   148  			// finding a broken snapshot.
   149  			if reader != nil {
   150  				err = f(reader)
   151  			} else {
   152  				// TODO: use warnings instead
   153  				logger.Noticef("Cannot open snapshot %q: %v.", name, openError)
   154  			}
   155  			if openError == nil {
   156  				// if openError was nil the snapshot was opened and needs closing
   157  				if closeError := reader.Close(); err == nil {
   158  					err = closeError
   159  				}
   160  			}
   161  			if err != nil {
   162  				break
   163  			}
   164  		}
   165  	}
   166  
   167  	if readErr != nil && readErr != io.EOF {
   168  		return readErr
   169  	}
   170  
   171  	if err == Stop {
   172  		err = nil
   173  	}
   174  
   175  	return err
   176  }
   177  
   178  // List valid snapshots sets.
   179  func List(ctx context.Context, setID uint64, snapNames []string) ([]client.SnapshotSet, error) {
   180  	setshots := map[uint64][]*client.Snapshot{}
   181  	err := Iter(ctx, func(reader *Reader) error {
   182  		if setID == 0 || reader.SetID == setID {
   183  			if len(snapNames) == 0 || strutil.ListContains(snapNames, reader.Snap) {
   184  				setshots[reader.SetID] = append(setshots[reader.SetID], &reader.Snapshot)
   185  			}
   186  		}
   187  		return nil
   188  	})
   189  
   190  	sets := make([]client.SnapshotSet, 0, len(setshots))
   191  	for id, shots := range setshots {
   192  		sort.Sort(bySnap(shots))
   193  		sets = append(sets, client.SnapshotSet{ID: id, Snapshots: shots})
   194  	}
   195  
   196  	sort.Sort(byID(sets))
   197  
   198  	return sets, err
   199  }
   200  
   201  // Filename of the given client.Snapshot in this backend.
   202  func Filename(snapshot *client.Snapshot) string {
   203  	// this _needs_ the snap name and version to be valid
   204  	return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s_%s_%s.zip", snapshot.SetID, snapshot.Snap, snapshot.Version, snapshot.Revision))
   205  }
   206  
   207  // isSnapshotFilename checks if the given filePath is a snapshot file name, i.e.
   208  // if it starts with a numeric set id and ends with .zip extension;
   209  // filePath can be just a file name, or a full path.
   210  func isSnapshotFilename(filePath string) (ok bool, setID uint64) {
   211  	fname := filepath.Base(filePath)
   212  	// XXX: we could use a regexp here to match very precisely all the elements
   213  	// of the filename following Filename() above, but perhaps it's better no to
   214  	// go overboard with it in case the format evolves in the future. Only check
   215  	// if the name starts with a set-id and ends with .zip.
   216  	//
   217  	// Filename is "<sid>_<snapName>_version_revision.zip", e.g. "16_snapcraft_4.2_5407.zip"
   218  	ext := filepath.Ext(fname)
   219  	if ext != ".zip" {
   220  		return false, 0
   221  	}
   222  	parts := strings.SplitN(fname, "_", 2)
   223  	if len(parts) != 2 {
   224  		return false, 0
   225  	}
   226  	// invalid: no parts following <sid>_
   227  	if parts[1] == ext {
   228  		return false, 0
   229  	}
   230  	id, err := strconv.Atoi(parts[0])
   231  	if err != nil {
   232  		return false, 0
   233  	}
   234  	return true, uint64(id)
   235  }
   236  
   237  // EstimateSnapshotSize calculates estimated size of the snapshot.
   238  func EstimateSnapshotSize(si *snap.Info, usernames []string) (uint64, error) {
   239  	var total uint64
   240  	calculateSize := func(path string, finfo os.FileInfo, err error) error {
   241  		if finfo.Mode().IsRegular() {
   242  			total += uint64(finfo.Size())
   243  		}
   244  		return err
   245  	}
   246  
   247  	visitDir := func(dir string) error {
   248  		exists, isDir, err := osutil.DirExists(dir)
   249  		if err != nil {
   250  			return err
   251  		}
   252  		if !(exists && isDir) {
   253  			return nil
   254  		}
   255  		return filepath.Walk(dir, calculateSize)
   256  	}
   257  
   258  	for _, dir := range []string{si.DataDir(), si.CommonDataDir()} {
   259  		if err := visitDir(dir); err != nil {
   260  			return 0, err
   261  		}
   262  	}
   263  
   264  	users, err := usersForUsernames(usernames)
   265  	if err != nil {
   266  		return 0, err
   267  	}
   268  	for _, usr := range users {
   269  		if err := visitDir(si.UserDataDir(usr.HomeDir)); err != nil {
   270  			return 0, err
   271  		}
   272  		if err := visitDir(si.UserCommonDataDir(usr.HomeDir)); err != nil {
   273  			return 0, err
   274  		}
   275  	}
   276  
   277  	// XXX: we could use a typical compression factor here
   278  	return total, nil
   279  }
   280  
   281  // Save a snapshot
   282  func Save(ctx context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, flags *Flags) (*client.Snapshot, error) {
   283  	if err := os.MkdirAll(dirs.SnapshotsDir, 0700); err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	var auto bool
   288  	if flags != nil {
   289  		auto = flags.Auto
   290  	}
   291  
   292  	snapshot := &client.Snapshot{
   293  		SetID:    id,
   294  		Snap:     si.InstanceName(),
   295  		SnapID:   si.SnapID,
   296  		Revision: si.Revision,
   297  		Version:  si.Version,
   298  		Epoch:    si.Epoch,
   299  		Time:     timeNow(),
   300  		SHA3_384: make(map[string]string),
   301  		Size:     0,
   302  		Conf:     cfg,
   303  		Auto:     auto,
   304  	}
   305  
   306  	aw, err := osutil.NewAtomicFile(Filename(snapshot), 0600, 0, osutil.NoChown, osutil.NoChown)
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	// if things worked, we'll commit (and Cancel becomes a NOP)
   311  	defer aw.Cancel()
   312  
   313  	w := zip.NewWriter(aw)
   314  	defer w.Close() // note this does not close the file descriptor (that's done by hand on the atomic writer, above)
   315  	if err := addDirToZip(ctx, snapshot, w, "root", archiveName, si.DataDir()); err != nil {
   316  		return nil, err
   317  	}
   318  
   319  	users, err := usersForUsernames(usernames)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  
   324  	for _, usr := range users {
   325  		if err := addDirToZip(ctx, snapshot, w, usr.Username, userArchiveName(usr), si.UserDataDir(usr.HomeDir)); err != nil {
   326  			return nil, err
   327  		}
   328  	}
   329  
   330  	metaWriter, err := w.Create(metadataName)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	hasher := crypto.SHA3_384.New()
   336  	enc := json.NewEncoder(io.MultiWriter(metaWriter, hasher))
   337  	if err := enc.Encode(snapshot); err != nil {
   338  		return nil, err
   339  	}
   340  
   341  	hashWriter, err := w.Create(metaHashName)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	fmt.Fprintf(hashWriter, "%x\n", hasher.Sum(nil))
   346  	if err := w.Close(); err != nil {
   347  		return nil, err
   348  	}
   349  
   350  	if err := ctx.Err(); err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	if err := aw.Commit(); err != nil {
   355  		return nil, err
   356  	}
   357  
   358  	return snapshot, nil
   359  }
   360  
   361  var isTesting = snapdenv.Testing()
   362  
   363  func addDirToZip(ctx context.Context, snapshot *client.Snapshot, w *zip.Writer, username string, entry, dir string) error {
   364  	parent, revdir := filepath.Split(dir)
   365  	exists, isDir, err := osutil.DirExists(parent)
   366  	if err != nil {
   367  		return err
   368  	}
   369  	if exists && !isDir {
   370  		logger.Noticef("Not saving directories under %q in snapshot #%d of %q as it is not a directory.", parent, snapshot.SetID, snapshot.Snap)
   371  		return nil
   372  	}
   373  	if !exists {
   374  		logger.Debugf("Not saving directories under %q in snapshot #%d of %q as it is does not exist.", parent, snapshot.SetID, snapshot.Snap)
   375  		return nil
   376  	}
   377  	tarArgs := []string{
   378  		"--create",
   379  		"--sparse", "--gzip",
   380  		"--directory", parent,
   381  	}
   382  
   383  	noRev, noCommon := true, true
   384  
   385  	exists, isDir, err = osutil.DirExists(dir)
   386  	if err != nil {
   387  		return err
   388  	}
   389  	switch {
   390  	case exists && isDir:
   391  		tarArgs = append(tarArgs, revdir)
   392  		noRev = false
   393  	case exists && !isDir:
   394  		logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", dir, snapshot.SetID, snapshot.Snap)
   395  	case !exists:
   396  		logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", dir, snapshot.SetID, snapshot.Snap)
   397  	}
   398  
   399  	common := filepath.Join(parent, "common")
   400  	exists, isDir, err = osutil.DirExists(common)
   401  	if err != nil {
   402  		return err
   403  	}
   404  	switch {
   405  	case exists && isDir:
   406  		tarArgs = append(tarArgs, "common")
   407  		noCommon = false
   408  	case exists && !isDir:
   409  		logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", common, snapshot.SetID, snapshot.Snap)
   410  	case !exists:
   411  		logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", common, snapshot.SetID, snapshot.Snap)
   412  	}
   413  
   414  	if noCommon && noRev {
   415  		return nil
   416  	}
   417  
   418  	archiveWriter, err := w.CreateHeader(&zip.FileHeader{Name: entry})
   419  	if err != nil {
   420  		return err
   421  	}
   422  
   423  	var sz osutil.Sizer
   424  	hasher := crypto.SHA3_384.New()
   425  
   426  	cmd := tarAsUser(username, tarArgs...)
   427  	cmd.Stdout = io.MultiWriter(archiveWriter, hasher, &sz)
   428  	matchCounter := &strutil.MatchCounter{N: 1}
   429  	cmd.Stderr = matchCounter
   430  	if isTesting {
   431  		matchCounter.N = -1
   432  		cmd.Stderr = io.MultiWriter(os.Stderr, matchCounter)
   433  	}
   434  	if err := osutil.RunWithContext(ctx, cmd); err != nil {
   435  		matches, count := matchCounter.Matches()
   436  		if count > 0 {
   437  			return fmt.Errorf("cannot create archive: %s (and %d more)", matches[0], count-1)
   438  		}
   439  		return fmt.Errorf("tar failed: %v", err)
   440  	}
   441  
   442  	snapshot.SHA3_384[entry] = fmt.Sprintf("%x", hasher.Sum(nil))
   443  	snapshot.Size += sz.Size()
   444  
   445  	return nil
   446  }
   447  
   448  type exportMetadata struct {
   449  	Format int       `json:"format"`
   450  	Date   time.Time `json:"date"`
   451  	Files  []string  `json:"files"`
   452  }
   453  
   454  type SnapshotExport struct {
   455  	// open snapshot files
   456  	snapshotFiles []*os.File
   457  
   458  	// remember setID mostly for nicer errors
   459  	setID uint64
   460  
   461  	// cached size, needs to be calculated with CalculateSize
   462  	size int64
   463  }
   464  
   465  // NewSnapshotExport will return a SnapshotExport structure. It must be
   466  // Close()ed after use to avoid leaking file descriptors.
   467  func NewSnapshotExport(ctx context.Context, setID uint64) (se *SnapshotExport, err error) {
   468  	var snapshotFiles []*os.File
   469  
   470  	defer func() {
   471  		// cleanup any open FDs if anything goes wrong
   472  		if err != nil {
   473  			for _, f := range snapshotFiles {
   474  				f.Close()
   475  			}
   476  		}
   477  	}()
   478  
   479  	// Open all files first and keep the file descriptors
   480  	// open. The caller should have locked the state so that no
   481  	// delete/change snapshot operations can happen while the
   482  	// files are getting opened.
   483  	err = Iter(ctx, func(reader *Reader) error {
   484  		if reader.SetID == setID {
   485  			// Duplicate the file descriptor of the reader we were handed as
   486  			// Iter() closes those as soon as this unnamed returns. We
   487  			// re-package the file descriptor into snapshotFiles below.
   488  			fd, err := syscall.Dup(int(reader.Fd()))
   489  			if err != nil {
   490  				return fmt.Errorf("cannot duplicate descriptor: %v", err)
   491  			}
   492  			f := os.NewFile(uintptr(fd), reader.Name())
   493  			if f == nil {
   494  				return fmt.Errorf("cannot open file from descriptor %d", fd)
   495  			}
   496  			snapshotFiles = append(snapshotFiles, f)
   497  		}
   498  		return nil
   499  	})
   500  	if err != nil {
   501  		return nil, fmt.Errorf("cannot export snapshot %v: %v", setID, err)
   502  	}
   503  	if len(snapshotFiles) == 0 {
   504  		return nil, fmt.Errorf("no snapshot data found for %v", setID)
   505  	}
   506  
   507  	se = &SnapshotExport{snapshotFiles: snapshotFiles, setID: setID}
   508  
   509  	// ensure we never leak FDs even if the user does not call close
   510  	runtime.SetFinalizer(se, (*SnapshotExport).Close)
   511  
   512  	return se, nil
   513  }
   514  
   515  // Init will calculate the snapshot size. This can take some time
   516  // so it should be called without any locks. The SnapshotExport
   517  // keeps the FDs open so even files moved/deleted will be found.
   518  func (se *SnapshotExport) Init() error {
   519  	// Export once into a dummy writer so that we can set the size
   520  	// of the export. This is then used to set the Content-Length
   521  	// in the response correctly.
   522  	//
   523  	// Note that the size of the generated tar could change if the
   524  	// time switches between this export and the export we stream
   525  	// to the client to a time after the year 2242. This is unlikely
   526  	// but a known issue with this approach here.
   527  	var sz osutil.Sizer
   528  	if err := se.StreamTo(&sz); err != nil {
   529  		return fmt.Errorf("cannot calculcate the size for %v: %s", se.setID, err)
   530  	}
   531  	se.size = sz.Size()
   532  	return nil
   533  }
   534  
   535  func (se *SnapshotExport) Size() int64 {
   536  	return se.size
   537  }
   538  
   539  func (se *SnapshotExport) Close() {
   540  	for _, f := range se.snapshotFiles {
   541  		f.Close()
   542  	}
   543  	se.snapshotFiles = nil
   544  }
   545  
   546  func (se *SnapshotExport) StreamTo(w io.Writer) error {
   547  	// write out a tar
   548  	var files []string
   549  	tw := tar.NewWriter(w)
   550  	defer tw.Close()
   551  	for _, snapshotFile := range se.snapshotFiles {
   552  		stat, err := snapshotFile.Stat()
   553  		if err != nil {
   554  			return err
   555  		}
   556  		if !stat.Mode().IsRegular() {
   557  			// should never happen
   558  			return fmt.Errorf("unexported special file %q in snapshot: %s", stat.Name(), stat.Mode())
   559  		}
   560  		if _, err := snapshotFile.Seek(0, 0); err != nil {
   561  			return fmt.Errorf("cannot seek on %v: %v", stat.Name(), err)
   562  		}
   563  		hdr, err := tar.FileInfoHeader(stat, "")
   564  		if err != nil {
   565  			return fmt.Errorf("symlink: %v", stat.Name())
   566  		}
   567  		if err = tw.WriteHeader(hdr); err != nil {
   568  			return fmt.Errorf("cannot write header for %v: %v", stat.Name(), err)
   569  		}
   570  		if _, err := io.Copy(tw, snapshotFile); err != nil {
   571  			return fmt.Errorf("cannot write data for %v: %v", stat.Name(), err)
   572  		}
   573  
   574  		files = append(files, path.Base(snapshotFile.Name()))
   575  	}
   576  
   577  	// write the metadata last, then the client can use that to
   578  	// validate the archive is complete
   579  	meta := exportMetadata{
   580  		Format: 1,
   581  		Date:   timeNow(),
   582  		Files:  files,
   583  	}
   584  	metaDataBuf, err := json.Marshal(&meta)
   585  	if err != nil {
   586  		return fmt.Errorf("cannot marshal meta-data: %v", err)
   587  	}
   588  	hdr := &tar.Header{
   589  		Typeflag: tar.TypeReg,
   590  		Name:     "export.json",
   591  		Size:     int64(len(metaDataBuf)),
   592  		Mode:     0640,
   593  		ModTime:  timeNow(),
   594  	}
   595  	if err := tw.WriteHeader(hdr); err != nil {
   596  		return err
   597  	}
   598  	if _, err := tw.Write(metaDataBuf); err != nil {
   599  		return err
   600  	}
   601  
   602  	return nil
   603  }