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 }