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 }