github.com/swiftstack/ProxyFS@v0.0.0-20210203235616-4017c267d62f/pfsagentd/request.go (about) 1 // Copyright (c) 2015-2021, NVIDIA CORPORATION. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package main 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "io" 10 "io/ioutil" 11 "math/rand" 12 "net/http" 13 "os" 14 "os/exec" 15 "strings" 16 "sync" 17 "sync/atomic" 18 "time" 19 20 "github.com/swiftstack/ProxyFS/jrpcfs" 21 "github.com/swiftstack/ProxyFS/retryrpc" 22 "github.com/swiftstack/ProxyFS/version" 23 ) 24 25 const ( 26 authPipeReadBufSize = 1024 27 ) 28 29 type authOutStruct struct { 30 AuthToken string 31 StorageURL string 32 } 33 34 func doMountProxyFS() { 35 var ( 36 accountName string 37 err error 38 mountReply *jrpcfs.MountByAccountNameReply 39 mountRequest *jrpcfs.MountByAccountNameRequest 40 swiftStorageURL string 41 swiftStorageURLSplit []string 42 ) 43 44 swiftStorageURL = fetchStorageURL() 45 if "" == swiftStorageURL { 46 logFatalf("unable to fetchStorageURL()") 47 } 48 49 swiftStorageURLSplit = strings.Split(swiftStorageURL, "/") 50 51 accountName = swiftStorageURLSplit[4] 52 53 mountRequest = &jrpcfs.MountByAccountNameRequest{ 54 AccountName: accountName, 55 MountOptions: 0, 56 AuthUserID: 0, 57 AuthGroupID: 0, 58 } 59 60 mountReply = &jrpcfs.MountByAccountNameReply{} 61 62 err = doJRPCRequest("Server.RpcMountByAccountName", mountRequest, mountReply) 63 if nil != err { 64 logFatalf("unable to mount Volume %s (Account: %s): %v", globals.config.FUSEVolumeName, accountName, err) 65 } 66 67 globals.mountID = mountReply.MountID 68 globals.rootDirInodeNumber = uint64(mountReply.RootDirInodeNumber) 69 globals.retryRPCPublicIPAddr = mountReply.RetryRPCPublicIPAddr 70 globals.retryRPCPort = mountReply.RetryRPCPort 71 globals.rootCAx509CertificatePEM = mountReply.RootCAx509CertificatePEM 72 73 retryrpcConfig := &retryrpc.ClientConfig{ 74 MyUniqueID: string(globals.mountID), 75 IPAddr: globals.retryRPCPublicIPAddr, 76 Port: int(globals.retryRPCPort), 77 RootCAx509CertificatePEM: globals.rootCAx509CertificatePEM, 78 Callbacks: &globals, 79 DeadlineIO: globals.config.RetryRPCDeadlineIO, 80 KeepAlivePeriod: globals.config.RetryRPCKeepAlivePeriod, 81 } 82 globals.retryRPCClient, err = retryrpc.NewClient(retryrpcConfig) 83 if nil != err { 84 logFatalf("unable to retryRPCClient.NewClient(%v,%v): Volume: %s (Account: %s) err: %v", globals.retryRPCPublicIPAddr, globals.retryRPCPort, globals.config.FUSEVolumeName, accountName, err) 85 } 86 } 87 88 func doUnmountProxyFS() { 89 var ( 90 err error 91 unmountReply *jrpcfs.Reply 92 unmountRequest *jrpcfs.UnmountRequest 93 ) 94 95 // TODO: Flush outstanding FileInode's 96 // TODO: Tell ProxyFS we are releasing all leases 97 // TODO: Tell ProxyFS we are unmounting 98 99 unmountRequest = &jrpcfs.UnmountRequest{ 100 MountID: globals.mountID, 101 } 102 103 unmountReply = &jrpcfs.Reply{} 104 105 err = doJRPCRequest("Server.RpcUnmount", unmountRequest, unmountReply) 106 if nil != err { 107 logFatalf("unable to unmount Volume %s: %v", globals.config.FUSEVolumeName, err) 108 } 109 110 globals.retryRPCClient.Close() 111 } 112 113 func doJRPCRequest(jrpcMethod string, jrpcParam interface{}, jrpcResult interface{}) (err error) { 114 var ( 115 httpErr error 116 httpRequest *http.Request 117 jrpcRequest []byte 118 jrpcRequestID uint64 119 jrpcResponse []byte 120 marshalErr error 121 ok bool 122 swiftStorageURL string 123 unmarshalErr error 124 ) 125 126 jrpcRequestID, jrpcRequest, marshalErr = jrpcMarshalRequest(jrpcMethod, jrpcParam) 127 if nil != marshalErr { 128 logFatalf("unable to marshal request (jrpcMethod=%s jrpcParam=%v): %#v", jrpcMethod, jrpcParam, marshalErr) 129 } 130 131 swiftStorageURL = fetchStorageURL() 132 if "" == swiftStorageURL { 133 logFatalf("unable to fetchStorageURL()") 134 } 135 136 httpRequest, httpErr = http.NewRequest("PROXYFS", swiftStorageURL, bytes.NewReader(jrpcRequest)) 137 if nil != httpErr { 138 logFatalf("unable to create PROXYFS http.Request (jrpcMethod=%s jrpcParam=%#v): %v", jrpcMethod, jrpcParam, httpErr) 139 } 140 141 httpRequest.Header["Content-Type"] = []string{"application/json"} 142 143 _, jrpcResponse, ok, _ = doHTTPRequest(httpRequest, http.StatusOK, http.StatusUnprocessableEntity) 144 if !ok { 145 logFatalf("unable to contact ProxyFS") 146 } 147 148 _, err, unmarshalErr = jrpcUnmarshalResponseForIDAndError(jrpcResponse) 149 if nil != unmarshalErr { 150 logFatalf("unable to unmarshal response [case 1] (jrpcMethod=%s jrpcParam=%#v): %v", jrpcMethod, jrpcParam, unmarshalErr) 151 } 152 153 if nil != err { 154 return 155 } 156 157 unmarshalErr = jrpcUnmarshalResponse(jrpcRequestID, jrpcResponse, jrpcResult) 158 if nil != unmarshalErr { 159 logFatalf("unable to unmarshal response [case 2] (jrpcMethod=%s jrpcParam=%#v): %v", jrpcMethod, jrpcParam, unmarshalErr) 160 } 161 162 return 163 } 164 165 func doHTTPRequest(request *http.Request, okStatusCodes ...int) (response *http.Response, responseBody []byte, ok bool, statusCode int) { 166 var ( 167 err error 168 okStatusCode int 169 okStatusCodesSet map[int]struct{} 170 retryDelay time.Duration 171 retryIndex uint64 172 swiftAuthToken string 173 ) 174 175 _ = atomic.AddUint64(&globals.metrics.HTTPRequests, 1) 176 177 request.Header["User-Agent"] = []string{"PFSAgent " + version.ProxyFSVersion} 178 179 okStatusCodesSet = make(map[int]struct{}) 180 for _, okStatusCode = range okStatusCodes { 181 okStatusCodesSet[okStatusCode] = struct{}{} 182 } 183 184 retryIndex = 0 185 186 for { 187 swiftAuthToken = fetchAuthToken() 188 189 request.Header["X-Auth-Token"] = []string{swiftAuthToken} 190 191 _ = atomic.AddUint64(&globals.metrics.HTTPRequestsInFlight, 1) 192 response, err = globals.httpClient.Do(request) 193 _ = atomic.AddUint64(&globals.metrics.HTTPRequestsInFlight, ^uint64(0)) 194 if nil != err { 195 _ = atomic.AddUint64(&globals.metrics.HTTPRequestSubmissionFailures, 1) 196 logErrorf("doHTTPRequest(%s %s) failed to submit request: %v", request.Method, request.URL.String(), err) 197 ok = false 198 return 199 } 200 201 responseBody, err = ioutil.ReadAll(response.Body) 202 _ = response.Body.Close() 203 if nil != err { 204 _ = atomic.AddUint64(&globals.metrics.HTTPRequestResponseBodyCorruptions, 1) 205 logErrorf("doHTTPRequest(%s %s) failed to read responseBody: %v", request.Method, request.URL.String(), err) 206 ok = false 207 return 208 } 209 210 _, ok = okStatusCodesSet[response.StatusCode] 211 if ok { 212 statusCode = response.StatusCode 213 return 214 } 215 216 if retryIndex >= globals.config.SwiftRetryLimit { 217 _ = atomic.AddUint64(&globals.metrics.HTTPRequestRetryLimitExceededCount, 1) 218 logWarnf("doHTTPRequest(%s %s) reached SwiftRetryLimit", request.Method, request.URL.String()) 219 ok = false 220 return 221 } 222 223 if http.StatusUnauthorized == response.StatusCode { 224 _ = atomic.AddUint64(&globals.metrics.HTTPRequestsRequiringReauthorization, 1) 225 logInfof("doHTTPRequest(%s %s) needs to call updateAuthTokenAndStorageURL()", request.Method, request.URL.String()) 226 updateAuthTokenAndStorageURL() 227 } else { 228 logWarnf("doHTTPRequest(%s %s) needs to retry due to unexpected http.Status: %s", request.Method, request.URL.String(), response.Status) 229 230 // Close request.Body (if any) at this time just in case... 231 // 232 // It appears that net/http.Do() will actually return 233 // even if it has an outstanding Read() call to 234 // request.Body.Read() and calling request.Body.Close() 235 // will give it a chance to force request.Body.Read() 236 // to exit cleanly. 237 238 if nil != request.Body { 239 _ = request.Body.Close() 240 } 241 } 242 243 retryDelay = globals.retryDelay[retryIndex].nominal - time.Duration(rand.Int63n(int64(globals.retryDelay[retryIndex].variance))) 244 time.Sleep(retryDelay) 245 retryIndex++ 246 247 _ = atomic.AddUint64(&globals.metrics.HTTPRequestRetries, 1) 248 } 249 } 250 251 func fetchAuthToken() (swiftAuthToken string) { 252 var ( 253 swiftAuthWaitGroup *sync.WaitGroup 254 ) 255 256 for { 257 globals.Lock() 258 259 // Make a copy of globals.swiftAuthWaitGroup (if any) thus 260 // avoiding a race where, after the active instance of 261 // updateAuthTokenAndStorageURL() signals completion, it 262 // will erase it from globals (indicating no auth is in 263 // progress anymore) 264 265 swiftAuthWaitGroup = globals.swiftAuthWaitGroup 266 267 if nil == swiftAuthWaitGroup { 268 swiftAuthToken = globals.swiftAuthToken 269 globals.Unlock() 270 return 271 } 272 273 globals.Unlock() 274 275 swiftAuthWaitGroup.Wait() 276 } 277 } 278 279 func fetchStorageURL() (swiftStorageURL string) { 280 var ( 281 swiftAuthWaitGroup *sync.WaitGroup 282 ) 283 284 for { 285 globals.Lock() 286 287 // Make a copy of globals.swiftAuthWaitGroup (if any) thus 288 // avoiding a race where, after the active instance of 289 // updateAuthTokenAndStorageURL() signals completion, it 290 // will erase it from globals (indicating no auth is in 291 // progress anymore) 292 293 swiftAuthWaitGroup = globals.swiftAuthWaitGroup 294 295 if nil == swiftAuthWaitGroup { 296 swiftStorageURL = globals.swiftStorageURL 297 globals.Unlock() 298 return 299 } 300 301 globals.Unlock() 302 303 swiftAuthWaitGroup.Wait() 304 } 305 } 306 307 func updateAuthTokenAndStorageURL() { 308 var ( 309 authOut authOutStruct 310 err error 311 stderrChanBuf []byte 312 stderrChanBufChunk []byte 313 stdoutChanBuf []byte 314 stdoutChanBufChunk []byte 315 swiftAuthWaitGroup *sync.WaitGroup 316 ) 317 318 // First check and see if another instance is already in-flight 319 320 globals.Lock() 321 322 // Make a copy of globals.swiftAuthWaitGroup (if any) thus 323 // avoiding a race where, after the active instance signals 324 // completion, it will erase it from globals (indicating no 325 // auth is in progress anymore) 326 327 swiftAuthWaitGroup = globals.swiftAuthWaitGroup 328 329 if nil != swiftAuthWaitGroup { 330 // Another instance is already in flight... just await its completion 331 332 globals.Unlock() 333 334 swiftAuthWaitGroup.Wait() 335 336 return 337 } 338 339 // We will be doing active instance performing the auth, 340 // so create a sync.WaitGroup for other instances and fetches to await 341 342 globals.swiftAuthWaitGroup = &sync.WaitGroup{} 343 globals.swiftAuthWaitGroup.Add(1) 344 345 globals.Unlock() 346 347 if nil != globals.authPlugInControl { 348 // There seems to be an active authPlugIn... drain any bytes sent to stdoutChan first 349 350 for { 351 select { 352 case _ = <-globals.authPlugInControl.stdoutChan: 353 default: 354 goto EscapeStdoutChanDrain 355 } 356 } 357 358 EscapeStdoutChanDrain: 359 360 // See if there is anything in stderrChan 361 362 stderrChanBuf = make([]byte, 0, authPipeReadBufSize) 363 364 for { 365 select { 366 case stderrChanBufChunk = <-globals.authPlugInControl.stderrChan: 367 stderrChanBuf = append(stderrChanBuf, stderrChanBufChunk...) 368 default: 369 goto EscapeStderrChanRead1 370 } 371 } 372 373 EscapeStderrChanRead1: 374 375 if 0 < len(stderrChanBuf) { 376 logWarnf("got unexpected authPlugInStderr data: %s", string(stderrChanBuf[:])) 377 378 stopAuthPlugIn() 379 } else { 380 // No errors... so lets try sending a byte to authPlugIn to request a fresh authorization 381 382 _, err = globals.authPlugInControl.stdinPipe.Write([]byte{0}) 383 if nil != err { 384 logWarnf("got unexpected error sending SIGHUP to authPlugIn: %v", err) 385 386 stopAuthPlugIn() 387 } 388 } 389 } 390 391 if nil == globals.authPlugInControl { 392 // Either authPlugIn wasn't (thought to be) running, or it failed above... so (re)start it 393 394 startAuthPlugIn() 395 } 396 397 // Now read authPlugInStdout for a valid JSON-marshalled authOutStruct 398 399 stdoutChanBuf = make([]byte, 0, authPipeReadBufSize) 400 401 for { 402 select { 403 case stdoutChanBufChunk = <-globals.authPlugInControl.stdoutChan: 404 stdoutChanBuf = append(stdoutChanBuf, stdoutChanBufChunk...) 405 406 // Perhaps we've received the entire authOutStruct 407 408 err = json.Unmarshal(stdoutChanBuf, &authOut) 409 if nil == err { 410 // Got a clean JSON-formatted authOutStruct 411 412 goto EscapeFetchAuthOut 413 } 414 case stderrChanBuf = <-globals.authPlugInControl.stderrChan: 415 // Uh oh... started receiving an error... drain it and "error out" 416 417 for { 418 select { 419 case stderrChanBufChunk = <-globals.authPlugInControl.stderrChan: 420 stderrChanBuf = append(stderrChanBuf, stderrChanBufChunk...) 421 default: 422 goto EscapeStderrChanRead2 423 } 424 } 425 426 EscapeStderrChanRead2: 427 428 logWarnf("got unexpected authPlugInStderr data: %s", string(stderrChanBuf[:])) 429 430 authOut.AuthToken = "" 431 authOut.StorageURL = "" 432 433 goto EscapeFetchAuthOut 434 } 435 } 436 437 EscapeFetchAuthOut: 438 439 globals.Lock() 440 441 globals.swiftAuthToken = authOut.AuthToken 442 globals.swiftStorageURL = authOut.StorageURL 443 444 // Finally, indicate to waiters we are done and also enable 445 // the next call to updateAuthTokenAndStorageURL() to perform 446 // the auth again 447 448 globals.swiftAuthWaitGroup.Done() 449 globals.swiftAuthWaitGroup = nil 450 451 globals.Unlock() 452 } 453 454 func authPlugInPipeReader(pipeToRead io.ReadCloser, chanToWrite chan []byte, wg *sync.WaitGroup) { 455 var ( 456 buf []byte 457 eof bool 458 err error 459 n int 460 ) 461 462 for { 463 buf = make([]byte, authPipeReadBufSize) 464 465 n, err = pipeToRead.Read(buf) 466 switch err { 467 case nil: 468 eof = false 469 case io.EOF: 470 eof = true 471 default: 472 logFatalf("got unexpected error reading authPlugInPipe: %v", err) 473 } 474 475 if 0 < n { 476 chanToWrite <- buf[:n] 477 } 478 479 if eof { 480 wg.Done() 481 return // Exits this goroutine 482 } 483 } 484 } 485 486 func startAuthPlugIn() { 487 var ( 488 err error 489 ) 490 491 globals.authPlugInControl = &authPlugInControlStruct{ 492 cmd: exec.Command(globals.config.PlugInPath, globals.config.PlugInEnvName), 493 stdoutChan: make(chan []byte), 494 stderrChan: make(chan []byte), 495 } 496 497 globals.authPlugInControl.stdinPipe, err = globals.authPlugInControl.cmd.StdinPipe() 498 if nil != err { 499 logFatalf("got unexpected error creating authPlugIn stdinPipe: %v", err) 500 } 501 globals.authPlugInControl.stdoutPipe, err = globals.authPlugInControl.cmd.StdoutPipe() 502 if nil != err { 503 logFatalf("got unexpected error creating authPlugIn stdoutPipe: %v", err) 504 } 505 globals.authPlugInControl.stderrPipe, err = globals.authPlugInControl.cmd.StderrPipe() 506 if nil != err { 507 logFatalf("got unexpected error creating authPlugIn stderrPipe: %v", err) 508 } 509 510 globals.authPlugInControl.wg.Add(2) 511 512 go authPlugInPipeReader(globals.authPlugInControl.stdoutPipe, globals.authPlugInControl.stdoutChan, &globals.authPlugInControl.wg) 513 go authPlugInPipeReader(globals.authPlugInControl.stderrPipe, globals.authPlugInControl.stderrChan, &globals.authPlugInControl.wg) 514 515 if "" != globals.config.PlugInEnvValue { 516 globals.authPlugInControl.cmd.Env = append(os.Environ(), globals.config.PlugInEnvName+"="+globals.config.PlugInEnvValue) 517 } 518 519 err = globals.authPlugInControl.cmd.Start() 520 if nil != err { 521 logFatalf("got unexpected error starting authPlugIn: %v", err) 522 } 523 } 524 525 func stopAuthPlugIn() { 526 if nil == globals.authPlugInControl { 527 // No authPlugIn running 528 return 529 } 530 531 // Stop authPlugIn (ignore errors since they just indicate authPlugIn failed) 532 533 _ = globals.authPlugInControl.stdinPipe.Close() 534 535 _ = globals.authPlugInControl.cmd.Wait() 536 537 // Drain stdoutChan & stderrChan 538 539 for { 540 select { 541 case _ = <-globals.authPlugInControl.stdoutChan: 542 default: 543 goto EscapeStdoutChanDrain 544 } 545 } 546 547 EscapeStdoutChanDrain: 548 549 for { 550 select { 551 case _ = <-globals.authPlugInControl.stderrChan: 552 default: 553 goto EscapeStderrChanDrain 554 } 555 } 556 557 EscapeStderrChanDrain: 558 559 // Tell stdoutPipe and stderrPipe readers to go away (ignore errors) 560 561 _ = globals.authPlugInControl.stdoutPipe.Close() 562 _ = globals.authPlugInControl.stderrPipe.Close() 563 564 // Wait for stdoutPipe and stderrPipe readers to exit 565 566 globals.authPlugInControl.wg.Wait() 567 568 // Finally, clean out authPlugInControl 569 570 globals.authPlugInControl = nil 571 }