diff --git a/doc/endpoints.md b/doc/endpoints.md index bd5da58fe..b8b93ebc5 100644 --- a/doc/endpoints.md +++ b/doc/endpoints.md @@ -16,6 +16,7 @@ Name | Methods | Path | Request | Response GetVersion | GET | /version | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [VersionResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VersionResp) VolumeCreate | POST | /volumes | [VolCreateReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolCreateReq) | [VolumeCreateResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolumeCreateResp) VolumeExpand | POST | /volumes/{volname}/expand | [VolExpandReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolExpandReq) | [VolumeExpandResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolumeExpandResp) +VolumeShrink | POST | /volumes/{volname}/shrink | [VolShrinkReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolShrinkReq) | [VolumeShrinkResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolumeShrinkResp) VolumeOptions | POST | /volumes/{volname}/options | [VolOptionReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolOptionReq) | [VolumeOptionResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolumeOptionResp) VolumeReset | DELETE | /volumes/{volname}/options | [VolOptionResetReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolOptionResetReq) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) OptionGroupList | GET | /volumes/options-group | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [OptionGroupListResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#OptionGroupListResp) @@ -33,6 +34,17 @@ VolfilesGenerate | POST | /volfiles | [](https://godoc.org/github.com/gluster/gl VolfilesGet | GET | /volfiles | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) VolfilesGet | GET | /volfiles/{volfileid:.*} | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) EditVolume | POST | /volumes/{volname}/edit | [VolEditReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolEditReq) | [VolumeEditResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolumeEditResp) +SnapshotCreate | POST | /snapshot | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotActivate | POST | /snapshot/{snapname}/activate | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotDeactivate | POST | /snapshot/{snapname}/deactivate | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotClone | POST | /snapshot/{snapname}/clone | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotRestore | POST | /snapshot/{snapname}/restore | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotInfo | GET | /snapshot/{snapname} | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotListAll | GET | /snapshots | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotDelete | DELETE | /snapshot/{snapname} | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotConfigGet | GET | /snapshot/config | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotConfigSet | PUT | /snapshot/config | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SnapshotConfigReset | DELETE | /snapshot/config | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) GetPeer | GET | /peers/{peerid} | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [PeerGetResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#PeerGetResp) GetPeers | GET | /peers | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [PeerListResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#PeerListResp) DeletePeer | DELETE | /peers/{peerid} | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) @@ -67,11 +79,15 @@ WebhookAdd | POST | /events/webhook | [](https://godoc.org/github.com/gluster/gl WebhookDelete | DELETE | /events/webhook | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) WebhookList | GET | /events/webhook | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) EventsList | GET | /events | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) -GlustershEnable | POST | /volumes/{name}/heal/enable | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) -GlustershDisable | POST | /volumes/{name}/heal/disable | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SelfHealInfo | GET | /volumes/{name}/{opts}/heal-info | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [BrickHealInfo](https://godoc.org/github.com/gluster/glusterd2/pkg/api#BrickHealInfo) +SelfHealInfo2 | GET | /volumes/{name}/heal-info | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [BrickHealInfo](https://godoc.org/github.com/gluster/glusterd2/pkg/api#BrickHealInfo) DeviceAdd | POST | /devices/{peerid} | [AddDeviceReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#AddDeviceReq) | [AddDeviceResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#AddDeviceResp) DeviceList | GET | /devices/{peerid} | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [ListDeviceResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#ListDeviceResp) -DeviceEditState | POST | /devices/{peerid} | [EditDeviceStateReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#EditDeviceStateReq) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +ListAllDevices | GET | /devices | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [ListDeviceResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#ListDeviceResp) +RebalanceStart | POST | /volumes/{volname}/rebalance/start | [StartReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#StartReq) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +RebalanceStop | POST | /volumes/{volname}/rebalance/stop | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +RebalanceStatus | GET | /volumes/{volname}/rebalance | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) +SmartVolumeCreate | POST | /smartvol | [VolCreateReq](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolCreateReq) | [VolumeCreateResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#VolumeCreateResp) Statedump | GET | /statedump | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) List Endpoints | GET | /endpoints | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [ListEndpointsResp](https://godoc.org/github.com/gluster/glusterd2/pkg/api#ListEndpointsResp) Glusterd2 service status | GET | /ping | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) | [](https://godoc.org/github.com/gluster/glusterd2/pkg/api#) diff --git a/glusterd2/commands/volumes/commands.go b/glusterd2/commands/volumes/commands.go index dcafd986c..24637501f 100644 --- a/glusterd2/commands/volumes/commands.go +++ b/glusterd2/commands/volumes/commands.go @@ -30,6 +30,14 @@ func (c *Command) Routes() route.Routes { RequestType: utils.GetTypeString((*api.VolExpandReq)(nil)), ResponseType: utils.GetTypeString((*api.VolumeExpandResp)(nil)), HandlerFunc: volumeExpandHandler}, + route.Route{ + Name: "VolumeShrink", + Method: "POST", + Pattern: "/volumes/{volname}/shrink", + Version: 1, + RequestType: utils.GetTypeString((*api.VolShrinkReq)(nil)), + ResponseType: utils.GetTypeString((*api.VolumeShrinkResp)(nil)), + HandlerFunc: volumeShrinkHandler}, // TODO: Implmement volume reset as // DELETE /volumes/{volname}/options route.Route{ @@ -160,6 +168,7 @@ func (c *Command) RegisterStepFuncs() { registerVolStopStepFuncs() registerBricksStatusStepFuncs() registerVolExpandStepFuncs() + registerVolShrinkStepFuncs() registerVolOptionStepFuncs() registerVolStatedumpFuncs() } diff --git a/glusterd2/commands/volumes/volume-shrink-txn.go b/glusterd2/commands/volumes/volume-shrink-txn.go new file mode 100644 index 000000000..5a7f86d5c --- /dev/null +++ b/glusterd2/commands/volumes/volume-shrink-txn.go @@ -0,0 +1,30 @@ +package volumecommands + +import ( + "github.com/gluster/glusterd2/glusterd2/daemon" + "github.com/gluster/glusterd2/glusterd2/transaction" + rebalance "github.com/gluster/glusterd2/plugins/rebalance" + rebalanceapi "github.com/gluster/glusterd2/plugins/rebalance/api" +) + +func startRebalance(c transaction.TxnCtx) error { + var rinfo rebalanceapi.RebalInfo + err := c.Get("rinfo", &rinfo) + if err != nil { + return err + } + + rebalanceProcess, err := rebalance.NewRebalanceProcess(rinfo) + if err != nil { + return err + } + + err = daemon.Start(rebalanceProcess, true, c.Logger()) + if err != nil { + c.Logger().WithError(err).WithField( + "volume", rinfo.Volname).Error("Starting rebalance process failed") + return err + } + + return err +} diff --git a/glusterd2/commands/volumes/volume-shrink.go b/glusterd2/commands/volumes/volume-shrink.go new file mode 100644 index 000000000..93cbf2b98 --- /dev/null +++ b/glusterd2/commands/volumes/volume-shrink.go @@ -0,0 +1,219 @@ +package volumecommands + +import ( + "errors" + "net/http" + "path/filepath" + "strings" + + "github.com/gluster/glusterd2/glusterd2/events" + "github.com/gluster/glusterd2/glusterd2/gdctx" + restutils "github.com/gluster/glusterd2/glusterd2/servers/rest/utils" + "github.com/gluster/glusterd2/glusterd2/transaction" + "github.com/gluster/glusterd2/glusterd2/volume" + "github.com/gluster/glusterd2/pkg/api" + customerror "github.com/gluster/glusterd2/pkg/errors" + rebalance "github.com/gluster/glusterd2/plugins/rebalance" + rebalanceapi "github.com/gluster/glusterd2/plugins/rebalance/api" + + "github.com/gorilla/mux" + "github.com/pborman/uuid" +) + +func registerVolShrinkStepFuncs() { + transaction.RegisterStepFunc(storeVolume, "vol-shrink.UpdateVolinfo") + transaction.RegisterStepFunc(notifyVolfileChange, "vol-shrink.NotifyClients") + transaction.RegisterStepFunc(startRebalance, "vol-shrink.StartRebalance") +} + +func validateVolumeShrinkReq(req api.VolShrinkReq) error { + dupEntry := map[string]bool{} + + for _, brick := range req.Bricks { + if dupEntry[brick.PeerID+filepath.Clean(brick.Path)] == true { + return customerror.ErrDuplicateBrickPath + } + dupEntry[brick.PeerID+filepath.Clean(brick.Path)] = true + + } + + return nil + +} + +func volumeShrinkHandler(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + logger := gdctx.GetReqLogger(ctx) + + volname := mux.Vars(r)["volname"] + + var req api.VolShrinkReq + if err := restutils.UnmarshalRequest(r, &req); err != nil { + restutils.SendHTTPError(ctx, w, http.StatusUnprocessableEntity, err) + return + } + + if err := validateVolumeShrinkReq(req); err != nil { + restutils.SendHTTPError(ctx, w, http.StatusBadRequest, err) + return + } + + txn, err := transaction.NewTxnWithLocks(ctx, volname) + if err != nil { + status, err := restutils.ErrToStatusCode(err) + restutils.SendHTTPError(ctx, w, status, err) + return + } + defer txn.Done() + + volinfo, err := volume.GetVolume(volname) + if err != nil { + restutils.SendHTTPError(ctx, w, http.StatusNotFound, err) + return + } + + for index := range req.Bricks { + for _, b := range req.Bricks { + isPresent := false + for _, brick := range volinfo.Subvols[index].Bricks { + if brick.PeerID.String() == b.PeerID && brick.Path == filepath.Clean(b.Path) { + isPresent = true + break + } + } + if !isPresent { + restutils.SendHTTPError(ctx, w, http.StatusBadRequest, "One or more bricks is not part of given volume") + return + } + } + } + + switch volinfo.Type { + case volume.Distribute: + case volume.Replicate: + case volume.DistReplicate: + if len(req.Bricks)%volinfo.Subvols[0].ReplicaCount != 0 { + err := errors.New("wrong number of bricks to remove") + restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) + return + } + default: + err := errors.New("not implemented: " + volinfo.Type.String()) + restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) + return + + } + + nodes, err := req.Nodes() + if err != nil { + logger.WithError(err).Error("could not prepare node list") + restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) + return + } + + txn.Steps = []*transaction.Step{ + { + DoFunc: "vol-shrink.UpdateVolinfo", + Nodes: []uuid.UUID{gdctx.MyUUID}, + }, + { + DoFunc: "vol-shrink.NotifyClients", + Nodes: nodes, + }, + { + DoFunc: "vol-shrink.StartRebalance", + Nodes: nodes, + }, + } + + decommissionedSubvols, err := findDecommissioned(req.Bricks, volinfo) + if err != nil { + restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) + return + } + + // TODO: Find a better way to store information in the rebalance volfile. + volinfo.Metadata["distribute.decommissioned-bricks"] = strings.TrimSpace(decommissionedSubvols) + + rinfo := rebalanceapi.RebalInfo{ + Volname: volname, + RebalanceID: uuid.NewRandom(), + Cmd: rebalanceapi.CmdStartForce, + State: rebalanceapi.NotStarted, + CommitHash: rebalance.SetCommitHash(), + RebalStats: []rebalanceapi.RebalNodeStatus{}, + } + + if err := txn.Ctx.Set("rinfo", rinfo); err != nil { + restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) + return + } + + if err := txn.Ctx.Set("volinfo", volinfo); err != nil { + restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) + return + } + + if err = txn.Do(); err != nil { + logger.WithError(err).Error("remove bricks start transaction failed") + status, err := restutils.ErrToStatusCode(err) + restutils.SendHTTPError(ctx, w, status, err) + return + } + logger.WithField("volume-name", volinfo.Name).Info("volume shrink successful") + events.Broadcast(volume.NewEvent(volume.EventVolumeShrink, volinfo)) + restutils.SendHTTPResponse(ctx, w, http.StatusOK, decommissionedSubvols) + +} + +func findDecommissioned(bricks []api.BrickReq, volinfo *volume.Volinfo) (string, error) { + + brickSet := make(map[string]bool) + for _, brick := range bricks { + u := uuid.Parse(brick.PeerID) + if u == nil { + return "", errors.New("Invalid nodeid") + } + path, err := filepath.Abs(brick.Path) + if err != nil { + return "", err + } + brickSet[brick.PeerID+":"+path] = true + } + + var subvolMap = make(map[string]int) + for _, subvol := range volinfo.Subvols { + for _, b := range subvol.Bricks { + if brickSet[b.PeerID.String()+":"+b.Path] { + if count, ok := subvolMap[subvol.Name]; !ok { + subvolMap[subvol.Name] = 1 + } else { + subvolMap[subvol.Name] = count + 1 + } + } + } + } + + var base int + switch volinfo.Type { + case volume.Distribute: + base = 1 + case volume.Replicate: + base = len(bricks) + case volume.DistReplicate: + base = volinfo.Subvols[0].ReplicaCount + default: + return "", errors.New("not implemented: " + volinfo.Type.String()) + } + + decommissioned := "" + for subvol, count := range subvolMap { + if count != base { + return "", errors.New("Wrong number of bricks in the subvolume") + } + decommissioned = decommissioned + subvol + " " + } + + return decommissioned, nil +} diff --git a/glusterd2/volume/events.go b/glusterd2/volume/events.go index b43ee2f6d..787a8d342 100644 --- a/glusterd2/volume/events.go +++ b/glusterd2/volume/events.go @@ -13,6 +13,8 @@ const ( EventVolumeCreated Event = "volume.created" // EventVolumeExpanded represents Volume Expand event EventVolumeExpanded = "volume.expanded" + // EventVolumeShrink represents Volume Shrink event + EventVolumeShrink = "volume.shrink" // EventVolumeStarted represents Volume Start event EventVolumeStarted = "volume.started" // EventVolumeStopped represents Volume Stop event diff --git a/pkg/api/brickutils.go b/pkg/api/brickutils.go index 28da271f4..a8c454b8c 100644 --- a/pkg/api/brickutils.go +++ b/pkg/api/brickutils.go @@ -41,3 +41,20 @@ func (req *VolExpandReq) Nodes() ([]uuid.UUID, error) { } return nodes, nil } + +// Nodes extracts list of Peer IDs from Volume Shrink request +func (req *VolShrinkReq) Nodes() ([]uuid.UUID, error) { + var nodesMap = make(map[string]bool) + var nodes []uuid.UUID + for _, brick := range req.Bricks { + if _, ok := nodesMap[brick.PeerID]; !ok { + nodesMap[brick.PeerID] = true + u := uuid.Parse(brick.PeerID) + if u == nil { + return nil, fmt.Errorf("Failed to parse peer ID: %s", brick.PeerID) + } + nodes = append(nodes, u) + } + } + return nodes, nil +} diff --git a/pkg/api/volume_req.go b/pkg/api/volume_req.go index 30a55d46e..ce9ee74a5 100644 --- a/pkg/api/volume_req.go +++ b/pkg/api/volume_req.go @@ -67,6 +67,11 @@ type VolExpandReq struct { Flags map[string]bool `json:"flags,omitempty"` } +// VolShrinkReq represents a request to remove bricks from a volume +type VolShrinkReq struct { + Bricks []BrickReq `json:"bricks"` +} + // VolumeOption represents an option that is part of a profile type VolumeOption struct { Name string `json:"name"` diff --git a/pkg/api/volume_resp.go b/pkg/api/volume_resp.go index a332163d3..1f0d5dedb 100644 --- a/pkg/api/volume_resp.go +++ b/pkg/api/volume_resp.go @@ -92,6 +92,9 @@ type VolumeGetResp VolumeInfo // VolumeExpandResp is the response sent for a volume expand request. type VolumeExpandResp VolumeInfo +// VolumeShrinkResp is the response sent for a volume expand request. +type VolumeShrinkResp VolumeInfo + // VolumeStartResp is the response sent for a volume start request. type VolumeStartResp VolumeInfo diff --git a/plugins/rebalance/rest.go b/plugins/rebalance/rest.go index 9f35d625d..5de115b22 100644 --- a/plugins/rebalance/rest.go +++ b/plugins/rebalance/rest.go @@ -22,7 +22,7 @@ func createRebalanceInfo(volname string, req *rebalanceapi.StartReq) *rebalancea RebalanceID: uuid.NewRandom(), State: rebalanceapi.Started, Cmd: getCmd(req), - CommitHash: setCommitHash(), + CommitHash: SetCommitHash(), RebalStats: []rebalanceapi.RebalNodeStatus{}, } } diff --git a/plugins/rebalance/utils.go b/plugins/rebalance/utils.go index 4065ad3b7..6a5d439a5 100644 --- a/plugins/rebalance/utils.go +++ b/plugins/rebalance/utils.go @@ -20,7 +20,7 @@ var ( hash uint64 ) -func setCommitHash() uint64 { +func SetCommitHash() uint64 { /* We need a commit hash that won't conflict with others we might have