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

Enhancements in registration and diagnostics (#4200)

- When the cluster is registered but either license or api-key
  are available in the config, and when `--airgap` is not passed 
  to the command, try to automatically fetch and store the other
  both.
- Add `--api-key` flag to `mc support diag`
- Use header-based auth with subnet
- Introduce a common function to initialize/check connectivity with subnet
- Make the health report upload function generic so that it can be used
  by other commands like profile in future
- Add a global variable for airgapped mode
- Add more examples to `mc license register`
This commit is contained in:
Shireesh Anjal
2022-08-23 11:15:20 +05:30
committed by GitHub
parent f70b7e537c
commit 07fffc3bc8
6 changed files with 344 additions and 333 deletions

View File

@@ -65,6 +65,7 @@ var (
globalInsecure = false // Insecure flag set via command line
globalDevMode = false // dev flag set via command line
globalSubnetProxyURL *url.URL // Proxy to be used for communication with subnet
globalAirgapped = false // Airgapped flag set via command line
globalConnReadDeadline time.Duration
globalConnWriteDeadline time.Duration
@@ -88,6 +89,7 @@ func setGlobalsFromContext(ctx *cli.Context) error {
noColor := ctx.IsSet("no-color") || ctx.GlobalIsSet("no-color")
insecure := ctx.IsSet("insecure") || ctx.GlobalIsSet("insecure")
devMode := ctx.IsSet("dev") || ctx.GlobalIsSet("dev")
airgapped := ctx.IsSet("airgap") || ctx.GlobalIsSet("airgap")
globalQuiet = globalQuiet || quiet
globalDebug = globalDebug || debug
@@ -96,6 +98,7 @@ func setGlobalsFromContext(ctx *cli.Context) error {
globalNoColor = globalNoColor || noColor || globalJSONLine
globalInsecure = globalInsecure || insecure
globalDevMode = globalDevMode || devMode
globalAirgapped = globalAirgapped || airgapped
// Disable colorified messages if requested.
if globalNoColor || globalQuiet {

View File

@@ -160,57 +160,56 @@ func mainLicenseInfo(ctx *cli.Context) error {
initLicInfoColors()
aliasedURL := ctx.Args().Get(0)
alias, _ := url2Alias(aliasedURL)
alias, _ := initSubnetConnectivity(ctx, aliasedURL)
apiKey, lic, e := getSubnetCreds(alias)
fatalIf(probe.NewError(e), "Error in checking cluster registration status")
if len(apiKey) == 0 && len(lic) == 0 {
// Not registered. Default to AGPLv3
printMsg(licInfoMessage{
Status: "success",
Info: licInfo{
Plan: "AGPLv3",
Message: getAGPLMessage(),
},
})
return nil
}
var ssm licInfoMessage
var lim licInfoMessage
if len(lic) > 0 {
// If set, the subnet public key will not be downloaded from subnet
// and the offline key embedded in mc will be used.
airgap := ctx.Bool("airgap")
li, e := parseLicense(lic, airgap)
if e != nil {
ssm = licInfoMessage{
Status: "error",
Error: e.Error(),
}
} else {
ssm = licInfoMessage{
Status: "success",
Info: licInfo{
Organization: li.Organization,
Plan: li.Plan,
IssuedAt: &li.IssuedAt,
ExpiresAt: &li.ExpiresAt,
DeploymentID: li.DeploymentID,
},
}
}
} else {
// Only api key is available, no license info
ssm = licInfoMessage{
lim = getLicInfoMsg(lic)
} else if len(apiKey) > 0 {
lim = licInfoMessage{
Status: "success",
Info: licInfo{
Message: fmt.Sprintf("%s is registered with SUBNET. License info not available.", alias),
},
}
} else {
// Not registered. Default to AGPLv3
lim = licInfoMessage{
Status: "success",
Info: licInfo{
Plan: "AGPLv3",
Message: getAGPLMessage(),
},
}
}
printMsg(ssm)
printMsg(lim)
return nil
}
func getLicInfoMsg(lic string) licInfoMessage {
li, e := parseLicense(lic)
if e != nil {
return licErrMsg(e)
}
return licInfoMessage{
Status: "success",
Info: licInfo{
Organization: li.Organization,
Plan: li.Plan,
IssuedAt: &li.IssuedAt,
ExpiresAt: &li.ExpiresAt,
DeploymentID: li.DeploymentID,
},
}
}
func licErrMsg(e error) licInfoMessage {
return licInfoMessage{
Status: "error",
Error: e.Error(),
}
}

View File

@@ -18,11 +18,9 @@
package cmd
import (
"errors"
"fmt"
"github.com/fatih/color"
"github.com/google/uuid"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/madmin-go"
@@ -30,6 +28,8 @@ import (
"github.com/minio/pkg/console"
)
const licRegisterMsgTag = "licenseRegisterMessage"
var licenseRegisterFlags = append([]cli.Flag{
cli.StringFlag{
Name: "api-key",
@@ -58,11 +58,19 @@ FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Register MinIO cluster at alias 'play' on SUBNET, using alias as the cluster name.
{{.Prompt}} {{.HelpName}} play
1. Register MinIO cluster at alias 'play' on SUBNET, using api key for auth
{{.Prompt}} {{.HelpName}} play --api-key 08efc836-4289-dbd4-ad82-b5e8b6d25577
2. Register MinIO cluster at alias 'play' on SUBNET, using the name "play-cluster".
{{.Prompt}} {{.HelpName}} play --name play-cluster
2. Register MinIO cluster at alias 'play' on SUBNET, using api key for auth,
and "play-cluster" as the preferred name for the cluster on SUBNET.
{{.Prompt}} {{.HelpName}} play --api-key 08efc836-4289-dbd4-ad82-b5e8b6d25577 --name play-cluster
3. Register MinIO cluster at alias 'play' on SUBNET in an airgapped environment
{{.Prompt}} {{.HelpName}} play --airgap
4. Register MinIO cluster at alias 'play' on SUBNET, using alias as the cluster name.
This asks for SUBNET credentials if the cluster is not already registered.
{{.Prompt}} {{.HelpName}} play
`,
}
@@ -84,7 +92,7 @@ func (li licRegisterMessage) String() string {
msg = fmt.Sprintln("Open the following URL in the browser to register", li.Alias, "on SUBNET:")
msg += li.URL
}
return console.Colorize(licUpdateMsgTag, msg)
return console.Colorize(licRegisterMsgTag, msg)
}
// JSON jsonified license register message
@@ -95,8 +103,8 @@ func (li licRegisterMessage) JSON() string {
return string(jsonBytes)
}
// checklicenseRegisterSyntax - validate arguments passed by a user
func checklicenseRegisterSyntax(ctx *cli.Context) {
// checkLicenseRegisterSyntax - validate arguments passed by a user
func checkLicenseRegisterSyntax(ctx *cli.Context) {
if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 {
cli.ShowCommandHelpAndExit(ctx, "register", 1) // last argument is exit code
}
@@ -142,43 +150,19 @@ type SubnetMFAReq struct {
Token string `json:"token"`
}
func validateAPIKey(apiKey string, offline bool) error {
if offline {
return errors.New("--api-key is not applicable in airgap mode")
}
_, e := uuid.Parse(apiKey)
if e != nil {
return e
}
return nil
}
func mainLicenseRegister(ctx *cli.Context) error {
console.SetColor("RegisterSuccessMessage", color.New(color.FgGreen, color.Bold))
checklicenseRegisterSyntax(ctx)
console.SetColor(licRegisterMsgTag, color.New(color.FgGreen, color.Bold))
checkLicenseRegisterSyntax(ctx)
// Get the alias parameter from cli
aliasedURL := ctx.Args().Get(0)
alias, accAPIKey := initSubnetConnectivity(ctx, aliasedURL)
offline := ctx.Bool("airgap") || ctx.Bool("offline")
if !offline {
fatalIf(checkURLReachable(subnetBaseURL()).Trace(aliasedURL), "Unable to reach %s register", subnetBaseURL())
}
accAPIKey := ctx.String("api-key")
if len(accAPIKey) > 0 {
e := validateAPIKey(accAPIKey, offline)
fatalIf(probe.NewError(e), "unable to parse input values")
}
alias, _ := url2Alias(aliasedURL)
clusterName := ctx.String("name")
if len(clusterName) == 0 {
clusterName = alias
} else {
if offline {
if globalAirgapped {
fatalIf(errInvalidArgument(), "'--name' is not allowed in airgapped mode")
}
}
@@ -190,10 +174,13 @@ func mainLicenseRegister(ctx *cli.Context) error {
fatalIf(probe.NewError(e), "Error in fetching subnet credentials")
if len(apiKey) > 0 || len(lic) > 0 {
alreadyRegistered = true
if len(accAPIKey) == 0 {
accAPIKey = apiKey
}
}
lrm := licRegisterMessage{Status: "success", Alias: alias}
if offline {
if globalAirgapped {
lrm.Type = "offline"
regToken, e := generateRegToken(regInfo)
@@ -202,7 +189,8 @@ func mainLicenseRegister(ctx *cli.Context) error {
lrm.URL = subnetOfflineRegisterURL(regToken)
} else {
lrm.Type = "online"
registerOnline(regInfo, alias, accAPIKey)
_, _, e = registerClusterOnSubnet(regInfo, alias, accAPIKey)
fatalIf(probe.NewError(e), "Could not register cluster with SUBNET:")
lrm.Action = "registered"
if alreadyRegistered {
@@ -214,25 +202,6 @@ func mainLicenseRegister(ctx *cli.Context) error {
return nil
}
func registerOnline(clusterRegInfo ClusterRegistrationInfo, alias string, accAPIKey string) {
var resp string
var e error
if len(accAPIKey) > 0 {
resp, e = registerClusterWithSubnetCreds(clusterRegInfo, accAPIKey, "")
if e == nil {
// save the api key in config
setSubnetAPIKey(alias, accAPIKey)
}
} else {
resp, e = registerClusterOnSubnet(alias, clusterRegInfo)
}
fatalIf(probe.NewError(e), "Could not register cluster with SUBNET:")
extractAndSaveLicense(alias, resp)
}
func getAdminInfo(aliasedURL string) madmin.InfoMessage {
// Create a new MinIO Admin Client
client := getClient(aliasedURL)

View File

@@ -84,15 +84,11 @@ func mainLicenseUpdate(ctx *cli.Context) error {
licFile := ctx.Args().Get(1)
// If set, the subnet public key will not be downloaded from subnet
// and the offline key embedded in mc will be used.
airgap := ctx.Bool("airgap")
printMsg(performLicenseUpdate(licFile, alias, airgap))
printMsg(performLicenseUpdate(licFile, alias))
return nil
}
func performLicenseUpdate(licFile string, alias string, airgap bool) licUpdateMessage {
func performLicenseUpdate(licFile string, alias string) licUpdateMessage {
lum := licUpdateMessage{
Alias: alias,
Status: "success",
@@ -102,7 +98,7 @@ func performLicenseUpdate(licFile string, alias string, airgap bool) licUpdateMe
fatalIf(probe.NewError(e), fmt.Sprintf("Unable to read license file %s", licFile))
lic := string(licBytes)
li, e := parseLicense(lic, airgap)
li, e := parseLicense(lic)
fatalIf(probe.NewError(e), fmt.Sprintf("Error parsing license from %s", licFile))
if li.ExpiresAt.Before(time.Now()) {

View File

@@ -26,13 +26,15 @@ import (
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"strconv"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"github.com/minio/cli"
"github.com/minio/madmin-go"
"github.com/minio/mc/pkg/probe"
@@ -68,12 +70,6 @@ mr/cKCUyBL7rcAvg0zNq1vcSrUSGlAmY3SEDCu3GOKnjG/U4E7+p957ocWSV+mQU
Usage: "Development mode - talks to local SUBNET",
Hidden: true,
},
cli.BoolFlag{
// Deprecated Oct 2021. Same as airgap, retaining as hidden for backward compatibility
Name: "offline",
Usage: "Use in environments without network access to SUBNET (e.g. airgapped, firewalled, etc.)",
Hidden: true,
},
}
)
@@ -96,8 +92,8 @@ func subnetLogWebhookURL() string {
return subnetBaseURL() + "/api/logs"
}
func subnetHealthUploadURL() string {
return subnetBaseURL() + "/api/health/upload"
func subnetUploadURL(uploadType string, filename string) string {
return fmt.Sprintf("%s/api/%s/upload?filename=%s", subnetBaseURL(), uploadType, filename)
}
func subnetRegisterURL() string {
@@ -112,8 +108,8 @@ func subnetLoginURL() string {
return subnetBaseURL() + "/api/auth/login"
}
func subnetOrgsURL() string {
return subnetBaseURL() + "/api/auth/organizations"
func subnetAPIKeyURL() string {
return subnetBaseURL() + "/api/auth/api-key"
}
func subnetMFAURL() string {
@@ -137,37 +133,34 @@ func checkURLReachable(url string) *probe.Error {
return nil
}
func subnetURLWithAuth(reqURL string, apiKey string, license string) (string, map[string]string, error) {
headers := map[string]string{}
if len(apiKey) > 0 {
// Add api key in url for authentication
reqURL = reqURL + "?api_key=" + apiKey
} else if len(license) > 0 {
// Add license in url for authentication
reqURL = reqURL + "?license=" + license
} else {
func subnetURLWithAuth(reqURL string, apiKey string) (string, map[string]string, error) {
if len(apiKey) == 0 {
// API key not available in minio/mc config.
// Ask the user to log in to get auth token
token, e := subnetLogin()
if e != nil {
return "", nil, e
}
headers = subnetAuthHeaders(token)
accID, err := getSubnetAccID(headers)
if err != nil {
return "", headers, e
apiKey, e = getSubnetAPIKeyUsingAuthToken(token)
if e != nil {
return "", nil, e
}
reqURL = reqURL + "?aid=" + accID
}
return reqURL, headers, nil
return reqURL, subnetAPIKeyAuthHeaders(apiKey), nil
}
func subnetAuthHeaders(authToken string) map[string]string {
func subnetTokenAuthHeaders(authToken string) map[string]string {
return map[string]string{"Authorization": "Bearer " + authToken}
}
func subnetLicenseAuthHeaders(lic string) map[string]string {
return map[string]string{"x-subnet-license": lic}
}
func subnetAPIKeyAuthHeaders(apiKey string) map[string]string {
return map[string]string{"x-subnet-api-key": apiKey}
}
func getSubnetClient() *http.Client {
client := httpClient(10 * time.Second)
if globalSubnetProxyURL != nil {
@@ -264,7 +257,7 @@ func getSubnetAPIKeyFromConfig(alias string) string {
return mcConfig().Aliases[alias].APIKey
}
func setSubnetProxyFromConfig(alias string) error {
func setGlobalSubnetProxyFromConfig(alias string) error {
if globalSubnetProxyURL != nil {
// proxy already set
return nil
@@ -461,83 +454,124 @@ func subnetLogin() (string, error) {
return "", fmt.Errorf("access token not found in response")
}
func getSubnetAccID(headers map[string]string) (string, error) {
respStr, e := subnetGetReq(subnetOrgsURL(), headers)
if e != nil {
return "", e
}
data := gjson.Parse(respStr)
orgs := data.Array()
idx := 1
if len(orgs) > 1 {
fmt.Println("You are part of multiple organizations on SUBNET:")
for idx, org := range orgs {
fmt.Println(" ", idx+1, ":", org.Get("company"))
}
fmt.Print("Please choose the organization for this cluster: ")
reader := bufio.NewReader(os.Stdin)
accIdx, _ := reader.ReadString('\n')
accIdx = strings.Trim(accIdx, "\n")
idx, e = strconv.Atoi(accIdx)
if e != nil {
return "", e
}
if idx > len(orgs) {
msg := "Invalid choice for organization. Please run the command again."
return "", fmt.Errorf(msg)
}
}
return orgs[idx-1].Get("accountId").String(), nil
}
// registerClusterOnSubnet - Registers the given cluster on SUBNET
func registerClusterOnSubnet(alias string, clusterRegInfo ClusterRegistrationInfo) (string, error) {
apiKey, lic, e := getSubnetCreds(alias)
if e != nil {
return "", e
}
return registerClusterWithSubnetCreds(clusterRegInfo, apiKey, lic)
}
// registerClusterOnSubnet - Registers the given cluster on SUBNET
func registerClusterWithSubnetCreds(clusterRegInfo ClusterRegistrationInfo, apiKey string, lic string) (string, error) {
regURL, headers, e := subnetURLWithAuth(subnetRegisterURL(), apiKey, lic)
if e != nil {
return "", e
}
regToken, e := generateRegToken(clusterRegInfo)
if e != nil {
return "", e
}
reqPayload := ClusterRegistrationReq{Token: regToken}
return subnetPostReq(regURL, reqPayload, headers)
}
// getSubnetCreds - returns the API key and license.
// If only one of them is available, and if `--airgap` is not
// passed, it will attempt to fetch the other from SUBNET
// and save to config
func getSubnetCreds(alias string) (string, string, error) {
e := setSubnetProxyFromConfig(alias)
if e != nil {
return "", "", e
}
apiKey := getSubnetAPIKeyFromConfig(alias)
lic := getSubnetLicenseFromConfig(alias)
apiKey := ""
if len(lic) == 0 {
apiKey = getSubnetAPIKeyFromConfig(alias)
if (len(apiKey) > 0 && len(lic) > 0) ||
(len(apiKey) == 0 && len(lic) == 0) ||
globalAirgapped {
return apiKey, lic, nil
}
var e error
// Not airgapped, and only one of api-key or license is available
// Try to fetch and save the other.
if len(apiKey) > 0 {
lic, e = getSubnetLicenseUsingAPIKey(alias, apiKey)
} else {
apiKey, e = getSubnetAPIKeyUsingLicense(lic)
if e == nil {
setSubnetAPIKey(alias, apiKey)
}
}
if e != nil {
return "", "", e
}
return apiKey, lic, nil
}
// extractAndSaveLicense - extract license from response and set it in minio config
func extractAndSaveLicense(alias string, resp string) {
subnetLic := gjson.Parse(resp).Get("license").String()
if len(subnetLic) > 0 {
setSubnetLicense(alias, subnetLic)
// getSubnetAPIKey - returns the SUBNET API key.
// Returns error if the cluster is not registered with SUBNET.
func getSubnetAPIKey(alias string) (string, error) {
apiKey, _, e := getSubnetCreds(alias)
if e != nil {
return "", e
}
if len(apiKey) > 0 {
return apiKey, nil
}
e = fmt.Errorf("Please register the cluster first by running 'mc support register %s', or use --airgap flag", alias)
return "", e
}
func getSubnetAPIKeyUsingLicense(lic string) (string, error) {
return getSubnetAPIKeyUsingAuthHeaders(subnetLicenseAuthHeaders(lic))
}
func getSubnetAPIKeyUsingAuthToken(authToken string) (string, error) {
return getSubnetAPIKeyUsingAuthHeaders(subnetTokenAuthHeaders(authToken))
}
func getSubnetAPIKeyUsingAuthHeaders(authHeaders map[string]string) (string, error) {
resp, e := subnetGetReq(subnetAPIKeyURL(), authHeaders)
if e != nil {
return "", e
}
return extractSubnetCred("api_key", gjson.Parse(resp))
}
func getSubnetLicenseUsingAPIKey(alias string, apiKey string) (string, error) {
regInfo := getClusterRegInfo(getAdminInfo(alias), alias)
_, lic, e := registerClusterOnSubnet(regInfo, alias, apiKey)
return lic, e
}
// 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 string, apiKey string) (string, string, error) {
regURL, headers, e := subnetURLWithAuth(subnetRegisterURL(), apiKey)
if e != nil {
return "", "", e
}
regToken, e := generateRegToken(clusterRegInfo)
if e != nil {
return "", "", e
}
reqPayload := ClusterRegistrationReq{Token: regToken}
resp, e := subnetPostReq(regURL, reqPayload, headers)
if e != nil {
return "", "", e
}
return extractAndSaveSubnetCreds(alias, resp)
}
// extractAndSaveSubnetCreds - extract license from response and set it in minio config
func extractAndSaveSubnetCreds(alias string, resp string) (string, string, error) {
parsedResp := gjson.Parse(resp)
apiKey, e := extractSubnetCred("api_key", parsedResp)
if e != nil {
return "", "", e
}
if len(apiKey) > 0 {
setSubnetAPIKey(alias, apiKey)
}
lic, e := extractSubnetCred("license", parsedResp)
if e != nil {
return "", "", e
}
if len(lic) > 0 {
setSubnetLicense(alias, lic)
}
return apiKey, lic, nil
}
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
}
// downloadSubnetPublicKey will download the current subnet public key.
@@ -558,10 +592,10 @@ func downloadSubnetPublicKey() (string, error) {
}
// parseLicense parses the license with the bundle public key and return it's information
func parseLicense(license string, airgap bool) (*licverifier.LicenseInfo, error) {
func parseLicense(license string) (*licverifier.LicenseInfo, error) {
var publicKey string
if airgap {
if globalAirgapped {
publicKey = subnetOfflinePublicKey()
} else {
subnetPubKey, e := downloadSubnetPublicKey()
@@ -582,3 +616,119 @@ func parseLicense(license string, airgap bool) (*licverifier.LicenseInfo, error)
li, e := lv.Verify(license)
return &li, e
}
func prepareSubnetUploadURL(uploadURL string, alias string, filename string, apiKey string) (string, map[string]string) {
var e error
if len(apiKey) == 0 {
// api key not passed as flag. check if it's available in the config
apiKey, e = getSubnetAPIKey(alias)
fatalIf(probe.NewError(e), "Unable to retrieve SUBNET API key")
}
reqURL, headers, e := subnetURLWithAuth(uploadURL, apiKey)
fatalIf(probe.NewError(e).Trace(uploadURL), "Unable to fetch SUBNET authentication")
return reqURL, headers
}
func uploadFileToSubnet(alias string, filename string, reqURL string, headers map[string]string) (string, error) {
req, e := subnetUploadReq(reqURL, filename)
if e != nil {
return "", e
}
resp, e := subnetReqDo(req, headers)
if e != nil {
return "", e
}
// Delete the file after successful upload
os.Remove(filename)
// ensure that both api-key and license from
// SUBNET response are saved in the config
extractAndSaveSubnetCreds(alias, resp)
return resp, e
}
func subnetUploadReq(url string, filename string) (*http.Request, error) {
file, e := os.Open(filename)
if e != nil {
return nil, e
}
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, e := writer.CreateFormFile("file", filepath.Base(file.Name()))
if e != nil {
return nil, e
}
if _, e = io.Copy(part, file); e != nil {
return nil, e
}
writer.Close()
r, e := http.NewRequest(http.MethodPost, url, &body)
if e != nil {
return nil, e
}
r.Header.Add("Content-Type", writer.FormDataContentType())
return r, nil
}
func getAPIKeyFlag(ctx *cli.Context) (string, error) {
apiKey := ctx.String("api-key")
if len(apiKey) == 0 {
return "", nil
}
_, e := uuid.Parse(apiKey)
if e != nil {
return "", e
}
return apiKey, nil
}
func initSubnetConnectivity(ctx *cli.Context, aliasedURL string) (string, string) {
e := validateSubnetFlags(ctx)
fatalIf(probe.NewError(e), "Invalid flags:")
alias, _ := url2Alias(aliasedURL)
e = setGlobalSubnetProxyFromConfig(alias)
fatalIf(probe.NewError(e), "Error in setting SUBNET proxy:")
apiKey, e := getAPIKeyFlag(ctx)
fatalIf(probe.NewError(e), "Error in reading --api-key flag:")
// if `--airgap` is provided no need to test SUBNET connectivity.
if !globalAirgapped {
sbu := subnetBaseURL()
fatalIf(checkURLReachable(sbu).Trace(aliasedURL), "Unable to reach %s, please use --airgap if there is no connectivity to SUBNET", sbu)
}
return alias, apiKey
}
func validateSubnetFlags(ctx *cli.Context) error {
if !globalAirgapped {
if globalJSON {
return errors.New("--json is applicable only when --airgap is also passed")
}
return nil
}
if globalDevMode {
return errors.New("--dev is not applicable in airgap mode")
}
if len(ctx.String("api-key")) > 0 {
return errors.New("--api-key is not applicable in airgap mode")
}
return nil
}

View File

@@ -18,15 +18,12 @@
package cmd
import (
"bytes"
"context"
gojson "encoding/json"
"errors"
"flag"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
@@ -45,6 +42,10 @@ import (
)
var supportDiagFlags = append([]cli.Flag{
cli.StringFlag{
Name: "api-key",
Usage: "SUBNET API key",
},
HealthDataTypeFlag{
Name: "test",
Usage: "choose specific diagnostics to run [" + options.String() + "]",
@@ -103,7 +104,7 @@ func checkSupportDiagSyntax(ctx *cli.Context) {
}
// compress and tar MinIO diagnostics output
func tarGZ(healthInfo interface{}, version string, filename string, showMessages bool) error {
func tarGZ(healthInfo interface{}, version string, filename string) error {
f, err := os.OpenFile(filename, os.O_CREATE|os.O_RDWR, 0o666)
if err != nil {
return err
@@ -127,7 +128,7 @@ func tarGZ(healthInfo interface{}, version string, filename string, showMessages
return err
}
if showMessages {
if globalAirgapped {
warningMsgBoundary := "*********************************************************************************"
warning := warnText(" WARNING!!")
warningContents := infoText(` ** THIS FILE MAY CONTAIN SENSITIVE INFORMATION ABOUT YOUR ENVIRONMENT **
@@ -162,64 +163,27 @@ func mainSupportDiag(ctx *cli.Context) error {
// Get the alias parameter from cli
aliasedURL := ctx.Args().Get(0)
alias, _ := url2Alias(aliasedURL)
license, offline := fetchSubnetUploadFlags(ctx)
// license should be provided for us to reach subnet
// if `--airgap` is provided do not need to reach out.
uploadToSubnet := !offline
if uploadToSubnet {
fatalIf(checkURLReachable(subnetBaseURL()).Trace(aliasedURL), "Unable to reach %s to upload MinIO diagnostics report, please use --airgap to upload manually", subnetBaseURL())
}
e := validateFlags(uploadToSubnet)
fatalIf(probe.NewError(e), "unable to parse input values")
alias, apiKey := initSubnetConnectivity(ctx, aliasedURL)
// Create a new MinIO Admin Client
client := getClient(aliasedURL)
// Main execution
execSupportDiag(ctx, client, alias, license, uploadToSubnet)
execSupportDiag(ctx, client, alias, apiKey)
return nil
}
func fetchSubnetUploadFlags(ctx *cli.Context) (string, bool) {
// license info to upload to subnet.
license := ctx.String("license")
// If set, the MinIO diagnostics will not be uploaded
// to subnet and will only be saved locally.
offline := ctx.Bool("airgap") || ctx.Bool("offline")
return license, offline
}
func validateFlags(uploadToSubnet bool) error {
if uploadToSubnet {
if globalJSON {
return errors.New("--json is applicable only when --airgap is also passed")
}
return nil
}
if globalDevMode {
return errors.New("--dev is not applicable in airgap mode")
}
return nil
}
func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias string, license string, uploadToSubnet bool) {
func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias string, apiKey string) {
var reqURL string
var headers map[string]string
filename := fmt.Sprintf("%s-health_%s.json.gz", filepath.Clean(alias), UTCNow().Format("20060102150405"))
if uploadToSubnet {
if !globalAirgapped {
// Retrieve subnet credentials (login/license) beforehand as
// it can take a long time to fetch the health information
reqURL, headers = prepareDiagUploadURL(alias, filename, license)
uploadURL := subnetUploadURL("health", filename)
reqURL, headers = prepareSubnetUploadURL(uploadURL, alias, filename, apiKey)
}
healthInfo, version, e := fetchServerDiagInfo(ctx, client)
@@ -229,98 +193,28 @@ func execSupportDiag(ctx *cli.Context, client *madmin.AdminClient, alias string,
switch version {
case madmin.HealthInfoVersion0:
printMsg(healthInfo.(madmin.HealthInfoV0))
case madmin.HealthInfoVersion2:
printMsg(healthInfo.(madmin.HealthInfoV2))
case madmin.HealthInfoVersion:
printMsg(healthInfo.(madmin.HealthInfo))
}
return
}
e = tarGZ(healthInfo, version, filename, !uploadToSubnet)
e = tarGZ(healthInfo, version, filename)
fatalIf(probe.NewError(e), "Unable to save MinIO diagnostics report")
if uploadToSubnet {
e = uploadDiagReport(alias, filename, reqURL, headers)
if !globalAirgapped {
resp, e := uploadFileToSubnet(alias, filename, reqURL, headers)
fatalIf(probe.NewError(e), "Unable to upload MinIO diagnostics report to SUBNET portal")
}
}
func prepareDiagUploadURL(alias string, filename string, license string) (string, map[string]string) {
apiKey := ""
if len(license) == 0 {
apiKey = getSubnetAPIKeyFromConfig(alias)
if len(apiKey) == 0 {
license = getSubnetLicenseFromConfig(alias)
if len(license) == 0 {
// Both api key and license not available. Ask user to register the cluster first
e := fmt.Errorf("Please register the cluster first by running 'mc support register %s', or use --airgap flag", alias)
fatalIf(probe.NewError(e), "Cluster not registered.")
}
msg := "MinIO diagnostics report was successfully uploaded to SUBNET."
clusterURL, _ := url.PathUnescape(gjson.Get(resp, "cluster_url").String())
if len(clusterURL) > 0 {
msg += fmt.Sprintf(" Please click here to view our analysis: %s", clusterURL)
}
console.Infoln(msg)
}
uploadURL := subnetHealthUploadURL()
reqURL, headers, e := subnetURLWithAuth(uploadURL, apiKey, license)
fatalIf(probe.NewError(e).Trace(uploadURL), "Unable to fetch SUBNET authentication")
reqURL = fmt.Sprintf("%s&filename=%s", reqURL, filename)
return reqURL, headers
}
func uploadDiagReport(alias string, filename string, reqURL string, headers map[string]string) error {
e := setSubnetProxyFromConfig(alias)
if e != nil {
return e
}
req, e := subnetUploadReq(reqURL, filename)
if e != nil {
return e
}
resp, e := subnetReqDo(req, headers)
if e != nil {
return e
}
// Delete the report after successful upload
os.Remove(filename)
msg := "MinIO diagnostics report was successfully uploaded to SUBNET."
clusterURL, _ := url.PathUnescape(gjson.Get(resp, "cluster_url").String())
if len(clusterURL) > 0 {
msg += fmt.Sprintf(" Please click here to view our analysis: %s", clusterURL)
}
console.Infoln(msg)
return nil
}
func subnetUploadReq(url string, filename string) (*http.Request, error) {
file, e := os.Open(filename)
if e != nil {
return nil, e
}
defer file.Close()
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, e := writer.CreateFormFile("file", filepath.Base(file.Name()))
if e != nil {
return nil, e
}
if _, e = io.Copy(part, file); e != nil {
return nil, e
}
writer.Close()
r, e := http.NewRequest(http.MethodPost, url, &body)
if e != nil {
return nil, e
}
r.Header.Add("Content-Type", writer.FormDataContentType())
return r, nil
}
func fetchServerDiagInfo(ctx *cli.Context, client *madmin.AdminClient) (interface{}, string, error) {