1
0
mirror of https://github.com/minio/minio synced 2024-07-08 19:56:05 +00:00

Send health diagnostics data as part of callhome (#16006)

This commit is contained in:
Shireesh Anjal 2022-11-16 03:23:05 +05:30 committed by GitHub
parent 8a07000e58
commit 5246e3be84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 195 additions and 102 deletions

View File

@ -1911,67 +1911,7 @@ func getKubernetesInfo(dctx context.Context) madmin.KubernetesInfo {
return ki return ki
} }
// HealthInfoHandler - GET /minio/admin/v3/healthinfo func fetchHealthInfo(healthCtx context.Context, objectAPI ObjectLayer, query *url.Values, healthInfoCh chan madmin.HealthInfo, healthInfo madmin.HealthInfo) {
// ----------
// Get server health info
func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "HealthInfo")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
if objectAPI == nil {
return
}
query := r.Form
healthInfo := madmin.HealthInfo{
Version: madmin.HealthInfoVersion,
Minio: madmin.MinioHealthInfo{
Info: madmin.MinioInfo{
DeploymentID: globalDeploymentID,
},
},
}
healthInfoCh := make(chan madmin.HealthInfo)
enc := json.NewEncoder(w)
setCommonHeaders(w)
setEventStreamHeaders(w)
w.WriteHeader(http.StatusOK)
errResp := func(err error) {
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
encodedErrorResponse := encodeResponse(errorResponse)
healthInfo.Error = string(encodedErrorResponse)
logger.LogIf(ctx, enc.Encode(healthInfo))
}
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
if dstr := r.Form.Get("deadline"); dstr != "" {
var err error
deadline, err = time.ParseDuration(dstr)
if err != nil {
errResp(err)
return
}
}
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
if err != nil { // returns a locked lock
errResp(err)
return
}
defer nsLock.Unlock(lkctx.Cancel)
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
defer healthCancel()
hostAnonymizer := createHostAnonymizer() hostAnonymizer := createHostAnonymizer()
// anonAddr - Anonymizes hosts in given input string. // anonAddr - Anonymizes hosts in given input string.
anonAddr := func(addr string) string { anonAddr := func(addr string) string {
@ -2196,7 +2136,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
getAndWriteMinioConfig := func() { getAndWriteMinioConfig := func() {
if query.Get("minioconfig") == "true" { if query.Get("minioconfig") == "true" {
config, err := readServerConfig(ctx, objectAPI) config, err := readServerConfig(healthCtx, objectAPI)
if err != nil { if err != nil {
healthInfo.Minio.Config = madmin.MinioConfig{ healthInfo.Minio.Config = madmin.MinioConfig{
Error: err.Error(), Error: err.Error(),
@ -2244,7 +2184,7 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
getAndWriteSysConfig() getAndWriteSysConfig()
if query.Get("minioinfo") == "true" { if query.Get("minioinfo") == "true" {
infoMessage := getServerInfo(ctx, r) infoMessage := getServerInfo(healthCtx, nil)
servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers)) servers := make([]madmin.ServerInfo, 0, len(infoMessage.Servers))
for _, server := range infoMessage.Servers { for _, server := range infoMessage.Servers {
anonEndpoint := anonAddr(server.Endpoint) anonEndpoint := anonAddr(server.Endpoint)
@ -2294,6 +2234,67 @@ func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Reque
partialWrite(healthInfo) partialWrite(healthInfo)
} }
}() }()
}
// HealthInfoHandler - GET /minio/admin/v3/healthinfo
// ----------
// Get server health info
func (a adminAPIHandlers) HealthInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "HealthInfo")
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.HealthInfoAdminAction)
if objectAPI == nil {
return
}
query := r.Form
healthInfoCh := make(chan madmin.HealthInfo)
enc := json.NewEncoder(w)
healthInfo := madmin.HealthInfo{
Version: madmin.HealthInfoVersion,
Minio: madmin.MinioHealthInfo{
Info: madmin.MinioInfo{
DeploymentID: globalDeploymentID,
},
},
}
errResp := func(err error) {
errorResponse := getAPIErrorResponse(ctx, toAdminAPIErr(ctx, err), r.URL.String(),
w.Header().Get(xhttp.AmzRequestID), globalDeploymentID)
encodedErrorResponse := encodeResponse(errorResponse)
healthInfo.Error = string(encodedErrorResponse)
logger.LogIf(ctx, enc.Encode(healthInfo))
}
deadline := 10 * time.Second // Default deadline is 10secs for health diagnostics.
if dstr := query.Get("deadline"); dstr != "" {
var err error
deadline, err = time.ParseDuration(dstr)
if err != nil {
errResp(err)
return
}
}
nsLock := objectAPI.NewNSLock(minioMetaBucket, "health-check-in-progress")
lkctx, err := nsLock.GetLock(ctx, newDynamicTimeout(deadline, deadline))
if err != nil { // returns a locked lock
errResp(err)
return
}
defer nsLock.Unlock(lkctx.Cancel)
healthCtx, healthCancel := context.WithTimeout(lkctx.Context(), deadline)
defer healthCancel()
go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
setCommonHeaders(w)
setEventStreamHeaders(w)
w.WriteHeader(http.StatusOK)
ticker := time.NewTicker(5 * time.Second) ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop() defer ticker.Stop()

View File

@ -18,29 +18,20 @@
package cmd package cmd
import ( import (
"bytes"
"compress/gzip"
"context" "context"
"encoding/json"
"errors"
"fmt" "fmt"
"math/rand" "math/rand"
"net/url"
"time" "time"
"github.com/minio/madmin-go" "github.com/minio/madmin-go"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
) )
const (
// callhomeSchemaVersion1 is callhome schema version 1
callhomeSchemaVersion1 = "1"
// callhomeSchemaVersion is current callhome schema version.
callhomeSchemaVersion = callhomeSchemaVersion1
)
// CallhomeInfo - Contains callhome information
type CallhomeInfo struct {
SchemaVersion string `json:"schema_version"`
AdminInfo madmin.InfoMessage `json:"admin_info"`
}
var callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second) var callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second)
// initCallhome will start the callhome task in the background. // initCallhome will start the callhome task in the background.
@ -118,26 +109,91 @@ func runCallhome(ctx context.Context, objAPI ObjectLayer) bool {
} }
func performCallhome(ctx context.Context) { func performCallhome(ctx context.Context) {
err := sendCallhomeInfo( deadline := 10 * time.Second // Default deadline is 10secs for callhome
CallhomeInfo{ objectAPI := newObjectLayerFn()
SchemaVersion: callhomeSchemaVersion, if objectAPI == nil {
AdminInfo: getServerInfo(ctx, nil), logger.LogIf(ctx, errors.New("Callhome: object layer not ready"))
}) return
if err != nil { }
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
healthCtx, healthCancel := context.WithTimeout(ctx, deadline)
defer healthCancel()
healthInfoCh := make(chan madmin.HealthInfo)
query := url.Values{}
for _, k := range madmin.HealthDataTypesList {
query.Set(string(k), "true")
}
healthInfo := madmin.HealthInfo{
Version: madmin.HealthInfoVersion,
Minio: madmin.MinioHealthInfo{
Info: madmin.MinioInfo{
DeploymentID: globalDeploymentID,
},
},
}
go fetchHealthInfo(healthCtx, objectAPI, &query, healthInfoCh, healthInfo)
for {
select {
case hi, hasMore := <-healthInfoCh:
if !hasMore {
// Received all data. Send to SUBNET and return
err := sendHealthInfo(ctx, healthInfo)
if err != nil {
logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err))
}
return
}
healthInfo = hi
case <-healthCtx.Done():
return
}
} }
} }
const ( const (
callhomeURL = "https://subnet.min.io/api/callhome" healthURL = "https://subnet.min.io/api/health/upload"
callhomeURLDev = "http://localhost:9000/api/callhome" healthURLDev = "http://localhost:9000/api/health/upload"
) )
func sendCallhomeInfo(ch CallhomeInfo) error { func sendHealthInfo(ctx context.Context, healthInfo madmin.HealthInfo) error {
url := callhomeURL url := healthURL
if globalIsCICD { if globalIsCICD {
url = callhomeURLDev url = healthURLDev
} }
_, err := globalSubnetConfig.Post(url, ch)
filename := fmt.Sprintf("health_%s.json.gz", UTCNow().Format("20060102150405"))
url += "?filename=" + filename
_, err := globalSubnetConfig.Upload(url, filename, createHealthJSONGzip(ctx, healthInfo))
return err return err
} }
func createHealthJSONGzip(ctx context.Context, healthInfo madmin.HealthInfo) []byte {
var b bytes.Buffer
gzWriter := gzip.NewWriter(&b)
header := struct {
Version string `json:"version"`
}{Version: healthInfo.Version}
enc := json.NewEncoder(gzWriter)
if e := enc.Encode(header); e != nil {
logger.LogIf(ctx, fmt.Errorf("Could not encode health info header: %w", e))
return nil
}
if e := enc.Encode(healthInfo); e != nil {
logger.LogIf(ctx, fmt.Errorf("Could not encode health info: %w", e))
return nil
}
gzWriter.Flush()
gzWriter.Close()
return b.Bytes()
}

