Openstack - Updating gophercloud version

This commit is contained in:
dkalleg 2016-06-06 13:28:07 -07:00
parent 8c3ff93395
commit 95731f2d26
21 changed files with 3495 additions and 288 deletions

View File

@ -283,25 +283,12 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service.
func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("volume")
eo.ApplyDefaults("volumev2")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
// Force using v2 API
if strings.Contains(url, "/v1") {
url = strings.Replace(url, "/v1", "/v2", -1)
}
if !strings.Contains(url, "/v2") {
return nil, fmt.Errorf("Block Storage v2 endpoint not found")
}
return &gophercloud.ServiceClient{
ProviderClient: client,
Endpoint: url,
ResourceBase: url,
}, nil
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1

View File

@ -0,0 +1,5 @@
// Package lbaas_v2 provides information and interaction with the Load Balancer
// as a Service v2 extension for the OpenStack Networking service.
// lbaas v2 api docs: http://developer.openstack.org/api-ref-networking-v2-ext.html#lbaas-v2.0
// lbaas v2 api schema: https://github.com/openstack/neutron-lbaas/blob/master/neutron_lbaas/extensions/loadbalancerv2.py
package lbaas_v2

View File

@ -0,0 +1,214 @@
// +build fixtures
package listeners
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListenersListBody contains the canned body of a listeners list response.
const ListenersListBody = `
{
"listeners":[
{
"id": "db902c0c-d5ff-4753-b465-668ad9656918",
"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
"name": "web",
"description": "listener config for the web tier",
"loadbalancers": [{"id": "53306cda-815d-4354-9444-59e09da9c3c5"}],
"protocol": "HTTP",
"protocol_port": 80,
"default_pool_id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
"admin_state_up": true,
"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
},
{
"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
"name": "db",
"description": "listener config for the db tier",
"loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
"protocol": "TCP",
"protocol_port": 3306,
"default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
"connection_limit": 2000,
"admin_state_up": true,
"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
}
]
}
`
// SingleServerBody is the canned body of a Get request on an existing listener.
const SingleListenerBody = `
{
"listener": {
"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
"name": "db",
"description": "listener config for the db tier",
"loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
"protocol": "TCP",
"protocol_port": 3306,
"default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
"connection_limit": 2000,
"admin_state_up": true,
"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
}
}
`
// PostUpdateListenerBody is the canned response body of a Update request on an existing listener.
const PostUpdateListenerBody = `
{
"listener": {
"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
"tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
"name": "NewListenerName",
"description": "listener config for the db tier",
"loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
"protocol": "TCP",
"protocol_port": 3306,
"default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
"connection_limit": 1000,
"admin_state_up": true,
"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
"sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
}
}
`
var (
ListenerWeb = Listener{
ID: "db902c0c-d5ff-4753-b465-668ad9656918",
TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
Name: "web",
Description: "listener config for the web tier",
Loadbalancers: []LoadBalancerID{{ID: "53306cda-815d-4354-9444-59e09da9c3c5"}},
Protocol: "HTTP",
ProtocolPort: 80,
DefaultPoolID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
AdminStateUp: true,
DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
}
ListenerDb = Listener{
ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
Name: "db",
Description: "listener config for the db tier",
Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
Protocol: "TCP",
ProtocolPort: 3306,
DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
ConnLimit: 2000,
AdminStateUp: true,
DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
}
ListenerUpdated = Listener{
ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
Name: "NewListenerName",
Description: "listener config for the db tier",
Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
Protocol: "TCP",
ProtocolPort: 3306,
DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
ConnLimit: 1000,
AdminStateUp: true,
DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
}
)
// HandleListenerListSuccessfully sets up the test server to respond to a listener List request.
func HandleListenerListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
marker := r.Form.Get("marker")
switch marker {
case "":
fmt.Fprintf(w, ListenersListBody)
case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
fmt.Fprintf(w, `{ "listeners": [] }`)
default:
t.Fatalf("/v2.0/lbaas/listeners invoked with unexpected marker=[%s]", marker)
}
})
}
// HandleListenerCreationSuccessfully sets up the test server to respond to a listener creation request
// with a given response.
func HandleListenerCreationSuccessfully(t *testing.T, response string) {
th.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{
"listener": {
"loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab",
"protocol": "TCP",
"name": "db",
"admin_state_up": true,
"default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
"default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
"protocol_port": 3306
}
}`)
w.WriteHeader(http.StatusAccepted)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, response)
})
}
// HandleListenerGetSuccessfully sets up the test server to respond to a listener Get request.
func HandleListenerGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, SingleListenerBody)
})
}
// HandleListenerDeletionSuccessfully sets up the test server to respond to a listener deletion request.
func HandleListenerDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
// HandleListenerUpdateSuccessfully sets up the test server to respond to a listener Update request.
func HandleListenerUpdateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestJSONRequest(t, r, `{
"listener": {
"name": "NewListenerName",
"connection_limit": 1001
}
}`)
fmt.Fprintf(w, PostUpdateListenerBody)
})
}

View File

@ -0,0 +1,279 @@
package listeners
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// AdminState gives users a solid type to work with for create and update
// operations. It is recommended that users use the `Up` and `Down` enums.
type AdminState *bool
type listenerOpts struct {
// Required. The protocol - can either be TCP, HTTP or HTTPS.
Protocol Protocol
// Required. The port on which to listen for client traffic.
ProtocolPort int
// Required for admins. Indicates the owner of the Listener.
TenantID string
// Required. The load balancer on which to provision this listener.
LoadbalancerID string
// Human-readable name for the Listener. Does not have to be unique.
Name string
// Optional. The ID of the default pool with which the Listener is associated.
DefaultPoolID string
// Optional. Human-readable description for the Listener.
Description string
// Optional. The maximum number of connections allowed for the Listener.
ConnLimit *int
// Optional. A reference to a container of TLS secrets.
DefaultTlsContainerRef string
// Optional. A list of references to TLS secrets.
SniContainerRefs []string
// Optional. The administrative state of the Listener. A valid value is true (UP)
// or false (DOWN).
AdminStateUp *bool
}
// Convenience vars for AdminStateUp values.
var (
iTrue = true
iFalse = false
Up AdminState = &iTrue
Down AdminState = &iFalse
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToListenerListQuery() (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 floating IP attributes you want to see returned. SortKey allows you to
// sort by a particular listener attribute. SortDir sets the direction, and is
// either `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
ID string `q:"id"`
Name string `q:"name"`
AdminStateUp *bool `q:"admin_state_up"`
TenantID string `q:"tenant_id"`
LoadbalancerID string `q:"loadbalancer_id"`
DefaultPoolID string `q:"default_pool_id"`
Protocol string `q:"protocol"`
ProtocolPort int `q:"protocol_port"`
ConnectionLimit int `q:"connection_limit"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"`
}
// ToListenerListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToListenerListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns a Pager which allows you to iterate over a collection of
// routers. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
//
// Default policy settings return only those routers that are owned by the
// tenant who submits the request, unless an admin user submits the request.
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := rootURL(c)
if opts != nil {
query, err := opts.ToListenerListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return ListenerPage{pagination.LinkedPageBase{PageResult: r}}
})
}
type Protocol string
// Supported attributes for create/update operations.
const (
ProtocolTCP Protocol = "TCP"
ProtocolHTTP Protocol = "HTTP"
ProtocolHTTPS Protocol = "HTTPS"
)
var (
errLoadbalancerIdRequired = fmt.Errorf("LoadbalancerID is required")
errProtocolRequired = fmt.Errorf("Protocol is required")
errProtocolPortRequired = fmt.Errorf("ProtocolPort is required")
)
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToListenerCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
// operation.
type CreateOpts listenerOpts
// ToListenerCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
if opts.LoadbalancerID != "" {
l["loadbalancer_id"] = opts.LoadbalancerID
} else {
return nil, errLoadbalancerIdRequired
}
if opts.Protocol != "" {
l["protocol"] = opts.Protocol
} else {
return nil, errProtocolRequired
}
if opts.ProtocolPort != 0 {
l["protocol_port"] = opts.ProtocolPort
} else {
return nil, errProtocolPortRequired
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.TenantID != "" {
l["tenant_id"] = opts.TenantID
}
if opts.DefaultPoolID != "" {
l["default_pool_id"] = opts.DefaultPoolID
}
if opts.Description != "" {
l["description"] = opts.Description
}
if opts.ConnLimit != nil {
l["connection_limit"] = &opts.ConnLimit
}
if opts.DefaultTlsContainerRef != "" {
l["default_tls_container_ref"] = opts.DefaultTlsContainerRef
}
if opts.SniContainerRefs != nil {
l["sni_container_refs"] = opts.SniContainerRefs
}
return map[string]interface{}{"listener": l}, nil
}
// Create is an operation which provisions a new Listeners based on the
// configuration defined in the CreateOpts struct. Once the request is
// validated and progress has started on the provisioning process, a
// CreateResult will be returned.
//
// Users with an admin role can create Listeners on behalf of other tenants by
// specifying a TenantID attribute different than their own.
func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
var res CreateResult
reqBody, err := opts.ToListenerCreateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
return res
}
// Get retrieves a particular Listeners based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
return res
}
// UpdateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Update operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type UpdateOptsBuilder interface {
ToListenerUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the common options struct used in this package's Update
// operation.
type UpdateOpts listenerOpts
// ToListenerUpdateMap casts a UpdateOpts struct to a map.
func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.Description != "" {
l["description"] = opts.Description
}
if opts.ConnLimit != nil {
l["connection_limit"] = &opts.ConnLimit
}
if opts.DefaultTlsContainerRef != "" {
l["default_tls_container_ref"] = opts.DefaultTlsContainerRef
}
if opts.SniContainerRefs != nil {
l["sni_container_refs"] = opts.SniContainerRefs
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
return map[string]interface{}{"listener": l}, nil
}
// Update is an operation which modifies the attributes of the specified Listener.
func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToListenerUpdateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 202},
})
return res
}
// Delete will permanently delete a particular Listeners based on its unique ID.
func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, id), nil)
return res
}

