github.com/diptanu/nomad@v0.5.7-0.20170516172507-d72e86cbe3d9/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 := make([]string, len(allocs)+1)
   163  		out[0] = "ID|Eval ID|Job ID|Task Group|Desired Status|Client Status"
   164  		for i, alloc := range allocs {
   165  			out[i+1] = fmt.Sprintf("%s|%s|%s|%s|%s|%s",
   166  				limit(alloc.ID, length),
   167  				limit(alloc.EvalID, length),
   168  				alloc.JobID,
   169  				alloc.TaskGroup,
   170  				alloc.DesiredStatus,
   171  				alloc.ClientStatus,
   172  			)
   173  		}
   174  		f.Ui.Output(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", formatList(out)))
   175  		return 0
   176  	}
   177  	// Prefix lookup matched a single allocation
   178  	alloc, _, err := client.Allocations().Info(allocs[0].ID, nil)
   179  	if err != nil {
   180  		f.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err))
   181  		return 1
   182  	}
   183  
   184  	// Get file stat info
   185  	file, _, err := client.AllocFS().Stat(alloc, path, nil)
   186  	if err != nil {
   187  		f.Ui.Error(err.Error())
   188  		return 1
   189  	}
   190  
   191  	// If we want file stats, print those and exit.
   192  	if stat {
   193  		// Display the file information
   194  		out := make([]string, 2)
   195  		out[0] = "Mode|Size|Modified Time|Name"
   196  		if file != nil {
   197  			fn := file.Name
   198  			if file.IsDir {
   199  				fn = fmt.Sprintf("%s/", fn)
   200  			}
   201  			var size string
   202  			if machine {
   203  				size = fmt.Sprintf("%d", file.Size)
   204  			} else {
   205  				size = humanize.IBytes(uint64(file.Size))
   206  			}
   207  			out[1] = fmt.Sprintf("%s|%s|%s|%s", file.FileMode, size,
   208  				formatTime(file.ModTime), fn)
   209  		}
   210  		f.Ui.Output(formatList(out))
   211  		return 0
   212  	}
   213  
   214  	// Determine if the path is a file or a directory.
   215  	if file.IsDir {
   216  		// We have a directory, list it.
   217  		files, _, err := client.AllocFS().List(alloc, path, nil)
   218  		if err != nil {
   219  			f.Ui.Error(fmt.Sprintf("Error listing alloc dir: %s", err))
   220  			return 1
   221  		}
   222  		// Display the file information in a tabular format
   223  		out := make([]string, len(files)+1)
   224  		out[0] = "Mode|Size|Modified Time|Name"
   225  		for i, file := range files {
   226  			fn := file.Name
   227  			if file.IsDir {
   228  				fn = fmt.Sprintf("%s/", fn)
   229  			}
   230  			var size string
   231  			if machine {
   232  				size = fmt.Sprintf("%d", file.Size)
   233  			} else {
   234  				size = humanize.IBytes(uint64(file.Size))
   235  			}
   236  			out[i+1] = fmt.Sprintf("%s|%s|%s|%s",
   237  				file.FileMode,
   238  				size,
   239  				formatTime(file.ModTime),
   240  				fn,
   241  			)
   242  		}
   243  		f.Ui.Output(formatList(out))
   244  		return 0
   245  	}
   246  
   247  	// We have a file, output it.
   248  	var r io.ReadCloser
   249  	var readErr error
   250  	if !tail {
   251  		if follow {
   252  			r, readErr = f.followFile(client, alloc, path, api.OriginStart, 0, -1)
   253  		} else {
   254  			r, readErr = client.AllocFS().Cat(alloc, path, nil)
   255  		}
   256  
   257  		if readErr != nil {
   258  			readErr = fmt.Errorf("Error reading file: %v", readErr)
   259  		}
   260  	} else {
   261  		// Parse the offset
   262  		var offset int64 = defaultTailLines * bytesToLines
   263  
   264  		if nLines, nBytes := numLines != -1, numBytes != -1; nLines && nBytes {
   265  			f.Ui.Error("Both -n and -c are not allowed")
   266  			return 1
   267  		} else if numLines < -1 || numBytes < -1 {
   268  			f.Ui.Error("Invalid size is specified")
   269  			return 1
   270  		} else if nLines {
   271  			offset = numLines * bytesToLines
   272  		} else if nBytes {
   273  			offset = numBytes
   274  		} else {
   275  			numLines = defaultTailLines
   276  		}
   277  
   278  		if offset > file.Size {
   279  			offset = file.Size
   280  		}
   281  
   282  		if follow {
   283  			r, readErr = f.followFile(client, alloc, path, api.OriginEnd, offset, numLines)
   284  		} else {
   285  			// This offset needs to be relative from the front versus the follow
   286  			// is relative to the end
   287  			offset = file.Size - offset
   288  			r, readErr = client.AllocFS().ReadAt(alloc, path, offset, -1, nil)
   289  
   290  			// If numLines is set, wrap the reader
   291  			if numLines != -1 {
   292  				r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second)
   293  			}
   294  		}
   295  
   296  		if readErr != nil {
   297  			readErr = fmt.Errorf("Error tailing file: %v", readErr)
   298  		}
   299  	}
   300  
   301  	if r != nil {
   302  		defer r.Close()
   303  	}
   304  	if readErr != nil {
   305  		f.Ui.Error(readErr.Error())
   306  		return 1
   307  	}
   308  
   309  	io.Copy(os.Stdout, r)
   310  	return 0
   311  }
   312  
   313  // followFile outputs the contents of the file to stdout relative to the end of
   314  // the file. If numLines does not equal -1, then tail -n behavior is used.
   315  func (f *FSCommand) followFile(client *api.Client, alloc *api.Allocation,
   316  	path, origin string, offset, numLines int64) (io.ReadCloser, error) {
   317  
   318  	cancel := make(chan struct{})
   319  	frames, err := client.AllocFS().Stream(alloc, path, origin, offset, cancel, nil)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  	signalCh := make(chan os.Signal, 1)
   324  	signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
   325  
   326  	// Create a reader
   327  	var r io.ReadCloser
   328  	frameReader := api.NewFrameReader(frames, cancel)
   329  	frameReader.SetUnblockTime(500 * time.Millisecond)
   330  	r = frameReader
   331  
   332  	// If numLines is set, wrap the reader
   333  	if numLines != -1 {
   334  		r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second)
   335  	}
   336  
   337  	go func() {
   338  		<-signalCh
   339  
   340  		// End the streaming
   341  		r.Close()
   342  	}()
   343  
   344  	return r, nil
   345  }
   346  
   347  // Get Random Allocation ID from a known jobID. Prefer to use a running allocation,
   348  // but use a dead allocation if no running allocations are found
   349  func getRandomJobAlloc(client *api.Client, jobID string) (string, error) {
   350  	var runningAllocs []*api.AllocationListStub
   351  	allocs, _, err := client.Jobs().Allocations(jobID, false, nil)
   352  
   353  	// Check that the job actually has allocations
   354  	if len(allocs) == 0 {
   355  		return "", fmt.Errorf("job %q doesn't exist or it has no allocations", jobID)
   356  	}
   357  
   358  	for _, v := range allocs {
   359  		if v.ClientStatus == "running" {
   360  			runningAllocs = append(runningAllocs, v)
   361  		}
   362  	}
   363  	// If we don't have any allocations running, use dead allocations
   364  	if len(runningAllocs) < 1 {
   365  		runningAllocs = allocs
   366  	}
   367  
   368  	r := rand.New(rand.NewSource(time.Now().UnixNano()))
   369  	allocID := runningAllocs[r.Intn(len(runningAllocs))].ID
   370  	return allocID, err
   371  }