From 0715032598fb2fb632dc9129a1e0b665359c9670 Mon Sep 17 00:00:00 2001 From: Anis Elleuch Date: Thu, 19 Jan 2017 18:34:18 +0100 Subject: [PATCH] heal: Add ListBucketsHeal object API (#3563) ListBucketsHeal will list which buckets that need to be healed: * ListBucketsHeal() (buckets []BucketInfo, err error) --- cmd/admin-handlers.go | 28 +++++ cmd/admin-handlers_test.go | 2 +- cmd/admin-router.go | 5 +- cmd/api-response.go | 14 ++- cmd/fs-v1.go | 5 + cmd/object-api-datatypes.go | 33 ++++-- cmd/object-api-interface.go | 1 + cmd/xl-v1-healing-common.go | 8 +- cmd/xl-v1-healing.go | 112 +++++++++++++++++- cmd/xl-v1-healing_test.go | 63 ++++++++++ cmd/xl-v1-list-objects-heal.go | 10 +- docs/admin-api/management-api.md | 6 + pkg/madmin/API.md | 32 ++++- pkg/madmin/examples/heal-buckets-list.go | 60 ++++++++++ .../{heal-list.go => heal-objects-list.go} | 0 pkg/madmin/heal-commands.go | 109 ++++++++++++++++- 16 files changed, 445 insertions(+), 43 deletions(-) create mode 100644 pkg/madmin/examples/heal-buckets-list.go rename pkg/madmin/examples/{heal-list.go => heal-objects-list.go} (100%) diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index 8d93f605d..794f89689 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -345,6 +345,34 @@ func (adminAPI adminAPIHandlers) ListObjectsHealHandler(w http.ResponseWriter, r writeSuccessResponseXML(w, encodeResponse(listResponse)) } +// ListBucketsHealHandler - GET /?heal +func (adminAPI adminAPIHandlers) ListBucketsHealHandler(w http.ResponseWriter, r *http.Request) { + // Get object layer instance. + objLayer := newObjectLayerFn() + if objLayer == nil { + writeErrorResponse(w, ErrServerNotInitialized, r.URL) + return + } + + // Validate request signature. + adminAPIErr := checkRequestAuthType(r, "", "", "") + if adminAPIErr != ErrNone { + writeErrorResponse(w, adminAPIErr, r.URL) + return + } + + // Get the list buckets to be healed. + bucketsInfo, err := objLayer.ListBucketsHeal() + if err != nil { + writeErrorResponse(w, toAPIErrorCode(err), r.URL) + return + } + + listResponse := generateListBucketsResponse(bucketsInfo) + // Write success response. + writeSuccessResponseXML(w, encodeResponse(listResponse)) +} + // HealBucketHandler - POST /?heal&bucket=mybucket // - bucket is mandatory query parameter // Heal a given bucket, if present. diff --git a/cmd/admin-handlers_test.go b/cmd/admin-handlers_test.go index 7daa801f7..138478213 100644 --- a/cmd/admin-handlers_test.go +++ b/cmd/admin-handlers_test.go @@ -687,7 +687,7 @@ func TestListObjectsHealHandler(t *testing.T) { if err != nil { t.Fatalf("Test %d - Failed to construct list objects needing heal request - %v", i+1, err) } - req.Header.Set(minioAdminOpHeader, "list") + req.Header.Set(minioAdminOpHeader, "list-objects") cred := serverConfig.GetCredential() err = signRequestV4(req, cred.AccessKey, cred.SecretKey) diff --git a/cmd/admin-router.go b/cmd/admin-router.go index 3c5f63a96..a21b1ffb6 100644 --- a/cmd/admin-router.go +++ b/cmd/admin-router.go @@ -49,7 +49,10 @@ func registerAdminRouter(mux *router.Router) { /// Heal operations // List Objects needing heal. - adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list").HandlerFunc(adminAPI.ListObjectsHealHandler) + adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-objects").HandlerFunc(adminAPI.ListObjectsHealHandler) + // List Buckets needing heal. + adminRouter.Methods("GET").Queries("heal", "").Headers(minioAdminOpHeader, "list-buckets").HandlerFunc(adminAPI.ListBucketsHealHandler) + // Heal Buckets. adminRouter.Methods("POST").Queries("heal", "").Headers(minioAdminOpHeader, "bucket").HandlerFunc(adminAPI.HealBucketHandler) // Heal Objects. diff --git a/cmd/api-response.go b/cmd/api-response.go index f07779112..447a1e732 100644 --- a/cmd/api-response.go +++ b/cmd/api-response.go @@ -181,8 +181,9 @@ type CommonPrefix struct { // Bucket container for bucket metadata type Bucket struct { - Name string - CreationDate string // time string of format "2006-01-02T15:04:05.000Z" + Name string + CreationDate string // time string of format "2006-01-02T15:04:05.000Z" + HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"` } // Object container for object metadata @@ -196,8 +197,8 @@ type Object struct { Owner Owner // The class of storage used to store the object. - StorageClass string - HealInfo *HealInfo `xml:"HealInfo,omitempty"` + StorageClass string + HealObjectInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"` } // CopyObjectResponse container returns ETag and LastModified of the successfully copied object @@ -285,6 +286,7 @@ func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse { var listbucket = Bucket{} listbucket.Name = bucket.Name listbucket.CreationDate = bucket.Created.Format(timeFormatAMZLong) + listbucket.HealBucketInfo = bucket.HealBucketInfo listbuckets = append(listbuckets, listbucket) } @@ -317,8 +319,8 @@ func generateListObjectsV1Response(bucket, prefix, marker, delimiter string, max content.Size = object.Size content.StorageClass = globalMinioDefaultStorageClass content.Owner = owner - // object.HealInfo is non-empty only when resp is constructed in ListObjectsHeal. - content.HealInfo = object.HealInfo + // object.HealObjectInfo is non-empty only when resp is constructed in ListObjectsHeal. + content.HealObjectInfo = object.HealObjectInfo contents = append(contents, content) } // TODO - support EncodingType in xml decoding diff --git a/cmd/fs-v1.go b/cmd/fs-v1.go index 24c25b124..adcdb7542 100644 --- a/cmd/fs-v1.go +++ b/cmd/fs-v1.go @@ -897,3 +897,8 @@ func (fs fsObjects) HealBucket(bucket string) error { func (fs fsObjects) ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) { return ListObjectsInfo{}, traceError(NotImplemented{}) } + +// ListBucketsHeal - list all buckets to be healed. Valid only for XL +func (fs fsObjects) ListBucketsHeal() ([]BucketInfo, error) { + return []BucketInfo{}, traceError(NotImplemented{}) +} diff --git a/cmd/object-api-datatypes.go b/cmd/object-api-datatypes.go index 336fb16b2..0da114699 100644 --- a/cmd/object-api-datatypes.go +++ b/cmd/object-api-datatypes.go @@ -50,6 +50,20 @@ type StorageInfo struct { } } +type healStatus int + +const ( + healthy healStatus = iota // Object is healthy + canHeal // Object can be healed + corrupted // Object can't be healed + quorumUnavailable // Object can't be healed until read quorum is available +) + +// HealBucketInfo - represents healing related information of a bucket. +type HealBucketInfo struct { + Status healStatus +} + // BucketInfo - represents bucket metadata. type BucketInfo struct { // Name of the bucket. @@ -57,18 +71,13 @@ type BucketInfo struct { // Date and time when the bucket was created. Created time.Time + + // Healing information + HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"` } -type healStatus int - -const ( - canHeal healStatus = iota // Object can be healed - corrupted // Object can't be healed - quorumUnavailable // Object can't be healed until read quorum is available -) - -// HealInfo - represents healing related information of an object. -type HealInfo struct { +// HealObjectInfo - represents healing related information of an object. +type HealObjectInfo struct { Status healStatus MissingDataCount int MissingPartityCount int @@ -103,8 +112,8 @@ type ObjectInfo struct { ContentEncoding string // User-Defined metadata - UserDefined map[string]string - HealInfo *HealInfo `xml:"HealInfo,omitempty"` + UserDefined map[string]string + HealObjectInfo *HealObjectInfo `xml:"HealObjectInfo,omitempty"` } // ListPartsInfo - represents list of all parts. diff --git a/cmd/object-api-interface.go b/cmd/object-api-interface.go index b0a04510c..6687d9594 100644 --- a/cmd/object-api-interface.go +++ b/cmd/object-api-interface.go @@ -48,6 +48,7 @@ type ObjectLayer interface { // Healing operations. HealBucket(bucket string) error + ListBucketsHeal() (buckets []BucketInfo, err error) HealObject(bucket, object string) error ListObjectsHeal(bucket, prefix, marker, delimiter string, maxKeys int) (ListObjectsInfo, error) } diff --git a/cmd/xl-v1-healing-common.go b/cmd/xl-v1-healing-common.go index c87d60977..d652bc462 100644 --- a/cmd/xl-v1-healing-common.go +++ b/cmd/xl-v1-healing-common.go @@ -141,12 +141,12 @@ func xlShouldHeal(partsMetadata []xlMetaV1, errs []error) bool { // xlHealStat - returns a structure which describes how many data, // parity erasure blocks are missing and if it is possible to heal // with the blocks present. -func xlHealStat(xl xlObjects, partsMetadata []xlMetaV1, errs []error) HealInfo { +func xlHealStat(xl xlObjects, partsMetadata []xlMetaV1, errs []error) HealObjectInfo { // Less than quorum erasure coded blocks of the object have the same create time. // This object can't be healed with the information we have. modTime, count := commonTime(listObjectModtimes(partsMetadata, errs)) if count < xl.readQuorum { - return HealInfo{ + return HealObjectInfo{ Status: quorumUnavailable, MissingDataCount: 0, MissingPartityCount: 0, @@ -156,7 +156,7 @@ func xlHealStat(xl xlObjects, partsMetadata []xlMetaV1, errs []error) HealInfo { // If there isn't a valid xlMeta then we can't heal the object. xlMeta, err := pickValidXLMeta(partsMetadata, modTime) if err != nil { - return HealInfo{ + return HealObjectInfo{ Status: corrupted, MissingDataCount: 0, MissingPartityCount: 0, @@ -183,7 +183,7 @@ func xlHealStat(xl xlObjects, partsMetadata []xlMetaV1, errs []error) HealInfo { // This object can be healed. We have enough object metadata // to reconstruct missing erasure coded blocks. - return HealInfo{ + return HealObjectInfo{ Status: canHeal, MissingDataCount: missingDataCount, MissingPartityCount: missingParityCount, diff --git a/cmd/xl-v1-healing.go b/cmd/xl-v1-healing.go index 4b1b89dbe..49a03676b 100644 --- a/cmd/xl-v1-healing.go +++ b/cmd/xl-v1-healing.go @@ -19,6 +19,7 @@ package cmd import ( "fmt" "path" + "sort" "sync" ) @@ -153,9 +154,11 @@ func healBucketMetadata(storageDisks []StorageAPI, bucket string, readQuorum int return healBucketMetaFn(lConfigPath) } -// listBucketNames list all bucket names from all disks to heal. -func listBucketNames(storageDisks []StorageAPI) (bucketNames map[string]struct{}, err error) { - bucketNames = make(map[string]struct{}) +// listAllBuckets lists all buckets from all disks. It also +// returns the occurrence of each buckets in all disks +func listAllBuckets(storageDisks []StorageAPI) (buckets map[string]VolInfo, bucketsOcc map[string]int, err error) { + buckets = make(map[string]VolInfo) + bucketsOcc = make(map[string]int) for _, disk := range storageDisks { if disk == nil { continue @@ -173,7 +176,10 @@ func listBucketNames(storageDisks []StorageAPI) (bucketNames map[string]struct{} if isMinioMetaBucketName(volInfo.Name) { continue } - bucketNames[volInfo.Name] = struct{}{} + // Increase counter per bucket name + bucketsOcc[volInfo.Name]++ + // Save volume info under bucket name + buckets[volInfo.Name] = volInfo } continue } @@ -183,7 +189,101 @@ func listBucketNames(storageDisks []StorageAPI) (bucketNames map[string]struct{} } break } - return bucketNames, err + return buckets, bucketsOcc, err +} + +// reduceHealStatus - fetches the worst heal status in a provided slice +func reduceHealStatus(status []healStatus) healStatus { + worstStatus := healthy + for _, st := range status { + if st > worstStatus { + worstStatus = st + } + } + return worstStatus +} + +// bucketHealStatus - returns the heal status of the provided bucket. Internally, +// this function lists all object heal status of objects inside meta bucket config +// directory and returns the worst heal status that can be found +func (xl xlObjects) bucketHealStatus(bucketName string) (healStatus, error) { + // A list of all the bucket config files + configFiles := []string{bucketPolicyConfig, bucketNotificationConfig, bucketListenerConfig} + // The status of buckets config files + configsHealStatus := make([]healStatus, len(configFiles)) + // The list of errors found during checking heal status of each config file + configsErrs := make([]error, len(configFiles)) + // The path of meta bucket that contains all config files + configBucket := path.Join(minioMetaBucket, bucketConfigPrefix, bucketName) + + // Check of config files heal status in go-routines + var wg sync.WaitGroup + // Loop over config files + for idx, configFile := range configFiles { + wg.Add(1) + // Compute heal status of current config file + go func(bucket, object string, index int) { + defer wg.Done() + // Check + listObjectsHeal, err := xl.listObjectsHeal(bucket, object, "", "", 1) + // If any error, save and immediately quit + if err != nil { + configsErrs[index] = err + return + } + // Check if current bucket contains any not healthy config file and save heal status + if len(listObjectsHeal.Objects) > 0 { + configsHealStatus[index] = listObjectsHeal.Objects[0].HealObjectInfo.Status + } + }(configBucket, configFile, idx) + } + wg.Wait() + + // Return any found error + for _, err := range configsErrs { + if err != nil { + return healthy, err + } + } + + // Reduce and return heal status + return reduceHealStatus(configsHealStatus), nil +} + +// ListBucketsHeal - Find all buckets that need to be healed +func (xl xlObjects) ListBucketsHeal() ([]BucketInfo, error) { + listBuckets := []BucketInfo{} + // List all buckets that can be found in all disks + buckets, occ, err := listAllBuckets(xl.storageDisks) + if err != nil { + return listBuckets, err + } + // Iterate over all buckets + for _, currBucket := range buckets { + // Check the status of bucket metadata + bucketHealStatus, err := xl.bucketHealStatus(currBucket.Name) + if err != nil { + return []BucketInfo{}, err + } + // If all metadata are sane, check if the bucket directory is present in all disks + if bucketHealStatus == healthy && occ[currBucket.Name] != len(xl.storageDisks) { + // Current bucket is missing in some of the storage disks + bucketHealStatus = canHeal + } + // Add current bucket to the returned result if not healthy + if bucketHealStatus != healthy { + listBuckets = append(listBuckets, + BucketInfo{ + Name: currBucket.Name, + Created: currBucket.Created, + HealBucketInfo: &HealBucketInfo{Status: bucketHealStatus}, + }) + } + + } + // Sort found buckets + sort.Sort(byBucketName(listBuckets)) + return listBuckets, nil } // This function is meant for all the healing that needs to be done @@ -196,7 +296,7 @@ func listBucketNames(storageDisks []StorageAPI) (bucketNames map[string]struct{} // - add support for healing dangling `xl.json`. func quickHeal(storageDisks []StorageAPI, writeQuorum int, readQuorum int) error { // List all bucket names from all disks. - bucketNames, err := listBucketNames(storageDisks) + bucketNames, _, err := listAllBuckets(storageDisks) if err != nil { return err } diff --git a/cmd/xl-v1-healing_test.go b/cmd/xl-v1-healing_test.go index 728e573e8..1acbd9909 100644 --- a/cmd/xl-v1-healing_test.go +++ b/cmd/xl-v1-healing_test.go @@ -423,3 +423,66 @@ func TestQuickHeal(t *testing.T) { t.Fatal("Got an unexpected error: ", err) } } + +// TestListBucketsHeal lists buckets heal result +func TestListBucketsHeal(t *testing.T) { + root, err := newTestConfig("us-east-1") + if err != nil { + t.Fatal(err) + } + defer removeAll(root) + + nDisks := 16 + fsDirs, err := getRandomDisks(nDisks) + if err != nil { + t.Fatal(err) + } + defer removeRoots(fsDirs) + + endpoints, err := parseStorageEndpoints(fsDirs) + if err != nil { + t.Fatal(err) + } + + obj, _, err := initObjectLayer(endpoints) + if err != nil { + t.Fatal(err) + } + + // Create a bucket that won't get corrupted + saneBucket := "sanebucket" + if err = obj.MakeBucket(saneBucket); err != nil { + t.Fatal(err) + } + + // Create a bucket that will be removed in some disks + corruptedBucketName := getRandomBucketName() + if err = obj.MakeBucket(corruptedBucketName); err != nil { + t.Fatal(err) + } + + xl := obj.(*xlObjects) + + // Remove bucket in disk 0, 1 and 2 + for i := 0; i <= 2; i++ { + if err = xl.storageDisks[i].DeleteVol(corruptedBucketName); err != nil { + t.Fatal(err) + } + } + + // List the missing buckets. + buckets, err := xl.ListBucketsHeal() + if err != nil { + t.Fatal(err) + } + + // Check the number of buckets in list buckets heal result + if len(buckets) != 1 { + t.Fatalf("Length of missing buckets is incorrect, expected: 1, found: %d", len(buckets)) + } + + // Check the name of bucket in list buckets heal result + if buckets[0].Name != corruptedBucketName { + t.Fatalf("Name of missing bucket is incorrect, expected: %s, found: %s", corruptedBucketName, buckets[0].Name) + } +} diff --git a/cmd/xl-v1-list-objects-heal.go b/cmd/xl-v1-list-objects-heal.go index 2438c17a2..1fb6702ac 100644 --- a/cmd/xl-v1-list-objects-heal.go +++ b/cmd/xl-v1-list-objects-heal.go @@ -159,11 +159,11 @@ func (xl xlObjects) listObjectsHeal(bucket, prefix, marker, delimiter string, ma if xlShouldHeal(partsMetadata, errs) { healStat := xlHealStat(xl, partsMetadata, errs) result.Objects = append(result.Objects, ObjectInfo{ - Name: objInfo.Name, - ModTime: objInfo.ModTime, - Size: objInfo.Size, - IsDir: false, - HealInfo: &healStat, + Name: objInfo.Name, + ModTime: objInfo.ModTime, + Size: objInfo.Size, + IsDir: false, + HealObjectInfo: &healStat, }) } objectLock.RUnlock() diff --git a/docs/admin-api/management-api.md b/docs/admin-api/management-api.md index b8ed45489..dc0167c82 100644 --- a/docs/admin-api/management-api.md +++ b/docs/admin-api/management-api.md @@ -112,3 +112,9 @@ - ErrInvalidBucketName - ErrInvalidObjectName - ErrInvalidDuration + +### Healing + +* ListBucketsHeal + - GET /?heal + - x-minio-operation: list-buckets diff --git a/pkg/madmin/API.md b/pkg/madmin/API.md index 87ba3a3d1..8b6d0393a 100644 --- a/pkg/madmin/API.md +++ b/pkg/madmin/API.md @@ -171,8 +171,8 @@ __Example__ log.Fatalln(err) return } - if object.HealInfo != nil { - switch healInfo := *object.HealInfo; healInfo.Status { + if object.HealObjectInfo != nil { + switch healInfo := *object.HealObjectInfo; healInfo.Status { case madmin.CanHeal: fmt.Println(object.Key, " can be healed.") case madmin.QuorumUnavailable: @@ -185,6 +185,34 @@ __Example__ } ``` + +### ListBucketsList() error +If successful returns information on the list of buckets that need healing. + +__Example__ + +``` go + // List buckets that need healing + healBucketsList, err := madmClnt.ListBucketsHeal() + if err != nil { + fmt.Println(err) + return + } + for bucket := range healBucketsList { + if bucket.HealBucketInfo != nil { + switch healInfo := *object.HealBucketInfo; healInfo.Status { + case madmin.CanHeal: + fmt.Println(bucket.Key, " can be healed.") + case madmin.QuorumUnavailable: + fmt.Println(bucket.Key, " can't be healed until quorum is available.") + case madmin.Corrupted: + fmt.Println(bucket.Key, " can't be healed, not enough information.") + } + } + fmt.Println("bucket: ", bucket) + } +``` + ### HealBucket(bucket string, isDryRun bool) error If bucket is successfully healed returns nil, otherwise returns error indicating the reason for failure. If isDryRun is true, then the bucket is not healed, but heal bucket request is validated by the server. e.g, if the bucket exists, if bucket name is valid etc. diff --git a/pkg/madmin/examples/heal-buckets-list.go b/pkg/madmin/examples/heal-buckets-list.go new file mode 100644 index 000000000..88b561651 --- /dev/null +++ b/pkg/madmin/examples/heal-buckets-list.go @@ -0,0 +1,60 @@ +// +build ignore + +package main + +/* + * Minio Cloud Storage, (C) 2017 Minio, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import ( + "fmt" + "log" + + "github.com/minio/minio/pkg/madmin" +) + +func main() { + + // Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are + // dummy values, please replace them with original values. + + // API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise. + // New returns an Minio Admin client object. + madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true) + if err != nil { + log.Fatalln(err) + } + + // List buckets that need healing + healBucketsList, err := madmClnt.ListBucketsHeal() + if err != nil { + log.Fatalln(err) + } + + for _, bucket := range healBucketsList { + if bucket.HealBucketInfo != nil { + switch healInfo := *bucket.HealBucketInfo; healInfo.Status { + case madmin.CanHeal: + fmt.Println(bucket.Name, " can be healed.") + case madmin.QuorumUnavailable: + fmt.Println(bucket.Name, " can't be healed until quorum is available.") + case madmin.Corrupted: + fmt.Println(bucket.Name, " can't be healed, not enough information.") + } + } + fmt.Println("bucket: ", bucket) + } +} diff --git a/pkg/madmin/examples/heal-list.go b/pkg/madmin/examples/heal-objects-list.go similarity index 100% rename from pkg/madmin/examples/heal-list.go rename to pkg/madmin/examples/heal-objects-list.go diff --git a/pkg/madmin/heal-commands.go b/pkg/madmin/heal-commands.go index 84f2aa3c8..e439b84d2 100644 --- a/pkg/madmin/heal-commands.go +++ b/pkg/madmin/heal-commands.go @@ -63,20 +63,65 @@ type commonPrefix struct { Prefix string } +// Owner - bucket owner/principal +type Owner struct { + ID string + DisplayName string +} + +// Bucket container for bucket metadata +type Bucket struct { + Name string + CreationDate string // time string of format "2006-01-02T15:04:05.000Z" + + HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"` +} + +// ListBucketsHealResponse - format for list buckets response +type ListBucketsHealResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"` + + Owner Owner + + // Container for one or more buckets. + Buckets struct { + Buckets []Bucket `xml:"Bucket"` + } // Buckets are nested +} + // HealStatus - represents different states of healing an object could be in. type healStatus int const ( + // Healthy - Object that is already healthy + Healthy healStatus = iota // CanHeal - Object can be healed - CanHeal healStatus = iota + CanHeal // Corrupted - Object can't be healed Corrupted // QuorumUnavailable - Object can't be healed until read quorum is available QuorumUnavailable ) -// HealInfo - represents healing related information of an object. -type HealInfo struct { +// HealBucketInfo - represents healing related information of a bucket. +type HealBucketInfo struct { + Status healStatus +} + +// BucketInfo - represents bucket metadata. +type BucketInfo struct { + // Name of the bucket. + Name string + + // Date and time when the bucket was created. + Created time.Time + + // Healing information + HealBucketInfo *HealBucketInfo `xml:"HealBucketInfo,omitempty"` +} + +// HealObjectInfo - represents healing related information of an object. +type HealObjectInfo struct { Status healStatus MissingDataCount int MissingPartityCount int @@ -108,8 +153,8 @@ type ObjectInfo struct { StorageClass string `json:"storageClass"` // Error - Err error `json:"-"` - HealInfo *HealInfo `json:"healInfo,omitempty"` + Err error `json:"-"` + HealObjectInfo *HealObjectInfo `json:"healObjectInfo,omitempty"` } type healQueryKey string @@ -143,7 +188,7 @@ func (adm *AdminClient) listObjectsHeal(bucket, prefix, delimiter, marker string queryVal := mkHealQueryVal(bucket, prefix, marker, delimiter, maxKeyStr) hdrs := make(http.Header) - hdrs.Set(minioAdminOpHeader, "list") + hdrs.Set(minioAdminOpHeader, "list-objects") reqData := requestData{ queryValues: queryVal, @@ -240,6 +285,58 @@ func (adm *AdminClient) ListObjectsHeal(bucket, prefix string, recursive bool, d return objectStatCh, nil } +const timeFormatAMZLong = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision. + +// ListBucketsHeal - issues heal bucket list API request +func (adm *AdminClient) ListBucketsHeal() ([]BucketInfo, error) { + queryVal := url.Values{} + queryVal.Set("heal", "") + + hdrs := make(http.Header) + hdrs.Set(minioAdminOpHeader, "list-buckets") + + reqData := requestData{ + queryValues: queryVal, + customHeaders: hdrs, + } + + // Execute GET on /?heal to list objects needing heal. + resp, err := adm.executeMethod("GET", reqData) + + defer closeResponse(resp) + if err != nil { + return []BucketInfo{}, err + } + + if resp.StatusCode != http.StatusOK { + return []BucketInfo{}, errors.New("Got HTTP Status: " + resp.Status) + } + + var listBucketsHealResult ListBucketsHealResponse + + err = xml.NewDecoder(resp.Body).Decode(&listBucketsHealResult) + if err != nil { + return []BucketInfo{}, err + } + + var bucketsToBeHealed []BucketInfo + + for _, bucket := range listBucketsHealResult.Buckets.Buckets { + creationDate, err := time.Parse(timeFormatAMZLong, bucket.CreationDate) + if err != nil { + return []BucketInfo{}, err + } + bucketsToBeHealed = append(bucketsToBeHealed, + BucketInfo{ + Name: bucket.Name, + Created: creationDate, + HealBucketInfo: bucket.HealBucketInfo, + }) + } + + return bucketsToBeHealed, nil +} + // HealBucket - Heal the given bucket func (adm *AdminClient) HealBucket(bucket string, dryrun bool) error { // Construct query params.