Tenant health info upload (#2606)
For registered clusters, after generating the Health Info report, Health Info is uploaded to Subnet, and latest metrics are visible in Subnet. Authored-by: Jillian Inapurapu <jillii@Jillians-MBP.attlocal.net>
This commit is contained in:
@@ -18,11 +18,13 @@ package subnet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
@@ -64,6 +66,62 @@ func LogWebhookURL() string {
|
||||
return subnetBaseURL() + "/api/logs"
|
||||
}
|
||||
|
||||
func UploadURL(uploadType string, filename string) string {
|
||||
return fmt.Sprintf("%s/api/%s/upload?filename=%s", subnetBaseURL(), uploadType, filename)
|
||||
}
|
||||
|
||||
func UploadAuthHeaders(apiKey string) map[string]string {
|
||||
return map[string]string{"x-subnet-api-key": apiKey}
|
||||
}
|
||||
|
||||
func UploadFileToSubnet(info interface{}, client *xhttp.Client, filename string, reqURL string, headers map[string]string) (string, error) {
|
||||
req, e := subnetUploadReq(info, reqURL, filename)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
resp, e := subnetReqDo(client, req, headers)
|
||||
return resp, e
|
||||
}
|
||||
|
||||
func subnetUploadReq(info interface{}, url string, filename string) (*http.Request, error) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
zipWriter := gzip.NewWriter(&body)
|
||||
version := "3"
|
||||
enc := json.NewEncoder(zipWriter)
|
||||
|
||||
header := struct {
|
||||
Version string `json:"version"`
|
||||
}{Version: version}
|
||||
|
||||
if e := enc.Encode(header); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
if e := enc.Encode(info); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
zipWriter.Close()
|
||||
temp := body
|
||||
part, e := writer.CreateFormFile("file", filename)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if _, e = io.Copy(part, &temp); 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 GenerateRegToken(clusterRegInfo mc.ClusterRegistrationInfo) (string, error) {
|
||||
token, e := json.Marshal(clusterRegInfo)
|
||||
if e != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// 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/>.
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
ICloseEvent,
|
||||
IMessageEvent,
|
||||
@@ -72,7 +72,7 @@ const styles = (theme: Theme) =>
|
||||
color: "#07193E",
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 10,
|
||||
marginBottom: 20,
|
||||
},
|
||||
progressResult: {
|
||||
textAlign: "center",
|
||||
@@ -94,8 +94,6 @@ const styles = (theme: Theme) =>
|
||||
|
||||
interface IHealthInfo {
|
||||
classes: any;
|
||||
namespace: string;
|
||||
tenant: string;
|
||||
}
|
||||
|
||||
const HealthInfo = ({ classes }: IHealthInfo) => {
|
||||
@@ -104,22 +102,19 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
|
||||
|
||||
const message = useSelector((state: AppState) => state.healthInfo.message);
|
||||
|
||||
const clusterRegistered = registeredCluster();
|
||||
|
||||
const serverDiagnosticStatus = useSelector(
|
||||
(state: AppState) => state.system.serverDiagnosticStatus
|
||||
);
|
||||
const [startDiagnostic, setStartDiagnostic] = useState(false);
|
||||
|
||||
const [downloadDisabled, setDownloadDisabled] = useState(true);
|
||||
const [localMessage, setMessage] = useState<string>("");
|
||||
const [buttonStartText, setButtonStartText] =
|
||||
useState<string>("Start Diagnostic");
|
||||
const [title, setTitle] = useState<string>("New Diagnostic");
|
||||
const [diagFileContent, setDiagFileContent] = useState<string>("");
|
||||
|
||||
const isDiagnosticComplete =
|
||||
serverDiagnosticStatus === DiagStatSuccess ||
|
||||
serverDiagnosticStatus === DiagStatError;
|
||||
const [subnetResponse, setSubnetResponse] = useState<string>("");
|
||||
const clusterRegistered = registeredCluster();
|
||||
|
||||
const download = () => {
|
||||
let element = document.createElement("a");
|
||||
@@ -195,7 +190,6 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
|
||||
const c = new W3CWebSocket(
|
||||
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/health-info?deadline=1h`
|
||||
);
|
||||
|
||||
let interval: any | null = null;
|
||||
if (c !== null) {
|
||||
c.onopen = () => {
|
||||
@@ -220,6 +214,9 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
|
||||
if (m.encoded !== "") {
|
||||
setDiagFileContent(m.encoded);
|
||||
}
|
||||
if (m.subnetResponse) {
|
||||
setSubnetResponse(m.subnetResponse);
|
||||
}
|
||||
};
|
||||
c.onerror = (error: Error) => {
|
||||
console.log("error closing websocket:", error.message);
|
||||
@@ -275,38 +272,70 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
|
||||
className={classes.progressResult}
|
||||
>
|
||||
<div className={classes.localMessage}>{localMessage}</div>
|
||||
<div className={classes.progressResult}>
|
||||
{" "}
|
||||
{subnetResponse !== "" &&
|
||||
!subnetResponse.toLowerCase().includes("error") && (
|
||||
<Grid item xs={12} className={classes.serversData}>
|
||||
<strong>
|
||||
Health report uploaded to Subnet successfully!
|
||||
</strong>
|
||||
{" "}
|
||||
<strong>
|
||||
See the results on your{" "}
|
||||
<a href={subnetResponse}>Subnet Dashboard</a>{" "}
|
||||
</strong>
|
||||
</Grid>
|
||||
)}
|
||||
{(subnetResponse === "" ||
|
||||
subnetResponse.toLowerCase().includes("error")) &&
|
||||
serverDiagnosticStatus === DiagStatSuccess && (
|
||||
<Grid item xs={12} className={classes.serversData}>
|
||||
<strong>
|
||||
Something went wrong uploading your Health report to
|
||||
Subnet.
|
||||
</strong>
|
||||
{" "}
|
||||
<strong>
|
||||
Log into your{" "}
|
||||
<a href="https://subnet.min.io">Subnet Account</a> to
|
||||
manually upload your Health report.
|
||||
</strong>
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
{serverDiagnosticStatus === DiagStatInProgress ? (
|
||||
<div className={classes.loading}>
|
||||
<Loader style={{ width: 25, height: 25 }} />
|
||||
</div>
|
||||
) : (
|
||||
<Fragment>
|
||||
{serverDiagnosticStatus !== DiagStatError &&
|
||||
!downloadDisabled && (
|
||||
<Grid container justifyItems={"flex-start"}>
|
||||
<Grid item xs={6}>
|
||||
{serverDiagnosticStatus !== DiagStatError &&
|
||||
!downloadDisabled && (
|
||||
<Button
|
||||
id={"download"}
|
||||
type="submit"
|
||||
variant="callAction"
|
||||
onClick={() => download()}
|
||||
disabled={downloadDisabled}
|
||||
label={"Download"}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Button
|
||||
id={"download"}
|
||||
id="start-new-diagnostic"
|
||||
type="submit"
|
||||
variant="callAction"
|
||||
onClick={() => download()}
|
||||
disabled={downloadDisabled}
|
||||
label={"Download"}
|
||||
variant={
|
||||
!clusterRegistered ? "regular" : "callAction"
|
||||
}
|
||||
disabled={startDiagnostic}
|
||||
onClick={startDiagnosticAction}
|
||||
label={buttonStartText}
|
||||
/>
|
||||
)}
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
className={clsx(classes.startDiagnostic, {
|
||||
[classes.startDiagnosticCenter]: !isDiagnosticComplete,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="start-new-diagnostic"
|
||||
type="submit"
|
||||
variant={!clusterRegistered ? "regular" : "callAction"}
|
||||
disabled={startDiagnostic}
|
||||
onClick={startDiagnosticAction}
|
||||
label={buttonStartText}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Fragment>
|
||||
)}
|
||||
@@ -322,7 +351,12 @@ const HealthInfo = ({ classes }: IHealthInfo) => {
|
||||
"During the health diagnostics run, all production traffic will be suspended."
|
||||
}
|
||||
iconComponent={<WarnIcon />}
|
||||
help={<Fragment />}
|
||||
help={
|
||||
<Fragment>
|
||||
Cluster Health Report will be uploaded to Subnet, and is
|
||||
viewable from your Subnet Diagnostics dashboard.
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface HealthInfoMessage {
|
||||
export interface ReportMessage {
|
||||
encoded: string;
|
||||
serverHealthInfo: HealthInfoMessage;
|
||||
subnetResponse: string;
|
||||
}
|
||||
|
||||
export interface perfInfo {
|
||||
|
||||
@@ -147,7 +147,7 @@ export const systemSlice = createSlice({
|
||||
setSiteReplicationInfo: (state, action: PayloadAction<SRInfoStateType>) => {
|
||||
state.siteReplicationInfo = action.payload;
|
||||
},
|
||||
setLicenseInfo: (state, action: PayloadAction<SubnetInfo | null>) => {
|
||||
setSystemLicenseInfo: (state, action: PayloadAction<SubnetInfo | null>) => {
|
||||
state.licenseInfo = action.payload;
|
||||
},
|
||||
setOverrideStyles: (
|
||||
@@ -181,7 +181,7 @@ export const {
|
||||
setServerDiagStat,
|
||||
globalSetDistributedSetup,
|
||||
setSiteReplicationInfo,
|
||||
setLicenseInfo,
|
||||
setSystemLicenseInfo,
|
||||
setOverrideStyles,
|
||||
setAnonymousMode,
|
||||
resetSystem,
|
||||
|
||||
@@ -22,11 +22,14 @@ import (
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/gzip"
|
||||
|
||||
xhttp "github.com/minio/console/pkg/http"
|
||||
subnet "github.com/minio/console/pkg/subnet"
|
||||
"github.com/minio/madmin-go/v2"
|
||||
"github.com/minio/websocket"
|
||||
)
|
||||
@@ -51,7 +54,6 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
|
||||
madmin.HealthDataTypeSysNet,
|
||||
madmin.HealthDataTypeSysProcess,
|
||||
}
|
||||
|
||||
var err error
|
||||
// Fetch info of all servers (cluster or single server)
|
||||
healthInfo, version, err := client.serverHealthInfo(ctx, healthDataTypes, *deadline)
|
||||
@@ -68,12 +70,19 @@ func startHealthInfo(ctx context.Context, conn WSConn, client MinioAdmin, deadli
|
||||
type messageReport struct {
|
||||
Encoded string `json:"encoded"`
|
||||
ServerHealthInfo interface{} `json:"serverHealthInfo"`
|
||||
SubnetResponse string `json:"subnetResponse"`
|
||||
}
|
||||
|
||||
subnetResp, err := sendHealthInfoToSubnet(ctx, healthInfo, client)
|
||||
report := messageReport{
|
||||
Encoded: encodedDiag,
|
||||
ServerHealthInfo: healthInfo,
|
||||
SubnetResponse: subnetResp,
|
||||
}
|
||||
if err != nil {
|
||||
report.SubnetResponse = fmt.Sprintf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
message, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -117,3 +126,35 @@ func getHealthInfoOptionsFromReq(req *http.Request) (*time.Duration, error) {
|
||||
}
|
||||
return &deadlineDuration, nil
|
||||
}
|
||||
|
||||
func sendHealthInfoToSubnet(ctx context.Context, healthInfo interface{}, client MinioAdmin) (string, error) {
|
||||
filename := fmt.Sprintf("health_%d.json", time.Now().Unix())
|
||||
|
||||
subnetUploadURL := subnet.UploadURL("health", filename)
|
||||
subnetHTTPClient := &xhttp.Client{Client: GetConsoleHTTPClient("")}
|
||||
subnetTokenConfig, e := GetSubnetKeyFromMinIOConfig(ctx, client)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
apiKey := subnetTokenConfig.APIKey
|
||||
headers := subnet.UploadAuthHeaders(apiKey)
|
||||
resp, e := subnet.UploadFileToSubnet(healthInfo, subnetHTTPClient, filename, subnetUploadURL, headers)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
type SubnetResponse struct {
|
||||
ClusterURL string `json:"cluster_url,omitempty"`
|
||||
}
|
||||
|
||||
var subnetResp SubnetResponse
|
||||
e = json.Unmarshal([]byte(resp), &subnetResp)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
if len(subnetResp.ClusterURL) != 0 {
|
||||
subnetClusterURL := strings.ReplaceAll(subnetResp.ClusterURL, "%2f", "/")
|
||||
return subnetClusterURL, nil
|
||||
}
|
||||
|
||||
return "", ErrSubnetUploadFail
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ var (
|
||||
ErrEncryptionConfigNotFound = errors.New("encryption configuration not found")
|
||||
ErrPolicyNotFound = errors.New("policy does not exist")
|
||||
ErrLoginNotAllowed = errors.New("login not allowed")
|
||||
ErrSubnetUploadFail = errors.New("Subnet upload failed")
|
||||
)
|
||||
|
||||
// ErrorWithContext :
|
||||
|
||||
@@ -502,7 +502,6 @@ func (wsc *wsAdminClient) healthInfo(ctx context.Context, deadline *time.Duratio
|
||||
LogInfo("health info started")
|
||||
|
||||
ctx = wsReadClientCtx(ctx, wsc.conn)
|
||||
|
||||
err := startHealthInfo(ctx, wsc.conn, wsc.client, deadline)
|
||||
|
||||
sendWsCloseMessage(wsc.conn, err)
|
||||
|
||||
Reference in New Issue
Block a user