github.com/minio/madmin-go/v2@v2.2.1/heal-commands.go (about)

     1  //
     2  // Copyright (c) 2015-2022 MinIO, Inc.
     3  //
     4  // This file is part of MinIO Object Storage stack
     5  //
     6  // This program is free software: you can redistribute it and/or modify
     7  // it under the terms of the GNU Affero General Public License as
     8  // published by the Free Software Foundation, either version 3 of the
     9  // License, or (at your option) any later version.
    10  //
    11  // This program is distributed in the hope that it will be useful,
    12  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  // GNU Affero General Public License for more details.
    15  //
    16  // You should have received a copy of the GNU Affero General Public License
    17  // along with this program. If not, see <http://www.gnu.org/licenses/>.
    18  //
    19  
    20  package madmin
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/url"
    29  	"sort"
    30  	"time"
    31  )
    32  
    33  // HealScanMode represents the type of healing scan
    34  type HealScanMode int
    35  
    36  const (
    37  	// HealUnknownScan default is unknown
    38  	HealUnknownScan HealScanMode = iota
    39  
    40  	// HealNormalScan checks if parts are present and not outdated
    41  	HealNormalScan
    42  
    43  	// HealDeepScan checks for parts bitrot checksums
    44  	HealDeepScan
    45  )
    46  
    47  // HealOpts - collection of options for a heal sequence
    48  type HealOpts struct {
    49  	Recursive    bool         `json:"recursive"`
    50  	DryRun       bool         `json:"dryRun"`
    51  	Remove       bool         `json:"remove"`
    52  	Recreate     bool         `json:"recreate"` // Rewrite all resources specified at the bucket or prefix.
    53  	ScanMode     HealScanMode `json:"scanMode"`
    54  	UpdateParity bool         `json:"updateParity"` // Update the parity of the existing object with a new one
    55  	NoLock       bool         `json:"nolock"`
    56  }
    57  
    58  // Equal returns true if no is same as o.
    59  func (o HealOpts) Equal(no HealOpts) bool {
    60  	if o.Recursive != no.Recursive {
    61  		return false
    62  	}
    63  	if o.DryRun != no.DryRun {
    64  		return false
    65  	}
    66  	if o.Remove != no.Remove {
    67  		return false
    68  	}
    69  	if o.Recreate != no.Recreate {
    70  		return false
    71  	}
    72  	if o.UpdateParity != no.UpdateParity {
    73  		return false
    74  	}
    75  
    76  	return o.ScanMode == no.ScanMode
    77  }
    78  
    79  // HealStartSuccess - holds information about a successfully started
    80  // heal operation
    81  type HealStartSuccess struct {
    82  	ClientToken   string    `json:"clientToken"`
    83  	ClientAddress string    `json:"clientAddress"`
    84  	StartTime     time.Time `json:"startTime"`
    85  }
    86  
    87  // HealStopSuccess - holds information about a successfully stopped
    88  // heal operation.
    89  type HealStopSuccess HealStartSuccess
    90  
    91  // HealTaskStatus - status struct for a heal task
    92  type HealTaskStatus struct {
    93  	Summary       string    `json:"summary"`
    94  	FailureDetail string    `json:"detail"`
    95  	StartTime     time.Time `json:"startTime"`
    96  	HealSettings  HealOpts  `json:"settings"`
    97  
    98  	Items []HealResultItem `json:"items,omitempty"`
    99  }
   100  
   101  // HealItemType - specify the type of heal operation in a healing
   102  // result
   103  type HealItemType string
   104  
   105  // HealItemType constants
   106  const (
   107  	HealItemMetadata       HealItemType = "metadata"
   108  	HealItemBucket                      = "bucket"
   109  	HealItemBucketMetadata              = "bucket-metadata"
   110  	HealItemObject                      = "object"
   111  )
   112  
   113  // Drive state constants
   114  const (
   115  	DriveStateOk          string = "ok"
   116  	DriveStateOffline            = "offline"
   117  	DriveStateCorrupt            = "corrupt"
   118  	DriveStateMissing            = "missing"
   119  	DriveStatePermission         = "permission-denied"
   120  	DriveStateFaulty             = "faulty"
   121  	DriveStateUnknown            = "unknown"
   122  	DriveStateUnformatted        = "unformatted" // only returned by disk
   123  )
   124  
   125  // HealDriveInfo - struct for an individual drive info item.
   126  type HealDriveInfo struct {
   127  	UUID     string `json:"uuid"`
   128  	Endpoint string `json:"endpoint"`
   129  	State    string `json:"state"`
   130  }
   131  
   132  // HealResultItem - struct for an individual heal result item
   133  type HealResultItem struct {
   134  	ResultIndex  int64        `json:"resultId"`
   135  	Type         HealItemType `json:"type"`
   136  	Bucket       string       `json:"bucket"`
   137  	Object       string       `json:"object"`
   138  	VersionID    string       `json:"versionId"`
   139  	Detail       string       `json:"detail"`
   140  	ParityBlocks int          `json:"parityBlocks,omitempty"`
   141  	DataBlocks   int          `json:"dataBlocks,omitempty"`
   142  	DiskCount    int          `json:"diskCount"`
   143  	SetCount     int          `json:"setCount"`
   144  	// below slices are from drive info.
   145  	Before struct {
   146  		Drives []HealDriveInfo `json:"drives"`
   147  	} `json:"before"`
   148  	After struct {
   149  		Drives []HealDriveInfo `json:"drives"`
   150  	} `json:"after"`
   151  	ObjectSize int64 `json:"objectSize"`
   152  }
   153  
   154  // GetMissingCounts - returns the number of missing disks before
   155  // and after heal
   156  func (hri *HealResultItem) GetMissingCounts() (b, a int) {
   157  	if hri == nil {
   158  		return
   159  	}
   160  	for _, v := range hri.Before.Drives {
   161  		if v.State == DriveStateMissing {
   162  			b++
   163  		}
   164  	}
   165  	for _, v := range hri.After.Drives {
   166  		if v.State == DriveStateMissing {
   167  			a++
   168  		}
   169  	}
   170  	return
   171  }
   172  
   173  // GetOfflineCounts - returns the number of offline disks before
   174  // and after heal
   175  func (hri *HealResultItem) GetOfflineCounts() (b, a int) {
   176  	if hri == nil {
   177  		return
   178  	}
   179  	for _, v := range hri.Before.Drives {
   180  		if v.State == DriveStateOffline {
   181  			b++
   182  		}
   183  	}
   184  	for _, v := range hri.After.Drives {
   185  		if v.State == DriveStateOffline {
   186  			a++
   187  		}
   188  	}
   189  	return
   190  }
   191  
   192  // GetCorruptedCounts - returns the number of corrupted disks before
   193  // and after heal
   194  func (hri *HealResultItem) GetCorruptedCounts() (b, a int) {
   195  	if hri == nil {
   196  		return
   197  	}
   198  	for _, v := range hri.Before.Drives {
   199  		if v.State == DriveStateCorrupt {
   200  			b++
   201  		}
   202  	}
   203  	for _, v := range hri.After.Drives {
   204  		if v.State == DriveStateCorrupt {
   205  			a++
   206  		}
   207  	}
   208  	return
   209  }
   210  
   211  // GetOnlineCounts - returns the number of online disks before
   212  // and after heal
   213  func (hri *HealResultItem) GetOnlineCounts() (b, a int) {
   214  	if hri == nil {
   215  		return
   216  	}
   217  	for _, v := range hri.Before.Drives {
   218  		if v.State == DriveStateOk {
   219  			b++
   220  		}
   221  	}
   222  	for _, v := range hri.After.Drives {
   223  		if v.State == DriveStateOk {
   224  			a++
   225  		}
   226  	}
   227  	return
   228  }
   229  
   230  // Heal - API endpoint to start heal and to fetch status
   231  // forceStart and forceStop are mutually exclusive, you can either
   232  // set one of them to 'true'. If both are set 'forceStart' will be
   233  // honored.
   234  func (adm *AdminClient) Heal(ctx context.Context, bucket, prefix string,
   235  	healOpts HealOpts, clientToken string, forceStart, forceStop bool) (
   236  	healStart HealStartSuccess, healTaskStatus HealTaskStatus, err error,
   237  ) {
   238  	if forceStart && forceStop {
   239  		return healStart, healTaskStatus, ErrInvalidArgument("forceStart and forceStop set to true is not allowed")
   240  	}
   241  
   242  	body, err := json.Marshal(healOpts)
   243  	if err != nil {
   244  		return healStart, healTaskStatus, err
   245  	}
   246  
   247  	path := fmt.Sprintf(adminAPIPrefix+"/heal/%s", bucket)
   248  	if bucket != "" && prefix != "" {
   249  		path += "/" + prefix
   250  	}
   251  
   252  	// execute POST request to heal api
   253  	queryVals := make(url.Values)
   254  	if clientToken != "" {
   255  		queryVals.Set("clientToken", clientToken)
   256  		body = []byte{}
   257  	}
   258  
   259  	// Anyone can be set, either force start or forceStop.
   260  	if forceStart {
   261  		queryVals.Set("forceStart", "true")
   262  	} else if forceStop {
   263  		queryVals.Set("forceStop", "true")
   264  	}
   265  
   266  	resp, err := adm.executeMethod(ctx,
   267  		http.MethodPost, requestData{
   268  			relPath:     path,
   269  			content:     body,
   270  			queryValues: queryVals,
   271  		})
   272  	defer closeResponse(resp)
   273  	if err != nil {
   274  		return healStart, healTaskStatus, err
   275  	}
   276  
   277  	if resp.StatusCode != http.StatusOK {
   278  		return healStart, healTaskStatus, httpRespToErrorResponse(resp)
   279  	}
   280  
   281  	respBytes, err := ioutil.ReadAll(resp.Body)
   282  	if err != nil {
   283  		return healStart, healTaskStatus, err
   284  	}
   285  
   286  	// Was it a status request?
   287  	if clientToken == "" {
   288  		// As a special operation forceStop would return a
   289  		// similar struct as healStart will have the
   290  		// heal sequence information about the heal which
   291  		// was stopped.
   292  		err = json.Unmarshal(respBytes, &healStart)
   293  	} else {
   294  		err = json.Unmarshal(respBytes, &healTaskStatus)
   295  	}
   296  	if err != nil {
   297  		// May be the server responded with error after success
   298  		// message, handle it separately here.
   299  		var errResp ErrorResponse
   300  		err = json.Unmarshal(respBytes, &errResp)
   301  		if err != nil {
   302  			// Unknown structure return error anyways.
   303  			return healStart, healTaskStatus, err
   304  		}
   305  		return healStart, healTaskStatus, errResp
   306  	}
   307  	return healStart, healTaskStatus, nil
   308  }
   309  
   310  // MRFStatus exposes MRF metrics of a server
   311  type MRFStatus struct {
   312  	BytesHealed uint64 `json:"bytes_healed"`
   313  	ItemsHealed uint64 `json:"items_healed"`
   314  }
   315  
   316  // BgHealState represents the status of the background heal
   317  type BgHealState struct {
   318  	// List of offline endpoints with no background heal state info
   319  	OfflineEndpoints []string `json:"offline_nodes"`
   320  	// Total items scanned by the continuous background healing
   321  	ScannedItemsCount int64
   322  	// Disks currently in heal states
   323  	HealDisks []string
   324  	// SetStatus contains information for each set.
   325  	Sets []SetStatus `json:"sets"`
   326  	// Endpoint -> MRF Status
   327  	MRF map[string]MRFStatus `json:"mrf"`
   328  	// Parity per storage class
   329  	SCParity map[string]int `json:"sc_parity"`
   330  }
   331  
   332  // SetStatus contains information about the heal status of a set.
   333  type SetStatus struct {
   334  	ID           string `json:"id"`
   335  	PoolIndex    int    `json:"pool_index"`
   336  	SetIndex     int    `json:"set_index"`
   337  	HealStatus   string `json:"heal_status"`
   338  	HealPriority string `json:"heal_priority"`
   339  	TotalObjects int    `json:"total_objects"`
   340  	Disks        []Disk `json:"disks"`
   341  }
   342  
   343  // HealingDisk contains information about
   344  type HealingDisk struct {
   345  	// Copied from cmd/background-newdisks-heal-ops.go
   346  	// When adding new field, update (*healingTracker).toHealingDisk
   347  
   348  	ID         string    `json:"id"`
   349  	HealID     string    `json:"heal_id"`
   350  	PoolIndex  int       `json:"pool_index"`
   351  	SetIndex   int       `json:"set_index"`
   352  	DiskIndex  int       `json:"disk_index"`
   353  	Endpoint   string    `json:"endpoint"`
   354  	Path       string    `json:"path"`
   355  	Started    time.Time `json:"started"`
   356  	LastUpdate time.Time `json:"last_update"`
   357  
   358  	ObjectsTotalCount uint64 `json:"objects_total_count"`
   359  	ObjectsTotalSize  uint64 `json:"objects_total_size"`
   360  
   361  	ItemsHealed uint64 `json:"items_healed"`
   362  	ItemsFailed uint64 `json:"items_failed"`
   363  	BytesDone   uint64 `json:"bytes_done"`
   364  	BytesFailed uint64 `json:"bytes_failed"`
   365  
   366  	ObjectsHealed uint64 `json:"objects_healed"` // Deprecated July 2021
   367  	ObjectsFailed uint64 `json:"objects_failed"` // Deprecated July 2021
   368  
   369  	// Last object scanned.
   370  	Bucket string `json:"current_bucket"`
   371  	Object string `json:"current_object"`
   372  
   373  	// Filled on startup/restarts.
   374  	QueuedBuckets []string `json:"queued_buckets"`
   375  
   376  	// Filled during heal.
   377  	HealedBuckets []string `json:"healed_buckets"`
   378  	// future add more tracking capabilities
   379  }
   380  
   381  // Merge others into b.
   382  func (b *BgHealState) Merge(others ...BgHealState) {
   383  	// SCParity is the same from all nodes, just pick
   384  	// the information from the first node.
   385  	if b.SCParity == nil && len(others) > 0 {
   386  		b.SCParity = make(map[string]int)
   387  		for k, v := range others[0].SCParity {
   388  			b.SCParity[k] = v
   389  		}
   390  	}
   391  	if b.MRF == nil {
   392  		b.MRF = make(map[string]MRFStatus)
   393  	}
   394  	for _, other := range others {
   395  		b.OfflineEndpoints = append(b.OfflineEndpoints, other.OfflineEndpoints...)
   396  		for k, v := range other.MRF {
   397  			b.MRF[k] = v
   398  		}
   399  		b.ScannedItemsCount += other.ScannedItemsCount
   400  
   401  		// Add disk if not present.
   402  		// If present select the one with latest lastupdate.
   403  		addDisksFromSet := func(set SetStatus) {
   404  			found := -1
   405  			for idx, s := range b.Sets {
   406  				if s.PoolIndex == set.PoolIndex && s.SetIndex == set.SetIndex {
   407  					found = idx
   408  				}
   409  			}
   410  
   411  			if found == -1 {
   412  				b.Sets = append(b.Sets, set)
   413  			} else {
   414  				b.Sets[found].Disks = append(b.Sets[found].Disks, set.Disks...)
   415  			}
   416  		}
   417  
   418  		for _, set := range other.Sets {
   419  			addDisksFromSet(set)
   420  		}
   421  	}
   422  	sort.Slice(b.Sets, func(i, j int) bool {
   423  		if b.Sets[i].PoolIndex != b.Sets[j].PoolIndex {
   424  			return b.Sets[i].PoolIndex < b.Sets[j].PoolIndex
   425  		}
   426  		return b.Sets[i].SetIndex < b.Sets[j].SetIndex
   427  	})
   428  }
   429  
   430  // BackgroundHealStatus returns the background heal status of the
   431  // current server or cluster.
   432  func (adm *AdminClient) BackgroundHealStatus(ctx context.Context) (BgHealState, error) {
   433  	// Execute POST request to background heal status api
   434  	resp, err := adm.executeMethod(ctx,
   435  		http.MethodPost,
   436  		requestData{relPath: adminAPIPrefix + "/background-heal/status"})
   437  	if err != nil {
   438  		return BgHealState{}, err
   439  	}
   440  	defer closeResponse(resp)
   441  
   442  	if resp.StatusCode != http.StatusOK {
   443  		return BgHealState{}, httpRespToErrorResponse(resp)
   444  	}
   445  
   446  	respBytes, err := ioutil.ReadAll(resp.Body)
   447  	if err != nil {
   448  		return BgHealState{}, err
   449  	}
   450  
   451  	var healState BgHealState
   452  
   453  	err = json.Unmarshal(respBytes, &healState)
   454  	if err != nil {
   455  		return BgHealState{}, err
   456  	}
   457  	return healState, nil
   458  }