Added Capability to get images and images with details

new file:   examples/30-image-v1.go
	modified:   examples/config.json.dist
	modified:   examples/setup.go
	modified:   identity/v2/auth.go
	new file:   image/v1/image.go
	new file:   image/v1/image_test.go
	new file:   misc/rfc8601DateTime.go
	new file:   misc/rfc8601DateTime_test.go
	modified:   misc/util.go
	modified:   misc/util_test.go
	modified:   objectstorage/v1/objectstorage.go
	modified:   objectstorage/v1/objectstorage_test.go
	new file:   testUtil/testUtil.go

Partially implements blueprint image-v1

Change-Id: I6277a5c8915f45f4b7585855d836015ffd9e1c12
This commit is contained in:
Chris Robinson 2014-10-27 12:08:04 -07:00
parent 03aa5209e0
commit 219cc1c2c1
13 changed files with 979 additions and 23 deletions

75
examples/30-image-v1.go Normal file
View File

@ -0,0 +1,75 @@
// 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"
"git.openstack.org/stackforge/golang-client.git/identity/v2"
"git.openstack.org/stackforge/golang-client.git/image/v1"
"net/http"
"time"
)
// Image examples.
func main() {
config := getConfig()
// Authenticate with a username, password, tenant id.
auth, err := identity.AuthUserNameTenantName(config.Host,
config.Username,
config.Password,
config.ProjectName)
if err != nil {
panicString := fmt.Sprint("There was an error authenticating:", err)
panic(panicString)
}
if !auth.Access.Token.Expires.After(time.Now()) {
panic("There was an error. The auth token has an invalid expiration.")
}
// Find the endpoint for the image service.
url := ""
for _, svc := range auth.Access.ServiceCatalog {
if svc.Type == "image" {
for _, ep := range svc.Endpoints {
if ep.VersionId == "1.0" && ep.Region == config.ImageRegion {
url = ep.PublicURL
break
}
}
}
}
if url == "" {
panic("v1 image service url not found during authentication")
}
imageService := image.Service{TokenID: auth.Access.Token.Id, Client: *http.DefaultClient, URL: url}
imagesDetails, err := imageService.ImagesDetail()
if err != nil {
panicString := fmt.Sprint("Cannot access images:", err)
panic(panicString)
}
var imageIDs = make([]string, 0)
for _, element := range imagesDetails {
imageIDs = append(imageIDs, element.ID)
}
if len(imageIDs) == 0 {
panicString := fmt.Sprint("No images found, check to make sure access is correct")
panic(panicString)
}
}

View File

@ -5,4 +5,5 @@
"ProjectID": "",
"ProjectName": "",
"Container": "I♡HPHelion"
"ImageRegion": ""
}

View File

@ -31,6 +31,7 @@ type testconfig struct {
ProjectID string
ProjectName string
Container string
ImageRegion string
}
// getConfig provides access to credentials in other tests and examples.

View File

