storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/madmin/heal-commands.go (about)

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