Fixed subnet Health report upload (#2927)

Fixed subnet file upload to allow upload of different report types
This commit is contained in:
jinapurapu
2023-07-27 15:29:30 -07:00
committed by GitHub
parent 3ce377dbd1
commit 6e8f5e0fc2
5 changed files with 149 additions and 43 deletions

View File

@@ -19,14 +19,20 @@ package subnet
import ( import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"mime/multipart" "mime/multipart"
"net"
"net/http" "net/http"
"time"
"github.com/mattn/go-ieproxy"
xhttp "github.com/minio/console/pkg/http" xhttp "github.com/minio/console/pkg/http"
"github.com/tidwall/gjson"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
mc "github.com/minio/mc/cmd" mc "github.com/minio/mc/cmd"
@@ -73,8 +79,15 @@ func UploadAuthHeaders(apiKey string) map[string]string {
return map[string]string{"x-subnet-api-key": apiKey} return map[string]string{"x-subnet-api-key": apiKey}
} }
func UploadFileToSubnet(info interface{}, client *xhttp.Client, filename string, reqURL string, headers map[string]string) (string, error) { func ProcessUploadInfo(info interface{}, uploadType string, filename string) ([]byte, string, error) {
req, e := subnetUploadReq(info, reqURL, filename) if uploadType == "health" {
return processHealthReport(info, filename)
}
return nil, "", errors.New("invalid Subnet upload type")
}
func UploadFileToSubnet(info []byte, client *xhttp.Client, reqURL string, headers map[string]string, formDataType string) (string, error) {
req, e := subnetUploadReq(info, reqURL, formDataType)
if e != nil { if e != nil {
return "", e return "", e
} }
@@ -82,7 +95,7 @@ func UploadFileToSubnet(info interface{}, client *xhttp.Client, filename string,
return resp, e return resp, e
} }
func subnetUploadReq(info interface{}, url string, filename string) (*http.Request, error) { func processHealthReport(info interface{}, filename string) ([]byte, string, error) {
var body bytes.Buffer var body bytes.Buffer
writer := multipart.NewWriter(&body) writer := multipart.NewWriter(&body)
zipWriter := gzip.NewWriter(&body) zipWriter := gzip.NewWriter(&body)
@@ -94,29 +107,33 @@ func subnetUploadReq(info interface{}, url string, filename string) (*http.Reque
}{Version: version} }{Version: version}
if e := enc.Encode(header); e != nil { if e := enc.Encode(header); e != nil {
return nil, e return nil, "", e
} }
if e := enc.Encode(info); e != nil { if e := enc.Encode(info); e != nil {
return nil, e return nil, "", e
} }
zipWriter.Close() zipWriter.Close()
temp := body temp := body
part, e := writer.CreateFormFile("file", filename) part, e := writer.CreateFormFile("file", filename)
if e != nil { if e != nil {
return nil, e return nil, "", e
} }
if _, e = io.Copy(part, &temp); e != nil { if _, e = io.Copy(part, &temp); e != nil {
return nil, e return nil, "", e
} }
writer.Close() writer.Close()
return body.Bytes(), writer.FormDataContentType(), nil
}
r, e := http.NewRequest(http.MethodPost, url, &body) func subnetUploadReq(body []byte, url string, formDataType string) (*http.Request, error) {
uploadDataBody := bytes.NewReader(body)
r, e := http.NewRequest(http.MethodPost, url, uploadDataBody)
if e != nil { if e != nil {
return nil, e return nil, e
} }
r.Header.Add("Content-Type", writer.FormDataContentType()) r.Header.Add("Content-Type", formDataType)
return r, nil return r, nil
} }
@@ -226,3 +243,93 @@ func getDriveSpaceInfo(admInfo madmin.InfoMessage) (uint64, uint64) {
} }
return total, used return total, used
} }
func GetSubnetAPIKeyUsingLicense(lic string) (string, error) {
return getSubnetAPIKeyUsingAuthHeaders(subnetLicenseAuthHeaders(lic))
}
func getSubnetAPIKeyUsingAuthHeaders(authHeaders map[string]string) (string, error) {
resp, e := subnetGetReqMC(subnetAPIKeyURL(), authHeaders)
if e != nil {
return "", e
}
return extractSubnetCred("api_key", gjson.Parse(resp))
}
func extractSubnetCred(key string, resp gjson.Result) (string, error) {
result := resp.Get(key)
if result.Index == 0 {
return "", fmt.Errorf("Couldn't extract %s from SUBNET response: %s", key, resp)
}
return result.String(), nil
}
func subnetLicenseAuthHeaders(lic string) map[string]string {
return map[string]string{"x-subnet-license": lic}
}
func subnetGetReqMC(reqURL string, headers map[string]string) (string, error) {
r, e := http.NewRequest(http.MethodGet, reqURL, nil)
if e != nil {
return "", e
}
return subnetReqDoMC(r, headers)
}
func subnetReqDoMC(r *http.Request, headers map[string]string) (string, error) {
for k, v := range headers {
r.Header.Add(k, v)
}
ct := r.Header.Get("Content-Type")
if len(ct) == 0 {
r.Header.Add("Content-Type", "application/json")
}
resp, e := subnetHTTPDo(r)
if e != nil {
return "", e
}
defer resp.Body.Close()
respBytes, e := io.ReadAll(io.LimitReader(resp.Body, subnetRespBodyLimit))
if e != nil {
return "", e
}
respStr := string(respBytes)
if resp.StatusCode == http.StatusOK {
return respStr, nil
}
return respStr, fmt.Errorf("Request failed with code %d with error: %s", resp.StatusCode, respStr)
}
func subnetHTTPDo(req *http.Request) (*http.Response, error) {
return getSubnetClient().Do(req)
}
func getSubnetClient() *http.Client {
client := httpClientSubnet(0)
return client
}
func httpClientSubnet(reqTimeout time.Duration) *http.Client {
return &http.Client{
Timeout: reqTimeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
Proxy: ieproxy.GetProxyFunc(),
TLSClientConfig: &tls.Config{
// Can't use SSLv3 because of POODLE and BEAST
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher
// Can't use TLSv1.1 because of RC4 cipher usage
MinVersion: tls.VersionTLS12,
},
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 10 * time.Second,
},
}
}

