github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/admin-heal.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 "fmt" 22 "net/url" 23 "path/filepath" 24 "sort" 25 "strings" 26 "time" 27 28 "github.com/dustin/go-humanize" 29 "github.com/fatih/color" 30 "github.com/minio/cli" 31 json "github.com/minio/colorjson" 32 "github.com/minio/madmin-go/v3" 33 "github.com/minio/mc/pkg/probe" 34 "github.com/minio/pkg/v2/console" 35 ) 36 37 const ( 38 scanNormalMode = "normal" 39 scanDeepMode = "deep" 40 ) 41 42 var adminHealFlags = []cli.Flag{ 43 cli.StringFlag{ 44 Name: "scan", 45 Usage: "select the healing scan mode (normal/deep)", 46 Value: scanNormalMode, 47 Hidden: true, 48 }, 49 cli.BoolFlag{ 50 Name: "recursive, r", 51 Usage: "heal recursively", 52 Hidden: true, 53 }, 54 cli.BoolFlag{ 55 Name: "dry-run, n", 56 Usage: "only inspect data, but do not mutate", 57 Hidden: true, 58 }, 59 cli.BoolFlag{ 60 Name: "force-start, f", 61 Usage: "force start a new heal sequence", 62 Hidden: true, 63 }, 64 cli.BoolFlag{ 65 Name: "force-stop, s", 66 Usage: "force stop a running heal sequence", 67 Hidden: true, 68 }, 69 cli.BoolFlag{ 70 Name: "remove", 71 Usage: "remove dangling objects in heal sequence", 72 Hidden: true, 73 }, 74 cli.StringFlag{ 75 Name: "storage-class", 76 Usage: "show server/drives failure tolerance with the given storage class", 77 Hidden: true, 78 }, 79 cli.BoolFlag{ 80 Name: "rewrite", 81 Usage: "rewrite objects from older to newer format", 82 Hidden: true, 83 }, 84 cli.BoolFlag{ 85 Name: "verbose, v", 86 Usage: "show verbose information", 87 }, 88 } 89 90 var adminHealCmd = cli.Command{ 91 Name: "heal", 92 Usage: "monitor healing for bucket(s) and object(s) on MinIO server", 93 Action: mainAdminHeal, 94 OnUsageError: onUsageError, 95 Before: setGlobalsFromContext, 96 Flags: append(adminHealFlags, globalFlags...), 97 HideHelpCommand: true, 98 CustomHelpTemplate: `NAME: 99 {{.HelpName}} - {{.Usage}} 100 101 USAGE: 102 {{.HelpName}} [FLAGS] TARGET 103 104 FLAGS: 105 {{range .VisibleFlags}}{{.}} 106 {{end}} 107 EXAMPLES: 108 1. Monitor healing status on a running server at alias 'myminio': 109 {{.Prompt}} {{.HelpName}} myminio/ 110 `, 111 } 112 113 func checkAdminHealSyntax(ctx *cli.Context) { 114 if len(ctx.Args()) != 1 { 115 showCommandHelpAndExit(ctx, 1) // last argument is exit code 116 } 117 118 // Check for scan argument 119 scanArg := ctx.String("scan") 120 scanArg = strings.ToLower(scanArg) 121 if scanArg != scanNormalMode && scanArg != scanDeepMode { 122 showCommandHelpAndExit(ctx, 1) // last argument is exit code 123 } 124 } 125 126 // stopHealMessage is container for stop heal success and failure messages. 127 type stopHealMessage struct { 128 Status string `json:"status"` 129 Alias string `json:"alias"` 130 } 131 132 // String colorized stop heal message. 133 func (s stopHealMessage) String() string { 134 return console.Colorize("HealStopped", "Heal stopped successfully at `"+s.Alias+"`.") 135 } 136 137 // JSON jsonified stop heal message. 138 func (s stopHealMessage) JSON() string { 139 stopHealJSONBytes, e := json.MarshalIndent(s, "", " ") 140 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 141 142 return string(stopHealJSONBytes) 143 } 144 145 type setIndex struct { 146 pool, set int 147 } 148 149 type poolInfo struct { 150 tolerance int 151 endpoints []string 152 } 153 154 type setInfo struct { 155 maxUsedSpace uint64 156 totalDisks int 157 incapableDisks int 158 } 159 160 type serverInfo struct { 161 pool int 162 disks []madmin.Disk 163 } 164 165 func (s serverInfo) onlineDisksForSet(index setIndex) (setFound bool, count int) { 166 for _, disk := range s.disks { 167 if disk.PoolIndex != index.pool || disk.SetIndex != index.set { 168 continue 169 } 170 setFound = true 171 if disk.State == "ok" && !disk.Healing { 172 count++ 173 } 174 } 175 return 176 } 177 178 // Get all drives from set statuses 179 func getAllDisks(sets []madmin.SetStatus) []madmin.Disk { 180 var disks []madmin.Disk 181 for _, set := range sets { 182 disks = append(disks, set.Disks...) 183 } 184 return disks 185 } 186 187 // Get all pools id from all drives 188 func getPoolsIndexes(disks []madmin.Disk) []int { 189 m := make(map[int]struct{}) 190 for _, d := range disks { 191 m[d.PoolIndex] = struct{}{} 192 } 193 var pools []int 194 for pool := range m { 195 pools = append(pools, pool) 196 } 197 sort.Ints(pools) 198 return pools 199 } 200 201 // Generate sets info from disks 202 func generateSetsStatus(disks []madmin.Disk) map[setIndex]setInfo { 203 m := make(map[setIndex]setInfo) 204 for _, d := range disks { 205 idx := setIndex{pool: d.PoolIndex, set: d.SetIndex} 206 setSt, ok := m[idx] 207 if !ok { 208 setSt = setInfo{} 209 } 210 setSt.totalDisks++ 211 if d.UsedSpace > setSt.maxUsedSpace { 212 setSt.maxUsedSpace = d.UsedSpace 213 } 214 if d.State != "ok" || d.Healing { 215 setSt.incapableDisks++ 216 } 217 m[idx] = setSt 218 } 219 return m 220 } 221 222 // Return a map of server endpoints and the corresponding status 223 func generateServersStatus(disks []madmin.Disk) map[string]serverInfo { 224 m := make(map[string]serverInfo) 225 for _, d := range disks { 226 u, e := url.Parse(d.Endpoint) 227 if e != nil { 228 continue 229 } 230 endpoint := u.Host 231 if endpoint == "" { 232 endpoint = "local-pool" + humanize.Ordinal(d.PoolIndex+1) 233 } 234 serverSt, ok := m[endpoint] 235 if !ok { 236 serverSt = serverInfo{ 237 pool: d.PoolIndex, 238 } 239 } 240 serverSt.disks = append(serverSt.disks, d) 241 m[endpoint] = serverSt 242 } 243 return m 244 } 245 246 // Return the list of endpoints of a given pool index 247 func computePoolEndpoints(pool int, serversStatus map[string]serverInfo) []string { 248 var endpoints []string 249 for endpoint, server := range serversStatus { 250 if server.pool != pool { 251 continue 252 } 253 endpoints = append(endpoints, endpoint) 254 } 255 return endpoints 256 } 257 258 // Compute the tolerance of each node in a given pool 259 func computePoolTolerance(pool, parity int, setsStatus map[setIndex]setInfo, serversStatus map[string]serverInfo) int { 260 var ( 261 onlineDisksPerSet = make(map[setIndex]int) 262 tolerancePerSet = make(map[setIndex]int) 263 ) 264 265 for set, setStatus := range setsStatus { 266 if set.pool != pool { 267 continue 268 } 269 270 onlineDisksPerSet[set] = setStatus.totalDisks - setStatus.incapableDisks 271 tolerancePerSet[set] = 0 272 273 for _, server := range serversStatus { 274 if server.pool != pool { 275 continue 276 } 277 278 canShutdown := true 279 setFound, count := server.onlineDisksForSet(set) 280 if !setFound { 281 continue 282 } 283 minDisks := setStatus.totalDisks - parity 284 if onlineDisksPerSet[set]-count < minDisks { 285 canShutdown = false 286 } 287 if canShutdown { 288 tolerancePerSet[set]++ 289 onlineDisksPerSet[set] -= count 290 } else { 291 break 292 } 293 } 294 } 295 296 minServerTolerance := len(serversStatus) 297 for _, tolerance := range tolerancePerSet { 298 if tolerance < minServerTolerance { 299 minServerTolerance = tolerance 300 } 301 } 302 303 return minServerTolerance 304 } 305 306 // Extract offline nodes from offline full path endpoints 307 func getOfflineNodes(endpoints []string) map[string]struct{} { 308 offlineNodes := make(map[string]struct{}) 309 for _, endpoint := range endpoints { 310 offlineNodes[endpoint] = struct{}{} 311 } 312 return offlineNodes 313 } 314 315 // verboseBackgroundHealStatusMessage is container for stop heal success and failure messages. 316 type verboseBackgroundHealStatusMessage struct { 317 Status string `json:"status"` 318 HealInfo madmin.BgHealState 319 320 // Specify storage class to show servers/disks tolerance 321 ToleranceForSC string `json:"-"` 322 } 323 324 // String colorized to show background heal status message. 325 func (s verboseBackgroundHealStatusMessage) String() string { 326 var msg strings.Builder 327 328 parity, showTolerance := s.HealInfo.SCParity[s.ToleranceForSC] 329 offlineEndpoints := getOfflineNodes(s.HealInfo.OfflineEndpoints) 330 allDisks := getAllDisks(s.HealInfo.Sets) 331 pools := getPoolsIndexes(allDisks) 332 setsStatus := generateSetsStatus(allDisks) 333 serversStatus := generateServersStatus(allDisks) 334 335 poolsInfo := make(map[int]poolInfo) 336 for _, pool := range pools { 337 tolerance := computePoolTolerance(pool, parity, setsStatus, serversStatus) 338 endpoints := computePoolEndpoints(pool, serversStatus) 339 poolsInfo[pool] = poolInfo{tolerance: tolerance, endpoints: endpoints} 340 } 341 342 distributed := len(serversStatus) > 1 343 344 plural := "" 345 if distributed { 346 plural = "s" 347 } 348 fmt.Fprintf(&msg, "Server%s status:\n", plural) 349 fmt.Fprintf(&msg, "==============\n") 350 351 for _, pool := range pools { 352 fmt.Fprintf(&msg, "Pool %s:\n", humanize.Ordinal(pool+1)) 353 354 // Sort servers in this pool by name 355 orderedEndpoints := make([]string, len(poolsInfo[pool].endpoints)) 356 copy(orderedEndpoints, poolsInfo[pool].endpoints) 357 sort.Strings(orderedEndpoints) 358 359 for _, endpoint := range orderedEndpoints { 360 // Print offline status if node is offline 361 _, ok := offlineEndpoints[endpoint] 362 if ok { 363 stateText := console.Colorize("NodeFailed", "OFFLINE") 364 fmt.Fprintf(&msg, " %s: %s\n", endpoint, stateText) 365 continue 366 } 367 serverStatus := serversStatus[endpoint] 368 switch { 369 case showTolerance: 370 serverHeader := " %s: (Tolerance: %d server(s))\n" 371 fmt.Fprintf(&msg, serverHeader, endpoint, poolsInfo[serverStatus.pool].tolerance) 372 default: 373 serverHeader := " %s:\n" 374 fmt.Fprintf(&msg, serverHeader, endpoint) 375 } 376 377 for _, d := range serverStatus.disks { 378 if d.PoolIndex != pool { 379 continue 380 } 381 stateText := "" 382 switch { 383 case d.State == "ok" && d.Healing: 384 stateText = console.Colorize("DiskHealing", "HEALING") 385 case d.State == "ok": 386 stateText = console.Colorize("DiskOK", "OK") 387 default: 388 stateText = console.Colorize("DiskFailed", d.State) 389 } 390 fmt.Fprintf(&msg, " + %s : %s\n", d.DrivePath, stateText) 391 if d.Healing && d.HealInfo != nil { 392 now := time.Now().UTC() 393 scanSpeed := float64(d.UsedSpace) / float64(now.Sub(d.HealInfo.Started)) 394 remainingTime := time.Duration(float64(setsStatus[setIndex{d.PoolIndex, d.SetIndex}].maxUsedSpace-d.UsedSpace) / scanSpeed) 395 estimationText := humanize.RelTime(now, now.Add(remainingTime), "", "") 396 fmt.Fprintf(&msg, " |__ Estimated: %s\n", estimationText) 397 } 398 fmt.Fprintf(&msg, " |__ Capacity: %s/%s\n", humanize.IBytes(d.UsedSpace), humanize.IBytes(d.TotalSpace)) 399 if showTolerance { 400 fmt.Fprintf(&msg, " |__ Tolerance: %d drive(s)\n", parity-setsStatus[setIndex{d.PoolIndex, d.SetIndex}].incapableDisks) 401 } 402 } 403 404 fmt.Fprintf(&msg, "\n") 405 } 406 } 407 408 if showTolerance { 409 fmt.Fprintf(&msg, "\n") 410 fmt.Fprintf(&msg, "Server Failure Tolerance:\n") 411 fmt.Fprintf(&msg, "========================\n") 412 for i, pool := range poolsInfo { 413 fmt.Fprintf(&msg, "Pool %s:\n", humanize.Ordinal(i+1)) 414 fmt.Fprintf(&msg, " Tolerance : %d server(s)\n", pool.tolerance) 415 fmt.Fprintf(&msg, " Nodes :") 416 for _, endpoint := range pool.endpoints { 417 fmt.Fprintf(&msg, " %s", endpoint) 418 } 419 fmt.Fprintf(&msg, "\n") 420 } 421 } 422 423 summary := shortBackgroundHealStatusMessage{HealInfo: s.HealInfo} 424 425 fmt.Fprint(&msg, "\n") 426 fmt.Fprint(&msg, "Summary:\n") 427 fmt.Fprint(&msg, "=======\n") 428 fmt.Fprint(&msg, summary.String()) 429 fmt.Fprint(&msg, "\n") 430 431 return msg.String() 432 } 433 434 // JSON jsonified stop heal message. 435 func (s verboseBackgroundHealStatusMessage) JSON() string { 436 healJSONBytes, e := json.MarshalIndent(s, "", " ") 437 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 438 439 return string(healJSONBytes) 440 } 441 442 // shortBackgroundHealStatusMessage is container for stop heal success and failure messages. 443 type shortBackgroundHealStatusMessage struct { 444 Status string `json:"status"` 445 HealInfo madmin.BgHealState 446 } 447 448 // String colorized to show background heal status message. 449 func (s shortBackgroundHealStatusMessage) String() string { 450 healPrettyMsg := "" 451 var ( 452 itemsHealed uint64 453 bytesHealed uint64 454 itemsFailed uint64 455 bytesFailed uint64 456 itemsHealedPerSec float64 457 bytesHealedPerSec float64 458 startedAt time.Time 459 setsExceedsStd int 460 setsExceedsReduced int 461 462 // The addition of Elapsed time of each parallel healing operation 463 // this is needed to calculate the rate of healing 464 accumulatedElapsedTime time.Duration 465 466 // ETA of healing - it is the latest ETA of all drives currently healing 467 healingRemaining time.Duration 468 ) 469 470 var problematicDisks int 471 leastPct := 100.0 472 473 for _, set := range s.HealInfo.Sets { 474 setsStatus := generateSetsStatus(set.Disks) 475 // Furthest along disk... 476 var furthestHealingDisk *madmin.Disk 477 missingInSet := 0 478 for _, disk := range set.Disks { 479 // Ignore disk with non 'ok' status 480 if disk.State != madmin.DriveStateOk { 481 if disk.State != madmin.DriveStateUnformatted { 482 missingInSet++ 483 problematicDisks++ 484 } 485 continue 486 } 487 488 if disk.HealInfo != nil { 489 missingInSet++ 490 491 diskSet := setIndex{pool: disk.PoolIndex, set: disk.SetIndex} 492 if maxUsedSpace := setsStatus[diskSet].maxUsedSpace; maxUsedSpace > 0 { 493 if pct := float64(disk.UsedSpace) / float64(maxUsedSpace); pct < leastPct { 494 leastPct = pct 495 } 496 } else { 497 // Unlikely to have max used space in an erasure set to be zero, but still set this to zero 498 leastPct = 0 499 } 500 501 scanSpeed := float64(disk.UsedSpace) / float64(time.Since(disk.HealInfo.Started)) 502 remainingTime := time.Duration(float64(setsStatus[diskSet].maxUsedSpace-disk.UsedSpace) / scanSpeed) 503 if remainingTime > healingRemaining { 504 healingRemaining = remainingTime 505 } 506 507 disk := disk 508 if furthestHealingDisk == nil { 509 furthestHealingDisk = &disk 510 continue 511 } 512 if disk.HealInfo.ItemsHealed+disk.HealInfo.ItemsFailed > furthestHealingDisk.HealInfo.ItemsHealed+furthestHealingDisk.HealInfo.ItemsFailed { 513 furthestHealingDisk = &disk 514 continue 515 } 516 } 517 } 518 519 if furthestHealingDisk != nil { 520 disk := furthestHealingDisk 521 522 // Approximate values 523 itemsHealed += disk.HealInfo.ItemsHealed 524 bytesHealed += disk.HealInfo.BytesDone 525 bytesFailed += disk.HealInfo.BytesFailed 526 itemsFailed += disk.HealInfo.ItemsFailed 527 528 if !disk.HealInfo.Started.IsZero() { 529 if !disk.HealInfo.Started.Before(startedAt) { 530 startedAt = disk.HealInfo.Started 531 } 532 533 if !disk.HealInfo.LastUpdate.IsZero() { 534 accumulatedElapsedTime += disk.HealInfo.LastUpdate.Sub(disk.HealInfo.Started) 535 } 536 537 bytesHealedPerSec += float64(time.Second) * float64(disk.HealInfo.BytesDone) / float64(disk.HealInfo.LastUpdate.Sub(disk.HealInfo.Started)) 538 itemsHealedPerSec += float64(time.Second) * float64(disk.HealInfo.ItemsHealed+disk.HealInfo.ItemsFailed) / float64(disk.HealInfo.LastUpdate.Sub(disk.HealInfo.Started)) 539 540 } 541 if n, ok := s.HealInfo.SCParity["STANDARD"]; ok && missingInSet > n { 542 setsExceedsStd++ 543 } 544 if n, ok := s.HealInfo.SCParity["REDUCED_REDUNDANCY"]; ok && missingInSet > n { 545 setsExceedsReduced++ 546 } 547 } 548 } 549 550 if startedAt.IsZero() && itemsHealed == 0 { 551 healPrettyMsg += "No active healing is detected for new disks" 552 if problematicDisks > 0 { 553 healPrettyMsg += fmt.Sprintf(", though %d offline disk(s) found.", problematicDisks) 554 } else { 555 healPrettyMsg += "." 556 } 557 return healPrettyMsg 558 } 559 560 // Objects healed information 561 healPrettyMsg += fmt.Sprintf("Objects Healed: %s, %s (%s)\n", 562 humanize.Comma(int64(itemsHealed)), humanize.IBytes(bytesHealed), humanize.CommafWithDigits(leastPct*100, 1)+"%") 563 healPrettyMsg += fmt.Sprintf("Objects Failed: %s\n", humanize.Comma(int64(itemsFailed))) 564 565 if accumulatedElapsedTime > 0 { 566 healPrettyMsg += fmt.Sprintf("Heal rate: %d obj/s, %s/s\n", int64(itemsHealedPerSec), humanize.IBytes(uint64(bytesHealedPerSec))) 567 } 568 569 // Estimation completion 570 now := time.Now() 571 healPrettyMsg += fmt.Sprintf("Estimated Completion: %s\n", humanize.RelTime(now, now.Add(healingRemaining), "", "")) 572 573 if problematicDisks > 0 { 574 healPrettyMsg += "\n" 575 healPrettyMsg += fmt.Sprintf("%d offline disk(s) found.", problematicDisks) 576 } 577 if setsExceedsStd > 0 { 578 healPrettyMsg += "\n" 579 healPrettyMsg += fmt.Sprintf("%d of %d sets exceeds standard parity count EC:%d lost/offline disks", setsExceedsStd, len(s.HealInfo.Sets), s.HealInfo.SCParity["STANDARD"]) 580 } 581 if setsExceedsReduced > 0 { 582 healPrettyMsg += "\n" 583 healPrettyMsg += fmt.Sprintf("%d of %d sets exceeds reduced parity count EC:%d lost/offline disks", setsExceedsReduced, len(s.HealInfo.Sets), s.HealInfo.SCParity["REDUCED_REDUNDANCY"]) 584 } 585 return healPrettyMsg 586 } 587 588 // JSON jsonified stop heal message. 589 func (s shortBackgroundHealStatusMessage) JSON() string { 590 healJSONBytes, e := json.MarshalIndent(s, "", " ") 591 fatalIf(probe.NewError(e), "Unable to marshal into JSON.") 592 593 return string(healJSONBytes) 594 } 595 596 func transformScanArg(scanArg string) madmin.HealScanMode { 597 switch scanArg { 598 case "deep": 599 return madmin.HealDeepScan 600 } 601 return madmin.HealNormalScan 602 } 603 604 // mainAdminHeal - the entry function of heal command 605 func mainAdminHeal(ctx *cli.Context) error { 606 // Check for command syntax 607 checkAdminHealSyntax(ctx) 608 609 // Get the alias parameter from cli 610 args := ctx.Args() 611 aliasedURL := args.Get(0) 612 613 console.SetColor("Heal", color.New(color.FgGreen, color.Bold)) 614 console.SetColor("Dot", color.New(color.FgGreen, color.Bold)) 615 console.SetColor("HealBackgroundTitle", color.New(color.FgGreen, color.Bold)) 616 console.SetColor("HealBackground", color.New(color.Bold)) 617 console.SetColor("HealUpdateUI", color.New(color.FgYellow, color.Bold)) 618 console.SetColor("HealStopped", color.New(color.FgGreen, color.Bold)) 619 620 console.SetColor("DiskHealing", color.New(color.FgYellow, color.Bold)) 621 console.SetColor("DiskOK", color.New(color.FgGreen, color.Bold)) 622 console.SetColor("DiskFailed", color.New(color.FgRed, color.Bold)) 623 console.SetColor("NodeFailed", color.New(color.FgRed, color.Bold)) 624 625 // Create a new MinIO Admin Client 626 adminClnt, err := newAdminClient(aliasedURL) 627 if err != nil { 628 fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.") 629 return nil 630 } 631 632 // Compute bucket and object from the aliased URL 633 aliasedURL = filepath.ToSlash(aliasedURL) 634 splits := splitStr(aliasedURL, "/", 3) 635 bucket, prefix := splits[1], splits[2] 636 637 clnt, err := newClient(aliasedURL) 638 if err != nil { 639 fatalIf(err.Trace(clnt.GetURL().String()), "Unable to create client for URL ", aliasedURL) 640 return nil 641 } 642 643 // Return the background heal status when the user 644 // doesn't pass a bucket or --recursive flag. 645 if bucket == "" && !ctx.Bool("recursive") { 646 bgHealStatus, e := adminClnt.BackgroundHealStatus(globalContext) 647 fatalIf(probe.NewError(e), "Unable to get background heal status.") 648 if ctx.Bool("verbose") { 649 printMsg(verboseBackgroundHealStatusMessage{ 650 Status: "success", 651 HealInfo: bgHealStatus, 652 ToleranceForSC: strings.ToUpper(ctx.String("storage-class")), 653 }) 654 } else { 655 printMsg(shortBackgroundHealStatusMessage{ 656 Status: "success", 657 HealInfo: bgHealStatus, 658 }) 659 } 660 return nil 661 } 662 663 opts := madmin.HealOpts{ 664 ScanMode: transformScanArg(ctx.String("scan")), 665 Remove: ctx.Bool("remove"), 666 Recursive: ctx.Bool("recursive"), 667 DryRun: ctx.Bool("dry-run"), 668 Recreate: ctx.Bool("rewrite"), 669 } 670 671 forceStart := ctx.Bool("force-start") 672 forceStop := ctx.Bool("force-stop") 673 if forceStop { 674 _, _, e := adminClnt.Heal(globalContext, bucket, prefix, opts, "", forceStart, forceStop) 675 fatalIf(probe.NewError(e), "Unable to stop healing.") 676 printMsg(stopHealMessage{Status: "success", Alias: aliasedURL}) 677 return nil 678 } 679 680 healStart, _, e := adminClnt.Heal(globalContext, bucket, prefix, opts, "", forceStart, false) 681 fatalIf(probe.NewError(e), "Unable to start healing.") 682 683 ui := uiData{ 684 Bucket: bucket, 685 Prefix: prefix, 686 Client: adminClnt, 687 ClientToken: healStart.ClientToken, 688 ForceStart: forceStart, 689 HealOpts: &opts, 690 ObjectsByOnlineDrives: make(map[int]int64), 691 HealthCols: make(map[col]int64), 692 CurChan: cursorAnimate(), 693 } 694 695 res, e := ui.DisplayAndFollowHealStatus(aliasedURL) 696 if e != nil { 697 if res.FailureDetail != "" { 698 data, _ := json.MarshalIndent(res, "", " ") 699 traceStr := string(data) 700 fatalIf(probe.NewError(e).Trace(aliasedURL, traceStr), "Unable to display heal status.") 701 } else { 702 fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to display heal status.") 703 } 704 } 705 return nil 706 }