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