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