17840: Deduplicate flag-parsing code.
[arvados.git] / lib / recovercollection / cmd.go
index d19bf311680814fa612b2ac6f22a56b4d026e4e7..5038e4788a2d555f8ca2c805a60a6921cd8d3612 100644 (file)
@@ -15,6 +15,7 @@ import (
        "sync"
        "time"
 
+       "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/lib/config"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
@@ -38,11 +39,10 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
        loader := config.NewLoader(stdin, logger)
        loader.SkipLegacy = true
 
-       flags := flag.NewFlagSet("", flag.ContinueOnError)
-       flags.SetOutput(stderr)
+       flags := flag.NewFlagSet(prog, flag.ContinueOnError)
        flags.Usage = func() {
                fmt.Fprintf(flags.Output(), `Usage:
-       %s [options ...] { /path/to/manifest.txt | log-entry-uuid } [...]
+       %s [options ...] { /path/to/manifest.txt | log-or-collection-uuid } [...]
 
        This program recovers deleted collections. Recovery is
        possible when the collection's manifest is still available and
@@ -52,10 +52,18 @@ func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
        collections, or the blocks have been trashed but not yet
        deleted).
 
-       Collections can be specified either by filename (a local file
-       containing a manifest with the desired data) or by log UUID
-       (an Arvados log entry, typically a "delete" or "update" event,
-       whose "old attributes" have a manifest with the desired data).
+       There are multiple ways to specify a collection to recover:
+
+        * Path to a local file containing a manifest with the desired
+         data
+
+       * UUID of an Arvados log entry, typically a "delete" or
+         "update" event, whose "old attributes" have a manifest with
+         the desired data
+
+       * UUID of an Arvados collection whose most recent log entry,
+          typically a "delete" or "update" event, has the desired
+          data in its "old attributes"
 
        For each provided collection manifest, once all data blocks
        are recovered/protected from garbage collection, a new
@@ -71,16 +79,10 @@ Options:
        }
        loader.SetupFlags(flags)
        loglevel := flags.String("log-level", "info", "logging level (debug, info, ...)")
-       err = flags.Parse(args)
-       if err == flag.ErrHelp {
-               err = nil
-               return 0
-       } else if err != nil {
-               return 2
-       }
-
-       if len(flags.Args()) == 0 {
-               flags.Usage()
+       if ok, code := cmd.ParseFlags(flags, prog, args, "source [...]", stderr); !ok {
+               return code
+       } else if flags.NArg() == 0 {
+               fmt.Fprintf(stderr, "missing required arguments (try -help)\n")
                return 2
        }
 
@@ -113,29 +115,58 @@ Options:
        for _, src := range flags.Args() {
                logger := logger.WithField("src", src)
                var mtxt string
-               if len(src) == 27 && src[5:12] == "-57u5n-" {
-                       var logent struct {
-                               EventType  string    `json:"event_type"`
-                               EventAt    time.Time `json:"event_at"`
-                               ObjectUUID string    `json:"object_uuid"`
-                               Properties struct {
-                                       OldAttributes struct {
-                                               ManifestText string `json:"manifest_text"`
-                                       } `json:"old_attributes"`
-                               } `json:"properties"`
+               if !strings.Contains(src, "/") && len(src) == 27 && src[5] == '-' && src[11] == '-' {
+                       var filters []arvados.Filter
+                       if src[5:12] == "-57u5n-" {
+                               filters = []arvados.Filter{{"uuid", "=", src}}
+                       } else if src[5:12] == "-4zz18-" {
+                               filters = []arvados.Filter{{"object_uuid", "=", src}}
+                       } else {
+                               logger.Error("looks like a UUID but not a log or collection UUID (if it's really a file, prepend './')")
+                               exitcode = 1
+                               continue
+                       }
+                       var resp struct {
+                               Items []struct {
+                                       UUID       string    `json:"uuid"`
+                                       EventType  string    `json:"event_type"`
+                                       EventAt    time.Time `json:"event_at"`
+                                       ObjectUUID string    `json:"object_uuid"`
+                                       Properties struct {
+                                               OldAttributes struct {
+                                                       ManifestText string `json:"manifest_text"`
+                                               } `json:"old_attributes"`
+                                       } `json:"properties"`
+                               }
                        }
-                       err = client.RequestAndDecode(&logent, "GET", "arvados/v1/logs/"+src, nil, nil)
+                       err = client.RequestAndDecode(&resp, "GET", "arvados/v1/logs", nil, arvados.ListOptions{
+                               Limit:   1,
+                               Order:   []string{"event_at desc"},
+                               Filters: filters,
+                       })
                        if err != nil {
-                               logger.WithError(err).Error("failed to load log entry")
+                               logger.WithError(err).Error("error looking up log entry")
+                               exitcode = 1
+                               continue
+                       } else if len(resp.Items) == 0 {
+                               logger.Error("log entry not found")
                                exitcode = 1
                                continue
                        }
+                       logent := resp.Items[0]
                        logger.WithFields(logrus.Fields{
+                               "uuid":                logent.UUID,
                                "old_collection_uuid": logent.ObjectUUID,
                                "logged_event_type":   logent.EventType,
                                "logged_event_time":   logent.EventAt,
+                               "logged_object_uuid":  logent.ObjectUUID,
                        }).Info("loaded log entry")
                        mtxt = logent.Properties.OldAttributes.ManifestText
+                       if mtxt == "" {
+                               logger.Error("log entry properties.old_attributes.manifest_text missing or empty")
+                               exitcode = 1
+                               continue
+                       }
                } else {
                        buf, err := ioutil.ReadFile(src)
                        if err != nil {
@@ -167,8 +198,8 @@ var errNotFound = errors.New("not found")
 
 // Finds the timestamp of the newest copy of blk on svc. Returns
 // errNotFound if blk is not on svc at all.
-func (rcvr recoverer) newestMtime(logger logrus.FieldLogger, blk string, svc arvados.KeepService) (time.Time, error) {
-       found, err := svc.Index(rcvr.client, blk)
+func (rcvr recoverer) newestMtime(ctx context.Context, logger logrus.FieldLogger, blk string, svc arvados.KeepService) (time.Time, error) {
+       found, err := svc.Index(ctx, rcvr.client, blk)
        if err != nil {
                logger.WithError(err).Warn("error getting index")
                return time.Time{}, err
@@ -199,7 +230,7 @@ var errTouchIneffective = errors.New("(BUG?) touch succeeded but had no effect -
 // saved. But if the block's timestamp is more recent than blobsigttl,
 // keepstore will refuse to trash it even if told to by keep-balance.
 func (rcvr recoverer) ensureSafe(ctx context.Context, logger logrus.FieldLogger, blk string, svc arvados.KeepService, blobsigttl time.Duration, blobsigexp time.Time) error {
-       if latest, err := rcvr.newestMtime(logger, blk, svc); err != nil {
+       if latest, err := rcvr.newestMtime(ctx, logger, blk, svc); err != nil {
                return err
        } else if latest.Add(blobsigttl).After(blobsigexp) {
                return nil
@@ -208,7 +239,7 @@ func (rcvr recoverer) ensureSafe(ctx context.Context, logger logrus.FieldLogger,
                return fmt.Errorf("error updating timestamp: %s", err)
        }
        logger.Debug("updated timestamp")
-       if latest, err := rcvr.newestMtime(logger, blk, svc); err == errNotFound {
+       if latest, err := rcvr.newestMtime(ctx, logger, blk, svc); err == errNotFound {
                return fmt.Errorf("(BUG?) touch succeeded, but then block did not appear in index")
        } else if err != nil {
                return err