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 }