View File

@ -0,0 +1,139 @@
package listeners
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
"github.com/rackspace/gophercloud/pagination"
)
type LoadBalancerID struct {
ID string `mapstructure:"id" json:"id"`
}
// Listener is the primary load balancing configuration object that specifies
// the loadbalancer and port on which client traffic is received, as well
// as other details such as the load balancing method to be use, protocol, etc.
type Listener struct {
// The unique ID for the Listener.
ID string `mapstructure:"id" json:"id"`
// Owner of the Listener. Only an admin user can specify a tenant ID other than its own.
TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
// Human-readable name for the Listener. Does not have to be unique.
Name string `mapstructure:"name" json:"name"`
// Human-readable description for the Listener.
Description string `mapstructure:"description" json:"description"`
// The protocol to loadbalance. A valid value is TCP, HTTP, or HTTPS.
Protocol string `mapstructure:"protocol" json:"protocol"`
// The port on which to listen to client traffic that is associated with the
// Loadbalancer. A valid value is from 0 to 65535.
ProtocolPort int `mapstructure:"protocol_port" json:"protocol_port"`
// The UUID of default pool. Must have compatible protocol with listener.
DefaultPoolID string `mapstructure:"default_pool_id" json:"default_pool_id"`
// A list of load balancer IDs.
Loadbalancers []LoadBalancerID `mapstructure:"loadbalancers" json:"loadbalancers"`
// The maximum number of connections allowed for the Loadbalancer. Default is -1,
// meaning no limit.
ConnLimit int `mapstructure:"connection_limit" json:"connection_limit"`
// The list of references to TLS secrets.
SniContainerRefs []string `mapstructure:"sni_container_refs" json:"sni_container_refs"`
// Optional. A reference to a container of TLS secrets.
DefaultTlsContainerRef string `mapstructure:"default_tls_container_ref" json:"default_tls_container_ref"`
// The administrative state of the Listener. A valid value is true (UP) or false (DOWN).
AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
Pools []pools.Pool `mapstructure:"pools" json:"pools"`
}
// ListenerPage is the page returned by a pager when traversing over a
// collection of routers.
type ListenerPage struct {
pagination.LinkedPageBase
}
// NextPageURL is invoked when a paginated collection of routers 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 (p ListenerPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"listeners_links"`
}
var r resp
err := mapstructure.Decode(p.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a RouterPage struct is empty.
func (p ListenerPage) IsEmpty() (bool, error) {
is, err := ExtractListeners(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractListeners accepts a Page struct, specifically a ListenerPage struct,
// and extracts the elements into a slice of Listener structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractListeners(page pagination.Page) ([]Listener, error) {
var resp struct {
Listeners []Listener `mapstructure:"listeners" json:"listeners"`
}
err := mapstructure.Decode(page.(ListenerPage).Body, &resp)
return resp.Listeners, err
}
type commonResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a router.
func (r commonResult) Extract() (*Listener, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Listener *Listener `mapstructure:"listener" json:"listener"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Listener, err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,16 @@
package listeners
import "github.com/rackspace/gophercloud"
const (
rootPath = "lbaas"
resourcePath = "listeners"
)
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(rootPath, resourcePath, id)
}

View File

@ -0,0 +1,278 @@
// +build fixtures
package loadbalancers
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
)
// LoadbalancersListBody contains the canned body of a loadbalancer list response.
const LoadbalancersListBody = `
{
"loadbalancers":[
{
"id": "c331058c-6a40-4144-948e-b9fb1df9db4b",
"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
"name": "web_lb",
"description": "lb config for the web tier",
"vip_subnet_id": "8a49c438-848f-467b-9655-ea1548708154",
"vip_address": "10.30.176.47",
"flavor": "small",
"provider": "haproxy",
"admin_state_up": true,
"provisioning_status": "ACTIVE",
"operating_status": "ONLINE"
},
{
"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
"name": "db_lb",
"description": "lb config for the db tier",
"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
"vip_address": "10.30.176.48",
"flavor": "medium",
"provider": "haproxy",
"admin_state_up": true,
"provisioning_status": "PENDING_CREATE",
"operating_status": "OFFLINE"
}
]
}
`
// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer.
const SingleLoadbalancerBody = `
{
"loadbalancer": {
"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
"name": "db_lb",
"description": "lb config for the db tier",
"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
"vip_address": "10.30.176.48",
"flavor": "medium",
"provider": "haproxy",
"admin_state_up": true,
"provisioning_status": "PENDING_CREATE",
"operating_status": "OFFLINE"
}
}
`
// PostUpdateLoadbalancerBody is the canned response body of a Update request on an existing loadbalancer.
const PostUpdateLoadbalancerBody = `
{
"loadbalancer": {
"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
"tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
"name": "NewLoadbalancerName",
"description": "lb config for the db tier",
"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
"vip_address": "10.30.176.48",
"flavor": "medium",
"provider": "haproxy",
"admin_state_up": true,
"provisioning_status": "PENDING_CREATE",
"operating_status": "OFFLINE"
}
}
`
// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer.
const LoadbalancerStatuesesTree = `
{
"statuses" : {
"loadbalancer": {
"id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
"name": "db_lb",
"provisioning_status": "PENDING_UPDATE",
"operating_status": "ACTIVE",
"listeners": [{
"id": "db902c0c-d5ff-4753-b465-668ad9656918",
"name": "db",
"pools": [{
"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
"name": "db",
"healthmonitor": {
"id": "67306cda-815d-4354-9fe4-59e09da9c3c5",
"type":"PING"
},
"members":[{
"id": "2a280670-c202-4b0b-a562-34077415aabf",
"name": "db",
"address": "10.0.2.11",
"protocol_port": 80
}]
}]
}]
}
}
}
`
var (
LoadbalancerWeb = LoadBalancer{
ID: "c331058c-6a40-4144-948e-b9fb1df9db4b",
TenantID: "54030507-44f7-473c-9342-b4d14a95f692",
Name: "web_lb",
Description: "lb config for the web tier",
VipSubnetID: "8a49c438-848f-467b-9655-ea1548708154",
VipAddress: "10.30.176.47",
Flavor: "small",
Provider: "haproxy",
AdminStateUp: true,
ProvisioningStatus: "ACTIVE",
OperatingStatus: "ONLINE",
}
LoadbalancerDb = LoadBalancer{
ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
TenantID: "54030507-44f7-473c-9342-b4d14a95f692",
Name: "db_lb",
Description: "lb config for the db tier",
VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
VipAddress: "10.30.176.48",
Flavor: "medium",
Provider: "haproxy",
AdminStateUp: true,
ProvisioningStatus: "PENDING_CREATE",
OperatingStatus: "OFFLINE",
}
LoadbalancerUpdated = LoadBalancer{
ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
TenantID: "54030507-44f7-473c-9342-b4d14a95f692",
Name: "NewLoadbalancerName",
Description: "lb config for the db tier",
VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
VipAddress: "10.30.176.48",
Flavor: "medium",
Provider: "haproxy",
AdminStateUp: true,
ProvisioningStatus: "PENDING_CREATE",
OperatingStatus: "OFFLINE",
}
LoadbalancerStatusesTree = LoadBalancer{
ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
Name: "db_lb",
ProvisioningStatus: "PENDING_UPDATE",
OperatingStatus: "ACTIVE",
Listeners: []listeners.Listener{{
ID: "db902c0c-d5ff-4753-b465-668ad9656918",
Name: "db",
Pools: []pools.Pool{{
ID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
Name: "db",
Monitor: monitors.Monitor{
ID: "67306cda-815d-4354-9fe4-59e09da9c3c5",
Type: "PING",
},
Members: []pools.Member{{
ID: "2a280670-c202-4b0b-a562-34077415aabf",
Name: "db",
Address: "10.0.2.11",
ProtocolPort: 80,
}},
}},
}},
}
)
// HandleLoadbalancerListSuccessfully sets up the test server to respond to a loadbalancer List request.
func HandleLoadbalancerListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
marker := r.Form.Get("marker")
switch marker {
case "":
fmt.Fprintf(w, LoadbalancersListBody)
case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
fmt.Fprintf(w, `{ "loadbalancers": [] }`)
default:
t.Fatalf("/v2.0/lbaas/loadbalancers invoked with unexpected marker=[%s]", marker)
}
})
}
// HandleLoadbalancerCreationSuccessfully sets up the test server to respond to a loadbalancer creation request
// with a given response.
func HandleLoadbalancerCreationSuccessfully(t *testing.T, response string) {
th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{
"loadbalancer": {
"name": "db_lb",
"vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
"vip_address": "10.30.176.48",
"flavor": "medium",
"provider": "haproxy",
"admin_state_up": true
}
}`)
w.WriteHeader(http.StatusAccepted)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, response)
})
}
// HandleLoadbalancerGetSuccessfully sets up the test server to respond to a loadbalancer Get request.
func HandleLoadbalancerGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, SingleLoadbalancerBody)
})
}
// HandleLoadbalancerGetStatusesTree sets up the test server to respond to a loadbalancer Get statuses tree request.
func HandleLoadbalancerGetStatusesTree(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab/statuses", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, LoadbalancerStatuesesTree)
})
}
// HandleLoadbalancerDeletionSuccessfully sets up the test server to respond to a loadbalancer deletion request.
func HandleLoadbalancerDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
// HandleLoadbalancerUpdateSuccessfully sets up the test server to respond to a loadbalancer Update request.
func HandleLoadbalancerUpdateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestJSONRequest(t, r, `{
"loadbalancer": {
"name": "NewLoadbalancerName"
}
}`)
fmt.Fprintf(w, PostUpdateLoadbalancerBody)
})
}

