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  }