github.com/whamcloud/lemur@v0.0.0-20190827193804-4655df8a52af/cmd/lhsm/hsm.go (about)

     1  // Copyright (c) 2018 DDN. All rights reserved.
     2  // Use of this source code is governed by a MIT-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/dustin/go-humanize"
    17  	"github.com/pkg/errors"
    18  	"gopkg.in/urfave/cli.v1"
    19  
    20  	"github.com/intel-hpdd/go-lustre"
    21  	"github.com/intel-hpdd/go-lustre/fs"
    22  	"github.com/intel-hpdd/go-lustre/hsm"
    23  )
    24  
    25  func init() {
    26  	hsmStateFlags := strings.Join(hsm.GetStateFlagNames(), ",")
    27  
    28  	hsmCommands := []cli.Command{
    29  		{
    30  			Name:      "archive",
    31  			Usage:     "Initiate HSM archive of specified paths",
    32  			ArgsUsage: "[path [path...]]",
    33  			Action:    hsmRequestAction(hsm.RequestArchive),
    34  			Flags: []cli.Flag{
    35  				cli.IntFlag{
    36  					Name:  "id, i",
    37  					Usage: "Numeric ID of archive backend",
    38  				},
    39  				cli.BoolFlag{
    40  					Name:  "null, 0",
    41  					Usage: "Null-separated paths are read from stdin (e.g. piped from find -print0)",
    42  				},
    43  			},
    44  		},
    45  		{
    46  			Name:      "release",
    47  			Usage:     "Release local data of HSM-archived paths",
    48  			ArgsUsage: "[path [path...]]",
    49  			Action:    hsmRequestAction(hsm.RequestRelease),
    50  			Flags: []cli.Flag{
    51  				cli.BoolFlag{
    52  					Name:  "null, 0",
    53  					Usage: "Null-separated paths are read from stdin (e.g. piped from find -print0)",
    54  				},
    55  			},
    56  		},
    57  		{
    58  			Name:      "restore",
    59  			Usage:     "Explicitly restore local data of HSM-archived paths",
    60  			ArgsUsage: "[path [path...]]",
    61  			Action:    hsmRequestAction(hsm.RequestRestore),
    62  			Flags: []cli.Flag{
    63  				cli.BoolFlag{
    64  					Name:  "null, 0",
    65  					Usage: "Null-separated paths are read from stdin (e.g. piped from find -print0)",
    66  				},
    67  			},
    68  		},
    69  		{
    70  			Name:      "remove",
    71  			Usage:     "Remove HSM-archived data of specified paths (local data is not removed)",
    72  			ArgsUsage: "[path [path...]]",
    73  			Action:    hsmRequestAction(hsm.RequestRemove),
    74  			Flags: []cli.Flag{
    75  				cli.BoolFlag{
    76  					Name:  "null, 0",
    77  					Usage: "Null-separated paths are read from stdin (e.g. piped from find -print0)",
    78  				},
    79  			},
    80  		},
    81  		{
    82  			Name:      "cancel",
    83  			Usage:     "Cancel HSM operations being performed on specified paths",
    84  			ArgsUsage: "[path [path...]]",
    85  			Action:    hsmRequestAction(hsm.RequestCancel),
    86  			Flags: []cli.Flag{
    87  				cli.BoolFlag{
    88  					Name:  "null, 0",
    89  					Usage: "Null-separated paths are read from stdin (e.g. piped from find -print0)",
    90  				},
    91  			},
    92  		},
    93  		{
    94  			Name:      "set",
    95  			Usage:     "Set HSM flags or archive ID for specified paths",
    96  			ArgsUsage: "[path [path...]]",
    97  			Action:    hsmSetAction,
    98  			Flags: []cli.Flag{
    99  				cli.BoolFlag{
   100  					Name:  "null, 0",
   101  					Usage: "Null-separated paths are read from stdin (e.g. piped from find -print0)",
   102  				},
   103  				cli.IntFlag{
   104  					Name:  "id, i",
   105  					Usage: "Numeric ID of archive backend",
   106  				},
   107  				cli.StringSliceFlag{
   108  					Name:  "flag, f",
   109  					Usage: fmt.Sprintf("HSM flag to set (%s)", hsmStateFlags),
   110  					Value: &cli.StringSlice{},
   111  				},
   112  				cli.StringSliceFlag{
   113  					Name:  "clear, F",
   114  					Usage: fmt.Sprintf("HSM flag to clear (%s)", hsmStateFlags),
   115  					Value: &cli.StringSlice{},
   116  				},
   117  			},
   118  		},
   119  		{
   120  			Name:      "status",
   121  			Usage:     "Display HSM status for specified paths",
   122  			ArgsUsage: "[path [path...]]",
   123  			Action:    hsmStatusAction,
   124  			Flags: []cli.Flag{
   125  				cli.BoolFlag{
   126  					Name:  "action, a",
   127  					Usage: "Include current HSM action",
   128  				},
   129  				cli.BoolFlag{
   130  					Name:  "hide-path, H",
   131  					Usage: "Hide pathname in output",
   132  				},
   133  				cli.BoolFlag{
   134  					Name:  "long, l",
   135  					Usage: "Show long-form states",
   136  				},
   137  				cli.BoolFlag{
   138  					Name:  "progress, p",
   139  					Usage: "Show copy progress for archive/restore actions",
   140  				},
   141  				cli.BoolFlag{
   142  					Name:  "null, 0",
   143  					Usage: "Null-separated paths are read from stdin (e.g. piped from find -print0)",
   144  				},
   145  			},
   146  		},
   147  		{
   148  			Name:      "import",
   149  			Usage:     "Import an HSM-backed file.",
   150  			ArgsUsage: "path",
   151  			Action:    hsmImportAction,
   152  			Flags: []cli.Flag{
   153  				cli.UintFlag{
   154  					Name:  "id, i",
   155  					Usage: "Numeric ID of archive backend",
   156  				},
   157  				cli.StringFlag{
   158  					Name:  "uuid",
   159  					Usage: "File's UUID",
   160  				},
   161  				cli.StringFlag{
   162  					Name:  "hash",
   163  					Usage: "Checksum hash value",
   164  				},
   165  				cli.StringFlag{
   166  					Name:  "uid",
   167  					Usage: "Owner uid",
   168  				},
   169  				cli.StringFlag{
   170  					Name:  "gid",
   171  					Usage: "Owner gid",
   172  				},
   173  				cli.Int64Flag{
   174  					Name:  "size",
   175  					Usage: "Size of file in bytes",
   176  				},
   177  				cli.UintFlag{
   178  					Name:  "mode",
   179  					Value: 0644,
   180  					Usage: "File mode",
   181  				},
   182  				cli.StringFlag{
   183  					Name:  "timefmt",
   184  					Value: "2006-01-02 15:04:05.999999999 -0700",
   185  					Usage: "Format for time stamp see golang time.Parse documentation",
   186  				},
   187  				cli.StringFlag{
   188  					Name:  "mtime",
   189  					Usage: "Modification time (default: current time)",
   190  				},
   191  				cli.StringFlag{
   192  					Name:  "atime",
   193  					Usage: "Last access time (default: set to mtime)",
   194  				},
   195  				cli.IntFlag{
   196  					Name:  "stripe_count",
   197  					Value: 1,
   198  					Usage: "Set number of stripes in file",
   199  				},
   200  				cli.IntFlag{
   201  					Name:  "stripe_size",
   202  					Value: 1 << 20,
   203  					Usage: "Set stripe size in bytes",
   204  				},
   205  				cli.StringFlag{
   206  					Name:  "pool",
   207  					Usage: "Set the start OST Pool name",
   208  				},
   209  			},
   210  		},
   211  		{
   212  			Name:      "clone",
   213  			Usage:     "Create a relased copy of an HSM-backed file.",
   214  			ArgsUsage: "source_file target_file",
   215  			Action:    hsmCloneAction,
   216  			Flags: []cli.Flag{
   217  				cli.IntFlag{
   218  					Name:  "stripe_count",
   219  					Usage: "Override the number of stripes in the target copy.",
   220  				},
   221  				cli.IntFlag{
   222  					Name:  "stripe_size",
   223  					Usage: "Override stripe size (bytes) in target copy.",
   224  				},
   225  				cli.StringFlag{
   226  					Name:  "pool",
   227  					Usage: "Set the start OST Pool name",
   228  				},
   229  			},
   230  		},
   231  		{
   232  			Name:      "restripe",
   233  			Usage:     "Change stripe parameters of a released file.",
   234  			ArgsUsage: "file",
   235  			Action:    hsmRestripeAction,
   236  			Flags: []cli.Flag{
   237  				cli.IntFlag{
   238  					Name:  "stripe_count",
   239  					Usage: "Override the number of stripes in the target copy.",
   240  				},
   241  				cli.IntFlag{
   242  					Name:  "stripe_size",
   243  					Usage: "Override stripe size (bytes) in target copy.",
   244  				},
   245  				cli.StringFlag{
   246  					Name:  "pool",
   247  					Usage: "Set the start OST Pool name",
   248  				},
   249  			},
   250  		},
   251  	}
   252  	commands = append(commands, hsmCommands...)
   253  }
   254  
   255  func getFilePaths(c *cli.Context) ([]string, error) {
   256  	var paths []string
   257  
   258  	if c.Bool("null") {
   259  		reader := bufio.NewReader(os.Stdin)
   260  		path, err := reader.ReadBytes('\000')
   261  		for err == nil {
   262  			paths = append(paths, string(path[:len(path)-1]))
   263  			path, err = reader.ReadBytes('\000')
   264  		}
   265  		if err != io.EOF {
   266  			return nil, err
   267  		}
   268  	} else {
   269  		paths = c.Args()
   270  	}
   271  
   272  	return paths, nil
   273  }
   274  
   275  func getPathStatus(c *cli.Context, filePath string) (string, error) {
   276  	var buf bytes.Buffer
   277  
   278  	s, err := hsm.GetFileStatus(filePath)
   279  	if err != nil {
   280  		return "", errors.Wrapf(err, "Failed to get HSM status for %s", filePath)
   281  	}
   282  
   283  	if !c.Bool("hide-path") {
   284  		fmt.Fprintf(&buf, "%s ", filePath)
   285  	}
   286  	fmt.Fprintf(&buf, hsm.FileStatusString(s, !c.Bool("long")))
   287  
   288  	if s.Exists() && c.Bool("action") {
   289  		a, err := hsm.GetFileAction(filePath)
   290  		if err != nil {
   291  			return "", errors.Wrapf(err, "Failed to get current HSM action for %s", filePath)
   292  		}
   293  		if a.IsNone() {
   294  			fmt.Fprintf(&buf, " -")
   295  		} else {
   296  			fmt.Fprintf(&buf, " [%s:%s]", a.Action(), a.State())
   297  			if c.Bool("progress") && (a.IsArchive() || a.IsRestore()) {
   298  				st, err := os.Stat(filePath)
   299  				if err != nil {
   300  					return "", errors.Wrapf(err, "Failed to stat() %s", filePath)
   301  				}
   302  				fmt.Fprintf(&buf, "(%s/%s)",
   303  					humanize.IBytes(uint64(a.BytesCopied)),
   304  					humanize.IBytes(uint64(st.Size())))
   305  			}
   306  		}
   307  	} else {
   308  		fmt.Fprintf(&buf, " -")
   309  	}
   310  
   311  	// TODO: Display xattrs, once we've standardized them?
   312  
   313  	return buf.String(), nil
   314  }
   315  
   316  func hsmSetAction(c *cli.Context) error {
   317  	logContext(c)
   318  
   319  	paths, err := getFilePaths(c)
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	if len(paths) < 1 {
   325  		return errors.New("HSM set request must be made with at least 1 path")
   326  	}
   327  
   328  	setFlags, err := hsm.GetStatusMask(c.StringSlice("flag"))
   329  	if err != nil {
   330  		return err
   331  	}
   332  	clearFlags, err := hsm.GetStatusMask(c.StringSlice("clear"))
   333  	if err != nil {
   334  		return err
   335  	}
   336  	archiveID := uint32(c.Int("id"))
   337  
   338  	if setFlags == 0 && clearFlags == 0 && archiveID == 0 {
   339  		return errors.New("HSM set request made with no flags to set or clear, and no new archive ID supplied")
   340  	}
   341  
   342  	// TODO: Parallelize this?
   343  	for _, path := range paths {
   344  		if err := hsm.SetFileStatus(path, setFlags, clearFlags, archiveID); err != nil {
   345  			return err
   346  		}
   347  	}
   348  
   349  	return nil
   350  }
   351  
   352  func hsmStatusAction(c *cli.Context) error {
   353  	logContext(c)
   354  
   355  	paths, err := getFilePaths(c)
   356  	if err != nil {
   357  		return err
   358  	}
   359  
   360  	if len(paths) < 1 {
   361  		return errors.New("HSM status request must be made with at least 1 path")
   362  	}
   363  
   364  	for _, path := range paths {
   365  		status, err := getPathStatus(c, path)
   366  		if err != nil {
   367  			return errors.Errorf("%s: %v", path, err)
   368  		}
   369  		fmt.Println(status)
   370  	}
   371  
   372  	return nil
   373  }
   374  
   375  type hsmRequestFn func(fs.RootDir, uint, []*lustre.Fid) error
   376  
   377  func hsmRequestAction(requestFn func(fs.RootDir, uint, []*lustre.Fid) error) cli.ActionFunc {
   378  	return func(c *cli.Context) error {
   379  		logContext(c)
   380  
   381  		paths, err := getFilePaths(c)
   382  		if err != nil {
   383  			return err
   384  		}
   385  
   386  		return submitHsmRequest(c.Command.Name, hsmRequestFn(requestFn), uint(c.Int("id")), paths...)
   387  	}
   388  }
   389  
   390  func submitHsmRequest(actionName string, requestFn hsmRequestFn, archiveID uint, paths ...string) error {
   391  	var fids []*lustre.Fid
   392  
   393  	if len(paths) < 1 {
   394  		return fmt.Errorf("HSM %s request must be made with at least 1 path", actionName)
   395  	}
   396  
   397  	fsRoot, err := fs.MountRoot(paths[0])
   398  	if err != nil {
   399  		return fmt.Errorf("Error getting fs root from %s: %s", paths[0], err)
   400  	}
   401  
   402  	// TODO: Occurs to me that it might be better to break up a large
   403  	// batch into multiple batches, each serviced by its own goroutine.
   404  	for _, path := range paths {
   405  		absPath, err2 := filepath.Abs(path)
   406  		if err2 != nil {
   407  			return fmt.Errorf("Cannot resolve absolute path for %s: %s", path, err)
   408  		}
   409  		if !strings.HasPrefix(absPath, fsRoot.Path()) {
   410  			return fmt.Errorf("All files in HSM request must be in the same filesystem (%s is not in %s)",
   411  				path, fsRoot)
   412  		}
   413  
   414  		fid, err2 := fs.LookupFid(path)
   415  		if err2 != nil {
   416  			return fmt.Errorf("Cannot resolve Fid for %s: %s", path, err)
   417  		}
   418  		fids = append(fids, fid)
   419  	}
   420  
   421  	if requestFn != nil {
   422  		err = requestFn(fsRoot, archiveID, fids)
   423  	} else {
   424  		err = fmt.Errorf("Unhandled HSM action: %s", actionName)
   425  	}
   426  
   427  	return err
   428  }