From 4c61f55c2f16cc12c1b524b79471b1514c08c08f Mon Sep 17 00:00:00 2001 From: Viraj Vora Date: Wed, 8 Jul 2020 12:01:28 -0400 Subject: [PATCH] Initial support for persistent volumes REST api endpoints Signed-off-by: Viraj Vora --- CHANGELOG.md | 7 + endpoints-support.md | 5 + .../persistent_volumes/persistent_volumes.go | 99 ++++++++ ovc/ovc.go | 2 + ovc/persistent_volumes.go | 238 ++++++++++++++++++ ovc/persistent_volumes_test.go | 234 +++++++++++++++++ 6 files changed, 585 insertions(+) create mode 100644 examples/persistent_volumes/persistent_volumes.go create mode 100644 ovc/persistent_volumes.go create mode 100644 ovc/persistent_volumes_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0a7c2..5ff2a56 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,10 @@ This is the first release of the SimpliVity Go SDK and it adds support for the b - OmniStack cluster - Policy - Virtual machine + +# unreleased +#### Notes +This version adds support of persistent volumes to the SimpliVity Go SDK + +#### Features supported + - Persistent volumes diff --git a/endpoints-support.md b/endpoints-support.md index a70424e..9fc79c6 100755 --- a/endpoints-support.md +++ b/endpoints-support.md @@ -16,6 +16,11 @@ Refer SimpliVity REST API doc for the resource endpoints documentation [HPE Simp |/omnistack_clusters |GET | | **Policies** |/policies |GET | +| **Persistent Volumes** +|/persistent_volumes |GET | +|/persistent_volumes/set_policy |POST | +|/persistent_volumes/{pvId} |GET | +|/persistent_volumes/{pvId}/backup |POST | | **Virtual Machines** |/virtual_machines |GET | |/virtual_machines/set_policy |POST | diff --git a/examples/persistent_volumes/persistent_volumes.go b/examples/persistent_volumes/persistent_volumes.go new file mode 100644 index 0000000..43df98f --- /dev/null +++ b/examples/persistent_volumes/persistent_volumes.go @@ -0,0 +1,99 @@ +package main + +import ( + "fmt" + "time" + + "github.com/HewlettPackard/simplivity-go/ovc" +) + +func main() { + var ( + pvName = "pvc-test_fcd" + pvByName *ovc.PersistentVolume + currentTime = time.Now().String() + ) + + //Create an ovc client + client, err := ovc.NewClient("username", "password", "ovc_ip", "certificate_path_if_needed") + if err != nil { + fmt.Println(err) + } + + //Get all persistent volume resources without Filter + fmt.Println("\nGet all persistent volumes without params") + pvList, err := client.PersistentVolumes.GetAll(ovc.GetAllParams{}) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvList.Limit, pvList.Count, pvList.Offset, pvList.Members[0]) + for _, pv := range pvList.Members { + fmt.Println(pv.Name + "\n") + } + + //Get All PV resources with Filters + fmt.Println("\nGet all PVs with params") + pvList, err = client.PersistentVolumes.GetAll(ovc.GetAllParams{Limit: 1, Filters: map[string]string{"name": pvName}}) + if err != nil { + fmt.Println(err) + } + for _, pv := range pvList.Members { + fmt.Println(pv.Name + "\n") + } + + //Get a PV resource by its name + fmt.Println("\nGet a PV resource by it's name.") + pvByName, err = client.PersistentVolumes.GetByName(pvName) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvByName) + + //Get a PV resource by its id + fmt.Println("\nGet a VM resource by it's id.") + pvById, err := client.PersistentVolumes.GetById(pvByName.Id) + if err != nil { + fmt.Println(err) + } + fmt.Println(pvById) + + //Set policy for a single and multiple PVs + policyList, err := client.Policies.GetAll(ovc.GetAllParams{Limit: 1}) + if err != nil { + fmt.Println(err) + } + if policyList != nil { + //Get one of the backup policies + policy := policyList.Members[0] + fmt.Println("\nPolicy to set is ", policy) + + //Set policy for multiple PVs + fmt.Println("\nSet policy for multiple PVs") + pvs := []*ovc.PersistentVolume{pvByName} + err := client.PersistentVolumes.SetPolicyForMultiplePVs(policy, pvs) + if err != nil { + fmt.Println(err) + } + } + + //Take a backup of a Persistent Volume + fmt.Println("Take backup of a Peristent Volume") + backupName := "backup_" + currentTime + backReq := &ovc.CreateBackupRequest{Name: backupName} + _, err = pvByName.CreateBackup(backReq, nil) + if err != nil { + fmt.Println(err) + } else { + fmt.Println("Backup operation was Successful") + } + + // Get backups of a PV + fmt.Println("\nGet all backups of a PV") + backupList, err := pvByName.GetBackups() + if err != nil { + fmt.Println(err) + } + for _, backup := range backupList.Members { + fmt.Println(backup.Name) + } +} diff --git a/ovc/ovc.go b/ovc/ovc.go index 46147d0..dc125d5 100644 --- a/ovc/ovc.go +++ b/ovc/ovc.go @@ -105,6 +105,7 @@ type Client struct { Datastores *DatastoreResource Hosts *HostResource OmniStackClusters *OmniStackClusterResource + PersistentVolumes *PersistentVolumeResource Policies *PolicyResource VirtualMachines *VirtualMachineResource Tasks *TaskResource @@ -149,6 +150,7 @@ func NewClient(username string, password string, ovc_ip string, ssl_certificate c.Hosts = (*HostResource)(&c.common) c.OmniStackClusters = (*OmniStackClusterResource)(&c.common) c.VirtualMachines = (*VirtualMachineResource)(&c.common) + c.PersistentVolumes = (*PersistentVolumeResource)(&c.common) c.Policies = (*PolicyResource)(&c.common) c.Tasks = (*TaskResource)(&c.common) diff --git a/ovc/persistent_volumes.go b/ovc/persistent_volumes.go new file mode 100644 index 0000000..5d0f589 --- /dev/null +++ b/ovc/persistent_volumes.go @@ -0,0 +1,238 @@ +package ovc + +import ( + "encoding/json" + "errors" + "fmt" + "log" +) + +// Support for persistent_volume is added in v1.16 in SimpliVity +var header = map[string]string{ + "Accept": "application/vnd.simplivity.v1.16+json", + "Content-Type": "application/vnd.simplivity.v1.16+json", +} + +// PersistentVolumeResource handles communications with the the Persistent Volume resource methods +// +// SimpliVity API docs: +type PersistentVolumeResource resourceClient + +// Persistent Volumes GetAll response +type PersistentVolumeList struct { + Offset int `json:"offset,omitempty"` + Count int `json:"count,omitempty"` + Limit int `json:"limit,omitempty"` + Members []*PersistentVolume `json:"persistent_volumes,omitempty"` +} + +// PersistentVolume represents a SimpliVity persistent volume +type PersistentVolume struct { + Name string `json:"name,omitempty"` + Id string `json:"id,omitempty"` + State string `json:"state,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + DeletedAt string `json:"deleted_at,omitempty"` + PolicyName string `json:"policy_name,omitempty"` + PolicyId string `json:"policy_id,omitempty"` + DatastoreName string `json:"datastore_name,omitempty"` + DatastoreId string `json:"datastore_id,omitempty"` + OmniStackClusterName string `json:"omnistack_cluster_name,omitempty"` + OmniStackClusterId string `json:"omnistack_cluster_id,omitempty"` + AppAwareVMStatus string `json:"app_aware_vm_status,omitempty"` + HypervisorObjectId string `json:"hypervisor_object_id,omitempty"` + HypervisorType string `json:"hypervisor_type,omitempty"` + HypervisorManagementSystem string `json:"hypervisor_management_system,omitempty"` + HypervisorManagementSystemName string `json:"hypervisor_management_system_name,omitempty"` + HostId string `json:"host_id,omitempty"` + ComputeClusterParentHypervisorObjectId string `json:"compute_cluster_parent_hypervisor_object_id,omitempty"` + ComputeClusterParentName string `json:"compute_cluster_parent_name,omitempty"` + ClusterGroupIds []string `json:"cluster_group_ids,omitempty"` + ReplicaSet []ReplicaSetList `json:"replica_set,omitempty"` +} + +// GetAll returns all the persistent volumes filtered by the query parameters. +// Filters: +// id: The unique identifier (UID) of the persistent volume to return +// Accepts: Single value, comma-separated list +// name: The name of the persistent_volumes to return +// Accepts: Single value, comma-separated list, pattern using one or more +// asterisk characters as a wildcard +// omnistack_cluster_id: The unique identifier (UID) of the omnistack_cluster +// that is associated with the instances to return +// Accepts: Single value, comma-separated list +// omnistack_cluster_name: The name of the omnistack_cluster that +// is associated with the instances to return. +// Accepts: Single value, comma-separated list. +// compute_cluster_parent_hypervisor_object_id: The unique identifier (UID) +// of the hypervisor that contains the omnistack_cluster that is associated +// with the instances to return +// Accepts: Single value, comma-separated list. +// compute_cluster_parent_name: The name of the hypervisor that contains the +// omnistack_cluster that is associated with the instances to return +// Accepts: Single value, comma-separated list +// hypervisor_management_system: The IP address of the hypervisor associated +// with the persistent volume. +// Accepts: Single value, comma-separated list, pattern using one +// or more asterisk characters as a wildcard +// hypervisor_management_system_name: The name of the hypervisor associated +// with the persistent volume +// Accepts: Single value, comma-separated list, pattern using one or more +// asterisk characters as a wildcard +// datastore_id: The unique identifier (UID) of the datastore that is associated +// with the instances to return +// Accepts: Single value, comma-separated list +// datastore_name: The name of the datastore that is associated with the +// instances to return +// Accepts: Single value, comma-separated list +// policy_id: The unique identifier (UID) of the policy that is associated +// with the instances to return +// Accepts: Single value, comma-separated list +// policy_name: The name of the policy that is associated with the instances to return +// Accepts: Single value, comma-separated list +// hypervisor_object_id: The unique identifier (UID) of the hypervisor-based instance +// that is associated with the instances to return +// Accepts: Single value, comma-separated list +// created_after: The earliest creation time after the persistent volumes to return were +// created, expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) +// created_before: The latest creation time before the persistent volumes to return were +// created, expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) +// state: The state of the persistent volume that is associated with the instances to return +// Accepts: Single value, comma-separated list +// app_aware_vm_status: The status of the ability of the persistent volume to take +// an application-consistent backup that uses Microsoft VSS +// Accepts: Single value, comma-separated list +// host_id: The unique identifier (UID) of the persistent_volume host. +func (p *PersistentVolumeResource) GetAll(params GetAllParams) (*PersistentVolumeList, error) { + var ( + path = "/persistent_volumes" + pvList PersistentVolumeList + ) + + qrStr := params.QueryString() + resp, err := p.client.DoRequest("GET", path, qrStr, nil, header) + if err != nil { + return &pvList, err + } + + err = json.Unmarshal(resp, &pvList) + if err != nil { + return &pvList, err + } + + return &pvList, nil +} + +// GetBy searches for PV resources with single filter. +func (p *PersistentVolumeResource) GetBy(fieldName string, value string) ([]*PersistentVolume, error) { + filters := map[string]string{fieldName: value} + pvList, err := p.GetAll(GetAllParams{Filters: filters}) + if err != nil { + log.Println(err) + return nil, err + } + + pvs := pvList.Members + return pvs, nil +} + +// GetByName searches for a PV by its name +func (p *PersistentVolumeResource) GetByName(name string) (*PersistentVolume, error) { + pvs, err := p.GetBy("name", name) + + if err != nil { + log.Println(err) + return nil, err + } + + if len(pvs) > 0 { + pv := pvs[0] + return pv, nil + } + + return nil, errors.New("Resource doesn't exist") +} + +// GetById searches for a PV by its id +func (p *PersistentVolumeResource) GetById(id string) (*PersistentVolume, error) { + pvs, err := p.GetBy("id", id) + + if err != nil { + log.Println(err) + return nil, err + } + + if len(pvs) > 0 { + pv := pvs[0] + return pv, nil + } + + return nil, errors.New("Resource doesn't exist") +} + +// SetPolicyForMultiplePVs sets a policy for list of PV resources. +func (p *PersistentVolumeResource) SetPolicyForMultiplePVs(policy *Policy, pvs []*PersistentVolume) error { + path := fmt.Sprintf("/persistent_volumes/set_policy") + if len(pvs) < 1 { + return errors.New("Pass a list of PV resoures") + } + + pv_ids := []string{} + for _, pv := range pvs { + pv_ids = append(pv_ids, pv.Id) + } + + body := map[string]interface{}{"policy_id": policy.Id, "persistent_volume_id": pv_ids} + resp, err := p.client.DoRequest("POST", path, "", body, header) + if err != nil { + return err + } + + _, err = commonClient.Tasks.WaitForTask(resp) + if err != nil { + return err + } + + return nil +} + +// CreateBackup creates a backup of the PV. +func (p *PersistentVolume) CreateBackup(req *CreateBackupRequest, dest *OmniStackCluster) (*Backup, error) { + path := fmt.Sprintf("/persistent_volumes/%s/backup", p.Id) + if dest != nil { + req.Destination = dest.Id + } + + resp, err := commonClient.DoRequest("POST", path, "", req, header) + if err != nil { + log.Println(err) + return nil, err + } + + task, err := commonClient.Tasks.WaitForTask(resp) + if err != nil { + log.Println(err) + return nil, err + } + + resources := task.AffectedResources + if len(resources) < 1 { + err_message := "Backup was not successful. Error code:" + string(task.ErrorCode) + return nil, errors.New(err_message) + } + + resource_id := resources[0].ObjectId + backup, err := commonClient.Backups.GetById(resource_id) + return backup, nil +} + +// GetBackups gets all the backups of a PV. +func (p *PersistentVolume) GetBackups() (*BackupList, error) { + backupList, err := commonClient.Backups.GetAll(GetAllParams{Filters: map[string]string{"pv": p.Name}}) + if err != nil { + log.Println(err) + return nil, err + } + + return backupList, nil +} diff --git a/ovc/persistent_volumes_test.go b/ovc/persistent_volumes_test.go new file mode 100644 index 0000000..4f65ac7 --- /dev/null +++ b/ovc/persistent_volumes_test.go @@ -0,0 +1,234 @@ +package ovc + +import ( + "fmt" + "net/http" + "reflect" + "testing" +) + +func mockGetPVById(apiHandler *http.ServeMux) { + apiHandler.HandleFunc("/persistent_volumes", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"offset": 1, "limit": 500, "count": 1, "persistent_volumes":[{"name": "pvc-123_fcd", "id":"1"}]}`) + }) +} + +func TestPVGetAllWithDefaultParameters(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + params := GetAllParams{} + + apiHandler.HandleFunc("/persistent_volumes", func(w http.ResponseWriter, r *http.Request) { + testRequestMethod(t, r, "GET") + testRequestHeader(t, r, "Authorization", "Bearer 12345") + fmVal := formValues{"limit": "500", "offset": "0", "sort": "", + "order": "", "fields": "", "case": "", + "show_optional_fields": "false"} + + testFormValues(t, r, fmVal) + fmt.Fprint(w, `{"offset": 1, "limit": 1, "count": 1, "persistent_volumes":[{"name": "pvc-123_fcd", "id":"1"}]}`) + }) + + pv_list, err := client.PersistentVolumes.GetAll(params) + if err != nil { + t.Error(err) + } + + pv := &PersistentVolume{Name: "pvc-123_fcd", Id: "1"} + expected := &PersistentVolumeList{Offset: 1, + Limit: 1, + Count: 1, + Members: []*PersistentVolume{pv}} + + if !reflect.DeepEqual(pv_list, expected) { + t.Errorf("Returned = %v, expected %v", pv_list, expected) + } +} + +func TestPVGetAllWithFilter(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + params := GetAllParams{Filters: map[string]string{"name": "testname"}} + + apiHandler.HandleFunc("/persistent_volumes", func(w http.ResponseWriter, r *http.Request) { + testRequestMethod(t, r, "GET") + testRequestHeader(t, r, "Authorization", "Bearer 12345") + fmVal := formValues{"limit": "500", "offset": "0", "sort": "", + "order": "", "fields": "", "case": "", + "show_optional_fields": "false", "name": "testname"} + + testFormValues(t, r, fmVal) + fmt.Fprint(w, `{"offset": 1, "limit": 1, "count": 1, "persistent_volumes":[{"name": "pvc-123_fcd", "id":"1"}]}`) + }) + + _, err := client.PersistentVolumes.GetAll(params) + if err != nil { + t.Error(err) + } +} + +func TestPVGetBy(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + + apiHandler.HandleFunc("/persistent_volumes", func(w http.ResponseWriter, r *http.Request) { + testRequestMethod(t, r, "GET") + testRequestHeader(t, r, "Authorization", "Bearer 12345") + fmVal := formValues{"limit": "500", "offset": "0", "sort": "", + "order": "", "fields": "", "case": "", + "show_optional_fields": "false", "name": "testname"} + + testFormValues(t, r, fmVal) + fmt.Fprint(w, `{"offset": 1, "limit": 500, "count": 1, "persistent_volumes":[{"name": "pvc-123_fcd", "id":"1"}]}`) + }) + + _, err := client.PersistentVolumes.GetBy("name", "testname") + if err != nil { + t.Error(err) + } +} + +func TestPVGetByName(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + + apiHandler.HandleFunc("/persistent_volumes", func(w http.ResponseWriter, r *http.Request) { + testRequestMethod(t, r, "GET") + testRequestHeader(t, r, "Authorization", "Bearer 12345") + fmVal := formValues{"limit": "500", "offset": "0", "sort": "", + "order": "", "fields": "", "case": "", + "show_optional_fields": "false", "name": "testname"} + + testFormValues(t, r, fmVal) + fmt.Fprint(w, `{"offset": 1, "limit": 500, "count": 1, "persistent_volumes":[{"name": "pvc-123_fcd", "id":"1"}]}`) + }) + + _, err := client.PersistentVolumes.GetByName("testname") + if err != nil { + t.Error(err) + } +} + +func TestPVGetById(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + + apiHandler.HandleFunc("/persistent_volumes", func(w http.ResponseWriter, r *http.Request) { + testRequestMethod(t, r, "GET") + testRequestHeader(t, r, "Authorization", "Bearer 12345") + fmVal := formValues{"limit": "500", "offset": "0", "sort": "", + "order": "", "fields": "", "case": "", + "show_optional_fields": "false", "id": "123"} + + testFormValues(t, r, fmVal) + fmt.Fprint(w, `{"offset": 1, "limit": 500, "count": 1, "persistent_volumes":[{"name": "pvc-123_fcd", "id":"1"}]}`) + }) + + _, err := client.PersistentVolumes.GetById("123") + if err != nil { + t.Error(err) + } +} + +func TestSetPolicyForMultiplePVs(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + + apiHandler.HandleFunc("/persistent_volumes/set_policy", func(w http.ResponseWriter, r *http.Request) { + testRequestMethod(t, r, "POST") + testRequestHeader(t, r, "Authorization", "Bearer 12345") + testRequestBody(t, r, `{"persistent_volume_id":["1"],"policy_id":"1"}`+"\n") + fmt.Fprint(w, `{"task":{"state": "IN_PROGRESS", "id": "1", "percent_complete": 1, + "affected_objects":[]}}`) + }) + + //Mock request to task endpoint + mockTaskRequest(apiHandler) + + policy := &Policy{Id: "1"} + pv := &PersistentVolume{Id: "1"} + pvs := []*PersistentVolume{pv} + + err := client.PersistentVolumes.SetPolicyForMultiplePVs(policy, pvs) + if err != nil { + t.Error(err) + } + + // Negative test + pvs = nil + err = client.PersistentVolumes.SetPolicyForMultiplePVs(policy, pvs) + if err.Error() != "Pass a list of PV resoures" { + t.Error(err) + } +} + +func TestPVCreateBackup(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + + //Mock get by id request + mockGetPVById(apiHandler) + + apiHandler.HandleFunc("/backups", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"offset": 1, "limit": 500, "count": 1, "backups":[{"name": "backup_name", "id":"1"}]}`) + }) + + apiHandler.HandleFunc("/persistent_volumes/1/backup", func(w http.ResponseWriter, r *http.Request) { + testRequestMethod(t, r, "POST") + testRequestHeader(t, r, "Authorization", "Bearer 12345") + testRequestBody(t, r, `{"backup_name":"backup_name","destination_id":"1"}`+"\n") + fmt.Fprint(w, `{"task":{"state": "IN_PROGRESS", "id": "1", "percent_complete": 1, "affected_objects":[]}}`) + }) + + //Mock request to task endpoint + mockTaskRequest(apiHandler) + + pv, err := client.PersistentVolumes.GetById("1") + if err != nil { + t.Error(err) + } + + cluster := &OmniStackCluster{Id: "1"} + req := &CreateBackupRequest{Name: "backup_name"} + backup, err := pv.CreateBackup(req, cluster) + if err != nil { + t.Error(err) + } + + expected := &Backup{Name: "backup_name", Id: "1"} + if !reflect.DeepEqual(backup, expected) { + t.Errorf("Returned = %v, expected %v", backup, expected) + } +} + +func TestPVGetBackups(t *testing.T) { + client, apiHandler, teardown := setup() + defer teardown() + + //Mock get by id request + mockGetPVById(apiHandler) + + apiHandler.HandleFunc("/backups", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, `{"offset": 1, "limit": 500, "count": 1, "backups":[{"name": "backup_name", "id":"1"}]}`) + }) + + pv, err := client.PersistentVolumes.GetById("1") + if err != nil { + t.Error(err) + } + + backups, err := pv.GetBackups() + if err != nil { + t.Error(err) + } + + backup := &Backup{Name: "backup_name", Id: "1"} + expected := &BackupList{Offset: 1, + Limit: 500, + Count: 1, + Members: []*Backup{backup}} + + if !reflect.DeepEqual(backups, expected) { + t.Errorf("Returned = %v, expected %v", backups, expected) + } +}