Compare commits
3 Commits
main
...
c222-verif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
313586a57e | ||
|
|
7b0171cdd1 | ||
|
|
651714fb4c |
@ -63,6 +63,14 @@ func AddCommands(cmd *cobra.Command) {
|
|||||||
flags.AddAllStorageFlags(sc)
|
flags.AddAllStorageFlags(sc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add verify command separately. It's not bound to a single service.
|
||||||
|
verifyCommand := verifyCmd()
|
||||||
|
flags.AddAllProviderFlags(verifyCommand)
|
||||||
|
flags.AddAllStorageFlags(verifyCommand)
|
||||||
|
flags.AddReadOnlyFlag(verifyCommand)
|
||||||
|
|
||||||
|
backupC.AddCommand(verifyCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -163,6 +171,42 @@ func handleDeleteCmd(cmd *cobra.Command, args []string) error {
|
|||||||
return cmd.Help()
|
return cmd.Help()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The backup verify subcommand.
|
||||||
|
// `corso backup verify`
|
||||||
|
var verifyCommand = "verify"
|
||||||
|
|
||||||
|
func verifyCmd() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: verifyCommand,
|
||||||
|
Short: "Verifies all backups have all their data",
|
||||||
|
RunE: handleVerifyCmd,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for calls to `corso backup delete`.
|
||||||
|
// Produces the same output as `corso backup delete --help`.
|
||||||
|
func handleVerifyCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
ctx := cmd.Context()
|
||||||
|
|
||||||
|
r, _, err := utils.AccountConnectAndWriteRepoConfig(
|
||||||
|
ctx,
|
||||||
|
cmd,
|
||||||
|
// Service doesn't really matter but we need to give it something valid.
|
||||||
|
path.OneDriveService)
|
||||||
|
if err != nil {
|
||||||
|
return Only(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer utils.CloseRepo(ctx, r)
|
||||||
|
|
||||||
|
if err := r.VerifyBackups(ctx); err != nil {
|
||||||
|
return Only(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// common handlers
|
// common handlers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const (
|
|||||||
// Corso Flags
|
// Corso Flags
|
||||||
PassphraseFN = "passphrase"
|
PassphraseFN = "passphrase"
|
||||||
NewPassphraseFN = "new-passphrase"
|
NewPassphraseFN = "new-passphrase"
|
||||||
|
ReadOnlyFN = "readonly"
|
||||||
SucceedIfExistsFN = "succeed-if-exists"
|
SucceedIfExistsFN = "succeed-if-exists"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,6 +26,7 @@ var (
|
|||||||
AWSSessionTokenFV string
|
AWSSessionTokenFV string
|
||||||
PassphraseFV string
|
PassphraseFV string
|
||||||
NewPhasephraseFV string
|
NewPhasephraseFV string
|
||||||
|
ReadOnlyFV bool
|
||||||
SucceedIfExistsFV bool
|
SucceedIfExistsFV bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -59,6 +61,13 @@ func AddAllStorageFlags(cmd *cobra.Command) {
|
|||||||
AddAWSCredsFlags(cmd)
|
AddAWSCredsFlags(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddReadOnlyFlag(cmd *cobra.Command) {
|
||||||
|
fs := cmd.Flags()
|
||||||
|
fs.BoolVar(&ReadOnlyFV, ReadOnlyFN, false, "open repository in read-only mode")
|
||||||
|
//nolint:errcheck
|
||||||
|
fs.MarkHidden(ReadOnlyFN)
|
||||||
|
}
|
||||||
|
|
||||||
func AddAWSCredsFlags(cmd *cobra.Command) {
|
func AddAWSCredsFlags(cmd *cobra.Command) {
|
||||||
fs := cmd.Flags()
|
fs := cmd.Flags()
|
||||||
fs.StringVar(&AWSAccessKeyFV, AWSAccessKeyFN, "", "S3 access key")
|
fs.StringVar(&AWSAccessKeyFV, AWSAccessKeyFN, "", "S3 access key")
|
||||||
|
|||||||
@ -30,6 +30,7 @@ func Control() control.Options {
|
|||||||
opt.ToggleFeatures.ExchangeImmutableIDs = flags.EnableImmutableIDFV
|
opt.ToggleFeatures.ExchangeImmutableIDs = flags.EnableImmutableIDFV
|
||||||
opt.ToggleFeatures.UseDeltaTree = flags.UseDeltaTreeFV
|
opt.ToggleFeatures.UseDeltaTree = flags.UseDeltaTreeFV
|
||||||
opt.Parallelism.ItemFetch = flags.FetchParallelismFV
|
opt.Parallelism.ItemFetch = flags.FetchParallelismFV
|
||||||
|
opt.Repo.ReadOnly = flags.ReadOnlyFV
|
||||||
|
|
||||||
return opt
|
return opt
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,6 +88,14 @@ type (
|
|||||||
) (*snapshot.Manifest, error)
|
) (*snapshot.Manifest, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
multiSnapshotLoader interface {
|
||||||
|
manifestFinder
|
||||||
|
LoadSnapshots(
|
||||||
|
ctx context.Context,
|
||||||
|
manifestIDs []manifest.ID,
|
||||||
|
) ([]*snapshot.Manifest, error)
|
||||||
|
}
|
||||||
|
|
||||||
snapshotLoader interface {
|
snapshotLoader interface {
|
||||||
SnapshotRoot(man *snapshot.Manifest) (fs.Entry, error)
|
SnapshotRoot(man *snapshot.Manifest) (fs.Entry, error)
|
||||||
}
|
}
|
||||||
@ -590,6 +598,14 @@ func persistRetentionConfigs(
|
|||||||
return clues.WrapWC(ctx, err, "persisting config changes").OrNil()
|
return clues.WrapWC(ctx, err, "persisting config changes").OrNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *conn) LoadSnapshots(
|
||||||
|
ctx context.Context,
|
||||||
|
manifestIDs []manifest.ID,
|
||||||
|
) ([]*snapshot.Manifest, error) {
|
||||||
|
mans, err := snapshot.LoadSnapshots(ctx, w.Repository, manifestIDs)
|
||||||
|
return mans, clues.StackWC(ctx, err).OrNil()
|
||||||
|
}
|
||||||
|
|
||||||
func (w *conn) LoadSnapshot(
|
func (w *conn) LoadSnapshot(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id manifest.ID,
|
id manifest.ID,
|
||||||
|
|||||||
168
src/internal/kopia/verify_backups.go
Normal file
168
src/internal/kopia/verify_backups.go
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
package kopia
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/kopia/kopia/repo/manifest"
|
||||||
|
"github.com/kopia/kopia/snapshot"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
|
"github.com/alcionai/corso/src/pkg/backup"
|
||||||
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// verifyBackups uses bs and mf to lookup all models/snapshots for backups
|
||||||
|
// and outputs summary information for backups that are not "complete" backups
|
||||||
|
// with:
|
||||||
|
// - a backup model
|
||||||
|
// - an item data snapshot
|
||||||
|
// - a details snapshot or details model
|
||||||
|
//
|
||||||
|
// Output summary information has the form:
|
||||||
|
//
|
||||||
|
// BackupID: <corso backup ID>
|
||||||
|
// ItemDataSnapshotID: <kopia manifest ID>
|
||||||
|
// DetailsSnapshotID: <kopia manifest ID>
|
||||||
|
//
|
||||||
|
// Items that are missing will have a (missing) note appended to them.
|
||||||
|
func verifyBackups(
|
||||||
|
ctx context.Context,
|
||||||
|
bs store.Storer,
|
||||||
|
mf multiSnapshotLoader,
|
||||||
|
) error {
|
||||||
|
logger.Ctx(ctx).Infow("scanning for incomplete backups")
|
||||||
|
|
||||||
|
// Get all snapshot manifests.
|
||||||
|
snapMetas, err := mf.FindManifests(
|
||||||
|
ctx,
|
||||||
|
map[string]string{
|
||||||
|
manifest.TypeLabelKey: snapshot.ManifestType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting snapshot metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
snapIDs := make([]manifest.ID, 0, len(snapMetas))
|
||||||
|
for _, m := range snapMetas {
|
||||||
|
snapIDs = append(snapIDs, m.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
snaps, err := mf.LoadSnapshots(ctx, snapIDs)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting snapshots")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// deets is a hash set of the ModelStoreID or snapshot IDs for backup
|
||||||
|
// details. It contains the IDs for both legacy details stored in the model
|
||||||
|
// store and newer details stored as a snapshot because it doesn't matter
|
||||||
|
// what the storage format is. We only need to know the ID so we can:
|
||||||
|
// 1. check if there's a corresponding backup for them
|
||||||
|
deets = map[manifest.ID]struct{}{}
|
||||||
|
// dataSnaps is a hash set of the snapshot IDs for item data snapshots.
|
||||||
|
dataSnaps = map[manifest.ID]struct{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort all the snapshots as either details snapshots or item data snapshots.
|
||||||
|
for _, snap := range snaps {
|
||||||
|
// Filter out checkpoint snapshots as they aren't expected to have a backup
|
||||||
|
// associated with them.
|
||||||
|
if snap.IncompleteReason == "checkpoint" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
k, _ := makeTagKV(TagBackupCategory)
|
||||||
|
if _, ok := snap.Tags[k]; ok {
|
||||||
|
dataSnaps[snap.ID] = struct{}{}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deets[snap.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all legacy backup details models. The initial version of backup delete
|
||||||
|
// didn't seem to delete them so they may also be orphaned if the repo is old
|
||||||
|
// enough.
|
||||||
|
deetsModels, err := bs.GetIDsForType(ctx, model.BackupDetailsSchema, nil)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting legacy backup details")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range deetsModels {
|
||||||
|
deets[d.ModelStoreID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all backup models.
|
||||||
|
bups, err := bs.GetIDsForType(ctx, model.BackupSchema, nil)
|
||||||
|
if err != nil {
|
||||||
|
return clues.Wrap(err, "getting all backup models")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Incomplete backups:")
|
||||||
|
|
||||||
|
for _, bup := range bups {
|
||||||
|
bm := backup.Backup{}
|
||||||
|
|
||||||
|
if err := bs.GetWithModelStoreID(
|
||||||
|
ctx,
|
||||||
|
model.BackupSchema,
|
||||||
|
bup.ModelStoreID,
|
||||||
|
&bm); err != nil {
|
||||||
|
logger.CtxErr(ctx, err).Infow(
|
||||||
|
"backup model not found",
|
||||||
|
"search_backup_id", bup.ModelStoreID)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ssid := bm.StreamStoreID
|
||||||
|
if len(ssid) == 0 {
|
||||||
|
ssid = bm.DetailsID
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMissing := ""
|
||||||
|
deetsMissing := ""
|
||||||
|
|
||||||
|
if _, dataOK := dataSnaps[manifest.ID(bm.SnapshotID)]; !dataOK {
|
||||||
|
dataMissing = " (missing)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, deetsOK := deets[manifest.ID(ssid)]; !deetsOK {
|
||||||
|
deetsMissing = " (missing)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from the set so we can mention items that don't seem to have
|
||||||
|
// backup models referring to them.
|
||||||
|
delete(dataSnaps, manifest.ID(bm.SnapshotID))
|
||||||
|
delete(deets, manifest.ID(ssid))
|
||||||
|
|
||||||
|
// Output info about the state of the backup if needed.
|
||||||
|
if len(dataMissing) > 0 || len(deetsMissing) > 0 {
|
||||||
|
fmt.Printf(
|
||||||
|
"\tBackupID: %s\n\t\tItemDataSnapshotID: %s%s\n\t\tDetailsSnapshotID: %s%s\n",
|
||||||
|
bm.ID,
|
||||||
|
bm.SnapshotID,
|
||||||
|
dataMissing,
|
||||||
|
ssid,
|
||||||
|
deetsMissing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Additional ItemDataSnapshotIDs missing backup models:")
|
||||||
|
printIDs(maps.Keys(dataSnaps))
|
||||||
|
|
||||||
|
fmt.Println("Additional DetailsSnapshotIDs missing backup models:")
|
||||||
|
printIDs(maps.Keys(deets))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printIDs(ids []manifest.ID) {
|
||||||
|
for _, id := range ids {
|
||||||
|
fmt.Printf("\t%s\n", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -746,3 +746,12 @@ func (w *Wrapper) UpdatePersistentConfig(
|
|||||||
) error {
|
) error {
|
||||||
return clues.Stack(w.c.updatePersistentConfig(ctx, config)).OrNil()
|
return clues.Stack(w.c.updatePersistentConfig(ctx, config)).OrNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Wrapper) VerifyBackups(
|
||||||
|
ctx context.Context,
|
||||||
|
storer store.Storer,
|
||||||
|
) error {
|
||||||
|
return clues.Wrap(
|
||||||
|
verifyBackups(ctx, storer, w.c),
|
||||||
|
"verifying completeness of backups").OrNil()
|
||||||
|
}
|
||||||
|
|||||||
@ -53,6 +53,7 @@ type Backuper interface {
|
|||||||
failOnMissing bool,
|
failOnMissing bool,
|
||||||
ids ...string,
|
ids ...string,
|
||||||
) error
|
) error
|
||||||
|
VerifyBackups(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBackup generates a BackupOperation runner.
|
// NewBackup generates a BackupOperation runner.
|
||||||
@ -356,3 +357,8 @@ func deleteBackups(
|
|||||||
|
|
||||||
return sw.DeleteWithModelStoreIDs(ctx, toDelete...)
|
return sw.DeleteWithModelStoreIDs(ctx, toDelete...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r repository) VerifyBackups(ctx context.Context) error {
|
||||||
|
return clues.Stack(r.dataLayer.VerifyBackups(ctx, store.NewWrapper(r.modelStore))).
|
||||||
|
OrNil()
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user