View File

@ -0,0 +1,248 @@
package loadbalancers
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// AdminState gives users a solid type to work with for create and update
// operations. It is recommended that users use the `Up` and `Down` enums.
type AdminState *bool
type loadbalancerOpts struct {
// Optional. Human-readable name for the Loadbalancer. Does not have to be unique.
Name string
// Optional. Human-readable description for the Loadbalancer.
Description string
// Required. The network on which to allocate the Loadbalancer's address. A tenant can
// only create Loadbalancers on networks authorized by policy (e.g. networks that
// belong to them or networks that are shared).
VipSubnetID string
// Required for admins. The UUID of the tenant who owns the Loadbalancer.
// Only administrative users can specify a tenant UUID other than their own.
TenantID string
// Optional. The IP address of the Loadbalancer.
VipAddress string
// Optional. The administrative state of the Loadbalancer. A valid value is true (UP)
// or false (DOWN).
AdminStateUp *bool
// Optional. The UUID of a flavor.
Flavor string
// Optional. The name of the provider.
Provider string
}
// Convenience vars for AdminStateUp values.
var (
iTrue = true
iFalse = false
Up AdminState = &iTrue
Down AdminState = &iFalse
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToLoadbalancerListQuery() (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 Loadbalancer attributes you want to see returned. SortKey allows you to
// sort by a particular attribute. SortDir sets the direction, and is
// either `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
Description string `q:"description"`
AdminStateUp *bool `q:"admin_state_up"`
TenantID string `q:"tenant_id"`
ProvisioningStatus string `q:"provisioning_status"`
VipAddress string `q:"vip_address"`
VipSubnetID string `q:"vip_subnet_id"`
ID string `q:"id"`
OperatingStatus string `q:"operating_status"`
Name string `q:"name"`
Flavor string `q:"flavor"`
Provider string `q:"provider"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"`
}
// ToLoadbalancerListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToLoadbalancerListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns a Pager which allows you to iterate over a collection of
// routers. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
//
// Default policy settings return only those routers that are owned by the
// tenant who submits the request, unless an admin user submits the request.
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := rootURL(c)
if opts != nil {
query, err := opts.ToLoadbalancerListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return LoadbalancerPage{pagination.LinkedPageBase{PageResult: r}}
})
}
var (
errVipSubnetIDRequried = fmt.Errorf("VipSubnetID is required")
)
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToLoadbalancerCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
// operation.
type CreateOpts loadbalancerOpts
// ToLoadbalancerCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToLoadbalancerCreateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
if opts.VipSubnetID != "" {
l["vip_subnet_id"] = opts.VipSubnetID
} else {
return nil, errVipSubnetIDRequried
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.TenantID != "" {
l["tenant_id"] = opts.TenantID
}
if opts.Description != "" {
l["description"] = opts.Description
}
if opts.VipAddress != "" {
l["vip_address"] = opts.VipAddress
}
if opts.Flavor != "" {
l["flavor"] = opts.Flavor
}
if opts.Provider != "" {
l["provider"] = opts.Provider
}
return map[string]interface{}{"loadbalancer": l}, nil
}
// Create is an operation which provisions a new loadbalancer based on the
// configuration defined in the CreateOpts struct. Once the request is
// validated and progress has started on the provisioning process, a
// CreateResult will be returned.
//
// Users with an admin role can create loadbalancers on behalf of other tenants by
// specifying a TenantID attribute different than their own.
func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
var res CreateResult
reqBody, err := opts.ToLoadbalancerCreateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
return res
}
// Get retrieves a particular Loadbalancer based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
return res
}
// UpdateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Update operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type UpdateOptsBuilder interface {
ToLoadbalancerUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the common options struct used in this package's Update
// operation.
type UpdateOpts loadbalancerOpts
// ToLoadbalancerUpdateMap casts a UpdateOpts struct to a map.
func (opts UpdateOpts) ToLoadbalancerUpdateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.Description != "" {
l["description"] = opts.Description
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
return map[string]interface{}{"loadbalancer": l}, nil
}
// Update is an operation which modifies the attributes of the specified Loadbalancer.
func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToLoadbalancerUpdateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 202},
})
return res
}
// Delete will permanently delete a particular Loadbalancer based on its unique ID.
func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, id), nil)
return res
}
func GetStatuses(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(statusRootURL(c, id), &res.Body, nil)
return res
}

View File

@ -0,0 +1,146 @@
package loadbalancers
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
"github.com/rackspace/gophercloud/pagination"
)
// LoadBalancer is the primary load balancing configuration object that specifies
// the virtual IP address on which client traffic is received, as well
// as other details such as the load balancing method to be use, protocol, etc.
type LoadBalancer struct {
// Human-readable description for the Loadbalancer.
Description string `mapstructure:"description" json:"description"`
// The administrative state of the Loadbalancer. A valid value is true (UP) or false (DOWN).
AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
// Owner of the LoadBalancer. Only an admin user can specify a tenant ID other than its own.
TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
// The provisioning status of the LoadBalancer. This value is ACTIVE, PENDING_CREATE or ERROR.
ProvisioningStatus string `mapstructure:"provisioning_status" json:"provisioning_status"`
// The IP address of the Loadbalancer.
VipAddress string `mapstructure:"vip_address" json:"vip_address"`
// The UUID of the subnet on which to allocate the virtual IP for the Loadbalancer address.
VipSubnetID string `mapstructure:"vip_subnet_id" json:"vip_subnet_id"`
// The unique ID for the LoadBalancer.
ID string `mapstructure:"id" json:"id"`
// The operating status of the LoadBalancer. This value is ONLINE or OFFLINE.
OperatingStatus string `mapstructure:"operating_status" json:"operating_status"`
// Human-readable name for the LoadBalancer. Does not have to be unique.
Name string `mapstructure:"name" json:"name"`
// The UUID of a flavor if set.
Flavor string `mapstructure:"flavor" json:"flavor"`
// The name of the provider.
Provider string `mapstructure:"provider" json:"provider"`
Listeners []listeners.Listener `mapstructure:"listeners" json:"listeners"`
}
type StatusTree struct {
Loadbalancer *LoadBalancer `mapstructure:"loadbalancer" json:"loadbalancer"`
}
// LoadbalancerPage is the page returned by a pager when traversing over a
// collection of routers.
type LoadbalancerPage struct {
pagination.LinkedPageBase
}
// NextPageURL is invoked when a paginated collection of routers 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 (p LoadbalancerPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"loadbalancers_links"`
}
var r resp
err := mapstructure.Decode(p.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a RouterPage struct is empty.
func (p LoadbalancerPage) IsEmpty() (bool, error) {
is, err := ExtractLoadbalancers(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractLoadbalancers accepts a Page struct, specifically a LoadbalancerPage struct,
// and extracts the elements into a slice of LoadBalancer structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractLoadbalancers(page pagination.Page) ([]LoadBalancer, error) {
var resp struct {
LoadBalancers []LoadBalancer `mapstructure:"loadbalancers" json:"loadbalancers"`
}
err := mapstructure.Decode(page.(LoadbalancerPage).Body, &resp)
return resp.LoadBalancers, err
}
type commonResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a router.
func (r commonResult) Extract() (*LoadBalancer, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
LoadBalancer *LoadBalancer `mapstructure:"loadbalancer" json:"loadbalancer"`
}
err := mapstructure.Decode(r.Body, &res)
return res.LoadBalancer, err
}
// Extract is a function that accepts a result and extracts a Loadbalancer.
func (r commonResult) ExtractStatuses() (*StatusTree, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
LoadBalancer *StatusTree `mapstructure:"statuses" json:"statuses"`
}
err := mapstructure.Decode(r.Body, &res)
return res.LoadBalancer, err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,21 @@
package loadbalancers
import "github.com/rackspace/gophercloud"
const (
rootPath = "lbaas"
resourcePath = "loadbalancers"
statusPath = "statuses"
)
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(rootPath, resourcePath, id)
}
func statusRootURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(rootPath, resourcePath, id, statusPath)
}

View File

