diff --git a/cmd/subnet-utils.go b/cmd/subnet-utils.go index edc35e5e..1eebb1cf 100644 --- a/cmd/subnet-utils.go +++ b/cmd/subnet-utils.go @@ -494,15 +494,15 @@ func getSubnetCreds(alias string) (string, string, error) { // 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) + apiKey, lic, e := getSubnetCreds(alias) if e != nil { return "", e } - if len(apiKey) > 0 { - return apiKey, nil + if len(apiKey) == 0 && len(lic) == 0 { + e = fmt.Errorf("Please register the cluster first by running 'mc license register %s'", alias) + return "", e } - e = fmt.Errorf("Please register the cluster first by running 'mc support register %s', or use --airgap flag", alias) - return "", e + return apiKey, nil } func getSubnetAPIKeyUsingLicense(lic string) (string, error) { diff --git a/cmd/support-perf-drive.go b/cmd/support-perf-drive.go index f0cbc46d..cfefd513 100644 --- a/cmd/support-perf-drive.go +++ b/cmd/support-perf-drive.go @@ -28,7 +28,7 @@ import ( "github.com/minio/mc/pkg/probe" ) -func mainAdminSpeedTestDrive(ctx *cli.Context, aliasedURL string) error { +func mainAdminSpeedTestDrive(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") @@ -103,10 +103,15 @@ func mainAdminSpeedTestDrive(ctx *cli.Context, aliasedURL string) error { go func() { if e != nil { - printMsg(PerfTestResult{ - Type: DrivePerfTest, - Err: e.Error(), - }) + r := PerfTestResult{ + Type: DrivePerfTest, + Err: e.Error(), + Final: true, + } + p.Send(r) + if outCh != nil { + outCh <- r + } return } @@ -121,11 +126,15 @@ func mainAdminSpeedTestDrive(ctx *cli.Context, aliasedURL string) error { }) } } - p.Send(PerfTestResult{ + r := PerfTestResult{ Type: DrivePerfTest, DriveResult: results, Final: true, - }) + } + p.Send(r) + if outCh != nil { + outCh <- r + } }() <-done diff --git a/cmd/support-perf-net.go b/cmd/support-perf-net.go index a7933981..706af681 100644 --- a/cmd/support-perf-net.go +++ b/cmd/support-perf-net.go @@ -28,7 +28,7 @@ import ( "github.com/minio/mc/pkg/probe" ) -func mainAdminSpeedTestNetperf(ctx *cli.Context, aliasedURL string) error { +func mainAdminSpeedTestNetperf(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") @@ -93,18 +93,26 @@ func mainAdminSpeedTestNetperf(ctx *cli.Context, aliasedURL string) error { for { select { case e := <-errorCh: - p.Send(PerfTestResult{ + r := PerfTestResult{ Type: NetPerfTest, Err: e.Error(), Final: true, - }) + } + p.Send(r) + if outCh != nil { + outCh <- r + } return case result := <-resultCh: - p.Send(PerfTestResult{ + r := PerfTestResult{ Type: NetPerfTest, NetResult: &result, Final: true, - }) + } + p.Send(r) + if outCh != nil { + outCh <- r + } return default: p.Send(PerfTestResult{ diff --git a/cmd/support-perf-object.go b/cmd/support-perf-object.go index a3e3c9cf..8f96d229 100644 --- a/cmd/support-perf-object.go +++ b/cmd/support-perf-object.go @@ -46,7 +46,7 @@ func mainAdminSpeedtest(ctx *cli.Context) error { return nil } -func mainAdminSpeedTestObject(ctx *cli.Context, aliasedURL string) error { +func mainAdminSpeedTestObject(ctx *cli.Context, aliasedURL string, outCh chan<- PerfTestResult) error { client, perr := newAdminClient(aliasedURL) if perr != nil { fatalIf(perr.Trace(aliasedURL), "Unable to initialize admin client.") @@ -135,11 +135,15 @@ func mainAdminSpeedTestObject(ctx *cli.Context, aliasedURL string) error { go func() { if e != nil { - p.Send(PerfTestResult{ + r := PerfTestResult{ Type: ObjectPerfTest, Err: e.Error(), Final: true, - }) + } + p.Send(r) + if outCh != nil { + outCh <- r + } return } @@ -150,11 +154,15 @@ func mainAdminSpeedTestObject(ctx *cli.Context, aliasedURL string) error { ObjectResult: &result, }) } - p.Send(PerfTestResult{ + r := PerfTestResult{ Type: ObjectPerfTest, ObjectResult: &result, Final: true, - }) + } + p.Send(r) + if outCh != nil { + outCh <- r + } }() <-done diff --git a/cmd/support-perf.go b/cmd/support-perf.go index 54682544..dcaf35e0 100644 --- a/cmd/support-perf.go +++ b/cmd/support-perf.go @@ -18,16 +18,22 @@ package cmd import ( + "archive/zip" + gojson "encoding/json" "fmt" + "os" + "path/filepath" humanize "github.com/dustin/go-humanize" + "github.com/fatih/color" "github.com/minio/cli" json "github.com/minio/colorjson" "github.com/minio/madmin-go" "github.com/minio/mc/pkg/probe" + "github.com/minio/pkg/console" ) -var supportPerfFlags = []cli.Flag{ +var supportPerfFlags = append([]cli.Flag{ cli.StringFlag{ Name: "duration", Usage: "duration the entire perf tests are run", @@ -72,7 +78,7 @@ var supportPerfFlags = []cli.Flag{ Usage: "run tests on drive(s) one-by-one", Hidden: true, }, -} +}, subnetCommonFlags...) var supportPerfCmd = cli.Command{ Name: "perf", @@ -146,6 +152,7 @@ func mainSupportPerf(ctx *cli.Context) error { // the alias parameter from cli aliasedURL := "" + perfType := "" switch len(args) { case 1: // cannot use alias by the name 'drive' or 'net' @@ -154,24 +161,143 @@ func mainSupportPerf(ctx *cli.Context) error { } aliasedURL = args[0] - mainAdminSpeedTestNetperf(ctx, aliasedURL) - mainAdminSpeedTestDrive(ctx, aliasedURL) - mainAdminSpeedTestObject(ctx, aliasedURL) case 2: - aliasedURL := args[1] - switch args[0] { - case "drive": - return mainAdminSpeedTestDrive(ctx, aliasedURL) - case "object": - return mainAdminSpeedTestObject(ctx, aliasedURL) - case "net": - return mainAdminSpeedTestNetperf(ctx, aliasedURL) - default: - cli.ShowCommandHelpAndExit(ctx, "perf", 1) // last argument is exit code - } + perfType = args[0] + aliasedURL = args[1] default: cli.ShowCommandHelpAndExit(ctx, "perf", 1) // last argument is exit code } + // Main execution + execSupportPerf(ctx, aliasedURL, perfType) + return nil } + +func execSupportPerf(ctx *cli.Context, aliasedURL string, perfType string) { + alias, apiKey := initSubnetConnectivity(ctx, aliasedURL) + + if len(apiKey) == 0 { + // api key not passed as flag. check if it's available in the config + var e error + apiKey, e = getSubnetAPIKey(alias) + + // Non-registered execution allowed only in debug+airgapped mode + if !(globalDebug && globalAirgapped) { + fatalIf(probe.NewError(e), "Unable to retrieve SUBNET API key") + } + } + + results := runPerfTests(ctx, aliasedURL, perfType) + if globalJSON { + // No file to be saved or uploaded to SUBNET in case of `--json` + return + } + + resultFileNamePfx := fmt.Sprintf("%s-perf_%s", filepath.Clean(alias), UTCNow().Format("20060102150405")) + resultFileName := resultFileNamePfx + ".json" + + regInfo := getClusterRegInfo(getAdminInfo(aliasedURL), alias) + tmpFileName, e := zipPerfResult(results, resultFileName, regInfo) + fatalIf(probe.NewError(e), "Error creating zip from perf test results:") + + if globalAirgapped { + savePerfResultFile(tmpFileName, resultFileNamePfx, alias) + return + } + + uploadURL := subnetUploadURL("perf", tmpFileName) + reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, tmpFileName, apiKey) + + _, e = uploadFileToSubnet(alias, tmpFileName, reqURL, headers) + if e != nil { + console.Errorln("Unable to upload perf test results to SUBNET portal: " + e.Error()) + savePerfResultFile(tmpFileName, resultFileNamePfx, alias) + return + } + + clr := color.New(color.FgGreen, color.Bold) + clr.Println("uploaded successfully to SUBNET.") + + if len(apiKey) > 0 { + setSubnetAPIKey(alias, apiKey) + } +} + +func savePerfResultFile(tmpFileName string, resultFileNamePfx string, alias string) { + zipFileName := resultFileNamePfx + ".zip" + e := moveFile(tmpFileName, zipFileName) + fatalIf(probe.NewError(e), fmt.Sprintf("Error moving temp file %s to %s:", tmpFileName, zipFileName)) + console.Infoln("MinIO performance report saved at", zipFileName) +} + +func runPerfTests(ctx *cli.Context, aliasedURL string, perfType string) []PerfTestResult { + resultCh := make(chan PerfTestResult) + results := []PerfTestResult{} + defer close(resultCh) + + tests := []string{perfType} + if len(perfType) == 0 { + // by default run all tests + tests = []string{"net", "drive", "object"} + } + + for _, t := range tests { + switch t { + case "drive": + mainAdminSpeedTestDrive(ctx, aliasedURL, resultCh) + case "object": + mainAdminSpeedTestObject(ctx, aliasedURL, resultCh) + case "net": + mainAdminSpeedTestNetperf(ctx, aliasedURL, resultCh) + default: + cli.ShowCommandHelpAndExit(ctx, "perf", 1) // last argument is exit code + } + + if !globalJSON { + results = append(results, <-resultCh) + } + } + + return results +} + +func writeJSONObjToZip(zipWriter *zip.Writer, obj interface{}, filename string) error { + writer, e := zipWriter.Create(filename) + if e != nil { + return e + } + + enc := gojson.NewEncoder(writer) + if e = enc.Encode(obj); e != nil { + return e + } + + return nil +} + +// compress MinIO performance output +func zipPerfResult(perfResult []PerfTestResult, resultFilename string, regInfo ClusterRegistrationInfo) (string, error) { + // Create profile zip file + tmpArchive, e := os.CreateTemp("", "mc-perf-") + + if e != nil { + return "", e + } + defer tmpArchive.Close() + + zipWriter := zip.NewWriter(tmpArchive) + defer zipWriter.Close() + + e = writeJSONObjToZip(zipWriter, perfResult, resultFilename) + if e != nil { + return "", e + } + + e = writeJSONObjToZip(zipWriter, regInfo, "cluster.info") + if e != nil { + return "", e + } + + return tmpArchive.Name(), nil +} diff --git a/cmd/support-profile.go b/cmd/support-profile.go index 4b4ab8b5..56b3e4d6 100644 --- a/cmd/support-profile.go +++ b/cmd/support-profile.go @@ -170,11 +170,6 @@ func mainSupportProfile(ctx *cli.Context) error { aliasedURL := ctx.Args().Get(0) alias, apiKey := initSubnetConnectivity(ctx, aliasedURL) - // if `--airgap` is provided do not try to upload to SUBNET. - if !globalAirgapped { - fatalIf(checkURLReachable(subnetBaseURL()).Trace(aliasedURL), "Unable to reach %s to upload MinIO profile file, please use --airgap to upload manually", subnetBaseURL()) - } - // Create a new MinIO Admin Client client := getClient(aliasedURL)