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