From 3a6d72d71bcb9e6a41d1e67694ebbf3d3c4f5970 Mon Sep 17 00:00:00 2001 From: wujian Date: Mon, 26 Sep 2016 14:46:04 +0800 Subject: [PATCH] Added Capability to get volumes and volumes with details Partially implements blueprint volume Change-Id: Ie7d6f0c6e137499e5837cf0a94ca612bc83512a4 --- examples/40-volume-v2.go | 79 ++++++++++++++ volume/volume.go | 210 +++++++++++++++++++++++++++++++++++++ volume/volume_test.go | 220 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 509 insertions(+) create mode 100644 examples/40-volume-v2.go create mode 100644 volume/volume.go create mode 100644 volume/volume_test.go diff --git a/examples/40-volume-v2.go b/examples/40-volume-v2.go new file mode 100644 index 0000000..544e2b1 --- /dev/null +++ b/examples/40-volume-v2.go @@ -0,0 +1,79 @@ +// Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package main + +import ( + "fmt" + "net/http" + "time" + + "git.openstack.org/openstack/golang-client.git/volume" + "git.openstack.org/openstack/golang-client.git/openstack" +) + +// Volume examples. +func main() { + config := getConfig() + + // Authenticate with a username, password, tenant id. + creds := openstack.AuthOpts{ + AuthUrl: config.Host, + Project: config.ProjectName, + Username: config.Username, + Password: config.Password, + } + auth, err := openstack.DoAuthRequest(creds) + if err != nil { + panicString := fmt.Sprint("There was an error authenticating:", err) + panic(panicString) + } + if !auth.GetExpiration().After(time.Now()) { + panic("There was an error. The auth token has an invalid expiration.") + } + + // Find the endpoint for the volume v2 service. + url, err := auth.GetEndpoint("volumev2", "") + if url == "" || err != nil { + panic("v2 volume service url not found during authentication") + } + + // Make a new client with these creds + sess, err := openstack.NewSession(nil, auth, nil) + if err != nil { + panicString := fmt.Sprint("Error crating new Session:", err) + panic(panicString) + } + + volumeService := volume.Service{ + Session: *sess, + Client: *http.DefaultClient, + URL: url, // We're forcing Volume v2 for now + } + volumesDetails, err := volumeService.VolumesDetail() + if err != nil { + panicString := fmt.Sprint("Cannot access volumes:", err) + panic(panicString) + } + + var volumeIDs = make([]string, 0) + for _, element := range volumesDetails { + volumeIDs = append(volumeIDs, element.ID) + } + + if len(volumeIDs) == 0 { + panicString := fmt.Sprint("No volumes found, check to make sure access is correct") + panic(panicString) + } +} diff --git a/volume/volume.go b/volume/volume.go new file mode 100644 index 0000000..6b1a9f1 --- /dev/null +++ b/volume/volume.go @@ -0,0 +1,210 @@ +// Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +/* +Package volume implements a client library for accessing OpenStack Volume service + +Volumes and VolumeDetails can be retrieved using the api. + +In addition more complex filtering and sort queries can by using the VolumeQueryParameters. + +*/ +package volume + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + + "git.openstack.org/openstack/golang-client.git/openstack" + "git.openstack.org/openstack/golang-client.git/util" +) + +// Service is a client service that can make +// requests against a OpenStack volume service. +// Below is an example on creating an volume service and getting volumes: +// volumeService := volume.VolumeService{Client: *http.DefaultClient, TokenId: tokenId, Url: "http://volumeservicelocation"} +// volumes:= volumeService.Volumes() +type Service struct { + Session openstack.Session + Client http.Client + URL string +} + +// Response is a structure for all properties of +// an volume for a non detailed query +type Response struct { + ID string `json:"id"` + Links []map[string]string `json:"links"` + Name string `json:"name"` +} + +// DetailResponse is a structure for all properties of +// an volume for a detailed query +type DetailResponse struct { + ID string `json:"id"` + Attachments []map[string]string `json:"attachments"` + Links []map[string]string `json:"links"` + Metadata map[string]string `json:"metadata"` + Protected bool `json:"protected"` + Status string `json:"status"` + MigrationStatus string `json:"migration_status"` + UserID string `json:"user_id"` + Encrypted bool `json:"encrypted"` + Multiattach bool `json:"multiattach"` + CreatedAt util.RFC8601DateTime `json:"created_at"` + Description string `json:"description"` + Volume_type string `json:"volume_type"` + Name string `json:"name"` + Source_volid string `json:"source_volid"` + Snapshot_id string `json:"snapshot_id"` + Size int64 `json:"size"` + + Aavailability_zone string `json:"availability_zone"` + Rreplication_status string `json:"replication_status"` + Consistencygroup_id string `json:"consistencygroup_id"` +} + +// QueryParameters is a structure that +// contains the filter, sort, and paging parameters for +// an volume or volumedetail query. +type QueryParameters struct { + All_tenant int64 + Marker string + Limit int64 + SortKey string + SortDirection SortDirection +} + +// SortDirection of the sort, ascending or descending. +type SortDirection string + +const ( + // Desc specifies the sort direction to be descending. + Desc SortDirection = "desc" + // Asc specifies the sort direction to be ascending. + Asc SortDirection = "asc" +) + +// Volumes will issue a get request to OpenStack to retrieve the list of volumes. +func (volumeService Service) Volumes() (volume []Response, err error) { + return volumeService.QueryVolumes(nil) +} + +// VolumesDetail will issue a get request to OpenStack to retrieve the list of volumes complete with +// additional details. +func (volumeService Service) VolumesDetail() (volume []DetailResponse, err error) { + return volumeService.QueryVolumesDetail(nil) +} + +// QueryVolumes will issue a get request with the specified VolumeQueryParameters to retrieve the list of +// volumes. +func (volumeService Service) QueryVolumes(queryParameters *QueryParameters) ([]Response, error) { + volumesContainer := volumesResponse{} + err := volumeService.queryVolumes(false /*includeDetails*/, &volumesContainer, queryParameters) + if err != nil { + return nil, err + } + + return volumesContainer.Volumes, nil +} + +// QueryVolumesDetail will issue a get request with the specified QueryParameters to retrieve the list of +// volumes with additional details. +func (volumeService Service) QueryVolumesDetail(queryParameters *QueryParameters) ([]DetailResponse, error) { + volumesDetailContainer := volumesDetailResponse{} + err := volumeService.queryVolumes(true /*includeDetails*/, &volumesDetailContainer, queryParameters) + if err != nil { + return nil, err + } + + return volumesDetailContainer.Volumes, nil +} + +func (volumeService Service) queryVolumes(includeDetails bool, volumesResponseContainer interface{}, queryParameters *QueryParameters) error { + urlPostFix := "/volumes" + if includeDetails { + urlPostFix = urlPostFix + "/detail" + } + + reqURL, err := buildQueryURL(volumeService, queryParameters, urlPostFix) + if err != nil { + return err + } + + var headers http.Header = http.Header{} + headers.Set("Accept", "application/json") + resp, err := volumeService.Session.Get(reqURL.String(), nil, &headers) + if err != nil { + return err + } + + err = util.CheckHTTPResponseStatusCode(resp) + if err != nil { + return err + } + + rbody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.New("aaa") + } + if err = json.Unmarshal(rbody, &volumesResponseContainer); err != nil { + return err + } + return nil +} + +func buildQueryURL(volumeService Service, queryParameters *QueryParameters, volumePartialURL string) (*url.URL, error) { + reqURL, err := url.Parse(volumeService.URL) + if err != nil { + return nil, err + } + + if queryParameters != nil { + values := url.Values{} + if queryParameters.All_tenant != 0 { + values.Set("all_tenant", fmt.Sprintf("%d", queryParameters.All_tenant)) + } + if queryParameters.Limit != 0 { + values.Set("limit", fmt.Sprintf("%d", queryParameters.Limit)) + } + if queryParameters.Marker != "" { + values.Set("marker", queryParameters.Marker) + } + if queryParameters.SortKey != "" { + values.Set("sort_key", queryParameters.SortKey) + } + if queryParameters.SortDirection != "" { + values.Set("sort_dir", string(queryParameters.SortDirection)) + } + + if len(values) > 0 { + reqURL.RawQuery = values.Encode() + } + } + reqURL.Path += volumePartialURL + + return reqURL, nil +} + +type volumesDetailResponse struct { + Volumes []DetailResponse `json:"volumes"` +} + +type volumesResponse struct { + Volumes []Response `json:"volumes"` +} diff --git a/volume/volume_test.go b/volume/volume_test.go new file mode 100644 index 0000000..eecf734 --- /dev/null +++ b/volume/volume_test.go @@ -0,0 +1,220 @@ +// Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + +// Licensed under the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// volume.go +package volume_test + +import ( + "errors" + "net/http" + "strings" + "testing" + + "git.openstack.org/openstack/golang-client.git/volume/v2" + "git.openstack.org/openstack/golang-client.git/openstack" + "git.openstack.org/openstack/golang-client.git/testUtil" + "git.openstack.org/openstack/golang-client.git/util" +) + +var tokn = "ae5aebe5-6a5d-4a40-840a-9736a067aff4" + +func TestListVolumes(t *testing.T) { + anon := func(volumeService *volume.Service) { + volumes, err := volumeService.Volumes() + if err != nil { + t.Error(err) + } + + if len(volumes) != 2 { + t.Error(errors.New("Incorrect number of volumes found")) + } + expectedVolume := volume.Response{ + Name: "volume_test1", + ID: "f5fc9874-fc89-4814-a358-23ba83a6115f", + Links: []map[string]string{{"href": "http://172.16.197.131:8776/v2/1d8837c5fcef4892951397df97661f97/volumes/f5fc9874-fc89-4814-a358-23ba83a6115f", "rel": "self"}, + {"href": "http://172.16.197.131:8776/1d8837c5fcef4892951397df97661f97/volumes/f5fc9874-fc89-4814-a358-23ba83a6115f", "rel": "bookmark"}}} + // Verify first one matches expected values + testUtil.Equals(t, expectedVolume, volumes[0]) + } + + testVolumeServiceAction(t, "volumes", sampleVolumesData, anon) +} + +func TestListVolumeDetails(t *testing.T) { + anon := func(volumeService *volume.Service) { + volumes, err := volumeService.VolumesDetail() + if err != nil { + t.Error(err) + } + + if len(volumes) != 2 { + t.Error(errors.New("Incorrect number of volumes found")) + } + createdAt, _ := util.NewDateTime(`"2014-09-29T14:44:31"`) + expectedVolumeDetail := volume.DetailResponse{ + ID: "30becf77-63fe-4f5e-9507-a0578ffe0949", + Attachments: []map[string]string{{"attachment_id": "ddb2ac07-ed62-49eb-93da-73b258dd9bec", "host_name": "host_test", "volume_id": "30becf77-63fe-4f5e-9507-a0578ffe0949", "device": "/dev/vdb", "id": "30becf77-63fe-4f5e-9507-a0578ffe0949", "server_id": "0f081aae-1b0c-4b89-930c-5f2562460c72"}}, + Links: []map[string]string{{"href": "http://172.16.197.131:8776/v2/1d8837c5fcef4892951397df97661f97/volumes/30becf77-63fe-4f5e-9507-a0578ffe0949", "rel": "self"}, + {"href": "http://172.16.197.131:8776/1d8837c5fcef4892951397df97661f97/volumes/30becf77-63fe-4f5e-9507-a0578ffe0949", "rel": "bookmark"}}, + Metadata: map[string]string{"readonly": "false", "attached_mode": "rw"}, + Protected: false, + Status: "available", + MigrationStatus: "", + UserID: "a971aa69-c61a-4a49-b392-b0e41609bc5d", + Encrypted: false, + Multiattach: false, + CreatedAt: createdAt, + Description: "test volume", + Volume_type: "test_type", + Name: "test_volume", + Source_volid: "4b58bbb8-3b00-4f87-8243-8c622707bbab", + Snapshot_id: "cc488e4a-9649-4e5f-ad12-20ab37c683b5", + Size: 2, + + Aavailability_zone: "default_cluster", + Rreplication_status: "", + Consistencygroup_id: ""} + testUtil.Equals(t, expectedVolumeDetail, volumes[0]) + } + + testVolumeServiceAction(t, "volumes/detail", sampleVolumeDetailsData, anon) +} + +func TestLimitFilterUrlProduced(t *testing.T) { + testVolumeQueryParameter(t, "volumes?limit=2", + volume.QueryParameters{Limit: 2}) +} + +func TestAll_tenantFilterUrlProduced(t *testing.T) { + testVolumeQueryParameter(t, "volumes?all_tenant=1", + volume.QueryParameters{All_tenant: 1}) +} + +func TestMarkerUrlProduced(t *testing.T) { + testVolumeQueryParameter(t, "volumes?marker=1776335d-72f1-48c9-b0e7-74c62cb8fede", + volume.QueryParameters{Marker: "1776335d-72f1-48c9-b0e7-74c62cb8fede"}) +} + +func TestSortKeySortUrlProduced(t *testing.T) { + testVolumeQueryParameter(t, "volumes?sort_key=id", + volume.QueryParameters{SortKey: "id"}) +} + +func TestSortDirSortUrlProduced(t *testing.T) { + testVolumeQueryParameter(t, "volumes?sort_dir=asc", + volume.QueryParameters{SortDirection: volume.Asc}) +} + +func testVolumeQueryParameter(t *testing.T, uriEndsWith string, queryParameters volume.QueryParameters) { + anon := func(volumeService *volume.Service) { + _, _ = volumeService.QueryVolumes(&queryParameters) + } + + testVolumeServiceAction(t, uriEndsWith, sampleVolumesData, anon) +} + +func testVolumeServiceAction(t *testing.T, uriEndsWith string, testData string, volumeServiceAction func(*volume.Service)) { + anon := func(req *http.Request) { + reqURL := req.URL.String() + if !strings.HasSuffix(reqURL, uriEndsWith) { + t.Error(errors.New("Incorrect url created, expected:" + uriEndsWith + " at the end, actual url:" + reqURL)) + } + } + apiServer := testUtil.CreateGetJSONTestRequestServer(t, tokn, testData, anon) + defer apiServer.Close() + + auth := openstack.AuthToken{ + Access: openstack.AccessType{ + Token: openstack.Token{ + ID: tokn, + }, + }, + } + sess, _ := openstack.NewSession(http.DefaultClient, auth, nil) + volumeService := volume.Service{ + Session: *sess, + URL: apiServer.URL, + } + volumeServiceAction(&volumeService) +} + +var sampleVolumesData = `{ + "volumes":[ + { + "name":"volume_test1", + "id":"f5fc9874-fc89-4814-a358-23ba83a6115f", + "links":[{"href": "http://172.16.197.131:8776/v2/1d8837c5fcef4892951397df97661f97/volumes/f5fc9874-fc89-4814-a358-23ba83a6115f", "rel": "self"}, + {"href": "http://172.16.197.131:8776/1d8837c5fcef4892951397df97661f97/volumes/f5fc9874-fc89-4814-a358-23ba83a6115f", "rel": "bookmark"}] + }, + { + "name":"volume_test1", + "id":"60055a0a-2451-4d78-af9c-f2302150602f", + "links":[{"href": "http://172.16.197.131:8776/v2/1d8837c5fcef4892951397df97661f97/volumes/60055a0a-2451-4d78-af9c-f2302150602f", "rel": "self"}, + {"href": "http://172.16.197.131:8776/1d8837c5fcef4892951397df97661f97/volumes/60055a0a-2451-4d78-af9c-f2302150602f", "rel": "bookmark"}] + } + ] +}` + +var sampleVolumeDetailsData = `{ + "volumes":[ + { + "id":"30becf77-63fe-4f5e-9507-a0578ffe0949", + "attachments":[{"attachment_id": "ddb2ac07-ed62-49eb-93da-73b258dd9bec", "host_name": "host_test", "volume_id": "30becf77-63fe-4f5e-9507-a0578ffe0949", "device": "/dev/vdb", "id": "30becf77-63fe-4f5e-9507-a0578ffe0949", "server_id": "0f081aae-1b0c-4b89-930c-5f2562460c72"}], + "links":[{"href": "http://172.16.197.131:8776/v2/1d8837c5fcef4892951397df97661f97/volumes/30becf77-63fe-4f5e-9507-a0578ffe0949", "rel": "self"}, + {"href": "http://172.16.197.131:8776/1d8837c5fcef4892951397df97661f97/volumes/30becf77-63fe-4f5e-9507-a0578ffe0949", "rel": "bookmark"}], + "metadata":{"readonly": "false", "attached_mode": "rw"}, + "protected":false, + "status":"available", + "migrationStatus":null, + "user_id":"a971aa69-c61a-4a49-b392-b0e41609bc5d", + "encrypted":false, + "multiattach":false, + "created_at":"2014-09-29T14:44:31", + "description":"test volume", + "volume_type":"test_type", + "name":"test_volume", + "source_volid":"4b58bbb8-3b00-4f87-8243-8c622707bbab", + "snapshot_id":"cc488e4a-9649-4e5f-ad12-20ab37c683b5", + "size":2, + + "availability_zone":"default_cluster", + "replication_status":null, + "consistencygroup_id":null + }, + { + "id":"242d3d14-2efd-4c63-9a6b-ef6bc8eed756", + "attachments":[{"attachment_id": "9d4fb045-f957-489b-9e7d-f6f156002c04", "host_name": "host_test2", "volume_id": "242d3d14-2efd-4c63-9a6b-ef6bc8eed756", "device": "/dev/vdb", "id": "242d3d14-2efd-4c63-9a6b-ef6bc8eed756", "server_id": "9f47bd1c-c596-424d-abbe-e5e1a7a65fdc"}], + "links":[{"href": "http://172.16.197.131:8776/v2/1d8837c5fcef4892951397df97661f97/volumes/242d3d14-2efd-4c63-9a6b-ef6bc8eed756", "rel": "self"}, + {"href": "http://172.16.197.131:8776/1d8837c5fcef4892951397df97661f97/volumes/242d3d14-2efd-4c63-9a6b-ef6bc8eed756", "rel": "bookmark"}], + "metadata":{"readonly": "false", "attached_mode": "rw"}, + "protected":false, + "status":"available", + "migrationStatus":null, + "user_id":"a971aa69-c61a-4a49-b392-b0e41609bc5d", + "encrypted":false, + "multiattach":false, + "created_at":"2014-09-29T14:44:35", + "description":"test volume 2", + "volume_type":"test_type", + "name":"test_volume2", + "source_volid":null, + "snapshot_id":null, + "size":2, + + "availability_zone":"default_cluster", + "replication_status":null, + "consistencygroup_id":null + } + ] +}`