1
0
mirror of https://github.com/minio/mc.git synced 2025-04-19 21:02:15 +03:00
mc/cmd/admin-replicate-status.go
2025-03-28 01:05:40 -07:00

920 lines
31 KiB
Go

// Copyright (c) 2015-2022 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 (
"fmt"
"sort"
"strings"
"time"
humanize "github.com/dustin/go-humanize"
"github.com/fatih/color"
"github.com/minio/cli"
json "github.com/minio/colorjson"
"github.com/minio/madmin-go/v3"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio-go/v7/pkg/replication"
"github.com/minio/pkg/v3/console"
)
var adminReplicateStatusFlags = []cli.Flag{
cli.BoolFlag{
Name: "buckets",
Usage: "display only buckets",
},
cli.BoolFlag{
Name: "policies",
Usage: "display only policies",
},
cli.BoolFlag{
Name: "users",
Usage: "display only users",
},
cli.BoolFlag{
Name: "groups",
Usage: "display only groups",
},
cli.BoolFlag{
Name: "ilm-expiry-rules",
Usage: "display only ilm expiry rules",
},
cli.BoolFlag{
Name: "all",
Usage: "display all available site replication status",
},
cli.StringFlag{
Name: "bucket",
Usage: "display bucket sync status",
},
cli.StringFlag{
Name: "policy",
Usage: "display policy sync status",
},
cli.StringFlag{
Name: "user",
Usage: "display user sync status",
},
cli.StringFlag{
Name: "group",
Usage: "display group sync status",
},
cli.StringFlag{
Name: "ilm-expiry-rule",
Usage: "display ILM expiry rule sync status",
},
}
// Some cell values
const (
tickCell string = "✔ "
crossTickCell string = "✗ "
blankCell string = " "
fieldLen = 15
)
var adminReplicateStatusCmd = cli.Command{
Name: "status",
Usage: "display site replication status",
Action: mainAdminReplicationStatus,
OnUsageError: onUsageError,
Before: setGlobalsFromContext,
Flags: append(globalFlags, adminReplicateStatusFlags...),
CustomHelpTemplate: `NAME:
{{.HelpName}} - {{.Usage}}
USAGE:
{{.HelpName}} TARGET
FLAGS:
{{range .VisibleFlags}}{{.}}
{{end}}
EXAMPLES:
1. Display overall site replication status:
{{.Prompt}} {{.HelpName}} minio1
2. Display site replication status of buckets across sites
{{.Prompt}} {{.HelpName}} minio1 --buckets
3. Drill down and view site replication status of bucket "bucket"
{{.Prompt}} {{.HelpName}} minio1 --bucket bucket
4. Drill down and view site replication status of user "foo"
{{.Prompt}} {{.HelpName}} minio1 --user foo
`,
}
type srStatus struct {
madmin.SRStatusInfo
opts madmin.SRStatusOptions
}
func (i srStatus) JSON() string {
bs, e := json.MarshalIndent(madmin.SRStatusInfo(i.SRStatusInfo), "", " ")
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
return string(bs)
}
func (i srStatus) String() string {
var messages []string
ms := i.Metrics
q := i.Metrics.Queued
w := i.Metrics.ActiveWorkers
// Color palette initialization
console.SetColor("Summary", color.New(color.FgWhite, color.Bold))
console.SetColor("SummaryHdr", color.New(color.FgCyan, color.Bold))
console.SetColor("SummaryDtl", color.New(color.FgGreen, color.Bold))
coloredDot := console.Colorize("Status", dot)
nameIDMap := make(map[string]string)
var siteNames []string
info := i.SRStatusInfo
for dID := range info.Sites {
sname := strings.ToTitle(info.Sites[dID].Name)
siteNames = append(siteNames, sname)
nameIDMap[sname] = dID
}
if !info.Enabled {
messages = []string{"SiteReplication is not enabled"}
return console.Colorize("UserMessage", strings.Join(messages, "\n"))
}
sort.Strings(siteNames)
legendHdr := []string{"Site"}
legendFields := []Field{{"Entity", 15}}
for _, sname := range siteNames {
legendHdr = append(legendHdr, sname)
legendFields = append(legendFields, Field{"sname", 15})
}
if i.opts.Buckets {
messages = append(messages,
console.Colorize("SummaryHdr", "Bucket replication status:"))
switch i.MaxBuckets {
case 0:
messages = append(messages, console.Colorize("Summary", "No Buckets present\n"))
default:
msg := console.Colorize(i.getTheme(len(info.BucketStats) == 0), fmt.Sprintf("%d/%d Buckets in sync", info.MaxBuckets-len(info.BucketStats), info.MaxBuckets)) + "\n"
messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg))
if len(i.BucketStats) > 0 {
messages = append(messages, i.siteHeader(siteNames, "Bucket"))
}
var detailFields []Field
for b, ssMap := range i.BucketStats {
var details []string
details = append(details, b)
detailFields = append(detailFields, legendFields[0])
for _, sname := range siteNames {
detailFields = append(detailFields, legendFields[0])
dID := nameIDMap[sname]
ss := ssMap[dID]
switch {
case !ss.HasBucket:
details = append(details, fmt.Sprintf("%s ", blankCell))
case ss.OLockConfigMismatch, ss.PolicyMismatch, ss.QuotaCfgMismatch, ss.ReplicationCfgMismatch, ss.TagMismatch:
details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
default:
details = append(details, fmt.Sprintf("%s in-sync", tickCell))
}
}
messages = append(messages, newPrettyTable(" | ",
detailFields...).buildRow(details...))
messages = append(messages, "")
}
}
}
if i.opts.Policies {
messages = append(messages,
console.Colorize("SummaryHdr", "Policy replication status:"))
switch i.MaxPolicies {
case 0:
messages = append(messages, console.Colorize("Summary", "No Policies present\n"))
default:
msg := console.Colorize(i.getTheme(len(i.PolicyStats) == 0), fmt.Sprintf("%d/%d Policies in sync", info.MaxPolicies-len(info.PolicyStats), info.MaxPolicies)) + "\n"
messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg))
if len(i.PolicyStats) > 0 {
messages = append(messages, i.siteHeader(siteNames, "Policy"))
}
var detailFields []Field
for b, ssMap := range i.PolicyStats {
var details []string
details = append(details, b)
detailFields = append(detailFields, legendFields[0])
for _, sname := range siteNames {
detailFields = append(detailFields, legendFields[0])
dID := nameIDMap[sname]
ss := ssMap[dID]
switch {
case !ss.HasPolicy:
details = append(details, blankCell)
case ss.PolicyMismatch:
details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
default:
details = append(details, fmt.Sprintf("%s in-sync", tickCell))
}
}
messages = append(messages, newPrettyTable(" | ",
detailFields...).buildRow(details...))
}
if len(i.PolicyStats) > 0 {
messages = append(messages, "")
}
}
}
if i.opts.Users {
messages = append(messages,
console.Colorize("SummaryHdr", "User replication status:"))
switch i.MaxUsers {
case 0:
messages = append(messages, console.Colorize("Summary", "No Users present\n"))
default:
msg := console.Colorize(i.getTheme(len(i.UserStats) == 0), fmt.Sprintf("%d/%d Users in sync", info.MaxUsers-len(i.UserStats), info.MaxUsers)) + "\n"
messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg))
if len(i.UserStats) > 0 {
messages = append(messages, i.siteHeader(siteNames, "User"))
}
var detailFields []Field
for b, ssMap := range i.UserStats {
var details []string
details = append(details, b)
detailFields = append(detailFields, legendFields[0])
for _, sname := range siteNames {
detailFields = append(detailFields, legendFields[0])
dID := nameIDMap[sname]
ss, ok := ssMap[dID]
switch {
case !ss.HasUser:
details = append(details, blankCell)
case !ok, ss.UserInfoMismatch:
details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
default:
details = append(details, fmt.Sprintf("%s in-sync", tickCell))
}
}
messages = append(messages, newPrettyTable(" | ",
detailFields...).buildRow(details...))
}
if len(i.UserStats) > 0 {
messages = append(messages, "")
}
}
}
if i.opts.Groups {
messages = append(messages,
console.Colorize("SummaryHdr", "Group replication status:"))
switch i.MaxGroups {
case 0:
messages = append(messages, console.Colorize("Summary", "No Groups present\n"))
default:
msg := console.Colorize(i.getTheme(len(i.GroupStats) == 0), fmt.Sprintf("%d/%d Groups in sync", i.MaxGroups-len(i.GroupStats), i.MaxGroups)) + "\n"
messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg))
if len(i.GroupStats) > 0 {
messages = append(messages, i.siteHeader(siteNames, "Group"))
}
var detailFields []Field
for b, ssMap := range i.GroupStats {
var details []string
details = append(details, b)
detailFields = append(detailFields, legendFields[0])
for _, sname := range siteNames {
detailFields = append(detailFields, legendFields[0])
dID := nameIDMap[sname]
ss := ssMap[dID]
switch {
case !ss.HasGroup:
details = append(details, blankCell)
case ss.GroupDescMismatch:
details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
default:
details = append(details, fmt.Sprintf("%s in-sync", tickCell))
}
}
messages = append(messages, newPrettyTable(" | ",
detailFields...).buildRow(details...))
}
if len(i.GroupStats) > 0 {
messages = append(messages, "")
}
}
}
if i.opts.ILMExpiryRules {
messages = append(messages,
console.Colorize("SummaryHdr", "ILM Expiry Rules replication status:"))
switch {
case i.MaxILMExpiryRules == 0:
messages = append(messages, console.Colorize("Summary", "No ILM Expiry Rules present\n"))
case i.ILMExpiryStats == nil:
messages = append(messages, console.Colorize("Summary", "Replication of ILM Expiry is not enabled\n"))
default:
msg := console.Colorize(i.getTheme(len(info.ILMExpiryStats) == 0), fmt.Sprintf("%d/%d ILM Expiry Rules in sync", info.MaxILMExpiryRules-len(info.ILMExpiryStats), info.MaxILMExpiryRules)) + "\n"
messages = append(messages, fmt.Sprintf("%s %s", coloredDot, msg))
if len(i.ILMExpiryStats) > 0 {
messages = append(messages, i.siteHeader(siteNames, "ILM Expiry Rules"))
}
var detailFields []Field
for b, ssMap := range i.ILMExpiryStats {
var details []string
details = append(details, b)
detailFields = append(detailFields, legendFields[0])
for _, sname := range siteNames {
detailFields = append(detailFields, legendFields[0])
dID := nameIDMap[sname]
ss := ssMap[dID]
switch {
case !ss.HasILMExpiryRules:
details = append(details, blankCell)
case ss.ILMExpiryRuleMismatch:
details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
default:
details = append(details, fmt.Sprintf("%s in-sync", tickCell))
}
}
messages = append(messages, newPrettyTable(" | ",
detailFields...).buildRow(details...))
messages = append(messages, "")
}
}
}
switch i.opts.Entity {
case madmin.SRBucketEntity:
messages = append(messages, i.getBucketStatusSummary(siteNames, nameIDMap, "Bucket")...)
case madmin.SRPolicyEntity:
messages = append(messages, i.getPolicyStatusSummary(siteNames, nameIDMap, "Policy")...)
case madmin.SRUserEntity:
messages = append(messages, i.getUserStatusSummary(siteNames, nameIDMap, "User")...)
case madmin.SRGroupEntity:
messages = append(messages, i.getGroupStatusSummary(siteNames, nameIDMap, "Group")...)
case madmin.SRILMExpiryRuleEntity:
messages = append(messages, i.getILMExpiryStatusSummary(siteNames, nameIDMap, "ILMExpiryRule")...)
}
if i.opts.Metrics {
uiFn := func(theme string) func(string) string {
return func(s string) string {
return console.Colorize(theme, s)
}
}
singleTgt := len(ms.Metrics) == 1
maxui := uiFn("Peak")
avgui := uiFn("Avg")
valueui := uiFn("Value")
messages = append(messages,
console.Colorize("SummaryHdr", "Object replication status:"))
messages = append(messages, console.Colorize("UptimeStr", fmt.Sprintf("Replication status since %s", uiFn("Uptime")(humanize.RelTime(time.Now(), time.Now().Add(time.Duration(ms.Uptime)*time.Second), "", "ago")))))
// for queue stats
coloredDot := console.Colorize("qStatusOK", dot)
if q.Curr.Count > q.Avg.Count {
coloredDot = console.Colorize("qStatusWarn", dot)
}
var replicatedCount, replicatedSize int64
for _, m := range ms.Metrics {
nodeName := m.Endpoint
nodeui := uiFn(getNodeTheme(nodeName))
messages = append(messages, nodeui(nodeName))
messages = append(messages, fmt.Sprintf("Replicated: %s objects (%s)", humanize.Comma(int64(m.ReplicatedCount)), valueui(humanize.IBytes(uint64(m.ReplicatedSize)))))
if singleTgt { // for single target - combine summary section into the target section
messages = append(messages, fmt.Sprintf("Received: %s objects (%s)", humanize.Comma(int64(ms.ReplicaCount)), humanize.IBytes(uint64(ms.ReplicaSize))))
messages = append(messages, fmt.Sprintf("Queued: %s %s objects, (%s) (%s: %s objects, %s; %s: %s objects, %s)", coloredDot, humanize.Comma(int64(q.Curr.Count)), valueui(humanize.IBytes(uint64(q.Curr.Bytes))), avgui("avg"),
humanize.Comma(int64(q.Avg.Count)), valueui(humanize.IBytes(uint64(q.Avg.Bytes))), maxui("max"),
humanize.Comma(int64(q.Max.Count)), valueui(humanize.IBytes(uint64(q.Max.Bytes)))))
messages = append(messages, fmt.Sprintf("Workers: %s (%s: %s; %s %s) ", humanize.Comma(int64(w.Curr)), avgui("avg"), humanize.Comma(int64(w.Avg)), maxui("max"), humanize.Comma(int64(w.Max))))
} else {
replicatedCount += m.ReplicatedCount
replicatedSize += m.ReplicatedSize
}
if m.XferStats != nil {
tgtXfer, ok := m.XferStats[replication.Total]
if ok {
messages = append(messages, fmt.Sprintf("Transfer Rate: %s/s (avg: %s/s; max %s/s)", valueui(humanize.Bytes(uint64(tgtXfer.CurrRate))), valueui(humanize.Bytes(uint64(tgtXfer.AvgRate))), valueui(humanize.Bytes(uint64(tgtXfer.PeakRate)))))
messages = append(messages, fmt.Sprintf("Latency: %s (avg: %s; max %s)", valueui(m.Latency.Curr.Round(time.Millisecond).String()), valueui(m.Latency.Avg.Round(time.Millisecond).String()), valueui(m.Latency.Max.Round(time.Millisecond).String())))
}
}
healthDot := console.Colorize("online", dot)
if !m.Online {
healthDot = console.Colorize("offline", dot)
}
currDowntime := time.Duration(0)
if !m.Online && !m.LastOnline.IsZero() {
currDowntime = UTCNow().Sub(m.LastOnline)
}
// normalize because total downtime is calculated at server side at heartbeat interval, may be slightly behind
totalDowntime := m.TotalDowntime
if currDowntime > totalDowntime {
totalDowntime = currDowntime
}
var linkStatus string
if m.Online {
linkStatus = healthDot + fmt.Sprintf(" online (total downtime: %s)", timeDurationToHumanizedDuration(totalDowntime).String())
} else {
linkStatus = healthDot + fmt.Sprintf(" offline %s (total downtime: %s)", timeDurationToHumanizedDuration(currDowntime).String(), valueui(timeDurationToHumanizedDuration(totalDowntime).String()))
}
messages = append(messages, fmt.Sprintf("Link: %s", linkStatus))
messages = append(messages, fmt.Sprintf("Errors: %s in last 1 minute; %s in last 1hr; %s since uptime", valueui(humanize.Comma(int64(m.Failed.LastMinute.Count))), valueui(humanize.Comma(int64(m.Failed.LastHour.Count))), valueui(humanize.Comma(int64(m.Failed.Totals.Count)))))
messages = append(messages, "")
}
if !singleTgt {
messages = append(messages,
console.Colorize("SummaryHdr", "Summary:"))
messages = append(messages, fmt.Sprintf("Replicated: %s objects (%s)", humanize.Comma(int64(replicatedCount)), valueui(humanize.IBytes(uint64(replicatedSize)))))
messages = append(messages, fmt.Sprintf("Queued: %s %s objects, (%s) (%s: %s objects, %s; %s: %s objects, %s)", coloredDot, humanize.Comma(int64(q.Curr.Count)), valueui(humanize.IBytes(uint64(q.Curr.Bytes))), avgui("avg"),
humanize.Comma(int64(q.Avg.Count)), valueui(humanize.IBytes(uint64(q.Avg.Bytes))), maxui("max"),
humanize.Comma(int64(q.Max.Count)), valueui(humanize.IBytes(uint64(q.Max.Bytes)))))
messages = append(messages, fmt.Sprintf("Received: %s objects (%s)", humanize.Comma(int64(ms.ReplicaCount)), humanize.IBytes(uint64(ms.ReplicaSize))))
}
}
return console.Colorize("UserMessage", strings.Join(messages, "\n"))
}
func (i srStatus) siteHeader(siteNames []string, legend string) string {
legendHdr := []string{legend}
legendFields := []Field{{"Entity", 15}}
for _, sname := range siteNames {
legendHdr = append(legendHdr, sname)
legendFields = append(legendFields, Field{"sname", 15})
}
return console.Colorize("SummaryHdr", newPrettyTable(" | ",
legendFields...,
).buildRow(legendHdr...))
}
func (i srStatus) getTheme(match bool) string {
theme := "UserMessage"
if !match {
theme = "WarningMessage"
}
return theme
}
func (i srStatus) getBucketStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
var messages []string
coloredDot := console.Colorize("Status", dot)
var found bool
for _, st := range i.BucketStats[i.opts.EntityValue] {
if st.HasBucket {
found = true
break
}
}
if !found {
messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Bucket %s not found\n", i.opts.EntityValue)))
return messages
}
messages = append(messages,
console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "Bucket config replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
siteHdr := i.siteHeader(siteNames, legend)
messages = append(messages, siteHdr)
rowLegend := []string{"Tags", "Policy", "Quota", "Retention", "Encryption", "Replication"}
detailFields := make([][]Field, len(rowLegend))
var retention, encryption, tags, bpolicies, quota, replication []string
for i, row := range rowLegend {
detailFields[i] = make([]Field, len(siteNames)+1)
detailFields[i][0] = Field{"Entity", 15}
switch i {
case 0:
tags = append(tags, row)
case 1:
bpolicies = append(bpolicies, row)
case 2:
quota = append(quota, row)
case 3:
retention = append(retention, row)
case 4:
encryption = append(encryption, row)
case 5:
replication = append(replication, row)
}
}
rows := make([]string, len(rowLegend))
for j, sname := range siteNames {
dID := nameIDMap[sname]
ss := i.BucketStats[i.opts.EntityValue][dID]
var theme, msgStr string
for r := range rowLegend {
switch r {
case 0:
theme, msgStr = syncStatus(ss.TagMismatch, ss.HasTagsSet)
tags = append(tags, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
case 1:
theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicySet)
bpolicies = append(bpolicies, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
case 2:
theme, msgStr = syncStatus(ss.QuotaCfgMismatch, ss.HasQuotaCfgSet)
quota = append(quota, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
case 3:
theme, msgStr = syncStatus(ss.OLockConfigMismatch, ss.HasOLockConfigSet)
retention = append(retention, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
case 4:
theme, msgStr = syncStatus(ss.SSEConfigMismatch, ss.HasSSECfgSet)
encryption = append(encryption, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
case 5:
theme, msgStr = syncStatus(ss.ReplicationCfgMismatch, ss.HasReplicationCfg)
replication = append(replication, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
}
}
}
for r := range rowLegend {
switch r {
case 0:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(tags...)
case 1:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(bpolicies...)
case 2:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(quota...)
case 3:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(retention...)
case 4:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(encryption...)
case 5:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(replication...)
}
}
messages = append(messages, rows...)
return messages
}
func (i srStatus) getPolicyStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
var messages []string
coloredDot := console.Colorize("Status", dot)
var found bool
for _, st := range i.PolicyStats[i.opts.EntityValue] {
if st.HasPolicy {
found = true
break
}
}
if !found {
messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Policy %s not found\n", i.opts.EntityValue)))
return messages
}
rowLegend := []string{"Policy"}
detailFields := make([][]Field, len(rowLegend))
var policies []string
detailFields[0] = make([]Field, len(siteNames)+1)
detailFields[0][0] = Field{"Entity", 15}
policies = append(policies, "Policy")
rows := make([]string, len(rowLegend))
for j, sname := range siteNames {
dID := nameIDMap[sname]
ss := i.PolicyStats[i.opts.EntityValue][dID]
var theme, msgStr string
for r := range rowLegend {
switch r {
case 0:
theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicy)
policies = append(policies, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
}
}
}
for r := range rowLegend {
switch r {
case 0:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(policies...)
}
}
messages = append(messages,
console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "Policy replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
siteHdr := i.siteHeader(siteNames, legend)
messages = append(messages, siteHdr)
messages = append(messages, rows...)
return messages
}
func (i srStatus) getUserStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
var messages []string
coloredDot := console.Colorize("Status", dot)
var found bool
for _, st := range i.UserStats[i.opts.EntityValue] {
if st.HasUser {
found = true
break
}
}
if !found {
messages = append(messages, console.Colorize("Summary", fmt.Sprintf("User %s not found\n", i.opts.EntityValue)))
return messages
}
rowLegend := []string{"Info", "Policy mapping"}
detailFields := make([][]Field, len(rowLegend))
var users, policyMapping []string
for i, row := range rowLegend {
detailFields[i] = make([]Field, len(siteNames)+1)
detailFields[i][0] = Field{"Entity", 15}
switch i {
case 0:
users = append(users, row)
default:
policyMapping = append(policyMapping, row)
}
}
rows := make([]string, len(rowLegend))
for j, sname := range siteNames {
dID := nameIDMap[sname]
ss := i.UserStats[i.opts.EntityValue][dID]
var theme, msgStr string
for r := range rowLegend {
switch r {
case 0:
theme, msgStr = syncStatus(ss.UserInfoMismatch, ss.HasUser)
users = append(users, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
case 1:
theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicyMapping)
policyMapping = append(policyMapping, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
}
}
}
for r := range rowLegend {
switch r {
case 0:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(users...)
case 1:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(policyMapping...)
}
}
messages = append(messages,
console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "User replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
siteHdr := i.siteHeader(siteNames, legend)
messages = append(messages, siteHdr)
messages = append(messages, rows...)
return messages
}
func (i srStatus) getGroupStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
var messages []string
coloredDot := console.Colorize("Status", dot)
rowLegend := []string{"Info", "Policy mapping"}
detailFields := make([][]Field, len(rowLegend))
var found bool
for _, st := range i.GroupStats[i.opts.EntityValue] {
if st.HasGroup {
found = true
break
}
}
if !found {
messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Group %s not found\n", i.opts.EntityValue)))
return messages
}
var groups, policyMapping []string
for i, row := range rowLegend {
detailFields[i] = make([]Field, len(siteNames)+1)
detailFields[i][0] = Field{"Entity", 15}
switch i {
case 0:
groups = append(groups, row)
default:
policyMapping = append(policyMapping, row)
}
}
rows := make([]string, len(rowLegend))
// b := i.opts.EntityValue
for j, sname := range siteNames {
dID := nameIDMap[sname]
ss := i.GroupStats[i.opts.EntityValue][dID]
// sm := i.SRStatusInfo.StatsSummary
var theme, msgStr string
for r := range rowLegend {
switch r {
case 0:
theme, msgStr = syncStatus(ss.GroupDescMismatch, ss.HasGroup)
groups = append(groups, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
case 1:
theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicyMapping)
policyMapping = append(policyMapping, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
}
}
}
for r := range rowLegend {
switch r {
case 0:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(groups...)
case 1:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(policyMapping...)
}
}
messages = append(messages,
console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "Group replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
siteHdr := i.siteHeader(siteNames, legend)
messages = append(messages, siteHdr)
messages = append(messages, rows...)
return messages
}
func (i srStatus) getILMExpiryStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
var messages []string
coloredDot := console.Colorize("Status", dot)
var found bool
for _, st := range i.ILMExpiryStats[i.opts.EntityValue] {
if st.HasILMExpiryRules {
found = true
break
}
}
if !found {
messages = append(messages, console.Colorize("Summary", fmt.Sprintf("ILM Expiry Rule %s not found\n", i.opts.EntityValue)))
return messages
}
rowLegend := []string{"ILM Expiry Rule"}
detailFields := make([][]Field, len(rowLegend))
var rules []string
detailFields[0] = make([]Field, len(siteNames)+1)
detailFields[0][0] = Field{"Entity", 15}
rules = append(rules, "ILM Expiry Rule")
rows := make([]string, len(rowLegend))
for j, sname := range siteNames {
dID := nameIDMap[sname]
ss := i.ILMExpiryStats[i.opts.EntityValue][dID]
var theme, msgStr string
for r := range rowLegend {
switch r {
case 0:
theme, msgStr = syncStatus(ss.ILMExpiryRuleMismatch, ss.HasILMExpiryRules)
rules = append(rules, msgStr)
detailFields[r][j+1] = Field{theme, fieldLen}
}
}
}
for r := range rowLegend {
switch r {
case 0:
rows[r] = newPrettyTable(" | ",
detailFields[r]...).buildRow(rules...)
}
}
messages = append(messages,
console.Colorize("SummaryHdr", fmt.Sprintf("%s %s\n", coloredDot, console.Colorize("Summary", "ILM Expiry Rule replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
siteHdr := i.siteHeader(siteNames, legend)
messages = append(messages, siteHdr)
messages = append(messages, rows...)
return messages
}
// Calculate srstatus options for command line flags
func srStatusOpts(ctx *cli.Context) (opts madmin.SRStatusOptions) {
if (!ctx.IsSet("buckets") && !ctx.IsSet("users") && !ctx.IsSet("groups") && !ctx.IsSet("policies") && !ctx.IsSet("ilm-expiry-rules") && !ctx.IsSet("bucket") && !ctx.IsSet("user") && !ctx.IsSet("group") && !ctx.IsSet("policy") && !ctx.IsSet("ilm-expiry-rule") && !ctx.IsSet("all")) || ctx.IsSet("all") {
opts.Buckets = true
opts.Users = true
opts.Groups = true
opts.Policies = true
opts.Metrics = true
opts.ILMExpiryRules = true
return
}
opts.Buckets = ctx.Bool("buckets")
opts.Policies = ctx.Bool("policies")
opts.Users = ctx.Bool("users")
opts.Groups = ctx.Bool("groups")
opts.ILMExpiryRules = ctx.Bool("ilm-expiry-rules")
for _, name := range []string{"bucket", "user", "group", "policy", "ilm-expiry-rule"} {
if ctx.IsSet(name) {
opts.Entity = madmin.GetSREntityType(name)
opts.EntityValue = ctx.String(name)
break
}
}
return
}
func mainAdminReplicationStatus(ctx *cli.Context) error {
{
// Check argument count
argsNr := len(ctx.Args())
if argsNr != 1 {
fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
"Need exactly one alias argument.")
}
groupStatus := ctx.IsSet("buckets") || ctx.IsSet("groups") || ctx.IsSet("users") || ctx.IsSet("policies") || ctx.IsSet("ilm-expiry-rules")
indivStatus := ctx.IsSet("bucket") || ctx.IsSet("group") || ctx.IsSet("user") || ctx.IsSet("policy") || ctx.IsSet("ilm-expiry-rule")
if groupStatus && indivStatus {
fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
"Cannot specify both (bucket|group|policy|user|ilm-expiry-rule) flag and one or more of buckets|groups|policies|users|ilm-expiry-rules) flag(s)")
}
setSlc := []bool{ctx.IsSet("bucket"), ctx.IsSet("user"), ctx.IsSet("group"), ctx.IsSet("policy"), ctx.IsSet("ilm-expiry-rule")}
count := 0
for _, s := range setSlc {
if s {
count++
}
}
if count > 1 {
fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
"Cannot specify more than one of --bucket, --policy, --user, --group, --ilm-expiry-rule flags at the same time")
}
}
console.SetColor("UserMessage", color.New(color.FgGreen))
console.SetColor("WarningMessage", color.New(color.FgYellow))
for _, c := range colors {
console.SetColor(fmt.Sprintf("Node%d", c), color.New(c, color.Bold))
}
console.SetColor("Replicated", color.New(color.FgCyan))
console.SetColor("In-Queue", color.New(color.Bold, color.FgYellow))
console.SetColor("Avg", color.New(color.FgCyan))
console.SetColor("Peak", color.New(color.FgYellow))
console.SetColor("Value", color.New(color.FgWhite, color.Bold))
console.SetColor("Current", color.New(color.FgCyan))
console.SetColor("Uptime", color.New(color.Bold, color.FgWhite))
console.SetColor("UptimeStr", color.New(color.FgHiWhite))
console.SetColor("qStatusWarn", color.New(color.FgYellow, color.Bold))
console.SetColor("qStatusOK", color.New(color.FgGreen, color.Bold))
console.SetColor("online", color.New(color.FgGreen, color.Bold))
console.SetColor("offline", color.New(color.FgRed, color.Bold))
// Get the alias parameter from cli
args := ctx.Args()
aliasedURL := args.Get(0)
// Create a new MinIO Admin Client
client, err := newAdminClient(aliasedURL)
fatalIf(err, "Unable to initialize admin connection.")
opts := srStatusOpts(ctx)
info, e := client.SRStatusInfo(globalContext, opts)
fatalIf(probe.NewError(e).Trace(args...), "Unable to get cluster replication status")
printMsg(srStatus{
SRStatusInfo: info,
opts: opts,
})
return nil
}
func syncStatus(mismatch, set bool) (string, string) {
if !set {
return "Entity", blankCell
}
if mismatch {
return "Entity", crossTickCell
}
return "Entity", tickCell
}