github.com/djenriquez/nomad-1@v0.8.1/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) Run(args []string) int { 113 var verbose, machine, job, stat, tail, follow bool 114 var numLines, numBytes int64 115 116 flags := f.Meta.FlagSet("alloc fs", FlagSetClient) 117 flags.Usage = func() { f.Ui.Output(f.Help()) } 118 flags.BoolVar(&verbose, "verbose", false, "") 119 flags.BoolVar(&machine, "H", false, "") 120 flags.BoolVar(&job, "job", false, "") 121 flags.BoolVar(&stat, "stat", false, "") 122 flags.BoolVar(&follow, "f", false, "") 123 flags.BoolVar(&tail, "tail", false, "") 124 flags.Int64Var(&numLines, "n", -1, "") 125 flags.Int64Var(&numBytes, "c", -1, "") 126 127 if err := flags.Parse(args); err != nil { 128 return 1 129 } 130 args = flags.Args() 131 132 if len(args) < 1 { 133 if job { 134 f.Ui.Error("job ID is required") 135 } else { 136 f.Ui.Error("allocation ID is required") 137 } 138 return 1 139 } 140 141 if len(args) > 2 { 142 f.Ui.Error(f.Help()) 143 return 1 144 } 145 146 path := "/" 147 if len(args) == 2 { 148 path = args[1] 149 } 150 151 client, err := f.Meta.Client() 152 if err != nil { 153 f.Ui.Error(fmt.Sprintf("Error initializing client: %v", err)) 154 return 1 155 } 156 157 // If -job is specified, use random allocation, otherwise use provided allocation 158 allocID := args[0] 159 if job { 160 allocID, err = getRandomJobAlloc(client, args[0]) 161 if err != nil { 162 f.Ui.Error(fmt.Sprintf("Error fetching allocations: %v", err)) 163 return 1 164 } 165 } 166 167 // Truncate the id unless full length is requested 168 length := shortId 169 if verbose { 170 length = fullId 171 } 172 // Query the allocation info 173 if len(allocID) == 1 { 174 f.Ui.Error(fmt.Sprintf("Alloc ID must contain at least two characters.")) 175 return 1 176 } 177 178 allocID = sanitizeUUIDPrefix(allocID) 179 allocs, _, err := client.Allocations().PrefixList(allocID) 180 if err != nil { 181 f.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err)) 182 return 1 183 } 184 if len(allocs) == 0 { 185 f.Ui.Error(fmt.Sprintf("No allocation(s) with prefix or id %q found", allocID)) 186 return 1 187 } 188 if len(allocs) > 1 { 189 // Format the allocs 190 out := formatAllocListStubs(allocs, verbose, length) 191 f.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out)) 192 return 1 193 } 194 // Prefix lookup matched a single allocation 195 alloc, _, err := client.Allocations().Info(allocs[0].ID, nil) 196 if err != nil { 197 f.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err)) 198 return 1 199 } 200 201 // Get file stat info 202 file, _, err := client.AllocFS().Stat(alloc, path, nil) 203 if err != nil { 204 f.Ui.Error(err.Error()) 205 return 1 206 } 207 208 // If we want file stats, print those and exit. 209 if stat { 210 // Display the file information 211 out := make([]string, 2) 212 out[0] = "Mode|Size|Modified Time|Name" 213 if file != nil { 214 fn := file.Name 215 if file.IsDir { 216 fn = fmt.Sprintf("%s/", fn) 217 } 218 var size string 219 if machine { 220 size = fmt.Sprintf("%d", file.Size) 221 } else { 222 size = humanize.IBytes(uint64(file.Size)) 223 } 224 out[1] = fmt.Sprintf("%s|%s|%s|%s", file.FileMode, size, 225 formatTime(file.ModTime), fn) 226 } 227 f.Ui.Output(formatList(out)) 228 return 0 229 } 230 231 // Determine if the path is a file or a directory. 232 if file.IsDir { 233 // We have a directory, list it. 234 files, _, err := client.AllocFS().List(alloc, path, nil) 235 if err != nil { 236 f.Ui.Error(fmt.Sprintf("Error listing alloc dir: %s", err)) 237 return 1 238 } 239 // Display the file information in a tabular format 240 out := make([]string, len(files)+1) 241 out[0] = "Mode|Size|Modified Time|Name" 242 for i, file := range files { 243 fn := file.Name 244 if file.IsDir { 245 fn = fmt.Sprintf("%s/", fn) 246 } 247 var size string 248 if machine { 249 size = fmt.Sprintf("%d", file.Size) 250 } else { 251 size = humanize.IBytes(uint64(file.Size)) 252 } 253 out[i+1] = fmt.Sprintf("%s|%s|%s|%s", 254 file.FileMode, 255 size, 256 formatTime(file.ModTime), 257 fn, 258 ) 259 } 260 f.Ui.Output(formatList(out)) 261 return 0 262 } 263 264 // We have a file, output it. 265 var r io.ReadCloser 266 var readErr error 267 if !tail { 268 if follow { 269 r, readErr = f.followFile(client, alloc, path, api.OriginStart, 0, -1) 270 } else { 271 r, readErr = client.AllocFS().Cat(alloc, path, nil) 272 } 273 274 if readErr != nil { 275 readErr = fmt.Errorf("Error reading file: %v", readErr) 276 } 277 } else { 278 // Parse the offset 279 var offset int64 = defaultTailLines * bytesToLines 280 281 if nLines, nBytes := numLines != -1, numBytes != -1; nLines && nBytes { 282 f.Ui.Error("Both -n and -c are not allowed") 283 return 1 284 } else if numLines < -1 || numBytes < -1 { 285 f.Ui.Error("Invalid size is specified") 286 return 1 287 } else if nLines { 288 offset = numLines * bytesToLines 289 } else if nBytes { 290 offset = numBytes 291 } else { 292 numLines = defaultTailLines 293 } 294 295 if offset > file.Size { 296 offset = file.Size 297 } 298 299 if follow { 300 r, readErr = f.followFile(client, alloc, path, api.OriginEnd, offset, numLines) 301 } else { 302 // This offset needs to be relative from the front versus the follow 303 // is relative to the end 304 offset = file.Size - offset 305 r, readErr = client.AllocFS().ReadAt(alloc, path, offset, -1, nil) 306 307 // If numLines is set, wrap the reader 308 if numLines != -1 { 309 r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second) 310 } 311 } 312 313 if readErr != nil { 314 readErr = fmt.Errorf("Error tailing file: %v", readErr) 315 } 316 } 317 318 if r != nil { 319 defer r.Close() 320 } 321 if readErr != nil { 322 f.Ui.Error(readErr.Error()) 323 return 1 324 } 325 326 _, err = io.Copy(os.Stdout, r) 327 if err != nil { 328 f.Ui.Error(fmt.Sprintf("error tailing file: %s", err)) 329 return 1 330 } 331 332 return 0 333 } 334 335 // followFile outputs the contents of the file to stdout relative to the end of 336 // the file. If numLines does not equal -1, then tail -n behavior is used. 337 func (f *AllocFSCommand) followFile(client *api.Client, alloc *api.Allocation, 338 path, origin string, offset, numLines int64) (io.ReadCloser, error) { 339 340 cancel := make(chan struct{}) 341 frames, errCh := client.AllocFS().Stream(alloc, path, origin, offset, cancel, nil) 342 select { 343 case err := <-errCh: 344 return nil, err 345 default: 346 } 347 signalCh := make(chan os.Signal, 1) 348 signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 349 350 // Create a reader 351 var r io.ReadCloser 352 frameReader := api.NewFrameReader(frames, errCh, cancel) 353 frameReader.SetUnblockTime(500 * time.Millisecond) 354 r = frameReader 355 356 // If numLines is set, wrap the reader 357 if numLines != -1 { 358 r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second) 359 } 360 361 go func() { 362 <-signalCh 363 364 // End the streaming 365 r.Close() 366 }() 367 368 return r, nil 369 } 370 371 // Get Random Allocation ID from a known jobID. Prefer to use a running allocation, 372 // but use a dead allocation if no running allocations are found 373 func getRandomJobAlloc(client *api.Client, jobID string) (string, error) { 374 var runningAllocs []*api.AllocationListStub 375 allocs, _, err := client.Jobs().Allocations(jobID, false, nil) 376 377 // Check that the job actually has allocations 378 if len(allocs) == 0 { 379 return "", fmt.Errorf("job %q doesn't exist or it has no allocations", jobID) 380 } 381 382 for _, v := range allocs { 383 if v.ClientStatus == "running" { 384 runningAllocs = append(runningAllocs, v) 385 } 386 } 387 // If we don't have any allocations running, use dead allocations 388 if len(runningAllocs) < 1 { 389 runningAllocs = allocs 390 } 391 392 r := rand.New(rand.NewSource(time.Now().UnixNano())) 393 allocID := runningAllocs[r.Intn(len(runningAllocs))].ID 394 return allocID, err 395 }