@ -0,0 +1,216 @@
// +build fixtures
package monitors
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// HealthmonitorsListBody contains the canned body of a healthmonitor list response.
const HealthmonitorsListBody = `
{
"healthmonitors":[
{
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"delay":10,
"name":"web",
"max_retries":1,
"timeout":1,
"type":"PING",
"pools": [{"id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}],
"id":"466c8345-28d8-4f84-a246-e04380b0461d"
},
{
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"delay":5,
"name":"db",
"expected_codes":"200",
"max_retries":2,
"http_method":"GET",
"timeout":2,
"url_path":"/",
"type":"HTTP",
"pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
"id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
}
]
}
`
// SingleHealthmonitorBody is the canned body of a Get request on an existing healthmonitor.
const SingleHealthmonitorBody = `
{
"healthmonitor": {
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"delay":5,
"name":"db",
"expected_codes":"200",
"max_retries":2,
"http_method":"GET",
"timeout":2,
"url_path":"/",
"type":"HTTP",
"pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
"id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
}
}
`
// PostUpdateHealthmonitorBody is the canned response body of a Update request on an existing healthmonitor.
const PostUpdateHealthmonitorBody = `
{
"healthmonitor": {
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"delay":3,
"name":"NewHealthmonitorName",
"expected_codes":"301",
"max_retries":10,
"http_method":"GET",
"timeout":20,
"url_path":"/another_check",
"type":"HTTP",
"pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
"id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
}
}
`
var (
HealthmonitorWeb = Monitor{
AdminStateUp: true,
Name: "web",
TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
Delay: 10,
MaxRetries: 1,
Timeout: 1,
Type: "PING",
ID: "466c8345-28d8-4f84-a246-e04380b0461d",
Pools: []PoolID{{ID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}},
}
HealthmonitorDb = Monitor{
AdminStateUp: true,
Name: "db",
TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
Delay: 5,
ExpectedCodes: "200",
MaxRetries: 2,
Timeout: 2,
URLPath: "/",
Type: "HTTP",
HTTPMethod: "GET",
ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
Pools: []PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
}
HealthmonitorUpdated = Monitor{
AdminStateUp: true,
Name: "NewHealthmonitorName",
TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
Delay: 3,
ExpectedCodes: "301",
MaxRetries: 10,
Timeout: 20,
URLPath: "/another_check",
Type: "HTTP",
HTTPMethod: "GET",
ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
Pools: []PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
}
)
// HandleHealthmonitorListSuccessfully sets up the test server to respond to a healthmonitor List request.
func HandleHealthmonitorListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
marker := r.Form.Get("marker")
switch marker {
case "":
fmt.Fprintf(w, HealthmonitorsListBody)
case "556c8345-28d8-4f84-a246-e04380b0461d":
fmt.Fprintf(w, `{ "healthmonitors": [] }`)
default:
t.Fatalf("/v2.0/lbaas/healthmonitors invoked with unexpected marker=[%s]", marker)
}
})
}
// HandleHealthmonitorCreationSuccessfully sets up the test server to respond to a healthmonitor creation request
// with a given response.
func HandleHealthmonitorCreationSuccessfully(t *testing.T, response string) {
th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{
"healthmonitor": {
"type":"HTTP",
"pool_id":"84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
"tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
"delay":20,
"name":"db",
"timeout":10,
"max_retries":5,
"url_path":"/check",
"expected_codes":"200-299"
}
}`)
w.WriteHeader(http.StatusAccepted)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, response)
})
}
// HandleHealthmonitorGetSuccessfully sets up the test server to respond to a healthmonitor Get request.
func HandleHealthmonitorGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, SingleHealthmonitorBody)
})
}
// HandleHealthmonitorDeletionSuccessfully sets up the test server to respond to a healthmonitor deletion request.
func HandleHealthmonitorDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
// HandleHealthmonitorUpdateSuccessfully sets up the test server to respond to a healthmonitor Update request.
func HandleHealthmonitorUpdateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestJSONRequest(t, r, `{
"healthmonitor": {
"name": "NewHealthmonitorName",
"delay": 3,
"timeout": 20,
"max_retries": 10,
"url_path": "/another_check",
"expected_codes": "301"
}
}`)
fmt.Fprintf(w, PostUpdateHealthmonitorBody)
})
}

View File

@ -0,0 +1,304 @@
package monitors
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type monitorOpts struct {
// Required. The Pool to Monitor.
PoolID string
// Optional. The Name of the Monitor.
Name string
// Required for admins. Indicates the owner of the Loadbalancer.
TenantID string
// Required. The type of probe, which is PING, TCP, HTTP, or HTTPS, that is
// sent by the load balancer to verify the member state.
Type string
// Required. The time, in seconds, between sending probes to members.
Delay int
// Required. Maximum number of seconds for a Monitor to wait for a ping reply
// before it times out. The value must be less than the delay value.
Timeout int
// Required. Number of permissible ping failures before changing the member's
// status to INACTIVE. Must be a number between 1 and 10.
MaxRetries int
// Required for HTTP(S) types. URI path that will be accessed if Monitor type
// is HTTP or HTTPS.
URLPath string
// Required for HTTP(S) types. The HTTP method used for requests by the
// Monitor. If this attribute is not specified, it defaults to "GET".
HTTPMethod string
// Required for HTTP(S) types. Expected HTTP codes for a passing HTTP(S)
// Monitor. You can either specify a single status like "200", or a range
// like "200-202".
ExpectedCodes string
AdminStateUp *bool
}
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToMonitorListQuery() (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 Monitor attributes you want to see returned. SortKey allows you to
// sort by a particular Monitor attribute. SortDir sets the direction, and is
// either `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
ID string `q:"id"`
Name string `q:"name"`
TenantID string `q:"tenant_id"`
PoolID string `q:"pool_id"`
Type string `q:"type"`
Delay int `q:"delay"`
Timeout int `q:"timeout"`
MaxRetries int `q:"max_retries"`
HTTPMethod string `q:"http_method"`
URLPath string `q:"url_path"`
ExpectedCodes string `q:"expected_codes"`
AdminStateUp *bool `q:"admin_state_up"`
Status string `q:"status"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"`
}
// ToMonitorListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToMonitorListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns a Pager which allows you to iterate over a collection of
// health monitors. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
//
// Default policy settings return only those health monitors that are owned by the
// tenant who submits the request, unless an admin user submits the request.
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := rootURL(c)
if opts != nil {
query, err := opts.ToMonitorListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return MonitorPage{pagination.LinkedPageBase{PageResult: r}}
})
}
// Constants that represent approved monitoring types.
const (
TypePING = "PING"
TypeTCP = "TCP"
TypeHTTP = "HTTP"
TypeHTTPS = "HTTPS"
)
var (
errPoolIDRequired = fmt.Errorf("PoolID to monitor is required")
errValidTypeRequired = fmt.Errorf("A valid Type is required. Supported values are PING, TCP, HTTP and HTTPS")
errDelayRequired = fmt.Errorf("Delay is required")
errTimeoutRequired = fmt.Errorf("Timeout is required")
errMaxRetriesRequired = fmt.Errorf("MaxRetries is required")
errURLPathRequired = fmt.Errorf("URL path is required")
errExpectedCodesRequired = fmt.Errorf("ExpectedCodes is required")
errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout")
)
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToMonitorCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
// operation.
type CreateOpts monitorOpts
// ToMonitorCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToMonitorCreateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true}
if allowed[opts.Type] {
l["type"] = opts.Type
} else {
return nil, errValidTypeRequired
}
if opts.Type == TypeHTTP || opts.Type == TypeHTTPS {
if opts.URLPath != "" {
l["url_path"] = opts.URLPath
} else {
return nil, errURLPathRequired
}
if opts.ExpectedCodes != "" {
l["expected_codes"] = opts.ExpectedCodes
} else {
return nil, errExpectedCodesRequired
}
}
if opts.PoolID != "" {
l["pool_id"] = opts.PoolID
} else {
return nil, errPoolIDRequired
}
if opts.Delay != 0 {
l["delay"] = opts.Delay
} else {
return nil, errDelayRequired
}
if opts.Timeout != 0 {
l["timeout"] = opts.Timeout
} else {
return nil, errMaxRetriesRequired
}
if opts.MaxRetries != 0 {
l["max_retries"] = opts.MaxRetries
} else {
return nil, errMaxRetriesRequired
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.TenantID != "" {
l["tenant_id"] = opts.TenantID
}
if opts.HTTPMethod != "" {
l["http_method"] = opts.HTTPMethod
}
return map[string]interface{}{"healthmonitor": l}, nil
}
/*
Create is an operation which provisions a new Health Monitor. There are
different types of Monitor you can provision: PING, TCP or HTTP(S). Below
are examples of how to create each one.
Here is an example config struct to use when creating a PING or TCP Monitor:
CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3}
CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3}
Here is an example config struct to use when creating a HTTP(S) Monitor:
CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3,
HttpMethod: "HEAD", ExpectedCodes: "200", PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"}
*/
func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToMonitorCreateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
return res
}
// Get retrieves a particular Health Monitor based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
return res
}
// UpdateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Update operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type UpdateOptsBuilder interface {
ToMonitorUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the common options struct used in this package's Update
// operation.
type UpdateOpts monitorOpts
// ToMonitorUpdateMap casts a UpdateOpts struct to a map.
func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
if opts.URLPath != "" {
l["url_path"] = opts.URLPath
}
if opts.ExpectedCodes != "" {
l["expected_codes"] = opts.ExpectedCodes
}
if opts.Delay != 0 {
l["delay"] = opts.Delay
}
if opts.Timeout != 0 {
l["timeout"] = opts.Timeout
}
if opts.MaxRetries != 0 {
l["max_retries"] = opts.MaxRetries
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.HTTPMethod != "" {
l["http_method"] = opts.HTTPMethod
}
return map[string]interface{}{"healthmonitor": l}, nil
}
// Update is an operation which modifies the attributes of the specified Monitor.
func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToMonitorUpdateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 202},
})
return res
}
// Delete will permanently delete a particular Monitor based on its unique ID.
func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, id), nil)
return res
}

View File

