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