Merge remote-tracking branch 'upstream/v0.2.0' into update-identity-v2
Conflicts:
openstack/common/extensions/requests.go
openstack/identity/v3/tokens/results.go
openstack/networking/v2/extensions/delegate_test.go
diff --git a/acceptance/openstack/blockstorage/v1/snapshots_test.go b/acceptance/openstack/blockstorage/v1/snapshots_test.go
new file mode 100644
index 0000000..5835048
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/snapshots_test.go
@@ -0,0 +1,87 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/snapshots"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+)
+
+func TestSnapshots(t *testing.T) {
+
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ v, err := volumes.Create(client, &volumes.CreateOpts{
+ Name: "gophercloud-test-volume",
+ Size: 1,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ err = volumes.WaitForStatus(client, v.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create volume: %v\n", err)
+ }
+
+ t.Logf("Created volume: %v\n", v)
+
+ ss, err := snapshots.Create(client, &snapshots.CreateOpts{
+ Name: "gophercloud-test-snapshot",
+ VolumeID: v.ID,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ err = snapshots.WaitForStatus(client, ss.ID, "available", 120)
+ if err != nil {
+ t.Fatalf("Failed to create snapshot: %v\n", err)
+ }
+
+ t.Logf("Created snapshot: %+v\n", ss)
+
+ err = snapshots.Delete(client, ss.ID)
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := snapshots.Get(client, ss.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Fatalf("Failed to delete snapshot: %v", err)
+ }
+
+ t.Log("Deleted snapshot\n")
+
+ err = volumes.Delete(client, v.ID)
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ err = gophercloud.WaitFor(120, func() (bool, error) {
+ _, err := volumes.Get(client, v.ID).Extract()
+ if err != nil {
+ return true, nil
+ }
+
+ return false, nil
+ })
+ if err != nil {
+ t.Errorf("Failed to delete volume: %v", err)
+ }
+
+ t.Log("Deleted volume\n")
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumes_test.go b/acceptance/openstack/blockstorage/v1/volumes_test.go
new file mode 100644
index 0000000..21a47ac
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumes_test.go
@@ -0,0 +1,88 @@
+// +build acceptance blockstorage
+
+package v1
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack"
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes"
+ "github.com/rackspace/gophercloud/openstack/utils"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func newClient() (*gophercloud.ServiceClient, error) {
+ ao, err := utils.AuthOptions()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+func TestVolumes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ cv, err := volumes.Create(client, &volumes.CreateOpts{
+ Size: 1,
+ Name: "gophercloud-test-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ err = volumes.WaitForStatus(client, cv.ID, "available", 60)
+ if err != nil {
+ t.Error(err)
+ }
+ err = volumes.Delete(client, cv.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+
+ _, err = volumes.Update(client, cv.ID, &volumes.UpdateOpts{
+ Name: "gophercloud-updated-volume",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+
+ v, err := volumes.Get(client, cv.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ fmt.Printf("Got volume: %+v\n", v)
+
+ if v.Name != "gophercloud-updated-volume" {
+ t.Errorf("Unable to update volume: Expected name: gophercloud-updated-volume\nActual name: %s", v.Name)
+ }
+
+ err = volumes.List(client, &volumes.ListOpts{Name: "gophercloud-updated-volume"}).EachPage(func(page pagination.Page) (bool, error) {
+ vols, err := volumes.ExtractVolumes(page)
+ if len(vols) != 1 {
+ t.Errorf("Expected 1 volume, got %d", len(vols))
+ }
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error listing volumes: %v", err)
+ }
+}
diff --git a/acceptance/openstack/blockstorage/v1/volumetypes_test.go b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
new file mode 100644
index 0000000..416e341
--- /dev/null
+++ b/acceptance/openstack/blockstorage/v1/volumetypes_test.go
@@ -0,0 +1,58 @@
+// +build acceptance
+
+package v1
+
+import (
+ "testing"
+ "time"
+
+ "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumetypes"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+func TestVolumeTypes(t *testing.T) {
+ client, err := newClient()
+ if err != nil {
+ t.Fatalf("Failed to create Block Storage v1 client: %v", err)
+ }
+
+ vt, err := volumetypes.Create(client, &volumetypes.CreateOpts{
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ "priority": 3,
+ },
+ Name: "gophercloud-test-volumeType",
+ }).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ defer func() {
+ time.Sleep(10000 * time.Millisecond)
+ err = volumetypes.Delete(client, vt.ID)
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ }()
+ t.Logf("Created volume type: %+v\n", vt)
+
+ vt, err = volumetypes.Get(client, vt.ID).Extract()
+ if err != nil {
+ t.Error(err)
+ return
+ }
+ t.Logf("Got volume type: %+v\n", vt)
+
+ err = volumetypes.List(client).EachPage(func(page pagination.Page) (bool, error) {
+ volTypes, err := volumetypes.ExtractVolumeTypes(page)
+ if len(volTypes) != 1 {
+ t.Errorf("Expected 1 volume type, got %d", len(volTypes))
+ }
+ t.Logf("Listing volume types: %+v\n", volTypes)
+ return true, err
+ })
+ if err != nil {
+ t.Errorf("Error trying to list volume types: %v", err)
+ }
+}
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index 620ef1b..131b089 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -46,10 +46,10 @@
name := tools.RandomString("ACPTTEST", 16)
t.Logf("Attempting to create server: %s\n", name)
- server, err := servers.Create(client, map[string]interface{}{
- "flavorRef": choices.FlavorID,
- "imageRef": choices.ImageID,
- "name": name,
+ server, err := servers.Create(client, servers.CreateOpts{
+ Name: name,
+ FlavorRef: choices.FlavorID,
+ ImageRef: choices.ImageID,
}).Extract()
if err != nil {
t.Fatalf("Unable to create server: %v", err)
@@ -114,9 +114,7 @@
t.Logf("Attempting to rename the server to %s.", alternateName)
- updated, err := servers.Update(client, server.ID, map[string]interface{}{
- "name": alternateName,
- }).Extract()
+ updated, err := servers.Update(client, server.ID, servers.UpdateOpts{Name: alternateName}).Extract()
if err != nil {
t.Fatalf("Unable to rename server: %v", err)
}
diff --git a/acceptance/openstack/identity/v3/token_test.go b/acceptance/openstack/identity/v3/token_test.go
index d5f9ea6..341acb7 100644
--- a/acceptance/openstack/identity/v3/token_test.go
+++ b/acceptance/openstack/identity/v3/token_test.go
@@ -34,15 +34,10 @@
service := openstack.NewIdentityV3(provider)
// Use the service to create a token.
- result, err := tokens3.Create(service, ao, nil)
+ token, err := tokens3.Create(service, ao, nil).Extract()
if err != nil {
t.Fatalf("Unable to get token: %v", err)
}
- token, err := result.TokenID()
- if err != nil {
- t.Fatalf("Unable to extract token from response: %v", err)
- }
-
- t.Logf("Acquired token: %s", token)
+ t.Logf("Acquired token: %s", token.ID)
}
diff --git a/acceptance/openstack/networking/v2/extensions/layer3_test.go b/acceptance/openstack/networking/v2/extensions/layer3_test.go
new file mode 100644
index 0000000..1289113
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/layer3_test.go
@@ -0,0 +1,300 @@
+// +build acceptance networking layer3ext
+
+package extensions
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/external"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+ cidr1 = "10.0.0.1/24"
+ cidr2 = "20.0.0.1/24"
+)
+
+func TestAll(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ testRouter(t)
+ testFloatingIP(t)
+}
+
+func testRouter(t *testing.T) {
+ // Setup: Create network
+ networkID := createNetwork(t)
+
+ // Create router
+ routerID := createRouter(t, networkID)
+
+ // Lists routers
+ listRouters(t)
+
+ // Update router
+ updateRouter(t, routerID)
+
+ // Get router
+ getRouter(t, routerID)
+
+ // Create new subnet. Note: this subnet will be deleted when networkID is deleted
+ subnetID := createSubnet(t, networkID, cidr2)
+
+ // Add interface
+ addInterface(t, routerID, subnetID)
+
+ // Remove interface
+ removeInterface(t, routerID, subnetID)
+
+ // Delete router
+ deleteRouter(t, routerID)
+
+ // Cleanup
+ deleteNetwork(t, networkID)
+}
+
+func testFloatingIP(t *testing.T) {
+ // Setup external network
+ extNetworkID := createNetwork(t)
+
+ // Setup internal network, subnet and port
+ intNetworkID, subnetID, portID := createInternalTopology(t)
+
+ // Now the important part: we need to allow the external network to talk to
+ // the internal subnet. For this we need a router that has an interface to
+ // the internal subnet.
+ routerID := bridgeIntSubnetWithExtNetwork(t, extNetworkID, subnetID)
+
+ // Create floating IP
+ ipID := createFloatingIP(t, extNetworkID, portID)
+
+ // Get floating IP
+ getFloatingIP(t, ipID)
+
+ // Update floating IP
+ updateFloatingIP(t, ipID, portID)
+
+ // Delete floating IP
+ deleteFloatingIP(t, ipID)
+
+ // Remove the internal subnet interface
+ removeInterface(t, routerID, subnetID)
+
+ // Delete router and external network
+ deleteRouter(t, routerID)
+ deleteNetwork(t, extNetworkID)
+
+ // Delete internal port and network
+ deletePort(t, portID)
+ deleteNetwork(t, intNetworkID)
+}
+
+func createNetwork(t *testing.T) string {
+ t.Logf("Creating a network")
+
+ asu := true
+ opts := external.CreateOpts{
+ Parent: networks.CreateOpts{Name: "sample_network", AdminStateUp: &asu},
+ External: true,
+ }
+ n, err := networks.Create(base.Client, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ if n.ID == "" {
+ t.Fatalf("No ID returned when creating a network")
+ }
+
+ createSubnet(t, n.ID, cidr1)
+
+ t.Logf("Network created: ID [%s]", n.ID)
+
+ return n.ID
+}
+
+func deleteNetwork(t *testing.T, networkID string) {
+ t.Logf("Deleting network %s", networkID)
+ networks.Delete(base.Client, networkID)
+}
+
+func deletePort(t *testing.T, portID string) {
+ t.Logf("Deleting port %s", portID)
+ ports.Delete(base.Client, portID)
+}
+
+func createInternalTopology(t *testing.T) (string, string, string) {
+ t.Logf("Creating an internal network (for port)")
+ opts := networks.CreateOpts{Name: "internal_network"}
+ n, err := networks.Create(base.Client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ // A subnet is also needed
+ subnetID := createSubnet(t, n.ID, cidr2)
+
+ t.Logf("Creating an internal port on network %s", n.ID)
+ p, err := ports.Create(base.Client, ports.CreateOpts{
+ NetworkID: n.ID,
+ Name: "fixed_internal_port",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ return n.ID, subnetID, p.ID
+}
+
+func bridgeIntSubnetWithExtNetwork(t *testing.T, networkID, subnetID string) string {
+ // Create router with external gateway info
+ routerID := createRouter(t, networkID)
+
+ // Add interface for internal subnet
+ addInterface(t, routerID, subnetID)
+
+ return routerID
+}
+
+func createSubnet(t *testing.T, networkID, cidr string) string {
+ t.Logf("Creating a subnet for network %s", networkID)
+
+ iFalse := false
+ s, err := subnets.Create(base.Client, subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: cidr,
+ IPVersion: subnets.IPv4,
+ Name: "my_subnet",
+ EnableDHCP: &iFalse,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Subnet created: ID [%s]", s.ID)
+
+ return s.ID
+}
+
+func createRouter(t *testing.T, networkID string) string {
+ t.Logf("Creating a router for network %s", networkID)
+
+ asu := false
+ gwi := routers.GatewayInfo{NetworkID: networkID}
+ r, err := routers.Create(base.Client, routers.CreateOpts{
+ Name: "foo_router",
+ AdminStateUp: &asu,
+ GatewayInfo: &gwi,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ if r.ID == "" {
+ t.Fatalf("No ID returned when creating a router")
+ }
+
+ t.Logf("Router created: ID [%s]", r.ID)
+
+ return r.ID
+}
+
+func listRouters(t *testing.T) {
+ pager := routers.List(base.Client, routers.ListOpts{})
+
+ err := pager.EachPage(func(page pagination.Page) (bool, error) {
+ routerList, err := routers.ExtractRouters(page)
+ th.AssertNoErr(t, err)
+
+ for _, r := range routerList {
+ t.Logf("Listing router: ID [%s] Name [%s] Status [%s] GatewayInfo [%#v]",
+ r.ID, r.Name, r.Status, r.GatewayInfo)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateRouter(t *testing.T, routerID string) {
+ r, err := routers.Update(base.Client, routerID, routers.UpdateOpts{
+ Name: "another_name",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+}
+
+func getRouter(t *testing.T, routerID string) {
+ r, err := routers.Get(base.Client, routerID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting router: ID [%s] Name [%s] Status [%s]", r.ID, r.Name, r.Status)
+}
+
+func addInterface(t *testing.T, routerID, subnetID string) {
+ ir, err := routers.AddInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Interface added to router %s: SubnetID [%s] PortID [%s]", routerID, ir.SubnetID, ir.PortID)
+}
+
+func removeInterface(t *testing.T, routerID, subnetID string) {
+ ir, err := routers.RemoveInterface(base.Client, routerID, routers.InterfaceOpts{SubnetID: subnetID}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Interface %s removed from %s", ir.ID, routerID)
+}
+
+func deleteRouter(t *testing.T, routerID string) {
+ t.Logf("Deleting router %s", routerID)
+
+ res := routers.Delete(base.Client, routerID)
+
+ th.AssertNoErr(t, res.Err)
+}
+
+func createFloatingIP(t *testing.T, networkID, portID string) string {
+ t.Logf("Creating floating IP on network [%s] with port [%s]", networkID, portID)
+
+ opts := floatingips.CreateOpts{
+ FloatingNetworkID: networkID,
+ PortID: portID,
+ }
+
+ ip, err := floatingips.Create(base.Client, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Floating IP created: ID [%s] Status [%s] Fixed (internal) IP: [%s] Floating (external) IP: [%s]",
+ ip.ID, ip.Status, ip.FixedIP, ip.FloatingIP)
+
+ return ip.ID
+}
+
+func getFloatingIP(t *testing.T, ipID string) {
+ ip, err := floatingips.Get(base.Client, ipID).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting floating IP: ID [%s] Status [%s]", ip.ID, ip.Status)
+}
+
+func updateFloatingIP(t *testing.T, ipID, portID string) {
+ t.Logf("Disassociate all ports from IP %s", ipID)
+ _, err := floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: ""}).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Re-associate the port %s", portID)
+ _, err = floatingips.Update(base.Client, ipID, floatingips.UpdateOpts{PortID: portID}).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func deleteFloatingIP(t *testing.T, ipID string) {
+ t.Logf("Deleting IP %s", ipID)
+ res := floatingips.Delete(base.Client, ipID)
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/common.go b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
new file mode 100644
index 0000000..a9db1af
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/common.go
@@ -0,0 +1,78 @@
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "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/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/subnets"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func SetupTopology(t *testing.T) (string, string) {
+ // create network
+ n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created network %s", n.ID)
+
+ // create subnet
+ s, err := subnets.Create(base.Client, subnets.CreateOpts{
+ NetworkID: n.ID,
+ CIDR: "192.168.199.0/24",
+ IPVersion: subnets.IPv4,
+ Name: "tmp_subnet",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created subnet %s", s.ID)
+
+ return n.ID, s.ID
+}
+
+func DeleteTopology(t *testing.T, networkID string) {
+ res := networks.Delete(base.Client, networkID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted network %s", networkID)
+}
+
+func CreatePool(t *testing.T, subnetID string) string {
+ p, err := pools.Create(base.Client, pools.CreateOpts{
+ LBMethod: pools.LBMethodRoundRobin,
+ Protocol: "HTTP",
+ Name: "tmp_pool",
+ SubnetID: subnetID,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created pool %s", p.ID)
+
+ return p.ID
+}
+
+func DeletePool(t *testing.T, poolID string) {
+ res := pools.Delete(base.Client, poolID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted pool %s", poolID)
+}
+
+func CreateMonitor(t *testing.T) string {
+ m, err := monitors.Create(base.Client, monitors.CreateOpts{
+ Delay: 5,
+ Timeout: 10,
+ MaxRetries: 3,
+ Type: monitors.TypeHTTP,
+ ExpectedCodes: "200",
+ URLPath: "/login",
+ HTTPMethod: "GET",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created monitor ID [%s]", m.ID)
+
+ return m.ID
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go
new file mode 100644
index 0000000..9b60582
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/member_test.go
@@ -0,0 +1,95 @@
+// +build acceptance networking lbaas lbaasmember
+
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMembers(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // setup
+ networkID, subnetID := SetupTopology(t)
+ poolID := CreatePool(t, subnetID)
+
+ // create member
+ memberID := createMember(t, poolID)
+
+ // list members
+ listMembers(t)
+
+ // update member
+ updateMember(t, memberID)
+
+ // get member
+ getMember(t, memberID)
+
+ // delete member
+ deleteMember(t, memberID)
+
+ // teardown
+ DeletePool(t, poolID)
+ DeleteTopology(t, networkID)
+}
+
+func createMember(t *testing.T, poolID string) string {
+ m, err := members.Create(base.Client, members.CreateOpts{
+ Address: "192.168.199.1",
+ ProtocolPort: 8080,
+ PoolID: poolID,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created member: ID [%s] Status [%s] Weight [%d] Address [%s] Port [%d]",
+ m.ID, m.Status, m.Weight, m.Address, m.ProtocolPort)
+
+ return m.ID
+}
+
+func listMembers(t *testing.T) {
+ err := members.List(base.Client, members.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ memberList, err := members.ExtractMembers(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+
+ for _, m := range memberList {
+ t.Logf("Listing member: ID [%s] Status [%s]", m.ID, m.Status)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateMember(t *testing.T, memberID string) {
+ m, err := members.Update(base.Client, memberID, members.UpdateOpts{AdminStateUp: true}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated member ID [%s]", m.ID)
+}
+
+func getMember(t *testing.T, memberID string) {
+ m, err := members.Get(base.Client, memberID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting member ID [%s]", m.ID)
+}
+
+func deleteMember(t *testing.T, memberID string) {
+ res := members.Delete(base.Client, memberID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted member %s", memberID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go
new file mode 100644
index 0000000..57e860c
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/monitor_test.go
@@ -0,0 +1,77 @@
+// +build acceptance networking lbaas lbaasmonitor
+
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestMonitors(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // create monitor
+ monitorID := CreateMonitor(t)
+
+ // list monitors
+ listMonitors(t)
+
+ // update monitor
+ updateMonitor(t, monitorID)
+
+ // get monitor
+ getMonitor(t, monitorID)
+
+ // delete monitor
+ deleteMonitor(t, monitorID)
+}
+
+func listMonitors(t *testing.T) {
+ err := monitors.List(base.Client, monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ monitorList, err := monitors.ExtractMonitors(page)
+ if err != nil {
+ t.Errorf("Failed to extract monitors: %v", err)
+ return false, err
+ }
+
+ for _, m := range monitorList {
+ t.Logf("Listing monitor: ID [%s] Type [%s] Delay [%ds] Timeout [%d] Retries [%d] Status [%s]",
+ m.ID, m.Type, m.Delay, m.Timeout, m.MaxRetries, m.Status)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateMonitor(t *testing.T, monitorID string) {
+ opts := monitors.UpdateOpts{Delay: 5, Timeout: 10, MaxRetries: 3}
+ m, err := monitors.Update(base.Client, monitorID, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated monitor ID [%s]", m.ID)
+}
+
+func getMonitor(t *testing.T, monitorID string) {
+ m, err := monitors.Get(base.Client, monitorID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting monitor ID [%s]: URL path [%s] HTTP Method [%s] Accepted codes [%s]",
+ m.ID, m.URLPath, m.HTTPMethod, m.ExpectedCodes)
+}
+
+func deleteMonitor(t *testing.T, monitorID string) {
+ res := monitors.Delete(base.Client, monitorID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Deleted monitor %s", monitorID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go b/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go
new file mode 100644
index 0000000..f5a7df7
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/pkg.go
@@ -0,0 +1 @@
+package lbaas
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
new file mode 100644
index 0000000..8194064
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/pool_test.go
@@ -0,0 +1,98 @@
+// +build acceptance networking lbaas lbaaspool
+
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestPools(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // setup
+ networkID, subnetID := SetupTopology(t)
+
+ // create pool
+ poolID := CreatePool(t, subnetID)
+
+ // list pools
+ listPools(t)
+
+ // update pool
+ updatePool(t, poolID)
+
+ // get pool
+ getPool(t, poolID)
+
+ // create monitor
+ monitorID := CreateMonitor(t)
+
+ // associate health monitor
+ associateMonitor(t, poolID, monitorID)
+
+ // disassociate health monitor
+ disassociateMonitor(t, poolID, monitorID)
+
+ // delete pool
+ DeletePool(t, poolID)
+
+ // teardown
+ DeleteTopology(t, networkID)
+}
+
+func listPools(t *testing.T) {
+ err := pools.List(base.Client, pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ poolList, err := pools.ExtractPools(page)
+ if err != nil {
+ t.Errorf("Failed to extract pools: %v", err)
+ return false, err
+ }
+
+ for _, p := range poolList {
+ t.Logf("Listing pool: ID [%s] Name [%s] Status [%s] LB algorithm [%s]", p.ID, p.Name, p.Status, p.LBMethod)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updatePool(t *testing.T, poolID string) {
+ opts := pools.UpdateOpts{Name: "SuperPool", LBMethod: pools.LBMethodLeastConnections}
+ p, err := pools.Update(base.Client, poolID, opts).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated pool ID [%s]", p.ID)
+}
+
+func getPool(t *testing.T, poolID string) {
+ p, err := pools.Get(base.Client, poolID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting pool ID [%s]", p.ID)
+}
+
+func associateMonitor(t *testing.T, poolID, monitorID string) {
+ res := pools.AssociateMonitor(base.Client, poolID, monitorID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Associated pool %s with monitor %s", poolID, monitorID)
+}
+
+func disassociateMonitor(t *testing.T, poolID, monitorID string) {
+ res := pools.DisassociateMonitor(base.Client, poolID, monitorID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Disassociated pool %s with monitor %s", poolID, monitorID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go b/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go
new file mode 100644
index 0000000..c8dff2d
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/lbaas/vip_test.go
@@ -0,0 +1,101 @@
+// +build acceptance networking lbaas lbaasvip
+
+package lbaas
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestVIPs(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // setup
+ networkID, subnetID := SetupTopology(t)
+ poolID := CreatePool(t, subnetID)
+
+ // create VIP
+ VIPID := createVIP(t, subnetID, poolID)
+
+ // list VIPs
+ listVIPs(t)
+
+ // update VIP
+ updateVIP(t, VIPID)
+
+ // get VIP
+ getVIP(t, VIPID)
+
+ // delete VIP
+ deleteVIP(t, VIPID)
+
+ // teardown
+ DeletePool(t, poolID)
+ DeleteTopology(t, networkID)
+}
+
+func createVIP(t *testing.T, subnetID, poolID string) string {
+ p, err := vips.Create(base.Client, vips.CreateOpts{
+ Protocol: "HTTP",
+ Name: "New_VIP",
+ AdminStateUp: vips.Up,
+ SubnetID: subnetID,
+ PoolID: poolID,
+ ProtocolPort: 80,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created pool %s", p.ID)
+
+ return p.ID
+}
+
+func listVIPs(t *testing.T) {
+ err := vips.List(base.Client, vips.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ vipList, err := vips.ExtractVIPs(page)
+ if err != nil {
+ t.Errorf("Failed to extract VIPs: %v", err)
+ return false, err
+ }
+
+ for _, vip := range vipList {
+ t.Logf("Listing VIP: ID [%s] Name [%s] Address [%s] Port [%s] Connection Limit [%d]",
+ vip.ID, vip.Name, vip.Address, vip.ProtocolPort, vip.ConnLimit)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func updateVIP(t *testing.T, VIPID string) {
+ i1000 := 1000
+ _, err := vips.Update(base.Client, VIPID, vips.UpdateOpts{ConnLimit: &i1000}).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Updated VIP ID [%s]", VIPID)
+}
+
+func getVIP(t *testing.T, VIPID string) {
+ vip, err := vips.Get(base.Client, VIPID).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Getting VIP ID [%s]: Status [%s]", vip.ID, vip.Status)
+}
+
+func deleteVIP(t *testing.T, VIPID string) {
+ res := vips.Delete(base.Client, VIPID)
+
+ th.AssertNoErr(t, res.Err)
+
+ t.Logf("Deleted VIP %s", VIPID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/pkg.go b/acceptance/openstack/networking/v2/extensions/pkg.go
new file mode 100644
index 0000000..aeec0fa
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/pkg.go
@@ -0,0 +1 @@
+package extensions
diff --git a/acceptance/openstack/networking/v2/extensions/provider_test.go b/acceptance/openstack/networking/v2/extensions/provider_test.go
new file mode 100644
index 0000000..dc21ae5
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/provider_test.go
@@ -0,0 +1,67 @@
+// +build acceptance networking
+
+package extensions
+
+import (
+ "strconv"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestNetworkCRUDOperations(t *testing.T) {
+ Setup(t)
+ defer Teardown()
+
+ // Create a network
+ n, err := networks.Create(Client, networks.CreateOpts{Name: "sample_network", AdminStateUp: true}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Name, "sample_network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ networkID := n.ID
+
+ // List networks
+ pager := networks.List(Client, networks.ListOpts{Limit: 2})
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ t.Logf("--- Page ---")
+
+ networkList, err := networks.ExtractNetworks(page)
+ th.AssertNoErr(t, err)
+
+ for _, n := range networkList {
+ t.Logf("Network: ID [%s] Name [%s] Status [%s] Is shared? [%s]",
+ n.ID, n.Name, n.Status, strconv.FormatBool(n.Shared))
+ }
+
+ return true, nil
+ })
+ th.CheckNoErr(t, err)
+
+ // Get a network
+ if networkID == "" {
+ t.Fatalf("In order to retrieve a network, the NetworkID must be set")
+ }
+ n, err = networks.Get(Client, networkID).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.Subnets, []string{})
+ th.AssertEquals(t, n.Name, "sample_network")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.Shared, false)
+ th.AssertEquals(t, n.ID, networkID)
+
+ // Update network
+ n, err = networks.Update(Client, networkID, networks.UpdateOpts{Name: "new_network_name"}).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, n.Name, "new_network_name")
+
+ // Delete network
+ res := networks.Delete(Client, networkID)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestCreateMultipleNetworks(t *testing.T) {
+ //networks.CreateMany()
+}
diff --git a/acceptance/openstack/networking/v2/extensions/security_test.go b/acceptance/openstack/networking/v2/extensions/security_test.go
new file mode 100644
index 0000000..522219a
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/security_test.go
@@ -0,0 +1,181 @@
+// +build acceptance networking security
+
+package extensions
+
+import (
+ "testing"
+
+ base "github.com/rackspace/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/groups"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/ports"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+func TestSecurityGroups(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // create security group
+ groupID := createSecGroup(t)
+
+ // list security group
+ listSecGroups(t)
+
+ // get security group
+ getSecGroup(t, groupID)
+
+ // create port with security group
+ networkID, portID := createPort(t, groupID)
+
+ // delete port
+ deletePort(t, portID)
+
+ // delete security group
+ deleteSecGroup(t, groupID)
+
+ // teardown
+ deleteNetwork(t, networkID)
+}
+
+func TestSecurityGroupRules(t *testing.T) {
+ base.Setup(t)
+ defer base.Teardown()
+
+ // create security group
+ groupID := createSecGroup(t)
+
+ // create security group rule
+ ruleID := createSecRule(t, groupID)
+
+ // list security group rule
+ listSecRules(t)
+
+ // get security group rule
+ getSecRule(t, ruleID)
+
+ // delete security group rule
+ deleteSecRule(t, ruleID)
+}
+
+func createSecGroup(t *testing.T) string {
+ sg, err := groups.Create(base.Client, groups.CreateOpts{
+ Name: "new-webservers",
+ Description: "security group for webservers",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created security group %s", sg.ID)
+
+ return sg.ID
+}
+
+func listSecGroups(t *testing.T) {
+ err := groups.List(base.Client, groups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ list, err := groups.ExtractGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract secgroups: %v", err)
+ return false, err
+ }
+
+ for _, sg := range list {
+ t.Logf("Listing security group: ID [%s] Name [%s]", sg.ID, sg.Name)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getSecGroup(t *testing.T, id string) {
+ sg, err := groups.Get(base.Client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting security group: ID [%s] Name [%s] Description [%s]", sg.ID, sg.Name, sg.Description)
+}
+
+func createPort(t *testing.T, groupID string) (string, string) {
+ n, err := networks.Create(base.Client, networks.CreateOpts{Name: "tmp_network"}).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created network %s", n.ID)
+
+ opts := ports.CreateOpts{
+ NetworkID: n.ID,
+ Name: "my_port",
+ SecurityGroups: []string{groupID},
+ }
+ p, err := ports.Create(base.Client, opts).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Created port %s with security group %s", p.ID, groupID)
+
+ return n.ID, p.ID
+}
+
+func deleteSecGroup(t *testing.T, groupID string) {
+ res := groups.Delete(base.Client, groupID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted security group %s", groupID)
+}
+
+func deletePort(t *testing.T, portID string) {
+ res := ports.Delete(base.Client, portID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted port %s", portID)
+}
+
+func deleteNetwork(t *testing.T, networkID string) {
+ res := networks.Delete(base.Client, networkID)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted network %s", networkID)
+}
+
+func createSecRule(t *testing.T, groupID string) string {
+ r, err := rules.Create(base.Client, rules.CreateOpts{
+ Direction: "ingress",
+ PortRangeMin: 80,
+ EtherType: "IPv4",
+ PortRangeMax: 80,
+ Protocol: "tcp",
+ SecGroupID: groupID,
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+
+ t.Logf("Created security group rule %s", r.ID)
+
+ return r.ID
+}
+
+func listSecRules(t *testing.T) {
+ err := rules.List(base.Client, rules.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ list, err := rules.ExtractRules(page)
+ if err != nil {
+ t.Errorf("Failed to extract sec rules: %v", err)
+ return false, err
+ }
+
+ for _, r := range list {
+ t.Logf("Listing security rule: ID [%s]", r.ID)
+ }
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+}
+
+func getSecRule(t *testing.T, id string) {
+ r, err := rules.Get(base.Client, id).Extract()
+ th.AssertNoErr(t, err)
+ t.Logf("Getting security rule: ID [%s] Direction [%s] EtherType [%s] Protocol [%s]",
+ r.ID, r.Direction, r.EtherType, r.Protocol)
+}
+
+func deleteSecRule(t *testing.T, id string) {
+ res := rules.Delete(base.Client, id)
+ th.AssertNoErr(t, res.Err)
+ t.Logf("Deleted security rule %s", id)
+}
diff --git a/openstack/blockstorage/v1/apiversions/doc.go b/openstack/blockstorage/v1/apiversions/doc.go
new file mode 100644
index 0000000..c3c486f
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/doc.go
@@ -0,0 +1,3 @@
+// Package apiversions provides information and interaction with the different
+// API versions for the OpenStack Cinder service.
+package apiversions
diff --git a/openstack/blockstorage/v1/apiversions/requests.go b/openstack/blockstorage/v1/apiversions/requests.go
new file mode 100644
index 0000000..b3a39f7
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests.go
@@ -0,0 +1,28 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// ListVersions lists all the Cinder API versions available to end-users.
+func List(c *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(c, listURL(c), func(r pagination.LastHTTPResponse) pagination.Page {
+ return APIVersionPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, v string) GetResult {
+ var res GetResult
+ _, err := perigee.Request("GET", getURL(client, v), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
diff --git a/openstack/blockstorage/v1/apiversions/requests_test.go b/openstack/blockstorage/v1/apiversions/requests_test.go
new file mode 100644
index 0000000..c135722
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/requests_test.go
@@ -0,0 +1,156 @@
+package apiversions
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestListVersions(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "versions": [
+ {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ }
+ ]
+ },
+ {
+ "status": "CURRENT",
+ "updated": "2012-11-21T11:33:21Z",
+ "id": "v2.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v2/",
+ "rel": "self"
+ }
+ ]
+ }
+ ]
+ }`)
+ })
+
+ count := 0
+
+ List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractAPIVersions(page)
+ if err != nil {
+ t.Errorf("Failed to extract API versions: %v", err)
+ return false, err
+ }
+
+ expected := []APIVersion{
+ APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ },
+ APIVersion{
+ ID: "v2.0",
+ Status: "CURRENT",
+ Updated: "2012-11-21T11:33:21Z",
+ },
+ }
+
+ th.AssertDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestAPIInfo(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "version": {
+ "status": "CURRENT",
+ "updated": "2012-01-04T11:33:21Z",
+ "media-types": [
+ {
+ "base": "application/xml",
+ "type": "application/vnd.openstack.volume+xml;version=1"
+ },
+ {
+ "base": "application/json",
+ "type": "application/vnd.openstack.volume+json;version=1"
+ }
+ ],
+ "id": "v1.0",
+ "links": [
+ {
+ "href": "http://23.253.228.211:8776/v1/",
+ "rel": "self"
+ },
+ {
+ "href": "http://jorgew.github.com/block-storage-api/content/os-block-storage-1.0.pdf",
+ "type": "application/pdf",
+ "rel": "describedby"
+ },
+ {
+ "href": "http://docs.rackspacecloud.com/servers/api/v1.1/application.wadl",
+ "type": "application/vnd.sun.wadl+xml",
+ "rel": "describedby"
+ }
+ ]
+ }
+ }`)
+ })
+
+ actual, err := Get(ServiceClient(), "v1").Extract()
+ if err != nil {
+ t.Errorf("Failed to extract version: %v", err)
+ }
+
+ expected := APIVersion{
+ ID: "v1.0",
+ Status: "CURRENT",
+ Updated: "2012-01-04T11:33:21Z",
+ }
+
+ th.AssertEquals(t, actual.ID, expected.ID)
+ th.AssertEquals(t, actual.Status, expected.Status)
+ th.AssertEquals(t, actual.Updated, expected.Updated)
+}
diff --git a/openstack/blockstorage/v1/apiversions/results.go b/openstack/blockstorage/v1/apiversions/results.go
new file mode 100644
index 0000000..eeff132
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/results.go
@@ -0,0 +1,62 @@
+package apiversions
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// APIVersion represents an API version for Cinder.
+type APIVersion struct {
+ ID string `json:"id" mapstructure:"id"` // unique identifier
+ Status string `json:"status" mapstructure:"status"` // current status
+ Updated string `json:"updated" mapstructure:"updated"` // date last updated
+}
+
+// APIVersionPage is the page returned by a pager when traversing over a
+// collection of API versions.
+type APIVersionPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty checks whether an APIVersionPage struct is empty.
+func (r APIVersionPage) IsEmpty() (bool, error) {
+ is, err := ExtractAPIVersions(r)
+ if err != nil {
+ return true, err
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractAPIVersions takes a collection page, extracts all of the elements,
+// and returns them a slice of APIVersion structs. It is effectively a cast.
+func ExtractAPIVersions(page pagination.Page) ([]APIVersion, error) {
+ var resp struct {
+ Versions []APIVersion `mapstructure:"versions"`
+ }
+
+ err := mapstructure.Decode(page.(APIVersionPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Versions, nil
+}
+
+type GetResult struct {
+ gophercloud.CommonResult
+}
+
+func (r GetResult) Extract() (*APIVersion, error) {
+ var resp struct {
+ Version *APIVersion `mapstructure:"version"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Version, nil
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls.go b/openstack/blockstorage/v1/apiversions/urls.go
new file mode 100644
index 0000000..56f8260
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls.go
@@ -0,0 +1,15 @@
+package apiversions
+
+import (
+ "strings"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func getURL(c *gophercloud.ServiceClient, version string) string {
+ return c.ServiceURL(strings.TrimRight(version, "/") + "/")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("")
+}
diff --git a/openstack/blockstorage/v1/apiversions/urls_test.go b/openstack/blockstorage/v1/apiversions/urls_test.go
new file mode 100644
index 0000000..37e9142
--- /dev/null
+++ b/openstack/blockstorage/v1/apiversions/urls_test.go
@@ -0,0 +1,26 @@
+package apiversions
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909/"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "v1")
+ expected := endpoint + "v1/"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests.go b/openstack/blockstorage/v1/snapshots/requests.go
new file mode 100644
index 0000000..40b44d8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests.go
@@ -0,0 +1,130 @@
+package snapshots
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Snapshot. This object is passed to
+// the snapshots.Create function. For more information about these parameters,
+// see the Snapshot object.
+type CreateOpts struct {
+ Description string // OPTIONAL
+ Force bool // OPTIONAL
+ Metadata map[string]interface{} // OPTIONAL
+ Name string // OPTIONAL
+ VolumeID string // REQUIRED
+}
+
+// Create will create a new Snapshot based on the values in CreateOpts. To extract
+// the Snapshot object from the response, call the Extract method on the
+// CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+ type snapshot struct {
+ Description *string `json:"display_description,omitempty"`
+ Force bool `json:"force,omitempty"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ VolumeID *string `json:"volume_id,omitempty"`
+ }
+
+ type request struct {
+ Snapshot snapshot `json:"snapshot"`
+ }
+
+ reqBody := request{
+ Snapshot: snapshot{},
+ }
+
+ reqBody.Snapshot.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Snapshot.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Snapshot.VolumeID = gophercloud.MaybeString(opts.VolumeID)
+
+ reqBody.Snapshot.Force = opts.Force
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the existing Snapshot with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// Get retrieves the Snapshot with the provided ID. To extract the Snapshot object
+// from the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListOpts hold options for listing Snapshots. It is passed to the
+// snapshots.List function.
+type ListOpts struct {
+ Name string `q:"display_name"`
+ Status string `q:"status"`
+ VolumeID string `q:"volume_id"`
+}
+
+// List returns Snapshots optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, url, createPage)
+}
+
+// UpdateOpts contain options for updating an existing Snapshot. This object is
+// passed to the snapshots.Update function. For more information about the
+// parameters, see the Snapshot object.
+type UpdateMetadataOpts struct {
+ Metadata map[string]interface{}
+}
+
+// Update will update the Snapshot with provided information. To extract the updated
+// Snapshot from the response, call the ExtractMetadata method on the UpdateResult.
+func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts *UpdateMetadataOpts) UpdateMetadataResult {
+ type request struct {
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ }
+
+ reqBody := request{}
+
+ reqBody.Metadata = opts.Metadata
+
+ var res UpdateMetadataResult
+
+ _, res.Err = perigee.Request("PUT", updateMetadataURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/openstack/blockstorage/v1/snapshots/requests_test.go b/openstack/blockstorage/v1/snapshots/requests_test.go
new file mode 100644
index 0000000..d29cc0d
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/requests_test.go
@@ -0,0 +1,198 @@
+package snapshots
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "snapshots": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "display_name": "snapshot-001"
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "display_name": "snapshot-002"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractSnapshots(page)
+ if err != nil {
+ t.Errorf("Failed to extract snapshots: %v", err)
+ return false, err
+ }
+
+ expected := []Snapshot{
+ Snapshot{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "snapshot-001",
+ },
+ Snapshot{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "snapshot-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "snapshot-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "snapshot": {
+ "display_name": "snapshot-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "snapshot-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "snapshot-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestUpdateMetadata(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/123/metadata", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+
+ fmt.Fprintf(w, `
+ {
+ "metadata": {
+ "key": "v1"
+ }
+ }
+ `)
+ })
+
+ expected := map[string]interface{}{"key": "v1"}
+
+ options := &UpdateMetadataOpts{
+ Metadata: map[string]interface{}{
+ "key": "v1",
+ },
+ }
+ actual, err := UpdateMetadata(ServiceClient(), "123", options).ExtractMetadata()
+
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, actual, expected)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/snapshots/results.go b/openstack/blockstorage/v1/snapshots/results.go
new file mode 100644
index 0000000..9509bca
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/results.go
@@ -0,0 +1,98 @@
+package snapshots
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Snapshot contains all the information associated with an OpenStack Snapshot.
+type Snapshot struct {
+ Status string `mapstructure:"status"` // currect status of the Snapshot
+ Name string `mapstructure:"display_name"` // display name
+ Attachments []string `mapstructure:"attachments"` // instances onto which the Snapshot is attached
+ AvailabilityZone string `mapstructure:"availability_zone"` // logical group
+ Bootable string `mapstructure:"bootable"` // is the Snapshot bootable
+ CreatedAt string `mapstructure:"created_at"` // date created
+ Description string `mapstructure:"display_discription"` // display description
+ VolumeType string `mapstructure:"volume_type"` // see VolumeType object for more information
+ SnapshotID string `mapstructure:"snapshot_id"` // ID of the Snapshot from which this Snapshot was created
+ SourceVolID string `mapstructure:"source_volid"` // ID of the Volume from which this Snapshot was created
+ Metadata map[string]string `mapstructure:"metadata"` // user-defined key-value pairs
+ ID string `mapstructure:"id"` // unique identifier
+ Size int `mapstructure:"size"` // size of the Snapshot, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Snapshots.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumes, err := ExtractSnapshots(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumes) == 0, nil
+}
+
+// ExtractSnapshots extracts and returns Snapshots. It is used while iterating over a snapshots.List call.
+func ExtractSnapshots(page pagination.Page) ([]Snapshot, error) {
+ var response struct {
+ Snapshots []Snapshot `json:"snapshots"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.Snapshots, err
+}
+
+// UpdateMetadataResult contains the response body and error from an UpdateMetadata request.
+type UpdateMetadataResult struct {
+ commonResult
+}
+
+// ExtractMetadata returns the metadata from a response from snapshots.UpdateMetadata.
+func (r UpdateMetadataResult) ExtractMetadata() (map[string]interface{}, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ m := r.Resp["metadata"].(map[string]interface{})
+
+ return m, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Snapshot object out of the commonResult object.
+func (r commonResult) Extract() (*Snapshot, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Snapshot *Snapshot `json:"snapshot"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("snapshots: Error decoding snapshots.commonResult: %v", err)
+ }
+ return res.Snapshot, nil
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls.go b/openstack/blockstorage/v1/snapshots/urls.go
new file mode 100644
index 0000000..4d635e8
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls.go
@@ -0,0 +1,27 @@
+package snapshots
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("snapshots")
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("snapshots", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func metadataURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("snapshots", id, "metadata")
+}
+
+func updateMetadataURL(c *gophercloud.ServiceClient, id string) string {
+ return metadataURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/snapshots/urls_test.go b/openstack/blockstorage/v1/snapshots/urls_test.go
new file mode 100644
index 0000000..feacf7f
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/urls_test.go
@@ -0,0 +1,50 @@
+package snapshots
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "snapshots"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestMetadataURL(t *testing.T) {
+ actual := metadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateMetadataURL(t *testing.T) {
+ actual := updateMetadataURL(endpointClient(), "foo")
+ expected := endpoint + "snapshots/foo/metadata"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/snapshots/util.go b/openstack/blockstorage/v1/snapshots/util.go
new file mode 100644
index 0000000..b882875
--- /dev/null
+++ b/openstack/blockstorage/v1/snapshots/util.go
@@ -0,0 +1,20 @@
+package snapshots
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := Get(c, id).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if current.Status == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/openstack/blockstorage/v1/volumes/requests.go b/openstack/blockstorage/v1/volumes/requests.go
new file mode 100644
index 0000000..bca27db
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests.go
@@ -0,0 +1,150 @@
+package volumes
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/racker/perigee"
+)
+
+// CreateOpts contains options for creating a Volume. This object is passed to
+// the volumes.Create function. For more information about these parameters,
+// see the Volume object.
+type CreateOpts struct {
+ Availability string // OPTIONAL
+ Description string // OPTIONAL
+ Metadata map[string]string // OPTIONAL
+ Name string // OPTIONAL
+ Size int // REQUIRED
+ SnapshotID, SourceVolID, ImageID string // REQUIRED (one of them)
+ VolumeType string // OPTIONAL
+}
+
+// Create will create a new Volume based on the values in CreateOpts. To extract
+// the Volume object from the response, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+
+ type volume struct {
+ Availability *string `json:"availability_zone,omitempty"`
+ Description *string `json:"display_description,omitempty"`
+ ImageID *string `json:"imageRef,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ Size *int `json:"size,omitempty"`
+ SnapshotID *string `json:"snapshot_id,omitempty"`
+ SourceVolID *string `json:"source_volid,omitempty"`
+ VolumeType *string `json:"volume_type,omitempty"`
+ }
+
+ type request struct {
+ Volume volume `json:"volume"`
+ }
+
+ reqBody := request{
+ Volume: volume{},
+ }
+
+ reqBody.Volume.Availability = gophercloud.MaybeString(opts.Availability)
+ reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Volume.ImageID = gophercloud.MaybeString(opts.ImageID)
+ reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Volume.Size = gophercloud.MaybeInt(opts.Size)
+ reqBody.Volume.SnapshotID = gophercloud.MaybeString(opts.SnapshotID)
+ reqBody.Volume.SourceVolID = gophercloud.MaybeString(opts.SourceVolID)
+ reqBody.Volume.VolumeType = gophercloud.MaybeString(opts.VolumeType)
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 201},
+ })
+ return res
+}
+
+// Delete will delete the existing Volume with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202, 204},
+ })
+ return err
+}
+
+// Get retrieves the Volume with the provided ID. To extract the Volume object from
+// the response, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", getURL(client, id), perigee.Options{
+ Results: &res.Resp,
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// ListOpts holds options for listing Volumes. It is passed to the volumes.List
+// function.
+type ListOpts struct {
+ AllTenants bool `q:"all_tenants"` // admin-only option. Set it to true to see all tenant volumes.
+ Metadata map[string]string `q:"metadata"` // List only volumes that contain Metadata.
+ Name string `q:"name"` // List only volumes that have Name as the display name.
+ Status string `q:"status"` // List only volumes that have a status of Status.
+}
+
+// List returns Volumes optionally limited by the conditions provided in ListOpts.
+func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query.String()
+ }
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+ return pagination.NewPager(client, listURL(client), createPage)
+}
+
+// UpdateOpts contain options for updating an existing Volume. This object is passed
+// to the volumes.Update function. For more information about the parameters, see
+// the Volume object.
+type UpdateOpts struct {
+ Name string // OPTIONAL
+ Description string // OPTIONAL
+ Metadata map[string]string // OPTIONAL
+}
+
+// Update will update the Volume with provided information. To extract the updated
+// Volume from the response, call the Extract method on the UpdateResult.
+func Update(client *gophercloud.ServiceClient, id string, opts *UpdateOpts) UpdateResult {
+ type update struct {
+ Description *string `json:"display_description,omitempty"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ Name *string `json:"display_name,omitempty"`
+ }
+
+ type request struct {
+ Volume update `json:"volume"`
+ }
+
+ reqBody := request{
+ Volume: update{},
+ }
+
+ reqBody.Volume.Description = gophercloud.MaybeString(opts.Description)
+ reqBody.Volume.Name = gophercloud.MaybeString(opts.Name)
+
+ var res UpdateResult
+
+ _, res.Err = perigee.Request("PUT", updateURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
diff --git a/openstack/blockstorage/v1/volumes/requests_test.go b/openstack/blockstorage/v1/volumes/requests_test.go
new file mode 100644
index 0000000..54ff91d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/requests_test.go
@@ -0,0 +1,160 @@
+package volumes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "volumes": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "display_name": "vol-001"
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "display_name": "vol-002"
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client, &ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volumes: %v", err)
+ return false, err
+ }
+
+ expected := []Volume{
+ Volume{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-001",
+ },
+ Volume{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-002",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "volume": {
+ "display_name": "vol-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ v, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, v.Name, "vol-001")
+ th.AssertEquals(t, v.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume": {
+ "display_name": "vol-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume": {
+ "display_name": "vol-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "vol-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "vol-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/volumes/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumes/results.go b/openstack/blockstorage/v1/volumes/results.go
new file mode 100644
index 0000000..78c863f
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/results.go
@@ -0,0 +1,87 @@
+package volumes
+
+import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Volume contains all the information associated with an OpenStack Volume.
+type Volume struct {
+ Status string `mapstructure:"status"` // current status of the Volume
+ Name string `mapstructure:"display_name"` // display name
+ Attachments []string `mapstructure:"attachments"` // instances onto which the Volume is attached
+ AvailabilityZone string `mapstructure:"availability_zone"` // logical group
+ Bootable string `mapstructure:"bootable"` // is the volume bootable
+ CreatedAt string `mapstructure:"created_at"` // date created
+ Description string `mapstructure:"display_discription"` // display description
+ VolumeType string `mapstructure:"volume_type"` // see VolumeType object for more information
+ SnapshotID string `mapstructure:"snapshot_id"` // ID of the Snapshot from which the Volume was created
+ SourceVolID string `mapstructure:"source_volid"` // ID of the Volume from which the Volume was created
+ Metadata map[string]string `mapstructure:"metadata"` // user-defined key-value pairs
+ ID string `mapstructure:"id"` // unique identifier
+ Size int `mapstructure:"size"` // size of the Volume, in GB
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volumes.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumes, err := ExtractVolumes(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumes) == 0, nil
+}
+
+// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
+func ExtractVolumes(page pagination.Page) ([]Volume, error) {
+ var response struct {
+ Volumes []Volume `json:"volumes"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.Volumes, err
+}
+
+// UpdateResult contains the response body and error from an Update request.
+type UpdateResult struct {
+ commonResult
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Volume object out of the commonResult object.
+func (r commonResult) Extract() (*Volume, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Volume *Volume `json:"volume"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("volumes: Error decoding volumes.commonResult: %v", err)
+ }
+ return res.Volume, nil
+}
diff --git a/openstack/blockstorage/v1/volumes/urls.go b/openstack/blockstorage/v1/volumes/urls.go
new file mode 100644
index 0000000..29629a1
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls.go
@@ -0,0 +1,23 @@
+package volumes
+
+import "github.com/rackspace/gophercloud"
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("volumes")
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return createURL(c)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("volumes", id)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return deleteURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumes/urls_test.go b/openstack/blockstorage/v1/volumes/urls_test.go
new file mode 100644
index 0000000..a95270e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/urls_test.go
@@ -0,0 +1,44 @@
+package volumes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "volumes"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestUpdateURL(t *testing.T) {
+ actual := updateURL(endpointClient(), "foo")
+ expected := endpoint + "volumes/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/blockstorage/v1/volumes/util.go b/openstack/blockstorage/v1/volumes/util.go
new file mode 100644
index 0000000..0e2f16e
--- /dev/null
+++ b/openstack/blockstorage/v1/volumes/util.go
@@ -0,0 +1,20 @@
+package volumes
+
+import (
+ "github.com/rackspace/gophercloud"
+)
+
+func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := Get(c, id).Extract()
+ if err != nil {
+ return false, err
+ }
+
+ if current.Status == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests.go b/openstack/blockstorage/v1/volumetypes/requests.go
new file mode 100644
index 0000000..afe650d
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests.go
@@ -0,0 +1,75 @@
+package volumetypes
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// CreateOpts are options for creating a volume type.
+type CreateOpts struct {
+ // OPTIONAL. See VolumeType.
+ ExtraSpecs map[string]interface{}
+ // OPTIONAL. See VolumeType.
+ Name string
+}
+
+// Create will create a new volume, optionally wih CreateOpts. To extract the
+// created volume type object, call the Extract method on the CreateResult.
+func Create(client *gophercloud.ServiceClient, opts *CreateOpts) CreateResult {
+ type volumeType struct {
+ ExtraSpecs map[string]interface{} `json:"extra_specs,omitempty"`
+ Name *string `json:"name,omitempty"`
+ }
+
+ type request struct {
+ VolumeType volumeType `json:"volume_type"`
+ }
+
+ reqBody := request{
+ VolumeType: volumeType{},
+ }
+
+ reqBody.VolumeType.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.VolumeType.ExtraSpecs = opts.ExtraSpecs
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", createURL(client), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 201},
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ })
+ return res
+}
+
+// Delete will delete the volume type with the provided ID.
+func Delete(client *gophercloud.ServiceClient, id string) error {
+ _, err := perigee.Request("DELETE", deleteURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{202},
+ })
+ return err
+}
+
+// Get will retrieve the volume type with the provided ID. To extract the volume
+// type from the result, call the Extract method on the GetResult.
+func Get(client *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, err := perigee.Request("GET", getURL(client, id), perigee.Options{
+ MoreHeaders: client.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200},
+ Results: &res.Resp,
+ })
+ res.Err = err
+ return res
+}
+
+// List returns all volume types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ return ListResult{pagination.SinglePageBase(r)}
+ }
+
+ return pagination.NewPager(client, listURL(client), createPage)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/requests_test.go b/openstack/blockstorage/v1/volumetypes/requests_test.go
new file mode 100644
index 0000000..a9c6512
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/requests_test.go
@@ -0,0 +1,172 @@
+package volumetypes
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const TokenID = "123"
+
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{
+ TokenID: TokenID,
+ },
+ Endpoint: th.Endpoint(),
+ }
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "volume_types": [
+ {
+ "id": "289da7f8-6440-407c-9fb4-7db01ec49164",
+ "name": "vol-type-001",
+ "extra_specs": {
+ "capabilities": "gpu"
+ }
+ },
+ {
+ "id": "96c3bda7-c82a-4f50-be73-ca7621794835",
+ "name": "vol-type-002",
+ "extra_specs": {}
+ }
+ ]
+ }
+ `)
+ })
+
+ client := ServiceClient()
+ count := 0
+
+ List(client).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVolumeTypes(page)
+ if err != nil {
+ t.Errorf("Failed to extract volume types: %v", err)
+ return false, err
+ }
+
+ expected := []VolumeType{
+ VolumeType{
+ ID: "289da7f8-6440-407c-9fb4-7db01ec49164",
+ Name: "vol-type-001",
+ ExtraSpecs: map[string]interface{}{
+ "capabilities": "gpu",
+ },
+ },
+ VolumeType{
+ ID: "96c3bda7-c82a-4f50-be73-ca7621794835",
+ Name: "vol-type-002",
+ ExtraSpecs: map[string]interface{}{},
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+{
+ "volume_type": {
+ "name": "vol-type-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "extra_specs": {
+ "serverNumber": "2"
+ }
+ }
+}
+ `)
+ })
+
+ vt, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, vt.ExtraSpecs, map[string]interface{}{"serverNumber": "2"})
+ th.AssertEquals(t, vt.Name, "vol-type-001")
+ th.AssertEquals(t, vt.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "volume_type": {
+ "name": "vol-type-001"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "volume_type": {
+ "name": "vol-type-001",
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22"
+ }
+}
+ `)
+ })
+
+ options := &CreateOpts{Name: "vol-type-001"}
+ n, err := Create(ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "vol-type-001")
+ th.AssertEquals(t, n.ID, "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/types/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ err := Delete(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/results.go b/openstack/blockstorage/v1/volumetypes/results.go
new file mode 100644
index 0000000..8e5932a
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/results.go
@@ -0,0 +1,72 @@
+package volumetypes
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// VolumeType contains all information associated with an OpenStack Volume Type.
+type VolumeType struct {
+ ExtraSpecs map[string]interface{} `json:"extra_specs" mapstructure:"extra_specs"` // user-defined metadata
+ ID string `json:"id" mapstructure:"id"` // unique identifier
+ Name string `json:"name" mapstructure:"name"` // display name
+}
+
+// CreateResult contains the response body and error from a Create request.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult contains the response body and error from a Get request.
+type GetResult struct {
+ commonResult
+}
+
+// ListResult is a pagination.Pager that is returned from a call to the List function.
+type ListResult struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty returns true if a ListResult contains no Volume Types.
+func (r ListResult) IsEmpty() (bool, error) {
+ volumeTypes, err := ExtractVolumeTypes(r)
+ if err != nil {
+ return true, err
+ }
+ return len(volumeTypes) == 0, nil
+}
+
+// ExtractVolumeTypes extracts and returns Volume Types.
+func ExtractVolumeTypes(page pagination.Page) ([]VolumeType, error) {
+ var response struct {
+ VolumeTypes []VolumeType `mapstructure:"volume_types"`
+ }
+
+ err := mapstructure.Decode(page.(ListResult).Body, &response)
+ return response.VolumeTypes, err
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract will get the Volume Type object out of the commonResult object.
+func (r commonResult) Extract() (*VolumeType, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ VolumeType *VolumeType `json:"volume_type" mapstructure:"volume_type"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Volume Type: %v", err)
+ }
+
+ return res.VolumeType, nil
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls.go b/openstack/blockstorage/v1/volumetypes/urls.go
new file mode 100644
index 0000000..cf8367b
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls.go
@@ -0,0 +1,19 @@
+package volumetypes
+
+import "github.com/rackspace/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("types")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return listURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("types", id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return getURL(c, id)
+}
diff --git a/openstack/blockstorage/v1/volumetypes/urls_test.go b/openstack/blockstorage/v1/volumetypes/urls_test.go
new file mode 100644
index 0000000..44016e2
--- /dev/null
+++ b/openstack/blockstorage/v1/volumetypes/urls_test.go
@@ -0,0 +1,38 @@
+package volumetypes
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ th "github.com/rackspace/gophercloud/testhelper"
+)
+
+const endpoint = "http://localhost:57909"
+
+func endpointClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{Endpoint: endpoint}
+}
+
+func TestListURL(t *testing.T) {
+ actual := listURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestCreateURL(t *testing.T) {
+ actual := createURL(endpointClient())
+ expected := endpoint + "types"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestGetURL(t *testing.T) {
+ actual := getURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
+
+func TestDeleteURL(t *testing.T) {
+ actual := deleteURL(endpointClient(), "foo")
+ expected := endpoint + "types/foo"
+ th.AssertEquals(t, expected, actual)
+}
diff --git a/openstack/client.go b/openstack/client.go
index 7b17a1c..c8bdb0f 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -169,15 +169,12 @@
v3Client.Endpoint = endpoint
}
- result, err := tokens3.Create(v3Client, options, nil)
+ token, err := tokens3.Create(v3Client, options, nil).Extract()
if err != nil {
return err
}
+ client.TokenID = token.ID
- client.TokenID, err = result.TokenID()
- if err != nil {
- return err
- }
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return v3endpointLocator(v3Client, opts)
}
@@ -307,3 +304,13 @@
}
return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
}
+
+// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
+func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ eo.ApplyDefaults("volume")
+ url, err := client.EndpointLocator(eo)
+ if err != nil {
+ return nil, err
+ }
+ return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
+}
diff --git a/openstack/common/extensions/errors.go b/openstack/common/extensions/errors.go
old mode 100644
new mode 100755
diff --git a/openstack/common/extensions/requests.go b/openstack/common/extensions/requests.go
old mode 100644
new mode 100755
index ed5ea4b..000151b
--- a/openstack/common/extensions/requests.go
+++ b/openstack/common/extensions/requests.go
@@ -9,12 +9,11 @@
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) GetResult {
var res GetResult
- _, err := perigee.Request("GET", ExtensionURL(c, alias), perigee.Options{
+ _, res.Err = perigee.Request("GET", ExtensionURL(c, alias), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
Results: &res.Resp,
OkCodes: []int{200},
})
- res.Err = err
return res
}
diff --git a/openstack/common/extensions/results.go b/openstack/common/extensions/results.go
old mode 100644
new mode 100755
diff --git a/openstack/common/extensions/urls_test.go b/openstack/common/extensions/urls_test.go
old mode 100644
new mode 100755
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index 6c72a0a..20ca52e 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -1,6 +1,7 @@
package servers
import (
+ "encoding/base64"
"fmt"
"github.com/racker/perigee"
@@ -17,14 +18,120 @@
return pagination.NewPager(client, detailURL(client), createPage)
}
+// CreateOptsBuilder describes struct types that can be accepted by the Create call.
+// The CreateOpts struct in this package does.
+type CreateOptsBuilder interface {
+ ToServerCreateMap() map[string]interface{}
+}
+
+// Network is used within CreateOpts to control a new server's network attachments.
+type Network struct {
+ // UUID of a nova-network to attach to the newly provisioned server.
+ // Required unless Port is provided.
+ UUID string
+
+ // Port of a neutron network to attach to the newly provisioned server.
+ // Required unless UUID is provided.
+ Port string
+
+ // FixedIP [optional] specifies a fixed IPv4 address to be used on this network.
+ FixedIP string
+}
+
+// CreateOpts specifies server creation parameters.
+type CreateOpts struct {
+ // Name [required] is the name to assign to the newly launched server.
+ Name string
+
+ // ImageRef [required] is the ID or full URL to the image that contains the server's OS and initial state.
+ // Optional if using the boot-from-volume extension.
+ ImageRef string
+
+ // FlavorRef [required] is the ID or full URL to the flavor that describes the server's specs.
+ FlavorRef string
+
+ // SecurityGroups [optional] lists the names of the security groups to which this server should belong.
+ SecurityGroups []string
+
+ // UserData [optional] contains configuration information or scripts to use upon launch.
+ // Create will base64-encode it for you.
+ UserData []byte
+
+ // AvailabilityZone [optional] in which to launch the server.
+ AvailabilityZone string
+
+ // Networks [optional] dictates how this server will be attached to available networks.
+ // By default, the server will be attached to all isolated networks for the tenant.
+ Networks []Network
+
+ // Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
+ Metadata map[string]string
+
+ // Personality [optional] includes the path and contents of a file to inject into the server at launch.
+ // The maximum size of the file is 255 bytes (decoded).
+ Personality []byte
+
+ // ConfigDrive [optional] enables metadata injection through a configuration drive.
+ ConfigDrive bool
+}
+
+// ToServerCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToServerCreateMap() map[string]interface{} {
+ server := make(map[string]interface{})
+
+ server["name"] = opts.Name
+ server["imageRef"] = opts.ImageRef
+ server["flavorRef"] = opts.FlavorRef
+
+ if opts.UserData != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.UserData)
+ server["user_data"] = &encoded
+ }
+ if opts.Personality != nil {
+ encoded := base64.StdEncoding.EncodeToString(opts.Personality)
+ server["personality"] = &encoded
+ }
+ if opts.ConfigDrive {
+ server["config_drive"] = "true"
+ }
+ if opts.AvailabilityZone != "" {
+ server["availability_zone"] = opts.AvailabilityZone
+ }
+ if opts.Metadata != nil {
+ server["metadata"] = opts.Metadata
+ }
+
+ if len(opts.SecurityGroups) > 0 {
+ securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
+ for i, groupName := range opts.SecurityGroups {
+ securityGroups[i] = map[string]interface{}{"name": groupName}
+ }
+ }
+ if len(opts.Networks) > 0 {
+ networks := make([]map[string]interface{}, len(opts.Networks))
+ for i, net := range opts.Networks {
+ networks[i] = make(map[string]interface{})
+ if net.UUID != "" {
+ networks[i]["uuid"] = net.UUID
+ }
+ if net.Port != "" {
+ networks[i]["port"] = net.Port
+ }
+ if net.FixedIP != "" {
+ networks[i]["fixed_ip"] = net.FixedIP
+ }
+ }
+ }
+
+ return map[string]interface{}{"server": server}
+}
+
// Create requests a server to be provisioned to the user in the current tenant.
-func Create(client *gophercloud.ServiceClient, opts map[string]interface{}) CreateResult {
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var result CreateResult
_, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
- Results: &result.Resp,
- ReqBody: map[string]interface{}{
- "server": opts,
- },
+ Results: &result.Resp,
+ ReqBody: opts.ToServerCreateMap(),
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{202},
})
@@ -50,14 +157,46 @@
return result
}
+// UpdateOptsLike allows extentions to add additional attributes to the Update request.
+type UpdateOptsLike interface {
+ ToServerUpdateMap() map[string]interface{}
+}
+
+// UpdateOpts specifies the base attributes that may be updated on an existing server.
+type UpdateOpts struct {
+ // Name [optional] changes the displayed name of the server.
+ // The server host name will *not* change.
+ // Server names are not constrained to be unique, even within the same tenant.
+ Name string
+
+ // AccessIPv4 [optional] provides a new IPv4 address for the instance.
+ AccessIPv4 string
+
+ // AccessIPv6 [optional] provides a new IPv6 address for the instance.
+ AccessIPv6 string
+}
+
+// ToServerUpdateMap formats an UpdateOpts structure into a request body.
+func (opts UpdateOpts) ToServerUpdateMap() map[string]interface{} {
+ server := make(map[string]string)
+ if opts.Name != "" {
+ server["name"] = opts.Name
+ }
+ if opts.AccessIPv4 != "" {
+ server["accessIPv4"] = opts.AccessIPv4
+ }
+ if opts.AccessIPv6 != "" {
+ server["accessIPv6"] = opts.AccessIPv6
+ }
+ return map[string]interface{}{"server": server}
+}
+
// Update requests that various attributes of the indicated server be changed.
-func Update(client *gophercloud.ServiceClient, id string, opts map[string]interface{}) UpdateResult {
+func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsLike) UpdateResult {
var result UpdateResult
_, result.Err = perigee.Request("PUT", serverURL(client, id), perigee.Options{
- Results: &result.Resp,
- ReqBody: map[string]interface{}{
- "server": opts,
- },
+ Results: &result.Resp,
+ ReqBody: opts.ToServerUpdateMap(),
MoreHeaders: client.Provider.AuthenticatedHeaders(),
})
return result
diff --git a/openstack/compute/v2/servers/requests_test.go b/openstack/compute/v2/servers/requests_test.go
index fa1a2f5..01c4cdb 100644
--- a/openstack/compute/v2/servers/requests_test.go
+++ b/openstack/compute/v2/servers/requests_test.go
@@ -88,10 +88,10 @@
})
client := serviceClient()
- actual, err := Create(client, map[string]interface{}{
- "name": "derp",
- "imageRef": "f90f6034-2570-4974-8351-6b49732ef2eb",
- "flavorRef": "1",
+ actual, err := Create(client, CreateOpts{
+ Name: "derp",
+ ImageRef: "f90f6034-2570-4974-8351-6b49732ef2eb",
+ FlavorRef: "1",
}).Extract()
if err != nil {
t.Fatalf("Unexpected Create error: %v", err)
@@ -154,9 +154,7 @@
})
client := serviceClient()
- actual, err := Update(client, "1234asdf", map[string]interface{}{
- "name": "new-name",
- }).Extract()
+ actual, err := Update(client, "1234asdf", UpdateOpts{Name: "new-name"}).Extract()
if err != nil {
t.Fatalf("Unexpected Update error: %v", err)
}
diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go
index 186d0fc..eb52573 100644
--- a/openstack/identity/v3/endpoints/requests.go
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -9,14 +9,6 @@
"github.com/rackspace/gophercloud/pagination"
)
-// maybeString returns nil for empty strings and nil for empty.
-func maybeString(original string) *string {
- if original != "" {
- return &original
- }
- return nil
-}
-
// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint.
type EndpointOpts struct {
Availability gophercloud.Availability
@@ -28,7 +20,7 @@
// Create inserts a new Endpoint into the service catalog.
// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required.
-func Create(client *gophercloud.ServiceClient, opts EndpointOpts) (*Endpoint, error) {
+func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult {
// Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output.
type endpoint struct {
Interface string `json:"interface"`
@@ -42,22 +34,18 @@
Endpoint endpoint `json:"endpoint"`
}
- type response struct {
- Endpoint Endpoint `json:"endpoint"`
- }
-
// Ensure that EndpointOpts is fully populated.
if opts.Availability == "" {
- return nil, ErrAvailabilityRequired
+ return createErr(ErrAvailabilityRequired)
}
if opts.Name == "" {
- return nil, ErrNameRequired
+ return createErr(ErrNameRequired)
}
if opts.URL == "" {
- return nil, ErrURLRequired
+ return createErr(ErrURLRequired)
}
if opts.ServiceID == "" {
- return nil, ErrServiceIDRequired
+ return createErr(ErrServiceIDRequired)
}
// Populate the request body.
@@ -69,20 +57,16 @@
ServiceID: opts.ServiceID,
},
}
- reqBody.Endpoint.Region = maybeString(opts.Region)
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
- var respBody response
- _, err := perigee.Request("POST", getListURL(client), perigee.Options{
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
- Results: &respBody,
+ Results: &result.Resp,
OkCodes: []int{201},
})
- if err != nil {
- return nil, err
- }
-
- return &respBody.Endpoint, nil
+ return result
}
// ListOpts allows finer control over the the endpoints returned by a List call.
@@ -114,13 +98,13 @@
return EndpointPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
}
- u := getListURL(client) + utils.BuildQuery(q)
+ u := listURL(client) + utils.BuildQuery(q)
return pagination.NewPager(client, u, createPage)
}
// Update changes an existing endpoint with new data.
// All fields are optional in the provided EndpointOpts.
-func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) (*Endpoint, error) {
+func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult {
type endpoint struct {
Interface *string `json:"interface,omitempty"`
Name *string `json:"name,omitempty"`
@@ -133,34 +117,26 @@
Endpoint endpoint `json:"endpoint"`
}
- type response struct {
- Endpoint Endpoint `json:"endpoint"`
- }
-
reqBody := request{Endpoint: endpoint{}}
- reqBody.Endpoint.Interface = maybeString(string(opts.Availability))
- reqBody.Endpoint.Name = maybeString(opts.Name)
- reqBody.Endpoint.Region = maybeString(opts.Region)
- reqBody.Endpoint.URL = maybeString(opts.URL)
- reqBody.Endpoint.ServiceID = maybeString(opts.ServiceID)
+ reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability))
+ reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name)
+ reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
+ reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL)
+ reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID)
- var respBody response
- _, err := perigee.Request("PATCH", getEndpointURL(client, endpointID), perigee.Options{
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", endpointURL(client, endpointID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
- Results: &respBody,
+ Results: &result.Resp,
OkCodes: []int{200},
})
- if err != nil {
- return nil, err
- }
-
- return &respBody.Endpoint, nil
+ return result
}
// Delete removes an endpoint from the service catalog.
func Delete(client *gophercloud.ServiceClient, endpointID string) error {
- _, err := perigee.Request("DELETE", getEndpointURL(client, endpointID), perigee.Options{
+ _, err := perigee.Request("DELETE", endpointURL(client, endpointID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
diff --git a/openstack/identity/v3/endpoints/requests_test.go b/openstack/identity/v3/endpoints/requests_test.go
index 241d175..c30bd55 100644
--- a/openstack/identity/v3/endpoints/requests_test.go
+++ b/openstack/identity/v3/endpoints/requests_test.go
@@ -59,13 +59,13 @@
client := serviceClient()
- result, err := Create(client, EndpointOpts{
+ actual, err := Create(client, EndpointOpts{
Availability: gophercloud.AvailabilityPublic,
Name: "the-endiest-of-points",
Region: "underground",
URL: "https://1.2.3.4:9000/",
ServiceID: "asdfasdfasdfasdf",
- })
+ }).Extract()
if err != nil {
t.Fatalf("Unable to create an endpoint: %v", err)
}
@@ -79,8 +79,8 @@
URL: "https://1.2.3.4:9000/",
}
- if !reflect.DeepEqual(result, expected) {
- t.Errorf("Expected %#v, was %#v", expected, result)
+ if !reflect.DeepEqual(actual, expected) {
+ t.Errorf("Expected %#v, was %#v", expected, actual)
}
}
@@ -205,7 +205,7 @@
actual, err := Update(client, "12", EndpointOpts{
Name: "renamed",
Region: "somewhere-else",
- })
+ }).Extract()
if err != nil {
t.Fatalf("Unexpected error from Update: %v", err)
}
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
index 8da90f3..2dd2357 100644
--- a/openstack/identity/v3/endpoints/results.go
+++ b/openstack/identity/v3/endpoints/results.go
@@ -1,11 +1,51 @@
package endpoints
import (
+ "fmt"
+
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Endpoint, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Endpoint `json:"endpoint"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Endpoint: %v", err)
+ }
+
+ return &res.Endpoint, nil
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly wraps an error in a CreateResult.
+func createErr(err error) CreateResult {
+ return CreateResult{commonResult{gophercloud.CommonResult{Err: err}}}
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
// Endpoint describes the entry point for another service's API.
type Endpoint struct {
ID string `mapstructure:"id" json:"id"`
diff --git a/openstack/identity/v3/endpoints/urls.go b/openstack/identity/v3/endpoints/urls.go
index 011cc01..547d7b1 100644
--- a/openstack/identity/v3/endpoints/urls.go
+++ b/openstack/identity/v3/endpoints/urls.go
@@ -2,10 +2,10 @@
import "github.com/rackspace/gophercloud"
-func getListURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("endpoints")
}
-func getEndpointURL(client *gophercloud.ServiceClient, endpointID string) string {
+func endpointURL(client *gophercloud.ServiceClient, endpointID string) string {
return client.ServiceURL("endpoints", endpointID)
}
diff --git a/openstack/identity/v3/endpoints/urls_test.go b/openstack/identity/v3/endpoints/urls_test.go
index fe1fb4a..0b183b7 100644
--- a/openstack/identity/v3/endpoints/urls_test.go
+++ b/openstack/identity/v3/endpoints/urls_test.go
@@ -8,7 +8,7 @@
func TestGetListURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getListURL(&client)
+ url := listURL(&client)
if url != "http://localhost:5000/v3/endpoints" {
t.Errorf("Unexpected list URL generated: [%s]", url)
}
@@ -16,7 +16,7 @@
func TestGetEndpointURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getEndpointURL(&client, "1234")
+ url := endpointURL(&client, "1234")
if url != "http://localhost:5000/v3/endpoints/1234" {
t.Errorf("Unexpected service URL generated: [%s]", url)
}
diff --git a/openstack/identity/v3/services/requests.go b/openstack/identity/v3/services/requests.go
index 405a9a6..7816aca 100644
--- a/openstack/identity/v3/services/requests.go
+++ b/openstack/identity/v3/services/requests.go
@@ -14,25 +14,21 @@
}
// Create adds a new service of the requested type to the catalog.
-func Create(client *gophercloud.ServiceClient, serviceType string) (*Service, error) {
+func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult {
type request struct {
Type string `json:"type"`
}
req := request{Type: serviceType}
- var resp response
- _, err := perigee.Request("POST", getListURL(client), perigee.Options{
+ var result CreateResult
+ _, result.Err = perigee.Request("POST", listURL(client), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &req,
- Results: &resp,
+ Results: &result.Resp,
OkCodes: []int{201},
})
- if err != nil {
- return nil, err
- }
-
- return &resp.Service, nil
+ return result
}
// ListOpts allows you to query the List method.
@@ -54,7 +50,7 @@
if opts.PerPage != 0 {
q["perPage"] = strconv.Itoa(opts.PerPage)
}
- u := getListURL(client) + utils.BuildQuery(q)
+ u := listURL(client) + utils.BuildQuery(q)
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
return ServicePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
@@ -64,45 +60,38 @@
}
// Get returns additional information about a service, given its ID.
-func Get(client *gophercloud.ServiceClient, serviceID string) (*Service, error) {
- var resp response
- _, err := perigee.Request("GET", getServiceURL(client, serviceID), perigee.Options{
+func Get(client *gophercloud.ServiceClient, serviceID string) GetResult {
+ var result GetResult
+ _, result.Err = perigee.Request("GET", serviceURL(client, serviceID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
- Results: &resp,
+ Results: &result.Resp,
OkCodes: []int{200},
})
- if err != nil {
- return nil, err
- }
- return &resp.Service, nil
+ return result
}
-// Update changes the service type of an existing service.s
-func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) (*Service, error) {
+// Update changes the service type of an existing service.
+func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult {
type request struct {
Type string `json:"type"`
}
req := request{Type: serviceType}
- var resp response
- _, err := perigee.Request("PATCH", getServiceURL(client, serviceID), perigee.Options{
+ var result UpdateResult
+ _, result.Err = perigee.Request("PATCH", serviceURL(client, serviceID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
ReqBody: &req,
- Results: &resp,
+ Results: &result.Resp,
OkCodes: []int{200},
})
- if err != nil {
- return nil, err
- }
-
- return &resp.Service, nil
+ return result
}
// Delete removes an existing service.
// It either deletes all associated endpoints, or fails until all endpoints are deleted.
func Delete(client *gophercloud.ServiceClient, serviceID string) error {
- _, err := perigee.Request("DELETE", getServiceURL(client, serviceID), perigee.Options{
+ _, err := perigee.Request("DELETE", serviceURL(client, serviceID), perigee.Options{
MoreHeaders: client.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
diff --git a/openstack/identity/v3/services/requests_test.go b/openstack/identity/v3/services/requests_test.go
index 804f034..a3d345b 100644
--- a/openstack/identity/v3/services/requests_test.go
+++ b/openstack/identity/v3/services/requests_test.go
@@ -45,7 +45,7 @@
client := serviceClient()
- result, err := Create(client, "compute")
+ result, err := Create(client, "compute").Extract()
if err != nil {
t.Fatalf("Unexpected error from Create: %v", err)
}
@@ -161,7 +161,7 @@
client := serviceClient()
- result, err := Get(client, "12345")
+ result, err := Get(client, "12345").Extract()
if err != nil {
t.Fatalf("Error fetching service information: %v", err)
}
@@ -202,13 +202,13 @@
client := serviceClient()
- result, err := Update(client, "12345", "lasermagic")
+ result, err := Update(client, "12345", "lasermagic").Extract()
if err != nil {
t.Fatalf("Unable to update service: %v", err)
}
if result.ID != "12345" {
-
+ t.Fatalf("Expected ID 12345, was %s", result.ID)
}
}
diff --git a/openstack/identity/v3/services/results.go b/openstack/identity/v3/services/results.go
index cccea8e..b4e7bd2 100644
--- a/openstack/identity/v3/services/results.go
+++ b/openstack/identity/v3/services/results.go
@@ -1,11 +1,52 @@
package services
import (
+ "fmt"
+
+ "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service.
+// An error is returned if the original call or the extraction failed.
+func (r commonResult) Extract() (*Service, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Service `json:"service"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Service: %v", err)
+ }
+
+ return &res.Service, nil
+}
+
+// CreateResult is the deferred result of a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult is the deferred result of a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult is the deferred result of an Update call.
+type UpdateResult struct {
+ commonResult
+}
+
// Service is the result of a list or information query.
type Service struct {
Description *string `json:"description,omitempty"`
diff --git a/openstack/identity/v3/services/urls.go b/openstack/identity/v3/services/urls.go
index 3556238..85443a4 100644
--- a/openstack/identity/v3/services/urls.go
+++ b/openstack/identity/v3/services/urls.go
@@ -2,10 +2,10 @@
import "github.com/rackspace/gophercloud"
-func getListURL(client *gophercloud.ServiceClient) string {
+func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("services")
}
-func getServiceURL(client *gophercloud.ServiceClient, serviceID string) string {
+func serviceURL(client *gophercloud.ServiceClient, serviceID string) string {
return client.ServiceURL("services", serviceID)
}
diff --git a/openstack/identity/v3/services/urls_test.go b/openstack/identity/v3/services/urls_test.go
index deded69..5a31b32 100644
--- a/openstack/identity/v3/services/urls_test.go
+++ b/openstack/identity/v3/services/urls_test.go
@@ -6,17 +6,17 @@
"github.com/rackspace/gophercloud"
)
-func TestGetListURL(t *testing.T) {
+func TestListURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getListURL(&client)
+ url := listURL(&client)
if url != "http://localhost:5000/v3/services" {
t.Errorf("Unexpected list URL generated: [%s]", url)
}
}
-func TestGetServiceURL(t *testing.T) {
+func TestServiceURL(t *testing.T) {
client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
- url := getServiceURL(&client, "1234")
+ url := serviceURL(&client, "1234")
if url != "http://localhost:5000/v3/services/1234" {
t.Errorf("Unexpected service URL generated: [%s]", url)
}
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index ab4ae05..c8587b6 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -20,7 +20,7 @@
}
// Create authenticates and either generates a new token, or changes the Scope of an existing token.
-func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) (gophercloud.AuthResults, error) {
+func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
type domainReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
@@ -73,13 +73,13 @@
// Test first for unrecognized arguments.
if options.APIKey != "" {
- return nil, ErrAPIKeyProvided
+ return createErr(ErrAPIKeyProvided)
}
if options.TenantID != "" {
- return nil, ErrTenantIDProvided
+ return createErr(ErrTenantIDProvided)
}
if options.TenantName != "" {
- return nil, ErrTenantNameProvided
+ return createErr(ErrTenantNameProvided)
}
if options.Password == "" {
@@ -87,16 +87,16 @@
// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
// parameters.
if options.Username != "" {
- return nil, ErrUsernameWithToken
+ return createErr(ErrUsernameWithToken)
}
if options.UserID != "" {
- return nil, ErrUserIDWithToken
+ return createErr(ErrUserIDWithToken)
}
if options.DomainID != "" {
- return nil, ErrDomainIDWithToken
+ return createErr(ErrDomainIDWithToken)
}
if options.DomainName != "" {
- return nil, ErrDomainNameWithToken
+ return createErr(ErrDomainNameWithToken)
}
// Configure the request for Token authentication.
@@ -106,7 +106,7 @@
}
} else {
// If no password or token ID are available, authentication can't continue.
- return nil, ErrMissingPassword
+ return createErr(ErrMissingPassword)
}
} else {
// Password authentication.
@@ -114,23 +114,23 @@
// At least one of Username and UserID must be specified.
if options.Username == "" && options.UserID == "" {
- return nil, ErrUsernameOrUserID
+ return createErr(ErrUsernameOrUserID)
}
if options.Username != "" {
// If Username is provided, UserID may not be provided.
if options.UserID != "" {
- return nil, ErrUsernameOrUserID
+ return createErr(ErrUsernameOrUserID)
}
// Either DomainID or DomainName must also be specified.
if options.DomainID == "" && options.DomainName == "" {
- return nil, ErrDomainIDOrDomainName
+ return createErr(ErrDomainIDOrDomainName)
}
if options.DomainID != "" {
if options.DomainName != "" {
- return nil, ErrDomainIDOrDomainName
+ return createErr(ErrDomainIDOrDomainName)
}
// Configure the request for Username and Password authentication with a DomainID.
@@ -158,10 +158,10 @@
if options.UserID != "" {
// If UserID is specified, neither DomainID nor DomainName may be.
if options.DomainID != "" {
- return nil, ErrDomainIDWithUserID
+ return createErr(ErrDomainIDWithUserID)
}
if options.DomainName != "" {
- return nil, ErrDomainNameWithUserID
+ return createErr(ErrDomainNameWithUserID)
}
// Configure the request for UserID and Password authentication.
@@ -177,10 +177,10 @@
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if scope.DomainID == "" && scope.DomainName == "" {
- return nil, ErrScopeDomainIDOrDomainName
+ return createErr(ErrScopeDomainIDOrDomainName)
}
if scope.ProjectID != "" {
- return nil, ErrScopeProjectIDOrProjectName
+ return createErr(ErrScopeProjectIDOrProjectName)
}
if scope.DomainID != "" {
@@ -205,10 +205,10 @@
} else if scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if scope.DomainID != "" {
- return nil, ErrScopeProjectIDAlone
+ return createErr(ErrScopeProjectIDAlone)
}
if scope.DomainName != "" {
- return nil, ErrScopeProjectIDAlone
+ return createErr(ErrScopeProjectIDAlone)
}
// ProjectID
@@ -218,7 +218,7 @@
} else if scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if scope.DomainName != "" {
- return nil, ErrScopeDomainIDOrDomainName
+ return createErr(ErrScopeDomainIDOrDomainName)
}
// DomainID
@@ -226,51 +226,45 @@
Domain: &domainReq{ID: &scope.DomainID},
}
} else if scope.DomainName != "" {
- return nil, ErrScopeDomainName
+ return createErr(ErrScopeDomainName)
} else {
- return nil, ErrScopeEmpty
+ return createErr(ErrScopeEmpty)
}
}
- var result TokenCreateResult
- response, err := perigee.Request("POST", getTokenURL(c), perigee.Options{
+ var result CreateResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("POST", tokenURL(c), perigee.Options{
ReqBody: &req,
- Results: &result.response,
+ Results: &result.Resp,
OkCodes: []int{201},
})
- if err != nil {
- return nil, err
+ if result.Err != nil {
+ return result
}
-
- // Extract the token ID from the response, if present.
- result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
- return &result, nil
+ result.header = response.HttpResponse.Header
+ return result
}
// Get validates and retrieves information about another token.
-func Get(c *gophercloud.ServiceClient, token string) (*TokenCreateResult, error) {
- var result TokenCreateResult
-
- response, err := perigee.Request("GET", getTokenURL(c), perigee.Options{
+func Get(c *gophercloud.ServiceClient, token string) GetResult {
+ var result GetResult
+ var response *perigee.Response
+ response, result.Err = perigee.Request("GET", tokenURL(c), perigee.Options{
MoreHeaders: subjectTokenHeaders(c, token),
- Results: &result.response,
+ Results: &result.Resp,
OkCodes: []int{200, 203},
})
-
- if err != nil {
- return nil, err
+ if result.Err != nil {
+ return result
}
-
- // Extract the token ID from the response, if present.
- result.tokenID = response.HttpResponse.Header.Get("X-Subject-Token")
-
- return &result, nil
+ result.header = response.HttpResponse.Header
+ return result
}
// Validate determines if a specified token is valid or not.
func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
- response, err := perigee.Request("HEAD", getTokenURL(c), perigee.Options{
+ response, err := perigee.Request("HEAD", tokenURL(c), perigee.Options{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{204, 404},
})
@@ -283,7 +277,7 @@
// Revoke immediately makes specified token invalid.
func Revoke(c *gophercloud.ServiceClient, token string) error {
- _, err := perigee.Request("DELETE", getTokenURL(c), perigee.Options{
+ _, err := perigee.Request("DELETE", tokenURL(c), perigee.Options{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{204},
})
diff --git a/openstack/identity/v3/tokens/requests_test.go b/openstack/identity/v3/tokens/requests_test.go
index d0813ac..367c73c 100644
--- a/openstack/identity/v3/tokens/requests_test.go
+++ b/openstack/identity/v3/tokens/requests_test.go
@@ -29,10 +29,14 @@
testhelper.TestJSONRequest(t, r, requestJSON)
w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `{}`)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
})
- _, err := Create(&client, options, scope)
+ _, err := Create(&client, options, scope).Extract()
if err != nil {
t.Errorf("Create returned an error: %v", err)
}
@@ -50,7 +54,7 @@
client.Provider.TokenID = "abcdef123456"
}
- _, err := Create(&client, options, scope)
+ _, err := Create(&client, options, scope).Extract()
if err == nil {
t.Errorf("Create did NOT return an error")
}
@@ -250,18 +254,21 @@
w.Header().Add("X-Subject-Token", "aaa111")
w.WriteHeader(http.StatusCreated)
- fmt.Fprintf(w, `{}`)
+ fmt.Fprintf(w, `{
+ "token": {
+ "expires_at": "2014-10-02T13:45:00.000000Z"
+ }
+ }`)
})
options := gophercloud.AuthOptions{UserID: "me", Password: "shhh"}
- result, err := Create(&client, options, nil)
+ token, err := Create(&client, options, nil).Extract()
if err != nil {
- t.Errorf("Create returned an error: %v", err)
+ t.Fatalf("Create returned an error: %v", err)
}
- token, _ := result.TokenID()
- if token != "aaa111" {
- t.Errorf("Expected token to be aaa111, but was %s", token)
+ if token.ID != "aaa111" {
+ t.Errorf("Expected token to be aaa111, but was %s", token.ID)
}
}
@@ -413,19 +420,14 @@
`)
})
- result, err := Get(&client, "abcdef12345")
+ token, err := Get(&client, "abcdef12345").Extract()
if err != nil {
t.Errorf("Info returned an error: %v", err)
}
- expires, err := result.ExpiresAt()
- if err != nil {
- t.Errorf("Error extracting token expiration time: %v", err)
- }
-
expected, _ := time.Parse(time.UnixDate, "Fri Aug 29 13:10:01 UTC 2014")
- if expires != expected {
- t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), expires.Format(time.UnixDate))
+ if token.ExpiresAt != expected {
+ t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), token.ExpiresAt.Format(time.UnixDate))
}
}
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index 145a43d..e96da51 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -1,44 +1,78 @@
package tokens
import (
+ "net/http"
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
)
-// TokenCreateResult contains the document structure returned from a Create call.
-type TokenCreateResult struct {
- response map[string]interface{}
- tokenID string
+// commonResult is the deferred result of a Create or a Get call.
+type commonResult struct {
+ gophercloud.CommonResult
+
+ // header stores the headers from the original HTTP response because token responses are returned in an X-Subject-Token header.
+ header http.Header
}
-// TokenID retrieves a token generated by a Create call from an token creation response.
-func (r *TokenCreateResult) TokenID() (string, error) {
- return r.tokenID, nil
-}
-
-// ExpiresAt retrieves the token expiration time.
-func (r *TokenCreateResult) ExpiresAt() (time.Time, error) {
- type tokenResp struct {
- ExpiresAt string `mapstructure:"expires_at"`
+// Extract interprets a commonResult as a Token.
+func (r commonResult) Extract() (*Token, error) {
+ if r.Err != nil {
+ return nil, r.Err
}
- type response struct {
- Token tokenResp `mapstructure:"token"`
+ var response struct {
+ Token struct {
+ ExpiresAt string `mapstructure:"expires_at"`
+ } `mapstructure:"token"`
}
- var resp response
- err := mapstructure.Decode(r.response, &resp)
+ var token Token
+
+ // Parse the token itself from the stored headers.
+ token.ID = r.header.Get("X-Subject-Token")
+
+ err := mapstructure.Decode(r.Resp, &response)
if err != nil {
- return time.Time{}, err
+ return nil, err
}
// Attempt to parse the timestamp.
- ts, err := time.Parse(gophercloud.RFC3339Milli, resp.Token.ExpiresAt)
+ token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
if err != nil {
- return time.Time{}, err
+ return nil, err
}
- return ts, nil
+ return &token, nil
+}
+
+// CreateResult is the deferred response from a Create call.
+type CreateResult struct {
+ commonResult
+}
+
+// createErr quickly creates a CreateResult that reports an error.
+func createErr(err error) CreateResult {
+ return CreateResult{
+ commonResult: commonResult{
+ CommonResult: gophercloud.CommonResult{Err: err},
+ header: nil,
+ },
+ }
+}
+
+// GetResult is the deferred response from a Get call.
+type GetResult struct {
+ commonResult
+}
+
+// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
+// Each Token is valid for a set length of time.
+type Token struct {
+ // ID is the issued token.
+ ID string
+
+ // ExpiresAt is the timestamp at which this token will no longer be accepted.
+ ExpiresAt time.Time
}
diff --git a/openstack/identity/v3/tokens/results_test.go b/openstack/identity/v3/tokens/results_test.go
deleted file mode 100644
index 669db61..0000000
--- a/openstack/identity/v3/tokens/results_test.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package tokens
-
-import (
- "testing"
- "time"
-)
-
-func TestTokenID(t *testing.T) {
- result := TokenCreateResult{tokenID: "1234"}
-
- token, _ := result.TokenID()
- if token != "1234" {
- t.Errorf("Expected tokenID of 1234, got %s", token)
- }
-}
-
-func TestExpiresAt(t *testing.T) {
- resp := map[string]interface{}{
- "token": map[string]string{
- "expires_at": "2013-02-02T18:30:59.000000Z",
- },
- }
-
- result := TokenCreateResult{
- tokenID: "1234",
- response: resp,
- }
-
- expected, _ := time.Parse(time.UnixDate, "Sat Feb 2 18:30:59 UTC 2013")
- actual, err := result.ExpiresAt()
- if err != nil {
- t.Errorf("Error extraction expiration time: %v", err)
- }
- if actual != expected {
- t.Errorf("Expected expiration time %s, but was %s", expected.Format(time.UnixDate), actual.Format(time.UnixDate))
- }
-}
diff --git a/openstack/identity/v3/tokens/urls.go b/openstack/identity/v3/tokens/urls.go
index 5b47c02..360b60a 100644
--- a/openstack/identity/v3/tokens/urls.go
+++ b/openstack/identity/v3/tokens/urls.go
@@ -2,6 +2,6 @@
import "github.com/rackspace/gophercloud"
-func getTokenURL(c *gophercloud.ServiceClient) string {
+func tokenURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("auth", "tokens")
}
diff --git a/openstack/identity/v3/tokens/urls_test.go b/openstack/identity/v3/tokens/urls_test.go
index 5ff8bc6..549c398 100644
--- a/openstack/identity/v3/tokens/urls_test.go
+++ b/openstack/identity/v3/tokens/urls_test.go
@@ -14,7 +14,7 @@
client := gophercloud.ServiceClient{Endpoint: testhelper.Endpoint()}
expected := testhelper.Endpoint() + "auth/tokens"
- actual := getTokenURL(&client)
+ actual := tokenURL(&client)
if actual != expected {
t.Errorf("Expected URL %s, but was %s", expected, actual)
}
diff --git a/openstack/networking/v2/apiversions/requests_test.go b/openstack/networking/v2/apiversions/requests_test.go
index 7c713e1..c49b509 100644
--- a/openstack/networking/v2/apiversions/requests_test.go
+++ b/openstack/networking/v2/apiversions/requests_test.go
@@ -5,29 +5,18 @@
"net/http"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
-const TokenID = "123"
-
-func ServiceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{
- TokenID: TokenID,
- },
- Endpoint: th.Endpoint(),
- }
-}
-
func TestListVersions(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -51,7 +40,7 @@
count := 0
- ListVersions(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ ListVersions(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractAPIVersions(page)
if err != nil {
@@ -82,7 +71,7 @@
th.Mux.HandleFunc("/v2.0/", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -127,7 +116,7 @@
count := 0
- ListVersionResources(ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
+ ListVersionResources(fake.ServiceClient(), "v2.0").EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractVersionResources(page)
if err != nil {
diff --git a/openstack/networking/v2/extensions/delegate_test.go b/openstack/networking/v2/extensions/delegate_test.go
old mode 100644
new mode 100755
index 969ea80..8de8906
--- a/openstack/networking/v2/extensions/delegate_test.go
+++ b/openstack/networking/v2/extensions/delegate_test.go
@@ -5,30 +5,19 @@
"net/http"
"testing"
- "github.com/rackspace/gophercloud"
common "github.com/rackspace/gophercloud/openstack/common/extensions"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
-const TokenID = "123"
-
-func ServiceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{
- TokenID: TokenID,
- },
- Endpoint: th.Endpoint(),
- }
-}
-
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
@@ -50,7 +39,7 @@
count := 0
- List(ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractExtensions(page)
if err != nil {
@@ -86,7 +75,7 @@
th.Mux.HandleFunc("/extensions/agent", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -104,7 +93,7 @@
}
`)
- ext, err := Get(ServiceClient(), "agent").Extract()
+ ext, err := Get(fake.ServiceClient(), "agent").Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, ext.Updated, "2013-02-03T10:00:00-00:00")
diff --git a/openstack/networking/v2/extensions/external/doc.go b/openstack/networking/v2/extensions/external/doc.go
new file mode 100755
index 0000000..d244f26
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/doc.go
@@ -0,0 +1 @@
+package external
diff --git a/openstack/networking/v2/extensions/external/requests.go b/openstack/networking/v2/extensions/external/requests.go
new file mode 100644
index 0000000..f195cfa
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/requests.go
@@ -0,0 +1,41 @@
+package external
+
+import "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Nothing AdminState = nil
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+type CreateOpts struct {
+ Parent networks.CreateOpts
+ External bool
+}
+
+func (o CreateOpts) ToNetworkCreateMap() map[string]map[string]interface{} {
+ outer := o.Parent.ToNetworkCreateMap()
+
+ outer["network"]["router:external"] = o.External
+
+ return outer
+}
+
+type UpdateOpts struct {
+ Parent networks.UpdateOpts
+ External bool
+}
+
+func (o UpdateOpts) ToNetworkUpdateMap() map[string]map[string]interface{} {
+ outer := o.Parent.ToNetworkUpdateMap()
+
+ outer["network"]["router:external"] = o.External
+
+ return outer
+}
diff --git a/openstack/networking/v2/extensions/external/results.go b/openstack/networking/v2/extensions/external/results.go
new file mode 100644
index 0000000..4cd2133
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/results.go
@@ -0,0 +1,102 @@
+package external
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// NetworkExternal represents a decorated form of a Network with based on the
+// "external-net" extension.
+type NetworkExternal struct {
+ // UUID for the network
+ ID string `mapstructure:"id" json:"id"`
+
+ // Human-readable name for the network. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // The administrative state of network. If false (down), the network 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"`
+
+ // Subnets associated with this network.
+ Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // Specifies whether the network resource can be accessed by any tenant or not.
+ Shared bool `mapstructure:"shared" json:"shared"`
+
+ // Specifies whether the network is an external network or not.
+ External bool `mapstructure:"router:external" json:"router:external"`
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExternal, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ Network *NetworkExternal `json:"network"`
+ }
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
+ }
+ return res.Network, nil
+}
+
+// ExtractCreate decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExternal, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ Network *NetworkExternal `json:"network"`
+ }
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
+ }
+ return res.Network, nil
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExternal, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ Network *NetworkExternal `json:"network"`
+ }
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
+ }
+ return res.Network, nil
+}
+
+// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
+// extracts the elements into a slice of NetworkExtAttrs structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractList(page pagination.Page) ([]NetworkExternal, error) {
+ var resp struct {
+ Networks []NetworkExternal `mapstructure:"networks" json:"networks"`
+ }
+
+ err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Networks, nil
+}
diff --git a/openstack/networking/v2/extensions/external/results_test.go b/openstack/networking/v2/extensions/external/results_test.go
new file mode 100644
index 0000000..41bc0c8
--- /dev/null
+++ b/openstack/networking/v2/extensions/external/results_test.go
@@ -0,0 +1,230 @@
+package external
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "networks": [
+ {
+ "admin_state_up": true,
+ "id": "0f38d5ad-10a6-428f-a5fc-825cfe0f1970",
+ "name": "net1",
+ "router:external": false,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "25778974-48a8-46e7-8998-9dc8c70d2f06"
+ ],
+ "tenant_id": "b575417a6c444a6eb5cc3a58eb4f714a"
+ },
+ {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "ext_net",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractList(page)
+ if err != nil {
+ t.Errorf("Failed to extract networks: %v", err)
+ return false, err
+ }
+
+ expected := []NetworkExternal{
+ NetworkExternal{
+ Status: "ACTIVE",
+ Subnets: []string{"25778974-48a8-46e7-8998-9dc8c70d2f06"},
+ Name: "net1",
+ AdminStateUp: true,
+ TenantID: "b575417a6c444a6eb5cc3a58eb4f714a",
+ Shared: false,
+ ID: "0f38d5ad-10a6-428f-a5fc-825cfe0f1970",
+ External: false,
+ },
+ NetworkExternal{
+ Status: "ACTIVE",
+ Subnets: []string{"2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"},
+ Name: "ext_net",
+ AdminStateUp: true,
+ TenantID: "5eb8995cf717462c9df8d1edfa498010",
+ Shared: false,
+ ID: "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ External: true,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "ext_net",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+}
+ `)
+ })
+
+ res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ n, err := ExtractGet(res)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, n.External)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "admin_state_up": true,
+ "name": "ext_net",
+ "router:external": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "ext_net",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{networks.CreateOpts{Name: "ext_net", AdminStateUp: Up}, true}
+ res := networks.Create(fake.ServiceClient(), options)
+
+ n, err := ExtractCreate(res)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, n.External)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "router:external": true,
+ "name": "new_name"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "admin_state_up": true,
+ "id": "8d05a1b1-297a-46ca-8974-17debf51ca3c",
+ "name": "new_name",
+ "router:external": true,
+ "shared": false,
+ "status": "ACTIVE",
+ "subnets": [
+ "2f1fb918-9b0e-4bf9-9a50-6cebbb4db2c5"
+ ],
+ "tenant_id": "5eb8995cf717462c9df8d1edfa498010"
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{networks.UpdateOpts{Name: "new_name"}, true}
+ res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+ n, err := ExtractUpdate(res)
+
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, true, n.External)
+}
diff --git a/openstack/networking/v2/extensions/layer3/doc.go b/openstack/networking/v2/extensions/layer3/doc.go
new file mode 100644
index 0000000..d533458
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/doc.go
@@ -0,0 +1,5 @@
+// Package layer3 provides access to the Layer-3 networking extension for the
+// OpenStack Neutron service. This extension allows API users to route packets
+// between subnets, forward packets from internal networks to external ones,
+// and access instances from external networks through floating IPs.
+package layer3
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
new file mode 100644
index 0000000..22a6cae
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
@@ -0,0 +1,190 @@
+package floatingips
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 network 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"`
+ FloatingNetworkID string `q:"floating_network_id"`
+ PortID string `q:"port_id"`
+ FixedIP string `q:"fixed_ip_address"`
+ FloatingIP string `q:"floating_ip_address"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// floating IP resources. It accepts a ListOpts struct, which allows you to
+// filter and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return FloatingIPPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// CreateOpts contains all the values needed to create a new floating IP
+// resource. The only required fields are FloatingNetworkID and PortID which
+// refer to the external network and internal port respectively.
+type CreateOpts struct {
+ FloatingNetworkID string
+ FloatingIP string
+ PortID string
+ FixedIP string
+ TenantID string
+}
+
+var (
+ errFloatingNetworkIDRequired = fmt.Errorf("A NetworkID is required")
+ errPortIDRequired = fmt.Errorf("A PortID is required")
+)
+
+// Create accepts a CreateOpts struct and uses the values provided to create a
+// new floating IP resource. You can create floating IPs on external networks
+// only. If you provide a FloatingNetworkID which refers to a network that is
+// not external (i.e. its `router:external' attribute is False), the operation
+// will fail and return a 400 error.
+//
+// If you do not specify a FloatingIP address value, the operation will
+// automatically allocate an available address for the new resource. If you do
+// choose to specify one, it must fall within the subnet range for the external
+// network - otherwise the operation returns a 400 error. If the FloatingIP
+// address is already in use, the operation returns a 409 error code.
+//
+// You can associate the new resource with an internal port by using the PortID
+// field. If you specify a PortID that is not valid, the operation will fail and
+// return 404 error code.
+//
+// You must also configure an IP address for the port associated with the PortID
+// you have provided - this is what the FixedIP refers to: an IP fixed to a port.
+// Because a port might be associated with multiple IP addresses, you can use
+// the FixedIP field to associate a particular IP address rather than have the
+// API assume for you. If you specify an IP address that is not valid, the
+// operation will fail and return a 400 error code. If the PortID and FixedIP
+// are already associated with another resource, the operation will fail and
+// returns a 409 error code.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate
+ if opts.FloatingNetworkID == "" {
+ res.Err = errFloatingNetworkIDRequired
+ return res
+ }
+ if opts.PortID == "" {
+ res.Err = errPortIDRequired
+ return res
+ }
+
+ // Define structures
+ type floatingIP struct {
+ FloatingNetworkID string `json:"floating_network_id"`
+ FloatingIP string `json:"floating_ip_address,omitempty"`
+ PortID string `json:"port_id"`
+ FixedIP string `json:"fixed_ip_address,omitempty"`
+ TenantID string `json:"tenant_id,omitempty"`
+ }
+ type request struct {
+ FloatingIP floatingIP `json:"floatingip"`
+ }
+
+ // Populate request body
+ reqBody := request{FloatingIP: floatingIP{
+ FloatingNetworkID: opts.FloatingNetworkID,
+ PortID: opts.PortID,
+ FixedIP: opts.FixedIP,
+ TenantID: opts.TenantID,
+ }}
+
+ // Send request to API
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular floating IP resource based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a floating IP resource. The
+// only value that can be updated is which internal port the floating IP is
+// linked to. To associate the floating IP with a new internal port, provide its
+// ID. To disassociate the floating IP from all ports, provide an empty string.
+type UpdateOpts struct {
+ PortID string
+}
+
+// Update allows floating IP resources to be updated. Currently, the only way to
+// "update" a floating IP is to associate it with a new internal port, or
+// disassociated it from all ports. See UpdateOpts for instructions of how to
+// do this.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type floatingIP struct {
+ PortID *string `json:"port_id"`
+ }
+
+ type request struct {
+ FloatingIP floatingIP `json:"floatingip"`
+ }
+
+ var portID *string
+ if opts.PortID == "" {
+ portID = nil
+ } else {
+ portID = &opts.PortID
+ }
+
+ reqBody := request{FloatingIP: floatingIP{PortID: portID}}
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular floating IP resource. Please
+// ensure this is what you want - you can also disassociate the IP from existing
+// internal ports.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
new file mode 100644
index 0000000..b9153fc
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests_test.go
@@ -0,0 +1,278 @@
+package floatingips
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "floatingips": [
+ {
+ "floating_network_id": "6d67c30a-ddb4-49a1-bec3-a65b286b4170",
+ "router_id": null,
+ "fixed_ip_address": null,
+ "floating_ip_address": "192.0.0.4",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "status": "DOWN",
+ "port_id": null,
+ "id": "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e"
+ },
+ {
+ "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64",
+ "router_id": "0a24cb83-faf5-4d7f-b723-3144ed8a2167",
+ "fixed_ip_address": "192.0.0.2",
+ "floating_ip_address": "10.0.0.3",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "status": "DOWN",
+ "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25",
+ "id": "ada25a95-f321-4f59-b0e0-f3a970dd3d63"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractFloatingIPs(page)
+ if err != nil {
+ t.Errorf("Failed to extract floating IPs: %v", err)
+ return false, err
+ }
+
+ expected := []FloatingIP{
+ FloatingIP{
+ FloatingNetworkID: "6d67c30a-ddb4-49a1-bec3-a65b286b4170",
+ FixedIP: "",
+ FloatingIP: "192.0.0.4",
+ TenantID: "017d8de156df4177889f31a9bd6edc00",
+ Status: "DOWN",
+ PortID: "",
+ ID: "2f95fd2b-9f6a-4e8e-9e9a-2cbe286cbf9e",
+ },
+ FloatingIP{
+ FloatingNetworkID: "90f742b1-6d17-487b-ba95-71881dbc0b64",
+ FixedIP: "192.0.0.2",
+ FloatingIP: "10.0.0.3",
+ TenantID: "017d8de156df4177889f31a9bd6edc00",
+ Status: "DOWN",
+ PortID: "74a342ce-8e07-4e91-880c-9f834b68fa25",
+ ID: "ada25a95-f321-4f59-b0e0-f3a970dd3d63",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "floatingip": {
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": "10.0.0.3",
+ "floating_ip_address": "",
+ "port_id": "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{
+ FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57",
+ PortID: "ce705c24-c1ef-408a-bda3-7bbd946164ab",
+ }
+
+ ip, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID)
+ th.AssertEquals(t, "4969c491a3c74ee4af974e6d800c62de", ip.TenantID)
+ th.AssertEquals(t, "376da547-b977-4cfe-9cba-275c80debf57", ip.FloatingNetworkID)
+ th.AssertEquals(t, "", ip.FloatingIP)
+ th.AssertEquals(t, "ce705c24-c1ef-408a-bda3-7bbd946164ab", ip.PortID)
+ th.AssertEquals(t, "10.0.0.3", ip.FixedIP)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "floating_network_id": "90f742b1-6d17-487b-ba95-71881dbc0b64",
+ "fixed_ip_address": "192.0.0.2",
+ "floating_ip_address": "10.0.0.3",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "status": "DOWN",
+ "port_id": "74a342ce-8e07-4e91-880c-9f834b68fa25",
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ ip, err := Get(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "90f742b1-6d17-487b-ba95-71881dbc0b64", ip.FloatingNetworkID)
+ th.AssertEquals(t, "10.0.0.3", ip.FloatingIP)
+ th.AssertEquals(t, "74a342ce-8e07-4e91-880c-9f834b68fa25", ip.PortID)
+ th.AssertEquals(t, "192.0.0.2", ip.FixedIP)
+ th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", ip.TenantID)
+ th.AssertEquals(t, "DOWN", ip.Status)
+ th.AssertEquals(t, "2f245a7b-796b-4f26-9cf9-9e82d248fda7", ip.ID)
+}
+
+func TestAssociate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "floatingip": {
+ "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": null,
+ "floating_ip_address": "172.24.4.228",
+ "port_id": "423abc8d-2991-4a55-ba98-2aaea84cc72e",
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{PortID: "423abc8d-2991-4a55-ba98-2aaea84cc72e"}).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, "423abc8d-2991-4a55-ba98-2aaea84cc72e", ip.PortID)
+}
+
+func TestDisassociate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "floatingip": {
+ "port_id": null
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "router_id": "d23abc8d-2991-4a55-ba98-2aaea84cc72f",
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": null,
+ "floating_ip_address": "172.24.4.228",
+ "port_id": null,
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ ip, err := Update(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7", UpdateOpts{}).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, "", ip.FixedIP)
+ th.AssertDeepEquals(t, "", ip.PortID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/floatingips/2f245a7b-796b-4f26-9cf9-9e82d248fda7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "2f245a7b-796b-4f26-9cf9-9e82d248fda7")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/results.go b/openstack/networking/v2/extensions/layer3/floatingips/results.go
new file mode 100644
index 0000000..4857c92
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -0,0 +1,142 @@
+package floatingips
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// FloatingIP represents a floating IP resource. A floating IP is an external
+// IP address that is mapped to an internal port and, optionally, a specific
+// IP address on a private network. In other words, it enables access to an
+// instance on a private network from an external network. For thsi reason,
+// floating IPs can only be defined on networks where the `router:external'
+// attribute (provided by the external network extension) is set to True.
+type FloatingIP struct {
+ // Unique identifier for the floating IP instance.
+ ID string `json:"id" mapstructure:"id"`
+
+ // UUID of the external network where the floating IP is to be created.
+ FloatingNetworkID string `json:"floating_network_id" mapstructure:"floating_network_id"`
+
+ // Address of the floating IP on the external network.
+ FloatingIP string `json:"floating_ip_address" mapstructure:"floating_ip_address"`
+
+ // UUID of the port on an internal network that is associated with the floating IP.
+ PortID string `json:"port_id" mapstructure:"port_id"`
+
+ // The specific IP address of the internal port which should be associated
+ // with the floating IP.
+ FixedIP string `json:"fixed_ip_address" mapstructure:"fixed_ip_address"`
+
+ // Owner of the floating IP. Only admin users can specify a tenant identifier
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+
+ // The condition of the API resource.
+ Status string `json:"status" mapstructure:"status"`
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract a result and extracts a FloatingIP resource.
+func (r commonResult) Extract() (*FloatingIP, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ FloatingIP *FloatingIP `json:"floatingip"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron floating IP: %v", err)
+ }
+
+ return res.FloatingIP, nil
+}
+
+// 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 an update operation.
+type DeleteResult commonResult
+
+// FloatingIPPage is the page returned by a pager when traversing over a
+// collection of floating IPs.
+type FloatingIPPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of floating IPs 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 FloatingIPPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"floatingips_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// IsEmpty checks whether a NetworkPage struct is empty.
+func (p FloatingIPPage) IsEmpty() (bool, error) {
+ is, err := ExtractFloatingIPs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage struct,
+// and extracts the elements into a slice of FloatingIP structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractFloatingIPs(page pagination.Page) ([]FloatingIP, error) {
+ var resp struct {
+ FloatingIPs []FloatingIP `mapstructure:"floatingips" json:"floatingips"`
+ }
+
+ err := mapstructure.Decode(page.(FloatingIPPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.FloatingIPs, nil
+}
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
new file mode 100644
index 0000000..dbe3f9f
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/floatingips/urls.go
@@ -0,0 +1,16 @@
+package floatingips
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ resourcePath = "floatingips"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go
new file mode 100755
index 0000000..dbfd36b
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -0,0 +1,246 @@
+package routers
+
+import (
+ "errors"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 network 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"`
+ Status string `q:"status"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// 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 ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return RouterPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// CreateOpts contains all the values needed to create a new router. There are
+// no required values.
+type CreateOpts struct {
+ Name string
+ AdminStateUp *bool
+ TenantID string
+ GatewayInfo *GatewayInfo
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// logical router. When it is created, the router does not have an internal
+// interface - it is not associated to any subnet.
+//
+// You can optionally specify an external gateway for a router using the
+// GatewayInfo struct. The external gateway for the router must be plugged into
+// an external network (it is external if its `router:external' field is set to
+// true).
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ type router struct {
+ Name *string `json:"name,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ TenantID *string `json:"tenant_id,omitempty"`
+ GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
+ }
+
+ type request struct {
+ Router router `json:"router"`
+ }
+
+ reqBody := request{Router: router{
+ Name: gophercloud.MaybeString(opts.Name),
+ AdminStateUp: opts.AdminStateUp,
+ TenantID: gophercloud.MaybeString(opts.TenantID),
+ }}
+
+ if opts.GatewayInfo != nil {
+ reqBody.Router.GatewayInfo = opts.GatewayInfo
+ }
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// Get retrieves a particular router based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a router.
+type UpdateOpts struct {
+ Name string
+ AdminStateUp *bool
+ GatewayInfo *GatewayInfo
+}
+
+// Update allows routers to be updated. You can update the name, administrative
+// state, and the external gateway. For more information about how to set the
+// external gateway for a router, see Create. This operation does not enable
+// the update of router interfaces. To do this, use the AddInterface and
+// RemoveInterface functions.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type router struct {
+ Name *string `json:"name,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
+ }
+
+ type request struct {
+ Router router `json:"router"`
+ }
+
+ reqBody := request{Router: router{
+ Name: gophercloud.MaybeString(opts.Name),
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ if opts.GatewayInfo != nil {
+ reqBody.Router.GatewayInfo = opts.GatewayInfo
+ }
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular router based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
+
+var errInvalidInterfaceOpts = errors.New("When adding a router interface you must provide either a subnet ID or a port ID")
+
+// InterfaceOpts allow you to work with operations that either add or remote
+// an internal interface from a router.
+type InterfaceOpts struct {
+ SubnetID string
+ PortID string
+}
+
+// AddInterface attaches a subnet to an internal router interface. You must
+// specify either a SubnetID or PortID in the request body. If you specify both,
+// the operation will fail and an error will be returned.
+//
+// If you specify a SubnetID, the gateway IP address for that particular subnet
+// is used to create the router interface. Alternatively, if you specify a
+// PortID, the IP address associated with the port is used to create the router
+// interface.
+//
+// If you reference a port that is associated with multiple IP addresses, or
+// if the port is associated with zero IP addresses, the operation will fail and
+// a 400 Bad Request error will be returned.
+//
+// If you reference a port already in use, the operation will fail and a 409
+// Conflict error will be returned.
+//
+// The PortID that is returned after using Extract() on the result of this
+// operation can either be the same PortID passed in or, on the other hand, the
+// identifier of a new port created by this operation. After the operation
+// completes, the device ID of the port is set to the router ID, and the
+// device owner attribute is set to `network:router_interface'.
+func AddInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult {
+ var res InterfaceResult
+
+ // Validate
+ if (opts.SubnetID == "" && opts.PortID == "") || (opts.SubnetID != "" && opts.PortID != "") {
+ res.Err = errInvalidInterfaceOpts
+ return res
+ }
+
+ type request struct {
+ SubnetID string `json:"subnet_id,omitempty"`
+ PortID string `json:"port_id,omitempty"`
+ }
+
+ body := request{SubnetID: opts.SubnetID, PortID: opts.PortID}
+
+ _, res.Err = perigee.Request("PUT", addInterfaceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &body,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
+
+// RemoveInterface removes an internal router interface, which detaches a
+// subnet from the router. You must specify either a SubnetID or PortID, since
+// these values are used to identify the router interface to remove.
+//
+// Unlike AddInterface, you can also specify both a SubnetID and PortID. If you
+// choose to specify both, the subnet ID must correspond to the subnet ID of
+// the first IP address on the port specified by the port ID. Otherwise, the
+// operation will fail and return a 409 Conflict error.
+//
+// If the router, subnet or port which are referenced do not exist or are not
+// visible to you, the operation will fail and a 404 Not Found error will be
+// returned. After this operation completes, the port connecting the router
+// with the subnet is removed from the subnet for the network.
+func RemoveInterface(c *gophercloud.ServiceClient, id string, opts InterfaceOpts) InterfaceResult {
+ var res InterfaceResult
+
+ type request struct {
+ SubnetID string `json:"subnet_id,omitempty"`
+ PortID string `json:"port_id,omitempty"`
+ }
+
+ body := request{SubnetID: opts.SubnetID, PortID: opts.PortID}
+
+ _, res.Err = perigee.Request("PUT", removeInterfaceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &body,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+
+ return res
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests_test.go b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
new file mode 100755
index 0000000..56a0d74
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/requests_test.go
@@ -0,0 +1,327 @@
+package routers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/routers", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "routers": [
+ {
+ "status": "ACTIVE",
+ "external_gateway_info": null,
+ "name": "second_routers",
+ "admin_state_up": true,
+ "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "id": "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b"
+ },
+ {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"
+ },
+ "name": "router1",
+ "admin_state_up": true,
+ "tenant_id": "33a40233088643acb66ff6eb0ebea679",
+ "id": "a9254bdb-2613-4a13-ac4c-adc581fba50d"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractRouters(page)
+ if err != nil {
+ t.Errorf("Failed to extract routers: %v", err)
+ return false, err
+ }
+
+ expected := []Router{
+ Router{
+ Status: "ACTIVE",
+ GatewayInfo: GatewayInfo{NetworkID: ""},
+ AdminStateUp: true,
+ Name: "second_routers",
+ ID: "7177abc4-5ae9-4bb7-b0d4-89e94a4abf3b",
+ TenantID: "6b96ff0cb17a4b859e1e575d221683d3",
+ },
+ Router{
+ Status: "ACTIVE",
+ GatewayInfo: GatewayInfo{NetworkID: "3c5bcddd-6af9-4e6b-9c3e-c153e521cab8"},
+ AdminStateUp: true,
+ Name: "router1",
+ ID: "a9254bdb-2613-4a13-ac4c-adc581fba50d",
+ TenantID: "33a40233088643acb66ff6eb0ebea679",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "router":{
+ "name": "foo_router",
+ "admin_state_up": false,
+ "external_gateway_info":{
+ "network_id":"8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ }
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "router": {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ },
+ "name": "foo_router",
+ "admin_state_up": false,
+ "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+ }
+}
+ `)
+ })
+
+ asu := false
+ gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+
+ options := CreateOpts{
+ Name: "foo_router",
+ AdminStateUp: &asu,
+ GatewayInfo: &gwi,
+ }
+ r, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "foo_router", r.Name)
+ th.AssertEquals(t, false, r.AdminStateUp)
+ th.AssertDeepEquals(t, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}, r.GatewayInfo)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/a07eea83-7710-4860-931b-5fe220fae533", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "router": {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "85d76829-6415-48ff-9c63-5c5ca8c61ac6"
+ },
+ "name": "router1",
+ "admin_state_up": true,
+ "tenant_id": "d6554fe62e2f41efbb6e026fad5c1542",
+ "id": "a07eea83-7710-4860-931b-5fe220fae533"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "a07eea83-7710-4860-931b-5fe220fae533").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Status, "ACTIVE")
+ th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "85d76829-6415-48ff-9c63-5c5ca8c61ac6"})
+ th.AssertEquals(t, n.Name, "router1")
+ th.AssertEquals(t, n.AdminStateUp, true)
+ th.AssertEquals(t, n.TenantID, "d6554fe62e2f41efbb6e026fad5c1542")
+ th.AssertEquals(t, n.ID, "a07eea83-7710-4860-931b-5fe220fae533")
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "router": {
+ "name": "new_name",
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ }
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "router": {
+ "status": "ACTIVE",
+ "external_gateway_info": {
+ "network_id": "8ca37218-28ff-41cb-9b10-039601ea7e6b"
+ },
+ "name": "new_name",
+ "admin_state_up": true,
+ "tenant_id": "6b96ff0cb17a4b859e1e575d221683d3",
+ "id": "8604a0de-7f6b-409a-a47c-a1cc7bc77b2e"
+ }
+}
+ `)
+ })
+
+ gwi := GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"}
+ options := UpdateOpts{Name: "new_name", GatewayInfo: &gwi}
+
+ n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "new_name")
+ th.AssertDeepEquals(t, n.GatewayInfo, GatewayInfo{NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b"})
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestAddInterface(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/add_router_interface", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1"
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31",
+ "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770"
+}
+`)
+ })
+
+ opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+ res, err := AddInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID)
+ th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID)
+ th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID)
+ th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
+}
+
+func TestRemoveInterface(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/routers/4e8e5957-649f-477b-9e5b-f1f75b21c03c/remove_router_interface", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "subnet_id": "a2f1f29d-571b-4533-907f-5803ab96ead1"
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "subnet_id": "0d32a837-8069-4ec3-84c4-3eef3e10b188",
+ "tenant_id": "017d8de156df4177889f31a9bd6edc00",
+ "port_id": "3f990102-4485-4df1-97a0-2c35bdb85b31",
+ "id": "9a83fa11-8da5-436e-9afe-3d3ac5ce7770"
+}
+`)
+ })
+
+ opts := InterfaceOpts{SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1"}
+ res, err := RemoveInterface(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "0d32a837-8069-4ec3-84c4-3eef3e10b188", res.SubnetID)
+ th.AssertEquals(t, "017d8de156df4177889f31a9bd6edc00", res.TenantID)
+ th.AssertEquals(t, "3f990102-4485-4df1-97a0-2c35bdb85b31", res.PortID)
+ th.AssertEquals(t, "9a83fa11-8da5-436e-9afe-3d3ac5ce7770", res.ID)
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go
new file mode 100755
index 0000000..d14578c
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -0,0 +1,184 @@
+package routers
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// GatewayInfo represents the information of an external gateway for any
+// particular network router.
+type GatewayInfo struct {
+ NetworkID string `json:"network_id" mapstructure:"network_id"`
+}
+
+// Router represents a Neutron router. A router is a logical entity that
+// forwards packets across internal subnets and NATs (network address
+// translation) them on external networks through an appropriate gateway.
+//
+// A router has an interface for each subnet with which it is associated. By
+// default, the IP address of such interface is the subnet's gateway IP. Also,
+// whenever a router is associated with a subnet, a port for that router
+// interface is added to the subnet's network.
+type Router struct {
+ // Indicates whether or not a router is currently operational.
+ Status string `json:"status" mapstructure:"status"`
+
+ // Information on external gateway for the router.
+ GatewayInfo GatewayInfo `json:"external_gateway_info" mapstructure:"external_gateway_info"`
+
+ // Administrative state of the router.
+ AdminStateUp bool `json:"admin_state_up" mapstructure:"admin_state_up"`
+
+ // Human readable name for the router. Does not have to be unique.
+ Name string `json:"name" mapstructure:"name"`
+
+ // Unique identifier for the router.
+ ID string `json:"id" mapstructure:"id"`
+
+ // Owner of the router. Only admin users can specify a tenant identifier
+ // other than its own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// RouterPage is the page returned by a pager when traversing over a
+// collection of routers.
+type RouterPage 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 RouterPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"routers_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p RouterPage) IsEmpty() (bool, error) {
+ is, err := ExtractRouters(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractRouters 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 ExtractRouters(page pagination.Page) ([]Router, error) {
+ var resp struct {
+ Routers []Router `mapstructure:"routers" json:"routers"`
+ }
+
+ err := mapstructure.Decode(page.(RouterPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Routers, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Router, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Router *Router `json:"router"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron router: %v", err)
+ }
+
+ return res.Router, nil
+}
+
+// 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 commonResult
+
+// InterfaceInfo represents information about a particular router interface. As
+// mentioned above, in order for a router to forward to a subnet, it needs an
+// interface.
+type InterfaceInfo struct {
+ // The ID of the subnet which this interface is associated with.
+ SubnetID string `json:"subnet_id" mapstructure:"subnet_id"`
+
+ // The ID of the port that is a part of the subnet.
+ PortID string `json:"port_id" mapstructure:"port_id"`
+
+ // The UUID of the interface.
+ ID string `json:"id" mapstructure:"id"`
+
+ // Owner of the interface.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// InterfaceResult represents the result of interface operations, such as
+// AddInterface() and RemoveInterface().
+type InterfaceResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts an information struct.
+func (r InterfaceResult) Extract() (*InterfaceInfo, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res *InterfaceInfo
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron router interface: %v", err)
+ }
+
+ return res, nil
+}
diff --git a/openstack/networking/v2/extensions/layer3/routers/urls.go b/openstack/networking/v2/extensions/layer3/routers/urls.go
new file mode 100644
index 0000000..512c8a4
--- /dev/null
+++ b/openstack/networking/v2/extensions/layer3/routers/urls.go
@@ -0,0 +1,24 @@
+package routers
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ resourcePath = "routers"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, resourcePath, id)
+}
+
+func addInterfaceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, resourcePath, id, "add_router_interface")
+}
+
+func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, resourcePath, id, "remove_router_interface")
+}
diff --git a/openstack/networking/v2/extensions/lbaas/members/requests.go b/openstack/networking/v2/extensions/lbaas/members/requests.go
new file mode 100644
index 0000000..d095706
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/requests.go
@@ -0,0 +1,139 @@
+package members
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 network 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"`
+ Weight int `q:"weight"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ TenantID string `q:"tenant_id"`
+ PoolID string `q:"pool_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"`
+}
+
+// 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 ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return MemberPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// CreateOpts contains all the values needed to create a new pool member.
+type CreateOpts struct {
+ // Only required if the caller has an admin role and wants to create a pool
+ // for another tenant.
+ TenantID string
+
+ // Required. The IP address of the member.
+ Address string
+
+ // Required. The port on which the application is hosted.
+ ProtocolPort int
+
+ // Required. The pool to which this member will belong.
+ PoolID string
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool member.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ type member struct {
+ TenantID string `json:"tenant_id"`
+ ProtocolPort int `json:"protocol_port"`
+ Address string `json:"address"`
+ PoolID string `json:"pool_id"`
+ }
+ type request struct {
+ Member member `json:"member"`
+ }
+
+ reqBody := request{Member: member{
+ Address: opts.Address,
+ TenantID: opts.TenantID,
+ ProtocolPort: opts.ProtocolPort,
+ PoolID: opts.PoolID,
+ }}
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// Get retrieves a particular pool member based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a pool member.
+type UpdateOpts struct {
+ // The administrative state of the member, which is up (true) or down (false).
+ AdminStateUp bool
+}
+
+// Update allows members to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type member struct {
+ AdminStateUp bool `json:"admin_state_up"`
+ }
+ type request struct {
+ Member member `json:"member"`
+ }
+
+ reqBody := request{Member: member{AdminStateUp: opts.AdminStateUp}}
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular member based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/members/requests_test.go b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
new file mode 100644
index 0000000..cae3524
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/requests_test.go
@@ -0,0 +1,243 @@
+package members
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/members", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "members":[
+ {
+ "status":"ACTIVE",
+ "weight":1,
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab",
+ "address":"10.0.0.4",
+ "protocol_port":80,
+ "id":"701b531b-111a-4f21-ad85-4795b7b12af6"
+ },
+ {
+ "status":"ACTIVE",
+ "weight":1,
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "pool_id":"72741b06-df4d-4715-b142-276b6bce75ab",
+ "address":"10.0.0.3",
+ "protocol_port":80,
+ "id":"beb53b4d-230b-4abd-8118-575b8fa006ef"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractMembers(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+
+ expected := []Member{
+ Member{
+ Status: "ACTIVE",
+ Weight: 1,
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ PoolID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ Address: "10.0.0.4",
+ ProtocolPort: 80,
+ ID: "701b531b-111a-4f21-ad85-4795b7b12af6",
+ },
+ Member{
+ Status: "ACTIVE",
+ Weight: 1,
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ PoolID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ Address: "10.0.0.3",
+ ProtocolPort: 80,
+ ID: "beb53b4d-230b-4abd-8118-575b8fa006ef",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "member": {
+ "tenant_id": "453105b9-1754-413f-aab1-55f1af620750",
+ "pool_id": "foo",
+ "address": "192.0.2.14",
+ "protocol_port":8080
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "member": {
+ "id": "975592ca-e308-48ad-8298-731935ee9f45",
+ "address": "192.0.2.14",
+ "protocol_port": 8080,
+ "tenant_id": "453105b9-1754-413f-aab1-55f1af620750",
+ "admin_state_up":true,
+ "weight": 1,
+ "status": "DOWN"
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{
+ TenantID: "453105b9-1754-413f-aab1-55f1af620750",
+ Address: "192.0.2.14",
+ ProtocolPort: 8080,
+ PoolID: "foo",
+ }
+ _, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members/975592ca-e308-48ad-8298-731935ee9f45", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "member":{
+ "id":"975592ca-e308-48ad-8298-731935ee9f45",
+ "address":"192.0.2.14",
+ "protocol_port":8080,
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "admin_state_up":true,
+ "weight":1,
+ "status":"DOWN"
+ }
+}
+ `)
+ })
+
+ m, err := Get(fake.ServiceClient(), "975592ca-e308-48ad-8298-731935ee9f45").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "975592ca-e308-48ad-8298-731935ee9f45", m.ID)
+ th.AssertEquals(t, "192.0.2.14", m.Address)
+ th.AssertEquals(t, 8080, m.ProtocolPort)
+ th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", m.TenantID)
+ th.AssertEquals(t, true, m.AdminStateUp)
+ th.AssertEquals(t, 1, m.Weight)
+ th.AssertEquals(t, "DOWN", m.Status)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "member":{
+ "admin_state_up":false
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "member":{
+ "status":"PENDING_UPDATE",
+ "protocol_port":8080,
+ "weight":1,
+ "admin_state_up":false,
+ "tenant_id":"4fd44f30292945e481c7b8a0c8908869",
+ "pool_id":"7803631d-f181-4500-b3a2-1b68ba2a75fd",
+ "address":"10.0.0.5",
+ "status_description":null,
+ "id":"48a471ea-64f1-4eb6-9be7-dae6bbe40a0f"
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{AdminStateUp: false}
+
+ _, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/members/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/members/results.go b/openstack/networking/v2/extensions/lbaas/members/results.go
new file mode 100644
index 0000000..9466fee
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/results.go
@@ -0,0 +1,139 @@
+package members
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Member represents the application running on a backend server.
+type Member struct {
+ // The status of the member. Indicates whether the member is operational.
+ Status string
+
+ // Weight of member.
+ Weight int
+
+ // 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"`
+
+ // The pool to which the member belongs.
+ PoolID string `json:"pool_id" mapstructure:"pool_id"`
+
+ // The IP address of the member.
+ Address string
+
+ // 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 pool members.
+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 link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"members_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// 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 MemberPage struct,
+// and extracts the elements into a slice of Member structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractMembers(page pagination.Page) ([]Member, error) {
+ var resp struct {
+ Members []Member `mapstructure:"members" json:"members"`
+ }
+
+ err := mapstructure.Decode(page.(MemberPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Members, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*Member, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Member *Member `json:"member"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron member: %v", err)
+ }
+
+ return res.Member, nil
+}
+
+// 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 commonResult
diff --git a/openstack/networking/v2/extensions/lbaas/members/urls.go b/openstack/networking/v2/extensions/lbaas/members/urls.go
new file mode 100644
index 0000000..9d5ecec
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/members/urls.go
@@ -0,0 +1,17 @@
+package members
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "lb"
+ resourcePath = "members"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/requests.go b/openstack/networking/v2/extensions/lbaas/monitors/requests.go
new file mode 100644
index 0000000..9f63fc5
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests.go
@@ -0,0 +1,273 @@
+package monitors
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 network 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"`
+ TenantID string `q:"tenant_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"`
+}
+
+// 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 ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return MonitorPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+const (
+ TypePING = "PING"
+ TypeTCP = "TCP"
+ TypeHTTP = "HTTP"
+ TypeHTTPS = "HTTPS"
+)
+
+var (
+ 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")
+)
+
+// CreateOpts contains all the values needed to create a new health monitor.
+type CreateOpts struct {
+ // Required for admins. Indicates the owner of the VIP.
+ 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
+}
+
+// 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"}
+//
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate inputs
+ allowed := map[string]bool{TypeHTTP: true, TypeHTTPS: true, TypeTCP: true, TypePING: true}
+ if opts.Type == "" || allowed[opts.Type] == false {
+ res.Err = errValidTypeRequired
+ }
+ if opts.Delay == 0 {
+ res.Err = errDelayRequired
+ }
+ if opts.Timeout == 0 {
+ res.Err = errTimeoutRequired
+ }
+ if opts.MaxRetries == 0 {
+ res.Err = errMaxRetriesRequired
+ }
+ if opts.Type == TypeHTTP || opts.Type == TypeHTTPS {
+ if opts.URLPath == "" {
+ res.Err = errURLPathRequired
+ }
+ if opts.ExpectedCodes == "" {
+ res.Err = errExpectedCodesRequired
+ }
+ }
+ if res.Err != nil {
+ return res
+ }
+
+ type monitor struct {
+ Type string `json:"type"`
+ Delay int `json:"delay"`
+ Timeout int `json:"timeout"`
+ MaxRetries int `json:"max_retries"`
+ TenantID *string `json:"tenant_id,omitempty"`
+ URLPath *string `json:"url_path,omitempty"`
+ ExpectedCodes *string `json:"expected_codes,omitempty"`
+ HTTPMethod *string `json:"http_method,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ Monitor monitor `json:"health_monitor"`
+ }
+
+ reqBody := request{Monitor: monitor{
+ Type: opts.Type,
+ Delay: opts.Delay,
+ Timeout: opts.Timeout,
+ MaxRetries: opts.MaxRetries,
+ TenantID: gophercloud.MaybeString(opts.TenantID),
+ URLPath: gophercloud.MaybeString(opts.URLPath),
+ ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes),
+ HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod),
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ 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 = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains all the values needed to update an existing virtual IP.
+// Attributes not listed here but appear in CreateOpts are immutable and cannot
+// be updated.
+type UpdateOpts struct {
+ // 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
+}
+
+// Update is an operation which modifies the attributes of the specified monitor.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type monitor struct {
+ Delay int `json:"delay"`
+ Timeout int `json:"timeout"`
+ MaxRetries int `json:"max_retries"`
+ URLPath *string `json:"url_path,omitempty"`
+ ExpectedCodes *string `json:"expected_codes,omitempty"`
+ HTTPMethod *string `json:"http_method,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ Monitor monitor `json:"health_monitor"`
+ }
+
+ reqBody := request{Monitor: monitor{
+ Delay: opts.Delay,
+ Timeout: opts.Timeout,
+ MaxRetries: opts.MaxRetries,
+ URLPath: gophercloud.MaybeString(opts.URLPath),
+ ExpectedCodes: gophercloud.MaybeString(opts.ExpectedCodes),
+ HTTPMethod: gophercloud.MaybeString(opts.HTTPMethod),
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ var res UpdateResult
+
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ 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 = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
new file mode 100644
index 0000000..68a3288
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/requests_test.go
@@ -0,0 +1,277 @@
+package monitors
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/health_monitors", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "health_monitors":[
+ {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":10,
+ "max_retries":1,
+ "timeout":1,
+ "type":"PING",
+ "id":"466c8345-28d8-4f84-a246-e04380b0461d"
+ },
+ {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":5,
+ "expected_codes":"200",
+ "max_retries":2,
+ "http_method":"GET",
+ "timeout":2,
+ "url_path":"/",
+ "type":"HTTP",
+ "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractMonitors(page)
+ if err != nil {
+ t.Errorf("Failed to extract monitors: %v", err)
+ return false, err
+ }
+
+ expected := []Monitor{
+ Monitor{
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 10,
+ MaxRetries: 1,
+ Timeout: 1,
+ Type: "PING",
+ ID: "466c8345-28d8-4f84-a246-e04380b0461d",
+ },
+ Monitor{
+ AdminStateUp: true,
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 5,
+ ExpectedCodes: "200",
+ MaxRetries: 2,
+ Timeout: 2,
+ URLPath: "/",
+ Type: "HTTP",
+ HTTPMethod: "GET",
+ ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "health_monitor":{
+ "type":"HTTP",
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "delay":20,
+ "timeout":10,
+ "max_retries":5,
+ "url_path":"/check",
+ "expected_codes":"200-299"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "health_monitor":{
+ "id":"f3eeab00-8367-4524-b662-55e64d4cacb5",
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "type":"HTTP",
+ "delay":20,
+ "timeout":10,
+ "max_retries":5,
+ "http_method":"GET",
+ "url_path":"/check",
+ "expected_codes":"200-299",
+ "admin_state_up":true,
+ "status":"ACTIVE"
+ }
+}
+ `)
+ })
+
+ _, err := Create(fake.ServiceClient(), CreateOpts{
+ Type: "HTTP",
+ TenantID: "453105b9-1754-413f-aab1-55f1af620750",
+ Delay: 20,
+ Timeout: 10,
+ MaxRetries: 5,
+ URLPath: "/check",
+ ExpectedCodes: "200-299",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors/f3eeab00-8367-4524-b662-55e64d4cacb5", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "health_monitor":{
+ "id":"f3eeab00-8367-4524-b662-55e64d4cacb5",
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "type":"HTTP",
+ "delay":20,
+ "timeout":10,
+ "max_retries":5,
+ "http_method":"GET",
+ "url_path":"/check",
+ "expected_codes":"200-299",
+ "admin_state_up":true,
+ "status":"ACTIVE"
+ }
+}
+ `)
+ })
+
+ hm, err := Get(fake.ServiceClient(), "f3eeab00-8367-4524-b662-55e64d4cacb5").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "f3eeab00-8367-4524-b662-55e64d4cacb5", hm.ID)
+ th.AssertEquals(t, "453105b9-1754-413f-aab1-55f1af620750", hm.TenantID)
+ th.AssertEquals(t, "HTTP", hm.Type)
+ th.AssertEquals(t, 20, hm.Delay)
+ th.AssertEquals(t, 10, hm.Timeout)
+ th.AssertEquals(t, 5, hm.MaxRetries)
+ th.AssertEquals(t, "GET", hm.HTTPMethod)
+ th.AssertEquals(t, "/check", hm.URLPath)
+ th.AssertEquals(t, "200-299", hm.ExpectedCodes)
+ th.AssertEquals(t, true, hm.AdminStateUp)
+ th.AssertEquals(t, "ACTIVE", hm.Status)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "health_monitor":{
+ "delay": 3,
+ "timeout": 20,
+ "max_retries": 10,
+ "url_path": "/another_check",
+ "expected_codes": "301"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "health_monitor": {
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "delay": 3,
+ "max_retries": 10,
+ "http_method": "GET",
+ "timeout": 20,
+ "pools": [
+ {
+ "status": "PENDING_CREATE",
+ "status_description": null,
+ "pool_id": "6e55751f-6ad4-4e53-b8d4-02e442cd21df"
+ }
+ ],
+ "type": "PING",
+ "id": "b05e44b5-81f9-4551-b474-711a722698f7"
+ }
+}
+ `)
+ })
+
+ _, err := Update(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7", UpdateOpts{
+ Delay: 3,
+ Timeout: 20,
+ MaxRetries: 10,
+ URLPath: "/another_check",
+ ExpectedCodes: "301",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/health_monitors/b05e44b5-81f9-4551-b474-711a722698f7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "b05e44b5-81f9-4551-b474-711a722698f7")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/results.go b/openstack/networking/v2/extensions/lbaas/monitors/results.go
new file mode 100644
index 0000000..a41f20e
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/results.go
@@ -0,0 +1,167 @@
+package monitors
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 VIP.
+ ID 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"`
+
+ // 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
+}
+
+// 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 link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"health_monitors_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// 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:"health_monitors" json:"health_monitors"`
+ }
+
+ err := mapstructure.Decode(page.(MonitorPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Monitors, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// 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:"health_monitor" mapstructure:"health_monitor"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron monitor: %v", err)
+ }
+
+ return res.Monitor, nil
+}
+
+// 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 commonResult
diff --git a/openstack/networking/v2/extensions/lbaas/monitors/urls.go b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
new file mode 100644
index 0000000..e4b2afc
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/monitors/urls.go
@@ -0,0 +1,17 @@
+package monitors
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "lb"
+ resourcePath = "health_monitors"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/requests.go b/openstack/networking/v2/extensions/lbaas/pools/requests.go
new file mode 100644
index 0000000..2688350
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests.go
@@ -0,0 +1,205 @@
+package pools
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 network 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"`
+ LBMethod string `q:"lb_method"`
+ Protocol string `q:"protocol"`
+ SubnetID string `q:"subnet_id"`
+ TenantID string `q:"tenant_id"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Name string `q:"name"`
+ ID string `q:"id"`
+ VIPID string `q:"vip_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// 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 ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return PoolPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Supported attributes for create/update operations.
+const (
+ LBMethodRoundRobin = "ROUND_ROBIN"
+ LBMethodLeastConnections = "LEAST_CONNECTIONS"
+
+ ProtocolTCP = "TCP"
+ ProtocolHTTP = "HTTP"
+ ProtocolHTTPS = "HTTPS"
+)
+
+// CreateOpts contains all the values needed to create a new pool.
+type CreateOpts struct {
+ // Only required if the caller has an admin role and wants to create a pool
+ // for another tenant.
+ TenantID string
+
+ // Required. Name of the pool.
+ Name string
+
+ // Required. The protocol used by the pool members, you can use either
+ // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
+ Protocol string
+
+ // 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
+
+ // The algorithm used to distribute load between the members of the pool. The
+ // current specification supports LBMethodRoundRobin and
+ // LBMethodLeastConnections as valid values for this attribute.
+ LBMethod string
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ type pool struct {
+ Name string `json:"name"`
+ TenantID string `json:"tenant_id,omitempty"`
+ Protocol string `json:"protocol"`
+ SubnetID string `json:"subnet_id"`
+ LBMethod string `json:"lb_method"`
+ }
+ type request struct {
+ Pool pool `json:"pool"`
+ }
+
+ reqBody := request{Pool: pool{
+ Name: opts.Name,
+ TenantID: opts.TenantID,
+ Protocol: opts.Protocol,
+ SubnetID: opts.SubnetID,
+ LBMethod: opts.LBMethod,
+ }}
+
+ var res CreateResult
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ 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 = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains the values used when updating a pool.
+type UpdateOpts struct {
+ // Required. Name of the pool.
+ Name string
+
+ // The algorithm used to distribute load between the members of the pool. The
+ // current specification supports LBMethodRoundRobin and
+ // LBMethodLeastConnections as valid values for this attribute.
+ LBMethod string
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type pool struct {
+ Name string `json:"name,"`
+ LBMethod string `json:"lb_method"`
+ }
+ type request struct {
+ Pool pool `json:"pool"`
+ }
+
+ reqBody := request{Pool: pool{
+ Name: opts.Name,
+ LBMethod: opts.LBMethod,
+ }}
+
+ // Send request to API
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ 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 = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
+
+// AssociateMonitor will associate a health monitor with a particular pool.
+// Once associated, the health monitor will start monitoring the members of the
+// pool and will deactivate these members if they are deemed unhealthy. A
+// member can be deactivated (status set to INACTIVE) if any of health monitors
+// finds it unhealthy.
+func AssociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult {
+ type hm struct {
+ ID string `json:"id"`
+ }
+ type request struct {
+ Monitor hm `json:"health_monitor"`
+ }
+
+ reqBody := request{hm{ID: monitorID}}
+
+ var res AssociateResult
+ _, res.Err = perigee.Request("POST", associateURL(c, poolID), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+ return res
+}
+
+// DisassociateMonitor will disassociate a health monitor with a particular
+// pool. When dissociation is successful, the health monitor will no longer
+// check for the health of the members of the pool.
+func DisassociateMonitor(c *gophercloud.ServiceClient, poolID, monitorID string) AssociateResult {
+ var res AssociateResult
+ _, res.Err = perigee.Request("DELETE", disassociateURL(c, poolID, monitorID), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/requests_test.go b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
new file mode 100644
index 0000000..6af47a1
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/requests_test.go
@@ -0,0 +1,317 @@
+package pools
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/pools", rootURL(fake.ServiceClient()))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "pools":[
+ {
+ "status":"ACTIVE",
+ "lb_method":"ROUND_ROBIN",
+ "protocol":"HTTP",
+ "description":"",
+ "health_monitors":[
+ "466c8345-28d8-4f84-a246-e04380b0461d",
+ "5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+ ],
+ "members":[
+ "701b531b-111a-4f21-ad85-4795b7b12af6",
+ "beb53b4d-230b-4abd-8118-575b8fa006ef"
+ ],
+ "status_description": null,
+ "id":"72741b06-df4d-4715-b142-276b6bce75ab",
+ "vip_id":"4ec89087-d057-4e2c-911f-60a3b47ee304",
+ "name":"app_pool",
+ "admin_state_up":true,
+ "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "health_monitors_status": [],
+ "provider": "haproxy"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractPools(page)
+ if err != nil {
+ t.Errorf("Failed to extract pools: %v", err)
+ return false, err
+ }
+
+ expected := []Pool{
+ Pool{
+ Status: "ACTIVE",
+ LBMethod: "ROUND_ROBIN",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorIDs: []string{
+ "466c8345-28d8-4f84-a246-e04380b0461d",
+ "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+ },
+ SubnetID: "8032909d-47a1-4715-90af-5153ffe39861",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "app_pool",
+ MemberIDs: []string{
+ "701b531b-111a-4f21-ad85-4795b7b12af6",
+ "beb53b4d-230b-4abd-8118-575b8fa006ef",
+ },
+ ID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ VIPID: "4ec89087-d057-4e2c-911f-60a3b47ee304",
+ Provider: "haproxy",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "pool": {
+ "lb_method": "ROUND_ROBIN",
+ "protocol": "HTTP",
+ "name": "Example pool",
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "pool": {
+ "status": "PENDING_CREATE",
+ "lb_method": "ROUND_ROBIN",
+ "protocol": "HTTP",
+ "description": "",
+ "health_monitors": [],
+ "members": [],
+ "status_description": null,
+ "id": "69055154-f603-4a28-8951-7cc2d9e54a9a",
+ "vip_id": null,
+ "name": "Example pool",
+ "admin_state_up": true,
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "health_monitors_status": []
+ }
+}
+ `)
+ })
+
+ options := CreateOpts{
+ LBMethod: LBMethodRoundRobin,
+ Protocol: "HTTP",
+ Name: "Example pool",
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ }
+ p, err := Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "PENDING_CREATE", p.Status)
+ th.AssertEquals(t, "ROUND_ROBIN", p.LBMethod)
+ th.AssertEquals(t, "HTTP", p.Protocol)
+ th.AssertEquals(t, "", p.Description)
+ th.AssertDeepEquals(t, []string{}, p.MonitorIDs)
+ th.AssertDeepEquals(t, []string{}, p.MemberIDs)
+ th.AssertEquals(t, "69055154-f603-4a28-8951-7cc2d9e54a9a", p.ID)
+ th.AssertEquals(t, "Example pool", p.Name)
+ th.AssertEquals(t, "1981f108-3c48-48d2-b908-30f7d28532c9", p.SubnetID)
+ th.AssertEquals(t, "2ffc6e22aae24e4795f87155d24c896f", p.TenantID)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "pool":{
+ "id":"332abe93-f488-41ba-870b-2ac66be7f853",
+ "tenant_id":"19eaa775-cf5d-49bc-902e-2f85f668d995",
+ "name":"Example pool",
+ "description":"",
+ "protocol":"tcp",
+ "lb_algorithm":"ROUND_ROBIN",
+ "session_persistence":{
+ },
+ "healthmonitor_id":null,
+ "members":[
+ ],
+ "admin_state_up":true,
+ "status":"ACTIVE"
+ }
+}
+ `)
+ })
+
+ n, err := Get(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.ID, "332abe93-f488-41ba-870b-2ac66be7f853")
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "pool":{
+ "name":"SuperPool",
+ "lb_method": "LEAST_CONNECTIONS"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "pool":{
+ "status":"PENDING_UPDATE",
+ "lb_method":"LEAST_CONNECTIONS",
+ "protocol":"TCP",
+ "description":"",
+ "health_monitors":[
+
+ ],
+ "subnet_id":"8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "admin_state_up":true,
+ "name":"SuperPool",
+ "members":[
+
+ ],
+ "id":"61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "vip_id":null
+ }
+}
+ `)
+ })
+
+ options := UpdateOpts{Name: "SuperPool", LBMethod: LBMethodLeastConnections}
+
+ n, err := Update(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "SuperPool", n.Name)
+ th.AssertDeepEquals(t, "LEAST_CONNECTIONS", n.LBMethod)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestAssociateHealthMonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "health_monitor":{
+ "id":"b624decf-d5d3-4c66-9a3d-f047e7786181"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ _, err := AssociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181").Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestDisassociateHealthMonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/pools/332abe93-f488-41ba-870b-2ac66be7f853/health_monitors/b624decf-d5d3-4c66-9a3d-f047e7786181", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := DisassociateMonitor(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "b624decf-d5d3-4c66-9a3d-f047e7786181")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/results.go b/openstack/networking/v2/extensions/lbaas/pools/results.go
new file mode 100644
index 0000000..5b2adde
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/results.go
@@ -0,0 +1,166 @@
+package pools
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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.
+// There is only one pool per virtual IP.
+type Pool struct {
+ // The status of the pool. Indicates whether the pool is operational.
+ Status string
+
+ // 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_method" mapstructure:"lb_method"`
+
+ // The protocol of the pool, which is TCP, HTTP, or HTTPS.
+ Protocol string
+
+ // Description for the pool.
+ Description string
+
+ // The IDs of associated monitors which check the health of the pool members.
+ MonitorIDs []string `json:"health_monitors" mapstructure:"health_monitors"`
+
+ // 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
+
+ // List of member IDs that belong to the pool.
+ MemberIDs []string `json:"members" mapstructure:"members"`
+
+ // The unique ID for the pool.
+ ID string
+
+ // The ID of the virtual IP associated with this pool
+ VIPID string `json:"vip_id" mapstructure:"vip_id"`
+
+ // The provider
+ Provider string
+}
+
+// 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 link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"pools_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// 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)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Pools, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// 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.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron pool: %v", err)
+ }
+
+ return res.Pool, nil
+}
+
+// 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 commonResult
+
+// AssociateResult represents the result of an association operation.
+type AssociateResult struct {
+ commonResult
+}
diff --git a/openstack/networking/v2/extensions/lbaas/pools/urls.go b/openstack/networking/v2/extensions/lbaas/pools/urls.go
new file mode 100644
index 0000000..124ef5d
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/pools/urls.go
@@ -0,0 +1,26 @@
+package pools
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "lb"
+ resourcePath = "pools"
+ monitorPath = "health_monitors"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, resourcePath, id)
+}
+
+func associateURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, resourcePath, id, monitorPath)
+}
+
+func disassociateURL(c *gophercloud.ServiceClient, poolID, monitorID string) string {
+ return c.ServiceURL(version, rootPath, resourcePath, poolID, monitorPath, monitorID)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/vips/requests.go b/openstack/networking/v2/extensions/lbaas/vips/requests.go
new file mode 100644
index 0000000..8fadebf
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests.go
@@ -0,0 +1,272 @@
+package vips
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Nothing AdminState
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// 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 network 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"`
+ Status string `q:"status"`
+ TenantID string `q:"tenant_id"`
+ SubnetID string `q:"subnet_id"`
+ Address string `q:"address"`
+ PortID string `q:"port_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"`
+}
+
+// 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 ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return VIPPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+var (
+ errNameRequired = fmt.Errorf("Name is required")
+ errSubnetIDRequried = fmt.Errorf("SubnetID is required")
+ errProtocolRequired = fmt.Errorf("Protocol is required")
+ errProtocolPortRequired = fmt.Errorf("Protocol port is required")
+ errPoolIDRequired = fmt.Errorf("PoolID is required")
+)
+
+// CreateOpts contains all the values needed to create a new virtual IP.
+type CreateOpts struct {
+ // Required. Human-readable name for the VIP. Does not have to be unique.
+ Name string
+
+ // Required. The network on which to allocate the VIP's address. A tenant can
+ // only create VIPs on networks authorized by policy (e.g. networks that
+ // belong to them or networks that are shared).
+ SubnetID string
+
+ // Required. The protocol - can either be TCP, HTTP or HTTPS.
+ Protocol string
+
+ // Required. The port on which to listen for client traffic.
+ ProtocolPort int
+
+ // Required. The ID of the pool with which the VIP is associated.
+ PoolID string
+
+ // Required for admins. Indicates the owner of the VIP.
+ TenantID string
+
+ // Optional. The IP address of the VIP.
+ Address string
+
+ // Optional. Human-readable description for the VIP.
+ Description string
+
+ // Optional. Omit this field to prevent session persistence.
+ Persistence *SessionPersistence
+
+ // Optional. The maximum number of connections allowed for the VIP.
+ ConnLimit *int
+
+ // Optional. The administrative state of the VIP. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+}
+
+// Create is an operation which provisions a new virtual IP 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.
+//
+// Please note that the PoolID should refer to a pool that is not already
+// associated with another vip. If the pool is already used by another vip,
+// then the operation will fail with a 409 Conflict error will be returned.
+//
+// Users with an admin role can create VIPs 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
+
+ // Validate required opts
+ if opts.Name == "" {
+ res.Err = errNameRequired
+ return res
+ }
+ if opts.SubnetID == "" {
+ res.Err = errSubnetIDRequried
+ return res
+ }
+ if opts.Protocol == "" {
+ res.Err = errProtocolRequired
+ return res
+ }
+ if opts.ProtocolPort == 0 {
+ res.Err = errProtocolPortRequired
+ return res
+ }
+ if opts.PoolID == "" {
+ res.Err = errPoolIDRequired
+ return res
+ }
+
+ type vip struct {
+ Name string `json:"name"`
+ SubnetID string `json:"subnet_id"`
+ Protocol string `json:"protocol"`
+ ProtocolPort int `json:"protocol_port"`
+ PoolID string `json:"pool_id"`
+ Description *string `json:"description,omitempty"`
+ TenantID *string `json:"tenant_id,omitempty"`
+ Address *string `json:"address,omitempty"`
+ Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+ ConnLimit *int `json:"connection_limit,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ VirtualIP vip `json:"vip"`
+ }
+
+ reqBody := request{VirtualIP: vip{
+ Name: opts.Name,
+ SubnetID: opts.SubnetID,
+ Protocol: opts.Protocol,
+ ProtocolPort: opts.ProtocolPort,
+ PoolID: opts.PoolID,
+ Description: gophercloud.MaybeString(opts.Description),
+ TenantID: gophercloud.MaybeString(opts.TenantID),
+ Address: gophercloud.MaybeString(opts.Address),
+ ConnLimit: opts.ConnLimit,
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ if opts.Persistence != nil {
+ reqBody.VirtualIP.Persistence = opts.Persistence
+ }
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular virtual IP based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// UpdateOpts contains all the values needed to update an existing virtual IP.
+// Attributes not listed here but appear in CreateOpts are immutable and cannot
+// be updated.
+type UpdateOpts struct {
+ // Human-readable name for the VIP. Does not have to be unique.
+ Name string
+
+ // Required. The ID of the pool with which the VIP is associated.
+ PoolID string
+
+ // Optional. Human-readable description for the VIP.
+ Description string
+
+ // Optional. Omit this field to prevent session persistence.
+ Persistence *SessionPersistence
+
+ // Optional. The maximum number of connections allowed for the VIP.
+ ConnLimit *int
+
+ // Optional. The administrative state of the VIP. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool
+}
+
+// Update is an operation which modifies the attributes of the specified VIP.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) UpdateResult {
+ type vip struct {
+ Name string `json:"name,omitempty"`
+ PoolID string `json:"pool_id,omitempty"`
+ Description *string `json:"description,omitempty"`
+ Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+ ConnLimit *int `json:"connection_limit,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+ }
+
+ type request struct {
+ VirtualIP vip `json:"vip"`
+ }
+
+ reqBody := request{VirtualIP: vip{
+ Name: opts.Name,
+ PoolID: opts.PoolID,
+ Description: gophercloud.MaybeString(opts.Description),
+ ConnLimit: opts.ConnLimit,
+ AdminStateUp: opts.AdminStateUp,
+ }}
+
+ if opts.Persistence != nil {
+ reqBody.VirtualIP.Persistence = opts.Persistence
+ }
+
+ var res UpdateResult
+ _, res.Err = perigee.Request("PUT", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{200, 202},
+ })
+
+ return res
+}
+
+// Delete will permanently delete a particular virtual IP based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/lbaas/vips/requests_test.go b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
new file mode 100644
index 0000000..f4e90c7
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/requests_test.go
@@ -0,0 +1,307 @@
+package vips
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/lb/vips/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "vips":[
+ {
+ "id": "db902c0c-d5ff-4753-b465-668ad9656918",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "web_vip",
+ "description": "lb config for the web tier",
+ "subnet_id": "96a4386a-f8c3-42ed-afce-d7954eee77b3",
+ "address" : "10.30.176.47",
+ "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ "protocol": "HTTP",
+ "protocol_port": 80,
+ "pool_id" : "cfc6589d-f949-4c66-99d2-c2da56ef3764",
+ "admin_state_up": true,
+ "status": "ACTIVE"
+ },
+ {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "db_vip",
+ "description": "lb config for the db tier",
+ "subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ "address" : "10.30.176.48",
+ "port_id" : "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ "protocol": "TCP",
+ "protocol_port": 3306,
+ "pool_id" : "41efe233-7591-43c5-9cf7-923964759f9e",
+ "session_persistence" : {"type" : "SOURCE_IP"},
+ "connection_limit" : 2000,
+ "admin_state_up": true,
+ "status": "INACTIVE"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractVIPs(page)
+ if err != nil {
+ t.Errorf("Failed to extract LBs: %v", err)
+ return false, err
+ }
+
+ expected := []VirtualIP{
+ VirtualIP{
+ ID: "db902c0c-d5ff-4753-b465-668ad9656918",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "web_vip",
+ Description: "lb config for the web tier",
+ SubnetID: "96a4386a-f8c3-42ed-afce-d7954eee77b3",
+ Address: "10.30.176.47",
+ PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ Protocol: "HTTP",
+ ProtocolPort: 80,
+ PoolID: "cfc6589d-f949-4c66-99d2-c2da56ef3764",
+ Persistence: SessionPersistence{},
+ ConnLimit: 0,
+ AdminStateUp: true,
+ Status: "ACTIVE",
+ },
+ VirtualIP{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "db_vip",
+ Description: "lb config for the db tier",
+ SubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ Address: "10.30.176.48",
+ PortID: "cd1f7a47-4fa6-449c-9ee7-632838aedfea",
+ Protocol: "TCP",
+ ProtocolPort: 3306,
+ PoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
+ Persistence: SessionPersistence{Type: "SOURCE_IP"},
+ ConnLimit: 2000,
+ AdminStateUp: true,
+ Status: "INACTIVE",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "vip": {
+ "protocol": "HTTP",
+ "name": "NewVip",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "protocol_port": 80
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "vip": {
+ "status": "PENDING_CREATE",
+ "protocol": "HTTP",
+ "description": "",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+ "connection_limit": -1,
+ "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "address": "10.0.0.11",
+ "protocol_port": 80,
+ "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5",
+ "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2",
+ "name": "NewVip"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{
+ Protocol: "HTTP",
+ Name: "NewVip",
+ AdminStateUp: Up,
+ SubnetID: "8032909d-47a1-4715-90af-5153ffe39861",
+ PoolID: "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ ProtocolPort: 80,
+ }
+
+ r, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "PENDING_CREATE", r.Status)
+ th.AssertEquals(t, "HTTP", r.Protocol)
+ th.AssertEquals(t, "", r.Description)
+ th.AssertEquals(t, true, r.AdminStateUp)
+ th.AssertEquals(t, "8032909d-47a1-4715-90af-5153ffe39861", r.SubnetID)
+ th.AssertEquals(t, "83657cfcdfe44cd5920adaf26c48ceea", r.TenantID)
+ th.AssertEquals(t, -1, r.ConnLimit)
+ th.AssertEquals(t, "61b1f87a-7a21-4ad3-9dda-7f81d249944f", r.PoolID)
+ th.AssertEquals(t, "10.0.0.11", r.Address)
+ th.AssertEquals(t, 80, r.ProtocolPort)
+ th.AssertEquals(t, "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5", r.PortID)
+ th.AssertEquals(t, "c987d2be-9a3c-4ac9-a046-e8716b1350e2", r.ID)
+ th.AssertEquals(t, "NewVip", r.Name)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "vip": {
+ "status": "ACTIVE",
+ "protocol": "HTTP",
+ "description": "",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+ "connection_limit": 1000,
+ "pool_id": "72741b06-df4d-4715-b142-276b6bce75ab",
+ "session_persistence": {
+ "cookie_name": "MyAppCookie",
+ "type": "APP_COOKIE"
+ },
+ "address": "10.0.0.10",
+ "protocol_port": 80,
+ "port_id": "b5a743d6-056b-468b-862d-fb13a9aa694e",
+ "id": "4ec89087-d057-4e2c-911f-60a3b47ee304",
+ "name": "my-vip"
+ }
+}
+ `)
+ })
+
+ vip, err := Get(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "ACTIVE", vip.Status)
+ th.AssertEquals(t, "HTTP", vip.Protocol)
+ th.AssertEquals(t, "", vip.Description)
+ th.AssertEquals(t, true, vip.AdminStateUp)
+ th.AssertEquals(t, 1000, vip.ConnLimit)
+ th.AssertEquals(t, SessionPersistence{Type: "APP_COOKIE", CookieName: "MyAppCookie"}, vip.Persistence)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "vip": {
+ "connection_limit": 1000
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+
+ fmt.Fprintf(w, `
+{
+ "vip": {
+ "status": "PENDING_UPDATE",
+ "protocol": "HTTP",
+ "description": "",
+ "admin_state_up": true,
+ "subnet_id": "8032909d-47a1-4715-90af-5153ffe39861",
+ "tenant_id": "83657cfcdfe44cd5920adaf26c48ceea",
+ "connection_limit": 1000,
+ "pool_id": "61b1f87a-7a21-4ad3-9dda-7f81d249944f",
+ "address": "10.0.0.11",
+ "protocol_port": 80,
+ "port_id": "f7e6fe6a-b8b5-43a8-8215-73456b32e0f5",
+ "id": "c987d2be-9a3c-4ac9-a046-e8716b1350e2",
+ "name": "NewVip"
+ }
+}
+ `)
+ })
+
+ i1000 := 1000
+ options := UpdateOpts{ConnLimit: &i1000}
+ vip, err := Update(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "PENDING_UPDATE", vip.Status)
+ th.AssertEquals(t, 1000, vip.ConnLimit)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/lb/vips/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas/vips/results.go b/openstack/networking/v2/extensions/lbaas/vips/results.go
new file mode 100644
index 0000000..62efe09
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/results.go
@@ -0,0 +1,186 @@
+package vips
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "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"`
+}
+
+// VirtualIP is the primary load balancing configuration object that specifies
+// the virtual IP address and port on which client traffic is received, as well
+// as other details such as the load balancing method to be use, protocol, etc.
+// This entity is sometimes known in LB products under the name of a "virtual
+// server", a "vserver" or a "listener".
+type VirtualIP struct {
+ // The unique ID for the VIP.
+ ID string `mapstructure:"id" json:"id"`
+
+ // Owner of the VIP. 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 VIP. Does not have to be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // Human-readable description for the VIP.
+ Description string `mapstructure:"description" json:"description"`
+
+ // The ID of the subnet on which to allocate the VIP address.
+ SubnetID string `mapstructure:"subnet_id" json:"subnet_id"`
+
+ // The IP address of the VIP.
+ Address string `mapstructure:"address" json:"address"`
+
+ // The protocol of the VIP address. 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
+ // VIP address. A valid value is from 0 to 65535.
+ ProtocolPort int `mapstructure:"protocol_port" json:"protocol_port"`
+
+ // The ID of the pool with which the VIP is associated.
+ PoolID string `mapstructure:"pool_id" json:"pool_id"`
+
+ // The ID of the port which belongs to the load balancer
+ PortID string `mapstructure:"port_id" json:"port_id"`
+
+ // 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 maximum number of connections allowed for the VIP. Default is -1,
+ // meaning no limit.
+ ConnLimit int `mapstructure:"connection_limit" json:"connection_limit"`
+
+ // The administrative state of the VIP. A valid value is true (UP) or false (DOWN).
+ AdminStateUp bool `mapstructure:"admin_state_up" json:"admin_state_up"`
+
+ // The status of the VIP. Indicates whether the VIP is operational.
+ Status string `mapstructure:"status" json:"status"`
+}
+
+// VIPPage is the page returned by a pager when traversing over a
+// collection of routers.
+type VIPPage 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 VIPPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"vips_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// IsEmpty checks whether a RouterPage struct is empty.
+func (p VIPPage) IsEmpty() (bool, error) {
+ is, err := ExtractVIPs(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractVIPs accepts a Page struct, specifically a VIPPage struct,
+// and extracts the elements into a slice of VirtualIP structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractVIPs(page pagination.Page) ([]VirtualIP, error) {
+ var resp struct {
+ VIPs []VirtualIP `mapstructure:"vips" json:"vips"`
+ }
+
+ err := mapstructure.Decode(page.(VIPPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.VIPs, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a router.
+func (r commonResult) Extract() (*VirtualIP, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ VirtualIP *VirtualIP `mapstructure:"vip" json:"vip"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron Virtual IP: %v", err)
+ }
+
+ return res.VirtualIP, nil
+}
+
+// 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 commonResult
diff --git a/openstack/networking/v2/extensions/lbaas/vips/urls.go b/openstack/networking/v2/extensions/lbaas/vips/urls.go
new file mode 100644
index 0000000..570db6d
--- /dev/null
+++ b/openstack/networking/v2/extensions/lbaas/vips/urls.go
@@ -0,0 +1,17 @@
+package vips
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "lb"
+ resourcePath = "vips"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/provider/doc.go b/openstack/networking/v2/extensions/provider/doc.go
new file mode 100755
index 0000000..612d26e
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/doc.go
@@ -0,0 +1,21 @@
+// Package networkattrs gives access to the provider Neutron plugin, allowing
+// network extended attributes. The provider extended attributes for networks
+// enable administrative users to specify how network objects map to the
+// underlying networking infrastructure. These extended attributes also appear
+// when administrative users query networks.
+//
+// For more information about extended attributes, see the NetworkExtAttrs
+// struct. The actual semantics of these attributes depend on the technology
+// back end of the particular plug-in. See the plug-in documentation and the
+// OpenStack Cloud Administrator Guide to understand which values should be
+// specific for each of these attributes when OpenStack Networking is deployed
+// with a particular plug-in. The examples shown in this chapter refer to the
+// Open vSwitch plug-in.
+//
+// The default policy settings enable only users with administrative rights to
+// specify these parameters in requests and to see their values in responses. By
+// default, the provider network extension attributes are completely hidden from
+// regular tenants. As a rule of thumb, if these attributes are not visible in a
+// GET /networks/<network-id> operation, this implies the user submitting the
+// request is not authorized to view or manipulate provider network attributes.
+package provider
diff --git a/openstack/networking/v2/extensions/provider/results.go b/openstack/networking/v2/extensions/provider/results.go
new file mode 100755
index 0000000..96caac1
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/results.go
@@ -0,0 +1,128 @@
+package provider
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+type AdminState *bool
+
+// Convenience vars for AdminStateUp values.
+var (
+ iTrue = true
+ iFalse = false
+
+ Nothing AdminState = nil
+ Up AdminState = &iTrue
+ Down AdminState = &iFalse
+)
+
+// NetworkExtAttrs represents an extended form of a Network with additional fields.
+type NetworkExtAttrs struct {
+ // UUID for the network
+ ID string `mapstructure:"id" json:"id"`
+
+ // Human-readable name for the network. Might not be unique.
+ Name string `mapstructure:"name" json:"name"`
+
+ // The administrative state of network. If false (down), the network 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"`
+
+ // Subnets associated with this network.
+ Subnets []string `mapstructure:"subnets" json:"subnets"`
+
+ // Owner of network. Only admin users can specify a tenant_id other than its own.
+ TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
+ // Specifies whether the network resource can be accessed by any tenant or not.
+ Shared bool `mapstructure:"shared" json:"shared"`
+
+ // Specifies the nature of the physical network mapped to this network
+ // resource. Examples are flat, vlan, or gre.
+ NetworkType string `json:"provider:network_type" mapstructure:"provider:network_type"`
+
+ // Identifies the physical network on top of which this network object is
+ // being implemented. The OpenStack Networking API does not expose any facility
+ // for retrieving the list of available physical networks. As an example, in
+ // the Open vSwitch plug-in this is a symbolic name which is then mapped to
+ // specific bridges on each compute host through the Open vSwitch plug-in
+ // configuration file.
+ PhysicalNetwork string `json:"provider:physical_network" mapstructure:"provider:physical_network"`
+
+ // Identifies an isolated segment on the physical network; the nature of the
+ // segment depends on the segmentation model defined by network_type. For
+ // instance, if network_type is vlan, then this is a vlan identifier;
+ // otherwise, if network_type is gre, then this will be a gre key.
+ SegmentationID string `json:"provider:segmentation_id" mapstructure:"provider:segmentation_id"`
+}
+
+// ExtractGet decorates a GetResult struct returned from a networks.Get()
+// function with extended attributes.
+func ExtractGet(r networks.GetResult) (*NetworkExtAttrs, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ Network *NetworkExtAttrs `json:"network"`
+ }
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
+ }
+ return res.Network, nil
+}
+
+// ExtractGet decorates a CreateResult struct returned from a networks.Create()
+// function with extended attributes.
+func ExtractCreate(r networks.CreateResult) (*NetworkExtAttrs, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ Network *NetworkExtAttrs `json:"network"`
+ }
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
+ }
+ return res.Network, nil
+}
+
+// ExtractUpdate decorates a UpdateResult struct returned from a
+// networks.Update() function with extended attributes.
+func ExtractUpdate(r networks.UpdateResult) (*NetworkExtAttrs, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+ var res struct {
+ Network *NetworkExtAttrs `json:"network"`
+ }
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron network: %v", err)
+ }
+ return res.Network, nil
+}
+
+// ExtractList accepts a Page struct, specifically a NetworkPage struct, and
+// extracts the elements into a slice of NetworkExtAttrs structs. In other
+// words, a generic collection is mapped into a relevant slice.
+func ExtractList(page pagination.Page) ([]NetworkExtAttrs, error) {
+ var resp struct {
+ Networks []NetworkExtAttrs `mapstructure:"networks" json:"networks"`
+ }
+
+ err := mapstructure.Decode(page.(networks.NetworkPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Networks, nil
+}
diff --git a/openstack/networking/v2/extensions/provider/results_test.go b/openstack/networking/v2/extensions/provider/results_test.go
new file mode 100644
index 0000000..ec0b528
--- /dev/null
+++ b/openstack/networking/v2/extensions/provider/results_test.go
@@ -0,0 +1,253 @@
+package provider
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/networks"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "networks": [
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null,
+ "provider:physical_network": null,
+ "provider:network_type": "local"
+ },
+ {
+ "status": "ACTIVE",
+ "subnets": [
+ "08eae331-0402-425a-923c-34f7cfe39c1b"
+ ],
+ "name": "private",
+ "admin_state_up": true,
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "shared": true,
+ "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "provider:segmentation_id": null,
+ "provider:physical_network": null,
+ "provider:network_type": "local"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ networks.List(fake.ServiceClient(), networks.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractList(page)
+ if err != nil {
+ t.Errorf("Failed to extract networks: %v", err)
+ return false, err
+ }
+
+ expected := []NetworkExtAttrs{
+ NetworkExtAttrs{
+ Status: "ACTIVE",
+ Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
+ Name: "private-network",
+ AdminStateUp: true,
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ Shared: true,
+ ID: "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ NetworkType: "local",
+ PhysicalNetwork: "",
+ SegmentationID: "",
+ },
+ NetworkExtAttrs{
+ Status: "ACTIVE",
+ Subnets: []string{"08eae331-0402-425a-923c-34f7cfe39c1b"},
+ Name: "private",
+ AdminStateUp: true,
+ TenantID: "26a7980765d0414dbc1fc1f88cdb7e6e",
+ Shared: true,
+ ID: "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ NetworkType: "local",
+ PhysicalNetwork: "",
+ SegmentationID: "",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "provider:physical_network": null,
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "provider:network_type": "local",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null
+ }
+}
+ `)
+ })
+
+ res := networks.Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22")
+ n, err := ExtractGet(res)
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "", n.PhysicalNetwork)
+ th.AssertEquals(t, "local", n.NetworkType)
+ th.AssertEquals(t, "", n.SegmentationID)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "name": "sample_network",
+ "admin_state_up": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "provider:physical_network": null,
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "provider:network_type": "local",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null
+ }
+}
+ `)
+ })
+
+ options := networks.CreateOpts{Name: "sample_network", AdminStateUp: Up}
+ res := networks.Create(fake.ServiceClient(), options)
+ n, err := ExtractCreate(res)
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "", n.PhysicalNetwork)
+ th.AssertEquals(t, "local", n.NetworkType)
+ th.AssertEquals(t, "", n.SegmentationID)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "network": {
+ "name": "new_network_name",
+ "admin_state_up": false,
+ "shared": true
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": [
+ "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ ],
+ "name": "private-network",
+ "provider:physical_network": null,
+ "admin_state_up": true,
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "provider:network_type": "local",
+ "shared": true,
+ "id": "d32019d3-bc6e-4319-9c1d-6722fc136a22",
+ "provider:segmentation_id": null
+ }
+}
+ `)
+ })
+
+ iTrue := true
+ options := networks.UpdateOpts{Name: "new_network_name", AdminStateUp: Down, Shared: &iTrue}
+ res := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options)
+ n, err := ExtractUpdate(res)
+
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "", n.PhysicalNetwork)
+ th.AssertEquals(t, "local", n.NetworkType)
+ th.AssertEquals(t, "", n.SegmentationID)
+}
diff --git a/openstack/networking/v2/extensions/security/doc.go b/openstack/networking/v2/extensions/security/doc.go
new file mode 100644
index 0000000..8ef455f
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/doc.go
@@ -0,0 +1,32 @@
+// Package security contains functionality to work with security group and
+// security group rules Neutron resources.
+//
+// Security groups and security group rules allows administrators and tenants
+// the ability to specify the type of traffic and direction (ingress/egress)
+// that is allowed to pass through a port. A security group is a container for
+// security group rules.
+//
+// When a port is created in Networking it is associated with a security group.
+// If a security group is not specified the port is associated with a 'default'
+// security group. By default, this group drops all ingress traffic and allows
+// all egress. Rules can be added to this group in order to change the behaviour.
+//
+// The basic characteristics of Neutron Security Groups are:
+//
+// For ingress traffic (to an instance)
+// - Only traffic matched with security group rules are allowed.
+// - When there is no rule defined, all traffic are dropped.
+//
+// For egress traffic (from an instance)
+// - Only traffic matched with security group rules are allowed.
+// - When there is no rule defined, all egress traffic are dropped.
+// - When a new security group is created, rules to allow all egress traffic
+// are automatically added.
+//
+// "default security group" is defined for each tenant.
+// - For the default security group a rule which allows intercommunication
+// among hosts associated with the default security group is defined by default.
+// - As a result, all egress traffic and intercommunication in the default
+// group are allowed and all ingress from outside of the default group is
+// dropped by default (in the default security group).
+package security
diff --git a/openstack/networking/v2/extensions/security/groups/requests.go b/openstack/networking/v2/extensions/security/groups/requests.go
new file mode 100644
index 0000000..6e9fe33
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests.go
@@ -0,0 +1,107 @@
+package groups
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 network 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"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// security groups. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return SecGroupPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+var (
+ errNameRequired = fmt.Errorf("Name is required")
+)
+
+// CreateOpts contains all the values needed to create a new security group.
+type CreateOpts struct {
+ // Required. Human-readable name for the VIP. Does not have to be unique.
+ Name string
+
+ // Optional. Describes the security group.
+ Description string
+}
+
+// Create is an operation which provisions a new security group with default
+// security group rules for the IPv4 and IPv6 ether types.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate required opts
+ if opts.Name == "" {
+ res.Err = errNameRequired
+ return res
+ }
+
+ type secgroup struct {
+ Name string `json:"name"`
+ Description string `json:"description,omitempty"`
+ }
+
+ type request struct {
+ SecGroup secgroup `json:"security_group"`
+ }
+
+ reqBody := request{SecGroup: secgroup{
+ Name: opts.Name,
+ Description: opts.Description,
+ }}
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/security/groups/requests_test.go b/openstack/networking/v2/extensions/security/groups/requests_test.go
new file mode 100644
index 0000000..857bdc6
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/requests_test.go
@@ -0,0 +1,213 @@
+package groups
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-groups/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_groups": [
+ {
+ "description": "default",
+ "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "name": "default",
+ "security_group_rules": [],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract secgroups: %v", err)
+ return false, err
+ }
+
+ expected := []SecGroup{
+ SecGroup{
+ Description: "default",
+ ID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ Name: "default",
+ Rules: []rules.SecGroupRule{},
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "security_group": {
+ "name": "new-webservers",
+ "description": "security group for webservers"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "security_group": {
+ "description": "security group for webservers",
+ "id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "name": "new-webservers",
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "38ce2d8e-e8f1-48bd-83c2-d33cb9f50c3d",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "565b9502-12de-4ffd-91e9-68885cff6ae1",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "2076db17-a522-4506-91de-c6dd8e837028",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{Name: "new-webservers", Description: "security group for webservers"}
+ _, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups/85cc3048-abc3-43cc-89b3-377341426ac5", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group": {
+ "description": "default",
+ "id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "name": "default",
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ],
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ sg, err := Get(fake.ServiceClient(), "85cc3048-abc3-43cc-89b3-377341426ac5").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "default", sg.Description)
+ th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sg.ID)
+ th.AssertEquals(t, "default", sg.Name)
+ th.AssertEquals(t, 2, len(sg.Rules))
+ th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sg.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-groups/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/security/groups/results.go b/openstack/networking/v2/extensions/security/groups/results.go
new file mode 100644
index 0000000..6db613e
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -0,0 +1,128 @@
+package groups
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/security/rules"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroup represents a container for security group rules.
+type SecGroup struct {
+ // The UUID for the security group.
+ ID string
+
+ // Human-readable name for the security group. Might not be unique. Cannot be
+ // named "default" as that is automatically created for a tenant.
+ Name string
+
+ // The security group description.
+ Description string
+
+ // A slice of security group rules that dictate the permitted behaviour for
+ // traffic entering and leaving the group.
+ Rules []rules.SecGroupRule `json:"security_group_rules" mapstructure:"security_group_rules"`
+
+ // Owner of the security group. Only admin users can specify a TenantID
+ // other than their own.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupPage is the page returned by a pager when traversing over a
+// collection of security groups.
+type SecGroupPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security groups 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 SecGroupPage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"security_groups_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// IsEmpty checks whether a SecGroupPage struct is empty.
+func (p SecGroupPage) IsEmpty() (bool, error) {
+ is, err := ExtractGroups(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct,
+// and extracts the elements into a slice of SecGroup structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractGroups(page pagination.Page) ([]SecGroup, error) {
+ var resp struct {
+ SecGroups []SecGroup `mapstructure:"security_groups" json:"security_groups"`
+ }
+
+ err := mapstructure.Decode(page.(SecGroupPage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.SecGroups, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a security group.
+func (r commonResult) Extract() (*SecGroup, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ SecGroup *SecGroup `mapstructure:"security_group" json:"security_group"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron secgroup: %v", err)
+ }
+
+ return res.SecGroup, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/security/groups/urls.go b/openstack/networking/v2/extensions/security/groups/urls.go
new file mode 100644
index 0000000..2f2bbdd
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/groups/urls.go
@@ -0,0 +1,16 @@
+package groups
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "security-groups"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, rootPath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, id)
+}
diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go
new file mode 100644
index 0000000..ea0f37d
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -0,0 +1,183 @@
+package rules
+
+import (
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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 security group attributes you want to see returned. SortKey allows you to
+// sort by a particular network attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Direction string `q:"direction"`
+ EtherType string `q:"ethertype"`
+ ID string `q:"id"`
+ PortRangeMax int `q:"port_range_max"`
+ PortRangeMin int `q:"port_range_min"`
+ Protocol string `q:"protocol"`
+ RemoteGroupID string `q:"remote_group_id"`
+ RemoteIPPrefix string `q:"remote_ip_prefix"`
+ SecGroupID string `q:"security_group_id"`
+ TenantID string `q:"tenant_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// security group rules. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ u := rootURL(c) + q.String()
+ return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return SecGroupRulePage{pagination.LinkedPageBase{LastHTTPResponse: r}}
+ })
+}
+
+// Errors
+var (
+ errValidDirectionRequired = fmt.Errorf("A valid Direction is required")
+ errValidEtherTypeRequired = fmt.Errorf("A valid EtherType is required")
+ errSecGroupIDRequired = fmt.Errorf("A valid SecGroupID is required")
+ errValidProtocolRequired = fmt.Errorf("A valid Protocol is required")
+)
+
+// Constants useful for CreateOpts
+const (
+ DirIngress = "ingress"
+ DirEgress = "egress"
+ Ether4 = "IPv4"
+ Ether6 = "IPv6"
+ ProtocolTCP = "tcp"
+ ProtocolUDP = "udp"
+ ProtocolICMP = "icmp"
+)
+
+// CreateOpts contains all the values needed to create a new security group rule.
+type CreateOpts struct {
+ // Required. Must be either "ingress" or "egress": the direction in which the
+ // security group rule is applied.
+ Direction string
+
+ // Required. Must be "IPv4" or "IPv6", and addresses represented in CIDR must
+ // match the ingress or egress rules.
+ EtherType string
+
+ // Required. The security group ID to associate with this security group rule.
+ SecGroupID string
+
+ // Optional. The maximum port number in the range that is matched by the
+ // security group rule. The PortRangeMin attribute constrains the PortRangeMax
+ // attribute. If the protocol is ICMP, this value must be an ICMP type.
+ PortRangeMax int
+
+ // Optional. The minimum port number in the range that is matched by the
+ // security group rule. If the protocol is TCP or UDP, this value must be
+ // less than or equal to the value of the PortRangeMax attribute. If the
+ // protocol is ICMP, this value must be an ICMP type.
+ PortRangeMin int
+
+ // Optional. The protocol that is matched by the security group rule. Valid
+ // values are "tcp", "udp", "icmp" or an empty string.
+ Protocol string
+
+ // Optional. The remote group ID to be associated with this security group
+ // rule. You can specify either RemoteGroupID or RemoteIPPrefix.
+ RemoteGroupID string
+
+ // Optional. The remote IP prefix to be associated with this security group
+ // rule. You can specify either RemoteGroupID or RemoteIPPrefix. This
+ // attribute matches the specified IP prefix as the source IP address of the
+ // IP packet.
+ RemoteIPPrefix string
+}
+
+// Create is an operation which provisions a new security group with default
+// security group rules for the IPv4 and IPv6 ether types.
+func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
+ var res CreateResult
+
+ // Validate required opts
+ if opts.Direction != DirIngress && opts.Direction != DirEgress {
+ res.Err = errValidDirectionRequired
+ return res
+ }
+ if opts.EtherType != Ether4 && opts.EtherType != Ether6 {
+ res.Err = errValidEtherTypeRequired
+ return res
+ }
+ if opts.SecGroupID == "" {
+ res.Err = errSecGroupIDRequired
+ return res
+ }
+ if opts.Protocol != "" && opts.Protocol != ProtocolTCP && opts.Protocol != ProtocolUDP && opts.Protocol != ProtocolICMP {
+ res.Err = errValidProtocolRequired
+ return res
+ }
+
+ type secrule struct {
+ Direction string `json:"direction"`
+ EtherType string `json:"ethertype"`
+ SecGroupID string `json:"security_group_id"`
+ PortRangeMax int `json:"port_range_max,omitempty"`
+ PortRangeMin int `json:"port_range_min,omitempty"`
+ Protocol string `json:"protocol,omitempty"`
+ RemoteGroupID string `json:"remote_group_id,omitempty"`
+ RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"`
+ }
+
+ type request struct {
+ SecRule secrule `json:"security_group_rule"`
+ }
+
+ reqBody := request{SecRule: secrule{
+ Direction: opts.Direction,
+ EtherType: opts.EtherType,
+ SecGroupID: opts.SecGroupID,
+ PortRangeMax: opts.PortRangeMax,
+ PortRangeMin: opts.PortRangeMin,
+ Protocol: opts.Protocol,
+ RemoteGroupID: opts.RemoteGroupID,
+ RemoteIPPrefix: opts.RemoteIPPrefix,
+ }}
+
+ _, res.Err = perigee.Request("POST", rootURL(c), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ ReqBody: &reqBody,
+ Results: &res.Resp,
+ OkCodes: []int{201},
+ })
+
+ return res
+}
+
+// Get retrieves a particular security group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) GetResult {
+ var res GetResult
+ _, res.Err = perigee.Request("GET", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ Results: &res.Resp,
+ OkCodes: []int{200},
+ })
+ return res
+}
+
+// Delete will permanently delete a particular security group based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
+ var res DeleteResult
+ _, res.Err = perigee.Request("DELETE", resourceURL(c, id), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ return res
+}
diff --git a/openstack/networking/v2/extensions/security/rules/requests_test.go b/openstack/networking/v2/extensions/security/rules/requests_test.go
new file mode 100644
index 0000000..a2c7fbb
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/requests_test.go
@@ -0,0 +1,224 @@
+package rules
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud/pagination"
+ th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestURLs(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules", rootURL(fake.ServiceClient()))
+ th.AssertEquals(t, th.Endpoint()+"v2.0/security-group-rules/foo", resourceURL(fake.ServiceClient(), "foo"))
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rules": [
+ {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ },
+ {
+ "direction": "egress",
+ "ethertype": "IPv4",
+ "id": "93aa42e5-80db-4581-9391-3a608bd0e448",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractRules(page)
+ if err != nil {
+ t.Errorf("Failed to extract secrules: %v", err)
+ return false, err
+ }
+
+ expected := []SecGroupRule{
+ SecGroupRule{
+ Direction: "egress",
+ EtherType: "IPv6",
+ ID: "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ PortRangeMax: 0,
+ PortRangeMin: 0,
+ Protocol: "",
+ RemoteGroupID: "",
+ RemoteIPPrefix: "",
+ SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ SecGroupRule{
+ Direction: "egress",
+ EtherType: "IPv4",
+ ID: "93aa42e5-80db-4581-9391-3a608bd0e448",
+ PortRangeMax: 0,
+ PortRangeMin: 0,
+ Protocol: "",
+ RemoteGroupID: "",
+ RemoteIPPrefix: "",
+ SecGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ TenantID: "e4f50856753b4dc6afee5fa6b9b6c550",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `
+{
+ "security_group_rule": {
+ "direction": "ingress",
+ "port_range_min": 80,
+ "ethertype": "IPv4",
+ "port_range_max": 80,
+ "protocol": "tcp",
+ "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rule": {
+ "direction": "ingress",
+ "ethertype": "IPv4",
+ "id": "2bc0accf-312e-429a-956e-e4407625eb62",
+ "port_range_max": 80,
+ "port_range_min": 80,
+ "protocol": "tcp",
+ "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "remote_ip_prefix": null,
+ "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ opts := CreateOpts{
+ Direction: "ingress",
+ PortRangeMin: 80,
+ EtherType: "IPv4",
+ PortRangeMax: 80,
+ Protocol: "tcp",
+ RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+ SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a",
+ }
+ _, err := Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules/3c0e45ff-adaf-4124-b083-bf390e5482ff", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "security_group_rule": {
+ "direction": "egress",
+ "ethertype": "IPv6",
+ "id": "3c0e45ff-adaf-4124-b083-bf390e5482ff",
+ "port_range_max": null,
+ "port_range_min": null,
+ "protocol": null,
+ "remote_group_id": null,
+ "remote_ip_prefix": null,
+ "security_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+ "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+ }
+}
+ `)
+ })
+
+ sr, err := Get(fake.ServiceClient(), "3c0e45ff-adaf-4124-b083-bf390e5482ff").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, "egress", sr.Direction)
+ th.AssertEquals(t, "IPv6", sr.EtherType)
+ th.AssertEquals(t, "3c0e45ff-adaf-4124-b083-bf390e5482ff", sr.ID)
+ th.AssertEquals(t, 0, sr.PortRangeMax)
+ th.AssertEquals(t, 0, sr.PortRangeMin)
+ th.AssertEquals(t, "", sr.Protocol)
+ th.AssertEquals(t, "", sr.RemoteGroupID)
+ th.AssertEquals(t, "", sr.RemoteIPPrefix)
+ th.AssertEquals(t, "85cc3048-abc3-43cc-89b3-377341426ac5", sr.SecGroupID)
+ th.AssertEquals(t, "e4f50856753b4dc6afee5fa6b9b6c550", sr.TenantID)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/security-group-rules/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ res := Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
new file mode 100644
index 0000000..13d5c7d
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -0,0 +1,153 @@
+package rules
+
+import (
+ "fmt"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// SecGroupRule represents a rule to dictate the behaviour of incoming or
+// outgoing traffic for a particular security group.
+type SecGroupRule struct {
+ // The UUID for this security group rule.
+ ID string
+
+ // The direction in which the security group rule is applied. The only values
+ // allowed are "ingress" or "egress". For a compute instance, an ingress
+ // security group rule is applied to incoming (ingress) traffic for that
+ // instance. An egress rule is applied to traffic leaving the instance.
+ Direction string
+
+ // Must be IPv4 or IPv6, and addresses represented in CIDR must match the
+ // ingress or egress rules.
+ EtherType string `json:"ethertype" mapstructure:"ethertype"`
+
+ // The security group ID to associate with this security group rule.
+ SecGroupID string `json:"security_group_id" mapstructure:"security_group_id"`
+
+ // The minimum port number in the range that is matched by the security group
+ // rule. If the protocol is TCP or UDP, this value must be less than or equal
+ // to the value of the PortRangeMax attribute. If the protocol is ICMP, this
+ // value must be an ICMP type.
+ PortRangeMin int `json:"port_range_min" mapstructure:"port_range_min"`
+
+ // The maximum port number in the range that is matched by the security group
+ // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If
+ // the protocol is ICMP, this value must be an ICMP type.
+ PortRangeMax int `json:"port_range_max" mapstructure:"port_range_max"`
+
+ // The protocol that is matched by the security group rule. Valid values are
+ // "tcp", "udp", "icmp" or an empty string.
+ Protocol string
+
+ // The remote group ID to be associated with this security group rule. You
+ // can specify either RemoteGroupID or RemoteIPPrefix.
+ RemoteGroupID string `json:"remote_group_id" mapstructure:"remote_group_id"`
+
+ // The remote IP prefix to be associated with this security group rule. You
+ // can specify either RemoteGroupID or RemoteIPPrefix . This attribute
+ // matches the specified IP prefix as the source IP address of the IP packet.
+ RemoteIPPrefix string `json:"remote_ip_prefix" mapstructure:"remote_ip_prefix"`
+
+ // The owner of this security group rule.
+ TenantID string `json:"tenant_id" mapstructure:"tenant_id"`
+}
+
+// SecGroupRulePage is the page returned by a pager when traversing over a
+// collection of security group rules.
+type SecGroupRulePage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of security group rules 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 SecGroupRulePage) NextPageURL() (string, error) {
+ type link struct {
+ Href string `mapstructure:"href"`
+ Rel string `mapstructure:"rel"`
+ }
+ type resp struct {
+ Links []link `mapstructure:"security_group_rules_links"`
+ }
+
+ var r resp
+ err := mapstructure.Decode(p.Body, &r)
+ if err != nil {
+ return "", err
+ }
+
+ var url string
+ for _, l := range r.Links {
+ if l.Rel == "next" {
+ url = l.Href
+ }
+ }
+ if url == "" {
+ return "", nil
+ }
+
+ return url, nil
+}
+
+// IsEmpty checks whether a SecGroupRulePage struct is empty.
+func (p SecGroupRulePage) IsEmpty() (bool, error) {
+ is, err := ExtractRules(p)
+ if err != nil {
+ return true, nil
+ }
+ return len(is) == 0, nil
+}
+
+// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct,
+// and extracts the elements into a slice of SecGroupRule structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRules(page pagination.Page) ([]SecGroupRule, error) {
+ var resp struct {
+ SecGroupRules []SecGroupRule `mapstructure:"security_group_rules" json:"security_group_rules"`
+ }
+
+ err := mapstructure.Decode(page.(SecGroupRulePage).Body, &resp)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.SecGroupRules, nil
+}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+// Extract is a function that accepts a result and extracts a security rule.
+func (r commonResult) Extract() (*SecGroupRule, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ SecGroupRule *SecGroupRule `mapstructure:"security_group_rule" json:"security_group_rule"`
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Neutron SecGroupRule: %v", err)
+ }
+
+ return res.SecGroupRule, nil
+}
+
+// CreateResult represents the result of a create operation.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
diff --git a/openstack/networking/v2/extensions/security/rules/urls.go b/openstack/networking/v2/extensions/security/rules/urls.go
new file mode 100644
index 0000000..2ffbf37
--- /dev/null
+++ b/openstack/networking/v2/extensions/security/rules/urls.go
@@ -0,0 +1,16 @@
+package rules
+
+import "github.com/rackspace/gophercloud"
+
+const (
+ version = "v2.0"
+ rootPath = "security-group-rules"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(version, rootPath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(version, rootPath, id)
+}
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index 7fed58e..6088b36 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -1,16 +1,13 @@
package networks
import (
- "strconv"
-
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/utils"
"github.com/rackspace/gophercloud/pagination"
)
type networkOpts struct {
- AdminStateUp bool
+ AdminStateUp *bool
Name string
Shared *bool
TenantID string
@@ -22,16 +19,16 @@
// by a particular network attribute. SortDir sets the direction, and is either
// `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
- Status string
- Name string
- AdminStateUp *bool
- TenantID string
- Shared *bool
- ID string
- Marker string
- Limit int
- SortKey string
- SortDir string
+ Status string `q:"status"`
+ Name string `q:"name"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ TenantID string `q:"tenant_id"`
+ Shared *bool `q:"shared"`
+ ID string `q:"id"`
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
}
// List returns a Pager which allows you to iterate over a collection of
@@ -39,39 +36,11 @@
// the returned collection for greater efficiency.
func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
// Build query parameters
- q := make(map[string]string)
- if opts.Status != "" {
- q["status"] = opts.Status
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
}
- if opts.Name != "" {
- q["name"] = opts.Name
- }
- if opts.AdminStateUp != nil {
- q["admin_state_up"] = strconv.FormatBool(*opts.AdminStateUp)
- }
- if opts.TenantID != "" {
- q["tenant_id"] = opts.TenantID
- }
- if opts.Shared != nil {
- q["shared"] = strconv.FormatBool(*opts.Shared)
- }
- if opts.ID != "" {
- q["id"] = opts.ID
- }
- if opts.Marker != "" {
- q["marker"] = opts.Marker
- }
- if opts.Limit != 0 {
- q["limit"] = strconv.Itoa(opts.Limit)
- }
- if opts.SortKey != "" {
- q["sort_key"] = opts.SortKey
- }
- if opts.SortDir != "" {
- q["sort_dir"] = opts.SortDir
- }
-
- u := listURL(c) + utils.BuildQuery(q)
+ u := listURL(c) + q.String()
return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
return NetworkPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
@@ -80,18 +49,42 @@
// Get retrieves a specific network based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
- _, err := perigee.Request("GET", getURL(c, id), perigee.Options{
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
Results: &res.Resp,
OkCodes: []int{200},
})
- res.Err = err
return res
}
-// CreateOpts represents the attributes used when creating a new network.
+type CreateOptsBuilder interface {
+ ToNetworkCreateMap() map[string]map[string]interface{}
+}
+
type CreateOpts networkOpts
+func (o CreateOpts) ToNetworkCreateMap() map[string]map[string]interface{} {
+ inner := make(map[string]interface{})
+
+ if o.AdminStateUp != nil {
+ inner["admin_state_up"] = &o.AdminStateUp
+ }
+ if o.Name != "" {
+ inner["name"] = o.Name
+ }
+ if o.Shared != nil {
+ inner["shared"] = &o.Shared
+ }
+ if o.TenantID != "" {
+ inner["tenant_id"] = o.TenantID
+ }
+
+ outer := make(map[string]map[string]interface{})
+ outer["network"] = inner
+
+ return outer
+}
+
// Create accepts a CreateOpts struct and creates a new network using the values
// provided. This operation does not actually require a request body, i.e. the
// CreateOpts struct argument can be empty.
@@ -99,84 +92,70 @@
// The tenant ID that is contained in the URI is the tenant that creates the
// network. An admin user, however, has the option of specifying another tenant
// ID in the CreateOpts struct.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) CreateResult {
- // Define structures
- type network struct {
- AdminStateUp bool `json:"admin_state_up,omitempty"`
- Name string `json:"name,omitempty"`
- Shared *bool `json:"shared,omitempty"`
- TenantID *string `json:"tenant_id,omitempty"`
- }
- type request struct {
- Network network `json:"network"`
- }
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
+ var res CreateResult
- // Populate request body
- reqBody := request{Network: network{
- AdminStateUp: opts.AdminStateUp,
- Name: opts.Name,
- Shared: opts.Shared,
- }}
-
- if opts.TenantID != "" {
- reqBody.Network.TenantID = &opts.TenantID
- }
+ reqBody := opts.ToNetworkCreateMap()
// Send request to API
- var res CreateResult
- _, err := perigee.Request("POST", createURL(c), perigee.Options{
+ _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
Results: &res.Resp,
OkCodes: []int{201},
})
- res.Err = err
return res
}
-// UpdateOpts represents the attributes used when updating an existing network.
+type UpdateOptsBuilder interface {
+ ToNetworkUpdateMap() map[string]map[string]interface{}
+}
+
type UpdateOpts networkOpts
+func (o UpdateOpts) ToNetworkUpdateMap() map[string]map[string]interface{} {
+ inner := make(map[string]interface{})
+
+ if o.AdminStateUp != nil {
+ inner["admin_state_up"] = &o.AdminStateUp
+ }
+ if o.Name != "" {
+ inner["name"] = o.Name
+ }
+ if o.Shared != nil {
+ inner["shared"] = &o.Shared
+ }
+
+ outer := make(map[string]map[string]interface{})
+ outer["network"] = inner
+
+ return outer
+}
+
// Update accepts a UpdateOpts struct and updates an existing network using the
// values provided. For more information, see the Create function.
-func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOpts) UpdateResult {
- // Define structures
- type network struct {
- AdminStateUp bool `json:"admin_state_up"`
- Name string `json:"name"`
- Shared *bool `json:"shared,omitempty"`
- }
+func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) UpdateResult {
+ var res UpdateResult
- type request struct {
- Network network `json:"network"`
- }
-
- // Populate request body
- reqBody := request{Network: network{
- AdminStateUp: opts.AdminStateUp,
- Name: opts.Name,
- Shared: opts.Shared,
- }}
+ reqBody := opts.ToNetworkUpdateMap()
// Send request to API
- var res UpdateResult
- _, err := perigee.Request("PUT", getURL(c, networkID), perigee.Options{
+ _, res.Err = perigee.Request("PUT", getURL(c, networkID), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
Results: &res.Resp,
OkCodes: []int{200, 201},
})
- res.Err = err
+
return res
}
// Delete accepts a unique ID and deletes the network associated with it.
func Delete(c *gophercloud.ServiceClient, networkID string) DeleteResult {
var res DeleteResult
- _, err := perigee.Request("DELETE", deleteURL(c, networkID), perigee.Options{
+ _, res.Err = perigee.Request("DELETE", deleteURL(c, networkID), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
- res.Err = err
return res
}
diff --git a/openstack/networking/v2/networks/requests_test.go b/openstack/networking/v2/networks/requests_test.go
index 4c50b87..03c297f 100644
--- a/openstack/networking/v2/networks/requests_test.go
+++ b/openstack/networking/v2/networks/requests_test.go
@@ -5,29 +5,18 @@
"net/http"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
-const TokenID = "123"
-
-func ServiceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{
- TokenID: TokenID,
- },
- Endpoint: th.Endpoint(),
- }
-}
-
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -62,7 +51,7 @@
`)
})
- client := ServiceClient()
+ client := fake.ServiceClient()
count := 0
List(client, ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
@@ -110,7 +99,7 @@
th.Mux.HandleFunc("/v2.0/networks/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -132,7 +121,7 @@
`)
})
- n, err := Get(ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
+ n, err := Get(fake.ServiceClient(), "d32019d3-bc6e-4319-9c1d-6722fc136a22").Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Status, "ACTIVE")
@@ -150,7 +139,7 @@
th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
@@ -180,8 +169,9 @@
`)
})
- options := CreateOpts{Name: "sample_network", AdminStateUp: true}
- n, err := Create(ServiceClient(), options).Extract()
+ iTrue := true
+ options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue}
+ n, err := Create(fake.ServiceClient(), options).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Status, "ACTIVE")
@@ -199,7 +189,7 @@
th.Mux.HandleFunc("/v2.0/networks", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
@@ -216,9 +206,9 @@
w.WriteHeader(http.StatusCreated)
})
- shared := true
- options := CreateOpts{Name: "sample_network", AdminStateUp: true, Shared: &shared, TenantID: "12345"}
- _, err := Create(ServiceClient(), options).Extract()
+ iTrue := true
+ options := CreateOpts{Name: "sample_network", AdminStateUp: &iTrue, Shared: &iTrue, TenantID: "12345"}
+ _, err := Create(fake.ServiceClient(), options).Extract()
th.AssertNoErr(t, err)
}
@@ -228,7 +218,7 @@
th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
@@ -259,9 +249,9 @@
`)
})
- shared := true
- options := UpdateOpts{Name: "new_network_name", AdminStateUp: false, Shared: &shared}
- n, err := Update(ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
+ iTrue, iFalse := true, false
+ options := UpdateOpts{Name: "new_network_name", AdminStateUp: &iFalse, Shared: &iTrue}
+ n, err := Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", options).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Name, "new_network_name")
@@ -276,10 +266,10 @@
th.Mux.HandleFunc("/v2.0/networks/4e8e5957-649f-477b-9e5b-f1f75b21c03c", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
- res := Delete(ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+ res := Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
th.AssertNoErr(t, res.Err)
}
diff --git a/openstack/networking/v2/networks/results.go b/openstack/networking/v2/networks/results.go
index 91182a4..2dbd55f 100644
--- a/openstack/networking/v2/networks/results.go
+++ b/openstack/networking/v2/networks/results.go
@@ -52,17 +52,23 @@
type Network struct {
// UUID for the network
ID string `mapstructure:"id" json:"id"`
+
// Human-readable name for the network. Might not be unique.
Name string `mapstructure:"name" json:"name"`
+
// The administrative state of network. If false (down), the network 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"`
+
// Subnets associated with this network.
Subnets []string `mapstructure:"subnets" json:"subnets"`
+
// Owner of network. Only admin users can specify a tenant_id other than its own.
TenantID string `mapstructure:"tenant_id" json:"tenant_id"`
+
// Specifies whether the network resource can be accessed by any tenant or not.
Shared bool `mapstructure:"shared" json:"shared"`
}
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index 0c4ae74..6928d8f 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -1,11 +1,8 @@
package ports
import (
- "strconv"
-
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/utils"
"github.com/rackspace/gophercloud/pagination"
)
@@ -15,22 +12,19 @@
// 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
- Name string
- AdminStateUp *bool
- NetworkID string
- TenantID string
- DeviceOwner string
- MACAddress string
- ID string
- DeviceID string
- BindingHostID string
- BindingVIFType string
- BindingVNICType string
- Limit int
- Marker string
- SortKey string
- SortDir string
+ 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"`
}
// List returns a Pager which allows you to iterate over a collection of
@@ -42,60 +36,12 @@
// administrative rights.
func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
// Build query parameters
- q := make(map[string]string)
- if opts.Status != "" {
- q["status"] = opts.Status
+ q, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
}
- if opts.Name != "" {
- q["name"] = opts.Name
- }
- if opts.AdminStateUp != nil {
- q["admin_state_up"] = strconv.FormatBool(*opts.AdminStateUp)
- }
- if opts.NetworkID != "" {
- q["network_id"] = opts.NetworkID
- }
- if opts.TenantID != "" {
- q["tenant_id"] = opts.TenantID
- }
- if opts.DeviceOwner != "" {
- q["device_owner"] = opts.DeviceOwner
- }
- if opts.MACAddress != "" {
- q["mac_address"] = opts.MACAddress
- }
- if opts.ID != "" {
- q["id"] = opts.ID
- }
- if opts.DeviceID != "" {
- q["device_id"] = opts.DeviceID
- }
- if opts.BindingHostID != "" {
- q["binding:host_id"] = opts.BindingHostID
- }
- if opts.BindingVIFType != "" {
- q["binding:vif_type"] = opts.BindingVIFType
- }
- if opts.BindingVNICType != "" {
- q["binding:vnic_type"] = opts.BindingVNICType
- }
- if opts.NetworkID != "" {
- q["network_id"] = opts.NetworkID
- }
- if opts.Limit != 0 {
- q["limit"] = strconv.Itoa(opts.Limit)
- }
- if opts.Marker != "" {
- q["marker"] = opts.Marker
- }
- if opts.SortKey != "" {
- q["sort_key"] = opts.SortKey
- }
- if opts.SortDir != "" {
- q["sort_dir"] = opts.SortDir
- }
+ u := listURL(c) + q.String()
- u := listURL(c) + utils.BuildQuery(q)
return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
return PortPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
@@ -104,12 +50,11 @@
// Get retrieves a specific port based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
- _, err := perigee.Request("GET", getURL(c, id), perigee.Options{
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
Results: &res.Resp,
OkCodes: []int{200},
})
- res.Err = err
return res
}
@@ -172,14 +117,14 @@
}
// Response
- _, err := perigee.Request("POST", createURL(c), perigee.Options{
+ _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
Results: &res.Resp,
OkCodes: []int{201},
DumpReqJson: true,
})
- res.Err = err
+
return res
}
@@ -226,23 +171,21 @@
// Response
var res UpdateResult
- _, err := perigee.Request("PUT", updateURL(c, id), perigee.Options{
+ _, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
Results: &res.Resp,
OkCodes: []int{200, 201},
})
- res.Err = err
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
- _, err := perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
+ _, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
- res.Err = err
return res
}
diff --git a/openstack/networking/v2/ports/requests_test.go b/openstack/networking/v2/ports/requests_test.go
index ad498a2..662d364 100644
--- a/openstack/networking/v2/ports/requests_test.go
+++ b/openstack/networking/v2/ports/requests_test.go
@@ -5,27 +5,18 @@
"net/http"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
-const tokenID = "123"
-
-func ServiceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{TokenID: tokenID},
- Endpoint: th.Endpoint(),
- }
-}
-
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -59,7 +50,7 @@
count := 0
- List(ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractPorts(page)
if err != nil {
@@ -104,7 +95,7 @@
th.Mux.HandleFunc("/v2.0/ports/46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -133,7 +124,7 @@
`)
})
- n, err := Get(ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract()
+ n, err := Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Status, "ACTIVE")
@@ -158,7 +149,7 @@
th.Mux.HandleFunc("/v2.0/ports", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
- th.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
@@ -203,7 +194,7 @@
asu := true
options := CreateOpts{Name: "private-port", AdminStateUp: &asu, NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7"}
- n, err := Create(ServiceClient(), options).Extract()
+ n, err := Create(fake.ServiceClient(), options).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, n.Status, "DOWN")
@@ -226,7 +217,7 @@
th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
- th.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
@@ -283,7 +274,7 @@
SecurityGroups: []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"},
}
- s, err := Update(ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
+ s, err := Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", options).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, s.Name, "new_port_name")
@@ -299,10 +290,10 @@
th.Mux.HandleFunc("/v2.0/ports/65c0ee9f-d634-4522-8954-51021b570b0d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
- th.TestHeader(t, r, "X-Auth-Token", tokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
- res := Delete(ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
+ res := Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
th.AssertNoErr(t, res.Err)
}
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 591af24..092c914 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -1,11 +1,8 @@
package subnets
import (
- "strconv"
-
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/utils"
"github.com/rackspace/gophercloud/pagination"
)
@@ -15,18 +12,18 @@
// by a particular subnet attribute. SortDir sets the direction, and is either
// `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
- Name string
- EnableDHCP *bool
- NetworkID string
- TenantID string
- IPVersion int
- GatewayIP string
- CIDR string
- ID string
- Limit int
- Marker string
- SortKey string
- SortDir string
+ Name string `q:"name"`
+ EnableDHCP *bool `q:"enable_dhcp"`
+ NetworkID string `q:"network_id"`
+ TenantID string `q:"tenant_id"`
+ IPVersion int `q:"ip_version"`
+ GatewayIP string `q:"gateway_ip"`
+ CIDR string `q:"cidr"`
+ ID string `q:"id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
}
// List returns a Pager which allows you to iterate over a collection of
@@ -38,46 +35,13 @@
// administrative rights.
func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
// Build query parameters
- q := make(map[string]string)
- if opts.Name != "" {
- q["name"] = opts.Name
+ query, err := gophercloud.BuildQueryString(&opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
}
- if opts.EnableDHCP != nil {
- q["enable_dhcp"] = strconv.FormatBool(*opts.EnableDHCP)
- }
- if opts.NetworkID != "" {
- q["network_id"] = opts.NetworkID
- }
- if opts.TenantID != "" {
- q["tenant_id"] = opts.TenantID
- }
- if opts.IPVersion != 0 {
- q["ip_version"] = strconv.Itoa(opts.IPVersion)
- }
- if opts.GatewayIP != "" {
- q["gateway_ip"] = opts.GatewayIP
- }
- if opts.CIDR != "" {
- q["cidr"] = opts.CIDR
- }
- if opts.ID != "" {
- q["id"] = opts.ID
- }
- if opts.Limit != 0 {
- q["limit"] = strconv.Itoa(opts.Limit)
- }
- if opts.Marker != "" {
- q["marker"] = opts.Marker
- }
- if opts.SortKey != "" {
- q["sort_key"] = opts.SortKey
- }
- if opts.SortDir != "" {
- q["sort_dir"] = opts.SortDir
- }
+ url := listURL(c) + query.String()
- u := listURL(c) + utils.BuildQuery(q)
- return pagination.NewPager(c, u, func(r pagination.LastHTTPResponse) pagination.Page {
+ return pagination.NewPager(c, url, func(r pagination.LastHTTPResponse) pagination.Page {
return SubnetPage{pagination.LinkedPageBase{LastHTTPResponse: r}}
})
}
@@ -85,12 +49,11 @@
// Get retrieves a specific subnet based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) GetResult {
var res GetResult
- _, err := perigee.Request("GET", getURL(c, id), perigee.Options{
+ _, res.Err = perigee.Request("GET", getURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
Results: &res.Resp,
OkCodes: []int{200},
})
- res.Err = err
return res
}
@@ -173,13 +136,12 @@
reqBody.Subnet.HostRoutes = opts.HostRoutes
}
- _, err := perigee.Request("POST", createURL(c), perigee.Options{
+ _, res.Err = perigee.Request("POST", createURL(c), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
Results: &res.Resp,
OkCodes: []int{201},
})
- res.Err = err
return res
}
@@ -222,13 +184,12 @@
}
var res UpdateResult
- _, err := perigee.Request("PUT", updateURL(c, id), perigee.Options{
+ _, res.Err = perigee.Request("PUT", updateURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
ReqBody: &reqBody,
Results: &res.Resp,
OkCodes: []int{200, 201},
})
- res.Err = err
return res
}
@@ -236,10 +197,9 @@
// Delete accepts a unique ID and deletes the subnet associated with it.
func Delete(c *gophercloud.ServiceClient, id string) DeleteResult {
var res DeleteResult
- _, err := perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
+ _, res.Err = perigee.Request("DELETE", deleteURL(c, id), perigee.Options{
MoreHeaders: c.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
- res.Err = err
return res
}
diff --git a/openstack/networking/v2/subnets/requests_test.go b/openstack/networking/v2/subnets/requests_test.go
index 3c7c8b2..c7562f7 100644
--- a/openstack/networking/v2/subnets/requests_test.go
+++ b/openstack/networking/v2/subnets/requests_test.go
@@ -5,29 +5,18 @@
"net/http"
"testing"
- "github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
th "github.com/rackspace/gophercloud/testhelper"
+ fake "github.com/rackspace/gophercloud/testhelper/client"
)
-const TokenID = "123"
-
-func ServiceClient() *gophercloud.ServiceClient {
- return &gophercloud.ServiceClient{
- Provider: &gophercloud.ProviderClient{
- TokenID: TokenID,
- },
- Endpoint: th.Endpoint(),
- }
-}
-
func TestList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -78,7 +67,7 @@
count := 0
- List(ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ List(fake.ServiceClient(), ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
count++
actual, err := ExtractSubnets(page)
if err != nil {
@@ -141,7 +130,7 @@
th.Mux.HandleFunc("/v2.0/subnets/54d6f61d-db07-451c-9ab3-b9609b6b6f0b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
@@ -170,7 +159,7 @@
`)
})
- s, err := Get(ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract()
+ s, err := Get(fake.ServiceClient(), "54d6f61d-db07-451c-9ab3-b9609b6b6f0b").Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, s.Name, "my_subnet")
@@ -197,7 +186,7 @@
th.Mux.HandleFunc("/v2.0/subnets", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
@@ -238,7 +227,7 @@
})
opts := CreateOpts{NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", IPVersion: 4, CIDR: "192.168.199.0/24"}
- s, err := Create(ServiceClient(), opts).Extract()
+ s, err := Create(fake.ServiceClient(), opts).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, s.Name, "")
@@ -265,7 +254,7 @@
th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
th.TestJSONRequest(t, r, `
@@ -304,7 +293,7 @@
})
opts := UpdateOpts{Name: "my_new_subnet"}
- s, err := Update(ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
+ s, err := Update(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b", opts).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, s.Name, "my_new_subnet")
@@ -317,10 +306,10 @@
th.Mux.HandleFunc("/v2.0/subnets/08eae331-0402-425a-923c-34f7cfe39c1b", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
- th.TestHeader(t, r, "X-Auth-Token", TokenID)
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
- res := Delete(ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b")
+ res := Delete(fake.ServiceClient(), "08eae331-0402-425a-923c-34f7cfe39c1b")
th.AssertNoErr(t, res.Err)
}
diff --git a/params.go b/params.go
index c492ec0..10aefea 100644
--- a/params.go
+++ b/params.go
@@ -21,6 +21,13 @@
return nil
}
+func MaybeInt(original int) *int {
+ if original != 0 {
+ return &original
+ }
+ return nil
+}
+
var t time.Time
func isZero(v reflect.Value) bool {
diff --git a/testhelper/client/fake.go b/testhelper/client/fake.go
new file mode 100644
index 0000000..3eb9e12
--- /dev/null
+++ b/testhelper/client/fake.go
@@ -0,0 +1,17 @@
+package client
+
+import (
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+// Fake token to use.
+const TokenID = "cbc36478b0bd8e67e89469c7749d4127"
+
+// ServiceClient returns a generic service client for use in tests.
+func ServiceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: TokenID},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
diff --git a/testhelper/convenience.go b/testhelper/convenience.go
index 85cb9ec..f6cb371 100644
--- a/testhelper/convenience.go
+++ b/testhelper/convenience.go
@@ -63,13 +63,13 @@
// an actual error
func AssertNoErr(t *testing.T, e error) {
if e != nil {
- logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e)))
+ logFatal(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
}
}
// CheckNoErr is similar to AssertNoErr, except with a non-fatal error
func CheckNoErr(t *testing.T, e error) {
if e != nil {
- logError(t, fmt.Sprintf("unexpected error %s", yellow(e)))
+ logError(t, fmt.Sprintf("unexpected error %s", yellow(e.Error())))
}
}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..c424759
--- /dev/null
+++ b/util.go
@@ -0,0 +1,22 @@
+package gophercloud
+
+import (
+ "fmt"
+ "time"
+)
+
+// WaitFor polls a predicate function once per second up to secs times to wait for a certain state to arrive.
+func WaitFor(secs int, predicate func() (bool, error)) error {
+ for i := 0; i < secs; i++ {
+ time.Sleep(1 * time.Second)
+
+ satisfied, err := predicate()
+ if err != nil {
+ return err
+ }
+ if satisfied {
+ return nil
+ }
+ }
+ return fmt.Errorf("Time out in WaitFor.")
+}