View File

@ -23,6 +23,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"mime/multipart"
"net/http" "net/http"
"time" "time"
@ -33,25 +34,38 @@ const (
respBodyLimit = 1 << 20 // 1 MiB respBodyLimit = 1 << 20 // 1 MiB
) )
// Post submit 'payload' to specified URL // Upload given file content (payload) to specified URL
func (c Config) Post(reqURL string, payload interface{}) (string, error) { func (c Config) Upload(reqURL string, filename string, payload []byte) (string, error) {
if !c.Registered() { if !c.Registered() {
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'") return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
} }
body, err := json.Marshal(payload)
if err != nil { var body bytes.Buffer
return "", err writer := multipart.NewWriter(&body)
} part, e := writer.CreateFormFile("file", filename)
r, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body)) if e != nil {
if err != nil { return "", e
return "", err
} }
if _, e = part.Write(payload); e != nil {
return "", e
}
writer.Close()
r, e := http.NewRequest(http.MethodPost, reqURL, &body)
if e != nil {
return "", e
}
r.Header.Add("Content-Type", writer.FormDataContentType())
return c.submitPost(r)
}
func (c Config) submitPost(r *http.Request) (string, error) {
configLock.RLock() configLock.RLock()
r.Header.Set("Authorization", c.APIKey) r.Header.Set(xhttp.SubnetAPIKey, c.APIKey)
configLock.RUnlock() configLock.RUnlock()
r.Header.Set(xhttp.MinioDeploymentID, xhttp.GlobalDeploymentID)
r.Header.Set("Content-Type", "application/json")
client := &http.Client{ client := &http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
@ -76,3 +90,22 @@ func (c Config) Post(reqURL string, payload interface{}) (string, error) {
return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr) return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr)
} }
// Post submit 'payload' to specified URL
func (c Config) Post(reqURL string, payload interface{}) (string, error) {
if !c.Registered() {
return "", errors.New("Deployment is not registered with SUBNET. Please register the deployment via 'mc license register ALIAS'")
}
body, err := json.Marshal(payload)
if err != nil {
return "", err
}
r, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body))
if err != nil {
return "", err
}
r.Header.Set("Content-Type", "application/json")
return c.submitPost(r)
}

View File

@ -210,6 +210,9 @@ const (
// MinIOCompressed is returned when object is compressed // MinIOCompressed is returned when object is compressed
MinIOCompressed = "X-Minio-Compressed" MinIOCompressed = "X-Minio-Compressed"
// SUBNET related
SubnetAPIKey = "x-subnet-api-key"
) )
// Common http query params S3 API // Common http query params S3 API