github.com/iqoqo/nomad@v0.11.3-0.20200911112621-d7021c74d101/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) Name() string { return "alloc fs" } 113 114 func (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. 341 func (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 377 func 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 }