github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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/zip"
    24  	"context"
    25  	"crypto"
    26  	"encoding/json"
    27  	"errors"
    28  	"fmt"
    29  	"io"
    30  	"os"
    31  	"path/filepath"
    32  	"sort"
    33  	"time"
    34  
    35  	"github.com/snapcore/snapd/client"
    36  	"github.com/snapcore/snapd/dirs"
    37  	"github.com/snapcore/snapd/logger"
    38  	"github.com/snapcore/snapd/osutil"
    39  	"github.com/snapcore/snapd/snap"
    40  	"github.com/snapcore/snapd/strutil"
    41  )
    42  
    43  const (
    44  	archiveName  = "archive.tgz"
    45  	metadataName = "meta.json"
    46  	metaHashName = "meta.sha3_384"
    47  
    48  	userArchivePrefix = "user/"
    49  	userArchiveSuffix = ".tgz"
    50  )
    51  
    52  var (
    53  	// Stop is used to ask Iter to stop iteration, without it being an error.
    54  	Stop = errors.New("stop iteration")
    55  
    56  	osOpen      = os.Open
    57  	dirNames    = (*os.File).Readdirnames
    58  	backendOpen = Open
    59  )
    60  
    61  // Flags encompasses extra flags for snapshots backend Save.
    62  type Flags struct {
    63  	Auto bool
    64  }
    65  
    66  // Iter loops over all snapshots in the snapshots directory, applying the given
    67  // function to each. The snapshot will be closed after the function returns. If
    68  // the function returns an error, iteration is stopped (and if the error isn't
    69  // Stop, it's returned as the error of the iterator).
    70  func Iter(ctx context.Context, f func(*Reader) error) error {
    71  	if err := ctx.Err(); err != nil {
    72  		return err
    73  	}
    74  
    75  	dir, err := osOpen(dirs.SnapshotsDir)
    76  	if err != nil {
    77  		if osutil.IsDirNotExist(err) {
    78  			// no dir -> no snapshots
    79  			return nil
    80  		}
    81  		return fmt.Errorf("cannot open snapshots directory: %v", err)
    82  	}
    83  	defer dir.Close()
    84  
    85  	var names []string
    86  	var readErr error
    87  	for readErr == nil && err == nil {
    88  		names, readErr = dirNames(dir, 100)
    89  		// note os.Readdirnames can return a non-empty names and a non-nil err
    90  		for _, name := range names {
    91  			if err = ctx.Err(); err != nil {
    92  				break
    93  			}
    94  
    95  			filename := filepath.Join(dirs.SnapshotsDir, name)
    96  			reader, openError := backendOpen(filename)
    97  			// reader can be non-nil even when openError is not nil (in
    98  			// which case reader.Broken will have a reason). f can
    99  			// check and either ignore or return an error when
   100  			// finding a broken snapshot.
   101  			if reader != nil {
   102  				err = f(reader)
   103  			} else {
   104  				// TODO: use warnings instead
   105  				logger.Noticef("Cannot open snapshot %q: %v.", name, openError)
   106  			}
   107  			if openError == nil {
   108  				// if openError was nil the snapshot was opened and needs closing
   109  				if closeError := reader.Close(); err == nil {
   110  					err = closeError
   111  				}
   112  			}
   113  			if err != nil {
   114  				break
   115  			}
   116  		}
   117  	}
   118  
   119  	if readErr != nil && readErr != io.EOF {
   120  		return readErr
   121  	}
   122  
   123  	if err == Stop {
   124  		err = nil
   125  	}
   126  
   127  	return err
   128  }
   129  
   130  // List valid snapshots sets.
   131  func List(ctx context.Context, setID uint64, snapNames []string) ([]client.SnapshotSet, error) {
   132  	setshots := map[uint64][]*client.Snapshot{}
   133  	err := Iter(ctx, func(reader *Reader) error {
   134  		if setID == 0 || reader.SetID == setID {
   135  			if len(snapNames) == 0 || strutil.ListContains(snapNames, reader.Snap) {
   136  				setshots[reader.SetID] = append(setshots[reader.SetID], &reader.Snapshot)
   137  			}
   138  		}
   139  		return nil
   140  	})
   141  
   142  	sets := make([]client.SnapshotSet, 0, len(setshots))
   143  	for id, shots := range setshots {
   144  		sort.Sort(bySnap(shots))
   145  		sets = append(sets, client.SnapshotSet{ID: id, Snapshots: shots})
   146  	}
   147  
   148  	sort.Sort(byID(sets))
   149  
   150  	return sets, err
   151  }
   152  
   153  // Filename of the given client.Snapshot in this backend.
   154  func Filename(snapshot *client.Snapshot) string {
   155  	// this _needs_ the snap name and version to be valid
   156  	return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s_%s_%s.zip", snapshot.SetID, snapshot.Snap, snapshot.Version, snapshot.Revision))
   157  }
   158  
   159  // Save a snapshot
   160  func Save(ctx context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, flags *Flags) (*client.Snapshot, error) {
   161  	if err := os.MkdirAll(dirs.SnapshotsDir, 0700); err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	var auto bool
   166  	if flags != nil {
   167  		auto = flags.Auto
   168  	}
   169  
   170  	snapshot := &client.Snapshot{
   171  		SetID:    id,
   172  		Snap:     si.InstanceName(),
   173  		SnapID:   si.SnapID,
   174  		Revision: si.Revision,
   175  		Version:  si.Version,
   176  		Epoch:    si.Epoch,
   177  		Time:     time.Now(),
   178  		SHA3_384: make(map[string]string),
   179  		Size:     0,
   180  		Conf:     cfg,
   181  		Auto:     auto,
   182  	}
   183  
   184  	aw, err := osutil.NewAtomicFile(Filename(snapshot), 0600, 0, osutil.NoChown, osutil.NoChown)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	// if things worked, we'll commit (and Cancel becomes a NOP)
   189  	defer aw.Cancel()
   190  
   191  	w := zip.NewWriter(aw)
   192  	defer w.Close() // note this does not close the file descriptor (that's done by hand on the atomic writer, above)
   193  	if err := addDirToZip(ctx, snapshot, w, "root", archiveName, si.DataDir()); err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	users, err := usersForUsernames(usernames)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	for _, usr := range users {
   203  		if err := addDirToZip(ctx, snapshot, w, usr.Username, userArchiveName(usr), si.UserDataDir(usr.HomeDir)); err != nil {
   204  			return nil, err
   205  		}
   206  	}
   207  
   208  	metaWriter, err := w.Create(metadataName)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	hasher := crypto.SHA3_384.New()
   214  	enc := json.NewEncoder(io.MultiWriter(metaWriter, hasher))
   215  	if err := enc.Encode(snapshot); err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	hashWriter, err := w.Create(metaHashName)
   220  	if err != nil {
   221  		return nil, err
   222  	}
   223  	fmt.Fprintf(hashWriter, "%x\n", hasher.Sum(nil))
   224  	if err := w.Close(); err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	if err := ctx.Err(); err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	if err := aw.Commit(); err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	return snapshot, nil
   237  }
   238  
   239  var isTesting = osutil.GetenvBool("SNAPPY_TESTING")
   240  
   241  func addDirToZip(ctx context.Context, snapshot *client.Snapshot, w *zip.Writer, username string, entry, dir string) error {
   242  	parent, revdir := filepath.Split(dir)
   243  	exists, isDir, err := osutil.DirExists(parent)
   244  	if err != nil {
   245  		return err
   246  	}
   247  	if exists && !isDir {
   248  		logger.Noticef("Not saving directories under %q in snapshot #%d of %q as it is not a directory.", parent, snapshot.SetID, snapshot.Snap)
   249  		return nil
   250  	}
   251  	if !exists {
   252  		logger.Debugf("Not saving directories under %q in snapshot #%d of %q as it is does not exist.", parent, snapshot.SetID, snapshot.Snap)
   253  		return nil
   254  	}
   255  	tarArgs := []string{
   256  		"--create",
   257  		"--sparse", "--gzip",
   258  		"--directory", parent,
   259  	}
   260  
   261  	noRev, noCommon := true, true
   262  
   263  	exists, isDir, err = osutil.DirExists(dir)
   264  	if err != nil {
   265  		return err
   266  	}
   267  	switch {
   268  	case exists && isDir:
   269  		tarArgs = append(tarArgs, revdir)
   270  		noRev = false
   271  	case exists && !isDir:
   272  		logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", dir, snapshot.SetID, snapshot.Snap)
   273  	case !exists:
   274  		logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", dir, snapshot.SetID, snapshot.Snap)
   275  	}
   276  
   277  	common := filepath.Join(parent, "common")
   278  	exists, isDir, err = osutil.DirExists(common)
   279  	if err != nil {
   280  		return err
   281  	}
   282  	switch {
   283  	case exists && isDir:
   284  		tarArgs = append(tarArgs, "common")
   285  		noCommon = false
   286  	case exists && !isDir:
   287  		logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", common, snapshot.SetID, snapshot.Snap)
   288  	case !exists:
   289  		logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", common, snapshot.SetID, snapshot.Snap)
   290  	}
   291  
   292  	if noCommon && noRev {
   293  		return nil
   294  	}
   295  
   296  	archiveWriter, err := w.CreateHeader(&zip.FileHeader{Name: entry})
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	var sz sizer
   302  	hasher := crypto.SHA3_384.New()
   303  
   304  	cmd := tarAsUser(username, tarArgs...)
   305  	cmd.Stdout = io.MultiWriter(archiveWriter, hasher, &sz)
   306  	matchCounter := &strutil.MatchCounter{N: 1}
   307  	cmd.Stderr = matchCounter
   308  	if isTesting {
   309  		matchCounter.N = -1
   310  		cmd.Stderr = io.MultiWriter(os.Stderr, matchCounter)
   311  	}
   312  	if err := osutil.RunWithContext(ctx, cmd); err != nil {
   313  		matches, count := matchCounter.Matches()
   314  		if count > 0 {
   315  			return fmt.Errorf("cannot create archive: %s (and %d more)", matches[0], count-1)
   316  		}
   317  		return fmt.Errorf("tar failed: %v", err)
   318  	}
   319  
   320  	snapshot.SHA3_384[entry] = fmt.Sprintf("%x", hasher.Sum(nil))
   321  	snapshot.Size += sz.size
   322  
   323  	return nil
   324  }