github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/admin-replicate-status.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  	"time"
    25  
    26  	humanize "github.com/dustin/go-humanize"
    27  	"github.com/fatih/color"
    28  	"github.com/minio/cli"
    29  	json "github.com/minio/colorjson"
    30  	"github.com/minio/madmin-go/v3"
    31  	"github.com/minio/mc/pkg/probe"
    32  	"github.com/minio/minio-go/v7/pkg/replication"
    33  	"github.com/minio/pkg/v2/console"
    34  )
    35  
    36  var adminReplicateStatusFlags = []cli.Flag{
    37  	cli.BoolFlag{
    38  		Name:  "buckets",
    39  		Usage: "display only buckets",
    40  	},
    41  	cli.BoolFlag{
    42  		Name:  "policies",
    43  		Usage: "display only policies",
    44  	},
    45  	cli.BoolFlag{
    46  		Name:  "users",
    47  		Usage: "display only users",
    48  	},
    49  	cli.BoolFlag{
    50  		Name:  "groups",
    51  		Usage: "display only groups",
    52  	},
    53  	cli.BoolFlag{
    54  		Name:  "ilm-expiry-rules",
    55  		Usage: "display only ilm expiry rules",
    56  	},
    57  	cli.BoolFlag{
    58  		Name:  "all",
    59  		Usage: "display all available site replication status",
    60  	},
    61  	cli.StringFlag{
    62  		Name:  "bucket",
    63  		Usage: "display bucket sync status",
    64  	},
    65  	cli.StringFlag{
    66  		Name:  "policy",
    67  		Usage: "display policy sync status",
    68  	},
    69  	cli.StringFlag{
    70  		Name:  "user",
    71  		Usage: "display user sync status",
    72  	},
    73  	cli.StringFlag{
    74  		Name:  "group",
    75  		Usage: "display group sync status",
    76  	},
    77  	cli.StringFlag{
    78  		Name:  "ilm-expiry-rule",
    79  		Usage: "display ILM expiry rule sync status",
    80  	},
    81  }
    82  
    83  // Some cell values
    84  const (
    85  	tickCell      string = "✔ "
    86  	crossTickCell string = "✗ "
    87  	blankCell     string = " "
    88  	fieldLen             = 15
    89  )
    90  
    91  var adminReplicateStatusCmd = cli.Command{
    92  	Name:         "status",
    93  	Usage:        "display site replication status",
    94  	Action:       mainAdminReplicationStatus,
    95  	OnUsageError: onUsageError,
    96  	Before:       setGlobalsFromContext,
    97  	Flags:        append(globalFlags, adminReplicateStatusFlags...),
    98  	CustomHelpTemplate: `NAME:
    99    {{.HelpName}} - {{.Usage}}
   100  
   101  USAGE:
   102    {{.HelpName}} TARGET
   103  
   104  FLAGS:
   105    {{range .VisibleFlags}}{{.}}
   106    {{end}}
   107  
   108  EXAMPLES:
   109      1. Display overall site replication status:
   110         {{.Prompt}} {{.HelpName}} minio1
   111  
   112      2. Display site replication status of buckets across sites
   113         {{.Prompt}} {{.HelpName}} minio1 --buckets
   114  
   115      3. Drill down and view site replication status of bucket "bucket"
   116         {{.Prompt}} {{.HelpName}} minio1 --bucket bucket
   117  
   118      4. Drill down and view site replication status of user "foo"
   119         {{.Prompt}} {{.HelpName}} minio1 --user foo
   120  `,
   121  }
   122  
   123  type srStatus struct {
   124  	madmin.SRStatusInfo
   125  	opts madmin.SRStatusOptions
   126  }
   127  
   128  func (i srStatus) JSON() string {
   129  	bs, e := json.MarshalIndent(madmin.SRStatusInfo(i.SRStatusInfo), "", " ")
   130  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   131  	return string(bs)
   132  }
   133  
   134  func (i srStatus) String() string {
   135  	var messages []string
   136  	ms := i.Metrics
   137  	q := i.Metrics.Queued
   138  	w := i.Metrics.ActiveWorkers
   139  	// Color palette initialization
   140  	console.SetColor("Summary", color.New(color.FgWhite, color.Bold))
   141  	console.SetColor("SummaryHdr", color.New(color.FgCyan, color.Bold))
   142  	console.SetColor("SummaryDtl", color.New(color.FgGreen, color.Bold))
   143  	coloredDot := console.Colorize("Status", dot)
   144  
   145  	nameIDMap := make(map[string]string)
   146  	var siteNames []string
   147  	info := i.SRStatusInfo
   148  
   149  	for dID := range info.Sites {
   150  		sname := strings.ToTitle(info.Sites[dID].Name)
   151  		siteNames = append(siteNames, sname)
   152  		nameIDMap[sname] = dID
   153  	}
   154  
   155  	if !info.Enabled {
   156  		messages = []string{"SiteReplication is not enabled"}
   157  		return console.Colorize("UserMessage", strings.Join(messages, "\n"))
   158  	}
   159  	sort.Strings(siteNames)
   160  	legendHdr := []string{"Site"}
   161  	legendFields := []Field{{"Entity", 15}}
   162  	for _, sname := range siteNames {
   163  		legendHdr = append(legendHdr, sname)
   164  		legendFields = append(legendFields, Field{"sname", 15})
   165  	}
   166  
   167  	if i.opts.Buckets {
   168  		messages = append(messages,
   169  			console.Colorize("SummaryHdr", "Bucket replication status:"))
   170  		switch {
   171  		case i.MaxBuckets == 0:
   172  			messages = append(messages, console.Colorize("Summary", "No Buckets present\n"))
   173  		default:
   174  			msg := console.Colorize(i.getTheme(len(info.BucketStats) == 0), fmt.Sprintf("%d/%d Buckets in sync", info.MaxBuckets-len(info.BucketStats), info.MaxBuckets)) + "\n"
   175  			messages = append(messages, fmt.Sprintf("%s  %s", coloredDot, msg))
   176  			if len(i.BucketStats) > 0 {
   177  				messages = append(messages, i.siteHeader(siteNames, "Bucket"))
   178  			}
   179  			var detailFields []Field
   180  			for b, ssMap := range i.BucketStats {
   181  				var details []string
   182  				details = append(details, b)
   183  				detailFields = append(detailFields, legendFields[0])
   184  				for _, sname := range siteNames {
   185  					detailFields = append(detailFields, legendFields[0])
   186  					dID := nameIDMap[sname]
   187  					ss := ssMap[dID]
   188  					switch {
   189  					case !ss.HasBucket:
   190  						details = append(details, fmt.Sprintf("%s ", blankCell))
   191  					case ss.OLockConfigMismatch, ss.PolicyMismatch, ss.QuotaCfgMismatch, ss.ReplicationCfgMismatch, ss.TagMismatch:
   192  						details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
   193  					default:
   194  						details = append(details, fmt.Sprintf("%s in-sync", tickCell))
   195  
   196  					}
   197  				}
   198  				messages = append(messages, newPrettyTable(" | ",
   199  					detailFields...).buildRow(details...))
   200  				messages = append(messages, "")
   201  			}
   202  		}
   203  	}
   204  	if i.opts.Policies {
   205  		messages = append(messages,
   206  			console.Colorize("SummaryHdr", "Policy replication status:"))
   207  		switch {
   208  		case i.MaxPolicies == 0:
   209  			messages = append(messages, console.Colorize("Summary", "No Policies present\n"))
   210  		default:
   211  			msg := console.Colorize(i.getTheme(len(i.PolicyStats) == 0), fmt.Sprintf("%d/%d Policies in sync", info.MaxPolicies-len(info.PolicyStats), info.MaxPolicies)) + "\n"
   212  			messages = append(messages, fmt.Sprintf("%s  %s", coloredDot, msg))
   213  
   214  			if len(i.PolicyStats) > 0 {
   215  				messages = append(messages, i.siteHeader(siteNames, "Policy"))
   216  			}
   217  			var detailFields []Field
   218  			for b, ssMap := range i.PolicyStats {
   219  				var details []string
   220  				details = append(details, b)
   221  				detailFields = append(detailFields, legendFields[0])
   222  				for _, sname := range siteNames {
   223  					detailFields = append(detailFields, legendFields[0])
   224  					dID := nameIDMap[sname]
   225  					ss := ssMap[dID]
   226  					switch {
   227  					case !ss.HasPolicy:
   228  						details = append(details, blankCell)
   229  					case ss.PolicyMismatch:
   230  						details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
   231  					default:
   232  						details = append(details, fmt.Sprintf("%s in-sync", tickCell))
   233  					}
   234  				}
   235  				messages = append(messages, newPrettyTable(" | ",
   236  					detailFields...).buildRow(details...))
   237  			}
   238  			if len(i.PolicyStats) > 0 {
   239  				messages = append(messages, "")
   240  			}
   241  		}
   242  	}
   243  	if i.opts.Users {
   244  		messages = append(messages,
   245  			console.Colorize("SummaryHdr", "User replication status:"))
   246  		switch {
   247  		case i.MaxUsers == 0:
   248  			messages = append(messages, console.Colorize("Summary", "No Users present\n"))
   249  		default:
   250  			msg := console.Colorize(i.getTheme(len(i.UserStats) == 0), fmt.Sprintf("%d/%d Users in sync", info.MaxUsers-len(i.UserStats), info.MaxUsers)) + "\n"
   251  			messages = append(messages, fmt.Sprintf("%s  %s", coloredDot, msg))
   252  
   253  			if len(i.UserStats) > 0 {
   254  				messages = append(messages, i.siteHeader(siteNames, "User"))
   255  			}
   256  			var detailFields []Field
   257  			for b, ssMap := range i.UserStats {
   258  				var details []string
   259  				details = append(details, b)
   260  				detailFields = append(detailFields, legendFields[0])
   261  				for _, sname := range siteNames {
   262  					detailFields = append(detailFields, legendFields[0])
   263  					dID := nameIDMap[sname]
   264  					ss, ok := ssMap[dID]
   265  
   266  					switch {
   267  					case !ss.HasUser:
   268  						details = append(details, blankCell)
   269  					case !ok, ss.UserInfoMismatch:
   270  						details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
   271  					default:
   272  						details = append(details, fmt.Sprintf("%s in-sync", tickCell))
   273  					}
   274  				}
   275  				messages = append(messages, newPrettyTable(" | ",
   276  					detailFields...).buildRow(details...))
   277  			}
   278  			if len(i.UserStats) > 0 {
   279  				messages = append(messages, "")
   280  			}
   281  
   282  		}
   283  	}
   284  	if i.opts.Groups {
   285  		messages = append(messages,
   286  			console.Colorize("SummaryHdr", "Group replication status:"))
   287  		switch {
   288  		case i.MaxGroups == 0:
   289  			messages = append(messages, console.Colorize("Summary", "No Groups present\n"))
   290  		default:
   291  			msg := console.Colorize(i.getTheme(len(i.GroupStats) == 0), fmt.Sprintf("%d/%d Groups in sync", i.MaxGroups-len(i.GroupStats), i.MaxGroups)) + "\n"
   292  			messages = append(messages, fmt.Sprintf("%s  %s", coloredDot, msg))
   293  
   294  			if len(i.GroupStats) > 0 {
   295  				messages = append(messages, i.siteHeader(siteNames, "Group"))
   296  			}
   297  			var detailFields []Field
   298  			for b, ssMap := range i.GroupStats {
   299  				var details []string
   300  				details = append(details, b)
   301  				detailFields = append(detailFields, legendFields[0])
   302  				for _, sname := range siteNames {
   303  					detailFields = append(detailFields, legendFields[0])
   304  					dID := nameIDMap[sname]
   305  					ss := ssMap[dID]
   306  					switch {
   307  					case !ss.HasGroup:
   308  						details = append(details, blankCell)
   309  					case ss.GroupDescMismatch:
   310  						details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
   311  					default:
   312  						details = append(details, fmt.Sprintf("%s in-sync", tickCell))
   313  					}
   314  				}
   315  				messages = append(messages, newPrettyTable(" | ",
   316  					detailFields...).buildRow(details...))
   317  			}
   318  			if len(i.GroupStats) > 0 {
   319  				messages = append(messages, "")
   320  			}
   321  		}
   322  	}
   323  	if i.opts.ILMExpiryRules {
   324  		messages = append(messages,
   325  			console.Colorize("SummaryHdr", "ILM Expiry Rules replication status:"))
   326  		switch {
   327  		case i.MaxILMExpiryRules == 0:
   328  			messages = append(messages, console.Colorize("Summary", "No ILM Expiry Rules present\n"))
   329  		case i.ILMExpiryStats == nil:
   330  			messages = append(messages, console.Colorize("Summary", "Replication of ILM Expiry is not enabled\n"))
   331  		default:
   332  			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"
   333  			messages = append(messages, fmt.Sprintf("%s  %s", coloredDot, msg))
   334  			if len(i.ILMExpiryStats) > 0 {
   335  				messages = append(messages, i.siteHeader(siteNames, "ILM Expiry Rules"))
   336  			}
   337  			var detailFields []Field
   338  			for b, ssMap := range i.ILMExpiryStats {
   339  				var details []string
   340  				details = append(details, b)
   341  				detailFields = append(detailFields, legendFields[0])
   342  				for _, sname := range siteNames {
   343  					detailFields = append(detailFields, legendFields[0])
   344  					dID := nameIDMap[sname]
   345  					ss := ssMap[dID]
   346  					switch {
   347  					case !ss.HasILMExpiryRules:
   348  						details = append(details, blankCell)
   349  					case ss.ILMExpiryRuleMismatch:
   350  						details = append(details, fmt.Sprintf("%s in-sync", crossTickCell))
   351  					default:
   352  						details = append(details, fmt.Sprintf("%s in-sync", tickCell))
   353  					}
   354  				}
   355  				messages = append(messages, newPrettyTable(" | ",
   356  					detailFields...).buildRow(details...))
   357  				messages = append(messages, "")
   358  			}
   359  		}
   360  	}
   361  
   362  	switch i.opts.Entity {
   363  	case madmin.SRBucketEntity:
   364  		messages = append(messages, i.getBucketStatusSummary(siteNames, nameIDMap, "Bucket")...)
   365  	case madmin.SRPolicyEntity:
   366  		messages = append(messages, i.getPolicyStatusSummary(siteNames, nameIDMap, "Policy")...)
   367  	case madmin.SRUserEntity:
   368  		messages = append(messages, i.getUserStatusSummary(siteNames, nameIDMap, "User")...)
   369  	case madmin.SRGroupEntity:
   370  		messages = append(messages, i.getGroupStatusSummary(siteNames, nameIDMap, "Group")...)
   371  	case madmin.SRILMExpiryRuleEntity:
   372  		messages = append(messages, i.getILMExpiryStatusSummary(siteNames, nameIDMap, "ILMExpiryRule")...)
   373  	}
   374  	if i.opts.Metrics {
   375  		uiFn := func(theme string) func(string) string {
   376  			return func(s string) string {
   377  				return console.Colorize(theme, s)
   378  			}
   379  		}
   380  		singleTgt := len(ms.Metrics) == 1
   381  		maxui := uiFn("Peak")
   382  		avgui := uiFn("Avg")
   383  		valueui := uiFn("Value")
   384  		messages = append(messages,
   385  			console.Colorize("SummaryHdr", "Object replication status:"))
   386  
   387  		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")))))
   388  		// for queue stats
   389  		coloredDot := console.Colorize("qStatusOK", dot)
   390  		if q.Curr.Count > q.Avg.Count {
   391  			coloredDot = console.Colorize("qStatusWarn", dot)
   392  		}
   393  
   394  		var replicatedCount, replicatedSize int64
   395  		for _, m := range ms.Metrics {
   396  			nodeName := m.Endpoint
   397  			nodeui := uiFn(getNodeTheme(nodeName))
   398  			messages = append(messages, nodeui(nodeName))
   399  			messages = append(messages, fmt.Sprintf("Replicated:    %s objects (%s)", humanize.Comma(int64(m.ReplicatedCount)), valueui(humanize.IBytes(uint64(m.ReplicatedSize)))))
   400  
   401  			if singleTgt { // for single target - combine summary section into the target section
   402  				messages = append(messages, fmt.Sprintf("Received:      %s objects (%s)", humanize.Comma(int64(ms.ReplicaCount)), humanize.IBytes(uint64(ms.ReplicaSize))))
   403  				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"),
   404  					humanize.Comma(int64(q.Avg.Count)), valueui(humanize.IBytes(uint64(q.Avg.Bytes))), maxui("max"),
   405  					humanize.Comma(int64(q.Max.Count)), valueui(humanize.IBytes(uint64(q.Max.Bytes)))))
   406  				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))))
   407  			} else {
   408  				replicatedCount += m.ReplicatedCount
   409  				replicatedSize += m.ReplicatedSize
   410  			}
   411  
   412  			if m.XferStats != nil {
   413  				tgtXfer, ok := m.XferStats[replication.Total]
   414  				if ok {
   415  					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)))))
   416  					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())))
   417  				}
   418  			}
   419  
   420  			healthDot := console.Colorize("online", dot)
   421  			if !m.Online {
   422  				healthDot = console.Colorize("offline", dot)
   423  			}
   424  			currDowntime := time.Duration(0)
   425  			if !m.Online && !m.LastOnline.IsZero() {
   426  				currDowntime = UTCNow().Sub(m.LastOnline)
   427  			}
   428  			// normalize because total downtime is calculated at server side at heartbeat interval, may be slightly behind
   429  			totalDowntime := m.TotalDowntime
   430  			if currDowntime > totalDowntime {
   431  				totalDowntime = currDowntime
   432  			}
   433  			var linkStatus string
   434  			if m.Online {
   435  				linkStatus = healthDot + fmt.Sprintf(" online (total downtime: %s)", timeDurationToHumanizedDuration(totalDowntime).String())
   436  			} else {
   437  				linkStatus = healthDot + fmt.Sprintf(" offline %s (total downtime: %s)", timeDurationToHumanizedDuration(currDowntime).String(), valueui(timeDurationToHumanizedDuration(totalDowntime).String()))
   438  			}
   439  			messages = append(messages, fmt.Sprintf("Link:          %s", linkStatus))
   440  			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)))))
   441  			messages = append(messages, "")
   442  		}
   443  		if !singleTgt {
   444  			messages = append(messages,
   445  				console.Colorize("SummaryHdr", "Summary:"))
   446  			messages = append(messages, fmt.Sprintf("Replicated:    %s objects (%s)", humanize.Comma(int64(replicatedCount)), valueui(humanize.IBytes(uint64(replicatedSize)))))
   447  			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"),
   448  				humanize.Comma(int64(q.Avg.Count)), valueui(humanize.IBytes(uint64(q.Avg.Bytes))), maxui("max"),
   449  				humanize.Comma(int64(q.Max.Count)), valueui(humanize.IBytes(uint64(q.Max.Bytes)))))
   450  
   451  			messages = append(messages, fmt.Sprintf("Received:      %s objects (%s)", humanize.Comma(int64(ms.ReplicaCount)), humanize.IBytes(uint64(ms.ReplicaSize))))
   452  		}
   453  	}
   454  	return console.Colorize("UserMessage", strings.Join(messages, "\n"))
   455  }
   456  
   457  func (i srStatus) siteHeader(siteNames []string, legend string) string {
   458  	legendHdr := []string{legend}
   459  	legendFields := []Field{{"Entity", 15}}
   460  	for _, sname := range siteNames {
   461  		legendHdr = append(legendHdr, sname)
   462  		legendFields = append(legendFields, Field{"sname", 15})
   463  	}
   464  	return console.Colorize("SummaryHdr", newPrettyTable(" | ",
   465  		legendFields...,
   466  	).buildRow(legendHdr...))
   467  }
   468  
   469  func (i srStatus) getTheme(match bool) string {
   470  	theme := "UserMessage"
   471  	if !match {
   472  		theme = "WarningMessage"
   473  	}
   474  	return theme
   475  }
   476  
   477  func (i srStatus) getBucketStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
   478  	var messages []string
   479  	coloredDot := console.Colorize("Status", dot)
   480  	var found bool
   481  	for _, st := range i.SRStatusInfo.BucketStats[i.opts.EntityValue] {
   482  		if st.HasBucket {
   483  			found = true
   484  			break
   485  		}
   486  	}
   487  	if !found {
   488  		messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Bucket %s not found\n", i.opts.EntityValue)))
   489  		return messages
   490  	}
   491  	messages = append(messages,
   492  		console.Colorize("SummaryHdr", fmt.Sprintf("%s  %s\n", coloredDot, console.Colorize("Summary", "Bucket config replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
   493  	siteHdr := i.siteHeader(siteNames, legend)
   494  	messages = append(messages, siteHdr)
   495  
   496  	rowLegend := []string{"Tags", "Policy", "Quota", "Retention", "Encryption", "Replication"}
   497  	detailFields := make([][]Field, len(rowLegend))
   498  
   499  	var retention, encryption, tags, bpolicies, quota, replication []string
   500  	for i, row := range rowLegend {
   501  		detailFields[i] = make([]Field, len(siteNames)+1)
   502  		detailFields[i][0] = Field{"Entity", 15}
   503  		switch i {
   504  		case 0:
   505  			tags = append(tags, row)
   506  		case 1:
   507  			bpolicies = append(bpolicies, row)
   508  		case 2:
   509  			quota = append(quota, row)
   510  		case 3:
   511  			retention = append(retention, row)
   512  		case 4:
   513  			encryption = append(encryption, row)
   514  		case 5:
   515  			replication = append(replication, row)
   516  		}
   517  	}
   518  	rows := make([]string, len(rowLegend))
   519  	for j, sname := range siteNames {
   520  		dID := nameIDMap[sname]
   521  		ss := i.SRStatusInfo.BucketStats[i.opts.EntityValue][dID]
   522  		var theme, msgStr string
   523  		for r := range rowLegend {
   524  			switch r {
   525  			case 0:
   526  				theme, msgStr = syncStatus(ss.TagMismatch, ss.HasTagsSet)
   527  				tags = append(tags, msgStr)
   528  				detailFields[r][j+1] = Field{theme, fieldLen}
   529  			case 1:
   530  				theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicySet)
   531  				bpolicies = append(bpolicies, msgStr)
   532  				detailFields[r][j+1] = Field{theme, fieldLen}
   533  			case 2:
   534  				theme, msgStr = syncStatus(ss.QuotaCfgMismatch, ss.HasQuotaCfgSet)
   535  				quota = append(quota, msgStr)
   536  				detailFields[r][j+1] = Field{theme, fieldLen}
   537  			case 3:
   538  				theme, msgStr = syncStatus(ss.OLockConfigMismatch, ss.HasOLockConfigSet)
   539  				retention = append(retention, msgStr)
   540  				detailFields[r][j+1] = Field{theme, fieldLen}
   541  			case 4:
   542  				theme, msgStr = syncStatus(ss.SSEConfigMismatch, ss.HasSSECfgSet)
   543  				encryption = append(encryption, msgStr)
   544  				detailFields[r][j+1] = Field{theme, fieldLen}
   545  			case 5:
   546  				theme, msgStr = syncStatus(ss.ReplicationCfgMismatch, ss.HasReplicationCfg)
   547  				replication = append(replication, msgStr)
   548  				detailFields[r][j+1] = Field{theme, fieldLen}
   549  
   550  			}
   551  		}
   552  	}
   553  	for r := range rowLegend {
   554  		switch r {
   555  		case 0:
   556  			rows[r] = newPrettyTable(" | ",
   557  				detailFields[r]...).buildRow(tags...)
   558  		case 1:
   559  			rows[r] = newPrettyTable(" | ",
   560  				detailFields[r]...).buildRow(bpolicies...)
   561  		case 2:
   562  			rows[r] = newPrettyTable(" | ",
   563  				detailFields[r]...).buildRow(quota...)
   564  		case 3:
   565  			rows[r] = newPrettyTable(" | ",
   566  				detailFields[r]...).buildRow(retention...)
   567  		case 4:
   568  			rows[r] = newPrettyTable(" | ",
   569  				detailFields[r]...).buildRow(encryption...)
   570  		case 5:
   571  			rows[r] = newPrettyTable(" | ",
   572  				detailFields[r]...).buildRow(replication...)
   573  
   574  		}
   575  	}
   576  	messages = append(messages, rows...)
   577  
   578  	return messages
   579  }
   580  
   581  func (i srStatus) getPolicyStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
   582  	var messages []string
   583  	coloredDot := console.Colorize("Status", dot)
   584  	var found bool
   585  	for _, st := range i.SRStatusInfo.PolicyStats[i.opts.EntityValue] {
   586  		if st.HasPolicy {
   587  			found = true
   588  			break
   589  		}
   590  	}
   591  	if !found {
   592  		messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Policy %s not found\n", i.opts.EntityValue)))
   593  		return messages
   594  	}
   595  
   596  	rowLegend := []string{"Policy"}
   597  	detailFields := make([][]Field, len(rowLegend))
   598  
   599  	var policies []string
   600  	detailFields[0] = make([]Field, len(siteNames)+1)
   601  	detailFields[0][0] = Field{"Entity", 15}
   602  	policies = append(policies, "Policy")
   603  	rows := make([]string, len(rowLegend))
   604  	for j, sname := range siteNames {
   605  		dID := nameIDMap[sname]
   606  		ss := i.SRStatusInfo.PolicyStats[i.opts.EntityValue][dID]
   607  		var theme, msgStr string
   608  		for r := range rowLegend {
   609  			switch r {
   610  			case 0:
   611  				theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicy)
   612  				policies = append(policies, msgStr)
   613  				detailFields[r][j+1] = Field{theme, fieldLen}
   614  			}
   615  		}
   616  	}
   617  	for r := range rowLegend {
   618  		switch r {
   619  		case 0:
   620  			rows[r] = newPrettyTable(" | ",
   621  				detailFields[r]...).buildRow(policies...)
   622  		}
   623  	}
   624  	messages = append(messages,
   625  		console.Colorize("SummaryHdr", fmt.Sprintf("%s  %s\n", coloredDot, console.Colorize("Summary", "Policy replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
   626  	siteHdr := i.siteHeader(siteNames, legend)
   627  	messages = append(messages, siteHdr)
   628  
   629  	messages = append(messages, rows...)
   630  	return messages
   631  }
   632  
   633  func (i srStatus) getUserStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
   634  	var messages []string
   635  	coloredDot := console.Colorize("Status", dot)
   636  	var found bool
   637  	for _, st := range i.SRStatusInfo.UserStats[i.opts.EntityValue] {
   638  		if st.HasUser {
   639  			found = true
   640  			break
   641  		}
   642  	}
   643  	if !found {
   644  		messages = append(messages, console.Colorize("Summary", fmt.Sprintf("User %s not found\n", i.opts.EntityValue)))
   645  		return messages
   646  	}
   647  
   648  	rowLegend := []string{"Info", "Policy mapping"}
   649  	detailFields := make([][]Field, len(rowLegend))
   650  
   651  	var users, policyMapping []string
   652  	for i, row := range rowLegend {
   653  		detailFields[i] = make([]Field, len(siteNames)+1)
   654  		detailFields[i][0] = Field{"Entity", 15}
   655  		switch i {
   656  		case 0:
   657  			users = append(users, row)
   658  		default:
   659  			policyMapping = append(policyMapping, row)
   660  		}
   661  	}
   662  	rows := make([]string, len(rowLegend))
   663  	for j, sname := range siteNames {
   664  		dID := nameIDMap[sname]
   665  		ss := i.SRStatusInfo.UserStats[i.opts.EntityValue][dID]
   666  		var theme, msgStr string
   667  		for r := range rowLegend {
   668  			switch r {
   669  			case 0:
   670  				theme, msgStr = syncStatus(ss.UserInfoMismatch, ss.HasUser)
   671  				users = append(users, msgStr)
   672  				detailFields[r][j+1] = Field{theme, fieldLen}
   673  			case 1:
   674  				theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicyMapping)
   675  				policyMapping = append(policyMapping, msgStr)
   676  				detailFields[r][j+1] = Field{theme, fieldLen}
   677  			}
   678  		}
   679  	}
   680  	for r := range rowLegend {
   681  		switch r {
   682  		case 0:
   683  			rows[r] = newPrettyTable(" | ",
   684  				detailFields[r]...).buildRow(users...)
   685  		case 1:
   686  			rows[r] = newPrettyTable(" | ",
   687  				detailFields[r]...).buildRow(policyMapping...)
   688  		}
   689  	}
   690  	messages = append(messages,
   691  		console.Colorize("SummaryHdr", fmt.Sprintf("%s  %s\n", coloredDot, console.Colorize("Summary", "User replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
   692  	siteHdr := i.siteHeader(siteNames, legend)
   693  	messages = append(messages, siteHdr)
   694  
   695  	messages = append(messages, rows...)
   696  	return messages
   697  }
   698  
   699  func (i srStatus) getGroupStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
   700  	var messages []string
   701  	coloredDot := console.Colorize("Status", dot)
   702  	rowLegend := []string{"Info", "Policy mapping"}
   703  	detailFields := make([][]Field, len(rowLegend))
   704  	var found bool
   705  	for _, st := range i.SRStatusInfo.GroupStats[i.opts.EntityValue] {
   706  		if st.HasGroup {
   707  			found = true
   708  			break
   709  		}
   710  	}
   711  	if !found {
   712  		messages = append(messages, console.Colorize("Summary", fmt.Sprintf("Group %s not found\n", i.opts.EntityValue)))
   713  		return messages
   714  	}
   715  
   716  	var groups, policyMapping []string
   717  	for i, row := range rowLegend {
   718  		detailFields[i] = make([]Field, len(siteNames)+1)
   719  		detailFields[i][0] = Field{"Entity", 15}
   720  		switch i {
   721  		case 0:
   722  			groups = append(groups, row)
   723  		default:
   724  			policyMapping = append(policyMapping, row)
   725  		}
   726  	}
   727  	rows := make([]string, len(rowLegend))
   728  	// b := i.opts.EntityValue
   729  	for j, sname := range siteNames {
   730  		dID := nameIDMap[sname]
   731  		ss := i.SRStatusInfo.GroupStats[i.opts.EntityValue][dID]
   732  		// sm := i.SRStatusInfo.StatsSummary
   733  		var theme, msgStr string
   734  		for r := range rowLegend {
   735  			switch r {
   736  			case 0:
   737  				theme, msgStr = syncStatus(ss.GroupDescMismatch, ss.HasGroup)
   738  				groups = append(groups, msgStr)
   739  				detailFields[r][j+1] = Field{theme, fieldLen}
   740  			case 1:
   741  				theme, msgStr = syncStatus(ss.PolicyMismatch, ss.HasPolicyMapping)
   742  				policyMapping = append(policyMapping, msgStr)
   743  				detailFields[r][j+1] = Field{theme, fieldLen}
   744  			}
   745  		}
   746  	}
   747  	for r := range rowLegend {
   748  		switch r {
   749  		case 0:
   750  			rows[r] = newPrettyTable(" | ",
   751  				detailFields[r]...).buildRow(groups...)
   752  		case 1:
   753  			rows[r] = newPrettyTable(" | ",
   754  				detailFields[r]...).buildRow(policyMapping...)
   755  		}
   756  	}
   757  	messages = append(messages,
   758  		console.Colorize("SummaryHdr", fmt.Sprintf("%s  %s\n", coloredDot, console.Colorize("Summary", "Group replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
   759  	siteHdr := i.siteHeader(siteNames, legend)
   760  	messages = append(messages, siteHdr)
   761  
   762  	messages = append(messages, rows...)
   763  	return messages
   764  }
   765  
   766  func (i srStatus) getILMExpiryStatusSummary(siteNames []string, nameIDMap map[string]string, legend string) []string {
   767  	var messages []string
   768  	coloredDot := console.Colorize("Status", dot)
   769  	var found bool
   770  	for _, st := range i.SRStatusInfo.ILMExpiryStats[i.opts.EntityValue] {
   771  		if st.HasILMExpiryRules {
   772  			found = true
   773  			break
   774  		}
   775  	}
   776  	if !found {
   777  		messages = append(messages, console.Colorize("Summary", fmt.Sprintf("ILM Expiry Rule %s not found\n", i.opts.EntityValue)))
   778  		return messages
   779  	}
   780  
   781  	rowLegend := []string{"ILM Expiry Rule"}
   782  	detailFields := make([][]Field, len(rowLegend))
   783  
   784  	var rules []string
   785  	detailFields[0] = make([]Field, len(siteNames)+1)
   786  	detailFields[0][0] = Field{"Entity", 15}
   787  	rules = append(rules, "ILM Expiry Rule")
   788  	rows := make([]string, len(rowLegend))
   789  	for j, sname := range siteNames {
   790  		dID := nameIDMap[sname]
   791  		ss := i.SRStatusInfo.ILMExpiryStats[i.opts.EntityValue][dID]
   792  		var theme, msgStr string
   793  		for r := range rowLegend {
   794  			switch r {
   795  			case 0:
   796  				theme, msgStr = syncStatus(ss.ILMExpiryRuleMismatch, ss.HasILMExpiryRules)
   797  				rules = append(rules, msgStr)
   798  				detailFields[r][j+1] = Field{theme, fieldLen}
   799  			}
   800  		}
   801  	}
   802  	for r := range rowLegend {
   803  		switch r {
   804  		case 0:
   805  			rows[r] = newPrettyTable(" | ",
   806  				detailFields[r]...).buildRow(rules...)
   807  		}
   808  	}
   809  	messages = append(messages,
   810  		console.Colorize("SummaryHdr", fmt.Sprintf("%s  %s\n", coloredDot, console.Colorize("Summary", "ILM Expiry Rule replication summary for: ")+console.Colorize("UserMessage", i.opts.EntityValue))))
   811  	siteHdr := i.siteHeader(siteNames, legend)
   812  	messages = append(messages, siteHdr)
   813  
   814  	messages = append(messages, rows...)
   815  	return messages
   816  }
   817  
   818  // Calculate srstatus options for command line flags
   819  func srStatusOpts(ctx *cli.Context) (opts madmin.SRStatusOptions) {
   820  	if !(ctx.IsSet("buckets") ||
   821  		ctx.IsSet("users") ||
   822  		ctx.IsSet("groups") ||
   823  		ctx.IsSet("policies") ||
   824  		ctx.IsSet("ilm-expiry-rules") ||
   825  		ctx.IsSet("bucket") ||
   826  		ctx.IsSet("user") ||
   827  		ctx.IsSet("group") ||
   828  		ctx.IsSet("policy") ||
   829  		ctx.IsSet("ilm-expiry-rule") ||
   830  		ctx.IsSet("all")) || ctx.IsSet("all") {
   831  		opts.Buckets = true
   832  		opts.Users = true
   833  		opts.Groups = true
   834  		opts.Policies = true
   835  		opts.Metrics = true
   836  		opts.ILMExpiryRules = true
   837  		return
   838  	}
   839  	opts.Buckets = ctx.Bool("buckets")
   840  	opts.Policies = ctx.Bool("policies")
   841  	opts.Users = ctx.Bool("users")
   842  	opts.Groups = ctx.Bool("groups")
   843  	opts.ILMExpiryRules = ctx.Bool("ilm-expiry-rules")
   844  	for _, name := range []string{"bucket", "user", "group", "policy", "ilm-expiry-rule"} {
   845  		if ctx.IsSet(name) {
   846  			opts.Entity = madmin.GetSREntityType(name)
   847  			opts.EntityValue = ctx.String(name)
   848  			break
   849  		}
   850  	}
   851  	return
   852  }
   853  
   854  func mainAdminReplicationStatus(ctx *cli.Context) error {
   855  	{
   856  		// Check argument count
   857  		argsNr := len(ctx.Args())
   858  		if argsNr != 1 {
   859  			fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
   860  				"Need exactly one alias argument.")
   861  		}
   862  		groupStatus := ctx.IsSet("buckets") || ctx.IsSet("groups") || ctx.IsSet("users") || ctx.IsSet("policies") || ctx.IsSet("ilm-expiry-rules")
   863  		indivStatus := ctx.IsSet("bucket") || ctx.IsSet("group") || ctx.IsSet("user") || ctx.IsSet("policy") || ctx.IsSet("ilm-expiry-rule")
   864  		if groupStatus && indivStatus {
   865  			fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
   866  				"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)")
   867  		}
   868  		setSlc := []bool{ctx.IsSet("bucket"), ctx.IsSet("user"), ctx.IsSet("group"), ctx.IsSet("policy"), ctx.IsSet("ilm-expiry-rule")}
   869  		count := 0
   870  		for _, s := range setSlc {
   871  			if s {
   872  				count++
   873  			}
   874  		}
   875  		if count > 1 {
   876  			fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
   877  				"Cannot specify more than one of --bucket, --policy, --user, --group, --ilm-expiry-rule  flags at the same time")
   878  		}
   879  	}
   880  
   881  	console.SetColor("UserMessage", color.New(color.FgGreen))
   882  	console.SetColor("WarningMessage", color.New(color.FgYellow))
   883  	for _, c := range colors {
   884  		console.SetColor(fmt.Sprintf("Node%d", c), color.New(c, color.Bold))
   885  	}
   886  	console.SetColor("Replicated", color.New(color.FgCyan))
   887  	console.SetColor("In-Queue", color.New(color.Bold, color.FgYellow))
   888  	console.SetColor("Avg", color.New(color.FgCyan))
   889  	console.SetColor("Peak", color.New(color.FgYellow))
   890  	console.SetColor("Value", color.New(color.FgWhite, color.Bold))
   891  
   892  	console.SetColor("Current", color.New(color.FgCyan))
   893  	console.SetColor("Uptime", color.New(color.Bold, color.FgWhite))
   894  	console.SetColor("UptimeStr", color.New(color.FgHiWhite))
   895  
   896  	console.SetColor("qStatusWarn", color.New(color.FgYellow, color.Bold))
   897  	console.SetColor("qStatusOK", color.New(color.FgGreen, color.Bold))
   898  	console.SetColor("online", color.New(color.FgGreen, color.Bold))
   899  	console.SetColor("offline", color.New(color.FgRed, color.Bold))
   900  
   901  	// Get the alias parameter from cli
   902  	args := ctx.Args()
   903  	aliasedURL := args.Get(0)
   904  
   905  	// Create a new MinIO Admin Client
   906  	client, err := newAdminClient(aliasedURL)
   907  	fatalIf(err, "Unable to initialize admin connection.")
   908  	opts := srStatusOpts(ctx)
   909  	info, e := client.SRStatusInfo(globalContext, opts)
   910  	fatalIf(probe.NewError(e).Trace(args...), "Unable to get cluster replication status")
   911  
   912  	printMsg(srStatus{
   913  		SRStatusInfo: info,
   914  		opts:         opts,
   915  	})
   916  
   917  	return nil
   918  }
   919  
   920  func syncStatus(mismatch, set bool) (string, string) {
   921  	if !set {
   922  		return "Entity", blankCell
   923  	}
   924  	if mismatch {
   925  		return "Entity", crossTickCell
   926  	}
   927  
   928  	return "Entity", tickCell
   929  }