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  }