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 }