Merge pull request #25987 from hpcloud/openstack-lbaas-v2

Automatic merge from submit-queue

LBaaS v2 Support for Openstack Cloud Provider Plugin

Resolves #19774.

This work is based on Gophercloud support for LBaaS v2 currently in review (this will have to merge first):
https://github.com/rackspace/gophercloud/pull/575

These changes includes the addition of a new loadbalancer configuration option:  **LBVersion**.  If this configuration attribute is missing or anything other than "v2", lbaas v1 implementation will be used.
This commit is contained in:
k8s-merge-robot 2016-06-09 18:32:35 -07:00 committed by GitHub
commit cbde2ec8c2
66 changed files with 5847 additions and 487 deletions

129
Godeps/Godeps.json generated
View File

@ -1615,133 +1615,158 @@
},
{
"ImportPath": "github.com/rackspace/gophercloud",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/bootfromvolume",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/diskconfig",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/flavors",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/images",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/compute/v2/servers",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/identity/v2/tenants",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/identity/v2/tokens",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/identity/v3/tokens",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners",
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers",
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors",
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools",
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/networking/v2/ports",
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/openstack/utils",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/pagination",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/rackspace",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/rackspace/blockstorage/v1/volumes",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/rackspace/compute/v2/servers",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/rackspace/compute/v2/volumeattach",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/rackspace/identity/v2/tokens",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/testhelper",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/rackspace/gophercloud/testhelper/client",
"Comment": "v1.0.0-842-g8992d74",
"Rev": "8992d7483a06748dea706e4716d042a4a9e73918"
"Comment": "v1.0.0-920-g934dbf8",
"Rev": "934dbf81977c67c521c75492dc1f55ca74dc5b04"
},
{
"ImportPath": "github.com/robfig/cron",

995
Godeps/LICENSES generated

File diff suppressed because it is too large Load Diff

View File

@ -28,24 +28,19 @@ import (
"strings"
"time"
"gopkg.in/gcfg.v1"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack"
"github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
"github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/rackspace/gophercloud/openstack/compute/v2/flavors"
"github.com/rackspace/gophercloud/openstack/compute/v2/servers"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
"github.com/rackspace/gophercloud/pagination"
"gopkg.in/gcfg.v1"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/resource"
"k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/cloudprovider"
)
@ -81,8 +76,15 @@ func (d *MyDuration) UnmarshalText(text []byte) error {
return nil
}
type LoadBalancer struct {
network *gophercloud.ServiceClient
compute *gophercloud.ServiceClient
opts LoadBalancerOpts
}
type LoadBalancerOpts struct {
SubnetId string `gcfg:"subnet-id"` // required
LBVersion string `gcfg:"lb-version"` // v1 or v2
SubnetId string `gcfg:"subnet-id"` // required
FloatingNetworkId string `gcfg:"floating-network-id"`
LBMethod string `gcfg:"lb-method"`
CreateMonitor bool `gcfg:"create-monitor"`
@ -504,12 +506,6 @@ func (os *OpenStack) ScrubDNS(nameservers, searches []string) (nsOut, srchOut []
return nameservers, searches
}
type LoadBalancer struct {
network *gophercloud.ServiceClient
compute *gophercloud.ServiceClient
opts LoadBalancerOpts
}
func (os *OpenStack) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
glog.V(4).Info("openstack.LoadBalancer() called")
@ -532,7 +528,12 @@ func (os *OpenStack) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
glog.V(1).Info("Claiming to support LoadBalancer")
return &LoadBalancer{network, compute, os.lbOpts}, true
if os.lbOpts.LBVersion == "v2" {
return &LbaasV2{LoadBalancer{network, compute, os.lbOpts}}, true
} else {
return &LbaasV1{LoadBalancer{network, compute, os.lbOpts}}, true
}
}
func isNotFound(err error) bool {
@ -540,412 +541,6 @@ func isNotFound(err error) bool {
return ok && e.Actual == http.StatusNotFound
}
func getPoolByName(client *gophercloud.ServiceClient, name string) (*pools.Pool, error) {
opts := pools.ListOpts{
Name: name,
}
pager := pools.List(client, opts)
poolList := make([]pools.Pool, 0, 1)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
p, err := pools.ExtractPools(page)
if err != nil {
return false, err
}
poolList = append(poolList, p...)
if len(poolList) > 1 {
return false, ErrMultipleResults
}
return true, nil
})
if err != nil {
if isNotFound(err) {
return nil, ErrNotFound
}
return nil, err
}
if len(poolList) == 0 {
return nil, ErrNotFound
} else if len(poolList) > 1 {
return nil, ErrMultipleResults
}
return &poolList[0], nil
}
func getVipByName(client *gophercloud.ServiceClient, name string) (*vips.VirtualIP, error) {
opts := vips.ListOpts{
Name: name,
}
pager := vips.List(client, opts)
vipList := make([]vips.VirtualIP, 0, 1)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
v, err := vips.ExtractVIPs(page)
if err != nil {
return false, err
}
vipList = append(vipList, v...)
if len(vipList) > 1 {
return false, ErrMultipleResults
}
return true, nil
})
if err != nil {
if isNotFound(err) {
return nil, ErrNotFound
}
return nil, err
}
if len(vipList) == 0 {
return nil, ErrNotFound
} else if len(vipList) > 1 {
return nil, ErrMultipleResults
}
return &vipList[0], nil
}
func getFloatingIPByPortID(client *gophercloud.ServiceClient, portID string) (*floatingips.FloatingIP, error) {
opts := floatingips.ListOpts{
PortID: portID,
}
pager := floatingips.List(client, opts)
floatingIPList := make([]floatingips.FloatingIP, 0, 1)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
f, err := floatingips.ExtractFloatingIPs(page)
if err != nil {
return false, err
}
floatingIPList = append(floatingIPList, f...)
if len(floatingIPList) > 1 {
return false, ErrMultipleResults
}
return true, nil
})
if err != nil {
if isNotFound(err) {
return nil, ErrNotFound
}
return nil, err
}
if len(floatingIPList) == 0 {
return nil, ErrNotFound
} else if len(floatingIPList) > 1 {
return nil, ErrMultipleResults
}
return &floatingIPList[0], nil
}
func (lb *LoadBalancer) GetLoadBalancer(service *api.Service) (*api.LoadBalancerStatus, bool, error) {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
vip, err := getVipByName(lb.network, loadBalancerName)
if err == ErrNotFound {
return nil, false, nil
}
if vip == nil {
return nil, false, err
}
status := &api.LoadBalancerStatus{}
status.Ingress = []api.LoadBalancerIngress{{IP: vip.Address}}
return status, true, err
}
// TODO: This code currently ignores 'region' and always creates a
// loadbalancer in only the current OpenStack region. We should take
// a list of regions (from config) and query/create loadbalancers in
// each region.
func (lb *LoadBalancer) EnsureLoadBalancer(apiService *api.Service, hosts []string) (*api.LoadBalancerStatus, error) {
glog.V(4).Infof("EnsureLoadBalancer(%v, %v, %v, %v, %v, %v)", apiService.Namespace, apiService.Name, apiService.Spec.LoadBalancerIP, apiService.Spec.Ports, hosts, apiService.Annotations)
ports := apiService.Spec.Ports
if len(ports) > 1 {
return nil, fmt.Errorf("multiple ports are not yet supported in openstack load balancers")
} else if len(ports) == 0 {
return nil, fmt.Errorf("no ports provided to openstack load balancer")
}
// The service controller verified all the protocols match on the ports, just check and use the first one
// TODO: Convert all error messages to use an event recorder
if ports[0].Protocol != api.ProtocolTCP {
return nil, fmt.Errorf("Only TCP LoadBalancer is supported for openstack load balancers")
}
affinity := apiService.Spec.SessionAffinity
var persistence *vips.SessionPersistence
switch affinity {
case api.ServiceAffinityNone:
persistence = nil
case api.ServiceAffinityClientIP:
persistence = &vips.SessionPersistence{Type: "SOURCE_IP"}
default:
return nil, fmt.Errorf("unsupported load balancer affinity: %v", affinity)
}
sourceRanges, err := service.GetLoadBalancerSourceRanges(apiService)
if err != nil {
return nil, err
}
if !service.IsAllowAll(sourceRanges) {
return nil, fmt.Errorf("Source range restrictions are not supported for openstack load balancers")
}
glog.V(2).Infof("Checking if openstack load balancer already exists: %s", cloudprovider.GetLoadBalancerName(apiService))
_, exists, err := lb.GetLoadBalancer(apiService)
if err != nil {
return nil, fmt.Errorf("error checking if openstack load balancer already exists: %v", err)
}
// TODO: Implement a more efficient update strategy for common changes than delete & create
// In particular, if we implement hosts update, we can get rid of UpdateHosts
if exists {
err := lb.EnsureLoadBalancerDeleted(apiService)
if err != nil {
return nil, fmt.Errorf("error deleting existing openstack load balancer: %v", err)
}
}
lbmethod := lb.opts.LBMethod
if lbmethod == "" {
lbmethod = pools.LBMethodRoundRobin
}
name := cloudprovider.GetLoadBalancerName(apiService)
pool, err := pools.Create(lb.network, pools.CreateOpts{
Name: name,
Protocol: pools.ProtocolTCP,
SubnetID: lb.opts.SubnetId,
LBMethod: lbmethod,
}).Extract()
if err != nil {
return nil, err
}
for _, host := range hosts {
addr, err := getAddressByName(lb.compute, host)
if err != nil {
return nil, err
}
_, err = members.Create(lb.network, members.CreateOpts{
PoolID: pool.ID,
ProtocolPort: int(ports[0].NodePort), //TODO: need to handle multi-port
Address: addr,
}).Extract()
if err != nil {
pools.Delete(lb.network, pool.ID)
return nil, err
}
}
var mon *monitors.Monitor
if lb.opts.CreateMonitor {
mon, err = monitors.Create(lb.network, monitors.CreateOpts{
Type: monitors.TypeTCP,
Delay: int(lb.opts.MonitorDelay.Duration.Seconds()),
Timeout: int(lb.opts.MonitorTimeout.Duration.Seconds()),
MaxRetries: int(lb.opts.MonitorMaxRetries),
}).Extract()
if err != nil {
pools.Delete(lb.network, pool.ID)
return nil, err
}
_, err = pools.AssociateMonitor(lb.network, pool.ID, mon.ID).Extract()
if err != nil {
monitors.Delete(lb.network, mon.ID)
pools.Delete(lb.network, pool.ID)
return nil, err
}
}
createOpts := vips.CreateOpts{
Name: name,
Description: fmt.Sprintf("Kubernetes external service %s", name),
Protocol: "TCP",
ProtocolPort: int(ports[0].Port), //TODO: need to handle multi-port
PoolID: pool.ID,
SubnetID: lb.opts.SubnetId,
Persistence: persistence,
}
loadBalancerIP := apiService.Spec.LoadBalancerIP
if loadBalancerIP != "" {
createOpts.Address = loadBalancerIP
}
vip, err := vips.Create(lb.network, createOpts).Extract()
if err != nil {
if mon != nil {
monitors.Delete(lb.network, mon.ID)
}
pools.Delete(lb.network, pool.ID)
return nil, err
}
status := &api.LoadBalancerStatus{}
status.Ingress = []api.LoadBalancerIngress{{IP: vip.Address}}
if lb.opts.FloatingNetworkId != "" {
floatIPOpts := floatingips.CreateOpts{
FloatingNetworkID: lb.opts.FloatingNetworkId,
PortID: vip.PortID,
}
floatIP, err := floatingips.Create(lb.network, floatIPOpts).Extract()
if err != nil {
return nil, err
}
status.Ingress = append(status.Ingress, api.LoadBalancerIngress{IP: floatIP.FloatingIP})
}
return status, nil
}
func (lb *LoadBalancer) UpdateLoadBalancer(service *api.Service, hosts []string) error {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
glog.V(4).Infof("UpdateLoadBalancer(%v, %v)", loadBalancerName, hosts)
vip, err := getVipByName(lb.network, loadBalancerName)
if err != nil {
return err
}
// Set of member (addresses) that _should_ exist
addrs := map[string]bool{}
for _, host := range hosts {
addr, err := getAddressByName(lb.compute, host)
if err != nil {
return err
}
addrs[addr] = true
}
// Iterate over members that _do_ exist
pager := members.List(lb.network, members.ListOpts{PoolID: vip.PoolID})
err = pager.EachPage(func(page pagination.Page) (bool, error) {
memList, err := members.ExtractMembers(page)
if err != nil {
return false, err
}
for _, member := range memList {
if _, found := addrs[member.Address]; found {
// Member already exists
delete(addrs, member.Address)
} else {
// Member needs to be deleted
err = members.Delete(lb.network, member.ID).ExtractErr()
if err != nil {
return false, err
}
}
}
return true, nil
})
if err != nil {
return err
}
// Anything left in addrs is a new member that needs to be added
for addr := range addrs {
_, err := members.Create(lb.network, members.CreateOpts{
PoolID: vip.PoolID,
Address: addr,
ProtocolPort: vip.ProtocolPort,
}).Extract()
if err != nil {
return err
}
}
return nil
}
func (lb *LoadBalancer) EnsureLoadBalancerDeleted(service *api.Service) error {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
glog.V(4).Infof("EnsureLoadBalancerDeleted(%v)", loadBalancerName)
vip, err := getVipByName(lb.network, loadBalancerName)
if err != nil && err != ErrNotFound {
return err
}
if lb.opts.FloatingNetworkId != "" && vip != nil {
floatingIP, err := getFloatingIPByPortID(lb.network, vip.PortID)
if err != nil && !isNotFound(err) {
return err
}
if floatingIP != nil {
err = floatingips.Delete(lb.network, floatingIP.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
}
// We have to delete the VIP before the pool can be deleted,
// so no point continuing if this fails.
if vip != nil {
err := vips.Delete(lb.network, vip.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
var pool *pools.Pool
if vip != nil {
pool, err = pools.Get(lb.network, vip.PoolID).Extract()
if err != nil && !isNotFound(err) {
return err
}
} else {
// The VIP is gone, but it is conceivable that a Pool
// still exists that we failed to delete on some
// previous occasion. Make a best effort attempt to
// cleanup any pools with the same name as the VIP.
pool, err = getPoolByName(lb.network, service.Name)
if err != nil && err != ErrNotFound {
return err
}
}
if pool != nil {
for _, monId := range pool.MonitorIDs {
_, err = pools.DisassociateMonitor(lb.network, pool.ID, monId).Extract()
if err != nil {
return err
}
err = monitors.Delete(lb.network, monId).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
err = pools.Delete(lb.network, pool.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
return nil
}
func (os *OpenStack) Zones() (cloudprovider.Zones, bool) {
glog.V(1).Info("Claiming to support Zones")

View File

@ -0,0 +1,993 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openstack
import (
"time"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners"
"github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers"
v2_monitors "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors"
v2_pools "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools"
neutron_ports "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
"github.com/rackspace/gophercloud/pagination"
"fmt"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/cloudprovider"
)
// Note: when creating a new Loadbalancer (VM), it can take some time before it is ready for use,
// this timeout is used for waiting until the Loadbalancer provisioning status goes to ACTIVE state.
const loadbalancerActiveTimeoutSeconds = 120
const loadbalancerDeleteTimeoutSeconds = 30
// LoadBalancer implementation for LBaaS v1
type LbaasV1 struct {
LoadBalancer
}
// LoadBalancer implementation for LBaaS v2
type LbaasV2 struct {
LoadBalancer
}
func getPortIDByIP(client *gophercloud.ServiceClient, ipAddress string) (string, error) {
var portID string
err := neutron_ports.List(client, neutron_ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
portList, err := neutron_ports.ExtractPorts(page)
if err != nil {
return false, err
}
for _, port := range portList {
for _, ip := range port.FixedIPs {
if ip.IPAddress == ipAddress {
portID = port.ID
return false, nil
}
}
}
return true, nil
})
return portID, err
}
func getFloatingIPByPortID(client *gophercloud.ServiceClient, portID string) (*floatingips.FloatingIP, error) {
opts := floatingips.ListOpts{
PortID: portID,
}
pager := floatingips.List(client, opts)
floatingIPList := make([]floatingips.FloatingIP, 0, 1)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
f, err := floatingips.ExtractFloatingIPs(page)
if err != nil {
return false, err
}
floatingIPList = append(floatingIPList, f...)
if len(floatingIPList) > 1 {
return false, ErrMultipleResults
}
return true, nil
})
if err != nil {
if isNotFound(err) {
return nil, ErrNotFound
}
return nil, err
}
if len(floatingIPList) == 0 {
return nil, ErrNotFound
} else if len(floatingIPList) > 1 {
return nil, ErrMultipleResults
}
return &floatingIPList[0], nil
}
func getPoolByName(client *gophercloud.ServiceClient, name string) (*pools.Pool, error) {
opts := pools.ListOpts{
Name: name,
}
pager := pools.List(client, opts)
poolList := make([]pools.Pool, 0, 1)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
p, err := pools.ExtractPools(page)
if err != nil {
return false, err
}
poolList = append(poolList, p...)
if len(poolList) > 1 {
return false, ErrMultipleResults
}
return true, nil
})
if err != nil {
if isNotFound(err) {
return nil, ErrNotFound
}
return nil, err
}
if len(poolList) == 0 {
return nil, ErrNotFound
} else if len(poolList) > 1 {
return nil, ErrMultipleResults
}
return &poolList[0], nil
}
func getVipByName(client *gophercloud.ServiceClient, name string) (*vips.VirtualIP, error) {
opts := vips.ListOpts{
Name: name,
}
pager := vips.List(client, opts)
vipList := make([]vips.VirtualIP, 0, 1)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
v, err := vips.ExtractVIPs(page)
if err != nil {
return false, err
}
vipList = append(vipList, v...)
if len(vipList) > 1 {
return false, ErrMultipleResults
}
return true, nil
})
if err != nil {
if isNotFound(err) {
return nil, ErrNotFound
}
return nil, err
}
if len(vipList) == 0 {
return nil, ErrNotFound
} else if len(vipList) > 1 {
return nil, ErrMultipleResults
}
return &vipList[0], nil
}
func getLoadbalancerByName(client *gophercloud.ServiceClient, name string) (*loadbalancers.LoadBalancer, error) {
opts := loadbalancers.ListOpts{
Name: name,
}
pager := loadbalancers.List(client, opts)
loadbalancerList := make([]loadbalancers.LoadBalancer, 0, 1)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
v, err := loadbalancers.ExtractLoadbalancers(page)
if err != nil {
return false, err
}
loadbalancerList = append(loadbalancerList, v...)
if len(loadbalancerList) > 1 {
return false, ErrMultipleResults
}
return true, nil
})
if err != nil {
if isNotFound(err) {
return nil, ErrNotFound
}
return nil, err
}
if len(loadbalancerList) == 0 {
return nil, ErrNotFound
} else if len(loadbalancerList) > 1 {
return nil, ErrMultipleResults
}
return &loadbalancerList[0], nil
}
func waitLoadbalancerActiveProvisioningStatus(client *gophercloud.ServiceClient, loadbalancerID string) error {
start := time.Now().Second()
for {
loadbalancer, err := loadbalancers.Get(client, loadbalancerID).Extract()
if err != nil {
return err
}
if loadbalancer.ProvisioningStatus == "ACTIVE" {
return nil
}
time.Sleep(1 * time.Second)
if time.Now().Second()-start >= loadbalancerActiveTimeoutSeconds {
return fmt.Errorf("Loadbalancer failed to go into ACTIVE provisioning status within alloted time")
}
}
}
func waitLoadbalancerDeleted(client *gophercloud.ServiceClient, loadbalancerID string) error {
start := time.Now().Second()
for {
_, err := loadbalancers.Get(client, loadbalancerID).Extract()
if err != nil {
if err == ErrNotFound {
return nil
} else {
return err
}
}
time.Sleep(1 * time.Second)
if time.Now().Second()-start >= loadbalancerDeleteTimeoutSeconds {
return fmt.Errorf("Loadbalancer failed to delete within the alloted time")
}
}
}
func (lbaas *LbaasV2) GetLoadBalancer(service *api.Service) (*api.LoadBalancerStatus, bool, error) {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
loadbalancer, err := getLoadbalancerByName(lbaas.network, loadBalancerName)
if err == ErrNotFound {
return nil, false, nil
}
if loadbalancer == nil {
return nil, false, err
}
status := &api.LoadBalancerStatus{}
status.Ingress = []api.LoadBalancerIngress{{IP: loadbalancer.VipAddress}}
return status, true, err
}
// TODO: This code currently ignores 'region' and always creates a
// loadbalancer in only the current OpenStack region. We should take
// a list of regions (from config) and query/create loadbalancers in
// each region.
func (lbaas *LbaasV2) EnsureLoadBalancer(apiService *api.Service, hosts []string) (*api.LoadBalancerStatus, error) {
glog.V(4).Infof("EnsureLoadBalancer(%v, %v, %v, %v, %v, %v)", apiService.Namespace, apiService.Name, apiService.Spec.LoadBalancerIP, apiService.Spec.Ports, hosts, apiService.Annotations)
ports := apiService.Spec.Ports
if len(ports) > 1 {
return nil, fmt.Errorf("multiple ports are not yet supported in openstack load balancers")
} else if len(ports) == 0 {
return nil, fmt.Errorf("no ports provided to openstack load balancer")
}
// The service controller verified all the protocols match on the ports, just check and use the first one
// TODO: Convert all error messages to use an event recorder
if ports[0].Protocol != api.ProtocolTCP {
return nil, fmt.Errorf("Only TCP LoadBalancer is supported for openstack load balancers")
}
affinity := api.ServiceAffinityNone //apiService.Spec.SessionAffinity
var persistence *v2_pools.SessionPersistence
switch affinity {
case api.ServiceAffinityNone:
persistence = nil
case api.ServiceAffinityClientIP:
persistence = &v2_pools.SessionPersistence{Type: "SOURCE_IP"}
default:
return nil, fmt.Errorf("unsupported load balancer affinity: %v", affinity)
}
sourceRanges, err := service.GetLoadBalancerSourceRanges(apiService)
if err != nil {
return nil, err
}
if !service.IsAllowAll(sourceRanges) {
return nil, fmt.Errorf("Source range restrictions are not supported for openstack load balancers")
}
glog.V(2).Infof("Checking if openstack load balancer already exists: %s", cloudprovider.GetLoadBalancerName(apiService))
_, exists, err := lbaas.GetLoadBalancer(apiService)
if err != nil {
return nil, fmt.Errorf("error checking if openstack load balancer already exists: %v", err)
}
// TODO: Implement a more efficient update strategy for common changes than delete & create
// In particular, if we implement hosts update, we can get rid of UpdateHosts
if exists {
err := lbaas.EnsureLoadBalancerDeleted(apiService)
if err != nil {
return nil, fmt.Errorf("error deleting existing openstack load balancer: %v", err)
}
}
lbmethod := v2_pools.LBMethod(lbaas.opts.LBMethod)
if lbmethod == "" {
lbmethod = v2_pools.LBMethodRoundRobin
}
name := cloudprovider.GetLoadBalancerName(apiService)
createOpts := loadbalancers.CreateOpts{
Name: name,
Description: fmt.Sprintf("Kubernetes external service %s", name),
VipSubnetID: lbaas.opts.SubnetId,
}
loadBalancerIP := apiService.Spec.LoadBalancerIP
if loadBalancerIP != "" {
createOpts.VipAddress = loadBalancerIP
}
loadbalancer, err := loadbalancers.Create(lbaas.network, createOpts).Extract()
if err != nil {
// cleanup what was created so far
_ = lbaas.EnsureLoadBalancerDeleted(apiService)
return nil, err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
listener, err := listeners.Create(lbaas.network, listeners.CreateOpts{
Name: name,
Protocol: listeners.ProtocolTCP,
ProtocolPort: (int)(ports[0].Port), //TODO: need to handle multi-port
LoadbalancerID: loadbalancer.ID,
}).Extract()
if err != nil {
// cleanup what was created so far
_ = lbaas.EnsureLoadBalancerDeleted(apiService)
return nil, err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
pool, err := v2_pools.Create(lbaas.network, v2_pools.CreateOpts{
Name: name,
Protocol: v2_pools.ProtocolTCP,
LBMethod: lbmethod,
ListenerID: listener.ID,
Persistence: persistence,
}).Extract()
if err != nil {
// cleanup what was created so far
_ = lbaas.EnsureLoadBalancerDeleted(apiService)
return nil, err
}
for _, host := range hosts {
addr, err := getAddressByName(lbaas.compute, host)
if err != nil {
return nil, err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
_, err = v2_pools.CreateAssociateMember(lbaas.network, pool.ID, v2_pools.MemberCreateOpts{
Name: name,
ProtocolPort: int(ports[0].NodePort), //TODO: need to handle multi-port
Address: addr,
SubnetID: lbaas.opts.SubnetId,
}).Extract()
if err != nil {
// cleanup what was created so far
_ = lbaas.EnsureLoadBalancerDeleted(apiService)
return nil, err
}
}
if lbaas.opts.CreateMonitor {
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
_, err = v2_monitors.Create(lbaas.network, v2_monitors.CreateOpts{
PoolID: pool.ID,
Type: monitors.TypeTCP,
Delay: int(lbaas.opts.MonitorDelay.Duration.Seconds()),
Timeout: int(lbaas.opts.MonitorTimeout.Duration.Seconds()),
MaxRetries: int(lbaas.opts.MonitorMaxRetries),
}).Extract()
if err != nil {
// cleanup what was created so far
_ = lbaas.EnsureLoadBalancerDeleted(apiService)
return nil, err
}
}
status := &api.LoadBalancerStatus{}
status.Ingress = []api.LoadBalancerIngress{{IP: loadbalancer.VipAddress}}
if lbaas.opts.FloatingNetworkId != "" {
portID, err := getPortIDByIP(lbaas.network, loadbalancer.VipAddress)
if err != nil {
// cleanup what was created so far
_ = lbaas.EnsureLoadBalancerDeleted(apiService)
return nil, err
}
floatIPOpts := floatingips.CreateOpts{
FloatingNetworkID: lbaas.opts.FloatingNetworkId,
PortID: portID,
}
floatIP, err := floatingips.Create(lbaas.network, floatIPOpts).Extract()
if err != nil {
// cleanup what was created so far
_ = lbaas.EnsureLoadBalancerDeleted(apiService)
return nil, err
}
status.Ingress = append(status.Ingress, api.LoadBalancerIngress{IP: floatIP.FloatingIP})
}
return status, nil
}
func (lbaas *LbaasV2) UpdateLoadBalancer(service *api.Service, hosts []string) error {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
glog.V(4).Infof("UpdateLoadBalancer(%v, %v)", loadBalancerName, hosts)
ports := service.Spec.Ports
if len(ports) > 1 {
return fmt.Errorf("multiple ports are not yet supported in openstack load balancers")
} else if len(ports) == 0 {
return fmt.Errorf("no ports provided to openstack load balancer")
}
loadbalancer, err := getLoadbalancerByName(lbaas.network, loadBalancerName)
if err != nil {
return err
}
if loadbalancer == nil {
return fmt.Errorf("Loadbalancer %s does not exist", loadBalancerName)
}
// Set of member (addresses) that _should_ exist
addrs := map[string]bool{}
for _, host := range hosts {
addr, err := getAddressByName(lbaas.compute, host)
if err != nil {
return err
}
addrs[addr] = true
}
// Iterate over members in each pool that _do_ exist
var poolID string
err = v2_pools.List(lbaas.network, v2_pools.ListOpts{LoadbalancerID: loadbalancer.ID}).EachPage(func(page pagination.Page) (bool, error) {
poolsList, err := v2_pools.ExtractPools(page)
if err != nil {
return false, err
}
for _, pool := range poolsList {
poolID = pool.ID
err := v2_pools.ListAssociateMembers(lbaas.network, poolID, v2_pools.MemberListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
membersList, err := v2_pools.ExtractMembers(page)
if err != nil {
return false, err
}
for _, member := range membersList {
if _, found := addrs[member.Address]; found {
// Member already exists, remove from update list
delete(addrs, member.Address)
} else {
// Member needs to be deleted
err = v2_pools.DeleteMember(lbaas.network, poolID, member.ID).ExtractErr()
if err != nil {
return false, err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
}
}
return true, nil
})
if err != nil {
return false, err
}
}
return true, nil
})
if err != nil {
return err
}
// Anything left in addrs is a new member that needs to be added to a pool
for addr := range addrs {
_, err := v2_pools.CreateAssociateMember(lbaas.network, poolID, v2_pools.MemberCreateOpts{
Address: addr,
ProtocolPort: int(ports[0].NodePort),
SubnetID: lbaas.opts.SubnetId,
}).Extract()
if err != nil {
return err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
}
return nil
}
func (lbaas *LbaasV2) EnsureLoadBalancerDeleted(service *api.Service) error {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
glog.V(4).Infof("EnsureLoadBalancerDeleted(%v)", loadBalancerName)
loadbalancer, err := getLoadbalancerByName(lbaas.network, loadBalancerName)
if err != nil && err != ErrNotFound {
return err
}
if loadbalancer == nil {
return nil
}
if lbaas.opts.FloatingNetworkId != "" && loadbalancer != nil {
portID, err := getPortIDByIP(lbaas.network, loadbalancer.VipAddress)
if err != nil {
return err
}
floatingIP, err := getFloatingIPByPortID(lbaas.network, portID)
if err != nil && err != ErrNotFound {
return err
}
if floatingIP != nil {
err = floatingips.Delete(lbaas.network, floatingIP.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
}
// get all listeners associated with this loadbalancer
var listenerIDs []string
err = listeners.List(lbaas.network, listeners.ListOpts{LoadbalancerID: loadbalancer.ID}).EachPage(func(page pagination.Page) (bool, error) {
listenerList, err := listeners.ExtractListeners(page)
if err != nil {
return false, err
}
for _, listener := range listenerList {
listenerIDs = append(listenerIDs, listener.ID)
}
return true, nil
})
if err != nil {
return err
}
// get all pools associated with this loadbalancer
var poolIDs []string
err = v2_pools.List(lbaas.network, v2_pools.ListOpts{LoadbalancerID: loadbalancer.ID}).EachPage(func(page pagination.Page) (bool, error) {
poolsList, err := v2_pools.ExtractPools(page)
if err != nil {
return false, err
}
for _, pool := range poolsList {
poolIDs = append(poolIDs, pool.ID)
}
return true, nil
})
if err != nil {
return err
}
// get all members associated with each poolIDs
var memberIDs []string
for _, poolID := range poolIDs {
err := v2_pools.ListAssociateMembers(lbaas.network, poolID, v2_pools.MemberListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
membersList, err := v2_pools.ExtractMembers(page)
if err != nil {
return false, err
}
for _, member := range membersList {
memberIDs = append(memberIDs, member.ID)
}
return true, nil
})
if err != nil {
return err
}
}
// get all monitors associated with each poolIDs
var monitorIDs []string
for _, poolID := range poolIDs {
err = v2_monitors.List(lbaas.network, v2_monitors.ListOpts{PoolID: poolID}).EachPage(func(page pagination.Page) (bool, error) {
monitorsList, err := v2_monitors.ExtractMonitors(page)
if err != nil {
return false, err
}
for _, monitor := range monitorsList {
monitorIDs = append(monitorIDs, monitor.ID)
}
return true, nil
})
if err != nil {
return err
}
}
// delete all monitors
for _, monitorID := range monitorIDs {
err := v2_monitors.Delete(lbaas.network, monitorID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
}
// delete all members and pools
for _, poolID := range poolIDs {
// delete all members for this pool
for _, memberID := range memberIDs {
err := v2_pools.DeleteMember(lbaas.network, poolID, memberID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
}
// delete pool
err := v2_pools.Delete(lbaas.network, poolID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
}
// delete all listeners
for _, listenerID := range listenerIDs {
err := listeners.Delete(lbaas.network, listenerID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
waitLoadbalancerActiveProvisioningStatus(lbaas.network, loadbalancer.ID)
}
// delete loadbalancer
err = loadbalancers.Delete(lbaas.network, loadbalancer.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
waitLoadbalancerDeleted(lbaas.network, loadbalancer.ID)
return nil
}
func (lb *LbaasV1) GetLoadBalancer(service *api.Service) (*api.LoadBalancerStatus, bool, error) {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
vip, err := getVipByName(lb.network, loadBalancerName)
if err == ErrNotFound {
return nil, false, nil
}
if vip == nil {
return nil, false, err
}
status := &api.LoadBalancerStatus{}
status.Ingress = []api.LoadBalancerIngress{{IP: vip.Address}}
return status, true, err
}
// TODO: This code currently ignores 'region' and always creates a
// loadbalancer in only the current OpenStack region. We should take
// a list of regions (from config) and query/create loadbalancers in
// each region.
func (lb *LbaasV1) EnsureLoadBalancer(apiService *api.Service, hosts []string) (*api.LoadBalancerStatus, error) {
glog.V(4).Infof("EnsureLoadBalancer(%v, %v, %v, %v, %v, %v)", apiService.Namespace, apiService.Name, apiService.Spec.LoadBalancerIP, apiService.Spec.Ports, hosts, apiService.Annotations)
ports := apiService.Spec.Ports
if len(ports) > 1 {
return nil, fmt.Errorf("multiple ports are not yet supported in openstack load balancers")
} else if len(ports) == 0 {
return nil, fmt.Errorf("no ports provided to openstack load balancer")
}
// The service controller verified all the protocols match on the ports, just check and use the first one
// TODO: Convert all error messages to use an event recorder
if ports[0].Protocol != api.ProtocolTCP {
return nil, fmt.Errorf("Only TCP LoadBalancer is supported for openstack load balancers")
}
affinity := apiService.Spec.SessionAffinity
var persistence *vips.SessionPersistence
switch affinity {
case api.ServiceAffinityNone:
persistence = nil
case api.ServiceAffinityClientIP:
persistence = &vips.SessionPersistence{Type: "SOURCE_IP"}
default:
return nil, fmt.Errorf("unsupported load balancer affinity: %v", affinity)
}
sourceRanges, err := service.GetLoadBalancerSourceRanges(apiService)
if err != nil {
return nil, err
}
if !service.IsAllowAll(sourceRanges) {
return nil, fmt.Errorf("Source range restrictions are not supported for openstack load balancers")
}
glog.V(2).Infof("Checking if openstack load balancer already exists: %s", cloudprovider.GetLoadBalancerName(apiService))
_, exists, err := lb.GetLoadBalancer(apiService)
if err != nil {
return nil, fmt.Errorf("error checking if openstack load balancer already exists: %v", err)
}
// TODO: Implement a more efficient update strategy for common changes than delete & create
// In particular, if we implement hosts update, we can get rid of UpdateHosts
if exists {
err := lb.EnsureLoadBalancerDeleted(apiService)
if err != nil {
return nil, fmt.Errorf("error deleting existing openstack load balancer: %v", err)
}
}
lbmethod := lb.opts.LBMethod
if lbmethod == "" {
lbmethod = pools.LBMethodRoundRobin
}
name := cloudprovider.GetLoadBalancerName(apiService)
pool, err := pools.Create(lb.network, pools.CreateOpts{
Name: name,
Protocol: pools.ProtocolTCP,
SubnetID: lb.opts.SubnetId,
LBMethod: lbmethod,
}).Extract()
if err != nil {
return nil, err
}
for _, host := range hosts {
addr, err := getAddressByName(lb.compute, host)
if err != nil {
return nil, err
}
_, err = members.Create(lb.network, members.CreateOpts{
PoolID: pool.ID,
ProtocolPort: int(ports[0].NodePort), //TODO: need to handle multi-port
Address: addr,
}).Extract()
if err != nil {
pools.Delete(lb.network, pool.ID)
return nil, err
}
}
var mon *monitors.Monitor
if lb.opts.CreateMonitor {
mon, err = monitors.Create(lb.network, monitors.CreateOpts{
Type: monitors.TypeTCP,
Delay: int(lb.opts.MonitorDelay.Duration.Seconds()),
Timeout: int(lb.opts.MonitorTimeout.Duration.Seconds()),
MaxRetries: int(lb.opts.MonitorMaxRetries),
}).Extract()
if err != nil {
pools.Delete(lb.network, pool.ID)
return nil, err
}
_, err = pools.AssociateMonitor(lb.network, pool.ID, mon.ID).Extract()
if err != nil {
monitors.Delete(lb.network, mon.ID)
pools.Delete(lb.network, pool.ID)
return nil, err
}
}
createOpts := vips.CreateOpts{
Name: name,
Description: fmt.Sprintf("Kubernetes external service %s", name),
Protocol: "TCP",
ProtocolPort: int(ports[0].Port), //TODO: need to handle multi-port
PoolID: pool.ID,
SubnetID: lb.opts.SubnetId,
Persistence: persistence,
}
loadBalancerIP := apiService.Spec.LoadBalancerIP
if loadBalancerIP != "" {
createOpts.Address = loadBalancerIP
}
vip, err := vips.Create(lb.network, createOpts).Extract()
if err != nil {
if mon != nil {
monitors.Delete(lb.network, mon.ID)
}
pools.Delete(lb.network, pool.ID)
return nil, err
}
status := &api.LoadBalancerStatus{}
status.Ingress = []api.LoadBalancerIngress{{IP: vip.Address}}
if lb.opts.FloatingNetworkId != "" {
floatIPOpts := floatingips.CreateOpts{
FloatingNetworkID: lb.opts.FloatingNetworkId,
PortID: vip.PortID,
}
floatIP, err := floatingips.Create(lb.network, floatIPOpts).Extract()
if err != nil {
return nil, err
}
status.Ingress = append(status.Ingress, api.LoadBalancerIngress{IP: floatIP.FloatingIP})
}
return status, nil
}
func (lb *LbaasV1) UpdateLoadBalancer(service *api.Service, hosts []string) error {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
glog.V(4).Infof("UpdateLoadBalancer(%v, %v)", loadBalancerName, hosts)
vip, err := getVipByName(lb.network, loadBalancerName)
if err != nil {
return err
}
// Set of member (addresses) that _should_ exist
addrs := map[string]bool{}
for _, host := range hosts {
addr, err := getAddressByName(lb.compute, host)
if err != nil {
return err
}
addrs[addr] = true
}
// Iterate over members that _do_ exist
pager := members.List(lb.network, members.ListOpts{PoolID: vip.PoolID})
err = pager.EachPage(func(page pagination.Page) (bool, error) {
memList, err := members.ExtractMembers(page)
if err != nil {
return false, err
}
for _, member := range memList {
if _, found := addrs[member.Address]; found {
// Member already exists
delete(addrs, member.Address)
} else {
// Member needs to be deleted
err = members.Delete(lb.network, member.ID).ExtractErr()
if err != nil {
return false, err
}
}
}
return true, nil
})
if err != nil {
return err
}
// Anything left in addrs is a new member that needs to be added
for addr := range addrs {
_, err := members.Create(lb.network, members.CreateOpts{
PoolID: vip.PoolID,
Address: addr,
ProtocolPort: vip.ProtocolPort,
}).Extract()
if err != nil {
return err
}
}
return nil
}
func (lb *LbaasV1) EnsureLoadBalancerDeleted(service *api.Service) error {
loadBalancerName := cloudprovider.GetLoadBalancerName(service)
glog.V(4).Infof("EnsureLoadBalancerDeleted(%v)", loadBalancerName)
vip, err := getVipByName(lb.network, loadBalancerName)
if err != nil && err != ErrNotFound {
return err
}
if lb.opts.FloatingNetworkId != "" && vip != nil {
floatingIP, err := getFloatingIPByPortID(lb.network, vip.PortID)
if err != nil && !isNotFound(err) {
return err
}
if floatingIP != nil {
err = floatingips.Delete(lb.network, floatingIP.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
}
// We have to delete the VIP before the pool can be deleted,
// so no point continuing if this fails.
if vip != nil {
err := vips.Delete(lb.network, vip.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
var pool *pools.Pool
if vip != nil {
pool, err = pools.Get(lb.network, vip.PoolID).Extract()
if err != nil && !isNotFound(err) {
return err
}
} else {
// The VIP is gone, but it is conceivable that a Pool
// still exists that we failed to delete on some
// previous occasion. Make a best effort attempt to
// cleanup any pools with the same name as the VIP.
pool, err = getPoolByName(lb.network, service.Name)
if err != nil && err != ErrNotFound {
return err
}
}
if pool != nil {
for _, monId := range pool.MonitorIDs {
_, err = pools.DisassociateMonitor(lb.network, pool.ID, monId).Extract()
if err != nil {
return err
}
err = monitors.Delete(lb.network, monId).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
err = pools.Delete(lb.network, pool.ID).ExtractErr()
if err != nil && !isNotFound(err) {
return err
}
}
return nil
}

View File

@ -67,15 +67,15 @@ func TestReadConfig(t *testing.T) {
}
cfg, err := readConfig(strings.NewReader(`
[Global]
auth-url = http://auth.url
username = user
[LoadBalancer]
create-monitor = yes
monitor-delay = 1m
monitor-timeout = 30s
monitor-max-retries = 3
`))
[Global]
auth-url = http://auth.url
username = user
[LoadBalancer]
create-monitor = yes
monitor-delay = 1m
monitor-timeout = 30s
monitor-max-retries = 3
`))
if err != nil {
t.Fatalf("Should succeed when a valid config is provided: %s", err)
}
@ -204,6 +204,8 @@ func TestLoadBalancer(t *testing.T) {
t.Skipf("No config found in environment")
}
cfg.LoadBalancer.LBVersion = "v2"
os, err := newOpenStack(cfg)
if err != nil {
t.Fatalf("Failed to construct/authenticate OpenStack: %s", err)
@ -223,6 +225,32 @@ func TestLoadBalancer(t *testing.T) {
}
}
func TestLoadBalancerV2(t *testing.T) {
cfg, ok := configFromEnv()
if !ok {
t.Skipf("No config found in environment")
}
cfg.LoadBalancer.LBVersion = "v2"
os, err := newOpenStack(cfg)
if err != nil {
t.Fatalf("Failed to construct/authenticate OpenStack: %s", err)
}
lbaas, ok := os.LoadBalancer()
if !ok {
t.Fatalf("LoadBalancer() returned false - perhaps your stack doesn't support Neutron?")
}
_, exists, err := lbaas.GetLoadBalancer(&api.Service{ObjectMeta: api.ObjectMeta{Name: "noexist"}})
if err != nil {
t.Fatalf("GetLoadBalancer(\"noexist\") returned error: %s", err)
}
if exists {
t.Fatalf("GetLoadBalancer(\"noexist\") returned exists")
}
}
func TestZones(t *testing.T) {
os := OpenStack{
provider: &gophercloud.ProviderClient{

0
vendor/github.com/armon/go-metrics/.gitignore generated vendored Normal file → Executable file
View File

0
vendor/github.com/armon/go-metrics/metrics.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/armon/go-metrics/sink.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/armon/go-metrics/start.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/armon/go-metrics/statsite.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/coreos/go-oidc/jose/sig.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/coreos/go-oidc/jose/sig_hmac.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/coreos/go-oidc/jose/sig_rsa.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/coreos/go-oidc/oidc/key.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/elazarl/goproxy/all.bash generated vendored Normal file → Executable file
View File

0
vendor/github.com/hashicorp/go-msgpack/codec/msgpack_test.py generated vendored Normal file → Executable file
View File

0
vendor/github.com/kr/pty/mktypes.bash generated vendored Normal file → Executable file
View File

0
vendor/github.com/pborman/uuid/dce.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/pborman/uuid/doc.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/pborman/uuid/node.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/pborman/uuid/time.go generated vendored Normal file → Executable file
View File

0
vendor/github.com/pborman/uuid/uuid.go generated vendored Normal file → Executable file
View File

View File

@ -42,6 +42,11 @@ type AuthOptions struct {
// re-authenticate automatically if/when your token expires. If you set it to
// false, it will not cache these settings, but re-authentication will not be
// possible. This setting defaults to false.
//
// NOTE: The reauth function will try to re-authenticate endlessly if left unchecked.
// The way to limit the number of attempts is to provide a custom HTTP client to the provider client
// and provide a transport that implements the RoundTripper interface and stores the number of failed retries.
// For an example of this, see here: https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
AllowReauth bool
// TokenID allows users to authenticate (possibly as another user) with an

View File

@ -134,13 +134,17 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
v3Client.Endpoint = endpoint
}
// copy the auth options to a local variable that we can change. `options`
// needs to stay as-is for reauth purposes
v3Options := options
var scope *tokens3.Scope
if options.TenantID != "" {
scope = &tokens3.Scope{
ProjectID: options.TenantID,
}
options.TenantID = ""
options.TenantName = ""
v3Options.TenantID = ""
v3Options.TenantName = ""
} else {
if options.TenantName != "" {
scope = &tokens3.Scope{
@ -148,11 +152,11 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
DomainID: options.DomainID,
DomainName: options.DomainName,
}
options.TenantName = ""
v3Options.TenantName = ""
}
}
result := tokens3.Create(v3Client, options, scope)
result := tokens3.Create(v3Client, v3Options, scope)
token, err := result.ExtractToken()
if err != nil {
@ -277,6 +281,29 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// 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")
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
}
// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
// CDN service.
func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {

View File

@ -51,6 +51,8 @@ type Image struct {
Status string
Updated string
Metadata map[string]string
}
// ImagePage contains a single page of results from a List operation.

View File

@ -25,6 +25,9 @@ type Monitor struct {
// The unique ID for the VIP.
ID string
// Monitor name. Does not have to be unique.
Name string
// Owner of the VIP. Only an administrative user can specify a tenant ID
// other than its own.
TenantID string `json:"tenant_id" mapstructure:"tenant_id"`

View File

@ -74,6 +74,9 @@ type CreateOpts struct {
// current specification supports LBMethodRoundRobin and
// LBMethodLeastConnections as valid values for this attribute.
LBMethod string
// The provider of the pool
Provider string
}
// Create accepts a CreateOpts struct and uses the values to create a new
@ -85,6 +88,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
Protocol string `json:"protocol"`
SubnetID string `json:"subnet_id"`
LBMethod string `json:"lb_method"`
Provider string `json:"provider,omitempty"`
}
type request struct {
Pool pool `json:"pool"`
@ -96,6 +100,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
Protocol: opts.Protocol,
SubnetID: opts.SubnetID,
LBMethod: opts.LBMethod,
Provider: opts.Provider,
}}
var res CreateResult

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

@ -0,0 +1,8 @@
// Package ports contains functionality for working with Neutron port resources.
// A port represents a virtual switch port on a logical network switch. Virtual
// instances attach their interfaces into ports. The logical port also defines
// the MAC address and the IP address(es) to be assigned to the interfaces
// plugged into them. When IP addresses are associated to a port, this also
// implies the port is associated with a subnet, as the IP address was taken
// from the allocation pool for a specific subnet.
package ports

View File

@ -0,0 +1,11 @@
package ports
import "fmt"
func err(str string) error {
return fmt.Errorf("%s", str)
}
var (
errNetworkIDRequired = err("A Network ID is required")
)

View File

@ -0,0 +1,268 @@
package ports
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
// 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 {
ToPortListQuery() (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 port attributes you want to see returned. SortKey allows you to sort
// by a particular port attribute. SortDir sets the direction, and is either
// `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
Status string `q:"status"`
Name string `q:"name"`
AdminStateUp *bool `q:"admin_state_up"`
NetworkID string `q:"network_id"`
TenantID string `q:"tenant_id"`
DeviceOwner string `q:"device_owner"`
MACAddress string `q:"mac_address"`
ID string `q:"id"`
DeviceID string `q:"device_id"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"`
}
// ToPortListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToPortListQuery() (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
// ports. It accepts a ListOpts struct, which allows you to filter and sort
// the returned collection for greater efficiency.
//
// Default policy settings return only those ports that are owned by the tenant
// who submits the request, unless the request is submitted by a user with
// administrative rights.
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(c)
if opts != nil {
query, err := opts.ToPortListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return PortPage{pagination.LinkedPageBase{PageResult: r}}
})
}
// Get retrieves a specific port based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
_, res.Err = c.Get(getURL(c, id), &res.Body, nil)
return res
}
// 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 {
ToPortCreateMap() (map[string]interface{}, error)
}
// CreateOpts represents the attributes used when creating a new port.
type CreateOpts struct {
NetworkID string
Name string
AdminStateUp *bool
MACAddress string
FixedIPs interface{}
DeviceID string
DeviceOwner string
TenantID string
SecurityGroups []string
AllowedAddressPairs []AddressPair
}
// ToPortCreateMap casts a CreateOpts struct to a map.
func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) {
p := make(map[string]interface{})
if opts.NetworkID == "" {
return nil, errNetworkIDRequired
}
p["network_id"] = opts.NetworkID
if opts.DeviceID != "" {
p["device_id"] = opts.DeviceID
}
if opts.DeviceOwner != "" {
p["device_owner"] = opts.DeviceOwner
}
if opts.FixedIPs != nil {
p["fixed_ips"] = opts.FixedIPs
}
if opts.SecurityGroups != nil {
p["security_groups"] = opts.SecurityGroups
}
if opts.TenantID != "" {
p["tenant_id"] = opts.TenantID
}
if opts.AdminStateUp != nil {
p["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
p["name"] = opts.Name
}
if opts.MACAddress != "" {
p["mac_address"] = opts.MACAddress
}
if opts.AllowedAddressPairs != nil {
p["allowed_address_pairs"] = opts.AllowedAddressPairs
}
return map[string]interface{}{"port": p}, nil
}
// Create accepts a CreateOpts struct and creates a new network using the values
// provided. You must remember to provide a NetworkID value.
func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToPortCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = c.Post(createURL(c), reqBody, &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 {
ToPortUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts represents the attributes used when updating an existing port.
type UpdateOpts struct {
Name string
AdminStateUp *bool
FixedIPs interface{}
DeviceID string
DeviceOwner string
SecurityGroups []string
AllowedAddressPairs []AddressPair
}
// ToPortUpdateMap casts an UpdateOpts struct to a map.
func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) {
p := make(map[string]interface{})
if opts.DeviceID != "" {
p["device_id"] = opts.DeviceID
}
if opts.DeviceOwner != "" {
p["device_owner"] = opts.DeviceOwner
}
if opts.FixedIPs != nil {
p["fixed_ips"] = opts.FixedIPs
}
if opts.SecurityGroups != nil {
p["security_groups"] = opts.SecurityGroups
}
if opts.AdminStateUp != nil {
p["admin_state_up"] = &opts.AdminStateUp
}
if opts.Name != "" {
p["name"] = opts.Name
}
if opts.AllowedAddressPairs != nil {
p["allowed_address_pairs"] = opts.AllowedAddressPairs
}
return map[string]interface{}{"port": p}, nil
}
// Update accepts a UpdateOpts struct and updates an existing port using the
// values provided.
func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var res UpdateResult
reqBody, err := opts.ToPortUpdateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = c.Put(updateURL(c, id), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return res
}
// Delete accepts a unique ID and deletes the port associated with it.
func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
_, res.Err = c.Delete(deleteURL(c, id), nil)
return res
}
// IDFromName is a convenience function that returns a port's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
portCount := 0
portID := ""
if name == "" {
return "", fmt.Errorf("A port name must be provided.")
}
pager := List(client, nil)
pager.EachPage(func(page pagination.Page) (bool, error) {
portList, err := ExtractPorts(page)
if err != nil {
return false, err
}
for _, p := range portList {
if p.Name == name {
portCount++
portID = p.ID
}
}
return true, nil
})
switch portCount {
case 0:
return "", fmt.Errorf("Unable to find port: %s", name)
case 1:
return portID, nil
default:
return "", fmt.Errorf("Found %d ports matching %s", portCount, name)
}
}

View File

@ -0,0 +1,132 @@
package ports
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type commonResult struct {
gophercloud.Result
}
// Extract is a function that accepts a result and extracts a port resource.
func (r commonResult) Extract() (*Port, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Port *Port `json:"port"`
}
err := mapstructure.Decode(r.Body, &res)
return res.Port, 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
}
// IP is a sub-struct that represents an individual IP.
type IP struct {
SubnetID string `mapstructure:"subnet_id" json:"subnet_id"`
IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
}
type AddressPair struct {
IPAddress string `mapstructure:"ip_address" json:"ip_address,omitempty"`
MACAddress string `mapstructure:"mac_address" json:"mac_address,omitempty"`
}
// Port represents a Neutron port. See package documentation for a top-level
// description of what this is.
type Port struct {
// UUID for the port.
ID string `mapstructure:"id" json:"id"`
// Network that this port is associated with.
NetworkID string `mapstructure:"network_id" json:"network_id"`
// Human-readable name for the port. Might not be unique.
Name string `mapstructure:"name" json:"name"`
// Administrative state of port. If false (down), port does not forward packets.
AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
// Indicates whether network is currently operational. Possible values include
// `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional values.
Status string `mapstructure:"status" json:"status"`
// Mac address to use on this port.
MACAddress string `mapstructure:"mac_address" json:"mac_address"`
// Specifies IP addresses for the port thus associating the port itself with
// the subnets where the IP addresses are picked from
FixedIPs []IP `mapstructure:"fixed_ips" json:"fixed_ips"`
// Owner of network. Only admin users can specify a tenant_id other than its own.
TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
// Identifies the entity (e.g.: dhcp agent) using this port.
DeviceOwner string `mapstructure:"device_owner" json:"device_owner"`
// Specifies the IDs of any security groups associated with a port.
SecurityGroups []string `mapstructure:"security_groups" json:"security_groups"`
// Identifies the device (e.g., virtual server) using this port.
DeviceID string `mapstructure:"device_id" json:"device_id"`
// Identifies the list of IP addresses the port will recognize/accept
AllowedAddressPairs []AddressPair `mapstructure:"allowed_address_pairs" json:"allowed_address_pairs"`
}
// PortPage is the page returned by a pager when traversing over a collection
// of network ports.
type PortPage struct {
pagination.LinkedPageBase
}
// NextPageURL is invoked when a paginated collection of ports 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 PortPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"ports_links"`
}
var r resp
err := mapstructure.Decode(p.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// IsEmpty checks whether a PortPage struct is empty.
func (p PortPage) IsEmpty() (bool, error) {
is, err := ExtractPorts(p)
if err != nil {
return true, nil
}
return len(is) == 0, nil
}
// ExtractPorts accepts a Page struct, specifically a PortPage struct,
// and extracts the elements into a slice of Port structs. In other words,
// a generic collection is mapped into a relevant slice.
func ExtractPorts(page pagination.Page) ([]Port, error) {
var resp struct {
Ports []Port `mapstructure:"ports" json:"ports"`
}
err := mapstructure.Decode(page.(PortPage).Body, &resp)
return resp.Ports, err
}

View File

@ -0,0 +1,31 @@
package ports
import "github.com/rackspace/gophercloud"
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("ports", id)
}
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("ports")
}
func listURL(c *gophercloud.ServiceClient) string {
return rootURL(c)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return resourceURL(c, id)
}
func createURL(c *gophercloud.ServiceClient) string {
return rootURL(c)
}
func updateURL(c *gophercloud.ServiceClient, id string) string {
return resourceURL(c, id)
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return resourceURL(c, id)
}

View File

@ -138,6 +138,11 @@ func (p Pager) AllPages() (Page, error) {
// that type.
pageType := reflect.TypeOf(testPage)
// if it's a single page, just return the testPage (first page)
if _, found := pageType.FieldByName("SinglePageBase"); found {
return testPage, nil
}
// Switch on the page body type. Recognized types are `map[string]interface{}`,
// `[]byte`, and `[]interface{}`.
switch testPage.GetBody().(type) {
@ -153,7 +158,14 @@ func (p Pager) AllPages() (Page, error) {
key = k
}
}
pagesSlice = append(pagesSlice, b[key].([]interface{})...)
switch keyType := b[key].(type) {
case map[string]interface{}:
pagesSlice = append(pagesSlice, keyType)
case []interface{}:
pagesSlice = append(pagesSlice, b[key].([]interface{})...)
default:
return false, fmt.Errorf("Unsupported page body type: %+v", keyType)
}
return true, nil
})
if err != nil {

View File

@ -222,3 +222,13 @@ func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}
// NewAutoScaleV1 creates a ServiceClient that may be used to access the v1 Auto Scale service.
func NewAutoScaleV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
eo.ApplyDefaults("rax:autoscale")
url, err := client.EndpointLocator(eo)
if err != nil {
return nil, err
}
return &gophercloud.ServiceClient{ProviderClient: client, Endpoint: url}, nil
}

0
vendor/github.com/ugorji/go/codec/prebuild.sh generated vendored Normal file → Executable file
View File

0
vendor/github.com/ugorji/go/codec/test.py generated vendored Normal file → Executable file
View File

0
vendor/github.com/ugorji/go/codec/tests.sh generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mkall.sh generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mkerrors.sh generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksyscall.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksyscall_solaris.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksysctl_openbsd.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksysnum_darwin.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksysnum_dragonfly.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksysnum_freebsd.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksysnum_linux.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksysnum_netbsd.pl generated vendored Normal file → Executable file
View File

0
vendor/golang.org/x/sys/unix/mksysnum_openbsd.pl generated vendored Normal file → Executable file
View File

0
vendor/google.golang.org/grpc/codegen.sh generated vendored Normal file → Executable file
View File

0
vendor/google.golang.org/grpc/coverage.sh generated vendored Normal file → Executable file
View File