github.com/blixtra/nomad@v0.7.2-0.20171221000451-da9a1d7bb050/command/fs.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"math/rand"
     7  	"os"
     8  	"os/signal"
     9  	"strings"
    10  	"syscall"
    11  	"time"
    12  
    13  	humanize "github.com/dustin/go-humanize"
    14  	"github.com/hashicorp/nomad/api"
    15  	"github.com/hashicorp/nomad/api/contexts"
    16  	"github.com/posener/complete"
    17  )
    18  
    19  const (
    20  	// bytesToLines is an estimation of how many bytes are in each log line.
    21  	// This is used to set the offset to read from when a user specifies how
    22  	// many lines to tail from.
    23  	bytesToLines int64 = 120
    24  
    25  	// defaultTailLines is the number of lines to tail by default if the value
    26  	// is not overridden.
    27  	defaultTailLines int64 = 10
    28  )
    29  
    30  type FSCommand struct {
    31  	Meta
    32  }
    33  
    34  func (f *FSCommand) Help() string {
    35  	helpText := `
    36  Usage: nomad fs [options] <allocation> <path>
    37  
    38    fs displays either the contents of an allocation directory for the passed allocation,
    39    or displays the file at the given path. The path is relative to the root of the alloc
    40    dir and defaults to root if unspecified.
    41  
    42  General Options:
    43  
    44    ` + generalOptionsUsage() + `
    45  
    46  FS Specific Options:
    47  
    48    -H
    49      Machine friendly output.
    50  
    51    -verbose
    52      Show full information.
    53  
    54    -job <job-id>
    55      Use a random allocation from the specified job ID.
    56  
    57    -stat
    58      Show file stat information instead of displaying the file, or listing the directory.
    59  
    60    -f
    61      Causes the output to not stop when the end of the file is reached, but rather to
    62      wait for additional output.
    63  
    64    -tail
    65      Show the files contents with offsets relative to the end of the file. If no
    66      offset is given, -n is defaulted to 10.
    67  
    68    -n
    69      Sets the tail location in best-efforted number of lines relative to the end
    70      of the file.
    71  
    72    -c
    73      Sets the tail location in number of bytes relative to the end of the file.
    74  `
    75  	return strings.TrimSpace(helpText)
    76  }
    77  
    78  func (f *FSCommand) Synopsis() string {
    79  	return "Inspect the contents of an allocation directory"
    80  }
    81  
    82  func (c *FSCommand) AutocompleteFlags() complete.Flags {
    83  	return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
    84  		complete.Flags{
    85  			"-H":       complete.PredictNothing,
    86  			"-verbose": complete.PredictNothing,
    87  			"-job":     complete.PredictAnything,
    88  			"-stat":    complete.PredictNothing,
    89  			"-f":       complete.PredictNothing,
    90  			"-tail":    complete.PredictNothing,
    91  			"-n":       complete.PredictAnything,
    92  			"-c":       complete.PredictAnything,
    93  		})
    94  }
    95  
    96  func (f *FSCommand) AutocompleteArgs() complete.Predictor {
    97  	return complete.PredictFunc(func(a complete.Args) []string {
    98  		client, err := f.Meta.Client()
    99  		if err != nil {
   100  			return nil
   101  		}
   102  
   103  		resp, _, err := client.Search().PrefixSearch(a.Last, contexts.Allocs, nil)
   104  		if err != nil {
   105  			return []string{}
   106  		}
   107  		return resp.Matches[contexts.Allocs]
   108  	})
   109  }
   110  
   111  func (f *FSCommand) Run(args []string) int {
   112  	var verbose, machine, job, stat, tail, follow bool
   113  	var numLines, numBytes int64
   114  
   115  	flags := f.Meta.FlagSet("fs", FlagSetClient)
   116  	flags.Usage = func() { f.Ui.Output(f.Help()) }
   117  	flags.BoolVar(&verbose, "verbose", false, "")
   118  	flags.BoolVar(&machine, "H", false, "")
   119  	flags.BoolVar(&job, "job", false, "")
   120  	flags.BoolVar(&stat, "stat", false, "")
   121  	flags.BoolVar(&follow, "f", false, "")
   122  	flags.BoolVar(&tail, "tail", false, "")
   123  	flags.Int64Var(&numLines, "n", -1, "")
   124  	flags.Int64Var(&numBytes, "c", -1, "")
   125  
   126  	if err := flags.Parse(args); err != nil {
   127  		return 1
   128  	}
   129  	args = flags.Args()
   130  
   131  	if len(args) < 1 {
   132  		if job {
   133  			f.Ui.Error("job ID is required")
   134  		} else {
   135  			f.Ui.Error("allocation ID is required")
   136  		}
   137  		return 1
   138  	}
   139  
   140  	if len(args) > 2 {
   141  		f.Ui.Error(f.Help())
   142  		return 1
   143  	}
   144  
   145  	path := "/"
   146  	if len(args) == 2 {
   147  		path = args[1]
   148  	}
   149  
   150  	client, err := f.Meta.Client()
   151  	if err != nil {
   152  		f.Ui.Error(fmt.Sprintf("Error initializing client: %v", err))
   153  		return 1
   154  	}
   155  
   156  	// If -job is specified, use random allocation, otherwise use provided allocation
   157  	allocID := args[0]
   158  	if job {
   159  		allocID, err = getRandomJobAlloc(client, args[0])
   160  		if err != nil {
   161  			f.Ui.Error(fmt.Sprintf("Error fetching allocations: %v", err))
   162  			return 1
   163  		}
   164  	}
   165  
   166  	// Truncate the id unless full length is requested
   167  	length := shortId
   168  	if verbose {
   169  		length = fullId
   170  	}
   171  	// Query the allocation info
   172  	if len(allocID) == 1 {
   173  		f.Ui.Error(fmt.Sprintf("Alloc ID must contain at least two characters."))
   174  		return 1
   175  	}
   176  
   177  	allocID = sanatizeUUIDPrefix(allocID)
   178  	allocs, _, err := client.Allocations().PrefixList(allocID)
   179  	if err != nil {
   180  		f.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err))
   181  		return 1
   182  	}
   183  	if len(allocs) == 0 {
   184  		f.Ui.Error(fmt.Sprintf("No allocation(s) with prefix or id %q found", allocID))
   185  		return 1
   186  	}
   187  	if len(allocs) > 1 {
   188  		// Format the allocs
   189  		out := formatAllocListStubs(allocs, verbose, length)
   190  		f.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out))
   191  		return 1
   192  	}
   193  	// Prefix lookup matched a single allocation
   194  	alloc, _, err := client.Allocations().Info(allocs[0].ID, nil)
   195  	if err != nil {
   196  		f.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err))
   197  		return 1
   198  	}
   199  
   200  	// Get file stat info
   201  	file, _, err := client.AllocFS().Stat(alloc, path, nil)
   202  	if err != nil {
   203  		f.Ui.Error(err.Error())
   204  		return 1
   205  	}
   206  
   207  	// If we want file stats, print those and exit.
   208  	if stat {
   209  		// Display the file information
   210  		out := make([]string, 2)
   211  		out[0] = "Mode|Size|Modified Time|Name"
   212  		if file != nil {
   213  			fn := file.Name
   214  			if file.IsDir {
   215  				fn = fmt.Sprintf("%s/", fn)
   216  			}
   217  			var size string
   218  			if machine {
   219  				size = fmt.Sprintf("%d", file.Size)
   220  			} else {
   221  				size = humanize.IBytes(uint64(file.Size))
   222  			}
   223  			out[1] = fmt.Sprintf("%s|%s|%s|%s", file.FileMode, size,
   224  				formatTime(file.ModTime), fn)
   225  		}
   226  		f.Ui.Output(formatList(out))
   227  		return 0
   228  	}
   229  
   230  	// Determine if the path is a file or a directory.
   231  	if file.IsDir {
   232  		// We have a directory, list it.
   233  		files, _, err := client.AllocFS().List(alloc, path, nil)
   234  		if err != nil {
   235  			f.Ui.Error(fmt.Sprintf("Error listing alloc dir: %s", err))
   236  			return 1
   237  		}
   238  		// Display the file information in a tabular format
   239  		out := make([]string, len(files)+1)
   240  		out[0] = "Mode|Size|Modified Time|Name"
   241  		for i, file := range files {
   242  			fn := file.Name
   243  			if file.IsDir {
   244  				fn = fmt.Sprintf("%s/", fn)
   245  			}
   246  			var size string
   247  			if machine {
   248  				size = fmt.Sprintf("%d", file.Size)
   249  			} else {
   250  				size = humanize.IBytes(uint64(file.Size))
   251  			}
   252  			out[i+1] = fmt.Sprintf("%s|%s|%s|%s",
   253  				file.FileMode,
   254  				size,
   255  				formatTime(file.ModTime),
   256  				fn,
   257  			)
   258  		}
   259  		f.Ui.Output(formatList(out))
   260  		return 0
   261  	}
   262  
   263  	// We have a file, output it.
   264  	var r io.ReadCloser
   265  	var readErr error
   266  	if !tail {
   267  		if follow {
   268  			r, readErr = f.followFile(client, alloc, path, api.OriginStart, 0, -1)
   269  		} else {
   270  			r, readErr = client.AllocFS().Cat(alloc, path, nil)
   271  		}
   272  
   273  		if readErr != nil {
   274  			readErr = fmt.Errorf("Error reading file: %v", readErr)
   275  		}
   276  	} else {
   277  		// Parse the offset
   278  		var offset int64 = defaultTailLines * bytesToLines
   279  
   280  		if nLines, nBytes := numLines != -1, numBytes != -1; nLines && nBytes {
   281  			f.Ui.Error("Both -n and -c are not allowed")
   282  			return 1
   283  		} else if numLines < -1 || numBytes < -1 {
   284  			f.Ui.Error("Invalid size is specified")
   285  			return 1
   286  		} else if nLines {
   287  			offset = numLines * bytesToLines
   288  		} else if nBytes {
   289  			offset = numBytes
   290  		} else {
   291  			numLines = defaultTailLines
   292  		}
   293  
   294  		if offset > file.Size {
   295  			offset = file.Size
   296  		}
   297  
   298  		if follow {
   299  			r, readErr = f.followFile(client, alloc, path, api.OriginEnd, offset, numLines)
   300  		} else {
   301  			// This offset needs to be relative from the front versus the follow
   302  			// is relative to the end
   303  			offset = file.Size - offset
   304  			r, readErr = client.AllocFS().ReadAt(alloc, path, offset, -1, nil)
   305  
   306  			// If numLines is set, wrap the reader
   307  			if numLines != -1 {
   308  				r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second)
   309  			}
   310  		}
   311  
   312  		if readErr != nil {
   313  			readErr = fmt.Errorf("Error tailing file: %v", readErr)
   314  		}
   315  	}
   316  
   317  	if r != nil {
   318  		defer r.Close()
   319  	}
   320  	if readErr != nil {
   321  		f.Ui.Error(readErr.Error())
   322  		return 1
   323  	}
   324  
   325  	_, err = io.Copy(os.Stdout, r)
   326  	if err != nil {
   327  		f.Ui.Error(fmt.Sprintf("error tailing file: %s", err))
   328  		return 1
   329  	}
   330  
   331  	return 0
   332  }
   333  
   334  // followFile outputs the contents of the file to stdout relative to the end of
   335  // the file. If numLines does not equal -1, then tail -n behavior is used.
   336  func (f *FSCommand) followFile(client *api.Client, alloc *api.Allocation,
   337  	path, origin string, offset, numLines int64) (io.ReadCloser, error) {
   338  
   339  	cancel := make(chan struct{})
   340  	frames, errCh := client.AllocFS().Stream(alloc, path, origin, offset, cancel, nil)
   341  	select {
   342  	case err := <-errCh:
   343  		return nil, err
   344  	default:
   345  	}
   346  	signalCh := make(chan os.Signal, 1)
   347  	signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
   348  
   349  	// Create a reader
   350  	var r io.ReadCloser
   351  	frameReader := api.NewFrameReader(frames, errCh, cancel)
   352  	frameReader.SetUnblockTime(500 * time.Millisecond)
   353  	r = frameReader
   354  
   355  	// If numLines is set, wrap the reader
   356  	if numLines != -1 {
   357  		r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second)
   358  	}
   359  
   360  	go func() {
   361  		<-signalCh
   362  
   363  		// End the streaming
   364  		r.Close()
   365  	}()
   366  
   367  	return r, nil
   368  }
   369  
   370  // Get Random Allocation ID from a known jobID. Prefer to use a running allocation,
   371  // but use a dead allocation if no running allocations are found
   372  func getRandomJobAlloc(client *api.Client, jobID string) (string, error) {
   373  	var runningAllocs []*api.AllocationListStub
   374  	allocs, _, err := client.Jobs().Allocations(jobID, false, nil)
   375  
   376  	// Check that the job actually has allocations
   377  	if len(allocs) == 0 {
   378  		return "", fmt.Errorf("job %q doesn't exist or it has no allocations", jobID)
   379  	}
   380  
   381  	for _, v := range allocs {
   382  		if v.ClientStatus == "running" {
   383  			runningAllocs = append(runningAllocs, v)
   384  		}
   385  	}
   386  	// If we don't have any allocations running, use dead allocations
   387  	if len(runningAllocs) < 1 {
   388  		runningAllocs = allocs
   389  	}
   390  
   391  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   392  	allocID := runningAllocs[r.Intn(len(runningAllocs))].ID
   393  	return allocID, err
   394  }