github.com/djenriquez/nomad-1@v0.8.1/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) Run(args []string) int {
   113  	var verbose, machine, job, stat, tail, follow bool
   114  	var numLines, numBytes int64
   115  
   116  	flags := f.Meta.FlagSet("alloc fs", FlagSetClient)
   117  	flags.Usage = func() { f.Ui.Output(f.Help()) }
   118  	flags.BoolVar(&verbose, "verbose", false, "")
   119  	flags.BoolVar(&machine, "H", false, "")
   120  	flags.BoolVar(&job, "job", false, "")
   121  	flags.BoolVar(&stat, "stat", false, "")
   122  	flags.BoolVar(&follow, "f", false, "")
   123  	flags.BoolVar(&tail, "tail", false, "")
   124  	flags.Int64Var(&numLines, "n", -1, "")
   125  	flags.Int64Var(&numBytes, "c", -1, "")
   126  
   127  	if err := flags.Parse(args); err != nil {
   128  		return 1
   129  	}
   130  	args = flags.Args()
   131  
   132  	if len(args) < 1 {
   133  		if job {
   134  			f.Ui.Error("job ID is required")
   135  		} else {
   136  			f.Ui.Error("allocation ID is required")
   137  		}
   138  		return 1
   139  	}
   140  
   141  	if len(args) > 2 {
   142  		f.Ui.Error(f.Help())
   143  		return 1
   144  	}
   145  
   146  	path := "/"
   147  	if len(args) == 2 {
   148  		path = args[1]
   149  	}
   150  
   151  	client, err := f.Meta.Client()
   152  	if err != nil {
   153  		f.Ui.Error(fmt.Sprintf("Error initializing client: %v", err))
   154  		return 1
   155  	}
   156  
   157  	// If -job is specified, use random allocation, otherwise use provided allocation
   158  	allocID := args[0]
   159  	if job {
   160  		allocID, err = getRandomJobAlloc(client, args[0])
   161  		if err != nil {
   162  			f.Ui.Error(fmt.Sprintf("Error fetching allocations: %v", err))
   163  			return 1
   164  		}
   165  	}
   166  
   167  	// Truncate the id unless full length is requested
   168  	length := shortId
   169  	if verbose {
   170  		length = fullId
   171  	}
   172  	// Query the allocation info
   173  	if len(allocID) == 1 {
   174  		f.Ui.Error(fmt.Sprintf("Alloc ID must contain at least two characters."))
   175  		return 1
   176  	}
   177  
   178  	allocID = sanitizeUUIDPrefix(allocID)
   179  	allocs, _, err := client.Allocations().PrefixList(allocID)
   180  	if err != nil {
   181  		f.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err))
   182  		return 1
   183  	}
   184  	if len(allocs) == 0 {
   185  		f.Ui.Error(fmt.Sprintf("No allocation(s) with prefix or id %q found", allocID))
   186  		return 1
   187  	}
   188  	if len(allocs) > 1 {
   189  		// Format the allocs
   190  		out := formatAllocListStubs(allocs, verbose, length)
   191  		f.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out))
   192  		return 1
   193  	}
   194  	// Prefix lookup matched a single allocation
   195  	alloc, _, err := client.Allocations().Info(allocs[0].ID, nil)
   196  	if err != nil {
   197  		f.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err))
   198  		return 1
   199  	}
   200  
   201  	// Get file stat info
   202  	file, _, err := client.AllocFS().Stat(alloc, path, nil)
   203  	if err != nil {
   204  		f.Ui.Error(err.Error())
   205  		return 1
   206  	}
   207  
   208  	// If we want file stats, print those and exit.
   209  	if stat {
   210  		// Display the file information
   211  		out := make([]string, 2)
   212  		out[0] = "Mode|Size|Modified Time|Name"
   213  		if file != nil {
   214  			fn := file.Name
   215  			if file.IsDir {
   216  				fn = fmt.Sprintf("%s/", fn)
   217  			}
   218  			var size string
   219  			if machine {
   220  				size = fmt.Sprintf("%d", file.Size)
   221  			} else {
   222  				size = humanize.IBytes(uint64(file.Size))
   223  			}
   224  			out[1] = fmt.Sprintf("%s|%s|%s|%s", file.FileMode, size,
   225  				formatTime(file.ModTime), fn)
   226  		}
   227  		f.Ui.Output(formatList(out))
   228  		return 0
   229  	}
   230  
   231  	// Determine if the path is a file or a directory.
   232  	if file.IsDir {
   233  		// We have a directory, list it.
   234  		files, _, err := client.AllocFS().List(alloc, path, nil)
   235  		if err != nil {
   236  			f.Ui.Error(fmt.Sprintf("Error listing alloc dir: %s", err))
   237  			return 1
   238  		}
   239  		// Display the file information in a tabular format
   240  		out := make([]string, len(files)+1)
   241  		out[0] = "Mode|Size|Modified Time|Name"
   242  		for i, file := range files {
   243  			fn := file.Name
   244  			if file.IsDir {
   245  				fn = fmt.Sprintf("%s/", fn)
   246  			}
   247  			var size string
   248  			if machine {
   249  				size = fmt.Sprintf("%d", file.Size)
   250  			} else {
   251  				size = humanize.IBytes(uint64(file.Size))
   252  			}
   253  			out[i+1] = fmt.Sprintf("%s|%s|%s|%s",
   254  				file.FileMode,
   255  				size,
   256  				formatTime(file.ModTime),
   257  				fn,
   258  			)
   259  		}
   260  		f.Ui.Output(formatList(out))
   261  		return 0
   262  	}
   263  
   264  	// We have a file, output it.
   265  	var r io.ReadCloser
   266  	var readErr error
   267  	if !tail {
   268  		if follow {
   269  			r, readErr = f.followFile(client, alloc, path, api.OriginStart, 0, -1)
   270  		} else {
   271  			r, readErr = client.AllocFS().Cat(alloc, path, nil)
   272  		}
   273  
   274  		if readErr != nil {
   275  			readErr = fmt.Errorf("Error reading file: %v", readErr)
   276  		}
   277  	} else {
   278  		// Parse the offset
   279  		var offset int64 = defaultTailLines * bytesToLines
   280  
   281  		if nLines, nBytes := numLines != -1, numBytes != -1; nLines && nBytes {
   282  			f.Ui.Error("Both -n and -c are not allowed")
   283  			return 1
   284  		} else if numLines < -1 || numBytes < -1 {
   285  			f.Ui.Error("Invalid size is specified")
   286  			return 1
   287  		} else if nLines {
   288  			offset = numLines * bytesToLines
   289  		} else if nBytes {
   290  			offset = numBytes
   291  		} else {
   292  			numLines = defaultTailLines
   293  		}
   294  
   295  		if offset > file.Size {
   296  			offset = file.Size
   297  		}
   298  
   299  		if follow {
   300  			r, readErr = f.followFile(client, alloc, path, api.OriginEnd, offset, numLines)
   301  		} else {
   302  			// This offset needs to be relative from the front versus the follow
   303  			// is relative to the end
   304  			offset = file.Size - offset
   305  			r, readErr = client.AllocFS().ReadAt(alloc, path, offset, -1, nil)
   306  
   307  			// If numLines is set, wrap the reader
   308  			if numLines != -1 {
   309  				r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second)
   310  			}
   311  		}
   312  
   313  		if readErr != nil {
   314  			readErr = fmt.Errorf("Error tailing file: %v", readErr)
   315  		}
   316  	}
   317  
   318  	if r != nil {
   319  		defer r.Close()
   320  	}
   321  	if readErr != nil {
   322  		f.Ui.Error(readErr.Error())
   323  		return 1
   324  	}
   325  
   326  	_, err = io.Copy(os.Stdout, r)
   327  	if err != nil {
   328  		f.Ui.Error(fmt.Sprintf("error tailing file: %s", err))
   329  		return 1
   330  	}
   331  
   332  	return 0
   333  }
   334  
   335  // followFile outputs the contents of the file to stdout relative to the end of
   336  // the file. If numLines does not equal -1, then tail -n behavior is used.
   337  func (f *AllocFSCommand) followFile(client *api.Client, alloc *api.Allocation,
   338  	path, origin string, offset, numLines int64) (io.ReadCloser, error) {
   339  
   340  	cancel := make(chan struct{})
   341  	frames, errCh := client.AllocFS().Stream(alloc, path, origin, offset, cancel, nil)
   342  	select {
   343  	case err := <-errCh:
   344  		return nil, err
   345  	default:
   346  	}
   347  	signalCh := make(chan os.Signal, 1)
   348  	signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
   349  
   350  	// Create a reader
   351  	var r io.ReadCloser
   352  	frameReader := api.NewFrameReader(frames, errCh, cancel)
   353  	frameReader.SetUnblockTime(500 * time.Millisecond)
   354  	r = frameReader
   355  
   356  	// If numLines is set, wrap the reader
   357  	if numLines != -1 {
   358  		r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second)
   359  	}
   360  
   361  	go func() {
   362  		<-signalCh
   363  
   364  		// End the streaming
   365  		r.Close()
   366  	}()
   367  
   368  	return r, nil
   369  }
   370  
   371  // Get Random Allocation ID from a known jobID. Prefer to use a running allocation,
   372  // but use a dead allocation if no running allocations are found
   373  func getRandomJobAlloc(client *api.Client, jobID string) (string, error) {
   374  	var runningAllocs []*api.AllocationListStub
   375  	allocs, _, err := client.Jobs().Allocations(jobID, false, nil)
   376  
   377  	// Check that the job actually has allocations
   378  	if len(allocs) == 0 {
   379  		return "", fmt.Errorf("job %q doesn't exist or it has no allocations", jobID)
   380  	}
   381  
   382  	for _, v := range allocs {
   383  		if v.ClientStatus == "running" {
   384  			runningAllocs = append(runningAllocs, v)
   385  		}
   386  	}
   387  	// If we don't have any allocations running, use dead allocations
   388  	if len(runningAllocs) < 1 {
   389  		runningAllocs = allocs
   390  	}
   391  
   392  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   393  	allocID := runningAllocs[r.Intn(len(runningAllocs))].ID
   394  	return allocID, err
   395  }