@ -0,0 +1,160 @@
package monitors
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type PoolID struct {
ID string `mapstructure:"id" json:"id"`
}
// Monitor represents a load balancer health monitor. A health monitor is used
// to determine whether or not back-end members of the VIP's pool are usable
// for processing a request. A pool can have several health monitors associated
// with it. There are different types of health monitors supported:
//
// PING: used to ping the members using ICMP.
// TCP: used to connect to the members using TCP.
// HTTP: used to send an HTTP request to the member.
// HTTPS: used to send a secure HTTP request to the member.
//
// When a pool has several monitors associated with it, each member of the pool
// is monitored by all these monitors. If any monitor declares the member as
// unhealthy, then the member status is changed to INACTIVE and the member
// won't participate in its pool's load balancing. In other words, ALL monitors
// must declare the member to be healthy for it to stay ACTIVE.
type Monitor struct {
// The unique ID for the Monitor.
ID string
// The Name of the Monitor.
Name string
// Only an administrative user can specify a tenant ID
// other than its own.
TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
// The type of probe sent by the load balancer to verify the member state,
// which is PING, TCP, HTTP, or HTTPS.
Type string
// The time, in seconds, between sending probes to members.
Delay int
// The maximum number of seconds for a monitor to wait for a connection to be
// established before it times out. This value must be less than the delay value.
Timeout int
// Number of allowed connection failures before changing the status of the
// member to INACTIVE. A valid value is from 1 to 10.
MaxRetries int `json:"max_retries" mapstructure:"max_retries"`
// The HTTP method that the monitor uses for requests.
HTTPMethod string `json:"http_method" mapstructure:"http_method"`
// The HTTP path of the request sent by the monitor to test the health of a
// member. Must be a string beginning with a forward slash (/).
URLPath string `json:"url_path" mapstructure:"url_path"`
// Expected HTTP codes for a passing HTTP(S) monitor.
ExpectedCodes string `json:"expected_codes" mapstructure:"expected_codes"`
// The administrative state of the health monitor, which is up (true) or down (false).
AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
// The status of the health monitor. Indicates whether the health monitor is
// operational.
Status string
// List of pools that are associated with the health monitor.
Pools []PoolID `mapstructure:"pools" json:"pools"`
}
type Pool struct {
}
// MonitorPage is the page returned by a pager when traversing over a
// collection of health monitors.
type MonitorPage struct {
pagination.LinkedPageBase
}
// NextPageURL is invoked when a paginated collection of monitors 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 (p MonitorPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"healthmonitors_links"`
}
var r resp
err := mapstructure.Decode(p.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a PoolPage struct is empty.
func (p MonitorPage) IsEmpty() (bool, error) {
is, err := ExtractMonitors(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct,
// and extracts the elements into a slice of Monitor structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractMonitors(page pagination.Page) ([]Monitor, error) {
var resp struct {
Monitors []Monitor `mapstructure:"healthmonitors" json:"healthmonitors"`
}
err := mapstructure.Decode(page.(MonitorPage).Body, &resp)
return resp.Monitors, err
}
type commonResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a monitor.
func (r commonResult) Extract() (*Monitor, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Monitor *Monitor `json:"healthmonitor" mapstructure:"healthmonitor"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Monitor, err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,16 @@
package monitors
import "github.com/rackspace/gophercloud"
const (
rootPath = "lbaas"
resourcePath = "healthmonitors"
)
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(rootPath, resourcePath, id)
}

View File

@ -0,0 +1,389 @@
// +build fixtures
package pools
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// PoolsListBody contains the canned body of a pool list response.
const PoolsListBody = `
{
"pools":[
{
"lb_algorithm":"ROUND_ROBIN",
"protocol":"HTTP",
"description":"",
"healthmonitor_id": "466c8345-28d8-4f84-a246-e04380b0461d",
"members":[{"id": "53306cda-815d-4354-9fe4-59e09da9c3c5"}],
"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
"id":"72741b06-df4d-4715-b142-276b6bce75ab",
"name":"web",
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"provider": "haproxy"
},
{
"lb_algorithm":"LEAST_CONNECTION",
"protocol":"HTTP",
"description":"",
"healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
"members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
"id":"c3741b06-df4d-4715-b142-276b6bce75ab",
"name":"db",
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"provider": "haproxy"
}
]
}
`
// SinglePoolBody is the canned body of a Get request on an existing pool.
const SinglePoolBody = `
{
"pool": {
"lb_algorithm":"LEAST_CONNECTION",
"protocol":"HTTP",
"description":"",
"healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
"members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
"id":"c3741b06-df4d-4715-b142-276b6bce75ab",
"name":"db",
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"provider": "haproxy"
}
}
`
// PostUpdatePoolBody is the canned response body of a Update request on an existing pool.
const PostUpdatePoolBody = `
{
"pool": {
"lb_algorithm":"LEAST_CONNECTION",
"protocol":"HTTP",
"description":"",
"healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
"members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
"listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
"loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
"id":"c3741b06-df4d-4715-b142-276b6bce75ab",
"name":"db",
"admin_state_up":true,
"tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
"provider": "haproxy"
}
}
`
var (
PoolWeb = Pool{
LBMethod: "ROUND_ROBIN",
Protocol: "HTTP",
Description: "",
MonitorID: "466c8345-28d8-4f84-a246-e04380b0461d",
TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
AdminStateUp: true,
Name: "web",
Members: []Member{{ID: "53306cda-815d-4354-9fe4-59e09da9c3c5"}},
ID: "72741b06-df4d-4715-b142-276b6bce75ab",
Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
Listeners: []ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
Provider: "haproxy",
}
PoolDb = Pool{
LBMethod: "LEAST_CONNECTION",
Protocol: "HTTP",
Description: "",
MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d",
TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
AdminStateUp: true,
Name: "db",
Members: []Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
ID: "c3741b06-df4d-4715-b142-276b6bce75ab",
Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
Listeners: []ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
Provider: "haproxy",
}
PoolUpdated = Pool{
LBMethod: "LEAST_CONNECTION",
Protocol: "HTTP",
Description: "",
MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d",
TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
AdminStateUp: true,
Name: "db",
Members: []Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
ID: "c3741b06-df4d-4715-b142-276b6bce75ab",
Loadbalancers: []LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
Listeners: []ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
Provider: "haproxy",
}
)
// HandlePoolListSuccessfully sets up the test server to respond to a pool List request.
func HandlePoolListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
marker := r.Form.Get("marker")
switch marker {
case "":
fmt.Fprintf(w, PoolsListBody)
case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
fmt.Fprintf(w, `{ "pools": [] }`)
default:
t.Fatalf("/v2.0/lbaas/pools invoked with unexpected marker=[%s]", marker)
}
})
}
// HandlePoolCreationSuccessfully sets up the test server to respond to a pool creation request
// with a given response.
func HandlePoolCreationSuccessfully(t *testing.T, response string) {
th.Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{
"pool": {
"lb_algorithm": "ROUND_ROBIN",
"protocol": "HTTP",
"name": "Example pool",
"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
"loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab"
}
}`)
w.WriteHeader(http.StatusAccepted)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, response)
})
}
// HandlePoolGetSuccessfully sets up the test server to respond to a pool Get request.
func HandlePoolGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, SinglePoolBody)
})
}
// HandlePoolDeletionSuccessfully sets up the test server to respond to a pool deletion request.
func HandlePoolDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
// HandlePoolUpdateSuccessfully sets up the test server to respond to a pool Update request.
func HandlePoolUpdateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestJSONRequest(t, r, `{
"pool": {
"name": "NewPoolName",
"lb_algorithm": "LEAST_CONNECTIONS"
}
}`)
fmt.Fprintf(w, PostUpdatePoolBody)
})
}
// MembersListBody contains the canned body of a member list response.
const MembersListBody = `
{
"members":[
{
"id": "2a280670-c202-4b0b-a562-34077415aabf",
"address": "10.0.2.10",
"weight": 5,
"name": "web",
"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
"admin_state_up":true,
"protocol_port": 80
},
{
"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
"address": "10.0.2.11",
"weight": 10,
"name": "db",
"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
"admin_state_up":false,
"protocol_port": 80
}
]
}
`
// SingleMemberBody is the canned body of a Get request on an existing member.
const SingleMemberBody = `
{
"member": {
"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
"address": "10.0.2.11",
"weight": 10,
"name": "db",
"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
"admin_state_up":false,
"protocol_port": 80
}
}
`
// PostUpdateMemberBody is the canned response body of a Update request on an existing member.
const PostUpdateMemberBody = `
{
"member": {
"id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
"address": "10.0.2.11",
"weight": 10,
"name": "db",
"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
"admin_state_up":false,
"protocol_port": 80
}
}
`
var (
MemberWeb = Member{
SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
TenantID: "2ffc6e22aae24e4795f87155d24c896f",
AdminStateUp: true,
Name: "web",
ID: "2a280670-c202-4b0b-a562-34077415aabf",
Address: "10.0.2.10",
Weight: 5,
ProtocolPort: 80,
}
MemberDb = Member{
SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
TenantID: "2ffc6e22aae24e4795f87155d24c896f",
AdminStateUp: false,
Name: "db",
ID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
Address: "10.0.2.11",
Weight: 10,
ProtocolPort: 80,
}
MemberUpdated = Member{
SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
TenantID: "2ffc6e22aae24e4795f87155d24c896f",
AdminStateUp: false,
Name: "db",
ID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
Address: "10.0.2.11",
Weight: 10,
ProtocolPort: 80,
}
)
// HandleMemberListSuccessfully sets up the test server to respond to a member List request.
func HandleMemberListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
r.ParseForm()
marker := r.Form.Get("marker")
switch marker {
case "":
fmt.Fprintf(w, MembersListBody)
case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
fmt.Fprintf(w, `{ "members": [] }`)
default:
t.Fatalf("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members invoked with unexpected marker=[%s]", marker)
}
})
}
// HandleMemberCreationSuccessfully sets up the test server to respond to a member creation request
// with a given response.
func HandleMemberCreationSuccessfully(t *testing.T, response string) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestJSONRequest(t, r, `{
"member": {
"address": "10.0.2.11",
"weight": 10,
"name": "db",
"subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
"tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
"protocol_port": 80
}
}`)
w.WriteHeader(http.StatusAccepted)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, response)
})
}
// HandleMemberGetSuccessfully sets up the test server to respond to a member Get request.
func HandleMemberGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
fmt.Fprintf(w, SingleMemberBody)
})
}
// HandleMemberDeletionSuccessfully sets up the test server to respond to a member deletion request.
func HandleMemberDeletionSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
// HandleMemberUpdateSuccessfully sets up the test server to respond to a member Update request.
func HandleMemberUpdateSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestJSONRequest(t, r, `{
"member": {
"name": "newMemberName",
"weight": 4
}
}`)
fmt.Fprintf(w, PostUpdateMemberBody)
})
}

