github.com/mattyr/nomad@v0.3.3-0.20160919021406-3485a065154a/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 <alloc-id> <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 defer r.Close() 302 if readErr != nil { 303 f.Ui.Error(readErr.Error()) 304 return 1 305 } 306 307 io.Copy(os.Stdout, r) 308 return 0 309 } 310 311 // followFile outputs the contents of the file to stdout relative to the end of 312 // the file. If numLines does not equal -1, then tail -n behavior is used. 313 func (f *FSCommand) followFile(client *api.Client, alloc *api.Allocation, 314 path, origin string, offset, numLines int64) (io.ReadCloser, error) { 315 316 cancel := make(chan struct{}) 317 frames, err := client.AllocFS().Stream(alloc, path, origin, offset, cancel, nil) 318 if err != nil { 319 return nil, err 320 } 321 signalCh := make(chan os.Signal, 1) 322 signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 323 324 // Create a reader 325 var r io.ReadCloser 326 frameReader := api.NewFrameReader(frames, cancel) 327 frameReader.SetUnblockTime(500 * time.Millisecond) 328 r = frameReader 329 330 // If numLines is set, wrap the reader 331 if numLines != -1 { 332 r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second) 333 } 334 335 go func() { 336 <-signalCh 337 338 // End the streaming 339 r.Close() 340 }() 341 342 return r, nil 343 } 344 345 // Get Random Allocation ID from a known jobID. Prefer to use a running allocation, 346 // but use a dead allocation if no running allocations are found 347 func getRandomJobAlloc(client *api.Client, jobID string) (string, error) { 348 var runningAllocs []*api.AllocationListStub 349 allocs, _, err := client.Jobs().Allocations(jobID, nil) 350 351 // Check that the job actually has allocations 352 if len(allocs) == 0 { 353 return "", fmt.Errorf("job %q doesn't exist or it has no allocations", jobID) 354 } 355 356 for _, v := range allocs { 357 if v.ClientStatus == "running" { 358 runningAllocs = append(runningAllocs, v) 359 } 360 } 361 // If we don't have any allocations running, use dead allocations 362 if len(runningAllocs) < 1 { 363 runningAllocs = allocs 364 } 365 366 r := rand.New(rand.NewSource(time.Now().UnixNano())) 367 allocID := runningAllocs[r.Intn(len(runningAllocs))].ID 368 return allocID, err 369 }