github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/admin-replicate-resync-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 "context" 22 "errors" 23 "fmt" 24 "strings" 25 "time" 26 27 "github.com/charmbracelet/bubbles/spinner" 28 tea "github.com/charmbracelet/bubbletea" 29 "github.com/charmbracelet/lipgloss" 30 "github.com/dustin/go-humanize" 31 "github.com/fatih/color" 32 "github.com/minio/cli" 33 "github.com/minio/madmin-go/v3" 34 "github.com/minio/mc/pkg/probe" 35 "github.com/minio/pkg/v2/console" 36 "github.com/olekukonko/tablewriter" 37 ) 38 39 var adminReplicateResyncStatusCmd = cli.Command{ 40 Name: "status", 41 Usage: "show site replication resync status", 42 Action: mainAdminReplicationResyncStatus, 43 OnUsageError: onUsageError, 44 Before: setGlobalsFromContext, 45 Flags: globalFlags, 46 CustomHelpTemplate: `NAME: 47 {{.HelpName}} - {{.Usage}} 48 49 USAGE: 50 {{.HelpName}} ALIAS1 ALIAS2 51 52 FLAGS: 53 {{range .VisibleFlags}}{{.}} 54 {{end}} 55 56 EXAMPLES: 57 1. Display status of resync from minio1 to minio2 58 {{.Prompt}} {{.HelpName}} minio1 minio2 59 `, 60 } 61 62 func mainAdminReplicationResyncStatus(ctx *cli.Context) error { 63 { 64 // Check argument count 65 argsNr := len(ctx.Args()) 66 if argsNr != 2 { 67 cli.ShowCommandHelpAndExit(ctx, "status", 1) // last argument is exit code 68 } 69 } 70 71 console.SetColor("ResyncMessage", color.New(color.FgGreen)) 72 console.SetColor("THeader", color.New(color.Bold, color.FgHiWhite)) 73 console.SetColor("THeader2", color.New(color.Bold, color.FgYellow)) 74 console.SetColor("TDetail", color.New(color.Bold, color.FgCyan)) 75 76 // Get the alias parameter from cli 77 args := ctx.Args() 78 aliasedURL := args.Get(0) 79 80 // Create a new MinIO Admin Client 81 client, err := newAdminClient(aliasedURL) 82 fatalIf(err, "Unable to initialize admin connection.") 83 info, e := client.SiteReplicationInfo(globalContext) 84 fatalIf(probe.NewError(e), "Unable to fetch site replication info.") 85 if !info.Enabled { 86 console.Colorize("ResyncMessage", "SiteReplication is not enabled") 87 return nil 88 } 89 90 peerClient := getClient(args.Get(1)) 91 peerAdmInfo, e := peerClient.ServerInfo(globalContext) 92 fatalIf(probe.NewError(e), "Unable to fetch server info of the peer.") 93 94 var peer madmin.PeerInfo 95 for _, site := range info.Sites { 96 if peerAdmInfo.DeploymentID == site.DeploymentID { 97 peer = site 98 } 99 } 100 if peer.DeploymentID == "" { 101 fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...), 102 "alias provided is not part of cluster replication.") 103 } 104 ctxt, cancel := context.WithCancel(globalContext) 105 defer cancel() 106 107 ui := tea.NewProgram(initResyncMetricsUI(peer.DeploymentID)) 108 go func() { 109 opts := madmin.MetricsOptions{ 110 Type: madmin.MetricsSiteResync, 111 ByDepID: peer.DeploymentID, 112 } 113 e := client.Metrics(ctxt, opts, func(metrics madmin.RealtimeMetrics) { 114 if globalJSON { 115 printMsg(metricsMessage{RealtimeMetrics: metrics}) 116 return 117 } 118 if metrics.Aggregated.SiteResync != nil { 119 sr := metrics.Aggregated.SiteResync 120 ui.Send(sr) 121 if sr.Complete() { 122 cancel() 123 return 124 } 125 } 126 }) 127 if e != nil && !errors.Is(e, context.Canceled) { 128 fatalIf(probe.NewError(e).Trace(ctx.Args()...), "Unable to get resync status") 129 } 130 }() 131 132 if !globalJSON { 133 if _, e := ui.Run(); e != nil { 134 cancel() 135 fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to get resync status") 136 } 137 } 138 139 return nil 140 } 141 142 func initResyncMetricsUI(deplID string) *resyncMetricsUI { 143 s := spinner.New() 144 s.Spinner = spinner.Points 145 s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) 146 return &resyncMetricsUI{ 147 spinner: s, 148 deplID: deplID, 149 } 150 } 151 152 type resyncMetricsUI struct { 153 current madmin.SiteResyncMetrics 154 spinner spinner.Model 155 quitting bool 156 deplID string 157 } 158 159 func (m *resyncMetricsUI) Init() tea.Cmd { 160 return m.spinner.Tick 161 } 162 163 func (m *resyncMetricsUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 164 switch msg := msg.(type) { 165 case tea.KeyMsg: 166 switch msg.String() { 167 case "ctrl+c": 168 m.quitting = true 169 return m, tea.Quit 170 default: 171 return m, nil 172 } 173 case *madmin.SiteResyncMetrics: 174 m.current = *msg 175 if msg.ResyncStatus == "Canceled" { 176 m.quitting = true 177 return m, tea.Quit 178 } 179 if msg.Complete() { 180 m.quitting = true 181 return m, tea.Quit 182 } 183 return m, nil 184 case spinner.TickMsg: 185 var cmd tea.Cmd 186 m.spinner, cmd = m.spinner.Update(msg) 187 return m, cmd 188 default: 189 return m, nil 190 } 191 } 192 193 func (m *resyncMetricsUI) View() string { 194 var s strings.Builder 195 196 // Set table header 197 table := tablewriter.NewWriter(&s) 198 table.SetAutoWrapText(false) 199 table.SetAutoFormatHeaders(true) 200 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 201 table.SetAlignment(tablewriter.ALIGN_LEFT) 202 table.SetCenterSeparator("") 203 table.SetColumnSeparator("") 204 table.SetRowSeparator("") 205 table.SetHeaderLine(false) 206 table.SetBorder(false) 207 table.SetTablePadding("\t") // pad with tabs 208 table.SetNoWhiteSpace(true) 209 210 var data [][]string 211 addLine := func(prefix string, value interface{}) { 212 data = append(data, []string{ 213 prefix, 214 whiteStyle.Render(fmt.Sprint(value)), 215 }) 216 } 217 218 if !m.quitting { 219 s.WriteString(m.spinner.View()) 220 } else { 221 if m.current.Complete() { 222 if m.current.FailedCount == 0 { 223 s.WriteString(m.spinner.Style.Render((tickCell + tickCell + tickCell))) 224 } else { 225 s.WriteString(m.spinner.Style.Render((crossTickCell + crossTickCell + crossTickCell))) 226 } 227 } 228 } 229 s.WriteString("\n") 230 if m.current.ResyncID != "" { 231 accElapsedTime := m.current.LastUpdate.Sub(m.current.StartTime) 232 addLine("ResyncID: ", m.current.ResyncID) 233 addLine("Status: ", m.current.ResyncStatus) 234 235 addLine("Objects: ", m.current.ReplicatedCount) 236 addLine("Versions: ", m.current.ReplicatedCount) 237 addLine("FailedObjects: ", m.current.FailedCount) 238 if accElapsedTime > 0 { 239 bytesTransferredPerSec := float64(int64(time.Second)*m.current.ReplicatedSize) / float64(accElapsedTime) 240 objectsPerSec := float64(int64(time.Second)*m.current.ReplicatedCount) / float64(accElapsedTime) 241 addLine("Throughput: ", fmt.Sprintf("%s/s", humanize.IBytes(uint64(bytesTransferredPerSec)))) 242 addLine("IOPs: ", fmt.Sprintf("%.2f objs/s", objectsPerSec)) 243 } 244 addLine("Transferred: ", humanize.IBytes(uint64(m.current.ReplicatedSize))) 245 addLine("Elapsed: ", accElapsedTime.String()) 246 addLine("CurrObjName: ", fmt.Sprintf("%s/%s", m.current.Bucket, m.current.Object)) 247 } 248 table.AppendBulk(data) 249 table.Render() 250 251 if m.quitting { 252 s.WriteString("\n") 253 } 254 255 return s.String() 256 }