blob: d9d81d2b5eb6e1e4c8af1157a7f0c83c1ad0e4db [file] [log] [blame]
// Copyright 2017 The Upspin Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Upspin-audit provides subcommands for auditing storage consumption.
// It has several subcommands that should be used in a way yet to be
// determined.
package main
import (
"bufio"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"upspin.io/config"
"upspin.io/errors"
"upspin.io/flags"
"upspin.io/subcmd"
"upspin.io/transports"
"upspin.io/upspin"
"upspin.io/version"
)
const (
timeFormat = "2006-01-02 15:04:05"
dirFilePrefix = "dir_"
storeFilePrefix = "store_"
orphanFilePrefix = "orphans_"
)
type State struct {
*subcmd.State
}
const help = `Upspin-audit provides subcommands for auditing storage consumption.
It has subcommands scandir and scanstore to scan the directory and storage servers
and report the storage consumed by those servers.
The set of tools will grow.
`
func main() {
const name = "audit"
log.SetFlags(0)
log.SetPrefix("upspin-audit: ")
flag.Usage = usage
flags.ParseArgsInto(flag.CommandLine, os.Args[1:], flags.Client, "version")
if flags.Version {
fmt.Fprint(os.Stdout, version.Version())
os.Exit(2)
}
if flag.NArg() < 1 {
usage()
}
s := &State{
State: subcmd.NewState(name),
}
cfg, err := config.FromFile(flags.Config)
if err != nil {
s.Exit(err)
}
transports.Init(cfg)
s.State.Init(cfg)
switch flag.Arg(0) {
case "scandir":
s.scanDirectories(flag.Args()[1:])
case "scanstore":
s.scanStore(flag.Args()[1:])
case "orphans":
s.orphans(flag.Args()[1:])
default:
usage()
}
s.ExitNow()
}
func usage() {
fmt.Fprintln(os.Stderr, help)
fmt.Fprintln(os.Stderr, "Usage of upspin audit:")
fmt.Fprintln(os.Stderr, "\tupspin [globalflags] audit <command> [flags] ...")
fmt.Fprintln(os.Stderr, "\twhere <command> is one of scandir, scanstore")
flag.PrintDefaults()
os.Exit(2)
}
// dataDirFlag returns a string pointer bound to a new flag that specifies the data directory.
// Done here so the definition can be common among the commands.
func dataDirFlag(fs *flag.FlagSet) *string {
var dataDir string
fs.StringVar(&dataDir, "data", filepath.Join(os.Getenv("HOME"), "upspin", "audit"), "`directory` storing scan data")
return &dataDir
}
// writeItems sorts and writes a list of reference/size pairs to file.
func (s *State) writeItems(file string, items []upspin.ListRefsItem) {
sort.Slice(items, func(i, j int) bool { return items[i].Ref < items[j].Ref })
f, err := os.Create(file)
if err != nil {
s.Exit(err)
}
defer func() {
if err := f.Close(); err != nil {
s.Exit(err)
}
}()
w := bufio.NewWriter(f)
for _, ri := range items {
if _, err := fmt.Fprintf(w, "%q %d\n", ri.Ref, ri.Size); err != nil {
s.Exit(err)
}
}
if err := w.Flush(); err != nil {
s.Exit(err)
}
}
// readItems reads a list of reference/size pairs from the given file and
// returns them as a map. The asymmetry with writeItems, which takes a slice,
// is to fit the most common usage pattern.
func (s *State) readItems(file string) (map[upspin.Reference]int64, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
defer f.Close()
sc := bufio.NewScanner(f)
items := make(map[upspin.Reference]int64)
for sc.Scan() {
line := sc.Text()
i := strings.LastIndex(line, " ")
if i < 0 {
return nil, errors.Errorf("malformed line in %q: %q", file, line)
}
quotedRef, sizeString := line[:i], line[i+1:]
ref, err := strconv.Unquote(quotedRef)
if err != nil {
return nil, errors.Errorf("malformed ref in %q: %v", file, err)
}
size, err := strconv.ParseInt(sizeString, 10, 64)
if err != nil {
return nil, errors.Errorf("malformed size in %q: %v", file, err)
}
items[upspin.Reference(ref)] = size
}
if err := sc.Err(); err != nil {
return nil, err
}
return items, nil
}
func itemMapToSlice(m map[upspin.Reference]int64) (items []upspin.ListRefsItem) {
for ref, size := range m {
items = append(items, upspin.ListRefsItem{Ref: ref, Size: size})
}
return
}
// fileInfo holds a description of a reference list file written by scanstore
// or scandir. It is derived from the name of the file, not its contents.
type fileInfo struct {
Path string
Addr upspin.NetAddr
User upspin.UserName // empty for store
Time time.Time
}
// errIgnoreFile is returned from filenameToFileInfo to signal that the given
// file name is not one generated by scandir or scanstore. It should be handled
// by callers to filenameToFileInfo and is not to be seen by users.
var errIgnoreFile = errors.Str("not a file we're interested in")
// filenameToFileInfo takes a file name generated by scandir or scanstore and
// returns the information held by that file name as a fileInfo.
func filenameToFileInfo(file string) (fi fileInfo, err error) {
fi.Path = file
file = filepath.Base(file)
s := file // We will consume this string.
// Check and trim prefix.
switch {
case strings.HasPrefix(s, dirFilePrefix):
s = strings.TrimPrefix(s, dirFilePrefix)
case strings.HasPrefix(s, storeFilePrefix):
s = strings.TrimPrefix(s, storeFilePrefix)
default:
err = errIgnoreFile
return
}
// Collect and trim endpoint name.
i := strings.Index(s, "_")
if i < 0 {
err = errors.Errorf("malformed file name %q", file)
return
}
fi.Addr = upspin.NetAddr(s[:i])
s = s[i+1:]
// For dir files, collect and trim user name.
if strings.HasPrefix(file, dirFilePrefix) {
i := strings.LastIndex(s, "_")
if i < 0 {
err = errors.Errorf("malformed file name %q: missing user name", file)
return
}
fi.User = upspin.UserName(s[:i])
s = s[i+1:]
}
// Collect time stamp.
ts, err := strconv.ParseInt(s, 10, 64)
if err != nil {
err = errors.Errorf("malformed file name %q: bad timestamp: %v", file, err)
return
}
fi.Time = time.Unix(ts, 0)
return
}