github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/admin-service-restart.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  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/fatih/color"
    27  	"github.com/jedib0t/go-pretty/v6/table"
    28  	"github.com/jedib0t/go-pretty/v6/text"
    29  	"github.com/minio/cli"
    30  	json "github.com/minio/colorjson"
    31  	"github.com/minio/madmin-go/v3"
    32  	"github.com/minio/mc/pkg/probe"
    33  	"github.com/minio/pkg/v2/console"
    34  )
    35  
    36  var serviceRestartFlag = []cli.Flag{
    37  	cli.BoolFlag{
    38  		Name:  "dry-run",
    39  		Usage: "do not attempt a restart, however verify the peer status",
    40  	},
    41  }
    42  
    43  var adminServiceRestartCmd = cli.Command{
    44  	Name:         "restart",
    45  	Usage:        "restart a MinIO cluster",
    46  	Action:       mainAdminServiceRestart,
    47  	OnUsageError: onUsageError,
    48  	Before:       setGlobalsFromContext,
    49  	Flags:        append(serviceRestartFlag, globalFlags...),
    50  	CustomHelpTemplate: `NAME:
    51    {{.HelpName}} - {{.Usage}}
    52  
    53  USAGE:
    54    {{.HelpName}} TARGET
    55  
    56  FLAGS:
    57    {{range .VisibleFlags}}{{.}}
    58    {{end}}
    59  EXAMPLES:
    60    1. Restart MinIO server represented by its alias 'play'.
    61       {{.Prompt}} {{.HelpName}} play/
    62  `,
    63  }
    64  
    65  // serviceRestartCommand is container for service restart command success and failure messages.
    66  type serviceRestartCommand struct {
    67  	Status    string                     `json:"status"`
    68  	ServerURL string                     `json:"serverURL"`
    69  	Result    madmin.ServiceActionResult `json:"result"`
    70  }
    71  
    72  // String colorized service restart command message.
    73  func (s serviceRestartCommand) String() string {
    74  	var s1 strings.Builder
    75  	s1.WriteString("Restart command successfully sent to `" + s.ServerURL + "`. Type Ctrl-C to quit or wait to follow the status of the restart process.")
    76  
    77  	if len(s.Result.Results) > 0 {
    78  		s1.WriteString("\n")
    79  		var rows []table.Row
    80  		for _, peerRes := range s.Result.Results {
    81  			errStr := tickCell
    82  			if peerRes.Err != "" {
    83  				errStr = peerRes.Err
    84  			} else if len(peerRes.WaitingDrives) > 0 {
    85  				errStr = fmt.Sprintf("%d drives are waiting for I/O and are offline, manual restart of OS is recommended", len(peerRes.WaitingDrives))
    86  			}
    87  			rows = append(rows, table.Row{peerRes.Host, errStr})
    88  		}
    89  
    90  		t := table.NewWriter()
    91  		t.SetOutputMirror(&s1)
    92  		t.SetColumnConfigs([]table.ColumnConfig{{Align: text.AlignCenter}})
    93  
    94  		t.AppendHeader(table.Row{"Host", "Status"})
    95  		t.AppendRows(rows)
    96  		t.SetStyle(table.StyleLight)
    97  		t.Render()
    98  	}
    99  
   100  	return console.Colorize("ServiceRestart", s1.String())
   101  }
   102  
   103  // JSON jsonified service restart command message.
   104  func (s serviceRestartCommand) JSON() string {
   105  	serviceRestartJSONBytes, e := json.MarshalIndent(s, "", " ")
   106  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   107  
   108  	return string(serviceRestartJSONBytes)
   109  }
   110  
   111  // serviceRestartMessage is container for service restart success and failure messages.
   112  type serviceRestartMessage struct {
   113  	Status    string        `json:"status"`
   114  	ServerURL string        `json:"serverURL"`
   115  	TimeTaken time.Duration `json:"timeTaken"`
   116  	Err       error         `json:"error,omitempty"`
   117  }
   118  
   119  // String colorized service restart message.
   120  func (s serviceRestartMessage) String() string {
   121  	if s.Err == nil {
   122  		return console.Colorize("ServiceRestart", fmt.Sprintf("\nRestarted `%s` successfully in %s", s.ServerURL, timeDurationToHumanizedDuration(s.TimeTaken).StringShort()))
   123  	}
   124  	return console.Colorize("FailedServiceRestart", "Failed to restart `"+s.ServerURL+"`. error: "+s.Err.Error())
   125  }
   126  
   127  // JSON jsonified service restart message.
   128  func (s serviceRestartMessage) JSON() string {
   129  	serviceRestartJSONBytes, e := json.MarshalIndent(s, "", " ")
   130  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   131  
   132  	return string(serviceRestartJSONBytes)
   133  }
   134  
   135  // checkAdminServiceRestartSyntax - validate all the passed arguments
   136  func checkAdminServiceRestartSyntax(ctx *cli.Context) {
   137  	if len(ctx.Args()) != 1 {
   138  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   139  	}
   140  }
   141  
   142  func mainAdminServiceRestart(ctx *cli.Context) error {
   143  	// Validate serivce restart syntax.
   144  	checkAdminServiceRestartSyntax(ctx)
   145  
   146  	ctxt, cancel := context.WithCancel(globalContext)
   147  	defer cancel()
   148  
   149  	// Set color.
   150  	console.SetColor("ServiceOffline", color.New(color.FgRed, color.Bold))
   151  	console.SetColor("ServiceInitializing", color.New(color.FgYellow, color.Bold))
   152  	console.SetColor("ServiceRestart", color.New(color.FgGreen, color.Bold))
   153  	console.SetColor("FailedServiceRestart", color.New(color.FgRed, color.Bold))
   154  
   155  	// Get the alias parameter from cli
   156  	args := ctx.Args()
   157  	aliasedURL := args.Get(0)
   158  
   159  	client, err := newAdminClient(aliasedURL)
   160  	fatalIf(err, "Unable to initialize admin connection.")
   161  
   162  	// Restart the specified MinIO server
   163  	result, e := client.ServiceAction(ctxt, madmin.ServiceActionOpts{
   164  		Action: madmin.ServiceActionRestart,
   165  		DryRun: ctx.Bool("dry-run"),
   166  	})
   167  	if e != nil {
   168  		// Attempt an older API server might be old
   169  		e = client.ServiceRestart(ctxt)
   170  	}
   171  	fatalIf(probe.NewError(e), "Unable to restart the server.")
   172  
   173  	// Success..
   174  	printMsg(serviceRestartCommand{Status: "success", ServerURL: aliasedURL, Result: result})
   175  
   176  	// Start pinging the service until it is ready
   177  
   178  	anonClient, err := newAnonymousClient(aliasedURL)
   179  	fatalIf(err.Trace(aliasedURL), "Could not ping `"+aliasedURL+"`.")
   180  
   181  	coloring := color.New(color.FgRed)
   182  	mark := "..."
   183  
   184  	// Print restart progress
   185  	printProgress := func() {
   186  		if !globalQuiet && !globalJSON {
   187  			coloring.Printf(mark)
   188  		}
   189  	}
   190  
   191  	printProgress()
   192  	mark = "."
   193  
   194  	t := time.Now()
   195  	for {
   196  		healthCtx, healthCancel := context.WithTimeout(ctxt, 2*time.Second)
   197  
   198  		// Fetch the health status of the specified MinIO server
   199  		healthResult, healthErr := anonClient.Healthy(healthCtx, madmin.HealthOpts{})
   200  		healthCancel()
   201  
   202  		switch {
   203  		case healthErr == nil && healthResult.Healthy:
   204  			printMsg(serviceRestartMessage{
   205  				Status:    "success",
   206  				ServerURL: aliasedURL,
   207  				TimeTaken: time.Since(t),
   208  			})
   209  			return nil
   210  		case healthErr == nil && !healthResult.Healthy:
   211  			coloring = color.New(color.FgYellow)
   212  			mark = "!"
   213  			fallthrough
   214  		default:
   215  			printProgress()
   216  		}
   217  
   218  		select {
   219  		case <-ctxt.Done():
   220  			return ctxt.Err()
   221  		default:
   222  			time.Sleep(500 * time.Millisecond)
   223  		}
   224  	}
   225  }