From 9b6e78fe0524cf34cc039df7330869abab17dcb5 Mon Sep 17 00:00:00 2001 From: Oshank Kumar Date: Fri, 30 Nov 2018 15:15:34 +0530 Subject: [PATCH] gd2 plugin: added a plugin for block volume management - added APIs for creation,deleting and listing block volumes. - added pluggable interface for block volume providers. Refer Design Doc: #1319 Signed-off-by: Oshank Kumar --- Gopkg.lock | 2 +- Gopkg.toml | 2 +- glusterd2/commands/volumes/volume-create.go | 94 +++-- glusterd2/commands/volumes/volume-start.go | 45 +-- glusterd2/plugin/plugins.go | 2 + glusterd2/volume/filters.go | 51 +++ pkg/size/size.go | 187 ++++++++++ pkg/size/size_test.go | 166 +++++++++ plugins/blockvolume/api/types.go | 39 ++ .../blockprovider/blockvolume_provider.go | 61 ++++ .../blockprovider/gluster-block/config.go | 65 ++++ .../gluster-block/glusterblock.go | 337 ++++++++++++++++++ .../blockprovider/gluster-block/volume.go | 108 ++++++ plugins/blockvolume/blockprovider/options.go | 71 ++++ plugins/blockvolume/handlers.go | 107 ++++++ plugins/blockvolume/init.go | 12 + plugins/blockvolume/routes.go | 82 +++++ 17 files changed, 1360 insertions(+), 71 deletions(-) create mode 100644 glusterd2/volume/filters.go create mode 100644 pkg/size/size.go create mode 100644 pkg/size/size_test.go create mode 100644 plugins/blockvolume/api/types.go create mode 100644 plugins/blockvolume/blockprovider/blockvolume_provider.go create mode 100644 plugins/blockvolume/blockprovider/gluster-block/config.go create mode 100644 plugins/blockvolume/blockprovider/gluster-block/glusterblock.go create mode 100644 plugins/blockvolume/blockprovider/gluster-block/volume.go create mode 100644 plugins/blockvolume/blockprovider/options.go create mode 100644 plugins/blockvolume/handlers.go create mode 100644 plugins/blockvolume/init.go create mode 100644 plugins/blockvolume/routes.go diff --git a/Gopkg.lock b/Gopkg.lock index 72d3d5575..fd1686094 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -588,4 +588,4 @@ analyzer-version = 1 inputs-digest = "1a1bf3511263a7300c00a98d7b55df50c80fec36ba01c25fbddde4a85dee99f5" solver-name = "gps-cdcl" - solver-version = 1 + solver-version = 1 \ No newline at end of file diff --git a/Gopkg.toml b/Gopkg.toml index f5c1133ce..ee2fe1b74 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -48,7 +48,7 @@ [[constraint]] name = "github.com/sirupsen/logrus" - version = "~1.0.3" + version = "~1.2.0" [[constraint]] name = "github.com/cockroachdb/cmux" diff --git a/glusterd2/commands/volumes/volume-create.go b/glusterd2/commands/volumes/volume-create.go index 716175e81..3190e3f91 100644 --- a/glusterd2/commands/volumes/volume-create.go +++ b/glusterd2/commands/volumes/volume-create.go @@ -1,6 +1,7 @@ package volumecommands import ( + "context" "errors" "net/http" "path/filepath" @@ -102,9 +103,6 @@ func registerVolCreateStepFuncs() { func volumeCreateHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "/volumeCreateHandler") - defer span.End() - logger := gdctx.GetReqLogger(ctx) var err error @@ -114,45 +112,68 @@ func volumeCreateHandler(w http.ResponseWriter, r *http.Request) { return } - if err := validateVolCreateReq(&req); err != nil { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, err) + if status, err := CreateVolume(ctx, req); err != nil { + restutils.SendHTTPError(ctx, w, status, err) return } - if containsReservedGroupProfile(req.Options) { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, gderrors.ErrReservedGroupProfile) + volinfo, err := volume.GetVolume(req.Name) + if err != nil { + // FIXME: If volume was created successfully in the txn above and + // then the store goes down by the time we reach here, what do + // we return to the client ? + restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) return } + logger.WithField("volume-name", volinfo.Name).Info("new volume created") + events.Broadcast(volume.NewEvent(volume.EventVolumeCreated, volinfo)) + + resp := createVolumeCreateResp(volinfo) + restutils.SetLocationHeader(r, w, volinfo.Name) + restutils.SendHTTPResponse(ctx, w, http.StatusCreated, resp) +} + +func createVolumeCreateResp(v *volume.Volinfo) *api.VolumeCreateResp { + return (*api.VolumeCreateResp)(volume.CreateVolumeInfoResp(v)) +} + +// CreateVolume creates a volume +func CreateVolume(ctx context.Context, req api.VolCreateReq) (status int, err error) { + ctx, span := trace.StartSpan(ctx, "/volumeCreateHandler") + defer span.End() + + if err := validateVolCreateReq(&req); err != nil { + return http.StatusBadRequest, err + } + + if containsReservedGroupProfile(req.Options) { + return http.StatusBadRequest, gderrors.ErrReservedGroupProfile + } + if req.Size > 0 { applyDefaults(&req) if req.SnapshotReserveFactor < 1 { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, - errors.New("invalid snapshot reserve factor")) - return + return http.StatusBadRequest, errors.New("invalid snapshot reserve factor") } if err := bricksplanner.PlanBricks(&req); err != nil { - restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } } else { if err := checkDupBrickEntryVolCreate(req); err != nil { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, err) - return + return http.StatusBadRequest, err } } req.Options, err = expandGroupOptions(req.Options) if err != nil { - restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } if err := validateOptions(req.Options, req.VolOptionFlags); err != nil { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, err) - return + return http.StatusBadRequest, err } // Include default Volume Options profile @@ -171,21 +192,17 @@ func volumeCreateHandler(w http.ResponseWriter, r *http.Request) { nodes, err := req.Nodes() if err != nil { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, err) - return + return http.StatusBadRequest, err } txn, err := transactionv2.NewTxnWithLocks(ctx, req.Name) if err != nil { - status, err := restutils.ErrToStatusCode(err) - restutils.SendHTTPError(ctx, w, status, err) - return + return restutils.ErrToStatusCode(err) } defer txn.Done() if volume.Exists(req.Name) { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, gderrors.ErrVolExists) - return + return http.StatusBadRequest, gderrors.ErrVolExists } txn.Steps = []*transaction.Step{ @@ -219,8 +236,7 @@ func volumeCreateHandler(w http.ResponseWriter, r *http.Request) { } if err := txn.Ctx.Set("req", &req); err != nil { - restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) - return + return http.StatusInternalServerError, err } // Add attributes to the span with info that can be viewed along with traces. @@ -231,28 +247,8 @@ func volumeCreateHandler(w http.ResponseWriter, r *http.Request) { ) if err := txn.Do(); err != nil { - status, err := restutils.ErrToStatusCode(err) - restutils.SendHTTPError(ctx, w, status, err) - return + return restutils.ErrToStatusCode(err) } - volinfo, err := volume.GetVolume(req.Name) - if err != nil { - // FIXME: If volume was created successfully in the txn above and - // then the store goes down by the time we reach here, what do - // we return to the client ? - restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) - return - } - - logger.WithField("volume-name", volinfo.Name).Info("new volume created") - events.Broadcast(volume.NewEvent(volume.EventVolumeCreated, volinfo)) - - resp := createVolumeCreateResp(volinfo) - restutils.SetLocationHeader(r, w, volinfo.Name) - restutils.SendHTTPResponse(ctx, w, http.StatusCreated, resp) -} - -func createVolumeCreateResp(v *volume.Volinfo) *api.VolumeCreateResp { - return (*api.VolumeCreateResp)(volume.CreateVolumeInfoResp(v)) + return http.StatusCreated, nil } diff --git a/glusterd2/commands/volumes/volume-start.go b/glusterd2/commands/volumes/volume-start.go index fe8253437..a195bb2da 100644 --- a/glusterd2/commands/volumes/volume-start.go +++ b/glusterd2/commands/volumes/volume-start.go @@ -129,10 +129,6 @@ func registerVolStartStepFuncs() { func volumeStartHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "/volumeStartHandler") - defer span.End() - - logger := gdctx.GetReqLogger(ctx) volname := mux.Vars(r)["volname"] var req api.VolumeStartReq @@ -142,24 +138,39 @@ func volumeStartHandler(w http.ResponseWriter, r *http.Request) { return } - txn, err := transactionv2.NewTxnWithLocks(ctx, volname) + volinfo, status, err := StartVolume(ctx, volname, req) if err != nil { - status, err := restutils.ErrToStatusCode(err) restutils.SendHTTPError(ctx, w, status, err) return } + + events.Broadcast(volume.NewEvent(volume.EventVolumeStarted, volinfo)) + + resp := createVolumeStartResp(volinfo) + restutils.SendHTTPResponse(ctx, w, http.StatusOK, resp) +} + +// StartVolume starts a volume +func StartVolume(ctx context.Context, volname string, req api.VolumeStartReq) (volInfo *volume.Volinfo, status int, err error) { + logger := gdctx.GetReqLogger(ctx) + ctx, span := trace.StartSpan(ctx, "/volumeStartHandler") + defer span.End() + + txn, err := transactionv2.NewTxnWithLocks(ctx, volname) + if err != nil { + status, err := restutils.ErrToStatusCode(err) + return nil, status, err + } defer txn.Done() volinfo, err := volume.GetVolume(volname) if err != nil { status, err := restutils.ErrToStatusCode(err) - restutils.SendHTTPError(ctx, w, status, err) - return + return nil, status, err } if volinfo.State == volume.VolStarted && !req.ForceStartBricks { - restutils.SendHTTPError(ctx, w, http.StatusBadRequest, errors.ErrVolAlreadyStarted) - return + return nil, http.StatusBadRequest, errors.ErrVolAlreadyStarted } txn.Steps = []*transaction.Step{ @@ -182,15 +193,13 @@ func volumeStartHandler(w http.ResponseWriter, r *http.Request) { } if err := txn.Ctx.Set("oldvolinfo", volinfo); err != nil { - restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) - return + return nil, http.StatusInternalServerError, err } volinfo.State = volume.VolStarted if err := txn.Ctx.Set("volinfo", volinfo); err != nil { - restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) - return + return nil, http.StatusInternalServerError, err } span.AddAttributes( @@ -201,14 +210,10 @@ func volumeStartHandler(w http.ResponseWriter, r *http.Request) { if err := txn.Do(); err != nil { logger.WithError(err).WithField( "volume", volname).Error("transaction to start volume failed") - restutils.SendHTTPError(ctx, w, http.StatusInternalServerError, err) - return + return nil, http.StatusInternalServerError, err } - events.Broadcast(volume.NewEvent(volume.EventVolumeStarted, volinfo)) - - resp := createVolumeStartResp(volinfo) - restutils.SendHTTPResponse(ctx, w, http.StatusOK, resp) + return volinfo, http.StatusOK, nil } func createVolumeStartResp(v *volume.Volinfo) *api.VolumeStartResp { diff --git a/glusterd2/plugin/plugins.go b/glusterd2/plugin/plugins.go index 90afd966a..03d46e1bc 100644 --- a/glusterd2/plugin/plugins.go +++ b/glusterd2/plugin/plugins.go @@ -4,6 +4,7 @@ package plugin import ( "github.com/gluster/glusterd2/plugins/bitrot" + "github.com/gluster/glusterd2/plugins/blockvolume" "github.com/gluster/glusterd2/plugins/device" "github.com/gluster/glusterd2/plugins/events" "github.com/gluster/glusterd2/plugins/georeplication" @@ -25,4 +26,5 @@ var PluginsList = []GlusterdPlugin{ &glustershd.Plugin{}, &device.Plugin{}, &rebalance.Plugin{}, + &blockvolume.BlockVolume{}, } diff --git a/glusterd2/volume/filters.go b/glusterd2/volume/filters.go new file mode 100644 index 000000000..30d141120 --- /dev/null +++ b/glusterd2/volume/filters.go @@ -0,0 +1,51 @@ +package volume + +const ( + // BlockHosted is plugin name for FilterBlockHostedVolumes + BlockHosted = "block-hosted" +) + +// Filter will receive a slice of *Volinfo and filters out the undesired one and return slice of desired one only +type Filter func([]*Volinfo) []*Volinfo + +var filters = make(map[string]Filter) + +// InstallFilter will register a custom Filter +func InstallFilter(name string, f Filter) { + filters[name] = f +} + +// ApplyFilters applies all registered filters passed in the args to a slice of *Volinfo +func ApplyFilters(volumes []*Volinfo, names ...string) []*Volinfo { + for _, name := range names { + if filter, found := filters[name]; found { + volumes = filter(volumes) + } + } + return volumes +} + +// ApplyCustomFilters applies all custom filter to a slice of *Volinfo +func ApplyCustomFilters(volumes []*Volinfo, filters ...Filter) []*Volinfo { + for _, filter := range filters { + volumes = filter(volumes) + } + + return volumes +} + +// FilterBlockHostedVolumes filters out volume which are suitable for hosting block volume +func FilterBlockHostedVolumes(volumes []*Volinfo) []*Volinfo { + var volInfos []*Volinfo + for _, volume := range volumes { + val, found := volume.Metadata["block-hosting"] + if found && val == "yes" { + volInfos = append(volInfos, volume) + } + } + return volInfos +} + +func init() { + InstallFilter(BlockHosted, FilterBlockHostedVolumes) +} diff --git a/pkg/size/size.go b/pkg/size/size.go new file mode 100644 index 000000000..98acf8a5d --- /dev/null +++ b/pkg/size/size.go @@ -0,0 +1,187 @@ +package size + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +// Size represents unit to measure information size +type Size uint64 + +// Byte represents one byte of information +const Byte Size = 1 + +const ( + // KiB is multiple of unite Byte the binary prefix Ki represents 2^10 + KiB = 1024 * Byte + // MiB is multiple of unite Byte the binary prefix Mi represents 2^20 + MiB = 1024 * KiB + // GiB is multiple of unite Byte the binary prefix Gi represents 2^30 + GiB = 1024 * MiB + // TiB is multiple of unite Byte the binary prefix Ti represents 2^40 + TiB = 1024 * GiB + // PiB is multiple of unite Byte the binary prefix Pi represents 2^50 + PiB = 1024 * TiB +) + +const ( + // KB is a multiple of the unit byte the prefix K represents 10^3 + KB = 1e3 * Byte + // MB is a multiple of the unit byte the prefix M represents 10^6 + MB = 1e3 * KB + // GB is a multiple of the unit byte the prefix G represents 10^9 + GB = 1e3 * MB + // TB is a multiple of the unit byte the prefix T represents 10^12 + TB = 1e3 * GB + // PB is a multiple of the unit byte the prefix T represents 10^15 + PB = 1e3 * TB +) + +// Bytes returns number of bytes +func (s Size) Bytes() int64 { return int64(s) } + +// KiloBytes returns numbers of KiloBytes in floating point +func (s Size) KiloBytes() float64 { + kb := s / KB + bytes := s % KB + + return float64(kb) + float64(bytes)/1e3 +} + +// MegaBytes returns numbers of MegaBytes in floating point +func (s Size) MegaBytes() float64 { + mb := s / MB + bytes := s % MB + + return float64(mb) + float64(bytes)/(1e6) +} + +// GigaBytes returns number of GigaBytes in floating point +func (s Size) GigaBytes() float64 { + gb := s / GB + bytes := s % GB + + return float64(gb) + float64(bytes)/(1e9) +} + +// TeraBytes returns number of TeraBytes in floating point +func (s Size) TeraBytes() float64 { + tb := s / TB + bytes := s % TB + + return float64(tb) + float64(bytes)/(1e12) +} + +// KibiBytes returns number of KiB in floating point +func (s Size) KibiBytes() float64 { + kib := s / KiB + bytes := s % KiB + + return float64(kib) + float64(bytes)/1024 +} + +// MebiBytes returns number of MiB in floating point +func (s Size) MebiBytes() float64 { + mib := s / MiB + bytes := s % MiB + + return float64(mib) + float64(bytes)/(1024*1024) +} + +// GibiBytes returns number of GiB in floating point +func (s Size) GibiBytes() float64 { + gib := s / GiB + bytes := s % GiB + + return float64(gib) + float64(bytes)/(1024*1024*1024) +} + +// TebiBytes returns number of TiB in floating point +func (s Size) TebiBytes() float64 { + tib := s / TiB + bytes := s % TiB + + return float64(tib) + float64(bytes)/(1024*1024*1024*1024) +} + +// String string representation of Size in form XXKB/MB/TB/GB/Bytes +// TODO: support for string representation in XiB format +func (s Size) String() string { + + if s >= TB { + return fmt.Sprintf("%.2fTB", s.TeraBytes()) + } + + if s >= GB { + return fmt.Sprintf("%.2fGB", s.GigaBytes()) + } + + if s >= MB { + return fmt.Sprintf("%.2fMB", s.MegaBytes()) + } + + if s >= KB { + return fmt.Sprintf("%.2fKB", s.KiloBytes()) + } + + return fmt.Sprintf("%d Bytes", s) +} + +// Parse parses a string representation of size and returns the Size value it represents. +// Supported formats are {TiB,GiB,MiB,KiB,TB,GB,MB,KB} +func Parse(s string) (Size, error) { + var ( + count float64 + size Size + err error + regex = regexp.MustCompile(`^([\d.]+)([KMGT]i?B)$`) + ) + + s = strings.Replace(s, " ", "", -1) + matches := regex.FindStringSubmatch(s) + + if len(matches) != 3 { + return size, errors.New("invalid size format") + } + + switch matches[2] { + case "GiB": + _, err = fmt.Sscanf(s, "%fGiB", &count) + size = Size(count * float64(1*GiB)) + + case "MiB": + _, err = fmt.Sscanf(s, "%fMiB", &count) + size = Size(count * float64(1*MiB)) + + case "KiB": + _, err = fmt.Sscanf(s, "%fKiB", &count) + size = Size(count * float64(1*KiB)) + + case "TiB": + _, err = fmt.Sscanf(s, "%fTiB", &count) + size = Size(count * float64(1*TiB)) + + case "KB": + _, err = fmt.Sscanf(s, "%fKB", &count) + size = Size(count * float64(1*KB)) + + case "MB": + _, err = fmt.Sscanf(s, "%fMB", &count) + size = Size(count * float64(1*MB)) + + case "GB": + _, err = fmt.Sscanf(s, "%fGB", &count) + size = Size(count * float64(1*GB)) + + case "TB": + _, err = fmt.Sscanf(s, "%fTB", &count) + size = Size(count * float64(1*TB)) + + default: + return 0, errors.New("can not parse to size") + } + + return size, err +} diff --git a/pkg/size/size_test.go b/pkg/size/size_test.go new file mode 100644 index 000000000..01b9c620c --- /dev/null +++ b/pkg/size/size_test.go @@ -0,0 +1,166 @@ +package size + +import "testing" + +var ( + kbSizeTests = []struct { + s Size + want float64 + }{ + {Size(1000), 1}, + {Size(2000), 2}, + {Size(2500), 2.5}, + {Size(8750), 8.75}, + } + + mbSizeTests = []struct { + s Size + want float64 + }{ + {Size(1e6), 1}, + {Size(2.5e6), 2.5}, + {Size(8.75e6), 8.75}, + {Size(2047e6), 2047}, + } + + gbSizeTests = []struct { + s Size + want float64 + }{ + {Size(1e9), 1}, + {Size(2.5e9), 2.5}, + {Size(8.75e9), 8.75}, + {Size(0.25e9), 0.25}, + } + + tbSizeTests = []struct { + s Size + want float64 + }{ + {Size(1e12), 1}, + {Size(2.5e12), 2.5}, + {Size(8.75e12), 8.75}, + {Size(0.75e12), 0.75}, + } + + stringSizeTests = []struct { + s Size + want string + }{ + {Size(1e12), "1.00TB"}, + {Size(2.5e9), "2.50GB"}, + {Size(8.75e6), "8.75MB"}, + {Size(768e3), "768.00KB"}, + {Size(500), "500 Bytes"}, + } + + parseSizeTests = []struct { + s string + want Size + }{ + // Binary format + {"1GiB", Size(1 * float64(GiB))}, + {"2.5GiB", Size(2.5 * float64(GiB))}, + {"1MiB", Size(1 * float64(MiB))}, + {"100.5MiB", Size(100.5 * float64(MiB))}, + {"50KiB", Size(50 * float64(KiB))}, + {"0050KiB", Size(50 * float64(KiB))}, + {"2.50KiB", Size(2.5 * float64(KiB))}, + {"2.50TiB", Size(2.5 * float64(TiB))}, + // Decimal format + {"2.50TB", Size(2.5 * float64(TB))}, + {"2.50MB", Size(2.5 * float64(MB))}, + {"0.5KB", Size(0.5 * float64(KB))}, + {"052GB", Size(52 * float64(GB))}, + // having space in between + {"0.5 KB", Size(0.5 * float64(KB))}, + {"052 GB", Size(52 * float64(GB))}, + {"0050 KiB", Size(50 * float64(KiB))}, + {"2.5 KiB", Size(2.5 * float64(KiB))}, + {"2.50 TiB", Size(2.5 * float64(TiB))}, + } + + parseSizeFailureTest = []string{ + "1xGiB", + "x1TiB", + "5kiB", + "7.4xKiB", + "7.4KKiB", + "7.4KMiB", + "7.4MiBT", + // + "5KBM", + "x5KB", + "05xMB", + "5.5.5MB", + } +) + +func TestSizeBytes(t *testing.T) { + var s Size = 2048 + bytes := s.Bytes() + if bytes != 2048 { + t.Errorf("s.Bytes() = %v; want: %v", bytes, 2048) + } +} + +func TestSizeKiloBytes(t *testing.T) { + for _, tt := range kbSizeTests { + if got := tt.s.KiloBytes(); got != tt.want { + t.Errorf("s.KiloBytes() = %v; want: %v", got, tt.want) + } + } +} + +func TestSizeMegaBytes(t *testing.T) { + for _, tt := range mbSizeTests { + if got := tt.s.MegaBytes(); got != tt.want { + t.Errorf("s.MegaBytes() = %v; want: %v", got, tt.want) + } + } +} + +func TestSizeGigaBytes(t *testing.T) { + for _, tt := range gbSizeTests { + if got := tt.s.GigaBytes(); got != tt.want { + t.Errorf("s.GigaBytes() = %v; want: %v", got, tt.want) + } + } +} + +func TestSizeTeraBytes(t *testing.T) { + for _, tt := range tbSizeTests { + if got := tt.s.TeraBytes(); got != tt.want { + t.Errorf("s.TeraBytes() = %v; want: %v", got, tt.want) + } + } +} + +func TestSizeString(t *testing.T) { + for _, tt := range stringSizeTests { + if got := tt.s.String(); got != tt.want { + t.Errorf("s.String() = %v; want: %v", got, tt.want) + } + } +} + +func TestParse(t *testing.T) { + for _, tt := range parseSizeTests { + got, err := Parse(tt.s) + if err != nil { + t.Error("error in s.Parse() :", err) + } else { + if got != tt.want { + t.Errorf("s.Parse() = %v; want: %v", got, tt.want) + } + } + } +} + +func TestParseFailure(t *testing.T) { + for _, s := range parseSizeFailureTest { + if sz, err := Parse(s); err == nil { + t.Errorf("s.Parse() = %v; wanted error", sz) + } + } +} diff --git a/plugins/blockvolume/api/types.go b/plugins/blockvolume/api/types.go new file mode 100644 index 000000000..1a6814685 --- /dev/null +++ b/plugins/blockvolume/api/types.go @@ -0,0 +1,39 @@ +package api + +// BlockVolumeInfo represents block volume info +type BlockVolumeInfo struct { + // HostingVolume name is optional + HostingVolume string `json:"hostingvolume"` + // Name represents block Volume name + Name string `json:"name"` + // Size represents Block Volume size in bytes + Size int64 `json:"size,omitempty"` + HaCount int `json:"hacount,omitempty"` +} + +// BlockVolumeCreateRequest represents req Body for Block vol create req +type BlockVolumeCreateRequest struct { + *BlockVolumeInfo + Clusters []string `json:"clusters,omitempty"` + Auth bool `json:"auth,omitempty"` +} + +// BlockVolumeCreateResp represents resp body for a Block Vol Create req +type BlockVolumeCreateResp struct { + *BlockVolumeInfo + Hosts []string `json:"hosts"` + Iqn string `json:"iqn"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +// BlockVolumeListResp represents resp body for a Block List req +type BlockVolumeListResp []BlockVolumeInfo + +// BlockVolumeGetResp represents a resp body for Block Vol Get req +type BlockVolumeGetResp struct { + *BlockVolumeInfo + Hosts []string `json:"hosts"` + GBID string `json:"gbid"` + Password string `json:"password,omitempty"` +} diff --git a/plugins/blockvolume/blockprovider/blockvolume_provider.go b/plugins/blockvolume/blockprovider/blockvolume_provider.go new file mode 100644 index 000000000..c7a89edca --- /dev/null +++ b/plugins/blockvolume/blockprovider/blockvolume_provider.go @@ -0,0 +1,61 @@ +package blockprovider + +import ( + "fmt" + "sync" + + log "github.com/sirupsen/logrus" +) + +// ProviderFunc returns a block Provider instance. It also returns an error +// If occurred any while creating a Provider instance +type ProviderFunc func() (Provider, error) + +var ( + providersMutex sync.Mutex + providerFactory = make(map[string]ProviderFunc) +) + +// Provider is an abstract, pluggable interface for block volume providers +type Provider interface { + CreateBlockVolume(name string, size int64, hosts []string, options ...BlockVolOption) (BlockVolume, error) + DeleteBlockVolume(name string, options ...BlockVolOption) error + GetBlockVolume(id string) (BlockVolume, error) + BlockVolumes() []BlockVolume + ProviderName() string +} + +// BlockVolume is an interface which provides information about a block volume +type BlockVolume interface { + Name() string + ID() string + HostVolume() string + HostAddresses() []string + IQN() string + Username() string + Password() string + Size() uint64 + HaCount() int +} + +// RegisterBlockProvider will register a block provider +func RegisterBlockProvider(name string, f ProviderFunc) { + providersMutex.Lock() + defer providersMutex.Unlock() + if _, found := providerFactory[name]; found { + log.WithField("name", name).Error("block provider was registered twice") + } + log.WithField("name", name).Infof("Registered block provider") + providerFactory[name] = f +} + +// GetBlockProvider will return a block Provider instance if it has been registered. +func GetBlockProvider(name string) (Provider, error) { + providersMutex.Lock() + defer providersMutex.Unlock() + f, found := providerFactory[name] + if !found { + return nil, fmt.Errorf("%s block provider does not exist", name) + } + return f() +} diff --git a/plugins/blockvolume/blockprovider/gluster-block/config.go b/plugins/blockvolume/blockprovider/gluster-block/config.go new file mode 100644 index 000000000..2ec621d02 --- /dev/null +++ b/plugins/blockvolume/blockprovider/gluster-block/config.go @@ -0,0 +1,65 @@ +package glusterblock + +import ( + "github.com/gluster/glusterd2/pkg/api" + + "github.com/pborman/uuid" + "github.com/spf13/viper" +) + +// VolumeType represents a volume type +type VolumeType string + +const ( + // Replica represents a replica volume type + Replica VolumeType = "Replica" +) + +// ClientConfig holds various config information needed to create a gluster-block rest client +type ClientConfig struct { + HostAddress string + User string + Secret string + CaCertFile string + Insecure bool +} + +// ApplyFromConfig sets the ClientConfig options from various config sources +func (c *ClientConfig) ApplyFromConfig(conf *viper.Viper) { + c.CaCertFile = conf.GetString("gluster-block-cacert") + c.HostAddress = conf.GetString("gluster-block-hostaddr") + c.User = conf.GetString("gluster-block-user") + c.Secret = conf.GetString("gluster-block-secret") + c.Insecure = conf.GetBool("gluster-block-insecure") +} + +// HostingVolumeOptions holds various information which will be used in creating hosting volume +type HostingVolumeOptions struct { + Size int64 + Type VolumeType + ReplicaCount int + AutoCreate bool +} + +// ApplyFromConfig sets HostingVolumeOptions member values from given config source +func (h *HostingVolumeOptions) ApplyFromConfig(conf *viper.Viper) { + h.Size = conf.GetInt64("block-hosting-volume-size") + h.Type = VolumeType(conf.GetString("block-hosting-volume-type")) + h.ReplicaCount = conf.GetInt("block-hosting-volume-replica-count") + h.AutoCreate = conf.GetBool("auto-create-block-hosting-volumes") +} + +// PrepareVolumeCreateReq will create a request body to be use for creating a gluster volume +func (h *HostingVolumeOptions) PrepareVolumeCreateReq() *api.VolCreateReq { + name := "block_hosting_volume_" + uuid.NewRandom().String() + + req := &api.VolCreateReq{ + Name: name, + Transport: "tcp", + Size: uint64(h.Size), + ReplicaCount: h.ReplicaCount, + SubvolType: string(h.Type), + } + + return req +} diff --git a/plugins/blockvolume/blockprovider/gluster-block/glusterblock.go b/plugins/blockvolume/blockprovider/gluster-block/glusterblock.go new file mode 100644 index 000000000..7df086785 --- /dev/null +++ b/plugins/blockvolume/blockprovider/gluster-block/glusterblock.go @@ -0,0 +1,337 @@ +package glusterblock + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/gluster/gluster-block-restapi/client" + "github.com/gluster/gluster-block-restapi/pkg/api" + "github.com/gluster/glusterd2/glusterd2/commands/volumes" + "github.com/gluster/glusterd2/glusterd2/gdctx" + "github.com/gluster/glusterd2/glusterd2/volume" + gd2api "github.com/gluster/glusterd2/pkg/api" + "github.com/gluster/glusterd2/pkg/size" + "github.com/gluster/glusterd2/plugins/blockvolume/blockprovider" + + log "github.com/sirupsen/logrus" + "github.com/spf13/viper" +) + +const providerName = "gluster-block" + +func init() { + log.WithField("name", providerName).Infof("Registering block provider") + blockprovider.RegisterBlockProvider(providerName, newGlusterBlock) +} + +// GlusterBlock implements block Provider interface. It represents a gluster-block +type GlusterBlock struct { + client client.GlusterBlockClient + ClientConf *ClientConfig + HostingVolumeOptions *HostingVolumeOptions +} + +func newGlusterBlock() (blockprovider.Provider, error) { + var ( + gb = &GlusterBlock{} + clientConf = &ClientConfig{} + hostVolOpts = &HostingVolumeOptions{} + opts = []client.OptFuncs{} + ) + + clientConf.ApplyFromConfig(viper.GetViper()) + hostVolOpts.ApplyFromConfig(viper.GetViper()) + + gb.ClientConf = clientConf + gb.HostingVolumeOptions = hostVolOpts + + opts = append(opts, + client.WithAuth(clientConf.User, clientConf.Secret), + client.WithTLSConfig(&client.TLSOptions{CaCertFile: clientConf.CaCertFile, InsecureSkipVerify: clientConf.Insecure}), + ) + + gbClient, err := client.NewClientWithOpts(clientConf.HostAddress, opts...) + if err != nil { + return nil, err + } + gb.client = gbClient + + return gb, nil +} + +// CreateBlockVolume will create a gluster block volume with given name and size. If hosting volume is not provide then it will +// create a gluster volume for hosting gluster block. +func (g *GlusterBlock) CreateBlockVolume(name string, size int64, hosts []string, options ...blockprovider.BlockVolOption) (blockprovider.BlockVolume, error) { + var ( + blockVolOpts = &blockprovider.BlockVolumeOptions{} + volCreateReq = g.HostingVolumeOptions.PrepareVolumeCreateReq() + volInfo *volume.Volinfo + ctx = gdctx.WithReqLogger(context.Background(), log.StandardLogger()) + ) + + blockVolOpts.ApplyOpts(options...) + + // ERROR if If HostingVolume is not specified and auto-create-block-hosting-volumes is false + if blockVolOpts.HostVol == "" && !g.HostingVolumeOptions.AutoCreate { + err := errors.New("host volume is not provided and auto creation is not enabled") + log.WithError(err).Error("failed in creating block volume") + return nil, err + } + + // If HostingVolume name is not empty, then create block volume with requested size. + // If available size is less than requested size then ERROR. Set block related + // metadata and volume options if not exists. + if blockVolOpts.HostVol != "" { + vInfo, err := volume.GetVolume(blockVolOpts.HostVol) + if err != nil { + log.WithError(err).Error("error in fetching volume info") + return nil, err + } + volInfo = vInfo + } + + // If HostingVolume is not specified. List all available volumes and see if any volume is + // available with Metadata:block-hosting=yes + if blockVolOpts.HostVol == "" { + vInfo, err := GetExistingBlockHostingVolume(size) + if err != nil { + log.WithError(err).Debug("no block hosting volumes present") + } + volInfo = vInfo + } + + // If No volumes are available with Metadata:block-hosting=yes or if no space available to create block + // volumes(Metadata:block-hosting-available-size is less than request size), then try to create a new + // block hosting Volume with generated name with default size and volume type configured + if blockVolOpts.HostVol == "" && volInfo == nil { + vInfo, err := CreateBlockHostingVolume(volCreateReq) + if err != nil { + log.WithError(err).Error("error in auto creating block hosting volume") + return nil, err + } + + vInfo, _, err = volumecommands.StartVolume(ctx, vInfo.Name, gd2api.VolumeStartReq{}) + if err != nil { + log.WithError(err).Error("error in starting auto created block hosting volume") + return nil, err + } + + volInfo = vInfo + } + + if _, found := volInfo.Metadata["block-hosting"]; !found { + volInfo.Metadata["block-hosting"] = "yes" + } + + blockHosting := volInfo.Metadata["block-hosting"] + + if strings.ToLower(blockHosting) == "no" { + return nil, errors.New("not a block hosting volume") + } + + if _, found := volInfo.Metadata["block-hosting-available-size"]; !found { + volInfo.Metadata["block-hosting-available-size"] = fmt.Sprintf("%d", g.HostingVolumeOptions.Size) + } + + availableSizeInBytes, err := strconv.Atoi(volInfo.Metadata["block-hosting-available-size"]) + + if err != nil { + return nil, err + } + + if int64(availableSizeInBytes) < size { + return nil, fmt.Errorf("available size is less than requested size,request size: %d, available size: %d", size, availableSizeInBytes) + } + + if volInfo.State != volume.VolStarted { + return nil, errors.New("volume has not been started") + } + + req := &api.BlockVolumeCreateReq{ + HaCount: blockVolOpts.Ha, + AuthEnabled: blockVolOpts.Auth, + FullPrealloc: blockVolOpts.FullPrealloc, + Size: uint64(size), + Storage: blockVolOpts.Storage, + RingBufferSizeInMB: blockVolOpts.RingBufferSizeInMB, + Hosts: hosts, + } + + resp, err := g.client.CreateBlockVolume(volInfo.Name, name, req) + if err != nil { + return nil, err + } + + volInfo.Metadata["block-hosting-available-size"] = fmt.Sprintf("%d", int64(availableSizeInBytes)-size) + + if err := volume.AddOrUpdateVolume(volInfo); err != nil { + log.WithError(err).Error("failed in updating volume info to store") + } + + return &BlockVolume{ + hostVolume: volInfo.Name, + name: name, + hosts: resp.Portals, + iqn: resp.IQN, + username: resp.Username, + password: resp.Password, + size: int64(size), + ha: blockVolOpts.Ha, + }, nil +} + +// DeleteBlockVolume deletes a gluster block volume of give name +func (g *GlusterBlock) DeleteBlockVolume(name string, options ...blockprovider.BlockVolOption) error { + var ( + blockVolOpts = &blockprovider.BlockVolumeOptions{} + hostVol string + ) + + blockVolOpts.ApplyOpts(options...) + + blockVols := g.BlockVolumes() + + for _, blockVol := range blockVols { + if blockVol.Name() == name { + hostVol = blockVol.HostVolume() + break + } + } + + if hostVol == "" { + return errors.New("block volume not found") + } + + blockInfo, err := g.client.BlockVolumeInfo(hostVol, name) + if err != nil { + return err + } + + req := &api.BlockVolumeDeleteReq{ + UnlinkStorage: blockVolOpts.UnlinkStorage, + Force: blockVolOpts.ForceDelete, + } + + if err := g.client.DeleteBlockVolume(hostVol, name, req); err != nil { + return err + } + + if err := ResizeBlockHostingVolume(hostVol, blockInfo.Size); err != nil { + log.WithError(err).Error("error in resizing the block hosting volume") + } + + return nil +} + +// GetBlockVolume gives info about a gluster block volume +func (g *GlusterBlock) GetBlockVolume(name string) (blockprovider.BlockVolume, error) { + var ( + blockVolume blockprovider.BlockVolume + availableBlockVolumes = g.BlockVolumes() + ) + + for _, blockVol := range availableBlockVolumes { + if blockVol.Name() == name { + blockVolume = blockVol + break + } + } + + if blockVolume == nil { + return nil, errors.New("block volume not found") + } + + blockInfo, err := g.client.BlockVolumeInfo(blockVolume.HostVolume(), blockVolume.Name()) + if err != nil { + return nil, err + } + + glusterBlockVol := &BlockVolume{ + name: blockInfo.Name, + hostVolume: blockInfo.Volume, + password: blockInfo.Password, + hosts: blockInfo.ExportedOn, + gbID: blockInfo.GBID, + ha: blockInfo.Ha, + } + + if blockSize, err := size.Parse(blockInfo.Size); err == nil { + glusterBlockVol.size = int64(blockSize) + } + + return glusterBlockVol, nil +} + +// BlockVolumes returns all available gluster block volume +func (g *GlusterBlock) BlockVolumes() []blockprovider.BlockVolume { + var glusterBlockVolumes = []blockprovider.BlockVolume{} + + volumes, err := volume.GetVolumes(context.Background()) + if err != nil { + return glusterBlockVolumes + } + + volumes = volume.ApplyFilters(volumes, volume.BlockHosted) + + for _, vol := range volumes { + blockList, err := g.client.ListBlockVolumes(vol.Name) + if err != nil { + continue + } + + for _, block := range blockList.Blocks { + glusterBlockVolumes = append(glusterBlockVolumes, &BlockVolume{name: block, hostVolume: vol.Name}) + } + } + + return glusterBlockVolumes +} + +// ProviderName returns name of block provider +func (g *GlusterBlock) ProviderName() string { + return providerName +} + +// BlockVolume implements blockprovider.BlockVolume interface. +// It holds information about a gluster-block volume +type BlockVolume struct { + hosts []string + iqn string + username string + password string + hostVolume string + name string + size int64 + gbID string + ha int +} + +// HostAddresses returns host addresses of a gluster block vol +func (gv *BlockVolume) HostAddresses() []string { return gv.hosts } + +// IQN returns IQN of a gluster block vol +func (gv *BlockVolume) IQN() string { return gv.iqn } + +// Username returns username of a gluster-block vol. +func (gv *BlockVolume) Username() string { return gv.username } + +// Password returns password for a gluster block vol +func (gv *BlockVolume) Password() string { return gv.password } + +// HostVolume returns host vol name of gluster block +func (gv *BlockVolume) HostVolume() string { return gv.hostVolume } + +// Name returns name of gluster block vol +func (gv *BlockVolume) Name() string { return gv.name } + +// Size returns size of a gluster block vol in bytes +func (gv *BlockVolume) Size() uint64 { return uint64(gv.size) } + +// ID returns Gluster Block ID +func (gv *BlockVolume) ID() string { return gv.gbID } + +// HaCount returns high availability count +func (gv *BlockVolume) HaCount() int { return gv.ha } diff --git a/plugins/blockvolume/blockprovider/gluster-block/volume.go b/plugins/blockvolume/blockprovider/gluster-block/volume.go new file mode 100644 index 000000000..632d8a044 --- /dev/null +++ b/plugins/blockvolume/blockprovider/gluster-block/volume.go @@ -0,0 +1,108 @@ +package glusterblock + +import ( + "context" + "errors" + "fmt" + "math/rand" + "net/http" + "strconv" + "time" + + "github.com/gluster/glusterd2/glusterd2/commands/volumes" + "github.com/gluster/glusterd2/glusterd2/volume" + "github.com/gluster/glusterd2/pkg/api" + "github.com/gluster/glusterd2/pkg/size" +) + +// BlockSizeFilter returns a volume Filter, which will filter out volumes +// haing block-hosting-available-size greater than give size. +func BlockSizeFilter(size int64) volume.Filter { + return func(volinfos []*volume.Volinfo) []*volume.Volinfo { + var volumes []*volume.Volinfo + + for _, volinfo := range volinfos { + availableSize, found := volinfo.Metadata["block-hosting-available-size"] + if !found { + continue + } + + if availableSizeInBytes, err := strconv.Atoi(availableSize); err == nil && int64(availableSizeInBytes) > size { + volumes = append(volumes, volinfo) + } + } + return volumes + } +} + +// GetExistingBlockHostingVolume returns a existing volume which is suitable for hosting a gluster-block +func GetExistingBlockHostingVolume(size int64) (*volume.Volinfo, error) { + var ( + filters = []volume.Filter{volume.FilterBlockHostedVolumes, BlockSizeFilter(size)} + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + ) + + defer cancel() + + volumes, err := volume.GetVolumes(ctx) + if err != nil || len(volumes) == 0 { + return nil, fmt.Errorf("%v/no volumes found", err) + } + + volumes = volume.ApplyCustomFilters(volumes, filters...) + + return SelectRandomVolume(volumes) +} + +// CreateBlockHostingVolume will create a gluster volume with metadata block-hosting-volume-auto-created=yes +func CreateBlockHostingVolume(req *api.VolCreateReq) (*volume.Volinfo, error) { + status, err := volumecommands.CreateVolume(context.Background(), *req) + if err != nil || status != http.StatusCreated { + return nil, err + } + + vInfo, err := volume.GetVolume(req.Name) + if err != nil { + return nil, err + } + + vInfo.Metadata["block-hosting-volume-auto-created"] = "yes" + return vInfo, nil +} + +// ResizeBlockHostingVolume will adds deletedBlockSize to block-hosting-available-size +// in metadata and update the new vol info to store. +func ResizeBlockHostingVolume(volname string, deletedBlockSize string) error { + volInfo, err := volume.GetVolume(volname) + if err != nil { + return err + } + + deletedSizeInBytes, err := size.Parse(deletedBlockSize) + if err != nil { + return err + } + + if _, found := volInfo.Metadata["block-hosting-available-size"]; !found { + return errors.New("block-hosting-available-size metadata not found for volume") + } + + availableSizeInBytes, err := strconv.Atoi(volInfo.Metadata["block-hosting-available-size"]) + if err != nil { + return err + } + + volInfo.Metadata["block-hosting-available-size"] = fmt.Sprintf("%d", size.Size(availableSizeInBytes)+deletedSizeInBytes) + + return volume.AddOrUpdateVolume(volInfo) +} + +// SelectRandomVolume will select a random volume from a given slice of volumes +func SelectRandomVolume(volumes []*volume.Volinfo) (*volume.Volinfo, error) { + if len(volumes) == 0 { + return nil, errors.New("no available volumes") + } + + i := rand.Int() % len(volumes) + return volumes[i], nil +} diff --git a/plugins/blockvolume/blockprovider/options.go b/plugins/blockvolume/blockprovider/options.go new file mode 100644 index 000000000..08990acfc --- /dev/null +++ b/plugins/blockvolume/blockprovider/options.go @@ -0,0 +1,71 @@ +package blockprovider + +// BlockVolOption configures various optional parameters for a block operation +type BlockVolOption func(*BlockVolumeOptions) + +// BlockVolumeOptions represents various optional params to be used for a block volume operation +type BlockVolumeOptions struct { + HostVol string + Auth bool + FullPrealloc bool + Storage string + Ha int + RingBufferSizeInMB uint64 + ForceDelete bool + UnlinkStorage bool +} + +// ApplyOpts applies configured optional parameters on BlockVolumeOptions +func (op *BlockVolumeOptions) ApplyOpts(optFuncs ...BlockVolOption) { + for _, optFunc := range optFuncs { + optFunc(op) + } +} + +// WithHostVolume configures a HostVolume param +func WithHostVolume(name string) BlockVolOption { + return func(options *BlockVolumeOptions) { + options.HostVol = name + } +} + +// WithHaCount configures haCount for block creation +func WithHaCount(count int) BlockVolOption { + return func(options *BlockVolumeOptions) { + options.Ha = count + } +} + +// WithStorage configures storage param for block-vol creation +func WithStorage(storage string) BlockVolOption { + return func(options *BlockVolumeOptions) { + options.Storage = storage + } +} + +// WithRingBufferSizeInMB configures ring-buffer param (size should in MB units) +func WithRingBufferSizeInMB(size uint64) BlockVolOption { + return func(options *BlockVolumeOptions) { + options.RingBufferSizeInMB = size + } +} + +// WithForceDelete configures force param in a block delete req +func WithForceDelete(options *BlockVolumeOptions) { + options.ForceDelete = true +} + +// WithUnlinkStorage configures unlink-storage param in block delete req +func WithUnlinkStorage(options *BlockVolumeOptions) { + options.UnlinkStorage = true +} + +// WithAuthEnabled enables auth for block creation +func WithAuthEnabled(options *BlockVolumeOptions) { + options.Auth = true +} + +// WithFullPrealloc configures "prealloc" param +func WithFullPrealloc(options *BlockVolumeOptions) { + options.FullPrealloc = true +} diff --git a/plugins/blockvolume/handlers.go b/plugins/blockvolume/handlers.go new file mode 100644 index 000000000..71a77736f --- /dev/null +++ b/plugins/blockvolume/handlers.go @@ -0,0 +1,107 @@ +package blockvolume + +import ( + "github.com/gorilla/mux" + "net/http" + + "github.com/gluster/glusterd2/glusterd2/servers/rest/utils" + "github.com/gluster/glusterd2/plugins/blockvolume/api" + "github.com/gluster/glusterd2/plugins/blockvolume/blockprovider" +) + +// CreateVolume is a http Handler for creating a block volume +func (b *BlockVolume) CreateVolume(w http.ResponseWriter, r *http.Request) { + var ( + req = &api.BlockVolumeCreateRequest{} + resp = &api.BlockVolumeCreateResp{} + opts = []blockprovider.BlockVolOption{} + ) + + if err := utils.UnmarshalRequest(r, req); err != nil { + utils.SendHTTPError(r.Context(), w, http.StatusBadRequest, err) + return + } + + opts = append(opts, + blockprovider.WithHostVolume(req.HostingVolume), + blockprovider.WithHaCount(req.HaCount), + ) + + if req.Auth { + opts = append(opts, blockprovider.WithAuthEnabled) + } + + blockVol, err := b.blockProvider.CreateBlockVolume(req.Name, req.Size, req.Clusters, opts...) + if err != nil { + utils.SendHTTPError(r.Context(), w, http.StatusInternalServerError, err) + return + } + + { + resp.BlockVolumeInfo = req.BlockVolumeInfo + resp.HostingVolume = blockVol.HostVolume() + resp.Name = blockVol.Name() + resp.Iqn = blockVol.IQN() + resp.Username = blockVol.Username() + resp.Password = blockVol.Password() + resp.Hosts = blockVol.HostAddresses() + } + + utils.SendHTTPResponse(r.Context(), w, http.StatusCreated, resp) +} + +// DeleteVolume is a http Handler for deleting a specific block-volume +func (b *BlockVolume) DeleteVolume(w http.ResponseWriter, r *http.Request) { + var ( + pathParams = mux.Vars(r) + ) + + if err := b.blockProvider.DeleteBlockVolume(pathParams["name"]); err != nil { + utils.SendHTTPError(r.Context(), w, http.StatusInternalServerError, err) + return + } + + utils.SendHTTPResponse(r.Context(), w, http.StatusNoContent, nil) +} + +// ListBlockVolumes is a http handler for listing all available block volumes +func (b *BlockVolume) ListBlockVolumes(w http.ResponseWriter, r *http.Request) { + var ( + resp = api.BlockVolumeListResp{} + ) + + blockVols := b.blockProvider.BlockVolumes() + + for _, blockVol := range blockVols { + resp = append(resp, api.BlockVolumeInfo{Name: blockVol.Name(), HostingVolume: blockVol.HostVolume()}) + } + + utils.SendHTTPResponse(r.Context(), w, http.StatusOK, resp) +} + +// GetBlockVolume is a http Handler for getting info about a block volume. +func (b *BlockVolume) GetBlockVolume(w http.ResponseWriter, r *http.Request) { + var ( + pathParams = mux.Vars(r) + resp = &api.BlockVolumeGetResp{} + ) + + blockVol, err := b.blockProvider.GetBlockVolume(pathParams["name"]) + if err != nil { + utils.SendHTTPError(r.Context(), w, http.StatusInternalServerError, err) + return + } + + { + resp.BlockVolumeInfo = &api.BlockVolumeInfo{} + resp.Name = blockVol.Name() + resp.HostingVolume = blockVol.HostVolume() + resp.Size = int64(blockVol.Size()) + resp.Hosts = blockVol.HostAddresses() + resp.Password = blockVol.Password() + resp.GBID = blockVol.ID() + resp.HaCount = blockVol.HaCount() + } + + utils.SendHTTPResponse(r.Context(), w, http.StatusOK, resp) +} diff --git a/plugins/blockvolume/init.go b/plugins/blockvolume/init.go new file mode 100644 index 000000000..4a1291199 --- /dev/null +++ b/plugins/blockvolume/init.go @@ -0,0 +1,12 @@ +package blockvolume + +import ( + // initialise all block providers + _ "github.com/gluster/glusterd2/plugins/blockvolume/blockprovider/gluster-block" + + config "github.com/spf13/viper" +) + +func init() { + config.SetDefault("block-provider", "gluster-block") +} diff --git a/plugins/blockvolume/routes.go b/plugins/blockvolume/routes.go new file mode 100644 index 000000000..f56948bcf --- /dev/null +++ b/plugins/blockvolume/routes.go @@ -0,0 +1,82 @@ +package blockvolume + +import ( + "net/http" + "sync" + + "github.com/gluster/glusterd2/glusterd2/servers/rest/route" + "github.com/gluster/glusterd2/pkg/utils" + "github.com/gluster/glusterd2/plugins/blockvolume/api" + "github.com/gluster/glusterd2/plugins/blockvolume/blockprovider" + + log "github.com/sirupsen/logrus" + config "github.com/spf13/viper" +) + +// BlockVolume represents BlockVolume plugin +type BlockVolume struct { + blockProvider blockprovider.Provider + initOnce sync.Once +} + +// Name returns underlying block provider name +func (b *BlockVolume) Name() string { + b.mustInitBlockProvider() + return b.blockProvider.ProviderName() +} + +// RestRoutes returns list of REST API routes of BlockVolume to register with Glusterd. +func (b *BlockVolume) RestRoutes() route.Routes { + b.mustInitBlockProvider() + return route.Routes{ + { + Name: "BlockCreate", + Method: http.MethodPost, + Pattern: "/blockvolumes", + Version: 1, + RequestType: utils.GetTypeString((*api.BlockVolumeCreateRequest)(nil)), + ResponseType: utils.GetTypeString((*api.BlockVolumeCreateResp)(nil)), + HandlerFunc: b.CreateVolume, + }, + { + Name: "BlockDelete", + Method: http.MethodDelete, + Pattern: "/blockvolumes/{name}", + Version: 1, + HandlerFunc: b.DeleteVolume, + }, + { + Name: "BlockList", + Method: http.MethodGet, + Pattern: "/blockvolumes", + Version: 1, + HandlerFunc: b.ListBlockVolumes, + }, + { + Name: "BlockGet", + Method: http.MethodGet, + Pattern: "/blockvolumes/{name}", + Version: 1, + HandlerFunc: b.GetBlockVolume, + }, + } +} + +// RegisterStepFuncs registers all step functions +// Here it is a no-op func +func (*BlockVolume) RegisterStepFuncs() { + +} + +// mustInitBlockProvider will initialize the underlying block provider only once. +// calling it multiple times will do nothing +func (b *BlockVolume) mustInitBlockProvider() { + b.initOnce.Do(func() { + providerName := config.GetString("block-provider") + provider, err := blockprovider.GetBlockProvider(providerName) + if err != nil { + log.WithError(err).Panic("failed in initializing block-volume provider") + } + b.blockProvider = provider + }) +}