github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/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 := formatAllocListStubs(allocs, verbose, length) 163 f.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out)) 164 return 1 165 } 166 // Prefix lookup matched a single allocation 167 alloc, _, err := client.Allocations().Info(allocs[0].ID, nil) 168 if err != nil { 169 f.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err)) 170 return 1 171 } 172 173 // Get file stat info 174 file, _, err := client.AllocFS().Stat(alloc, path, nil) 175 if err != nil { 176 f.Ui.Error(err.Error()) 177 return 1 178 } 179 180 // If we want file stats, print those and exit. 181 if stat { 182 // Display the file information 183 out := make([]string, 2) 184 out[0] = "Mode|Size|Modified Time|Name" 185 if file != nil { 186 fn := file.Name 187 if file.IsDir { 188 fn = fmt.Sprintf("%s/", fn) 189 } 190 var size string 191 if machine { 192 size = fmt.Sprintf("%d", file.Size) 193 } else { 194 size = humanize.IBytes(uint64(file.Size)) 195 } 196 out[1] = fmt.Sprintf("%s|%s|%s|%s", file.FileMode, size, 197 formatTime(file.ModTime), fn) 198 } 199 f.Ui.Output(formatList(out)) 200 return 0 201 } 202 203 // Determine if the path is a file or a directory. 204 if file.IsDir { 205 // We have a directory, list it. 206 files, _, err := client.AllocFS().List(alloc, path, nil) 207 if err != nil { 208 f.Ui.Error(fmt.Sprintf("Error listing alloc dir: %s", err)) 209 return 1 210 } 211 // Display the file information in a tabular format 212 out := make([]string, len(files)+1) 213 out[0] = "Mode|Size|Modified Time|Name" 214 for i, file := range files { 215 fn := file.Name 216 if file.IsDir { 217 fn = fmt.Sprintf("%s/", fn) 218 } 219 var size string 220 if machine { 221 size = fmt.Sprintf("%d", file.Size) 222 } else { 223 size = humanize.IBytes(uint64(file.Size)) 224 } 225 out[i+1] = fmt.Sprintf("%s|%s|%s|%s", 226 file.FileMode, 227 size, 228 formatTime(file.ModTime), 229 fn, 230 ) 231 } 232 f.Ui.Output(formatList(out)) 233 return 0 234 } 235 236 // We have a file, output it. 237 var r io.ReadCloser 238 var readErr error 239 if !tail { 240 if follow { 241 r, readErr = f.followFile(client, alloc, path, api.OriginStart, 0, -1) 242 } else { 243 r, readErr = client.AllocFS().Cat(alloc, path, nil) 244 } 245 246 if readErr != nil { 247 readErr = fmt.Errorf("Error reading file: %v", readErr) 248 } 249 } else { 250 // Parse the offset 251 var offset int64 = defaultTailLines * bytesToLines 252 253 if nLines, nBytes := numLines != -1, numBytes != -1; nLines && nBytes { 254 f.Ui.Error("Both -n and -c are not allowed") 255 return 1 256 } else if numLines < -1 || numBytes < -1 { 257 f.Ui.Error("Invalid size is specified") 258 return 1 259 } else if nLines { 260 offset = numLines * bytesToLines 261 } else if nBytes { 262 offset = numBytes 263 } else { 264 numLines = defaultTailLines 265 } 266 267 if offset > file.Size { 268 offset = file.Size 269 } 270 271 if follow { 272 r, readErr = f.followFile(client, alloc, path, api.OriginEnd, offset, numLines) 273 } else { 274 // This offset needs to be relative from the front versus the follow 275 // is relative to the end 276 offset = file.Size - offset 277 r, readErr = client.AllocFS().ReadAt(alloc, path, offset, -1, nil) 278 279 // If numLines is set, wrap the reader 280 if numLines != -1 { 281 r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second) 282 } 283 } 284 285 if readErr != nil { 286 readErr = fmt.Errorf("Error tailing file: %v", readErr) 287 } 288 } 289 290 if r != nil { 291 defer r.Close() 292 } 293 if readErr != nil { 294 f.Ui.Error(readErr.Error()) 295 return 1 296 } 297 298 io.Copy(os.Stdout, r) 299 return 0 300 } 301 302 // followFile outputs the contents of the file to stdout relative to the end of 303 // the file. If numLines does not equal -1, then tail -n behavior is used. 304 func (f *FSCommand) followFile(client *api.Client, alloc *api.Allocation, 305 path, origin string, offset, numLines int64) (io.ReadCloser, error) { 306 307 cancel := make(chan struct{}) 308 frames, err := client.AllocFS().Stream(alloc, path, origin, offset, cancel, nil) 309 if err != nil { 310 return nil, err 311 } 312 signalCh := make(chan os.Signal, 1) 313 signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 314 315 // Create a reader 316 var r io.ReadCloser 317 frameReader := api.NewFrameReader(frames, cancel) 318 frameReader.SetUnblockTime(500 * time.Millisecond) 319 r = frameReader 320 321 // If numLines is set, wrap the reader 322 if numLines != -1 { 323 r = NewLineLimitReader(r, int(numLines), int(numLines*bytesToLines), 1*time.Second) 324 } 325 326 go func() { 327 <-signalCh 328 329 // End the streaming 330 r.Close() 331 }() 332 333 return r, nil 334 } 335 336 // Get Random Allocation ID from a known jobID. Prefer to use a running allocation, 337 // but use a dead allocation if no running allocations are found 338 func getRandomJobAlloc(client *api.Client, jobID string) (string, error) { 339 var runningAllocs []*api.AllocationListStub 340 allocs, _, err := client.Jobs().Allocations(jobID, false, nil) 341 342 // Check that the job actually has allocations 343 if len(allocs) == 0 { 344 return "", fmt.Errorf("job %q doesn't exist or it has no allocations", jobID) 345 } 346 347 for _, v := range allocs { 348 if v.ClientStatus == "running" { 349 runningAllocs = append(runningAllocs, v) 350 } 351 } 352 // If we don't have any allocations running, use dead allocations 353 if len(runningAllocs) < 1 { 354 runningAllocs = allocs 355 } 356 357 r := rand.New(rand.NewSource(time.Now().UnixNano())) 358 allocID := runningAllocs[r.Intn(len(runningAllocs))].ID 359 return allocID, err 360 }