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  }