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