diff --git a/openstack/client.go b/openstack/client.go index 90e917331..7ed6be8c3 100644 --- a/openstack/client.go +++ b/openstack/client.go @@ -511,3 +511,11 @@ func NewOBSService(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) sc, err := initClientOpts(client, eo, "object") return sc, err } + +//TODO: Need to change to sfs client type from evs once available +//NewSFSV2 creates a service client that is used for Huawei cloud for SFS , it replaces the EVS type. +func NewHwSFSV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "evs") + sc.Endpoint = strings.Replace(sc.Endpoint, "evs", "sfs", 1) + return sc, err +} diff --git a/openstack/sfs/v2/common/common_tests.go b/openstack/sfs/v2/common/common_tests.go new file mode 100644 index 000000000..7cbcb6b07 --- /dev/null +++ b/openstack/sfs/v2/common/common_tests.go @@ -0,0 +1,18 @@ +package common + +import ( + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const TokenID = client.TokenID + +// Fake project id to use. +const ProjectID = "17fbda95add24720a4038ba4b1c705ed" + +func ServiceClient() *golangsdk.ServiceClient { + sc := client.ServiceClient() + sc.ResourceBase = sc.Endpoint + "v2/" + sc.ProjectID = ProjectID + return sc +} diff --git a/openstack/sfs/v2/shares/doc.go b/openstack/sfs/v2/shares/doc.go new file mode 100644 index 000000000..3fa6defee --- /dev/null +++ b/openstack/sfs/v2/shares/doc.go @@ -0,0 +1,83 @@ +/* +Package shares enables management and retrieval of shares +Share service. +Example to List Shares + listshares := shares.ListOpts{} + allshares, err := shares.List(client,listshares) + if err != nil { + panic(err) + } + fmt.Println(allshares) + +Example to Create a share + share:=shares.CreateOpts{ + Name:"sfs-test", + ShareProto:"NFS", + Size:1, + } + outshare,err:=shares.Create(client,share).Extract() + if err != nil { + panic(err) + } + fmt.Println(outshare) + +Example to Update a share + updateshare:=shares.UpdateOpts{DisplayName:"sfs-test-1",DisplayDescription:"test sfs"} + out,err:=shares.Update(client,"6149e448-dcac-4691-96d9-041e09ef617f",updateshare).Extract() + if err != nil { + panic(err) + } + fmt.Println(out) + +Example to Delete a share + out:=shares.Delete(client,"6149e448-dcac-4691-96d9-041e09ef617f") + fmt.Println(out) + if err != nil { + panic(err) + } + +Example to Get share + getshare,err:=shares.Get(client, "6149e448-dcac-4691-96d9-041e09ef617f").Extract() + fmt.Println(getshare) + if err != nil { + panic(err) + } +Example to Allow Access +createSet:=shares.GrantAccessOpts{AccessLevel:"rw",AccessTo:"5232f396-d6cc-4a81-8de3-afd7a7ecdfd8",AccessType:"cert"} + access,err:=shares.GrantAccess(client,"dff2df5f-00e7-4517-ac32-1d0ab8dc0d68",createSet).Extract() + fmt.Println(access) + if err != nil { + panic(err) + } + +Example to Deny Access + deleteSet := shares.DeleteAccessOpts{AccessID:"fc32500f-fa78-4f06-8caf-06ad7fb9726c"} + remove:=shares.DeleteAccess(client,"1b8facf8-b822-4349-a033-e078b2a84b7f",deleteSet) + fmt.Println(remove) + if err != nil { + panic(err) + } + +Example to Get Access Rule Detail + rule_list,err:= shares.ListAccessRights(client,"42381b5b-f8cb-445e-9465-89a718e071a7").ExtractAccessRights() + if err != nil { + panic(err) + } + fmt.Println(rule_list) + +Example to Get Mount Location Details + mount, err := shares.GetExportLocations(client, "dff2df5f-00e7-4517-ac32-1d0ab8dc0d68").ExtractExportLocations() + fmt.Println(mount) + if err != nil { + panic(err) + } + +Example to Extend share + extendsfs:=shares.ExpandOpts{OSExtend: shares.OSExtendOpts{NewSize: 512}} + shares.Expand(client,"45a3af18-8ab0-405c-9ead-06c51a415f79",extendsfs) + +Example to Shrink share + shrinksfs:=shares.ShrinkOpts{OSShrink: shares.OSShrinkOpts{NewSize: 8}} + shares.Shrink(client,"45a3af18-8ab0-405c-9ead-06c51a415f79",shrinksfs) +*/ +package shares diff --git a/openstack/sfs/v2/shares/requests.go b/openstack/sfs/v2/shares/requests.go new file mode 100644 index 000000000..bef4422df --- /dev/null +++ b/openstack/sfs/v2/shares/requests.go @@ -0,0 +1,395 @@ +package shares + +import ( + "reflect" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +var RequestOpts golangsdk.RequestOpts = golangsdk.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/json", + "X-Openstack-Manila-Api-Version": "2.9"}, +} + +// SortDir is a type for specifying in which direction to sort a list of Shares. +type SortDir string + +// SortKey is a type for specifying by which key to sort a list of Shares. +type SortKey string + +var ( + // SortAsc is used to sort a list of Shares in ascending order. + SortAsc SortDir = "asc" + // SortDesc is used to sort a list of Shares in descending order. + SortDesc SortDir = "desc" + // SortId is used to sort a list of Shares by id. + SortId SortKey = "id" + // SortName is used to sort a list of Shares by name. + SortName SortKey = "name" + // SortSize is used to sort a list of Shares by size. + SortSize SortKey = "size" + // SortHost is used to sort a list of Shares by host. + SortHost SortKey = "host" + // SortShareProto is used to sort a list of Shares by share_proto. + SortShareProto SortKey = "share_proto" + // SortStatus is used to sort a list of Shares by status. + SortStatus SortKey = "status" + // SortProjectId is used to sort a list of Shares by project_id. + SortProjectId SortKey = "project_id" + // SortShareTypeId is used to sort a list of Shares by share_type_id. + SortShareTypeId SortKey = "share_type_id" + // SortShareNetworkId is used to sort a list of Shares by share_network_id. + SortShareNetworkId SortKey = "share_network_id" + // SortSnapshotId is used to sort a list of Shares by snapshot_id. + SortSnapshotId SortKey = "snapshot_id" + // SortCreatedAt is used to sort a list of Shares by date created. + SortCreatedAt SortKey = "created_at" + // SortUpdatedAt is used to sort a list of Shares by date updated. + SortUpdatedAt SortKey = "updated_at" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToShareListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the share attributes you want to see returned. SortKey allows you to sort +// by a particular share attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string + Status string `q:"status"` + Name string `q:"name"` + Limit int `q:"limit"` + Offset int `q:"offset"` + SortKey SortKey `q:"sort_key"` + SortDir SortDir `q:"sort_dir"` + IsPublic bool `q:"is_public"` +} + +// List returns a Pager which allows you to iterate over a collection of +// share resources. It accepts a ListOpts struct, which allows you to +// filter the returned collection for greater efficiency. +func List(c *golangsdk.ServiceClient, opts ListOpts) ([]Share, error) { + q, err := golangsdk.BuildQueryString(&opts) + if err != nil { + return nil, err + } + u := listURL(c) + q.String() + pages, err := pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SharePage{pagination.LinkedPageBase{PageResult: r}} + }).AllPages() + + allShares, err := ExtractShares(pages) + if err != nil { + return nil, err + } + + return FilterShares(allShares, opts) +} + +func FilterShares(shares []Share, opts ListOpts) ([]Share, error) { + + var refinedShares []Share + var matched bool + m := map[string]interface{}{} + + if opts.ID != "" { + m["ID"] = opts.ID + } + + if len(m) > 0 && len(shares) > 0 { + for _, share := range shares { + matched = true + + for key, value := range m { + if sVal := getStructField(&share, key); !(sVal == value) { + matched = false + } + } + + if matched { + refinedShares = append(refinedShares, share) + } + } + } else { + refinedShares = shares + } + return refinedShares, nil +} + +func getStructField(v *Share, field string) string { + r := reflect.ValueOf(v) + f := reflect.Indirect(r).FieldByName(field) + return string(f.String()) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToShareCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains the options for create a Share. This object is +// passed to shares.Create(). For more information about these parameters, +// please refer to the Share object, or the shared file systems API v2 +// documentation +type CreateOpts struct { + // Defines the share protocol to use + ShareProto string `json:"share_proto" required:"true"` + // Size in GB + Size int `json:"size" required:"true"` + // Defines the share name + Name string `json:"name,omitempty"` + // Share description + Description string `json:"description,omitempty"` + // ShareType defines the sharetype. If omitted, a default share type is used + ShareType string `json:"share_type,omitempty"` + // The UUID from which to create a share + SnapshotID string `json:"snapshot_id,omitempty"` + // Determines whether or not the share is public + IsPublic bool `json:"is_public,omitempty"` + // Key value pairs of user defined metadata + Metadata map[string]string `json:"metadata,omitempty"` + // The UUID of the share network to which the share belongs to + ShareNetworkID string `json:"share_network_id,omitempty"` + // The UUID of the consistency group to which the share belongs to + ConsistencyGroupID string `json:"consistency_group_id,omitempty"` + // The availability zone of the share + AvailabilityZone string `json:"availability_zone,omitempty"` +} + +// ToShareCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToShareCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "share") +} + +// Create will create a new Share based on the values in CreateOpts. To extract +// the Share object from the response, call the Extract method on the +// CreateResult. +func Create(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToShareCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200, 201}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToShareUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts contains the values used when updating a Share. +type UpdateOpts struct { + // DisplayName is equivalent to Name. The API supports using both + // This is an inherited attribute from the block storage API + DisplayName string `json:"display_name" required:"true"` + // DisplayDescription is equivalent to Description. The API supports using bot + // This is an inherited attribute from the block storage API + DisplayDescription string `json:"display_description,omitempty"` +} + +// ToShareUpdateMap builds an update body based on UpdateOpts. +func (opts UpdateOpts) ToShareUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "share") +} + +// Update allows shares to be updated. You can update the DisplayName, DisplayDescription. +func Update(c *golangsdk.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToShareUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Get will get a single share with given UUID +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(resourceURL(client, id), &r.Body, nil) + + return +} + +// Delete will delete an existing Share with the given UUID. +func Delete(client *golangsdk.ServiceClient, id string) (r DeleteResult) { + _, r.Err = client.Delete(resourceURL(client, id), nil) + return +} + +// ListAccessRights lists all access rules assigned to a Share based on its id. To extract +// the AccessRight slice from the response, call the Extract method on the AccessRightsResult. +// Client must have Microversion set; minimum supported microversion for ListAccessRights is 2.7. +func ListAccessRights(client *golangsdk.ServiceClient, share_id string) (r AccessRightsResult) { + requestBody := map[string]interface{}{"os-access_list": nil} + _, r.Err = client.Post(rootURL(client, share_id), requestBody, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// GrantAccessOptsBuilder allows extensions to add additional parameters to the +// GrantAccess request. +type GrantAccessOptsBuilder interface { + ToGrantAccessMap() (map[string]interface{}, error) +} + +// GrantAccessOpts contains the options for creation of an GrantAccess request. +// For more information about these parameters, please, refer to the shared file systems API v2, +// Share Actions, Grant Access documentation +type GrantAccessOpts struct { + // The access rule type that can be "ip", "cert" or "user". + AccessType string `json:"access_type"` + // The value that defines the access that can be a valid format of IP, cert or user. + AccessTo string `json:"access_to"` + // The access level to the share is either "rw" or "ro". + AccessLevel string `json:"access_level"` +} + +// ToGrantAccessMap assembles a request body based on the contents of a +// GrantAccessOpts. +func (opts GrantAccessOpts) ToGrantAccessMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "os-allow_access") +} + +// GrantAccess will grant access to a Share based on the values in GrantAccessOpts. To extract +// the GrantAccess object from the response, call the Extract method on the GrantAccessResult. +// Client must have Microversion set; minimum supported microversion for GrantAccess is 2.7. +func GrantAccess(client *golangsdk.ServiceClient, share_id string, opts GrantAccessOptsBuilder) (r GrantAccessResult) { + b, err := opts.ToGrantAccessMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootURL(client, share_id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete the Access Rule +type DeleteAccessOptsBuilder interface { + ToDeleteAccessMap() (map[string]interface{}, error) +} + +type DeleteAccessOpts struct { + // The access ID to be deleted + AccessID string `json:"access_id"` +} + +func (opts DeleteAccessOpts) ToDeleteAccessMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "os-deny_access") +} + +//Deletes the Access Rule +func DeleteAccess(client *golangsdk.ServiceClient, share_id string, opts DeleteAccessOptsBuilder) (r DeleteAccessResult) { + b, err := opts.ToDeleteAccessMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(rootURL(client, share_id), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +//Gets the Mount/Export Locations of the SFS specified +func GetExportLocations(client *golangsdk.ServiceClient, id string) (r GetExportLocationsResult) { + reqOpt := &golangsdk.RequestOpts{OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders} + _, r.Err = client.Get(getMountLocationsURL(client, id), &r.Body, reqOpt) + return +} + +// ExpandOptsBuilder allows extensions to add additional parameters to the +// Expand request. +type ExpandOptsBuilder interface { + ToShareExpandMap() (map[string]interface{}, error) +} + +// ExpandOpts contains the options for expanding a Share. This object is +// passed to shares.Expand(). For more information about these parameters, +// please refer to the Share object, or the shared file systems API v2 +// documentation +type ExpandOpts struct { + // Specifies the os-extend object. + OSExtend OSExtendOpts `json:"os-extend" required:"true"` +} + +type OSExtendOpts struct { + // Specifies the post-expansion capacity (GB) of the shared file system. + NewSize int `json:"new_size" required:"true"` +} + +// ToShareExpandMap assembles a request body based on the contents of a +// ExpandOpts. +func (opts ExpandOpts) ToShareExpandMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Expand will expand a Share based on the values in ExpandOpts. +func Expand(client *golangsdk.ServiceClient, share_id string, opts ExpandOptsBuilder) (r ExpandResult) { + b, err := opts.ToShareExpandMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(grantAccessURL(client, share_id), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{202}, + }) + return +} + +// ShrinkOptsBuilder allows extensions to add additional parameters to the +// Shrink request. +type ShrinkOptsBuilder interface { + ToShareShrinkMap() (map[string]interface{}, error) +} + +// ShrinkOpts contains the options for shrinking a Share. This object is +// passed to shares.Shrink(). For more information about these parameters, +// please refer to the Share object, or the shared file systems API v2 +// documentation +type ShrinkOpts struct { + // Specifies the os-shrink object. + OSShrink OSShrinkOpts `json:"os-shrink" required:"true"` +} + +type OSShrinkOpts struct { + // Specifies the post-shrinking capacity (GB) of the shared file system. + NewSize int `json:"new_size" required:"true"` +} + +// ToShareShrinkMap assembles a request body based on the contents of a +// ShrinkOpts. +func (opts ShrinkOpts) ToShareShrinkMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Shrink will shrink a Share based on the values in ShrinkOpts. +func Shrink(client *golangsdk.ServiceClient, share_id string, opts ShrinkOptsBuilder) (r ShrinkResult) { + b, err := opts.ToShareShrinkMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(grantAccessURL(client, share_id), b, nil, &golangsdk.RequestOpts{ + OkCodes: []int{202}, + }) + return +} diff --git a/openstack/sfs/v2/shares/results.go b/openstack/sfs/v2/shares/results.go new file mode 100644 index 000000000..0196d1e4d --- /dev/null +++ b/openstack/sfs/v2/shares/results.go @@ -0,0 +1,226 @@ +package shares + +import ( + "encoding/json" + "time" + + "github.com/huaweicloud/golangsdk" + "github.com/huaweicloud/golangsdk/pagination" +) + +// Share contains all information associated with an OpenStack Share +type Share struct { + // The availability zone of the share + AvailabilityZone string `json:"availability_zone"` + // A description of the share + Description string `json:"description"` + // The host name of the share + Host string `json:"host"` + // The UUID of the share + ID string `json:"id"` + // Indicates the visibility of the share + IsPublic bool `json:"is_public"` + // Share links for pagination + Links []map[string]string `json:"links"` + // Key, value -pairs of custom metadata + Metadata map[string]string `json:"metadata"` + // The name of the share + Name string `json:"name"` + // The UUID of the project to which this share belongs to + ProjectID string `json:"project_id"` + // The UUID of the share network + ShareNetworkID string `json:"share_network_id"` + // The shared file system protocol + ShareProto string `json:"share_proto"` + // The UUID of the share type. + ShareType string `json:"share_type"` + // Size of the share in GB + Size int `json:"size"` + // UUID of the snapshot from which to create the share + SnapshotID string `json:"snapshot_id"` + // The share status + Status string `json:"status"` + // The type of the volume + VolumeType string `json:"volume_type"` + // Timestamp when the share was created + CreatedAt time.Time `json:"-"` + //Specifies the mount location. + ExportLocation string `json:"export_location"` + //Lists the mount locations. + ExportLocations []string `json:"export_locations"` +} + +// AccessRight contains all information associated with an OpenStack share +// Grant Access Response +type AccessRight struct { + // The access rule type that can be "ip", "cert" or "user". + AccessType string `json:"access_type"` + // The value that defines the access that can be a valid format of IP, cert or user. + AccessTo string `json:"access_to"` + // The access level to the share is either "rw" or "ro". + AccessLevel string `json:"access_level"` + // The state of the access rule + State string `json:"state"` + // The access rule ID. + ID string `json:"id"` +} + +// ExportLocation contains all information associated with a share export location +type ExportLocation struct { + // The export location path that should be used for mount operation. + Path string `json:"path"` + // The UUID of the share instance that this export location belongs to. + ShareInstanceID string `json:"share_instance_id"` + // Defines purpose of an export location. + // If set to true, then it is expected to be used for service needs + // and by administrators only. + // If it is set to false, then this export location can be used by end users. + IsAdminOnly bool `json:"is_admin_only"` + // The share export location UUID. + ID string `json:"id"` + Preferred bool `json:"preferred"` +} + +// SharePage is the page returned by a pager when traversing over a +// collection of Shares. +type SharePage struct { + pagination.LinkedPageBase +} + +// Extract will get the GrantAccess object from the commonResult +func (r GrantAccessResult) ExtractAccess() (*AccessRight, error) { + var s struct { + AccessRight *AccessRight `json:"access"` + } + err := r.ExtractInto(&s) + return s.AccessRight, err +} + +// Extract will get a slice of AccessRight objects from the AccessRightsResult +func (r AccessRightsResult) ExtractAccessRights() ([]AccessRight, error) { + var s struct { + AccessRights []AccessRight `json:"access_list"` + } + err := r.ExtractInto(&s) + return s.AccessRights, err +} + +// Extract will get the Share object from the commonResult +func (r commonResult) Extract() (*Share, error) { + var s struct { + Share *Share `json:"share"` + } + err := r.ExtractInto(&s) + return s.Share, err +} + +// ExtractShares accepts a Page struct, specifically a SharePage struct, +// and extracts the elements into a slice of share structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractShares(r pagination.Page) ([]Share, error) { + var s struct { + ListedShares []Share `json:"shares"` + } + err := (r.(SharePage)).ExtractInto(&s) + return s.ListedShares, err +} + +// Extract will get the Export Locations from the commonResult +func (r GetExportLocationsResult) ExtractExportLocations() ([]ExportLocation, error) { + var s struct { + ExportLocations []ExportLocation `json:"export_locations"` + } + err := r.ExtractInto(&s) + return s.ExportLocations, err +} + +func (r *Share) UnmarshalJSON(b []byte) error { + type tmp Share + var s struct { + tmp + CreatedAt golangsdk.JSONRFC3339MilliNoZ `json:"created_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Share(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + + return nil +} + +// IsEmpty returns true if a ListResult contains no Shares. +func (r SharePage) IsEmpty() (bool, error) { + shares, err := ExtractShares(r) + return len(shares) == 0, err +} + +// NextPageURL is invoked when a paginated collection of shares has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r SharePage) NextPageURL() (string, error) { + var s struct { + Links []golangsdk.Link `json:"shares_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return golangsdk.ExtractNextURL(s.Links) +} + +// GrantAccessResult contains the result body and error from an GrantAccess request. +type GrantAccessResult struct { + commonResult +} + +// AccessRightsResult contains the result body and error from a AccessRight request. +type AccessRightsResult struct { + golangsdk.Result +} + +//DeleteAccessResult contains the response body from DeleteAccess rights +type DeleteAccessResult struct { + golangsdk.Result +} + +//GetExportLocationsResult contains the response body from GetExportLocations +type GetExportLocationsResult struct { + golangsdk.Result +} + +type commonResult struct { + golangsdk.Result +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + golangsdk.ErrResult +} + +// UpdateResult contains the response body and error from a update request. +type UpdateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// ExpandResult contains the response body and error from a Expand request. +type ExpandResult struct { + golangsdk.ErrResult +} + +// ShrinkResult contains the response body and error from a Shrink request. +type ShrinkResult struct { + golangsdk.ErrResult +} diff --git a/openstack/sfs/v2/shares/testing/fixtures.go b/openstack/sfs/v2/shares/testing/fixtures.go new file mode 100644 index 000000000..abccceaa5 --- /dev/null +++ b/openstack/sfs/v2/shares/testing/fixtures.go @@ -0,0 +1,278 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/huaweicloud/golangsdk/testhelper" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +const ( + shareEndpoint = "/shares" + shareID = "011d21e2-fbc3-4e4a-9993-9ea223f73264" +) + +var createRequest = `{ + "share": { + "name": "my_test_share", + "size": 1, + "share_proto": "NFS" + } + }` + +var createResponse = `{ + "share": { + "name": "my_test_share", + "share_proto": "NFS", + "size": 1, + "status": null, + "share_server_id": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7", + "share_type_name": "default", + "availability_zone": null, + "created_at": "2015-09-18T10:25:24.533287", + "export_location": null, + "links": [ + { + "href": "http://172.18.198.54:8786/v1/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self" + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark" + } + ], + "share_network_id": null, + "export_locations": [], + "host": null, + "access_rules_status": "active", + "task_state": null, + "snapshot_support": true, + "consistency_group_id": "9397c191-8427-4661-a2e8-b23820dc01d4", + "source_cgsnapshot_member_id": null, + "volume_type": "default", + "snapshot_id": null, + "is_public": true, + "metadata": { + "project": "my_app", + "aim": "doc" + }, + "id": "011d21e2-fbc3-4e4a-9993-9ea223f73264", + "description": "My custom share London" + } + }` + +// MockCreateResponse creates a mock response +func MockCreateResponse(t *testing.T) { + th.Mux.HandleFunc(shareEndpoint, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, createRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, createResponse) + }) +} + +var updateRequest = `{ + "share": { + "display_name": "my_test_share_sfs", + "display_description": "test" + } +}` + +var updateResponse = `{ + "share": { + "name": "my_test_share_sfs", + "share_proto": "NFS", + "size": 1, + "status": null, + "share_server_id": null, + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7", + "share_type_name": "default", + "availability_zone": null, + "created_at": "2015-09-18T10:25:24.533287", + "export_location": null, + "links": [ + { + "href": "http://172.18.198.54:8786/v1/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self" + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark" + } + ], + "share_network_id": null, + "export_locations": [], + "host": null, + "access_rules_status": "active", + "task_state": null, + "snapshot_support": true, + "consistency_group_id": "9397c191-8427-4661-a2e8-b23820dc01d4", + "source_cgsnapshot_member_id": null, + "volume_type": "default", + "snapshot_id": null, + "is_public": true, + "metadata": { + "project": "my_app", + "aim": "doc" + }, + "id": "011d21e2-fbc3-4e4a-9993-9ea223f73264", + "description": "test" + } + }` + +// MockCreateResponse creates a mock response +func MockUpdateResponse(t *testing.T) { + th.Mux.HandleFunc(shareEndpoint+"/"+shareID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, updateRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, updateResponse) + }) +} + +// MockDeleteResponse creates a mock delete response +func MockDeleteResponse(t *testing.T) { + th.Mux.HandleFunc(shareEndpoint+"/"+shareID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusAccepted) + }) +} + +var getResponse = `{ + "share": { + "links": [ + { + "href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self" + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark" + } + ], + "availability_zone": "nova", + "share_network_id": "713df749-aac0-4a54-af52-10f6c991e80c", + "snapshot_id": null, + "id": "011d21e2-fbc3-4e4a-9993-9ea223f73264", + "size": 1, + "share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7", + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "metadata": { + "project": "my_app", + "aim": "doc" + }, + "status": "available", + "description": "My custom share London", + "host": "manila2@generic1#GENERIC1", + "is_public": true, + "name": "my_test_share", + "created_at": "2015-09-18T10:25:24.000000", + "share_proto": "NFS", + "volume_type": "default" + } +}` + +// MockGetResponse creates a mock get response +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc(shareEndpoint+"/"+shareID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, getResponse) + }) +} + +var grantAccessRequest = `{ + "os-allow_access": { + "access_to": "5232f396-d6cc-4a81-8de3-afd7a7ecdfd8", + "access_type": "cert", + "access_level": "rw" + } +}` + +var grantAccessResponse = `{ + "access": { + "share_id": "1b8facf8-b822-4349-a033-e078b2a84b7f", + "access_type": "cert", + "access_to": "5232f396-d6cc-4a81-8de3-afd7a7ecdfd8", + "access_level": "rw", + "state": "new", + "id": "fc32500f-fa78-4f06-8caf-06ad7fb9726c" + } +}` + +var listAccessRightsRequest = `{ + "os-access_list": null +}` + +var listAccessRightsResponse = `{ + "access_list": [ + { + "access_level": "rw", + "state": "active", + "id": "5158f095-4c43-49c0-b5a7-c458e85ed8c8", + "access_type": "cert", + "access_to": "5232f396-d6cc-4a81-8de3-afd7a7ecdfd8" + } + ] +}` + +var deleteAccessRequest = `{ + "os-deny_access": { + "access_id": "ea07152b-d08b-4f6b-8785-ce64dce52679" + } +}` + +var getExportLocationsResponse = `{ + "export_locations": [ + { + "path": "sfs-nas1.eu-de.otc.t-systems.com:/share-d41ee18b", + "id": "fab962ba-4b9a-475e-a380-8e856ed3f92d" + } + ] +}` + +var listResponse = `{ + "shares": [ + { + "links": [ + { + "href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self" + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark" + } + ], + "availability_zone": "nova", + "share_network_id": "713df749-aac0-4a54-af52-10f6c991e80c", + "id": "011d21e2-fbc3-4e4a-9993-9ea223f73264", + "size": 1, + "share_type": "25747776-08e5-494f-ab40-a64b9d20d8f7", + "project_id": "16e1ab15c35a457e9c2b2aa189f544e1", + "status": "available", + "description": "My custom share London", + "host": "manila2@generic1#GENERIC1", + "is_public": true, + "created_at": "2015-09-18T10:25:24.000000", + "name": "my_test_share", + "share_proto": "NFS", + "volume_type": "default" + } + ] +}` diff --git a/openstack/sfs/v2/shares/testing/request_test.go b/openstack/sfs/v2/shares/testing/request_test.go new file mode 100644 index 000000000..f4ae6ff01 --- /dev/null +++ b/openstack/sfs/v2/shares/testing/request_test.go @@ -0,0 +1,289 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/huaweicloud/golangsdk/openstack/sfs/v2/shares" + th "github.com/huaweicloud/golangsdk/testhelper" + "github.com/huaweicloud/golangsdk/testhelper/client" + fake "github.com/huaweicloud/golangsdk/testhelper/client" +) + +func TestCreateShare(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockCreateResponse(t) + + options := &shares.CreateOpts{Size: 1, Name: "my_test_share", ShareProto: "NFS"} + n, err := shares.Create(client.ServiceClient(), options).Extract() + + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "my_test_share") + th.AssertEquals(t, n.Size, 1) + th.AssertEquals(t, n.ShareProto, "NFS") +} + +func TestDeleteShare(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockDeleteResponse(t) + + result := shares.Delete(client.ServiceClient(), shareID) + th.AssertNoErr(t, result.Err) +} + +func TestUpdate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockUpdateResponse(t) + + options := &shares.UpdateOpts{DisplayName: "my_test_share_sfs", DisplayDescription: "test"} + n, err := shares.Update(client.ServiceClient(), shareID, options).Extract() + + th.AssertNoErr(t, err) + th.AssertEquals(t, n.Name, "my_test_share_sfs") + th.AssertEquals(t, n.Description, "test") +} + +func TestGetShare(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + s, err := shares.Get(client.ServiceClient(), shareID).Extract() + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, s, &shares.Share{ + AvailabilityZone: "nova", + ShareNetworkID: "713df749-aac0-4a54-af52-10f6c991e80c", + SnapshotID: "", + ID: shareID, + Size: 1, + ShareType: "25747776-08e5-494f-ab40-a64b9d20d8f7", + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Metadata: map[string]string{ + "project": "my_app", + "aim": "doc", + }, + Status: "available", + Description: "My custom share London", + Host: "manila2@generic1#GENERIC1", + Name: "my_test_share", + CreatedAt: time.Date(2015, time.September, 18, 10, 25, 24, 0, time.UTC), + ShareProto: "NFS", + VolumeType: "default", + IsPublic: true, + Links: []map[string]string{ + { + "href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self", + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark", + }, + }, + }) +} + +func TestListAccessRights(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(shareEndpoint+"/"+shareID+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, listAccessRightsRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, listAccessRightsResponse) + }) + + c := client.ServiceClient() + // Client c must have Microversion set; minimum supported microversion for Grant Access is 2.7 + c.Microversion = "2.7" + + s, err := shares.ListAccessRights(c, shareID).ExtractAccessRights() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, s, []shares.AccessRight{ + { + AccessType: "cert", + AccessTo: "5232f396-d6cc-4a81-8de3-afd7a7ecdfd8", + AccessLevel: "rw", + State: "active", + ID: "5158f095-4c43-49c0-b5a7-c458e85ed8c8", + }, + }) +} + +func TestGrantAcessRight(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc(shareEndpoint+"/"+shareID+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, grantAccessRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, grantAccessResponse) + }) + + c := client.ServiceClient() + // Client c must have Microversion set; minimum supported microversion for Grant Access is 2.7 + c.Microversion = "2.7" + + grantaccOpts := shares.GrantAccessOpts{AccessTo: "5232f396-d6cc-4a81-8de3-afd7a7ecdfd8", AccessType: "cert", AccessLevel: "rw"} + s, err := shares.GrantAccess(c, shareID, grantaccOpts).ExtractAccess() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, s, &shares.AccessRight{ + AccessType: "cert", + AccessTo: "5232f396-d6cc-4a81-8de3-afd7a7ecdfd8", + AccessLevel: "rw", + State: "new", + ID: "fc32500f-fa78-4f06-8caf-06ad7fb9726c", + }) +} + +func TestDeleteAcess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + th.Mux.HandleFunc(shareEndpoint+"/"+shareID+"/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, deleteAccessRequest) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + + }) + c := client.ServiceClient() + c.Microversion = "2.7" + + res := shares.DeleteAccessOpts{AccessID: "ea07152b-d08b-4f6b-8785-ce64dce52679"} + s := shares.DeleteAccess(c, shareID, res) + + th.AssertNoErr(t, s.Err) +} + +func TestGetExportLocationsSuccess(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(shareEndpoint+"/"+shareID+"/export_locations", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, getExportLocationsResponse) + }) + + c := client.ServiceClient() + s, err := shares.GetExportLocations(c, shareID).ExtractExportLocations() + + th.AssertNoErr(t, err) + th.AssertDeepEquals(t, s, []shares.ExportLocation{ + { + Path: "sfs-nas1.eu-de.otc.t-systems.com:/share-d41ee18b", + ID: "fab962ba-4b9a-475e-a380-8e856ed3f92d", + }, + }) +} + +func TestListShare(t *testing.T) { + + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(shareEndpoint+"/detail", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, listResponse) + }) + + //count := 0 + + actual, err := shares.List(fake.ServiceClient(), shares.ListOpts{}) + if err != nil { + t.Errorf("Failed to extract clusters: %v", err) + } + + expected := []shares.Share{ + { + Status: "available", + AvailabilityZone: "nova", + ShareNetworkID: "713df749-aac0-4a54-af52-10f6c991e80c", + Name: "my_test_share", + ID: "011d21e2-fbc3-4e4a-9993-9ea223f73264", + Size: 1, + ShareType: "25747776-08e5-494f-ab40-a64b9d20d8f7", + ProjectID: "16e1ab15c35a457e9c2b2aa189f544e1", + Description: "My custom share London", + Host: "manila2@generic1#GENERIC1", + IsPublic: true, + ShareProto: "NFS", + VolumeType: "default", + CreatedAt: time.Date(2015, time.September, 18, 10, 25, 24, 0, time.UTC), + Links: []map[string]string{ + { + "href": "http://172.18.198.54:8786/v2/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "self", + }, + { + "href": "http://172.18.198.54:8786/16e1ab15c35a457e9c2b2aa189f544e1/shares/011d21e2-fbc3-4e4a-9993-9ea223f73264", + "rel": "bookmark", + }, + }, + }, + } + + th.AssertDeepEquals(t, expected, actual) +} + +func TestExpand(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(shareEndpoint+"/"+shareID+"/"+"action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) + options := shares.ExpandOpts{OSExtend: shares.OSExtendOpts{NewSize: 3}} + resp := shares.Expand(fake.ServiceClient(), shareID, options) + th.AssertNoErr(t, resp.Err) + +} + +func TestShrink(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc(shareEndpoint+"/"+shareID+"/"+"action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + }) + options := shares.ShrinkOpts{OSShrink: shares.OSShrinkOpts{NewSize: 2}} + resp := shares.Shrink(fake.ServiceClient(), shareID, options) + th.AssertNoErr(t, resp.Err) + +} diff --git a/openstack/sfs/v2/shares/urls.go b/openstack/sfs/v2/shares/urls.go new file mode 100644 index 000000000..c199665fa --- /dev/null +++ b/openstack/sfs/v2/shares/urls.go @@ -0,0 +1,30 @@ +package shares + +import "github.com/huaweicloud/golangsdk" + +//For access rule create , update and delete +func rootURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL("shares", id, "action") +} + +// To fetch mount locations of the specified share id +func getMountLocationsURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL("shares", id, "export_locations") +} + +func createURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("shares") +} + +func resourceURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL("shares", id) +} + +//For access rule create , update and delete +func listURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("shares", "detail") +} + +func grantAccessURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL("shares", id, "action") +}