1
0
mirror of https://github.com/minio/mc.git synced 2025-12-08 22:28:28 +03:00

Expose health diagnostics related functions (#4885)

So that dependent projects (e.g. console) can make use of them instead
of duplicating the logic.
This commit is contained in:
Shireesh Anjal
2024-03-30 20:58:09 +05:30
committed by GitHub
parent 89d742a76d
commit 206d017878
11 changed files with 257 additions and 201 deletions

View File

@@ -61,16 +61,20 @@ const (
)
var (
globalQuiet = false // Quiet flag set via command line
globalJSON = false // Json flag set via command line
globalJSONLine = false // Print json as single line.
globalDebug = false // Debug flag set via command line
globalNoColor = false // No Color flag set via command line
globalInsecure = false // Insecure flag set via command line
globalDevMode = false // dev flag set via command line
globalAirgapped = false // Airgapped flag set via command line
globalSubnetProxyURL *url.URL // Proxy to be used for communication with subnet
globalSubnetConfig []madmin.SubsysConfig // Subnet config
globalQuiet = false // Quiet flag set via command line
globalJSON = false // Json flag set via command line
globalJSONLine = false // Print json as single line.
globalDebug = false // Debug flag set via command line
globalNoColor = false // No Color flag set via command line
globalInsecure = false // Insecure flag set via command line
globalAirgapped = false // Airgapped flag set via command line
globalSubnetConfig []madmin.SubsysConfig // Subnet config
// GlobalDevMode is set to true if the program is running in development mode
GlobalDevMode = false
// GlobalSubnetProxyURL is the proxy to be used for communication with subnet
GlobalSubnetProxyURL *url.URL
globalConnReadDeadline time.Duration
globalConnWriteDeadline time.Duration
@@ -117,7 +121,7 @@ func setGlobalsFromContext(ctx *cli.Context) error {
globalJSON = globalJSON || json
globalNoColor = globalNoColor || noColor || globalJSONLine
globalInsecure = globalInsecure || insecure
globalDevMode = globalDevMode || devMode
GlobalDevMode = GlobalDevMode || devMode
globalAirgapped = globalAirgapped || airgapped
// Disable colorified messages if requested.

View File

@@ -106,9 +106,9 @@ func performLicenseRenew(alias string) licUpdateMessage {
}
renewURL := subnetLicenseRenewURL()
headers := subnetAPIKeyAuthHeaders(apiKey)
headers := SubnetAPIKeyAuthHeaders(apiKey)
headers.addDeploymentIDHeader(alias)
resp, e := subnetPostReq(renewURL, nil, headers)
resp, e := SubnetPostReq(renewURL, nil, headers)
fatalIf(probe.NewError(e), "Error renewing license for %s", alias)
extractAndSaveSubnetCreds(alias, resp)

132
cmd/subnet-file-uploader.go Normal file
View File

@@ -0,0 +1,132 @@
// Copyright (c) 2015-2024 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package cmd
import (
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/klauspost/compress/zstd"
)
// SubnetFileUploader - struct to upload files to SUBNET
type SubnetFileUploader struct {
alias string // used for saving api-key and license from response
filename string // filename passed in the SUBNET request
FilePath string // file to upload
ReqURL string // SUBNET upload URL
Params url.Values // query params to be sent in the request
Headers SubnetHeaders // headers to be sent in the request
AutoCompress bool // whether to compress (zst) the file before uploading
DeleteAfterUpload bool // whether to delete the file after successful upload
}
// UploadFileToSubnet - uploads the file to SUBNET
func (i *SubnetFileUploader) UploadFileToSubnet() (string, error) {
req, e := i.subnetUploadReq()
if e != nil {
return "", e
}
resp, e := subnetReqDo(req, i.Headers)
if e != nil {
return "", e
}
if i.DeleteAfterUpload {
os.Remove(i.FilePath)
}
// ensure that both api-key and license from
// SUBNET response are saved in the config
if len(i.alias) > 0 {
extractAndSaveSubnetCreds(i.alias, resp)
}
return resp, nil
}
func (i *SubnetFileUploader) updateParams() {
if i.Params == nil {
i.Params = url.Values{}
}
if i.filename == "" {
i.filename = filepath.Base(i.FilePath)
}
i.AutoCompress = i.AutoCompress && !strings.HasSuffix(strings.ToLower(i.FilePath), ".zst")
if i.AutoCompress {
i.filename += ".zst"
i.Params.Add("auto-compression", "zstd")
}
i.Params.Add("filename", i.filename)
i.ReqURL += "?" + i.Params.Encode()
}
func (i *SubnetFileUploader) subnetUploadReq() (*http.Request, error) {
i.updateParams()
r, w := io.Pipe()
mwriter := multipart.NewWriter(w)
contentType := mwriter.FormDataContentType()
go func() {
var (
part io.Writer
e error
)
defer func() {
mwriter.Close()
w.CloseWithError(e)
}()
part, e = mwriter.CreateFormFile("file", i.filename)
if e != nil {
return
}
file, e := os.Open(i.FilePath)
if e != nil {
return
}
defer file.Close()
if i.AutoCompress {
z, _ := zstd.NewWriter(part, zstd.WithEncoderConcurrency(2))
defer z.Close()
_, e = z.ReadFrom(file)
} else {
_, e = io.Copy(part, file)
}
}()
req, e := http.NewRequest(http.MethodPost, i.ReqURL, r)
if e != nil {
return nil, e
}
req.Header.Add("Content-Type", contentType)
return req, nil
}

View File

@@ -26,17 +26,14 @@ import (
"fmt"
"io"
"math"
"mime/multipart"
"net/http"
"net/http/httputil"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"github.com/klauspost/compress/zstd"
"github.com/minio/cli"
"github.com/minio/madmin-go/v3"
"github.com/minio/mc/pkg/probe"
@@ -59,48 +56,51 @@ var subnetCommonFlags = append(supportGlobalFlags, cli.StringFlag{
EnvVar: "_MC_SUBNET_API_KEY",
})
func subnetBaseURL() string {
return subnet.BaseURL(globalDevMode)
// SubnetBaseURL - returns the base URL of SUBNET
func SubnetBaseURL() string {
return subnet.BaseURL(GlobalDevMode)
}
func subnetIssueURL(issueNum int) string {
return fmt.Sprintf("%s/issues/%d", subnetBaseURL(), issueNum)
return fmt.Sprintf("%s/issues/%d", SubnetBaseURL(), issueNum)
}
func subnetLogWebhookURL() string {
return subnetBaseURL() + "/api/logs"
return SubnetBaseURL() + "/api/logs"
}
func subnetUploadURL(uploadType string) string {
return fmt.Sprintf("%s/api/%s/upload", subnetBaseURL(), uploadType)
// SubnetUploadURL - returns the upload URL for the given upload type
func SubnetUploadURL(uploadType string) string {
return fmt.Sprintf("%s/api/%s/upload", SubnetBaseURL(), uploadType)
}
func subnetRegisterURL() string {
return subnetBaseURL() + "/api/cluster/register"
// SubnetRegisterURL - returns the cluster registration URL
func SubnetRegisterURL() string {
return SubnetBaseURL() + "/api/cluster/register"
}
func subnetUnregisterURL(depID string) string {
return subnetBaseURL() + "/api/cluster/unregister?deploymentId=" + depID
return SubnetBaseURL() + "/api/cluster/unregister?deploymentId=" + depID
}
func subnetLicenseRenewURL() string {
return subnetBaseURL() + "/api/cluster/renew-license"
return SubnetBaseURL() + "/api/cluster/renew-license"
}
func subnetOfflineRegisterURL(regToken string) string {
return subnetBaseURL() + "/cluster/register?token=" + regToken
return SubnetBaseURL() + "/cluster/register?token=" + regToken
}
func subnetLoginURL() string {
return subnetBaseURL() + "/api/auth/login"
return SubnetBaseURL() + "/api/auth/login"
}
func subnetAPIKeyURL() string {
return subnetBaseURL() + "/api/auth/api-key"
return SubnetBaseURL() + "/api/auth/api-key"
}
func subnetMFAURL() string {
return subnetBaseURL() + "/api/auth/mfa-login"
return SubnetBaseURL() + "/api/auth/mfa-login"
}
func checkURLReachable(url string) *probe.Error {
@@ -124,12 +124,13 @@ func subnetURLWithAuth(reqURL, apiKey string) (string, map[string]string, error)
return "", nil, e
}
}
return reqURL, subnetAPIKeyAuthHeaders(apiKey), nil
return reqURL, SubnetAPIKeyAuthHeaders(apiKey), nil
}
type subnetHeaders map[string]string
// SubnetHeaders - type for SUBNET request headers
type SubnetHeaders map[string]string
func (h subnetHeaders) addDeploymentIDHeader(alias string) {
func (h SubnetHeaders) addDeploymentIDHeader(alias string) {
h[minioDeploymentIDHeader] = getAdminInfo(alias).DeploymentID
}
@@ -137,18 +138,20 @@ func subnetTokenAuthHeaders(authToken string) map[string]string {
return map[string]string{"Authorization": "Bearer " + authToken}
}
func subnetLicenseAuthHeaders(lic string) map[string]string {
// SubnetLicenseAuthHeaders - returns the headers for SUBNET license authentication
func SubnetLicenseAuthHeaders(lic string) map[string]string {
return map[string]string{"x-subnet-license": lic}
}
func subnetAPIKeyAuthHeaders(apiKey string) subnetHeaders {
// SubnetAPIKeyAuthHeaders - returns the headers for SUBNET API key authentication
func SubnetAPIKeyAuthHeaders(apiKey string) SubnetHeaders {
return map[string]string{"x-subnet-api-key": apiKey}
}
func getSubnetClient() *http.Client {
client := httpClient(0)
if globalSubnetProxyURL != nil {
client.Transport.(*http.Transport).Proxy = http.ProxyURL(globalSubnetProxyURL)
if GlobalSubnetProxyURL != nil {
client.Transport.(*http.Transport).Proxy = http.ProxyURL(GlobalSubnetProxyURL)
}
return client
}
@@ -256,7 +259,8 @@ func subnetGetReq(reqURL string, headers map[string]string) (string, error) {
return subnetReqDo(r, headers)
}
func subnetPostReq(reqURL string, payload interface{}, headers map[string]string) (string, error) {
// SubnetPostReq - makes a POST request to SUBNET
func SubnetPostReq(reqURL string, payload interface{}, headers map[string]string) (string, error) {
body, e := json.Marshal(payload)
if e != nil {
return "", e
@@ -318,7 +322,7 @@ func getSubnetAPIKeyFromConfig(alias string) string {
}
func setGlobalSubnetProxyFromConfig(alias string) error {
if globalSubnetProxyURL != nil {
if GlobalSubnetProxyURL != nil {
// proxy already set
return nil
}
@@ -341,7 +345,7 @@ func setGlobalSubnetProxyFromConfig(alias string) error {
if e != nil {
return e
}
globalSubnetProxyURL = proxyURL
GlobalSubnetProxyURL = proxyURL
}
return nil
}
@@ -507,7 +511,7 @@ func subnetLogin() (string, error) {
"username": username,
"password": string(bytepw),
}
respStr, e := subnetPostReq(subnetLoginURL(), loginReq, nil)
respStr, e := SubnetPostReq(subnetLoginURL(), loginReq, nil)
if e != nil {
return "", e
}
@@ -520,7 +524,7 @@ func subnetLogin() (string, error) {
fmt.Println()
mfaLoginReq := SubnetMFAReq{Username: username, OTP: string(byteotp), Token: mfaToken}
respStr, e = subnetPostReq(subnetMFAURL(), mfaLoginReq, nil)
respStr, e = SubnetPostReq(subnetMFAURL(), mfaLoginReq, nil)
if e != nil {
return "", e
}
@@ -581,7 +585,7 @@ func getSubnetAPIKey(alias string) (string, error) {
}
func getSubnetAPIKeyUsingLicense(lic string) (string, error) {
return getSubnetAPIKeyUsingAuthHeaders(subnetLicenseAuthHeaders(lic))
return getSubnetAPIKeyUsingAuthHeaders(SubnetLicenseAuthHeaders(lic))
}
func getSubnetAPIKeyUsingAuthToken(authToken string) (string, error) {
@@ -605,7 +609,7 @@ func getSubnetLicenseUsingAPIKey(alias, apiKey string) (string, error) {
// registerClusterOnSubnet - Registers the given cluster on SUBNET using given API key for auth
// If the API key is empty, user will be asked to log in using SUBNET credentials.
func registerClusterOnSubnet(clusterRegInfo ClusterRegistrationInfo, alias, apiKey string) (string, string, error) {
regURL, headers, e := subnetURLWithAuth(subnetRegisterURL(), apiKey)
regURL, headers, e := subnetURLWithAuth(SubnetRegisterURL(), apiKey)
if e != nil {
return "", "", e
}
@@ -616,7 +620,7 @@ func registerClusterOnSubnet(clusterRegInfo ClusterRegistrationInfo, alias, apiK
}
reqPayload := ClusterRegistrationReq{Token: regToken}
resp, e := subnetPostReq(regURL, reqPayload, headers)
resp, e := SubnetPostReq(regURL, reqPayload, headers)
if e != nil {
return "", "", e
}
@@ -636,7 +640,7 @@ func unregisterClusterFromSubnet(depID, apiKey string) error {
return e
}
_, e = subnetPostReq(regURL, nil, headers)
_, e = SubnetPostReq(regURL, nil, headers)
return e
}
@@ -704,7 +708,7 @@ func parseLicense(license string) (*licverifier.LicenseInfo, error) {
Client: *client,
ExpiryGracePeriod: 0,
}
lv.Init(globalDevMode)
lv.Init(GlobalDevMode)
return lv.ParseLicense(license)
}
@@ -722,104 +726,6 @@ func prepareSubnetUploadURL(uploadURL, alias, apiKey string) (string, map[string
return reqURL, headers
}
type subnetFileUploader struct {
alias string
filePath string
filename string
reqURL string
headers subnetHeaders
autoCompress bool
deleteAfterUpload bool
params url.Values
}
func (i *subnetFileUploader) uploadFileToSubnet() (string, error) {
req, e := i.subnetUploadReq()
if e != nil {
return "", e
}
resp, e := subnetReqDo(req, i.headers)
if e != nil {
return "", e
}
if i.deleteAfterUpload {
os.Remove(i.filePath)
}
// ensure that both api-key and license from
// SUBNET response are saved in the config
extractAndSaveSubnetCreds(i.alias, resp)
return resp, e
}
func (i *subnetFileUploader) updateParams() {
if i.params == nil {
i.params = url.Values{}
}
if i.filename == "" {
i.filename = filepath.Base(i.filePath)
}
i.autoCompress = i.autoCompress && !strings.HasSuffix(strings.ToLower(i.filePath), ".zst")
if i.autoCompress {
i.filename += ".zst"
i.params.Add("auto-compression", "zstd")
}
i.params.Add("filename", i.filename)
i.reqURL += "?" + i.params.Encode()
}
func (i *subnetFileUploader) subnetUploadReq() (*http.Request, error) {
i.updateParams()
r, w := io.Pipe()
mwriter := multipart.NewWriter(w)
contentType := mwriter.FormDataContentType()
go func() {
var (
part io.Writer
e error
)
defer func() {
mwriter.Close()
w.CloseWithError(e)
}()
part, e = mwriter.CreateFormFile("file", i.filename)
if e != nil {
return
}
file, e := os.Open(i.filePath)
if e != nil {
return
}
defer file.Close()
if i.autoCompress {
z, _ := zstd.NewWriter(part, zstd.WithEncoderConcurrency(2))
defer z.Close()
_, e = z.ReadFrom(file)
} else {
_, e = io.Copy(part, file)
}
}()
req, e := http.NewRequest(http.MethodPost, i.reqURL, r)
if e != nil {
return nil, e
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
func getAPIKeyFlag(ctx *cli.Context) (string, error) {
apiKey := ctx.String("api-key")
@@ -850,7 +756,7 @@ func initSubnetConnectivity(ctx *cli.Context, aliasedURL string, failOnConnErr b
e = setGlobalSubnetProxyFromConfig(alias)
fatalIf(probe.NewError(e), "Error in setting SUBNET proxy:")
sbu := subnetBaseURL()
sbu := SubnetBaseURL()
err := checkURLReachable(sbu)
if err != nil && failOnConnErr {
fatal(err.Trace(aliasedURL), "Unable to reach %s, please use --airgap if there is no connectivity to SUBNET", sbu)

View File

@@ -23,7 +23,7 @@ import (
)
func TestSubnetBaseURL(t *testing.T) {
sbu := subnetBaseURL()
sbu := SubnetBaseURL()
u, err := url.ParseRequestURI(sbu)
if err != nil {
t.Fatal(err)

View File

@@ -18,6 +18,7 @@
package cmd
import (
"bytes"
"context"
gojson "encoding/json"
"errors"
@@ -123,26 +124,13 @@ func checkSupportDiagSyntax(ctx *cli.Context) {
// compress and tar MinIO diagnostics output
func tarGZ(healthInfo interface{}, version, filename string) error {
f, e := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, 0o666)
data, e := TarGZHealthInfo(healthInfo, version)
if e != nil {
return e
}
defer f.Close()
gzWriter := gzip.NewWriter(f)
defer gzWriter.Close()
enc := gojson.NewEncoder(gzWriter)
header := struct {
Version string `json:"version"`
}{Version: version}
if e := enc.Encode(header); e != nil {
return e
}
if e := enc.Encode(healthInfo); e != nil {
e = os.WriteFile(filename, data, 0o666)
if e != nil {
return e
}
@@ -161,6 +149,32 @@ func tarGZ(healthInfo interface{}, version, filename string) error {
return nil
}
// TarGZHealthInfo - compress and tar MinIO diagnostics output
func TarGZHealthInfo(healthInfo interface{}, version string) ([]byte, error) {
buffer := bytes.NewBuffer(nil)
gzWriter := gzip.NewWriter(buffer)
enc := gojson.NewEncoder(gzWriter)
header := struct {
Version string `json:"version"`
}{Version: version}
if e := enc.Encode(header); e != nil {
return nil, e
}
if e := enc.Encode(healthInfo); e != nil {
return nil, e
}
if e := gzWriter.Close(); e != nil {
return nil, e
}
return buffer.Bytes(), nil
}
func infoText(s string) string {
console.SetColor("INFO", color.New(color.FgGreen, color.Bold))
return console.Colorize("INFO", s)
@@ -205,7 +219,7 @@ func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias, apiKey
if !globalAirgapped {
// Retrieve subnet credentials (login/license) beforehand as
// it can take a long time to fetch the health information
uploadURL := subnetUploadURL("health")
uploadURL := SubnetUploadURL("health")
reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, apiKey)
}
@@ -228,13 +242,13 @@ func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias, apiKey
fatalIf(probe.NewError(e), "Unable to save MinIO diagnostics report")
if !globalAirgapped {
_, e := (&subnetFileUploader{
_, e = (&SubnetFileUploader{
alias: alias,
filePath: filename,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
FilePath: filename,
ReqURL: reqURL,
Headers: headers,
DeleteAfterUpload: true,
}).UploadFileToSubnet()
fatalIf(probe.NewError(e), "Unable to upload MinIO diagnostics report to SUBNET portal")
printMsg(supportDiagMessage{})

View File

@@ -196,18 +196,18 @@ func mainSupportInspect(ctx *cli.Context) error {
return nil
}
uploadURL := subnetUploadURL("inspect")
uploadURL := SubnetUploadURL("inspect")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
tmpFileName := tmpFile.Name()
_, e = (&subnetFileUploader{
_, e = (&SubnetFileUploader{
alias: alias,
filePath: tmpFileName,
FilePath: tmpFileName,
filename: inspectOutputFilename,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
ReqURL: reqURL,
Headers: headers,
DeleteAfterUpload: true,
}).UploadFileToSubnet()
if e != nil {
console.Errorln("Unable to upload inspect data to SUBNET portal: " + e.Error())
saveInspectDataFile(key, tmpFile)

View File

@@ -490,16 +490,16 @@ func execSupportPerf(ctx *cli.Context, aliasedURL, perfType string) {
return
}
uploadURL := subnetUploadURL("perf")
uploadURL := SubnetUploadURL("perf")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
_, e = (&subnetFileUploader{
_, e = (&SubnetFileUploader{
alias: alias,
filePath: tmpFileName,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
FilePath: tmpFileName,
ReqURL: reqURL,
Headers: headers,
DeleteAfterUpload: true,
}).UploadFileToSubnet()
if e != nil {
errorIf(probe.NewError(e), "Unable to upload performance results to SUBNET portal")
savePerfResultFile(tmpFileName, resultFileNamePfx)

View File

@@ -226,7 +226,7 @@ func execSupportProfile(ctx *cli.Context, client *madmin.AdminClient, alias, api
if !globalAirgapped {
// Retrieve subnet credentials (login/license) beforehand as
// it can take a long time to fetch the profile data
uploadURL := subnetUploadURL("profile")
uploadURL := SubnetUploadURL("profile")
reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, apiKey)
}
@@ -239,13 +239,13 @@ func execSupportProfile(ctx *cli.Context, client *madmin.AdminClient, alias, api
saveProfileFile(data)
if !globalAirgapped {
_, e = (&subnetFileUploader{
_, e = (&SubnetFileUploader{
alias: alias,
filePath: profileFile,
reqURL: reqURL,
headers: headers,
deleteAfterUpload: true,
}).uploadFileToSubnet()
FilePath: profileFile,
ReqURL: reqURL,
Headers: headers,
DeleteAfterUpload: true,
}).UploadFileToSubnet()
if e != nil {
printMsg(supportProfileMessage{
Status: "error",

View File

@@ -128,17 +128,17 @@ func execSupportUpload(ctx *cli.Context, alias, apiKey string) {
params.Add("message", msg)
}
uploadURL := subnetUploadURL("attachment")
uploadURL := SubnetUploadURL("attachment")
reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
_, e := (&subnetFileUploader{
_, e := (&SubnetFileUploader{
alias: alias,
filePath: filePath,
reqURL: reqURL,
headers: headers,
autoCompress: true,
params: params,
}).uploadFileToSubnet()
FilePath: filePath,
ReqURL: reqURL,
Headers: headers,
AutoCompress: true,
Params: params,
}).UploadFileToSubnet()
if e != nil {
fatalIf(probe.NewError(e), "Unable to upload file to SUBNET")
}

View File

@@ -113,9 +113,9 @@ func validateClusterRegistered(alias string, cmdTalksToSubnet bool) string {
// Non-registered execution allowed only in following scenarios
// command doesn't talk to subnet: dev mode (`--dev` passed)
// command talks to subnet: dev+airgapped mode (both `--dev` and `--airgap` passed)
requireRegistration := !globalDevMode
requireRegistration := !GlobalDevMode
if cmdTalksToSubnet {
requireRegistration = !(globalDevMode && globalAirgapped)
requireRegistration = !(GlobalDevMode && globalAirgapped)
}
apiKey, e := getSubnetAPIKey(alias)