View File

@ -0,0 +1,485 @@
package pools
import (
"fmt"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// AdminState gives users a solid type to work with for create and update
// operations. It is recommended that users use the `Up` and `Down` enums.
type AdminState *bool
type poolOpts struct {
// Only required if the caller has an admin role and wants to create a pool
// for another tenant.
TenantID string
// Optional. Name of the pool.
Name string
// Optional. Human-readable description for the pool.
Description string
// Required. The protocol used by the pool members, you can use either
// ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
Protocol Protocol
// The Loadbalancer on which the members of the pool will be associated with.
// Note: one of LoadbalancerID or ListenerID must be provided.
LoadbalancerID string
// The Listener on which the members of the pool will be associated with.
// Note: one of LoadbalancerID or ListenerID must be provided.
ListenerID string
// Required. The algorithm used to distribute load between the members of the pool. The
// current specification supports LBMethodRoundRobin, LBMethodLeastConnections
// and LBMethodSourceIp as valid values for this attribute.
LBMethod LBMethod
// Optional. Omit this field to prevent session persistence.
Persistence *SessionPersistence
// Optional. The administrative state of the Pool. A valid value is true (UP)
// or false (DOWN).
AdminStateUp *bool
}
// Convenience vars for AdminStateUp values.
var (
iTrue = true
iFalse = false
Up AdminState = &iTrue
Down AdminState = &iFalse
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToPoolListQuery() (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 Pool attributes you want to see returned. SortKey allows you to
// sort by a particular Pool attribute. SortDir sets the direction, and is
// either `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
LBMethod string `q:"lb_algorithm"`
Protocol string `q:"protocol"`
TenantID string `q:"tenant_id"`
AdminStateUp *bool `q:"admin_state_up"`
Name string `q:"name"`
ID string `q:"id"`
LoadbalancerID string `q:"loadbalancer_id"`
ListenerID string `q:"listener_id"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"`
}
// ToPoolListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToPoolListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns a Pager which allows you to iterate over a collection of
// pools. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
//
// Default policy settings return only those pools that are owned by the
// tenant who submits the request, unless an admin user submits the request.
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := rootURL(c)
if opts != nil {
query, err := opts.ToPoolListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return PoolPage{pagination.LinkedPageBase{PageResult: r}}
})
}
type LBMethod string
type Protocol string
// Supported attributes for create/update operations.
const (
LBMethodRoundRobin LBMethod = "ROUND_ROBIN"
LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS"
LBMethodSourceIp LBMethod = "SOURCE_IP"
ProtocolTCP Protocol = "TCP"
ProtocolHTTP Protocol = "HTTP"
ProtocolHTTPS Protocol = "HTTPS"
)
var (
errLoadbalancerOrListenerRequired = fmt.Errorf("A ListenerID or LoadbalancerID is required")
errValidLBMethodRequired = fmt.Errorf("A valid LBMethod is required. Supported values are ROUND_ROBIN, LEAST_CONNECTIONS, SOURCE_IP")
errValidProtocolRequired = fmt.Errorf("A valid Protocol is required. Supported values are TCP, HTTP, HTTPS")
)
// CreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type CreateOptsBuilder interface {
ToPoolCreateMap() (map[string]interface{}, error)
}
// CreateOpts is the common options struct used in this package's Create
// operation.
type CreateOpts poolOpts
// ToPoolCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToPoolCreateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
allowedLBMethod := map[LBMethod]bool{LBMethodRoundRobin: true, LBMethodLeastConnections: true, LBMethodSourceIp: true}
allowedProtocol := map[Protocol]bool{ProtocolTCP: true, ProtocolHTTP: true, ProtocolHTTPS: true}
if allowedLBMethod[opts.LBMethod] {
l["lb_algorithm"] = opts.LBMethod
} else {
return nil, errValidLBMethodRequired
}
if allowedProtocol[opts.Protocol] {
l["protocol"] = opts.Protocol
} else {
return nil, errValidProtocolRequired
}
if opts.LoadbalancerID == "" && opts.ListenerID == "" {
return nil, errLoadbalancerOrListenerRequired
} else {
if opts.LoadbalancerID != "" {
l["loadbalancer_id"] = opts.LoadbalancerID
}
if opts.ListenerID != "" {
l["listener_id"] = opts.ListenerID
}
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.TenantID != "" {
l["tenant_id"] = opts.TenantID
}
if opts.Persistence != nil {
l["session_persistence"] = &opts.Persistence
}
return map[string]interface{}{"pool": l}, nil
}
// Create accepts a CreateOpts struct and uses the values to create a new
// load balancer pool.
func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
var res CreateResult
reqBody, err := opts.ToPoolCreateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Post(rootURL(c), reqBody, &res.Body, nil)
return res
}
// Get retrieves a particular pool based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(resourceURL(c, id), &res.Body, nil)
return res
}
// UpdateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Update operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type UpdateOptsBuilder interface {
ToPoolUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the common options struct used in this package's Update
// operation.
type UpdateOpts poolOpts
// ToPoolUpdateMap casts a UpdateOpts struct to a map.
func (opts UpdateOpts) ToPoolUpdateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
allowedLBMethod := map[LBMethod]bool{LBMethodRoundRobin: true, LBMethodLeastConnections: true, LBMethodSourceIp: true}
if opts.LBMethod != "" {
if allowedLBMethod[opts.LBMethod] {
l["lb_algorithm"] = opts.LBMethod
} else {
return nil, errValidLBMethodRequired
}
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.Description != "" {
l["description"] = opts.Description
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
return map[string]interface{}{"pool": l}, nil
}
// Update allows pools to be updated.
func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToPoolUpdateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Put(resourceURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return res
}
// Delete will permanently delete a particular pool based on its unique ID.
func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(resourceURL(c, id), nil)
return res
}
// CreateOpts contains all the values needed to create a new Member for a Pool.
type memberOpts struct {
// Optional. Name of the Member.
Name string
// Only required if the caller has an admin role and wants to create a Member
// for another tenant.
TenantID string
// Required. The IP address of the member to receive traffic from the load balancer.
Address string
// Required. The port on which to listen for client traffic.
ProtocolPort int
// Optional. A positive integer value that indicates the relative portion of
// traffic that this member should receive from the pool. For example, a
// member with a weight of 10 receives five times as much traffic as a member
// with a weight of 2.
Weight int
// Optional. If you omit this parameter, LBaaS uses the vip_subnet_id
// parameter value for the subnet UUID.
SubnetID string
// Optional. The administrative state of the Pool. A valid value is true (UP)
// or false (DOWN).
AdminStateUp *bool
}
// MemberListOptsBuilder allows extensions to add additional parameters to the
// Member List request.
type MemberListOptsBuilder interface {
ToMemberListQuery() (string, error)
}
// MemberListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to
// the Member attributes you want to see returned. SortKey allows you to
// sort by a particular Member attribute. SortDir sets the direction, and is
// either `asc' or `desc'. Marker and Limit are used for pagination.
type MemberListOpts struct {
Name string `q:"name"`
Weight int `q:"weight"`
AdminStateUp *bool `q:"admin_state_up"`
TenantID string `q:"tenant_id"`
Address string `q:"address"`
ProtocolPort int `q:"protocol_port"`
ID string `q:"id"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"`
}
// ToMemberListQuery formats a ListOpts into a query string.
func (opts MemberListOpts) ToMemberListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// List returns a Pager which allows you to iterate over a collection of
// members. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
//
// Default policy settings return only those members that are owned by the
// tenant who submits the request, unless an admin user submits the request.
func ListAssociateMembers(c *gophercloud.ServiceClient, poolID string, opts MemberListOptsBuilder) pagination.Pager {
url := memberRootURL(c, poolID)
if opts != nil {
query, err := opts.ToMemberListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return MemberPage{pagination.LinkedPageBase{PageResult: r}}
})
}
var (
errPoolIdRequired = fmt.Errorf("PoolID is required")
errAddressRequired = fmt.Errorf("Address is required")
errProtocolPortRequired = fmt.Errorf("ProtocolPort is required")
)
// MemberCreateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Create operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type MemberCreateOptsBuilder interface {
ToMemberCreateMap() (map[string]interface{}, error)
}
// MemberCreateOpts is the common options struct used in this package's Create
// operation.
type MemberCreateOpts memberOpts
// ToMemberCreateMap casts a CreateOpts struct to a map.
func (opts MemberCreateOpts) ToMemberCreateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
if opts.Address != "" {
l["address"] = opts.Address
} else {
return nil, errAddressRequired
}
if opts.ProtocolPort != 0 {
l["protocol_port"] = opts.ProtocolPort
} else {
return nil, errProtocolPortRequired
}
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.TenantID != "" {
l["tenant_id"] = opts.TenantID
}
if opts.SubnetID != "" {
l["subnet_id"] = opts.SubnetID
}
if opts.Weight != 0 {
l["weight"] = opts.Weight
}
return map[string]interface{}{"member": l}, nil
}
// CreateAssociateMember will create and associate a Member with a particular Pool.
func CreateAssociateMember(c *gophercloud.ServiceClient, poolID string, opts MemberCreateOpts) AssociateResult {
var res AssociateResult
if poolID == "" {
res.Err = errPoolIdRequired
return res
}
reqBody, err := opts.ToMemberCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = c.Post(memberRootURL(c, poolID), reqBody, &res.Body, nil)
return res
}
// Get retrieves a particular Pool Member based on its unique ID.
func GetAssociateMember(c *gophercloud.ServiceClient, poolID string, memberID string) GetResult {
var res GetResult
_, res.Err = c.Get(memberResourceURL(c, poolID, memberID), &res.Body, nil)
return res
}
// MemberUpdateOptsBuilder is the interface options structs have to satisfy in order
// to be used in the main Update operation in this package. Since many
// extensions decorate or modify the common logic, it is useful for them to
// satisfy a basic interface in order for them to be used.
type MemberUpdateOptsBuilder interface {
ToMemberUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts is the common options struct used in this package's Update
// operation.
type MemberUpdateOpts memberOpts
// ToMemberUpdateMap casts a UpdateOpts struct to a map.
func (opts MemberUpdateOpts) ToMemberUpdateMap() (map[string]interface{}, error) {
l := make(map[string]interface{})
if opts.AdminStateUp != nil {
l["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
l["name"] = opts.Name
}
if opts.Weight != 0 {
l["weight"] = opts.Weight
}
return map[string]interface{}{"member": l}, nil
}
// Update allows Member to be updated.
func UpdateAssociateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts MemberUpdateOpts) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToMemberUpdateMap()
if err != nil {
res.Err = err
return res
}
// Send request to API
_, res.Err = c.Put(memberResourceURL(c, poolID, memberID), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
return res
}
// DisassociateMember will remove and disassociate a Member from a particular Pool.
func DeleteMember(c *gophercloud.ServiceClient, poolID string, memberID string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(memberResourceURL(c, poolID, memberID), nil)
return res
}

View File

@ -0,0 +1,274 @@
package pools
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
"github.com/rackspace/gophercloud/pagination"
)
// SessionPersistence represents the session persistence feature of the load
// balancing service. It attempts to force connections or requests in the same
// session to be processed by the same member as long as it is ative. Three
// types of persistence are supported:
//
// SOURCE_IP: With this mode, all connections originating from the same source
// IP address, will be handled by the same Member of the Pool.
// HTTP_COOKIE: With this persistence mode, the load balancing function will
// create a cookie on the first request from a client. Subsequent
// requests containing the same cookie value will be handled by
// the same Member of the Pool.
// APP_COOKIE: With this persistence mode, the load balancing function will
// rely on a cookie established by the backend application. All
// requests carrying the same cookie value will be handled by the
// same Member of the Pool.
type SessionPersistence struct {
// The type of persistence mode
Type string `mapstructure:"type" json:"type"`
// Name of cookie if persistence mode is set appropriately
CookieName string `mapstructure:"cookie_name" json:"cookie_name,omitempty"`
}
type LoadBalancerID struct {
ID string `mapstructure:"id" json:"id"`
}
type ListenerID struct {
ID string `mapstructure:"id" json:"id"`
}
// Pool represents a logical set of devices, such as web servers, that you
// group together to receive and process traffic. The load balancing function
// chooses a Member of the Pool according to the configured load balancing
// method to handle the new requests or connections received on the VIP address.
type Pool struct {
// The load-balancer algorithm, which is round-robin, least-connections, and
// so on. This value, which must be supported, is dependent on the provider.
// Round-robin must be supported.
LBMethod string `json:"lb_algorithm" mapstructure:"lb_algorithm"`
// The protocol of the Pool, which is TCP, HTTP, or HTTPS.
Protocol string
// Description for the Pool.
Description string
// A list of listeners objects IDs.
Listeners []ListenerID `mapstructure:"listeners" json:"listeners"` //[]map[string]interface{}
// A list of member objects IDs.
Members []Member `mapstructure:"members" json:"members"`
// The ID of associated health monitor.
MonitorID string `json:"healthmonitor_id" mapstructure:"healthmonitor_id"`
// The network on which the members of the Pool will be located. Only members
// that are on this network can be added to the Pool.
SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
// Owner of the Pool. Only an administrative user can specify a tenant ID
// other than its own.
TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
// The administrative state of the Pool, which is up (true) or down (false).
AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
// Pool name. Does not have to be unique.
Name string
// The unique ID for the Pool.
ID string
// A list of load balancer objects IDs.
Loadbalancers []LoadBalancerID `mapstructure:"loadbalancers" json:"loadbalancers"`
// Indicates whether connections in the same session will be processed by the
// same Pool member or not.
Persistence SessionPersistence `mapstructure:"session_persistence" json:"session_persistence"`
// The provider
Provider string
Monitor monitors.Monitor `mapstructure:"healthmonitor" json:"healthmonitor"`
}
// PoolPage is the page returned by a pager when traversing over a
// collection of pools.
type PoolPage struct {
pagination.LinkedPageBase
}
// NextPageURL is invoked when a paginated collection of pools 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 (p PoolPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"pools_links"`
}
var r resp
err := mapstructure.Decode(p.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a PoolPage struct is empty.
func (p PoolPage) IsEmpty() (bool, error) {
is, err := ExtractPools(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractPools accepts a Page struct, specifically a RouterPage struct,
// and extracts the elements into a slice of Router structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractPools(page pagination.Page) ([]Pool, error) {
var resp struct {
Pools []Pool `mapstructure:"pools" json:"pools"`
}
err := mapstructure.Decode(page.(PoolPage).Body, &resp)
return resp.Pools, err
}
type commonResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a router.
func (r commonResult) Extract() (*Pool, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Pool *Pool `json:"pool"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Pool, err
}
// Member represents the application running on a backend server.
type Member struct {
// Name of the Member.
Name string `json:"name" mapstructure:"name"`
// Weight of Member.
Weight int `json:"weight" mapstructure:"weight"`
// The administrative state of the member, which is up (true) or down (false).
AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
// Owner of the Member. Only an administrative user can specify a tenant ID
// other than its own.
TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
// parameter value for the subnet UUID.
SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
// The Pool to which the Member belongs.
PoolID string `json:"pool_id" mapstructure:"pool_id"`
// The IP address of the Member.
Address string `json:"address" mapstructure:"address"`
// The port on which the application is hosted.
ProtocolPort int `json:"protocol_port" mapstructure:"protocol_port"`
// The unique ID for the Member.
ID string
}
// MemberPage is the page returned by a pager when traversing over a
// collection of Members in a Pool.
type MemberPage struct {
pagination.LinkedPageBase
}
// NextPageURL is invoked when a paginated collection of members 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 (p MemberPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"members_links"`
}
var r resp
err := mapstructure.Decode(p.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a MemberPage struct is empty.
func (p MemberPage) IsEmpty() (bool, error) {
is, err := ExtractMembers(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractMembers accepts a Page struct, specifically a RouterPage struct,
// and extracts the elements into a slice of Router structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractMembers(page pagination.Page) ([]Member, error) {
var resp struct {
Member []Member `mapstructure:"members" json:"members"`
}
err := mapstructure.Decode(page.(MemberPage).Body, &resp)
return resp.Member, err
}
// ExtractMember is a function that accepts a result and extracts a router.
func (r commonResult) ExtractMember() (*Member, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Member *Member `json:"member"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Member, err
}
// CreateResult represents the result of a create operation.
type CreateResult struct {
commonResult
}
// GetResult represents the result of a get operation.
type GetResult struct {
commonResult
}
// UpdateResult represents the result of an update operation.
type UpdateResult struct {
commonResult
}
// DeleteResult represents the result of a delete operation.
type DeleteResult struct {
gophercloud.ErrResult
}
// AssociateResult represents the result of an association operation.
type AssociateResult struct {
commonResult
}

View File

@ -0,0 +1,25 @@
package pools
import "github.com/rackspace/gophercloud"
const (
rootPath = "lbaas"
resourcePath = "pools"
memberPath = "members"
)
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(rootPath, resourcePath)
}
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(rootPath, resourcePath, id)
}
func memberRootURL(c *gophercloud.ServiceClient, poolId string) string {
return c.ServiceURL(rootPath, resourcePath, poolId, memberPath)
}
func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memeberID string) string {
return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memeberID)
}

View File

@ -125,6 +125,24 @@ func HandleCreateTextObjectSuccessfully(t *testing.T, content string) {
})
}
// HandleCreateTextWithCacheControlSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler
// mux that responds with a `Create` response. A Cache-Control of `max-age="3600", public` is expected.
func HandleCreateTextWithCacheControlSuccessfully(t *testing.T, content string) {
th.Mux.HandleFunc("/testContainer/testObject", 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, "Cache-Control", `max-age="3600", public`)
th.TestHeader(t, r, "Accept", "application/json")
hash := md5.New()
io.WriteString(hash, content)
localChecksum := hash.Sum(nil)
w.Header().Set("ETag", fmt.Sprintf("%x", localChecksum))
w.WriteHeader(http.StatusCreated)
})
}
// HandleCreateTypelessObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler
// mux that responds with a `Create` response. No Content-Type header may be present in the request, so that server-
// side content-type detection will be triggered properly.

View File

@ -155,6 +155,7 @@ type CreateOptsBuilder interface {
// CreateOpts is a structure that holds parameters for creating an object.
type CreateOpts struct {
Metadata map[string]string
CacheControl string `h:"Cache-Control"`
ContentDisposition string `h:"Content-Disposition"`
ContentEncoding string `h:"Content-Encoding"`
ContentLength int64 `h:"Content-Length"`

532
vendor/vendor.json vendored
View File

@ -1068,279 +1068,6 @@
"path": "github.com/pkg/errors",
"revision": "42fa80f2ac6ed17a977ce826074bd3009593fa9d"
},
{
"checksumSHA1": "4Q/JrLYYcwGRPVpd6sJ5yW/E9GQ=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "gltXWIbMCLQ27gCSv9VaS3HpAhk=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "gmzAuay4zJhVck2kZaPgKGZ8rLU=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "YDsUjMTIX7JIquqMdB2rpNvOYEw=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "lxRP3M3GUh35Bb+f8kP/fByBWBo=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "kCkaqHFDBuZem57TEmfCpCNeUWs=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "A8CBSjxtNeXBtM3M51qqs5FMVNE=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "TK3yB7n99AUof19ej+61dcgeEvo=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "CPt0Oy9AuqIpvS+dE4Mktb5Ia60=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "uhJIkUvunG8D06eKjdILg+w7A/k=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "3TlySPDYsQjLkwHzvWf+bEoeKRU=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "QbXm7h4qu7+A/D6y40XHN4/QUOg=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "C2K3kYQgwa65cVRvWrNZACQFX7Q=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/flavors",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "N7MHp4Vh095Q0zSJD3/ZgHWTHAU=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/images",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "+GkNX1ZNVOdbevUkfJUt/xGPZU0=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/servers",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "kBNUmTmGZgMc7RA8b/MfF8mNLrw=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/identity/v2/tenants",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "QWhBGvvt47Wpw4qf3bt/rVgXdyw=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/identity/v2/tokens",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "MKnoVPcRvTrooZtNQwOnJy0bglc=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/identity/v3/tokens",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "iv6vxAhsrWtAQ2JMYh2ZxuP6yUY=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "Ov2A4adPjxO4PIcyYxnMSs7CmE0=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "w2TnbyEXzN8im5gVbcJDaNdGUdM=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "auiGgiAaicHX8JcNN+CTEEs1mgQ=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "AbS0T9msZuTFmON0HecmnwVAaUI=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "yNQNbZAHt4Y4lX5b3oFuoJ7Xpi4=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "zzzNS1HlIoHBjiNT6eOccUxyUDg=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "QSSFu2FGtwTQPeuKHIfwcidfVqM=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "Cnh0CpIejol279pKdR8RjE3s8p4=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "xUTTsfCAWNhd0LA9Jj14hCEhq8Y=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "geb6kqfYLhI4jxaUjyjtrZxwOfw=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "LuEBbhG7jYZ6P7yUASISjs6tboE=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/networks",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "3dwh+/nX7iknJ0k8B4MnxvQWAYw=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/ports",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "6aSXoX7T+hfPeS9W1pFV8eO9GFM=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/subnets",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "SVLhcb5xn1rL88OuZixL9tAFvfc=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "jFKg8AdD75nOKzeYmKUz6doqxU0=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "HeAr0Ae/k5vrDPfE1rI4m7eN6lU=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "e4ussrqujJHizWbdBaLjIpWYmgY=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/openstack/utils",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "YZ3h9gY2+9Cc11grugfLr9JASyU=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/pagination",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "U2yzK8GFNeHZqcoeotcHmK57lAI=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/testhelper",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"checksumSHA1": "fXtvTbQBML8QLu/qpD9sAt53J00=",
"comment": "v1.0.0-905-gadc2065",
"path": "github.com/rackspace/gophercloud/testhelper/client",
"revision": "67139b9485d6fd682c5314e963b0915e18f7947a",
"revisionTime": "2016-06-01T11:32:05Z"
},
{
"path": "github.com/rainycape/unidecode",
"revision": "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c"
@ -1598,6 +1325,265 @@
"comment": "v1.8.5",
"path": "gopkg.in/ini.v1",
"revision": "77178f22699a4ecafce485fb8d86b7afeb7e3e28"
},
{
"checksumSHA1": "9y6a4i+MNtKDGCbRLtZUCCgra5o=",
"path": "github.com/rackspace/gophercloud",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "NGUTP3ljMt7yrbec44cS1snPtTo=",
"path": "github.com/rackspace/gophercloud/openstack",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "KXOy+EDMJgyZT0MoTXMhcFZVgNM=",
"path": "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "uh4DrOO+91jmxNlamBoloURUPO0=",
"path": "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes/testing",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "ptDng4MELpgmyMbKx7q46euXYck=",
"path": "github.com/rackspace/gophercloud/openstack/blockstorage/v2/volumes",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "GCDblO2RKSQRRQQFCUPVG3Okjg8=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "Jpju6sEOCeMRL9SP+zy86ydhPDo=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "I9QKv1AKFpSzX/O3KT6BSFat+tk=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "mA4UpOZ0dWxMKkzZqSJ3yzmDGng=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/schedulerhints",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "foERSFhAuTWCXRL11GUOX+uWiaI=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "uNrf4iwP1ABn2ArI1mUS7jwnTQo=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/servergroups",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "rHEOEAm10HDsfBLU8FqKSUdwqFY=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/tenantnetworks",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "yMt7J+JXn8fC/b8qQLyeJYK6Kyc=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "/CUwRPw3GTR4kDDyhlevEJOz8VM=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach/testing",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "evzUqx0LmT1wg1XLJBvfaDhb6Pc=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/flavors",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "il+znRRlkKOSfP+hxrz+0IAb2FU=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/images",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "LMxGZMRGX1MK7qaPmamj7cDJLbU=",
"path": "github.com/rackspace/gophercloud/openstack/compute/v2/servers",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "gJg3QKHt4wCjM5kUjUZnXGlL57c=",
"path": "github.com/rackspace/gophercloud/openstack/identity/v2/tenants",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "6R8yUmY2FsIYn5rjvBEx4eZuVfQ=",
"path": "github.com/rackspace/gophercloud/openstack/identity/v2/tokens",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "mGcd8xg4aongnWHTgjR7kkbyOeQ=",
"path": "github.com/rackspace/gophercloud/openstack/identity/v3/tokens",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "EMdUpMYV9vQAEtBkpUdSqYaYBVc=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/common",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "CIMIys+Dh/kkLj4i+Xyn7CUDTyM=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "Dq/QBMYHUlISQXYrDCltTaF2HKU=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "NppejKsGwws2GmxajE6ZZVuP61M=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "2zGAc21qrvbg0BWMqb6ADhiIJvc=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "N1I73FBojC4D1kiASMHBLhYNV5A=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "qqJpW4mn6W25zzaXgVS9FeRYmlU=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "YaxzH3aa+FSPo7kt5wVxJfKFE04=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "61RDejBCkPnueotbY/2uVzCPt/Y=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "dD5y2ImtufAHjyVFdShtBhtAWkA=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "fChoykh7X7OjiV29nV+pl3k6y3Y=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z",
"tree": true
},
{
"checksumSHA1": "VlAkiJAb6yIWthZpDM/hNXBzmo4=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "CKBV6wraj8j/fwkUllbry6kx/TE=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "3p4PfOfnJ9ZpqpbkgP2hpe/ZhCs=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/networks",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "UGPFc7aRab78XkJp9/c0MUfVy3I=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/ports",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "23UqLsJHksZiopcSwWmkrX3e0CY=",
"path": "github.com/rackspace/gophercloud/openstack/networking/v2/subnets",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "HecTWfPTprklkbLV5wOycG/PiGA=",
"path": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/accounts",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "bCETKrDen/3DNnzvR0fTr91Sq+k=",
"path": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "gaOb5FwWD06v8fAyHqcejyD7wHY=",
"path": "github.com/rackspace/gophercloud/openstack/objectstorage/v1/objects",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "WkgQ5ZIv6/KQlNfweJd+889nWPk=",
"path": "github.com/rackspace/gophercloud/openstack/utils",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "6YVL0nw4z/n45Mx2SXTsQ8N1LYk=",
"path": "github.com/rackspace/gophercloud/pagination",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "U2yzK8GFNeHZqcoeotcHmK57lAI=",
"path": "github.com/rackspace/gophercloud/testhelper",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
},
{
"checksumSHA1": "fXtvTbQBML8QLu/qpD9sAt53J00=",
"path": "github.com/rackspace/gophercloud/testhelper/client",
"revision": "d47105ce4ef90cea9a14b85c8dd172b760085828",
"revisionTime": "2016-06-03T22:34:01Z"
}
],
"rootPath": "github.com/hashicorp/terraform"