View File

@@ -362,7 +362,9 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
"Cluster Health Report will be uploaded to Subnet, and is viewable from your Subnet Diagnostics dashboard." "Cluster Health Report will be uploaded to Subnet, and is viewable from your Subnet Diagnostics dashboard."
} }
iconComponent={<InfoIcon />} iconComponent={<InfoIcon />}
help={""} help={
"If the Health report cannot be generated at this time, please wait a moment and try again."
}
/> />
</Fragment> </Fragment>
)} )}

View File

@@ -68,7 +68,6 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
return err return err
} }
encodedDiag := b64.StdEncoding.EncodeToString(compressedDiag) encodedDiag := b64.StdEncoding.EncodeToString(compressedDiag)
type messageReport struct { type messageReport struct {
Encoded string `json:"encoded"` Encoded string `json:"encoded"`
ServerHealthInfo interface{} `json:"serverHealthInfo"` ServerHealthInfo interface{} `json:"serverHealthInfo"`
@@ -76,7 +75,6 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
} }
ctx = context.WithValue(ctx, utils.ContextClientIP, conn.remoteAddress()) ctx = context.WithValue(ctx, utils.ContextClientIP, conn.remoteAddress())
subnetResp, err := sendHealthInfoToSubnet(ctx, healthInfo, client) subnetResp, err := sendHealthInfoToSubnet(ctx, healthInfo, client)
report := messageReport{ report := messageReport{
Encoded: encodedDiag, Encoded: encodedDiag,
@@ -142,9 +140,22 @@ func sendHealthInfoToSubnet(ctx context.Context, healthInfo interface{}, client
if e != nil { if e != nil {
return "", e return "", e
} }
apiKey := subnetTokenConfig.APIKey var apiKey string
if len(subnetTokenConfig.APIKey) != 0 {
apiKey = subnetTokenConfig.APIKey
} else {
apiKey, e = subnet.GetSubnetAPIKeyUsingLicense(subnetTokenConfig.License)
if e != nil {
return "", e
}
}
headers := subnet.UploadAuthHeaders(apiKey) headers := subnet.UploadAuthHeaders(apiKey)
resp, e := subnet.UploadFileToSubnet(healthInfo, subnetHTTPClient, filename, subnetUploadURL, headers) uploadInfo, formDataType, e := subnet.ProcessUploadInfo(healthInfo, "health", filename)
if e != nil {
return "", e
}
resp, e := subnet.UploadFileToSubnet(uploadInfo, subnetHTTPClient, subnetUploadURL, headers, formDataType)
if e != nil { if e != nil {
return "", e return "", e
} }

View File

@@ -32,7 +32,6 @@ import (
"github.com/minio/console/models" "github.com/minio/console/models"
"github.com/minio/madmin-go/v3" "github.com/minio/madmin-go/v3"
mcCmd "github.com/minio/mc/cmd"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
iampolicy "github.com/minio/pkg/iam/policy" iampolicy "github.com/minio/pkg/iam/policy"
) )
@@ -388,43 +387,29 @@ func (ac AdminClient) getBucketQuota(ctx context.Context, bucket string) (madmin
// serverHealthInfo implements mc.ServerHealthInfo - Connect to a minio server and call Health Info Management API // serverHealthInfo implements mc.ServerHealthInfo - Connect to a minio server and call Health Info Management API
func (ac AdminClient) serverHealthInfo(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) (interface{}, string, error) { func (ac AdminClient) serverHealthInfo(ctx context.Context, healthDataTypes []madmin.HealthDataType, deadline time.Duration) (interface{}, string, error) {
resp, version, err := ac.Client.ServerHealthInfo(ctx, healthDataTypes, deadline) info := madmin.HealthInfo{}
if err != nil {
return nil, version, err
}
var healthInfo interface{} var healthInfo interface{}
var version string
decoder := json.NewDecoder(resp.Body) var tryCount int
switch version { for info.Version == "" && tryCount < 10 {
case madmin.HealthInfoVersion0: resp, version, err := ac.Client.ServerHealthInfo(ctx, healthDataTypes, deadline)
info := madmin.HealthInfoV0{}
for {
if err = decoder.Decode(&info); err != nil {
break
}
}
// Old minio versions don't return the MinIO info in
// response of the healthinfo api. So fetch it separately
minioInfo, err := ac.Client.ServerInfo(ctx)
if err != nil { if err != nil {
info.Minio.Error = err.Error() return nil, version, err
} else {
info.Minio.Info = minioInfo
} }
decoder := json.NewDecoder(resp.Body)
healthInfo = mcCmd.MapHealthInfoToV1(info, nil)
version = madmin.HealthInfoVersion1
case madmin.HealthInfoVersion:
info := madmin.HealthInfo{}
for { for {
if err = decoder.Decode(&info); err != nil { if err = decoder.Decode(&info); err != nil {
break break
} }
} }
healthInfo = info tryCount++
time.Sleep(2 * time.Second)
} }
if info.Version == "" {
return nil, "", ErrHealthReportFail
}
healthInfo = info
return healthInfo, version, nil return healthInfo, version, nil
} }

View File

@@ -72,6 +72,7 @@ var (
ErrPolicyNotFound = errors.New("policy does not exist") ErrPolicyNotFound = errors.New("policy does not exist")
ErrLoginNotAllowed = errors.New("login not allowed") ErrLoginNotAllowed = errors.New("login not allowed")
ErrSubnetUploadFail = errors.New("Subnet upload failed") ErrSubnetUploadFail = errors.New("Subnet upload failed")
ErrHealthReportFail = errors.New("failure to generate Health report")
) )
// ErrorWithContext : // ErrorWithContext :