@ -135,7 +135,7 @@ func auth(url, jsonStr *string) (Auth, error) {
if err != nil {
return Auth{}, err
}
if err = misc.CheckHttpResponseStatusCode(resp); err != nil {
if err = misc.CheckHTTPResponseStatusCode(resp); err != nil {
return Auth{}, err
}
var contentType string = strings.ToLower(resp.Header.Get("Content-Type"))

211
image/v1/image.go Normal file
View File

@ -0,0 +1,211 @@
// 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 image implements a client library for accessing OpenStack Image V1 service
Images and ImageDetails can be retrieved using the api.
In addition more complex filtering and sort queries can by using the ImageQueryParameters.
*/
package image
import (
"fmt"
"git.openstack.org/stackforge/golang-client.git/misc"
"net/http"
"net/url"
)
// Service is a client service that can make
// requests against a OpenStack version 1 image service.
// Below is an example on creating an image service and getting images:
// imageService := image.ImageService{Client: *http.DefaultClient, TokenId: tokenId, Url: "http://imageservicelocation"}
// images:= imageService.Images()
type Service struct {
Client http.Client
TokenID string
URL string
}
// Response is a structure for all properties of
// an image for a non detailed query
type Response struct {
CheckSum string `json:"checksum"`
ContainerFormat string `json:"container_format"`
DiskFormat string `json:"disk_format"`
ID string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
}
// DetailResponse is a structure for all properties of
// an image for a detailed query
type DetailResponse struct {
CheckSum string `json:"checksum"`
ContainerFormat string `json:"container_format"`
CreatedAt misc.RFC8601DateTime `json:"created_at"`
Deleted bool `json:"deleted"`
DeletedAt *misc.RFC8601DateTime `json:"deleted_at"`
DiskFormat string `json:"disk_format"`
ID string `json:"id"`
IsPublic bool `json:"is_public"`
MinDisk int64 `json:"min_disk"`
MinRAM int64 `json:"min_ram"`
Name string `json:"name"`
Owner *string `json:"owner"`
UpdatedAt misc.RFC8601DateTime `json:"updated_at"`
Properties map[string]string `json:"properties"`
Protected bool `json:"protected"`
Status string `json:"status"`
Size int64 `json:"size"`
VirtualSize *int64 `json:"virtual_size"` // Note: Property exists in OpenStack dev stack payloads but not Helion public cloud.
}
// QueryParameters is a structure that
// contains the filter, sort, and paging parameters for
// an image or imagedetail query.
type QueryParameters struct {
Name string
Status string
ContainerFormat string
DiskFormat string
MinSize int64
MaxSize int64
SortKey string
SortDirection SortDirection
Marker string
Limit int64
}
// 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"
)
// Images will issue a get request to OpenStack to retrieve the list of images.
func (imageService Service) Images() (image []Response, err error) {
return imageService.QueryImages(nil)
}
// ImagesDetail will issue a get request to OpenStack to retrieve the list of images complete with
// additional details.
func (imageService Service) ImagesDetail() (image []DetailResponse, err error) {
return imageService.QueryImagesDetail(nil)
}
// QueryImages will issue a get request with the specified ImageQueryParameters to retrieve the list of
// images.
func (imageService Service) QueryImages(queryParameters *QueryParameters) ([]Response, error) {
imagesContainer := imagesResponse{}
err := imageService.queryImages(false /*includeDetails*/, &imagesContainer, queryParameters)
if err != nil {
return nil, err
}
return imagesContainer.Images, nil
}
// QueryImagesDetail will issue a get request with the specified QueryParameters to retrieve the list of
// images with additional details.
func (imageService Service) QueryImagesDetail(queryParameters *QueryParameters) ([]DetailResponse, error) {
imagesDetailContainer := imagesDetailResponse{}
err := imageService.queryImages(true /*includeDetails*/, &imagesDetailContainer, queryParameters)
if err != nil {
return nil, err
}
return imagesDetailContainer.Images, nil
}
func (imageService Service) queryImages(includeDetails bool, imagesResponseContainer interface{}, queryParameters *QueryParameters) error {
urlPostFix := "/images"
if includeDetails {
urlPostFix = urlPostFix + "/detail"
}
reqURL, err := buildQueryURL(imageService, queryParameters, urlPostFix)
if err != nil {
return err
}
err = misc.GetJSON(reqURL.String(), imageService.TokenID, imageService.Client, &imagesResponseContainer)
if err != nil {
return err
}
return nil
}
func buildQueryURL(imageService Service, queryParameters *QueryParameters, imagePartialURL string) (*url.URL, error) {
reqURL, err := url.Parse(imageService.URL)
if err != nil {
return nil, err
}
if queryParameters != nil {
values := url.Values{}
if queryParameters.Name != "" {
values.Set("name", queryParameters.Name)
}
if queryParameters.ContainerFormat != "" {
values.Set("container_format", queryParameters.ContainerFormat)
}
if queryParameters.DiskFormat != "" {
values.Set("disk_format", queryParameters.DiskFormat)
}
if queryParameters.Status != "" {
values.Set("status", queryParameters.Status)
}
if queryParameters.MinSize != 0 {
values.Set("size_min", fmt.Sprintf("%d", queryParameters.MinSize))
}
if queryParameters.MaxSize != 0 {
values.Set("size_max", fmt.Sprintf("%d", queryParameters.MaxSize))
}
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 += imagePartialURL
return reqURL, nil
}
type imagesDetailResponse struct {
Images []DetailResponse `json:"images"`
}
type imagesResponse struct {
Images []Response `json:"images"`
}

261
image/v1/image_test.go Normal file
View File

@ -0,0 +1,261 @@
// 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.
// image.go
package image_test
import (
"errors"
"git.openstack.org/stackforge/golang-client.git/image/v1"
"git.openstack.org/stackforge/golang-client.git/misc"
"git.openstack.org/stackforge/golang-client.git/testUtil"
"net/http"
"strings"
"testing"
)
var tokn = "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb"
func TestListImages(t *testing.T) {
anon := func(imageService *image.Service) {
images, err := imageService.Images()
if err != nil {
t.Error(err)
}
if len(images) != 3 {
t.Error(errors.New("Incorrect number of images found"))
}
expectedImage := image.Response{
Name: "Ubuntu Server 14.04.1 LTS (amd64 20140927) - Partner Image",
ContainerFormat: "bare",
DiskFormat: "qcow2",
CheckSum: "6798a7d67ff0b241b6fe165798914d86",
ID: "bec3cab5-4722-40b9-a78a-3489218e22fe",
Size: 255525376}
// Verify first one matches expected values
testUtil.Equals(t, expectedImage, images[0])
}
testImageServiceAction(t, "images", sampleImagesData, anon)
}
func TestListImageDetails(t *testing.T) {
anon := func(imageService *image.Service) {
images, err := imageService.ImagesDetail()
if err != nil {
t.Error(err)
}
if len(images) != 2 {
t.Error(errors.New("Incorrect number of images found"))
}
createdAt, _ := misc.NewDateTime(`"2014-09-29T14:44:31"`)
updatedAt, _ := misc.NewDateTime(`"2014-09-29T15:33:37"`)
owner := "10014302369510"
virtualSize := int64(2525125)
expectedImageDetail := image.DetailResponse{
Status: "active",
Name: "Ubuntu Server 12.04.5 LTS (amd64 20140927) - Partner Image",
Deleted: false,
ContainerFormat: "bare",
CreatedAt: createdAt,
DiskFormat: "qcow2",
UpdatedAt: updatedAt,
MinDisk: 8,
Protected: false,
ID: "8ca068c5-6fde-4701-bab8-322b3e7c8d81",
MinRAM: 0,
CheckSum: "de1831ea85702599a27e7e63a9a444c3",
Owner: &owner,
IsPublic: true,
DeletedAt: nil,
Properties: map[string]string{
"com.ubuntu.cloud__1__milestone": "release",
"com.hp__1__os_distro": "com.ubuntu",
"description": "Ubuntu Server 12.04.5 LTS (amd64 20140927) for HP Public Cloud. Ubuntu Server is the world's most popular Linux for cloud environments. Updates and patches for Ubuntu 12.04.5 LTS will be available until 2017-04-26. Ubuntu Server is the perfect platform for all workloads from web applications to NoSQL databases and Hadoop. More information regarding Ubuntu Cloud is available from http://www.ubuntu.com/cloud and instructions for using Juju to deploy workloads are available from http://juju.ubuntu.com EULA: http://www.ubuntu.com/about/about-ubuntu/licensing Privacy Policy: http://www.ubuntu.com/privacy-policy",
"com.ubuntu.cloud__1__suite": "precise",
"com.ubuntu.cloud__1__serial": "20140927",
"com.hp__1__bootable_volume": "True",
"com.hp__1__vendor": "Canonical",
"com.hp__1__image_lifecycle": "active",
"com.hp__1__image_type": "disk",
"os_version": "12.04",
"architecture": "x86_64",
"os_type": "linux-ext4",
"com.ubuntu.cloud__1__stream": "server",
"com.ubuntu.cloud__1__official": "True",
"com.ubuntu.cloud__1__published_at": "2014-09-29T15:33:36"},
Size: 261423616,
VirtualSize: &virtualSize}
testUtil.Equals(t, expectedImageDetail, images[0])
}
testImageServiceAction(t, "images/detail", sampleImageDetailsData, anon)
}
func TestNameFilterUrlProduced(t *testing.T) {
testImageQueryParameter(t, "images?name=CentOS+deprecated",
image.QueryParameters{Name: "CentOS deprecated"})
}
func TestStatusUrlProduced(t *testing.T) {
testImageQueryParameter(t, "images?status=active",
image.QueryParameters{Status: "active"})
}
func TestMinMaxSizeUrlProduced(t *testing.T) {
testImageQueryParameter(t, "images?size_max=5300014&size_min=100158",
image.QueryParameters{MinSize: 100158, MaxSize: 5300014})
}
func TestMarkerLimitUrlProduced(t *testing.T) {
testImageQueryParameter(t, "images?limit=20&marker=bec3cab5-4722-40b9-a78a-3489218e22fe",
image.QueryParameters{Marker: "bec3cab5-4722-40b9-a78a-3489218e22fe", Limit: 20})
}
func TestContainerFormatFilterUrlProduced(t *testing.T) {
testImageQueryParameter(t, "images?container_format=bare",
image.QueryParameters{ContainerFormat: "bare"})
}
func TestSortKeySortUrlProduced(t *testing.T) {
testImageQueryParameter(t, "images?sort_key=id",
image.QueryParameters{SortKey: "id"})
}
func TestSortDirSortUrlProduced(t *testing.T) {
testImageQueryParameter(t, "images?sort_dir=asc",
image.QueryParameters{SortDirection: image.Asc})
}
func testImageQueryParameter(t *testing.T, uriEndsWith string, queryParameters image.QueryParameters) {
anon := func(imageService *image.Service) {
_, _ = imageService.QueryImages(&queryParameters)
}
testImageServiceAction(t, uriEndsWith, sampleImagesData, anon)
}
func testImageServiceAction(t *testing.T, uriEndsWith string, testData string, imageServiceAction func(*image.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()
imageService := image.Service{TokenID: tokn, Client: *http.DefaultClient, URL: apiServer.URL}
imageServiceAction(&imageService)
}
var sampleImagesData = `{
"images":[
{
"name":"Ubuntu Server 14.04.1 LTS (amd64 20140927) - Partner Image",
"container_format":"bare",
"disk_format":"qcow2",
"checksum":"6798a7d67ff0b241b6fe165798914d86",
"id":"bec3cab5-4722-40b9-a78a-3489218e22fe",
"size":255525376
},
{
"name":"Ubuntu Server 12.04.5 LTS (amd64 20140927) - Partner Image",
"container_format":"bare",
"disk_format":"qcow2",
"checksum":"de1831ea85702599a27e7e63a9a444c3",
"id":"8ca068c5-6fde-4701-bab8-322b3e7c8d81",
"size":261423616
},
{
"name":"HP_LR-PC_Load_Generator_12-02_Windows-2008R2x64",
"container_format":"bare",
"disk_format":"qcow2",
"checksum":"052d70c2b4d4988a8816197381e9083a",
"id":"12b9c19b-8823-4f40-9531-0f05fb0933f2",
"size":14012055552
}
]
}`
var sampleImageDetailsData = `{
"images":[
{
"status":"active",
"name":"Ubuntu Server 12.04.5 LTS (amd64 20140927) - Partner Image",
"deleted":false,
"container_format":"bare",
"created_at":"2014-09-29T14:44:31",
"disk_format":"qcow2",
"updated_at":"2014-09-29T15:33:37",
"min_disk":8,
"protected":false,
"id":"8ca068c5-6fde-4701-bab8-322b3e7c8d81",
"min_ram":0,
"checksum":"de1831ea85702599a27e7e63a9a444c3",
"owner":"10014302369510",
"is_public":true,
"deleted_at":null,
"properties":{
"com.ubuntu.cloud__1__milestone":"release",
"com.hp__1__os_distro":"com.ubuntu",
"description":"Ubuntu Server 12.04.5 LTS (amd64 20140927) for HP Public Cloud. Ubuntu Server is the world's most popular Linux for cloud environments. Updates and patches for Ubuntu 12.04.5 LTS will be available until 2017-04-26. Ubuntu Server is the perfect platform for all workloads from web applications to NoSQL databases and Hadoop. More information regarding Ubuntu Cloud is available from http://www.ubuntu.com/cloud and instructions for using Juju to deploy workloads are available from http://juju.ubuntu.com EULA: http://www.ubuntu.com/about/about-ubuntu/licensing Privacy Policy: http://www.ubuntu.com/privacy-policy",
"com.ubuntu.cloud__1__suite":"precise",
"com.ubuntu.cloud__1__serial":"20140927",
"com.hp__1__bootable_volume":"True",
"com.hp__1__vendor":"Canonical",
"com.hp__1__image_lifecycle":"active",
"com.hp__1__image_type":"disk",
"os_version":"12.04",
"architecture":"x86_64",
"os_type":"linux-ext4",
"com.ubuntu.cloud__1__stream":"server",
"com.ubuntu.cloud__1__official":"True",
"com.ubuntu.cloud__1__published_at":"2014-09-29T15:33:36"
},
"size":261423616,
"virtual_size":2525125
},
{
"status":"active",
"name":"Windows Server 2008 Enterprise SP2 x64 Volume License 20140415 (b)",
"deleted":false,
"container_format":"bare",
"created_at":"2014-04-25T19:53:24",
"disk_format":"qcow2",
"updated_at":"2014-04-25T19:57:11",
"min_disk":30,
"protected":true,
"id":"1294610e-fdc4-579b-829b-d0c9f5c0a612",
"min_ram":0,
"checksum":"37208aa6d49929f12132235c5f834f2d",
"owner":null,
"is_public":true,
"deleted_at":null,
"properties":{
"hp_image_license":"1002",
"com.hp__1__os_distro":"com.microsoft.server",
"com.hp__1__image_lifecycle":"active",
"com.hp__1__image_type":"disk",
"architecture":"x86_64",
"com.hp__1__license_os":"1002",
"com.hp__1__bootable_volume":"true"
},
"size":6932856832,
"virtual_size":null
}
]
}`

57
misc/rfc8601DateTime.go Normal file
View File

@ -0,0 +1,57 @@
// 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 misc
import (
"time"
)
// RFC8601DateTime is a type for decoding and encoding json
// date times that follow RFC 8601 format. The type currently
// decodes and encodes with exactly precision to seconds. If more
// formats of RFC8601 need to be supported additional work
// will be needed.
type RFC8601DateTime struct {
time.Time
}
// NewDateTime creates a new RFC8601DateTime taking a string as input.
// It must follow the "2006-01-02T15:04:05" pattern.
func NewDateTime(input string) (val RFC8601DateTime, err error) {
val = RFC8601DateTime{}
err = val.formatValue(input)
return val, err
}
// UnmarshalJSON converts the bytes give to a RFC8601DateTime
// Errors will occur if the bytes when converted to a string
// don't match the format "2006-01-02T15:04:05".
func (r *RFC8601DateTime) UnmarshalJSON(data []byte) error {
return r.formatValue(string(data))
}
// MarshalJSON converts a RFC8601DateTime to a []byte.
func (r RFC8601DateTime) MarshalJSON() ([]byte, error) {
val := r.Time.Format(format)
return []byte(val), nil
}
func (r *RFC8601DateTime) formatValue(input string) (err error) {
timeVal, err := time.Parse(format, input)
r.Time = timeVal
return
}
const format = `"2006-01-02T15:04:05"`

View File

@ -0,0 +1,58 @@
// 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 misc_test
import (
"encoding/json"
"git.openstack.org/stackforge/golang-client.git/misc"
"git.openstack.org/stackforge/golang-client.git/testUtil"
"testing"
"time"
)
var testValue = `{"created_at":"2014-09-29T14:44:31"}`
var testTime, _ = time.Parse(`"2006-01-02T15:04:05"`, `"2014-09-29T14:44:31"`)
var timeTestValue = timeTest{CreatedAt: misc.RFC8601DateTime{testTime}}
func TestMarshalTimeTest(t *testing.T) {
bytes, _ := json.Marshal(timeTestValue)
testUtil.Equals(t, testValue, string(bytes))
}
func TestUnmarshalValidTimeTest(t *testing.T) {
val := timeTest{}
err := json.Unmarshal([]byte(testValue), &val)
testUtil.IsNil(t, err)
testUtil.Equals(t, timeTestValue.CreatedAt.Time, val.CreatedAt.Time)
}
func TestUnmarshalInvalidDataFormatTimeTest(t *testing.T) {
val := timeTest{}
err := json.Unmarshal([]byte("something other than date time"), &val)
testUtil.Assert(t, err != nil, "expected an error")
}
// Added this test to ensure that its understood that
// only one specific format is supported at this time.
func TestUnmarshalInvalidDateTimeFormatTimeTest(t *testing.T) {
val := timeTest{}
err := json.Unmarshal([]byte("2014-09-29T14:44"), &val)
testUtil.Assert(t, err != nil, "expected an error")
}
type timeTest struct {
CreatedAt misc.RFC8601DateTime `json:"created_at"`
}

View File

@ -16,11 +16,106 @@ package misc
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
)
var zeroByte = new([]byte) //pointer to empty []byte
// PostJSON sends an Http Request with using the "POST" method and with
// a "Content-Type" header with application/json and X-Auth-Token" header
// set to the specified token value. The inputValue is encoded to json
// and sent in the body of the request. The response json body is
// decoded into the outputValue. If the response does sends an invalid
// or error status code then an error will be returned. If the Content-Type
// value of the response is not "application/json" an error is returned.
func PostJSON(url string, token string, client http.Client, inputValue interface{}, outputValue interface{}) (err error) {
body, err := json.Marshal(inputValue)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Auth-Token", token)
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 201 && resp.StatusCode != 202 {
err = errors.New("Error: status code != 201 or 202, actual status code '" + resp.Status + "'")
return
}
contentTypeValue := resp.Header.Get("Content-Type")
if contentTypeValue != "application/json" {
err = errors.New("Error: Expected a json payload but instead recieved '" + contentTypeValue + "'")
return
}
err = json.NewDecoder(resp.Body).Decode(&outputValue)
defer resp.Body.Close()
if err != nil {
return err
}
return nil
}
// Delete sends an Http Request with using the "DELETE" method and with
// an "X-Auth-Token" header set to the specified token value. The request
// is made by the specified client.
func Delete(url string, token string, client http.Client) (err error) {
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return err
}
req.Header.Set("X-Auth-Token", token)
resp, err := client.Do(req)
if err != nil {
return err
}
// Expecting a successful delete
if !(resp.StatusCode == 200 || resp.StatusCode == 202 || resp.StatusCode == 204) {
err = fmt.Errorf("Unexpected server response status code on Delete '%s'", resp.StatusCode)
return
}
return nil
}
//GetJSON sends an Http Request with using the "GET" method and with
//an "Accept" header set to "application/json" and the authenication token
//set to the specified token value. The request is made by the
//specified client. The val interface should be a pointer to the
//structure that the json response should be decoded into.
func GetJSON(url string, token string, client http.Client, val interface{}) (err error) {
req, err := createJSONGetRequest(url, token)
if err != nil {
return err
}
err = executeRequestCheckStatusDecodeJSONResponse(client, req, val)
if err != nil {
return err
}
return nil
}
//CallAPI sends an HTTP request using "method" to "url".
//For uploading / sending file, caller needs to set the "content". Otherwise,
//set it to zero length []byte. If Header fields need to be set, then set it in
@ -71,11 +166,11 @@ func (readCloser) Close() error {
return nil
}
//CheckStatusCode compares http response header StatusCode against expected
// CheckHTTPResponseStatusCode compares http response header StatusCode against expected
// statuses. Primary function is to ensure StatusCode is in the 20x (return nil).
// Ok: 200. Created: 201. Accepted: 202. No Content: 204.
// Otherwise return error message.
func CheckHttpResponseStatusCode(resp *http.Response) error {
func CheckHTTPResponseStatusCode(resp *http.Response) error {
switch resp.StatusCode {
case 200, 201, 202, 204:
return nil
@ -108,3 +203,32 @@ func CheckHttpResponseStatusCode(resp *http.Response) error {
}
return errors.New("Error: unexpected response status code")
}
func createJSONGetRequest(url string, token string) (req *http.Request, err error) {
req, err = http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Auth-Token", token)
return req, nil
}
func executeRequestCheckStatusDecodeJSONResponse(client http.Client, req *http.Request, val interface{}) (err error) {
resp, err := client.Do(req)
if err != nil {
return err
}
err = CheckHTTPResponseStatusCode(resp)
if err != nil {
return err
}
err = json.NewDecoder(resp.Body).Decode(&val)
defer resp.Body.Close()
return err
}

View File

@ -18,6 +18,7 @@ import (
"bytes"
"errors"
"git.openstack.org/stackforge/golang-client.git/misc"
"git.openstack.org/stackforge/golang-client.git/testUtil"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -25,6 +26,29 @@ import (
"testing"
)
var token = "2350971-5716-8165"
func TestDelete(t *testing.T) {
var apiServer = testUtil.CreateDeleteTestRequestServer(t, token, "/other")
defer apiServer.Close()
err := misc.Delete(apiServer.URL+"/other", token, *http.DefaultClient)
testUtil.IsNil(t, err)
}
func TestPostJsonWithValidResponse(t *testing.T) {
var apiServer = testUtil.CreatePostJSONTestRequestServer(t, token, `{"id":"id1","name":"Chris"}`, "", `{"id":"id1","name":"name"}`)
defer apiServer.Close()
actual := TestStruct{}
ti := TestStruct{ID: "id1", Name: "name"}
err := misc.PostJSON(apiServer.URL, token, *http.DefaultClient, ti, &actual)
testUtil.IsNil(t, err)
expected := TestStruct{ID: "id1", Name: "Chris"}
testUtil.Equals(t, expected, actual)
}
func TestCallAPI(t *testing.T) {
tokn := "eaaafd18-0fed-4b3a-81b4-663c99ec1cbb"
var apiServer = httptest.NewServer(http.HandlerFunc(
@ -108,3 +132,8 @@ func TestCallAPIPutContent(t *testing.T) {
t.Error(err)
}
}
type TestStruct struct {
ID string `json:"id"`
Name string `json:"name"`
}

View File

@ -69,8 +69,8 @@ func PutContainer(url, token string, s ...string) error {
//obtained token. "Limit", "marker", "prefix", "path", "delim" corresponds
//to the API's "limit", "marker", "prefix", "path", and "delimiter".
func ListObjects(limit int64,
marker, prefix, path, delim, conUrl, token string) ([]byte, error) {
var query string = "?format=json"
marker, prefix, path, delim, conURL, token string) ([]byte, error) {
var query = "?format=json"
if limit > 0 {
query += "&limit=" + strconv.FormatInt(limit, 10)
}
@ -86,12 +86,12 @@ func ListObjects(limit int64,
if delim != "" {
query += "&delimiter=" + url.QueryEscape(delim)
}
resp, err := misc.CallAPI("GET", conUrl+query, zeroByte,
resp, err := misc.CallAPI("GET", conURL+query, zeroByte,
"X-Auth-Token", token)
if err != nil {
return nil, err
}
if err = misc.CheckHttpResponseStatusCode(resp); err != nil {
if err = misc.CheckHTTPResponseStatusCode(resp); err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
@ -112,20 +112,20 @@ func PutObject(fContent *[]byte, url, token string, s ...string) (err error) {
if err != nil {
return err
}
return misc.CheckHttpResponseStatusCode(resp)
return misc.CheckHTTPResponseStatusCode(resp)
}
//CopyObject calls the OpenStack copy object API using previously obtained
//token. Note from API doc: "The destination container must exist before
//attempting the copy."
func CopyObject(srcUrl, destUrl, token string) (err error) {
resp, err := misc.CallAPI("COPY", srcUrl, zeroByte,
func CopyObject(srcURL, destURL, token string) (err error) {
resp, err := misc.CallAPI("COPY", srcURL, zeroByte,
"X-Auth-Token", token,
"Destination", destUrl)
"Destination", destURL)
if err != nil {
return err
}
return misc.CheckHttpResponseStatusCode(resp)
return misc.CheckHTTPResponseStatusCode(resp)
}
//DeleteObject calls the OpenStack delete object API using
@ -141,7 +141,7 @@ func DeleteObject(url, token string) (err error) {
if err != nil {
return err
}
return misc.CheckHttpResponseStatusCode(resp)
return misc.CheckHTTPResponseStatusCode(resp)
}
//SetObjectMeta calls the OpenStack API to create/update meta data for
@ -153,7 +153,7 @@ func SetObjectMeta(url string, token string, s ...string) (err error) {
if err != nil {
return err
}
return misc.CheckHttpResponseStatusCode(resp)
return misc.CheckHTTPResponseStatusCode(resp)
}
//GetObjectMeta calls the OpenStack retrieve object metadata API using
@ -163,7 +163,7 @@ func GetObjectMeta(url, token string) (http.Header, error) {
if err != nil {
return nil, err
}
return resp.Header, misc.CheckHttpResponseStatusCode(resp)
return resp.Header, misc.CheckHTTPResponseStatusCode(resp)
}
//GetObject calls the OpenStack retrieve object API using previously
@ -178,7 +178,7 @@ func GetObject(url, token string) (http.Header, []byte, error) {
if err != nil {
return nil, nil, err
}
if err = misc.CheckHttpResponseStatusCode(resp); err != nil {
if err = misc.CheckHTTPResponseStatusCode(resp); err != nil {
return nil, nil, err
}
var body []byte

View File

@ -224,18 +224,18 @@ func TestPutObject(t *testing.T) {
}
func TestCopyObject(t *testing.T) {
destUrl := "/destContainer/dest/Obj"
destURL := "/destContainer/dest/Obj"
var apiServer = httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.Method == "COPY" && r.Header.Get("Destination") == destUrl {
if r.Method == "COPY" && r.Header.Get("Destination") == destURL {
w.WriteHeader(200)
return
}
t.Error(errors.New(
"Failed: r.Method == COPY && r.Header.Get(Destination) == destUrl"))
"Failed: r.Method == COPY && r.Header.Get(Destination) == destURL"))
}))
defer apiServer.Close()
if err := objectstorage.CopyObject(apiServer.URL+objPrefix, destUrl,
if err := objectstorage.CopyObject(apiServer.URL+objPrefix, destURL,
tokn); err != nil {
t.Error(err)
}

139
testUtil/testUtil.go Normal file
View File

@ -0,0 +1,139 @@
// 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 testUtil has helpers to be used with unit tests
package testUtil
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/http/httputil"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
)
// Equals fails the test if exp is not equal to act.
// Code was copied from https://github.com/benbjohnson/testing MIT license
func Equals(tb testing.TB, exp, act interface{}) {
if !reflect.DeepEqual(exp, act) {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
tb.FailNow()
}
}
// Assert fails the test if the condition is false.
// Code was copied from https://github.com/benbjohnson/testing MIT license
func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) {
if !condition {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...)
tb.FailNow()
}
}
// IsNil ensures that the act interface is nil
// otherwise an error is raised.
func IsNil(tb testing.TB, act interface{}) {
if act != nil {
tb.Error("expected nil", act)
tb.FailNow()
}
}
// CreateGetJSONTestRequestServer creates a httptest.Server that can be used to test GetJson requests. Just specify the token,
// json payload that is to be read by the response, and a verification func that can be used
// to do additional validation of the request that is built
func CreateGetJSONTestRequestServer(t *testing.T, expectedAuthTokenValue string, jsonResponsePayload string, verifyRequest func(*http.Request)) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
headerValuesEqual(t, r, "X-Auth-Token", expectedAuthTokenValue)
headerValuesEqual(t, r, "Accept", "application/json")
verifyRequest(r)
if r.Method == "GET" {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(jsonResponsePayload))
w.WriteHeader(http.StatusOK)
return
}
t.Error(errors.New("Failed: r.Method == GET"))
}))
}
// CreatePostJSONTestRequestServer creates a http.Server that can be used to test PostJson requests. Specify the token,
// response json payload and the url and request body that is expected.
func CreatePostJSONTestRequestServer(t *testing.T, expectedAuthTokenValue string, outputResponseJSONPayload string, expectedRequestURLEndsWith string, expectedRequestBody string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
headerValuesEqual(t, r, "X-Auth-Token", expectedAuthTokenValue)
headerValuesEqual(t, r, "Accept", "application/json")
headerValuesEqual(t, r, "Content-Type", "application/json")
reqURL := r.URL.String()
if !strings.HasSuffix(reqURL, expectedRequestURLEndsWith) {
t.Error(errors.New("Incorrect url created, expected:" + expectedRequestURLEndsWith + " at the end, actual url:" + reqURL))
}
actualRequestBody := dumpRequestBody(r)
if actualRequestBody != expectedRequestBody {
t.Error(errors.New("Incorrect payload created, expected:'" + expectedRequestBody + "', actual '" + actualRequestBody + "'"))
}
if r.Method == "POST" {
w.Header().Set("Content-Type", "application/json")
// Status Code has to be written before writing the payload or
// else it defaults to 200 OK instead.
w.WriteHeader(http.StatusCreated)
w.Write([]byte(outputResponseJSONPayload))
return
}
t.Error(errors.New("Failed: r.Method == POST"))
}))
}
// CreateDeleteTestRequestServer creates a http.Server that can be used to test Delete requests.
func CreateDeleteTestRequestServer(t *testing.T, expectedAuthTokenValue string, urlEndsWith string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
headerValuesEqual(t, r, "X-Auth-Token", expectedAuthTokenValue)
reqURL := r.URL.String()
if !strings.HasSuffix(reqURL, urlEndsWith) {
t.Error(errors.New("Incorrect url created, expected '" + urlEndsWith + "' at the end, actual url:" + reqURL))
}
if r.Method == "DELETE" {
w.WriteHeader(204)
return
}
t.Error(errors.New("Failed: r.Method == DELETE"))
}))
}
func dumpRequestBody(request *http.Request) (body string) {
requestWithBody, _ := httputil.DumpRequest(request, true)
requestWithoutBody, _ := httputil.DumpRequest(request, false)
body = strings.Replace(string(requestWithBody), string(requestWithoutBody), "", 1)
return
}
func headerValuesEqual(t *testing.T, req *http.Request, name string, expectedValue string) {
actualValue := req.Header.Get(name)
if actualValue != expectedValue {
t.Error(fmt.Errorf("Expected Header {Name:'%s', Value:'%s', actual value '%s'", name, expectedValue, actualValue))
}
}