1package command
2
3import (
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
19const (
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
30type AllocFSCommand struct {
31	Meta
32}
33
34func (f *AllocFSCommand) Help() string {
35	helpText := `
36Usage: nomad alloc fs [options] <allocation> <path>
37Alias: 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
43General Options:
44
45  ` + generalOptionsUsage() + `
46
47FS 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
79func (f *AllocFSCommand) Synopsis() string {
80	return "Inspect the contents of an allocation directory"
81}
82
83func (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
97func (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
112func (f *AllocFSCommand) Name() string { return "alloc fs" }
113
114func (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|Content Type|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|%s", file.FileMode, size,
229				formatTime(file.ModTime), file.ContentType, 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.
341func (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
377func 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}
400