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

     1  // Copyright (c) 2015-2023 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  	"errors"
    22  	"fmt"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/dustin/go-humanize"
    29  	"github.com/dustin/go-humanize/english"
    30  	"github.com/fatih/color"
    31  	"github.com/minio/cli"
    32  	json "github.com/minio/colorjson"
    33  	"github.com/minio/madmin-go/v3"
    34  	"github.com/minio/mc/pkg/probe"
    35  	"github.com/minio/minio-go/v7/pkg/set"
    36  	"github.com/minio/pkg/v2/console"
    37  )
    38  
    39  var adminInfoCmd = cli.Command{
    40  	Name:         "info",
    41  	Usage:        "display MinIO server information",
    42  	Action:       mainAdminInfo,
    43  	OnUsageError: onUsageError,
    44  	Before:       setGlobalsFromContext,
    45  	Flags:        globalFlags,
    46  	CustomHelpTemplate: `NAME:
    47    {{.HelpName}} - {{.Usage}}
    48  
    49  USAGE:
    50    {{.HelpName}} TARGET
    51  
    52  FLAGS:
    53    {{range .VisibleFlags}}{{.}}
    54    {{end}}
    55  EXAMPLES:
    56    1. Get server information of the 'play' MinIO server.
    57       {{.Prompt}} {{.HelpName}} play/
    58  `,
    59  }
    60  
    61  type poolSummary struct {
    62  	index          int
    63  	setsCount      int
    64  	drivesPerSet   int
    65  	driveTolerance int
    66  	endpoints      set.StringSet
    67  }
    68  
    69  type clusterInfo map[int]*poolSummary
    70  
    71  func clusterSummaryInfo(info madmin.InfoMessage) clusterInfo {
    72  	summary := make(clusterInfo)
    73  
    74  	for _, srv := range info.Servers {
    75  		for _, disk := range srv.Disks {
    76  			pool := summary[disk.PoolIndex]
    77  			if pool == nil {
    78  				pool = &poolSummary{
    79  					index:          disk.PoolIndex,
    80  					endpoints:      set.NewStringSet(),
    81  					driveTolerance: info.StandardParity(),
    82  				}
    83  			}
    84  			// Deprecated calculation based on disk location
    85  			if disk.SetIndex+1 > pool.setsCount {
    86  				pool.setsCount = disk.SetIndex + 1
    87  			}
    88  			// Deprecated calculation based on disk location
    89  			if disk.DiskIndex+1 > pool.drivesPerSet {
    90  				pool.drivesPerSet = disk.DiskIndex + 1
    91  			}
    92  			pool.endpoints.Add(srv.Endpoint)
    93  			summary[disk.PoolIndex] = pool
    94  		}
    95  	}
    96  
    97  	if len(info.Backend.TotalSets) > 0 { // Check if this is a recent enough MinIO version
    98  		for _, pool := range summary {
    99  			pool.setsCount = info.Backend.TotalSets[pool.index]
   100  			pool.drivesPerSet = info.Backend.DrivesPerSet[pool.index]
   101  		}
   102  	}
   103  	return summary
   104  }
   105  
   106  func endpointToPools(endpoint string, c clusterInfo) (pools []int) {
   107  	for poolNumber, poolSummary := range c {
   108  		if poolSummary.endpoints.Contains(endpoint) {
   109  			pools = append(pools, poolNumber)
   110  		}
   111  	}
   112  	sort.Ints(pools)
   113  	return
   114  }
   115  
   116  // Wrap "Info" message together with fields "Status" and "Error"
   117  type clusterStruct struct {
   118  	Status string             `json:"status"`
   119  	Error  string             `json:"error,omitempty"`
   120  	Info   madmin.InfoMessage `json:"info,omitempty"`
   121  }
   122  
   123  // String provides colorized info messages depending on the type of a server
   124  //
   125  //	FS server                          non-FS server
   126  //
   127  // ==============================  ===================================
   128  // ● <ip>:<port>                   ● <ip>:<port>
   129  //
   130  //	Uptime: xxx                     Uptime: xxx
   131  //	Version: xxx                    Version: xxx
   132  //	Network: X/Y OK                 Network: X/Y OK
   133  //
   134  // U Used, B Buckets, O Objects    Drives: N/N OK
   135  //
   136  //	U Used, B Buckets, O Objects
   137  //	N drives online, K drives offline
   138  func (u clusterStruct) String() (msg string) {
   139  	// Check cluster level "Status" field for error
   140  	if u.Status == "error" {
   141  		fatal(probe.NewError(errors.New(u.Error)), "Unable to get service status")
   142  	}
   143  
   144  	// If nothing has been collected, error out
   145  	if u.Info.Servers == nil {
   146  		fatal(probe.NewError(errors.New("Unable to get service status")), "")
   147  	}
   148  
   149  	// Initialization
   150  	var totalOfflineNodes int
   151  
   152  	// Color palette initialization
   153  	console.SetColor("Info", color.New(color.FgGreen, color.Bold))
   154  	console.SetColor("InfoFail", color.New(color.FgRed, color.Bold))
   155  	console.SetColor("InfoWarning", color.New(color.FgYellow, color.Bold))
   156  
   157  	backendType := u.Info.BackendType()
   158  
   159  	coloredDot := console.Colorize("Info", dot)
   160  	if madmin.ItemState(u.Info.Mode) == madmin.ItemInitializing {
   161  		coloredDot = console.Colorize("InfoWarning", dot)
   162  	}
   163  
   164  	sort.Slice(u.Info.Servers, func(i, j int) bool {
   165  		return u.Info.Servers[i].Endpoint < u.Info.Servers[j].Endpoint
   166  	})
   167  
   168  	clusterSummary := clusterSummaryInfo(u.Info)
   169  
   170  	// Loop through each server and put together info for each one
   171  	for _, srv := range u.Info.Servers {
   172  		// Check if MinIO server is not online ("Mode" field),
   173  		if srv.State != string(madmin.ItemOnline) {
   174  			totalOfflineNodes++
   175  			// "PrintB" is color blue in console library package
   176  			msg += fmt.Sprintf("%s  %s\n", console.Colorize("InfoFail", dot), console.Colorize("PrintB", srv.Endpoint))
   177  			msg += fmt.Sprintf("   Uptime: %s\n", console.Colorize("InfoFail", srv.State))
   178  
   179  			if backendType == madmin.Erasure {
   180  				// Info about drives on a server, only available for non-FS types
   181  				var OffDrives int
   182  				var OnDrives int
   183  				var dispNoOfDrives string
   184  				for _, disk := range srv.Disks {
   185  					switch disk.State {
   186  					case madmin.DriveStateOk, madmin.DriveStateUnformatted:
   187  						OnDrives++
   188  					default:
   189  						OffDrives++
   190  					}
   191  				}
   192  
   193  				totalDrivesPerServer := OnDrives + OffDrives
   194  
   195  				dispNoOfDrives = strconv.Itoa(OnDrives) + "/" + strconv.Itoa(totalDrivesPerServer)
   196  				msg += fmt.Sprintf("   Drives: %s %s\n", dispNoOfDrives, console.Colorize("InfoFail", "OK "))
   197  			}
   198  
   199  			msg += "\n"
   200  
   201  			// Continue to the next server
   202  			continue
   203  		}
   204  
   205  		// Print server title
   206  		msg += fmt.Sprintf("%s  %s\n", coloredDot, console.Colorize("PrintB", srv.Endpoint))
   207  
   208  		// Uptime
   209  		msg += fmt.Sprintf("   Uptime: %s\n", console.Colorize("Info",
   210  			humanize.RelTime(time.Now(), time.Now().Add(time.Duration(srv.Uptime)*time.Second), "", "")))
   211  
   212  		// Version
   213  		version := srv.Version
   214  		if srv.Version == "DEVELOPMENT.GOGET" {
   215  			version = "<development>"
   216  		}
   217  		msg += fmt.Sprintf("   Version: %s\n", version)
   218  		// Network info, only available for non-FS types
   219  		connectionAlive := 0
   220  		totalNodes := len(srv.Network)
   221  		if srv.Network != nil && backendType == madmin.Erasure {
   222  			for _, v := range srv.Network {
   223  				if v == "online" {
   224  					connectionAlive++
   225  				}
   226  			}
   227  			clr := "Info"
   228  			if connectionAlive != totalNodes {
   229  				clr = "InfoWarning"
   230  			}
   231  			displayNwInfo := strconv.Itoa(connectionAlive) + "/" + strconv.Itoa(totalNodes)
   232  			msg += fmt.Sprintf("   Network: %s %s\n", displayNwInfo, console.Colorize(clr, "OK "))
   233  		}
   234  
   235  		if backendType == madmin.Erasure {
   236  			// Info about drives on a server, only available for non-FS types
   237  			var OffDrives int
   238  			var OnDrives int
   239  			var dispNoOfDrives string
   240  			for _, disk := range srv.Disks {
   241  				switch disk.State {
   242  				case madmin.DriveStateOk, madmin.DriveStateUnformatted:
   243  					OnDrives++
   244  				default:
   245  					OffDrives++
   246  				}
   247  			}
   248  
   249  			totalDrivesPerServer := OnDrives + OffDrives
   250  			clr := "Info"
   251  			if OnDrives != totalDrivesPerServer {
   252  				clr = "InfoWarning"
   253  			}
   254  			dispNoOfDrives = strconv.Itoa(OnDrives) + "/" + strconv.Itoa(totalDrivesPerServer)
   255  			msg += fmt.Sprintf("   Drives: %s %s\n", dispNoOfDrives, console.Colorize(clr, "OK "))
   256  
   257  			// Print pools belonging to this server
   258  			var prettyPools []string
   259  			for _, pool := range endpointToPools(srv.Endpoint, clusterSummary) {
   260  				prettyPools = append(prettyPools, strconv.Itoa(pool+1))
   261  			}
   262  			msg += fmt.Sprintf("   Pool: %s\n", console.Colorize("Info", fmt.Sprintf("%+v", strings.Join(prettyPools, ", "))))
   263  		}
   264  
   265  		msg += "\n"
   266  	}
   267  
   268  	if backendType == madmin.Erasure {
   269  		msg += "Pools:\n"
   270  		for pool, summary := range clusterSummary {
   271  			msg += fmt.Sprintf("   %s, Erasure sets: %d, Drives per erasure set: %d\n",
   272  				console.Colorize("Info", humanize.Ordinal(pool+1)), summary.setsCount, summary.drivesPerSet)
   273  		}
   274  	}
   275  
   276  	msg += "\n"
   277  
   278  	// Summary on used space, total no of buckets and
   279  	// total no of objects at the Cluster level
   280  	usedTotal := humanize.IBytes(u.Info.Usage.Size)
   281  	if u.Info.Buckets.Count > 0 {
   282  		msg += fmt.Sprintf("%s Used, %s, %s", usedTotal,
   283  			english.Plural(int(u.Info.Buckets.Count), "Bucket", ""),
   284  			english.Plural(int(u.Info.Objects.Count), "Object", ""))
   285  		if u.Info.Versions.Count > 0 {
   286  			msg += ", " + english.Plural(int(u.Info.Versions.Count), "Version", "")
   287  		}
   288  		if u.Info.DeleteMarkers.Count > 0 {
   289  			msg += ", " + english.Plural(int(u.Info.DeleteMarkers.Count), "Delete Marker", "")
   290  		}
   291  		msg += "\n"
   292  	}
   293  	if backendType == madmin.Erasure {
   294  		if totalOfflineNodes != 0 {
   295  			msg += fmt.Sprintf("%s offline, ", english.Plural(totalOfflineNodes, "node", ""))
   296  		}
   297  		// Summary on total no of online and total
   298  		// number of offline drives at the Cluster level
   299  		msg += fmt.Sprintf("%s online, %s offline, EC:%d\n",
   300  			english.Plural(u.Info.Backend.OnlineDisks, "drive", ""),
   301  			english.Plural(u.Info.Backend.OfflineDisks, "drive", ""),
   302  			u.Info.Backend.StandardSCParity)
   303  	}
   304  
   305  	// Remove the last new line if any
   306  	// since this is a String() function
   307  	msg = strings.TrimSuffix(msg, "\n")
   308  	return
   309  }
   310  
   311  // JSON jsonifies service status message.
   312  func (u clusterStruct) JSON() string {
   313  	statusJSONBytes, e := json.MarshalIndent(u, "", "    ")
   314  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   315  
   316  	return string(statusJSONBytes)
   317  }
   318  
   319  // checkAdminInfoSyntax - validate arguments passed by a user
   320  func checkAdminInfoSyntax(ctx *cli.Context) {
   321  	if len(ctx.Args()) == 0 || len(ctx.Args()) > 1 {
   322  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   323  	}
   324  }
   325  
   326  func mainAdminInfo(ctx *cli.Context) error {
   327  	checkAdminInfoSyntax(ctx)
   328  
   329  	// Get the alias parameter from cli
   330  	args := ctx.Args()
   331  	aliasedURL := args.Get(0)
   332  
   333  	// Create a new MinIO Admin Client
   334  	client, err := newAdminClient(aliasedURL)
   335  	fatalIf(err, "Unable to initialize admin connection.")
   336  
   337  	var clusterInfo clusterStruct
   338  	// Fetch info of all servers (cluster or single server)
   339  	admInfo, e := client.ServerInfo(globalContext)
   340  	if e != nil {
   341  		clusterInfo.Status = "error"
   342  		clusterInfo.Error = e.Error()
   343  	} else {
   344  		clusterInfo.Status = "success"
   345  		clusterInfo.Error = ""
   346  	}
   347  	clusterInfo.Info = admInfo
   348  	printMsg(clusterInfo)
   349  
   350  	return nil
   351  }