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 }