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 }