github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/admin-heal-ui.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 "errors" 22 "fmt" 23 "math" 24 "strings" 25 "time" 26 27 humanize "github.com/dustin/go-humanize" 28 "github.com/fatih/color" 29 json "github.com/minio/colorjson" 30 "github.com/minio/madmin-go/v3" 31 "github.com/minio/mc/pkg/probe" 32 "github.com/minio/pkg/v2/console" 33 ) 34 35 const ( 36 lineWidth = 80 37 ) 38 39 var ( 40 hColOrder = []col{colRed, colYellow, colGreen} 41 hColTable = map[int][]int{ 42 1: {0, -1, 1}, 43 2: {0, 1, 2}, 44 3: {1, 2, 3}, 45 4: {1, 2, 4}, 46 5: {1, 3, 5}, 47 6: {2, 4, 6}, 48 7: {2, 4, 7}, 49 8: {2, 5, 8}, 50 } 51 ) 52 53 func getHColCode(surplusShards, parityShards int) (c col, err error) { 54 if parityShards < 1 || parityShards > 8 || surplusShards > parityShards { 55 return c, fmt.Errorf("Invalid parity shard count/surplus shard count given") 56 } 57 if surplusShards < 0 { 58 return colGrey, err 59 } 60 colRow := hColTable[parityShards] 61 for index, val := range colRow { 62 if val != -1 && surplusShards <= val { 63 return hColOrder[index], err 64 } 65 } 66 return c, fmt.Errorf("cannot get a heal color code") 67 } 68 69 type uiData struct { 70 Bucket, Prefix string 71 Client *madmin.AdminClient 72 ClientToken string 73 ForceStart bool 74 HealOpts *madmin.HealOpts 75 LastItem *hri 76 77 // Total time since heal start 78 HealDuration time.Duration 79 80 // Accumulated statistics of heal result records 81 BytesScanned int64 82 83 // Counter for objects, and another counter for all kinds of 84 // items 85 ObjectsScanned, ItemsScanned int64 86 87 // Counters for healed objects and all kinds of healed items 88 ObjectsHealed, ItemsHealed int64 89 90 // Map from online drives to number of objects with that many 91 // online drives. 92 ObjectsByOnlineDrives map[int]int64 93 // Map of health color code to number of objects with that 94 // health color code. 95 HealthCols map[col]int64 96 97 // channel to receive a prompt string to indicate activity on 98 // the terminal 99 CurChan (<-chan string) 100 } 101 102 func (ui *uiData) updateStats(i madmin.HealResultItem) error { 103 if i.Type == madmin.HealItemObject { 104 // Objects whose size could not be found have -1 size 105 // returned. 106 if i.ObjectSize >= 0 { 107 ui.BytesScanned += i.ObjectSize 108 } 109 110 ui.ObjectsScanned++ 111 } 112 ui.ItemsScanned++ 113 114 beforeUp, afterUp := i.GetOnlineCounts() 115 if afterUp > beforeUp { 116 if i.Type == madmin.HealItemObject { 117 ui.ObjectsHealed++ 118 } 119 ui.ItemsHealed++ 120 } 121 ui.ObjectsByOnlineDrives[afterUp]++ 122 123 // Update health color stats: 124 125 // Fetch health color after heal: 126 var err error 127 var afterCol col 128 h := newHRI(&i) 129 switch h.Type { 130 case madmin.HealItemMetadata, madmin.HealItemBucket: 131 _, afterCol, err = h.getReplicatedFileHCCChange() 132 default: 133 _, afterCol, err = h.getObjectHCCChange() 134 } 135 if err != nil { 136 return err 137 } 138 139 ui.HealthCols[afterCol]++ 140 return nil 141 } 142 143 func (ui *uiData) updateDuration(s *madmin.HealTaskStatus) { 144 ui.HealDuration = UTCNow().Sub(s.StartTime) 145 } 146 147 func (ui *uiData) getProgress() (oCount, objSize, duration string) { 148 oCount = humanize.Comma(ui.ObjectsScanned) 149 150 duration = ui.HealDuration.Round(time.Second).String() 151 152 bytesScanned := float64(ui.BytesScanned) 153 154 // Compute unit for object size 155 magnitudes := []float64{1 << 10, 1 << 20, 1 << 30, 1 << 40, 1 << 50, 1 << 60} 156 units := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} 157 var i int 158 for i = 0; i < len(magnitudes); i++ { 159 if bytesScanned <= magnitudes[i] { 160 break 161 } 162 } 163 numUnits := int(bytesScanned * (1 << 10) / magnitudes[i]) 164 objSize = fmt.Sprintf("%d %s", numUnits, units[i]) 165 return 166 } 167 168 func (ui *uiData) getPercentsNBars() (p map[col]float64, b map[col]string) { 169 // barChar, emptyBarChar := "█", "░" 170 barChar, emptyBarChar := "█", " " 171 barLen := 12 172 sum := float64(ui.ItemsScanned) 173 cols := []col{colGrey, colRed, colYellow, colGreen} 174 175 p = make(map[col]float64, len(cols)) 176 b = make(map[col]string, len(cols)) 177 var filledLen int 178 for _, col := range cols { 179 v := float64(ui.HealthCols[col]) 180 if sum == 0 { 181 p[col] = 0 182 filledLen = 0 183 } else { 184 p[col] = v * 100 / sum 185 // round up the filled part 186 filledLen = int(math.Ceil(float64(barLen) * v / sum)) 187 } 188 b[col] = strings.Repeat(barChar, filledLen) + 189 strings.Repeat(emptyBarChar, barLen-filledLen) 190 } 191 return 192 } 193 194 func (ui *uiData) printItemsQuietly(s *madmin.HealTaskStatus) (err error) { 195 lpad := func(s col) string { 196 return fmt.Sprintf("%-6s", string(s)) 197 } 198 rpad := func(s col) string { 199 return fmt.Sprintf("%6s", string(s)) 200 } 201 printColStr := func(before, after col) { 202 console.PrintC("[" + lpad(before) + " -> " + rpad(after) + "] ") 203 } 204 205 var b, a col 206 for _, item := range s.Items { 207 h := newHRI(&item) 208 switch h.Type { 209 case madmin.HealItemMetadata, madmin.HealItemBucket: 210 b, a, err = h.getReplicatedFileHCCChange() 211 default: 212 b, a, err = h.getObjectHCCChange() 213 } 214 if err != nil { 215 return err 216 } 217 printColStr(b, a) 218 hrStr := h.getHealResultStr() 219 switch h.Type { 220 case madmin.HealItemMetadata, madmin.HealItemBucketMetadata: 221 console.PrintC(fmt.Sprintln("**", hrStr, "**")) 222 default: 223 console.PrintC(hrStr, "\n") 224 } 225 } 226 return nil 227 } 228 229 func (ui *uiData) printStatsQuietly() { 230 totalObjects, totalSize, totalTime := ui.getProgress() 231 232 healedStr := fmt.Sprintf("Healed:\t%s/%s objects; %s in %s\n", 233 humanize.Comma(ui.ObjectsHealed), totalObjects, 234 totalSize, totalTime) 235 236 console.PrintC(healedStr) 237 } 238 239 func (ui *uiData) printItemsJSON(s *madmin.HealTaskStatus) (err error) { 240 type healRec struct { 241 Status string `json:"status"` 242 Error string `json:"error,omitempty"` 243 Type string `json:"type"` 244 Name string `json:"name"` 245 Before struct { 246 Color string `json:"color"` 247 Offline int `json:"offline"` 248 Online int `json:"online"` 249 Missing int `json:"missing"` 250 Corrupted int `json:"corrupted"` 251 Drives []madmin.HealDriveInfo `json:"drives"` 252 } `json:"before"` 253 After struct { 254 Color string `json:"color"` 255 Offline int `json:"offline"` 256 Online int `json:"online"` 257 Missing int `json:"missing"` 258 Corrupted int `json:"corrupted"` 259 Drives []madmin.HealDriveInfo `json:"drives"` 260 } `json:"after"` 261 Size int64 `json:"size"` 262 } 263 makeHR := func(h *hri) (r healRec) { 264 r.Status = "success" 265 r.Type, r.Name = h.getHRTypeAndName() 266 267 var b, a col 268 var err error 269 switch h.Type { 270 case madmin.HealItemMetadata, madmin.HealItemBucket: 271 b, a, err = h.getReplicatedFileHCCChange() 272 default: 273 if h.Type == madmin.HealItemObject { 274 r.Size = h.ObjectSize 275 } 276 b, a, err = h.getObjectHCCChange() 277 } 278 if err != nil { 279 r.Error = err.Error() 280 } 281 r.Before.Color = strings.ToLower(string(b)) 282 r.After.Color = strings.ToLower(string(a)) 283 r.Before.Online, r.After.Online = h.GetOnlineCounts() 284 r.Before.Missing, r.After.Missing = h.GetMissingCounts() 285 r.Before.Corrupted, r.After.Corrupted = h.GetCorruptedCounts() 286 r.Before.Offline, r.After.Offline = h.GetOfflineCounts() 287 r.Before.Drives = h.Before.Drives 288 r.After.Drives = h.After.Drives 289 return r 290 } 291 292 for _, item := range s.Items { 293 h := newHRI(&item) 294 jsonBytes, e := json.MarshalIndent(makeHR(h), "", " ") 295 fatalIf(probe.NewError(e), "Unable to marshal to JSON.") 296 console.Println(string(jsonBytes)) 297 } 298 return nil 299 } 300 301 func (ui *uiData) printStatsJSON(_ *madmin.HealTaskStatus) { 302 var summary struct { 303 Status string `json:"status"` 304 Error string `json:"error,omitempty"` 305 Type string `json:"type"` 306 ObjectsScanned int64 `json:"objects_scanned"` 307 ObjectsHealed int64 `json:"objects_healed"` 308 ItemsScanned int64 `json:"items_scanned"` 309 ItemsHealed int64 `json:"items_healed"` 310 Size int64 `json:"size"` 311 ElapsedTime int64 `json:"duration"` 312 } 313 314 summary.Status = "success" 315 summary.Type = "summary" 316 317 summary.ObjectsScanned = ui.ObjectsScanned 318 summary.ObjectsHealed = ui.ObjectsHealed 319 summary.ItemsScanned = ui.ItemsScanned 320 summary.ItemsHealed = ui.ItemsHealed 321 summary.Size = ui.BytesScanned 322 summary.ElapsedTime = int64(ui.HealDuration.Round(time.Second).Seconds()) 323 324 jBytes, e := json.MarshalIndent(summary, "", " ") 325 fatalIf(probe.NewError(e), "Unable to marshal to JSON.") 326 console.Println(string(jBytes)) 327 } 328 329 func (ui *uiData) updateUI(s *madmin.HealTaskStatus) (err error) { 330 itemCount := len(s.Items) 331 h := ui.LastItem 332 if itemCount > 0 { 333 item := s.Items[itemCount-1] 334 h = newHRI(&item) 335 ui.LastItem = h 336 } 337 scannedStr := "** waiting for status from server **" 338 if h != nil { 339 scannedStr = lineTrunc(h.makeHealEntityString(), lineWidth-len("Scanned: ")) 340 } 341 342 totalObjects, totalSize, totalTime := ui.getProgress() 343 healedStr := fmt.Sprintf("%s/%s objects; %s in %s", 344 humanize.Comma(ui.ObjectsHealed), totalObjects, 345 totalSize, totalTime) 346 347 console.Print(console.Colorize("HealUpdateUI", fmt.Sprintf(" %s", <-ui.CurChan))) 348 console.PrintC(fmt.Sprintf(" %s\n", scannedStr)) 349 console.PrintC(fmt.Sprintf(" %s\n", healedStr)) 350 351 dspOrder := []col{colGreen, colYellow, colRed, colGrey} 352 printColors := []*color.Color{} 353 for _, c := range dspOrder { 354 printColors = append(printColors, getPrintCol(c)) 355 } 356 t := console.NewTable(printColors, []bool{false, true, true}, 4) 357 358 percentMap, barMap := ui.getPercentsNBars() 359 cellText := make([][]string, len(dspOrder)) 360 for i := range cellText { 361 cellText[i] = []string{ 362 string(dspOrder[i]), 363 fmt.Sprint(humanize.Comma(ui.HealthCols[dspOrder[i]])), 364 fmt.Sprintf("%5.1f%% %s", percentMap[dspOrder[i]], barMap[dspOrder[i]]), 365 } 366 } 367 368 t.DisplayTable(cellText) 369 return nil 370 } 371 372 func (ui *uiData) UpdateDisplay(s *madmin.HealTaskStatus) (err error) { 373 // Update state 374 ui.updateDuration(s) 375 for _, i := range s.Items { 376 ui.updateStats(i) 377 } 378 379 // Update display 380 switch { 381 case globalJSON: 382 err = ui.printItemsJSON(s) 383 case globalQuiet: 384 err = ui.printItemsQuietly(s) 385 default: 386 err = ui.updateUI(s) 387 } 388 return 389 } 390 391 func (ui *uiData) healResumeMsg(aliasedURL string) string { 392 var flags string 393 if ui.HealOpts.Recursive { 394 flags += "--recursive " 395 } 396 if ui.HealOpts.DryRun { 397 flags += "--dry-run " 398 } 399 return fmt.Sprintf("Healing is backgrounded, to resume watching use `mc admin heal %s %s`", flags, aliasedURL) 400 } 401 402 func (ui *uiData) DisplayAndFollowHealStatus(aliasedURL string) (res madmin.HealTaskStatus, err error) { 403 quitMsg := ui.healResumeMsg(aliasedURL) 404 405 firstIter := true 406 for { 407 select { 408 case <-globalContext.Done(): 409 return res, errors.New(quitMsg) 410 default: 411 _, res, err = ui.Client.Heal(globalContext, ui.Bucket, ui.Prefix, *ui.HealOpts, 412 ui.ClientToken, ui.ForceStart, false) 413 if err != nil { 414 return res, err 415 } 416 if firstIter { 417 firstIter = false 418 } else { 419 if !globalQuiet && !globalJSON { 420 console.RewindLines(8) 421 } 422 } 423 err = ui.UpdateDisplay(&res) 424 if err != nil { 425 return res, err 426 } 427 428 if res.Summary == "finished" { 429 if globalJSON { 430 ui.printStatsJSON(&res) 431 } else if globalQuiet { 432 ui.printStatsQuietly() 433 } 434 return res, nil 435 } 436 437 if res.Summary == "stopped" { 438 return res, fmt.Errorf("Heal had an error - %s", res.FailureDetail) 439 } 440 441 time.Sleep(time.Second) 442 } 443 } 444 }