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:
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user