diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 3092511a3b..0d511dbf9f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -31,13 +31,15 @@ enthusiasm as any other contribution!
### 3. Working on a new feature
-If you've found something we've left out, definitely feel free to start work on
-introducing that feature. It's always useful to open an issue or submit a pull
-request early on to indicate your intent to a core contributor - this enables
-quick/early feedback and can help steer you in the right direction by avoiding
-known issues. It might also help you avoid losing time implementing something
-that might not ever work. One tip is to prefix your Pull Request issue title
-with [wip] - then people know it's a work in progress.
+If you've found something we've left out, we'd love for you to add it! Please
+first open an issue to indicate your interest to a core contributor - this
+enables quick/early feedback and can help steer you in the right direction by
+avoiding known issues. It might also help you avoid losing time implementing
+something that might not ever work or is outside the scope of the project.
+
+While you're implementing the feature, one tip is to prefix your Pull Request
+title with `[wip]` - then people know it's a work in progress. Once the PR is
+ready for review, you can remove the `[wip]` tag and request a review.
We ask that you do not submit a feature that you have not spent time researching
and testing first-hand in an actual OpenStack environment. While we appreciate
@@ -99,7 +101,7 @@ need to checkout a new feature branch:
git commit
```
-7. Submit your branch as a [Pull Request](https://help.github.com/articles/creating-a-pull-request/). When submitting a Pull Request, please follow our [Style Guide](https://github.com/gophercloud/gophercloud/blob/master/STYLEGUIDE.md).
+7. Submit your branch as a [Pull Request](https://help.github.com/articles/creating-a-pull-request/). When submitting a Pull Request, please follow our [Style Guide](https://github.com/gophercloud/gophercloud/blob/master/docs/STYLEGUIDE.md).
> Further information about using Git can be found [here](https://git-scm.com/book/en/v2).
@@ -245,4 +247,4 @@ To run tests for a particular sub-package:
## Style guide
-See [here](/STYLEGUIDE.md)
+See [here](/docs/STYLEGUIDE.md)
diff --git a/.travis.yml b/.travis.yml
index 59c4194952..02728f4968 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,8 +7,8 @@ install:
- go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/goimports
go:
-- 1.8
-- tip
+- "1.10"
+- "tip"
env:
global:
- secure: "xSQsAG5wlL9emjbCdxzz/hYQsSpJ/bABO1kkbwMSISVcJ3Nk0u4ywF+LS4bgeOnwPfmFvNTOqVDu3RwEvMeWXSI76t1piCPcObutb2faKLVD/hLoAS76gYX+Z8yGWGHrSB7Do5vTPj1ERe2UljdrnsSeOXzoDwFxYRaZLX4bBOB4AyoGvRniil5QXPATiA1tsWX1VMicj8a4F8X+xeESzjt1Q5Iy31e7vkptu71bhvXCaoo5QhYwT+pLR9dN0S1b7Ro0KVvkRefmr1lUOSYd2e74h6Lc34tC1h3uYZCS4h47t7v5cOXvMNxinEj2C51RvbjvZI1RLVdkuAEJD1Iz4+Ote46nXbZ//6XRZMZz/YxQ13l7ux1PFjgEB6HAapmF5Xd8PRsgeTU9LRJxpiTJ3P5QJ3leS1va8qnziM5kYipj/Rn+V8g2ad/rgkRox9LSiR9VYZD2Pe45YCb1mTKSl2aIJnV7nkOqsShY5LNB4JZSg7xIffA+9YVDktw8dJlATjZqt7WvJJ49g6A61mIUV4C15q2JPGKTkZzDiG81NtmS7hFa7k0yaE2ELgYocbcuyUcAahhxntYTC0i23nJmEHVNiZmBO3u7EgpWe4KGVfumU+lt12tIn5b3dZRBBUk3QakKKozSK1QPHGpk/AZGrhu7H6l8to6IICKWtDcyMPQ="
diff --git a/.zuul.yaml b/.zuul.yaml
index c259d03e18..436fbb6e94 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -7,6 +7,15 @@
recheck-mitaka:
jobs:
- gophercloud-acceptance-test-mitaka
+ recheck-newton:
+ jobs:
+ - gophercloud-acceptance-test-newton
+ recheck-ocata:
+ jobs:
+ - gophercloud-acceptance-test-ocata
recheck-pike:
jobs:
- gophercloud-acceptance-test-pike
+ recheck-queens:
+ jobs:
+ - gophercloud-acceptance-test-queens
diff --git a/README.md b/README.md
index bb218c3fe9..8c5bfce796 100644
--- a/README.md
+++ b/README.md
@@ -127,7 +127,7 @@ new resource in the `server` variable (a
## Advanced Usage
-Have a look at the [FAQ](./FAQ.md) for some tips on customizing the way Gophercloud works.
+Have a look at the [FAQ](./docs/FAQ.md) for some tips on customizing the way Gophercloud works.
## Backwards-Compatibility Guarantees
@@ -148,12 +148,12 @@ We'd like to extend special thanks and appreciation to the following:
### OpenLab
-
+
OpenLab is providing a full CI environment to test each PR and merge for a variety of OpenStack releases.
### VEXXHOST
-
+
VEXXHOST is providing their services to assist with the development and testing of Gophercloud.
diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go
index 796736c36c..7dc43ad911 100644
--- a/acceptance/clients/clients.go
+++ b/acceptance/clients/clients.go
@@ -5,6 +5,7 @@ package clients
import (
"fmt"
+ "net/http"
"os"
"strings"
@@ -123,6 +124,8 @@ func NewBlockStorageV1Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -142,6 +145,8 @@ func NewBlockStorageV2Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewBlockStorageV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -161,6 +166,8 @@ func NewBlockStorageV3Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewBlockStorageV3(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -178,6 +185,8 @@ func NewBlockStorageV2NoAuthClient() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return noauth.NewBlockStorageNoAuth(client, noauth.EndpointOpts{
CinderEndpoint: os.Getenv("CINDER_ENDPOINT"),
})
@@ -195,6 +204,8 @@ func NewBlockStorageV3NoAuthClient() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return noauth.NewBlockStorageNoAuth(client, noauth.EndpointOpts{
CinderEndpoint: os.Getenv("CINDER_ENDPOINT"),
})
@@ -214,6 +225,8 @@ func NewComputeV2Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewComputeV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -233,6 +246,8 @@ func NewDBV1Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewDBV1(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -252,6 +267,8 @@ func NewDNSV2Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewDNSV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -271,6 +288,8 @@ func NewIdentityV2Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -290,6 +309,8 @@ func NewIdentityV2AdminClient() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
Availability: gophercloud.AvailabilityAdmin,
@@ -310,6 +331,8 @@ func NewIdentityV2UnauthenticatedClient() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{})
}
@@ -327,6 +350,8 @@ func NewIdentityV3Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -346,6 +371,8 @@ func NewIdentityV3UnauthenticatedClient() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{})
}
@@ -363,6 +390,8 @@ func NewImageServiceV2Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewImageServiceV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -382,6 +411,8 @@ func NewNetworkV2Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewNetworkV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -401,6 +432,8 @@ func NewObjectStorageV1Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
@@ -420,11 +453,88 @@ func NewSharedFileSystemV2Client() (*gophercloud.ServiceClient, error) {
return nil, err
}
+ client = configureDebug(client)
+
return openstack.NewSharedFileSystemV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
}
+// NewLoadBalancerV2Client returns a *ServiceClient for making calls to the
+// OpenStack Octavia v2 API. An error will be returned if authentication
+// or client creation was not possible.
+func NewLoadBalancerV2Client() (*gophercloud.ServiceClient, error) {
+ ao, err := openstack.AuthOptionsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ client = configureDebug(client)
+
+ return openstack.NewLoadBalancerV2(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+// NewClusteringV1Client returns a *ServiceClient for making calls
+// to the OpenStack Clustering v1 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewClusteringV1Client() (*gophercloud.ServiceClient, error) {
+ ao, err := openstack.AuthOptionsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ return openstack.NewClusteringV1(client, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+// NewMessagingV2Client returns a *ServiceClient for making calls
+// to the OpenStack Messaging (Zaqar) v2 API. An error will be returned
+// if authentication or client creation was not possible.
+func NewMessagingV2Client(clientID string) (*gophercloud.ServiceClient, error) {
+ ao, err := openstack.AuthOptionsFromEnv()
+ if err != nil {
+ return nil, err
+ }
+
+ client, err := openstack.AuthenticatedClient(ao)
+ if err != nil {
+ return nil, err
+ }
+
+ client = configureDebug(client)
+
+ return openstack.NewMessagingV2(client, clientID, gophercloud.EndpointOpts{
+ Region: os.Getenv("OS_REGION_NAME"),
+ })
+}
+
+// configureDebug will configure the provider client to print the API
+// requests and responses if OS_DEBUG is enabled.
+func configureDebug(client *gophercloud.ProviderClient) *gophercloud.ProviderClient {
+ if os.Getenv("OS_DEBUG") != "" {
+ client.HTTPClient = http.Client{
+ Transport: &LogRoundTripper{
+ Rt: &http.Transport{},
+ },
+ }
+ }
+
+ return client
+}
+
// NewContainerV1Client returns a *ServiceClient for making calls
// to the OpenStack Container V1 API. An error will be returned
// if authentication or client creation was not possible.
diff --git a/acceptance/clients/conditions.go b/acceptance/clients/conditions.go
new file mode 100644
index 0000000000..7bf3f26265
--- /dev/null
+++ b/acceptance/clients/conditions.go
@@ -0,0 +1,60 @@
+package clients
+
+import (
+ "os"
+ "testing"
+)
+
+// RequireAdmin will restrict a test to only be run by admin users.
+func RequireAdmin(t *testing.T) {
+ if os.Getenv("OS_USERNAME") != "admin" {
+ t.Skip("must be admin to run this test")
+ }
+}
+
+// RequireDNS will restrict a test to only be run in environments
+// that support DNSaaS.
+func RequireDNS(t *testing.T) {
+ if os.Getenv("OS_DNS_ENVIRONMENT") == "" {
+ t.Skip("this test requires DNSaaS")
+ }
+}
+
+// RequireGuestAgent will restrict a test to only be run in
+// environments that support the QEMU guest agent.
+func RequireGuestAgent(t *testing.T) {
+ if os.Getenv("OS_GUEST_AGENT") == "" {
+ t.Skip("this test requires support for qemu guest agent and to set OS_GUEST_AGENT to 1")
+ }
+}
+
+// RequireIdentityV2 will restrict a test to only be run in
+// environments that support the Identity V2 API.
+func RequireIdentityV2(t *testing.T) {
+ if os.Getenv("OS_IDENTITY_API_VERSION") != "2.0" {
+ t.Skip("this test requires support for the identity v2 API")
+ }
+}
+
+// RequireLiveMigration will restrict a test to only be run in
+// environments that support live migration.
+func RequireLiveMigration(t *testing.T) {
+ if os.Getenv("OS_LIVE_MIGRATE") == "" {
+ t.Skip("this test requires support for live migration and to set OS_LIVE_MIGRATE to 1")
+ }
+}
+
+// RequireLong will ensure long-running tests can run.
+func RequireLong(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping test in short mode")
+ }
+}
+
+// RequireNovaNetwork will restrict a test to only be run in
+// environments that support nova-network.
+func RequireNovaNetwork(t *testing.T) {
+ if os.Getenv("OS_NOVANET") == "" {
+ t.Skip("this test requires nova-network and to set OS_NOVANET to 1")
+ }
+}
diff --git a/acceptance/clients/http.go b/acceptance/clients/http.go
new file mode 100644
index 0000000000..3f42231e32
--- /dev/null
+++ b/acceptance/clients/http.go
@@ -0,0 +1,170 @@
+package clients
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "sort"
+ "strings"
+)
+
+// List of headers that need to be redacted
+var REDACT_HEADERS = []string{"x-auth-token", "x-auth-key", "x-service-token",
+ "x-storage-token", "x-account-meta-temp-url-key", "x-account-meta-temp-url-key-2",
+ "x-container-meta-temp-url-key", "x-container-meta-temp-url-key-2", "set-cookie",
+ "x-subject-token"}
+
+// LogRoundTripper satisfies the http.RoundTripper interface and is used to
+// customize the default http client RoundTripper to allow logging.
+type LogRoundTripper struct {
+ Rt http.RoundTripper
+}
+
+// RoundTrip performs a round-trip HTTP request and logs relevant information
+// about it.
+func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
+ defer func() {
+ if request.Body != nil {
+ request.Body.Close()
+ }
+ }()
+
+ var err error
+
+ log.Printf("[DEBUG] OpenStack Request URL: %s %s", request.Method, request.URL)
+ log.Printf("[DEBUG] OpenStack request Headers:\n%s", formatHeaders(request.Header))
+
+ if request.Body != nil {
+ request.Body, err = lrt.logRequest(request.Body, request.Header.Get("Content-Type"))
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ response, err := lrt.Rt.RoundTrip(request)
+ if response == nil {
+ return nil, err
+ }
+
+ log.Printf("[DEBUG] OpenStack Response Code: %d", response.StatusCode)
+ log.Printf("[DEBUG] OpenStack Response Headers:\n%s", formatHeaders(response.Header))
+
+ response.Body, err = lrt.logResponse(response.Body, response.Header.Get("Content-Type"))
+
+ return response, err
+}
+
+// logRequest will log the HTTP Request details.
+// If the body is JSON, it will attempt to be pretty-formatted.
+func (lrt *LogRoundTripper) logRequest(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
+ defer original.Close()
+
+ var bs bytes.Buffer
+ _, err := io.Copy(&bs, original)
+ if err != nil {
+ return nil, err
+ }
+
+ // Handle request contentType
+ if strings.HasPrefix(contentType, "application/json") {
+ debugInfo := lrt.formatJSON(bs.Bytes())
+ log.Printf("[DEBUG] OpenStack Request Body: %s", debugInfo)
+ } else {
+ log.Printf("[DEBUG] OpenStack Request Body: %s", bs.String())
+ }
+
+ return ioutil.NopCloser(strings.NewReader(bs.String())), nil
+}
+
+// logResponse will log the HTTP Response details.
+// If the body is JSON, it will attempt to be pretty-formatted.
+func (lrt *LogRoundTripper) logResponse(original io.ReadCloser, contentType string) (io.ReadCloser, error) {
+ if strings.HasPrefix(contentType, "application/json") {
+ var bs bytes.Buffer
+ defer original.Close()
+ _, err := io.Copy(&bs, original)
+ if err != nil {
+ return nil, err
+ }
+ debugInfo := lrt.formatJSON(bs.Bytes())
+ if debugInfo != "" {
+ log.Printf("[DEBUG] OpenStack Response Body: %s", debugInfo)
+ }
+ return ioutil.NopCloser(strings.NewReader(bs.String())), nil
+ }
+
+ log.Printf("[DEBUG] Not logging because OpenStack response body isn't JSON")
+ return original, nil
+}
+
+// formatJSON will try to pretty-format a JSON body.
+// It will also mask known fields which contain sensitive information.
+func (lrt *LogRoundTripper) formatJSON(raw []byte) string {
+ var data map[string]interface{}
+
+ err := json.Unmarshal(raw, &data)
+ if err != nil {
+ log.Printf("[DEBUG] Unable to parse OpenStack JSON: %s", err)
+ return string(raw)
+ }
+
+ // Mask known password fields
+ if v, ok := data["auth"].(map[string]interface{}); ok {
+ if v, ok := v["identity"].(map[string]interface{}); ok {
+ if v, ok := v["password"].(map[string]interface{}); ok {
+ if v, ok := v["user"].(map[string]interface{}); ok {
+ v["password"] = "***"
+ }
+ }
+ }
+ }
+
+ // Ignore the catalog
+ if v, ok := data["token"].(map[string]interface{}); ok {
+ if _, ok := v["catalog"]; ok {
+ return ""
+ }
+ }
+
+ pretty, err := json.MarshalIndent(data, "", " ")
+ if err != nil {
+ log.Printf("[DEBUG] Unable to re-marshal OpenStack JSON: %s", err)
+ return string(raw)
+ }
+
+ return string(pretty)
+}
+
+// redactHeaders processes a headers object, returning a redacted list
+func redactHeaders(headers http.Header) (processedHeaders []string) {
+ for name, header := range headers {
+ var sensitive bool
+
+ for _, redact_header := range REDACT_HEADERS {
+ if strings.ToLower(name) == strings.ToLower(redact_header) {
+ sensitive = true
+ }
+ }
+
+ for _, v := range header {
+ if sensitive {
+ processedHeaders = append(processedHeaders, fmt.Sprintf("%v: %v", name, "***"))
+ } else {
+ processedHeaders = append(processedHeaders, fmt.Sprintf("%v: %v", name, v))
+ }
+ }
+ }
+ return
+}
+
+// formatHeaders processes a headers object plus a deliminator, returning a string
+func formatHeaders(headers http.Header) string {
+ redactedHeaders := redactHeaders(headers)
+ sort.Strings(redactedHeaders)
+
+ return strings.Join(redactedHeaders, "\n")
+}
diff --git a/acceptance/openstack/blockstorage/extensions/services_test.go b/acceptance/openstack/blockstorage/extensions/services_test.go
new file mode 100644
index 0000000000..b65f586ec8
--- /dev/null
+++ b/acceptance/openstack/blockstorage/extensions/services_test.go
@@ -0,0 +1,32 @@
+// +build acceptance blockstorage
+
+package extensions
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/services"
+)
+
+func TestServicesList(t *testing.T) {
+ blockClient, err := clients.NewBlockStorageV3Client()
+ if err != nil {
+ t.Fatalf("Unable to create a blockstorage client: %v", err)
+ }
+
+ allPages, err := services.List(blockClient, services.ListOpts{}).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list services: %v", err)
+ }
+
+ allServices, err := services.ExtractServices(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract services")
+ }
+
+ for _, service := range allServices {
+ tools.PrintResource(t, service)
+ }
+}
diff --git a/acceptance/openstack/blockstorage/v2/blockstorage.go b/acceptance/openstack/blockstorage/v2/blockstorage.go
index 51c8e59cad..7b4682bbf1 100644
--- a/acceptance/openstack/blockstorage/v2/blockstorage.go
+++ b/acceptance/openstack/blockstorage/v2/blockstorage.go
@@ -11,6 +11,7 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/snapshots"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
// CreateVolume will create a volume with a random name and size of 1GB. An
@@ -44,10 +45,6 @@ func CreateVolume(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Vol
// CreateVolumeFromImage will create a volume from with a random name and size of
// 1GB. An error will be returned if the volume was unable to be created.
func CreateVolumeFromImage(t *testing.T, client *gophercloud.ServiceClient) (*volumes.Volume, error) {
- if testing.Short() {
- t.Skip("Skipping test that requires volume creation in short mode.")
- }
-
choices, err := clients.AcceptanceTestChoicesFromEnv()
if err != nil {
t.Fatal(err)
@@ -72,7 +69,15 @@ func CreateVolumeFromImage(t *testing.T, client *gophercloud.ServiceClient) (*vo
return volume, err
}
- return volume, nil
+ newVolume, err := volumes.Get(client, volume.ID).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ th.AssertEquals(t, newVolume.Name, volumeName)
+ th.AssertEquals(t, newVolume.Size, 1)
+
+ return newVolume, nil
}
// DeleteVolume will delete a volume. A fatal error will occur if the volume
diff --git a/acceptance/openstack/clustering/v1/pkg.go b/acceptance/openstack/clustering/v1/pkg.go
new file mode 100644
index 0000000000..e2200bc5ba
--- /dev/null
+++ b/acceptance/openstack/clustering/v1/pkg.go
@@ -0,0 +1,2 @@
+// Package v1 package contains acceptance tests for the Openstack Clustering V1 service.
+package v1
diff --git a/acceptance/openstack/clustering/v1/policies_test.go b/acceptance/openstack/clustering/v1/policies_test.go
new file mode 100644
index 0000000000..8fff7a5f54
--- /dev/null
+++ b/acceptance/openstack/clustering/v1/policies_test.go
@@ -0,0 +1,72 @@
+// +build acceptance clustering policies
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/policies"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestPolicyList(t *testing.T) {
+ client, err := clients.NewClusteringV1Client()
+ th.AssertNoErr(t, err)
+
+ allPages, err := policies.List(client, nil).AllPages()
+ th.AssertNoErr(t, err)
+
+ allPolicies, err := policies.ExtractPolicies(allPages)
+ th.AssertNoErr(t, err)
+
+ for _, v := range allPolicies {
+ tools.PrintResource(t, v)
+
+ if v.CreatedAt.IsZero() {
+ t.Fatalf("CreatedAt value should not be zero")
+ }
+ t.Log("Created at: " + v.CreatedAt.String())
+
+ if !v.UpdatedAt.IsZero() {
+ t.Log("Updated at: " + v.UpdatedAt.String())
+ }
+ }
+}
+
+func TestPolicyCreateAndDelete(t *testing.T) {
+ client, err := clients.NewClusteringV1Client()
+ th.AssertNoErr(t, err)
+
+ opts := policies.CreateOpts{
+ Name: "new_policy2",
+ Spec: policies.Spec{
+ Description: "new policy description",
+ Properties: map[string]interface{}{
+ "destroy_after_deletion": true,
+ "grace_period": 60,
+ "reduce_desired_capacity": false,
+ "criteria": "OLDEST_FIRST",
+ },
+ Type: "senlin.policy.deletion",
+ Version: "1.0",
+ },
+ }
+
+ createdPolicy, err := policies.Create(client, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ defer policies.Delete(client, createdPolicy.ID)
+
+ tools.PrintResource(t, createdPolicy)
+
+ if createdPolicy.CreatedAt.IsZero() {
+ t.Fatalf("CreatedAt value should not be zero")
+ }
+ t.Log("Created at: " + createdPolicy.CreatedAt.String())
+
+ if !createdPolicy.UpdatedAt.IsZero() {
+ t.Log("Updated at: " + createdPolicy.UpdatedAt.String())
+ }
+}
diff --git a/acceptance/openstack/clustering/v1/policytypes_test.go b/acceptance/openstack/clustering/v1/policytypes_test.go
new file mode 100644
index 0000000000..fdb42a3153
--- /dev/null
+++ b/acceptance/openstack/clustering/v1/policytypes_test.go
@@ -0,0 +1,64 @@
+// +build acceptance clustering policytypes
+
+package v1
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/policytypes"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestPolicyTypeList(t *testing.T) {
+ client, err := clients.NewClusteringV1Client()
+ th.AssertNoErr(t, err)
+
+ allPages, err := policytypes.List(client).AllPages()
+ th.AssertNoErr(t, err)
+
+ allPolicyTypes, err := policytypes.ExtractPolicyTypes(allPages)
+ th.AssertNoErr(t, err)
+
+ for _, v := range allPolicyTypes {
+ tools.PrintResource(t, v)
+ }
+}
+
+func TestPolicyTypeList_v_1_5(t *testing.T) {
+ client, err := clients.NewClusteringV1Client()
+ th.AssertNoErr(t, err)
+
+ client.Microversion = "1.5"
+ allPages, err := policytypes.List(client).AllPages()
+ th.AssertNoErr(t, err)
+
+ allPolicyTypes, err := policytypes.ExtractPolicyTypes(allPages)
+ th.AssertNoErr(t, err)
+
+ for _, v := range allPolicyTypes {
+ tools.PrintResource(t, v)
+ }
+}
+
+func TestPolicyTypeGet(t *testing.T) {
+ client, err := clients.NewClusteringV1Client()
+ th.AssertNoErr(t, err)
+
+ policyType, err := policytypes.Get(client, "senlin.policy.batch-1.0").Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, policyType)
+}
+
+func TestPolicyTypeGet_v_1_5(t *testing.T) {
+ client, err := clients.NewClusteringV1Client()
+ th.AssertNoErr(t, err)
+
+ client.Microversion = "1.5"
+ policyType, err := policytypes.Get(client, "senlin.policy.batch-1.0").Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, policyType)
+}
diff --git a/acceptance/openstack/clustering/v1/webhooktrigger_test.go b/acceptance/openstack/clustering/v1/webhooktrigger_test.go
new file mode 100644
index 0000000000..4acbabe5f4
--- /dev/null
+++ b/acceptance/openstack/clustering/v1/webhooktrigger_test.go
@@ -0,0 +1,32 @@
+package v1
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/webhooks"
+
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestClusteringWebhookTrigger(t *testing.T) {
+
+ client, err := clients.NewClusteringV1Client()
+ if err != nil {
+ t.Fatalf("Unable to create clustering client: %v", err)
+ }
+
+ // TODO: need to have cluster receiver created
+ receiverUUID := "f93f83f6-762b-41b6-b757-80507834d394"
+ actionID, err := webhooks.Trigger(client, receiverUUID, nil).Extract()
+ if err != nil {
+ // TODO: Uncomment next line once using real receiver
+ //t.Fatalf("Unable to extract webhooks trigger: %v", err)
+ t.Logf("TODO: Need to implement webhook trigger once PR receiver")
+ } else {
+ t.Logf("Webhook trigger action id %s", actionID)
+ }
+
+ // TODO: Need to compare to make sure action ID exists
+ th.AssertEquals(t, true, true)
+}
diff --git a/acceptance/openstack/compute/v2/aggregates_test.go b/acceptance/openstack/compute/v2/aggregates_test.go
index d0771155ce..352adb38e3 100644
--- a/acceptance/openstack/compute/v2/aggregates_test.go
+++ b/acceptance/openstack/compute/v2/aggregates_test.go
@@ -3,30 +3,144 @@
package v2
import (
+ "fmt"
"testing"
+ "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/aggregates"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestAggregatesList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := aggregates.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list aggregates: %v", err)
- }
+ th.AssertNoErr(t, err)
allAggregates, err := aggregates.ExtractAggregates(allPages)
- if err != nil {
- t.Fatalf("Unable to extract aggregates")
+ th.AssertNoErr(t, err)
+
+ for _, v := range allAggregates {
+ tools.PrintResource(t, v)
+ }
+}
+
+func TestAggregatesCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ aggregate, err := CreateAggregate(t, client)
+ th.AssertNoErr(t, err)
+
+ defer DeleteAggregate(t, client, aggregate)
+
+ tools.PrintResource(t, aggregate)
+
+ updateOpts := aggregates.UpdateOpts{
+ Name: "new_aggregate_name",
+ AvailabilityZone: "new_azone",
}
- for _, h := range allAggregates {
- tools.PrintResource(t, h)
+ updatedAggregate, err := aggregates.Update(client, aggregate.ID, updateOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, aggregate)
+
+ th.AssertEquals(t, updatedAggregate.Name, "new_aggregate_name")
+ th.AssertEquals(t, updatedAggregate.AvailabilityZone, "new_azone")
+}
+
+func TestAggregatesAddRemoveHost(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ hostToAdd, err := getHypervisor(t, client)
+ th.AssertNoErr(t, err)
+
+ aggregate, err := CreateAggregate(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteAggregate(t, client, aggregate)
+
+ addHostOpts := aggregates.AddHostOpts{
+ Host: hostToAdd.HypervisorHostname,
+ }
+
+ aggregateWithNewHost, err := aggregates.AddHost(client, aggregate.ID, addHostOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, aggregateWithNewHost)
+
+ th.AssertEquals(t, aggregateWithNewHost.Hosts[0], hostToAdd.HypervisorHostname)
+
+ removeHostOpts := aggregates.RemoveHostOpts{
+ Host: hostToAdd.HypervisorHostname,
}
+
+ aggregateWithRemovedHost, err := aggregates.RemoveHost(client, aggregate.ID, removeHostOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, aggregateWithRemovedHost)
+
+ th.AssertEquals(t, len(aggregateWithRemovedHost.Hosts), 0)
+}
+
+func TestAggregatesSetRemoveMetadata(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ aggregate, err := CreateAggregate(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteAggregate(t, client, aggregate)
+
+ opts := aggregates.SetMetadataOpts{
+ Metadata: map[string]interface{}{"key": "value"},
+ }
+
+ aggregateWithMetadata, err := aggregates.SetMetadata(client, aggregate.ID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, aggregateWithMetadata)
+
+ if _, ok := aggregateWithMetadata.Metadata["key"]; !ok {
+ t.Fatalf("aggregate %s did not contain metadata", aggregateWithMetadata.Name)
+ }
+
+ optsToRemove := aggregates.SetMetadataOpts{
+ Metadata: map[string]interface{}{"key": nil},
+ }
+
+ aggregateWithRemovedKey, err := aggregates.SetMetadata(client, aggregate.ID, optsToRemove).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, aggregateWithRemovedKey)
+
+ if _, ok := aggregateWithRemovedKey.Metadata["key"]; ok {
+ t.Fatalf("aggregate %s still contains metadata", aggregateWithRemovedKey.Name)
+ }
+}
+
+func getHypervisor(t *testing.T, client *gophercloud.ServiceClient) (*hypervisors.Hypervisor, error) {
+ allPages, err := hypervisors.List(client).AllPages()
+ th.AssertNoErr(t, err)
+
+ allHypervisors, err := hypervisors.ExtractHypervisors(allPages)
+ th.AssertNoErr(t, err)
+
+ for _, h := range allHypervisors {
+ return &h, nil
+ }
+
+ return nil, fmt.Errorf("Unable to get hypervisor")
}
diff --git a/acceptance/openstack/compute/v2/attachinterfaces_test.go b/acceptance/openstack/compute/v2/attachinterfaces_test.go
index 766a3aec8e..b8f9680285 100644
--- a/acceptance/openstack/compute/v2/attachinterfaces_test.go
+++ b/acceptance/openstack/compute/v2/attachinterfaces_test.go
@@ -7,54 +7,45 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
- "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestAttachDetachInterface(t *testing.T) {
+ clients.RequireLong(t)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
-
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
- newServer, err := servers.Get(client, server.ID).Extract()
- if err != nil {
- t.Errorf("Unable to retrieve server: %v", err)
- }
- tools.PrintResource(t, newServer)
-
- intOpts := attachinterfaces.CreateOpts{}
-
- iface, err := attachinterfaces.Create(client, server.ID, intOpts).Extract()
- if err != nil {
- t.Fatal(err)
- }
+ iface, err := AttachInterface(t, client, server.ID)
+ th.AssertNoErr(t, err)
+ defer DetachInterface(t, client, server.ID, iface.PortID)
tools.PrintResource(t, iface)
- allPages, err := attachinterfaces.List(client, server.ID).AllPages()
- if err != nil {
- t.Fatal(err)
- }
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
- allIfaces, err := attachinterfaces.ExtractInterfaces(allPages)
- if err != nil {
- t.Fatal(err)
- }
+ var found bool
+ for _, networkAddresses := range server.Addresses[choices.NetworkName].([]interface{}) {
+ address := networkAddresses.(map[string]interface{})
+ if address["OS-EXT-IPS:type"] == "fixed" {
+ fixedIP := address["addr"].(string)
- for _, i := range allIfaces {
- tools.PrintResource(t, i)
+ for _, v := range iface.FixedIPs {
+ if fixedIP == v.IPAddress {
+ found = true
+ }
+ }
+ }
}
- err = attachinterfaces.Delete(client, server.ID, iface.PortID).ExtractErr()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertEquals(t, found, true)
}
diff --git a/acceptance/openstack/compute/v2/availabilityzones_test.go b/acceptance/openstack/compute/v2/availabilityzones_test.go
new file mode 100644
index 0000000000..4d030c2968
--- /dev/null
+++ b/acceptance/openstack/compute/v2/availabilityzones_test.go
@@ -0,0 +1,58 @@
+// +build acceptance compute availabilityzones
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestAvailabilityZonesList(t *testing.T) {
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ allPages, err := availabilityzones.List(client).AllPages()
+ th.AssertNoErr(t, err)
+
+ availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages)
+ th.AssertNoErr(t, err)
+
+ var found bool
+ for _, zoneInfo := range availabilityZoneInfo {
+ tools.PrintResource(t, zoneInfo)
+
+ if zoneInfo.ZoneName == "nova" {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, true)
+}
+
+func TestAvailabilityZonesListDetail(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ allPages, err := availabilityzones.ListDetail(client).AllPages()
+ th.AssertNoErr(t, err)
+
+ availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages)
+ th.AssertNoErr(t, err)
+
+ var found bool
+ for _, zoneInfo := range availabilityZoneInfo {
+ tools.PrintResource(t, zoneInfo)
+
+ if zoneInfo.ZoneName == "nova" {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, true)
+}
diff --git a/acceptance/openstack/compute/v2/bootfromvolume_test.go b/acceptance/openstack/compute/v2/bootfromvolume_test.go
index 2ba8888bf2..50a2396f94 100644
--- a/acceptance/openstack/compute/v2/bootfromvolume_test.go
+++ b/acceptance/openstack/compute/v2/bootfromvolume_test.go
@@ -9,22 +9,18 @@ import (
blockstorage "github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestBootFromImage(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
@@ -37,28 +33,22 @@ func TestBootFromImage(t *testing.T) {
}
server, err := CreateBootableVolumeServer(t, client, blockDevices)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
tools.PrintResource(t, server)
+
+ th.AssertEquals(t, server.Image["id"], choices.ImageID)
}
func TestBootFromNewVolume(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
@@ -71,33 +61,40 @@ func TestBootFromNewVolume(t *testing.T) {
}
server, err := CreateBootableVolumeServer(t, client, blockDevices)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
+ attachPages, err := volumeattach.List(client, server.ID).AllPages()
+ th.AssertNoErr(t, err)
+
+ attachments, err := volumeattach.ExtractVolumeAttachments(attachPages)
+ th.AssertNoErr(t, err)
+
tools.PrintResource(t, server)
+ tools.PrintResource(t, attachments)
+
+ if server.Image != nil {
+ t.Fatalf("server image should be nil")
+ }
+
+ th.AssertEquals(t, len(attachments), 1)
+
+ // TODO: volumes_attached extension
}
func TestBootFromExistingVolume(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
computeClient, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
blockStorageClient, err := clients.NewBlockStorageV2Client()
- if err != nil {
- t.Fatalf("Unable to create a block storage client: %v", err)
- }
+ th.AssertNoErr(t, err)
volume, err := blockstorage.CreateVolumeFromImage(t, blockStorageClient)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, volume)
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
@@ -109,28 +106,35 @@ func TestBootFromExistingVolume(t *testing.T) {
}
server, err := CreateBootableVolumeServer(t, computeClient, blockDevices)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, computeClient, server)
+ attachPages, err := volumeattach.List(computeClient, server.ID).AllPages()
+ th.AssertNoErr(t, err)
+
+ attachments, err := volumeattach.ExtractVolumeAttachments(attachPages)
+ th.AssertNoErr(t, err)
+
tools.PrintResource(t, server)
+ tools.PrintResource(t, attachments)
+
+ if server.Image != nil {
+ t.Fatalf("server image should be nil")
+ }
+
+ th.AssertEquals(t, len(attachments), 1)
+ th.AssertEquals(t, attachments[0].VolumeID, volume.ID)
+ // TODO: volumes_attached extension
}
func TestBootFromMultiEphemeralServer(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
@@ -160,28 +164,20 @@ func TestBootFromMultiEphemeralServer(t *testing.T) {
}
server, err := CreateMultiEphemeralServer(t, client, blockDevices)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
tools.PrintResource(t, server)
}
func TestAttachNewVolume(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
@@ -201,38 +197,38 @@ func TestAttachNewVolume(t *testing.T) {
}
server, err := CreateBootableVolumeServer(t, client, blockDevices)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
+ attachPages, err := volumeattach.List(client, server.ID).AllPages()
+ th.AssertNoErr(t, err)
+
+ attachments, err := volumeattach.ExtractVolumeAttachments(attachPages)
+ th.AssertNoErr(t, err)
+
tools.PrintResource(t, server)
+ tools.PrintResource(t, attachments)
+
+ th.AssertEquals(t, server.Image["id"], choices.ImageID)
+ th.AssertEquals(t, len(attachments), 1)
+
+ // TODO: volumes_attached extension
}
func TestAttachExistingVolume(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
computeClient, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
blockStorageClient, err := clients.NewBlockStorageV2Client()
- if err != nil {
- t.Fatalf("Unable to create a block storage client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
volume, err := blockstorage.CreateVolume(t, blockStorageClient)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
@@ -252,10 +248,21 @@ func TestAttachExistingVolume(t *testing.T) {
}
server, err := CreateBootableVolumeServer(t, computeClient, blockDevices)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, computeClient, server)
+ attachPages, err := volumeattach.List(computeClient, server.ID).AllPages()
+ th.AssertNoErr(t, err)
+
+ attachments, err := volumeattach.ExtractVolumeAttachments(attachPages)
+ th.AssertNoErr(t, err)
+
tools.PrintResource(t, server)
+ tools.PrintResource(t, attachments)
+
+ th.AssertEquals(t, server.Image["id"], choices.ImageID)
+ th.AssertEquals(t, len(attachments), 1)
+ th.AssertEquals(t, attachments[0].VolumeID, volume.ID)
+
+ // TODO: volumes_attached extension
}
diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go
index fad4673b42..570378ddfd 100644
--- a/acceptance/openstack/compute/v2/compute.go
+++ b/acceptance/openstack/compute/v2/compute.go
@@ -11,7 +11,8 @@ import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
- "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
+ "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
dsr "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
@@ -25,7 +26,9 @@ import (
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/aggregates"
"golang.org/x/crypto/ssh"
)
@@ -63,14 +66,69 @@ func AssociateFloatingIPWithFixedIP(t *testing.T, client *gophercloud.ServiceCli
return nil
}
+// AttachInterface will create and attach an interface on a given server.
+// An error will returned if the interface could not be created.
+func AttachInterface(t *testing.T, client *gophercloud.ServiceClient, serverID string) (*attachinterfaces.Interface, error) {
+ t.Logf("Attempting to attach interface to server %s", serverID)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
+ if err != nil {
+ return nil, err
+ }
+
+ createOpts := attachinterfaces.CreateOpts{
+ NetworkID: networkID,
+ }
+
+ iface, err := attachinterfaces.Create(client, serverID, createOpts).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ t.Logf("Successfully created interface %s on server %s", iface.PortID, serverID)
+
+ return iface, nil
+}
+
+// CreateAggregate will create an aggregate with random name and available zone.
+// An error will be returned if the aggregate could not be created.
+func CreateAggregate(t *testing.T, client *gophercloud.ServiceClient) (*aggregates.Aggregate, error) {
+ aggregateName := tools.RandomString("aggregate_", 5)
+ availabilityZone := tools.RandomString("zone_", 5)
+ t.Logf("Attempting to create aggregate %s", aggregateName)
+
+ createOpts := aggregates.CreateOpts{
+ Name: aggregateName,
+ AvailabilityZone: availabilityZone,
+ }
+
+ aggregate, err := aggregates.Create(client, createOpts).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ t.Logf("Successfully created aggregate %d", aggregate.ID)
+
+ aggregate, err = aggregates.Get(client, aggregate.ID).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ th.AssertEquals(t, aggregate.Name, aggregateName)
+ th.AssertEquals(t, aggregate.AvailabilityZone, availabilityZone)
+
+ return aggregate, nil
+}
+
// CreateBootableVolumeServer works like CreateServer but is configured with
// one or more block devices defined by passing in []bootfromvolume.BlockDevice.
// An error will be returned if a server was unable to be created.
func CreateBootableVolumeServer(t *testing.T, client *gophercloud.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
-
var server *servers.Server
choices, err := clients.AcceptanceTestChoicesFromEnv()
@@ -112,6 +170,12 @@ func CreateBootableVolumeServer(t *testing.T, client *gophercloud.ServiceClient,
}
newServer, err := servers.Get(client, server.ID).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ th.AssertEquals(t, newServer.Name, name)
+ th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID)
return newServer, nil
}
@@ -159,6 +223,12 @@ func CreateFlavor(t *testing.T, client *gophercloud.ServiceClient) (*flavors.Fla
t.Logf("Successfully created flavor %s", flavor.ID)
+ th.AssertEquals(t, flavor.Name, flavorName)
+ th.AssertEquals(t, flavor.RAM, 1)
+ th.AssertEquals(t, flavor.Disk, 1)
+ th.AssertEquals(t, flavor.VCPUs, 1)
+ th.AssertEquals(t, flavor.IsPublic, true)
+
return flavor, nil
}
@@ -215,6 +285,9 @@ func CreateKeyPair(t *testing.T, client *gophercloud.ServiceClient) (*keypairs.K
}
t.Logf("Created keypair: %s", keyPairName)
+
+ th.AssertEquals(t, keyPair.Name, keyPairName)
+
return keyPair, nil
}
@@ -224,10 +297,6 @@ func CreateKeyPair(t *testing.T, client *gophercloud.ServiceClient) (*keypairs.K
// are actually local ephemeral disks.
// An error will be returned if a server was unable to be created.
func CreateMultiEphemeralServer(t *testing.T, client *gophercloud.ServiceClient, blockDevices []bootfromvolume.BlockDevice) (*servers.Server, error) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
-
var server *servers.Server
choices, err := clients.AcceptanceTestChoicesFromEnv()
@@ -267,6 +336,10 @@ func CreateMultiEphemeralServer(t *testing.T, client *gophercloud.ServiceClient,
newServer, err := servers.Get(client, server.ID).Extract()
+ th.AssertEquals(t, newServer.Name, name)
+ th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID)
+ th.AssertEquals(t, newServer.Image["id"], choices.ImageID)
+
return newServer, nil
}
@@ -292,45 +365,63 @@ func CreatePrivateFlavor(t *testing.T, client *gophercloud.ServiceClient) (*flav
t.Logf("Successfully created flavor %s", flavor.ID)
+ th.AssertEquals(t, flavor.Name, flavorName)
+ th.AssertEquals(t, flavor.RAM, 1)
+ th.AssertEquals(t, flavor.Disk, 1)
+ th.AssertEquals(t, flavor.VCPUs, 1)
+ th.AssertEquals(t, flavor.IsPublic, false)
+
return flavor, nil
}
// CreateSecurityGroup will create a security group with a random name.
// An error will be returned if one was failed to be created.
-func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (secgroups.SecurityGroup, error) {
+func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (*secgroups.SecurityGroup, error) {
+ name := tools.RandomString("secgroup_", 5)
+
createOpts := secgroups.CreateOpts{
- Name: tools.RandomString("secgroup_", 5),
+ Name: name,
Description: "something",
}
securityGroup, err := secgroups.Create(client, createOpts).Extract()
if err != nil {
- return *securityGroup, err
+ return nil, err
}
t.Logf("Created security group: %s", securityGroup.ID)
- return *securityGroup, nil
+
+ th.AssertEquals(t, securityGroup.Name, name)
+
+ return securityGroup, nil
}
// CreateSecurityGroupRule will create a security group rule with a random name
// and a random TCP port range between port 80 and 99. An error will be
// returned if the rule failed to be created.
-func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, securityGroupID string) (secgroups.Rule, error) {
+func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, securityGroupID string) (*secgroups.Rule, error) {
+ fromPort := tools.RandomInt(80, 89)
+ toPort := tools.RandomInt(90, 99)
createOpts := secgroups.CreateRuleOpts{
ParentGroupID: securityGroupID,
- FromPort: tools.RandomInt(80, 89),
- ToPort: tools.RandomInt(90, 99),
+ FromPort: fromPort,
+ ToPort: toPort,
IPProtocol: "TCP",
CIDR: "0.0.0.0/0",
}
rule, err := secgroups.CreateRule(client, createOpts).Extract()
if err != nil {
- return *rule, err
+ return nil, err
}
t.Logf("Created security group rule: %s", rule.ID)
- return *rule, nil
+
+ th.AssertEquals(t, rule.FromPort, fromPort)
+ th.AssertEquals(t, rule.ToPort, toPort)
+ th.AssertEquals(t, rule.ParentGroupID, securityGroupID)
+
+ return rule, nil
}
// CreateServer creates a basic instance with a randomly generated name.
@@ -339,12 +430,6 @@ func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, se
// The instance will be launched on the network specified in OS_NETWORK_NAME.
// An error will be returned if the instance was unable to be created.
func CreateServer(t *testing.T, client *gophercloud.ServiceClient) (*servers.Server, error) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
-
- var server *servers.Server
-
choices, err := clients.AcceptanceTestChoicesFromEnv()
if err != nil {
t.Fatal(err)
@@ -352,7 +437,7 @@ func CreateServer(t *testing.T, client *gophercloud.ServiceClient) (*servers.Ser
networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
if err != nil {
- return server, err
+ return nil, err
}
name := tools.RandomString("ACPTTEST", 16)
@@ -360,7 +445,7 @@ func CreateServer(t *testing.T, client *gophercloud.ServiceClient) (*servers.Ser
pwd := tools.MakeNewPassword("")
- server, err = servers.Create(client, servers.CreateOpts{
+ server, err := servers.Create(client, servers.CreateOpts{
Name: name,
FlavorRef: choices.FlavorID,
ImageRef: choices.ImageID,
@@ -383,10 +468,19 @@ func CreateServer(t *testing.T, client *gophercloud.ServiceClient) (*servers.Ser
}
if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
- return server, err
+ return nil, err
}
- return server, nil
+ newServer, err := servers.Get(client, server.ID).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ th.AssertEquals(t, newServer.Name, name)
+ th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID)
+ th.AssertEquals(t, newServer.Image["id"], choices.ImageID)
+
+ return newServer, nil
}
// CreateServerWithoutImageRef creates a basic instance with a randomly generated name.
@@ -395,12 +489,6 @@ func CreateServer(t *testing.T, client *gophercloud.ServiceClient) (*servers.Ser
// The instance will be launched on the network specified in OS_NETWORK_NAME.
// An error will be returned if the instance was unable to be created.
func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient) (*servers.Server, error) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
-
- var server *servers.Server
-
choices, err := clients.AcceptanceTestChoicesFromEnv()
if err != nil {
t.Fatal(err)
@@ -408,7 +496,7 @@ func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient
networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
if err != nil {
- return server, err
+ return nil, err
}
name := tools.RandomString("ACPTTEST", 16)
@@ -416,7 +504,7 @@ func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient
pwd := tools.MakeNewPassword("")
- server, err = servers.Create(client, servers.CreateOpts{
+ server, err := servers.Create(client, servers.CreateOpts{
Name: name,
FlavorRef: choices.FlavorID,
AdminPass: pwd,
@@ -431,11 +519,11 @@ func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient
},
}).Extract()
if err != nil {
- return server, err
+ return nil, err
}
if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
- return server, err
+ return nil, err
}
return server, nil
@@ -444,27 +532,29 @@ func CreateServerWithoutImageRef(t *testing.T, client *gophercloud.ServiceClient
// CreateServerGroup will create a server with a random name. An error will be
// returned if the server group failed to be created.
func CreateServerGroup(t *testing.T, client *gophercloud.ServiceClient, policy string) (*servergroups.ServerGroup, error) {
+ name := tools.RandomString("ACPTTEST", 16)
+
+ t.Logf("Attempting to create server group %s", name)
+
sg, err := servergroups.Create(client, &servergroups.CreateOpts{
- Name: "test",
+ Name: name,
Policies: []string{policy},
}).Extract()
if err != nil {
- return sg, err
+ return nil, err
}
+ t.Logf("Successfully created server group %s", name)
+
+ th.AssertEquals(t, sg.Name, name)
+
return sg, nil
}
// CreateServerInServerGroup works like CreateServer but places the instance in
// a specified Server Group.
func CreateServerInServerGroup(t *testing.T, client *gophercloud.ServiceClient, serverGroup *servergroups.ServerGroup) (*servers.Server, error) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
-
- var server *servers.Server
-
choices, err := clients.AcceptanceTestChoicesFromEnv()
if err != nil {
t.Fatal(err)
@@ -472,7 +562,7 @@ func CreateServerInServerGroup(t *testing.T, client *gophercloud.ServiceClient,
networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
if err != nil {
- return server, err
+ return nil, err
}
name := tools.RandomString("ACPTTEST", 16)
@@ -496,23 +586,30 @@ func CreateServerInServerGroup(t *testing.T, client *gophercloud.ServiceClient,
Group: serverGroup.ID,
},
}
- server, err = servers.Create(client, schedulerHintsOpts).Extract()
+ server, err := servers.Create(client, schedulerHintsOpts).Extract()
if err != nil {
- return server, err
+ return nil, err
}
- return server, nil
+ if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
+ return nil, err
+ }
+
+ newServer, err := servers.Get(client, server.ID).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ th.AssertEquals(t, newServer.Name, name)
+ th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID)
+ th.AssertEquals(t, newServer.Image["id"], choices.ImageID)
+
+ return newServer, nil
}
// CreateServerWithPublicKey works the same as CreateServer, but additionally
// configures the server with a specified Key Pair name.
func CreateServerWithPublicKey(t *testing.T, client *gophercloud.ServiceClient, keyPairName string) (*servers.Server, error) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
-
- var server *servers.Server
-
choices, err := clients.AcceptanceTestChoicesFromEnv()
if err != nil {
t.Fatal(err)
@@ -520,7 +617,7 @@ func CreateServerWithPublicKey(t *testing.T, client *gophercloud.ServiceClient,
networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
if err != nil {
- return server, err
+ return nil, err
}
name := tools.RandomString("ACPTTEST", 16)
@@ -535,19 +632,28 @@ func CreateServerWithPublicKey(t *testing.T, client *gophercloud.ServiceClient,
},
}
- server, err = servers.Create(client, keypairs.CreateOptsExt{
+ server, err := servers.Create(client, keypairs.CreateOptsExt{
CreateOptsBuilder: serverCreateOpts,
KeyName: keyPairName,
}).Extract()
if err != nil {
- return server, err
+ return nil, err
}
if err := WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
- return server, err
+ return nil, err
}
- return server, nil
+ newServer, err := servers.Get(client, server.ID).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ th.AssertEquals(t, newServer.Name, name)
+ th.AssertEquals(t, newServer.Flavor["id"], choices.FlavorID)
+ th.AssertEquals(t, newServer.Image["id"], choices.ImageID)
+
+ return newServer, nil
}
// CreateVolumeAttachment will attach a volume to a server. An error will be
@@ -570,6 +676,18 @@ func CreateVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blo
return volumeAttachment, nil
}
+// DeleteAggregate will delete a given host aggregate. A fatal error will occur if
+// the aggregate deleting is failed. This works best when using it as a
+// deferred function.
+func DeleteAggregate(t *testing.T, client *gophercloud.ServiceClient, aggregate *aggregates.Aggregate) {
+ err := aggregates.Delete(client, aggregate.ID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete aggregate %d", aggregate.ID)
+ }
+
+ t.Logf("Deleted aggregate: %d", aggregate.ID)
+}
+
// DeleteDefaultRule deletes a default security group rule.
// A fatal error will occur if the rule failed to delete. This works best when
// using it as a deferred function.
@@ -619,25 +737,25 @@ func DeleteKeyPair(t *testing.T, client *gophercloud.ServiceClient, keyPair *key
// DeleteSecurityGroup will delete a security group. A fatal error will occur
// if the group failed to be deleted. This works best as a deferred function.
-func DeleteSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, securityGroup secgroups.SecurityGroup) {
- err := secgroups.Delete(client, securityGroup.ID).ExtractErr()
+func DeleteSecurityGroup(t *testing.T, client *gophercloud.ServiceClient, securityGroupID string) {
+ err := secgroups.Delete(client, securityGroupID).ExtractErr()
if err != nil {
- t.Fatalf("Unable to delete security group %s: %s", securityGroup.ID, err)
+ t.Fatalf("Unable to delete security group %s: %s", securityGroupID, err)
}
- t.Logf("Deleted security group: %s", securityGroup.ID)
+ t.Logf("Deleted security group: %s", securityGroupID)
}
// DeleteSecurityGroupRule will delete a security group rule. A fatal error
// will occur if the rule failed to be deleted. This works best when used
// as a deferred function.
-func DeleteSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, rule secgroups.Rule) {
- err := secgroups.DeleteRule(client, rule.ID).ExtractErr()
+func DeleteSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, ruleID string) {
+ err := secgroups.DeleteRule(client, ruleID).ExtractErr()
if err != nil {
t.Fatalf("Unable to delete rule: %v", err)
}
- t.Logf("Deleted security group rule: %s", rule.ID)
+ t.Logf("Deleted security group rule: %s", ruleID)
}
// DeleteServer deletes an instance via its UUID.
@@ -649,6 +767,16 @@ func DeleteServer(t *testing.T, client *gophercloud.ServiceClient, server *serve
t.Fatalf("Unable to delete server %s: %s", server.ID, err)
}
+ if err := WaitForComputeStatus(client, server, "DELETED"); err != nil {
+ if _, ok := err.(gophercloud.ErrDefault404); ok {
+ t.Logf("Deleted server: %s", server.ID)
+ return
+ }
+ t.Fatalf("Error deleting server %s: %s", server.ID, err)
+ }
+
+ // If we reach this point, the API returned an actual DELETED status
+ // which is a very short window of time, but happens occasionally.
t.Logf("Deleted server: %s", server.ID)
}
@@ -680,6 +808,20 @@ func DeleteVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blo
t.Logf("Deleted volume: %s", volumeAttachment.VolumeID)
}
+// DetachInterface will detach an interface from a server. A fatal
+// error will occur if the interface could not be detached. This works best
+// when used as a deferred function.
+func DetachInterface(t *testing.T, client *gophercloud.ServiceClient, serverID, portID string) {
+ t.Logf("Attempting to detach interface %s from server %s", portID, serverID)
+
+ err := attachinterfaces.Delete(client, serverID, portID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to detach interface %s from server %s", portID, serverID)
+ }
+
+ t.Logf("Detached interface %s from server %s", portID, serverID)
+}
+
// DisassociateFloatingIP will disassociate a floating IP from an instance. A
// fatal error will occur if the floating IP failed to disassociate. This works
// best when using it as a deferred function.
@@ -762,6 +904,10 @@ func ImportPublicKey(t *testing.T, client *gophercloud.ServiceClient, publicKey
}
t.Logf("Created keypair: %s", keyPairName)
+
+ th.AssertEquals(t, keyPair.Name, keyPairName)
+ th.AssertEquals(t, keyPair.PublicKey, publicKey)
+
return keyPair, nil
}
diff --git a/acceptance/openstack/compute/v2/defsecrules_test.go b/acceptance/openstack/compute/v2/defsecrules_test.go
index 16c43f4c75..e97a378718 100644
--- a/acceptance/openstack/compute/v2/defsecrules_test.go
+++ b/acceptance/openstack/compute/v2/defsecrules_test.go
@@ -8,23 +8,21 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
dsr "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/defsecrules"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestDefSecRulesList(t *testing.T) {
+ clients.RequireAdmin(t)
+ clients.RequireNovaNetwork(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := dsr.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list default rules: %v", err)
- }
+ th.AssertNoErr(t, err)
allDefaultRules, err := dsr.ExtractDefaultRules(allPages)
- if err != nil {
- t.Fatalf("Unable to extract default rules: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, defaultRule := range allDefaultRules {
tools.PrintResource(t, defaultRule)
@@ -32,36 +30,32 @@ func TestDefSecRulesList(t *testing.T) {
}
func TestDefSecRulesCreate(t *testing.T) {
+ clients.RequireAdmin(t)
+ clients.RequireNovaNetwork(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
defaultRule, err := CreateDefaultRule(t, client)
- if err != nil {
- t.Fatalf("Unable to create default rule: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteDefaultRule(t, client, defaultRule)
tools.PrintResource(t, defaultRule)
}
func TestDefSecRulesGet(t *testing.T) {
+ clients.RequireAdmin(t)
+ clients.RequireNovaNetwork(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
defaultRule, err := CreateDefaultRule(t, client)
- if err != nil {
- t.Fatalf("Unable to create default rule: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteDefaultRule(t, client, defaultRule)
newDefaultRule, err := dsr.Get(client, defaultRule.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get default rule %s: %v", defaultRule.ID, err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newDefaultRule)
}
diff --git a/acceptance/openstack/compute/v2/extension_test.go b/acceptance/openstack/compute/v2/extension_test.go
index 5b2cf4a42d..f76cc52e06 100644
--- a/acceptance/openstack/compute/v2/extension_test.go
+++ b/acceptance/openstack/compute/v2/extension_test.go
@@ -8,39 +8,39 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/common/extensions"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestExtensionsList(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := extensions.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list extensions: %v", err)
- }
+ th.AssertNoErr(t, err)
allExtensions, err := extensions.ExtractExtensions(allPages)
- if err != nil {
- t.Fatalf("Unable to extract extensions: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, extension := range allExtensions {
tools.PrintResource(t, extension)
+
+ if extension.Name == "SchedulerHints" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
-func TestExtensionGet(t *testing.T) {
+func TestExtensionsGet(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
extension, err := extensions.Get(client, "os-admin-actions").Extract()
- if err != nil {
- t.Fatalf("Unable to get extension os-admin-actions: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, extension)
+
+ th.AssertEquals(t, extension.Name, "AdminActions")
}
diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go
index bb6c5c41d9..d4aa341a74 100644
--- a/acceptance/openstack/compute/v2/flavors_test.go
+++ b/acceptance/openstack/compute/v2/flavors_test.go
@@ -8,136 +8,122 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
+ th "github.com/gophercloud/gophercloud/testhelper"
identity "github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3"
)
func TestFlavorsList(t *testing.T) {
- t.Logf("** Default flavors (same as Project flavors): **")
- t.Logf("")
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
allPages, err := flavors.ListDetail(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve flavors: %v", err)
- }
+ th.AssertNoErr(t, err)
allFlavors, err := flavors.ExtractFlavors(allPages)
- if err != nil {
- t.Fatalf("Unable to extract flavor results: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, flavor := range allFlavors {
tools.PrintResource(t, flavor)
+
+ if flavor.ID == choices.FlavorID {
+ found = true
+ }
}
- flavorAccessTypes := [3]flavors.AccessType{flavors.PublicAccess, flavors.PrivateAccess, flavors.AllAccess}
- for _, flavorAccessType := range flavorAccessTypes {
- t.Logf("** %s flavors: **", flavorAccessType)
- t.Logf("")
+ th.AssertEquals(t, found, true)
+}
+
+func TestFlavorsAccessTypeList(t *testing.T) {
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ flavorAccessTypes := map[string]flavors.AccessType{
+ "public": flavors.PublicAccess,
+ "private": flavors.PrivateAccess,
+ "all": flavors.AllAccess,
+ }
+
+ for flavorTypeName, flavorAccessType := range flavorAccessTypes {
+ t.Logf("** %s flavors: **", flavorTypeName)
allPages, err := flavors.ListDetail(client, flavors.ListOpts{AccessType: flavorAccessType}).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve flavors: %v", err)
- }
+ th.AssertNoErr(t, err)
allFlavors, err := flavors.ExtractFlavors(allPages)
- if err != nil {
- t.Fatalf("Unable to extract flavor results: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, flavor := range allFlavors {
tools.PrintResource(t, flavor)
- t.Logf("")
}
}
-
}
func TestFlavorsGet(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
flavor, err := flavors.Get(client, choices.FlavorID).Extract()
- if err != nil {
- t.Fatalf("Unable to get flavor information: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, flavor)
+
+ th.AssertEquals(t, flavor.ID, choices.FlavorID)
}
-func TestFlavorCreateDelete(t *testing.T) {
+func TestFlavorsCreateDelete(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
flavor, err := CreateFlavor(t, client)
- if err != nil {
- t.Fatalf("Unable to create flavor: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteFlavor(t, client, flavor)
tools.PrintResource(t, flavor)
}
-func TestFlavorAccessesList(t *testing.T) {
+func TestFlavorsAccessesList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
flavor, err := CreatePrivateFlavor(t, client)
- if err != nil {
- t.Fatalf("Unable to create flavor: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteFlavor(t, client, flavor)
allPages, err := flavors.ListAccesses(client, flavor.ID).AllPages()
- if err != nil {
- t.Fatalf("Unable to list flavor accesses: %v", err)
- }
+ th.AssertNoErr(t, err)
allAccesses, err := flavors.ExtractAccesses(allPages)
- if err != nil {
- t.Fatalf("Unable to extract accesses: %v", err)
- }
+ th.AssertNoErr(t, err)
- for _, access := range allAccesses {
- tools.PrintResource(t, access)
- }
+ th.AssertEquals(t, len(allAccesses), 0)
}
-func TestFlavorAccessCRUD(t *testing.T) {
+func TestFlavorsAccessCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
identityClient, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatal("Unable to create identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
project, err := identity.CreateProject(t, identityClient, nil)
- if err != nil {
- t.Fatal("Unable to create project: %v", err)
- }
+ th.AssertNoErr(t, err)
defer identity.DeleteProject(t, identityClient, project.ID)
flavor, err := CreatePrivateFlavor(t, client)
- if err != nil {
- t.Fatalf("Unable to create flavor: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteFlavor(t, client, flavor)
addAccessOpts := flavors.AddAccessOpts{
@@ -145,25 +131,34 @@ func TestFlavorAccessCRUD(t *testing.T) {
}
accessList, err := flavors.AddAccess(client, flavor.ID, addAccessOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to add access to flavor: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, len(accessList), 1)
+ th.AssertEquals(t, accessList[0].TenantID, project.ID)
+ th.AssertEquals(t, accessList[0].FlavorID, flavor.ID)
for _, access := range accessList {
tools.PrintResource(t, access)
}
+
+ removeAccessOpts := flavors.RemoveAccessOpts{
+ Tenant: project.ID,
+ }
+
+ accessList, err = flavors.RemoveAccess(client, flavor.ID, removeAccessOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, len(accessList), 0)
}
-func TestFlavorExtraSpecsCRUD(t *testing.T) {
+func TestFlavorsExtraSpecsCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
flavor, err := CreatePrivateFlavor(t, client)
- if err != nil {
- t.Fatalf("Unable to create flavor: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteFlavor(t, client, flavor)
createOpts := flavors.ExtraSpecsOpts{
@@ -171,22 +166,37 @@ func TestFlavorExtraSpecsCRUD(t *testing.T) {
"hw:cpu_thread_policy": "CPU-THREAD-POLICY",
}
createdExtraSpecs, err := flavors.CreateExtraSpecs(client, flavor.ID, createOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to create flavor extra_specs: %v", err)
- }
+ th.AssertNoErr(t, err)
+
tools.PrintResource(t, createdExtraSpecs)
- allExtraSpecs, err := flavors.ListExtraSpecs(client, flavor.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get flavor extra_specs: %v", err)
+ th.AssertEquals(t, len(createdExtraSpecs), 2)
+ th.AssertEquals(t, createdExtraSpecs["hw:cpu_policy"], "CPU-POLICY")
+ th.AssertEquals(t, createdExtraSpecs["hw:cpu_thread_policy"], "CPU-THREAD-POLICY")
+
+ err = flavors.DeleteExtraSpec(client, flavor.ID, "hw:cpu_policy").ExtractErr()
+ th.AssertNoErr(t, err)
+
+ updateOpts := flavors.ExtraSpecsOpts{
+ "hw:cpu_thread_policy": "CPU-THREAD-POLICY-BETTER",
}
+ updatedExtraSpec, err := flavors.UpdateExtraSpec(client, flavor.ID, updateOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, updatedExtraSpec)
+
+ allExtraSpecs, err := flavors.ListExtraSpecs(client, flavor.ID).Extract()
+ th.AssertNoErr(t, err)
+
tools.PrintResource(t, allExtraSpecs)
- for key, _ := range allExtraSpecs {
- spec, err := flavors.GetExtraSpec(client, flavor.ID, key).Extract()
- if err != nil {
- t.Fatalf("Unable to get flavor extra spec: %v", err)
- }
- tools.PrintResource(t, spec)
- }
+ th.AssertEquals(t, len(allExtraSpecs), 1)
+ th.AssertEquals(t, allExtraSpecs["hw:cpu_thread_policy"], "CPU-THREAD-POLICY-BETTER")
+
+ spec, err := flavors.GetExtraSpec(client, flavor.ID, "hw:cpu_thread_policy").Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, spec)
+
+ th.AssertEquals(t, spec["hw:cpu_thread_policy"], "CPU-THREAD-POLICY-BETTER")
}
diff --git a/acceptance/openstack/compute/v2/floatingip_test.go b/acceptance/openstack/compute/v2/floatingip_test.go
index 26b7bfe16a..8130873676 100644
--- a/acceptance/openstack/compute/v2/floatingip_test.go
+++ b/acceptance/openstack/compute/v2/floatingip_test.go
@@ -9,114 +9,90 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
-func TestFloatingIPsList(t *testing.T) {
+func TestFloatingIPsCreateDelete(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ floatingIP, err := CreateFloatingIP(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteFloatingIP(t, client, floatingIP)
+
+ tools.PrintResource(t, floatingIP)
allPages, err := floatingips.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve floating IPs: %v", err)
- }
+ th.AssertNoErr(t, err)
allFloatingIPs, err := floatingips.ExtractFloatingIPs(allPages)
- if err != nil {
- t.Fatalf("Unable to extract floating IPs: %v", err)
- }
+ th.AssertNoErr(t, err)
- for _, floatingIP := range allFloatingIPs {
+ var found bool
+ for _, fip := range allFloatingIPs {
tools.PrintResource(t, floatingIP)
- }
-}
-func TestFloatingIPsCreate(t *testing.T) {
- client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
+ if fip.ID == floatingIP.ID {
+ found = true
+ }
}
- floatingIP, err := CreateFloatingIP(t, client)
- if err != nil {
- t.Fatalf("Unable to create floating IP: %v", err)
- }
- defer DeleteFloatingIP(t, client, floatingIP)
+ th.AssertEquals(t, found, true)
- tools.PrintResource(t, floatingIP)
+ fip, err := floatingips.Get(client, floatingIP.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, floatingIP.ID, fip.ID)
}
func TestFloatingIPsAssociate(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
floatingIP, err := CreateFloatingIP(t, client)
- if err != nil {
- t.Fatalf("Unable to create floating IP: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteFloatingIP(t, client, floatingIP)
tools.PrintResource(t, floatingIP)
err = AssociateFloatingIP(t, client, floatingIP, server)
- if err != nil {
- t.Fatalf("Unable to associate floating IP %s with server %s: %v", floatingIP.IP, server.ID, err)
- }
+ th.AssertNoErr(t, err)
defer DisassociateFloatingIP(t, client, floatingIP, server)
newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get floating IP %s: %v", floatingIP.ID, err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP)
tools.PrintResource(t, newFloatingIP)
+
+ th.AssertEquals(t, newFloatingIP.InstanceID, server.ID)
}
func TestFloatingIPsFixedIPAssociate(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
newServer, err := servers.Get(client, server.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get server %s: %v", server.ID, err)
- }
+ th.AssertNoErr(t, err)
floatingIP, err := CreateFloatingIP(t, client)
- if err != nil {
- t.Fatalf("Unable to create floating IP: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteFloatingIP(t, client, floatingIP)
tools.PrintResource(t, floatingIP)
@@ -132,17 +108,16 @@ func TestFloatingIPsFixedIPAssociate(t *testing.T) {
}
err = AssociateFloatingIPWithFixedIP(t, client, floatingIP, newServer, fixedIP)
- if err != nil {
- t.Fatalf("Unable to associate floating IP %s with server %s: %v", floatingIP.IP, newServer.ID, err)
- }
+ th.AssertNoErr(t, err)
defer DisassociateFloatingIP(t, client, floatingIP, newServer)
newFloatingIP, err := floatingips.Get(client, floatingIP.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get floating IP %s: %v", floatingIP.ID, err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Floating IP %s is associated with Fixed IP %s", floatingIP.IP, newFloatingIP.FixedIP)
tools.PrintResource(t, newFloatingIP)
+
+ th.AssertEquals(t, newFloatingIP.InstanceID, server.ID)
+ th.AssertEquals(t, newFloatingIP.FixedIP, fixedIP)
}
diff --git a/acceptance/openstack/compute/v2/hypervisors_test.go b/acceptance/openstack/compute/v2/hypervisors_test.go
index 627dc76345..29d49a277e 100644
--- a/acceptance/openstack/compute/v2/hypervisors_test.go
+++ b/acceptance/openstack/compute/v2/hypervisors_test.go
@@ -3,30 +3,93 @@
package v2
import (
+ "fmt"
"testing"
+ "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestHypervisorsList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := hypervisors.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list hypervisors: %v", err)
- }
+ th.AssertNoErr(t, err)
allHypervisors, err := hypervisors.ExtractHypervisors(allPages)
- if err != nil {
- t.Fatalf("Unable to extract hypervisors")
- }
+ th.AssertNoErr(t, err)
for _, h := range allHypervisors {
tools.PrintResource(t, h)
}
}
+
+func TestHypervisorsGet(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ hypervisorID, err := getHypervisorID(t, client)
+ th.AssertNoErr(t, err)
+
+ hypervisor, err := hypervisors.Get(client, hypervisorID).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, hypervisor)
+
+ th.AssertEquals(t, hypervisorID, hypervisor.ID)
+}
+
+func TestHypervisorsGetStatistics(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ hypervisorsStats, err := hypervisors.GetStatistics(client).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, hypervisorsStats)
+
+ if hypervisorsStats.Count == 0 {
+ t.Fatalf("Unable to get hypervisor stats")
+ }
+}
+
+func TestHypervisorsGetUptime(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ hypervisorID, err := getHypervisorID(t, client)
+ th.AssertNoErr(t, err)
+
+ hypervisor, err := hypervisors.GetUptime(client, hypervisorID).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, hypervisor)
+
+ th.AssertEquals(t, hypervisorID, hypervisor.ID)
+}
+
+func getHypervisorID(t *testing.T, client *gophercloud.ServiceClient) (int, error) {
+ allPages, err := hypervisors.List(client).AllPages()
+ th.AssertNoErr(t, err)
+
+ allHypervisors, err := hypervisors.ExtractHypervisors(allPages)
+ th.AssertNoErr(t, err)
+
+ if len(allHypervisors) > 0 {
+ return allHypervisors[0].ID, nil
+ }
+
+ return 0, fmt.Errorf("Unable to get hypervisor ID")
+}
diff --git a/acceptance/openstack/compute/v2/images_test.go b/acceptance/openstack/compute/v2/images_test.go
index a34ce3ea62..d7fe19b35b 100644
--- a/acceptance/openstack/compute/v2/images_test.go
+++ b/acceptance/openstack/compute/v2/images_test.go
@@ -8,44 +8,45 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestImagesList(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute: client: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
allPages, err := images.ListDetail(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve images: %v", err)
- }
+ th.AssertNoErr(t, err)
allImages, err := images.ExtractImages(allPages)
- if err != nil {
- t.Fatalf("Unable to extract image results: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, image := range allImages {
tools.PrintResource(t, image)
+
+ if image.ID == choices.ImageID {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestImagesGet(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute: client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
image, err := images.Get(client, choices.ImageID).Extract()
- if err != nil {
- t.Fatalf("Unable to get image information: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, image)
+
+ th.AssertEquals(t, choices.ImageID, image.ID)
}
diff --git a/acceptance/openstack/compute/v2/keypairs_test.go b/acceptance/openstack/compute/v2/keypairs_test.go
index c4b91ec854..a3a17d19e6 100644
--- a/acceptance/openstack/compute/v2/keypairs_test.go
+++ b/acceptance/openstack/compute/v2/keypairs_test.go
@@ -9,99 +9,72 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
const keyName = "gophercloud_test_key_pair"
-func TestKeypairsList(t *testing.T) {
+func TestKeypairsCreateDelete(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ keyPair, err := CreateKeyPair(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteKeyPair(t, client, keyPair)
+
+ tools.PrintResource(t, keyPair)
allPages, err := keypairs.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve keypairs: %s", err)
- }
+ th.AssertNoErr(t, err)
allKeys, err := keypairs.ExtractKeyPairs(allPages)
- if err != nil {
- t.Fatalf("Unable to extract keypairs results: %s", err)
- }
-
- for _, keypair := range allKeys {
- tools.PrintResource(t, keypair)
- }
-}
+ th.AssertNoErr(t, err)
-func TestKeypairsCreate(t *testing.T) {
- client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ var found bool
+ for _, kp := range allKeys {
+ tools.PrintResource(t, kp)
- keyPair, err := CreateKeyPair(t, client)
- if err != nil {
- t.Fatalf("Unable to create key pair: %v", err)
+ if kp.Name == keyPair.Name {
+ found = true
+ }
}
- defer DeleteKeyPair(t, client, keyPair)
- tools.PrintResource(t, keyPair)
+ th.AssertEquals(t, found, true)
}
func TestKeypairsImportPublicKey(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
publicKey, err := createKey()
- if err != nil {
- t.Fatalf("Unable to create public key: %s", err)
- }
+ th.AssertNoErr(t, err)
keyPair, err := ImportPublicKey(t, client, publicKey)
- if err != nil {
- t.Fatalf("Unable to create keypair: %s", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteKeyPair(t, client, keyPair)
tools.PrintResource(t, keyPair)
}
func TestKeypairsServerCreateWithKey(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
publicKey, err := createKey()
- if err != nil {
- t.Fatalf("Unable to create public key: %s", err)
- }
+ th.AssertNoErr(t, err)
keyPair, err := ImportPublicKey(t, client, publicKey)
- if err != nil {
- t.Fatalf("Unable to create keypair: %s", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteKeyPair(t, client, keyPair)
server, err := CreateServerWithPublicKey(t, client, keyPair.Name)
- if err != nil {
- t.Fatalf("Unable to create server: %s", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
server, err = servers.Get(client, server.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to retrieve server: %s", err)
- }
+ th.AssertNoErr(t, err)
- if server.KeyName != keyPair.Name {
- t.Fatalf("key name of server %s is %s, not %s", server.ID, server.KeyName, keyPair.Name)
- }
+ th.AssertEquals(t, server.KeyName, keyPair.Name)
}
diff --git a/acceptance/openstack/compute/v2/limits_test.go b/acceptance/openstack/compute/v2/limits_test.go
index 2bf5ce6b85..8133999c6c 100644
--- a/acceptance/openstack/compute/v2/limits_test.go
+++ b/acceptance/openstack/compute/v2/limits_test.go
@@ -7,29 +7,28 @@ import (
"testing"
"github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestLimits(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
limits, err := limits.Get(client, nil).Extract()
- if err != nil {
- t.Fatalf("Unable to get limits: %v", err)
- }
+ th.AssertNoErr(t, err)
- t.Logf("Limits for scoped user:")
- t.Logf("%#v", limits)
+ tools.PrintResource(t, limits)
+
+ th.AssertEquals(t, limits.Absolute.MaxPersonalitySize, 10240)
}
func TestLimitsForTenant(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
// I think this is the easiest way to get the tenant ID while being
// agnostic to Identity v2 and v3.
@@ -43,10 +42,9 @@ func TestLimitsForTenant(t *testing.T) {
}
limits, err := limits.Get(client, getOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to get absolute limits: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, limits)
- t.Logf("Limits for tenant %s:", tenantID)
- t.Logf("%#v", limits)
+ th.AssertEquals(t, limits.Absolute.MaxPersonalitySize, 10240)
}
diff --git a/acceptance/openstack/compute/v2/migrate_test.go b/acceptance/openstack/compute/v2/migrate_test.go
index 4d03350100..3f61188ae4 100644
--- a/acceptance/openstack/compute/v2/migrate_test.go
+++ b/acceptance/openstack/compute/v2/migrate_test.go
@@ -7,24 +7,48 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/migrate"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestMigrate(t *testing.T) {
+ clients.RequireLong(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
t.Logf("Attempting to migrate server %s", server.ID)
err = migrate.Migrate(client, server.ID).ExtractErr()
- if err != nil {
- t.Fatalf("Error during migration: %v", err)
+ th.AssertNoErr(t, err)
+}
+
+func TestLiveMigrate(t *testing.T) {
+ clients.RequireLong(t)
+ clients.RequireAdmin(t)
+ clients.RequireLiveMigration(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ server, err := CreateServer(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteServer(t, client, server)
+
+ t.Logf("Attempting to migrate server %s", server.ID)
+
+ blockMigration := false
+ diskOverCommit := false
+
+ liveMigrateOpts := migrate.LiveMigrateOpts{
+ BlockMigration: &blockMigration,
+ DiskOverCommit: &diskOverCommit,
}
+
+ err = migrate.LiveMigrate(client, server.ID, liveMigrateOpts).ExtractErr()
+ th.AssertNoErr(t, err)
}
diff --git a/acceptance/openstack/compute/v2/network_test.go b/acceptance/openstack/compute/v2/network_test.go
index 745151829d..25fbe4144c 100644
--- a/acceptance/openstack/compute/v2/network_test.go
+++ b/acceptance/openstack/compute/v2/network_test.go
@@ -8,49 +8,48 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/networks"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestNetworksList(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
allPages, err := networks.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list networks: %v", err)
- }
+ th.AssertNoErr(t, err)
allNetworks, err := networks.ExtractNetworks(allPages)
- if err != nil {
- t.Fatalf("Unable to list networks: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, network := range allNetworks {
tools.PrintResource(t, network)
+
+ if network.Label == choices.NetworkName {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestNetworksGet(t *testing.T) {
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
networkID, err := GetNetworkIDFromNetworks(t, client, choices.NetworkName)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
network, err := networks.Get(client, networkID).Extract()
- if err != nil {
- t.Fatalf("Unable to get network %s: %v", networkID, err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, network)
+
+ th.AssertEquals(t, network.Label, choices.NetworkName)
}
diff --git a/acceptance/openstack/compute/v2/quotaset_test.go b/acceptance/openstack/compute/v2/quotaset_test.go
index 28f2be1a88..62b2042b8c 100644
--- a/acceptance/openstack/compute/v2/quotaset_test.go
+++ b/acceptance/openstack/compute/v2/quotaset_test.go
@@ -17,38 +17,28 @@ import (
func TestQuotasetGet(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
identityClient, err := clients.NewIdentityV2Client()
- if err != nil {
- t.Fatalf("Unable to get a new identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
tenantID, err := getTenantID(t, identityClient)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
quotaSet, err := quotasets.Get(client, tenantID).Extract()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, quotaSet)
+
+ th.AssertEquals(t, quotaSet.FixedIPs, -1)
}
func getTenantID(t *testing.T, client *gophercloud.ServiceClient) (string, error) {
allPages, err := tenants.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to get list of tenants: %v", err)
- }
+ th.AssertNoErr(t, err)
allTenants, err := tenants.ExtractTenants(allPages)
- if err != nil {
- t.Fatalf("Unable to extract tenants: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, tenant := range allTenants {
return tenant.ID, nil
@@ -59,14 +49,10 @@ func getTenantID(t *testing.T, client *gophercloud.ServiceClient) (string, error
func getTenantIDByName(t *testing.T, client *gophercloud.ServiceClient, name string) (string, error) {
allPages, err := tenants.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to get list of tenants: %v", err)
- }
+ th.AssertNoErr(t, err)
allTenants, err := tenants.ExtractTenants(allPages)
- if err != nil {
- t.Fatalf("Unable to extract tenants: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, tenant := range allTenants {
if tenant.Name == name {
@@ -77,8 +63,8 @@ func getTenantIDByName(t *testing.T, client *gophercloud.ServiceClient, name str
return "", fmt.Errorf("Unable to get tenant ID")
}
-//What will be sent as desired Quotas to the Server
-var UpdatQuotaOpts = quotasets.UpdateOpts{
+// What will be sent as desired Quotas to the Server
+var UpdateQuotaOpts = quotasets.UpdateOpts{
FixedIPs: gophercloud.IntToPointer(10),
FloatingIPs: gophercloud.IntToPointer(10),
InjectedFileContentBytes: gophercloud.IntToPointer(10240),
@@ -95,7 +81,7 @@ var UpdatQuotaOpts = quotasets.UpdateOpts{
ServerGroupMembers: gophercloud.IntToPointer(3),
}
-//What the Server hopefully returns as the new Quotas
+// What the Server hopefully returns as the new Quotas
var UpdatedQuotas = quotasets.QuotaSet{
FixedIPs: 10,
FloatingIPs: 10,
@@ -114,71 +100,44 @@ var UpdatedQuotas = quotasets.QuotaSet{
}
func TestQuotasetUpdateDelete(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
idclient, err := clients.NewIdentityV2Client()
- if err != nil {
- t.Fatalf("Could not create IdentityClient to look up tenant id!")
- }
+ th.AssertNoErr(t, err)
tenantid, err := getTenantIDByName(t, idclient, os.Getenv("OS_TENANT_NAME"))
- if err != nil {
- t.Fatalf("Id for Tenant named '%' not found. Please set OS_TENANT_NAME appropriately", os.Getenv("OS_TENANT_NAME"))
- }
+ th.AssertNoErr(t, err)
- //save original quotas
+ // save original quotas
orig, err := quotasets.Get(client, tenantid).Extract()
th.AssertNoErr(t, err)
- //Test Update
- res, err := quotasets.Update(client, tenantid, UpdatQuotaOpts).Extract()
+ // Test Update
+ res, err := quotasets.Update(client, tenantid, UpdateQuotaOpts).Extract()
th.AssertNoErr(t, err)
th.AssertEquals(t, UpdatedQuotas, *res)
- //Test Delete
+ // Test Delete
_, err = quotasets.Delete(client, tenantid).Extract()
th.AssertNoErr(t, err)
- //We dont know the default quotas, so just check if the quotas are not the same as before
+
+ // We dont know the default quotas, so just check if the quotas are not the same as before
newres, err := quotasets.Get(client, tenantid).Extract()
- if newres == res {
- t.Fatalf("Quotas after delete equal quotas before delete!")
+ th.AssertNoErr(t, err)
+ if newres.RAM == res.RAM {
+ t.Fatalf("Failed to update quotas")
}
restore := quotasets.UpdateOpts{}
FillUpdateOptsFromQuotaSet(*orig, &restore)
- //restore original quotas
+ // restore original quotas
res, err = quotasets.Update(client, tenantid, restore).Extract()
th.AssertNoErr(t, err)
orig.ID = ""
- th.AssertEquals(t, *orig, *res)
-
-}
-
-// Makes sure that the FillUpdateOptsFromQuotaSet() helper function works properly
-func TestFillFromQuotaSetHelperFunction(t *testing.T) {
- op := "asets.UpdateOpts{}
- expected := `
- {
- "fixed_ips": 10,
- "floating_ips": 10,
- "injected_file_content_bytes": 10240,
- "injected_file_path_bytes": 255,
- "injected_files": 5,
- "key_pairs": 10,
- "metadata_items": 128,
- "ram": 20000,
- "security_group_rules": 20,
- "security_groups": 10,
- "cores": 10,
- "instances": 4,
- "server_groups": 2,
- "server_group_members": 3
- }`
- FillUpdateOptsFromQuotaSet(UpdatedQuotas, op)
- th.AssertJSONEquals(t, expected, op)
+ th.AssertDeepEquals(t, orig, res)
}
diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go
index c0d023037d..6f3028432a 100644
--- a/acceptance/openstack/compute/v2/secgroup_test.go
+++ b/acceptance/openstack/compute/v2/secgroup_test.go
@@ -8,130 +8,133 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestSecGroupsList(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := secgroups.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve security groups: %v", err)
- }
+ th.AssertNoErr(t, err)
allSecGroups, err := secgroups.ExtractSecurityGroups(allPages)
- if err != nil {
- t.Fatalf("Unable to extract security groups: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, secgroup := range allSecGroups {
tools.PrintResource(t, secgroup)
- }
-}
-func TestSecGroupsCreate(t *testing.T) {
- client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
+ if secgroup.Name == "default" {
+ found = true
+ }
}
- securityGroup, err := CreateSecurityGroup(t, client)
- if err != nil {
- t.Fatalf("Unable to create security group: %v", err)
- }
- defer DeleteSecurityGroup(t, client, securityGroup)
+ th.AssertEquals(t, found, true)
}
-func TestSecGroupsUpdate(t *testing.T) {
+func TestSecGroupsCRUD(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
securityGroup, err := CreateSecurityGroup(t, client)
- if err != nil {
- t.Fatalf("Unable to create security group: %v", err)
- }
- defer DeleteSecurityGroup(t, client, securityGroup)
+ th.AssertNoErr(t, err)
+ defer DeleteSecurityGroup(t, client, securityGroup.ID)
+
+ tools.PrintResource(t, securityGroup)
+ newName := tools.RandomString("secgroup_", 4)
updateOpts := secgroups.UpdateOpts{
- Name: tools.RandomString("secgroup_", 4),
+ Name: newName,
Description: tools.RandomString("dec_", 10),
}
updatedSecurityGroup, err := secgroups.Update(client, securityGroup.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update security group: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, updatedSecurityGroup)
t.Logf("Updated %s's name to %s", updatedSecurityGroup.ID, updatedSecurityGroup.Name)
+
+ th.AssertEquals(t, updatedSecurityGroup.Name, newName)
}
func TestSecGroupsRuleCreate(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
securityGroup, err := CreateSecurityGroup(t, client)
- if err != nil {
- t.Fatalf("Unable to create security group: %v", err)
- }
- defer DeleteSecurityGroup(t, client, securityGroup)
+ th.AssertNoErr(t, err)
+ defer DeleteSecurityGroup(t, client, securityGroup.ID)
+
+ tools.PrintResource(t, securityGroup)
rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID)
- if err != nil {
- t.Fatalf("Unable to create rule: %v", err)
- }
- defer DeleteSecurityGroupRule(t, client, rule)
+ th.AssertNoErr(t, err)
+ defer DeleteSecurityGroupRule(t, client, rule.ID)
+
+ tools.PrintResource(t, rule)
newSecurityGroup, err := secgroups.Get(client, securityGroup.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to obtain security group: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newSecurityGroup)
+ th.AssertEquals(t, len(newSecurityGroup.Rules), 1)
}
func TestSecGroupsAddGroupToServer(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
securityGroup, err := CreateSecurityGroup(t, client)
- if err != nil {
- t.Fatalf("Unable to create security group: %v", err)
- }
- defer DeleteSecurityGroup(t, client, securityGroup)
+ th.AssertNoErr(t, err)
+ defer DeleteSecurityGroup(t, client, securityGroup.ID)
rule, err := CreateSecurityGroupRule(t, client, securityGroup.ID)
- if err != nil {
- t.Fatalf("Unable to create rule: %v", err)
- }
- defer DeleteSecurityGroupRule(t, client, rule)
+ th.AssertNoErr(t, err)
+ defer DeleteSecurityGroupRule(t, client, rule.ID)
t.Logf("Adding group %s to server %s", securityGroup.ID, server.ID)
err = secgroups.AddServer(client, server.ID, securityGroup.Name).ExtractErr()
- if err != nil && err.Error() != "EOF" {
- t.Fatalf("Unable to add group %s to server %s: %s", securityGroup.ID, server.ID, err)
+ th.AssertNoErr(t, err)
+
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, server)
+
+ var found bool
+ for _, sg := range server.SecurityGroups {
+ if sg["name"] == securityGroup.Name {
+ found = true
+ }
}
+ th.AssertEquals(t, found, true)
+
t.Logf("Removing group %s from server %s", securityGroup.ID, server.ID)
err = secgroups.RemoveServer(client, server.ID, securityGroup.Name).ExtractErr()
- if err != nil && err.Error() != "EOF" {
- t.Fatalf("Unable to remove group %s from server %s: %s", securityGroup.ID, server.ID, err)
+ th.AssertNoErr(t, err)
+
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ found = false
+
+ tools.PrintResource(t, server)
+
+ for _, sg := range server.SecurityGroups {
+ if sg["name"] == securityGroup.Name {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, false)
}
diff --git a/acceptance/openstack/compute/v2/servergroup_test.go b/acceptance/openstack/compute/v2/servergroup_test.go
index 547b82fd5a..8b7af0f3c1 100644
--- a/acceptance/openstack/compute/v2/servergroup_test.go
+++ b/acceptance/openstack/compute/v2/servergroup_test.go
@@ -9,85 +9,63 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
-func TestServergroupsList(t *testing.T) {
+func TestServergroupsCreateDelete(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ serverGroup, err := CreateServerGroup(t, client, "anti-affinity")
+ th.AssertNoErr(t, err)
+ defer DeleteServerGroup(t, client, serverGroup)
+
+ serverGroup, err = servergroups.Get(client, serverGroup.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, serverGroup)
allPages, err := servergroups.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list server groups: %v", err)
- }
+ th.AssertNoErr(t, err)
allServerGroups, err := servergroups.ExtractServerGroups(allPages)
- if err != nil {
- t.Fatalf("Unable to extract server groups: %v", err)
- }
+ th.AssertNoErr(t, err)
- for _, serverGroup := range allServerGroups {
+ var found bool
+ for _, sg := range allServerGroups {
tools.PrintResource(t, serverGroup)
- }
-}
-
-func TestServergroupsCreate(t *testing.T) {
- client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
- serverGroup, err := CreateServerGroup(t, client, "anti-affinity")
- if err != nil {
- t.Fatalf("Unable to create server group: %v", err)
- }
- defer DeleteServerGroup(t, client, serverGroup)
-
- serverGroup, err = servergroups.Get(client, serverGroup.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get server group: %v", err)
+ if sg.ID == serverGroup.ID {
+ found = true
+ }
}
- tools.PrintResource(t, serverGroup)
+ th.AssertEquals(t, found, true)
}
func TestServergroupsAffinityPolicy(t *testing.T) {
+ clients.RequireLong(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
serverGroup, err := CreateServerGroup(t, client, "affinity")
- if err != nil {
- t.Fatalf("Unable to create server group: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServerGroup(t, client, serverGroup)
firstServer, err := CreateServerInServerGroup(t, client, serverGroup)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
- if err = WaitForComputeStatus(client, firstServer, "ACTIVE"); err != nil {
- t.Fatalf("Unable to wait for server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, firstServer)
firstServer, err = servers.Get(client, firstServer.ID).Extract()
+ th.AssertNoErr(t, err)
secondServer, err := CreateServerInServerGroup(t, client, serverGroup)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
-
- if err = WaitForComputeStatus(client, secondServer, "ACTIVE"); err != nil {
- t.Fatalf("Unable to wait for server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, secondServer)
secondServer, err = servers.Get(client, secondServer.ID).Extract()
+ th.AssertNoErr(t, err)
- if firstServer.HostID != secondServer.HostID {
- t.Fatalf("%s and %s were not scheduled on the same host.", firstServer.ID, secondServer.ID)
- }
+ th.AssertEquals(t, firstServer.HostID, secondServer.HostID)
}
diff --git a/acceptance/openstack/compute/v2/servers_test.go b/acceptance/openstack/compute/v2/servers_test.go
index db7422f9b0..917795d326 100644
--- a/acceptance/openstack/compute/v2/servers_test.go
+++ b/acceptance/openstack/compute/v2/servers_test.go
@@ -19,88 +19,61 @@ import (
th "github.com/gophercloud/gophercloud/testhelper"
)
-func TestServersList(t *testing.T) {
+func TestServersCreateDestroy(t *testing.T) {
+ clients.RequireLong(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
+
+ server, err := CreateServer(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteServer(t, client, server)
allPages, err := servers.List(client, servers.ListOpts{}).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve servers: %v", err)
- }
+ th.AssertNoErr(t, err)
allServers, err := servers.ExtractServers(allPages)
- if err != nil {
- t.Fatalf("Unable to extract servers: %v", err)
- }
+ th.AssertNoErr(t, err)
- for _, server := range allServers {
+ var found bool
+ for _, s := range allServers {
tools.PrintResource(t, server)
- }
-}
-
-func TestServersCreateDestroy(t *testing.T) {
- client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
- choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
-
- server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
+ if s.ID == server.ID {
+ found = true
+ }
}
- defer DeleteServer(t, client, server)
-
- newServer, err := servers.Get(client, server.ID).Extract()
- if err != nil {
- t.Errorf("Unable to retrieve server: %v", err)
- }
- tools.PrintResource(t, newServer)
+ th.AssertEquals(t, found, true)
allAddressPages, err := servers.ListAddresses(client, server.ID).AllPages()
- if err != nil {
- t.Errorf("Unable to list server addresses: %v", err)
- }
+ th.AssertNoErr(t, err)
allAddresses, err := servers.ExtractAddresses(allAddressPages)
- if err != nil {
- t.Errorf("Unable to extract server addresses: %v", err)
- }
+ th.AssertNoErr(t, err)
for network, address := range allAddresses {
t.Logf("Addresses on %s: %+v", network, address)
}
allInterfacePages, err := attachinterfaces.List(client, server.ID).AllPages()
- if err != nil {
- t.Errorf("Unable to list server Interfaces: %v", err)
- }
+ th.AssertNoErr(t, err)
allInterfaces, err := attachinterfaces.ExtractInterfaces(allInterfacePages)
- if err != nil {
- t.Errorf("Unable to extract server Interfaces: %v", err)
- }
+ th.AssertNoErr(t, err)
- for _, Interface := range allInterfaces {
- t.Logf("Interfaces: %+v", Interface)
+ for _, iface := range allInterfaces {
+ t.Logf("Interfaces: %+v", iface)
}
allNetworkAddressPages, err := servers.ListAddressesByNetwork(client, server.ID, choices.NetworkName).AllPages()
- if err != nil {
- t.Errorf("Unable to list server addresses: %v", err)
- }
+ th.AssertNoErr(t, err)
allNetworkAddresses, err := servers.ExtractNetworkAddresses(allNetworkAddressPages)
- if err != nil {
- t.Errorf("Unable to extract server addresses: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Addresses on %s:", choices.NetworkName)
for _, address := range allNetworkAddresses {
@@ -108,7 +81,9 @@ func TestServersCreateDestroy(t *testing.T) {
}
}
-func TestServersCreateDestroyWithExtensions(t *testing.T) {
+func TestServersWithExtensionsCreateDestroy(t *testing.T) {
+ clients.RequireLong(t)
+
var extendedServer struct {
servers.Server
availabilityzones.ServerAvailabilityZoneExt
@@ -116,33 +91,25 @@ func TestServersCreateDestroyWithExtensions(t *testing.T) {
}
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
err = servers.Get(client, server.ID).ExtractInto(&extendedServer)
- if err != nil {
- t.Errorf("Unable to retrieve server: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, extendedServer)
- t.Logf("Availability Zone: %s\n", extendedServer.AvailabilityZone)
- t.Logf("Power State: %s\n", extendedServer.PowerState)
- t.Logf("Task State: %s\n", extendedServer.TaskState)
- t.Logf("VM State: %s\n", extendedServer.VmState)
+ th.AssertEquals(t, extendedServer.AvailabilityZone, "nova")
+ th.AssertEquals(t, int(extendedServer.PowerState), extendedstatus.RUNNING)
+ th.AssertEquals(t, extendedServer.TaskState, "")
+ th.AssertEquals(t, extendedServer.VmState, "active")
}
func TestServersWithoutImageRef(t *testing.T) {
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServerWithoutImageRef(t, client)
if err != nil {
@@ -155,15 +122,13 @@ func TestServersWithoutImageRef(t *testing.T) {
}
func TestServersUpdate(t *testing.T) {
+ clients.RequireLong(t)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
alternateName := tools.RandomString("ACPTTEST", 16)
@@ -178,13 +143,9 @@ func TestServersUpdate(t *testing.T) {
}
updated, err := servers.Update(client, server.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to rename server: %v", err)
- }
+ th.AssertNoErr(t, err)
- if updated.ID != server.ID {
- t.Errorf("Updated server ID [%s] didn't match original server ID [%s]!", updated.ID, server.ID)
- }
+ th.AssertEquals(t, updated.ID, server.ID)
err = tools.WaitFor(func() (bool, error) {
latest, err := servers.Get(client, updated.ID).Extract()
@@ -197,81 +158,99 @@ func TestServersUpdate(t *testing.T) {
}
func TestServersMetadata(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
+ tools.PrintResource(t, server)
+
metadata, err := servers.UpdateMetadata(client, server.ID, servers.MetadataOpts{
"foo": "bar",
"this": "that",
}).Extract()
- if err != nil {
- t.Fatalf("Unable to update metadata: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("UpdateMetadata result: %+v\n", metadata)
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, server)
+
+ expectedMetadata := map[string]string{
+ "abc": "def",
+ "foo": "bar",
+ "this": "that",
+ }
+ th.AssertDeepEquals(t, expectedMetadata, server.Metadata)
+
err = servers.DeleteMetadatum(client, server.ID, "foo").ExtractErr()
- if err != nil {
- t.Fatalf("Unable to delete metadatum: %v", err)
+ th.AssertNoErr(t, err)
+
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, server)
+
+ expectedMetadata = map[string]string{
+ "abc": "def",
+ "this": "that",
}
+ th.AssertDeepEquals(t, expectedMetadata, server.Metadata)
metadata, err = servers.CreateMetadatum(client, server.ID, servers.MetadatumOpts{
"foo": "baz",
}).Extract()
- if err != nil {
- t.Fatalf("Unable to create metadatum: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("CreateMetadatum result: %+v\n", metadata)
- metadata, err = servers.Metadatum(client, server.ID, "foo").Extract()
- if err != nil {
- t.Fatalf("Unable to get metadatum: %v", err)
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, server)
+
+ expectedMetadata = map[string]string{
+ "abc": "def",
+ "this": "that",
+ "foo": "baz",
}
+ th.AssertDeepEquals(t, expectedMetadata, server.Metadata)
+
+ metadata, err = servers.Metadatum(client, server.ID, "foo").Extract()
+ th.AssertNoErr(t, err)
t.Logf("Metadatum result: %+v\n", metadata)
th.AssertEquals(t, "baz", metadata["foo"])
metadata, err = servers.Metadata(client, server.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get metadata: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Metadata result: %+v\n", metadata)
+ th.AssertDeepEquals(t, expectedMetadata, metadata)
+
metadata, err = servers.ResetMetadata(client, server.ID, servers.MetadataOpts{}).Extract()
- if err != nil {
- t.Fatalf("Unable to reset metadata: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("ResetMetadata result: %+v\n", metadata)
th.AssertDeepEquals(t, map[string]string{}, metadata)
}
func TestServersActionChangeAdminPassword(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
+ clients.RequireGuestAgent(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
randomPassword := tools.MakeNewPassword(server.AdminPass)
res := servers.ChangeAdminPassword(client, server.ID, randomPassword)
- if res.Err != nil {
- t.Fatal(res.Err)
- }
+ th.AssertNoErr(t, res.Err)
if err = WaitForComputeStatus(client, server, "PASSWORD"); err != nil {
t.Fatal(err)
@@ -283,17 +262,13 @@ func TestServersActionChangeAdminPassword(t *testing.T) {
}
func TestServersActionReboot(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
rebootOpts := &servers.RebootOpts{
@@ -302,9 +277,7 @@ func TestServersActionReboot(t *testing.T) {
t.Logf("Attempting reboot of server %s", server.ID)
res := servers.Reboot(client, server.ID, rebootOpts)
- if res.Err != nil {
- t.Fatalf("Unable to reboot server: %v", res.Err)
- }
+ th.AssertNoErr(t, res.Err)
if err = WaitForComputeStatus(client, server, "REBOOT"); err != nil {
t.Fatal(err)
@@ -316,22 +289,16 @@ func TestServersActionReboot(t *testing.T) {
}
func TestServersActionRebuild(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
t.Logf("Attempting to rebuild server %s", server.ID)
@@ -343,13 +310,9 @@ func TestServersActionRebuild(t *testing.T) {
}
rebuilt, err := servers.Rebuild(client, server.ID, rebuildOpts).Extract()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
- if rebuilt.ID != server.ID {
- t.Errorf("Expected rebuilt server ID of [%s]; got [%s]", server.ID, rebuilt.ID)
- }
+ th.AssertEquals(t, rebuilt.ID, server.ID)
if err = WaitForComputeStatus(client, rebuilt, "REBUILD"); err != nil {
t.Fatal(err)
@@ -361,17 +324,16 @@ func TestServersActionRebuild(t *testing.T) {
}
func TestServersActionResizeConfirm(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
t.Logf("Attempting to resize server %s", server.ID)
@@ -385,20 +347,24 @@ func TestServersActionResizeConfirm(t *testing.T) {
if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
t.Fatal(err)
}
+
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, server.Flavor["id"], choices.FlavorIDResize)
}
func TestServersActionResizeRevert(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
t.Logf("Attempting to resize server %s", server.ID)
@@ -412,112 +378,81 @@ func TestServersActionResizeRevert(t *testing.T) {
if err = WaitForComputeStatus(client, server, "ACTIVE"); err != nil {
t.Fatal(err)
}
+
+ server, err = servers.Get(client, server.ID).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, server.Flavor["id"], choices.FlavorID)
}
func TestServersActionPause(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
t.Logf("Attempting to pause server %s", server.ID)
err = pauseunpause.Pause(client, server.ID).ExtractErr()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = WaitForComputeStatus(client, server, "PAUSED")
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = pauseunpause.Unpause(client, server.ID).ExtractErr()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = WaitForComputeStatus(client, server, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
}
func TestServersActionSuspend(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
t.Logf("Attempting to suspend server %s", server.ID)
err = suspendresume.Suspend(client, server.ID).ExtractErr()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = WaitForComputeStatus(client, server, "SUSPENDED")
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = suspendresume.Resume(client, server.ID).ExtractErr()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = WaitForComputeStatus(client, server, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
}
func TestServersActionLock(t *testing.T) {
- t.Parallel()
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
t.Logf("Attempting to Lock server %s", server.ID)
err = lockunlock.Lock(client, server.ID).ExtractErr()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = servers.Delete(client, server.ID).ExtractErr()
- if err == nil {
- t.Fatalf("Should not have been able to delete the server")
- }
+ th.AssertNoErr(t, err)
err = lockunlock.Unlock(client, server.ID).ExtractErr()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
err = WaitForComputeStatus(client, server, "ACTIVE")
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
}
diff --git a/acceptance/openstack/compute/v2/services_test.go b/acceptance/openstack/compute/v2/services_test.go
new file mode 100644
index 0000000000..5c6484e0e6
--- /dev/null
+++ b/acceptance/openstack/compute/v2/services_test.go
@@ -0,0 +1,36 @@
+// +build acceptance compute services
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/services"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestServicesList(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ allPages, err := services.List(client).AllPages()
+ th.AssertNoErr(t, err)
+
+ allServices, err := services.ExtractServices(allPages)
+ th.AssertNoErr(t, err)
+
+ var found bool
+ for _, service := range allServices {
+ tools.PrintResource(t, service)
+
+ if service.Binary == "nova-scheduler" {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, true)
+}
diff --git a/acceptance/openstack/compute/v2/tenantnetworks_test.go b/acceptance/openstack/compute/v2/tenantnetworks_test.go
index 9b6b527022..a53c64d353 100644
--- a/acceptance/openstack/compute/v2/tenantnetworks_test.go
+++ b/acceptance/openstack/compute/v2/tenantnetworks_test.go
@@ -8,49 +8,46 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tenantnetworks"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestTenantNetworksList(t *testing.T) {
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ th.AssertNoErr(t, err)
+
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := tenantnetworks.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list networks: %v", err)
- }
+ th.AssertNoErr(t, err)
allTenantNetworks, err := tenantnetworks.ExtractNetworks(allPages)
- if err != nil {
- t.Fatalf("Unable to list networks: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, network := range allTenantNetworks {
tools.PrintResource(t, network)
+
+ if network.Name == choices.NetworkName {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestTenantNetworksGet(t *testing.T) {
choices, err := clients.AcceptanceTestChoicesFromEnv()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
networkID, err := GetNetworkIDFromTenantNetworks(t, client, choices.NetworkName)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
network, err := tenantnetworks.Get(client, networkID).Extract()
- if err != nil {
- t.Fatalf("Unable to get network %s: %v", networkID, err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, network)
}
diff --git a/acceptance/openstack/compute/v2/usage_test.go b/acceptance/openstack/compute/v2/usage_test.go
new file mode 100644
index 0000000000..0511f8937c
--- /dev/null
+++ b/acceptance/openstack/compute/v2/usage_test.go
@@ -0,0 +1,47 @@
+// +build acceptance compute usage
+
+package v2
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestUsageSingleTenant(t *testing.T) {
+ clients.RequireLong(t)
+
+ client, err := clients.NewComputeV2Client()
+ th.AssertNoErr(t, err)
+
+ server, err := CreateServer(t, client)
+ th.AssertNoErr(t, err)
+ DeleteServer(t, client, server)
+
+ endpointParts := strings.Split(client.Endpoint, "/")
+ tenantID := endpointParts[4]
+
+ end := time.Now()
+ start := end.AddDate(0, -1, 0)
+ opts := usage.SingleTenantOpts{
+ Start: &start,
+ End: &end,
+ }
+
+ page, err := usage.SingleTenant(client, tenantID, opts).AllPages()
+ th.AssertNoErr(t, err)
+
+ tenantUsage, err := usage.ExtractSingleTenant(page)
+ th.AssertNoErr(t, err)
+
+ tools.PrintResource(t, tenantUsage)
+
+ if tenantUsage.TotalHours == 0 {
+ t.Fatalf("TotalHours should not be 0")
+ }
+}
diff --git a/acceptance/openstack/compute/v2/volumeattach_test.go b/acceptance/openstack/compute/v2/volumeattach_test.go
index 78d85a9bfc..022df830ca 100644
--- a/acceptance/openstack/compute/v2/volumeattach_test.go
+++ b/acceptance/openstack/compute/v2/volumeattach_test.go
@@ -5,74 +5,34 @@ package v2
import (
"testing"
- "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/acceptance/clients"
+ bs "github.com/gophercloud/gophercloud/acceptance/openstack/blockstorage/v2"
"github.com/gophercloud/gophercloud/acceptance/tools"
- "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestVolumeAttachAttachment(t *testing.T) {
- if testing.Short() {
- t.Skip("Skipping test that requires server creation in short mode.")
- }
+ clients.RequireLong(t)
client, err := clients.NewComputeV2Client()
- if err != nil {
- t.Fatalf("Unable to create a compute client: %v", err)
- }
+ th.AssertNoErr(t, err)
- blockClient, err := clients.NewBlockStorageV1Client()
- if err != nil {
- t.Fatalf("Unable to create a blockstorage client: %v", err)
- }
+ blockClient, err := clients.NewBlockStorageV2Client()
+ th.AssertNoErr(t, err)
server, err := CreateServer(t, client)
- if err != nil {
- t.Fatalf("Unable to create server: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteServer(t, client, server)
- volume, err := createVolume(t, blockClient)
- if err != nil {
- t.Fatalf("Unable to create volume: %v", err)
- }
-
- if err = volumes.WaitForStatus(blockClient, volume.ID, "available", 60); err != nil {
- t.Fatalf("Unable to wait for volume: %v", err)
- }
- defer deleteVolume(t, blockClient, volume)
+ volume, err := bs.CreateVolume(t, blockClient)
+ th.AssertNoErr(t, err)
+ defer bs.DeleteVolume(t, blockClient, volume)
volumeAttachment, err := CreateVolumeAttachment(t, client, blockClient, server, volume)
- if err != nil {
- t.Fatalf("Unable to attach volume: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteVolumeAttachment(t, client, blockClient, server, volumeAttachment)
tools.PrintResource(t, volumeAttachment)
-}
-
-func createVolume(t *testing.T, blockClient *gophercloud.ServiceClient) (*volumes.Volume, error) {
- volumeName := tools.RandomString("ACPTTEST", 16)
- createOpts := volumes.CreateOpts{
- Size: 1,
- Name: volumeName,
- }
-
- volume, err := volumes.Create(blockClient, createOpts).Extract()
- if err != nil {
- return volume, err
- }
-
- t.Logf("Created volume: %s", volume.ID)
- return volume, nil
-}
-
-func deleteVolume(t *testing.T, blockClient *gophercloud.ServiceClient, volume *volumes.Volume) {
- err := volumes.Delete(blockClient, volume.ID).ExtractErr()
- if err != nil {
- t.Fatalf("Unable to delete volume: %v", err)
- }
-
- t.Logf("Deleted volume: %s", volume.ID)
+ th.AssertEquals(t, volumeAttachment.ServerID, server.ID)
}
diff --git a/acceptance/openstack/container/v1/capsules_test.go b/acceptance/openstack/container/v1/capsules_test.go
index 41dad46339..3e93edce29 100644
--- a/acceptance/openstack/container/v1/capsules_test.go
+++ b/acceptance/openstack/container/v1/capsules_test.go
@@ -26,3 +26,59 @@ func TestCapsuleGet(t *testing.T) {
th.AssertEquals(t, capsule.MetaName, "template")
th.AssertEquals(t, capsule.CPU, float64(2.0))
}
+
+func TestCapsuleCreate(t *testing.T) {
+ client, err := clients.NewContainerV1Client()
+ if err != nil {
+ t.Fatalf("Unable to create an container v1 client: %v", err)
+ }
+ th.AssertNoErr(t, err)
+ template := new(capsules.Template)
+ template.Bin = []byte(`{
+ "capsuleVersion": "beta",
+ "kind": "capsule",
+ "metadata": {
+ "labels": {
+ "app": "web",
+ "app1": "web1"
+ },
+ "name": "template"
+ },
+ "restartPolicy": "Always",
+ "spec": {
+ "containers": [
+ {
+ "command": [
+ "/bin/bash"
+ ],
+ "env": {
+ "ENV1": "/usr/local/bin",
+ "ENV2": "/usr/bin"
+ },
+ "image": "ubuntu",
+ "imagePullPolicy": "ifnotpresent",
+ "ports": [
+ {
+ "containerPort": 80,
+ "hostPort": 80,
+ "name": "nginx-port",
+ "protocol": "TCP"
+ }
+ ],
+ "resources": {
+ "requests": {
+ "cpu": 1,
+ "memory": 1024
+ }
+ },
+ "workDir": "/root"
+ }
+ ]
+ }
+ }`)
+ createOpts := capsules.CreateOpts{
+ TemplateOpts: template,
+ }
+ err = capsules.Create(client, createOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/acceptance/openstack/dns/v2/dns.go b/acceptance/openstack/dns/v2/dns.go
index 7a0893ff5c..18cd157fc1 100644
--- a/acceptance/openstack/dns/v2/dns.go
+++ b/acceptance/openstack/dns/v2/dns.go
@@ -7,6 +7,7 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
// CreateRecordSet will create a RecordSet with a random name. An error will
@@ -38,6 +39,8 @@ func CreateRecordSet(t *testing.T, client *gophercloud.ServiceClient, zone *zone
t.Logf("Created record set: %s", newRS.Name)
+ th.AssertEquals(t, newRS.Name, zone.Name)
+
return rs, nil
}
@@ -70,6 +73,10 @@ func CreateZone(t *testing.T, client *gophercloud.ServiceClient) (*zones.Zone, e
}
t.Logf("Created Zone: %s", zoneName)
+
+ th.AssertEquals(t, newZone.Name, zoneName)
+ th.AssertEquals(t, newZone.TTL, 7200)
+
return newZone, nil
}
@@ -102,6 +109,10 @@ func CreateSecondaryZone(t *testing.T, client *gophercloud.ServiceClient) (*zone
}
t.Logf("Created Zone: %s", zoneName)
+
+ th.AssertEquals(t, newZone.Name, zoneName)
+ th.AssertEquals(t, newZone.Masters[0], "10.0.0.1")
+
return newZone, nil
}
@@ -132,7 +143,7 @@ func DeleteZone(t *testing.T, client *gophercloud.ServiceClient, zone *zones.Zon
// WaitForRecordSetStatus will poll a record set's status until it either matches
// the specified status or the status becomes ERROR.
func WaitForRecordSetStatus(client *gophercloud.ServiceClient, rs *recordsets.RecordSet, status string) error {
- return gophercloud.WaitFor(60, func() (bool, error) {
+ return gophercloud.WaitFor(600, func() (bool, error) {
current, err := recordsets.Get(client, rs.ZoneID, rs.ID).Extract()
if err != nil {
return false, err
@@ -149,7 +160,7 @@ func WaitForRecordSetStatus(client *gophercloud.ServiceClient, rs *recordsets.Re
// WaitForZoneStatus will poll a zone's status until it either matches
// the specified status or the status becomes ERROR.
func WaitForZoneStatus(client *gophercloud.ServiceClient, zone *zones.Zone, status string) error {
- return gophercloud.WaitFor(60, func() (bool, error) {
+ return gophercloud.WaitFor(600, func() (bool, error) {
current, err := zones.Get(client, zone.ID).Extract()
if err != nil {
return false, err
diff --git a/acceptance/openstack/dns/v2/recordsets_test.go b/acceptance/openstack/dns/v2/recordsets_test.go
index 17c40bb0ce..d2d862ba55 100644
--- a/acceptance/openstack/dns/v2/recordsets_test.go
+++ b/acceptance/openstack/dns/v2/recordsets_test.go
@@ -8,85 +8,66 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestRecordSetsListByZone(t *testing.T) {
+ clients.RequireDNS(t)
+
client, err := clients.NewDNSV2Client()
- if err != nil {
- t.Fatalf("Unable to create a DNS client: %v", err)
- }
+ th.AssertNoErr(t, err)
zone, err := CreateZone(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteZone(t, client, zone)
- var allRecordSets []recordsets.RecordSet
allPages, err := recordsets.ListByZone(client, zone.ID, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve recordsets: %v", err)
- }
+ th.AssertNoErr(t, err)
- allRecordSets, err = recordsets.ExtractRecordSets(allPages)
- if err != nil {
- t.Fatalf("Unable to extract recordsets: %v", err)
- }
+ allRecordSets, err := recordsets.ExtractRecordSets(allPages)
+ th.AssertNoErr(t, err)
+ var found bool
for _, recordset := range allRecordSets {
tools.PrintResource(t, &recordset)
- }
-}
-func TestRecordSetsListByZoneLimited(t *testing.T) {
- client, err := clients.NewDNSV2Client()
- if err != nil {
- t.Fatalf("Unable to create a DNS client: %v", err)
+ if recordset.ZoneID == zone.ID {
+ found = true
+ }
}
- zone, err := CreateZone(t, client)
- if err != nil {
- t.Fatal(err)
- }
- defer DeleteZone(t, client, zone)
+ th.AssertEquals(t, found, true)
- var allRecordSets []recordsets.RecordSet
listOpts := recordsets.ListOpts{
Limit: 1,
}
- allPages, err := recordsets.ListByZone(client, zone.ID, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve recordsets: %v", err)
- }
- allRecordSets, err = recordsets.ExtractRecordSets(allPages)
- if err != nil {
- t.Fatalf("Unable to extract recordsets: %v", err)
- }
-
- for _, recordset := range allRecordSets {
- tools.PrintResource(t, &recordset)
- }
+ err = recordsets.ListByZone(client, zone.ID, listOpts).EachPage(
+ func(page pagination.Page) (bool, error) {
+ rr, err := recordsets.ExtractRecordSets(page)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, len(rr), 1)
+ return true, nil
+ },
+ )
+ th.AssertNoErr(t, err)
}
-func TestRecordSetCRUD(t *testing.T) {
+func TestRecordSetsCRUD(t *testing.T) {
+ clients.RequireDNS(t)
+
client, err := clients.NewDNSV2Client()
- if err != nil {
- t.Fatalf("Unable to create a DNS client: %v", err)
- }
+ th.AssertNoErr(t, err)
zone, err := CreateZone(t, client)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteZone(t, client, zone)
tools.PrintResource(t, &zone)
rs, err := CreateRecordSet(t, client, zone)
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
defer DeleteRecordSet(t, client, rs)
tools.PrintResource(t, &rs)
@@ -97,9 +78,9 @@ func TestRecordSetCRUD(t *testing.T) {
}
newRS, err := recordsets.Update(client, rs.ZoneID, rs.ID, updateOpts).Extract()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, &newRS)
+
+ th.AssertEquals(t, newRS.Description, "New description")
}
diff --git a/acceptance/openstack/dns/v2/zones_test.go b/acceptance/openstack/dns/v2/zones_test.go
index 8e71687898..263e9fea0a 100644
--- a/acceptance/openstack/dns/v2/zones_test.go
+++ b/acceptance/openstack/dns/v2/zones_test.go
@@ -8,43 +8,37 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
-func TestZonesList(t *testing.T) {
+func TestZonesCRUD(t *testing.T) {
+ clients.RequireDNS(t)
+
client, err := clients.NewDNSV2Client()
- if err != nil {
- t.Fatalf("Unable to create a DNS client: %v", err)
- }
+ th.AssertNoErr(t, err)
- var allZones []zones.Zone
- allPages, err := zones.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve zones: %v", err)
- }
+ zone, err := CreateZone(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteZone(t, client, zone)
- allZones, err = zones.ExtractZones(allPages)
- if err != nil {
- t.Fatalf("Unable to extract zones: %v", err)
- }
+ tools.PrintResource(t, &zone)
- for _, zone := range allZones {
- tools.PrintResource(t, &zone)
- }
-}
+ allPages, err := zones.List(client, nil).AllPages()
+ th.AssertNoErr(t, err)
-func TestZonesCRUD(t *testing.T) {
- client, err := clients.NewDNSV2Client()
- if err != nil {
- t.Fatalf("Unable to create a DNS client: %v", err)
- }
+ allZones, err := zones.ExtractZones(allPages)
+ th.AssertNoErr(t, err)
- zone, err := CreateZone(t, client)
- if err != nil {
- t.Fatal(err)
+ var found bool
+ for _, z := range allZones {
+ tools.PrintResource(t, &z)
+
+ if zone.Name == z.Name {
+ found = true
+ }
}
- defer DeleteZone(t, client, zone)
- tools.PrintResource(t, &zone)
+ th.AssertEquals(t, found, true)
updateOpts := zones.UpdateOpts{
Description: "New description",
@@ -52,9 +46,9 @@ func TestZonesCRUD(t *testing.T) {
}
newZone, err := zones.Update(client, zone.ID, updateOpts).Extract()
- if err != nil {
- t.Fatal(err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, &newZone)
+
+ th.AssertEquals(t, newZone.Description, "New description")
}
diff --git a/acceptance/openstack/identity/v2/extension_test.go b/acceptance/openstack/identity/v2/extension_test.go
index c6a2bdef41..593d75ca45 100644
--- a/acceptance/openstack/identity/v2/extension_test.go
+++ b/acceptance/openstack/identity/v2/extension_test.go
@@ -8,39 +8,42 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestExtensionsList(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2Client()
- if err != nil {
- t.Fatalf("Unable to create an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := extensions.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list extensions: %v", err)
- }
+ th.AssertNoErr(t, err)
allExtensions, err := extensions.ExtractExtensions(allPages)
- if err != nil {
- t.Fatalf("Unable to extract extensions: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, extension := range allExtensions {
tools.PrintResource(t, extension)
+ if extension.Name == "OS-KSCRUD" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestExtensionsGet(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2Client()
- if err != nil {
- t.Fatalf("Unable to create an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
extension, err := extensions.Get(client, "OS-KSCRUD").Extract()
- if err != nil {
- t.Fatalf("Unable to get extension OS-KSCRUD: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, extension)
}
diff --git a/acceptance/openstack/identity/v2/identity.go b/acceptance/openstack/identity/v2/identity.go
index 6d0d0f2090..b8e9ee2207 100644
--- a/acceptance/openstack/identity/v2/identity.go
+++ b/acceptance/openstack/identity/v2/identity.go
@@ -10,6 +10,7 @@ import (
"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles"
"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
"github.com/gophercloud/gophercloud/openstack/identity/v2/users"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
// AddUserRole will grant a role to a user in a tenant. An error will be
@@ -46,12 +47,13 @@ func CreateTenant(t *testing.T, client *gophercloud.ServiceClient, c *tenants.Cr
tenant, err := tenants.Create(client, createOpts).Extract()
if err != nil {
- t.Logf("Foo")
return tenant, err
}
t.Logf("Successfully created project %s with ID %s", name, tenant.ID)
+ th.AssertEquals(t, name, tenant.Name)
+
return tenant, nil
}
@@ -74,6 +76,8 @@ func CreateUser(t *testing.T, client *gophercloud.ServiceClient, tenant *tenants
return user, err
}
+ th.AssertEquals(t, userName, user.Name)
+
return user, nil
}
@@ -182,5 +186,7 @@ func UpdateUser(t *testing.T, client *gophercloud.ServiceClient, user *users.Use
return newUser, err
}
+ th.AssertEquals(t, userName, newUser.Name)
+
return newUser, nil
}
diff --git a/acceptance/openstack/identity/v2/role_test.go b/acceptance/openstack/identity/v2/role_test.go
index 83fbd318fa..bc9d26ec34 100644
--- a/acceptance/openstack/identity/v2/role_test.go
+++ b/acceptance/openstack/identity/v2/role_test.go
@@ -9,69 +9,68 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v2/extensions/admin/roles"
"github.com/gophercloud/gophercloud/openstack/identity/v2/users"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestRolesAddToUser(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2AdminClient()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
tenant, err := FindTenant(t, client)
- if err != nil {
- t.Fatalf("Unable to get a tenant: %v", err)
- }
+ th.AssertNoErr(t, err)
role, err := FindRole(t, client)
- if err != nil {
- t.Fatalf("Unable to get a role: %v", err)
- }
+ th.AssertNoErr(t, err)
user, err := CreateUser(t, client, tenant)
- if err != nil {
- t.Fatalf("Unable to create a user: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteUser(t, client, user)
err = AddUserRole(t, client, tenant, user, role)
- if err != nil {
- t.Fatalf("Unable to add role to user: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteUserRole(t, client, tenant, user, role)
allPages, err := users.ListRoles(client, tenant.ID, user.ID).AllPages()
- if err != nil {
- t.Fatalf("Unable to obtain roles for user: %v", err)
- }
+ th.AssertNoErr(t, err)
allRoles, err := users.ExtractRoles(allPages)
- if err != nil {
- t.Fatalf("Unable to extract roles: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Roles of user %s:", user.Name)
- for _, role := range allRoles {
+ var found bool
+ for _, r := range allRoles {
tools.PrintResource(t, role)
+ if r.Name == role.Name {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestRolesList(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2AdminClient()
- if err != nil {
- t.Fatalf("Unable to create an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := roles.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list all roles: %v", err)
- }
+ th.AssertNoErr(t, err)
allRoles, err := roles.ExtractRoles(allPages)
- if err != nil {
- t.Fatalf("Unable to extract roles: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, r := range allRoles {
tools.PrintResource(t, r)
+ if r.Name == "admin" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
diff --git a/acceptance/openstack/identity/v2/tenant_test.go b/acceptance/openstack/identity/v2/tenant_test.go
index 049ec910a1..13a1c08c9e 100644
--- a/acceptance/openstack/identity/v2/tenant_test.go
+++ b/acceptance/openstack/identity/v2/tenant_test.go
@@ -8,45 +8,47 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestTenantsList(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
allPages, err := tenants.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list tenants: %v", err)
- }
+ th.AssertNoErr(t, err)
allTenants, err := tenants.ExtractTenants(allPages)
- if err != nil {
- t.Fatalf("Unable to extract tenants: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, tenant := range allTenants {
tools.PrintResource(t, tenant)
+
+ if tenant.Name == "admin" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestTenantsCRUD(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2AdminClient()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
tenant, err := CreateTenant(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create tenant: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteTenant(t, client, tenant.ID)
tenant, err = tenants.Get(client, tenant.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get tenant: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, tenant)
@@ -55,9 +57,9 @@ func TestTenantsCRUD(t *testing.T) {
}
newTenant, err := tenants.Update(client, tenant.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update tenant: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newTenant)
+
+ th.AssertEquals(t, newTenant.Description, "some tenant")
}
diff --git a/acceptance/openstack/identity/v2/token_test.go b/acceptance/openstack/identity/v2/token_test.go
index 82a317a157..30ebcc2bf0 100644
--- a/acceptance/openstack/identity/v2/token_test.go
+++ b/acceptance/openstack/identity/v2/token_test.go
@@ -9,31 +9,27 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestTokenAuthenticate(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2UnauthenticatedClient()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
authOptions, err := openstack.AuthOptionsFromEnv()
- if err != nil {
- t.Fatalf("Unable to obtain authentication options: %v", err)
- }
+ th.AssertNoErr(t, err)
result := tokens.Create(client, authOptions)
token, err := result.ExtractToken()
- if err != nil {
- t.Fatalf("Unable to extract token: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, token)
catalog, err := result.ExtractServiceCatalog()
- if err != nil {
- t.Fatalf("Unable to extract service catalog: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, entry := range catalog.Entries {
tools.PrintResource(t, entry)
@@ -41,29 +37,24 @@ func TestTokenAuthenticate(t *testing.T) {
}
func TestTokenValidate(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
authOptions, err := openstack.AuthOptionsFromEnv()
- if err != nil {
- t.Fatalf("Unable to obtain authentication options: %v", err)
- }
+ th.AssertNoErr(t, err)
result := tokens.Create(client, authOptions)
token, err := result.ExtractToken()
- if err != nil {
- t.Fatalf("Unable to extract token: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, token)
getResult := tokens.Get(client, token.ID)
user, err := getResult.ExtractUser()
- if err != nil {
- t.Fatalf("Unable to extract user: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, user)
}
diff --git a/acceptance/openstack/identity/v2/user_test.go b/acceptance/openstack/identity/v2/user_test.go
index faa5bba2f8..caaaaf936a 100644
--- a/acceptance/openstack/identity/v2/user_test.go
+++ b/acceptance/openstack/identity/v2/user_test.go
@@ -8,52 +8,52 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v2/users"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestUsersList(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2AdminClient()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := users.List(client).AllPages()
- if err != nil {
- t.Fatalf("Unable to list users: %v", err)
- }
+ th.AssertNoErr(t, err)
allUsers, err := users.ExtractUsers(allPages)
- if err != nil {
- t.Fatalf("Unable to extract users: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, user := range allUsers {
tools.PrintResource(t, user)
+
+ if user.Name == "admin" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestUsersCreateUpdateDelete(t *testing.T) {
+ clients.RequireIdentityV2(t)
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV2AdminClient()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
tenant, err := FindTenant(t, client)
- if err != nil {
- t.Fatalf("Unable to get a tenant: %v", err)
- }
+ th.AssertNoErr(t, err)
user, err := CreateUser(t, client, tenant)
- if err != nil {
- t.Fatalf("Unable to create a user: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteUser(t, client, user)
tools.PrintResource(t, user)
newUser, err := UpdateUser(t, client, user)
- if err != nil {
- t.Fatalf("Unable to update user: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newUser)
}
diff --git a/acceptance/openstack/identity/v3/domains_test.go b/acceptance/openstack/identity/v3/domains_test.go
index b340bed4bd..7d146464c5 100644
--- a/acceptance/openstack/identity/v3/domains_test.go
+++ b/acceptance/openstack/identity/v3/domains_test.go
@@ -8,13 +8,14 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v3/domains"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestDomainsList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
var iTrue bool = true
listOpts := domains.ListOpts{
@@ -22,50 +23,42 @@ func TestDomainsList(t *testing.T) {
}
allPages, err := domains.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to list domains: %v", err)
- }
+ th.AssertNoErr(t, err)
allDomains, err := domains.ExtractDomains(allPages)
- if err != nil {
- t.Fatalf("Unable to extract domains: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, domain := range allDomains {
tools.PrintResource(t, domain)
+
+ if domain.Name == "Default" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestDomainsGet(t *testing.T) {
- client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ clients.RequireAdmin(t)
- allPages, err := domains.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list domains: %v", err)
- }
-
- allDomains, err := domains.ExtractDomains(allPages)
- if err != nil {
- t.Fatalf("Unable to extract domains: %v", err)
- }
+ client, err := clients.NewIdentityV3Client()
+ th.AssertNoErr(t, err)
- domain := allDomains[0]
- p, err := domains.Get(client, domain.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get domain: %v", err)
- }
+ p, err := domains.Get(client, "default").Extract()
+ th.AssertNoErr(t, err)
tools.PrintResource(t, p)
+
+ th.AssertEquals(t, p.Name, "Default")
}
func TestDomainsCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
var iTrue bool = true
createOpts := domains.CreateOpts{
@@ -74,13 +67,13 @@ func TestDomainsCRUD(t *testing.T) {
}
domain, err := CreateDomain(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create domain: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteDomain(t, client, domain.ID)
tools.PrintResource(t, domain)
+ th.AssertEquals(t, domain.Description, "Testing Domain")
+
var iFalse bool = false
updateOpts := domains.UpdateOpts{
Description: "Staging Test Domain",
@@ -88,9 +81,9 @@ func TestDomainsCRUD(t *testing.T) {
}
newDomain, err := domains.Update(client, domain.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update domain: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newDomain)
+
+ th.AssertEquals(t, newDomain.Description, "Staging Test Domain")
}
diff --git a/acceptance/openstack/identity/v3/endpoint_test.go b/acceptance/openstack/identity/v3/endpoint_test.go
index a589970606..4bc606b34c 100644
--- a/acceptance/openstack/identity/v3/endpoint_test.go
+++ b/acceptance/openstack/identity/v3/endpoint_test.go
@@ -3,6 +3,7 @@
package v3
import (
+ "strings"
"testing"
"github.com/gophercloud/gophercloud"
@@ -10,34 +11,38 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v3/endpoints"
"github.com/gophercloud/gophercloud/openstack/identity/v3/services"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestEndpointsList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
allPages, err := endpoints.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list endpoints: %v", err)
- }
+ th.AssertNoErr(t, err)
allEndpoints, err := endpoints.ExtractEndpoints(allPages)
- if err != nil {
- t.Fatalf("Unable to extract endpoints: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, endpoint := range allEndpoints {
tools.PrintResource(t, endpoint)
+
+ if strings.Contains(endpoint.URL, "/v3") {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestEndpointsNavigateCatalog(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
// Discover the service we're interested in.
serviceListOpts := services.ListOpts{
@@ -45,18 +50,12 @@ func TestEndpointsNavigateCatalog(t *testing.T) {
}
allPages, err := services.List(client, serviceListOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to lookup compute service: %v", err)
- }
+ th.AssertNoErr(t, err)
allServices, err := services.ExtractServices(allPages)
- if err != nil {
- t.Fatalf("Unable to extract service: %v")
- }
+ th.AssertNoErr(t, err)
- if len(allServices) != 1 {
- t.Fatalf("Expected one service, got %d", len(allServices))
- }
+ th.AssertEquals(t, len(allServices), 1)
computeService := allServices[0]
tools.PrintResource(t, computeService)
@@ -68,19 +67,12 @@ func TestEndpointsNavigateCatalog(t *testing.T) {
}
allPages, err = endpoints.List(client, endpointListOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to lookup compute endpoint: %v", err)
- }
+ th.AssertNoErr(t, err)
allEndpoints, err := endpoints.ExtractEndpoints(allPages)
- if err != nil {
- t.Fatalf("Unable to extract endpoint: %v")
- }
+ th.AssertNoErr(t, err)
- if len(allEndpoints) != 1 {
- t.Fatalf("Expected one endpoint, got %d", len(allEndpoints))
- }
+ th.AssertEquals(t, len(allServices), 1)
tools.PrintResource(t, allEndpoints[0])
-
}
diff --git a/acceptance/openstack/identity/v3/groups_test.go b/acceptance/openstack/identity/v3/groups_test.go
index 3e832c2022..3b411bd032 100644
--- a/acceptance/openstack/identity/v3/groups_test.go
+++ b/acceptance/openstack/identity/v3/groups_test.go
@@ -8,13 +8,14 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v3/groups"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestGroupCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
createOpts := groups.CreateOpts{
Name: "testgroup",
@@ -26,9 +27,7 @@ func TestGroupCRUD(t *testing.T) {
// Create Group in the default domain
group, err := CreateGroup(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create group: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteGroup(t, client, group.ID)
tools.PrintResource(t, group)
@@ -42,9 +41,7 @@ func TestGroupCRUD(t *testing.T) {
}
newGroup, err := groups.Update(client, group.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update group: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newGroup)
tools.PrintResource(t, newGroup.Extra)
@@ -55,14 +52,10 @@ func TestGroupCRUD(t *testing.T) {
// List all Groups in default domain
allPages, err := groups.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to list groups: %v", err)
- }
+ th.AssertNoErr(t, err)
allGroups, err := groups.ExtractGroups(allPages)
- if err != nil {
- t.Fatalf("Unable to extract groups: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, g := range allGroups {
tools.PrintResource(t, g)
@@ -71,9 +64,7 @@ func TestGroupCRUD(t *testing.T) {
// Get the recently created group by ID
p, err := groups.Get(client, group.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get group: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, p)
}
diff --git a/acceptance/openstack/identity/v3/identity.go b/acceptance/openstack/identity/v3/identity.go
index ae7560dd58..88dee1ec93 100644
--- a/acceptance/openstack/identity/v3/identity.go
+++ b/acceptance/openstack/identity/v3/identity.go
@@ -12,6 +12,7 @@ import (
"github.com/gophercloud/gophercloud/openstack/identity/v3/roles"
"github.com/gophercloud/gophercloud/openstack/identity/v3/services"
"github.com/gophercloud/gophercloud/openstack/identity/v3/users"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
// CreateProject will create a project with a random name.
@@ -38,6 +39,8 @@ func CreateProject(t *testing.T, client *gophercloud.ServiceClient, c *projects.
t.Logf("Successfully created project %s with ID %s", name, project.ID)
+ th.AssertEquals(t, project.Name, name)
+
return project, nil
}
@@ -65,6 +68,8 @@ func CreateUser(t *testing.T, client *gophercloud.ServiceClient, c *users.Create
t.Logf("Successfully created user %s with ID %s", name, user.ID)
+ th.AssertEquals(t, user.Name, name)
+
return user, nil
}
@@ -92,6 +97,8 @@ func CreateGroup(t *testing.T, client *gophercloud.ServiceClient, c *groups.Crea
t.Logf("Successfully created group %s with ID %s", name, group.ID)
+ th.AssertEquals(t, group.Name, name)
+
return group, nil
}
@@ -119,6 +126,8 @@ func CreateDomain(t *testing.T, client *gophercloud.ServiceClient, c *domains.Cr
t.Logf("Successfully created domain %s with ID %s", name, domain.ID)
+ th.AssertEquals(t, domain.Name, name)
+
return domain, nil
}
@@ -146,6 +155,8 @@ func CreateRole(t *testing.T, client *gophercloud.ServiceClient, c *roles.Create
t.Logf("Successfully created role %s with ID %s", name, role.ID)
+ th.AssertEquals(t, role.Name, name)
+
return role, nil
}
@@ -173,6 +184,8 @@ func CreateRegion(t *testing.T, client *gophercloud.ServiceClient, c *regions.Cr
t.Logf("Successfully created region %s", id)
+ th.AssertEquals(t, region.ID, id)
+
return region, nil
}
@@ -200,6 +213,8 @@ func CreateService(t *testing.T, client *gophercloud.ServiceClient, c *services.
t.Logf("Successfully created service %s", service.ID)
+ th.AssertEquals(t, service.Extra["name"], name)
+
return service, nil
}
diff --git a/acceptance/openstack/identity/v3/projects_test.go b/acceptance/openstack/identity/v3/projects_test.go
index 08a5cfdad4..e6d63c82a5 100644
--- a/acceptance/openstack/identity/v3/projects_test.go
+++ b/acceptance/openstack/identity/v3/projects_test.go
@@ -8,13 +8,14 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestProjectsList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
var iTrue bool = true
listOpts := projects.ListOpts{
@@ -22,35 +23,34 @@ func TestProjectsList(t *testing.T) {
}
allPages, err := projects.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to list projects: %v", err)
- }
+ th.AssertNoErr(t, err)
allProjects, err := projects.ExtractProjects(allPages)
- if err != nil {
- t.Fatalf("Unable to extract projects: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, project := range allProjects {
tools.PrintResource(t, project)
+
+ if project.Name == "admin" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestProjectsGet(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := projects.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list projects: %v", err)
- }
+ th.AssertNoErr(t, err)
allProjects, err := projects.ExtractProjects(allPages)
- if err != nil {
- t.Fatalf("Unable to extract projects: %v", err)
- }
+ th.AssertNoErr(t, err)
project := allProjects[0]
p, err := projects.Get(client, project.ID).Extract()
@@ -59,18 +59,18 @@ func TestProjectsGet(t *testing.T) {
}
tools.PrintResource(t, p)
+
+ th.AssertEquals(t, project.Name, p.Name)
}
func TestProjectsCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
project, err := CreateProject(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create project: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, project.ID)
tools.PrintResource(t, project)
@@ -81,18 +81,16 @@ func TestProjectsCRUD(t *testing.T) {
}
updatedProject, err := projects.Update(client, project.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update project: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, updatedProject)
}
func TestProjectsDomain(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
var iTrue = true
createOpts := projects.CreateOpts{
@@ -100,9 +98,7 @@ func TestProjectsDomain(t *testing.T) {
}
projectDomain, err := CreateProject(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create project: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, projectDomain.ID)
tools.PrintResource(t, projectDomain)
@@ -112,34 +108,30 @@ func TestProjectsDomain(t *testing.T) {
}
project, err := CreateProject(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create project: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, project.ID)
tools.PrintResource(t, project)
+ th.AssertEquals(t, project.DomainID, projectDomain.ID)
+
var iFalse = false
updateOpts := projects.UpdateOpts{
Enabled: &iFalse,
}
_, err = projects.Update(client, projectDomain.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to disable domain: %v")
- }
+ th.AssertNoErr(t, err)
}
func TestProjectsNested(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
projectMain, err := CreateProject(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create project: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, projectMain.ID)
tools.PrintResource(t, projectMain)
@@ -149,10 +141,10 @@ func TestProjectsNested(t *testing.T) {
}
project, err := CreateProject(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create project: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, project.ID)
tools.PrintResource(t, project)
+
+ th.AssertEquals(t, project.ParentID, projectMain.ID)
}
diff --git a/acceptance/openstack/identity/v3/regions_test.go b/acceptance/openstack/identity/v3/regions_test.go
index f98c232314..f44a65be8b 100644
--- a/acceptance/openstack/identity/v3/regions_test.go
+++ b/acceptance/openstack/identity/v3/regions_test.go
@@ -8,27 +8,24 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v3/regions"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestRegionsList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
listOpts := regions.ListOpts{
ParentRegionID: "RegionOne",
}
allPages, err := regions.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to list regions: %v", err)
- }
+ th.AssertNoErr(t, err)
allRegions, err := regions.ExtractRegions(allPages)
- if err != nil {
- t.Fatalf("Unable to extract regions: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, region := range allRegions {
tools.PrintResource(t, region)
@@ -36,35 +33,31 @@ func TestRegionsList(t *testing.T) {
}
func TestRegionsGet(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := regions.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list regions: %v", err)
- }
+ th.AssertNoErr(t, err)
allRegions, err := regions.ExtractRegions(allPages)
- if err != nil {
- t.Fatalf("Unable to extract regions: %v", err)
- }
+ th.AssertNoErr(t, err)
region := allRegions[0]
p, err := regions.Get(client, region.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get region: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, p)
+
+ th.AssertEquals(t, region.ID, p.ID)
}
func TestRegionsCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
createOpts := regions.CreateOpts{
ID: "testregion",
@@ -76,9 +69,7 @@ func TestRegionsCRUD(t *testing.T) {
// Create region in the default domain
region, err := CreateRegion(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create region: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteRegion(t, client, region.ID)
tools.PrintResource(t, region)
@@ -98,9 +89,7 @@ func TestRegionsCRUD(t *testing.T) {
}
newRegion, err := regions.Update(client, region.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update region: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newRegion)
tools.PrintResource(t, newRegion.Extra)
diff --git a/acceptance/openstack/identity/v3/roles_test.go b/acceptance/openstack/identity/v3/roles_test.go
index be8e73f4e3..f442439086 100644
--- a/acceptance/openstack/identity/v3/roles_test.go
+++ b/acceptance/openstack/identity/v3/roles_test.go
@@ -10,27 +10,24 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v3/domains"
"github.com/gophercloud/gophercloud/openstack/identity/v3/roles"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestRolesList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
listOpts := roles.ListOpts{
DomainID: "default",
}
allPages, err := roles.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to list roles: %v", err)
- }
+ th.AssertNoErr(t, err)
allRoles, err := roles.ExtractRoles(allPages)
- if err != nil {
- t.Fatalf("Unable to extract roles: %v", err)
- }
+ th.AssertNoErr(t, err)
for _, role := range allRoles {
tools.PrintResource(t, role)
@@ -38,29 +35,25 @@ func TestRolesList(t *testing.T) {
}
func TestRolesGet(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
role, err := FindRole(t, client)
- if err != nil {
- t.Fatalf("Unable to find a role: %v", err)
- }
+ th.AssertNoErr(t, err)
p, err := roles.Get(client, role.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get role: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, p)
}
-func TestRoleCRUD(t *testing.T) {
+func TestRolesCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
createOpts := roles.CreateOpts{
Name: "testrole",
@@ -72,9 +65,7 @@ func TestRoleCRUD(t *testing.T) {
// Create Role in the default domain
role, err := CreateRole(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create role: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteRole(t, client, role.ID)
tools.PrintResource(t, role)
@@ -87,242 +78,494 @@ func TestRoleCRUD(t *testing.T) {
}
newRole, err := roles.Update(client, role.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update role: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newRole)
tools.PrintResource(t, newRole.Extra)
+
+ th.AssertEquals(t, newRole.Extra["description"], "updated test role description")
}
-func TestRoleAssignToUserOnProject(t *testing.T) {
+func TestRoleListAssignmentForUserOnProject(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an indentity client: %v", err)
- }
+ th.AssertNoErr(t, err)
project, err := CreateProject(t, client, nil)
- if err != nil {
- t.Fatal("Unable to create a project")
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, project.ID)
role, err := FindRole(t, client)
- if err != nil {
- t.Fatalf("Unable to get a role: %v", err)
+ th.AssertNoErr(t, err)
+
+ user, err := CreateUser(t, client, nil)
+ th.AssertNoErr(t, err)
+ defer DeleteUser(t, client, user.ID)
+
+ t.Logf("Attempting to assign a role %s to a user %s on a project %s",
+ role.Name, user.Name, project.Name)
+
+ assignOpts := roles.AssignOpts{
+ UserID: user.ID,
+ ProjectID: project.ID,
}
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a user %s on a project %s",
+ role.Name, user.Name, project.Name)
+
+ defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
+ UserID: user.ID,
+ ProjectID: project.ID,
+ })
+
+ listAssignmentsOnResourceOpts := roles.ListAssignmentsOnResourceOpts{
+ UserID: user.ID,
+ ProjectID: project.ID,
+ }
+ allPages, err := roles.ListAssignmentsOnResource(client, listAssignmentsOnResourceOpts).AllPages()
+ th.AssertNoErr(t, err)
+
+ allRoles, err := roles.ExtractRoles(allPages)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Role assignments of user %s on project %s:", user.Name, project.Name)
+ var found bool
+ for _, _role := range allRoles {
+ tools.PrintResource(t, _role)
+
+ if _role.ID == role.ID {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, true)
+}
+
+func TestRoleListAssignmentForUserOnDomain(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewIdentityV3Client()
+ th.AssertNoErr(t, err)
+
+ domain, err := CreateDomain(t, client, &domains.CreateOpts{
+ Enabled: gophercloud.Disabled,
+ })
+ th.AssertNoErr(t, err)
+ defer DeleteDomain(t, client, domain.ID)
+
+ role, err := FindRole(t, client)
+ th.AssertNoErr(t, err)
user, err := CreateUser(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create user: %v", err)
+ th.AssertNoErr(t, err)
+ defer DeleteUser(t, client, user.ID)
+
+ t.Logf("Attempting to assign a role %s to a user %s on a domain %s",
+ role.Name, user.Name, domain.Name)
+
+ assignOpts := roles.AssignOpts{
+ UserID: user.ID,
+ DomainID: domain.ID,
+ }
+
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a user %s on a domain %s",
+ role.Name, user.Name, domain.Name)
+
+ defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
+ UserID: user.ID,
+ DomainID: domain.ID,
+ })
+
+ listAssignmentsOnResourceOpts := roles.ListAssignmentsOnResourceOpts{
+ UserID: user.ID,
+ DomainID: domain.ID,
+ }
+ allPages, err := roles.ListAssignmentsOnResource(client, listAssignmentsOnResourceOpts).AllPages()
+ th.AssertNoErr(t, err)
+
+ allRoles, err := roles.ExtractRoles(allPages)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Role assignments of user %s on domain %s:", user.Name, domain.Name)
+ var found bool
+ for _, _role := range allRoles {
+ tools.PrintResource(t, _role)
+
+ if _role.ID == role.ID {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, true)
+}
+
+func TestRoleListAssignmentForGroupOnProject(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewIdentityV3Client()
+ th.AssertNoErr(t, err)
+
+ project, err := CreateProject(t, client, nil)
+ th.AssertNoErr(t, err)
+ defer DeleteProject(t, client, project.ID)
+
+ role, err := FindRole(t, client)
+ th.AssertNoErr(t, err)
+
+ group, err := CreateGroup(t, client, nil)
+ th.AssertNoErr(t, err)
+ defer DeleteGroup(t, client, group.ID)
+
+ t.Logf("Attempting to assign a role %s to a group %s on a project %s",
+ role.Name, group.Name, project.Name)
+
+ assignOpts := roles.AssignOpts{
+ GroupID: group.ID,
+ ProjectID: project.ID,
+ }
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a group %s on a project %s",
+ role.Name, group.Name, project.Name)
+
+ defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
+ GroupID: group.ID,
+ ProjectID: project.ID,
+ })
+
+ listAssignmentsOnResourceOpts := roles.ListAssignmentsOnResourceOpts{
+ GroupID: group.ID,
+ ProjectID: project.ID,
}
+ allPages, err := roles.ListAssignmentsOnResource(client, listAssignmentsOnResourceOpts).AllPages()
+ th.AssertNoErr(t, err)
+
+ allRoles, err := roles.ExtractRoles(allPages)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Role assignments of group %s on project %s:", group.Name, project.Name)
+ var found bool
+ for _, _role := range allRoles {
+ tools.PrintResource(t, _role)
+
+ if _role.ID == role.ID {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, true)
+}
+
+func TestRoleListAssignmentForGroupOnDomain(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewIdentityV3Client()
+ th.AssertNoErr(t, err)
+
+ domain, err := CreateDomain(t, client, &domains.CreateOpts{
+ Enabled: gophercloud.Disabled,
+ })
+ th.AssertNoErr(t, err)
+ defer DeleteDomain(t, client, domain.ID)
+
+ role, err := FindRole(t, client)
+ th.AssertNoErr(t, err)
+
+ group, err := CreateGroup(t, client, nil)
+ th.AssertNoErr(t, err)
+ defer DeleteGroup(t, client, group.ID)
+
+ t.Logf("Attempting to assign a role %s to a group %s on a domain %s",
+ role.Name, group.Name, domain.Name)
+
+ assignOpts := roles.AssignOpts{
+ GroupID: group.ID,
+ DomainID: domain.ID,
+ }
+
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a group %s on a domain %s",
+ role.Name, group.Name, domain.Name)
+
+ defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
+ GroupID: group.ID,
+ DomainID: domain.ID,
+ })
+
+ listAssignmentsOnResourceOpts := roles.ListAssignmentsOnResourceOpts{
+ GroupID: group.ID,
+ DomainID: domain.ID,
+ }
+ allPages, err := roles.ListAssignmentsOnResource(client, listAssignmentsOnResourceOpts).AllPages()
+ th.AssertNoErr(t, err)
+
+ allRoles, err := roles.ExtractRoles(allPages)
+ th.AssertNoErr(t, err)
+
+ t.Logf("Role assignments of group %s on domain %s:", group.Name, domain.Name)
+ var found bool
+ for _, _role := range allRoles {
+ tools.PrintResource(t, _role)
+
+ if _role.ID == role.ID {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, true)
+}
+
+func TestRolesAssignToUserOnProject(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewIdentityV3Client()
+ th.AssertNoErr(t, err)
+
+ project, err := CreateProject(t, client, nil)
+ th.AssertNoErr(t, err)
+ defer DeleteProject(t, client, project.ID)
+
+ role, err := FindRole(t, client)
+ th.AssertNoErr(t, err)
+
+ user, err := CreateUser(t, client, nil)
+ th.AssertNoErr(t, err)
defer DeleteUser(t, client, user.ID)
- t.Logf("Attempting to assign a role %s to a user %s on a project %s", role.Name, user.Name, project.Name)
- err = roles.Assign(client, role.ID, roles.AssignOpts{
+ t.Logf("Attempting to assign a role %s to a user %s on a project %s",
+ role.Name, user.Name, project.Name)
+
+ assignOpts := roles.AssignOpts{
UserID: user.ID,
ProjectID: project.ID,
- }).ExtractErr()
- if err != nil {
- t.Fatalf("Unable to assign a role to a user on a project: %v", err)
}
- t.Logf("Successfully assigned a role %s to a user %s on a project %s", role.Name, user.Name, project.Name)
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a user %s on a project %s",
+ role.Name, user.Name, project.Name)
+
defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
UserID: user.ID,
ProjectID: project.ID,
})
- allPages, err := roles.ListAssignments(client, roles.ListAssignmentsOpts{
+ lao := roles.ListAssignmentsOpts{
RoleID: role.ID,
ScopeProjectID: project.ID,
UserID: user.ID,
- }).AllPages()
- if err != nil {
- t.Fatalf("Unable to list role assignments: %v", err)
}
+ allPages, err := roles.ListAssignments(client, lao).AllPages()
+ th.AssertNoErr(t, err)
+
allRoleAssignments, err := roles.ExtractRoleAssignments(allPages)
- if err != nil {
- t.Fatalf("Unable to extract role assignments: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Role assignments of user %s on project %s:", user.Name, project.Name)
+ var found bool
for _, roleAssignment := range allRoleAssignments {
tools.PrintResource(t, roleAssignment)
+
+ if roleAssignment.Role.ID == role.ID {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
-func TestRoleAssignToUserOnDomain(t *testing.T) {
+func TestRolesAssignToUserOnDomain(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an indentity client: %v", err)
- }
+ th.AssertNoErr(t, err)
domain, err := CreateDomain(t, client, &domains.CreateOpts{
Enabled: gophercloud.Disabled,
})
- if err != nil {
- t.Fatal("Unable to create a domain")
- }
+ th.AssertNoErr(t, err)
defer DeleteDomain(t, client, domain.ID)
role, err := FindRole(t, client)
- if err != nil {
- t.Fatalf("Unable to get a role: %v", err)
- }
+ th.AssertNoErr(t, err)
user, err := CreateUser(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create user: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteUser(t, client, user.ID)
- t.Logf("Attempting to assign a role %s to a user %s on a domain %s", role.Name, user.Name, domain.Name)
- err = roles.Assign(client, role.ID, roles.AssignOpts{
+ t.Logf("Attempting to assign a role %s to a user %s on a domain %s",
+ role.Name, user.Name, domain.Name)
+
+ assignOpts := roles.AssignOpts{
UserID: user.ID,
DomainID: domain.ID,
- }).ExtractErr()
- if err != nil {
- t.Fatalf("Unable to assign a role to a user on a domain: %v", err)
}
- t.Logf("Successfully assigned a role %s to a user %s on a domain %s", role.Name, user.Name, domain.Name)
+
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a user %s on a domain %s",
+ role.Name, user.Name, domain.Name)
+
defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
UserID: user.ID,
DomainID: domain.ID,
})
- allPages, err := roles.ListAssignments(client, roles.ListAssignmentsOpts{
+ lao := roles.ListAssignmentsOpts{
RoleID: role.ID,
ScopeDomainID: domain.ID,
UserID: user.ID,
- }).AllPages()
- if err != nil {
- t.Fatalf("Unable to list role assignments: %v", err)
}
+ allPages, err := roles.ListAssignments(client, lao).AllPages()
+ th.AssertNoErr(t, err)
+
allRoleAssignments, err := roles.ExtractRoleAssignments(allPages)
- if err != nil {
- t.Fatalf("Unable to extract role assignments: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Role assignments of user %s on domain %s:", user.Name, domain.Name)
+ var found bool
for _, roleAssignment := range allRoleAssignments {
tools.PrintResource(t, roleAssignment)
+
+ if roleAssignment.Role.ID == role.ID {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
-func TestRoleAssignToGroupOnDomain(t *testing.T) {
+func TestRolesAssignToGroupOnDomain(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an indentity client: %v", err)
- }
+ th.AssertNoErr(t, err)
domain, err := CreateDomain(t, client, &domains.CreateOpts{
Enabled: gophercloud.Disabled,
})
- if err != nil {
- t.Fatal("Unable to create a domain")
- }
+ th.AssertNoErr(t, err)
defer DeleteDomain(t, client, domain.ID)
role, err := FindRole(t, client)
- if err != nil {
- t.Fatalf("Unable to get a role: %v", err)
- }
+ th.AssertNoErr(t, err)
group, err := CreateGroup(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create group: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteGroup(t, client, group.ID)
- t.Logf("Attempting to assign a role %s to a group %s on a domain %s", role.Name, group.Name, domain.Name)
- err = roles.Assign(client, role.ID, roles.AssignOpts{
+ t.Logf("Attempting to assign a role %s to a group %s on a domain %s",
+ role.Name, group.Name, domain.Name)
+
+ assignOpts := roles.AssignOpts{
GroupID: group.ID,
DomainID: domain.ID,
- }).ExtractErr()
- if err != nil {
- t.Fatalf("Unable to assign a role to a group on a domain: %v", err)
}
- t.Logf("Successfully assigned a role %s to a group %s on a domain %s", role.Name, group.Name, domain.Name)
+
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a group %s on a domain %s",
+ role.Name, group.Name, domain.Name)
+
defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
GroupID: group.ID,
DomainID: domain.ID,
})
- allPages, err := roles.ListAssignments(client, roles.ListAssignmentsOpts{
+ lao := roles.ListAssignmentsOpts{
RoleID: role.ID,
ScopeDomainID: domain.ID,
GroupID: group.ID,
- }).AllPages()
- if err != nil {
- t.Fatalf("Unable to list role assignments: %v", err)
}
+ allPages, err := roles.ListAssignments(client, lao).AllPages()
+ th.AssertNoErr(t, err)
+
allRoleAssignments, err := roles.ExtractRoleAssignments(allPages)
- if err != nil {
- t.Fatalf("Unable to extract role assignments: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Role assignments of group %s on domain %s:", group.Name, domain.Name)
+ var found bool
for _, roleAssignment := range allRoleAssignments {
tools.PrintResource(t, roleAssignment)
+
+ if roleAssignment.Role.ID == role.ID {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
-func TestRoleAssignToGroupOnProject(t *testing.T) {
+func TestRolesAssignToGroupOnProject(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an indentity client: %v", err)
- }
+ th.AssertNoErr(t, err)
project, err := CreateProject(t, client, nil)
- if err != nil {
- t.Fatal("Unable to create a project")
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, project.ID)
role, err := FindRole(t, client)
- if err != nil {
- t.Fatalf("Unable to get a role: %v", err)
- }
+ th.AssertNoErr(t, err)
group, err := CreateGroup(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create group: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteGroup(t, client, group.ID)
- t.Logf("Attempting to assign a role %s to a group %s on a project %s", role.Name, group.Name, project.Name)
- err = roles.Assign(client, role.ID, roles.AssignOpts{
+ t.Logf("Attempting to assign a role %s to a group %s on a project %s",
+ role.Name, group.Name, project.Name)
+
+ assignOpts := roles.AssignOpts{
GroupID: group.ID,
ProjectID: project.ID,
- }).ExtractErr()
- if err != nil {
- t.Fatalf("Unable to assign a role to a group on a project: %v", err)
}
- t.Logf("Successfully assigned a role %s to a group %s on a project %s", role.Name, group.Name, project.Name)
+ err = roles.Assign(client, role.ID, assignOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ t.Logf("Successfully assigned a role %s to a group %s on a project %s",
+ role.Name, group.Name, project.Name)
+
defer UnassignRole(t, client, role.ID, &roles.UnassignOpts{
GroupID: group.ID,
ProjectID: project.ID,
})
- allPages, err := roles.ListAssignments(client, roles.ListAssignmentsOpts{
+ lao := roles.ListAssignmentsOpts{
RoleID: role.ID,
ScopeProjectID: project.ID,
GroupID: group.ID,
- }).AllPages()
- if err != nil {
- t.Fatalf("Unable to list role assignments: %v", err)
}
+ allPages, err := roles.ListAssignments(client, lao).AllPages()
+ th.AssertNoErr(t, err)
+
allRoleAssignments, err := roles.ExtractRoleAssignments(allPages)
- if err != nil {
- t.Fatalf("Unable to extract role assignments: %v", err)
- }
+ th.AssertNoErr(t, err)
t.Logf("Role assignments of group %s on project %s:", group.Name, project.Name)
+ var found bool
for _, roleAssignment := range allRoleAssignments {
tools.PrintResource(t, roleAssignment)
+
+ if roleAssignment.Role.ID == role.ID {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
diff --git a/acceptance/openstack/identity/v3/service_test.go b/acceptance/openstack/identity/v3/service_test.go
index ffd7e4d1fb..7e072ce3a4 100644
--- a/acceptance/openstack/identity/v3/service_test.go
+++ b/acceptance/openstack/identity/v3/service_test.go
@@ -8,39 +8,42 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/identity/v3/services"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestServicesList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
listOpts := services.ListOpts{
ServiceType: "identity",
}
allPages, err := services.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to list services: %v", err)
- }
+ th.AssertNoErr(t, err)
allServices, err := services.ExtractServices(allPages)
- if err != nil {
- t.Fatalf("Unable to extract services: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, service := range allServices {
tools.PrintResource(t, service)
+
+ if service.Type == "identity" {
+ found = true
+ }
}
+ th.AssertEquals(t, found, true)
}
func TestServicesCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
createOpts := services.CreateOpts{
Type: "testing",
@@ -51,9 +54,7 @@ func TestServicesCRUD(t *testing.T) {
// Create service in the default domain
service, err := CreateService(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create service: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteService(t, client, service.ID)
tools.PrintResource(t, service)
@@ -68,10 +69,10 @@ func TestServicesCRUD(t *testing.T) {
}
newService, err := services.Update(client, service.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update service: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newService)
tools.PrintResource(t, newService.Extra)
+
+ th.AssertEquals(t, newService.Extra["description"], "Test Users")
}
diff --git a/acceptance/openstack/identity/v3/token_test.go b/acceptance/openstack/identity/v3/token_test.go
index 0f471f776b..ff6e91d49f 100644
--- a/acceptance/openstack/identity/v3/token_test.go
+++ b/acceptance/openstack/identity/v3/token_test.go
@@ -9,18 +9,17 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
-func TestGetToken(t *testing.T) {
+func TestTokensGet(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v")
- }
+ th.AssertNoErr(t, err)
ao, err := openstack.AuthOptionsFromEnv()
- if err != nil {
- t.Fatalf("Unable to obtain environment auth options: %v", err)
- }
+ th.AssertNoErr(t, err)
authOptions := tokens.AuthOptions{
Username: ao.Username,
@@ -29,32 +28,22 @@ func TestGetToken(t *testing.T) {
}
token, err := tokens.Create(client, &authOptions).Extract()
- if err != nil {
- t.Fatalf("Unable to get token: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, token)
catalog, err := tokens.Get(client, token.ID).ExtractServiceCatalog()
- if err != nil {
- t.Fatalf("Unable to get catalog from token: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, catalog)
user, err := tokens.Get(client, token.ID).ExtractUser()
- if err != nil {
- t.Fatalf("Unable to get user from token: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, user)
roles, err := tokens.Get(client, token.ID).ExtractRoles()
- if err != nil {
- t.Fatalf("Unable to get roles from token: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, roles)
project, err := tokens.Get(client, token.ID).ExtractProject()
- if err != nil {
- t.Fatalf("Unable to get project from token: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, project)
}
diff --git a/acceptance/openstack/identity/v3/users_test.go b/acceptance/openstack/identity/v3/users_test.go
index 3ba1e87cf5..d51eba18d2 100644
--- a/acceptance/openstack/identity/v3/users_test.go
+++ b/acceptance/openstack/identity/v3/users_test.go
@@ -10,13 +10,14 @@ import (
"github.com/gophercloud/gophercloud/openstack/identity/v3/groups"
"github.com/gophercloud/gophercloud/openstack/identity/v3/projects"
"github.com/gophercloud/gophercloud/openstack/identity/v3/users"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestUsersList(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
var iTrue bool = true
listOpts := users.ListOpts{
@@ -24,56 +25,53 @@ func TestUsersList(t *testing.T) {
}
allPages, err := users.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to list users: %v", err)
- }
+ th.AssertNoErr(t, err)
allUsers, err := users.ExtractUsers(allPages)
- if err != nil {
- t.Fatalf("Unable to extract users: %v", err)
- }
+ th.AssertNoErr(t, err)
+ var found bool
for _, user := range allUsers {
tools.PrintResource(t, user)
tools.PrintResource(t, user.Extra)
+
+ if user.Name == "admin" {
+ found = true
+ }
}
+
+ th.AssertEquals(t, found, true)
}
func TestUsersGet(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
allPages, err := users.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list users: %v", err)
- }
+ th.AssertNoErr(t, err)
allUsers, err := users.ExtractUsers(allPages)
- if err != nil {
- t.Fatalf("Unable to extract users: %v", err)
- }
+ th.AssertNoErr(t, err)
user := allUsers[0]
p, err := users.Get(client, user.ID).Extract()
- if err != nil {
- t.Fatalf("Unable to get user: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, p)
+
+ th.AssertEquals(t, user.Name, p.Name)
}
func TestUserCRUD(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
+ th.AssertNoErr(t, err)
project, err := CreateProject(t, client, nil)
- if err != nil {
- t.Fatalf("Unable to create project: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteProject(t, client, project.ID)
tools.PrintResource(t, project)
@@ -95,9 +93,7 @@ func TestUserCRUD(t *testing.T) {
}
user, err := CreateUser(t, client, &createOpts)
- if err != nil {
- t.Fatalf("Unable to create user: %v", err)
- }
+ th.AssertNoErr(t, err)
defer DeleteUser(t, client, user.ID)
tools.PrintResource(t, user)
@@ -115,108 +111,172 @@ func TestUserCRUD(t *testing.T) {
}
newUser, err := users.Update(client, user.ID, updateOpts).Extract()
- if err != nil {
- t.Fatalf("Unable to update user: %v", err)
- }
+ th.AssertNoErr(t, err)
tools.PrintResource(t, newUser)
tools.PrintResource(t, newUser.Extra)
+
+ th.AssertEquals(t, newUser.Extra["disabled_reason"], "DDOS")
}
-func TestUsersListGroups(t *testing.T) {
+func TestUserChangePassword(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
- allUserPages, err := users.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list users: %v", err)
- }
+ th.AssertNoErr(t, err)
- allUsers, err := users.ExtractUsers(allUserPages)
- if err != nil {
- t.Fatalf("Unable to extract users: %v", err)
+ createOpts := users.CreateOpts{
+ Password: "secretsecret",
+ DomainID: "default",
}
- user := allUsers[0]
-
- allGroupPages, err := users.ListGroups(client, user.ID).AllPages()
- if err != nil {
- t.Fatalf("Unable to list groups: %v", err)
- }
+ user, err := CreateUser(t, client, &createOpts)
+ th.AssertNoErr(t, err)
+ defer DeleteUser(t, client, user.ID)
- allGroups, err := groups.ExtractGroups(allGroupPages)
- if err != nil {
- t.Fatalf("Unable to extract groups: %v", err)
- }
+ tools.PrintResource(t, user)
+ tools.PrintResource(t, user.Extra)
- for _, group := range allGroups {
- tools.PrintResource(t, group)
- tools.PrintResource(t, group.Extra)
+ changePasswordOpts := users.ChangePasswordOpts{
+ OriginalPassword: "secretsecret",
+ Password: "new_secretsecret",
}
+ err = users.ChangePassword(client, user.ID, changePasswordOpts).ExtractErr()
+ th.AssertNoErr(t, err)
}
-func TestUsersListProjects(t *testing.T) {
+func TestUsersGroups(t *testing.T) {
+ clients.RequireAdmin(t)
+
client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
- allUserPages, err := users.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list users: %v", err)
- }
+ th.AssertNoErr(t, err)
- allUsers, err := users.ExtractUsers(allUserPages)
- if err != nil {
- t.Fatalf("Unable to extract users: %v", err)
+ createOpts := users.CreateOpts{
+ Password: "foobar",
+ DomainID: "default",
}
- user := allUsers[0]
+ user, err := CreateUser(t, client, &createOpts)
+ th.AssertNoErr(t, err)
+ defer DeleteUser(t, client, user.ID)
- allProjectPages, err := users.ListProjects(client, user.ID).AllPages()
- if err != nil {
- t.Fatalf("Unable to list projects: %v", err)
- }
+ tools.PrintResource(t, user)
+ tools.PrintResource(t, user.Extra)
- allProjects, err := projects.ExtractProjects(allProjectPages)
- if err != nil {
- t.Fatalf("Unable to extract projects: %v", err)
+ createGroupOpts := groups.CreateOpts{
+ Name: "testgroup",
+ DomainID: "default",
}
- for _, project := range allProjects {
- tools.PrintResource(t, project)
- }
-}
+ // Create Group in the default domain
+ group, err := CreateGroup(t, client, &createGroupOpts)
+ th.AssertNoErr(t, err)
+ defer DeleteGroup(t, client, group.ID)
-func TestUsersListInGroup(t *testing.T) {
- client, err := clients.NewIdentityV3Client()
- if err != nil {
- t.Fatalf("Unable to obtain an identity client: %v", err)
- }
- allGroupPages, err := groups.List(client, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list groups: %v", err)
- }
+ tools.PrintResource(t, group)
+ tools.PrintResource(t, group.Extra)
+
+ err = users.AddToGroup(client, group.ID, user.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ allGroupPages, err := users.ListGroups(client, user.ID).AllPages()
+ th.AssertNoErr(t, err)
allGroups, err := groups.ExtractGroups(allGroupPages)
- if err != nil {
- t.Fatalf("Unable to extract groups: %v", err)
+ th.AssertNoErr(t, err)
+
+ var found bool
+ for _, g := range allGroups {
+ tools.PrintResource(t, g)
+ tools.PrintResource(t, g.Extra)
+
+ if g.ID == group.ID {
+ found = true
+ }
}
- group := allGroups[0]
+ th.AssertEquals(t, found, true)
+ found = false
allUserPages, err := users.ListInGroup(client, group.ID, nil).AllPages()
- if err != nil {
- t.Fatalf("Unable to list users: %v", err)
- }
+ th.AssertNoErr(t, err)
allUsers, err := users.ExtractUsers(allUserPages)
- if err != nil {
- t.Fatalf("Unable to extract users: %v", err)
+ th.AssertNoErr(t, err)
+
+ for _, u := range allUsers {
+ tools.PrintResource(t, user)
+ tools.PrintResource(t, user.Extra)
+
+ if u.ID == user.ID {
+ found = true
+ }
}
- for _, user := range allUsers {
+ th.AssertEquals(t, found, true)
+
+ err = users.RemoveFromGroup(client, group.ID, user.ID).ExtractErr()
+ th.AssertNoErr(t, err)
+
+ allGroupPages, err = users.ListGroups(client, user.ID).AllPages()
+ th.AssertNoErr(t, err)
+
+ allGroups, err = groups.ExtractGroups(allGroupPages)
+ th.AssertNoErr(t, err)
+
+ found = false
+ for _, g := range allGroups {
+ tools.PrintResource(t, g)
+ tools.PrintResource(t, g.Extra)
+
+ if g.ID == group.ID {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, false)
+
+ found = false
+ allUserPages, err = users.ListInGroup(client, group.ID, nil).AllPages()
+ th.AssertNoErr(t, err)
+
+ allUsers, err = users.ExtractUsers(allUserPages)
+ th.AssertNoErr(t, err)
+
+ for _, u := range allUsers {
tools.PrintResource(t, user)
tools.PrintResource(t, user.Extra)
+
+ if u.ID == user.ID {
+ found = true
+ }
+ }
+
+ th.AssertEquals(t, found, false)
+
+}
+
+func TestUsersListProjects(t *testing.T) {
+ clients.RequireAdmin(t)
+
+ client, err := clients.NewIdentityV3Client()
+ th.AssertNoErr(t, err)
+
+ allUserPages, err := users.List(client, nil).AllPages()
+ th.AssertNoErr(t, err)
+
+ allUsers, err := users.ExtractUsers(allUserPages)
+ th.AssertNoErr(t, err)
+
+ user := allUsers[0]
+
+ allProjectPages, err := users.ListProjects(client, user.ID).AllPages()
+ th.AssertNoErr(t, err)
+
+ allProjects, err := projects.ExtractProjects(allProjectPages)
+ th.AssertNoErr(t, err)
+
+ for _, project := range allProjects {
+ tools.PrintResource(t, project)
}
}
diff --git a/acceptance/openstack/imageservice/v2/images_test.go b/acceptance/openstack/imageservice/v2/images_test.go
index c2a8987319..9c6cf32a06 100644
--- a/acceptance/openstack/imageservice/v2/images_test.go
+++ b/acceptance/openstack/imageservice/v2/images_test.go
@@ -4,18 +4,18 @@ package v2
import (
"testing"
+ "time"
"github.com/gophercloud/gophercloud/acceptance/clients"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
func TestImagesListEachPage(t *testing.T) {
client, err := clients.NewImageServiceV2Client()
- if err != nil {
- t.Fatalf("Unable to create an image service client: %v", err)
- }
+ th.AssertNoErr(t, err)
listOpts := images.ListOpts{
Limit: 1,
@@ -39,42 +39,102 @@ func TestImagesListEachPage(t *testing.T) {
func TestImagesListAllPages(t *testing.T) {
client, err := clients.NewImageServiceV2Client()
- if err != nil {
- t.Fatalf("Unable to create an image service client: %v", err)
+ th.AssertNoErr(t, err)
+
+ image, err := CreateEmptyImage(t, client)
+ th.AssertNoErr(t, err)
+ defer DeleteImage(t, client, image)
+
+ listOpts := images.ListOpts{}
+
+ allPages, err := images.List(client, listOpts).AllPages()
+ th.AssertNoErr(t, err)
+
+ allImages, err := images.ExtractImages(allPages)
+ th.AssertNoErr(t, err)
+
+ var found bool
+ for _, i := range allImages {
+ tools.PrintResource(t, i)
+ tools.PrintResource(t, i.Properties)
+
+ if i.Name == image.Name {
+ found = true
+ }
}
+ th.AssertEquals(t, found, true)
+}
+
+func TestImagesListByDate(t *testing.T) {
+ client, err := clients.NewImageServiceV2Client()
+ th.AssertNoErr(t, err)
+
+ date := time.Date(2014, 1, 1, 1, 1, 1, 0, time.UTC)
listOpts := images.ListOpts{
Limit: 1,
+ CreatedAtQuery: &images.ImageDateQuery{
+ Date: date,
+ Filter: images.FilterGTE,
+ },
}
allPages, err := images.List(client, listOpts).AllPages()
- if err != nil {
- t.Fatalf("Unable to retrieve all images: %v", err)
- }
+ th.AssertNoErr(t, err)
allImages, err := images.ExtractImages(allPages)
- if err != nil {
- t.Fatalf("Unable to extract images: %v", err)
+ th.AssertNoErr(t, err)
+
+ if len(allImages) == 0 {
+ t.Fatalf("Query resulted in no results")
}
for _, image := range allImages {
tools.PrintResource(t, image)
tools.PrintResource(t, image.Properties)
}
+
+ date = time.Date(2049, 1, 1, 1, 1, 1, 0, time.UTC)
+ listOpts = images.ListOpts{
+ Limit: 1,
+ CreatedAtQuery: &images.ImageDateQuery{
+ Date: date,
+ Filter: images.FilterGTE,
+ },
+ }
+
+ allPages, err = images.List(client, listOpts).AllPages()
+ th.AssertNoErr(t, err)
+
+ allImages, err = images.ExtractImages(allPages)
+ th.AssertNoErr(t, err)
+
+ if len(allImages) > 0 {
+ t.Fatalf("Expected 0 images, got %d", len(allImages))
+ }
}
-func TestImagesCreateDestroyEmptyImage(t *testing.T) {
+func TestImagesFilter(t *testing.T) {
client, err := clients.NewImageServiceV2Client()
- if err != nil {
- t.Fatalf("Unable to create an image service client: %v", err)
- }
+ th.AssertNoErr(t, err)
image, err := CreateEmptyImage(t, client)
- if err != nil {
- t.Fatalf("Unable to create empty image: %v", err)
+ th.AssertNoErr(t, err)
+ defer DeleteImage(t, client, image)
+
+ listOpts := images.ListOpts{
+ Tags: []string{"foo", "bar"},
+ ContainerFormat: "bare",
+ DiskFormat: "qcow2",
}
- defer DeleteImage(t, client, image)
+ allPages, err := images.List(client, listOpts).AllPages()
+ th.AssertNoErr(t, err)
+
+ allImages, err := images.ExtractImages(allPages)
+ th.AssertNoErr(t, err)
- tools.PrintResource(t, image)
+ if len(allImages) == 0 {
+ t.Fatalf("Query resulted in no results")
+ }
}
diff --git a/acceptance/openstack/imageservice/v2/imageservice.go b/acceptance/openstack/imageservice/v2/imageservice.go
index 8aaeeb74b8..54035b0fcc 100644
--- a/acceptance/openstack/imageservice/v2/imageservice.go
+++ b/acceptance/openstack/imageservice/v2/imageservice.go
@@ -8,6 +8,7 @@ import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
+ th "github.com/gophercloud/gophercloud/testhelper"
)
// CreateEmptyImage will create an image, but with no actual image data.
@@ -31,6 +32,7 @@ func CreateEmptyImage(t *testing.T, client *gophercloud.ServiceClient) (*images.
Properties: map[string]string{
"architecture": "x86_64",
},
+ Tags: []string{"foo", "bar", "baz"},
}
image, err := images.Create(client, createOpts).Extract()
@@ -38,8 +40,16 @@ func CreateEmptyImage(t *testing.T, client *gophercloud.ServiceClient) (*images.
return image, err
}
- t.Logf("Created image %s: %#v", name, image)
- return image, nil
+ newImage, err := images.Get(client, image.ID).Extract()
+ if err != nil {
+ return image, err
+ }
+
+ t.Logf("Created image %s: %#v", name, newImage)
+
+ th.CheckEquals(t, newImage.Name, name)
+ th.CheckEquals(t, newImage.Properties["architecture"], "x86_64")
+ return newImage, nil
}
// DeleteImage deletes an image.
diff --git a/acceptance/openstack/loadbalancer/v2/l7policies_test.go b/acceptance/openstack/loadbalancer/v2/l7policies_test.go
new file mode 100644
index 0000000000..848d4e85bc
--- /dev/null
+++ b/acceptance/openstack/loadbalancer/v2/l7policies_test.go
@@ -0,0 +1,32 @@
+// +build acceptance networking loadbalancer l7policies
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies"
+)
+
+func TestL7PoliciesList(t *testing.T) {
+ client, err := clients.NewLoadBalancerV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ allPages, err := l7policies.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list l7policies: %v", err)
+ }
+
+ allL7Policies, err := l7policies.ExtractL7Policies(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract l7policies: %v", err)
+ }
+
+ for _, policy := range allL7Policies {
+ tools.PrintResource(t, policy)
+ }
+}
diff --git a/acceptance/openstack/loadbalancer/v2/listeners_test.go b/acceptance/openstack/loadbalancer/v2/listeners_test.go
new file mode 100644
index 0000000000..f643676544
--- /dev/null
+++ b/acceptance/openstack/loadbalancer/v2/listeners_test.go
@@ -0,0 +1,32 @@
+// +build acceptance networking loadbalancer listeners
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
+)
+
+func TestListenersList(t *testing.T) {
+ client, err := clients.NewLoadBalancerV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a loadbalancer client: %v", err)
+ }
+
+ allPages, err := listeners.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list listeners: %v", err)
+ }
+
+ allListeners, err := listeners.ExtractListeners(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract listeners: %v", err)
+ }
+
+ for _, listener := range allListeners {
+ tools.PrintResource(t, listener)
+ }
+}
diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancer.go b/acceptance/openstack/loadbalancer/v2/loadbalancer.go
new file mode 100644
index 0000000000..d9c8ce86c0
--- /dev/null
+++ b/acceptance/openstack/loadbalancer/v2/loadbalancer.go
@@ -0,0 +1,380 @@
+package v2
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools"
+)
+
+const loadbalancerActiveTimeoutSeconds = 300
+const loadbalancerDeleteTimeoutSeconds = 300
+
+// CreateListener will create a listener for a given load balancer on a random
+// port with a random name. An error will be returned if the listener could not
+// be created.
+func CreateListener(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer) (*listeners.Listener, error) {
+ listenerName := tools.RandomString("TESTACCT-", 8)
+ listenerPort := tools.RandomInt(1, 100)
+
+ t.Logf("Attempting to create listener %s on port %d", listenerName, listenerPort)
+
+ createOpts := listeners.CreateOpts{
+ Name: listenerName,
+ LoadbalancerID: lb.ID,
+ Protocol: "TCP",
+ ProtocolPort: listenerPort,
+ }
+
+ listener, err := listeners.Create(client, createOpts).Extract()
+ if err != nil {
+ return listener, err
+ }
+
+ t.Logf("Successfully created listener %s", listenerName)
+
+ if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ return listener, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+ }
+
+ return listener, nil
+}
+
+// CreateLoadBalancer will create a load balancer with a random name on a given
+// subnet. An error will be returned if the loadbalancer could not be created.
+func CreateLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, subnetID string) (*loadbalancers.LoadBalancer, error) {
+ lbName := tools.RandomString("TESTACCT-", 8)
+
+ t.Logf("Attempting to create loadbalancer %s on subnet %s", lbName, subnetID)
+
+ createOpts := loadbalancers.CreateOpts{
+ Name: lbName,
+ VipSubnetID: subnetID,
+ AdminStateUp: gophercloud.Enabled,
+ }
+
+ lb, err := loadbalancers.Create(client, createOpts).Extract()
+ if err != nil {
+ return lb, err
+ }
+
+ t.Logf("Successfully created loadbalancer %s on subnet %s", lbName, subnetID)
+ t.Logf("Waiting for loadbalancer %s to become active", lbName)
+
+ if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ return lb, err
+ }
+
+ t.Logf("LoadBalancer %s is active", lbName)
+
+ return lb, nil
+}
+
+// CreateMember will create a member with a random name, port, address, and
+// weight. An error will be returned if the member could not be created.
+func CreateMember(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer, pool *pools.Pool, subnetID, subnetCIDR string) (*pools.Member, error) {
+ memberName := tools.RandomString("TESTACCT-", 8)
+ memberPort := tools.RandomInt(100, 1000)
+ memberWeight := tools.RandomInt(1, 10)
+
+ cidrParts := strings.Split(subnetCIDR, "/")
+ subnetParts := strings.Split(cidrParts[0], ".")
+ memberAddress := fmt.Sprintf("%s.%s.%s.%d", subnetParts[0], subnetParts[1], subnetParts[2], tools.RandomInt(10, 100))
+
+ t.Logf("Attempting to create member %s", memberName)
+
+ createOpts := pools.CreateMemberOpts{
+ Name: memberName,
+ ProtocolPort: memberPort,
+ Weight: memberWeight,
+ Address: memberAddress,
+ SubnetID: subnetID,
+ }
+
+ t.Logf("Member create opts: %#v", createOpts)
+
+ member, err := pools.CreateMember(client, pool.ID, createOpts).Extract()
+ if err != nil {
+ return member, err
+ }
+
+ t.Logf("Successfully created member %s", memberName)
+
+ if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ return member, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+ }
+
+ return member, nil
+}
+
+// CreateMonitor will create a monitor with a random name for a specific pool.
+// An error will be returned if the monitor could not be created.
+func CreateMonitor(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer, pool *pools.Pool) (*monitors.Monitor, error) {
+ monitorName := tools.RandomString("TESTACCT-", 8)
+
+ t.Logf("Attempting to create monitor %s", monitorName)
+
+ createOpts := monitors.CreateOpts{
+ PoolID: pool.ID,
+ Name: monitorName,
+ Delay: 10,
+ Timeout: 5,
+ MaxRetries: 5,
+ Type: "PING",
+ }
+
+ monitor, err := monitors.Create(client, createOpts).Extract()
+ if err != nil {
+ return monitor, err
+ }
+
+ t.Logf("Successfully created monitor: %s", monitorName)
+
+ if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ return monitor, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+ }
+
+ return monitor, nil
+}
+
+// CreatePool will create a pool with a random name with a specified listener
+// and loadbalancer. An error will be returned if the pool could not be
+// created.
+func CreatePool(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalancers.LoadBalancer) (*pools.Pool, error) {
+ poolName := tools.RandomString("TESTACCT-", 8)
+
+ t.Logf("Attempting to create pool %s", poolName)
+
+ createOpts := pools.CreateOpts{
+ Name: poolName,
+ Protocol: pools.ProtocolTCP,
+ LoadbalancerID: lb.ID,
+ LBMethod: pools.LBMethodLeastConnections,
+ }
+
+ pool, err := pools.Create(client, createOpts).Extract()
+ if err != nil {
+ return pool, err
+ }
+
+ t.Logf("Successfully created pool %s", poolName)
+
+ if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ return pool, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+ }
+
+ return pool, nil
+}
+
+// CreateL7Policy will create a l7 policy with a random name with a specified listener
+// and loadbalancer. An error will be returned if the l7 policy could not be
+// created.
+func CreateL7Policy(t *testing.T, client *gophercloud.ServiceClient, listener *listeners.Listener, lb *loadbalancers.LoadBalancer) (*l7policies.L7Policy, error) {
+ policyName := tools.RandomString("TESTACCT-", 8)
+
+ t.Logf("Attempting to create l7 policy %s", policyName)
+
+ createOpts := l7policies.CreateOpts{
+ Name: policyName,
+ ListenerID: listener.ID,
+ Action: l7policies.ActionRedirectToURL,
+ RedirectURL: "http://www.example.com",
+ }
+
+ policy, err := l7policies.Create(client, createOpts).Extract()
+ if err != nil {
+ return policy, err
+ }
+
+ t.Logf("Successfully created l7 policy %s", policyName)
+
+ if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ return policy, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+ }
+
+ return policy, nil
+}
+
+// CreateL7Rule creates a l7 rule for specified l7 policy.
+func CreateL7Rule(t *testing.T, client *gophercloud.ServiceClient, policyID string, lb *loadbalancers.LoadBalancer) (*l7policies.Rule, error) {
+ t.Logf("Attempting to create l7 rule for policy %s", policyID)
+
+ createOpts := l7policies.CreateRuleOpts{
+ RuleType: l7policies.TypePath,
+ CompareType: l7policies.CompareTypeStartWith,
+ Value: "/api",
+ }
+
+ rule, err := l7policies.CreateRule(client, policyID, createOpts).Extract()
+ if err != nil {
+ return rule, err
+ }
+
+ t.Logf("Successfully created l7 rule for policy %s", policyID)
+
+ if err := WaitForLoadBalancerState(client, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ return rule, fmt.Errorf("Timed out waiting for loadbalancer to become active")
+ }
+
+ return rule, nil
+}
+
+// DeleteL7Policy will delete a specified l7 policy. A fatal error will occur if
+// the l7 policy could not be deleted. This works best when used as a deferred
+// function.
+func DeleteL7Policy(t *testing.T, client *gophercloud.ServiceClient, lbID, policyID string) {
+ t.Logf("Attempting to delete l7 policy %s", policyID)
+
+ if err := l7policies.Delete(client, policyID).ExtractErr(); err != nil {
+ t.Fatalf("Unable to delete l7 policy: %v", err)
+ }
+
+ if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ t.Logf("Successfully deleted l7 policy %s", policyID)
+}
+
+// DeleteListener will delete a specified listener. A fatal error will occur if
+// the listener could not be deleted. This works best when used as a deferred
+// function.
+func DeleteListener(t *testing.T, client *gophercloud.ServiceClient, lbID, listenerID string) {
+ t.Logf("Attempting to delete listener %s", listenerID)
+
+ if err := listeners.Delete(client, listenerID).ExtractErr(); err != nil {
+ t.Fatalf("Unable to delete listener: %v", err)
+ }
+
+ if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ t.Logf("Successfully deleted listener %s", listenerID)
+}
+
+// DeleteMember will delete a specified member. A fatal error will occur if the
+// member could not be deleted. This works best when used as a deferred
+// function.
+func DeleteMember(t *testing.T, client *gophercloud.ServiceClient, lbID, poolID, memberID string) {
+ t.Logf("Attempting to delete member %s", memberID)
+
+ if err := pools.DeleteMember(client, poolID, memberID).ExtractErr(); err != nil {
+ t.Fatalf("Unable to delete member: %s", memberID)
+ }
+
+ if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ t.Logf("Successfully deleted member %s", memberID)
+}
+
+// DeleteLoadBalancer will delete a specified loadbalancer. A fatal error will
+// occur if the loadbalancer could not be deleted. This works best when used
+// as a deferred function.
+func DeleteLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, lbID string) {
+ t.Logf("Attempting to delete loadbalancer %s", lbID)
+
+ deleteOpts := loadbalancers.DeleteOpts{
+ Cascade: false,
+ }
+
+ if err := loadbalancers.Delete(client, lbID, deleteOpts).ExtractErr(); err != nil {
+ t.Fatalf("Unable to delete loadbalancer: %v", err)
+ }
+
+ t.Logf("Waiting for loadbalancer %s to delete", lbID)
+
+ if err := WaitForLoadBalancerState(client, lbID, "DELETED", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Loadbalancer did not delete in time.")
+ }
+
+ t.Logf("Successfully deleted loadbalancer %s", lbID)
+}
+
+// CascadeDeleteLoadBalancer will perform a cascading delete on a loadbalancer.
+// A fatal error will occur if the loadbalancer could not be deleted. This works
+// best when used as a deferred function.
+func CascadeDeleteLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, lbID string) {
+ t.Logf("Attempting to cascade delete loadbalancer %s", lbID)
+
+ deleteOpts := loadbalancers.DeleteOpts{
+ Cascade: true,
+ }
+
+ if err := loadbalancers.Delete(client, lbID, deleteOpts).ExtractErr(); err != nil {
+ t.Fatalf("Unable to cascade delete loadbalancer: %v", err)
+ }
+
+ t.Logf("Waiting for loadbalancer %s to cascade delete", lbID)
+
+ if err := WaitForLoadBalancerState(client, lbID, "DELETED", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Loadbalancer did not delete in time.")
+ }
+
+ t.Logf("Successfully deleted loadbalancer %s", lbID)
+}
+
+// DeleteMonitor will delete a specified monitor. A fatal error will occur if
+// the monitor could not be deleted. This works best when used as a deferred
+// function.
+func DeleteMonitor(t *testing.T, client *gophercloud.ServiceClient, lbID, monitorID string) {
+ t.Logf("Attempting to delete monitor %s", monitorID)
+
+ if err := monitors.Delete(client, monitorID).ExtractErr(); err != nil {
+ t.Fatalf("Unable to delete monitor: %v", err)
+ }
+
+ if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ t.Logf("Successfully deleted monitor %s", monitorID)
+}
+
+// DeletePool will delete a specified pool. A fatal error will occur if the
+// pool could not be deleted. This works best when used as a deferred function.
+func DeletePool(t *testing.T, client *gophercloud.ServiceClient, lbID, poolID string) {
+ t.Logf("Attempting to delete pool %s", poolID)
+
+ if err := pools.Delete(client, poolID).ExtractErr(); err != nil {
+ t.Fatalf("Unable to delete pool: %v", err)
+ }
+
+ if err := WaitForLoadBalancerState(client, lbID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ t.Logf("Successfully deleted pool %s", poolID)
+}
+
+// WaitForLoadBalancerState will wait until a loadbalancer reaches a given state.
+func WaitForLoadBalancerState(client *gophercloud.ServiceClient, lbID, status string, secs int) error {
+ return gophercloud.WaitFor(secs, func() (bool, error) {
+ current, err := loadbalancers.Get(client, lbID).Extract()
+ if err != nil {
+ if httpStatus, ok := err.(gophercloud.ErrDefault404); ok {
+ if httpStatus.Actual == 404 {
+ if status == "DELETED" {
+ return true, nil
+ }
+ }
+ }
+ return false, err
+ }
+
+ if current.ProvisioningStatus == status {
+ return true, nil
+ }
+
+ return false, nil
+ })
+}
diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go
new file mode 100644
index 0000000000..0149b19ea4
--- /dev/null
+++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go
@@ -0,0 +1,360 @@
+// +build acceptance networking loadbalancer loadbalancers
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools"
+)
+
+func TestLoadbalancersList(t *testing.T) {
+ client, err := clients.NewLoadBalancerV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a loadbalancer client: %v", err)
+ }
+
+ allPages, err := loadbalancers.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list loadbalancers: %v", err)
+ }
+
+ allLoadbalancers, err := loadbalancers.ExtractLoadBalancers(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract loadbalancers: %v", err)
+ }
+
+ for _, lb := range allLoadbalancers {
+ tools.PrintResource(t, lb)
+ }
+}
+
+func TestLoadbalancersCRUD(t *testing.T) {
+ netClient, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a networking client: %v", err)
+ }
+
+ lbClient, err := clients.NewLoadBalancerV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a loadbalancer client: %v", err)
+ }
+
+ network, err := networking.CreateNetwork(t, netClient)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer networking.DeleteNetwork(t, netClient, network.ID)
+
+ subnet, err := networking.CreateSubnet(t, netClient, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create subnet: %v", err)
+ }
+ defer networking.DeleteSubnet(t, netClient, subnet.ID)
+
+ lb, err := CreateLoadBalancer(t, lbClient, subnet.ID)
+ if err != nil {
+ t.Fatalf("Unable to create loadbalancer: %v", err)
+ }
+ defer DeleteLoadBalancer(t, lbClient, lb.ID)
+
+ newLB, err := loadbalancers.Get(lbClient, lb.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get loadbalancer: %v", err)
+ }
+
+ tools.PrintResource(t, newLB)
+
+ // Because of the time it takes to create a loadbalancer,
+ // this test will include some other resources.
+
+ // Listener
+ listener, err := CreateListener(t, lbClient, lb)
+ if err != nil {
+ t.Fatalf("Unable to create listener: %v", err)
+ }
+ defer DeleteListener(t, lbClient, lb.ID, listener.ID)
+
+ updateListenerOpts := listeners.UpdateOpts{
+ Description: "Some listener description",
+ }
+ _, err = listeners.Update(lbClient, listener.ID, updateListenerOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update listener")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newListener, err := listeners.Get(lbClient, listener.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get listener")
+ }
+
+ tools.PrintResource(t, newListener)
+
+ // L7 policy
+ policy, err := CreateL7Policy(t, lbClient, listener, lb)
+ if err != nil {
+ t.Fatalf("Unable to create l7 policy: %v", err)
+ }
+ defer DeleteL7Policy(t, lbClient, lb.ID, policy.ID)
+
+ newDescription := "New l7 policy description"
+ updateL7policyOpts := l7policies.UpdateOpts{
+ Description: &newDescription,
+ }
+ _, err = l7policies.Update(lbClient, policy.ID, updateL7policyOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update l7 policy")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newPolicy, err := l7policies.Get(lbClient, policy.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get l7 policy: %v", err)
+ }
+
+ tools.PrintResource(t, newPolicy)
+
+ // L7 rule
+ _, err = CreateL7Rule(t, lbClient, newPolicy.ID, lb)
+ if err != nil {
+ t.Fatalf("Unable to create l7 rule: %v", err)
+ }
+
+ // Pool
+ pool, err := CreatePool(t, lbClient, lb)
+ if err != nil {
+ t.Fatalf("Unable to create pool: %v", err)
+ }
+ defer DeletePool(t, lbClient, lb.ID, pool.ID)
+
+ updatePoolOpts := pools.UpdateOpts{
+ Description: "Some pool description",
+ }
+ _, err = pools.Update(lbClient, pool.ID, updatePoolOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update pool")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newPool, err := pools.Get(lbClient, pool.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get pool")
+ }
+
+ tools.PrintResource(t, newPool)
+
+ // Member
+ member, err := CreateMember(t, lbClient, lb, newPool, subnet.ID, subnet.CIDR)
+ if err != nil {
+ t.Fatalf("Unable to create member: %v", err)
+ }
+ defer DeleteMember(t, lbClient, lb.ID, pool.ID, member.ID)
+
+ newWeight := tools.RandomInt(11, 100)
+ updateMemberOpts := pools.UpdateMemberOpts{
+ Weight: newWeight,
+ }
+ _, err = pools.UpdateMember(lbClient, pool.ID, member.ID, updateMemberOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update pool")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newMember, err := pools.GetMember(lbClient, pool.ID, member.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get member")
+ }
+
+ tools.PrintResource(t, newMember)
+
+ // Monitor
+ monitor, err := CreateMonitor(t, lbClient, lb, newPool)
+ if err != nil {
+ t.Fatalf("Unable to create monitor: %v", err)
+ }
+ defer DeleteMonitor(t, lbClient, lb.ID, monitor.ID)
+
+ newDelay := tools.RandomInt(20, 30)
+ updateMonitorOpts := monitors.UpdateOpts{
+ Delay: newDelay,
+ }
+ _, err = monitors.Update(lbClient, monitor.ID, updateMonitorOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update monitor")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newMonitor, err := monitors.Get(lbClient, monitor.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get monitor")
+ }
+
+ tools.PrintResource(t, newMonitor)
+
+}
+
+func TestLoadbalancersCascadeCRUD(t *testing.T) {
+ netClient, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a networking client: %v", err)
+ }
+
+ lbClient, err := clients.NewLoadBalancerV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a loadbalancer client: %v", err)
+ }
+
+ network, err := networking.CreateNetwork(t, netClient)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer networking.DeleteNetwork(t, netClient, network.ID)
+
+ subnet, err := networking.CreateSubnet(t, netClient, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create subnet: %v", err)
+ }
+ defer networking.DeleteSubnet(t, netClient, subnet.ID)
+
+ lb, err := CreateLoadBalancer(t, lbClient, subnet.ID)
+ if err != nil {
+ t.Fatalf("Unable to create loadbalancer: %v", err)
+ }
+ defer CascadeDeleteLoadBalancer(t, lbClient, lb.ID)
+
+ newLB, err := loadbalancers.Get(lbClient, lb.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get loadbalancer: %v", err)
+ }
+
+ tools.PrintResource(t, newLB)
+
+ // Because of the time it takes to create a loadbalancer,
+ // this test will include some other resources.
+
+ // Listener
+ listener, err := CreateListener(t, lbClient, lb)
+ if err != nil {
+ t.Fatalf("Unable to create listener: %v", err)
+ }
+
+ updateListenerOpts := listeners.UpdateOpts{
+ Description: "Some listener description",
+ }
+ _, err = listeners.Update(lbClient, listener.ID, updateListenerOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update listener")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newListener, err := listeners.Get(lbClient, listener.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get listener")
+ }
+
+ tools.PrintResource(t, newListener)
+
+ // Pool
+ pool, err := CreatePool(t, lbClient, lb)
+ if err != nil {
+ t.Fatalf("Unable to create pool: %v", err)
+ }
+
+ updatePoolOpts := pools.UpdateOpts{
+ Description: "Some pool description",
+ }
+ _, err = pools.Update(lbClient, pool.ID, updatePoolOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update pool")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newPool, err := pools.Get(lbClient, pool.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get pool")
+ }
+
+ tools.PrintResource(t, newPool)
+
+ // Member
+ member, err := CreateMember(t, lbClient, lb, newPool, subnet.ID, subnet.CIDR)
+ if err != nil {
+ t.Fatalf("Unable to create member: %v", err)
+ }
+
+ newWeight := tools.RandomInt(11, 100)
+ updateMemberOpts := pools.UpdateMemberOpts{
+ Weight: newWeight,
+ }
+ _, err = pools.UpdateMember(lbClient, pool.ID, member.ID, updateMemberOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update pool")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newMember, err := pools.GetMember(lbClient, pool.ID, member.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get member")
+ }
+
+ tools.PrintResource(t, newMember)
+
+ // Monitor
+ monitor, err := CreateMonitor(t, lbClient, lb, newPool)
+ if err != nil {
+ t.Fatalf("Unable to create monitor: %v", err)
+ }
+
+ newDelay := tools.RandomInt(20, 30)
+ updateMonitorOpts := monitors.UpdateOpts{
+ Delay: newDelay,
+ }
+ _, err = monitors.Update(lbClient, monitor.ID, updateMonitorOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update monitor")
+ }
+
+ if err := WaitForLoadBalancerState(lbClient, lb.ID, "ACTIVE", loadbalancerActiveTimeoutSeconds); err != nil {
+ t.Fatalf("Timed out waiting for loadbalancer to become active")
+ }
+
+ newMonitor, err := monitors.Get(lbClient, monitor.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get monitor")
+ }
+
+ tools.PrintResource(t, newMonitor)
+
+}
diff --git a/acceptance/openstack/loadbalancer/v2/monitors_test.go b/acceptance/openstack/loadbalancer/v2/monitors_test.go
new file mode 100644
index 0000000000..93688fdb2e
--- /dev/null
+++ b/acceptance/openstack/loadbalancer/v2/monitors_test.go
@@ -0,0 +1,32 @@
+// +build acceptance networking loadbalancer monitors
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
+)
+
+func TestMonitorsList(t *testing.T) {
+ client, err := clients.NewLoadBalancerV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a loadbalancer client: %v", err)
+ }
+
+ allPages, err := monitors.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list monitors: %v", err)
+ }
+
+ allMonitors, err := monitors.ExtractMonitors(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract monitors: %v", err)
+ }
+
+ for _, monitor := range allMonitors {
+ tools.PrintResource(t, monitor)
+ }
+}
diff --git a/acceptance/openstack/loadbalancer/v2/pkg.go b/acceptance/openstack/loadbalancer/v2/pkg.go
new file mode 100644
index 0000000000..5ec3cc8e83
--- /dev/null
+++ b/acceptance/openstack/loadbalancer/v2/pkg.go
@@ -0,0 +1 @@
+package v2
diff --git a/acceptance/openstack/loadbalancer/v2/pools_test.go b/acceptance/openstack/loadbalancer/v2/pools_test.go
new file mode 100644
index 0000000000..f7ec2a4ac4
--- /dev/null
+++ b/acceptance/openstack/loadbalancer/v2/pools_test.go
@@ -0,0 +1,32 @@
+// +build acceptance networking loadbalancer pools
+
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools"
+)
+
+func TestPoolsList(t *testing.T) {
+ client, err := clients.NewLoadBalancerV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a loadbalancer client: %v", err)
+ }
+
+ allPages, err := pools.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list pools: %v", err)
+ }
+
+ allPools, err := pools.ExtractPools(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract pools: %v", err)
+ }
+
+ for _, pool := range allPools {
+ tools.PrintResource(t, pool)
+ }
+}
diff --git a/acceptance/openstack/messaging/v2/messaging.go b/acceptance/openstack/messaging/v2/messaging.go
new file mode 100644
index 0000000000..d3b2480aab
--- /dev/null
+++ b/acceptance/openstack/messaging/v2/messaging.go
@@ -0,0 +1,54 @@
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/messaging/v2/queues"
+)
+
+func CreateQueue(t *testing.T, client *gophercloud.ServiceClient) (string, error) {
+ queueName := tools.RandomString("ACPTTEST", 5)
+
+ t.Logf("Attempting to create Queue: %s", queueName)
+
+ createOpts := queues.CreateOpts{
+ QueueName: queueName,
+ MaxMessagesPostSize: 262143,
+ DefaultMessageTTL: 3700,
+ DefaultMessageDelay: 25,
+ DeadLetterQueueMessagesTTL: 3500,
+ MaxClaimCount: 10,
+ Extra: map[string]interface{}{"description": "Test Queue for Gophercloud acceptance tests."},
+ }
+
+ createErr := queues.Create(client, createOpts).ExtractErr()
+ if createErr != nil {
+ t.Fatalf("Unable to create Queue: %v", createErr)
+ }
+
+ GetQueue(t, client, queueName)
+
+ t.Logf("Created Queue: %s", queueName)
+ return queueName, nil
+}
+
+func DeleteQueue(t *testing.T, client *gophercloud.ServiceClient, queueName string) {
+ t.Logf("Attempting to delete Queue: %s", queueName)
+ err := queues.Delete(client, queueName).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete Queue %s: %v", queueName, err)
+ }
+
+ t.Logf("Deleted Queue: %s", queueName)
+}
+
+func GetQueue(t *testing.T, client *gophercloud.ServiceClient, queueName string) (queues.QueueDetails, error) {
+ t.Logf("Attempting to get Queue: %s", queueName)
+ queue, err := queues.Get(client, queueName).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get Queue %s: %v", queueName, err)
+ }
+ return queue, nil
+}
diff --git a/acceptance/openstack/messaging/v2/queue_test.go b/acceptance/openstack/messaging/v2/queue_test.go
new file mode 100644
index 0000000000..5ab131847b
--- /dev/null
+++ b/acceptance/openstack/messaging/v2/queue_test.go
@@ -0,0 +1,86 @@
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/messaging/v2/queues"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+func TestCRUDQueues(t *testing.T) {
+ clientID := "3381af92-2b9e-11e3-b191-71861300734d"
+
+ client, err := clients.NewMessagingV2Client(clientID)
+ if err != nil {
+ t.Fatalf("Unable to create a messaging service client: %v", err)
+ }
+
+ createdQueueName, err := CreateQueue(t, client)
+ defer DeleteQueue(t, client, createdQueueName)
+
+ createdQueue, err := queues.Get(client, createdQueueName).Extract()
+
+ tools.PrintResource(t, createdQueue)
+ tools.PrintResource(t, createdQueue.Extra)
+
+ updateOpts := queues.BatchUpdateOpts{
+ queues.UpdateOpts{
+ Op: "replace",
+ Path: "/metadata/_max_claim_count",
+ Value: 15,
+ },
+ queues.UpdateOpts{
+ Op: "replace",
+ Path: "/metadata/description",
+ Value: "Updated description for queues acceptance test.",
+ },
+ }
+
+ t.Logf("Attempting to update Queue: %s", createdQueueName)
+ updateResult, updateErr := queues.Update(client, createdQueueName, updateOpts).Extract()
+ if updateErr != nil {
+ t.Fatalf("Unable to update Queue %s: %v", createdQueueName, updateErr)
+ }
+
+ updatedQueue, err := GetQueue(t, client, createdQueueName)
+
+ tools.PrintResource(t, updateResult)
+ tools.PrintResource(t, updatedQueue)
+ tools.PrintResource(t, updatedQueue.Extra)
+}
+
+func TestListQueues(t *testing.T) {
+ clientID := "3381af92-2b9e-11e3-b191-71861300734d"
+
+ client, err := clients.NewMessagingV2Client(clientID)
+ if err != nil {
+ t.Fatalf("Unable to create a messaging service client: %v", err)
+ }
+
+ firstQueueName, err := CreateQueue(t, client)
+ defer DeleteQueue(t, client, firstQueueName)
+
+ secondQueueName, err := CreateQueue(t, client)
+ defer DeleteQueue(t, client, secondQueueName)
+
+ listOpts := queues.ListOpts{
+ Limit: 10,
+ Detailed: true,
+ }
+
+ pager := queues.List(client, listOpts)
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ allQueues, err := queues.ExtractQueues(page)
+ if err != nil {
+ t.Fatalf("Unable to extract Queues: %v", err)
+ }
+
+ for _, queue := range allQueues {
+ tools.PrintResource(t, queue)
+ }
+
+ return true, nil
+ })
+}
diff --git a/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go b/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go
index 351020410e..b38d7283ca 100644
--- a/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go
+++ b/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go
@@ -3,6 +3,7 @@
package layer3
import (
+ "os"
"testing"
"github.com/gophercloud/gophercloud/acceptance/clients"
@@ -10,6 +11,7 @@ import (
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
)
func TestLayer3FloatingIPsList(t *testing.T) {
@@ -98,3 +100,48 @@ func TestLayer3FloatingIPsCreateDelete(t *testing.T) {
t.Fatalf("Unable to disassociate floating IP: %v", err)
}
}
+
+func TestLayer3FloatingIPsCreateDeleteBySubnetID(t *testing.T) {
+ username := os.Getenv("OS_USERNAME")
+ if username != "admin" {
+ t.Skip("must be admin to run this test")
+ }
+
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a compute client: %v", err)
+ }
+
+ choices, err := clients.AcceptanceTestChoicesFromEnv()
+ if err != nil {
+ t.Fatalf("Unable to get choices: %v", err)
+ }
+
+ listOpts := subnets.ListOpts{
+ NetworkID: choices.ExternalNetworkID,
+ }
+
+ subnetPages, err := subnets.List(client, listOpts).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list subnets: %v", err)
+ }
+
+ allSubnets, err := subnets.ExtractSubnets(subnetPages)
+ if err != nil {
+ t.Fatalf("Unable to extract subnets: %v", err)
+ }
+
+ createOpts := floatingips.CreateOpts{
+ FloatingNetworkID: choices.ExternalNetworkID,
+ SubnetID: allSubnets[0].ID,
+ }
+
+ fip, err := floatingips.Create(client, createOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to create floating IP: %v")
+ }
+
+ tools.PrintResource(t, fip)
+
+ DeleteFloatingIP(t, client, fip.ID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/rbacpolicies/pkg.go b/acceptance/openstack/networking/v2/extensions/rbacpolicies/pkg.go
new file mode 100644
index 0000000000..f682aeab06
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/pkg.go
@@ -0,0 +1 @@
+package rbacpolicies
diff --git a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go
new file mode 100644
index 0000000000..46903dff78
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go
@@ -0,0 +1,43 @@
+package rbacpolicies
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/rbacpolicies"
+)
+
+// CreateRBACPolicy will create a rbac-policy. An error will be returned if the
+// rbac-policy could not be created.
+func CreateRBACPolicy(t *testing.T, client *gophercloud.ServiceClient, tenantID, networkID string) (*rbacpolicies.RBACPolicy, error) {
+ createOpts := rbacpolicies.CreateOpts{
+ Action: rbacpolicies.ActionAccessShared,
+ ObjectType: "network",
+ TargetTenant: tenantID,
+ ObjectID: networkID,
+ }
+
+ t.Logf("Trying to create rbac_policy")
+
+ rbacPolicy, err := rbacpolicies.Create(client, createOpts).Extract()
+ if err != nil {
+ return rbacPolicy, err
+ }
+
+ t.Logf("Successfully created rbac_policy")
+ return rbacPolicy, nil
+}
+
+// DeleteRBACPolicy will delete a rbac-policy with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeleteRBACPolicy(t *testing.T, client *gophercloud.ServiceClient, rbacPolicyID string) {
+ t.Logf("Trying to delete rbac_policy: %s", rbacPolicyID)
+
+ err := rbacpolicies.Delete(client, rbacPolicyID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete rbac_policy %s: %v", rbacPolicyID, err)
+ }
+
+ t.Logf("Deleted rbac_policy: %s", rbacPolicyID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go
new file mode 100644
index 0000000000..db838d2816
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go
@@ -0,0 +1,113 @@
+// +build acceptance
+
+package rbacpolicies
+
+import (
+ "os"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ projects "github.com/gophercloud/gophercloud/acceptance/openstack/identity/v3"
+ networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/rbacpolicies"
+)
+
+func TestRBACPolicyCRUD(t *testing.T) {
+ username := os.Getenv("OS_USERNAME")
+ if username != "admin" {
+ t.Skip("must be admin to run this test")
+ }
+
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create a network
+ network, err := networking.CreateNetwork(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer networking.DeleteNetwork(t, client, network.ID)
+
+ tools.PrintResource(t, network)
+
+ identityClient, err := clients.NewIdentityV3Client()
+ if err != nil {
+ t.Fatalf("Unable to obtain an identity client: %v")
+ }
+
+ // Create a project/tenant
+ project, err := projects.CreateProject(t, identityClient, nil)
+ if err != nil {
+ t.Fatalf("Unable to create project: %v", err)
+ }
+ defer projects.DeleteProject(t, identityClient, project.ID)
+
+ tools.PrintResource(t, project)
+
+ // Create a rbac-policy
+ rbacPolicy, err := CreateRBACPolicy(t, client, project.ID, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create rbac-policy: %v", err)
+ }
+ defer DeleteRBACPolicy(t, client, rbacPolicy.ID)
+
+ tools.PrintResource(t, rbacPolicy)
+
+ // Create another project/tenant for rbac-update
+ project2, err := projects.CreateProject(t, identityClient, nil)
+ if err != nil {
+ t.Fatalf("Unable to create project2: %v", err)
+ }
+ defer projects.DeleteProject(t, identityClient, project2.ID)
+
+ tools.PrintResource(t, project2)
+
+ // Update a rbac-policy
+ updateOpts := rbacpolicies.UpdateOpts{
+ TargetTenant: project2.ID,
+ }
+
+ _, err = rbacpolicies.Update(client, rbacPolicy.ID, updateOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update rbac-policy: %v", err)
+ }
+
+ // Get the rbac-policy by ID
+ t.Logf("Get rbac_policy by ID")
+ newrbacPolicy, err := rbacpolicies.Get(client, rbacPolicy.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to retrieve rbac policy: %v", err)
+ }
+
+ tools.PrintResource(t, newrbacPolicy)
+}
+
+func TestRBACPolicyList(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ type rbacPolicy struct {
+ rbacpolicies.RBACPolicy
+ }
+
+ var allRBACPolicies []rbacPolicy
+
+ allPages, err := rbacpolicies.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list rbac policies: %v", err)
+ }
+
+ err = rbacpolicies.ExtractRBACPolicesInto(allPages, &allRBACPolicies)
+ if err != nil {
+ t.Fatalf("Unable to extract rbac policies: %v", err)
+ }
+
+ for _, rbacpolicy := range allRBACPolicies {
+ tools.PrintResource(t, rbacpolicy)
+ }
+}
diff --git a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go
new file mode 100644
index 0000000000..fdf6318d52
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go
@@ -0,0 +1,45 @@
+package v2
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools"
+)
+
+// CreateSubnetPool will create a subnetpool. An error will be returned if the
+// subnetpool could not be created.
+func CreateSubnetPool(t *testing.T, client *gophercloud.ServiceClient) (*subnetpools.SubnetPool, error) {
+ subnetPoolName := tools.RandomString("TESTACC-", 8)
+ subnetPoolPrefixes := []string{
+ "10.0.0.0/8",
+ }
+ createOpts := subnetpools.CreateOpts{
+ Name: subnetPoolName,
+ Prefixes: subnetPoolPrefixes,
+ }
+
+ t.Logf("Attempting to create a subnetpool: %s", subnetPoolName)
+
+ subnetPool, err := subnetpools.Create(client, createOpts).Extract()
+ if err != nil {
+ return nil, err
+ }
+
+ t.Logf("Successfully created the subnetpool.")
+ return subnetPool, nil
+}
+
+// DeleteSubnetPool will delete a subnetpool with a specified ID.
+// A fatal error will occur if the delete was not successful.
+func DeleteSubnetPool(t *testing.T, client *gophercloud.ServiceClient, subnetPoolID string) {
+ t.Logf("Attempting to delete the subnetpool: %s", subnetPoolID)
+
+ err := subnetpools.Delete(client, subnetPoolID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete subnetpool %s: %v", subnetPoolID, err)
+ }
+
+ t.Logf("Deleted subnetpool: %s", subnetPoolID)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go
index 9884a70a9c..bdef6e6794 100644
--- a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go
+++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go
@@ -10,6 +10,39 @@ import (
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools"
)
+func TestSubnetPoolsCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create a subnetpool
+ subnetPool, err := CreateSubnetPool(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create a subnetpool: %v", err)
+ }
+ defer DeleteSubnetPool(t, client, subnetPool.ID)
+
+ tools.PrintResource(t, subnetPool)
+
+ newName := tools.RandomString("TESTACC-", 8)
+ updateOpts := &subnetpools.UpdateOpts{
+ Name: newName,
+ }
+
+ _, err = subnetpools.Update(client, subnetPool.ID, updateOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update the subnetpool: %v", err)
+ }
+
+ newSubnetPool, err := subnetpools.Get(client, subnetPool.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get subnetpool: %v", err)
+ }
+
+ tools.PrintResource(t, newSubnetPool)
+}
+
func TestSubnetPoolsList(t *testing.T) {
client, err := clients.NewNetworkV2Client()
if err != nil {
diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go
new file mode 100644
index 0000000000..853065d219
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go
@@ -0,0 +1,65 @@
+// +build acceptance networking vpnaas
+
+package vpnaas
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/endpointgroups"
+)
+
+func TestGroupList(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ allPages, err := endpointgroups.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list endpoint groups: %v", err)
+ }
+
+ allGroups, err := endpointgroups.ExtractEndpointGroups(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract endpoint groups: %v", err)
+ }
+
+ for _, group := range allGroups {
+ tools.PrintResource(t, group)
+ }
+}
+
+func TestGroupCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ group, err := CreateEndpointGroup(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create Endpoint group: %v", err)
+ }
+ defer DeleteEndpointGroup(t, client, group.ID)
+ tools.PrintResource(t, group)
+
+ newGroup, err := endpointgroups.Get(client, group.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to retrieve Endpoint group: %v", err)
+ }
+ tools.PrintResource(t, newGroup)
+
+ updatedName := "updatedname"
+ updatedDescription := "updated description"
+ updateOpts := endpointgroups.UpdateOpts{
+ Name: &updatedName,
+ Description: &updatedDescription,
+ }
+ updatedGroup, err := endpointgroups.Update(client, group.ID, updateOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update endpoint group: %v", err)
+ }
+ tools.PrintResource(t, updatedGroup)
+
+}
diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go
new file mode 100644
index 0000000000..2efa1e1b65
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go
@@ -0,0 +1,69 @@
+// +build acceptance networking vpnaas
+
+package vpnaas
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ikepolicies"
+)
+
+func TestIKEPolicyList(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ allPages, err := ikepolicies.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list IKE policies: %v", err)
+ }
+
+ allPolicies, err := ikepolicies.ExtractPolicies(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract IKE policies: %v", err)
+ }
+
+ for _, policy := range allPolicies {
+ tools.PrintResource(t, policy)
+ }
+}
+
+func TestIKEPolicyCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ policy, err := CreateIKEPolicy(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create IKE policy: %v", err)
+ }
+ defer DeleteIKEPolicy(t, client, policy.ID)
+
+ tools.PrintResource(t, policy)
+
+ newPolicy, err := ikepolicies.Get(client, policy.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get IKE policy: %v", err)
+ }
+ tools.PrintResource(t, newPolicy)
+
+ updatedName := "updatedname"
+ updatedDescription := "updated policy"
+ updateOpts := ikepolicies.UpdateOpts{
+ Name: &updatedName,
+ Description: &updatedDescription,
+ Lifetime: &ikepolicies.LifetimeUpdateOpts{
+ Value: 7000,
+ },
+ }
+ updatedPolicy, err := ikepolicies.Update(client, policy.ID, updateOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update IKE policy: %v", err)
+ }
+ tools.PrintResource(t, updatedPolicy)
+
+}
diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go
new file mode 100644
index 0000000000..2fdee7dd92
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go
@@ -0,0 +1,63 @@
+// +build acceptance networking vpnaas
+
+package vpnaas
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies"
+)
+
+func TestIPSecPolicyList(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ allPages, err := ipsecpolicies.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list IPSec policies: %v", err)
+ }
+
+ allPolicies, err := ipsecpolicies.ExtractPolicies(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract policies: %v", err)
+ }
+
+ for _, policy := range allPolicies {
+ tools.PrintResource(t, policy)
+ }
+}
+
+func TestIPSecPolicyCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ policy, err := CreateIPSecPolicy(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create IPSec policy: %v", err)
+ }
+ defer DeleteIPSecPolicy(t, client, policy.ID)
+ tools.PrintResource(t, policy)
+
+ updatedDescription := "Updated policy description"
+ updateOpts := ipsecpolicies.UpdateOpts{
+ Description: &updatedDescription,
+ }
+
+ policy, err = ipsecpolicies.Update(client, policy.ID, updateOpts).Extract()
+ if err != nil {
+ t.Fatalf("Unable to update IPSec policy: %v", err)
+ }
+ tools.PrintResource(t, policy)
+
+ newPolicy, err := ipsecpolicies.Get(client, policy.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get IPSec policy: %v", err)
+ }
+ tools.PrintResource(t, newPolicy)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go
new file mode 100644
index 0000000000..f88aa7611d
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go
@@ -0,0 +1,60 @@
+// +build acceptance networking fwaas
+
+package vpnaas
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ layer3 "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/services"
+)
+
+func TestServiceList(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ allPages, err := services.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list services: %v", err)
+ }
+
+ allServices, err := services.ExtractServices(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract services: %v", err)
+ }
+
+ for _, service := range allServices {
+ tools.PrintResource(t, service)
+ }
+}
+
+func TestServiceCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ router, err := layer3.CreateExternalRouter(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create router: %v", err)
+ }
+ defer layer3.DeleteRouter(t, client, router.ID)
+
+ service, err := CreateService(t, client, router.ID)
+ if err != nil {
+ t.Fatalf("Unable to create service: %v", err)
+ }
+ defer DeleteService(t, client, service.ID)
+
+ newService, err := services.Get(client, service.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get service: %v", err)
+ }
+
+ tools.PrintResource(t, service)
+ tools.PrintResource(t, newService)
+}
diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go
new file mode 100644
index 0000000000..5bf7560747
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go
@@ -0,0 +1,125 @@
+// +build acceptance networking vpnaas
+
+package vpnaas
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/acceptance/clients"
+ networks "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2"
+ layer3 "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/layer3"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
+
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/siteconnections"
+)
+
+func TestConnectionList(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ allPages, err := siteconnections.List(client, nil).AllPages()
+ if err != nil {
+ t.Fatalf("Unable to list IPSec site connections: %v", err)
+ }
+
+ allConnections, err := siteconnections.ExtractConnections(allPages)
+ if err != nil {
+ t.Fatalf("Unable to extract IPSec site connections: %v", err)
+ }
+
+ for _, connection := range allConnections {
+ tools.PrintResource(t, connection)
+ }
+}
+
+func TestConnectionCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create Network
+ network, err := networks.CreateNetwork(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer networks.DeleteNetwork(t, client, network.ID)
+
+ // Create Subnet
+ subnet, err := networks.CreateSubnet(t, client, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create subnet: %v", err)
+ }
+ defer networks.DeleteSubnet(t, client, subnet.ID)
+
+ router, err := layer3.CreateExternalRouter(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create router: %v", err)
+ }
+ defer layer3.DeleteRouter(t, client, router.ID)
+
+ // Link router and subnet
+ aiOpts := routers.AddInterfaceOpts{
+ SubnetID: subnet.ID,
+ }
+
+ _, err = routers.AddInterface(client, router.ID, aiOpts).Extract()
+ if err != nil {
+ t.Fatalf("Failed to add interface to router: %v", err)
+ }
+ defer func() {
+ riOpts := routers.RemoveInterfaceOpts{
+ SubnetID: subnet.ID,
+ }
+ routers.RemoveInterface(client, router.ID, riOpts)
+ }()
+
+ // Create all needed resources for the connection
+ service, err := CreateService(t, client, router.ID)
+ if err != nil {
+ t.Fatalf("Unable to create service: %v", err)
+ }
+ defer DeleteService(t, client, service.ID)
+
+ ikepolicy, err := CreateIKEPolicy(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create IKE policy: %v", err)
+ }
+ defer DeleteIKEPolicy(t, client, ikepolicy.ID)
+
+ ipsecpolicy, err := CreateIPSecPolicy(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create IPSec Policy: %v", err)
+ }
+ defer DeleteIPSecPolicy(t, client, ipsecpolicy.ID)
+
+ peerEPGroup, err := CreateEndpointGroup(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create Endpoint Group with CIDR endpoints: %v", err)
+ }
+ defer DeleteEndpointGroup(t, client, peerEPGroup.ID)
+
+ localEPGroup, err := CreateEndpointGroupWithSubnet(t, client, subnet.ID)
+ if err != nil {
+ t.Fatalf("Unable to create Endpoint Group with subnet endpoints: %v", err)
+ }
+ defer DeleteEndpointGroup(t, client, localEPGroup.ID)
+
+ conn, err := CreateSiteConnection(t, client, ikepolicy.ID, ipsecpolicy.ID, service.ID, peerEPGroup.ID, localEPGroup.ID)
+ if err != nil {
+ t.Fatalf("Unable to create IPSec Site Connection: %v", err)
+ }
+ defer DeleteSiteConnection(t, client, conn.ID)
+
+ newConnection, err := siteconnections.Get(client, conn.ID).Extract()
+ if err != nil {
+ t.Fatalf("Unable to get connection: %v", err)
+ }
+
+ tools.PrintResource(t, conn)
+ tools.PrintResource(t, newConnection)
+
+}
diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go
new file mode 100644
index 0000000000..2194a4c70e
--- /dev/null
+++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go
@@ -0,0 +1,258 @@
+package vpnaas
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/endpointgroups"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ikepolicies"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/services"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/siteconnections"
+)
+
+// CreateService will create a Service with a random name and a specified router ID
+// An error will be returned if the service could not be created.
+func CreateService(t *testing.T, client *gophercloud.ServiceClient, routerID string) (*services.Service, error) {
+ serviceName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create service %s", serviceName)
+
+ iTrue := true
+ createOpts := services.CreateOpts{
+ Name: serviceName,
+ AdminStateUp: &iTrue,
+ RouterID: routerID,
+ }
+ service, err := services.Create(client, createOpts).Extract()
+ if err != nil {
+ return service, err
+ }
+
+ t.Logf("Successfully created service %s", serviceName)
+
+ return service, nil
+}
+
+// DeleteService will delete a service with a specified ID. A fatal error
+// will occur if the delete was not successful. This works best when used as
+// a deferred function.
+func DeleteService(t *testing.T, client *gophercloud.ServiceClient, serviceID string) {
+ t.Logf("Attempting to delete service: %s", serviceID)
+
+ err := services.Delete(client, serviceID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete service %s: %v", serviceID, err)
+ }
+
+ t.Logf("Service deleted: %s", serviceID)
+}
+
+// CreateIPSecPolicy will create an IPSec Policy with a random name and given
+// rule. An error will be returned if the rule could not be created.
+func CreateIPSecPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ipsecpolicies.Policy, error) {
+ policyName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create IPSec policy %s", policyName)
+
+ createOpts := ipsecpolicies.CreateOpts{
+ Name: policyName,
+ }
+
+ policy, err := ipsecpolicies.Create(client, createOpts).Extract()
+ if err != nil {
+ return policy, err
+ }
+
+ t.Logf("Successfully created IPSec policy %s", policyName)
+
+ return policy, nil
+}
+
+// CreateIKEPolicy will create an IKE Policy with a random name and given
+// rule. An error will be returned if the policy could not be created.
+func CreateIKEPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ikepolicies.Policy, error) {
+ policyName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create IKE policy %s", policyName)
+
+ createOpts := ikepolicies.CreateOpts{
+ Name: policyName,
+ EncryptionAlgorithm: ikepolicies.EncryptionAlgorithm3DES,
+ PFS: ikepolicies.PFSGroup5,
+ }
+
+ policy, err := ikepolicies.Create(client, createOpts).Extract()
+ if err != nil {
+ return policy, err
+ }
+
+ t.Logf("Successfully created IKE policy %s", policyName)
+
+ return policy, nil
+}
+
+// DeleteIPSecPolicy will delete an IPSec policy with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeleteIPSecPolicy(t *testing.T, client *gophercloud.ServiceClient, policyID string) {
+ t.Logf("Attempting to delete IPSec policy: %s", policyID)
+
+ err := ipsecpolicies.Delete(client, policyID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete IPSec policy %s: %v", policyID, err)
+ }
+
+ t.Logf("Deleted IPSec policy: %s", policyID)
+}
+
+// DeleteIKEPolicy will delete an IKE policy with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeleteIKEPolicy(t *testing.T, client *gophercloud.ServiceClient, policyID string) {
+ t.Logf("Attempting to delete policy: %s", policyID)
+
+ err := ikepolicies.Delete(client, policyID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete IKE policy %s: %v", policyID, err)
+ }
+
+ t.Logf("Deleted IKE policy: %s", policyID)
+}
+
+// CreateEndpointGroup will create an endpoint group with a random name.
+// An error will be returned if the group could not be created.
+func CreateEndpointGroup(t *testing.T, client *gophercloud.ServiceClient) (*endpointgroups.EndpointGroup, error) {
+ groupName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create group %s", groupName)
+
+ createOpts := endpointgroups.CreateOpts{
+ Name: groupName,
+ Type: endpointgroups.TypeCIDR,
+ Endpoints: []string{
+ "10.2.0.0/24",
+ "10.3.0.0/24",
+ },
+ }
+ group, err := endpointgroups.Create(client, createOpts).Extract()
+ if err != nil {
+ return group, err
+ }
+
+ t.Logf("Successfully created group %s", groupName)
+
+ return group, nil
+}
+
+// CreateEndpointGroupWithCIDR will create an endpoint group with a random name and a specified CIDR.
+// An error will be returned if the group could not be created.
+func CreateEndpointGroupWithCIDR(t *testing.T, client *gophercloud.ServiceClient, cidr string) (*endpointgroups.EndpointGroup, error) {
+ groupName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create group %s", groupName)
+
+ createOpts := endpointgroups.CreateOpts{
+ Name: groupName,
+ Type: endpointgroups.TypeCIDR,
+ Endpoints: []string{
+ cidr,
+ },
+ }
+ group, err := endpointgroups.Create(client, createOpts).Extract()
+ if err != nil {
+ return group, err
+ }
+
+ t.Logf("Successfully created group %s", groupName)
+ t.Logf("%v", group)
+
+ return group, nil
+}
+
+// DeleteEndpointGroup will delete an Endpoint group with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeleteEndpointGroup(t *testing.T, client *gophercloud.ServiceClient, epGroupID string) {
+ t.Logf("Attempting to delete endpoint group: %s", epGroupID)
+
+ err := endpointgroups.Delete(client, epGroupID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete endpoint group %s: %v", epGroupID, err)
+ }
+
+ t.Logf("Deleted endpoint group: %s", epGroupID)
+
+}
+
+// CreateEndpointGroupWithSubnet will create an endpoint group with a random name.
+// An error will be returned if the group could not be created.
+func CreateEndpointGroupWithSubnet(t *testing.T, client *gophercloud.ServiceClient, subnetID string) (*endpointgroups.EndpointGroup, error) {
+ groupName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create group %s", groupName)
+
+ createOpts := endpointgroups.CreateOpts{
+ Name: groupName,
+ Type: endpointgroups.TypeSubnet,
+ Endpoints: []string{
+ subnetID,
+ },
+ }
+ group, err := endpointgroups.Create(client, createOpts).Extract()
+ if err != nil {
+ return group, err
+ }
+
+ t.Logf("Successfully created group %s", groupName)
+
+ return group, nil
+}
+
+// CreateSiteConnection will create an IPSec site connection with a random name and specified
+// IKE policy, IPSec policy, service, peer EP group and local EP Group.
+// An error will be returned if the connection could not be created.
+func CreateSiteConnection(t *testing.T, client *gophercloud.ServiceClient, ikepolicyID string, ipsecpolicyID string, serviceID string, peerEPGroupID string, localEPGroupID string) (*siteconnections.Connection, error) {
+ connectionName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create IPSec site connection %s", connectionName)
+
+ createOpts := siteconnections.CreateOpts{
+ Name: connectionName,
+ PSK: "secret",
+ Initiator: siteconnections.InitiatorBiDirectional,
+ AdminStateUp: gophercloud.Enabled,
+ IPSecPolicyID: ipsecpolicyID,
+ PeerEPGroupID: peerEPGroupID,
+ IKEPolicyID: ikepolicyID,
+ VPNServiceID: serviceID,
+ LocalEPGroupID: localEPGroupID,
+ PeerAddress: "172.24.4.233",
+ PeerID: "172.24.4.233",
+ MTU: 1500,
+ }
+ connection, err := siteconnections.Create(client, createOpts).Extract()
+ if err != nil {
+ return connection, err
+ }
+
+ t.Logf("Successfully created IPSec Site Connection %s", connectionName)
+
+ return connection, nil
+}
+
+// DeleteSiteConnection will delete an IPSec site connection with a specified ID. A fatal error will
+// occur if the delete was not successful. This works best when used as a
+// deferred function.
+func DeleteSiteConnection(t *testing.T, client *gophercloud.ServiceClient, siteConnectionID string) {
+ t.Logf("Attempting to delete site connection: %s", siteConnectionID)
+
+ err := siteconnections.Delete(client, siteConnectionID).ExtractErr()
+ if err != nil {
+ t.Fatalf("Unable to delete site connection %s: %v", siteConnectionID, err)
+ }
+
+ t.Logf("Deleted site connection: %s", siteConnectionID)
+
+}
diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go
index bc463dde93..b5dcb6d2c7 100644
--- a/acceptance/openstack/networking/v2/networking.go
+++ b/acceptance/openstack/networking/v2/networking.go
@@ -6,6 +6,8 @@ import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/extradhcpopts"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsecurity"
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
@@ -31,6 +33,32 @@ func CreateNetwork(t *testing.T, client *gophercloud.ServiceClient) (*networks.N
return network, nil
}
+// CreateNetworkWithoutPortSecurity will create a network without port security.
+// An error will be returned if the network could not be created.
+func CreateNetworkWithoutPortSecurity(t *testing.T, client *gophercloud.ServiceClient) (*networks.Network, error) {
+ networkName := tools.RandomString("TESTACC-", 8)
+ networkCreateOpts := networks.CreateOpts{
+ Name: networkName,
+ AdminStateUp: gophercloud.Enabled,
+ }
+
+ iFalse := false
+ createOpts := portsecurity.NetworkCreateOptsExt{
+ CreateOptsBuilder: networkCreateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ t.Logf("Attempting to create network: %s", networkName)
+
+ network, err := networks.Create(client, createOpts).Extract()
+ if err != nil {
+ return network, err
+ }
+
+ t.Logf("Successfully created network.")
+ return network, nil
+}
+
// CreatePort will create a port on the specified subnet. An error will be
// returned if the port could not be created.
func CreatePort(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID string) (*ports.Port, error) {
@@ -99,6 +127,45 @@ func CreatePortWithNoSecurityGroup(t *testing.T, client *gophercloud.ServiceClie
return newPort, nil
}
+// CreatePortWithoutPortSecurity will create a port without port security on the
+// specified subnet. An error will be returned if the port could not be created.
+func CreatePortWithoutPortSecurity(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID string) (*ports.Port, error) {
+ portName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create port: %s", portName)
+
+ portCreateOpts := ports.CreateOpts{
+ NetworkID: networkID,
+ Name: portName,
+ AdminStateUp: gophercloud.Enabled,
+ FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}},
+ }
+
+ iFalse := false
+ createOpts := portsecurity.PortCreateOptsExt{
+ CreateOptsBuilder: portCreateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ port, err := ports.Create(client, createOpts).Extract()
+ if err != nil {
+ return port, err
+ }
+
+ if err := WaitForPortToCreate(client, port.ID, 60); err != nil {
+ return port, err
+ }
+
+ newPort, err := ports.Get(client, port.ID).Extract()
+ if err != nil {
+ return newPort, err
+ }
+
+ t.Logf("Successfully created port: %s", portName)
+
+ return newPort, nil
+}
+
// CreateSubnet will create a subnet on the specified Network ID. An error
// will be returned if the subnet could not be created.
func CreateSubnet(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) {
@@ -188,6 +255,32 @@ func CreateSubnetWithNoGateway(t *testing.T, client *gophercloud.ServiceClient,
return subnet, nil
}
+// CreateSubnetWithSubnetPool will create a subnet associated with the provided subnetpool on the specified Network ID.
+// An error will be returned if the subnet or the subnetpool could not be created.
+func CreateSubnetWithSubnetPool(t *testing.T, client *gophercloud.ServiceClient, networkID string, subnetPoolID string) (*subnets.Subnet, error) {
+ subnetName := tools.RandomString("TESTACC-", 8)
+ subnetOctet := tools.RandomInt(1, 250)
+ subnetCIDR := fmt.Sprintf("10.%d.0.0/24", subnetOctet)
+ createOpts := subnets.CreateOpts{
+ NetworkID: networkID,
+ CIDR: subnetCIDR,
+ IPVersion: 4,
+ Name: subnetName,
+ EnableDHCP: gophercloud.Disabled,
+ SubnetPoolID: subnetPoolID,
+ }
+
+ t.Logf("Attempting to create subnet: %s", subnetName)
+
+ subnet, err := subnets.Create(client, createOpts).Extract()
+ if err != nil {
+ return subnet, err
+ }
+
+ t.Logf("Successfully created subnet.")
+ return subnet, nil
+}
+
// DeleteNetwork will delete a network with a specified ID. A fatal error will
// occur if the delete was not successful. This works best when used as a
// deferred function.
@@ -244,3 +337,53 @@ func WaitForPortToCreate(client *gophercloud.ServiceClient, portID string, secs
return false, nil
})
}
+
+// PortWithExtraDHCPOpts represents a port with extra DHCP options configuration.
+type PortWithExtraDHCPOpts struct {
+ ports.Port
+ extradhcpopts.ExtraDHCPOptsExt
+}
+
+// CreatePortWithExtraDHCPOpts will create a port with DHCP options on the
+// specified subnet. An error will be returned if the port could not be created.
+func CreatePortWithExtraDHCPOpts(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID string) (*PortWithExtraDHCPOpts, error) {
+ portName := tools.RandomString("TESTACC-", 8)
+
+ t.Logf("Attempting to create port: %s", portName)
+
+ portCreateOpts := ports.CreateOpts{
+ NetworkID: networkID,
+ Name: portName,
+ AdminStateUp: gophercloud.Enabled,
+ FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}},
+ }
+
+ createOpts := extradhcpopts.CreateOptsExt{
+ CreateOptsBuilder: portCreateOpts,
+ ExtraDHCPOpts: []extradhcpopts.CreateExtraDHCPOpt{
+ {
+ OptName: "test_option_1",
+ OptValue: "test_value_1",
+ },
+ },
+ }
+ port := &PortWithExtraDHCPOpts{}
+
+ err := ports.Create(client, createOpts).ExtractInto(port)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := WaitForPortToCreate(client, port.ID, 60); err != nil {
+ return nil, err
+ }
+
+ err = ports.Get(client, port.ID).ExtractInto(port)
+ if err != nil {
+ return port, err
+ }
+
+ t.Logf("Successfully created port: %s", portName)
+
+ return port, nil
+}
diff --git a/acceptance/openstack/networking/v2/networks_test.go b/acceptance/openstack/networking/v2/networks_test.go
index c100bd4160..ab4c2b1ce9 100644
--- a/acceptance/openstack/networking/v2/networks_test.go
+++ b/acceptance/openstack/networking/v2/networks_test.go
@@ -71,3 +71,43 @@ func TestNetworksCRUD(t *testing.T) {
tools.PrintResource(t, newNetwork)
}
+
+func TestNetworksPortSecurityCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create a network without port security
+ network, err := CreateNetworkWithoutPortSecurity(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer DeleteNetwork(t, client, network.ID)
+
+ var networkWithExtensions struct {
+ networks.Network
+ portsecurity.PortSecurityExt
+ }
+
+ err = networks.Get(client, network.ID).ExtractInto(&networkWithExtensions)
+ if err != nil {
+ t.Fatalf("Unable to retrieve network: %v", err)
+ }
+
+ tools.PrintResource(t, networkWithExtensions)
+
+ iTrue := true
+ networkUpdateOpts := networks.UpdateOpts{}
+ updateOpts := portsecurity.NetworkUpdateOptsExt{
+ UpdateOptsBuilder: networkUpdateOpts,
+ PortSecurityEnabled: &iTrue,
+ }
+
+ err = networks.Update(client, network.ID, updateOpts).ExtractInto(&networkWithExtensions)
+ if err != nil {
+ t.Fatalf("Unable to update network: %v", err)
+ }
+
+ tools.PrintResource(t, networkWithExtensions)
+}
diff --git a/acceptance/openstack/networking/v2/ports_test.go b/acceptance/openstack/networking/v2/ports_test.go
index eddddf64f2..5b820d7e57 100644
--- a/acceptance/openstack/networking/v2/ports_test.go
+++ b/acceptance/openstack/networking/v2/ports_test.go
@@ -8,6 +8,8 @@ import (
"github.com/gophercloud/gophercloud/acceptance/clients"
extensions "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions"
"github.com/gophercloud/gophercloud/acceptance/tools"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/extradhcpopts"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsecurity"
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
)
@@ -347,3 +349,118 @@ func TestPortsDontUpdateAllowedAddressPairs(t *testing.T) {
t.Fatalf("Address Pairs were removed")
}
}
+
+func TestPortsPortSecurityCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create Network
+ network, err := CreateNetwork(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer DeleteNetwork(t, client, network.ID)
+
+ // Create Subnet
+ subnet, err := CreateSubnet(t, client, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create subnet: %v", err)
+ }
+ defer DeleteSubnet(t, client, subnet.ID)
+
+ // Create port
+ port, err := CreatePortWithoutPortSecurity(t, client, network.ID, subnet.ID)
+ if err != nil {
+ t.Fatalf("Unable to create port: %v", err)
+ }
+ defer DeletePort(t, client, port.ID)
+
+ var portWithExt struct {
+ ports.Port
+ portsecurity.PortSecurityExt
+ }
+
+ err = ports.Get(client, port.ID).ExtractInto(&portWithExt)
+ if err != nil {
+ t.Fatalf("Unable to create port: %v", err)
+ }
+
+ tools.PrintResource(t, portWithExt)
+
+ iTrue := true
+ portUpdateOpts := ports.UpdateOpts{}
+ updateOpts := portsecurity.PortUpdateOptsExt{
+ UpdateOptsBuilder: portUpdateOpts,
+ PortSecurityEnabled: &iTrue,
+ }
+
+ err = ports.Update(client, port.ID, updateOpts).ExtractInto(&portWithExt)
+ if err != nil {
+ t.Fatalf("Unable to update port: %v", err)
+ }
+
+ tools.PrintResource(t, portWithExt)
+}
+
+func TestPortsWithExtraDHCPOptsCRUD(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create a Network
+ network, err := CreateNetwork(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create a network: %v", err)
+ }
+ defer DeleteNetwork(t, client, network.ID)
+
+ // Create a Subnet
+ subnet, err := CreateSubnet(t, client, network.ID)
+ if err != nil {
+ t.Fatalf("Unable to create a subnet: %v", err)
+ }
+ defer DeleteSubnet(t, client, subnet.ID)
+
+ // Create a port with extra DHCP options.
+ port, err := CreatePortWithExtraDHCPOpts(t, client, network.ID, subnet.ID)
+ if err != nil {
+ t.Fatalf("Unable to create a port: %v", err)
+ }
+ defer DeletePort(t, client, port.ID)
+
+ tools.PrintResource(t, port)
+
+ // Update the port with extra DHCP options.
+ newPortName := tools.RandomString("TESTACC-", 8)
+ portUpdateOpts := ports.UpdateOpts{
+ Name: newPortName,
+ }
+
+ existingOpt := port.ExtraDHCPOpts[0]
+ newOptValue := "test_value_2"
+
+ updateOpts := extradhcpopts.UpdateOptsExt{
+ UpdateOptsBuilder: portUpdateOpts,
+ ExtraDHCPOpts: []extradhcpopts.UpdateExtraDHCPOpt{
+ {
+ OptName: existingOpt.OptName,
+ OptValue: nil,
+ },
+ {
+ OptName: "test_option_2",
+ OptValue: &newOptValue,
+ },
+ },
+ }
+
+ newPort := &PortWithExtraDHCPOpts{}
+ err = ports.Update(client, port.ID, updateOpts).ExtractInto(newPort)
+ if err != nil {
+ t.Fatalf("Could not update port: %v", err)
+ }
+
+ tools.PrintResource(t, newPort)
+}
diff --git a/acceptance/openstack/networking/v2/subnets_test.go b/acceptance/openstack/networking/v2/subnets_test.go
index fd50a1f84b..ee665286ed 100644
--- a/acceptance/openstack/networking/v2/subnets_test.go
+++ b/acceptance/openstack/networking/v2/subnets_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"github.com/gophercloud/gophercloud/acceptance/clients"
+ subnetpools "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/subnetpools"
"github.com/gophercloud/gophercloud/acceptance/tools"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
)
@@ -156,3 +157,37 @@ func TestSubnetsNoGateway(t *testing.T) {
t.Fatalf("Gateway was not updated correctly")
}
}
+
+func TestSubnetsWithSubnetPool(t *testing.T) {
+ client, err := clients.NewNetworkV2Client()
+ if err != nil {
+ t.Fatalf("Unable to create a network client: %v", err)
+ }
+
+ // Create Network
+ network, err := CreateNetwork(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create network: %v", err)
+ }
+ defer DeleteNetwork(t, client, network.ID)
+
+ // Create SubnetPool
+ subnetPool, err := subnetpools.CreateSubnetPool(t, client)
+ if err != nil {
+ t.Fatalf("Unable to create subnet pool: %v", err)
+ }
+ defer subnetpools.DeleteSubnetPool(t, client, subnetPool.ID)
+
+ // Create Subnet
+ subnet, err := CreateSubnetWithSubnetPool(t, client, network.ID, subnetPool.ID)
+ if err != nil {
+ t.Fatalf("Unable to create subnet: %v", err)
+ }
+ defer DeleteSubnet(t, client, subnet.ID)
+
+ tools.PrintResource(t, subnet)
+
+ if subnet.GatewayIP == "" {
+ t.Fatalf("A subnet pool was not associated.")
+ }
+}
diff --git a/auth_options.go b/auth_options.go
index 4211470020..5e693585c2 100644
--- a/auth_options.go
+++ b/auth_options.go
@@ -81,6 +81,17 @@ type AuthOptions struct {
// TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID.
TokenID string `json:"-"`
+
+ // Scope determines the scoping of the authentication request.
+ Scope *AuthScope `json:"-"`
+}
+
+// AuthScope allows a created token to be limited to a specific domain or project.
+type AuthScope struct {
+ ProjectID string
+ ProjectName string
+ DomainID string
+ DomainName string
}
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
@@ -263,85 +274,83 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
}
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
-
- var scope struct {
- ProjectID string
- ProjectName string
- DomainID string
- DomainName string
- }
-
- if opts.TenantID != "" {
- scope.ProjectID = opts.TenantID
- } else {
- if opts.TenantName != "" {
- scope.ProjectName = opts.TenantName
- scope.DomainID = opts.DomainID
- scope.DomainName = opts.DomainName
+ // For backwards compatibility.
+ // If AuthOptions.Scope was not set, try to determine it.
+ // This works well for common scenarios.
+ if opts.Scope == nil {
+ opts.Scope = new(AuthScope)
+ if opts.TenantID != "" {
+ opts.Scope.ProjectID = opts.TenantID
+ } else {
+ if opts.TenantName != "" {
+ opts.Scope.ProjectName = opts.TenantName
+ opts.Scope.DomainID = opts.DomainID
+ opts.Scope.DomainName = opts.DomainName
+ }
}
}
- if scope.ProjectName != "" {
+ if opts.Scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
- if scope.DomainID == "" && scope.DomainName == "" {
+ if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
return nil, ErrScopeDomainIDOrDomainName{}
}
- if scope.ProjectID != "" {
+ if opts.Scope.ProjectID != "" {
return nil, ErrScopeProjectIDOrProjectName{}
}
- if scope.DomainID != "" {
+ if opts.Scope.DomainID != "" {
// ProjectName + DomainID
return map[string]interface{}{
"project": map[string]interface{}{
- "name": &scope.ProjectName,
- "domain": map[string]interface{}{"id": &scope.DomainID},
+ "name": &opts.Scope.ProjectName,
+ "domain": map[string]interface{}{"id": &opts.Scope.DomainID},
},
}, nil
}
- if scope.DomainName != "" {
+ if opts.Scope.DomainName != "" {
// ProjectName + DomainName
return map[string]interface{}{
"project": map[string]interface{}{
- "name": &scope.ProjectName,
- "domain": map[string]interface{}{"name": &scope.DomainName},
+ "name": &opts.Scope.ProjectName,
+ "domain": map[string]interface{}{"name": &opts.Scope.DomainName},
},
}, nil
}
- } else if scope.ProjectID != "" {
+ } else if opts.Scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
- if scope.DomainID != "" {
+ if opts.Scope.DomainID != "" {
return nil, ErrScopeProjectIDAlone{}
}
- if scope.DomainName != "" {
+ if opts.Scope.DomainName != "" {
return nil, ErrScopeProjectIDAlone{}
}
// ProjectID
return map[string]interface{}{
"project": map[string]interface{}{
- "id": &scope.ProjectID,
+ "id": &opts.Scope.ProjectID,
},
}, nil
- } else if scope.DomainID != "" {
+ } else if opts.Scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
- if scope.DomainName != "" {
+ if opts.Scope.DomainName != "" {
return nil, ErrScopeDomainIDOrDomainName{}
}
// DomainID
return map[string]interface{}{
"domain": map[string]interface{}{
- "id": &scope.DomainID,
+ "id": &opts.Scope.DomainID,
},
}, nil
- } else if scope.DomainName != "" {
+ } else if opts.Scope.DomainName != "" {
// DomainName
return map[string]interface{}{
"domain": map[string]interface{}{
- "name": &scope.DomainName,
+ "name": &opts.Scope.DomainName,
},
}, nil
}
diff --git a/FAQ.md b/docs/FAQ.md
similarity index 100%
rename from FAQ.md
rename to docs/FAQ.md
diff --git a/MIGRATING.md b/docs/MIGRATING.md
similarity index 100%
rename from MIGRATING.md
rename to docs/MIGRATING.md
diff --git a/STYLEGUIDE.md b/docs/STYLEGUIDE.md
similarity index 100%
rename from STYLEGUIDE.md
rename to docs/STYLEGUIDE.md
diff --git a/assets/openlab.png b/docs/assets/openlab.png
similarity index 100%
rename from assets/openlab.png
rename to docs/assets/openlab.png
diff --git a/assets/vexxhost.png b/docs/assets/vexxhost.png
similarity index 100%
rename from assets/vexxhost.png
rename to docs/assets/vexxhost.png
diff --git a/docs/contributor-tutorial/README.md b/docs/contributor-tutorial/README.md
new file mode 100644
index 0000000000..14950b2bd6
--- /dev/null
+++ b/docs/contributor-tutorial/README.md
@@ -0,0 +1,12 @@
+Contributor Tutorial
+====================
+
+This tutorial is to help new contributors become familiar with the processes
+used by the Gophercloud team when adding a new feature or fixing a bug.
+
+While we have a defined process for working on Gophercloud, we're very mindful
+that everyone is new to this in the beginning. Please reach out for help or ask
+for clarification if needed. No question is ever "dumb" or not worth our time
+answering.
+
+To begin, go to [Step 1](step-01-introduction.md).
diff --git a/docs/contributor-tutorial/step-01-introduction.md b/docs/contributor-tutorial/step-01-introduction.md
new file mode 100644
index 0000000000..d806143d77
--- /dev/null
+++ b/docs/contributor-tutorial/step-01-introduction.md
@@ -0,0 +1,16 @@
+Step 1: Read Our Guides
+========================
+
+There are two introductory guides you should read before proceeding:
+
+* [CONTRIBUTING](/.github/CONTRIBUTING.md): The Contributing guide is a detailed
+ document which describes the different ways you can contribute to Gophercloud
+ and how to get started. This tutorial you're reading is very similar to that
+ guide, but presented in a different way. We still recommend you read it over.
+
+* [STYLE](/docs/STYLEGUIDE.md): The Style Guide documents coding conventions used
+ in the Gophercloud project.
+
+---
+
+When you've finished reading those guides, proceed to [Step 2](step-02-issues.md).
diff --git a/docs/contributor-tutorial/step-02-issues.md b/docs/contributor-tutorial/step-02-issues.md
new file mode 100644
index 0000000000..a3ae2a237b
--- /dev/null
+++ b/docs/contributor-tutorial/step-02-issues.md
@@ -0,0 +1,124 @@
+Step 2: Create an Issue
+========================
+
+Every patch / Pull Request requires a corresponding issue. If you're fixing
+a bug for an existing issue, then there's no need to create a new issue.
+
+However, if no prior issue exists, you must create an issue.
+
+Reporting a Bug
+---------------
+
+When reporting a bug, please try to provide as much information as you
+can.
+
+The following issues are good examples for reporting a bug:
+
+* https://github.com/gophercloud/gophercloud/issues/108
+* https://github.com/gophercloud/gophercloud/issues/212
+* https://github.com/gophercloud/gophercloud/issues/424
+* https://github.com/gophercloud/gophercloud/issues/588
+* https://github.com/gophercloud/gophercloud/issues/629
+* https://github.com/gophercloud/gophercloud/issues/647
+
+Feature Request
+---------------
+
+If you've noticed that a feature is missing from Gophercloud, you'll also
+need to create an issue before doing any work. This is start a discussion about
+whether or not the feature should be included in Gophercloud. We don't want to
+want to see you put in hours of work only to learn that the feature is out of
+scope of the project.
+
+Feature requests can come in different forms:
+
+### Adding a Feature to Gophercloud Core
+
+The "core" of Gophercloud is the code which supports API requests and
+responses: pagination, error handling, building request bodies, and parsing
+response bodies are all examples of core code.
+
+Modifications to core will usually have the most amount of discussion than
+other requests since a change to core will affect _all_ of Gophercloud.
+
+The following issues are examples of core change discussions:
+
+* https://github.com/gophercloud/gophercloud/issues/310
+* https://github.com/gophercloud/gophercloud/issues/613
+* https://github.com/gophercloud/gophercloud/issues/729
+* https://github.com/gophercloud/gophercloud/issues/713
+
+### Adding a Missing Field
+
+If you've found a missing field in an existing struct, submit an issue to
+request having it added. These kinds of issues are pretty easy to report
+and resolve.
+
+You should also provide a link to the actual service's Python code which
+defines the missing field.
+
+The following issues are examples of missing fields:
+
+* https://github.com/gophercloud/gophercloud/issues/620
+* https://github.com/gophercloud/gophercloud/issues/621
+* https://github.com/gophercloud/gophercloud/issues/658
+
+There's one situation which can make adding fields more difficult: if the field
+is part of an API extension rather than the base API itself. An example of this
+can be seen in [this](https://github.com/gophercloud/gophercloud/issues/749)
+issue.
+
+Here, a user reported fields missing in the `Get` function of
+`networking/v2/networks`. The fields reported missing weren't missing at all,
+they're just part of various Networking extensions located in
+`networking/v2/extensions`.
+
+### Adding a Missing API Call
+
+If you've found a missing API action, create an issue with details of
+the action. For example:
+
+* https://github.com/gophercloud/gophercloud/issues/715
+* https://github.com/gophercloud/gophercloud/issues/719
+
+You'll want to make sure the API call is part of the upstream OpenStack project
+and not an extension created by a third-party or vendor. Gophercloud only
+supports the OpenStack projects proper.
+
+### Adding a Missing API Suite
+
+Adding support to a missing suite of API calls will require more than one Pull
+Request. However, you can use a single issue for all PRs.
+
+Examples of issues which track the addition of a missing API suite are:
+
+* https://github.com/gophercloud/gophercloud/issues/539
+* https://github.com/gophercloud/gophercloud/issues/555
+* https://github.com/gophercloud/gophercloud/issues/571
+* https://github.com/gophercloud/gophercloud/issues/583
+* https://github.com/gophercloud/gophercloud/issues/605
+
+Note how the issue breaks down the implementation by request types (Create,
+Update, Delete, Get, List).
+
+Also note how these issues provide links to the service's Python code. These
+links are not required for _issues_, but it's usually a good idea to provide
+them, anyway. These links _are required_ for PRs and that will be covered in
+detail in a later step of this tutorial.
+
+### Adding a Missing OpenStack Project
+
+These kinds of feature additions are large undertakings. Adding support for
+an entire OpenStack project is something the Gophercloud team very much
+appreciates, but you should be prepared for several weeks of work and
+interaction with the Gophercloud team.
+
+An example of how to create an issue for an entire project can be seen
+here:
+
+* https://github.com/gophercloud/gophercloud/issues/723
+
+---
+
+With all of the above in mind, proceed to [Step 3](step-03-code-hunting.md) to
+learn about Code Hunting.
diff --git a/docs/contributor-tutorial/step-03-code-hunting.md b/docs/contributor-tutorial/step-03-code-hunting.md
new file mode 100644
index 0000000000..f773eec040
--- /dev/null
+++ b/docs/contributor-tutorial/step-03-code-hunting.md
@@ -0,0 +1,104 @@
+Step 3: Code Hunting
+====================
+
+If you plan to submit a feature or bug fix to Gophercloud, you must be
+able to prove your code correctly works with the OpenStack service in
+question.
+
+Let's use the following issue as an example:
+[https://github.com/gophercloud/gophercloud/issues/621](https://github.com/gophercloud/gophercloud/issues/621).
+In this issue, there's a request being made to add support for
+`availability_zone_hints` to the `networking/v2/networks` package.
+Meaning, we want to change:
+
+```go
+type Network struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ AdminStateUp bool `json:"admin_state_up"`
+ Status string `json:"status"`
+ Subnets []string `json:"subnets"`
+ TenantID string `json:"tenant_id"`
+ Shared bool `json:"shared"`
+}
+```
+
+to look like
+
+```go
+type Network struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ AdminStateUp bool `json:"admin_state_up"`
+ Status string `json:"status"`
+ Subnets []string `json:"subnets"`
+ TenantID string `json:"tenant_id"`
+ Shared bool `json:"shared"`
+
+ AvailabilityZoneHints []string `json:"availability_zone_hints"`
+}
+```
+
+We need to be sure that `availability_zone_hints` is a field which really does
+exist in the OpenStack Neutron project and it's not a field which was added as
+a customization to a single OpenStack cloud.
+
+In addition, we need to ensure that `availability_zone_hints` is really a
+`[]string` and not a different kind of type.
+
+One way of verifying this is through the [OpenStack API reference
+documentation](https://developer.openstack.org/api-ref/network/v2/).
+However, the API docs might either be incorrect or they might not provide all of
+the details we need to know in order to ensure this field is added correctly.
+
+> Note: when we say the API docs might be incorrect, we are _not_ implying
+> that the API docs aren't useful or that the contributors who work on the API
+> docs are wrong. OpenStack moves fast. Typos happen. Forgetting to update
+> documentation happens.
+
+Since the OpenStack service itself correctly accepts and processes the fields,
+the best source of information on how the field works is in the service code
+itself.
+
+Continuing on with using #621 as an example, we can find the definition of
+`availability_zone_hints` in the following piece of code:
+
+https://github.com/openstack/neutron/blob/8e9959725eda4063a318b4ba6af1e3494cad9e35/neutron/objects/network.py#L191
+
+The above code confirms that `availability_zone_hints` is indeed part of the
+`Network` object and that its type is a list of strings (`[]string`).
+
+This example is a best-case situation: the code is relatively easy to find
+and it's simple to understand. However, there will be times when proving the
+implementation in the service code is difficult. Make no mistake, this is _not_
+fun work. This can sometimes be more difficult than writing the actual patch
+for Gophercloud. However, this is an essential step to ensuring the feature
+or bug fix is correctly added to Gophercloud.
+
+Examples of good code hunting can be seen here:
+
+* https://github.com/gophercloud/gophercloud/issues/539
+* https://github.com/gophercloud/gophercloud/issues/555
+* https://github.com/gophercloud/gophercloud/issues/571
+* https://github.com/gophercloud/gophercloud/issues/583
+* https://github.com/gophercloud/gophercloud/issues/605
+
+Code Hunting Tips
+-----------------
+
+OpenStack projects differ from one to another. Code is organized in different
+ways. However, the following tips should be useful across all projects.
+
+* The logic which implements Create and Delete actions is usually either located
+ in the "model" or "controller" portion of the code.
+
+* Use Github's search box to search for the exact field you're working on.
+ Review all results to gain a good understanding of everywhere the field is
+ used.
+
+* When adding a field, look for an object model or a schema of some sort.
+
+---
+
+Proceed to [Step 4](step-04-acceptance-testing.md) to learn about Acceptance
+Testing.
diff --git a/docs/contributor-tutorial/step-04-acceptance-testing.md b/docs/contributor-tutorial/step-04-acceptance-testing.md
new file mode 100644
index 0000000000..fe82717439
--- /dev/null
+++ b/docs/contributor-tutorial/step-04-acceptance-testing.md
@@ -0,0 +1,27 @@
+Step 4: Acceptance Testing
+==========================
+
+If we haven't started working on the feature or bug fix, why are we talking
+about Acceptance Testing now?
+
+Before you implement a feature or bug fix, you _must_ be able to test your code
+in a working OpenStack environment. Please do not submit code which you have
+only tested with offline unit tests.
+
+Blindly submitting code is dangerous to the Gophercloud project. Developers
+from all over the world use Gophercloud in many different projects. If you
+submit code which is untested, it can cause these projects to break or become
+unstable.
+
+And, to be frank, submitting untested code will inevitably cause someone else
+to have to spend time fixing it.
+
+If you don't have an OpenStack environment to test with, we have lots of
+documentation [here](/acceptance) to help you build your own small OpenStack
+environment for testing.
+
+---
+
+Once you've confirmed you are able to test your code, proceed to
+[Step 5](step-05-pull-requests.md) to (finally!) start working on a Pull
+Request.
diff --git a/docs/contributor-tutorial/step-05-pull-requests.md b/docs/contributor-tutorial/step-05-pull-requests.md
new file mode 100644
index 0000000000..9bf1e0a4d8
--- /dev/null
+++ b/docs/contributor-tutorial/step-05-pull-requests.md
@@ -0,0 +1,183 @@
+Step 5: Writing the Code
+========================
+
+At this point, you should have:
+
+- [x] Identified a feature or bug fix
+- [x] Opened an Issue about it
+- [x] Located the project's service code which validates the feature or fix
+- [x] Have an OpenStack environment available to test with
+
+Now it's time to write the actual code! We recommend reading over the
+[CONTRIBUTING](/.github/CONTRIBUTING.md) guide again as a refresh. Notably
+the [Getting Started](/.github/CONTRIBUTING.md#getting-started) section will
+help you set up a `git` repository correctly.
+
+We encourage you to browse the existing Gophercloud code to find examples
+of similar implementations. It would be a _very_ rare occurrence for you
+to be implementing something that hasn't already been done.
+
+Use the existing packages as templates and mirror the style, naming, and
+logic.
+
+Types of Pull Requests
+----------------------
+
+The amount of changes you plan to make will determine how much code you should
+submit as Pull Requests.
+
+### A Single Bug Fix
+
+If you're implementing a single bug fix, then creating one `git` branch and
+submitting one Pull Request is fine.
+
+### Adding a Single Field
+
+If you're adding a single field, then a single Pull Request is also fine. See
+[#662](https://github.com/gophercloud/gophercloud/pull/662) as an example of
+this.
+
+If you plan to add more than one missing field, you will need to open a Pull
+Request for _each_ field.
+
+### Adding a Single API Call
+
+Single API calls can also be submitted as a single Pull Request. See
+[#722](https://github.com/gophercloud/gophercloud/pull/722) as an example of
+this.
+
+### Adding a Suite of API Calls
+
+If you're adding support for a "suite" of API calls (meaning: Create, Update,
+Delete, Get), then you will need to create one Pull Request for _each_ call.
+
+The following Pull Requests are good examples of how to do this:
+
+* https://github.com/gophercloud/gophercloud/pull/584
+* https://github.com/gophercloud/gophercloud/pull/586
+* https://github.com/gophercloud/gophercloud/pull/587
+* https://github.com/gophercloud/gophercloud/pull/594
+
+### Adding an Entire OpenStack Project
+
+To add an entire OpenStack project, you must break each set of API calls into
+individual Pull Requests. Implementing an entire project can be thought of as
+implementing multiple API suites.
+
+An example of this can be seen from the Pull Requests referenced in
+[#723](https://github.com/gophercloud/gophercloud/issues/723).
+
+What to Include in a Pull Request
+---------------------------------
+
+Each Pull Request should contain the following:
+
+1. The actual Go code to implement the feature or bug fix
+2. Unit tests
+3. Acceptance tests
+4. Documentation
+
+Whether you want to bundle all of the above into a single commit or multiple
+commits is up to you. Use your preferred style.
+
+### Unit Tests
+
+Unit tests should provide basic validation that your code works as intended.
+
+Please do not use JSON fixtures from the API reference documentation. Please
+generate your own fixtures using the OpenStack environment you're
+[testing](step-04-acceptance-testing.md) with.
+
+### Acceptance Tests
+
+Since unit tests are not run against an actual OpenStack environment,
+acceptance tests can arguably be more important. The acceptance tests that you
+include in your Pull Request should confirm that your implemented code works
+as intended with an actual OpenStack environment.
+
+### Documentation
+
+All documentation in Gophercloud is done through in-line `godoc`. Please make
+sure to document all fields, functions, and methods appropriately. In addition,
+each package has a `doc.go` file which should be created or amended with
+details of your Pull Request, where appropriate.
+
+Dealing with Related Pull Requests
+----------------------------------
+
+If you plan to open more than one Pull Request, it's only natural that code
+from one Pull Request will be dependent on code from the prior Pull Request.
+
+There are two methods of handling this:
+
+### Create Independent Pull Requests
+
+With this method, each Pull Request has all of the code to fully implement
+the code in question. Each Pull Request can be merged in any order because
+it's self contained.
+
+Use the following `git` workflow to implement this method:
+
+```shell
+$ git checkout master
+$ git pull
+$ git checkout -b identityv3-regions-create
+$ (write your code)
+$ git add .
+$ git commit -m "Implementing Regions Create"
+
+$ git checkout master
+$ git checkout -b identityv3-regions-update
+$ (write your code)
+$ git add .
+$ git commit -m "Implementing Regions Update"
+```
+
+Advantages of this Method:
+
+* Pull Requests can be merged in any order
+* Additional commits to one Pull Request are independent of other Pull Requests
+
+Disadvantages of this Method:
+
+* There will be _a lot_ of duplicate code in each Pull Request
+* You will have to rebase all other Pull Requests and resolve a good amount of
+ merge conflicts.
+
+### Create a Chain of Pull Requests
+
+With this method, each Pull Request is based off of a previous Pull Request.
+Pull Requests will have to be merged in a specific order since there is a
+defined relationship.
+
+Use the following `git` workflow to implement this method:
+
+```shell
+$ git checkout master
+$ git pull
+$ git checkout -b identityv3-regions-create
+$ (write your code)
+$ git add .
+$ git commit -m "Implementing Regions Create"
+
+$ git checkout -b identityv3-regions-update
+$ (write your code)
+$ git add .
+$ git commit -m "Implementing Regions Update"
+```
+
+Advantages of this Method:
+
+* Each Pull Request becomes smaller since you are building off of the last
+
+Disadvantages of this Method:
+
+* If a Pull Request requires changes, you will have to rebase _all_ child
+ Pull Requests based off of the parent.
+
+The choice of method is up to you.
+
+---
+
+Once you have your code written, submit a Pull Request to Gophercloud and
+proceed to [Step 6](step-06-code-review.md).
diff --git a/docs/contributor-tutorial/step-06-code-review.md b/docs/contributor-tutorial/step-06-code-review.md
new file mode 100644
index 0000000000..ac3b68808b
--- /dev/null
+++ b/docs/contributor-tutorial/step-06-code-review.md
@@ -0,0 +1,93 @@
+Step 6: Code Review
+===================
+
+Once you've submitted a Pull Request, three things will happen automatically:
+
+1. Travis-CI will run a set of simple tests:
+
+ a. Unit Tests
+
+ b. Code Formatting checks
+
+ c. `go vet` checks
+
+2. Coveralls will run a coverage test.
+3. [OpenLab](https://openlabtesting.org/) will run acceptance tests.
+
+Depending on the results of the above, you might need to make additional
+changes to your code.
+
+While you're working on the finishing touches to your code, it is helpful
+to add a `[wip]` tag to the title of your Pull Request.
+
+You are most welcomed to take as much time as you need to work on your Pull
+Request. As well, take advantage of the automatic testing that is done to
+each commit.
+
+### Travis-CI
+
+If Travis reports code formatting issues, please make sure to run `gofmt` on all
+of your code. Travis will also report errors with unit tests, so you should
+ensure those are fixed, too.
+
+### Coveralls
+
+If Coveralls reports a decrease in test coverage, check and make sure you have
+provided unit tests. A decrease in test coverage is _sometimes_ unavoidable and
+ignorable.
+
+### OpenLab
+
+OpenLab does not yet run a full suite of acceptance tests, so it's possible
+that the acceptance tests you've included were not run. When this happens,
+a core member for Gophercloud will run the tests manually.
+
+There are times when a core reviewer does not have access to the resources
+required to run the acceptance tests. When this happens, it is essential
+that you've run them yourself (See [Step 4](step-04.md)).
+
+Request a Code Review
+---------------------
+
+When you feel your Pull Request is ready for review, please leave a comment
+requesting a code review. If you don't explicitly ask for a code review, a
+core member might not know the Pull Request is ready for review.
+
+Additionally, if there are parts of your implementation that you are unsure
+about, please ask for help. We're more than happy to provide advice.
+
+During the code review process, a core member will review the code you've
+submitted and either request changes or request additional information.
+Generally these requests fall under the following categories:
+
+1. Code which needs to be reformatted (See our [Style Guide](/docs/STYLEGUIDE.md)
+ for conventions used.
+
+2. Requests for additional information about the validity of something. This
+ might happen because the included supporting service code URLs don't have
+ enough information.
+
+3. Missing unit tests or acceptance tests.
+
+Submitting Changes
+------------------
+
+If a code review requires changes to be submitted, please do not squash your
+commits. Please only add new commits to the Pull Request. This is to help the
+code reviewer see only the changes that were made.
+
+It's Never Personal
+-------------------
+
+Code review is a healthy exercise where a new set of eyes can sometimes spot
+items forgotten by the author.
+
+Please don't take change requests personally. Our intention is to ensure the
+code is correct before merging.
+
+---
+
+Once the code has been reviewed and approved, a core member will merge your
+Pull Request.
+
+Please proceed to [Step 7](step-07-congratulations.md).
diff --git a/docs/contributor-tutorial/step-07-congratulations.md b/docs/contributor-tutorial/step-07-congratulations.md
new file mode 100644
index 0000000000..e14b794143
--- /dev/null
+++ b/docs/contributor-tutorial/step-07-congratulations.md
@@ -0,0 +1,9 @@
+Step 7: Congratulations!
+========================
+
+At this point your code is merged and you've either fixed a bug or added a new
+feature to Gophercloud!
+
+We completely understand that this has been a long process. We appreciate your
+patience as well as the time you have taken for working on this. You've made
+Gophercloud a better project with your work.
diff --git a/errors.go b/errors.go
index 88fd2ac676..2466932efe 100644
--- a/errors.go
+++ b/errors.go
@@ -72,6 +72,11 @@ type ErrDefault401 struct {
ErrUnexpectedResponseCode
}
+// ErrDefault403 is the default error type returned on a 403 HTTP response code.
+type ErrDefault403 struct {
+ ErrUnexpectedResponseCode
+}
+
// ErrDefault404 is the default error type returned on a 404 HTTP response code.
type ErrDefault404 struct {
ErrUnexpectedResponseCode
@@ -108,6 +113,13 @@ func (e ErrDefault400) Error() string {
func (e ErrDefault401) Error() string {
return "Authentication failed"
}
+func (e ErrDefault403) Error() string {
+ e.DefaultErrString = fmt.Sprintf(
+ "Request forbidden: [%s %s], error message: %s",
+ e.Method, e.URL, e.Body,
+ )
+ return e.choseErrString()
+}
func (e ErrDefault404) Error() string {
return "Resource not found"
}
@@ -141,6 +153,12 @@ type Err401er interface {
Error401(ErrUnexpectedResponseCode) error
}
+// Err403er is the interface resource error types implement to override the error message
+// from a 403 error.
+type Err403er interface {
+ Error403(ErrUnexpectedResponseCode) error
+}
+
// Err404er is the interface resource error types implement to override the error message
// from a 404 error.
type Err404er interface {
diff --git a/openstack/blockstorage/extensions/services/doc.go b/openstack/blockstorage/extensions/services/doc.go
new file mode 100644
index 0000000000..b3fba4cd62
--- /dev/null
+++ b/openstack/blockstorage/extensions/services/doc.go
@@ -0,0 +1,22 @@
+/*
+Package services returns information about the blockstorage services in the
+OpenStack cloud.
+
+Example of Retrieving list of all services
+
+ allPages, err := services.List(blockstorageClient, services.ListOpts{}).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allServices, err := services.ExtractServices(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, service := range allServices {
+ fmt.Printf("%+v\n", service)
+ }
+*/
+
+package services
diff --git a/openstack/blockstorage/extensions/services/requests.go b/openstack/blockstorage/extensions/services/requests.go
new file mode 100644
index 0000000000..0edcfc9d7e
--- /dev/null
+++ b/openstack/blockstorage/extensions/services/requests.go
@@ -0,0 +1,42 @@
+package services
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+ ToServiceListQuery() (string, error)
+}
+
+// ListOpts holds options for listing Services.
+type ListOpts struct {
+ // Filter the service list result by binary name of the service.
+ Binary string `q:"binary"`
+
+ // Filter the service list result by host name of the service.
+ Host string `q:"host"`
+}
+
+// ToServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToServiceListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List makes a request against the API to list services.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := opts.ToServiceListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return ServicePage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/openstack/blockstorage/extensions/services/results.go b/openstack/blockstorage/extensions/services/results.go
new file mode 100644
index 0000000000..49ad48ef61
--- /dev/null
+++ b/openstack/blockstorage/extensions/services/results.go
@@ -0,0 +1,84 @@
+package services
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// Service represents a Blockstorage service in the OpenStack cloud.
+type Service struct {
+ // The binary name of the service.
+ Binary string `json:"binary"`
+
+ // The reason for disabling a service.
+ DisabledReason string `json:"disabled_reason"`
+
+ // The name of the host.
+ Host string `json:"host"`
+
+ // The state of the service. One of up or down.
+ State string `json:"state"`
+
+ // The status of the service. One of available or unavailable.
+ Status string `json:"status"`
+
+ // The date and time stamp when the extension was last updated.
+ UpdatedAt time.Time `json:"-"`
+
+ // The availability zone name.
+ Zone string `json:"zone"`
+
+ // The following fields are optional
+
+ // The host is frozen or not. Only in cinder-volume service.
+ Frozen bool `json:"frozen"`
+
+ // The cluster name. Only in cinder-volume service.
+ Cluster string `json:"cluster"`
+
+ // The volume service replication status. Only in cinder-volume service.
+ ReplicationStatus string `json:"replication_status"`
+
+ // The ID of active storage backend. Only in cinder-volume service.
+ ActiveBackendID string `json:"active_backend_id"`
+}
+
+// UnmarshalJSON to override default
+func (r *Service) UnmarshalJSON(b []byte) error {
+ type tmp Service
+ var s struct {
+ tmp
+ UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+ }
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ *r = Service(s.tmp)
+
+ r.UpdatedAt = time.Time(s.UpdatedAt)
+
+ return nil
+}
+
+// ServicePage represents a single page of all Services from a List request.
+type ServicePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Services contains any results.
+func (page ServicePage) IsEmpty() (bool, error) {
+ services, err := ExtractServices(page)
+ return len(services) == 0, err
+}
+
+func ExtractServices(r pagination.Page) ([]Service, error) {
+ var s struct {
+ Service []Service `json:"services"`
+ }
+ err := (r.(ServicePage)).ExtractInto(&s)
+ return s.Service, err
+}
diff --git a/openstack/blockstorage/extensions/services/testing/fixtures.go b/openstack/blockstorage/extensions/services/testing/fixtures.go
new file mode 100644
index 0000000000..9d14723c12
--- /dev/null
+++ b/openstack/blockstorage/extensions/services/testing/fixtures.go
@@ -0,0 +1,97 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/services"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// ServiceListBody is sample response to the List call
+const ServiceListBody = `
+{
+ "services": [{
+ "status": "enabled",
+ "binary": "cinder-scheduler",
+ "zone": "nova",
+ "state": "up",
+ "updated_at": "2017-06-29T05:50:35.000000",
+ "host": "devstack",
+ "disabled_reason": null
+ },
+ {
+ "status": "enabled",
+ "binary": "cinder-backup",
+ "zone": "nova",
+ "state": "up",
+ "updated_at": "2017-06-29T05:50:42.000000",
+ "host": "devstack",
+ "disabled_reason": null
+ },
+ {
+ "status": "enabled",
+ "binary": "cinder-volume",
+ "zone": "nova",
+ "frozen": false,
+ "state": "up",
+ "updated_at": "2017-06-29T05:50:39.000000",
+ "cluster": null,
+ "host": "devstack@lvmdriver-1",
+ "replication_status": "disabled",
+ "active_backend_id": null,
+ "disabled_reason": null
+ }]
+}
+`
+
+// First service from the ServiceListBody
+var FirstFakeService = services.Service{
+ Binary: "cinder-scheduler",
+ DisabledReason: "",
+ Host: "devstack",
+ State: "up",
+ Status: "enabled",
+ UpdatedAt: time.Date(2017, 6, 29, 5, 50, 35, 0, time.UTC),
+ Zone: "nova",
+}
+
+// Second service from the ServiceListBody
+var SecondFakeService = services.Service{
+ Binary: "cinder-backup",
+ DisabledReason: "",
+ Host: "devstack",
+ State: "up",
+ Status: "enabled",
+ UpdatedAt: time.Date(2017, 6, 29, 5, 50, 42, 0, time.UTC),
+ Zone: "nova",
+}
+
+// Third service from the ServiceListBody
+var ThirdFakeService = services.Service{
+ ActiveBackendID: "",
+ Binary: "cinder-volume",
+ Cluster: "",
+ DisabledReason: "",
+ Frozen: false,
+ Host: "devstack@lvmdriver-1",
+ ReplicationStatus: "disabled",
+ State: "up",
+ Status: "enabled",
+ UpdatedAt: time.Date(2017, 6, 29, 5, 50, 39, 0, time.UTC),
+ Zone: "nova",
+}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, ServiceListBody)
+ })
+}
diff --git a/openstack/blockstorage/extensions/services/testing/requests_test.go b/openstack/blockstorage/extensions/services/testing/requests_test.go
new file mode 100644
index 0000000000..4178c23699
--- /dev/null
+++ b/openstack/blockstorage/extensions/services/testing/requests_test.go
@@ -0,0 +1,41 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/services"
+ "github.com/gophercloud/gophercloud/pagination"
+ "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListServices(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ HandleListSuccessfully(t)
+
+ pages := 0
+ err := services.List(client.ServiceClient(), services.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := services.ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 3 {
+ t.Fatalf("Expected 3 services, got %d", len(actual))
+ }
+ testhelper.CheckDeepEquals(t, FirstFakeService, actual[0])
+ testhelper.CheckDeepEquals(t, SecondFakeService, actual[1])
+ testhelper.CheckDeepEquals(t, ThirdFakeService, actual[2])
+
+ return true, nil
+ })
+
+ testhelper.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
diff --git a/openstack/blockstorage/extensions/services/urls.go b/openstack/blockstorage/extensions/services/urls.go
new file mode 100644
index 0000000000..61d794007e
--- /dev/null
+++ b/openstack/blockstorage/extensions/services/urls.go
@@ -0,0 +1,7 @@
+package services
+
+import "github.com/gophercloud/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("os-services")
+}
diff --git a/openstack/client.go b/openstack/client.go
index e83051e5a7..405821dfeb 100644
--- a/openstack/client.go
+++ b/openstack/client.go
@@ -399,3 +399,17 @@ func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "container")
}
+
+// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering
+// package.
+func NewClusteringV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ return initClientOpts(client, eo, "clustering")
+}
+
+// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging
+// service.
+func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
+ sc, err := initClientOpts(client, eo, "messaging")
+ sc.MoreHeaders = map[string]string{"Client-ID": clientID}
+ return sc, err
+}
diff --git a/openstack/clustering/v1/policies/doc.go b/openstack/clustering/v1/policies/doc.go
new file mode 100644
index 0000000000..90830c8dcf
--- /dev/null
+++ b/openstack/clustering/v1/policies/doc.go
@@ -0,0 +1,52 @@
+/*
+Package policies provides information and interaction with the policies through
+the OpenStack Clustering service.
+
+Example to List Policies
+
+ listOpts := policies.ListOpts{
+ Limit: 2,
+ }
+
+ allPages, err := policies.List(clusteringClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allPolicies, err := policies.ExtractPolicies(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, policy := range allPolicies {
+ fmt.Printf("%+v\n", policy)
+ }
+
+
+Example to Create a policy
+
+ opts := policies.CreateOpts{
+ Name: "new_policy",
+ Spec: policies.Spec{
+ Description: "new policy description",
+ Properties: map[string]interface{}{
+ "hooks": map[string]interface{}{
+ "type": "zaqar",
+ "params": map[string]interface{}{
+ "queue": "my_zaqar_queue",
+ },
+ "timeout": 10,
+ },
+ },
+ Type: "senlin.policy.deletion",
+ Version: "1.1",
+ },
+ }
+
+ createdPolicy, err := policies.Create(client, opts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+*/
+package policies
diff --git a/openstack/clustering/v1/policies/requests.go b/openstack/clustering/v1/policies/requests.go
new file mode 100644
index 0000000000..6a6f1657b7
--- /dev/null
+++ b/openstack/clustering/v1/policies/requests.go
@@ -0,0 +1,97 @@
+package policies
+
+import (
+ "net/http"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder Builder.
+type ListOptsBuilder interface {
+ ToPolicyListQuery() (string, error)
+}
+
+// ListOpts params
+type ListOpts struct {
+ // Limit limits the number of Policies to return.
+ Limit int `q:"limit"`
+
+ // Marker and Limit control paging. Marker instructs List where to start listing from.
+ Marker string `q:"marker"`
+
+ // Sorts the response by one or more attribute and optional sort direction combinations.
+ Sort string `q:"sort"`
+
+ // GlobalProject indicates whether to include resources for all projects or resources for the current project
+ GlobalProject *bool `q:"global_project"`
+
+ // Name to filter the response by the specified name property of the object
+ Name string `q:"name"`
+
+ // Filter the response by the specified type property of the object
+ Type string `q:"type"`
+}
+
+// ToPolicyListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPolicyListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List instructs OpenStack to retrieve a list of policies.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := policyListURL(client)
+ if opts != nil {
+ query, err := opts.ToPolicyListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ p := PolicyPage{pagination.MarkerPageBase{PageResult: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ })
+}
+
+// CreateOpts params
+type CreateOpts struct {
+ Name string `json:"name"`
+ Spec Spec `json:"spec"`
+}
+
+// ToPolicyCreateMap formats a CreateOpts into a body map.
+func (opts CreateOpts) ToPolicyCreateMap() (map[string]interface{}, error) {
+ b, err := gophercloud.BuildRequestBody(opts, "")
+ if err != nil {
+ return nil, err
+ }
+
+ return map[string]interface{}{"policy": b}, nil
+}
+
+// Create makes a request against the API to create a policy
+func Create(client *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+ b, err := opts.ToPolicyCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(policyCreateURL(client), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{201},
+ })
+ return
+}
+
+// Create makes a request against the API to delete a policy
+func Delete(client *gophercloud.ServiceClient, policyID string) (r DeleteResult) {
+ var result *http.Response
+ result, r.Err = client.Delete(policyDeleteURL(client, policyID), &gophercloud.RequestOpts{
+ OkCodes: []int{204},
+ })
+ r.Header = result.Header
+ return
+}
diff --git a/openstack/clustering/v1/policies/results.go b/openstack/clustering/v1/policies/results.go
new file mode 100644
index 0000000000..a0a0356fa9
--- /dev/null
+++ b/openstack/clustering/v1/policies/results.go
@@ -0,0 +1,160 @@
+package policies
+
+import (
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "time"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// Policy represents a clustering policy in the Openstack cloud
+type Policy struct {
+ CreatedAt time.Time `json:"-"`
+ Data map[string]interface{} `json:"data"`
+ Domain string `json:"domain"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Project string `json:"project"`
+ Spec Spec `json:"spec"`
+ Type string `json:"type"`
+ UpdatedAt time.Time `json:"-"`
+ User string `json:"user"`
+}
+
+type Spec struct {
+ Description string `json:"description"`
+ Properties map[string]interface{} `json:"properties"`
+ Type string `json:"type"`
+ Version string `json:"version"`
+}
+
+// ExtractPolicies interprets a page of results as a slice of Policy.
+func ExtractPolicies(r pagination.Page) ([]Policy, error) {
+ var s struct {
+ Policies []Policy `json:"policies"`
+ }
+ err := (r.(PolicyPage)).ExtractInto(&s)
+ return s.Policies, err
+}
+
+// PolicyPage contains a list page of all policies from a List call.
+type PolicyPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty determines if a PolicyPage contains any results.
+func (page PolicyPage) IsEmpty() (bool, error) {
+ policies, err := ExtractPolicies(page)
+ return len(policies) == 0, err
+}
+
+// LastMarker returns the last policy ID in a ListResult.
+func (r PolicyPage) LastMarker() (string, error) {
+ policies, err := ExtractPolicies(r)
+ if err != nil {
+ return "", err
+ }
+ if len(policies) == 0 {
+ return "", nil
+ }
+ return policies[len(policies)-1].ID, nil
+}
+
+const RFC3339WithZ = "2006-01-02T15:04:05Z"
+
+func (r *Policy) UnmarshalJSON(b []byte) error {
+ type tmp Policy
+ var s struct {
+ tmp
+ CreatedAt string `json:"created_at,omitempty"`
+ UpdatedAt string `json:"updated_at,omitempty"`
+ }
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ *r = Policy(s.tmp)
+
+ if s.CreatedAt != "" {
+ r.CreatedAt, err = time.Parse(gophercloud.RFC3339MilliNoZ, s.CreatedAt)
+ if err != nil {
+ r.CreatedAt, err = time.Parse(RFC3339WithZ, s.CreatedAt)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ if s.UpdatedAt != "" {
+ r.UpdatedAt, err = time.Parse(gophercloud.RFC3339MilliNoZ, s.UpdatedAt)
+ if err != nil {
+ r.UpdatedAt, err = time.Parse(RFC3339WithZ, s.UpdatedAt)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func (r *Spec) UnmarshalJSON(b []byte) error {
+ type tmp Spec
+ var s struct {
+ tmp
+ Version interface{} `json:"version"`
+ }
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ *r = Spec(s.tmp)
+
+ switch t := s.Version.(type) {
+ case float64:
+ if t == 1 {
+ r.Version = fmt.Sprintf("%.1f", t)
+ } else {
+ r.Version = strconv.FormatFloat(t, 'f', -1, 64)
+ }
+ case string:
+ r.Version = t
+ }
+
+ return nil
+}
+
+type policyResult struct {
+ gophercloud.Result
+}
+
+func (r CreateResult) Extract() (*Policy, error) {
+ var s struct {
+ Policy *Policy `json:"policy"`
+ }
+ err := r.ExtractInto(&s)
+
+ return s.Policy, err
+}
+
+type CreateResult struct {
+ policyResult
+}
+
+type DeleteResult struct {
+ gophercloud.HeaderResult
+}
+
+// DeleteResult contains the delete information from a delete policy request
+type DeleteHeader struct {
+ RequestID string `json:"X-OpenStack-Request-ID"`
+}
+
+func (r DeleteResult) Extract() (*DeleteHeader, error) {
+ var s *DeleteHeader
+ err := r.HeaderResult.ExtractInto(&s)
+ return s, err
+}
diff --git a/openstack/clustering/v1/policies/testing/doc.go b/openstack/clustering/v1/policies/testing/doc.go
new file mode 100644
index 0000000000..61bc1c3b6d
--- /dev/null
+++ b/openstack/clustering/v1/policies/testing/doc.go
@@ -0,0 +1,2 @@
+// clustering_policies_v1
+package testing
diff --git a/openstack/clustering/v1/policies/testing/fixtures.go b/openstack/clustering/v1/policies/testing/fixtures.go
new file mode 100644
index 0000000000..7f2aaa92bb
--- /dev/null
+++ b/openstack/clustering/v1/policies/testing/fixtures.go
@@ -0,0 +1,239 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/policies"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const PolicyListBody1 = `
+{
+ "policies": [
+ {
+ "created_at": "2018-04-02T21:43:30.000000",
+ "data": {},
+ "domain": null,
+ "id": "PolicyListBodyID1",
+ "name": "delpol",
+ "project": "018cd0909fb44cd5bc9b7a3cd664920e",
+ "spec": {
+ "description": "A policy for choosing victim node(s) from a cluster for deletion.",
+ "properties": {
+ "criteria": "OLDEST_FIRST",
+ "destroy_after_deletion": true,
+ "grace_period": 60,
+ "reduce_desired_capacity": false
+ },
+ "type": "senlin.policy.deletion",
+ "version": 1
+ },
+ "type": "senlin.policy.deletion-1.0",
+ "updated_at": "2018-04-02T00:19:12Z",
+ "user": "fe43e41739154b72818565e0d2580819"
+ }
+ ]
+}
+`
+
+const PolicyListBody2 = `
+{
+ "policies": [
+ {
+ "created_at": "2018-04-02T22:29:36.000000",
+ "data": {},
+ "domain": null,
+ "id": "PolicyListBodyID2",
+ "name": "delpol2",
+ "project": "018cd0909fb44cd5bc9b7a3cd664920e",
+ "spec": {
+ "description": "A policy for choosing victim node(s) from a cluster for deletion.",
+ "properties": {
+ "criteria": "OLDEST_FIRST",
+ "destroy_after_deletion": true,
+ "grace_period": 60,
+ "reduce_desired_capacity": false
+ },
+ "type": "senlin.policy.deletion",
+ "version": "1.0"
+ },
+ "type": "senlin.policy.deletion-1.0",
+ "updated_at": "2018-04-02T23:15:11.000000",
+ "user": "fe43e41739154b72818565e0d2580819"
+ }
+ ]
+}
+`
+
+const PolicyCreateBody = `
+{
+ "policy": {
+ "created_at": "2018-04-04T00:18:36Z",
+ "data": {},
+ "domain": null,
+ "id": "b99b3ab4-3aa6-4fba-b827-69b88b9c544a",
+ "name": "delpol4",
+ "project": "018cd0909fb44cd5bc9b7a3cd664920e",
+ "spec": {
+ "description": "A policy for choosing victim node(s) from a cluster for deletion.",
+ "properties": {
+ "hooks": {
+ "params": {
+ "queue": "zaqar_queue_name"
+ },
+ "timeout": 180,
+ "type": "zaqar"
+ }
+ },
+ "type": "senlin.policy.deletion",
+ "version": 1.1
+ },
+ "type": "senlin.policy.deletion-1.1",
+ "updated_at": null,
+ "user": "fe43e41739154b72818565e0d2580819"
+ }
+}
+`
+
+const PolicyDeleteRequestID = "req-7328d1b0-9945-456f-b2cd-5166b77d14a8"
+
+var (
+ ExpectedPolicyCreatedAt1, _ = time.Parse(time.RFC3339, "2018-04-02T21:43:30.000000Z")
+ ExpectedPolicyUpdatedAt1, _ = time.Parse(time.RFC3339, "2018-04-02T00:19:12.000000Z")
+ ExpectedPolicyCreatedAt2, _ = time.Parse(time.RFC3339, "2018-04-02T22:29:36.000000Z")
+ ExpectedPolicyUpdatedAt2, _ = time.Parse(time.RFC3339, "2018-04-02T23:15:11.000000Z")
+ ExpectedCreatePolicyCreatedAt, _ = time.Parse(time.RFC3339, "2018-04-04T00:18:36.000000Z")
+ ZeroTime, _ = time.Parse(time.RFC3339, "1-01-01T00:00:00.000000Z")
+
+ // Policy ID to delete
+ PolicyIDtoDelete = "1"
+
+ ExpectedPolicies = [][]policies.Policy{
+ {
+ {
+ CreatedAt: ExpectedPolicyCreatedAt1,
+ Data: map[string]interface{}{},
+ Domain: "",
+ ID: "PolicyListBodyID1",
+ Name: "delpol",
+ Project: "018cd0909fb44cd5bc9b7a3cd664920e",
+
+ Spec: policies.Spec{
+ Description: "A policy for choosing victim node(s) from a cluster for deletion.",
+ Properties: map[string]interface{}{
+ "criteria": "OLDEST_FIRST",
+ "destroy_after_deletion": true,
+ "grace_period": float64(60),
+ "reduce_desired_capacity": false,
+ },
+ Type: "senlin.policy.deletion",
+ Version: "1.0",
+ },
+ Type: "senlin.policy.deletion-1.0",
+ User: "fe43e41739154b72818565e0d2580819",
+ UpdatedAt: ExpectedPolicyUpdatedAt1,
+ },
+ },
+ {
+ {
+ CreatedAt: ExpectedPolicyCreatedAt2,
+ Data: map[string]interface{}{},
+ Domain: "",
+ ID: "PolicyListBodyID2",
+ Name: "delpol2",
+ Project: "018cd0909fb44cd5bc9b7a3cd664920e",
+
+ Spec: policies.Spec{
+ Description: "A policy for choosing victim node(s) from a cluster for deletion.",
+ Properties: map[string]interface{}{
+ "criteria": "OLDEST_FIRST",
+ "destroy_after_deletion": true,
+ "grace_period": float64(60),
+ "reduce_desired_capacity": false,
+ },
+ Type: "senlin.policy.deletion",
+ Version: "1.0",
+ },
+ Type: "senlin.policy.deletion-1.0",
+ User: "fe43e41739154b72818565e0d2580819",
+ UpdatedAt: ExpectedPolicyUpdatedAt2,
+ },
+ },
+ }
+
+ ExpectedCreatePolicy = policies.Policy{
+ CreatedAt: ExpectedCreatePolicyCreatedAt,
+ Data: map[string]interface{}{},
+ Domain: "",
+ ID: "b99b3ab4-3aa6-4fba-b827-69b88b9c544a",
+ Name: "delpol4",
+ Project: "018cd0909fb44cd5bc9b7a3cd664920e",
+
+ Spec: policies.Spec{
+ Description: "A policy for choosing victim node(s) from a cluster for deletion.",
+ Properties: map[string]interface{}{
+ "hooks": map[string]interface{}{
+ "params": map[string]interface{}{
+ "queue": "zaqar_queue_name",
+ },
+ "timeout": float64(180),
+ "type": "zaqar",
+ },
+ },
+ Type: "senlin.policy.deletion",
+ Version: "1.1",
+ },
+ Type: "senlin.policy.deletion-1.1",
+ User: "fe43e41739154b72818565e0d2580819",
+ UpdatedAt: ZeroTime,
+ }
+)
+
+func HandlePolicyList(t *testing.T) {
+ th.Mux.HandleFunc("/v1/policies", 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)
+
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, PolicyListBody1)
+ case "PolicyListBodyID1":
+ fmt.Fprintf(w, PolicyListBody2)
+ case "PolicyListBodyID2":
+ fmt.Fprintf(w, `{"policies":[]}`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+}
+
+func HandlePolicyCreate(t *testing.T) {
+ th.Mux.HandleFunc("/v1/policies", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, PolicyCreateBody)
+ })
+}
+
+func HandlePolicyDelete(t *testing.T) {
+ th.Mux.HandleFunc("/v1/policies/"+PolicyIDtoDelete, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("X-OpenStack-Request-ID", PolicyDeleteRequestID)
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
diff --git a/openstack/clustering/v1/policies/testing/requests_test.go b/openstack/clustering/v1/policies/testing/requests_test.go
new file mode 100644
index 0000000000..f077a5459a
--- /dev/null
+++ b/openstack/clustering/v1/policies/testing/requests_test.go
@@ -0,0 +1,72 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/policies"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListPolicies(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandlePolicyList(t)
+
+ listOpts := policies.ListOpts{
+ Limit: 1,
+ }
+
+ count := 0
+ err := policies.List(fake.ServiceClient(), listOpts).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := policies.ExtractPolicies(page)
+ if err != nil {
+ t.Errorf("Failed to extract policies: %v", err)
+ return false, err
+ }
+
+ th.AssertDeepEquals(t, ExpectedPolicies[count], actual)
+ count++
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if count != 2 {
+ t.Errorf("Expected 2 pages, got %d", count)
+ }
+}
+
+func TestCreatePolicy(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandlePolicyCreate(t)
+
+ expected := ExpectedCreatePolicy
+
+ opts := policies.CreateOpts{
+ Name: ExpectedCreatePolicy.Name,
+ Spec: ExpectedCreatePolicy.Spec,
+ }
+
+ actual, err := policies.Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &expected, actual)
+}
+
+func TestDeletePolicy(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandlePolicyDelete(t)
+
+ actual, err := policies.Delete(fake.ServiceClient(), PolicyIDtoDelete).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, PolicyDeleteRequestID, actual.RequestID)
+}
diff --git a/openstack/clustering/v1/policies/urls.go b/openstack/clustering/v1/policies/urls.go
new file mode 100644
index 0000000000..d705e8a3fc
--- /dev/null
+++ b/openstack/clustering/v1/policies/urls.go
@@ -0,0 +1,20 @@
+package policies
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ apiVersion = "v1"
+ apiName = "policies"
+)
+
+func policyListURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL(apiVersion, apiName)
+}
+
+func policyCreateURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL(apiVersion, apiName)
+}
+
+func policyDeleteURL(client *gophercloud.ServiceClient, policyID string) string {
+ return client.ServiceURL(apiVersion, apiName, policyID)
+}
diff --git a/openstack/clustering/v1/policytypes/doc.go b/openstack/clustering/v1/policytypes/doc.go
new file mode 100644
index 0000000000..1886939f39
--- /dev/null
+++ b/openstack/clustering/v1/policytypes/doc.go
@@ -0,0 +1,30 @@
+/*
+Package policytypes lists all policy types and shows details for a policy type from the OpenStack
+Clustering Service.
+
+Example to list policy types
+
+ allPages, err := policytypes.List(clusteringClient).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allPolicyTypes, err := actions.ExtractPolicyTypes(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, policyType := range allPolicyTypes {
+ fmt.Printf("%+v\n", policyType)
+ }
+
+Example of get policy type details
+
+ policyTypeName := "senlin.policy.affinity-1.0"
+ policyTypeDetail, err := policyTypes.Get(clusteringClient, policyTypeName).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", policyTypeDetail)
+*/
+package policytypes
diff --git a/openstack/clustering/v1/policytypes/requests.go b/openstack/clustering/v1/policytypes/requests.go
new file mode 100644
index 0000000000..bb8a48d40f
--- /dev/null
+++ b/openstack/clustering/v1/policytypes/requests.go
@@ -0,0 +1,21 @@
+package policytypes
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// List makes a request against the API to list policy types.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ url := policyTypeListURL(client)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return PolicyTypePage{pagination.SinglePageBase(r)}
+ })
+}
+
+// Get makes a request against the API to get details for a policy type
+func Get(client *gophercloud.ServiceClient, policyTypeName string) (r GetResult) {
+ _, r.Err = client.Get(policyTypeGetURL(client, policyTypeName), &r.Body,
+ &gophercloud.RequestOpts{OkCodes: []int{200}})
+ return
+}
diff --git a/openstack/clustering/v1/policytypes/results.go b/openstack/clustering/v1/policytypes/results.go
new file mode 100644
index 0000000000..4d5d8e1c3f
--- /dev/null
+++ b/openstack/clustering/v1/policytypes/results.go
@@ -0,0 +1,63 @@
+package policytypes
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// PolicyType represents a clustering policy type in the Openstack cloud
+type PolicyType struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ SupportStatus map[string][]SupportStatusType `json:"support_status"`
+}
+
+// SupportStatusType represents the support status information for a clustering policy type
+type SupportStatusType struct {
+ Status string `json:"status"`
+ Since string `json:"since"`
+}
+
+// ExtractPolicyTypes interprets a page of results as a slice of PolicyTypes.
+func ExtractPolicyTypes(r pagination.Page) ([]PolicyType, error) {
+ var s struct {
+ PolicyTypes []PolicyType `json:"policy_types"`
+ }
+ err := (r.(PolicyTypePage)).ExtractInto(&s)
+ return s.PolicyTypes, err
+}
+
+// PolicyTypePage contains a single page of all policy types from a List call.
+type PolicyTypePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines if a PolicyType contains any results.
+func (page PolicyTypePage) IsEmpty() (bool, error) {
+ policyTypes, err := ExtractPolicyTypes(page)
+ return len(policyTypes) == 0, err
+}
+
+// PolicyTypeDetail represents the detailed policy type information for a clustering policy type
+type PolicyTypeDetail struct {
+ Name string `json:"name"`
+ Schema map[string]interface{} `json:"schema"`
+ SupportStatus map[string][]SupportStatusType `json:"support_status,omitempty"`
+}
+
+// Extract provides access to the individual policy type returned by Get and extracts PolicyTypeDetail
+func (r policyTypeResult) Extract() (*PolicyTypeDetail, error) {
+ var s struct {
+ PolicyType *PolicyTypeDetail `json:"policy_type"`
+ }
+ err := r.ExtractInto(&s)
+ return s.PolicyType, err
+}
+
+type policyTypeResult struct {
+ gophercloud.Result
+}
+
+type GetResult struct {
+ policyTypeResult
+}
diff --git a/openstack/clustering/v1/policytypes/testing/doc.go b/openstack/clustering/v1/policytypes/testing/doc.go
new file mode 100644
index 0000000000..5bd30a4704
--- /dev/null
+++ b/openstack/clustering/v1/policytypes/testing/doc.go
@@ -0,0 +1,2 @@
+// clustering_policytypes_v1
+package testing
diff --git a/openstack/clustering/v1/policytypes/testing/fixtures.go b/openstack/clustering/v1/policytypes/testing/fixtures.go
new file mode 100644
index 0000000000..3db56cdadc
--- /dev/null
+++ b/openstack/clustering/v1/policytypes/testing/fixtures.go
@@ -0,0 +1,229 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/policytypes"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const FakePolicyTypetoGet = "fake-policytype"
+
+const PolicyTypeBody = `
+{
+ "policy_types": [
+ {
+ "name": "senlin.policy.affinity",
+ "version": "1.0",
+ "support_status": {
+ "1.0": [
+ {
+ "status": "SUPPORTED",
+ "since": "2016.10"
+ }
+ ]
+ }
+ },
+ {
+ "name": "senlin.policy.health",
+ "version": "1.0",
+ "support_status": {
+ "1.0": [
+ {
+ "status": "EXPERIMENTAL",
+ "since": "2016.10"
+ }
+ ]
+ }
+ },
+ {
+ "name": "senlin.policy.scaling",
+ "version": "1.0",
+ "support_status": {
+ "1.0": [
+ {
+ "status": "SUPPORTED",
+ "since": "2016.04"
+ }
+ ]
+ }
+ },
+ {
+ "name": "senlin.policy.region_placement",
+ "version": "1.0",
+ "support_status": {
+ "1.0": [
+ {
+ "status": "EXPERIMENTAL",
+ "since": "2016.04"
+ },
+ {
+ "status": "SUPPORTED",
+ "since": "2016.10"
+ }
+ ]
+ }
+ }
+ ]
+}
+`
+
+const PolicyTypeDetailBody = `
+{
+ "policy_type": {
+ "name": "senlin.policy.batch-1.0",
+ "schema": {
+ "max_batch_size": {
+ "default": -1,
+ "description": "Maximum number of nodes that will be updated in parallel.",
+ "required": false,
+ "type": "Integer",
+ "updatable": false
+ },
+ "min_in_service": {
+ "default": 1,
+ "description": "Minimum number of nodes in service when performing updates.",
+ "required": false,
+ "type": "Integer",
+ "updatable": false
+ },
+ "pause_time": {
+ "default": 60,
+ "description": "Interval in seconds between update batches if any.",
+ "required": false,
+ "type": "Integer",
+ "updatable": false
+ }
+ },
+ "support_status": {
+ "1.0": [
+ {
+ "status": "EXPERIMENTAL",
+ "since": "2017.02"
+ }
+ ]
+ }
+ }
+}
+`
+
+var (
+ ExpectedPolicyTypes = []policytypes.PolicyType{
+ {
+ Name: "senlin.policy.affinity",
+ Version: "1.0",
+ SupportStatus: map[string][]policytypes.SupportStatusType{
+ "1.0": {
+ {
+ Status: "SUPPORTED",
+ Since: "2016.10",
+ },
+ },
+ },
+ },
+ {
+ Name: "senlin.policy.health",
+ Version: "1.0",
+ SupportStatus: map[string][]policytypes.SupportStatusType{
+ "1.0": {
+ {
+ Status: "EXPERIMENTAL",
+ Since: "2016.10",
+ },
+ },
+ },
+ },
+ {
+ Name: "senlin.policy.scaling",
+ Version: "1.0",
+ SupportStatus: map[string][]policytypes.SupportStatusType{
+ "1.0": {
+ {
+ Status: "SUPPORTED",
+ Since: "2016.04",
+ },
+ },
+ },
+ },
+ {
+ Name: "senlin.policy.region_placement",
+ Version: "1.0",
+ SupportStatus: map[string][]policytypes.SupportStatusType{
+ "1.0": {
+ {
+ Status: "EXPERIMENTAL",
+ Since: "2016.04",
+ },
+ {
+ Status: "SUPPORTED",
+ Since: "2016.10",
+ },
+ },
+ },
+ },
+ }
+
+ ExpectedPolicyTypeDetail = &policytypes.PolicyTypeDetail{
+ Name: "senlin.policy.batch-1.0",
+ Schema: map[string]interface{}{
+ "max_batch_size": map[string]interface{}{
+ "default": float64(-1),
+ "description": "Maximum number of nodes that will be updated in parallel.",
+ "required": false,
+ "type": "Integer",
+ "updatable": false,
+ },
+ "min_in_service": map[string]interface{}{
+ "default": float64(1),
+ "description": "Minimum number of nodes in service when performing updates.",
+ "required": false,
+ "type": "Integer",
+ "updatable": false,
+ },
+ "pause_time": map[string]interface{}{
+ "default": float64(60),
+ "description": "Interval in seconds between update batches if any.",
+ "required": false,
+ "type": "Integer",
+ "updatable": false,
+ },
+ },
+ SupportStatus: map[string][]policytypes.SupportStatusType{
+ "1.0": []policytypes.SupportStatusType{
+ {
+ Status: "EXPERIMENTAL",
+ Since: "2017.02",
+ },
+ },
+ },
+ }
+)
+
+func HandlePolicyTypeList(t *testing.T) {
+ th.Mux.HandleFunc("/v1/policy-types",
+ 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, PolicyTypeBody)
+ })
+}
+
+func HandlePolicyTypeGet(t *testing.T) {
+ th.Mux.HandleFunc("/v1/policy-types/"+FakePolicyTypetoGet,
+ 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, PolicyTypeDetailBody)
+ })
+}
diff --git a/openstack/clustering/v1/policytypes/testing/requests_test.go b/openstack/clustering/v1/policytypes/testing/requests_test.go
new file mode 100644
index 0000000000..87e42fab1a
--- /dev/null
+++ b/openstack/clustering/v1/policytypes/testing/requests_test.go
@@ -0,0 +1,49 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/policytypes"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListPolicyTypes(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandlePolicyTypeList(t)
+
+ count := 0
+ err := policytypes.List(fake.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := policytypes.ExtractPolicyTypes(page)
+ if err != nil {
+ t.Errorf("Failed to extract policy types: %v", err)
+ return false, err
+ }
+ th.AssertDeepEquals(t, ExpectedPolicyTypes, actual)
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestGetPolicyType(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandlePolicyTypeGet(t)
+
+ actual, err := policytypes.Get(fake.ServiceClient(), FakePolicyTypetoGet).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, ExpectedPolicyTypeDetail, actual)
+}
diff --git a/openstack/clustering/v1/policytypes/urls.go b/openstack/clustering/v1/policytypes/urls.go
new file mode 100644
index 0000000000..b291a95c70
--- /dev/null
+++ b/openstack/clustering/v1/policytypes/urls.go
@@ -0,0 +1,16 @@
+package policytypes
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ apiVersion = "v1"
+ apiName = "policy-types"
+)
+
+func policyTypeListURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL(apiVersion, apiName)
+}
+
+func policyTypeGetURL(client *gophercloud.ServiceClient, policyTypeName string) string {
+ return client.ServiceURL(apiVersion, apiName, policyTypeName)
+}
diff --git a/openstack/clustering/v1/webhooks/doc.go b/openstack/clustering/v1/webhooks/doc.go
new file mode 100644
index 0000000000..c76dc11ffb
--- /dev/null
+++ b/openstack/clustering/v1/webhooks/doc.go
@@ -0,0 +1,15 @@
+/*
+Package webhooks provides the ability to trigger an action represented by a webhook from the OpenStack Clustering
+Service.
+
+Example to Trigger webhook action
+
+ result, err := webhooks.Trigger(serviceClient(), "f93f83f6-762b-41b6-b757-80507834d394", webhooks.TriggerOpts{V: "1"}).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println("result", result)
+
+*/
+package webhooks
diff --git a/openstack/clustering/v1/webhooks/requests.go b/openstack/clustering/v1/webhooks/requests.go
new file mode 100644
index 0000000000..0b38861b98
--- /dev/null
+++ b/openstack/clustering/v1/webhooks/requests.go
@@ -0,0 +1,53 @@
+package webhooks
+
+import (
+ "net/url"
+
+ "github.com/gophercloud/gophercloud"
+ "golang.org/x/crypto/openpgp/errors"
+)
+
+// TriggerOpts represents options used for triggering an action
+type TriggerOpts struct {
+ V string `q:"V,required"`
+ Params map[string]string
+}
+
+// TriggerOptsBuilder Query string builder interface for webhooks
+type TriggerOptsBuilder interface {
+ ToWebhookTriggerQuery() (string, error)
+}
+
+// Query string builder for webhooks
+func (opts TriggerOpts) ToWebhookTriggerQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ params := q.Query()
+
+ for k, v := range opts.Params {
+ params.Add(k, v)
+ }
+
+ q = &url.URL{RawQuery: params.Encode()}
+ return q.String(), err
+}
+
+// Trigger an action represented by a webhook.
+func Trigger(client *gophercloud.ServiceClient, id string, opts TriggerOptsBuilder) (r TriggerResult) {
+ url := triggerURL(client, id)
+ if opts != nil {
+ query, err := opts.ToWebhookTriggerQuery()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ url += query
+ } else {
+ r.Err = errors.InvalidArgumentError("Must contain V for TriggerOpt")
+ return
+ }
+
+ _, r.Err = client.Post(url, nil, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 201, 202},
+ })
+ return
+}
diff --git a/openstack/clustering/v1/webhooks/results.go b/openstack/clustering/v1/webhooks/results.go
new file mode 100644
index 0000000000..ccb06086a2
--- /dev/null
+++ b/openstack/clustering/v1/webhooks/results.go
@@ -0,0 +1,22 @@
+package webhooks
+
+import (
+ "github.com/gophercloud/gophercloud"
+)
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+type TriggerResult struct {
+ commonResult
+}
+
+// Extract retrieves the response action
+func (r commonResult) Extract() (string, error) {
+ var s struct {
+ Action string `json:"action"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Action, err
+}
diff --git a/openstack/clustering/v1/webhooks/testing/doc.go b/openstack/clustering/v1/webhooks/testing/doc.go
new file mode 100644
index 0000000000..9a759ee29a
--- /dev/null
+++ b/openstack/clustering/v1/webhooks/testing/doc.go
@@ -0,0 +1,2 @@
+// clustering_webhooks_v1
+package testing
diff --git a/openstack/clustering/v1/webhooks/testing/requests_test.go b/openstack/clustering/v1/webhooks/testing/requests_test.go
new file mode 100644
index 0000000000..9d0da6f335
--- /dev/null
+++ b/openstack/clustering/v1/webhooks/testing/requests_test.go
@@ -0,0 +1,136 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "encoding/json"
+
+ "github.com/gophercloud/gophercloud/openstack/clustering/v1/webhooks"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestWebhookTrigger(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "action": "290c44fa-c60f-4d75-a0eb-87433ba982a3"
+ }`)
+ })
+
+ triggerOpts := webhooks.TriggerOpts{
+ V: "1",
+ Params: map[string]string{
+ "foo": "bar",
+ "bar": "baz",
+ },
+ }
+ result, err := webhooks.Trigger(fake.ServiceClient(), "f93f83f6-762b-41b6-b757-80507834d394", triggerOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, result, "290c44fa-c60f-4d75-a0eb-87433ba982a3")
+}
+
+// Test webhook with params that generates query strings
+func TestWebhookParams(t *testing.T) {
+ triggerOpts := webhooks.TriggerOpts{
+ V: "1",
+ Params: map[string]string{
+ "foo": "bar",
+ "bar": "baz",
+ },
+ }
+ expected := "?V=1&bar=baz&foo=bar"
+ actual, err := triggerOpts.ToWebhookTriggerQuery()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, actual, expected)
+}
+
+// Nagative test case for returning invalid type (integer) for action id
+func TestWebhooksInvalidAction(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "action": 123
+ }`)
+ })
+
+ triggerOpts := webhooks.TriggerOpts{
+ V: "1",
+ Params: map[string]string{
+ "foo": "bar",
+ "bar": "baz",
+ },
+ }
+ _, err := webhooks.Trigger(fake.ServiceClient(), "f93f83f6-762b-41b6-b757-80507834d394", triggerOpts).Extract()
+ isValid := err.(*json.UnmarshalTypeError) == nil
+ th.AssertEquals(t, false, isValid)
+}
+
+// Negative test case for passing empty TriggerOpt
+func TestWebhookTriggerInvalidEmptyOpt(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "action": "290c44fa-c60f-4d75-a0eb-87433ba982a3"
+ }`)
+ })
+
+ _, err := webhooks.Trigger(fake.ServiceClient(), "f93f83f6-762b-41b6-b757-80507834d394", webhooks.TriggerOpts{}).Extract()
+ if err == nil {
+ t.Errorf("Expected error without V param")
+ }
+}
+
+// Negative test case for passing in nil for TriggerOpt
+func TestWebhookTriggerInvalidNilOpt(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+ {
+ "action": "290c44fa-c60f-4d75-a0eb-87433ba982a3"
+ }`)
+ })
+
+ _, err := webhooks.Trigger(fake.ServiceClient(), "f93f83f6-762b-41b6-b757-80507834d394", nil).Extract()
+
+ if err == nil {
+ t.Errorf("Expected error with nil param")
+ }
+}
diff --git a/openstack/clustering/v1/webhooks/urls.go b/openstack/clustering/v1/webhooks/urls.go
new file mode 100644
index 0000000000..563cf81122
--- /dev/null
+++ b/openstack/clustering/v1/webhooks/urls.go
@@ -0,0 +1,7 @@
+package webhooks
+
+import "github.com/gophercloud/gophercloud"
+
+func triggerURL(client *gophercloud.ServiceClient, id string) string {
+ return client.ServiceURL("v1", "webhooks", id, "trigger")
+}
diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go
index 43699cb916..97f1b033da 100644
--- a/openstack/compute/v2/extensions/aggregates/doc.go
+++ b/openstack/compute/v2/extensions/aggregates/doc.go
@@ -1,7 +1,51 @@
/*
-Package aggregates returns information about the host aggregates in the
+Package aggregates manages information about the host aggregates in the
OpenStack cloud.
+Example of Create Aggregate
+
+ opts := aggregates.CreateOpts{
+ Name: "name",
+ AvailabilityZone: "london",
+ }
+
+ aggregate, err := aggregates.Create(computeClient, opts).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", aggregate)
+
+Example of Show Aggregate Details
+
+ aggregateID := 42
+ aggregate, err := aggregates.Get(computeClient, aggregateID).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", aggregate)
+
+Example of Delete Aggregate
+
+ aggregateID := 32
+ err := aggregates.Delete(computeClient, aggregateID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example of Update Aggregate
+
+ aggregateID := 42
+ opts := aggregates.UpdateOpts{
+ Name: "new_name",
+ AvailabilityZone: "nova2",
+ }
+
+ aggregate, err := aggregates.Update(computeClient, aggregateID, opts).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", aggregate)
+
Example of Retrieving list of all aggregates
allPages, err := aggregates.List(computeClient).AllPages()
@@ -17,5 +61,45 @@ Example of Retrieving list of all aggregates
for _, aggregate := range allAggregates {
fmt.Printf("%+v\n", aggregate)
}
+
+Example of Add Host
+
+ aggregateID := 22
+ opts := aggregates.AddHostOpts{
+ Host: "newhost-cmp1",
+ }
+
+ aggregate, err := aggregates.AddHost(computeClient, aggregateID, opts).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", aggregate)
+
+Example of Remove Host
+
+ aggregateID := 22
+ opts := aggregates.RemoveHostOpts{
+ Host: "newhost-cmp1",
+ }
+
+ aggregate, err := aggregates.RemoveHost(computeClient, aggregateID, opts).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", aggregate)
+
+Example of Create or Update Metadata
+
+ aggregateID := 22
+ opts := aggregates.SetMetadata{
+ Metadata: map[string]string{"key": "value"},
+ }
+
+ aggregate, err := aggregates.SetMetadata(computeClient, aggregateID, opts).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v\n", aggregate)
+
*/
package aggregates
diff --git a/openstack/compute/v2/extensions/aggregates/requests.go b/openstack/compute/v2/extensions/aggregates/requests.go
index 5f31136c52..c37531c56a 100644
--- a/openstack/compute/v2/extensions/aggregates/requests.go
+++ b/openstack/compute/v2/extensions/aggregates/requests.go
@@ -1,6 +1,8 @@
package aggregates
import (
+ "strconv"
+
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@@ -11,3 +13,150 @@ func List(client *gophercloud.ServiceClient) pagination.Pager {
return AggregatesPage{pagination.SinglePageBase(r)}
})
}
+
+type CreateOpts struct {
+ // The name of the host aggregate.
+ Name string `json:"name" required:"true"`
+
+ // The availability zone of the host aggregate.
+ // You should use a custom availability zone rather than
+ // the default returned by the os-availability-zone API.
+ // The availability zone must not include ‘:’ in its name.
+ AvailabilityZone string `json:"availability_zone,omitempty"`
+}
+
+func (opts CreateOpts) ToAggregatesCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "aggregate")
+}
+
+// Create makes a request against the API to create an aggregate.
+func Create(client *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+ b, err := opts.ToAggregatesCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(aggregatesCreateURL(client), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// Delete makes a request against the API to delete an aggregate.
+func Delete(client *gophercloud.ServiceClient, aggregateID int) (r DeleteResult) {
+ v := strconv.Itoa(aggregateID)
+ _, r.Err = client.Delete(aggregatesDeleteURL(client, v), &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// Get makes a request against the API to get details for a specific aggregate.
+func Get(client *gophercloud.ServiceClient, aggregateID int) (r GetResult) {
+ v := strconv.Itoa(aggregateID)
+ _, r.Err = client.Get(aggregatesGetURL(client, v), &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+type UpdateOpts struct {
+ // The name of the host aggregate.
+ Name string `json:"name,omitempty"`
+
+ // The availability zone of the host aggregate.
+ // You should use a custom availability zone rather than
+ // the default returned by the os-availability-zone API.
+ // The availability zone must not include ‘:’ in its name.
+ AvailabilityZone string `json:"availability_zone,omitempty"`
+}
+
+func (opts UpdateOpts) ToAggregatesUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "aggregate")
+}
+
+// Update makes a request against the API to update a specific aggregate.
+func Update(client *gophercloud.ServiceClient, aggregateID int, opts UpdateOpts) (r UpdateResult) {
+ v := strconv.Itoa(aggregateID)
+
+ b, err := opts.ToAggregatesUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Put(aggregatesUpdateURL(client, v), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+type AddHostOpts struct {
+ // The name of the host.
+ Host string `json:"host" required:"true"`
+}
+
+func (opts AddHostOpts) ToAggregatesAddHostMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "add_host")
+}
+
+// AddHost makes a request against the API to add host to a specific aggregate.
+func AddHost(client *gophercloud.ServiceClient, aggregateID int, opts AddHostOpts) (r ActionResult) {
+ v := strconv.Itoa(aggregateID)
+
+ b, err := opts.ToAggregatesAddHostMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(aggregatesAddHostURL(client, v), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+type RemoveHostOpts struct {
+ // The name of the host.
+ Host string `json:"host" required:"true"`
+}
+
+func (opts RemoveHostOpts) ToAggregatesRemoveHostMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "remove_host")
+}
+
+// RemoveHost makes a request against the API to remove host from a specific aggregate.
+func RemoveHost(client *gophercloud.ServiceClient, aggregateID int, opts RemoveHostOpts) (r ActionResult) {
+ v := strconv.Itoa(aggregateID)
+
+ b, err := opts.ToAggregatesRemoveHostMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(aggregatesRemoveHostURL(client, v), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+type SetMetadataOpts struct {
+ Metadata map[string]interface{} `json:"metadata" required:"true"`
+}
+
+func (opts SetMetadataOpts) ToSetMetadataMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "set_metadata")
+}
+
+// SetMetadata makes a request against the API to set metadata to a specific aggregate.
+func SetMetadata(client *gophercloud.ServiceClient, aggregateID int, opts SetMetadataOpts) (r ActionResult) {
+ v := strconv.Itoa(aggregateID)
+
+ b, err := opts.ToSetMetadataMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(aggregatesSetMetadataURL(client, v), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
diff --git a/openstack/compute/v2/extensions/aggregates/results.go b/openstack/compute/v2/extensions/aggregates/results.go
index 19fc4443d6..2ab0cf22f0 100644
--- a/openstack/compute/v2/extensions/aggregates/results.go
+++ b/openstack/compute/v2/extensions/aggregates/results.go
@@ -1,6 +1,12 @@
package aggregates
-import "github.com/gophercloud/gophercloud/pagination"
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
// Aggregate represents a host aggregate in the OpenStack cloud.
type Aggregate struct {
@@ -18,6 +24,43 @@ type Aggregate struct {
// Name of the aggregate.
Name string `json:"name"`
+
+ // The date and time when the resource was created.
+ CreatedAt time.Time `json:"-"`
+
+ // The date and time when the resource was updated,
+ // if the resource has not been updated, this field will show as null.
+ UpdatedAt time.Time `json:"-"`
+
+ // The date and time when the resource was deleted,
+ // if the resource has not been deleted yet, this field will be null.
+ DeletedAt time.Time `json:"-"`
+
+ // A boolean indicates whether this aggregate is deleted or not,
+ // if it has not been deleted, false will appear.
+ Deleted bool `json:"deleted"`
+}
+
+// UnmarshalJSON to override default
+func (r *Aggregate) UnmarshalJSON(b []byte) error {
+ type tmp Aggregate
+ var s struct {
+ tmp
+ CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
+ UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+ DeletedAt gophercloud.JSONRFC3339MilliNoZ `json:"deleted_at"`
+ }
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ *r = Aggregate(s.tmp)
+
+ r.CreatedAt = time.Time(s.CreatedAt)
+ r.UpdatedAt = time.Time(s.UpdatedAt)
+ r.DeletedAt = time.Time(s.DeletedAt)
+
+ return nil
}
// AggregatesPage represents a single page of all Aggregates from a List
@@ -40,3 +83,35 @@ func ExtractAggregates(p pagination.Page) ([]Aggregate, error) {
err := (p.(AggregatesPage)).ExtractInto(&a)
return a.Aggregates, err
}
+
+type aggregatesResult struct {
+ gophercloud.Result
+}
+
+func (r aggregatesResult) Extract() (*Aggregate, error) {
+ var s struct {
+ Aggregate *Aggregate `json:"aggregate"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Aggregate, err
+}
+
+type CreateResult struct {
+ aggregatesResult
+}
+
+type GetResult struct {
+ aggregatesResult
+}
+
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+type UpdateResult struct {
+ aggregatesResult
+}
+
+type ActionResult struct {
+ aggregatesResult
+}
diff --git a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go
index 758da2fb00..9ae71d2230 100644
--- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go
+++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go
@@ -3,7 +3,9 @@ package testing
import (
"fmt"
"net/http"
+ "strconv"
"testing"
+ "time"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/aggregates"
th "github.com/gophercloud/gophercloud/testhelper"
@@ -44,23 +46,216 @@ const AggregateListBody = `
}
`
-// First aggregate from the AggregateListBody
-var FirstFakeAggregate = aggregates.Aggregate{
- AvailabilityZone: "",
- Hosts: []string{},
- ID: 1,
- Metadata: map[string]string{},
- Name: "test-aggregate1",
+const AggregateCreateBody = `
+{
+ "aggregate": {
+ "availability_zone": "london",
+ "created_at": "2016-12-27T22:51:32.000000",
+ "deleted": false,
+ "deleted_at": null,
+ "id": 32,
+ "name": "name",
+ "updated_at": null
+ }
+}
+`
+
+const AggregateGetBody = `
+{
+ "aggregate": {
+ "name": "test-aggregate2",
+ "availability_zone": "test-az",
+ "deleted": false,
+ "created_at": "2017-12-22T10:16:07.000000",
+ "updated_at": null,
+ "hosts": [
+ "cmp0"
+ ],
+ "deleted_at": null,
+ "id": 4,
+ "metadata": {
+ "availability_zone": "test-az"
+ }
+ }
+}
+`
+
+const AggregateUpdateBody = `
+{
+ "aggregate": {
+ "name": "test-aggregate2",
+ "availability_zone": "nova2",
+ "deleted": false,
+ "created_at": "2017-12-22T10:12:06.000000",
+ "updated_at": "2017-12-23T10:18:00.000000",
+ "hosts": [],
+ "deleted_at": null,
+ "id": 1,
+ "metadata": {
+ "availability_zone": "nova2"
+ }
+ }
+}
+`
+
+const AggregateAddHostBody = `
+{
+ "aggregate": {
+ "name": "test-aggregate2",
+ "availability_zone": "test-az",
+ "deleted": false,
+ "created_at": "2017-12-22T10:16:07.000000",
+ "updated_at": null,
+ "hosts": [
+ "cmp0",
+ "cmp1"
+ ],
+ "deleted_at": null,
+ "id": 4,
+ "metadata": {
+ "availability_zone": "test-az"
+ }
+ }
}
+`
-// Second aggregate from the AggregateListBody
-var SecondFakeAggregate = aggregates.Aggregate{
- AvailabilityZone: "test-az",
- Hosts: []string{"cmp0"},
- ID: 4,
- Metadata: map[string]string{"availability_zone": "test-az"},
- Name: "test-aggregate2",
+const AggregateRemoveHostBody = `
+{
+ "aggregate": {
+ "name": "test-aggregate2",
+ "availability_zone": "nova2",
+ "deleted": false,
+ "created_at": "2017-12-22T10:12:06.000000",
+ "updated_at": "2017-12-23T10:18:00.000000",
+ "hosts": [],
+ "deleted_at": null,
+ "id": 1,
+ "metadata": {
+ "availability_zone": "nova2"
+ }
+ }
}
+`
+
+const AggregateSetMetadataBody = `
+{
+ "aggregate": {
+ "name": "test-aggregate2",
+ "availability_zone": "test-az",
+ "deleted": false,
+ "created_at": "2017-12-22T10:16:07.000000",
+ "updated_at": "2017-12-23T10:18:00.000000",
+ "hosts": [
+ "cmp0"
+ ],
+ "deleted_at": null,
+ "id": 4,
+ "metadata": {
+ "availability_zone": "test-az",
+ "key": "value"
+ }
+ }
+}
+`
+
+var (
+ // First aggregate from the AggregateListBody
+ FirstFakeAggregate = aggregates.Aggregate{
+ AvailabilityZone: "",
+ Hosts: []string{},
+ ID: 1,
+ Metadata: map[string]string{},
+ Name: "test-aggregate1",
+ CreatedAt: time.Date(2017, 12, 22, 10, 12, 6, 0, time.UTC),
+ UpdatedAt: time.Time{},
+ DeletedAt: time.Time{},
+ Deleted: false,
+ }
+
+ // Second aggregate from the AggregateListBody
+ SecondFakeAggregate = aggregates.Aggregate{
+ AvailabilityZone: "test-az",
+ Hosts: []string{"cmp0"},
+ ID: 4,
+ Metadata: map[string]string{"availability_zone": "test-az"},
+ Name: "test-aggregate2",
+ CreatedAt: time.Date(2017, 12, 22, 10, 16, 7, 0, time.UTC),
+ UpdatedAt: time.Time{},
+ DeletedAt: time.Time{},
+ Deleted: false,
+ }
+
+ // Aggregate from the AggregateCreateBody
+ CreatedAggregate = aggregates.Aggregate{
+ AvailabilityZone: "london",
+ Hosts: nil,
+ ID: 32,
+ Metadata: nil,
+ Name: "name",
+ CreatedAt: time.Date(2016, 12, 27, 22, 51, 32, 0, time.UTC),
+ UpdatedAt: time.Time{},
+ DeletedAt: time.Time{},
+ Deleted: false,
+ }
+
+ // Aggregate ID to delete
+ AggregateIDtoDelete = 1
+
+ // Aggregate ID to get, from the AggregateGetBody
+ AggregateIDtoGet = SecondFakeAggregate.ID
+
+ // Aggregate ID to update
+ AggregateIDtoUpdate = FirstFakeAggregate.ID
+
+ // Updated aggregate
+ UpdatedAggregate = aggregates.Aggregate{
+ AvailabilityZone: "nova2",
+ Hosts: []string{},
+ ID: 1,
+ Metadata: map[string]string{"availability_zone": "nova2"},
+ Name: "test-aggregate2",
+ CreatedAt: time.Date(2017, 12, 22, 10, 12, 6, 0, time.UTC),
+ UpdatedAt: time.Date(2017, 12, 23, 10, 18, 0, 0, time.UTC),
+ DeletedAt: time.Time{},
+ Deleted: false,
+ }
+
+ AggregateWithAddedHost = aggregates.Aggregate{
+ AvailabilityZone: "test-az",
+ Hosts: []string{"cmp0", "cmp1"},
+ ID: 4,
+ Metadata: map[string]string{"availability_zone": "test-az"},
+ Name: "test-aggregate2",
+ CreatedAt: time.Date(2017, 12, 22, 10, 16, 7, 0, time.UTC),
+ UpdatedAt: time.Time{},
+ DeletedAt: time.Time{},
+ Deleted: false,
+ }
+
+ AggregateWithRemovedHost = aggregates.Aggregate{
+ AvailabilityZone: "nova2",
+ Hosts: []string{},
+ ID: 1,
+ Metadata: map[string]string{"availability_zone": "nova2"},
+ Name: "test-aggregate2",
+ CreatedAt: time.Date(2017, 12, 22, 10, 12, 6, 0, time.UTC),
+ UpdatedAt: time.Date(2017, 12, 23, 10, 18, 0, 0, time.UTC),
+ DeletedAt: time.Time{},
+ Deleted: false,
+ }
+
+ AggregateWithUpdatedMetadata = aggregates.Aggregate{
+ AvailabilityZone: "test-az",
+ Hosts: []string{"cmp0"},
+ ID: 4,
+ Metadata: map[string]string{"availability_zone": "test-az", "key": "value"},
+ Name: "test-aggregate2",
+ CreatedAt: time.Date(2017, 12, 22, 10, 16, 7, 0, time.UTC),
+ UpdatedAt: time.Date(2017, 12, 23, 10, 18, 0, 0, time.UTC),
+ DeletedAt: time.Time{},
+ Deleted: false,
+ }
+)
// HandleListSuccessfully configures the test server to respond to a List request.
func HandleListSuccessfully(t *testing.T) {
@@ -72,3 +267,78 @@ func HandleListSuccessfully(t *testing.T) {
fmt.Fprintf(w, AggregateListBody)
})
}
+
+func HandleCreateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-aggregates", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, AggregateCreateBody)
+ })
+}
+
+func HandleDeleteSuccessfully(t *testing.T) {
+ v := strconv.Itoa(AggregateIDtoDelete)
+ th.Mux.HandleFunc("/os-aggregates/"+v, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusOK)
+ })
+}
+
+func HandleGetSuccessfully(t *testing.T) {
+ v := strconv.Itoa(AggregateIDtoGet)
+ th.Mux.HandleFunc("/os-aggregates/"+v, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, AggregateGetBody)
+ })
+}
+
+func HandleUpdateSuccessfully(t *testing.T) {
+ v := strconv.Itoa(AggregateIDtoUpdate)
+ th.Mux.HandleFunc("/os-aggregates/"+v, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, AggregateUpdateBody)
+ })
+}
+
+func HandleAddHostSuccessfully(t *testing.T) {
+ v := strconv.Itoa(AggregateWithAddedHost.ID)
+ th.Mux.HandleFunc("/os-aggregates/"+v+"/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, AggregateAddHostBody)
+ })
+}
+
+func HandleRemoveHostSuccessfully(t *testing.T) {
+ v := strconv.Itoa(AggregateWithRemovedHost.ID)
+ th.Mux.HandleFunc("/os-aggregates/"+v+"/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, AggregateRemoveHostBody)
+ })
+}
+
+func HandleSetMetadataSuccessfully(t *testing.T) {
+ v := strconv.Itoa(AggregateWithUpdatedMetadata.ID)
+ th.Mux.HandleFunc("/os-aggregates/"+v+"/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, AggregateSetMetadataBody)
+ })
+}
diff --git a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go
index 903b675c9f..bfd18614cc 100644
--- a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go
+++ b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go
@@ -5,13 +5,13 @@ import (
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/aggregates"
"github.com/gophercloud/gophercloud/pagination"
- "github.com/gophercloud/gophercloud/testhelper"
+ th "github.com/gophercloud/gophercloud/testhelper"
"github.com/gophercloud/gophercloud/testhelper/client"
)
func TestListAggregates(t *testing.T) {
- testhelper.SetupHTTP()
- defer testhelper.TeardownHTTP()
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
HandleListSuccessfully(t)
pages := 0
@@ -26,15 +26,124 @@ func TestListAggregates(t *testing.T) {
if len(actual) != 2 {
t.Fatalf("Expected 2 aggregates, got %d", len(actual))
}
- testhelper.CheckDeepEquals(t, FirstFakeAggregate, actual[0])
- testhelper.CheckDeepEquals(t, SecondFakeAggregate, actual[1])
+ th.CheckDeepEquals(t, FirstFakeAggregate, actual[0])
+ th.CheckDeepEquals(t, SecondFakeAggregate, actual[1])
return true, nil
})
- testhelper.AssertNoErr(t, err)
+ th.AssertNoErr(t, err)
if pages != 1 {
t.Errorf("Expected 1 page, saw %d", pages)
}
}
+
+func TestCreateAggregates(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreateSuccessfully(t)
+
+ expected := CreatedAggregate
+
+ opts := aggregates.CreateOpts{
+ Name: "name",
+ AvailabilityZone: "london",
+ }
+
+ actual, err := aggregates.Create(client.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &expected, actual)
+}
+
+func TestDeleteAggregates(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleDeleteSuccessfully(t)
+
+ err := aggregates.Delete(client.ServiceClient(), AggregateIDtoDelete).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestGetAggregates(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t)
+
+ expected := SecondFakeAggregate
+
+ actual, err := aggregates.Get(client.ServiceClient(), AggregateIDtoGet).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &expected, actual)
+}
+
+func TestUpdateAggregate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleUpdateSuccessfully(t)
+
+ expected := UpdatedAggregate
+
+ opts := aggregates.UpdateOpts{
+ Name: "test-aggregates2",
+ AvailabilityZone: "nova2",
+ }
+
+ actual, err := aggregates.Update(client.ServiceClient(), expected.ID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &expected, actual)
+}
+
+func TestAddHostAggregate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleAddHostSuccessfully(t)
+
+ expected := AggregateWithAddedHost
+
+ opts := aggregates.AddHostOpts{
+ Host: "cmp1",
+ }
+
+ actual, err := aggregates.AddHost(client.ServiceClient(), expected.ID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &expected, actual)
+}
+
+func TestRemoveHostAggregate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleRemoveHostSuccessfully(t)
+
+ expected := AggregateWithRemovedHost
+
+ opts := aggregates.RemoveHostOpts{
+ Host: "cmp1",
+ }
+
+ actual, err := aggregates.RemoveHost(client.ServiceClient(), expected.ID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &expected, actual)
+}
+
+func TestSetMetadataAggregate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleSetMetadataSuccessfully(t)
+
+ expected := AggregateWithUpdatedMetadata
+
+ opts := aggregates.SetMetadataOpts{
+ Metadata: map[string]interface{}{"key": "value"},
+ }
+
+ actual, err := aggregates.SetMetadata(client.ServiceClient(), expected.ID, opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/aggregates/urls.go b/openstack/compute/v2/extensions/aggregates/urls.go
index 88b15009fa..bb30c7fc90 100644
--- a/openstack/compute/v2/extensions/aggregates/urls.go
+++ b/openstack/compute/v2/extensions/aggregates/urls.go
@@ -5,3 +5,31 @@ import "github.com/gophercloud/gophercloud"
func aggregatesListURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-aggregates")
}
+
+func aggregatesCreateURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("os-aggregates")
+}
+
+func aggregatesDeleteURL(c *gophercloud.ServiceClient, aggregateID string) string {
+ return c.ServiceURL("os-aggregates", aggregateID)
+}
+
+func aggregatesGetURL(c *gophercloud.ServiceClient, aggregateID string) string {
+ return c.ServiceURL("os-aggregates", aggregateID)
+}
+
+func aggregatesUpdateURL(c *gophercloud.ServiceClient, aggregateID string) string {
+ return c.ServiceURL("os-aggregates", aggregateID)
+}
+
+func aggregatesAddHostURL(c *gophercloud.ServiceClient, aggregateID string) string {
+ return c.ServiceURL("os-aggregates", aggregateID, "action")
+}
+
+func aggregatesRemoveHostURL(c *gophercloud.ServiceClient, aggregateID string) string {
+ return c.ServiceURL("os-aggregates", aggregateID, "action")
+}
+
+func aggregatesSetMetadataURL(c *gophercloud.ServiceClient, aggregateID string) string {
+ return c.ServiceURL("os-aggregates", aggregateID, "action")
+}
diff --git a/openstack/compute/v2/extensions/availabilityzones/doc.go b/openstack/compute/v2/extensions/availabilityzones/doc.go
index 80464ba399..29b554d213 100644
--- a/openstack/compute/v2/extensions/availabilityzones/doc.go
+++ b/openstack/compute/v2/extensions/availabilityzones/doc.go
@@ -1,6 +1,9 @@
/*
-Package availabilityzones provides the ability to extend a server result with
-availability zone information. Example:
+Package availabilityzones provides the ability to get lists and detailed
+availability zone information and to extend a server result with
+availability zone information.
+
+Example of Extend server result with Availability Zone Information:
type ServerWithAZ struct {
servers.Server
@@ -22,5 +25,37 @@ availability zone information. Example:
for _, server := range allServers {
fmt.Println(server.AvailabilityZone)
}
+
+Example of Get Availability Zone Information
+
+ allPages, err := availabilityzones.List(computeClient).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, zoneInfo := range availabilityZoneInfo {
+ fmt.Printf("%+v\n", zoneInfo)
+ }
+
+Example of Get Detailed Availability Zone Information
+
+ allPages, err := availabilityzones.ListDetail(computeClient).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, zoneInfo := range availabilityZoneInfo {
+ fmt.Printf("%+v\n", zoneInfo)
+ }
*/
package availabilityzones
diff --git a/openstack/compute/v2/extensions/availabilityzones/requests.go b/openstack/compute/v2/extensions/availabilityzones/requests.go
new file mode 100644
index 0000000000..f9a2e86e03
--- /dev/null
+++ b/openstack/compute/v2/extensions/availabilityzones/requests.go
@@ -0,0 +1,20 @@
+package availabilityzones
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// List will return the existing availability zones.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+ return AvailabilityZonePage{pagination.SinglePageBase(r)}
+ })
+}
+
+// ListDetail will return the existing availability zones with detailed information.
+func ListDetail(client *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(client, listDetailURL(client), func(r pagination.PageResult) pagination.Page {
+ return AvailabilityZonePage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/openstack/compute/v2/extensions/availabilityzones/results.go b/openstack/compute/v2/extensions/availabilityzones/results.go
index ae87404137..d48a0ea858 100644
--- a/openstack/compute/v2/extensions/availabilityzones/results.go
+++ b/openstack/compute/v2/extensions/availabilityzones/results.go
@@ -1,8 +1,76 @@
package availabilityzones
-// ServerAvailabilityZoneExt is an extension to the base Server result which
-// includes the Availability Zone information.
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ServerAvailabilityZoneExt is an extension to the base Server object.
type ServerAvailabilityZoneExt struct {
// AvailabilityZone is the availabilty zone the server is in.
AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"`
}
+
+// ServiceState represents the state of a service in an AvailabilityZone.
+type ServiceState struct {
+ Active bool `json:"active"`
+ Available bool `json:"available"`
+ UpdatedAt time.Time `json:"-"`
+}
+
+// UnmarshalJSON to override default
+func (r *ServiceState) UnmarshalJSON(b []byte) error {
+ type tmp ServiceState
+ var s struct {
+ tmp
+ UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+ }
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ *r = ServiceState(s.tmp)
+
+ r.UpdatedAt = time.Time(s.UpdatedAt)
+
+ return nil
+}
+
+// Services is a map of services contained in an AvailabilityZone.
+type Services map[string]ServiceState
+
+// Hosts is map of hosts/nodes contained in an AvailabilityZone.
+// Each host can have multiple services.
+type Hosts map[string]Services
+
+// ZoneState represents the current state of the availability zone.
+type ZoneState struct {
+ // Returns true if the availability zone is available
+ Available bool `json:"available"`
+}
+
+// AvailabilityZone contains all the information associated with an OpenStack
+// AvailabilityZone.
+type AvailabilityZone struct {
+ Hosts Hosts `json:"hosts"`
+ // The availability zone name
+ ZoneName string `json:"zoneName"`
+ ZoneState ZoneState `json:"zoneState"`
+}
+
+type AvailabilityZonePage struct {
+ pagination.SinglePageBase
+}
+
+// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a
+// single page of results.
+func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) {
+ var s struct {
+ AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"`
+ }
+ err := (r.(AvailabilityZonePage)).ExtractInto(&s)
+ return s.AvailabilityZoneInfo, err
+}
diff --git a/openstack/compute/v2/extensions/availabilityzones/testing/doc.go b/openstack/compute/v2/extensions/availabilityzones/testing/doc.go
new file mode 100644
index 0000000000..a4408d7a0d
--- /dev/null
+++ b/openstack/compute/v2/extensions/availabilityzones/testing/doc.go
@@ -0,0 +1,2 @@
+// availabilityzones unittests
+package testing
diff --git a/openstack/compute/v2/extensions/availabilityzones/testing/fixtures.go b/openstack/compute/v2/extensions/availabilityzones/testing/fixtures.go
new file mode 100644
index 0000000000..9cc6d46379
--- /dev/null
+++ b/openstack/compute/v2/extensions/availabilityzones/testing/fixtures.go
@@ -0,0 +1,197 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ az "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const GetOutput = `
+{
+ "availabilityZoneInfo": [
+ {
+ "hosts": null,
+ "zoneName": "nova",
+ "zoneState": {
+ "available": true
+ }
+ }
+ ]
+}
+`
+
+const GetDetailOutput = `
+{
+ "availabilityZoneInfo": [
+ {
+ "hosts": {
+ "localhost": {
+ "nova-cert": {
+ "active": true,
+ "available": false,
+ "updated_at": "2017-10-14T17:03:39.000000"
+ },
+ "nova-conductor": {
+ "active": true,
+ "available": false,
+ "updated_at": "2017-10-14T17:04:09.000000"
+ },
+ "nova-consoleauth": {
+ "active": true,
+ "available": false,
+ "updated_at": "2017-10-14T17:04:18.000000"
+ },
+ "nova-scheduler": {
+ "active": true,
+ "available": false,
+ "updated_at": "2017-10-14T17:04:30.000000"
+ }
+ },
+ "openstack-acc-tests.novalocal": {
+ "nova-cert": {
+ "active": true,
+ "available": true,
+ "updated_at": "2018-01-04T04:11:19.000000"
+ },
+ "nova-conductor": {
+ "active": true,
+ "available": true,
+ "updated_at": "2018-01-04T04:11:22.000000"
+ },
+ "nova-consoleauth": {
+ "active": true,
+ "available": true,
+ "updated_at": "2018-01-04T04:11:20.000000"
+ },
+ "nova-scheduler": {
+ "active": true,
+ "available": true,
+ "updated_at": "2018-01-04T04:11:23.000000"
+ }
+ }
+ },
+ "zoneName": "internal",
+ "zoneState": {
+ "available": true
+ }
+ },
+ {
+ "hosts": {
+ "openstack-acc-tests.novalocal": {
+ "nova-compute": {
+ "active": true,
+ "available": true,
+ "updated_at": "2018-01-04T04:11:23.000000"
+ }
+ }
+ },
+ "zoneName": "nova",
+ "zoneState": {
+ "available": true
+ }
+ }
+ ]
+}`
+
+var AZResult = []az.AvailabilityZone{
+ {
+ Hosts: nil,
+ ZoneName: "nova",
+ ZoneState: az.ZoneState{Available: true},
+ },
+}
+
+var AZDetailResult = []az.AvailabilityZone{
+ {
+ Hosts: az.Hosts{
+ "localhost": az.Services{
+ "nova-cert": az.ServiceState{
+ Active: true,
+ Available: false,
+ UpdatedAt: time.Date(2017, 10, 14, 17, 3, 39, 0, time.UTC),
+ },
+ "nova-conductor": az.ServiceState{
+ Active: true,
+ Available: false,
+ UpdatedAt: time.Date(2017, 10, 14, 17, 4, 9, 0, time.UTC),
+ },
+ "nova-consoleauth": az.ServiceState{
+ Active: true,
+ Available: false,
+ UpdatedAt: time.Date(2017, 10, 14, 17, 4, 18, 0, time.UTC),
+ },
+ "nova-scheduler": az.ServiceState{
+ Active: true,
+ Available: false,
+ UpdatedAt: time.Date(2017, 10, 14, 17, 4, 30, 0, time.UTC),
+ },
+ },
+ "openstack-acc-tests.novalocal": az.Services{
+ "nova-cert": az.ServiceState{
+ Active: true,
+ Available: true,
+ UpdatedAt: time.Date(2018, 1, 4, 4, 11, 19, 0, time.UTC),
+ },
+ "nova-conductor": az.ServiceState{
+ Active: true,
+ Available: true,
+ UpdatedAt: time.Date(2018, 1, 4, 4, 11, 22, 0, time.UTC),
+ },
+ "nova-consoleauth": az.ServiceState{
+ Active: true,
+ Available: true,
+ UpdatedAt: time.Date(2018, 1, 4, 4, 11, 20, 0, time.UTC),
+ },
+ "nova-scheduler": az.ServiceState{
+ Active: true,
+ Available: true,
+ UpdatedAt: time.Date(2018, 1, 4, 4, 11, 23, 0, time.UTC),
+ },
+ },
+ },
+ ZoneName: "internal",
+ ZoneState: az.ZoneState{Available: true},
+ },
+ {
+ Hosts: az.Hosts{
+ "openstack-acc-tests.novalocal": az.Services{
+ "nova-compute": az.ServiceState{
+ Active: true,
+ Available: true,
+ UpdatedAt: time.Date(2018, 1, 4, 4, 11, 23, 0, time.UTC),
+ },
+ },
+ },
+ ZoneName: "nova",
+ ZoneState: az.ZoneState{Available: true},
+ },
+}
+
+// HandleGetSuccessfully configures the test server to respond to a Get request
+// for availability zone information.
+func HandleGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-availability-zone", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, GetOutput)
+ })
+}
+
+// HandleGetDetailSuccessfully configures the test server to respond to a Get request
+// for detailed availability zone information.
+func HandleGetDetailSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-availability-zone/detail", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, GetDetailOutput)
+ })
+}
diff --git a/openstack/compute/v2/extensions/availabilityzones/testing/requests_test.go b/openstack/compute/v2/extensions/availabilityzones/testing/requests_test.go
new file mode 100644
index 0000000000..8996d366d0
--- /dev/null
+++ b/openstack/compute/v2/extensions/availabilityzones/testing/requests_test.go
@@ -0,0 +1,41 @@
+package testing
+
+import (
+ "testing"
+
+ az "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/availabilityzones"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// Verifies that availability zones can be listed correctly
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleGetSuccessfully(t)
+
+ allPages, err := az.List(client.ServiceClient()).AllPages()
+ th.AssertNoErr(t, err)
+
+ actual, err := az.ExtractAvailabilityZones(allPages)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, AZResult, actual)
+}
+
+// Verifies that detailed availability zones can be listed correctly
+func TestListDetail(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleGetDetailSuccessfully(t)
+
+ allPages, err := az.ListDetail(client.ServiceClient()).AllPages()
+ th.AssertNoErr(t, err)
+
+ actual, err := az.ExtractAvailabilityZones(allPages)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, AZDetailResult, actual)
+}
diff --git a/openstack/compute/v2/extensions/availabilityzones/urls.go b/openstack/compute/v2/extensions/availabilityzones/urls.go
new file mode 100644
index 0000000000..9d99ec74b7
--- /dev/null
+++ b/openstack/compute/v2/extensions/availabilityzones/urls.go
@@ -0,0 +1,11 @@
+package availabilityzones
+
+import "github.com/gophercloud/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("os-availability-zone")
+}
+
+func listDetailURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("os-availability-zone", "detail")
+}
diff --git a/openstack/compute/v2/extensions/hypervisors/doc.go b/openstack/compute/v2/extensions/hypervisors/doc.go
index cf603a9f32..b8eb699edb 100644
--- a/openstack/compute/v2/extensions/hypervisors/doc.go
+++ b/openstack/compute/v2/extensions/hypervisors/doc.go
@@ -1,6 +1,16 @@
/*
-Package hypervisors returns details about the hypervisors in the OpenStack
-cloud.
+Package hypervisors returns details about list of hypervisors, shows details for a hypervisor
+and shows summary statistics for all hypervisors over all compute nodes in the OpenStack cloud.
+
+Example of Show Hypervisor Details
+
+ hypervisorID := 42
+ hypervisor, err := hypervisors.Get(computeClient, 42).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v\n", hypervisor)
Example of Retrieving Details of All Hypervisors
@@ -17,5 +27,25 @@ Example of Retrieving Details of All Hypervisors
for _, hypervisor := range allHypervisors {
fmt.Printf("%+v\n", hypervisor)
}
+
+Example of Show Hypervisor Statistics
+
+ hypervisorsStatistics, err := hypervisors.GetStatistics(computeClient).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v\n", hypervisorsStatistics)
+
+Example of Show Hypervisor Uptime
+
+ hypervisorID := 42
+ hypervisorUptime, err := hypervisors.GetUptime(computeClient, hypervisorID).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v\n", hypervisorUptime)
+
*/
package hypervisors
diff --git a/openstack/compute/v2/extensions/hypervisors/requests.go b/openstack/compute/v2/extensions/hypervisors/requests.go
index 57cc19a71f..b6f1c541cb 100644
--- a/openstack/compute/v2/extensions/hypervisors/requests.go
+++ b/openstack/compute/v2/extensions/hypervisors/requests.go
@@ -1,6 +1,8 @@
package hypervisors
import (
+ "strconv"
+
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@@ -11,3 +13,29 @@ func List(client *gophercloud.ServiceClient) pagination.Pager {
return HypervisorPage{pagination.SinglePageBase(r)}
})
}
+
+// Statistics makes a request against the API to get hypervisors statistics.
+func GetStatistics(client *gophercloud.ServiceClient) (r StatisticsResult) {
+ _, r.Err = client.Get(hypervisorsStatisticsURL(client), &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// Get makes a request against the API to get details for specific hypervisor.
+func Get(client *gophercloud.ServiceClient, hypervisorID int) (r HypervisorResult) {
+ v := strconv.Itoa(hypervisorID)
+ _, r.Err = client.Get(hypervisorsGetURL(client, v), &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// GetUptime makes a request against the API to get uptime for specific hypervisor.
+func GetUptime(client *gophercloud.ServiceClient, hypervisorID int) (r UptimeResult) {
+ v := strconv.Itoa(hypervisorID)
+ _, r.Err = client.Get(hypervisorsUptimeURL(client, v), &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
diff --git a/openstack/compute/v2/extensions/hypervisors/results.go b/openstack/compute/v2/extensions/hypervisors/results.go
index d4e87de083..7f3fafe1aa 100644
--- a/openstack/compute/v2/extensions/hypervisors/results.go
+++ b/openstack/compute/v2/extensions/hypervisors/results.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
+ "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@@ -189,3 +190,101 @@ func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) {
err := (p.(HypervisorPage)).ExtractInto(&h)
return h.Hypervisors, err
}
+
+type HypervisorResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any HypervisorResult as a Hypervisor, if possible.
+func (r HypervisorResult) Extract() (*Hypervisor, error) {
+ var s struct {
+ Hypervisor Hypervisor `json:"hypervisor"`
+ }
+ err := r.ExtractInto(&s)
+ return &s.Hypervisor, err
+}
+
+// Statistics represents a summary statistics for all enabled
+// hypervisors over all compute nodes in the OpenStack cloud.
+type Statistics struct {
+ // The number of hypervisors.
+ Count int `json:"count"`
+
+ // The current_workload is the number of tasks the hypervisor is responsible for
+ CurrentWorkload int `json:"current_workload"`
+
+ // The actual free disk on this hypervisor(in GB).
+ DiskAvailableLeast int `json:"disk_available_least"`
+
+ // The free disk remaining on this hypervisor(in GB).
+ FreeDiskGB int `json:"free_disk_gb"`
+
+ // The free RAM in this hypervisor(in MB).
+ FreeRamMB int `json:"free_ram_mb"`
+
+ // The disk in this hypervisor(in GB).
+ LocalGB int `json:"local_gb"`
+
+ // The disk used in this hypervisor(in GB).
+ LocalGBUsed int `json:"local_gb_used"`
+
+ // The memory of this hypervisor(in MB).
+ MemoryMB int `json:"memory_mb"`
+
+ // The memory used in this hypervisor(in MB).
+ MemoryMBUsed int `json:"memory_mb_used"`
+
+ // The total number of running vms on all hypervisors.
+ RunningVMs int `json:"running_vms"`
+
+ // The number of vcpu in this hypervisor.
+ VCPUs int `json:"vcpus"`
+
+ // The number of vcpu used in this hypervisor.
+ VCPUsUsed int `json:"vcpus_used"`
+}
+
+type StatisticsResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any StatisticsResult as a Statistics, if possible.
+func (r StatisticsResult) Extract() (*Statistics, error) {
+ var s struct {
+ Stats Statistics `json:"hypervisor_statistics"`
+ }
+ err := r.ExtractInto(&s)
+ return &s.Stats, err
+}
+
+// Uptime represents uptime and additional info for a specific hypervisor.
+type Uptime struct {
+ // The hypervisor host name provided by the Nova virt driver.
+ // For the Ironic driver, it is the Ironic node uuid.
+ HypervisorHostname string `json:"hypervisor_hostname"`
+
+ // The id of the hypervisor.
+ ID int `json:"id"`
+
+ // The state of the hypervisor. One of up or down.
+ State string `json:"state"`
+
+ // The status of the hypervisor. One of enabled or disabled.
+ Status string `json:"status"`
+
+ // The total uptime of the hypervisor and information about average load.
+ Uptime string `json:"uptime"`
+}
+
+type UptimeResult struct {
+ gophercloud.Result
+}
+
+// Extract interprets any UptimeResult as a Uptime, if possible.
+func (r UptimeResult) Extract() (*Uptime, error) {
+ var s struct {
+ Uptime Uptime `json:"hypervisor"`
+ }
+ err := r.ExtractInto(&s)
+ return &s.Uptime, err
+}
diff --git a/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go b/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go
index 45a32de18d..1dc05fb9b0 100644
--- a/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go
+++ b/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go
@@ -3,6 +3,7 @@ package testing
import (
"fmt"
"net/http"
+ "strconv"
"testing"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/hypervisors"
@@ -83,6 +84,81 @@ const HypervisorListBody = `
]
}`
+const HypervisorsStatisticsBody = `
+{
+ "hypervisor_statistics": {
+ "count": 1,
+ "current_workload": 0,
+ "disk_available_least": 0,
+ "free_disk_gb": 1028,
+ "free_ram_mb": 7680,
+ "local_gb": 1028,
+ "local_gb_used": 0,
+ "memory_mb": 8192,
+ "memory_mb_used": 512,
+ "running_vms": 0,
+ "vcpus": 2,
+ "vcpus_used": 0
+ }
+}
+`
+
+const HypervisorGetBody = `
+{
+ "hypervisor":{
+ "cpu_info":{
+ "arch":"x86_64",
+ "model":"Nehalem",
+ "vendor":"Intel",
+ "features":[
+ "pge",
+ "clflush"
+ ],
+ "topology":{
+ "cores":1,
+ "threads":1,
+ "sockets":4
+ }
+ },
+ "current_workload":0,
+ "status":"enabled",
+ "state":"up",
+ "disk_available_least":0,
+ "host_ip":"1.1.1.1",
+ "free_disk_gb":1028,
+ "free_ram_mb":7680,
+ "hypervisor_hostname":"fake-mini",
+ "hypervisor_type":"fake",
+ "hypervisor_version":2002000,
+ "id":1,
+ "local_gb":1028,
+ "local_gb_used":0,
+ "memory_mb":8192,
+ "memory_mb_used":512,
+ "running_vms":0,
+ "service":{
+ "host":"e6a37ee802d74863ab8b91ade8f12a67",
+ "id":2,
+ "disabled_reason":null
+ },
+ "vcpus":1,
+ "vcpus_used":0
+ }
+}
+`
+
+const HypervisorUptimeBody = `
+{
+ "hypervisor": {
+ "hypervisor_hostname": "fake-mini",
+ "id": 1,
+ "state": "up",
+ "status": "enabled",
+ "uptime": " 08:32:11 up 93 days, 18:25, 12 users, load average: 0.20, 0.12, 0.14"
+ }
+}
+`
+
var (
HypervisorFake = hypervisors.Hypervisor{
CPUInfo: hypervisors.CPUInfo{
@@ -123,8 +199,39 @@ var (
VCPUs: 1,
VCPUsUsed: 0,
}
+ HypervisorsStatisticsExpected = hypervisors.Statistics{
+ Count: 1,
+ CurrentWorkload: 0,
+ DiskAvailableLeast: 0,
+ FreeDiskGB: 1028,
+ FreeRamMB: 7680,
+ LocalGB: 1028,
+ LocalGBUsed: 0,
+ MemoryMB: 8192,
+ MemoryMBUsed: 512,
+ RunningVMs: 0,
+ VCPUs: 2,
+ VCPUsUsed: 0,
+ }
+ HypervisorUptimeExpected = hypervisors.Uptime{
+ HypervisorHostname: "fake-mini",
+ ID: 1,
+ State: "up",
+ Status: "enabled",
+ Uptime: " 08:32:11 up 93 days, 18:25, 12 users, load average: 0.20, 0.12, 0.14",
+ }
)
+func HandleHypervisorsStatisticsSuccessfully(t *testing.T) {
+ testhelper.Mux.HandleFunc("/os-hypervisors/statistics", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, HypervisorsStatisticsBody)
+ })
+}
+
func HandleHypervisorListSuccessfully(t *testing.T) {
testhelper.Mux.HandleFunc("/os-hypervisors/detail", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "GET")
@@ -134,3 +241,25 @@ func HandleHypervisorListSuccessfully(t *testing.T) {
fmt.Fprintf(w, HypervisorListBody)
})
}
+
+func HandleHypervisorGetSuccessfully(t *testing.T) {
+ v := strconv.Itoa(HypervisorFake.ID)
+ testhelper.Mux.HandleFunc("/os-hypervisors/"+v, func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, HypervisorGetBody)
+ })
+}
+
+func HandleHypervisorUptimeSuccessfully(t *testing.T) {
+ v := strconv.Itoa(HypervisorFake.ID)
+ testhelper.Mux.HandleFunc("/os-hypervisors/"+v+"/uptime", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, HypervisorUptimeBody)
+ })
+}
diff --git a/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go b/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go
index 1da3b1de50..95f9636c0c 100644
--- a/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go
+++ b/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go
@@ -51,3 +51,39 @@ func TestListAllHypervisors(t *testing.T) {
testhelper.CheckDeepEquals(t, HypervisorFake, actual[0])
testhelper.CheckDeepEquals(t, HypervisorFake, actual[1])
}
+
+func TestHypervisorsStatistics(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ HandleHypervisorsStatisticsSuccessfully(t)
+
+ expected := HypervisorsStatisticsExpected
+
+ actual, err := hypervisors.GetStatistics(client.ServiceClient()).Extract()
+ testhelper.AssertNoErr(t, err)
+ testhelper.CheckDeepEquals(t, &expected, actual)
+}
+
+func TestGetHypervisor(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ HandleHypervisorGetSuccessfully(t)
+
+ expected := HypervisorFake
+
+ actual, err := hypervisors.Get(client.ServiceClient(), expected.ID).Extract()
+ testhelper.AssertNoErr(t, err)
+ testhelper.CheckDeepEquals(t, &expected, actual)
+}
+
+func TestHypervisorsUptime(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ HandleHypervisorUptimeSuccessfully(t)
+
+ expected := HypervisorUptimeExpected
+
+ actual, err := hypervisors.GetUptime(client.ServiceClient(), HypervisorFake.ID).Extract()
+ testhelper.AssertNoErr(t, err)
+ testhelper.CheckDeepEquals(t, &expected, actual)
+}
diff --git a/openstack/compute/v2/extensions/hypervisors/urls.go b/openstack/compute/v2/extensions/hypervisors/urls.go
index 5e6f679e96..4c18ed43c4 100644
--- a/openstack/compute/v2/extensions/hypervisors/urls.go
+++ b/openstack/compute/v2/extensions/hypervisors/urls.go
@@ -5,3 +5,15 @@ import "github.com/gophercloud/gophercloud"
func hypervisorsListDetailURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-hypervisors", "detail")
}
+
+func hypervisorsStatisticsURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("os-hypervisors", "statistics")
+}
+
+func hypervisorsGetURL(c *gophercloud.ServiceClient, hypervisorID string) string {
+ return c.ServiceURL("os-hypervisors", hypervisorID)
+}
+
+func hypervisorsUptimeURL(c *gophercloud.ServiceClient, hypervisorID string) string {
+ return c.ServiceURL("os-hypervisors", hypervisorID, "uptime")
+}
diff --git a/openstack/compute/v2/extensions/keypairs/doc.go b/openstack/compute/v2/extensions/keypairs/doc.go
index dc7b65fda1..24c4607722 100644
--- a/openstack/compute/v2/extensions/keypairs/doc.go
+++ b/openstack/compute/v2/extensions/keypairs/doc.go
@@ -58,7 +58,7 @@ Example to Create a Server With a Key Pair
FlavorRef: "flavor-uuid",
}
- createOpts := keypairs.CreateOpts{
+ createOpts := keypairs.CreateOptsExt{
CreateOptsBuilder: serverCreateOpts,
KeyName: "keypair-name",
}
diff --git a/openstack/compute/v2/extensions/migrate/doc.go b/openstack/compute/v2/extensions/migrate/doc.go
index 86750d6c6f..cf3067716d 100644
--- a/openstack/compute/v2/extensions/migrate/doc.go
+++ b/openstack/compute/v2/extensions/migrate/doc.go
@@ -2,12 +2,29 @@
Package migrate provides functionality to migrate servers that have been
provisioned by the OpenStack Compute service.
-Example to Migrate a Server
+Example of Migrate Server (migrate Action)
serverID := "b16ba811-199d-4ffd-8839-ba96c1185a67"
err := migrate.Migrate(computeClient, serverID).ExtractErr()
if err != nil {
panic(err)
}
+
+Example of Live-Migrate Server (os-migrateLive Action)
+
+ serverID := "b16ba811-199d-4ffd-8839-ba96c1185a67"
+ host := "01c0cadef72d47e28a672a76060d492c"
+ blockMigration := false
+
+ migrationOpts := migrate.LiveMigrateOpts{
+ Host: &host,
+ BlockMigration: &blockMigration,
+ }
+
+ err := migrate.LiveMigrate(computeClient, serverID, migrationOpts).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
*/
package migrate
diff --git a/openstack/compute/v2/extensions/migrate/requests.go b/openstack/compute/v2/extensions/migrate/requests.go
index 9f263fa3ba..90ae62e381 100644
--- a/openstack/compute/v2/extensions/migrate/requests.go
+++ b/openstack/compute/v2/extensions/migrate/requests.go
@@ -9,3 +9,42 @@ func Migrate(client *gophercloud.ServiceClient, id string) (r MigrateResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"migrate": nil}, nil, nil)
return
}
+
+// LiveMigrateOptsBuilder allows extensions to add additional parameters to the
+// LiveMigrate request.
+type LiveMigrateOptsBuilder interface {
+ ToLiveMigrateMap() (map[string]interface{}, error)
+}
+
+// LiveMigrateOpts specifies parameters of live migrate action.
+type LiveMigrateOpts struct {
+ // The host to which to migrate the server.
+ // If this parameter is None, the scheduler chooses a host.
+ Host *string `json:"host"`
+
+ // Set to True to migrate local disks by using block migration.
+ // If the source or destination host uses shared storage and you set
+ // this value to True, the live migration fails.
+ BlockMigration *bool `json:"block_migration,omitempty"`
+
+ // Set to True to enable over commit when the destination host is checked
+ // for available disk space. Set to False to disable over commit. This setting
+ // affects only the libvirt virt driver.
+ DiskOverCommit *bool `json:"disk_over_commit,omitempty"`
+}
+
+// ToLiveMigrateMap constructs a request body from LiveMigrateOpts.
+func (opts LiveMigrateOpts) ToLiveMigrateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "os-migrateLive")
+}
+
+// LiveMigrate will initiate a live-migration (without rebooting) of the instance to another host.
+func LiveMigrate(client *gophercloud.ServiceClient, id string, opts LiveMigrateOptsBuilder) (r MigrateResult) {
+ b, err := opts.ToLiveMigrateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(actionURL(client, id), b, nil, nil)
+ return
+}
diff --git a/openstack/compute/v2/extensions/migrate/testing/fixtures.go b/openstack/compute/v2/extensions/migrate/testing/fixtures.go
index 8a59aa3478..1d2f5902c2 100644
--- a/openstack/compute/v2/extensions/migrate/testing/fixtures.go
+++ b/openstack/compute/v2/extensions/migrate/testing/fixtures.go
@@ -16,3 +16,18 @@ func mockMigrateResponse(t *testing.T, id string) {
w.WriteHeader(http.StatusAccepted)
})
}
+
+func mockLiveMigrateResponse(t *testing.T, id string) {
+ th.Mux.HandleFunc("/servers/"+id+"/action", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "os-migrateLive": {
+ "host": "01c0cadef72d47e28a672a76060d492c",
+ "block_migration": false,
+ "disk_over_commit": true
+ }
+ }`)
+ w.WriteHeader(http.StatusAccepted)
+ })
+}
diff --git a/openstack/compute/v2/extensions/migrate/testing/requests_test.go b/openstack/compute/v2/extensions/migrate/testing/requests_test.go
index 7d14365d6a..b6906b7839 100644
--- a/openstack/compute/v2/extensions/migrate/testing/requests_test.go
+++ b/openstack/compute/v2/extensions/migrate/testing/requests_test.go
@@ -19,3 +19,23 @@ func TestMigrate(t *testing.T) {
err := migrate.Migrate(client.ServiceClient(), serverID).ExtractErr()
th.AssertNoErr(t, err)
}
+
+func TestLiveMigrate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ mockLiveMigrateResponse(t, serverID)
+
+ host := "01c0cadef72d47e28a672a76060d492c"
+ blockMigration := false
+ diskOverCommit := true
+
+ migrationOpts := migrate.LiveMigrateOpts{
+ Host: &host,
+ BlockMigration: &blockMigration,
+ DiskOverCommit: &diskOverCommit,
+ }
+
+ err := migrate.LiveMigrate(client.ServiceClient(), serverID, migrationOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/compute/v2/extensions/quotasets/testing/fixtures.go b/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
index 53516413db..2915d31037 100644
--- a/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
+++ b/openstack/compute/v2/extensions/quotasets/testing/fixtures.go
@@ -121,7 +121,7 @@ var FirstQuotaSet = quotasets.QuotaSet{
// FirstQuotaDetailsSet is the first result in ListOutput.
var FirstQuotaDetailsSet = quotasets.QuotaDetailSet{
- ID: FirstTenantID,
+ ID: FirstTenantID,
InjectedFileContentBytes: quotasets.QuotaDetail{InUse: 0, Reserved: 0, Limit: 10240},
InjectedFilePathBytes: quotasets.QuotaDetail{InUse: 0, Reserved: 0, Limit: 255},
InjectedFiles: quotasets.QuotaDetail{InUse: 0, Reserved: 0, Limit: 5},
diff --git a/openstack/compute/v2/extensions/secgroups/requests.go b/openstack/compute/v2/extensions/secgroups/requests.go
index bcceaeacdd..8b93f08b07 100644
--- a/openstack/compute/v2/extensions/secgroups/requests.go
+++ b/openstack/compute/v2/extensions/secgroups/requests.go
@@ -172,12 +172,12 @@ func actionMap(prefix, groupName string) map[string]map[string]string {
// AddServer will associate a server and a security group, enforcing the
// rules of the group on the server.
func AddServer(client *gophercloud.ServiceClient, serverID, groupName string) (r AddServerResult) {
- _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), &r.Body, nil)
+ _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("add", groupName), nil, nil)
return
}
// RemoveServer will disassociate a server from a security group.
func RemoveServer(client *gophercloud.ServiceClient, serverID, groupName string) (r RemoveServerResult) {
- _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), &r.Body, nil)
+ _, r.Err = client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), nil, nil)
return
}
diff --git a/openstack/compute/v2/extensions/secgroups/testing/fixtures.go b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
index 536e7f8ea1..27bd56f364 100644
--- a/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
+++ b/openstack/compute/v2/extensions/secgroups/testing/fixtures.go
@@ -303,7 +303,6 @@ func mockAddServerToGroupResponse(t *testing.T, serverID string) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
- fmt.Fprintf(w, `{}`)
})
}
@@ -323,6 +322,5 @@ func mockRemoveServerFromGroupResponse(t *testing.T, serverID string) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
- fmt.Fprintf(w, `{}`)
})
}
diff --git a/openstack/compute/v2/extensions/services/doc.go b/openstack/compute/v2/extensions/services/doc.go
new file mode 100644
index 0000000000..2d38c42a95
--- /dev/null
+++ b/openstack/compute/v2/extensions/services/doc.go
@@ -0,0 +1,22 @@
+/*
+Package services returns information about the compute services in the OpenStack
+cloud.
+
+Example of Retrieving list of all services
+
+ allPages, err := services.List(computeClient).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allServices, err := services.ExtractServices(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, service := range allServices {
+ fmt.Printf("%+v\n", service)
+ }
+*/
+
+package services
diff --git a/openstack/compute/v2/extensions/services/requests.go b/openstack/compute/v2/extensions/services/requests.go
new file mode 100644
index 0000000000..d2e31f82d3
--- /dev/null
+++ b/openstack/compute/v2/extensions/services/requests.go
@@ -0,0 +1,13 @@
+package services
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// List makes a request against the API to list services.
+func List(client *gophercloud.ServiceClient) pagination.Pager {
+ return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
+ return ServicePage{pagination.SinglePageBase(r)}
+ })
+}
diff --git a/openstack/compute/v2/extensions/services/results.go b/openstack/compute/v2/extensions/services/results.go
new file mode 100644
index 0000000000..1ffc99cf9d
--- /dev/null
+++ b/openstack/compute/v2/extensions/services/results.go
@@ -0,0 +1,73 @@
+package services
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// Service represents a Compute service in the OpenStack cloud.
+type Service struct {
+ // The binary name of the service.
+ Binary string `json:"binary"`
+
+ // The reason for disabling a service.
+ DisabledReason string `json:"disabled_reason"`
+
+ // The name of the host.
+ Host string `json:"host"`
+
+ // The id of the service.
+ ID int `json:"id"`
+
+ // The state of the service. One of up or down.
+ State string `json:"state"`
+
+ // The status of the service. One of enabled or disabled.
+ Status string `json:"status"`
+
+ // The date and time when the resource was updated.
+ UpdatedAt time.Time `json:"-"`
+
+ // The availability zone name.
+ Zone string `json:"zone"`
+}
+
+// UnmarshalJSON to override default
+func (r *Service) UnmarshalJSON(b []byte) error {
+ type tmp Service
+ var s struct {
+ tmp
+ UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
+ }
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ *r = Service(s.tmp)
+
+ r.UpdatedAt = time.Time(s.UpdatedAt)
+
+ return nil
+}
+
+// ServicePage represents a single page of all Services from a List request.
+type ServicePage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a page of Services contains any results.
+func (page ServicePage) IsEmpty() (bool, error) {
+ services, err := ExtractServices(page)
+ return len(services) == 0, err
+}
+
+func ExtractServices(r pagination.Page) ([]Service, error) {
+ var s struct {
+ Service []Service `json:"services"`
+ }
+ err := (r.(ServicePage)).ExtractInto(&s)
+ return s.Service, err
+}
diff --git a/openstack/compute/v2/extensions/services/testing/fixtures.go b/openstack/compute/v2/extensions/services/testing/fixtures.go
new file mode 100644
index 0000000000..79e704a7a7
--- /dev/null
+++ b/openstack/compute/v2/extensions/services/testing/fixtures.go
@@ -0,0 +1,123 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/services"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// ServiceListBody is sample response to the List call
+const ServiceListBody = `
+{
+ "services": [
+ {
+ "id": 1,
+ "binary": "nova-scheduler",
+ "disabled_reason": "test1",
+ "host": "host1",
+ "state": "up",
+ "status": "disabled",
+ "updated_at": "2012-10-29T13:42:02.000000",
+ "forced_down": false,
+ "zone": "internal"
+ },
+ {
+ "id": 2,
+ "binary": "nova-compute",
+ "disabled_reason": "test2",
+ "host": "host1",
+ "state": "up",
+ "status": "disabled",
+ "updated_at": "2012-10-29T13:42:05.000000",
+ "forced_down": false,
+ "zone": "nova"
+ },
+ {
+ "id": 3,
+ "binary": "nova-scheduler",
+ "disabled_reason": null,
+ "host": "host2",
+ "state": "down",
+ "status": "enabled",
+ "updated_at": "2012-09-19T06:55:34.000000",
+ "forced_down": false,
+ "zone": "internal"
+ },
+ {
+ "id": 4,
+ "binary": "nova-compute",
+ "disabled_reason": "test4",
+ "host": "host2",
+ "state": "down",
+ "status": "disabled",
+ "updated_at": "2012-09-18T08:03:38.000000",
+ "forced_down": false,
+ "zone": "nova"
+ }
+ ]
+}
+`
+
+// First service from the ServiceListBody
+var FirstFakeService = services.Service{
+ Binary: "nova-scheduler",
+ DisabledReason: "test1",
+ Host: "host1",
+ ID: 1,
+ State: "up",
+ Status: "disabled",
+ UpdatedAt: time.Date(2012, 10, 29, 13, 42, 2, 0, time.UTC),
+ Zone: "internal",
+}
+
+// Second service from the ServiceListBody
+var SecondFakeService = services.Service{
+ Binary: "nova-compute",
+ DisabledReason: "test2",
+ Host: "host1",
+ ID: 2,
+ State: "up",
+ Status: "disabled",
+ UpdatedAt: time.Date(2012, 10, 29, 13, 42, 5, 0, time.UTC),
+ Zone: "nova",
+}
+
+// Third service from the ServiceListBody
+var ThirdFakeService = services.Service{
+ Binary: "nova-scheduler",
+ DisabledReason: "",
+ Host: "host2",
+ ID: 3,
+ State: "down",
+ Status: "enabled",
+ UpdatedAt: time.Date(2012, 9, 19, 6, 55, 34, 0, time.UTC),
+ Zone: "internal",
+}
+
+// Fourth service from the ServiceListBody
+var FourthFakeService = services.Service{
+ Binary: "nova-compute",
+ DisabledReason: "test4",
+ Host: "host2",
+ ID: 4,
+ State: "down",
+ Status: "disabled",
+ UpdatedAt: time.Date(2012, 9, 18, 8, 3, 38, 0, time.UTC),
+ Zone: "nova",
+}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-services", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, ServiceListBody)
+ })
+}
diff --git a/openstack/compute/v2/extensions/services/testing/requests_test.go b/openstack/compute/v2/extensions/services/testing/requests_test.go
new file mode 100644
index 0000000000..7f998814be
--- /dev/null
+++ b/openstack/compute/v2/extensions/services/testing/requests_test.go
@@ -0,0 +1,42 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/services"
+ "github.com/gophercloud/gophercloud/pagination"
+ "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestListServices(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+ HandleListSuccessfully(t)
+
+ pages := 0
+ err := services.List(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := services.ExtractServices(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 4 {
+ t.Fatalf("Expected 4 services, got %d", len(actual))
+ }
+ testhelper.CheckDeepEquals(t, FirstFakeService, actual[0])
+ testhelper.CheckDeepEquals(t, SecondFakeService, actual[1])
+ testhelper.CheckDeepEquals(t, ThirdFakeService, actual[2])
+ testhelper.CheckDeepEquals(t, FourthFakeService, actual[3])
+
+ return true, nil
+ })
+
+ testhelper.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
diff --git a/openstack/compute/v2/extensions/services/urls.go b/openstack/compute/v2/extensions/services/urls.go
new file mode 100644
index 0000000000..61d794007e
--- /dev/null
+++ b/openstack/compute/v2/extensions/services/urls.go
@@ -0,0 +1,7 @@
+package services
+
+import "github.com/gophercloud/gophercloud"
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("os-services")
+}
diff --git a/openstack/compute/v2/extensions/usage/doc.go b/openstack/compute/v2/extensions/usage/doc.go
new file mode 100644
index 0000000000..32e8643e4d
--- /dev/null
+++ b/openstack/compute/v2/extensions/usage/doc.go
@@ -0,0 +1,27 @@
+/*
+Package usage provides information and interaction with the
+SimpleTenantUsage extension for the OpenStack Compute service.
+
+Example to Retrieve Usage for a Single Tenant:
+ start := time.Date(2017, 01, 21, 10, 4, 20, 0, time.UTC)
+ end := time.Date(2017, 01, 21, 10, 4, 20, 0, time.UTC)
+
+ singleTenantOpts := usage.SingleTenantOpts{
+ Start: &start,
+ End: &end,
+ }
+
+ page, err := usage.SingleTenant(computeClient, tenantID, singleTenantOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ tenantUsage, err := usage.ExtractSingleTenant(page)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v\n", tenantUsage)
+
+*/
+package usage
diff --git a/openstack/compute/v2/extensions/usage/requests.go b/openstack/compute/v2/extensions/usage/requests.go
new file mode 100644
index 0000000000..ee66e2a212
--- /dev/null
+++ b/openstack/compute/v2/extensions/usage/requests.go
@@ -0,0 +1,54 @@
+package usage
+
+import (
+ "net/url"
+ "time"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// SingleTenant returns usage data about a single tenant.
+func SingleTenant(client *gophercloud.ServiceClient, tenantID string, opts SingleTenantOptsBuilder) pagination.Pager {
+ url := getTenantURL(client, tenantID)
+ if opts != nil {
+ query, err := opts.ToUsageSingleTenantQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return SingleTenantPage{pagination.SinglePageBase(r)}
+ })
+}
+
+// SingleTenantOpts are options for fetching usage of a single tenant.
+type SingleTenantOpts struct {
+ // The ending time to calculate usage statistics on compute and storage resources.
+ End *time.Time `q:"end"`
+
+ // The beginning time to calculate usage statistics on compute and storage resources.
+ Start *time.Time `q:"start"`
+}
+
+// SingleTenantOptsBuilder allows extensions to add additional parameters to the
+// SingleTenant request.
+type SingleTenantOptsBuilder interface {
+ ToUsageSingleTenantQuery() (string, error)
+}
+
+// ToUsageSingleTenantQuery formats a SingleTenantOpts into a query string.
+func (opts SingleTenantOpts) ToUsageSingleTenantQuery() (string, error) {
+ params := make(url.Values)
+ if opts.Start != nil {
+ params.Add("start", opts.Start.Format(gophercloud.RFC3339MilliNoZ))
+ }
+
+ if opts.End != nil {
+ params.Add("end", opts.End.Format(gophercloud.RFC3339MilliNoZ))
+ }
+
+ q := &url.URL{RawQuery: params.Encode()}
+ return q.String(), nil
+}
diff --git a/openstack/compute/v2/extensions/usage/results.go b/openstack/compute/v2/extensions/usage/results.go
new file mode 100644
index 0000000000..39661ba463
--- /dev/null
+++ b/openstack/compute/v2/extensions/usage/results.go
@@ -0,0 +1,137 @@
+package usage
+
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// TenantUsage is a set of usage information about a tenant over the sampling window
+type TenantUsage struct {
+ // ServerUsages is an array of ServerUsage maps
+ ServerUsages []ServerUsage `json:"server_usages"`
+
+ // Start is the beginning time to calculate usage statistics on compute and storage resources
+ Start time.Time `json:"-"`
+
+ // Stop is the ending time to calculate usage statistics on compute and storage resources
+ Stop time.Time `json:"-"`
+
+ // TenantID is the ID of the tenant whose usage is being reported on
+ TenantID string `json:"tenant_id"`
+
+ // TotalHours is the total duration that servers exist (in hours)
+ TotalHours float64 `json:"total_hours"`
+
+ // TotalLocalGBUsage multiplies the server disk size (in GiB) by hours the server exists, and then adding that all together for each server
+ TotalLocalGBUsage float64 `json:"total_local_gb_usage"`
+
+ // TotalMemoryMBUsage multiplies the server memory size (in MB) by hours the server exists, and then adding that all together for each server
+ TotalMemoryMBUsage float64 `json:"total_memory_mb_usage"`
+
+ // TotalVCPUsUsage multiplies the number of virtual CPUs of the server by hours the server exists, and then adding that all together for each server
+ TotalVCPUsUsage float64 `json:"total_vcpus_usage"`
+}
+
+// UnmarshalJSON sets *u to a copy of data.
+func (u *TenantUsage) UnmarshalJSON(b []byte) error {
+ type tmp TenantUsage
+ var s struct {
+ tmp
+ Start gophercloud.JSONRFC3339MilliNoZ `json:"start"`
+ Stop gophercloud.JSONRFC3339MilliNoZ `json:"stop"`
+ }
+
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+ *u = TenantUsage(s.tmp)
+
+ u.Start = time.Time(s.Start)
+ u.Stop = time.Time(s.Stop)
+
+ return nil
+}
+
+// ServerUsage is a detailed set of information about a specific instance inside a tenant
+type ServerUsage struct {
+ // EndedAt is the date and time when the server was deleted
+ EndedAt time.Time `json:"-"`
+
+ // Flavor is the display name of a flavor
+ Flavor string `json:"flavor"`
+
+ // Hours is the duration that the server exists in hours
+ Hours float64 `json:"hours"`
+
+ // InstanceID is the UUID of the instance
+ InstanceID string `json:"instance_id"`
+
+ // LocalGB is the sum of the root disk size of the server and the ephemeral disk size of it (in GiB)
+ LocalGB int `json:"local_gb"`
+
+ // MemoryMB is the memory size of the server (in MB)
+ MemoryMB int `json:"memory_mb"`
+
+ // Name is the name assigned to the server when it was created
+ Name string `json:"name"`
+
+ // StartedAt is the date and time when the server was started
+ StartedAt time.Time `json:"-"`
+
+ // State is the VM power state
+ State string `json:"state"`
+
+ // TenantID is the UUID of the tenant in a multi-tenancy cloud
+ TenantID string `json:"tenant_id"`
+
+ // Uptime is the uptime of the server in seconds
+ Uptime int `json:"uptime"`
+
+ // VCPUs is the number of virtual CPUs that the server uses
+ VCPUs int `json:"vcpus"`
+}
+
+// UnmarshalJSON sets *u to a copy of data.
+func (u *ServerUsage) UnmarshalJSON(b []byte) error {
+ type tmp ServerUsage
+ var s struct {
+ tmp
+ EndedAt gophercloud.JSONRFC3339MilliNoZ `json:"ended_at"`
+ StartedAt gophercloud.JSONRFC3339MilliNoZ `json:"started_at"`
+ }
+
+ if err := json.Unmarshal(b, &s); err != nil {
+ return err
+ }
+ *u = ServerUsage(s.tmp)
+
+ u.EndedAt = time.Time(s.EndedAt)
+ u.StartedAt = time.Time(s.StartedAt)
+
+ return nil
+}
+
+// SingleTenantPage stores a single, only page of TenantUsage results from a
+// SingleTenant call.
+type SingleTenantPage struct {
+ pagination.SinglePageBase
+}
+
+// IsEmpty determines whether or not a SingleTenantPage is empty.
+func (page SingleTenantPage) IsEmpty() (bool, error) {
+ ks, err := ExtractSingleTenant(page)
+ return ks == nil, err
+}
+
+// ExtractSingleTenant interprets a SingleTenantPage as a TenantUsage result.
+func ExtractSingleTenant(page pagination.Page) (*TenantUsage, error) {
+ var s struct {
+ TenantUsage *TenantUsage `json:"tenant_usage"`
+ TenantUsageLinks []gophercloud.Link `json:"tenant_usage_links"`
+ }
+ err := (page.(SingleTenantPage)).ExtractInto(&s)
+ return s.TenantUsage, err
+}
diff --git a/openstack/compute/v2/extensions/usage/testing/doc.go b/openstack/compute/v2/extensions/usage/testing/doc.go
new file mode 100644
index 0000000000..a3521795bb
--- /dev/null
+++ b/openstack/compute/v2/extensions/usage/testing/doc.go
@@ -0,0 +1,2 @@
+// simple tenant usage unit tests
+package testing
diff --git a/openstack/compute/v2/extensions/usage/testing/fixtures.go b/openstack/compute/v2/extensions/usage/testing/fixtures.go
new file mode 100644
index 0000000000..b7c1ae55f5
--- /dev/null
+++ b/openstack/compute/v2/extensions/usage/testing/fixtures.go
@@ -0,0 +1,137 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const FirstTenantID = "aabbccddeeff112233445566"
+
+// GetSingleTenant holds the fixtures for the content of the request for a
+// single tenant.
+const GetSingleTenant = `{
+ "tenant_usage": {
+ "server_usages": [
+ {
+ "ended_at": null,
+ "flavor": "m1.tiny",
+ "hours": 0.021675453333333334,
+ "instance_id": "a70096fd-8196-406b-86c4-045840f53ad7",
+ "local_gb": 1,
+ "memory_mb": 512,
+ "name": "jttest",
+ "started_at": "2017-11-30T03:23:43.000000",
+ "state": "active",
+ "tenant_id": "aabbccddeeff112233445566",
+ "uptime": 78,
+ "vcpus": 1
+ },
+ {
+ "ended_at": "2017-11-21T04:10:11.000000",
+ "flavor": "m1.acctest",
+ "hours": 0.33444444444444443,
+ "instance_id": "c04e38f2-dcee-4ca8-9466-7708d0a9b6dd",
+ "local_gb": 15,
+ "memory_mb": 512,
+ "name": "basic",
+ "started_at": "2017-11-21T03:50:07.000000",
+ "state": "terminated",
+ "tenant_id": "aabbccddeeff112233445566",
+ "uptime": 1204,
+ "vcpus": 1
+ },
+ {
+ "ended_at": "2017-11-30T03:21:21.000000",
+ "flavor": "m1.acctest",
+ "hours": 0.004166666666666667,
+ "instance_id": "ceb654fa-e0e8-44fb-8942-e4d0bfad3941",
+ "local_gb": 15,
+ "memory_mb": 512,
+ "name": "ACPTTESTJSxbPQAC34lTnBE1",
+ "started_at": "2017-11-30T03:21:06.000000",
+ "state": "terminated",
+ "tenant_id": "aabbccddeeff112233445566",
+ "uptime": 15,
+ "vcpus": 1
+ }
+ ],
+ "start": "2017-11-02T03:25:01.000000",
+ "stop": "2017-11-30T03:25:01.000000",
+ "tenant_id": "aabbccddeeff112233445566",
+ "total_hours": 1.25834212,
+ "total_local_gb_usage": 18.571675453333334,
+ "total_memory_mb_usage": 644.27116544,
+ "total_vcpus_usage": 1.25834212
+ }
+}`
+
+// HandleGetSingleTenantSuccessfully configures the test server to respond to a
+// Get request for a single tenant
+func HandleGetSingleTenantSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/os-simple-tenant-usage/"+FirstTenantID, func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprint(w, GetSingleTenant)
+ })
+}
+
+// SingleTenantUsageResults is the code fixture for GetSingleTenant.
+var SingleTenantUsageResults = usage.TenantUsage{
+ ServerUsages: []usage.ServerUsage{
+ {
+ Flavor: "m1.tiny",
+ Hours: 0.021675453333333334,
+ InstanceID: "a70096fd-8196-406b-86c4-045840f53ad7",
+ LocalGB: 1,
+ MemoryMB: 512,
+ Name: "jttest",
+ StartedAt: time.Date(2017, 11, 30, 3, 23, 43, 0, time.UTC),
+ State: "active",
+ TenantID: "aabbccddeeff112233445566",
+ Uptime: 78,
+ VCPUs: 1,
+ },
+ {
+ Flavor: "m1.acctest",
+ Hours: 0.33444444444444443,
+ InstanceID: "c04e38f2-dcee-4ca8-9466-7708d0a9b6dd",
+ LocalGB: 15,
+ MemoryMB: 512,
+ Name: "basic",
+ StartedAt: time.Date(2017, 11, 21, 3, 50, 7, 0, time.UTC),
+ EndedAt: time.Date(2017, 11, 21, 4, 10, 11, 0, time.UTC),
+ State: "terminated",
+ TenantID: "aabbccddeeff112233445566",
+ Uptime: 1204,
+ VCPUs: 1,
+ },
+ {
+ Flavor: "m1.acctest",
+ Hours: 0.004166666666666667,
+ InstanceID: "ceb654fa-e0e8-44fb-8942-e4d0bfad3941",
+ LocalGB: 15,
+ MemoryMB: 512,
+ Name: "ACPTTESTJSxbPQAC34lTnBE1",
+ StartedAt: time.Date(2017, 11, 30, 3, 21, 6, 0, time.UTC),
+ EndedAt: time.Date(2017, 11, 30, 3, 21, 21, 0, time.UTC),
+ State: "terminated",
+ TenantID: "aabbccddeeff112233445566",
+ Uptime: 15,
+ VCPUs: 1,
+ },
+ },
+ Start: time.Date(2017, 11, 2, 3, 25, 1, 0, time.UTC),
+ Stop: time.Date(2017, 11, 30, 3, 25, 1, 0, time.UTC),
+ TenantID: "aabbccddeeff112233445566",
+ TotalHours: 1.25834212,
+ TotalLocalGBUsage: 18.571675453333334,
+ TotalMemoryMBUsage: 644.27116544,
+ TotalVCPUsUsage: 1.25834212,
+}
diff --git a/openstack/compute/v2/extensions/usage/testing/requests_test.go b/openstack/compute/v2/extensions/usage/testing/requests_test.go
new file mode 100644
index 0000000000..1b43f12aec
--- /dev/null
+++ b/openstack/compute/v2/extensions/usage/testing/requests_test.go
@@ -0,0 +1,21 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestGetTenant(t *testing.T) {
+ var getOpts usage.SingleTenantOpts
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSingleTenantSuccessfully(t)
+ page, err := usage.SingleTenant(client.ServiceClient(), FirstTenantID, getOpts).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := usage.ExtractSingleTenant(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &SingleTenantUsageResults, actual)
+}
diff --git a/openstack/compute/v2/extensions/usage/urls.go b/openstack/compute/v2/extensions/usage/urls.go
new file mode 100644
index 0000000000..f172b62211
--- /dev/null
+++ b/openstack/compute/v2/extensions/usage/urls.go
@@ -0,0 +1,13 @@
+package usage
+
+import "github.com/gophercloud/gophercloud"
+
+const resourcePath = "os-simple-tenant-usage"
+
+func getURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL(resourcePath)
+}
+
+func getTenantURL(client *gophercloud.ServiceClient, tenantID string) string {
+ return client.ServiceURL(resourcePath, tenantID)
+}
diff --git a/openstack/compute/v2/flavors/doc.go b/openstack/compute/v2/flavors/doc.go
index 867d53a819..34d8764fad 100644
--- a/openstack/compute/v2/flavors/doc.go
+++ b/openstack/compute/v2/flavors/doc.go
@@ -73,6 +73,19 @@ Example to Grant Access to a Flavor
panic(err)
}
+Example to Remove/Revoke Access to a Flavor
+
+ flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
+
+ accessOpts := flavors.RemoveAccessOpts{
+ Tenant: "15153a0979884b59b0592248ef947921",
+ }
+
+ accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
Example to Create Extra Specs for a Flavor
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
@@ -99,5 +112,26 @@ Example to Get Extra Specs for a Flavor
fmt.Printf("%+v", extraSpecs)
+Example to Update Extra Specs for a Flavor
+
+ flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
+
+ updateOpts := flavors.ExtraSpecsOpts{
+ "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED",
+ }
+ updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf("%+v", updatedExtraSpec)
+
+Example to Delete an Extra Spec for a Flavor
+
+ flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
+ err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr()
+ if err != nil {
+ panic(err)
+ }
*/
package flavors
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
index 965d271d1d..4b406df957 100644
--- a/openstack/compute/v2/flavors/requests.go
+++ b/openstack/compute/v2/flavors/requests.go
@@ -165,7 +165,7 @@ func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager
// AddAccessOptsBuilder allows extensions to add additional parameters to the
// AddAccess requests.
type AddAccessOptsBuilder interface {
- ToAddAccessMap() (map[string]interface{}, error)
+ ToFlavorAddAccessMap() (map[string]interface{}, error)
}
// AddAccessOpts represents options for adding access to a flavor.
@@ -174,14 +174,44 @@ type AddAccessOpts struct {
Tenant string `json:"tenant"`
}
-// ToAddAccessMap constructs a request body from AddAccessOpts.
-func (opts AddAccessOpts) ToAddAccessMap() (map[string]interface{}, error) {
+// ToFlavorAddAccessMap constructs a request body from AddAccessOpts.
+func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "addTenantAccess")
}
// AddAccess grants a tenant/project access to a flavor.
func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) {
- b, err := opts.ToAddAccessMap()
+ b, err := opts.ToFlavorAddAccessMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// RemoveAccessOptsBuilder allows extensions to add additional parameters to the
+// RemoveAccess requests.
+type RemoveAccessOptsBuilder interface {
+ ToFlavorRemoveAccessMap() (map[string]interface{}, error)
+}
+
+// RemoveAccessOpts represents options for removing access to a flavor.
+type RemoveAccessOpts struct {
+ // Tenant is the project/tenant ID to grant access.
+ Tenant string `json:"tenant"`
+}
+
+// ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts.
+func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "removeTenantAccess")
+}
+
+// RemoveAccess removes/revokes a tenant/project access to a flavor.
+func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) {
+ b, err := opts.ToFlavorRemoveAccessMap()
if err != nil {
r.Err = err
return
@@ -206,21 +236,22 @@ func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string
// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the
// CreateExtraSpecs requests.
type CreateExtraSpecsOptsBuilder interface {
- ToExtraSpecsCreateMap() (map[string]interface{}, error)
+ ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error)
}
// ExtraSpecsOpts is a map that contains key-value pairs.
type ExtraSpecsOpts map[string]string
-// ToExtraSpecsCreateMap assembles a body for a Create request based on the
-// contents of a ExtraSpecsOpts
-func (opts ExtraSpecsOpts) ToExtraSpecsCreateMap() (map[string]interface{}, error) {
+// ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on
+// the contents of ExtraSpecsOpts.
+func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) {
return map[string]interface{}{"extra_specs": opts}, nil
}
-// CreateExtraSpecs will create or update the extra-specs key-value pairs for the specified Flavor
+// CreateExtraSpecs will create or update the extra-specs key-value pairs for
+// the specified Flavor.
func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) {
- b, err := opts.ToExtraSpecsCreateMap()
+ b, err := opts.ToFlavorExtraSpecsCreateMap()
if err != nil {
r.Err = err
return
@@ -231,6 +262,53 @@ func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts C
return
}
+// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to
+// the Update request.
+type UpdateExtraSpecOptsBuilder interface {
+ ToFlavorExtraSpecUpdateMap() (map[string]string, string, error)
+}
+
+// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on
+// the contents of a ExtraSpecOpts.
+func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) {
+ if len(opts) != 1 {
+ err := gophercloud.ErrInvalidInput{}
+ err.Argument = "flavors.ExtraSpecOpts"
+ err.Info = "Must have 1 and only one key-value pair"
+ return nil, "", err
+ }
+
+ var key string
+ for k := range opts {
+ key = k
+ }
+
+ return opts, key, nil
+}
+
+// UpdateExtraSpec will updates the value of the specified flavor's extra spec
+// for the key in opts.
+func UpdateExtraSpec(client *gophercloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) {
+ b, key, err := opts.ToFlavorExtraSpecUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// DeleteExtraSpec will delete the key-value pair with the given key for the given
+// flavor ID.
+func DeleteExtraSpec(client *gophercloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) {
+ _, r.Err = client.Delete(extraSpecDeleteURL(client, flavorID, key), &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
// IDFromName is a convienience function that returns a flavor's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go
index 4451be38c9..525cddaea2 100644
--- a/openstack/compute/v2/flavors/results.go
+++ b/openstack/compute/v2/flavors/results.go
@@ -66,6 +66,9 @@ type Flavor struct {
// IsPublic indicates whether the flavor is public.
IsPublic bool `json:"os-flavor-access:is_public"`
+
+ // Ephemeral is the amount of ephemeral disk space, measured in GB.
+ Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"`
}
func (r *Flavor) UnmarshalJSON(b []byte) error {
@@ -158,12 +161,18 @@ type accessResult struct {
gophercloud.Result
}
-// AddAccessResult is the response of an AddAccess operations. Call its
+// AddAccessResult is the response of an AddAccess operation. Call its
// Extract method to interpret it as a slice of FlavorAccess.
type AddAccessResult struct {
accessResult
}
+// RemoveAccessResult is the response of a RemoveAccess operation. Call its
+// Extract method to interpret it as a slice of FlavorAccess.
+type RemoveAccessResult struct {
+ accessResult
+}
+
// Extract provides access to the result of an access create or delete.
// The result will be all accesses that the flavor has.
func (r accessResult) Extract() ([]FlavorAccess, error) {
@@ -223,6 +232,18 @@ type GetExtraSpecResult struct {
extraSpecResult
}
+// UpdateExtraSpecResult contains the result of an Update operation. Call its
+// Extract method to interpret it as a map[string]interface.
+type UpdateExtraSpecResult struct {
+ extraSpecResult
+}
+
+// DeleteExtraSpecResult contains the result of a Delete operation. Call its
+// ExtractErr method to determine if the call succeeded or failed.
+type DeleteExtraSpecResult struct {
+ gophercloud.ErrResult
+}
+
// Extract interprets any extraSpecResult as an ExtraSpec, if possible.
func (r extraSpecResult) Extract() (map[string]string, error) {
var s map[string]string
diff --git a/openstack/compute/v2/flavors/testing/fixtures.go b/openstack/compute/v2/flavors/testing/fixtures.go
index 536a7c11d4..445f769b2b 100644
--- a/openstack/compute/v2/flavors/testing/fixtures.go
+++ b/openstack/compute/v2/flavors/testing/fixtures.go
@@ -19,13 +19,20 @@ const ExtraSpecsGetBody = `
}
`
-// ExtraSpecGetBody provides a GET result of a particular extra_spec for a flavor
+// GetExtraSpecBody provides a GET result of a particular extra_spec for a flavor
const GetExtraSpecBody = `
{
"hw:cpu_policy": "CPU-POLICY"
}
`
+// UpdatedExtraSpecBody provides an PUT result of a particular updated extra_spec for a flavor
+const UpdatedExtraSpecBody = `
+{
+ "hw:cpu_policy": "CPU-POLICY-2"
+}
+`
+
// ExtraSpecs is the expected extra_specs returned from GET on a flavor's extra_specs
var ExtraSpecs = map[string]string{
"hw:cpu_policy": "CPU-POLICY",
@@ -37,6 +44,11 @@ var ExtraSpec = map[string]string{
"hw:cpu_policy": "CPU-POLICY",
}
+// UpdatedExtraSpec is the expected extra_spec returned from PUT on a flavor's extra_specs
+var UpdatedExtraSpec = map[string]string{
+ "hw:cpu_policy": "CPU-POLICY-2",
+}
+
func HandleExtraSpecsListSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/flavors/1/os-extra_specs", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
@@ -78,3 +90,27 @@ func HandleExtraSpecsCreateSuccessfully(t *testing.T) {
fmt.Fprintf(w, ExtraSpecsGetBody)
})
}
+
+func HandleExtraSpecUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/flavors/1/os-extra_specs/hw:cpu_policy", 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, "Accept", "application/json")
+ th.TestJSONRequest(t, r, `{
+ "hw:cpu_policy": "CPU-POLICY-2"
+ }`)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, UpdatedExtraSpecBody)
+ })
+}
+
+func HandleExtraSpecDeleteSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/flavors/1/os-extra_specs/hw:cpu_policy", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.WriteHeader(http.StatusOK)
+ })
+}
diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go
index ef40e5973f..fba0d4776b 100644
--- a/openstack/compute/v2/flavors/testing/requests_test.go
+++ b/openstack/compute/v2/flavors/testing/requests_test.go
@@ -37,7 +37,8 @@ func TestListFlavors(t *testing.T) {
"disk": 1,
"ram": 512,
"swap":"",
- "os-flavor-access:is_public": true
+ "os-flavor-access:is_public": true,
+ "OS-FLV-EXT-DATA:ephemeral": 10
},
{
"id": "2",
@@ -46,7 +47,8 @@ func TestListFlavors(t *testing.T) {
"disk": 20,
"ram": 2048,
"swap": 1000,
- "os-flavor-access:is_public": true
+ "os-flavor-access:is_public": true,
+ "OS-FLV-EXT-DATA:ephemeral": 0
},
{
"id": "3",
@@ -55,7 +57,8 @@ func TestListFlavors(t *testing.T) {
"disk": 40,
"ram": 4096,
"swap": 1000,
- "os-flavor-access:is_public": false
+ "os-flavor-access:is_public": false,
+ "OS-FLV-EXT-DATA:ephemeral": 0
}
],
"flavors_links": [
@@ -84,9 +87,9 @@ func TestListFlavors(t *testing.T) {
}
expected := []flavors.Flavor{
- {ID: "1", Name: "m1.tiny", VCPUs: 1, Disk: 1, RAM: 512, Swap: 0, IsPublic: true},
- {ID: "2", Name: "m1.small", VCPUs: 1, Disk: 20, RAM: 2048, Swap: 1000, IsPublic: true},
- {ID: "3", Name: "m1.medium", VCPUs: 2, Disk: 40, RAM: 4096, Swap: 1000, IsPublic: false},
+ {ID: "1", Name: "m1.tiny", VCPUs: 1, Disk: 1, RAM: 512, Swap: 0, IsPublic: true, Ephemeral: 10},
+ {ID: "2", Name: "m1.small", VCPUs: 1, Disk: 20, RAM: 2048, Swap: 1000, IsPublic: true, Ephemeral: 0},
+ {ID: "3", Name: "m1.medium", VCPUs: 2, Disk: 40, RAM: 4096, Swap: 1000, IsPublic: false, Ephemeral: 0},
}
if !reflect.DeepEqual(expected, actual) {
@@ -300,6 +303,44 @@ func TestFlavorAccessAdd(t *testing.T) {
}
}
+func TestFlavorAccessRemove(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/flavors/12345678/action", 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, "accept", "application/json")
+ th.TestJSONRequest(t, r, `
+ {
+ "removeTenantAccess": {
+ "tenant": "2f954bcf047c4ee9b09a37d49ae6db54"
+ }
+ }
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `
+ {
+ "flavor_access": []
+ }
+ `)
+ })
+
+ expected := []flavors.FlavorAccess{}
+ removeAccessOpts := flavors.RemoveAccessOpts{
+ Tenant: "2f954bcf047c4ee9b09a37d49ae6db54",
+ }
+
+ actual, err := flavors.RemoveAccess(fake.ServiceClient(), "12345678", removeAccessOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ if !reflect.DeepEqual(expected, actual) {
+ t.Errorf("Expected %#v, but was %#v", expected, actual)
+ }
+}
+
func TestFlavorExtraSpecsList(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -336,3 +377,26 @@ func TestFlavorExtraSpecsCreate(t *testing.T) {
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, expected, actual)
}
+
+func TestFlavorExtraSpecUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleExtraSpecUpdateSuccessfully(t)
+
+ updateOpts := flavors.ExtraSpecsOpts{
+ "hw:cpu_policy": "CPU-POLICY-2",
+ }
+ expected := UpdatedExtraSpec
+ actual, err := flavors.UpdateExtraSpec(fake.ServiceClient(), "1", updateOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestFlavorExtraSpecDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleExtraSpecDeleteSuccessfully(t)
+
+ res := flavors.DeleteExtraSpec(fake.ServiceClient(), "1", "hw:cpu_policy")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go
index b74f81625d..8620dd78ad 100644
--- a/openstack/compute/v2/flavors/urls.go
+++ b/openstack/compute/v2/flavors/urls.go
@@ -39,3 +39,11 @@ func extraSpecsGetURL(client *gophercloud.ServiceClient, id, key string) string
func extraSpecsCreateURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id, "os-extra_specs")
}
+
+func extraSpecUpdateURL(client *gophercloud.ServiceClient, id, key string) string {
+ return client.ServiceURL("flavors", id, "os-extra_specs", key)
+}
+
+func extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string {
+ return client.ServiceURL("flavors", id, "os-extra_specs", key)
+}
diff --git a/openstack/container/v1/capsules/errors.go b/openstack/container/v1/capsules/errors.go
new file mode 100644
index 0000000000..347bbadb18
--- /dev/null
+++ b/openstack/container/v1/capsules/errors.go
@@ -0,0 +1,33 @@
+package capsules
+
+import (
+ "fmt"
+
+ "github.com/gophercloud/gophercloud"
+)
+
+type ErrInvalidEnvironment struct {
+ gophercloud.BaseError
+ Section string
+}
+
+func (e ErrInvalidEnvironment) Error() string {
+ return fmt.Sprintf("Environment has wrong section: %s", e.Section)
+}
+
+type ErrInvalidDataFormat struct {
+ gophercloud.BaseError
+}
+
+func (e ErrInvalidDataFormat) Error() string {
+ return fmt.Sprintf("Data in neither json nor yaml format.")
+}
+
+type ErrInvalidTemplateFormatVersion struct {
+ gophercloud.BaseError
+ Version string
+}
+
+func (e ErrInvalidTemplateFormatVersion) Error() string {
+ return fmt.Sprintf("Template format version not found.")
+}
diff --git a/openstack/container/v1/capsules/fixtures.go b/openstack/container/v1/capsules/fixtures.go
new file mode 100644
index 0000000000..e547263b94
--- /dev/null
+++ b/openstack/container/v1/capsules/fixtures.go
@@ -0,0 +1,166 @@
+package capsules
+
+// ValidJSONTemplate is a valid OpenStack Capsule template in JSON format
+const ValidJSONTemplate = `
+{
+ "capsuleVersion": "beta",
+ "kind": "capsule",
+ "metadata": {
+ "labels": {
+ "app": "web",
+ "app1": "web1"
+ },
+ "name": "template"
+ },
+ "restartPolicy": "Always",
+ "spec": {
+ "containers": [
+ {
+ "command": [
+ "/bin/bash"
+ ],
+ "env": {
+ "ENV1": "/usr/local/bin",
+ "ENV2": "/usr/bin"
+ },
+ "image": "ubuntu",
+ "imagePullPolicy": "ifnotpresent",
+ "ports": [
+ {
+ "containerPort": 80,
+ "hostPort": 80,
+ "name": "nginx-port",
+ "protocol": "TCP"
+ }
+ ],
+ "resources": {
+ "requests": {
+ "cpu": 1,
+ "memory": 1024
+ }
+ },
+ "workDir": "/root"
+ }
+ ]
+ }
+}
+`
+
+// ValidYAMLTemplate is a valid OpenStack Capsule template in YAML format
+const ValidYAMLTemplate = `
+capsuleVersion: beta
+kind: capsule
+metadata:
+ name: template
+ labels:
+ app: web
+ app1: web1
+restartPolicy: Always
+spec:
+ containers:
+ - image: ubuntu
+ command:
+ - "/bin/bash"
+ imagePullPolicy: ifnotpresent
+ workDir: /root
+ ports:
+ - name: nginx-port
+ containerPort: 80
+ hostPort: 80
+ protocol: TCP
+ resources:
+ requests:
+ cpu: 1
+ memory: 1024
+ env:
+ ENV1: /usr/local/bin
+ ENV2: /usr/bin
+`
+
+// ValidJSONTemplateParsed is the expected parsed version of ValidJSONTemplate
+var ValidJSONTemplateParsed = map[string]interface{}{
+ "capsuleVersion": "beta",
+ "kind": "capsule",
+ "restartPolicy": "Always",
+ "metadata": map[string]interface{}{
+ "name": "template",
+ "labels": map[string]string{
+ "app": "web",
+ "app1": "web1",
+ },
+ },
+ "spec": map[string]interface{}{
+ "containers": []map[string]interface{}{
+ map[string]interface{}{
+ "image": "ubuntu",
+ "command": []interface{}{
+ "/bin/bash",
+ },
+ "imagePullPolicy": "ifnotpresent",
+ "workDir": "/root",
+ "ports": []interface{}{
+ map[string]interface{}{
+ "name": "nginx-port",
+ "containerPort": float64(80),
+ "hostPort": float64(80),
+ "protocol": "TCP",
+ },
+ },
+ "resources": map[string]interface{}{
+ "requests": map[string]interface{}{
+ "cpu": float64(1),
+ "memory": float64(1024),
+ },
+ },
+ "env": map[string]interface{}{
+ "ENV1": "/usr/local/bin",
+ "ENV2": "/usr/bin",
+ },
+ },
+ },
+ },
+}
+
+// ValidYAMLTemplateParsed is the expected parsed version of ValidYAMLTemplate
+var ValidYAMLTemplateParsed = map[string]interface{}{
+ "capsuleVersion": "beta",
+ "kind": "capsule",
+ "restartPolicy": "Always",
+ "metadata": map[string]interface{}{
+ "name": "template",
+ "labels": map[string]string{
+ "app": "web",
+ "app1": "web1",
+ },
+ },
+ "spec": map[interface{}]interface{}{
+ "containers": []map[interface{}]interface{}{
+ map[interface{}]interface{}{
+ "image": "ubuntu",
+ "command": []interface{}{
+ "/bin/bash",
+ },
+ "imagePullPolicy": "ifnotpresent",
+ "workDir": "/root",
+ "ports": []interface{}{
+ map[interface{}]interface{}{
+ "name": "nginx-port",
+ "containerPort": 80,
+ "hostPort": 80,
+ "protocol": "TCP",
+ },
+ },
+ "resources": map[interface{}]interface{}{
+ "requests": map[interface{}]interface{}{
+ "cpu": 1,
+ "memory": 1024,
+ },
+ },
+ "env": map[interface{}]interface{}{
+ "ENV1": "/usr/local/bin",
+ "ENV2": "/usr/bin",
+ },
+ },
+ },
+ },
+}
diff --git a/openstack/container/v1/capsules/requests.go b/openstack/container/v1/capsules/requests.go
index 25b1ffa875..0b9c161f8e 100644
--- a/openstack/container/v1/capsules/requests.go
+++ b/openstack/container/v1/capsules/requests.go
@@ -4,6 +4,14 @@ import (
"github.com/gophercloud/gophercloud"
)
+// CreateOptsBuilder is the interface options structs have to satisfy in order
+// to be used in the main Create operation in this package. Since many
+// extensions decorate or modify the common logic, it is useful for them to
+// satisfy a basic interface in order for them to be used.
+type CreateOptsBuilder interface {
+ ToCapsuleCreateMap() (map[string]interface{}, error)
+}
+
// Get requests details on a single capsule, by ID.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
@@ -11,3 +19,38 @@ func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
})
return
}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // A structure that contains either the template file or url. Call the
+ // associated methods to extract the information relevant to send in a create request.
+ TemplateOpts *Template `json:"-" required:"true"`
+}
+
+// ToCapsuleCreateMap assembles a request body based on the contents of
+// a CreateOpts.
+func (opts CreateOpts) ToCapsuleCreateMap() (map[string]interface{}, error) {
+ b, err := gophercloud.BuildRequestBody(opts, "")
+ if err != nil {
+ return nil, err
+ }
+
+ if err := opts.TemplateOpts.Parse(); err != nil {
+ return nil, err
+ }
+ b["template"] = string(opts.TemplateOpts.Bin)
+
+ return b, nil
+}
+
+// Create implements create capsule request.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToCapsuleCreateMap()
+ if err != nil {
+ r.Err = err
+ return r
+ }
+ _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{202}})
+ return
+}
diff --git a/openstack/container/v1/capsules/results.go b/openstack/container/v1/capsules/results.go
index 26b56e45e1..cfc19663c1 100644
--- a/openstack/container/v1/capsules/results.go
+++ b/openstack/container/v1/capsules/results.go
@@ -23,6 +23,12 @@ type GetResult struct {
commonResult
}
+// CreateResult is the response from a Create operation. Call its Extract
+// method to interpret it as a Server.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
// Represents a Container Orchestration Engine Bay, i.e. a cluster
type Capsule struct {
// UUID for the capsule
diff --git a/openstack/container/v1/capsules/template.go b/openstack/container/v1/capsules/template.go
new file mode 100644
index 0000000000..7917221319
--- /dev/null
+++ b/openstack/container/v1/capsules/template.go
@@ -0,0 +1,6 @@
+package capsules
+
+// Template is a structure that represents OpenStack Heat templates
+type Template struct {
+ TE
+}
diff --git a/openstack/container/v1/capsules/template_test.go b/openstack/container/v1/capsules/template_test.go
new file mode 100644
index 0000000000..33f28de81a
--- /dev/null
+++ b/openstack/container/v1/capsules/template_test.go
@@ -0,0 +1,40 @@
+package capsules
+
+import (
+ "testing"
+
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestTemplateValidation(t *testing.T) {
+ templateJSON := new(Template)
+ templateJSON.Bin = []byte(ValidJSONTemplate)
+ err := templateJSON.Validate()
+ th.AssertNoErr(t, err)
+
+ templateYAML := new(Template)
+ templateYAML.Bin = []byte(ValidYAMLTemplate)
+ err = templateYAML.Validate()
+ th.AssertNoErr(t, err)
+}
+
+func TestTemplateParsing(t *testing.T) {
+ templateJSON := new(Template)
+ templateJSON.Bin = []byte(ValidJSONTemplate)
+ err := templateJSON.Parse()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, ValidJSONTemplateParsed, templateJSON.Parsed)
+
+ templateYAML := new(Template)
+ templateYAML.Bin = []byte(ValidYAMLTemplate)
+ err = templateYAML.Parse()
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, ValidYAMLTemplateParsed, templateYAML.Parsed)
+
+ templateInvalid := new(Template)
+ templateInvalid.Bin = []byte("Keep Austin Weird")
+ err = templateInvalid.Parse()
+ if err == nil {
+ t.Error("Template parsing did not catch invalid template")
+ }
+}
diff --git a/openstack/container/v1/capsules/testing/fixtures.go b/openstack/container/v1/capsules/testing/fixtures.go
index 017d12bc7e..d3d1d7fe0c 100644
--- a/openstack/container/v1/capsules/testing/fixtures.go
+++ b/openstack/container/v1/capsules/testing/fixtures.go
@@ -9,11 +9,6 @@ import (
fakeclient "github.com/gophercloud/gophercloud/testhelper/client"
)
-type imageEntry struct {
- ID string
- JSON string
-}
-
// HandleImageGetSuccessfully test setup
func HandleCapsuleGetSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/capsules/cc654059-1a77-47a3-bfcf-715bde5aad9e", func(w http.ResponseWriter, r *http.Request) {
@@ -66,3 +61,15 @@ func HandleCapsuleGetSuccessfully(t *testing.T) {
}`)
})
}
+
+// HandleCapsuleCreateSuccessfully creates an HTTP handler at `/capsules` on the test handler mux
+// that responds with a `Create` response.
+func HandleCapsuleCreateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/capsules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusAccepted)
+ fmt.Fprintf(w, `{}`)
+ })
+}
diff --git a/openstack/container/v1/capsules/testing/requests_test.go b/openstack/container/v1/capsules/testing/requests_test.go
index 016ad0d277..f7e6c37227 100644
--- a/openstack/container/v1/capsules/testing/requests_test.go
+++ b/openstack/container/v1/capsules/testing/requests_test.go
@@ -89,3 +89,57 @@ func TestGetCapsule(t *testing.T) {
th.AssertDeepEquals(t, &expectedCapsule, actualCapsule)
}
+
+func TestCreateCapsule(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCapsuleCreateSuccessfully(t)
+ template := new(capsules.Template)
+ template.Bin = []byte(`{
+ "capsuleVersion": "beta",
+ "kind": "capsule",
+ "metadata": {
+ "labels": {
+ "app": "web",
+ "app1": "web1"
+ },
+ "name": "template"
+ },
+ "restartPolicy": "Always",
+ "spec": {
+ "containers": [
+ {
+ "command": [
+ "/bin/bash"
+ ],
+ "env": {
+ "ENV1": "/usr/local/bin",
+ "ENV2": "/usr/bin"
+ },
+ "image": "ubuntu",
+ "imagePullPolicy": "ifnotpresent",
+ "ports": [
+ {
+ "containerPort": 80,
+ "hostPort": 80,
+ "name": "nginx-port",
+ "protocol": "TCP"
+ }
+ ],
+ "resources": {
+ "requests": {
+ "cpu": 1,
+ "memory": 1024
+ }
+ },
+ "workDir": "/root"
+ }
+ ]
+ }
+ }`)
+ createOpts := capsules.CreateOpts{
+ TemplateOpts: template,
+ }
+ err := capsules.Create(fakeclient.ServiceClient(), createOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/container/v1/capsules/urls.go b/openstack/container/v1/capsules/urls.go
index c3baf01a84..b276c4509f 100644
--- a/openstack/container/v1/capsules/urls.go
+++ b/openstack/container/v1/capsules/urls.go
@@ -5,3 +5,7 @@ import "github.com/gophercloud/gophercloud"
func getURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("capsules", id)
}
+
+func createURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL("capsules")
+}
diff --git a/openstack/container/v1/capsules/utils.go b/openstack/container/v1/capsules/utils.go
new file mode 100644
index 0000000000..9c0142e7b8
--- /dev/null
+++ b/openstack/container/v1/capsules/utils.go
@@ -0,0 +1,32 @@
+package capsules
+
+import (
+ "encoding/json"
+
+ "gopkg.in/yaml.v2"
+)
+
+// TE is a base structure for both Template and Environment
+type TE struct {
+ // Bin stores the contents of the template or environment.
+ Bin []byte
+ // Parsed contains a parsed version of Bin. Since there are 2 different
+ // fields referring to the same value, you must be careful when accessing
+ // this filed.
+ Parsed map[string]interface{}
+}
+
+// Parse will parse the contents and then validate. The contents MUST be either JSON or YAML.
+func (t *TE) Parse() error {
+ if jerr := json.Unmarshal(t.Bin, &t.Parsed); jerr != nil {
+ if yerr := yaml.Unmarshal(t.Bin, &t.Parsed); yerr != nil {
+ return ErrInvalidDataFormat{}
+ }
+ }
+ return t.Validate()
+}
+
+// Validate validates the contents of TE
+func (t *TE) Validate() error {
+ return nil
+}
diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go
index 070ea7cbef..12c8aebcf7 100644
--- a/openstack/endpoint_location.go
+++ b/openstack/endpoint_location.go
@@ -84,7 +84,7 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt
return "", err
}
if (opts.Availability == gophercloud.Availability(endpoint.Interface)) &&
- (opts.Region == "" || endpoint.Region == opts.Region) {
+ (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) {
endpoints = append(endpoints, endpoint)
}
}
diff --git a/openstack/identity/v3/roles/doc.go b/openstack/identity/v3/roles/doc.go
index 2886a872d8..f0e4d045e9 100644
--- a/openstack/identity/v3/roles/doc.go
+++ b/openstack/identity/v3/roles/doc.go
@@ -79,6 +79,29 @@ Example to List Role Assignments
fmt.Printf("%+v\n", role)
}
+Example to List Role Assignments for a User on a Project
+
+ projectID := "a99e9b4e620e4db09a2dfb6e42a01e66"
+ userID := "9df1a02f5eb2416a9781e8b0c022d3ae"
+ listAssignmentsOnResourceOpts := roles.ListAssignmentsOnResourceOpts{
+ UserID: userID,
+ ProjectID: projectID,
+ }
+
+ allPages, err := roles.ListAssignmentsOnResource(identityClient, listAssignmentsOnResourceOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allRoles, err := roles.ExtractRoles(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, role := range allRoles {
+ fmt.Printf("%+v\n", role)
+ }
+
Example to Assign a Role to a User in a Project
projectID := "a99e9b4e620e4db09a2dfb6e42a01e66"
diff --git a/openstack/identity/v3/roles/requests.go b/openstack/identity/v3/roles/requests.go
index 7908baa0e4..86c68c66ab 100644
--- a/openstack/identity/v3/roles/requests.go
+++ b/openstack/identity/v3/roles/requests.go
@@ -201,6 +201,26 @@ func ListAssignments(client *gophercloud.ServiceClient, opts ListAssignmentsOpts
})
}
+// ListAssignmentsOnResourceOpts provides options to list role assignments
+// for a user/group on a project/domain
+type ListAssignmentsOnResourceOpts struct {
+ // UserID is the ID of a user to assign a role
+ // Note: exactly one of UserID or GroupID must be provided
+ UserID string `xor:"GroupID"`
+
+ // GroupID is the ID of a group to assign a role
+ // Note: exactly one of UserID or GroupID must be provided
+ GroupID string `xor:"UserID"`
+
+ // ProjectID is the ID of a project to assign a role on
+ // Note: exactly one of ProjectID or DomainID must be provided
+ ProjectID string `xor:"DomainID"`
+
+ // DomainID is the ID of a domain to assign a role on
+ // Note: exactly one of ProjectID or DomainID must be provided
+ DomainID string `xor:"ProjectID"`
+}
+
// AssignOpts provides options to assign a role
type AssignOpts struct {
// UserID is the ID of a user to assign a role
@@ -239,6 +259,42 @@ type UnassignOpts struct {
DomainID string `xor:"ProjectID"`
}
+// ListAssignmentsOnResource is the operation responsible for listing role
+// assignments for a user/group on a project/domain.
+func ListAssignmentsOnResource(client *gophercloud.ServiceClient, opts ListAssignmentsOnResourceOpts) pagination.Pager {
+ // Check xor conditions
+ _, err := gophercloud.BuildRequestBody(opts, "")
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+
+ // Get corresponding URL
+ var targetID string
+ var targetType string
+ if opts.ProjectID != "" {
+ targetID = opts.ProjectID
+ targetType = "projects"
+ } else {
+ targetID = opts.DomainID
+ targetType = "domains"
+ }
+
+ var actorID string
+ var actorType string
+ if opts.UserID != "" {
+ actorID = opts.UserID
+ actorType = "users"
+ } else {
+ actorID = opts.GroupID
+ actorType = "groups"
+ }
+
+ url := listAssignmentsOnResourceURL(client, targetType, targetID, actorType, actorID)
+ return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return RolePage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
// Assign is the operation responsible for assigning a role
// to a user/group on a project/domain.
func Assign(client *gophercloud.ServiceClient, roleID string, opts AssignOpts) (r AssignmentResult) {
diff --git a/openstack/identity/v3/roles/testing/fixtures.go b/openstack/identity/v3/roles/testing/fixtures.go
index fa73b11ee0..fc56220bc8 100644
--- a/openstack/identity/v3/roles/testing/fixtures.go
+++ b/openstack/identity/v3/roles/testing/fixtures.go
@@ -141,6 +141,29 @@ const ListAssignmentOutput = `
}
`
+// ListAssignmentsOnResourceOutput provides a result of ListAssignmentsOnResource request.
+const ListAssignmentsOnResourceOutput = `
+{
+ "links": {
+ "next": null,
+ "previous": null,
+ "self": "http://example.com/identity/v3/projects/9e5a15/users/b964a9/roles"
+ },
+ "roles": [
+ {
+ "id": "9fe1d3",
+ "links": {
+ "self": "https://example.com/identity/v3/roles/9fe1d3"
+ },
+ "name": "support",
+ "extra": {
+ "description": "read-only support role"
+ }
+ }
+ ]
+}
+`
+
// FirstRole is the first role in the List request.
var FirstRole = roles.Role{
DomainID: "default",
@@ -331,3 +354,36 @@ func HandleListRoleAssignmentsSuccessfully(t *testing.T) {
fmt.Fprintf(w, ListAssignmentOutput)
})
}
+
+// RoleOnResource is the role in the ListAssignmentsOnResource request.
+var RoleOnResource = roles.Role{
+ ID: "9fe1d3",
+ Links: map[string]interface{}{
+ "self": "https://example.com/identity/v3/roles/9fe1d3",
+ },
+ Name: "support",
+ Extra: map[string]interface{}{
+ "description": "read-only support role",
+ },
+}
+
+// ExpectedRolesOnResourceSlice is the slice of roles expected to be returned
+// from ListAssignmentsOnResourceOutput.
+var ExpectedRolesOnResourceSlice = []roles.Role{RoleOnResource}
+
+func HandleListAssignmentsOnResourceSuccessfully(t *testing.T) {
+ fn := func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, ListAssignmentsOnResourceOutput)
+ }
+
+ th.Mux.HandleFunc("/projects/{project_id}/users/{user_id}/roles", fn)
+ th.Mux.HandleFunc("/projects/{project_id}/groups/{group_id}/roles", fn)
+ th.Mux.HandleFunc("/domains/{domain_id}/users/{user_id}/roles", fn)
+ th.Mux.HandleFunc("/domains/{domain_id}/groups/{group_id}/roles", fn)
+}
diff --git a/openstack/identity/v3/roles/testing/requests_test.go b/openstack/identity/v3/roles/testing/requests_test.go
index c683197bfa..ea8b19a7bb 100644
--- a/openstack/identity/v3/roles/testing/requests_test.go
+++ b/openstack/identity/v3/roles/testing/requests_test.go
@@ -115,6 +115,76 @@ func TestListAssignmentsSinglePage(t *testing.T) {
th.CheckEquals(t, count, 1)
}
+func TestListAssignmentsOnResource(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListAssignmentsOnResourceSuccessfully(t)
+
+ count := 0
+ err := roles.ListAssignmentsOnResource(client.ServiceClient(), roles.ListAssignmentsOnResourceOpts{
+ UserID: "{user_id}",
+ ProjectID: "{project_id}",
+ }).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := roles.ExtractRoles(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedRolesOnResourceSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+
+ count = 0
+ err = roles.ListAssignmentsOnResource(client.ServiceClient(), roles.ListAssignmentsOnResourceOpts{
+ UserID: "{user_id}",
+ DomainID: "{domain_id}",
+ }).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := roles.ExtractRoles(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedRolesOnResourceSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+
+ count = 0
+ err = roles.ListAssignmentsOnResource(client.ServiceClient(), roles.ListAssignmentsOnResourceOpts{
+ GroupID: "{group_id}",
+ ProjectID: "{project_id}",
+ }).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := roles.ExtractRoles(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedRolesOnResourceSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+
+ count = 0
+ err = roles.ListAssignmentsOnResource(client.ServiceClient(), roles.ListAssignmentsOnResourceOpts{
+ GroupID: "{group_id}",
+ DomainID: "{domain_id}",
+ }).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+
+ actual, err := roles.ExtractRoles(page)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ExpectedRolesOnResourceSlice, actual)
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, count, 1)
+}
+
func TestAssign(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/identity/v3/roles/urls.go b/openstack/identity/v3/roles/urls.go
index 38d592dca6..2b82011424 100644
--- a/openstack/identity/v3/roles/urls.go
+++ b/openstack/identity/v3/roles/urls.go
@@ -30,6 +30,10 @@ func listAssignmentsURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("role_assignments")
}
+func listAssignmentsOnResourceURL(client *gophercloud.ServiceClient, targetType, targetID, actorType, actorID string) string {
+ return client.ServiceURL(targetType, targetID, actorType, actorID, rolePath)
+}
+
func assignURL(client *gophercloud.ServiceClient, targetType, targetID, actorType, actorID, roleID string) string {
return client.ServiceURL(targetType, targetID, actorType, actorID, rolePath, roleID)
}
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
index ca35851e4a..0323e063df 100644
--- a/openstack/identity/v3/tokens/requests.go
+++ b/openstack/identity/v3/tokens/requests.go
@@ -72,72 +72,15 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
// ToTokenV3CreateMap builds a scope request body from AuthOptions.
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
- if opts.Scope.ProjectName != "" {
- // ProjectName provided: either DomainID or DomainName must also be supplied.
- // ProjectID may not be supplied.
- if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
- return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
- }
- if opts.Scope.ProjectID != "" {
- return nil, gophercloud.ErrScopeProjectIDOrProjectName{}
- }
-
- if opts.Scope.DomainID != "" {
- // ProjectName + DomainID
- return map[string]interface{}{
- "project": map[string]interface{}{
- "name": &opts.Scope.ProjectName,
- "domain": map[string]interface{}{"id": &opts.Scope.DomainID},
- },
- }, nil
- }
-
- if opts.Scope.DomainName != "" {
- // ProjectName + DomainName
- return map[string]interface{}{
- "project": map[string]interface{}{
- "name": &opts.Scope.ProjectName,
- "domain": map[string]interface{}{"name": &opts.Scope.DomainName},
- },
- }, nil
- }
- } else if opts.Scope.ProjectID != "" {
- // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
- if opts.Scope.DomainID != "" {
- return nil, gophercloud.ErrScopeProjectIDAlone{}
- }
- if opts.Scope.DomainName != "" {
- return nil, gophercloud.ErrScopeProjectIDAlone{}
- }
-
- // ProjectID
- return map[string]interface{}{
- "project": map[string]interface{}{
- "id": &opts.Scope.ProjectID,
- },
- }, nil
- } else if opts.Scope.DomainID != "" {
- // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
- if opts.Scope.DomainName != "" {
- return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
- }
-
- // DomainID
- return map[string]interface{}{
- "domain": map[string]interface{}{
- "id": &opts.Scope.DomainID,
- },
- }, nil
- } else if opts.Scope.DomainName != "" {
- // DomainName
- return map[string]interface{}{
- "domain": map[string]interface{}{
- "name": &opts.Scope.DomainName,
- },
- }, nil
+ scope := gophercloud.AuthScope(opts.Scope)
+
+ gophercloudAuthOpts := gophercloud.AuthOptions{
+ Scope: &scope,
+ DomainID: opts.DomainID,
+ DomainName: opts.DomainName,
}
- return nil, nil
+ return gophercloudAuthOpts.ToTokenV3ScopeMap()
}
func (opts *AuthOptions) CanReauth() bool {
diff --git a/openstack/identity/v3/tokens/results.go b/openstack/identity/v3/tokens/results.go
index 6e78d1cbdb..ebdca58f65 100644
--- a/openstack/identity/v3/tokens/results.go
+++ b/openstack/identity/v3/tokens/results.go
@@ -13,6 +13,7 @@ import (
type Endpoint struct {
ID string `json:"id"`
Region string `json:"region"`
+ RegionID string `json:"region_id"`
Interface string `json:"interface"`
URL string `json:"url"`
}
diff --git a/openstack/identity/v3/tokens/testing/fixtures.go b/openstack/identity/v3/tokens/testing/fixtures.go
index a475acb1b7..e6f44178a4 100644
--- a/openstack/identity/v3/tokens/testing/fixtures.go
+++ b/openstack/identity/v3/tokens/testing/fixtures.go
@@ -125,18 +125,21 @@ var catalogEntry1 = tokens.CatalogEntry{
tokens.Endpoint{
ID: "3eac9e7588eb4eb2a4650cf5e079505f",
Region: "RegionOne",
+ RegionID: "RegionOne",
Interface: "admin",
URL: "http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66",
},
tokens.Endpoint{
ID: "6b33fabc69c34ea782a3f6282582b59f",
Region: "RegionOne",
+ RegionID: "RegionOne",
Interface: "internal",
URL: "http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66",
},
tokens.Endpoint{
ID: "dae63c71bee24070a71f5425e7a916b5",
Region: "RegionOne",
+ RegionID: "RegionOne",
Interface: "public",
URL: "http://127.0.0.1:8774/v2.1/a99e9b4e620e4db09a2dfb6e42a01e66",
},
@@ -150,18 +153,21 @@ var catalogEntry2 = tokens.CatalogEntry{
tokens.Endpoint{
ID: "0539aeff80954a0bb756cec496768d3d",
Region: "RegionOne",
+ RegionID: "RegionOne",
Interface: "admin",
URL: "http://127.0.0.1:35357/v3",
},
tokens.Endpoint{
ID: "15bdf2d0853e4c939993d29548b1b56f",
Region: "RegionOne",
+ RegionID: "RegionOne",
Interface: "public",
URL: "http://127.0.0.1:5000/v3",
},
tokens.Endpoint{
ID: "3b4423c54ba343c58226bc424cb11c4b",
Region: "RegionOne",
+ RegionID: "RegionOne",
Interface: "internal",
URL: "http://127.0.0.1:5000/v3",
},
diff --git a/openstack/identity/v3/users/doc.go b/openstack/identity/v3/users/doc.go
index aa7ec196f5..c51a3fb607 100644
--- a/openstack/identity/v3/users/doc.go
+++ b/openstack/identity/v3/users/doc.go
@@ -54,6 +54,22 @@ Example to Update a User
panic(err)
}
+Example to Change Password of a User
+
+ userID := "0fe36e73809d46aeae6705c39077b1b3"
+ originalPassword := "secretsecret"
+ password := "new_secretsecret"
+
+ changePasswordOpts := users.ChangePasswordOpts{
+ OriginalPassword: originalPassword,
+ Password: password,
+ }
+
+ err := users.ChangePassword(identityClient, userID, changePasswordOpts).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
Example to Delete a User
userID := "0fe36e73809d46aeae6705c39077b1b3"
@@ -80,6 +96,26 @@ Example to List Groups a User Belongs To
fmt.Printf("%+v\n", group)
}
+Example to Add a User to a Group
+
+ groupID := "bede500ee1124ae9b0006ff859758b3a"
+ userID := "0fe36e73809d46aeae6705c39077b1b3"
+ err := users.AddToGroup(identityClient, groupID, userID).ExtractErr()
+
+ if err != nil {
+ panic(err)
+ }
+
+Example to Remove a User from a Group
+
+ groupID := "bede500ee1124ae9b0006ff859758b3a"
+ userID := "0fe36e73809d46aeae6705c39077b1b3"
+ err := users.RemoveFromGroup(identityClient, groupID, userID).ExtractErr()
+
+ if err != nil {
+ panic(err)
+ }
+
Example to List Projects a User Belongs To
userID := "0fe36e73809d46aeae6705c39077b1b3"
diff --git a/openstack/identity/v3/users/requests.go b/openstack/identity/v3/users/requests.go
index 779d116fcc..e1be94e7d9 100644
--- a/openstack/identity/v3/users/requests.go
+++ b/openstack/identity/v3/users/requests.go
@@ -204,6 +204,45 @@ func Update(client *gophercloud.ServiceClient, userID string, opts UpdateOptsBui
return
}
+// ChangePasswordOptsBuilder allows extensions to add additional parameters to
+// the ChangePassword request.
+type ChangePasswordOptsBuilder interface {
+ ToUserChangePasswordMap() (map[string]interface{}, error)
+}
+
+// ChangePasswordOpts provides options for changing password for a user.
+type ChangePasswordOpts struct {
+ // OriginalPassword is the original password of the user.
+ OriginalPassword string `json:"original_password"`
+
+ // Password is the new password of the user.
+ Password string `json:"password"`
+}
+
+// ToUserChangePasswordMap formats a ChangePasswordOpts into a ChangePassword request.
+func (opts ChangePasswordOpts) ToUserChangePasswordMap() (map[string]interface{}, error) {
+ b, err := gophercloud.BuildRequestBody(opts, "user")
+ if err != nil {
+ return nil, err
+ }
+
+ return b, nil
+}
+
+// ChangePassword changes password for a user.
+func ChangePassword(client *gophercloud.ServiceClient, userID string, opts ChangePasswordOptsBuilder) (r ChangePasswordResult) {
+ b, err := opts.ToUserChangePasswordMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+
+ _, r.Err = client.Post(changePasswordURL(client, userID), &b, nil, &gophercloud.RequestOpts{
+ OkCodes: []int{204},
+ })
+ return
+}
+
// Delete deletes a user.
func Delete(client *gophercloud.ServiceClient, userID string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, userID), nil)
@@ -218,6 +257,24 @@ func ListGroups(client *gophercloud.ServiceClient, userID string) pagination.Pag
})
}
+// AddToGroup adds a user to a group.
+func AddToGroup(client *gophercloud.ServiceClient, groupID, userID string) (r AddToGroupResult) {
+ url := addToGroupURL(client, groupID, userID)
+ _, r.Err = client.Put(url, nil, nil, &gophercloud.RequestOpts{
+ OkCodes: []int{204},
+ })
+ return
+}
+
+// RemoveFromGroup removes a user from a group.
+func RemoveFromGroup(client *gophercloud.ServiceClient, groupID, userID string) (r RemoveFromGroupResult) {
+ url := removeFromGroupURL(client, groupID, userID)
+ _, r.Err = client.Delete(url, &gophercloud.RequestOpts{
+ OkCodes: []int{204},
+ })
+ return
+}
+
// ListProjects enumerates groups user belongs to.
func ListProjects(client *gophercloud.ServiceClient, userID string) pagination.Pager {
url := listProjectsURL(client, userID)
diff --git a/openstack/identity/v3/users/results.go b/openstack/identity/v3/users/results.go
index c474e882b9..00a1a00062 100644
--- a/openstack/identity/v3/users/results.go
+++ b/openstack/identity/v3/users/results.go
@@ -98,12 +98,30 @@ type UpdateResult struct {
userResult
}
+// ChangePasswordResult is the response from a ChangePassword operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type ChangePasswordResult struct {
+ gophercloud.ErrResult
+}
+
// DeleteResult is the response from a Delete operation. Call its ExtractErr to
// determine if the request succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}
+// AddToGroupResult is the response from a AddToGroup operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type AddToGroupResult struct {
+ gophercloud.ErrResult
+}
+
+// RemoveFromGroupResult is the response from a RemoveFromGroup operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type RemoveFromGroupResult struct {
+ gophercloud.ErrResult
+}
+
// UserPage is a single page of User results.
type UserPage struct {
pagination.LinkedPageBase
diff --git a/openstack/identity/v3/users/testing/fixtures.go b/openstack/identity/v3/users/testing/fixtures.go
index 8d8e6df642..73e6acb767 100644
--- a/openstack/identity/v3/users/testing/fixtures.go
+++ b/openstack/identity/v3/users/testing/fixtures.go
@@ -129,7 +129,7 @@ const CreateNoOptionsRequest = `
}
`
-// UpdateRequest provides the input to as Update request.
+// UpdateRequest provides the input to an Update request.
const UpdateRequest = `
{
"user": {
@@ -164,6 +164,16 @@ const UpdateOutput = `
}
`
+// ChangePasswordRequest provides the input to a ChangePassword request.
+const ChangePasswordRequest = `
+{
+ "user": {
+ "password": "new_secretsecret",
+ "original_password": "secretsecret"
+ }
+}
+`
+
// ListGroupsOutput provides a ListGroups result.
const ListGroupsOutput = `
{
@@ -423,6 +433,18 @@ func HandleUpdateUserSuccessfully(t *testing.T) {
})
}
+// HandleChangeUserPasswordSuccessfully creates an HTTP handler at `/users` on the
+// test handler mux that tests change user password.
+func HandleChangeUserPasswordSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/users/9fe1d3/password", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, ChangePasswordRequest)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
// HandleDeleteUserSuccessfully creates an HTTP handler at `/users` on the
// test handler mux that tests user deletion.
func HandleDeleteUserSuccessfully(t *testing.T) {
@@ -448,6 +470,28 @@ func HandleListUserGroupsSuccessfully(t *testing.T) {
})
}
+// HandleAddToGroupSuccessfully creates an HTTP handler at /groups/{groupID}/users/{userID}
+// on the test handler mux that tests adding user to group.
+func HandleAddToGroupSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/groups/ea167b/users/9fe1d3", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleRemoveFromGroupSuccessfully creates an HTTP handler at /groups/{groupID}/users/{userID}
+// on the test handler mux that tests removing user from group.
+func HandleRemoveFromGroupSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/groups/ea167b/users/9fe1d3", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
// HandleListUserProjectsSuccessfully creates an HTTP handler at /users/{userID}/projects
// on the test handler mux that respons wit a list of two projects
func HandleListUserProjectsSuccessfully(t *testing.T) {
diff --git a/openstack/identity/v3/users/testing/requests_test.go b/openstack/identity/v3/users/testing/requests_test.go
index 15314ca61c..8cd4c47ac5 100644
--- a/openstack/identity/v3/users/testing/requests_test.go
+++ b/openstack/identity/v3/users/testing/requests_test.go
@@ -128,6 +128,20 @@ func TestUpdateUser(t *testing.T) {
th.CheckDeepEquals(t, SecondUserUpdated, *actual)
}
+func TestChangeUserPassword(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleChangeUserPasswordSuccessfully(t)
+
+ changePasswordOpts := users.ChangePasswordOpts{
+ OriginalPassword: "secretsecret",
+ Password: "new_secretsecret",
+ }
+
+ res := users.ChangePassword(client.ServiceClient(), "9fe1d3", changePasswordOpts)
+ th.AssertNoErr(t, res.Err)
+}
+
func TestDeleteUser(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -148,6 +162,22 @@ func TestListUserGroups(t *testing.T) {
th.CheckDeepEquals(t, ExpectedGroupsSlice, actual)
}
+func TestAddToGroup(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleAddToGroupSuccessfully(t)
+ res := users.AddToGroup(client.ServiceClient(), "ea167b", "9fe1d3")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestRemoveFromGroup(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleRemoveFromGroupSuccessfully(t)
+ res := users.RemoveFromGroup(client.ServiceClient(), "ea167b", "9fe1d3")
+ th.AssertNoErr(t, res.Err)
+}
+
func TestListUserProjects(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/identity/v3/users/urls.go b/openstack/identity/v3/users/urls.go
index 1db2831b5e..35468ad28e 100644
--- a/openstack/identity/v3/users/urls.go
+++ b/openstack/identity/v3/users/urls.go
@@ -18,6 +18,10 @@ func updateURL(client *gophercloud.ServiceClient, userID string) string {
return client.ServiceURL("users", userID)
}
+func changePasswordURL(client *gophercloud.ServiceClient, userID string) string {
+ return client.ServiceURL("users", userID, "password")
+}
+
func deleteURL(client *gophercloud.ServiceClient, userID string) string {
return client.ServiceURL("users", userID)
}
@@ -26,6 +30,14 @@ func listGroupsURL(client *gophercloud.ServiceClient, userID string) string {
return client.ServiceURL("users", userID, "groups")
}
+func addToGroupURL(client *gophercloud.ServiceClient, groupID, userID string) string {
+ return client.ServiceURL("groups", groupID, "users", userID)
+}
+
+func removeFromGroupURL(client *gophercloud.ServiceClient, groupID, userID string) string {
+ return client.ServiceURL("groups", groupID, "users", userID)
+}
+
func listProjectsURL(client *gophercloud.ServiceClient, userID string) string {
return client.ServiceURL("users", userID, "projects")
}
diff --git a/openstack/imageservice/v2/images/requests.go b/openstack/imageservice/v2/images/requests.go
index 081262f1ff..88cd4d265e 100644
--- a/openstack/imageservice/v2/images/requests.go
+++ b/openstack/imageservice/v2/images/requests.go
@@ -1,6 +1,10 @@
package images
import (
+ "fmt"
+ "net/url"
+ "time"
+
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@@ -18,6 +22,11 @@ type ListOptsBuilder interface {
//
// http://developer.openstack.org/api-ref-image-v2.html
type ListOpts struct {
+ // ID is the ID of the image.
+ // Multiple IDs can be specified by constructing a string
+ // such as "in:uuid1,uuid2,uuid3".
+ ID string `q:"id"`
+
// Integer value for the limit of values to return.
Limit int `q:"limit"`
@@ -25,6 +34,8 @@ type ListOpts struct {
Marker string `q:"marker"`
// Name filters on the name of the image.
+ // Multiple names can be specified by constructing a string
+ // such as "in:name1,name2,name3".
Name string `q:"name"`
// Visibility filters on the visibility of the image.
@@ -37,6 +48,8 @@ type ListOpts struct {
Owner string `q:"owner"`
// Status filters on the status of the image.
+ // Multiple statuses can be specified by constructing a string
+ // such as "in:saving,queued".
Status ImageStatus `q:"status"`
// SizeMin filters on the size_min image property.
@@ -56,12 +69,52 @@ type ListOpts struct {
// SortDir will sort the list results either ascending or decending.
SortDir string `q:"sort_dir"`
- Tag string `q:"tag"`
+
+ // Tags filters on specific image tags.
+ Tags []string `q:"tag"`
+
+ // CreatedAtQuery filters images based on their creation date.
+ CreatedAtQuery *ImageDateQuery
+
+ // UpdatedAtQuery filters images based on their updated date.
+ UpdatedAtQuery *ImageDateQuery
+
+ // ContainerFormat filters images based on the container_format.
+ // Multiple container formats can be specified by constructing a
+ // string such as "in:bare,ami".
+ ContainerFormat string `q:"container_format"`
+
+ // DiskFormat filters images based on the disk_format.
+ // Multiple disk formats can be specified by constructing a string
+ // such as "in:qcow2,iso".
+ DiskFormat string `q:"disk_format"`
}
// ToImageListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToImageListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
+ params := q.Query()
+
+ if opts.CreatedAtQuery != nil {
+ createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339)
+ if v := opts.CreatedAtQuery.Filter; v != "" {
+ createdAt = fmt.Sprintf("%s:%s", v, createdAt)
+ }
+
+ params.Add("created_at", createdAt)
+ }
+
+ if opts.UpdatedAtQuery != nil {
+ updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339)
+ if v := opts.UpdatedAtQuery.Filter; v != "" {
+ updatedAt = fmt.Sprintf("%s:%s", v, updatedAt)
+ }
+
+ params.Add("updated_at", updatedAt)
+ }
+
+ q = &url.URL{RawQuery: params.Encode()}
+
return q.String(), err
}
diff --git a/openstack/imageservice/v2/images/results.go b/openstack/imageservice/v2/images/results.go
index cd819ec9c8..e256068844 100644
--- a/openstack/imageservice/v2/images/results.go
+++ b/openstack/imageservice/v2/images/results.go
@@ -102,7 +102,7 @@ func (r *Image) UnmarshalJSON(b []byte) error {
switch t := s.SizeBytes.(type) {
case nil:
- return nil
+ r.SizeBytes = 0
case float32:
r.SizeBytes = int64(t)
case float64:
diff --git a/openstack/imageservice/v2/images/testing/fixtures.go b/openstack/imageservice/v2/images/testing/fixtures.go
index 33177c23f4..ddfe0438ed 100644
--- a/openstack/imageservice/v2/images/testing/fixtures.go
+++ b/openstack/imageservice/v2/images/testing/fixtures.go
@@ -212,6 +212,7 @@ func HandleImageCreationSuccessfullyNulls(t *testing.T) {
th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
th.TestJSONRequest(t, r, `{
"id": "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
+ "architecture": "x86_64",
"name": "Ubuntu 12.10",
"tags": [
"ubuntu",
@@ -222,6 +223,7 @@ func HandleImageCreationSuccessfullyNulls(t *testing.T) {
w.WriteHeader(http.StatusCreated)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `{
+ "architecture": "x86_64",
"status": "queued",
"name": "Ubuntu 12.10",
"protected": false,
@@ -345,3 +347,44 @@ func HandleImageUpdateSuccessfully(t *testing.T) {
}`)
})
}
+
+// HandleImageListByTagsSuccessfully tests a list operation with tags.
+func HandleImageListByTagsSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", fakeclient.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `{
+ "images": [
+ {
+ "status": "active",
+ "name": "cirros-0.3.2-x86_64-disk",
+ "tags": ["foo", "bar"],
+ "container_format": "bare",
+ "created_at": "2014-05-05T17:15:10Z",
+ "disk_format": "qcow2",
+ "updated_at": "2014-05-05T17:15:11Z",
+ "visibility": "public",
+ "self": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+ "min_disk": 0,
+ "protected": false,
+ "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+ "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
+ "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
+ "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
+ "size": 13167616,
+ "min_ram": 0,
+ "schema": "/v2/schemas/image",
+ "virtual_size": null,
+ "hw_disk_bus": "scsi",
+ "hw_disk_bus_model": "virtio-scsi",
+ "hw_scsi_model": "virtio-scsi"
+ }
+ ]
+ }`)
+ })
+}
diff --git a/openstack/imageservice/v2/images/testing/requests_test.go b/openstack/imageservice/v2/images/testing/requests_test.go
index d1f0966a42..ac71db86d6 100644
--- a/openstack/imageservice/v2/images/testing/requests_test.go
+++ b/openstack/imageservice/v2/images/testing/requests_test.go
@@ -2,6 +2,7 @@ package testing
import (
"testing"
+ "time"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/gophercloud/gophercloud/pagination"
@@ -131,6 +132,9 @@ func TestCreateImageNulls(t *testing.T) {
ID: id,
Name: name,
Tags: []string{"ubuntu", "quantal"},
+ Properties: map[string]string{
+ "architecture": "x86_64",
+ },
}).Extract()
th.AssertNoErr(t, err)
@@ -144,6 +148,10 @@ func TestCreateImageNulls(t *testing.T) {
createdDate := actualImage.CreatedAt
lastUpdate := actualImage.UpdatedAt
schema := "/v2/schemas/image"
+ properties := map[string]interface{}{
+ "architecture": "x86_64",
+ }
+ sizeBytes := int64(0)
expectedImage := images.Image{
ID: "e7db3b45-8db7-47ad-8109-3fb55c2c24fd",
@@ -165,6 +173,8 @@ func TestCreateImageNulls(t *testing.T) {
CreatedAt: createdDate,
UpdatedAt: lastUpdate,
Schema: schema,
+ Properties: properties,
+ SizeBytes: sizeBytes,
}
th.AssertDeepEquals(t, &expectedImage, actualImage)
@@ -291,3 +301,90 @@ func TestUpdateImage(t *testing.T) {
th.AssertDeepEquals(t, &expectedImage, actualImage)
}
+
+func TestImageDateQuery(t *testing.T) {
+ date := time.Date(2014, 1, 1, 1, 1, 1, 0, time.UTC)
+
+ listOpts := images.ListOpts{
+ CreatedAtQuery: &images.ImageDateQuery{
+ Date: date,
+ Filter: images.FilterGTE,
+ },
+ UpdatedAtQuery: &images.ImageDateQuery{
+ Date: date,
+ },
+ }
+
+ expectedQueryString := "?created_at=gte%3A2014-01-01T01%3A01%3A01Z&updated_at=2014-01-01T01%3A01%3A01Z"
+ actualQueryString, err := listOpts.ToImageListQuery()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expectedQueryString, actualQueryString)
+}
+
+func TestImageListByTags(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ HandleImageListByTagsSuccessfully(t)
+
+ listOpts := images.ListOpts{
+ Tags: []string{"foo", "bar"},
+ }
+
+ expectedQueryString := "?tag=foo&tag=bar"
+ actualQueryString, err := listOpts.ToImageListQuery()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, expectedQueryString, actualQueryString)
+
+ pages, err := images.List(fakeclient.ServiceClient(), listOpts).AllPages()
+ th.AssertNoErr(t, err)
+ allImages, err := images.ExtractImages(pages)
+ th.AssertNoErr(t, err)
+
+ checksum := "64d7c1cd2b6f60c92c14662941cb7913"
+ sizeBytes := int64(13167616)
+ containerFormat := "bare"
+ diskFormat := "qcow2"
+ minDiskGigabytes := 0
+ minRAMMegabytes := 0
+ owner := "5ef70662f8b34079a6eddb8da9d75fe8"
+ file := allImages[0].File
+ createdDate := allImages[0].CreatedAt
+ lastUpdate := allImages[0].UpdatedAt
+ schema := "/v2/schemas/image"
+ tags := []string{"foo", "bar"}
+
+ expectedImage := images.Image{
+ ID: "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
+ Name: "cirros-0.3.2-x86_64-disk",
+ Tags: tags,
+
+ Status: images.ImageStatusActive,
+
+ ContainerFormat: containerFormat,
+ DiskFormat: diskFormat,
+
+ MinDiskGigabytes: minDiskGigabytes,
+ MinRAMMegabytes: minRAMMegabytes,
+
+ Owner: owner,
+
+ Protected: false,
+ Visibility: images.ImageVisibilityPublic,
+
+ Checksum: checksum,
+ SizeBytes: sizeBytes,
+ File: file,
+ CreatedAt: createdDate,
+ UpdatedAt: lastUpdate,
+ Schema: schema,
+ VirtualSize: 0,
+ Properties: map[string]interface{}{
+ "hw_disk_bus": "scsi",
+ "hw_disk_bus_model": "virtio-scsi",
+ "hw_scsi_model": "virtio-scsi",
+ },
+ }
+
+ th.AssertDeepEquals(t, expectedImage, allImages[0])
+}
diff --git a/openstack/imageservice/v2/images/types.go b/openstack/imageservice/v2/images/types.go
index 2e01b38f5c..d2f9cbd3bf 100644
--- a/openstack/imageservice/v2/images/types.go
+++ b/openstack/imageservice/v2/images/types.go
@@ -1,5 +1,9 @@
package images
+import (
+ "time"
+)
+
// ImageStatus image statuses
// http://docs.openstack.org/developer/glance/statuses.html
type ImageStatus string
@@ -77,3 +81,24 @@ const (
// ImageMemberStatusAll
ImageMemberStatusAll ImageMemberStatus = "all"
)
+
+// ImageDateFilter represents a valid filter to use for filtering
+// images by their date during a List.
+type ImageDateFilter string
+
+const (
+ FilterGT ImageDateFilter = "gt"
+ FilterGTE ImageDateFilter = "gte"
+ FilterLT ImageDateFilter = "lt"
+ FilterLTE ImageDateFilter = "lte"
+ FilterNEQ ImageDateFilter = "neq"
+ FilterEQ ImageDateFilter = "eq"
+)
+
+// ImageDateQuery represents a date field to be used for listing images.
+// If no filter is specified, the query will act as though FilterEQ was
+// set.
+type ImageDateQuery struct {
+ Date time.Time
+ Filter ImageDateFilter
+}
diff --git a/openstack/loadbalancer/v2/doc.go b/openstack/loadbalancer/v2/doc.go
new file mode 100644
index 0000000000..ec7f9d6f04
--- /dev/null
+++ b/openstack/loadbalancer/v2/doc.go
@@ -0,0 +1,3 @@
+// Package lbaas_v2 provides information and interaction with the Load Balancer
+// as a Service v2 extension for the OpenStack Networking service.
+package lbaas_v2
diff --git a/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go
new file mode 100644
index 0000000000..86135e3d37
--- /dev/null
+++ b/openstack/loadbalancer/v2/l7policies/doc.go
@@ -0,0 +1,75 @@
+/*
+Package l7policies provides information and interaction with L7Policies and
+Rules of the LBaaS v2 extension for the OpenStack Networking service.
+
+Example to Create a L7Policy
+
+ createOpts := l7policies.CreateOpts{
+ Name: "redirect-example.com",
+ ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d",
+ Action: l7policies.ActionRedirectToURL,
+ RedirectURL: "http://www.example.com",
+ }
+ l7policy, err := l7policies.Create(lbClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to List L7Policies
+
+ listOpts := l7policies.ListOpts{
+ ListenerID: "c79a4468-d788-410c-bf79-9a8ef6354852",
+ }
+ allPages, err := l7policies.List(lbClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+ allL7Policies, err := l7policies.ExtractL7Policies(allPages)
+ if err != nil {
+ panic(err)
+ }
+ for _, l7policy := range allL7Policies {
+ fmt.Printf("%+v\n", l7policy)
+ }
+
+Example to Get a L7Policy
+
+ l7policy, err := l7policies.Get(lbClient, "023f2e34-7806-443b-bfae-16c324569a3d").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a L7Policy
+
+ l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ err := l7policies.Delete(lbClient, l7policyID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a L7Policy
+
+ l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ name := "new-name"
+ updateOpts := l7policies.UpdateOpts{
+ Name: &name,
+ }
+ l7policy, err := l7policies.Update(lbClient, l7policyID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Create a Rule
+
+ l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ createOpts := l7policies.CreateRuleOpts{
+ RuleType: l7policies.TypePath,
+ CompareType: l7policies.CompareTypeRegex,
+ Value: "/images*",
+ }
+ rule, err := l7policies.CreateRule(lbClient, l7policyID, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+*/
+package l7policies
diff --git a/openstack/loadbalancer/v2/l7policies/requests.go b/openstack/loadbalancer/v2/l7policies/requests.go
new file mode 100644
index 0000000000..16a655947b
--- /dev/null
+++ b/openstack/loadbalancer/v2/l7policies/requests.go
@@ -0,0 +1,225 @@
+package l7policies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToL7PolicyCreateMap() (map[string]interface{}, error)
+}
+
+type Action string
+type RuleType string
+type CompareType string
+
+const (
+ ActionRedirectToPool Action = "REDIRECT_TO_POOL"
+ ActionRedirectToURL Action = "REDIRECT_TO_URL"
+ ActionReject Action = "REJECT"
+
+ TypeCookie RuleType = "COOKIE"
+ TypeFileType RuleType = "FILE_TYPE"
+ TypeHeader RuleType = "HEADER"
+ TypeHostName RuleType = "HOST_NAME"
+ TypePath RuleType = "PATH"
+
+ CompareTypeContains CompareType = "CONTAINS"
+ CompareTypeEndWith CompareType = "ENDS_WITH"
+ CompareTypeEqual CompareType = "EQUAL_TO"
+ CompareTypeRegex CompareType = "REGEX"
+ CompareTypeStartWith CompareType = "STARTS_WITH"
+)
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Name of the L7 policy.
+ Name string `json:"name,omitempty"`
+
+ // The ID of the listener.
+ ListenerID string `json:"listener_id" required:"true"`
+
+ // The L7 policy action. One of REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT.
+ Action Action `json:"action" required:"true"`
+
+ // The position of this policy on the listener.
+ Position int32 `json:"position,omitempty"`
+
+ // A human-readable description for the resource.
+ Description string `json:"description,omitempty"`
+
+ // ProjectID is the UUID of the project who owns the L7 policy in octavia.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // Requests matching this policy will be redirected to the pool with this ID.
+ // Only valid if action is REDIRECT_TO_POOL.
+ RedirectPoolID string `json:"redirect_pool_id,omitempty"`
+
+ // Requests matching this policy will be redirected to this URL.
+ // Only valid if action is REDIRECT_TO_URL.
+ RedirectURL string `json:"redirect_url,omitempty"`
+}
+
+// ToL7PolicyCreateMap builds a request body from CreateOpts.
+func (opts CreateOpts) ToL7PolicyCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "l7policy")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new l7policy.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToL7PolicyCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToL7PolicyListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API.
+type ListOpts struct {
+ Name string `q:"name"`
+ ListenerID string `q:"listener_id"`
+ Action string `q:"action"`
+ ProjectID string `q:"project_id"`
+ RedirectPoolID string `q:"redirect_pool_id"`
+ RedirectURL string `q:"redirect_url"`
+ ID string `q:"id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToL7PolicyListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToL7PolicyListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// l7policies. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those l7policies that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToL7PolicyListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return L7PolicyPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// Get retrieves a particular l7policy based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// Delete will permanently delete a particular l7policy based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToL7PolicyUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+ // Name of the L7 policy, empty string is allowed.
+ Name *string `json:"name,omitempty"`
+
+ // The L7 policy action. One of REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT.
+ Action Action `json:"action,omitempty"`
+
+ // The position of this policy on the listener.
+ Position int32 `json:"position,omitempty"`
+
+ // A human-readable description for the resource, empty string is allowed.
+ Description *string `json:"description,omitempty"`
+
+ // Requests matching this policy will be redirected to the pool with this ID.
+ // Only valid if action is REDIRECT_TO_POOL.
+ RedirectPoolID string `json:"redirect_pool_id,omitempty"`
+
+ // Requests matching this policy will be redirected to this URL.
+ // Only valid if action is REDIRECT_TO_URL.
+ RedirectURL string `json:"redirect_url,omitempty"`
+}
+
+// ToL7PolicyUpdateMap builds a request body from UpdateOpts.
+func (opts UpdateOpts) ToL7PolicyUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "l7policy")
+}
+
+// Update allows l7policy to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, _ := opts.ToL7PolicyUpdateMap()
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// CreateRuleOpts is the common options struct used in this package's CreateRule
+// operation.
+type CreateRuleOpts struct {
+ // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH.
+ RuleType RuleType `json:"type" required:"true"`
+
+ // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH.
+ CompareType CompareType `json:"compare_type" required:"true"`
+
+ // The value to use for the comparison. For example, the file type to compare.
+ Value string `json:"value" required:"true"`
+
+ // ProjectID is the UUID of the project who owns the rule in octavia.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // The key to use for the comparison. For example, the name of the cookie to evaluate.
+ Key string `json:"key,omitempty"`
+
+ // When true the logic of the rule is inverted. For example, with invert true,
+ // equal to would become not equal to. Default is false.
+ Invert bool `json:"invert,omitempty"`
+}
+
+// ToRuleCreateMap builds a request body from CreateRuleOpts.
+func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "rule")
+}
+
+// CreateRule will create and associate a Rule with a particular L7Policy.
+func CreateRule(c *gophercloud.ServiceClient, policyID string, opts CreateRuleOpts) (r CreateRuleResult) {
+ b, err := opts.ToRuleCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(ruleRootURL(c, policyID), b, &r.Body, nil)
+ return
+}
diff --git a/openstack/loadbalancer/v2/l7policies/results.go b/openstack/loadbalancer/v2/l7policies/results.go
new file mode 100644
index 0000000000..78a8e83db0
--- /dev/null
+++ b/openstack/loadbalancer/v2/l7policies/results.go
@@ -0,0 +1,168 @@
+package l7policies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// L7Policy is a collection of L7 rules associated with a Listener, and which
+// may also have an association to a back-end pool.
+type L7Policy struct {
+ // The unique ID for the L7 policy.
+ ID string `json:"id"`
+
+ // Name of the L7 policy.
+ Name string `json:"name"`
+
+ // The ID of the listener.
+ ListenerID string `json:"listener_id"`
+
+ // The L7 policy action. One of REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT.
+ Action string `json:"action"`
+
+ // The position of this policy on the listener.
+ Position int32 `json:"position"`
+
+ // A human-readable description for the resource.
+ Description string `json:"description"`
+
+ // ProjectID is the UUID of the project who owns the L7 policy in octavia.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id"`
+
+ // Requests matching this policy will be redirected to the pool with this ID.
+ // Only valid if action is REDIRECT_TO_POOL.
+ RedirectPoolID string `json:"redirect_pool_id"`
+
+ // Requests matching this policy will be redirected to this URL.
+ // Only valid if action is REDIRECT_TO_URL.
+ RedirectURL string `json:"redirect_url"`
+
+ // The administrative state of the L7 policy, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // Rules are List of associated L7 rule IDs.
+ Rules []Rule `json:"rules"`
+}
+
+// Rule represents layer 7 load balancing rule.
+type Rule struct {
+ // The unique ID for the L7 rule.
+ ID string `json:"id"`
+
+ // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH.
+ RuleType string `json:"type"`
+
+ // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH.
+ CompareType string `json:"compare_type"`
+
+ // The value to use for the comparison. For example, the file type to compare.
+ Value string `json:"value"`
+
+ // ProjectID is the UUID of the project who owns the rule in octavia.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id"`
+
+ // The key to use for the comparison. For example, the name of the cookie to evaluate.
+ Key string `json:"key"`
+
+ // When true the logic of the rule is inverted. For example, with invert true,
+ // equal to would become not equal to. Default is false.
+ Invert bool `json:"invert"`
+
+ // The administrative state of the L7 rule, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up"`
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a l7policy.
+func (r commonResult) Extract() (*L7Policy, error) {
+ var s struct {
+ L7Policy *L7Policy `json:"l7policy"`
+ }
+ err := r.ExtractInto(&s)
+ return s.L7Policy, err
+}
+
+// CreateResult represents the result of a Create operation. Call its Extract
+// method to interpret the result as a L7Policy.
+type CreateResult struct {
+ commonResult
+}
+
+// L7PolicyPage is the page returned by a pager when traversing over a
+// collection of l7policies.
+type L7PolicyPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of l7policies 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 (r L7PolicyPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"l7policies_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a L7PolicyPage struct is empty.
+func (r L7PolicyPage) IsEmpty() (bool, error) {
+ is, err := ExtractL7Policies(r)
+ return len(is) == 0, err
+}
+
+// ExtractL7Policies accepts a Page struct, specifically a L7PolicyPage struct,
+// and extracts the elements into a slice of L7Policy structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractL7Policies(r pagination.Page) ([]L7Policy, error) {
+ var s struct {
+ L7Policies []L7Policy `json:"l7policies"`
+ }
+ err := (r.(L7PolicyPage)).ExtractInto(&s)
+ return s.L7Policies, err
+}
+
+// GetResult represents the result of a Get operation. Call its Extract
+// method to interpret the result as a L7Policy.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a Delete operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an Update operation. Call its Extract
+// method to interpret the result as a L7Policy.
+type UpdateResult struct {
+ commonResult
+}
+
+type commonRuleResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a rule.
+func (r commonRuleResult) Extract() (*Rule, error) {
+ var s struct {
+ Rule *Rule `json:"rule"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Rule, err
+}
+
+// CreateRuleResult represents the result of a CreateRule operation.
+// Call its Extract method to interpret it as a Rule.
+type CreateRuleResult struct {
+ commonRuleResult
+}
diff --git a/openstack/loadbalancer/v2/l7policies/testing/doc.go b/openstack/loadbalancer/v2/l7policies/testing/doc.go
new file mode 100644
index 0000000000..f8068dfb6b
--- /dev/null
+++ b/openstack/loadbalancer/v2/l7policies/testing/doc.go
@@ -0,0 +1,2 @@
+// l7policies unit tests
+package testing
diff --git a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go
new file mode 100644
index 0000000000..a4d2e13144
--- /dev/null
+++ b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go
@@ -0,0 +1,251 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// SingleL7PolicyBody is the canned body of a Get request on an existing l7policy.
+const SingleL7PolicyBody = `
+{
+ "l7policy": {
+ "listener_id": "023f2e34-7806-443b-bfae-16c324569a3d",
+ "description": "",
+ "admin_state_up": true,
+ "redirect_pool_id": null,
+ "redirect_url": "http://www.example.com",
+ "action": "REDIRECT_TO_URL",
+ "position": 1,
+ "project_id": "e3cd678b11784734bc366148aa37580e",
+ "id": "8a1412f0-4c32-4257-8b07-af4770b604fd",
+ "name": "redirect-example.com",
+ "rules": []
+ }
+}
+`
+
+var (
+ L7PolicyToURL = l7policies.L7Policy{
+ ID: "8a1412f0-4c32-4257-8b07-af4770b604fd",
+ Name: "redirect-example.com",
+ ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d",
+ Action: "REDIRECT_TO_URL",
+ Position: 1,
+ Description: "",
+ ProjectID: "e3cd678b11784734bc366148aa37580e",
+ RedirectPoolID: "",
+ RedirectURL: "http://www.example.com",
+ AdminStateUp: true,
+ Rules: []l7policies.Rule{},
+ }
+ L7PolicyToPool = l7policies.L7Policy{
+ ID: "964f4ba4-f6cd-405c-bebd-639460af7231",
+ Name: "redirect-pool",
+ ListenerID: "be3138a3-5cf7-4513-a4c2-bb137e668bab",
+ Action: "REDIRECT_TO_POOL",
+ Position: 1,
+ Description: "",
+ ProjectID: "c1f7910086964990847dc6c8b128f63c",
+ RedirectPoolID: "bac433c6-5bea-4311-80da-bd1cd90fbd25",
+ RedirectURL: "",
+ AdminStateUp: true,
+ Rules: []l7policies.Rule{},
+ }
+ L7PolicyUpdated = l7policies.L7Policy{
+ ID: "8a1412f0-4c32-4257-8b07-af4770b604fd",
+ Name: "NewL7PolicyName",
+ ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d",
+ Action: "REDIRECT_TO_URL",
+ Position: 1,
+ Description: "Redirect requests to example.com",
+ ProjectID: "e3cd678b11784734bc366148aa37580e",
+ RedirectPoolID: "",
+ RedirectURL: "http://www.new-example.com",
+ AdminStateUp: true,
+ Rules: []l7policies.Rule{},
+ }
+ RulePath = l7policies.Rule{
+ ID: "16621dbb-a736-4888-a57a-3ecd53df784c",
+ RuleType: "PATH",
+ CompareType: "REGEX",
+ Value: "/images*",
+ ProjectID: "e3cd678b11784734bc366148aa37580e",
+ Key: "",
+ Invert: false,
+ AdminStateUp: true,
+ }
+)
+
+// HandleL7PolicyCreationSuccessfully sets up the test server to respond to a l7policy creation request
+// with a given response.
+func HandleL7PolicyCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/v2.0/lbaas/l7policies", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "l7policy": {
+ "listener_id": "023f2e34-7806-443b-bfae-16c324569a3d",
+ "redirect_url": "http://www.example.com",
+ "name": "redirect-example.com",
+ "action": "REDIRECT_TO_URL"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
+
+// L7PoliciesListBody contains the canned body of a l7policy list response.
+const L7PoliciesListBody = `
+{
+ "l7policies": [
+ {
+ "redirect_pool_id": null,
+ "description": "",
+ "admin_state_up": true,
+ "rules": [],
+ "project_id": "e3cd678b11784734bc366148aa37580e",
+ "listener_id": "023f2e34-7806-443b-bfae-16c324569a3d",
+ "redirect_url": "http://www.example.com",
+ "action": "REDIRECT_TO_URL",
+ "position": 1,
+ "id": "8a1412f0-4c32-4257-8b07-af4770b604fd",
+ "name": "redirect-example.com"
+ },
+ {
+ "redirect_pool_id": "bac433c6-5bea-4311-80da-bd1cd90fbd25",
+ "description": "",
+ "admin_state_up": true,
+ "rules": [],
+ "project_id": "c1f7910086964990847dc6c8b128f63c",
+ "listener_id": "be3138a3-5cf7-4513-a4c2-bb137e668bab",
+ "action": "REDIRECT_TO_POOL",
+ "position": 1,
+ "id": "964f4ba4-f6cd-405c-bebd-639460af7231",
+ "name": "redirect-pool"
+ }
+ ]
+}
+`
+
+// PostUpdateL7PolicyBody is the canned response body of a Update request on an existing l7policy.
+const PostUpdateL7PolicyBody = `
+{
+ "l7policy": {
+ "listener_id": "023f2e34-7806-443b-bfae-16c324569a3d",
+ "description": "Redirect requests to example.com",
+ "admin_state_up": true,
+ "redirect_pool_id": null,
+ "redirect_url": "http://www.new-example.com",
+ "action": "REDIRECT_TO_URL",
+ "position": 1,
+ "project_id": "e3cd678b11784734bc366148aa37580e",
+ "id": "8a1412f0-4c32-4257-8b07-af4770b604fd",
+ "name": "NewL7PolicyName",
+ "rules": []
+ }
+}
+`
+
+// HandleL7PolicyListSuccessfully sets up the test server to respond to a l7policy List request.
+func HandleL7PolicyListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/l7policies", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, L7PoliciesListBody)
+ case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+ fmt.Fprintf(w, `{ "l7policies": [] }`)
+ default:
+ t.Fatalf("/v2.0/lbaas/l7policies invoked with unexpected marker=[%s]", marker)
+ }
+ })
+}
+
+// HandleL7PolicyGetSuccessfully sets up the test server to respond to a l7policy Get request.
+func HandleL7PolicyGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/l7policies/8a1412f0-4c32-4257-8b07-af4770b604fd", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, SingleL7PolicyBody)
+ })
+}
+
+// HandleL7PolicyDeletionSuccessfully sets up the test server to respond to a l7policy deletion request.
+func HandleL7PolicyDeletionSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/l7policies/8a1412f0-4c32-4257-8b07-af4770b604fd", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleL7PolicyUpdateSuccessfully sets up the test server to respond to a l7policy Update request.
+func HandleL7PolicyUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/l7policies/8a1412f0-4c32-4257-8b07-af4770b604fd", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `{
+ "l7policy": {
+ "name": "NewL7PolicyName",
+ "action": "REDIRECT_TO_URL",
+ "redirect_url": "http://www.new-example.com"
+ }
+ }`)
+
+ fmt.Fprintf(w, PostUpdateL7PolicyBody)
+ })
+}
+
+// SingleRuleBody is the canned body of a Get request on an existing rule.
+const SingleRuleBody = `
+{
+ "rule": {
+ "compare_type": "REGEX",
+ "invert": false,
+ "admin_state_up": true,
+ "value": "/images*",
+ "key": null,
+ "project_id": "e3cd678b11784734bc366148aa37580e",
+ "type": "PATH",
+ "id": "16621dbb-a736-4888-a57a-3ecd53df784c"
+ }
+}
+`
+
+// HandleRuleCreationSuccessfully sets up the test server to respond to a rule creation request
+// with a given response.
+func HandleRuleCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/v2.0/lbaas/l7policies/8a1412f0-4c32-4257-8b07-af4770b604fd/rules", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "rule": {
+ "compare_type": "REGEX",
+ "type": "PATH",
+ "value": "/images*"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
diff --git a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go
new file mode 100644
index 0000000000..9fac83960b
--- /dev/null
+++ b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go
@@ -0,0 +1,176 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies"
+ fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreateL7Policy(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleL7PolicyCreationSuccessfully(t, SingleL7PolicyBody)
+
+ actual, err := l7policies.Create(fake.ServiceClient(), l7policies.CreateOpts{
+ Name: "redirect-example.com",
+ ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d",
+ Action: l7policies.ActionRedirectToURL,
+ RedirectURL: "http://www.example.com",
+ }).Extract()
+
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, L7PolicyToURL, *actual)
+}
+
+func TestRequiredL7PolicyCreateOpts(t *testing.T) {
+ // no param specified.
+ res := l7policies.Create(fake.ServiceClient(), l7policies.CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ // Action is invalid.
+ res = l7policies.Create(fake.ServiceClient(), l7policies.CreateOpts{
+ ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d",
+ Action: l7policies.Action("invalid"),
+ })
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+}
+
+func TestListL7Policies(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleL7PolicyListSuccessfully(t)
+
+ pages := 0
+ err := l7policies.List(fake.ServiceClient(), l7policies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := l7policies.ExtractL7Policies(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 l7policies, got %d", len(actual))
+ }
+ th.CheckDeepEquals(t, L7PolicyToURL, actual[0])
+ th.CheckDeepEquals(t, L7PolicyToPool, actual[1])
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestListAllL7Policies(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleL7PolicyListSuccessfully(t)
+
+ allPages, err := l7policies.List(fake.ServiceClient(), l7policies.ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := l7policies.ExtractL7Policies(allPages)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, L7PolicyToURL, actual[0])
+ th.CheckDeepEquals(t, L7PolicyToPool, actual[1])
+}
+
+func TestGetL7Policy(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleL7PolicyGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := l7policies.Get(client, "8a1412f0-4c32-4257-8b07-af4770b604fd").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, L7PolicyToURL, *actual)
+}
+
+func TestDeleteL7Policy(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleL7PolicyDeletionSuccessfully(t)
+
+ res := l7policies.Delete(fake.ServiceClient(), "8a1412f0-4c32-4257-8b07-af4770b604fd")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateL7Policy(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleL7PolicyUpdateSuccessfully(t)
+
+ client := fake.ServiceClient()
+ newName := "NewL7PolicyName"
+ actual, err := l7policies.Update(client, "8a1412f0-4c32-4257-8b07-af4770b604fd",
+ l7policies.UpdateOpts{
+ Name: &newName,
+ Action: l7policies.ActionRedirectToURL,
+ RedirectURL: "http://www.new-example.com",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, L7PolicyUpdated, *actual)
+}
+
+func TestCreateRule(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleRuleCreationSuccessfully(t, SingleRuleBody)
+
+ actual, err := l7policies.CreateRule(fake.ServiceClient(), "8a1412f0-4c32-4257-8b07-af4770b604fd", l7policies.CreateRuleOpts{
+ RuleType: l7policies.TypePath,
+ CompareType: l7policies.CompareTypeRegex,
+ Value: "/images*",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, RulePath, *actual)
+}
+
+func TestRequiredRuleCreateOpts(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ res := l7policies.CreateRule(fake.ServiceClient(), "", l7policies.CreateRuleOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = l7policies.CreateRule(fake.ServiceClient(), "8a1412f0-4c32-4257-8b07-af4770b604fd", l7policies.CreateRuleOpts{
+ RuleType: l7policies.TypePath,
+ })
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+ res = l7policies.CreateRule(fake.ServiceClient(), "8a1412f0-4c32-4257-8b07-af4770b604fd", l7policies.CreateRuleOpts{
+ RuleType: l7policies.RuleType("invalid"),
+ CompareType: l7policies.CompareTypeRegex,
+ Value: "/images*",
+ })
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+ res = l7policies.CreateRule(fake.ServiceClient(), "8a1412f0-4c32-4257-8b07-af4770b604fd", l7policies.CreateRuleOpts{
+ RuleType: l7policies.TypePath,
+ CompareType: l7policies.CompareType("invalid"),
+ Value: "/images*",
+ })
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+}
diff --git a/openstack/loadbalancer/v2/l7policies/urls.go b/openstack/loadbalancer/v2/l7policies/urls.go
new file mode 100644
index 0000000000..44ebdd444f
--- /dev/null
+++ b/openstack/loadbalancer/v2/l7policies/urls.go
@@ -0,0 +1,21 @@
+package l7policies
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "lbaas"
+ resourcePath = "l7policies"
+ rulePath = "rules"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
+
+func ruleRootURL(c *gophercloud.ServiceClient, policyID string) string {
+ return c.ServiceURL(rootPath, resourcePath, policyID, rulePath)
+}
diff --git a/openstack/loadbalancer/v2/listeners/doc.go b/openstack/loadbalancer/v2/listeners/doc.go
new file mode 100644
index 0000000000..108cdb03d8
--- /dev/null
+++ b/openstack/loadbalancer/v2/listeners/doc.go
@@ -0,0 +1,63 @@
+/*
+Package listeners provides information and interaction with Listeners of the
+LBaaS v2 extension for the OpenStack Networking service.
+
+Example to List Listeners
+
+ listOpts := listeners.ListOpts{
+ LoadbalancerID : "ca430f80-1737-4712-8dc6-3f640d55594b",
+ }
+
+ allPages, err := listeners.List(networkClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allListeners, err := listeners.ExtractListeners(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, listener := range allListeners {
+ fmt.Printf("%+v\n", listener)
+ }
+
+Example to Create a Listener
+
+ createOpts := listeners.CreateOpts{
+ Protocol: "TCP",
+ Name: "db",
+ LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab",
+ AdminStateUp: gophercloud.Enabled,
+ DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
+ ProtocolPort: 3306,
+ }
+
+ listener, err := listeners.Create(networkClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Listener
+
+ listenerID := "d67d56a6-4a86-4688-a282-f46444705c64"
+
+ i1001 := 1001
+ updateOpts := listeners.UpdateOpts{
+ ConnLimit: &i1001,
+ }
+
+ listener, err := listeners.Update(networkClient, listenerID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Listener
+
+ listenerID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ err := listeners.Delete(networkClient, listenerID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+*/
+package listeners
diff --git a/openstack/loadbalancer/v2/listeners/requests.go b/openstack/loadbalancer/v2/listeners/requests.go
new file mode 100644
index 0000000000..dd190f606f
--- /dev/null
+++ b/openstack/loadbalancer/v2/listeners/requests.go
@@ -0,0 +1,199 @@
+package listeners
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// Type Protocol represents a listener protocol.
+type Protocol string
+
+// Supported attributes for create/update operations.
+const (
+ ProtocolTCP Protocol = "TCP"
+ ProtocolHTTP Protocol = "HTTP"
+ ProtocolHTTPS Protocol = "HTTPS"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToListenerListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the floating IP attributes you want to see returned. SortKey allows you to
+// sort by a particular listener attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ ID string `q:"id"`
+ Name string `q:"name"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ LoadbalancerID string `q:"loadbalancer_id"`
+ DefaultPoolID string `q:"default_pool_id"`
+ Protocol string `q:"protocol"`
+ ProtocolPort int `q:"protocol_port"`
+ ConnectionLimit int `q:"connection_limit"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToListenerListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToListenerListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// listeners. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those listeners that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToListenerListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return ListenerPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToListenerCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts represents options for creating a listener.
+type CreateOpts struct {
+ // The load balancer on which to provision this listener.
+ LoadbalancerID string `json:"loadbalancer_id" required:"true"`
+
+ // The protocol - can either be TCP, HTTP or HTTPS.
+ Protocol Protocol `json:"protocol" required:"true"`
+
+ // The port on which to listen for client traffic.
+ ProtocolPort int `json:"protocol_port" required:"true"`
+
+ // TenantID is only required if the caller has an admin role and wants
+ // to create a pool for another project.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // ProjectID is only required if the caller has an admin role and wants
+ // to create a pool for another project.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // Human-readable name for the Listener. Does not have to be unique.
+ Name string `json:"name,omitempty"`
+
+ // The ID of the default pool with which the Listener is associated.
+ DefaultPoolID string `json:"default_pool_id,omitempty"`
+
+ // Human-readable description for the Listener.
+ Description string `json:"description,omitempty"`
+
+ // The maximum number of connections allowed for the Listener.
+ ConnLimit *int `json:"connection_limit,omitempty"`
+
+ // A reference to a Barbican container of TLS secrets.
+ DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"`
+
+ // A list of references to TLS secrets.
+ SniContainerRefs []string `json:"sni_container_refs,omitempty"`
+
+ // The administrative state of the Listener. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToListenerCreateMap builds a request body from CreateOpts.
+func (opts CreateOpts) ToListenerCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "listener")
+}
+
+// Create is an operation which provisions a new Listeners based on the
+// configuration defined in the CreateOpts struct. Once the request is
+// validated and progress has started on the provisioning process, a
+// CreateResult will be returned.
+//
+// Users with an admin role can create Listeners on behalf of other tenants by
+// specifying a TenantID attribute different than their own.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToListenerCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Get retrieves a particular Listeners based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToListenerUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents options for updating a Listener.
+type UpdateOpts struct {
+ // Human-readable name for the Listener. Does not have to be unique.
+ Name string `json:"name,omitempty"`
+
+ // Human-readable description for the Listener.
+ Description string `json:"description,omitempty"`
+
+ // The maximum number of connections allowed for the Listener.
+ ConnLimit *int `json:"connection_limit,omitempty"`
+
+ // A reference to a Barbican container of TLS secrets.
+ DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"`
+
+ // A list of references to TLS secrets.
+ SniContainerRefs []string `json:"sni_container_refs,omitempty"`
+
+ // The administrative state of the Listener. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToListenerUpdateMap builds a request body from UpdateOpts.
+func (opts UpdateOpts) ToListenerUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "listener")
+}
+
+// Update is an operation which modifies the attributes of the specified
+// Listener.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+ b, err := opts.ToListenerUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 202},
+ })
+ return
+}
+
+// Delete will permanently delete a particular Listeners based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
diff --git a/openstack/loadbalancer/v2/listeners/results.go b/openstack/loadbalancer/v2/listeners/results.go
new file mode 100644
index 0000000000..728d04266d
--- /dev/null
+++ b/openstack/loadbalancer/v2/listeners/results.go
@@ -0,0 +1,131 @@
+package listeners
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+type LoadBalancerID struct {
+ ID string `json:"id"`
+}
+
+// Listener is the primary load balancing configuration object that specifies
+// the loadbalancer and port on which client traffic is received, as well
+// as other details such as the load balancing method to be use, protocol, etc.
+type Listener struct {
+ // The unique ID for the Listener.
+ ID string `json:"id"`
+
+ // Owner of the Listener.
+ TenantID string `json:"tenant_id"`
+
+ // Human-readable name for the Listener. Does not have to be unique.
+ Name string `json:"name"`
+
+ // Human-readable description for the Listener.
+ Description string `json:"description"`
+
+ // The protocol to loadbalance. A valid value is TCP, HTTP, or HTTPS.
+ Protocol string `json:"protocol"`
+
+ // The port on which to listen to client traffic that is associated with the
+ // Loadbalancer. A valid value is from 0 to 65535.
+ ProtocolPort int `json:"protocol_port"`
+
+ // The UUID of default pool. Must have compatible protocol with listener.
+ DefaultPoolID string `json:"default_pool_id"`
+
+ // A list of load balancer IDs.
+ Loadbalancers []LoadBalancerID `json:"loadbalancers"`
+
+ // The maximum number of connections allowed for the Loadbalancer.
+ // Default is -1, meaning no limit.
+ ConnLimit int `json:"connection_limit"`
+
+ // The list of references to TLS secrets.
+ SniContainerRefs []string `json:"sni_container_refs"`
+
+ // A reference to a Barbican container of TLS secrets.
+ DefaultTlsContainerRef string `json:"default_tls_container_ref"`
+
+ // The administrative state of the Listener. A valid value is true (UP) or false (DOWN).
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // Pools are the pools which are part of this listener.
+ Pools []pools.Pool `json:"pools"`
+}
+
+// ListenerPage is the page returned by a pager when traversing over a
+// collection of listeners.
+type ListenerPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of listeners 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 (r ListenerPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"listeners_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a ListenerPage struct is empty.
+func (r ListenerPage) IsEmpty() (bool, error) {
+ is, err := ExtractListeners(r)
+ return len(is) == 0, err
+}
+
+// ExtractListeners accepts a Page struct, specifically a ListenerPage struct,
+// and extracts the elements into a slice of Listener structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractListeners(r pagination.Page) ([]Listener, error) {
+ var s struct {
+ Listeners []Listener `json:"listeners"`
+ }
+ err := (r.(ListenerPage)).ExtractInto(&s)
+ return s.Listeners, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a listener.
+func (r commonResult) Extract() (*Listener, error) {
+ var s struct {
+ Listener *Listener `json:"listener"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Listener, err
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a Listener.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a Listener.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a Listener.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/loadbalancer/v2/listeners/testing/doc.go b/openstack/loadbalancer/v2/listeners/testing/doc.go
new file mode 100644
index 0000000000..f41387e827
--- /dev/null
+++ b/openstack/loadbalancer/v2/listeners/testing/doc.go
@@ -0,0 +1,2 @@
+// listeners unit tests
+package testing
diff --git a/openstack/loadbalancer/v2/listeners/testing/fixtures.go b/openstack/loadbalancer/v2/listeners/testing/fixtures.go
new file mode 100644
index 0000000000..a3df254b43
--- /dev/null
+++ b/openstack/loadbalancer/v2/listeners/testing/fixtures.go
@@ -0,0 +1,213 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// ListenersListBody contains the canned body of a listeners list response.
+const ListenersListBody = `
+{
+ "listeners":[
+ {
+ "id": "db902c0c-d5ff-4753-b465-668ad9656918",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "web",
+ "description": "listener config for the web tier",
+ "loadbalancers": [{"id": "53306cda-815d-4354-9444-59e09da9c3c5"}],
+ "protocol": "HTTP",
+ "protocol_port": 80,
+ "default_pool_id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ "admin_state_up": true,
+ "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+ "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+ },
+ {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "db",
+ "description": "listener config for the db tier",
+ "loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+ "protocol": "TCP",
+ "protocol_port": 3306,
+ "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+ "connection_limit": 2000,
+ "admin_state_up": true,
+ "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+ "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+ }
+ ]
+}
+`
+
+// SingleServerBody is the canned body of a Get request on an existing listener.
+const SingleListenerBody = `
+{
+ "listener": {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "db",
+ "description": "listener config for the db tier",
+ "loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+ "protocol": "TCP",
+ "protocol_port": 3306,
+ "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+ "connection_limit": 2000,
+ "admin_state_up": true,
+ "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+ "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+ }
+}
+`
+
+// PostUpdateListenerBody is the canned response body of a Update request on an existing listener.
+const PostUpdateListenerBody = `
+{
+ "listener": {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "310df60f-2a10-4ee5-9554-98393092194c",
+ "name": "NewListenerName",
+ "description": "listener config for the db tier",
+ "loadbalancers": [{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+ "protocol": "TCP",
+ "protocol_port": 3306,
+ "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+ "connection_limit": 1000,
+ "admin_state_up": true,
+ "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+ "sni_container_refs": ["3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"]
+ }
+}
+`
+
+var (
+ ListenerWeb = listeners.Listener{
+ ID: "db902c0c-d5ff-4753-b465-668ad9656918",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "web",
+ Description: "listener config for the web tier",
+ Loadbalancers: []listeners.LoadBalancerID{{ID: "53306cda-815d-4354-9444-59e09da9c3c5"}},
+ Protocol: "HTTP",
+ ProtocolPort: 80,
+ DefaultPoolID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ AdminStateUp: true,
+ DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+ SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
+ }
+ ListenerDb = listeners.Listener{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "db",
+ Description: "listener config for the db tier",
+ Loadbalancers: []listeners.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Protocol: "TCP",
+ ProtocolPort: 3306,
+ DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
+ ConnLimit: 2000,
+ AdminStateUp: true,
+ DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+ SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
+ }
+ ListenerUpdated = listeners.Listener{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "310df60f-2a10-4ee5-9554-98393092194c",
+ Name: "NewListenerName",
+ Description: "listener config for the db tier",
+ Loadbalancers: []listeners.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Protocol: "TCP",
+ ProtocolPort: 3306,
+ DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
+ ConnLimit: 1000,
+ AdminStateUp: true,
+ DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+ SniContainerRefs: []string{"3d328d82-2547-4921-ac2f-61c3b452b5ff", "b3cfd7e3-8c19-455c-8ebb-d78dfd8f7e7d"},
+ }
+)
+
+// HandleListenerListSuccessfully sets up the test server to respond to a listener List request.
+func HandleListenerListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, ListenersListBody)
+ case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+ fmt.Fprintf(w, `{ "listeners": [] }`)
+ default:
+ t.Fatalf("/v2.0/lbaas/listeners invoked with unexpected marker=[%s]", marker)
+ }
+ })
+}
+
+// HandleListenerCreationSuccessfully sets up the test server to respond to a listener creation request
+// with a given response.
+func HandleListenerCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/v2.0/lbaas/listeners", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "listener": {
+ "loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab",
+ "protocol": "TCP",
+ "name": "db",
+ "admin_state_up": true,
+ "default_tls_container_ref": "2c433435-20de-4411-84ae-9cc8917def76",
+ "default_pool_id": "41efe233-7591-43c5-9cf7-923964759f9e",
+ "protocol_port": 3306
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
+
+// HandleListenerGetSuccessfully sets up the test server to respond to a listener Get request.
+func HandleListenerGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, SingleListenerBody)
+ })
+}
+
+// HandleListenerDeletionSuccessfully sets up the test server to respond to a listener deletion request.
+func HandleListenerDeletionSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleListenerUpdateSuccessfully sets up the test server to respond to a listener Update request.
+func HandleListenerUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/listeners/4ec89087-d057-4e2c-911f-60a3b47ee304", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `{
+ "listener": {
+ "name": "NewListenerName",
+ "connection_limit": 1001
+ }
+ }`)
+
+ fmt.Fprintf(w, PostUpdateListenerBody)
+ })
+}
diff --git a/openstack/loadbalancer/v2/listeners/testing/requests_test.go b/openstack/loadbalancer/v2/listeners/testing/requests_test.go
new file mode 100644
index 0000000000..4f0a0db0f3
--- /dev/null
+++ b/openstack/loadbalancer/v2/listeners/testing/requests_test.go
@@ -0,0 +1,137 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
+ fake "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/testhelper"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListListeners(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListenerListSuccessfully(t)
+
+ pages := 0
+ err := listeners.List(fake.ServiceClient(), listeners.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := listeners.ExtractListeners(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 listeners, got %d", len(actual))
+ }
+ th.CheckDeepEquals(t, ListenerWeb, actual[0])
+ th.CheckDeepEquals(t, ListenerDb, actual[1])
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestListAllListeners(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListenerListSuccessfully(t)
+
+ allPages, err := listeners.List(fake.ServiceClient(), listeners.ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := listeners.ExtractListeners(allPages)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, ListenerWeb, actual[0])
+ th.CheckDeepEquals(t, ListenerDb, actual[1])
+}
+
+func TestCreateListener(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListenerCreationSuccessfully(t, SingleListenerBody)
+
+ actual, err := listeners.Create(fake.ServiceClient(), listeners.CreateOpts{
+ Protocol: "TCP",
+ Name: "db",
+ LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab",
+ AdminStateUp: gophercloud.Enabled,
+ DefaultTlsContainerRef: "2c433435-20de-4411-84ae-9cc8917def76",
+ DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e",
+ ProtocolPort: 3306,
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ListenerDb, *actual)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := listeners.Create(fake.ServiceClient(), listeners.CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar", Protocol: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = listeners.Create(fake.ServiceClient(), listeners.CreateOpts{Name: "foo", TenantID: "bar", Protocol: "bar", ProtocolPort: 80})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestGetListener(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListenerGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := listeners.Get(client, "4ec89087-d057-4e2c-911f-60a3b47ee304").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, ListenerDb, *actual)
+}
+
+func TestDeleteListener(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListenerDeletionSuccessfully(t)
+
+ res := listeners.Delete(fake.ServiceClient(), "4ec89087-d057-4e2c-911f-60a3b47ee304")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateListener(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListenerUpdateSuccessfully(t)
+
+ client := fake.ServiceClient()
+ i1001 := 1001
+ actual, err := listeners.Update(client, "4ec89087-d057-4e2c-911f-60a3b47ee304", listeners.UpdateOpts{
+ Name: "NewListenerName",
+ ConnLimit: &i1001,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, ListenerUpdated, *actual)
+}
diff --git a/openstack/loadbalancer/v2/listeners/urls.go b/openstack/loadbalancer/v2/listeners/urls.go
new file mode 100644
index 0000000000..02fb1eb39e
--- /dev/null
+++ b/openstack/loadbalancer/v2/listeners/urls.go
@@ -0,0 +1,16 @@
+package listeners
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "lbaas"
+ resourcePath = "listeners"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/loadbalancer/v2/loadbalancers/doc.go b/openstack/loadbalancer/v2/loadbalancers/doc.go
new file mode 100644
index 0000000000..b0a20b8fab
--- /dev/null
+++ b/openstack/loadbalancer/v2/loadbalancers/doc.go
@@ -0,0 +1,76 @@
+/*
+Package loadbalancers provides information and interaction with Load Balancers
+of the LBaaS v2 extension for the OpenStack Networking service.
+
+Example to List Load Balancers
+
+ listOpts := loadbalancers.ListOpts{
+ Provider: "haproxy",
+ }
+
+ allPages, err := loadbalancers.List(networkClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allLoadbalancers, err := loadbalancers.ExtractLoadBalancers(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, lb := range allLoadbalancers {
+ fmt.Printf("%+v\n", lb)
+ }
+
+Example to Create a Load Balancer
+
+ createOpts := loadbalancers.CreateOpts{
+ Name: "db_lb",
+ AdminStateUp: gophercloud.Enabled,
+ VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ VipAddress: "10.30.176.48",
+ Flavor: "medium",
+ Provider: "haproxy",
+ }
+
+ lb, err := loadbalancers.Create(networkClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Load Balancer
+
+ lbID := "d67d56a6-4a86-4688-a282-f46444705c64"
+
+ i1001 := 1001
+ updateOpts := loadbalancers.UpdateOpts{
+ Name: "new-name",
+ }
+
+ lb, err := loadbalancers.Update(networkClient, lbID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Load Balancers
+
+ deleteOpts := loadbalancers.DeleteOpts{
+ Cascade: true,
+ }
+
+ lbID := "d67d56a6-4a86-4688-a282-f46444705c64"
+
+ err := loadbalancers.Delete(networkClient, lbID, deleteOpts).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Get the Status of a Load Balancer
+
+ lbID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ status, err := loadbalancers.GetStatuses(networkClient, LBID).Extract()
+ if err != nil {
+ panic(err)
+ }
+*/
+package loadbalancers
diff --git a/openstack/loadbalancer/v2/loadbalancers/requests.go b/openstack/loadbalancer/v2/loadbalancers/requests.go
new file mode 100644
index 0000000000..9d82f9efa0
--- /dev/null
+++ b/openstack/loadbalancer/v2/loadbalancers/requests.go
@@ -0,0 +1,210 @@
+package loadbalancers
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToLoadBalancerListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the Loadbalancer attributes you want to see returned. SortKey allows you to
+// sort by a particular attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ Description string `q:"description"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ ProvisioningStatus string `q:"provisioning_status"`
+ VipAddress string `q:"vip_address"`
+ VipPortID string `q:"vip_port_id"`
+ VipSubnetID string `q:"vip_subnet_id"`
+ ID string `q:"id"`
+ OperatingStatus string `q:"operating_status"`
+ Name string `q:"name"`
+ Flavor string `q:"flavor"`
+ Provider string `q:"provider"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToLoadBalancerListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToLoadBalancerListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// load balancers. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those load balancers that are owned by
+// the tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToLoadBalancerListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToLoadBalancerCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // Human-readable name for the Loadbalancer. Does not have to be unique.
+ Name string `json:"name,omitempty"`
+
+ // Human-readable description for the Loadbalancer.
+ Description string `json:"description,omitempty"`
+
+ // The network on which to allocate the Loadbalancer's address. A tenant can
+ // only create Loadbalancers on networks authorized by policy (e.g. networks
+ // that belong to them or networks that are shared).
+ VipSubnetID string `json:"vip_subnet_id" required:"true"`
+
+ // TenantID is the UUID of the project who owns the Loadbalancer.
+ // Only administrative users can specify a project UUID other than their own.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // ProjectID is the UUID of the project who owns the Loadbalancer.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // The IP address of the Loadbalancer.
+ VipAddress string `json:"vip_address,omitempty"`
+
+ // The administrative state of the Loadbalancer. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+
+ // The UUID of a flavor.
+ Flavor string `json:"flavor,omitempty"`
+
+ // The name of the provider.
+ Provider string `json:"provider,omitempty"`
+}
+
+// ToLoadBalancerCreateMap builds a request body from CreateOpts.
+func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "loadbalancer")
+}
+
+// Create is an operation which provisions a new loadbalancer based on the
+// configuration defined in the CreateOpts struct. Once the request is
+// validated and progress has started on the provisioning process, a
+// CreateResult will be returned.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToLoadBalancerCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Get retrieves a particular Loadbalancer based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToLoadBalancerUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+ // Human-readable name for the Loadbalancer. Does not have to be unique.
+ Name string `json:"name,omitempty"`
+
+ // Human-readable description for the Loadbalancer.
+ Description string `json:"description,omitempty"`
+
+ // The administrative state of the Loadbalancer. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToLoadBalancerUpdateMap builds a request body from UpdateOpts.
+func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "loadbalancer")
+}
+
+// Update is an operation which modifies the attributes of the specified
+// LoadBalancer.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+ b, err := opts.ToLoadBalancerUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 202},
+ })
+ return
+}
+
+// DeleteOptsBuilder allows extensions to add additional parameters to the
+// Delete request.
+type DeleteOptsBuilder interface {
+ ToLoadBalancerDeleteQuery() (string, error)
+}
+
+// DeleteOpts is the common options struct used in this package's Delete
+// operation.
+type DeleteOpts struct {
+ // Cascade will delete all children of the load balancer (listners, monitors, etc).
+ Cascade bool `q:"cascade"`
+}
+
+// ToLoadBalancerDeleteQuery formats a DeleteOpts into a query string.
+func (opts DeleteOpts) ToLoadBalancerDeleteQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// Delete will permanently delete a particular LoadBalancer based on its
+// unique ID.
+func Delete(c *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) {
+ url := resourceURL(c, id)
+ if opts != nil {
+ query, err := opts.ToLoadBalancerDeleteQuery()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ url += query
+ }
+ _, r.Err = c.Delete(url, nil)
+ return
+}
+
+// GetStatuses will return the status of a particular LoadBalancer.
+func GetStatuses(c *gophercloud.ServiceClient, id string) (r GetStatusesResult) {
+ _, r.Err = c.Get(statusRootURL(c, id), &r.Body, nil)
+ return
+}
diff --git a/openstack/loadbalancer/v2/loadbalancers/results.go b/openstack/loadbalancer/v2/loadbalancers/results.go
new file mode 100644
index 0000000000..80a9ff0557
--- /dev/null
+++ b/openstack/loadbalancer/v2/loadbalancers/results.go
@@ -0,0 +1,149 @@
+package loadbalancers
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// LoadBalancer is the primary load balancing configuration object that
+// specifies the virtual IP address on which client traffic is received, as well
+// as other details such as the load balancing method to be use, protocol, etc.
+type LoadBalancer struct {
+ // Human-readable description for the Loadbalancer.
+ Description string `json:"description"`
+
+ // The administrative state of the Loadbalancer.
+ // A valid value is true (UP) or false (DOWN).
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // Owner of the LoadBalancer.
+ TenantID string `json:"tenant_id"`
+
+ // The provisioning status of the LoadBalancer.
+ // This value is ACTIVE, PENDING_CREATE or ERROR.
+ ProvisioningStatus string `json:"provisioning_status"`
+
+ // The IP address of the Loadbalancer.
+ VipAddress string `json:"vip_address"`
+
+ // The UUID of the port associated with the IP address.
+ VipPortID string `json:"vip_port_id"`
+
+ // The UUID of the subnet on which to allocate the virtual IP for the
+ // Loadbalancer address.
+ VipSubnetID string `json:"vip_subnet_id"`
+
+ // The unique ID for the LoadBalancer.
+ ID string `json:"id"`
+
+ // The operating status of the LoadBalancer. This value is ONLINE or OFFLINE.
+ OperatingStatus string `json:"operating_status"`
+
+ // Human-readable name for the LoadBalancer. Does not have to be unique.
+ Name string `json:"name"`
+
+ // The UUID of a flavor if set.
+ Flavor string `json:"flavor"`
+
+ // The name of the provider.
+ Provider string `json:"provider"`
+
+ // Listeners are the listeners related to this Loadbalancer.
+ Listeners []listeners.Listener `json:"listeners"`
+}
+
+// StatusTree represents the status of a loadbalancer.
+type StatusTree struct {
+ Loadbalancer *LoadBalancer `json:"loadbalancer"`
+}
+
+// LoadBalancerPage is the page returned by a pager when traversing over a
+// collection of load balancers.
+type LoadBalancerPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of load balancers 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 (r LoadBalancerPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"loadbalancers_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a LoadBalancerPage struct is empty.
+func (r LoadBalancerPage) IsEmpty() (bool, error) {
+ is, err := ExtractLoadBalancers(r)
+ return len(is) == 0, err
+}
+
+// ExtractLoadBalancers accepts a Page struct, specifically a LoadbalancerPage
+// struct, and extracts the elements into a slice of LoadBalancer structs. In
+// other words, a generic collection is mapped into a relevant slice.
+func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) {
+ var s struct {
+ LoadBalancers []LoadBalancer `json:"loadbalancers"`
+ }
+ err := (r.(LoadBalancerPage)).ExtractInto(&s)
+ return s.LoadBalancers, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a loadbalancer.
+func (r commonResult) Extract() (*LoadBalancer, error) {
+ var s struct {
+ LoadBalancer *LoadBalancer `json:"loadbalancer"`
+ }
+ err := r.ExtractInto(&s)
+ return s.LoadBalancer, err
+}
+
+// GetStatusesResult represents the result of a GetStatuses operation.
+// Call its Extract method to interpret it as a StatusTree.
+type GetStatusesResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts the status of
+// a Loadbalancer.
+func (r GetStatusesResult) Extract() (*StatusTree, error) {
+ var s struct {
+ Statuses *StatusTree `json:"statuses"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Statuses, err
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a LoadBalancer.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a LoadBalancer.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a LoadBalancer.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/loadbalancer/v2/loadbalancers/testing/doc.go b/openstack/loadbalancer/v2/loadbalancers/testing/doc.go
new file mode 100644
index 0000000000..b54468c82f
--- /dev/null
+++ b/openstack/loadbalancer/v2/loadbalancers/testing/doc.go
@@ -0,0 +1,2 @@
+// loadbalancers unit tests
+package testing
diff --git a/openstack/loadbalancer/v2/loadbalancers/testing/fixtures.go b/openstack/loadbalancer/v2/loadbalancers/testing/fixtures.go
new file mode 100644
index 0000000000..759d2d9ed6
--- /dev/null
+++ b/openstack/loadbalancer/v2/loadbalancers/testing/fixtures.go
@@ -0,0 +1,284 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools"
+)
+
+// LoadbalancersListBody contains the canned body of a loadbalancer list response.
+const LoadbalancersListBody = `
+{
+ "loadbalancers":[
+ {
+ "id": "c331058c-6a40-4144-948e-b9fb1df9db4b",
+ "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+ "name": "web_lb",
+ "description": "lb config for the web tier",
+ "vip_subnet_id": "8a49c438-848f-467b-9655-ea1548708154",
+ "vip_address": "10.30.176.47",
+ "vip_port_id": "2a22e552-a347-44fd-b530-1f2b1b2a6735",
+ "flavor": "small",
+ "provider": "haproxy",
+ "admin_state_up": true,
+ "provisioning_status": "ACTIVE",
+ "operating_status": "ONLINE"
+ },
+ {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+ "name": "db_lb",
+ "description": "lb config for the db tier",
+ "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ "vip_address": "10.30.176.48",
+ "vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+ "flavor": "medium",
+ "provider": "haproxy",
+ "admin_state_up": true,
+ "provisioning_status": "PENDING_CREATE",
+ "operating_status": "OFFLINE"
+ }
+ ]
+}
+`
+
+// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer.
+const SingleLoadbalancerBody = `
+{
+ "loadbalancer": {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+ "name": "db_lb",
+ "description": "lb config for the db tier",
+ "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ "vip_address": "10.30.176.48",
+ "vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+ "flavor": "medium",
+ "provider": "haproxy",
+ "admin_state_up": true,
+ "provisioning_status": "PENDING_CREATE",
+ "operating_status": "OFFLINE"
+ }
+}
+`
+
+// PostUpdateLoadbalancerBody is the canned response body of a Update request on an existing loadbalancer.
+const PostUpdateLoadbalancerBody = `
+{
+ "loadbalancer": {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "tenant_id": "54030507-44f7-473c-9342-b4d14a95f692",
+ "name": "NewLoadbalancerName",
+ "description": "lb config for the db tier",
+ "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ "vip_address": "10.30.176.48",
+ "vip_port_id": "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+ "flavor": "medium",
+ "provider": "haproxy",
+ "admin_state_up": true,
+ "provisioning_status": "PENDING_CREATE",
+ "operating_status": "OFFLINE"
+ }
+}
+`
+
+// SingleLoadbalancerBody is the canned body of a Get request on an existing loadbalancer.
+const LoadbalancerStatuesesTree = `
+{
+ "statuses" : {
+ "loadbalancer": {
+ "id": "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ "name": "db_lb",
+ "provisioning_status": "PENDING_UPDATE",
+ "operating_status": "ACTIVE",
+ "listeners": [{
+ "id": "db902c0c-d5ff-4753-b465-668ad9656918",
+ "name": "db",
+ "pools": [{
+ "id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ "name": "db",
+ "healthmonitor": {
+ "id": "67306cda-815d-4354-9fe4-59e09da9c3c5",
+ "type":"PING"
+ },
+ "members":[{
+ "id": "2a280670-c202-4b0b-a562-34077415aabf",
+ "name": "db",
+ "address": "10.0.2.11",
+ "protocol_port": 80
+ }]
+ }]
+ }]
+ }
+ }
+}
+`
+
+var (
+ LoadbalancerWeb = loadbalancers.LoadBalancer{
+ ID: "c331058c-6a40-4144-948e-b9fb1df9db4b",
+ TenantID: "54030507-44f7-473c-9342-b4d14a95f692",
+ Name: "web_lb",
+ Description: "lb config for the web tier",
+ VipSubnetID: "8a49c438-848f-467b-9655-ea1548708154",
+ VipAddress: "10.30.176.47",
+ VipPortID: "2a22e552-a347-44fd-b530-1f2b1b2a6735",
+ Flavor: "small",
+ Provider: "haproxy",
+ AdminStateUp: true,
+ ProvisioningStatus: "ACTIVE",
+ OperatingStatus: "ONLINE",
+ }
+ LoadbalancerDb = loadbalancers.LoadBalancer{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "54030507-44f7-473c-9342-b4d14a95f692",
+ Name: "db_lb",
+ Description: "lb config for the db tier",
+ VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ VipAddress: "10.30.176.48",
+ VipPortID: "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+ Flavor: "medium",
+ Provider: "haproxy",
+ AdminStateUp: true,
+ ProvisioningStatus: "PENDING_CREATE",
+ OperatingStatus: "OFFLINE",
+ }
+ LoadbalancerUpdated = loadbalancers.LoadBalancer{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ TenantID: "54030507-44f7-473c-9342-b4d14a95f692",
+ Name: "NewLoadbalancerName",
+ Description: "lb config for the db tier",
+ VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ VipAddress: "10.30.176.48",
+ VipPortID: "2bf413c8-41a9-4477-b505-333d5cbe8b55",
+ Flavor: "medium",
+ Provider: "haproxy",
+ AdminStateUp: true,
+ ProvisioningStatus: "PENDING_CREATE",
+ OperatingStatus: "OFFLINE",
+ }
+ LoadbalancerStatusesTree = loadbalancers.LoadBalancer{
+ ID: "36e08a3e-a78f-4b40-a229-1e7e23eee1ab",
+ Name: "db_lb",
+ ProvisioningStatus: "PENDING_UPDATE",
+ OperatingStatus: "ACTIVE",
+ Listeners: []listeners.Listener{{
+ ID: "db902c0c-d5ff-4753-b465-668ad9656918",
+ Name: "db",
+ Pools: []pools.Pool{{
+ ID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ Name: "db",
+ Monitor: monitors.Monitor{
+ ID: "67306cda-815d-4354-9fe4-59e09da9c3c5",
+ Type: "PING",
+ },
+ Members: []pools.Member{{
+ ID: "2a280670-c202-4b0b-a562-34077415aabf",
+ Name: "db",
+ Address: "10.0.2.11",
+ ProtocolPort: 80,
+ }},
+ }},
+ }},
+ }
+)
+
+// HandleLoadbalancerListSuccessfully sets up the test server to respond to a loadbalancer List request.
+func HandleLoadbalancerListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, LoadbalancersListBody)
+ case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+ fmt.Fprintf(w, `{ "loadbalancers": [] }`)
+ default:
+ t.Fatalf("/v2.0/lbaas/loadbalancers invoked with unexpected marker=[%s]", marker)
+ }
+ })
+}
+
+// HandleLoadbalancerCreationSuccessfully sets up the test server to respond to a loadbalancer creation request
+// with a given response.
+func HandleLoadbalancerCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "loadbalancer": {
+ "name": "db_lb",
+ "vip_subnet_id": "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ "vip_address": "10.30.176.48",
+ "flavor": "medium",
+ "provider": "haproxy",
+ "admin_state_up": true
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
+
+// HandleLoadbalancerGetSuccessfully sets up the test server to respond to a loadbalancer Get request.
+func HandleLoadbalancerGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, SingleLoadbalancerBody)
+ })
+}
+
+// HandleLoadbalancerGetStatusesTree sets up the test server to respond to a loadbalancer Get statuses tree request.
+func HandleLoadbalancerGetStatusesTree(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab/statuses", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, LoadbalancerStatuesesTree)
+ })
+}
+
+// HandleLoadbalancerDeletionSuccessfully sets up the test server to respond to a loadbalancer deletion request.
+func HandleLoadbalancerDeletionSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleLoadbalancerUpdateSuccessfully sets up the test server to respond to a loadbalancer Update request.
+func HandleLoadbalancerUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/loadbalancers/36e08a3e-a78f-4b40-a229-1e7e23eee1ab", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `{
+ "loadbalancer": {
+ "name": "NewLoadbalancerName"
+ }
+ }`)
+
+ fmt.Fprintf(w, PostUpdateLoadbalancerBody)
+ })
+}
diff --git a/openstack/loadbalancer/v2/loadbalancers/testing/requests_test.go b/openstack/loadbalancer/v2/loadbalancers/testing/requests_test.go
new file mode 100644
index 0000000000..51374e0460
--- /dev/null
+++ b/openstack/loadbalancer/v2/loadbalancers/testing/requests_test.go
@@ -0,0 +1,162 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers"
+ fake "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/testhelper"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListLoadbalancers(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerListSuccessfully(t)
+
+ pages := 0
+ err := loadbalancers.List(fake.ServiceClient(), loadbalancers.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := loadbalancers.ExtractLoadBalancers(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 loadbalancers, got %d", len(actual))
+ }
+ th.CheckDeepEquals(t, LoadbalancerWeb, actual[0])
+ th.CheckDeepEquals(t, LoadbalancerDb, actual[1])
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestListAllLoadbalancers(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerListSuccessfully(t)
+
+ allPages, err := loadbalancers.List(fake.ServiceClient(), loadbalancers.ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := loadbalancers.ExtractLoadBalancers(allPages)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, LoadbalancerWeb, actual[0])
+ th.CheckDeepEquals(t, LoadbalancerDb, actual[1])
+}
+
+func TestCreateLoadbalancer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerCreationSuccessfully(t, SingleLoadbalancerBody)
+
+ actual, err := loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{
+ Name: "db_lb",
+ AdminStateUp: gophercloud.Enabled,
+ VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086",
+ VipAddress: "10.30.176.48",
+ Flavor: "medium",
+ Provider: "haproxy",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, LoadbalancerDb, *actual)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo", Description: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = loadbalancers.Create(fake.ServiceClient(), loadbalancers.CreateOpts{Name: "foo", Description: "bar", VipAddress: "bar"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestGetLoadbalancer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := loadbalancers.Get(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, LoadbalancerDb, *actual)
+}
+
+func TestGetLoadbalancerStatusesTree(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerGetStatusesTree(t)
+
+ client := fake.ServiceClient()
+ actual, err := loadbalancers.GetStatuses(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, LoadbalancerStatusesTree, *(actual.Loadbalancer))
+}
+
+func TestDeleteLoadbalancer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerDeletionSuccessfully(t)
+
+ res := loadbalancers.Delete(fake.ServiceClient(), "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", nil)
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateLoadbalancer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerUpdateSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := loadbalancers.Update(client, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", loadbalancers.UpdateOpts{
+ Name: "NewLoadbalancerName",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, LoadbalancerUpdated, *actual)
+}
+
+func TestCascadingDeleteLoadbalancer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerDeletionSuccessfully(t)
+
+ sc := fake.ServiceClient()
+ deleteOpts := loadbalancers.DeleteOpts{
+ Cascade: true,
+ }
+
+ query, err := deleteOpts.ToLoadBalancerDeleteQuery()
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, query, "?cascade=true")
+
+ err = loadbalancers.Delete(sc, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab", deleteOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/loadbalancer/v2/loadbalancers/urls.go b/openstack/loadbalancer/v2/loadbalancers/urls.go
new file mode 100644
index 0000000000..73cf5dc126
--- /dev/null
+++ b/openstack/loadbalancer/v2/loadbalancers/urls.go
@@ -0,0 +1,21 @@
+package loadbalancers
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "lbaas"
+ resourcePath = "loadbalancers"
+ statusPath = "statuses"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
+
+func statusRootURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id, statusPath)
+}
diff --git a/openstack/loadbalancer/v2/monitors/doc.go b/openstack/loadbalancer/v2/monitors/doc.go
new file mode 100644
index 0000000000..6ed8c8fb5f
--- /dev/null
+++ b/openstack/loadbalancer/v2/monitors/doc.go
@@ -0,0 +1,69 @@
+/*
+Package monitors provides information and interaction with Monitors
+of the LBaaS v2 extension for the OpenStack Networking service.
+
+Example to List Monitors
+
+ listOpts := monitors.ListOpts{
+ PoolID: "c79a4468-d788-410c-bf79-9a8ef6354852",
+ }
+
+ allPages, err := monitors.List(networkClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allMonitors, err := monitors.ExtractMonitors(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, monitor := range allMonitors {
+ fmt.Printf("%+v\n", monitor)
+ }
+
+Example to Create a Monitor
+
+ createOpts := monitors.CreateOpts{
+ Type: "HTTP",
+ Name: "db",
+ PoolID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
+ Delay: 20,
+ Timeout: 10,
+ MaxRetries: 5,
+ URLPath: "/check",
+ ExpectedCodes: "200-299",
+ }
+
+ monitor, err := monitors.Create(networkClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Monitor
+
+ monitorID := "d67d56a6-4a86-4688-a282-f46444705c64"
+
+ updateOpts := monitors.UpdateOpts{
+ Name: "NewHealthmonitorName",
+ Delay: 3,
+ Timeout: 20,
+ MaxRetries: 10,
+ URLPath: "/another_check",
+ ExpectedCodes: "301",
+ }
+
+ monitor, err := monitors.Update(networkClient, monitorID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Monitor
+
+ monitorID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ err := monitors.Delete(networkClient, monitorID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+*/
+package monitors
diff --git a/openstack/loadbalancer/v2/monitors/requests.go b/openstack/loadbalancer/v2/monitors/requests.go
new file mode 100644
index 0000000000..c173e1c64e
--- /dev/null
+++ b/openstack/loadbalancer/v2/monitors/requests.go
@@ -0,0 +1,257 @@
+package monitors
+
+import (
+ "fmt"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToMonitorListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the Monitor attributes you want to see returned. SortKey allows you to
+// sort by a particular Monitor attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ ID string `q:"id"`
+ Name string `q:"name"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ PoolID string `q:"pool_id"`
+ Type string `q:"type"`
+ Delay int `q:"delay"`
+ Timeout int `q:"timeout"`
+ MaxRetries int `q:"max_retries"`
+ HTTPMethod string `q:"http_method"`
+ URLPath string `q:"url_path"`
+ ExpectedCodes string `q:"expected_codes"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Status string `q:"status"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToMonitorListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToMonitorListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return "", err
+ }
+ return q.String(), nil
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// health monitors. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those health monitors that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToMonitorListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return MonitorPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// Constants that represent approved monitoring types.
+const (
+ TypePING = "PING"
+ TypeTCP = "TCP"
+ TypeHTTP = "HTTP"
+ TypeHTTPS = "HTTPS"
+)
+
+var (
+ errDelayMustGETimeout = fmt.Errorf("Delay must be greater than or equal to timeout")
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type CreateOptsBuilder interface {
+ ToMonitorCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // The Pool to Monitor.
+ PoolID string `json:"pool_id" required:"true"`
+
+ // 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 `json:"type" required:"true"`
+
+ // The time, in seconds, between sending probes to members.
+ Delay int `json:"delay" required:"true"`
+
+ // 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 `json:"timeout" required:"true"`
+
+ // Number of permissible ping failures before changing the member's
+ // status to INACTIVE. Must be a number between 1 and 10.
+ MaxRetries int `json:"max_retries" required:"true"`
+
+ // URI path that will be accessed if Monitor type is HTTP or HTTPS.
+ // Required for HTTP(S) types.
+ URLPath string `json:"url_path,omitempty"`
+
+ // The HTTP method used for requests by the Monitor. If this attribute
+ // is not specified, it defaults to "GET". Required for HTTP(S) types.
+ HTTPMethod string `json:"http_method,omitempty"`
+
+ // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify
+ // a single status like "200", or a range like "200-202". Required for HTTP(S)
+ // types.
+ ExpectedCodes string `json:"expected_codes,omitempty"`
+
+ // TenantID is the UUID of the project who owns the Monitor.
+ // Only administrative users can specify a project UUID other than their own.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // ProjectID is the UUID of the project who owns the Monitor.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // The Name of the Monitor.
+ Name string `json:"name,omitempty"`
+
+ // The administrative state of the Monitor. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToMonitorCreateMap builds a request body from CreateOpts.
+func (opts CreateOpts) ToMonitorCreateMap() (map[string]interface{}, error) {
+ b, err := gophercloud.BuildRequestBody(opts, "healthmonitor")
+ if err != nil {
+ return nil, err
+ }
+
+ switch opts.Type {
+ case TypeHTTP, TypeHTTPS:
+ switch opts.URLPath {
+ case "":
+ return nil, fmt.Errorf("URLPath must be provided for HTTP and HTTPS")
+ }
+ switch opts.ExpectedCodes {
+ case "":
+ return nil, fmt.Errorf("ExpectedCodes must be provided for HTTP and HTTPS")
+ }
+ }
+
+ return b, nil
+}
+
+/*
+ Create is an operation which provisions a new Health Monitor. There are
+ different types of Monitor you can provision: PING, TCP or HTTP(S). Below
+ are examples of how to create each one.
+
+ Here is an example config struct to use when creating a PING or TCP Monitor:
+
+ CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3}
+ CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3}
+
+ Here is an example config struct to use when creating a HTTP(S) Monitor:
+
+ CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3,
+ HttpMethod: "HEAD", ExpectedCodes: "200", PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"}
+*/
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToMonitorCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Get retrieves a particular Health Monitor based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToMonitorUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+ // The time, in seconds, between sending probes to members.
+ Delay int `json:"delay,omitempty"`
+
+ // 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 `json:"timeout,omitempty"`
+
+ // Number of permissible ping failures before changing the member's
+ // status to INACTIVE. Must be a number between 1 and 10.
+ MaxRetries int `json:"max_retries,omitempty"`
+
+ // URI path that will be accessed if Monitor type is HTTP or HTTPS.
+ // Required for HTTP(S) types.
+ URLPath string `json:"url_path,omitempty"`
+
+ // The HTTP method used for requests by the Monitor. If this attribute
+ // is not specified, it defaults to "GET". Required for HTTP(S) types.
+ HTTPMethod string `json:"http_method,omitempty"`
+
+ // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify
+ // a single status like "200", or a range like "200-202". Required for HTTP(S)
+ // types.
+ ExpectedCodes string `json:"expected_codes,omitempty"`
+
+ // The Name of the Monitor.
+ Name string `json:"name,omitempty"`
+
+ // The administrative state of the Monitor. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToMonitorUpdateMap builds a request body from UpdateOpts.
+func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "healthmonitor")
+}
+
+// Update is an operation which modifies the attributes of the specified
+// Monitor.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToMonitorUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 202},
+ })
+ return
+}
+
+// Delete will permanently delete a particular Monitor based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
diff --git a/openstack/loadbalancer/v2/monitors/results.go b/openstack/loadbalancer/v2/monitors/results.go
new file mode 100644
index 0000000000..ea832cc5d0
--- /dev/null
+++ b/openstack/loadbalancer/v2/monitors/results.go
@@ -0,0 +1,149 @@
+package monitors
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+type PoolID struct {
+ ID string `json:"id"`
+}
+
+// Monitor represents a load balancer health monitor. A health monitor is used
+// to determine whether or not back-end members of the VIP's pool are usable
+// for processing a request. A pool can have several health monitors associated
+// with it. There are different types of health monitors supported:
+//
+// PING: used to ping the members using ICMP.
+// TCP: used to connect to the members using TCP.
+// HTTP: used to send an HTTP request to the member.
+// HTTPS: used to send a secure HTTP request to the member.
+//
+// When a pool has several monitors associated with it, each member of the pool
+// is monitored by all these monitors. If any monitor declares the member as
+// unhealthy, then the member status is changed to INACTIVE and the member
+// won't participate in its pool's load balancing. In other words, ALL monitors
+// must declare the member to be healthy for it to stay ACTIVE.
+type Monitor struct {
+ // The unique ID for the Monitor.
+ ID string `json:"id"`
+
+ // The Name of the Monitor.
+ Name string `json:"name"`
+
+ // TenantID is the owner of the Monitor.
+ TenantID string `json:"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 `json:"type"`
+
+ // The time, in seconds, between sending probes to members.
+ Delay int `json:"delay"`
+
+ // 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 `json:"timeout"`
+
+ // 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"`
+
+ // The HTTP method that the monitor uses for requests.
+ HTTPMethod string `json:"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" `
+
+ // Expected HTTP codes for a passing HTTP(S) monitor.
+ ExpectedCodes string `json:"expected_codes"`
+
+ // The administrative state of the health monitor, which is up (true) or
+ // down (false).
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // The status of the health monitor. Indicates whether the health monitor is
+ // operational.
+ Status string `json:"status"`
+
+ // List of pools that are associated with the health monitor.
+ Pools []PoolID `json:"pools"`
+}
+
+// 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 (r MonitorPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"healthmonitors_links"`
+ }
+
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a MonitorPage struct is empty.
+func (r MonitorPage) IsEmpty() (bool, error) {
+ is, err := ExtractMonitors(r)
+ return len(is) == 0, err
+}
+
+// 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(r pagination.Page) ([]Monitor, error) {
+ var s struct {
+ Monitors []Monitor `json:"healthmonitors"`
+ }
+ err := (r.(MonitorPage)).ExtractInto(&s)
+ return s.Monitors, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a monitor.
+func (r commonResult) Extract() (*Monitor, error) {
+ var s struct {
+ Monitor *Monitor `json:"healthmonitor"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Monitor, err
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a Monitor.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a Monitor.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a Monitor.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation. Call its
+// ExtractErr method to determine if the result succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/loadbalancer/v2/monitors/testing/doc.go b/openstack/loadbalancer/v2/monitors/testing/doc.go
new file mode 100644
index 0000000000..e2b6f12a92
--- /dev/null
+++ b/openstack/loadbalancer/v2/monitors/testing/doc.go
@@ -0,0 +1,2 @@
+// monitors unit tests
+package testing
diff --git a/openstack/loadbalancer/v2/monitors/testing/fixtures.go b/openstack/loadbalancer/v2/monitors/testing/fixtures.go
new file mode 100644
index 0000000000..262ebbe8f1
--- /dev/null
+++ b/openstack/loadbalancer/v2/monitors/testing/fixtures.go
@@ -0,0 +1,215 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// HealthmonitorsListBody contains the canned body of a healthmonitor list response.
+const HealthmonitorsListBody = `
+{
+ "healthmonitors":[
+ {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":10,
+ "name":"web",
+ "max_retries":1,
+ "timeout":1,
+ "type":"PING",
+ "pools": [{"id": "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}],
+ "id":"466c8345-28d8-4f84-a246-e04380b0461d"
+ },
+ {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":5,
+ "name":"db",
+ "expected_codes":"200",
+ "max_retries":2,
+ "http_method":"GET",
+ "timeout":2,
+ "url_path":"/",
+ "type":"HTTP",
+ "pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
+ "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+ }
+ ]
+}
+`
+
+// SingleHealthmonitorBody is the canned body of a Get request on an existing healthmonitor.
+const SingleHealthmonitorBody = `
+{
+ "healthmonitor": {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":5,
+ "name":"db",
+ "expected_codes":"200",
+ "max_retries":2,
+ "http_method":"GET",
+ "timeout":2,
+ "url_path":"/",
+ "type":"HTTP",
+ "pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
+ "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+ }
+}
+`
+
+// PostUpdateHealthmonitorBody is the canned response body of a Update request on an existing healthmonitor.
+const PostUpdateHealthmonitorBody = `
+{
+ "healthmonitor": {
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "delay":3,
+ "name":"NewHealthmonitorName",
+ "expected_codes":"301",
+ "max_retries":10,
+ "http_method":"GET",
+ "timeout":20,
+ "url_path":"/another_check",
+ "type":"HTTP",
+ "pools": [{"id": "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}],
+ "id":"5d4b5228-33b0-4e60-b225-9b727c1a20e7"
+ }
+}
+`
+
+var (
+ HealthmonitorWeb = monitors.Monitor{
+ AdminStateUp: true,
+ Name: "web",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 10,
+ MaxRetries: 1,
+ Timeout: 1,
+ Type: "PING",
+ ID: "466c8345-28d8-4f84-a246-e04380b0461d",
+ Pools: []monitors.PoolID{{ID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d"}},
+ }
+ HealthmonitorDb = monitors.Monitor{
+ AdminStateUp: true,
+ Name: "db",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 5,
+ ExpectedCodes: "200",
+ MaxRetries: 2,
+ Timeout: 2,
+ URLPath: "/",
+ Type: "HTTP",
+ HTTPMethod: "GET",
+ ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+ Pools: []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
+ }
+ HealthmonitorUpdated = monitors.Monitor{
+ AdminStateUp: true,
+ Name: "NewHealthmonitorName",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ Delay: 3,
+ ExpectedCodes: "301",
+ MaxRetries: 10,
+ Timeout: 20,
+ URLPath: "/another_check",
+ Type: "HTTP",
+ HTTPMethod: "GET",
+ ID: "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
+ Pools: []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
+ }
+)
+
+// HandleHealthmonitorListSuccessfully sets up the test server to respond to a healthmonitor List request.
+func HandleHealthmonitorListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, HealthmonitorsListBody)
+ case "556c8345-28d8-4f84-a246-e04380b0461d":
+ fmt.Fprintf(w, `{ "healthmonitors": [] }`)
+ default:
+ t.Fatalf("/v2.0/lbaas/healthmonitors invoked with unexpected marker=[%s]", marker)
+ }
+ })
+}
+
+// HandleHealthmonitorCreationSuccessfully sets up the test server to respond to a healthmonitor creation request
+// with a given response.
+func HandleHealthmonitorCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "healthmonitor": {
+ "type":"HTTP",
+ "pool_id":"84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
+ "tenant_id":"453105b9-1754-413f-aab1-55f1af620750",
+ "delay":20,
+ "name":"db",
+ "timeout":10,
+ "max_retries":5,
+ "url_path":"/check",
+ "expected_codes":"200-299"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
+
+// HandleHealthmonitorGetSuccessfully sets up the test server to respond to a healthmonitor Get request.
+func HandleHealthmonitorGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, SingleHealthmonitorBody)
+ })
+}
+
+// HandleHealthmonitorDeletionSuccessfully sets up the test server to respond to a healthmonitor deletion request.
+func HandleHealthmonitorDeletionSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleHealthmonitorUpdateSuccessfully sets up the test server to respond to a healthmonitor Update request.
+func HandleHealthmonitorUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/healthmonitors/5d4b5228-33b0-4e60-b225-9b727c1a20e7", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `{
+ "healthmonitor": {
+ "name": "NewHealthmonitorName",
+ "delay": 3,
+ "timeout": 20,
+ "max_retries": 10,
+ "url_path": "/another_check",
+ "expected_codes": "301"
+ }
+ }`)
+
+ fmt.Fprintf(w, PostUpdateHealthmonitorBody)
+ })
+}
diff --git a/openstack/loadbalancer/v2/monitors/testing/requests_test.go b/openstack/loadbalancer/v2/monitors/testing/requests_test.go
new file mode 100644
index 0000000000..80cba9ca70
--- /dev/null
+++ b/openstack/loadbalancer/v2/monitors/testing/requests_test.go
@@ -0,0 +1,154 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
+ fake "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/testhelper"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListHealthmonitors(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorListSuccessfully(t)
+
+ pages := 0
+ err := monitors.List(fake.ServiceClient(), monitors.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := monitors.ExtractMonitors(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 healthmonitors, got %d", len(actual))
+ }
+ th.CheckDeepEquals(t, HealthmonitorWeb, actual[0])
+ th.CheckDeepEquals(t, HealthmonitorDb, actual[1])
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestListAllHealthmonitors(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorListSuccessfully(t)
+
+ allPages, err := monitors.List(fake.ServiceClient(), monitors.ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := monitors.ExtractMonitors(allPages)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, HealthmonitorWeb, actual[0])
+ th.CheckDeepEquals(t, HealthmonitorDb, actual[1])
+}
+
+func TestCreateHealthmonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorCreationSuccessfully(t, SingleHealthmonitorBody)
+
+ actual, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{
+ Type: "HTTP",
+ Name: "db",
+ PoolID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
+ TenantID: "453105b9-1754-413f-aab1-55f1af620750",
+ Delay: 20,
+ Timeout: 10,
+ MaxRetries: 5,
+ URLPath: "/check",
+ ExpectedCodes: "200-299",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, HealthmonitorDb, *actual)
+}
+
+func TestRequiredCreateOpts(t *testing.T) {
+ res := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = monitors.Create(fake.ServiceClient(), monitors.CreateOpts{Type: monitors.TypeHTTP})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
+
+func TestGetHealthmonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := monitors.Get(client, "5d4b5228-33b0-4e60-b225-9b727c1a20e7").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, HealthmonitorDb, *actual)
+}
+
+func TestDeleteHealthmonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorDeletionSuccessfully(t)
+
+ res := monitors.Delete(fake.ServiceClient(), "5d4b5228-33b0-4e60-b225-9b727c1a20e7")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateHealthmonitor(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleHealthmonitorUpdateSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := monitors.Update(client, "5d4b5228-33b0-4e60-b225-9b727c1a20e7", monitors.UpdateOpts{
+ Name: "NewHealthmonitorName",
+ Delay: 3,
+ Timeout: 20,
+ MaxRetries: 10,
+ URLPath: "/another_check",
+ ExpectedCodes: "301",
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, HealthmonitorUpdated, *actual)
+}
+
+func TestDelayMustBeGreaterOrEqualThanTimeout(t *testing.T) {
+ _, err := monitors.Create(fake.ServiceClient(), monitors.CreateOpts{
+ Type: "HTTP",
+ PoolID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d",
+ Delay: 1,
+ Timeout: 10,
+ MaxRetries: 5,
+ URLPath: "/check",
+ ExpectedCodes: "200-299",
+ }).Extract()
+
+ if err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+
+ _, err = monitors.Update(fake.ServiceClient(), "453105b9-1754-413f-aab1-55f1af620750", monitors.UpdateOpts{
+ Delay: 1,
+ Timeout: 10,
+ }).Extract()
+
+ if err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+}
diff --git a/openstack/loadbalancer/v2/monitors/urls.go b/openstack/loadbalancer/v2/monitors/urls.go
new file mode 100644
index 0000000000..a222e52a93
--- /dev/null
+++ b/openstack/loadbalancer/v2/monitors/urls.go
@@ -0,0 +1,16 @@
+package monitors
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "lbaas"
+ resourcePath = "healthmonitors"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/loadbalancer/v2/pools/doc.go b/openstack/loadbalancer/v2/pools/doc.go
new file mode 100644
index 0000000000..2d57ed4393
--- /dev/null
+++ b/openstack/loadbalancer/v2/pools/doc.go
@@ -0,0 +1,124 @@
+/*
+Package pools provides information and interaction with Pools and
+Members of the LBaaS v2 extension for the OpenStack Networking service.
+
+Example to List Pools
+
+ listOpts := pools.ListOpts{
+ LoadbalancerID: "c79a4468-d788-410c-bf79-9a8ef6354852",
+ }
+
+ allPages, err := pools.List(networkClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allPools, err := pools.ExtractMonitors(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, pools := range allPools {
+ fmt.Printf("%+v\n", pool)
+ }
+
+Example to Create a Pool
+
+ createOpts := pools.CreateOpts{
+ LBMethod: pools.LBMethodRoundRobin,
+ Protocol: "HTTP",
+ Name: "Example pool",
+ LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab",
+ }
+
+ pool, err := pools.Create(networkClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Pool
+
+ poolID := "d67d56a6-4a86-4688-a282-f46444705c64"
+
+ updateOpts := pools.UpdateOpts{
+ Name: "new-name",
+ }
+
+ pool, err := pools.Update(networkClient, poolID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Pool
+
+ poolID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ err := pools.Delete(networkClient, poolID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to List Pool Members
+
+ poolID := "d67d56a6-4a86-4688-a282-f46444705c64"
+
+ listOpts := pools.ListMemberOpts{
+ ProtocolPort: 80,
+ }
+
+ allPages, err := pools.ListMembers(networkClient, poolID, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allMembers, err := pools.ExtractMembers(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, member := allMembers {
+ fmt.Printf("%+v\n", member)
+ }
+
+Example to Create a Member
+
+ poolID := "d67d56a6-4a86-4688-a282-f46444705c64"
+
+ createOpts := pools.CreateMemberOpts{
+ Name: "db",
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ Address: "10.0.2.11",
+ ProtocolPort: 80,
+ Weight: 10,
+ }
+
+ member, err := pools.CreateMember(networkClient, poolID, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Member
+
+ poolID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ memberID := "64dba99f-8af8-4200-8882-e32a0660f23e"
+
+ updateOpts := pools.UpdateMemberOpts{
+ Name: "new-name",
+ Weight: 4,
+ }
+
+ member, err := pools.UpdateMember(networkClient, poolID, memberID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Member
+
+ poolID := "d67d56a6-4a86-4688-a282-f46444705c64"
+ memberID := "64dba99f-8af8-4200-8882-e32a0660f23e"
+
+ err := pools.DeleteMember(networkClient, poolID, memberID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+*/
+package pools
diff --git a/openstack/loadbalancer/v2/pools/requests.go b/openstack/loadbalancer/v2/pools/requests.go
new file mode 100644
index 0000000000..11564be83f
--- /dev/null
+++ b/openstack/loadbalancer/v2/pools/requests.go
@@ -0,0 +1,356 @@
+package pools
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToPoolListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the Pool attributes you want to see returned. SortKey allows you to
+// sort by a particular Pool attribute. SortDir sets the direction, and is
+// either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ LBMethod string `q:"lb_algorithm"`
+ Protocol string `q:"protocol"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Name string `q:"name"`
+ ID string `q:"id"`
+ LoadbalancerID string `q:"loadbalancer_id"`
+ ListenerID string `q:"listener_id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToPoolListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPoolListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// pools. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+//
+// Default policy settings return only those pools that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToPoolListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return PoolPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+type LBMethod string
+type Protocol string
+
+// Supported attributes for create/update operations.
+const (
+ LBMethodRoundRobin LBMethod = "ROUND_ROBIN"
+ LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS"
+ LBMethodSourceIp LBMethod = "SOURCE_IP"
+
+ ProtocolTCP Protocol = "TCP"
+ ProtocolHTTP Protocol = "HTTP"
+ ProtocolHTTPS Protocol = "HTTPS"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToPoolCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts is the common options struct used in this package's Create
+// operation.
+type CreateOpts struct {
+ // The algorithm used to distribute load between the members of the pool. The
+ // current specification supports LBMethodRoundRobin, LBMethodLeastConnections
+ // and LBMethodSourceIp as valid values for this attribute.
+ LBMethod LBMethod `json:"lb_algorithm" required:"true"`
+
+ // The protocol used by the pool members, you can use either
+ // ProtocolTCP, ProtocolHTTP, or ProtocolHTTPS.
+ Protocol Protocol `json:"protocol" required:"true"`
+
+ // The Loadbalancer on which the members of the pool will be associated with.
+ // Note: one of LoadbalancerID or ListenerID must be provided.
+ LoadbalancerID string `json:"loadbalancer_id,omitempty" xor:"ListenerID"`
+
+ // The Listener on which the members of the pool will be associated with.
+ // Note: one of LoadbalancerID or ListenerID must be provided.
+ ListenerID string `json:"listener_id,omitempty" xor:"LoadbalancerID"`
+
+ // TenantID is the UUID of the project who owns the Pool.
+ // Only administrative users can specify a project UUID other than their own.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // ProjectID is the UUID of the project who owns the Pool.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // Name of the pool.
+ Name string `json:"name,omitempty"`
+
+ // Human-readable description for the pool.
+ Description string `json:"description,omitempty"`
+
+ // Persistence is the session persistence of the pool.
+ // Omit this field to prevent session persistence.
+ Persistence *SessionPersistence `json:"session_persistence,omitempty"`
+
+ // The administrative state of the Pool. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToPoolCreateMap builds a request body from CreateOpts.
+func (opts CreateOpts) ToPoolCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "pool")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// load balancer pool.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToPoolCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Get retrieves a particular pool based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToPoolUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts is the common options struct used in this package's Update
+// operation.
+type UpdateOpts struct {
+ // Name of the pool.
+ Name string `json:"name,omitempty"`
+
+ // Human-readable description for the pool.
+ Description string `json:"description,omitempty"`
+
+ // The algorithm used to distribute load between the members of the pool. The
+ // current specification supports LBMethodRoundRobin, LBMethodLeastConnections
+ // and LBMethodSourceIp as valid values for this attribute.
+ LBMethod LBMethod `json:"lb_algorithm,omitempty"`
+
+ // The administrative state of the Pool. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToPoolUpdateMap builds a request body from UpdateOpts.
+func (opts UpdateOpts) ToPoolUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "pool")
+}
+
+// Update allows pools to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToPoolUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// Delete will permanently delete a particular pool based on its unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
+
+// ListMemberOptsBuilder allows extensions to add additional parameters to the
+// ListMembers request.
+type ListMembersOptsBuilder interface {
+ ToMembersListQuery() (string, error)
+}
+
+// ListMembersOpts allows the filtering and sorting of paginated collections
+// through the API. Filtering is achieved by passing in struct field values
+// that map to the Member attributes you want to see returned. SortKey allows
+// you to sort by a particular Member attribute. SortDir sets the direction,
+// and is either `asc' or `desc'. Marker and Limit are used for pagination.
+type ListMembersOpts struct {
+ Name string `q:"name"`
+ Weight int `q:"weight"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ TenantID string `q:"tenant_id"`
+ Address string `q:"address"`
+ ProtocolPort int `q:"protocol_port"`
+ ID string `q:"id"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToMemberListQuery formats a ListOpts into a query string.
+func (opts ListMembersOpts) ToMembersListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// ListMembers returns a Pager which allows you to iterate over a collection of
+// members. It accepts a ListMembersOptsBuilder, which allows you to filter and
+// sort the returned collection for greater efficiency.
+//
+// Default policy settings return only those members that are owned by the
+// tenant who submits the request, unless an admin user submits the request.
+func ListMembers(c *gophercloud.ServiceClient, poolID string, opts ListMembersOptsBuilder) pagination.Pager {
+ url := memberRootURL(c, poolID)
+ if opts != nil {
+ query, err := opts.ToMembersListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return MemberPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// CreateMemberOptsBuilder allows extensions to add additional parameters to the
+// CreateMember request.
+type CreateMemberOptsBuilder interface {
+ ToMemberCreateMap() (map[string]interface{}, error)
+}
+
+// CreateMemberOpts is the common options struct used in this package's CreateMember
+// operation.
+type CreateMemberOpts struct {
+ // The IP address of the member to receive traffic from the load balancer.
+ Address string `json:"address" required:"true"`
+
+ // The port on which to listen for client traffic.
+ ProtocolPort int `json:"protocol_port" required:"true"`
+
+ // Name of the Member.
+ Name string `json:"name,omitempty"`
+
+ // TenantID is the UUID of the project who owns the Member.
+ // Only administrative users can specify a project UUID other than their own.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // ProjectID is the UUID of the project who owns the Member.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // A positive integer value that indicates the relative portion of traffic
+ // that this member should receive from the pool. For example, a member with
+ // a weight of 10 receives five times as much traffic as a member with a
+ // weight of 2.
+ Weight int `json:"weight,omitempty"`
+
+ // If you omit this parameter, LBaaS uses the vip_subnet_id parameter value
+ // for the subnet UUID.
+ SubnetID string `json:"subnet_id,omitempty"`
+
+ // The administrative state of the Pool. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToMemberCreateMap builds a request body from CreateMemberOpts.
+func (opts CreateMemberOpts) ToMemberCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "member")
+}
+
+// CreateMember will create and associate a Member with a particular Pool.
+func CreateMember(c *gophercloud.ServiceClient, poolID string, opts CreateMemberOpts) (r CreateMemberResult) {
+ b, err := opts.ToMemberCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(memberRootURL(c, poolID), b, &r.Body, nil)
+ return
+}
+
+// GetMember retrieves a particular Pool Member based on its unique ID.
+func GetMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r GetMemberResult) {
+ _, r.Err = c.Get(memberResourceURL(c, poolID, memberID), &r.Body, nil)
+ return
+}
+
+// UpdateMemberOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type UpdateMemberOptsBuilder interface {
+ ToMemberUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateMemberOpts is the common options struct used in this package's Update
+// operation.
+type UpdateMemberOpts struct {
+ // Name of the Member.
+ Name string `json:"name,omitempty"`
+
+ // A positive integer value that indicates the relative portion of traffic
+ // that this member should receive from the pool. For example, a member with
+ // a weight of 10 receives five times as much traffic as a member with a
+ // weight of 2.
+ Weight int `json:"weight,omitempty"`
+
+ // The administrative state of the Pool. A valid value is true (UP)
+ // or false (DOWN).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToMemberUpdateMap builds a request body from UpdateMemberOpts.
+func (opts UpdateMemberOpts) ToMemberUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "member")
+}
+
+// Update allows Member to be updated.
+func UpdateMember(c *gophercloud.ServiceClient, poolID string, memberID string, opts UpdateMemberOptsBuilder) (r UpdateMemberResult) {
+ b, err := opts.ToMemberUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(memberResourceURL(c, poolID, memberID), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 201, 202},
+ })
+ return
+}
+
+// DisassociateMember will remove and disassociate a Member from a particular
+// Pool.
+func DeleteMember(c *gophercloud.ServiceClient, poolID string, memberID string) (r DeleteMemberResult) {
+ _, r.Err = c.Delete(memberResourceURL(c, poolID, memberID), nil)
+ return
+}
diff --git a/openstack/loadbalancer/v2/pools/results.go b/openstack/loadbalancer/v2/pools/results.go
new file mode 100644
index 0000000000..81d3ebf7d6
--- /dev/null
+++ b/openstack/loadbalancer/v2/pools/results.go
@@ -0,0 +1,273 @@
+package pools
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
+ "github.com/gophercloud/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 `json:"type"`
+
+ // Name of cookie if persistence mode is set appropriately.
+ CookieName string `json:"cookie_name,omitempty"`
+}
+
+// LoadBalancerID represents a load balancer.
+type LoadBalancerID struct {
+ ID string `json:"id"`
+}
+
+// ListenerID represents a listener.
+type ListenerID struct {
+ ID string `json:"id"`
+}
+
+// Pool represents a logical set of devices, such as web servers, that you
+// group together to receive and process traffic. The load balancing function
+// chooses a Member of the Pool according to the configured load balancing
+// method to handle the new requests or connections received on the VIP address.
+type Pool struct {
+ // The load-balancer algorithm, which is round-robin, least-connections, and
+ // so on. This value, which must be supported, is dependent on the provider.
+ // Round-robin must be supported.
+ LBMethod string `json:"lb_algorithm"`
+
+ // The protocol of the Pool, which is TCP, HTTP, or HTTPS.
+ Protocol string `json:"protocol"`
+
+ // Description for the Pool.
+ Description string `json:"description"`
+
+ // A list of listeners objects IDs.
+ Listeners []ListenerID `json:"listeners"` //[]map[string]interface{}
+
+ // A list of member objects IDs.
+ Members []Member `json:"members"`
+
+ // The ID of associated health monitor.
+ MonitorID string `json:"healthmonitor_id"`
+
+ // The network on which the members of the Pool will be located. Only members
+ // that are on this network can be added to the Pool.
+ SubnetID string `json:"subnet_id"`
+
+ // Owner of the Pool.
+ TenantID string `json:"tenant_id"`
+
+ // The administrative state of the Pool, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // Pool name. Does not have to be unique.
+ Name string `json:"name"`
+
+ // The unique ID for the Pool.
+ ID string `json:"id"`
+
+ // A list of load balancer objects IDs.
+ Loadbalancers []LoadBalancerID `json:"loadbalancers"`
+
+ // Indicates whether connections in the same session will be processed by the
+ // same Pool member or not.
+ Persistence SessionPersistence `json:"session_persistence"`
+
+ // The load balancer provider.
+ Provider string `json:"provider"`
+
+ // The Monitor associated with this Pool.
+ Monitor monitors.Monitor `json:"healthmonitor"`
+}
+
+// PoolPage is the page returned by a pager when traversing over a
+// collection of pools.
+type PoolPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of pools has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (r PoolPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"pools_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PoolPage struct is empty.
+func (r PoolPage) IsEmpty() (bool, error) {
+ is, err := ExtractPools(r)
+ return len(is) == 0, err
+}
+
+// ExtractPools accepts a Page struct, specifically a PoolPage struct,
+// and extracts the elements into a slice of Pool structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPools(r pagination.Page) ([]Pool, error) {
+ var s struct {
+ Pools []Pool `json:"pools"`
+ }
+ err := (r.(PoolPage)).ExtractInto(&s)
+ return s.Pools, err
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a pool.
+func (r commonResult) Extract() (*Pool, error) {
+ var s struct {
+ Pool *Pool `json:"pool"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Pool, err
+}
+
+// CreateResult represents the result of a Create operation. Call its Extract
+// method to interpret the result as a Pool.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a Get operation. Call its Extract
+// method to interpret the result as a Pool.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an Update operation. Call its Extract
+// method to interpret the result as a Pool.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a Delete operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// Member represents the application running on a backend server.
+type Member struct {
+ // Name of the Member.
+ Name string `json:"name"`
+
+ // Weight of Member.
+ Weight int `json:"weight"`
+
+ // The administrative state of the member, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // Owner of the Member.
+ TenantID string `json:"tenant_id"`
+
+ // Parameter value for the subnet UUID.
+ SubnetID string `json:"subnet_id"`
+
+ // The Pool to which the Member belongs.
+ PoolID string `json:"pool_id"`
+
+ // The IP address of the Member.
+ Address string `json:"address"`
+
+ // The port on which the application is hosted.
+ ProtocolPort int `json:"protocol_port"`
+
+ // The unique ID for the Member.
+ ID string `json:"id"`
+}
+
+// MemberPage is the page returned by a pager when traversing over a
+// collection of Members in a Pool.
+type MemberPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of members has reached
+// the end of a page and the pager seeks to traverse over a new one. In order
+// to do this, it needs to construct the next page's URL.
+func (r MemberPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"members_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a MemberPage struct is empty.
+func (r MemberPage) IsEmpty() (bool, error) {
+ is, err := ExtractMembers(r)
+ return len(is) == 0, err
+}
+
+// ExtractMembers accepts a Page struct, specifically a MemberPage struct,
+// and extracts the elements into a slice of Members structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractMembers(r pagination.Page) ([]Member, error) {
+ var s struct {
+ Members []Member `json:"members"`
+ }
+ err := (r.(MemberPage)).ExtractInto(&s)
+ return s.Members, err
+}
+
+type commonMemberResult struct {
+ gophercloud.Result
+}
+
+// ExtractMember is a function that accepts a result and extracts a member.
+func (r commonMemberResult) Extract() (*Member, error) {
+ var s struct {
+ Member *Member `json:"member"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Member, err
+}
+
+// CreateMemberResult represents the result of a CreateMember operation.
+// Call its Extract method to interpret it as a Member.
+type CreateMemberResult struct {
+ commonMemberResult
+}
+
+// GetMemberResult represents the result of a GetMember operation.
+// Call its Extract method to interpret it as a Member.
+type GetMemberResult struct {
+ commonMemberResult
+}
+
+// UpdateMemberResult represents the result of an UpdateMember operation.
+// Call its Extract method to interpret it as a Member.
+type UpdateMemberResult struct {
+ commonMemberResult
+}
+
+// DeleteMemberResult represents the result of a DeleteMember operation.
+// Call its ExtractErr method to determine if the request succeeded or failed.
+type DeleteMemberResult struct {
+ gophercloud.ErrResult
+}
diff --git a/openstack/loadbalancer/v2/pools/testing/doc.go b/openstack/loadbalancer/v2/pools/testing/doc.go
new file mode 100644
index 0000000000..46e335f3f2
--- /dev/null
+++ b/openstack/loadbalancer/v2/pools/testing/doc.go
@@ -0,0 +1,2 @@
+// pools unit tests
+package testing
diff --git a/openstack/loadbalancer/v2/pools/testing/fixtures.go b/openstack/loadbalancer/v2/pools/testing/fixtures.go
new file mode 100644
index 0000000000..fe0a85123b
--- /dev/null
+++ b/openstack/loadbalancer/v2/pools/testing/fixtures.go
@@ -0,0 +1,388 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// PoolsListBody contains the canned body of a pool list response.
+const PoolsListBody = `
+{
+ "pools":[
+ {
+ "lb_algorithm":"ROUND_ROBIN",
+ "protocol":"HTTP",
+ "description":"",
+ "healthmonitor_id": "466c8345-28d8-4f84-a246-e04380b0461d",
+ "members":[{"id": "53306cda-815d-4354-9fe4-59e09da9c3c5"}],
+ "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+ "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+ "id":"72741b06-df4d-4715-b142-276b6bce75ab",
+ "name":"web",
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "provider": "haproxy"
+ },
+ {
+ "lb_algorithm":"LEAST_CONNECTION",
+ "protocol":"HTTP",
+ "description":"",
+ "healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
+ "members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
+ "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+ "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+ "id":"c3741b06-df4d-4715-b142-276b6bce75ab",
+ "name":"db",
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "provider": "haproxy"
+ }
+ ]
+}
+`
+
+// SinglePoolBody is the canned body of a Get request on an existing pool.
+const SinglePoolBody = `
+{
+ "pool": {
+ "lb_algorithm":"LEAST_CONNECTION",
+ "protocol":"HTTP",
+ "description":"",
+ "healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
+ "members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
+ "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+ "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+ "id":"c3741b06-df4d-4715-b142-276b6bce75ab",
+ "name":"db",
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "provider": "haproxy"
+ }
+}
+`
+
+// PostUpdatePoolBody is the canned response body of a Update request on an existing pool.
+const PostUpdatePoolBody = `
+{
+ "pool": {
+ "lb_algorithm":"LEAST_CONNECTION",
+ "protocol":"HTTP",
+ "description":"",
+ "healthmonitor_id": "5f6c8345-28d8-4f84-a246-e04380b0461d",
+ "members":[{"id": "67306cda-815d-4354-9fe4-59e09da9c3c5"}],
+ "listeners":[{"id": "2a280670-c202-4b0b-a562-34077415aabf"}],
+ "loadbalancers":[{"id": "79e05663-7f03-45d2-a092-8b94062f22ab"}],
+ "id":"c3741b06-df4d-4715-b142-276b6bce75ab",
+ "name":"db",
+ "admin_state_up":true,
+ "tenant_id":"83657cfcdfe44cd5920adaf26c48ceea",
+ "provider": "haproxy"
+ }
+}
+`
+
+var (
+ PoolWeb = pools.Pool{
+ LBMethod: "ROUND_ROBIN",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorID: "466c8345-28d8-4f84-a246-e04380b0461d",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "web",
+ Members: []pools.Member{{ID: "53306cda-815d-4354-9fe4-59e09da9c3c5"}},
+ ID: "72741b06-df4d-4715-b142-276b6bce75ab",
+ Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Listeners: []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+ Provider: "haproxy",
+ }
+ PoolDb = pools.Pool{
+ LBMethod: "LEAST_CONNECTION",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "db",
+ Members: []pools.Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
+ ID: "c3741b06-df4d-4715-b142-276b6bce75ab",
+ Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Listeners: []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+ Provider: "haproxy",
+ }
+ PoolUpdated = pools.Pool{
+ LBMethod: "LEAST_CONNECTION",
+ Protocol: "HTTP",
+ Description: "",
+ MonitorID: "5f6c8345-28d8-4f84-a246-e04380b0461d",
+ TenantID: "83657cfcdfe44cd5920adaf26c48ceea",
+ AdminStateUp: true,
+ Name: "db",
+ Members: []pools.Member{{ID: "67306cda-815d-4354-9fe4-59e09da9c3c5"}},
+ ID: "c3741b06-df4d-4715-b142-276b6bce75ab",
+ Loadbalancers: []pools.LoadBalancerID{{ID: "79e05663-7f03-45d2-a092-8b94062f22ab"}},
+ Listeners: []pools.ListenerID{{ID: "2a280670-c202-4b0b-a562-34077415aabf"}},
+ Provider: "haproxy",
+ }
+)
+
+// HandlePoolListSuccessfully sets up the test server to respond to a pool List request.
+func HandlePoolListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, PoolsListBody)
+ case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+ fmt.Fprintf(w, `{ "pools": [] }`)
+ default:
+ t.Fatalf("/v2.0/lbaas/pools invoked with unexpected marker=[%s]", marker)
+ }
+ })
+}
+
+// HandlePoolCreationSuccessfully sets up the test server to respond to a pool creation request
+// with a given response.
+func HandlePoolCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "pool": {
+ "lb_algorithm": "ROUND_ROBIN",
+ "protocol": "HTTP",
+ "name": "Example pool",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "loadbalancer_id": "79e05663-7f03-45d2-a092-8b94062f22ab"
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
+
+// HandlePoolGetSuccessfully sets up the test server to respond to a pool Get request.
+func HandlePoolGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, SinglePoolBody)
+ })
+}
+
+// HandlePoolDeletionSuccessfully sets up the test server to respond to a pool deletion request.
+func HandlePoolDeletionSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandlePoolUpdateSuccessfully sets up the test server to respond to a pool Update request.
+func HandlePoolUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/c3741b06-df4d-4715-b142-276b6bce75ab", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `{
+ "pool": {
+ "name": "NewPoolName",
+ "lb_algorithm": "LEAST_CONNECTIONS"
+ }
+ }`)
+
+ fmt.Fprintf(w, PostUpdatePoolBody)
+ })
+}
+
+// MembersListBody contains the canned body of a member list response.
+const MembersListBody = `
+{
+ "members":[
+ {
+ "id": "2a280670-c202-4b0b-a562-34077415aabf",
+ "address": "10.0.2.10",
+ "weight": 5,
+ "name": "web",
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "admin_state_up":true,
+ "protocol_port": 80
+ },
+ {
+ "id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ "address": "10.0.2.11",
+ "weight": 10,
+ "name": "db",
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "admin_state_up":false,
+ "protocol_port": 80
+ }
+ ]
+}
+`
+
+// SingleMemberBody is the canned body of a Get request on an existing member.
+const SingleMemberBody = `
+{
+ "member": {
+ "id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ "address": "10.0.2.11",
+ "weight": 10,
+ "name": "db",
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "admin_state_up":false,
+ "protocol_port": 80
+ }
+}
+`
+
+// PostUpdateMemberBody is the canned response body of a Update request on an existing member.
+const PostUpdateMemberBody = `
+{
+ "member": {
+ "id": "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ "address": "10.0.2.11",
+ "weight": 10,
+ "name": "db",
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "admin_state_up":false,
+ "protocol_port": 80
+ }
+}
+`
+
+var (
+ MemberWeb = pools.Member{
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ AdminStateUp: true,
+ Name: "web",
+ ID: "2a280670-c202-4b0b-a562-34077415aabf",
+ Address: "10.0.2.10",
+ Weight: 5,
+ ProtocolPort: 80,
+ }
+ MemberDb = pools.Member{
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ AdminStateUp: false,
+ Name: "db",
+ ID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ Address: "10.0.2.11",
+ Weight: 10,
+ ProtocolPort: 80,
+ }
+ MemberUpdated = pools.Member{
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ AdminStateUp: false,
+ Name: "db",
+ ID: "fad389a3-9a4a-4762-a365-8c7038508b5d",
+ Address: "10.0.2.11",
+ Weight: 10,
+ ProtocolPort: 80,
+ }
+)
+
+// HandleMemberListSuccessfully sets up the test server to respond to a member List request.
+func HandleMemberListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, MembersListBody)
+ case "45e08a3e-a78f-4b40-a229-1e7e23eee1ab":
+ fmt.Fprintf(w, `{ "members": [] }`)
+ default:
+ t.Fatalf("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members invoked with unexpected marker=[%s]", marker)
+ }
+ })
+}
+
+// HandleMemberCreationSuccessfully sets up the test server to respond to a member creation request
+// with a given response.
+func HandleMemberCreationSuccessfully(t *testing.T, response string) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "POST")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestJSONRequest(t, r, `{
+ "member": {
+ "address": "10.0.2.11",
+ "weight": 10,
+ "name": "db",
+ "subnet_id": "1981f108-3c48-48d2-b908-30f7d28532c9",
+ "tenant_id": "2ffc6e22aae24e4795f87155d24c896f",
+ "protocol_port": 80
+ }
+ }`)
+
+ w.WriteHeader(http.StatusAccepted)
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, response)
+ })
+}
+
+// HandleMemberGetSuccessfully sets up the test server to respond to a member Get request.
+func HandleMemberGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "GET")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+
+ fmt.Fprintf(w, SingleMemberBody)
+ })
+}
+
+// HandleMemberDeletionSuccessfully sets up the test server to respond to a member deletion request.
+func HandleMemberDeletionSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "DELETE")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleMemberUpdateSuccessfully sets up the test server to respond to a member Update request.
+func HandleMemberUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2.0/lbaas/pools/332abe93-f488-41ba-870b-2ac66be7f853/members/2a280670-c202-4b0b-a562-34077415aabf", func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestJSONRequest(t, r, `{
+ "member": {
+ "name": "newMemberName",
+ "weight": 4
+ }
+ }`)
+
+ fmt.Fprintf(w, PostUpdateMemberBody)
+ })
+}
diff --git a/openstack/loadbalancer/v2/pools/testing/requests_test.go b/openstack/loadbalancer/v2/pools/testing/requests_test.go
new file mode 100644
index 0000000000..9eaec03e01
--- /dev/null
+++ b/openstack/loadbalancer/v2/pools/testing/requests_test.go
@@ -0,0 +1,262 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools"
+ fake "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/testhelper"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestListPools(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePoolListSuccessfully(t)
+
+ pages := 0
+ err := pools.List(fake.ServiceClient(), pools.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := pools.ExtractPools(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 pools, got %d", len(actual))
+ }
+ th.CheckDeepEquals(t, PoolWeb, actual[0])
+ th.CheckDeepEquals(t, PoolDb, actual[1])
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestListAllPools(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePoolListSuccessfully(t)
+
+ allPages, err := pools.List(fake.ServiceClient(), pools.ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := pools.ExtractPools(allPages)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, PoolWeb, actual[0])
+ th.CheckDeepEquals(t, PoolDb, actual[1])
+}
+
+func TestCreatePool(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePoolCreationSuccessfully(t, SinglePoolBody)
+
+ actual, err := pools.Create(fake.ServiceClient(), pools.CreateOpts{
+ LBMethod: pools.LBMethodRoundRobin,
+ Protocol: "HTTP",
+ Name: "Example pool",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab",
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, PoolDb, *actual)
+}
+
+func TestGetPool(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePoolGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := pools.Get(client, "c3741b06-df4d-4715-b142-276b6bce75ab").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, PoolDb, *actual)
+}
+
+func TestDeletePool(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePoolDeletionSuccessfully(t)
+
+ res := pools.Delete(fake.ServiceClient(), "c3741b06-df4d-4715-b142-276b6bce75ab")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdatePool(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandlePoolUpdateSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := pools.Update(client, "c3741b06-df4d-4715-b142-276b6bce75ab", pools.UpdateOpts{
+ Name: "NewPoolName",
+ LBMethod: pools.LBMethodLeastConnections,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, PoolUpdated, *actual)
+}
+
+func TestRequiredPoolCreateOpts(t *testing.T) {
+ res := pools.Create(fake.ServiceClient(), pools.CreateOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = pools.Create(fake.ServiceClient(), pools.CreateOpts{
+ LBMethod: pools.LBMethod("invalid"),
+ Protocol: pools.ProtocolHTTPS,
+ LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a",
+ })
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+
+ res = pools.Create(fake.ServiceClient(), pools.CreateOpts{
+ LBMethod: pools.LBMethodRoundRobin,
+ Protocol: pools.Protocol("invalid"),
+ LoadbalancerID: "69055154-f603-4a28-8951-7cc2d9e54a9a",
+ })
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+
+ res = pools.Create(fake.ServiceClient(), pools.CreateOpts{
+ LBMethod: pools.LBMethodRoundRobin,
+ Protocol: pools.ProtocolHTTPS,
+ })
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+}
+
+func TestListMembers(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberListSuccessfully(t)
+
+ pages := 0
+ err := pools.ListMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.ListMembersOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ pages++
+
+ actual, err := pools.ExtractMembers(page)
+ if err != nil {
+ return false, err
+ }
+
+ if len(actual) != 2 {
+ t.Fatalf("Expected 2 members, got %d", len(actual))
+ }
+ th.CheckDeepEquals(t, MemberWeb, actual[0])
+ th.CheckDeepEquals(t, MemberDb, actual[1])
+
+ return true, nil
+ })
+
+ th.AssertNoErr(t, err)
+
+ if pages != 1 {
+ t.Errorf("Expected 1 page, saw %d", pages)
+ }
+}
+
+func TestListAllMembers(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberListSuccessfully(t)
+
+ allPages, err := pools.ListMembers(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.ListMembersOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+ actual, err := pools.ExtractMembers(allPages)
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, MemberWeb, actual[0])
+ th.CheckDeepEquals(t, MemberDb, actual[1])
+}
+
+func TestCreateMember(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberCreationSuccessfully(t, SingleMemberBody)
+
+ actual, err := pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{
+ Name: "db",
+ SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9",
+ TenantID: "2ffc6e22aae24e4795f87155d24c896f",
+ Address: "10.0.2.11",
+ ProtocolPort: 80,
+ Weight: 10,
+ }).Extract()
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, MemberDb, *actual)
+}
+
+func TestRequiredMemberCreateOpts(t *testing.T) {
+ res := pools.CreateMember(fake.ServiceClient(), "", pools.CreateMemberOpts{})
+ if res.Err == nil {
+ t.Fatalf("Expected error, got none")
+ }
+ res = pools.CreateMember(fake.ServiceClient(), "", pools.CreateMemberOpts{Address: "1.2.3.4", ProtocolPort: 80})
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+ res = pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{ProtocolPort: 80})
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+ res = pools.CreateMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", pools.CreateMemberOpts{Address: "1.2.3.4"})
+ if res.Err == nil {
+ t.Fatalf("Expected error, but got none")
+ }
+}
+
+func TestGetMember(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberGetSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := pools.GetMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf").Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Get error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, MemberDb, *actual)
+}
+
+func TestDeleteMember(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberDeletionSuccessfully(t)
+
+ res := pools.DeleteMember(fake.ServiceClient(), "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdateMember(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleMemberUpdateSuccessfully(t)
+
+ client := fake.ServiceClient()
+ actual, err := pools.UpdateMember(client, "332abe93-f488-41ba-870b-2ac66be7f853", "2a280670-c202-4b0b-a562-34077415aabf", pools.UpdateMemberOpts{
+ Name: "newMemberName",
+ Weight: 4,
+ }).Extract()
+ if err != nil {
+ t.Fatalf("Unexpected Update error: %v", err)
+ }
+
+ th.CheckDeepEquals(t, MemberUpdated, *actual)
+}
diff --git a/openstack/loadbalancer/v2/pools/urls.go b/openstack/loadbalancer/v2/pools/urls.go
new file mode 100644
index 0000000000..bceca67707
--- /dev/null
+++ b/openstack/loadbalancer/v2/pools/urls.go
@@ -0,0 +1,25 @@
+package pools
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "lbaas"
+ resourcePath = "pools"
+ memberPath = "members"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
+
+func memberRootURL(c *gophercloud.ServiceClient, poolId string) string {
+ return c.ServiceURL(rootPath, resourcePath, poolId, memberPath)
+}
+
+func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memeberID string) string {
+ return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memeberID)
+}
diff --git a/openstack/loadbalancer/v2/testhelper/client.go b/openstack/loadbalancer/v2/testhelper/client.go
new file mode 100644
index 0000000000..7e1d917280
--- /dev/null
+++ b/openstack/loadbalancer/v2/testhelper/client.go
@@ -0,0 +1,14 @@
+package common
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+const TokenID = client.TokenID
+
+func ServiceClient() *gophercloud.ServiceClient {
+ sc := client.ServiceClient()
+ sc.ResourceBase = sc.Endpoint + "v2.0/"
+ return sc
+}
diff --git a/openstack/messaging/v2/queues/doc.go b/openstack/messaging/v2/queues/doc.go
new file mode 100644
index 0000000000..c24e8d94d9
--- /dev/null
+++ b/openstack/messaging/v2/queues/doc.go
@@ -0,0 +1,79 @@
+/*
+Package queues provides information and interaction with the queues through
+the OpenStack Messaging (Zaqar) service.
+
+Lists all queues and creates, shows information for updates, deletes, and actions on a queue.
+
+Example to List Queues
+
+ listOpts := queues.ListOpts{
+ Limit: 10,
+ }
+
+ pager := queues.List(client, listOpts)
+
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ queues, err := queues.ExtractQueues(page)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, queue := range queues {
+ fmt.Printf("%+v\n", queue)
+ }
+
+ return true, nil
+ })
+
+Example to Create a Queue
+
+ createOpts := queues.CreateOpts{
+ QueueName: "My_Queue",
+ MaxMessagesPostSize: 262143,
+ DefaultMessageTTL: 3700,
+ DefaultMessageDelay: 25,
+ DeadLetterQueueMessageTTL: 3500,
+ MaxClaimCount: 10,
+ Extra: map[string]interface{}{"description": "Test queue."},
+ }
+
+ err := queues.Create(client, createOpts).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Queue
+
+ updateOpts := queues.BatchUpdateOpts{
+ queues.UpdateOpts{
+ Op: "replace",
+ Path: "/metadata/_max_claim_count",
+ Value: 15,
+ },
+ queues.UpdateOpts{
+ Op: "replace",
+ Path: "/metadata/description",
+ Value: "Updated description test queue.",
+ },
+ }
+
+ updateResult, err := queues.Update(client, queueName, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Get a Queue
+
+ queue, err := queues.Get(client, queueName).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Queue
+
+ err := queues.Delete(client, queueName).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+*/
+package queues
diff --git a/openstack/messaging/v2/queues/requests.go b/openstack/messaging/v2/queues/requests.go
new file mode 100644
index 0000000000..327f6f887e
--- /dev/null
+++ b/openstack/messaging/v2/queues/requests.go
@@ -0,0 +1,191 @@
+package queues
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToQueueListQuery() (string, error)
+}
+
+// ListOpts params to be used with List
+type ListOpts struct {
+ // Limit instructs List to refrain from sending excessively large lists of queues
+ Limit int `q:"limit,omitempty"`
+
+ // Marker and Limit control paging. Marker instructs List where to start listing from.
+ Marker string `q:"marker,omitempty"`
+
+ // Specifies if showing the detailed information when querying queues
+ Detailed bool `q:"detailed,omitempty"`
+}
+
+// ToQueueListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToQueueListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List instructs OpenStack to provide a list of queues.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(client)
+ if opts != nil {
+ query, err := opts.ToQueueListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+
+ pager := pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+ return QueuePage{pagination.LinkedPageBase{PageResult: r}}
+
+ })
+ return pager
+}
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToQueueCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies the queue creation parameters.
+type CreateOpts struct {
+ // The name of the queue to create.
+ QueueName string `json:"queue_name" required:"true"`
+
+ // The target incoming messages will be moved to when a message can’t
+ // processed successfully after meet the max claim count is met.
+ DeadLetterQueue string `json:"_dead_letter_queue,omitempty"`
+
+ // The new TTL setting for messages when moved to dead letter queue.
+ DeadLetterQueueMessagesTTL int `json:"_dead_letter_queue_messages_ttl,omitempty"`
+
+ // The delay of messages defined for a queue. When the messages send to
+ // the queue, it will be delayed for some times and means it can not be
+ // claimed until the delay expired.
+ DefaultMessageDelay int `json:"_default_message_delay,omitempty"`
+
+ // The default TTL of messages defined for a queue, which will effect for
+ // any messages posted to the queue.
+ DefaultMessageTTL int `json:"_default_message_ttl" required:"true"`
+
+ // The flavor name which can tell Zaqar which storage pool will be used
+ // to create the queue.
+ Flavor string `json:"_flavor,omitempty"`
+
+ // The max number the message can be claimed.
+ MaxClaimCount int `json:"_max_claim_count,omitempty"`
+
+ // The max post size of messages defined for a queue, which will effect
+ // for any messages posted to the queue.
+ MaxMessagesPostSize int `json:"_max_messages_post_size,omitempty"`
+
+ // Extra is free-form extra key/value pairs to describe the queue.
+ Extra map[string]interface{} `json:"-"`
+}
+
+// ToQueueCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToQueueCreateMap() (map[string]interface{}, error) {
+ b, err := gophercloud.BuildRequestBody(opts, "")
+ if err != nil {
+ return nil, err
+ }
+
+ if opts.Extra != nil {
+ for key, value := range opts.Extra {
+ b[key] = value
+ }
+
+ }
+ return b, nil
+}
+
+// Create requests the creation of a new queue.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToQueueCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+
+ queueName := b["queue_name"].(string)
+ delete(b, "queue_name")
+
+ _, r.Err = client.Put(createURL(client, queueName), b, r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{201, 204},
+ })
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// update request.
+type UpdateOptsBuilder interface {
+ ToQueueUpdateMap() ([]map[string]interface{}, error)
+}
+
+// UpdateOpts is an array of UpdateQueueBody.
+type BatchUpdateOpts []UpdateOpts
+
+// UpdateOpts is the struct responsible for updating a property of a queue.
+type UpdateOpts struct {
+ Op UpdateOp `json:"op" required:"true"`
+ Path string `json:"path" required:"true"`
+ Value interface{} `json:"value" required:"true"`
+}
+
+type UpdateOp string
+
+const (
+ ReplaceOp UpdateOp = "replace"
+ AddOp UpdateOp = "add"
+ RemoveOp UpdateOp = "remove"
+)
+
+// ToQueueUpdateMap constructs a request body from UpdateOpts.
+func (opts BatchUpdateOpts) ToQueueUpdateMap() ([]map[string]interface{}, error) {
+ queuesUpdates := make([]map[string]interface{}, len(opts))
+ for i, queue := range opts {
+ queueMap, err := queue.ToMap()
+ if err != nil {
+ return nil, err
+ }
+ queuesUpdates[i] = queueMap
+ }
+ return queuesUpdates, nil
+}
+
+// ToMap constructs a request body from UpdateOpts.
+func (opts UpdateOpts) ToMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "")
+}
+
+// Update Updates the specified queue.
+func Update(client *gophercloud.ServiceClient, queueName string, opts UpdateOptsBuilder) (r UpdateResult) {
+ _, r.Err = client.Patch(updateURL(client, queueName), opts, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 201, 204},
+ MoreHeaders: map[string]string{
+ "Content-Type": "application/openstack-messaging-v2.0-json-patch"},
+ })
+ return
+}
+
+// Get requests details on a single queue, by name.
+func Get(client *gophercloud.ServiceClient, queueName string) (r GetResult) {
+ _, r.Err = client.Get(getURL(client, queueName), &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// Delete deletes the specified queue.
+func Delete(client *gophercloud.ServiceClient, queueName string) (r DeleteResult) {
+ _, r.Err = client.Delete(deleteURL(client, queueName), &gophercloud.RequestOpts{
+ OkCodes: []int{204},
+ })
+ return
+}
diff --git a/openstack/messaging/v2/queues/results.go b/openstack/messaging/v2/queues/results.go
new file mode 100644
index 0000000000..6800ec7bce
--- /dev/null
+++ b/openstack/messaging/v2/queues/results.go
@@ -0,0 +1,149 @@
+package queues
+
+import (
+ "encoding/json"
+
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/internal"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// commonResult is the response of a base result.
+type commonResult struct {
+ gophercloud.Result
+}
+
+// QueuePage contains a single page of all queues from a List operation.
+type QueuePage struct {
+ pagination.LinkedPageBase
+}
+
+// CreateResult is the response of a Create operation.
+type CreateResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult is the response of a Update operation.
+type UpdateResult struct {
+ commonResult
+}
+
+// GetResult is the response of a Get operation.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult is the result from a Delete operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// Queue represents a messaging queue.
+type Queue struct {
+ Href string `json:"href"`
+ Methods []string `json:"methods"`
+ Name string `json:"name"`
+ Paths []string `json:"paths"`
+ ResourceTypes []string `json:"resource_types"`
+ Metadata QueueDetails `json:"metadata"`
+}
+
+// QueueDetails represents the metadata of a queue.
+type QueueDetails struct {
+ // The queue the message will be moved to when the message can’t
+ // be processed successfully after the max claim count is met.
+ DeadLetterQueue string `json:"_dead_letter_queue"`
+
+ // The TTL setting for messages when moved to dead letter queue.
+ DeadLetterQueueMessageTTL int `json:"_dead_letter_queue_messages_ttl"`
+
+ // The delay of messages defined for the queue.
+ DefaultMessageDelay int `json:"_default_message_delay"`
+
+ // The default TTL of messages defined for the queue.
+ DefaultMessageTTL int `json:"_default_message_ttl"`
+
+ // Extra is a collection of miscellaneous key/values.
+ Extra map[string]interface{} `json:"-"`
+
+ // The max number the message can be claimed from the queue.
+ MaxClaimCount int `json:"_max_claim_count"`
+
+ // The max post size of messages defined for the queue.
+ MaxMessagesPostSize int `json:"_max_messages_post_size"`
+
+ // The flavor defined for the queue.
+ Flavor string `json:"flavor"`
+}
+
+// Extract interprets any commonResult as a Queue.
+func (r commonResult) Extract() (QueueDetails, error) {
+ var s QueueDetails
+ err := r.ExtractInto(&s)
+ return s, err
+}
+
+// ExtractQueues interprets the results of a single page from a
+// List() call, producing a map of queues.
+func ExtractQueues(r pagination.Page) ([]Queue, error) {
+ var s struct {
+ Queues []Queue `json:"queues"`
+ }
+ err := (r.(QueuePage)).ExtractInto(&s)
+ return s.Queues, err
+}
+
+// IsEmpty determines if a QueuesPage contains any results.
+func (r QueuePage) IsEmpty() (bool, error) {
+ s, err := ExtractQueues(r)
+ return len(s) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the
+// next page of results.
+func (r QueuePage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+
+ next, err := gophercloud.ExtractNextURL(s.Links)
+ if err != nil {
+ return "", err
+ }
+ return nextPageURL(r.URL.String(), next)
+}
+
+func (r *QueueDetails) UnmarshalJSON(b []byte) error {
+ type tmp QueueDetails
+ var s struct {
+ tmp
+ Extra map[string]interface{} `json:"extra"`
+ }
+ err := json.Unmarshal(b, &s)
+ if err != nil {
+ return err
+ }
+ *r = QueueDetails(s.tmp)
+
+ // Collect other fields and bundle them into Extra
+ // but only if a field titled "extra" wasn't sent.
+ if s.Extra != nil {
+ r.Extra = s.Extra
+ } else {
+ var result interface{}
+ err := json.Unmarshal(b, &result)
+ if err != nil {
+ return err
+ }
+ if resultMap, ok := result.(map[string]interface{}); ok {
+ r.Extra = internal.RemainingKeys(QueueDetails{}, resultMap)
+ }
+ }
+
+ return err
+}
diff --git a/openstack/messaging/v2/queues/testing/doc.go b/openstack/messaging/v2/queues/testing/doc.go
new file mode 100644
index 0000000000..0937008836
--- /dev/null
+++ b/openstack/messaging/v2/queues/testing/doc.go
@@ -0,0 +1,2 @@
+// queues unit tests
+package testing
diff --git a/openstack/messaging/v2/queues/testing/fixtures.go b/openstack/messaging/v2/queues/testing/fixtures.go
new file mode 100644
index 0000000000..eb05dae826
--- /dev/null
+++ b/openstack/messaging/v2/queues/testing/fixtures.go
@@ -0,0 +1,210 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/messaging/v2/queues"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+// QueueName is the name of the queue
+var QueueName = "FakeTestQueue"
+
+// CreateQueueRequest is a sample request to create a queue.
+const CreateQueueRequest = `
+{
+ "_max_messages_post_size": 262144,
+ "_default_message_ttl": 3600,
+ "_default_message_delay": 30,
+ "_dead_letter_queue": "dead_letter",
+ "_dead_letter_queue_messages_ttl": 3600,
+ "_max_claim_count": 10,
+ "description": "Queue for unit testing."
+}`
+
+// ListQueuesResponse1 is a sample response to a List queues.
+const ListQueuesResponse1 = `
+{
+ "queues":[
+ {
+ "href":"/v2/queues/london",
+ "name":"london",
+ "metadata":{
+ "_dead_letter_queue":"fake_queue",
+ "_dead_letter_queue_messages_ttl":3500,
+ "_default_message_delay":25,
+ "_default_message_ttl":3700,
+ "_max_claim_count":10,
+ "_max_messages_post_size":262143,
+ "description":"Test queue."
+ }
+ }
+ ],
+ "links":[
+ {
+ "href":"/v2/queues?marker=london",
+ "rel":"next"
+ }
+ ]
+}`
+
+// ListQueuesResponse2 is a sample response to a List queues.
+const ListQueuesResponse2 = `
+{
+ "queues":[
+ {
+ "href":"/v2/queues/beijing",
+ "name":"beijing",
+ "metadata":{
+ "_dead_letter_queue":"fake_queue",
+ "_dead_letter_queue_messages_ttl":3500,
+ "_default_message_delay":25,
+ "_default_message_ttl":3700,
+ "_max_claim_count":10,
+ "_max_messages_post_size":262143,
+ "description":"Test queue."
+ }
+ }
+ ],
+ "links":[
+ {
+ "href":"/v2/queues?marker=beijing",
+ "rel":"next"
+ }
+ ]
+}`
+
+// UpdateQueueRequest is a sample request to update a queue.
+const UpdateQueueRequest = `
+[
+ {
+ "op": "replace",
+ "path": "/metadata/description",
+ "value": "Update queue description"
+ }
+]`
+
+// UpdateQueueResponse is a sample response to a update queue.
+const UpdateQueueResponse = `
+{
+ "description": "Update queue description"
+}`
+
+// GetQueueResponse is a sample response to a get queue.
+const GetQueueResponse = `
+{
+ "_max_messages_post_size": 262144,
+ "_default_message_ttl": 3600,
+ "description": "Queue used for unit testing."
+}`
+
+// FirstQueue is the first result in a List.
+var FirstQueue = queues.Queue{
+ Href: "/v2/queues/london",
+ Name: "london",
+ Metadata: queues.QueueDetails{
+ DeadLetterQueue: "fake_queue",
+ DeadLetterQueueMessageTTL: 3500,
+ DefaultMessageDelay: 25,
+ DefaultMessageTTL: 3700,
+ MaxClaimCount: 10,
+ MaxMessagesPostSize: 262143,
+ Extra: map[string]interface{}{"description": "Test queue."},
+ },
+}
+
+// SecondQueue is the second result in a List.
+var SecondQueue = queues.Queue{
+ Href: "/v2/queues/beijing",
+ Name: "beijing",
+ Metadata: queues.QueueDetails{
+ DeadLetterQueue: "fake_queue",
+ DeadLetterQueueMessageTTL: 3500,
+ DefaultMessageDelay: 25,
+ DefaultMessageTTL: 3700,
+ MaxClaimCount: 10,
+ MaxMessagesPostSize: 262143,
+ Extra: map[string]interface{}{"description": "Test queue."},
+ },
+}
+
+// ExpectedQueueSlice is the expected result in a List.
+var ExpectedQueueSlice = [][]queues.Queue{{FirstQueue}, {SecondQueue}}
+
+// QueueDetails is the expected result in a Get.
+var QueueDetails = queues.QueueDetails{
+ DefaultMessageTTL: 3600,
+ MaxMessagesPostSize: 262144,
+ Extra: map[string]interface{}{"description": "Queue used for unit testing."},
+}
+
+// HandleListSuccessfully configures the test server to respond to a List request.
+func HandleListSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc("/v2/queues",
+ 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")
+ next := r.RequestURI
+
+ switch next {
+ case "/v2/queues?limit=1":
+ fmt.Fprintf(w, ListQueuesResponse1)
+ case "/v2/queues?marker=london":
+ fmt.Fprint(w, ListQueuesResponse2)
+ case "/v2/queues?marker=beijing":
+ fmt.Fprint(w, `{ "queues": [] }`)
+ }
+ })
+}
+
+// HandleCreateSuccessfully configures the test server to respond to a Create request.
+func HandleCreateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc(fmt.Sprintf("/v2/queues/%s", QueueName),
+ func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PUT")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, CreateQueueRequest)
+
+ w.WriteHeader(http.StatusNoContent)
+ })
+}
+
+// HandleUpdateSuccessfully configures the test server to respond to an Update request.
+func HandleUpdateSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc(fmt.Sprintf("/v2/queues/%s", QueueName),
+ func(w http.ResponseWriter, r *http.Request) {
+ th.TestMethod(t, r, "PATCH")
+ th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+ th.TestJSONRequest(t, r, UpdateQueueRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ fmt.Fprintf(w, UpdateQueueResponse)
+ })
+}
+
+// HandleGetSuccessfully configures the test server to respond to a Get request.
+func HandleGetSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc(fmt.Sprintf("/v2/queues/%s", QueueName),
+ 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")
+ fmt.Fprintf(w, GetQueueResponse)
+ })
+}
+
+// HandleDeleteSuccessfully configures the test server to respond to a Delete request.
+func HandleDeleteSuccessfully(t *testing.T) {
+ th.Mux.HandleFunc(fmt.Sprintf("/v2/queues/%s", QueueName),
+ 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)
+ })
+}
diff --git a/openstack/messaging/v2/queues/testing/requests_test.go b/openstack/messaging/v2/queues/testing/requests_test.go
new file mode 100644
index 0000000000..5b8252e43f
--- /dev/null
+++ b/openstack/messaging/v2/queues/testing/requests_test.go
@@ -0,0 +1,94 @@
+package testing
+
+import (
+ "testing"
+
+ "github.com/gophercloud/gophercloud/openstack/messaging/v2/queues"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+ fake "github.com/gophercloud/gophercloud/testhelper/client"
+)
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleListSuccessfully(t)
+
+ listOpts := queues.ListOpts{
+ Limit: 1,
+ }
+
+ count := 0
+ err := queues.List(fake.ServiceClient(), listOpts).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := queues.ExtractQueues(page)
+ th.AssertNoErr(t, err)
+
+ th.CheckDeepEquals(t, ExpectedQueueSlice[count], actual)
+ count++
+
+ return true, nil
+ })
+ th.AssertNoErr(t, err)
+
+ th.CheckEquals(t, 2, count)
+}
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleCreateSuccessfully(t)
+
+ createOpts := queues.CreateOpts{
+ QueueName: QueueName,
+ MaxMessagesPostSize: 262144,
+ DefaultMessageTTL: 3600,
+ DefaultMessageDelay: 30,
+ DeadLetterQueue: "dead_letter",
+ DeadLetterQueueMessagesTTL: 3600,
+ MaxClaimCount: 10,
+ Extra: map[string]interface{}{"description": "Queue for unit testing."},
+ }
+
+ err := queues.Create(fake.ServiceClient(), createOpts).ExtractErr()
+ th.AssertNoErr(t, err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleUpdateSuccessfully(t)
+
+ updateOpts := queues.BatchUpdateOpts{
+ queues.UpdateOpts{
+ Op: queues.ReplaceOp,
+ Path: "/metadata/description",
+ Value: "Update queue description",
+ },
+ }
+ updatedQueueResult := queues.QueueDetails{
+ Extra: map[string]interface{}{"description": "Update queue description"},
+ }
+
+ actual, err := queues.Update(fake.ServiceClient(), QueueName, updateOpts).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, updatedQueueResult, actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleGetSuccessfully(t)
+
+ actual, err := queues.Get(fake.ServiceClient(), QueueName).Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, QueueDetails, actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleDeleteSuccessfully(t)
+
+ err := queues.Delete(fake.ServiceClient(), QueueName).ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/messaging/v2/queues/urls.go b/openstack/messaging/v2/queues/urls.go
new file mode 100644
index 0000000000..efea4528b2
--- /dev/null
+++ b/openstack/messaging/v2/queues/urls.go
@@ -0,0 +1,47 @@
+package queues
+
+import (
+ "net/url"
+
+ "github.com/gophercloud/gophercloud"
+)
+
+const ApiVersion = "v2"
+const ApiName = "queues"
+
+func commonURL(client *gophercloud.ServiceClient) string {
+ return client.ServiceURL(ApiVersion, ApiName)
+}
+
+func createURL(client *gophercloud.ServiceClient, queueName string) string {
+ return client.ServiceURL(ApiVersion, ApiName, queueName)
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+ return commonURL(client)
+}
+
+// builds next page full url based on current url
+func nextPageURL(currentURL string, next string) (string, error) {
+ base, err := url.Parse(currentURL)
+ if err != nil {
+ return "", err
+ }
+ rel, err := url.Parse(next)
+ if err != nil {
+ return "", err
+ }
+ return base.ResolveReference(rel).String(), nil
+}
+
+func updateURL(client *gophercloud.ServiceClient, queueName string) string {
+ return client.ServiceURL(ApiVersion, ApiName, queueName)
+}
+
+func getURL(client *gophercloud.ServiceClient, queueName string) string {
+ return client.ServiceURL(ApiVersion, ApiName, queueName)
+}
+
+func deleteURL(client *gophercloud.ServiceClient, queueName string) string {
+ return client.ServiceURL(ApiVersion, ApiName, queueName)
+}
diff --git a/openstack/networking/v2/extensions/extradhcpopts/doc.go b/openstack/networking/v2/extensions/extradhcpopts/doc.go
new file mode 100644
index 0000000000..ec5d6181d6
--- /dev/null
+++ b/openstack/networking/v2/extensions/extradhcpopts/doc.go
@@ -0,0 +1,80 @@
+/*
+Package extradhcpopts allow to work with extra DHCP functionality of Neutron ports.
+
+Example to Get a Port with Extra DHCP Options
+
+ portID := "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2"
+ var s struct {
+ ports.Port
+ extradhcpopts.ExtraDHCPOptsExt
+ }
+
+ err := ports.Get(networkClient, portID).ExtractInto(&s)
+ if err != nil {
+ panic(err)
+ }
+
+Example to Create a Port with Extra DHCP Options
+
+ var s struct {
+ ports.Port
+ extradhcpopts.ExtraDHCPOptsExt
+ }
+
+ adminStateUp := true
+ portCreateOpts := ports.CreateOpts{
+ Name: "dhcp-conf-port",
+ AdminStateUp: &adminStateUp,
+ NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ FixedIPs: []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ },
+ }
+
+ createOpts := extradhcpopts.CreateOptsExt{
+ CreateOptsBuilder: portCreateOpts,
+ ExtraDHCPOpts: []extradhcpopts.CreateExtraDHCPOpt{
+ {
+ OptName: "optionA",
+ OptValue: "valueA",
+ },
+ },
+ }
+
+ err := ports.Create(networkClient, createOpts).ExtractInto(&s)
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Port with Extra DHCP Options
+
+ var s struct {
+ ports.Port
+ extradhcpopts.ExtraDHCPOptsExt
+ }
+
+ portUpdateOpts := ports.UpdateOpts{
+ Name: "updated-dhcp-conf-port",
+ FixedIPs: []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ },
+ }
+
+ value := "valueB"
+ updateOpts := extradhcpopts.UpdateOptsExt{
+ UpdateOptsBuilder: portUpdateOpts,
+ ExtraDHCPOpts: []extradhcpopts.UpdateExtraDHCPOpt{
+ {
+ OptName: "optionB",
+ OptValue: &value,
+ },
+ },
+ }
+
+ portID := "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2"
+ err := ports.Update(networkClient, portID, updateOpts).ExtractInto(&s)
+ if err != nil {
+ panic(err)
+ }
+*/
+package extradhcpopts
diff --git a/openstack/networking/v2/extensions/extradhcpopts/requests.go b/openstack/networking/v2/extensions/extradhcpopts/requests.go
new file mode 100644
index 0000000000..f3eb9bc450
--- /dev/null
+++ b/openstack/networking/v2/extensions/extradhcpopts/requests.go
@@ -0,0 +1,102 @@
+package extradhcpopts
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+// CreateOptsExt adds extra DHCP options to the base ports.CreateOpts.
+type CreateOptsExt struct {
+ // CreateOptsBuilder is the interface options structs have to satisfy in order
+ // to be used in the main Create operation in this package.
+ ports.CreateOptsBuilder
+
+ // ExtraDHCPOpts field is a set of DHCP options for a single port.
+ ExtraDHCPOpts []CreateExtraDHCPOpt `json:"extra_dhcp_opts,omitempty"`
+}
+
+// CreateExtraDHCPOpt represents the options required to create an extra DHCP
+// option on a port.
+type CreateExtraDHCPOpt struct {
+ // OptName is the name of a DHCP option.
+ OptName string `json:"opt_name" required:"true"`
+
+ // OptValue is the value of the DHCP option.
+ OptValue string `json:"opt_value" required:"true"`
+
+ // IPVersion is the IP protocol version of a DHCP option.
+ IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"`
+}
+
+// ToPortCreateMap casts a CreateOptsExt struct to a map.
+func (opts CreateOptsExt) ToPortCreateMap() (map[string]interface{}, error) {
+ base, err := opts.CreateOptsBuilder.ToPortCreateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ port := base["port"].(map[string]interface{})
+
+ // Convert opts.ExtraDHCPOpts to a slice of maps.
+ if opts.ExtraDHCPOpts != nil {
+ extraDHCPOpts := make([]map[string]interface{}, len(opts.ExtraDHCPOpts))
+ for i, opt := range opts.ExtraDHCPOpts {
+ b, err := gophercloud.BuildRequestBody(opt, "")
+ if err != nil {
+ return nil, err
+ }
+ extraDHCPOpts[i] = b
+ }
+ port["extra_dhcp_opts"] = extraDHCPOpts
+ }
+
+ return base, nil
+}
+
+// UpdateOptsExt adds extra DHCP options to the base ports.UpdateOpts.
+type UpdateOptsExt struct {
+ // UpdateOptsBuilder is the interface options structs have to satisfy in order
+ // to be used in the main Update operation in this package.
+ ports.UpdateOptsBuilder
+
+ // ExtraDHCPOpts field is a set of DHCP options for a single port.
+ ExtraDHCPOpts []UpdateExtraDHCPOpt `json:"extra_dhcp_opts,omitempty"`
+}
+
+// UpdateExtraDHCPOpt represents the options required to update an extra DHCP
+// option on a port.
+type UpdateExtraDHCPOpt struct {
+ // OptName is the name of a DHCP option.
+ OptName string `json:"opt_name" required:"true"`
+
+ // OptValue is the value of the DHCP option.
+ OptValue *string `json:"opt_value"`
+
+ // IPVersion is the IP protocol version of a DHCP option.
+ IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"`
+}
+
+// ToPortUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOptsExt) ToPortUpdateMap() (map[string]interface{}, error) {
+ base, err := opts.UpdateOptsBuilder.ToPortUpdateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ port := base["port"].(map[string]interface{})
+
+ // Convert opts.ExtraDHCPOpts to a slice of maps.
+ if opts.ExtraDHCPOpts != nil {
+ extraDHCPOpts := make([]map[string]interface{}, len(opts.ExtraDHCPOpts))
+ for i, opt := range opts.ExtraDHCPOpts {
+ b, err := gophercloud.BuildRequestBody(opt, "")
+ if err != nil {
+ return nil, err
+ }
+ extraDHCPOpts[i] = b
+ }
+ port["extra_dhcp_opts"] = extraDHCPOpts
+ }
+
+ return base, nil
+}
diff --git a/openstack/networking/v2/extensions/extradhcpopts/results.go b/openstack/networking/v2/extensions/extradhcpopts/results.go
new file mode 100644
index 0000000000..8e3132ea4a
--- /dev/null
+++ b/openstack/networking/v2/extensions/extradhcpopts/results.go
@@ -0,0 +1,20 @@
+package extradhcpopts
+
+// ExtraDHCPOptsExt is a struct that contains different DHCP options for a
+// single port.
+type ExtraDHCPOptsExt struct {
+ ExtraDHCPOpts []ExtraDHCPOpt `json:"extra_dhcp_opts"`
+}
+
+// ExtraDHCPOpt represents a single set of extra DHCP options for a single port.
+type ExtraDHCPOpt struct {
+ // OptName is the name of a single DHCP option.
+ OptName string `json:"opt_name"`
+
+ // OptValue is the value of a single DHCP option.
+ OptValue string `json:"opt_value"`
+
+ // IPVersion is the IP protocol version of a single DHCP option.
+ // Valid value is 4 or 6. Default is 4.
+ IPVersion int `json:"ip_version"`
+}
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/requests.go b/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
index aa30194668..cbd6c1fb0d 100644
--- a/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/requests.go
@@ -18,6 +18,7 @@ type ListOptsBuilder interface {
// `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
Name string `q:"name"`
Description string `q:"description"`
AdminStateUp bool `q:"admin_state_up"`
@@ -69,6 +70,7 @@ type CreateOpts struct {
// an admin role in order to set this. Otherwise, this field is left unset
// and the caller will be the owner.
TenantID string `json:"tenant_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
AdminStateUp *bool `json:"admin_state_up,omitempty"`
diff --git a/openstack/networking/v2/extensions/fwaas/firewalls/results.go b/openstack/networking/v2/extensions/fwaas/firewalls/results.go
index f6786a4433..9543f0fae4 100644
--- a/openstack/networking/v2/extensions/fwaas/firewalls/results.go
+++ b/openstack/networking/v2/extensions/fwaas/firewalls/results.go
@@ -14,6 +14,7 @@ type Firewall struct {
Status string `json:"status"`
PolicyID string `json:"firewall_policy_id"`
TenantID string `json:"tenant_id"`
+ ProjectID string `json:"project_id"`
}
type commonResult struct {
diff --git a/openstack/networking/v2/extensions/fwaas/policies/requests.go b/openstack/networking/v2/extensions/fwaas/policies/requests.go
index b1a6a5598b..40ab7a8c46 100644
--- a/openstack/networking/v2/extensions/fwaas/policies/requests.go
+++ b/openstack/networking/v2/extensions/fwaas/policies/requests.go
@@ -18,6 +18,7 @@ type ListOptsBuilder interface {
// and is either `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
Name string `q:"name"`
Description string `q:"description"`
Shared *bool `q:"shared"`
@@ -67,6 +68,7 @@ type CreateOpts struct {
// an admin role in order to set this. Otherwise, this field is left unset
// and the caller will be the owner.
TenantID string `json:"tenant_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Shared *bool `json:"shared,omitempty"`
diff --git a/openstack/networking/v2/extensions/fwaas/policies/results.go b/openstack/networking/v2/extensions/fwaas/policies/results.go
index bbe22b1361..495cef2c0e 100644
--- a/openstack/networking/v2/extensions/fwaas/policies/results.go
+++ b/openstack/networking/v2/extensions/fwaas/policies/results.go
@@ -11,6 +11,7 @@ type Policy struct {
Name string `json:"name"`
Description string `json:"description"`
TenantID string `json:"tenant_id"`
+ ProjectID string `json:"project_id"`
Audited bool `json:"audited"`
Shared bool `json:"shared"`
Rules []string `json:"firewall_rules,omitempty"`
diff --git a/openstack/networking/v2/extensions/fwaas/rules/requests.go b/openstack/networking/v2/extensions/fwaas/rules/requests.go
index 83bbe99b6d..17979b637b 100644
--- a/openstack/networking/v2/extensions/fwaas/rules/requests.go
+++ b/openstack/networking/v2/extensions/fwaas/rules/requests.go
@@ -37,6 +37,7 @@ type ListOptsBuilder interface {
// is either `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct {
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
Name string `q:"name"`
Description string `q:"description"`
Protocol string `q:"protocol"`
@@ -96,6 +97,7 @@ type CreateOpts struct {
Protocol Protocol `json:"protocol" required:"true"`
Action string `json:"action" required:"true"`
TenantID string `json:"tenant_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"`
diff --git a/openstack/networking/v2/extensions/fwaas/rules/results.go b/openstack/networking/v2/extensions/fwaas/rules/results.go
index 1af03e573d..82bf4a36a8 100644
--- a/openstack/networking/v2/extensions/fwaas/rules/results.go
+++ b/openstack/networking/v2/extensions/fwaas/rules/results.go
@@ -22,6 +22,7 @@ type Rule struct {
PolicyID string `json:"firewall_policy_id"`
Position int `json:"position"`
TenantID string `json:"tenant_id"`
+ ProjectID string `json:"project_id"`
}
// RulePage is the page returned by a pager when traversing over a
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
index 1c8a8b2f13..d82a1bc8e2 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/requests.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go
@@ -17,6 +17,7 @@ type ListOpts struct {
FixedIP string `q:"fixed_ip_address"`
FloatingIP string `q:"floating_ip_address"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
@@ -53,7 +54,9 @@ type CreateOpts struct {
FloatingIP string `json:"floating_ip_address,omitempty"`
PortID string `json:"port_id,omitempty"`
FixedIP string `json:"fixed_ip_address,omitempty"`
+ SubnetID string `json:"subnet_id,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
}
// ToFloatingIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/results.go b/openstack/networking/v2/extensions/layer3/floatingips/results.go
index f1af23d4a6..8b1a517645 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/results.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go
@@ -30,10 +30,13 @@ type FloatingIP struct {
// associated with the floating IP.
FixedIP string `json:"fixed_ip_address"`
- // TenantID is the Owner of the floating IP. Only admin users can specify a
- // tenant identifier other than its own.
+ // TenantID is the project owner of the floating IP. Only admin users can
+ // specify a project identifier other than its own.
TenantID string `json:"tenant_id"`
+ // ProjectID is the project owner of the floating IP.
+ ProjectID string `json:"project_id"`
+
// Status is the condition of the API resource.
Status string `json:"status"`
diff --git a/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go b/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go
index c665a2ef18..89c028e8a7 100644
--- a/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go
+++ b/openstack/networking/v2/extensions/layer3/floatingips/testing/requests_test.go
@@ -224,6 +224,58 @@ func TestCreateEmptyPort(t *testing.T) {
th.AssertEquals(t, "10.0.0.3", ip.FixedIP)
}
+func TestCreateWithSubnetID(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",
+ "subnet_id": "37adf01c-24db-467a-b845-7ab1e8216c01"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "floatingip": {
+ "router_id": null,
+ "tenant_id": "4969c491a3c74ee4af974e6d800c62de",
+ "floating_network_id": "376da547-b977-4cfe-9cba-275c80debf57",
+ "fixed_ip_address": null,
+ "floating_ip_address": "172.24.4.3",
+ "port_id": null,
+ "id": "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
+ }
+}
+ `)
+ })
+
+ options := floatingips.CreateOpts{
+ FloatingNetworkID: "376da547-b977-4cfe-9cba-275c80debf57",
+ SubnetID: "37adf01c-24db-467a-b845-7ab1e8216c01",
+ }
+
+ ip, err := floatingips.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, "172.24.4.3", ip.FloatingIP)
+ th.AssertEquals(t, "", ip.PortID)
+ th.AssertEquals(t, "", ip.FixedIP)
+}
+
func TestGet(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go
index fa346c8555..8b2bde530e 100644
--- a/openstack/networking/v2/extensions/layer3/routers/requests.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -17,6 +17,7 @@ type ListOpts struct {
Distributed *bool `q:"distributed"`
Status string `q:"status"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
@@ -53,6 +54,7 @@ type CreateOpts struct {
AdminStateUp *bool `json:"admin_state_up,omitempty"`
Distributed *bool `json:"distributed,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
AvailabilityZoneHints []string `json:"availability_zone_hints,omitempty"`
}
diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go
index da1b9e4bdf..dffdce8f48 100644
--- a/openstack/networking/v2/extensions/layer3/routers/results.go
+++ b/openstack/networking/v2/extensions/layer3/routers/results.go
@@ -54,10 +54,13 @@ type Router struct {
// ID is the unique identifier for the router.
ID string `json:"id"`
- // TenantID is the owner of the router. Only admin users can specify a tenant
- // identifier other than its own.
+ // TenantID is the project owner of the router. Only admin users can
+ // specify a project identifier other than its own.
TenantID string `json:"tenant_id"`
+ // ProjectID is the project owner of the router.
+ ProjectID string `json:"project_id"`
+
// Routes are a collection of static routes that the router will host.
Routes []Route `json:"routes"`
diff --git a/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
index 625748fd25..dd190f606f 100644
--- a/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
+++ b/openstack/networking/v2/extensions/lbaas_v2/listeners/requests.go
@@ -31,6 +31,7 @@ type ListOpts struct {
Name string `q:"name"`
AdminStateUp *bool `q:"admin_state_up"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
LoadbalancerID string `q:"loadbalancer_id"`
DefaultPoolID string `q:"default_pool_id"`
Protocol string `q:"protocol"`
@@ -86,9 +87,13 @@ type CreateOpts struct {
ProtocolPort int `json:"protocol_port" required:"true"`
// TenantID is only required if the caller has an admin role and wants
- // to create a pool for another tenant.
+ // to create a pool for another project.
TenantID string `json:"tenant_id,omitempty"`
+ // ProjectID is only required if the caller has an admin role and wants
+ // to create a pool for another project.
+ ProjectID string `json:"project_id,omitempty"`
+
// Human-readable name for the Listener. Does not have to be unique.
Name string `json:"name,omitempty"`
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
index 839776dd28..1ed23c3c82 100644
--- a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go
@@ -1,6 +1,8 @@
package loadbalancers
import (
+ "fmt"
+
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@@ -20,6 +22,7 @@ type ListOpts struct {
Description string `q:"description"`
AdminStateUp *bool `q:"admin_state_up"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
ProvisioningStatus string `q:"provisioning_status"`
VipAddress string `q:"vip_address"`
VipPortID string `q:"vip_port_id"`
@@ -35,7 +38,7 @@ type ListOpts struct {
SortDir string `q:"sort_dir"`
}
-// ToLoadbalancerListQuery formats a ListOpts into a query string.
+// ToLoadBalancerListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToLoadBalancerListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
@@ -81,10 +84,14 @@ type CreateOpts struct {
// that belong to them or networks that are shared).
VipSubnetID string `json:"vip_subnet_id" required:"true"`
- // The UUID of the tenant who owns the Loadbalancer. Only administrative users
- // can specify a tenant UUID other than their own.
+ // TenantID is the UUID of the project who owns the Loadbalancer.
+ // Only administrative users can specify a project UUID other than their own.
TenantID string `json:"tenant_id,omitempty"`
+ // ProjectID is the UUID of the project who owns the Loadbalancer.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
// The IP address of the Loadbalancer.
VipAddress string `json:"vip_address,omitempty"`
@@ -170,6 +177,20 @@ func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
return
}
+// CascadingDelete is like `Delete`, but will also delete any of the load balancer's
+// children (listener, monitor, etc).
+// NOTE: This function will only work with Octavia load balancers; Neutron does not
+// support this.
+func CascadingDelete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ if c.Type != "load-balancer" {
+ r.Err = fmt.Errorf("error prior to running cascade delete: only Octavia LBs supported")
+ return
+ }
+ u := fmt.Sprintf("%s?cascade=true", resourceURL(c, id))
+ _, r.Err = c.Delete(u, nil)
+ return
+}
+
// GetStatuses will return the status of a particular LoadBalancer.
func GetStatuses(c *gophercloud.ServiceClient, id string) (r GetStatusesResult) {
_, r.Err = c.Get(statusRootURL(c, id), &r.Body, nil)
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
index 9f8f19d7c5..42fff57131 100644
--- a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/results.go
@@ -79,8 +79,8 @@ func (r LoadBalancerPage) NextPageURL() (string, error) {
}
// IsEmpty checks whether a LoadBalancerPage struct is empty.
-func (p LoadBalancerPage) IsEmpty() (bool, error) {
- is, err := ExtractLoadBalancers(p)
+func (r LoadBalancerPage) IsEmpty() (bool, error) {
+ is, err := ExtractLoadBalancers(r)
return len(is) == 0, err
}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go
index 270bdf5a66..e370c669bf 100644
--- a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go
+++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/testing/requests_test.go
@@ -142,3 +142,20 @@ func TestUpdateLoadbalancer(t *testing.T) {
th.CheckDeepEquals(t, LoadbalancerUpdated, *actual)
}
+
+func TestCascadingDeleteLoadbalancer(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ HandleLoadbalancerDeletionSuccessfully(t)
+
+ sc := fake.ServiceClient()
+ sc.Type = "network"
+ err := loadbalancers.CascadingDelete(sc, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").ExtractErr()
+ if err == nil {
+ t.Fatalf("expected error running CascadingDelete with Neutron service client but didn't get one")
+ }
+
+ sc.Type = "load-balancer"
+ err = loadbalancers.CascadingDelete(sc, "36e08a3e-a78f-4b40-a229-1e7e23eee1ab").ExtractErr()
+ th.AssertNoErr(t, err)
+}
diff --git a/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go
index 6d9ab8ba79..c173e1c64e 100644
--- a/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go
+++ b/openstack/networking/v2/extensions/lbaas_v2/monitors/requests.go
@@ -22,6 +22,7 @@ type ListOpts struct {
ID string `q:"id"`
Name string `q:"name"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
PoolID string `q:"pool_id"`
Type string `q:"type"`
Delay int `q:"delay"`
@@ -119,10 +120,14 @@ type CreateOpts struct {
// types.
ExpectedCodes string `json:"expected_codes,omitempty"`
- // The UUID of the tenant who owns the Monitor. Only administrative users
- // can specify a tenant UUID other than their own.
+ // TenantID is the UUID of the project who owns the Monitor.
+ // Only administrative users can specify a project UUID other than their own.
TenantID string `json:"tenant_id,omitempty"`
+ // ProjectID is the UUID of the project who owns the Monitor.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
// The Name of the Monitor.
Name string `json:"name,omitempty"`
diff --git a/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go b/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
index 2173ee8171..11564be83f 100644
--- a/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
+++ b/openstack/networking/v2/extensions/lbaas_v2/pools/requests.go
@@ -20,6 +20,7 @@ type ListOpts struct {
LBMethod string `q:"lb_algorithm"`
Protocol string `q:"protocol"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
AdminStateUp *bool `q:"admin_state_up"`
Name string `q:"name"`
ID string `q:"id"`
@@ -97,10 +98,14 @@ type CreateOpts struct {
// Note: one of LoadbalancerID or ListenerID must be provided.
ListenerID string `json:"listener_id,omitempty" xor:"LoadbalancerID"`
- // The UUID of the tenant who owns the Pool. Only administrative users
- // can specify a tenant UUID other than their own.
+ // TenantID is the UUID of the project who owns the Pool.
+ // Only administrative users can specify a project UUID other than their own.
TenantID string `json:"tenant_id,omitempty"`
+ // ProjectID is the UUID of the project who owns the Pool.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
// Name of the pool.
Name string `json:"name,omitempty"`
@@ -257,10 +262,14 @@ type CreateMemberOpts struct {
// Name of the Member.
Name string `json:"name,omitempty"`
- // The UUID of the tenant who owns the Member. Only administrative users
- // can specify a tenant UUID other than their own.
+ // TenantID is the UUID of the project who owns the Member.
+ // Only administrative users can specify a project UUID other than their own.
TenantID string `json:"tenant_id,omitempty"`
+ // ProjectID is the UUID of the project who owns the Member.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
// A positive integer value that indicates the relative portion of traffic
// that this member should receive from the pool. For example, a member with
// a weight of 10 receives five times as much traffic as a member with a
diff --git a/openstack/networking/v2/extensions/portsecurity/doc.go b/openstack/networking/v2/extensions/portsecurity/doc.go
index 9de4fcf750..2b9a391681 100644
--- a/openstack/networking/v2/extensions/portsecurity/doc.go
+++ b/openstack/networking/v2/extensions/portsecurity/doc.go
@@ -29,20 +29,117 @@ Example to List Networks with Port Security Information
fmt.Println("%+v\n", network)
}
+Example to Create a Network without Port Security
+
+ var networkWithPortSecurityExt struct {
+ networks.Network
+ portsecurity.PortSecurityExt
+ }
+
+ networkCreateOpts := networks.CreateOpts{
+ Name: "private",
+ }
+
+ iFalse := false
+ createOpts := portsecurity.NetworkCreateOptsExt{
+ CreateOptsBuilder: networkCreateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := networks.Create(networkClient, createOpts).ExtractInto(&networkWithPortSecurityExt)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println("%+v\n", networkWithPortSecurityExt)
+
+Example to Disable Port Security on an Existing Network
+
+ var networkWithPortSecurityExt struct {
+ networks.Network
+ portsecurity.PortSecurityExt
+ }
+
+ iFalse := false
+ networkID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+ networkUpdateOpts := networks.UpdateOpts{}
+ updateOpts := portsecurity.NetworkUpdateOptsExt{
+ UpdateOptsBuilder: networkUpdateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := networks.Update(networkClient, networkID, updateOpts).ExtractInto(&networkWithPortSecurityExt)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println("%+v\n", networkWithPortSecurityExt)
+
Example to Get a Port with Port Security Information
- var portWithExtensions struct {
+ var portWithPortSecurityExtensions struct {
ports.Port
portsecurity.PortSecurityExt
}
portID := "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2"
- err := ports.Get(networkingClient, portID).ExtractInto(&portWithExtensions)
+ err := ports.Get(networkingClient, portID).ExtractInto(&portWithPortSecurityExtensions)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println("%+v\n", portWithPortSecurityExtensions)
+
+Example to Create a Port Without Port Security
+
+ var portWithPortSecurityExtensions struct {
+ ports.Port
+ portsecurity.PortSecurityExt
+ }
+
+ iFalse := false
+ networkID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c"
+ subnetID := "a87cc70a-3e15-4acf-8205-9b711a3531b7"
+
+ portCreateOpts := ports.CreateOpts{
+ NetworkID: networkID,
+ FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}},
+ }
+
+ createOpts := portsecurity.PortCreateOptsExt{
+ CreateOptsBuilder: portCreateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := ports.Create(networkingClient, createOpts).ExtractInto(&portWithPortSecurityExtensions)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println("%+v\n", portWithPortSecurityExtensions)
+
+Example to Disable Port Security on an Existing Port
+
+ var portWithPortSecurityExtensions struct {
+ ports.Port
+ portsecurity.PortSecurityExt
+ }
+
+ iFalse := false
+ portID := "65c0ee9f-d634-4522-8954-51021b570b0d"
+
+ portUpdateOpts := ports.UpdateOpts{}
+ updateOpts := portsecurity.PortUpdateOptsExt{
+ UpdateOptsBuilder: portUpdateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := ports.Update(networkingClient, portID, updateOpts).ExtractInto(&portWithPortSecurityExtensions)
if err != nil {
panic(err)
}
- fmt.Println("%+v\n", portWithExtensions)
+ fmt.Println("%+v\n", portWithPortSecurityExtensions)
*/
package portsecurity
diff --git a/openstack/networking/v2/extensions/portsecurity/requests.go b/openstack/networking/v2/extensions/portsecurity/requests.go
new file mode 100644
index 0000000000..c80f47cf61
--- /dev/null
+++ b/openstack/networking/v2/extensions/portsecurity/requests.go
@@ -0,0 +1,104 @@
+package portsecurity
+
+import (
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
+)
+
+// PortCreateOptsExt adds port security options to the base ports.CreateOpts.
+type PortCreateOptsExt struct {
+ ports.CreateOptsBuilder
+
+ // PortSecurityEnabled toggles port security on a port.
+ PortSecurityEnabled *bool `json:"port_security_enabled,omitempty"`
+}
+
+// ToPortCreateMap casts a CreateOpts struct to a map.
+func (opts PortCreateOptsExt) ToPortCreateMap() (map[string]interface{}, error) {
+ base, err := opts.CreateOptsBuilder.ToPortCreateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ port := base["port"].(map[string]interface{})
+
+ if opts.PortSecurityEnabled != nil {
+ port["port_security_enabled"] = &opts.PortSecurityEnabled
+ }
+
+ return base, nil
+}
+
+// PortUpdateOptsExt adds port security options to the base ports.UpdateOpts.
+type PortUpdateOptsExt struct {
+ ports.UpdateOptsBuilder
+
+ // PortSecurityEnabled toggles port security on a port.
+ PortSecurityEnabled *bool `json:"port_security_enabled,omitempty"`
+}
+
+// ToPortUpdateMap casts a UpdateOpts struct to a map.
+func (opts PortUpdateOptsExt) ToPortUpdateMap() (map[string]interface{}, error) {
+ base, err := opts.UpdateOptsBuilder.ToPortUpdateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ port := base["port"].(map[string]interface{})
+
+ if opts.PortSecurityEnabled != nil {
+ port["port_security_enabled"] = &opts.PortSecurityEnabled
+ }
+
+ return base, nil
+}
+
+// NetworkCreateOptsExt adds port security options to the base
+// networks.CreateOpts.
+type NetworkCreateOptsExt struct {
+ networks.CreateOptsBuilder
+
+ // PortSecurityEnabled toggles port security on a port.
+ PortSecurityEnabled *bool `json:"port_security_enabled,omitempty"`
+}
+
+// ToNetworkCreateMap casts a CreateOpts struct to a map.
+func (opts NetworkCreateOptsExt) ToNetworkCreateMap() (map[string]interface{}, error) {
+ base, err := opts.CreateOptsBuilder.ToNetworkCreateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ network := base["network"].(map[string]interface{})
+
+ if opts.PortSecurityEnabled != nil {
+ network["port_security_enabled"] = &opts.PortSecurityEnabled
+ }
+
+ return base, nil
+}
+
+// NetworkUpdateOptsExt adds port security options to the base
+// networks.UpdateOpts.
+type NetworkUpdateOptsExt struct {
+ networks.UpdateOptsBuilder
+
+ // PortSecurityEnabled toggles port security on a port.
+ PortSecurityEnabled *bool `json:"port_security_enabled,omitempty"`
+}
+
+// ToNetworkUpdateMap casts a UpdateOpts struct to a map.
+func (opts NetworkUpdateOptsExt) ToNetworkUpdateMap() (map[string]interface{}, error) {
+ base, err := opts.UpdateOptsBuilder.ToNetworkUpdateMap()
+ if err != nil {
+ return nil, err
+ }
+
+ network := base["network"].(map[string]interface{})
+
+ if opts.PortSecurityEnabled != nil {
+ network["port_security_enabled"] = &opts.PortSecurityEnabled
+ }
+
+ return base, nil
+}
diff --git a/openstack/networking/v2/extensions/rbacpolicies/doc.go b/openstack/networking/v2/extensions/rbacpolicies/doc.go
new file mode 100644
index 0000000000..f0ddbc0f67
--- /dev/null
+++ b/openstack/networking/v2/extensions/rbacpolicies/doc.go
@@ -0,0 +1,79 @@
+/*
+Package rbacpolicies contains functionality for working with Neutron RBAC Policies.
+Role-Based Access Control (RBAC) policy framework enables both operators
+and users to grant access to resources for specific projects.
+
+Sharing an object with a specific project is accomplished by creating a
+policy entry that permits the target project the access_as_shared action
+on that object.
+
+To make a network available as an external network for specific projects
+rather than all projects, use the access_as_external action.
+If a network is marked as external during creation, it now implicitly creates
+a wildcard RBAC policy granting everyone access to preserve previous behavior
+before this feature was added.
+
+Example to Create a RBAC Policy
+
+ createOpts := rbacpolicies.CreateOpts{
+ Action: rbacpolicies.ActionAccessShared,
+ ObjectType: "network",
+ TargetTenant: "6e547a3bcfe44702889fdeff3c3520c3",
+ ObjectID: "240d22bf-bd17-4238-9758-25f72610ecdc"
+ }
+
+ rbacPolicy, err := rbacpolicies.Create(rbacClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to List RBAC Policies
+
+ listOpts := rbacpolicies.ListOpts{
+ TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66",
+ }
+
+ allPages, err := rbacpolicies.List(rbacClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allRBACPolicies, err := rbacpolicies.ExtractRBACPolicies(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, rbacpolicy := range allRBACPolicies {
+ fmt.Printf("%+v", rbacpolicy)
+ }
+
+Example to Delete a RBAC Policy
+
+ rbacPolicyID := "94fe107f-da78-4d92-a9d7-5611b06dad8d"
+ err := rbacpolicies.Delete(rbacClient, rbacPolicyID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Get RBAC Policy by ID
+
+ rbacPolicyID := "94fe107f-da78-4d92-a9d7-5611b06dad8d"
+ rbacpolicy, err := rbacpolicies.Get(rbacClient, rbacPolicyID).Extract()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%+v", rbacpolicy)
+
+Example to Update a RBAC Policy
+
+ rbacPolicyID := "570b0306-afb5-4d3b-ab47-458fdc16baaa"
+ updateOpts := rbacpolicies.UpdateOpts{
+ TargetTenant: "9d766060b6354c9e8e2da44cab0e8f38",
+ }
+ rbacPolicy, err := rbacpolicies.Update(rbacClient, rbacPolicyID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+*/
+package rbacpolicies
diff --git a/openstack/networking/v2/extensions/rbacpolicies/requests.go b/openstack/networking/v2/extensions/rbacpolicies/requests.go
new file mode 100644
index 0000000000..532a2f23d2
--- /dev/null
+++ b/openstack/networking/v2/extensions/rbacpolicies/requests.go
@@ -0,0 +1,142 @@
+package rbacpolicies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToRBACPolicyListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the rbac attributes you want to see returned. SortKey allows you to sort
+// by a particular rbac attribute. SortDir sets the direction, and is either
+// `asc' or `desc'. Marker and Limit are used for pagination.
+type ListOpts struct {
+ TargetTenant string `q:"target_tenant"`
+ ObjectType string `q:"object_type"`
+ ObjectID string `q:"object_id"`
+ Action PolicyAction `q:"action"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ Marker string `q:"marker"`
+ Limit int `q:"limit"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
+}
+
+// ToRBACPolicyListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToRBACPolicyListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// rbac policies. It accepts a ListOpts struct, which allows you to filter and sort
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := listURL(c)
+ if opts != nil {
+ query, err := opts.ToRBACPolicyListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return RBACPolicyPage{pagination.LinkedPageBase{PageResult: r}}
+
+ })
+}
+
+// Get retrieves a specific rbac policy based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(getURL(c, id), &r.Body, nil)
+ return
+}
+
+// PolicyAction maps to Action for the RBAC policy.
+// Which allows access_as_external or access_as_shared.
+type PolicyAction string
+
+const (
+ // ActionAccessExternal returns Action for the RBAC policy as access_as_external.
+ ActionAccessExternal PolicyAction = "access_as_external"
+
+ // ActionAccessShared returns Action for the RBAC policy as access_as_shared.
+ ActionAccessShared PolicyAction = "access_as_shared"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToRBACPolicyCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts represents options used to create a rbac-policy.
+type CreateOpts struct {
+ Action PolicyAction `json:"action" required:"true"`
+ ObjectType string `json:"object_type" required:"true"`
+ TargetTenant string `json:"target_tenant" required:"true"`
+ ObjectID string `json:"object_id" required:"true"`
+}
+
+// ToRBACPolicyCreateMap builds a request body from CreateOpts.
+func (opts CreateOpts) ToRBACPolicyCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "rbac_policy")
+}
+
+// Create accepts a CreateOpts struct and creates a new rbac-policy using the values
+// provided.
+//
+// The tenant ID that is contained in the URI is the tenant that creates the
+// rbac-policy.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToRBACPolicyCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(createURL(c), b, &r.Body, nil)
+ return
+}
+
+// Delete accepts a unique ID and deletes the rbac-policy associated with it.
+func Delete(c *gophercloud.ServiceClient, rbacPolicyID string) (r DeleteResult) {
+ _, r.Err = c.Delete(deleteURL(c, rbacPolicyID), nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToRBACPolicyUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents options used to update a rbac-policy.
+type UpdateOpts struct {
+ TargetTenant string `json:"target_tenant" required:"true"`
+}
+
+// ToRBACPolicyUpdateMap builds a request body from UpdateOpts.
+func (opts UpdateOpts) ToRBACPolicyUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "rbac_policy")
+}
+
+// Update accepts a UpdateOpts struct and updates an existing rbac-policy using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, rbacPolicyID string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToRBACPolicyUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(updateURL(c, rbacPolicyID), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200, 201},
+ })
+ return
+}
diff --git a/openstack/networking/v2/extensions/rbacpolicies/results.go b/openstack/networking/v2/extensions/rbacpolicies/results.go
new file mode 100644
index 0000000000..a62facc0dc
--- /dev/null
+++ b/openstack/networking/v2/extensions/rbacpolicies/results.go
@@ -0,0 +1,98 @@
+package rbacpolicies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts RBAC Policy resource.
+func (r commonResult) Extract() (*RBACPolicy, error) {
+ var s RBACPolicy
+ err := r.ExtractInto(&s)
+ return &s, err
+}
+
+func (r commonResult) ExtractInto(v interface{}) error {
+ return r.Result.ExtractIntoStructPtr(v, "rbac_policy")
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a RBAC Policy.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a RBAC Policy.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a RBAC Policy.
+type UpdateResult struct {
+ commonResult
+}
+
+// RBACPolicy represents a RBAC policy.
+type RBACPolicy struct {
+ // UUID of the RBAC policy.
+ ID string `json:"id"`
+
+ // Action for the RBAC policy which is access_as_external or access_as_shared.
+ Action PolicyAction `json:"action"`
+
+ // ObjectID is the ID of the object_type resource.
+ // An object_type of network returns a network ID and
+ // object_type of qos-policy returns a QoS ID.
+ ObjectID string `json:"object_id"`
+
+ // ObjectType is the type of the object that the RBAC policy affects.
+ // Types include qos-policy or network.
+ ObjectType string `json:"object_type"`
+
+ // TenantID is the ID of the project that owns the resource.
+ TenantID string `json:"tenant_id"`
+
+ // TargetTenant is the ID of the tenant to which the RBAC policy will be enforced.
+ TargetTenant string `json:"target_tenant"`
+
+ // ProjectID is the ID of the project.
+ ProjectID string `json:"project_id"`
+}
+
+// RBACPolicyPage is the page returned by a pager when traversing over a
+// collection of rbac policies.
+type RBACPolicyPage struct {
+ pagination.LinkedPageBase
+}
+
+// IsEmpty checks whether a RBACPolicyPage struct is empty.
+func (r RBACPolicyPage) IsEmpty() (bool, error) {
+ is, err := ExtractRBACPolicies(r)
+ return len(is) == 0, err
+}
+
+// ExtractRBACPolicies accepts a Page struct, specifically a RBAC Policy struct,
+// and extracts the elements into a slice of RBAC Policy structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractRBACPolicies(r pagination.Page) ([]RBACPolicy, error) {
+ var s []RBACPolicy
+ err := ExtractRBACPolicesInto(r, &s)
+ return s, err
+}
+
+// ExtractRBACPolicesInto extracts the elements into a slice of RBAC Policy structs.
+func ExtractRBACPolicesInto(r pagination.Page, v interface{}) error {
+ return r.(RBACPolicyPage).Result.ExtractIntoSlicePtr(v, "rbac_policies")
+}
diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/doc.go b/openstack/networking/v2/extensions/rbacpolicies/testing/doc.go
new file mode 100644
index 0000000000..e95610ae4f
--- /dev/null
+++ b/openstack/networking/v2/extensions/rbacpolicies/testing/doc.go
@@ -0,0 +1,2 @@
+// Package testing includes rbac unit tests
+package testing
diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go
new file mode 100644
index 0000000000..f63fa2b89f
--- /dev/null
+++ b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go
@@ -0,0 +1,112 @@
+package testing
+
+import (
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/rbacpolicies"
+)
+
+const ListResponse = `
+{
+ "rbac_policies": [
+ {
+ "target_tenant": "6e547a3bcfe44702889fdeff3c3520c3",
+ "tenant_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "object_type": "network",
+ "object_id": "240d22bf-bd17-4238-9758-25f72610ecdc",
+ "action": "access_as_shared",
+ "project_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "id": "2cf7523a-93b5-4e69-9360-6c6bf986bb7c"
+ },
+ {
+ "target_tenant": "1a547a3bcfe44702889fdeff3c3520c3",
+ "tenant_id": "1ae27ce0a2a54cc6ae06dc62dd0ec832",
+ "object_type": "network",
+ "object_id": "120d22bf-bd17-4238-9758-25f72610ecdc",
+ "action": "access_as_shared",
+ "project_id": "1ae27ce0a2a54cc6ae06dc62dd0ec832",
+ "id":"1ab7523a-93b5-4e69-9360-6c6bf986bb7c"
+ }
+ ]
+}`
+
+// CreateRequest is the structure of request body to create rbac-policy.
+const CreateRequest = `
+{
+ "rbac_policy": {
+ "action": "access_as_shared",
+ "object_type": "network",
+ "target_tenant": "6e547a3bcfe44702889fdeff3c3520c3",
+ "object_id": "240d22bf-bd17-4238-9758-25f72610ecdc"
+ }
+}`
+
+// CreateResponse is the structure of response body of rbac-policy create.
+const CreateResponse = `
+{
+ "rbac_policy": {
+ "target_tenant": "6e547a3bcfe44702889fdeff3c3520c3",
+ "tenant_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "object_type": "network",
+ "object_id": "240d22bf-bd17-4238-9758-25f72610ecdc",
+ "action": "access_as_shared",
+ "project_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "id": "2cf7523a-93b5-4e69-9360-6c6bf986bb7c"
+ }
+}`
+
+// GetResponse is the structure of the response body of rbac-policy get operation.
+const GetResponse = `
+{
+ "rbac_policy": {
+ "target_tenant": "6e547a3bcfe44702889fdeff3c3520c3",
+ "tenant_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "object_type": "network",
+ "object_id": "240d22bf-bd17-4238-9758-25f72610ecdc",
+ "action": "access_as_shared",
+ "project_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "id": "2cf7523a-93b5-4e69-9360-6c6bf986bb7c"
+ }
+}`
+
+// UpdateRequest is the structure of request body to update rbac-policy.
+const UpdateRequest = `
+{
+ "rbac_policy": {
+ "target_tenant": "9d766060b6354c9e8e2da44cab0e8f38"
+ }
+}`
+
+// UpdateResponse is the structure of response body of rbac-policy update.
+const UpdateResponse = `
+{
+ "rbac_policy": {
+ "target_tenant": "9d766060b6354c9e8e2da44cab0e8f38",
+ "tenant_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "object_type": "network",
+ "object_id": "240d22bf-bd17-4238-9758-25f72610ecdc",
+ "action": "access_as_shared",
+ "project_id": "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ "id": "2cf7523a-93b5-4e69-9360-6c6bf986bb7c"
+ }
+}`
+
+var rbacPolicy1 = rbacpolicies.RBACPolicy{
+ ID: "2cf7523a-93b5-4e69-9360-6c6bf986bb7c",
+ Action: rbacpolicies.ActionAccessShared,
+ ObjectID: "240d22bf-bd17-4238-9758-25f72610ecdc",
+ ObjectType: "network",
+ TenantID: "3de27ce0a2a54cc6ae06dc62dd0ec832",
+ TargetTenant: "6e547a3bcfe44702889fdeff3c3520c3",
+ ProjectID: "3de27ce0a2a54cc6ae06dc62dd0ec832",
+}
+
+var rbacPolicy2 = rbacpolicies.RBACPolicy{
+ ID: "1ab7523a-93b5-4e69-9360-6c6bf986bb7c",
+ Action: rbacpolicies.ActionAccessShared,
+ ObjectID: "120d22bf-bd17-4238-9758-25f72610ecdc",
+ ObjectType: "network",
+ TenantID: "1ae27ce0a2a54cc6ae06dc62dd0ec832",
+ TargetTenant: "1a547a3bcfe44702889fdeff3c3520c3",
+ ProjectID: "1ae27ce0a2a54cc6ae06dc62dd0ec832",
+}
+
+var ExpectedRBACPoliciesSlice = []rbacpolicies.RBACPolicy{rbacPolicy1, rbacPolicy2}
diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go
new file mode 100644
index 0000000000..8aad843459
--- /dev/null
+++ b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go
@@ -0,0 +1,169 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/rbacpolicies"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/rbac-policies", 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, CreateRequest)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, CreateResponse)
+ })
+
+ options := rbacpolicies.CreateOpts{
+ Action: rbacpolicies.ActionAccessShared,
+ ObjectType: "network",
+ TargetTenant: "6e547a3bcfe44702889fdeff3c3520c3",
+ ObjectID: "240d22bf-bd17-4238-9758-25f72610ecdc",
+ }
+ rbacResult, err := rbacpolicies.Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertDeepEquals(t, &rbacPolicy1, rbacResult)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/rbac-policies/2cf7523a-93b5-4e69-9360-6c6bf986bb7c", 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, GetResponse)
+ })
+
+ n, err := rbacpolicies.Get(fake.ServiceClient(), "2cf7523a-93b5-4e69-9360-6c6bf986bb7c").Extract()
+ th.AssertNoErr(t, err)
+ th.CheckDeepEquals(t, &rbacPolicy1, n)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/rbac-policies", 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, ListResponse)
+ })
+
+ client := fake.ServiceClient()
+ count := 0
+
+ rbacpolicies.List(client, rbacpolicies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := rbacpolicies.ExtractRBACPolicies(page)
+ if err != nil {
+ t.Errorf("Failed to extract rbac policies: %v", err)
+ return false, err
+ }
+
+ th.CheckDeepEquals(t, ExpectedRBACPoliciesSlice, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestListWithAllPages(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/rbac-policies", 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, ListResponse)
+ })
+
+ client := fake.ServiceClient()
+
+ type newRBACPolicy struct {
+ rbacpolicies.RBACPolicy
+ }
+
+ var allRBACpolicies []newRBACPolicy
+
+ allPages, err := rbacpolicies.List(client, rbacpolicies.ListOpts{}).AllPages()
+ th.AssertNoErr(t, err)
+
+ err = rbacpolicies.ExtractRBACPolicesInto(allPages, &allRBACpolicies)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, allRBACpolicies[0].ObjectType, "network")
+ th.AssertEquals(t, allRBACpolicies[0].Action, rbacpolicies.ActionAccessShared)
+
+ th.AssertEquals(t, allRBACpolicies[1].ProjectID, "1ae27ce0a2a54cc6ae06dc62dd0ec832")
+ th.AssertEquals(t, allRBACpolicies[1].TargetTenant, "1a547a3bcfe44702889fdeff3c3520c3")
+
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/rbac-policies/71d55b18-d2f8-4c76-a5e6-e0a3dd114361", 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 := rbacpolicies.Delete(fake.ServiceClient(), "71d55b18-d2f8-4c76-a5e6-e0a3dd114361").ExtractErr()
+ th.AssertNoErr(t, res)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/rbac-policies/2cf7523a-93b5-4e69-9360-6c6bf986bb7c", 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, UpdateRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, UpdateResponse)
+ })
+
+ options := rbacpolicies.UpdateOpts{TargetTenant: "9d766060b6354c9e8e2da44cab0e8f38"}
+ rbacResult, err := rbacpolicies.Update(fake.ServiceClient(), "2cf7523a-93b5-4e69-9360-6c6bf986bb7c", options).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, rbacResult.TargetTenant, "9d766060b6354c9e8e2da44cab0e8f38")
+ th.AssertEquals(t, rbacResult.ID, "2cf7523a-93b5-4e69-9360-6c6bf986bb7c")
+}
diff --git a/openstack/networking/v2/extensions/rbacpolicies/urls.go b/openstack/networking/v2/extensions/rbacpolicies/urls.go
new file mode 100644
index 0000000000..8beeed9a5f
--- /dev/null
+++ b/openstack/networking/v2/extensions/rbacpolicies/urls.go
@@ -0,0 +1,31 @@
+package rbacpolicies
+
+import "github.com/gophercloud/gophercloud"
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL("rbac-policies", id)
+}
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL("rbac-policies")
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func listURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
diff --git a/openstack/networking/v2/extensions/security/groups/requests.go b/openstack/networking/v2/extensions/security/groups/requests.go
index 0a7ef79cf6..ebacc6ee34 100644
--- a/openstack/networking/v2/extensions/security/groups/requests.go
+++ b/openstack/networking/v2/extensions/security/groups/requests.go
@@ -11,13 +11,14 @@ import (
// 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"`
+ ID string `q:"id"`
+ Name string `q:"name"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_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
@@ -45,10 +46,14 @@ type CreateOpts struct {
// Human-readable name for the Security Group. Does not have to be unique.
Name string `json:"name" required:"true"`
- // The UUID of the tenant who owns the Group. Only administrative users
- // can specify a tenant UUID other than their own.
+ // TenantID is the UUID of the project who owns the Group.
+ // Only administrative users can specify a tenant UUID other than their own.
TenantID string `json:"tenant_id,omitempty"`
+ // ProjectID is the UUID of the project who owns the Group.
+ // Only administrative users can specify a tenant UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
// Describes the security group.
Description string `json:"description,omitempty"`
}
diff --git a/openstack/networking/v2/extensions/security/groups/results.go b/openstack/networking/v2/extensions/security/groups/results.go
index 8a8e0ffcfd..66915e6e55 100644
--- a/openstack/networking/v2/extensions/security/groups/results.go
+++ b/openstack/networking/v2/extensions/security/groups/results.go
@@ -22,8 +22,11 @@ type SecGroup struct {
// traffic entering and leaving the group.
Rules []rules.SecGroupRule `json:"security_group_rules"`
- // Owner of the security group.
+ // TenantID is the project owner of the security group.
TenantID string `json:"tenant_id"`
+
+ // ProjectID is the project owner of the security group.
+ ProjectID string `json:"project_id"`
}
// SecGroupPage is the page returned by a pager when traversing over a
diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go
index 197710fc4c..96cce2817d 100644
--- a/openstack/networking/v2/extensions/security/rules/requests.go
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -21,6 +21,7 @@ type ListOpts struct {
RemoteIPPrefix string `q:"remote_ip_prefix"`
SecGroupID string `q:"security_group_id"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
@@ -118,9 +119,9 @@ type CreateOpts struct {
// specified IP prefix as the source IP address of the IP packet.
RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"`
- // The UUID of the tenant who owns the Rule. Only administrative users
- // can specify a tenant UUID other than their own.
- TenantID string `json:"tenant_id,omitempty"`
+ // TenantID is the UUID of the project who owns the Rule.
+ // Only administrative users can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
}
// ToSecGroupRuleCreateMap builds a request body from CreateOpts.
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
index 0d8c43f8ed..377e753140 100644
--- a/openstack/networking/v2/extensions/security/rules/results.go
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -48,8 +48,11 @@ type SecGroupRule struct {
// matches the specified IP prefix as the source IP address of the IP packet.
RemoteIPPrefix string `json:"remote_ip_prefix"`
- // The owner of this security group rule.
+ // TenantID is the project owner of this security group rule.
TenantID string `json:"tenant_id"`
+
+ // ProjectID is the project owner of this security group rule.
+ ProjectID string `json:"project_id"`
}
// SecGroupRulePage is the page returned by a pager when traversing over a
diff --git a/openstack/networking/v2/extensions/subnetpools/doc.go b/openstack/networking/v2/extensions/subnetpools/doc.go
index 82d09a320d..2a9fe63dd4 100644
--- a/openstack/networking/v2/extensions/subnetpools/doc.go
+++ b/openstack/networking/v2/extensions/subnetpools/doc.go
@@ -1,7 +1,7 @@
/*
Package subnetpools provides the ability to retrieve and manage subnetpools through the Neutron API.
-Example of Listing Subnetpools.
+Example of Listing Subnetpools
listOpts := subnets.ListOpts{
IPVersion: 6,
@@ -20,5 +20,53 @@ Example of Listing Subnetpools.
for _, subnetpools := range allSubnetpools {
fmt.Printf("%+v\n", subnetpools)
}
+
+Example to Get a Subnetpool
+
+ subnetPoolID = "23d5d3f7-9dfa-4f73-b72b-8b0b0063ec55"
+ subnetPool, err := subnetpools.Get(networkClient, subnetPoolID).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Create a new Subnetpool
+
+ subnetPoolName := "private_pool"
+ subnetPoolPrefixes := []string{
+ "10.0.0.0/8",
+ "172.16.0.0/12",
+ "192.168.0.0/16",
+ }
+ subnetPoolOpts := subnetpools.CreateOpts{
+ Name: subnetPoolName,
+ Prefixes: subnetPoolPrefixes,
+ }
+ subnetPool, err := subnetpools.Create(networkClient, subnetPoolOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Subnetpool
+
+ subnetPoolID := "099546ca-788d-41e5-a76d-17d8cd282d3e"
+ updateOpts := networks.UpdateOpts{
+ Prefixes: []string{
+ "fdf7:b13d:dead:beef::/64",
+ },
+ MaxPrefixLen: 72,
+ }
+
+ subnetPool, err := subnetpools.Update(networkClient, subnetPoolID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Subnetpool
+
+ subnetPoolID := "23d5d3f7-9dfa-4f73-b72b-8b0b0063ec55"
+ err := subnetpools.Delete(networkClient, subnetPoolID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
*/
package subnetpools
diff --git a/openstack/networking/v2/extensions/subnetpools/requests.go b/openstack/networking/v2/extensions/subnetpools/requests.go
index 506a867d5c..e7ee96aef6 100644
--- a/openstack/networking/v2/extensions/subnetpools/requests.go
+++ b/openstack/networking/v2/extensions/subnetpools/requests.go
@@ -18,27 +18,24 @@ type ListOptsBuilder interface {
// SortDir sets the direction, and is either `asc' or `desc'.
// Marker and Limit are used for the pagination.
type ListOpts struct {
- ID string `q:"id"`
- Name string `q:"name"`
- DefaultQuota int `q:"default_quota"`
- TenantID string `q:"tenant_id"`
- ProjectID string `q:"project_id"`
- CreatedAt string `q:"created_at"`
- UpdatedAt string `q:"updated_at"`
- Prefixes []string `q:"prefixes"`
- DefaultPrefixLen int `q:"default_prefixlen"`
- MinPrefixLen int `q:"min_prefixlen"`
- MaxPrefixLen int `q:"max_prefixlen"`
- AddressScopeID string `q:"address_scope_id"`
- IPversion int `q:"ip_version"`
- Shared bool `q:"shared"`
- Description string `q:"description"`
- IsDefault bool `q:"is_default"`
- RevisionNumber int `q:"revision_number"`
- Limit int `q:"limit"`
- Marker string `q:"marker"`
- SortKey string `q:"sort_key"`
- SortDir string `q:"sort_dir"`
+ ID string `q:"id"`
+ Name string `q:"name"`
+ DefaultQuota int `q:"default_quota"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ DefaultPrefixLen int `q:"default_prefixlen"`
+ MinPrefixLen int `q:"min_prefixlen"`
+ MaxPrefixLen int `q:"max_prefixlen"`
+ AddressScopeID string `q:"address_scope_id"`
+ IPVersion int `q:"ip_version"`
+ Shared *bool `q:"shared"`
+ Description string `q:"description"`
+ IsDefault *bool `q:"is_default"`
+ RevisionNumber int `q:"revision_number"`
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ SortKey string `q:"sort_key"`
+ SortDir string `q:"sort_dir"`
}
// ToSubnetPoolListQuery formats a ListOpts into a query string.
@@ -66,3 +63,159 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
return SubnetPoolPage{pagination.LinkedPageBase{PageResult: r}}
})
}
+
+// Get retrieves a specific subnetpool based on its ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(getURL(c, id), &r.Body, nil)
+ return
+}
+
+// CreateOptsBuilder allows to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToSubnetPoolCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts specifies parameters of a new subnetpool.
+type CreateOpts struct {
+ // Name is the human-readable name of the subnetpool.
+ Name string `json:"name"`
+
+ // DefaultQuota is the per-project quota on the prefix space
+ // that can be allocated from the subnetpool for project subnets.
+ DefaultQuota int `json:"default_quota,omitempty"`
+
+ // TenantID is the id of the Identity project.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // ProjectID is the id of the Identity project.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // Prefixes is the list of subnet prefixes to assign to the subnetpool.
+ // Neutron API merges adjacent prefixes and treats them as a single prefix.
+ // Each subnet prefix must be unique among all subnet prefixes in all subnetpools
+ // that are associated with the address scope.
+ Prefixes []string `json:"prefixes"`
+
+ // DefaultPrefixLen is the size of the prefix to allocate when the cidr
+ // or prefixlen attributes are omitted when you create the subnet.
+ // Defaults to the MinPrefixLen.
+ DefaultPrefixLen int `json:"default_prefixlen,omitempty"`
+
+ // MinPrefixLen is the smallest prefix that can be allocated from a subnetpool.
+ // For IPv4 subnetpools, default is 8.
+ // For IPv6 subnetpools, default is 64.
+ MinPrefixLen int `json:"min_prefixlen,omitempty"`
+
+ // MaxPrefixLen is the maximum prefix size that can be allocated from the subnetpool.
+ // For IPv4 subnetpools, default is 32.
+ // For IPv6 subnetpools, default is 128.
+ MaxPrefixLen int `json:"max_prefixlen,omitempty"`
+
+ // AddressScopeID is the Neutron address scope to assign to the subnetpool.
+ AddressScopeID string `json:"address_scope_id,omitempty"`
+
+ // Shared indicates whether this network is shared across all projects.
+ Shared bool `json:"shared,omitempty"`
+
+ // Description is the human-readable description for the resource.
+ Description string `json:"description,omitempty"`
+
+ // IsDefault indicates if the subnetpool is default pool or not.
+ IsDefault bool `json:"is_default,omitempty"`
+}
+
+// ToSubnetPoolCreateMap constructs a request body from CreateOpts.
+func (opts CreateOpts) ToSubnetPoolCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "subnetpool")
+}
+
+// Create requests the creation of a new subnetpool on the server.
+func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToSubnetPoolCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{201},
+ })
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToSubnetPoolUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts represents options used to update a network.
+type UpdateOpts struct {
+ // Name is the human-readable name of the subnetpool.
+ Name string `json:"name,omitempty"`
+
+ // DefaultQuota is the per-project quota on the prefix space
+ // that can be allocated from the subnetpool for project subnets.
+ DefaultQuota *int `json:"default_quota,omitempty"`
+
+ // TenantID is the id of the Identity project.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // ProjectID is the id of the Identity project.
+ ProjectID string `json:"project_id,omitempty"`
+
+ // Prefixes is the list of subnet prefixes to assign to the subnetpool.
+ // Neutron API merges adjacent prefixes and treats them as a single prefix.
+ // Each subnet prefix must be unique among all subnet prefixes in all subnetpools
+ // that are associated with the address scope.
+ Prefixes []string `json:"prefixes,omitempty"`
+
+ // DefaultPrefixLen is yhe size of the prefix to allocate when the cidr
+ // or prefixlen attributes are omitted when you create the subnet.
+ // Defaults to the MinPrefixLen.
+ DefaultPrefixLen int `json:"default_prefixlen,omitempty"`
+
+ // MinPrefixLen is the smallest prefix that can be allocated from a subnetpool.
+ // For IPv4 subnetpools, default is 8.
+ // For IPv6 subnetpools, default is 64.
+ MinPrefixLen int `json:"min_prefixlen,omitempty"`
+
+ // MaxPrefixLen is the maximum prefix size that can be allocated from the subnetpool.
+ // For IPv4 subnetpools, default is 32.
+ // For IPv6 subnetpools, default is 128.
+ MaxPrefixLen int `json:"max_prefixlen,omitempty"`
+
+ // AddressScopeID is the Neutron address scope to assign to the subnetpool.
+ AddressScopeID *string `json:"address_scope_id,omitempty"`
+
+ // Description is thehuman-readable description for the resource.
+ Description *string `json:"description,omitempty"`
+
+ // IsDefault indicates if the subnetpool is default pool or not.
+ IsDefault *bool `json:"is_default,omitempty"`
+}
+
+// ToSubnetPoolUpdateMap builds a request body from UpdateOpts.
+func (opts UpdateOpts) ToSubnetPoolUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "subnetpool")
+}
+
+// Update accepts a UpdateOpts struct and updates an existing subnetpool using the
+// values provided.
+func Update(c *gophercloud.ServiceClient, subnetPoolID string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToSubnetPoolUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(updateURL(c, subnetPoolID), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// Delete accepts a unique ID and deletes the subnetpool associated with it.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(deleteURL(c, id), nil)
+ return
+}
diff --git a/openstack/networking/v2/extensions/subnetpools/results.go b/openstack/networking/v2/extensions/subnetpools/results.go
index b4eb8fb2db..e761eac44d 100644
--- a/openstack/networking/v2/extensions/subnetpools/results.go
+++ b/openstack/networking/v2/extensions/subnetpools/results.go
@@ -4,11 +4,49 @@ import (
"encoding/json"
"fmt"
"strconv"
+ "time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts a subnetpool resource.
+func (r commonResult) Extract() (*SubnetPool, error) {
+ var s struct {
+ SubnetPool *SubnetPool `json:"subnetpool"`
+ }
+ err := r.ExtractInto(&s)
+ return s.SubnetPool, err
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a SubnetPool.
+type GetResult struct {
+ commonResult
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a SubnetPool.
+type CreateResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a SubnetPool.
+type UpdateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation. Call its
+// ExtractErr method to determine if the request succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
// SubnetPool represents a Neutron subnetpool.
// A subnetpool is a pool of addresses from which subnets can be allocated.
type SubnetPool struct {
@@ -29,10 +67,10 @@ type SubnetPool struct {
ProjectID string `json:"project_id"`
// CreatedAt is the time at which subnetpool has been created.
- CreatedAt string `json:"created_at"`
+ CreatedAt time.Time `json:"created_at"`
// UpdatedAt is the time at which subnetpool has been created.
- UpdatedAt string `json:"updated_at"`
+ UpdatedAt time.Time `json:"updated_at"`
// Prefixes is the list of subnet prefixes to assign to the subnetpool.
// Neutron API merges adjacent prefixes and treats them as a single prefix.
diff --git a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go
index d22074c148..2742028905 100644
--- a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go
+++ b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go
@@ -1,6 +1,8 @@
package testing
import (
+ "time"
+
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools"
)
@@ -78,7 +80,7 @@ const SubnetPoolsListResult = `
var SubnetPool1 = subnetpools.SubnetPool{
AddressScopeID: "",
- CreatedAt: "2017-12-28T07:21:41Z",
+ CreatedAt: time.Date(2017, 12, 28, 7, 21, 41, 0, time.UTC),
DefaultPrefixLen: 8,
DefaultQuota: 0,
Description: "IPv4",
@@ -96,12 +98,12 @@ var SubnetPool1 = subnetpools.SubnetPool{
TenantID: "1e2b9857295a4a3e841809ef492812c5",
RevisionNumber: 1,
Shared: false,
- UpdatedAt: "2017-12-28T07:21:41Z",
+ UpdatedAt: time.Date(2017, 12, 28, 7, 21, 41, 0, time.UTC),
}
var SubnetPool2 = subnetpools.SubnetPool{
AddressScopeID: "0bc38e22-be49-4e67-969e-fec3f36508bd",
- CreatedAt: "2017-12-28T07:21:34Z",
+ CreatedAt: time.Date(2017, 12, 28, 7, 21, 34, 0, time.UTC),
DefaultPrefixLen: 64,
DefaultQuota: 0,
Description: "IPv6",
@@ -119,12 +121,12 @@ var SubnetPool2 = subnetpools.SubnetPool{
TenantID: "1e2b9857295a4a3e841809ef492812c5",
RevisionNumber: 1,
Shared: false,
- UpdatedAt: "2017-12-28T07:21:34Z",
+ UpdatedAt: time.Date(2017, 12, 28, 7, 21, 34, 0, time.UTC),
}
var SubnetPool3 = subnetpools.SubnetPool{
AddressScopeID: "",
- CreatedAt: "2017-12-28T07:21:27Z",
+ CreatedAt: time.Date(2017, 12, 28, 7, 21, 27, 0, time.UTC),
DefaultPrefixLen: 64,
DefaultQuota: 4,
Description: "PublicPool",
@@ -141,5 +143,117 @@ var SubnetPool3 = subnetpools.SubnetPool{
TenantID: "ceb366d50ad54fe39717df3af60f9945",
RevisionNumber: 1,
Shared: true,
- UpdatedAt: "2017-12-28T07:21:27Z",
+ UpdatedAt: time.Date(2017, 12, 28, 7, 21, 27, 0, time.UTC),
+}
+
+const SubnetPoolGetResult = `
+{
+ "subnetpool": {
+ "min_prefixlen": "64",
+ "address_scope_id": null,
+ "default_prefixlen": "64",
+ "id": "0a738452-8057-4ad3-89c2-92f6a74afa76",
+ "max_prefixlen": "128",
+ "name": "my-ipv6-pool",
+ "default_quota": 2,
+ "is_default": true,
+ "project_id": "1e2b9857295a4a3e841809ef492812c5",
+ "tenant_id": "1e2b9857295a4a3e841809ef492812c5",
+ "created_at": "2018-01-01T00:00:01Z",
+ "prefixes": [
+ "2001:db8::a3/64"
+ ],
+ "updated_at": "2018-01-01T00:10:10Z",
+ "ip_version": 6,
+ "shared": false,
+ "description": "ipv6 prefixes",
+ "revision_number": 2
+ }
}
+`
+
+const SubnetPoolCreateRequest = `
+{
+ "subnetpool": {
+ "name": "my_ipv4_pool",
+ "prefixes": [
+ "10.10.0.0/16",
+ "10.11.11.0/24"
+ ],
+ "address_scope_id": "3d4e2e2a-552b-42ad-a16d-820bbf3edaf3",
+ "min_prefixlen": 25,
+ "max_prefixlen": 30,
+ "description": "ipv4 prefixes"
+ }
+}
+`
+
+const SubnetPoolCreateResult = `
+{
+ "subnetpool": {
+ "address_scope_id": "3d4e2e2a-552b-42ad-a16d-820bbf3edaf3",
+ "created_at": "2018-01-01T00:00:15Z",
+ "default_prefixlen": "25",
+ "default_quota": null,
+ "description": "ipv4 prefixes",
+ "id": "55b5999c-c2fe-42cd-bce0-961a551b80f5",
+ "ip_version": 4,
+ "is_default": false,
+ "max_prefixlen": "30",
+ "min_prefixlen": "25",
+ "name": "my_ipv4_pool",
+ "prefixes": [
+ "10.10.0.0/16",
+ "10.11.11.0/24"
+ ],
+ "project_id": "1e2b9857295a4a3e841809ef492812c5",
+ "revision_number": 1,
+ "shared": false,
+ "tenant_id": "1e2b9857295a4a3e841809ef492812c5",
+ "updated_at": "2018-01-01T00:00:15Z"
+ }
+}
+`
+
+const SubnetPoolUpdateRequest = `
+{
+ "subnetpool": {
+ "name": "new_subnetpool_name",
+ "prefixes": [
+ "10.11.12.0/24",
+ "10.24.0.0/16"
+ ],
+ "max_prefixlen": 16,
+ "address_scope_id": "",
+ "default_quota": 0,
+ "description": ""
+ }
+}
+`
+
+const SubnetPoolUpdateResponse = `
+{
+ "subnetpool": {
+ "address_scope_id": null,
+ "created_at": "2018-01-03T07:21:34Z",
+ "default_prefixlen": 8,
+ "default_quota": null,
+ "description": null,
+ "id": "099546ca-788d-41e5-a76d-17d8cd282d3e",
+ "ip_version": 4,
+ "is_default": true,
+ "max_prefixlen": 16,
+ "min_prefixlen": 8,
+ "name": "new_subnetpool_name",
+ "prefixes": [
+ "10.8.0.0/16",
+ "10.11.12.0/24",
+ "10.24.0.0/16"
+ ],
+ "revision_number": 2,
+ "shared": false,
+ "tenant_id": "1e2b9857295a4a3e841809ef492812c5",
+ "updated_at": "2018-01-05T09:56:56Z"
+ }
+}
+`
diff --git a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go
index 9624bf4420..3d138d37b3 100644
--- a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go
+++ b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go
@@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"testing"
+ "time"
fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/subnetpools"
@@ -50,3 +51,142 @@ func TestList(t *testing.T) {
t.Errorf("Expected 1 page, got %d", count)
}
}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnetpools/0a738452-8057-4ad3-89c2-92f6a74afa76", 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, SubnetPoolGetResult)
+ })
+
+ s, err := subnetpools.Get(fake.ServiceClient(), "0a738452-8057-4ad3-89c2-92f6a74afa76").Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.ID, "0a738452-8057-4ad3-89c2-92f6a74afa76")
+ th.AssertEquals(t, s.Name, "my-ipv6-pool")
+ th.AssertEquals(t, s.DefaultQuota, 2)
+ th.AssertEquals(t, s.TenantID, "1e2b9857295a4a3e841809ef492812c5")
+ th.AssertEquals(t, s.ProjectID, "1e2b9857295a4a3e841809ef492812c5")
+ th.AssertEquals(t, s.CreatedAt, time.Date(2018, 1, 1, 0, 0, 1, 0, time.UTC))
+ th.AssertEquals(t, s.UpdatedAt, time.Date(2018, 1, 1, 0, 10, 10, 0, time.UTC))
+ th.AssertDeepEquals(t, s.Prefixes, []string{
+ "2001:db8::a3/64",
+ })
+ th.AssertEquals(t, s.DefaultPrefixLen, 64)
+ th.AssertEquals(t, s.MinPrefixLen, 64)
+ th.AssertEquals(t, s.MaxPrefixLen, 128)
+ th.AssertEquals(t, s.AddressScopeID, "")
+ th.AssertEquals(t, s.IPversion, 6)
+ th.AssertEquals(t, s.Shared, false)
+ th.AssertEquals(t, s.Description, "ipv6 prefixes")
+ th.AssertEquals(t, s.IsDefault, true)
+ th.AssertEquals(t, s.RevisionNumber, 2)
+}
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnetpools", 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, SubnetPoolCreateRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, SubnetPoolCreateResult)
+ })
+
+ opts := subnetpools.CreateOpts{
+ Name: "my_ipv4_pool",
+ Prefixes: []string{
+ "10.10.0.0/16",
+ "10.11.11.0/24",
+ },
+ MinPrefixLen: 25,
+ MaxPrefixLen: 30,
+ AddressScopeID: "3d4e2e2a-552b-42ad-a16d-820bbf3edaf3",
+ Description: "ipv4 prefixes",
+ }
+ s, err := subnetpools.Create(fake.ServiceClient(), opts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Name, "my_ipv4_pool")
+ th.AssertDeepEquals(t, s.Prefixes, []string{
+ "10.10.0.0/16",
+ "10.11.11.0/24",
+ })
+ th.AssertEquals(t, s.MinPrefixLen, 25)
+ th.AssertEquals(t, s.MaxPrefixLen, 30)
+ th.AssertEquals(t, s.AddressScopeID, "3d4e2e2a-552b-42ad-a16d-820bbf3edaf3")
+ th.AssertEquals(t, s.Description, "ipv4 prefixes")
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnetpools/099546ca-788d-41e5-a76d-17d8cd282d3e", 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, SubnetPoolUpdateRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, SubnetPoolUpdateResponse)
+ })
+
+ nullString := ""
+ nullInt := 0
+ updateOpts := subnetpools.UpdateOpts{
+ Name: "new_subnetpool_name",
+ Prefixes: []string{
+ "10.11.12.0/24",
+ "10.24.0.0/16",
+ },
+ MaxPrefixLen: 16,
+ AddressScopeID: &nullString,
+ DefaultQuota: &nullInt,
+ Description: &nullString,
+ }
+ n, err := subnetpools.Update(fake.ServiceClient(), "099546ca-788d-41e5-a76d-17d8cd282d3e", updateOpts).Extract()
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, n.Name, "new_subnetpool_name")
+ th.AssertDeepEquals(t, n.Prefixes, []string{
+ "10.8.0.0/16",
+ "10.11.12.0/24",
+ "10.24.0.0/16",
+ })
+ th.AssertEquals(t, n.MaxPrefixLen, 16)
+ th.AssertEquals(t, n.ID, "099546ca-788d-41e5-a76d-17d8cd282d3e")
+ th.AssertEquals(t, n.AddressScopeID, "")
+ th.AssertEquals(t, n.DefaultQuota, 0)
+ th.AssertEquals(t, n.Description, "")
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/subnetpools/099546ca-788d-41e5-a76d-17d8cd282d3e", 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 := subnetpools.Delete(fake.ServiceClient(), "099546ca-788d-41e5-a76d-17d8cd282d3e")
+ th.AssertNoErr(t, res.Err)
+}
diff --git a/openstack/networking/v2/extensions/subnetpools/urls.go b/openstack/networking/v2/extensions/subnetpools/urls.go
index c8fc5cb13e..a05062c96d 100644
--- a/openstack/networking/v2/extensions/subnetpools/urls.go
+++ b/openstack/networking/v2/extensions/subnetpools/urls.go
@@ -4,6 +4,10 @@ import "github.com/gophercloud/gophercloud"
const resourcePath = "subnetpools"
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(resourcePath, id)
+}
+
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(resourcePath)
}
@@ -11,3 +15,19 @@ func rootURL(c *gophercloud.ServiceClient) string {
func listURL(c *gophercloud.ServiceClient) string {
return rootURL(c)
}
+
+func getURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func createURL(c *gophercloud.ServiceClient) string {
+ return rootURL(c)
+}
+
+func updateURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
+
+func deleteURL(c *gophercloud.ServiceClient, id string) string {
+ return resourceURL(c, id)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go
new file mode 100644
index 0000000000..5f49bd1da4
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go
@@ -0,0 +1,58 @@
+/*
+Package endpointgroups allows management of endpoint groups in the Openstack Network Service
+
+Example to create an Endpoint Group
+
+ createOpts := endpointgroups.CreateOpts{
+ Name: groupName,
+ Type: endpointgroups.TypeCIDR,
+ Endpoints: []string{
+ "10.2.0.0/24",
+ "10.3.0.0/24",
+ },
+ }
+ group, err := endpointgroups.Create(client, createOpts).Extract()
+ if err != nil {
+ return group, err
+ }
+
+Example to retrieve an Endpoint Group
+
+ group, err := endpointgroups.Get(client, "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete an Endpoint Group
+
+ err := endpointgroups.Delete(client, "5291b189-fd84-46e5-84bd-78f40c05d69c").ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to List Endpoint groups
+
+ allPages, err := endpointgroups.List(client, nil).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allGroups, err := endpointgroups.ExtractEndpointGroups(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update an endpoint group
+
+ name := "updatedname"
+ description := "updated description"
+ updateOpts := endpointgroups.UpdateOpts{
+ Name: &name,
+ Description: &description,
+ }
+ updatedPolicy, err := endpointgroups.Update(client, "5c561d9d-eaea-45f6-ae3e-08d1a7080828", updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+*/
+package endpointgroups
diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go
new file mode 100644
index 0000000000..c12d0a8004
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go
@@ -0,0 +1,144 @@
+package endpointgroups
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+type EndpointType string
+
+const (
+ TypeSubnet EndpointType = "subnet"
+ TypeCIDR EndpointType = "cidr"
+ TypeVLAN EndpointType = "vlan"
+ TypeNetwork EndpointType = "network"
+ TypeRouter EndpointType = "router"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToEndpointGroupCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new endpoint group
+type CreateOpts struct {
+ // TenantID specifies a tenant to own the endpoint group. The caller must have
+ // an admin role in order to set this. Otherwise, this field is left unset
+ // and the caller will be the owner.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // Description is the human readable description of the endpoint group.
+ Description string `json:"description,omitempty"`
+
+ // Name is the human readable name of the endpoint group.
+ Name string `json:"name,omitempty"`
+
+ // The type of the endpoints in the group.
+ // A valid value is subnet, cidr, network, router, or vlan.
+ Type EndpointType `json:"type,omitempty"`
+
+ // List of endpoints of the same type, for the endpoint group.
+ // The values will depend on the type.
+ Endpoints []string `json:"endpoints"`
+}
+
+// ToEndpointGroupCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToEndpointGroupCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "endpoint_group")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// endpoint group.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToEndpointGroupCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Get retrieves a particular endpoint group based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToEndpointGroupListQuery() (string, error)
+}
+
+// ListOpts allows the filtering of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the Endpoint group attributes you want to see returned.
+type ListOpts struct {
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ Description string `q:"description"`
+ Name string `q:"name"`
+ Type string `q:"type"`
+}
+
+// ToEndpointGroupListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToEndpointGroupListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// Endpoint groups. It accepts a ListOpts struct, which allows you to filter
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToEndpointGroupListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return EndpointGroupPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// Delete will permanently delete a particular endpoint group based on its
+// unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToEndpointGroupUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating an endpoint group.
+type UpdateOpts struct {
+ Description *string `json:"description,omitempty"`
+ Name *string `json:"name,omitempty"`
+}
+
+// ToEndpointGroupUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToEndpointGroupUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "endpoint_group")
+}
+
+// Update allows endpoint groups to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToEndpointGroupUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go
new file mode 100644
index 0000000000..822b70002c
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go
@@ -0,0 +1,104 @@
+package endpointgroups
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// EndpointGroup is an endpoint group.
+type EndpointGroup struct {
+ // TenantID specifies a tenant to own the endpoint group.
+ TenantID string `json:"tenant_id"`
+
+ // TenantID specifies a tenant to own the endpoint group.
+ ProjectID string `json:"project_id"`
+
+ // Description is the human readable description of the endpoint group.
+ Description string `json:"description"`
+
+ // Name is the human readable name of the endpoint group.
+ Name string `json:"name"`
+
+ // Type is the type of the endpoints in the group.
+ Type string `json:"type"`
+
+ // Endpoints is a list of endpoints.
+ Endpoints []string `json:"endpoints"`
+
+ // ID is the id of the endpoint group
+ ID string `json:"id"`
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts an endpoint group.
+func (r commonResult) Extract() (*EndpointGroup, error) {
+ var s struct {
+ Service *EndpointGroup `json:"endpoint_group"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Service, err
+}
+
+// EndpointGroupPage is the page returned by a pager when traversing over a
+// collection of Policies.
+type EndpointGroupPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of Endpoint 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 (r EndpointGroupPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"endpoint_groups_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether an EndpointGroupPage struct is empty.
+func (r EndpointGroupPage) IsEmpty() (bool, error) {
+ is, err := ExtractEndpointGroups(r)
+ return len(is) == 0, err
+}
+
+// ExtractEndpointGroups accepts a Page struct, specifically an EndpointGroupPage struct,
+// and extracts the elements into a slice of Endpoint group structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractEndpointGroups(r pagination.Page) ([]EndpointGroup, error) {
+ var s struct {
+ EndpointGroups []EndpointGroup `json:"endpoint_groups"`
+ }
+ err := (r.(EndpointGroupPage)).ExtractInto(&s)
+ return s.EndpointGroups, err
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as an endpoint group.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as an EndpointGroup.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the results of a Delete operation. Call its ExtractErr method
+// to determine whether the operation succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract method
+// to interpret it as an EndpointGroup.
+type UpdateResult struct {
+ commonResult
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go
new file mode 100644
index 0000000000..7feac37f78
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go
@@ -0,0 +1,265 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/endpointgroups"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/endpoint-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, `
+{
+ "endpoint_group": {
+ "endpoints": [
+ "10.2.0.0/24",
+ "10.3.0.0/24"
+ ],
+ "type": "cidr",
+ "name": "peers"
+ }
+} `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "endpoint_group": {
+ "description": "",
+ "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "project_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "endpoints": [
+ "10.2.0.0/24",
+ "10.3.0.0/24"
+ ],
+ "type": "cidr",
+ "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ "name": "peers"
+ }
+}
+ `)
+ })
+
+ options := endpointgroups.CreateOpts{
+ Name: "peers",
+ Type: endpointgroups.TypeCIDR,
+ Endpoints: []string{
+ "10.2.0.0/24",
+ "10.3.0.0/24",
+ },
+ }
+ actual, err := endpointgroups.Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+ expected := endpointgroups.EndpointGroup{
+ Name: "peers",
+ TenantID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ProjectID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ID: "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ Description: "",
+ Endpoints: []string{
+ "10.2.0.0/24",
+ "10.3.0.0/24",
+ },
+ Type: "cidr",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/endpoint-groups/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+{
+ "endpoint_group": {
+ "description": "",
+ "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "project_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "endpoints": [
+ "10.2.0.0/24",
+ "10.3.0.0/24"
+ ],
+ "type": "cidr",
+ "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ "name": "peers"
+ }
+}
+ `)
+ })
+
+ actual, err := endpointgroups.Get(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828").Extract()
+ th.AssertNoErr(t, err)
+ expected := endpointgroups.EndpointGroup{
+ Name: "peers",
+ TenantID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ProjectID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ID: "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ Description: "",
+ Endpoints: []string{
+ "10.2.0.0/24",
+ "10.3.0.0/24",
+ },
+ Type: "cidr",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/endpoint-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, `
+ {
+ "endpoint_groups": [
+ {
+ "description": "",
+ "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "project_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "endpoints": [
+ "10.2.0.0/24",
+ "10.3.0.0/24"
+ ],
+ "type": "cidr",
+ "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ "name": "peers"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ endpointgroups.List(fake.ServiceClient(), endpointgroups.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := endpointgroups.ExtractEndpointGroups(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+ expected := []endpointgroups.EndpointGroup{
+ {
+ Name: "peers",
+ TenantID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ProjectID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ID: "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ Description: "",
+ Endpoints: []string{
+ "10.2.0.0/24",
+ "10.3.0.0/24",
+ },
+ Type: "cidr",
+ },
+ }
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/endpoint-groups/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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 := endpointgroups.Delete(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/endpoint-groups/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+{
+ "endpoint_group": {
+ "description": "updated description",
+ "name": "updatedname"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "endpoint_group": {
+ "description": "updated description",
+ "tenant_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "project_id": "4ad57e7ce0b24fca8f12b9834d91079d",
+ "endpoints": [
+ "10.2.0.0/24",
+ "10.3.0.0/24"
+ ],
+ "type": "cidr",
+ "id": "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ "name": "updatedname"
+ }
+}
+`)
+ })
+
+ updatedName := "updatedname"
+ updatedDescription := "updated description"
+ options := endpointgroups.UpdateOpts{
+ Name: &updatedName,
+ Description: &updatedDescription,
+ }
+
+ actual, err := endpointgroups.Update(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828", options).Extract()
+ th.AssertNoErr(t, err)
+ expected := endpointgroups.EndpointGroup{
+ Name: "updatedname",
+ TenantID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ProjectID: "4ad57e7ce0b24fca8f12b9834d91079d",
+ ID: "6ecd9cf3-ca64-46c7-863f-f2eb1b9e838a",
+ Description: "updated description",
+ Endpoints: []string{
+ "10.2.0.0/24",
+ "10.3.0.0/24",
+ },
+ Type: "cidr",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/urls.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/urls.go
new file mode 100644
index 0000000000..9e83563ce1
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/urls.go
@@ -0,0 +1,16 @@
+package endpointgroups
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "vpn"
+ resourcePath = "endpoint-groups"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go
new file mode 100644
index 0000000000..ee44279afa
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go
@@ -0,0 +1,64 @@
+/*
+Package ikepolicies allows management and retrieval of IKE policies in the
+OpenStack Networking Service.
+
+
+Example to Create an IKE policy
+
+ createOpts := ikepolicies.CreateOpts{
+ Name: "ikepolicy1",
+ Description: "Description of ikepolicy1",
+ EncryptionAlgorithm: ikepolicies.EncryptionAlgorithm3DES,
+ PFS: ikepolicies.PFSGroup5,
+ }
+
+ policy, err := ikepolicies.Create(networkClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Show the details of a specific IKE policy by ID
+
+ policy, err := ikepolicies.Get(client, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+
+Example to Delete a Policy
+
+ err := ikepolicies.Delete(client, "5291b189-fd84-46e5-84bd-78f40c05d69c").ExtractErr()
+ if err != nil {
+ panic(err)
+
+Example to Update an IKE policy
+
+ name := "updatedname"
+ description := "updated policy"
+ updateOpts := ikepolicies.UpdateOpts{
+ Name: &name,
+ Description: &description,
+ Lifetime: &ikepolicies.LifetimeUpdateOpts{
+ Value: 7000,
+ },
+ }
+ updatedPolicy, err := ikepolicies.Update(client, "5c561d9d-eaea-45f6-ae3e-08d1a7080828", updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+
+Example to List IKE policies
+
+ allPages, err := ikepolicies.List(client, nil).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allPolicies, err := ikepolicies.ExtractPolicies(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+*/
+package ikepolicies
diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go
new file mode 100644
index 0000000000..6b084ff624
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go
@@ -0,0 +1,209 @@
+package ikepolicies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+type AuthAlgorithm string
+type EncryptionAlgorithm string
+type PFS string
+type Unit string
+type IKEVersion string
+type Phase1NegotiationMode string
+
+const (
+ AuthAlgorithmSHA1 AuthAlgorithm = "sha1"
+ AuthAlgorithmSHA256 AuthAlgorithm = "sha256"
+ AuthAlgorithmSHA384 AuthAlgorithm = "sha384"
+ AuthAlgorithmSHA512 AuthAlgorithm = "sha512"
+ EncryptionAlgorithm3DES EncryptionAlgorithm = "3des"
+ EncryptionAlgorithmAES128 EncryptionAlgorithm = "aes-128"
+ EncryptionAlgorithmAES256 EncryptionAlgorithm = "aes-256"
+ EncryptionAlgorithmAES192 EncryptionAlgorithm = "aes-192"
+ UnitSeconds Unit = "seconds"
+ UnitKilobytes Unit = "kilobytes"
+ PFSGroup2 PFS = "group2"
+ PFSGroup5 PFS = "group5"
+ PFSGroup14 PFS = "group14"
+ IKEVersionv1 IKEVersion = "v1"
+ IKEVersionv2 IKEVersion = "v2"
+ Phase1NegotiationModeMain Phase1NegotiationMode = "main"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToPolicyCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new IKE policy
+type CreateOpts struct {
+ // TenantID specifies a tenant to own the IKE policy. The caller must have
+ // an admin role in order to set this. Otherwise, this field is left unset
+ // and the caller will be the owner.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // Description is the human readable description of the policy.
+ Description string `json:"description,omitempty"`
+
+ // Name is the human readable name of the policy.
+ // Does not have to be unique.
+ Name string `json:"name,omitempty"`
+
+ // AuthAlgorithm is the authentication hash algorithm.
+ // Valid values are sha1, sha256, sha384, sha512.
+ // The default is sha1.
+ AuthAlgorithm AuthAlgorithm `json:"auth_algorithm,omitempty"`
+
+ // EncryptionAlgorithm is the encryption algorithm.
+ // A valid value is 3des, aes-128, aes-192, aes-256, and so on.
+ // Default is aes-128.
+ EncryptionAlgorithm EncryptionAlgorithm `json:"encryption_algorithm,omitempty"`
+
+ // PFS is the Perfect forward secrecy mode.
+ // A valid value is Group2, Group5, Group14, and so on.
+ // Default is Group5.
+ PFS PFS `json:"pfs,omitempty"`
+
+ // The IKE mode.
+ // A valid value is main, which is the default.
+ Phase1NegotiationMode Phase1NegotiationMode `json:"phase1_negotiation_mode,omitempty"`
+
+ // The IKE version.
+ // A valid value is v1 or v2.
+ // Default is v1.
+ IKEVersion IKEVersion `json:"ike_version,omitempty"`
+
+ //Lifetime is the lifetime of the security association
+ Lifetime *LifetimeCreateOpts `json:"lifetime,omitempty"`
+}
+
+// The lifetime consists of a unit and integer value
+// You can omit either the unit or value portion of the lifetime
+type LifetimeCreateOpts struct {
+ // Units is the units for the lifetime of the security association
+ // Default unit is seconds
+ Units Unit `json:"units,omitempty"`
+
+ // The lifetime value.
+ // Must be a positive integer.
+ // Default value is 3600.
+ Value int `json:"value,omitempty"`
+}
+
+// ToPolicyCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPolicyCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "ikepolicy")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// IKE policy
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToPolicyCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Get retrieves a particular IKE policy based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// Delete will permanently delete a particular IKE policy based on its
+// unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToPolicyListQuery() (string, error)
+}
+
+// ListOpts allows the filtering of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the IKE policy attributes you want to see returned.
+type ListOpts struct {
+ TenantID string `q:"tenant_id"`
+ Name string `q:"name"`
+ Description string `q:"description"`
+ ProjectID string `q:"project_id"`
+ AuthAlgorithm string `q:"auth_algorithm"`
+ EncapsulationMode string `q:"encapsulation_mode"`
+ EncryptionAlgorithm string `q:"encryption_algorithm"`
+ PFS string `q:"pfs"`
+ Phase1NegotiationMode string `q:"phase_1_negotiation_mode"`
+ IKEVersion string `q:"ike_version"`
+}
+
+// ToPolicyListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPolicyListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// IKE policies. It accepts a ListOpts struct, which allows you to filter
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToPolicyListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return PolicyPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToPolicyUpdateMap() (map[string]interface{}, error)
+}
+
+type LifetimeUpdateOpts struct {
+ Units Unit `json:"units,omitempty"`
+ Value int `json:"value,omitempty"`
+}
+
+// UpdateOpts contains the values used when updating an IKE policy
+type UpdateOpts struct {
+ Description *string `json:"description,omitempty"`
+ Name *string `json:"name,omitempty"`
+ AuthAlgorithm AuthAlgorithm `json:"auth_algorithm,omitempty"`
+ EncryptionAlgorithm EncryptionAlgorithm `json:"encryption_algorithm,omitempty"`
+ PFS PFS `json:"pfs,omitempty"`
+ Lifetime *LifetimeUpdateOpts `json:"lifetime,omitempty"`
+ Phase1NegotiationMode Phase1NegotiationMode `json:"phase_1_negotiation_mode,omitempty"`
+ IKEVersion IKEVersion `json:"ike_version,omitempty"`
+}
+
+// ToPolicyUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPolicyUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "ikepolicy")
+}
+
+// Update allows IKE policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToPolicyUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go
new file mode 100644
index 0000000000..b825f5754f
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go
@@ -0,0 +1,125 @@
+package ikepolicies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// Policy is an IKE Policy
+type Policy struct {
+ // TenantID is the ID of the project
+ TenantID string `json:"tenant_id"`
+
+ // ProjectID is the ID of the project
+ ProjectID string `json:"project_id"`
+
+ // Description is the human readable description of the policy
+ Description string `json:"description"`
+
+ // Name is the human readable name of the policy
+ Name string `json:"name"`
+
+ // AuthAlgorithm is the authentication hash algorithm
+ AuthAlgorithm string `json:"auth_algorithm"`
+
+ // EncryptionAlgorithm is the encryption algorithm
+ EncryptionAlgorithm string `json:"encryption_algorithm"`
+
+ // PFS is the Perfect forward secrecy (PFS) mode
+ PFS string `json:"pfs"`
+
+ // Lifetime is the lifetime of the security association
+ Lifetime Lifetime `json:"lifetime"`
+
+ // ID is the ID of the policy
+ ID string `json:"id"`
+
+ // Phase1NegotiationMode is the IKE mode
+ Phase1NegotiationMode string `json:"phase1_negotiation_mode"`
+
+ // IKEVersion is the IKE version.
+ IKEVersion string `json:"ike_version"`
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+type Lifetime struct {
+ // Units is the unit for the lifetime
+ // Default is seconds
+ Units string `json:"units"`
+
+ // Value is the lifetime
+ // Default is 3600
+ Value int `json:"value"`
+}
+
+// Extract is a function that accepts a result and extracts an IKE Policy.
+func (r commonResult) Extract() (*Policy, error) {
+ var s struct {
+ Policy *Policy `json:"ikepolicy"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Policy, err
+}
+
+// PolicyPage is the page returned by a pager when traversing over a
+// collection of Policies.
+type PolicyPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of IKE policies 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 (r PolicyPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"ikepolicies_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PolicyPage struct is empty.
+func (r PolicyPage) IsEmpty() (bool, error) {
+ is, err := ExtractPolicies(r)
+ return len(is) == 0, err
+}
+
+// ExtractPolicies accepts a Page struct, specifically a Policy struct,
+// and extracts the elements into a slice of Policy structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPolicies(r pagination.Page) ([]Policy, error) {
+ var s struct {
+ Policies []Policy `json:"ikepolicies"`
+ }
+ err := (r.(PolicyPage)).ExtractInto(&s)
+ return s.Policies, err
+}
+
+// CreateResult represents the result of a Create operation. Call its Extract method to
+// interpret it as a Policy.
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a Get operation. Call its Extract method to
+// interpret it as a Policy.
+type GetResult struct {
+ commonResult
+}
+
+// DeleteResult represents the results of a Delete operation. Call its ExtractErr method
+// to determine whether the operation succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract method
+// to interpret it as a Policy.
+type UpdateResult struct {
+ commonResult
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go
new file mode 100644
index 0000000000..b3ec548da7
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go
@@ -0,0 +1,304 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ikepolicies"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ikepolicies", 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, `
+{
+ "ikepolicy":{
+ "name": "policy",
+ "description": "IKE policy",
+ "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+ "ike_version": "v2"
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "ikepolicy":{
+ "name": "policy",
+ "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+ "project_id": "9145d91459d248b1b02fdaca97c6a75d",
+ "id": "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+ "description": "IKE policy",
+ "auth_algorithm": "sha1",
+ "encryption_algorithm": "aes-128",
+ "pfs": "Group5",
+ "lifetime": {
+ "value": 3600,
+ "units": "seconds"
+ },
+ "phase1_negotiation_mode": "main",
+ "ike_version": "v2"
+ }
+}
+ `)
+ })
+
+ options := ikepolicies.CreateOpts{
+ TenantID: "9145d91459d248b1b02fdaca97c6a75d",
+ Name: "policy",
+ Description: "IKE policy",
+ IKEVersion: ikepolicies.IKEVersionv2,
+ }
+
+ actual, err := ikepolicies.Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+ expectedLifetime := ikepolicies.Lifetime{
+ Units: "seconds",
+ Value: 3600,
+ }
+ expected := ikepolicies.Policy{
+ AuthAlgorithm: "sha1",
+ IKEVersion: "v2",
+ TenantID: "9145d91459d248b1b02fdaca97c6a75d",
+ Phase1NegotiationMode: "main",
+ PFS: "Group5",
+ EncryptionAlgorithm: "aes-128",
+ Description: "IKE policy",
+ Name: "policy",
+ ID: "f2b08c1e-aa81-4668-8ae1-1401bcb0576c",
+ Lifetime: expectedLifetime,
+ ProjectID: "9145d91459d248b1b02fdaca97c6a75d",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ikepolicies/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+{
+ "ikepolicy":{
+ "name": "policy",
+ "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+ "project_id": "9145d91459d248b1b02fdaca97c6a75d",
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": "IKE policy",
+ "auth_algorithm": "sha1",
+ "encryption_algorithm": "aes-128",
+ "pfs": "Group5",
+ "lifetime": {
+ "value": 3600,
+ "units": "seconds"
+ },
+ "phase1_negotiation_mode": "main",
+ "ike_version": "v2"
+ }
+}
+ `)
+ })
+
+ actual, err := ikepolicies.Get(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828").Extract()
+ th.AssertNoErr(t, err)
+ expectedLifetime := ikepolicies.Lifetime{
+ Units: "seconds",
+ Value: 3600,
+ }
+ expected := ikepolicies.Policy{
+ AuthAlgorithm: "sha1",
+ IKEVersion: "v2",
+ TenantID: "9145d91459d248b1b02fdaca97c6a75d",
+ ProjectID: "9145d91459d248b1b02fdaca97c6a75d",
+ Phase1NegotiationMode: "main",
+ PFS: "Group5",
+ EncryptionAlgorithm: "aes-128",
+ Description: "IKE policy",
+ Name: "policy",
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ Lifetime: expectedLifetime,
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ikepolicies/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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 := ikepolicies.Delete(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ikepolicies", 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, `
+ {
+ "ikepolicies": [
+ {
+ "name": "policy",
+ "tenant_id": "9145d91459d248b1b02fdaca97c6a75d",
+ "project_id": "9145d91459d248b1b02fdaca97c6a75d",
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": "IKE policy",
+ "auth_algorithm": "sha1",
+ "encryption_algorithm": "aes-128",
+ "pfs": "Group5",
+ "lifetime": {
+ "value": 3600,
+ "units": "seconds"
+ },
+ "phase1_negotiation_mode": "main",
+ "ike_version": "v2"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ ikepolicies.List(fake.ServiceClient(), ikepolicies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ikepolicies.ExtractPolicies(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+ expectedLifetime := ikepolicies.Lifetime{
+ Units: "seconds",
+ Value: 3600,
+ }
+ expected := []ikepolicies.Policy{
+ {
+ AuthAlgorithm: "sha1",
+ IKEVersion: "v2",
+ TenantID: "9145d91459d248b1b02fdaca97c6a75d",
+ ProjectID: "9145d91459d248b1b02fdaca97c6a75d",
+ Phase1NegotiationMode: "main",
+ PFS: "Group5",
+ EncryptionAlgorithm: "aes-128",
+ Description: "IKE policy",
+ Name: "policy",
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ Lifetime: expectedLifetime,
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ikepolicies/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+ {
+ "ikepolicy":{
+ "name": "updatedname",
+ "description": "updated policy",
+ "lifetime": {
+ "value": 7000
+ }
+ }
+ }
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "ikepolicy": {
+ "name": "updatedname",
+ "transform_protocol": "esp",
+ "auth_algorithm": "sha1",
+ "encapsulation_mode": "tunnel",
+ "encryption_algorithm": "aes-128",
+ "pfs": "group5",
+ "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "project_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "lifetime": {
+ "units": "seconds",
+ "value": 7000
+ },
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": "updated policy"
+ }
+}
+`)
+ })
+
+ updatedName := "updatedname"
+ updatedDescription := "updated policy"
+ options := ikepolicies.UpdateOpts{
+ Name: &updatedName,
+ Description: &updatedDescription,
+ Lifetime: &ikepolicies.LifetimeUpdateOpts{
+ Value: 7000,
+ },
+ }
+
+ actual, err := ikepolicies.Update(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828", options).Extract()
+ th.AssertNoErr(t, err)
+ expectedLifetime := ikepolicies.Lifetime{
+ Units: "seconds",
+ Value: 7000,
+ }
+ expected := ikepolicies.Policy{
+ TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ ProjectID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ Name: "updatedname",
+ AuthAlgorithm: "sha1",
+ EncryptionAlgorithm: "aes-128",
+ PFS: "group5",
+ Description: "updated policy",
+ Lifetime: expectedLifetime,
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/urls.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/urls.go
new file mode 100644
index 0000000000..a364a881e6
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/urls.go
@@ -0,0 +1,16 @@
+package ikepolicies
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "vpn"
+ resourcePath = "ikepolicies"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go
new file mode 100644
index 0000000000..91d5451a6e
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go
@@ -0,0 +1,56 @@
+/*
+Package ipsecpolicies allows management and retrieval of IPSec Policies in the
+OpenStack Networking Service.
+
+Example to Create a Policy
+
+ createOpts := ipsecpolicies.CreateOpts{
+ Name: "IPSecPolicy_1",
+ }
+
+ policy, err := policies.Create(networkClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Policy
+
+ err := ipsecpolicies.Delete(client, "5291b189-fd84-46e5-84bd-78f40c05d69c").ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Show the details of a specific IPSec policy by ID
+
+ policy, err := ipsecpolicies.Get(client, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update an IPSec policy
+
+ name := "updatedname"
+ description := "updated policy"
+ updateOpts := ipsecpolicies.UpdateOpts{
+ Name: &name,
+ Description: &description,
+ }
+ updatedPolicy, err := ipsecpolicies.Update(client, "5c561d9d-eaea-45f6-ae3e-08d1a7080828", updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to List IPSec policies
+
+ allPages, err := ipsecpolicies.List(client, nil).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allPolicies, err := ipsecpolicies.ExtractPolicies(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+*/
+package ipsecpolicies
diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go
new file mode 100644
index 0000000000..9496365ca4
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go
@@ -0,0 +1,211 @@
+package ipsecpolicies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+type TransformProtocol string
+type AuthAlgorithm string
+type EncapsulationMode string
+type EncryptionAlgorithm string
+type PFS string
+type Unit string
+
+const (
+ TransformProtocolESP TransformProtocol = "esp"
+ TransformProtocolAH TransformProtocol = "ah"
+ TransformProtocolAHESP TransformProtocol = "ah-esp"
+ AuthAlgorithmSHA1 AuthAlgorithm = "sha1"
+ AuthAlgorithmSHA256 AuthAlgorithm = "sha256"
+ AuthAlgorithmSHA384 AuthAlgorithm = "sha384"
+ AuthAlgorithmSHA512 AuthAlgorithm = "sha512"
+ EncryptionAlgorithm3DES EncryptionAlgorithm = "3des"
+ EncryptionAlgorithmAES128 EncryptionAlgorithm = "aes-128"
+ EncryptionAlgorithmAES256 EncryptionAlgorithm = "aes-256"
+ EncryptionAlgorithmAES192 EncryptionAlgorithm = "aes-192"
+ EncapsulationModeTunnel EncapsulationMode = "tunnel"
+ EncapsulationModeTransport EncapsulationMode = "transport"
+ UnitSeconds Unit = "seconds"
+ UnitKilobytes Unit = "kilobytes"
+ PFSGroup2 PFS = "group2"
+ PFSGroup5 PFS = "group5"
+ PFSGroup14 PFS = "group14"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToPolicyCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new IPSec policy
+type CreateOpts struct {
+ // TenantID specifies a tenant to own the IPSec policy. The caller must have
+ // an admin role in order to set this. Otherwise, this field is left unset
+ // and the caller will be the owner.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // Description is the human readable description of the policy.
+ Description string `json:"description,omitempty"`
+
+ // Name is the human readable name of the policy.
+ // Does not have to be unique.
+ Name string `json:"name,omitempty"`
+
+ // AuthAlgorithm is the authentication hash algorithm.
+ // Valid values are sha1, sha256, sha384, sha512.
+ // The default is sha1.
+ AuthAlgorithm AuthAlgorithm `json:"auth_algorithm,omitempty"`
+
+ // EncapsulationMode is the encapsulation mode.
+ // A valid value is tunnel or transport.
+ // Default is tunnel.
+ EncapsulationMode EncapsulationMode `json:"encapsulation_mode,omitempty"`
+
+ // EncryptionAlgorithm is the encryption algorithm.
+ // A valid value is 3des, aes-128, aes-192, aes-256, and so on.
+ // Default is aes-128.
+ EncryptionAlgorithm EncryptionAlgorithm `json:"encryption_algorithm,omitempty"`
+
+ // PFS is the Perfect forward secrecy mode.
+ // A valid value is Group2, Group5, Group14, and so on.
+ // Default is Group5.
+ PFS PFS `json:"pfs,omitempty"`
+
+ // TransformProtocol is the transform protocol.
+ // A valid value is ESP, AH, or AH- ESP.
+ // Default is ESP.
+ TransformProtocol TransformProtocol `json:"transform_protocol,omitempty"`
+
+ //Lifetime is the lifetime of the security association
+ Lifetime *LifetimeCreateOpts `json:"lifetime,omitempty"`
+}
+
+// The lifetime consists of a unit and integer value
+// You can omit either the unit or value portion of the lifetime
+type LifetimeCreateOpts struct {
+ // Units is the units for the lifetime of the security association
+ // Default unit is seconds
+ Units Unit `json:"units,omitempty"`
+
+ // The lifetime value.
+ // Must be a positive integer.
+ // Default value is 3600.
+ Value int `json:"value,omitempty"`
+}
+
+// ToPolicyCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToPolicyCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "ipsecpolicy")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// IPSec policy
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToPolicyCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Delete will permanently delete a particular IPSec policy based on its
+// unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
+
+// Get retrieves a particular IPSec policy based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToPolicyListQuery() (string, error)
+}
+
+// ListOpts allows the filtering of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the IPSec policy attributes you want to see returned.
+type ListOpts struct {
+ TenantID string `q:"tenant_id"`
+ Name string `q:"name"`
+ Description string `q:"description"`
+ ProjectID string `q:"project_id"`
+ AuthAlgorithm string `q:"auth_algorithm"`
+ EncapsulationMode string `q:"encapsulation_mode"`
+ EncryptionAlgorithm string `q:"encryption_algorithm"`
+ PFS string `q:"pfs"`
+ TransformProtocol string `q:"transform_protocol"`
+}
+
+// ToPolicyListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPolicyListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// IPSec policies. It accepts a ListOpts struct, which allows you to filter
+// the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToPolicyListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return PolicyPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToPolicyUpdateMap() (map[string]interface{}, error)
+}
+
+type LifetimeUpdateOpts struct {
+ Units Unit `json:"units,omitempty"`
+ Value int `json:"value,omitempty"`
+}
+
+// UpdateOpts contains the values used when updating an IPSec policy
+type UpdateOpts struct {
+ Description *string `json:"description,omitempty"`
+ Name *string `json:"name,omitempty"`
+ AuthAlgorithm AuthAlgorithm `json:"auth_algorithm,omitempty"`
+ EncapsulationMode EncapsulationMode `json:"encapsulation_mode,omitempty"`
+ EncryptionAlgorithm EncryptionAlgorithm `json:"encryption_algorithm,omitempty"`
+ PFS PFS `json:"pfs,omitempty"`
+ TransformProtocol TransformProtocol `json:"transform_protocol,omitempty"`
+ Lifetime *LifetimeUpdateOpts `json:"lifetime,omitempty"`
+}
+
+// ToPolicyUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToPolicyUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "ipsecpolicy")
+}
+
+// Update allows IPSec policies to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToPolicyUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go
new file mode 100644
index 0000000000..eda4a1bd23
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go
@@ -0,0 +1,126 @@
+package ipsecpolicies
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// Policy is an IPSec Policy
+type Policy struct {
+ // TenantID is the ID of the project
+ TenantID string `json:"tenant_id"`
+
+ // ProjectID is the ID of the project
+ ProjectID string `json:"project_id"`
+
+ // Description is the human readable description of the policy
+ Description string `json:"description"`
+
+ // Name is the human readable name of the policy
+ Name string `json:"name"`
+
+ // AuthAlgorithm is the authentication hash algorithm
+ AuthAlgorithm string `json:"auth_algorithm"`
+
+ // EncapsulationMode is the encapsulation mode
+ EncapsulationMode string `json:"encapsulation_mode"`
+
+ // EncryptionAlgorithm is the encryption algorithm
+ EncryptionAlgorithm string `json:"encryption_algorithm"`
+
+ // PFS is the Perfect forward secrecy (PFS) mode
+ PFS string `json:"pfs"`
+
+ // TransformProtocol is the transform protocol
+ TransformProtocol string `json:"transform_protocol"`
+
+ // Lifetime is the lifetime of the security association
+ Lifetime Lifetime `json:"lifetime"`
+
+ // ID is the ID of the policy
+ ID string `json:"id"`
+}
+
+type Lifetime struct {
+ // Units is the unit for the lifetime
+ // Default is seconds
+ Units string `json:"units"`
+
+ // Value is the lifetime
+ // Default is 3600
+ Value int `json:"value"`
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// Extract is a function that accepts a result and extracts an IPSec Policy.
+func (r commonResult) Extract() (*Policy, error) {
+ var s struct {
+ Policy *Policy `json:"ipsecpolicy"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Policy, err
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a Policy.
+type CreateResult struct {
+ commonResult
+}
+
+// CreateResult represents the result of a delete operation. Call its ExtractErr method
+// to determine if the operation succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a Policy.
+type GetResult struct {
+ commonResult
+}
+
+// PolicyPage is the page returned by a pager when traversing over a
+// collection of Policies.
+type PolicyPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of IPSec policies 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 (r PolicyPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"ipsecpolicies_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a PolicyPage struct is empty.
+func (r PolicyPage) IsEmpty() (bool, error) {
+ is, err := ExtractPolicies(r)
+ return len(is) == 0, err
+}
+
+// ExtractPolicies accepts a Page struct, specifically a Policy struct,
+// and extracts the elements into a slice of Policy structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractPolicies(r pagination.Page) ([]Policy, error) {
+ var s struct {
+ Policies []Policy `json:"ipsecpolicies"`
+ }
+ err := (r.(PolicyPage)).ExtractInto(&s)
+ return s.Policies, err
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a Policy.
+type UpdateResult struct {
+ commonResult
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go
new file mode 100644
index 0000000000..702bd38355
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go
@@ -0,0 +1,321 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsecpolicies", 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, `
+{
+ "ipsecpolicy": {
+ "name": "ipsecpolicy1",
+ "transform_protocol": "esp",
+ "auth_algorithm": "sha1",
+ "encapsulation_mode": "tunnel",
+ "encryption_algorithm": "aes-128",
+ "pfs": "group5",
+ "lifetime": {
+ "units": "seconds",
+ "value": 7200
+ },
+ "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b"
+}
+} `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "ipsecpolicy": {
+ "name": "ipsecpolicy1",
+ "transform_protocol": "esp",
+ "auth_algorithm": "sha1",
+ "encapsulation_mode": "tunnel",
+ "encryption_algorithm": "aes-128",
+ "pfs": "group5",
+ "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "project_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "lifetime": {
+ "units": "seconds",
+ "value": 7200
+ },
+ "id": "5291b189-fd84-46e5-84bd-78f40c05d69c",
+ "description": ""
+ }
+}
+ `)
+ })
+
+ lifetime := ipsecpolicies.LifetimeCreateOpts{
+ Units: ipsecpolicies.UnitSeconds,
+ Value: 7200,
+ }
+ options := ipsecpolicies.CreateOpts{
+ TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ Name: "ipsecpolicy1",
+ TransformProtocol: ipsecpolicies.TransformProtocolESP,
+ AuthAlgorithm: ipsecpolicies.AuthAlgorithmSHA1,
+ EncapsulationMode: ipsecpolicies.EncapsulationModeTunnel,
+ EncryptionAlgorithm: ipsecpolicies.EncryptionAlgorithmAES128,
+ PFS: ipsecpolicies.PFSGroup5,
+ Lifetime: &lifetime,
+ Description: "",
+ }
+ actual, err := ipsecpolicies.Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+ expectedLifetime := ipsecpolicies.Lifetime{
+ Units: "seconds",
+ Value: 7200,
+ }
+ expected := ipsecpolicies.Policy{
+ TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ Name: "ipsecpolicy1",
+ TransformProtocol: "esp",
+ AuthAlgorithm: "sha1",
+ EncapsulationMode: "tunnel",
+ EncryptionAlgorithm: "aes-128",
+ PFS: "group5",
+ Description: "",
+ Lifetime: expectedLifetime,
+ ID: "5291b189-fd84-46e5-84bd-78f40c05d69c",
+ ProjectID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsecpolicies/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+{
+ "ipsecpolicy": {
+ "name": "ipsecpolicy1",
+ "transform_protocol": "esp",
+ "auth_algorithm": "sha1",
+ "encapsulation_mode": "tunnel",
+ "encryption_algorithm": "aes-128",
+ "pfs": "group5",
+ "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "project_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "lifetime": {
+ "units": "seconds",
+ "value": 7200
+ },
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": ""
+ }
+}
+ `)
+ })
+
+ actual, err := ipsecpolicies.Get(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828").Extract()
+ th.AssertNoErr(t, err)
+ expectedLifetime := ipsecpolicies.Lifetime{
+ Units: "seconds",
+ Value: 7200,
+ }
+ expected := ipsecpolicies.Policy{
+ Name: "ipsecpolicy1",
+ TransformProtocol: "esp",
+ Description: "",
+ AuthAlgorithm: "sha1",
+ EncapsulationMode: "tunnel",
+ EncryptionAlgorithm: "aes-128",
+ PFS: "group5",
+ Lifetime: expectedLifetime,
+ TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ ProjectID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsecpolicies/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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 := ipsecpolicies.Delete(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsecpolicies", 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, `
+ {
+ "ipsecpolicies": [
+ {
+ "name": "ipsecpolicy1",
+ "transform_protocol": "esp",
+ "auth_algorithm": "sha1",
+ "encapsulation_mode": "tunnel",
+ "encryption_algorithm": "aes-128",
+ "pfs": "group5",
+ "project_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "lifetime": {
+ "units": "seconds",
+ "value": 7200
+ },
+ "id": "5291b189-fd84-46e5-84bd-78f40c05d69c",
+ "description": ""
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ ipsecpolicies.List(fake.ServiceClient(), ipsecpolicies.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ipsecpolicies.ExtractPolicies(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+
+ expected := []ipsecpolicies.Policy{
+ {
+ Name: "ipsecpolicy1",
+ TransformProtocol: "esp",
+ TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ ProjectID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ AuthAlgorithm: "sha1",
+ EncapsulationMode: "tunnel",
+ EncryptionAlgorithm: "aes-128",
+ PFS: "group5",
+ Lifetime: ipsecpolicies.Lifetime{
+ Value: 7200,
+ Units: "seconds",
+ },
+ Description: "",
+ ID: "5291b189-fd84-46e5-84bd-78f40c05d69c",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsecpolicies/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+ {
+ "ipsecpolicy":{
+ "name": "updatedname",
+ "description": "updated policy",
+ "lifetime": {
+ "value": 7000
+ }
+ }
+ }
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+
+ {
+ "ipsecpolicy": {
+ "name": "updatedname",
+ "transform_protocol": "esp",
+ "auth_algorithm": "sha1",
+ "encapsulation_mode": "tunnel",
+ "encryption_algorithm": "aes-128",
+ "pfs": "group5",
+ "project_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b",
+ "lifetime": {
+ "units": "seconds",
+ "value": 7000
+ },
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": "updated policy"
+ }
+}
+`)
+ })
+ updatedName := "updatedname"
+ updatedDescription := "updated policy"
+ options := ipsecpolicies.UpdateOpts{
+ Name: &updatedName,
+ Description: &updatedDescription,
+ Lifetime: &ipsecpolicies.LifetimeUpdateOpts{
+ Value: 7000,
+ },
+ }
+
+ actual, err := ipsecpolicies.Update(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828", options).Extract()
+ th.AssertNoErr(t, err)
+ expectedLifetime := ipsecpolicies.Lifetime{
+ Units: "seconds",
+ Value: 7000,
+ }
+ expected := ipsecpolicies.Policy{
+ TenantID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ ProjectID: "b4eedccc6fb74fa8a7ad6b08382b852b",
+ Name: "updatedname",
+ TransformProtocol: "esp",
+ AuthAlgorithm: "sha1",
+ EncapsulationMode: "tunnel",
+ EncryptionAlgorithm: "aes-128",
+ PFS: "group5",
+ Description: "updated policy",
+ Lifetime: expectedLifetime,
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/urls.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/urls.go
new file mode 100644
index 0000000000..8781cc4499
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/urls.go
@@ -0,0 +1,16 @@
+package ipsecpolicies
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "vpn"
+ resourcePath = "ipsecpolicies"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/services/doc.go b/openstack/networking/v2/extensions/vpnaas/services/doc.go
new file mode 100644
index 0000000000..6bd3236c84
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/services/doc.go
@@ -0,0 +1,68 @@
+/*
+Package services allows management and retrieval of VPN services in the
+OpenStack Networking Service.
+
+Example to List Services
+
+ listOpts := services.ListOpts{
+ TenantID: "966b3c7d36a24facaf20b7e458bf2192",
+ }
+
+ allPages, err := services.List(networkClient, listOpts).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allPolicies, err := services.ExtractServices(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+ for _, service := range allServices {
+ fmt.Printf("%+v\n", service)
+ }
+
+Example to Create a Service
+
+ createOpts := services.CreateOpts{
+ Name: "vpnservice1",
+ Description: "A service",
+ RouterID: "2512e759-e8d7-4eea-a0af-4a85927a2e59",
+ AdminStateUp: gophercloud.Enabled,
+ }
+
+ service, err := services.Create(networkClient, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update a Service
+
+ serviceID := "38aee955-6283-4279-b091-8b9c828000ec"
+
+ updateOpts := services.UpdateOpts{
+ Description: "New Description",
+ }
+
+ service, err := services.Update(networkClient, serviceID, updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a Service
+
+ serviceID := "38aee955-6283-4279-b091-8b9c828000ec"
+ err := services.Delete(networkClient, serviceID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Show the details of a specific Service by ID
+
+ service, err := services.Get(client, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+*/
+package services
diff --git a/openstack/networking/v2/extensions/vpnaas/services/requests.go b/openstack/networking/v2/extensions/vpnaas/services/requests.go
new file mode 100644
index 0000000000..8d642197e0
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/services/requests.go
@@ -0,0 +1,150 @@
+package services
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToServiceCreateMap() (map[string]interface{}, error)
+}
+
+// CreateOpts contains all the values needed to create a new VPN service
+type CreateOpts struct {
+ // TenantID specifies a tenant to own the VPN service. The caller must have
+ // an admin role in order to set this. Otherwise, this field is left unset
+ // and the caller will be the owner.
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // SubnetID is the ID of the subnet.
+ SubnetID string `json:"subnet_id,omitempty"`
+
+ // RouterID is the ID of the router.
+ RouterID string `json:"router_id" required:"true"`
+
+ // Description is the human readable description of the service.
+ Description string `json:"description,omitempty"`
+
+ // AdminStateUp is the administrative state of the resource, which is up (true) or down (false).
+ AdminStateUp *bool `json:"admin_state_up"`
+
+ // Name is the human readable name of the service.
+ Name string `json:"name,omitempty"`
+
+ // The ID of the flavor.
+ FlavorID string `json:"flavor_id,omitempty"`
+}
+
+// ToServiceCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToServiceCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "vpnservice")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// VPN service.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToServiceCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+ return
+}
+
+// Delete will permanently delete a particular VPN service based on its
+// unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToServiceUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating a VPN service
+type UpdateOpts struct {
+ // Name is the human readable name of the service.
+ Name *string `json:"name,omitempty"`
+
+ // Description is the human readable description of the service.
+ Description *string `json:"description,omitempty"`
+
+ // AdminStateUp is the administrative state of the resource, which is up (true) or down (false).
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToServiceUpdateMap casts aa UodateOpts struct to a map.
+func (opts UpdateOpts) ToServiceUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "vpnservice")
+}
+
+// Update allows VPN services to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToServiceUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToServiceListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the VPN service attributes you want to see returned.
+type ListOpts struct {
+ TenantID string `q:"tenant_id"`
+ Name string `q:"name"`
+ Description string `q:"description"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ Status string `q:"status"`
+ SubnetID string `q:"subnet_id"`
+ RouterID string `q:"router_id"`
+ ProjectID string `q:"project_id"`
+ ExternalV6IP string `q:"external_v6_ip"`
+ ExternalV4IP string `q:"external_v4_ip"`
+ FlavorID string `q:"flavor_id"`
+}
+
+// ToServiceListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToServiceListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// VPN services. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToServiceListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return ServicePage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// Get retrieves a particular VPN service based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/services/results.go b/openstack/networking/v2/extensions/vpnaas/services/results.go
new file mode 100644
index 0000000000..5e555699fc
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/services/results.go
@@ -0,0 +1,121 @@
+package services
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// Service is a VPN Service
+type Service struct {
+ // TenantID is the ID of the project.
+ TenantID string `json:"tenant_id"`
+
+ // ProjectID is the ID of the project.
+ ProjectID string `json:"project_id"`
+
+ // SubnetID is the ID of the subnet.
+ SubnetID string `json:"subnet_id"`
+
+ // RouterID is the ID of the router.
+ RouterID string `json:"router_id"`
+
+ // Description is a human-readable description for the resource.
+ // Default is an empty string
+ Description string `json:"description"`
+
+ // AdminStateUp is the administrative state of the resource, which is up (true) or down (false).
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // Name is the human readable name of the service.
+ Name string `json:"name"`
+
+ // Status indicates whether IPsec VPN service is currently operational.
+ // Values are ACTIVE, DOWN, BUILD, ERROR, PENDING_CREATE, PENDING_UPDATE, or PENDING_DELETE.
+ Status string `json:"status"`
+
+ // ID is the unique ID of the VPN service.
+ ID string `json:"id"`
+
+ // ExternalV6IP is the read-only external (public) IPv6 address that is used for the VPN service.
+ ExternalV6IP string `json:"external_v6_ip"`
+
+ // ExternalV4IP is the read-only external (public) IPv4 address that is used for the VPN service.
+ ExternalV4IP string `json:"external_v4_ip"`
+
+ // FlavorID is the ID of the flavor.
+ FlavorID string `json:"flavor_id"`
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// ServicePage is the page returned by a pager when traversing over a
+// collection of VPN services.
+type ServicePage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of VPN services 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 (r ServicePage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"vpnservices_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a ServicePage struct is empty.
+func (r ServicePage) IsEmpty() (bool, error) {
+ is, err := ExtractServices(r)
+ return len(is) == 0, err
+}
+
+// ExtractServices accepts a Page struct, specifically a Service struct,
+// and extracts the elements into a slice of Service structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractServices(r pagination.Page) ([]Service, error) {
+ var s struct {
+ Services []Service `json:"vpnservices"`
+ }
+ err := (r.(ServicePage)).ExtractInto(&s)
+ return s.Services, err
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a Service.
+type GetResult struct {
+ commonResult
+}
+
+// Extract is a function that accepts a result and extracts a VPN service.
+func (r commonResult) Extract() (*Service, error) {
+ var s struct {
+ Service *Service `json:"vpnservice"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Service, err
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a Service.
+type CreateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation. Call its
+// ExtractErr method to determine if the operation succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a service.
+type UpdateResult struct {
+ commonResult
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go
new file mode 100644
index 0000000000..ca7adf327c
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go
@@ -0,0 +1,267 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/services"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/vpnservices", 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, `
+{
+ "vpnservice": {
+ "router_id": "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ "name": "vpn",
+ "admin_state_up": true,
+ "description": "OpenStack VPN service",
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578"
+ }
+} `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "vpnservice": {
+ "router_id": "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ "status": "PENDING_CREATE",
+ "name": "vpn",
+ "external_v6_ip": "2001:db8::1",
+ "admin_state_up": true,
+ "subnet_id": null,
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "external_v4_ip": "172.32.1.11",
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": "OpenStack VPN service",
+ "project_id": "10039663455a446d8ba2cbb058b0f578"
+ }
+}
+ `)
+ })
+
+ options := services.CreateOpts{
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ Name: "vpn",
+ Description: "OpenStack VPN service",
+ AdminStateUp: gophercloud.Enabled,
+ RouterID: "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ }
+ actual, err := services.Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+ expected := services.Service{
+ RouterID: "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ Status: "PENDING_CREATE",
+ Name: "vpn",
+ ExternalV6IP: "2001:db8::1",
+ AdminStateUp: true,
+ SubnetID: "",
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ ExternalV4IP: "172.32.1.11",
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ Description: "OpenStack VPN service",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/vpnservices", 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, `
+{
+ "vpnservices":[
+ {
+ "router_id": "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ "status": "PENDING_CREATE",
+ "name": "vpnservice1",
+ "admin_state_up": true,
+ "subnet_id": null,
+ "project_id": "10039663455a446d8ba2cbb058b0f578",
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "description": "Test VPN service"
+ }
+ ]
+}
+ `)
+ })
+
+ count := 0
+
+ services.List(fake.ServiceClient(), services.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := services.ExtractServices(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+
+ expected := []services.Service{
+ {
+ Status: "PENDING_CREATE",
+ Name: "vpnservice1",
+ AdminStateUp: true,
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ Description: "Test VPN service",
+ SubnetID: "",
+ RouterID: "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ },
+ }
+
+ 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/vpn/vpnservices/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+{
+ "vpnservice": {
+ "router_id": "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ "status": "PENDING_CREATE",
+ "name": "vpnservice1",
+ "admin_state_up": true,
+ "subnet_id": null,
+ "project_id": "10039663455a446d8ba2cbb058b0f578",
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": "VPN test service"
+ }
+}
+ `)
+ })
+
+ actual, err := services.Get(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828").Extract()
+ th.AssertNoErr(t, err)
+ expected := services.Service{
+ Status: "PENDING_CREATE",
+ Name: "vpnservice1",
+ Description: "VPN test service",
+ AdminStateUp: true,
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ RouterID: "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ SubnetID: "",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/vpnservices/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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 := services.Delete(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/vpnservices/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+{
+ "vpnservice":{
+ "name": "updatedname",
+ "description": "updated service",
+ "admin_state_up": false
+ }
+}
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+{
+ "vpnservice": {
+ "router_id": "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ "status": "PENDING_CREATE",
+ "name": "updatedname",
+ "admin_state_up": false,
+ "subnet_id": null,
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "project_id": "10039663455a446d8ba2cbb058b0f578",
+ "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "description": "updated service",
+ "external_v4_ip": "172.32.1.11",
+ "external_v6_ip": "2001:db8::1"
+ }
+}
+ `)
+ })
+ updatedName := "updatedname"
+ updatedServiceDescription := "updated service"
+ options := services.UpdateOpts{
+ Name: &updatedName,
+ Description: &updatedServiceDescription,
+ AdminStateUp: gophercloud.Disabled,
+ }
+
+ actual, err := services.Update(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828", options).Extract()
+ th.AssertNoErr(t, err)
+ expected := services.Service{
+ RouterID: "66e3b16c-8ce5-40fb-bb49-ab6d8dc3f2aa",
+ Status: "PENDING_CREATE",
+ Name: "updatedname",
+ ExternalV6IP: "2001:db8::1",
+ AdminStateUp: false,
+ SubnetID: "",
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ ExternalV4IP: "172.32.1.11",
+ ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ Description: "updated service",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/services/urls.go b/openstack/networking/v2/extensions/vpnaas/services/urls.go
new file mode 100644
index 0000000000..fe8b343fe3
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/services/urls.go
@@ -0,0 +1,16 @@
+package services
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "vpn"
+ resourcePath = "vpnservices"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go
new file mode 100644
index 0000000000..66befd3ba2
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go
@@ -0,0 +1,68 @@
+/*
+Package siteconnections allows management and retrieval of IPSec site connections in the
+OpenStack Networking Service.
+
+
+Example to create an IPSec site connection
+
+createOpts := siteconnections.CreateOpts{
+ Name: "Connection1",
+ PSK: "secret",
+ Initiator: siteconnections.InitiatorBiDirectional,
+ AdminStateUp: gophercloud.Enabled,
+ IPSecPolicyID: "4ab0a72e-64ef-4809-be43-c3f7e0e5239b",
+ PeerEPGroupID: "5f5801b1-b383-4cf0-bf61-9e85d4044b2d",
+ IKEPolicyID: "47a880f9-1da9-468c-b289-219c9eca78f0",
+ VPNServiceID: "692c1ec8-a7cd-44d9-972b-8ed3fe4cc476",
+ LocalEPGroupID: "498bb96a-1517-47ea-b1eb-c4a53db46a16",
+ PeerAddress: "172.24.4.233",
+ PeerID: "172.24.4.233",
+ MTU: 1500,
+ }
+ connection, err := siteconnections.Create(client, createOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Show the details of a specific IPSec site connection by ID
+
+ conn, err := siteconnections.Get(client, "f2b08c1e-aa81-4668-8ae1-1401bcb0576c").Extract()
+ if err != nil {
+ panic(err)
+ }
+
+Example to Delete a site connection
+
+ connID := "38aee955-6283-4279-b091-8b9c828000ec"
+ err := siteconnections.Delete(networkClient, connID).ExtractErr()
+ if err != nil {
+ panic(err)
+ }
+
+Example to List site connections
+
+ allPages, err := siteconnections.List(client, nil).AllPages()
+ if err != nil {
+ panic(err)
+ }
+
+ allConnections, err := siteconnections.ExtractConnections(allPages)
+ if err != nil {
+ panic(err)
+ }
+
+Example to Update an IPSec site connection
+
+ description := "updated connection"
+ name := "updatedname"
+ updateOpts := siteconnections.UpdateOpts{
+ Name: &name,
+ Description: &description,
+ }
+ updatedConnection, err := siteconnections.Update(client, "5c561d9d-eaea-45f6-ae3e-08d1a7080828", updateOpts).Extract()
+ if err != nil {
+ panic(err)
+ }
+
+*/
+package siteconnections
diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go
new file mode 100644
index 0000000000..15ce54b5fb
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go
@@ -0,0 +1,243 @@
+package siteconnections
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+ ToConnectionCreateMap() (map[string]interface{}, error)
+}
+type Action string
+type Initiator string
+
+const (
+ ActionHold Action = "hold"
+ ActionClear Action = "clear"
+ ActionRestart Action = "restart"
+ ActionDisabled Action = "disabled"
+ ActionRestartByPeer Action = "restart-by-peer"
+ InitiatorBiDirectional Initiator = "bi-directional"
+ InitiatorResponseOnly Initiator = "response-only"
+)
+
+// DPDCreateOpts contains all the values needed to create a valid configuration for Dead Peer detection protocols
+type DPDCreateOpts struct {
+ // The dead peer detection (DPD) action.
+ // A valid value is clear, hold, restart, disabled, or restart-by-peer.
+ // Default value is hold.
+ Action Action `json:"action,omitempty"`
+
+ // The dead peer detection (DPD) timeout in seconds.
+ // A valid value is a positive integer that is greater than the DPD interval value.
+ // Default is 120.
+ Timeout int `json:"timeout,omitempty"`
+
+ // The dead peer detection (DPD) interval, in seconds.
+ // A valid value is a positive integer.
+ // Default is 30.
+ Interval int `json:"interval,omitempty"`
+}
+
+// CreateOpts contains all the values needed to create a new IPSec site connection
+type CreateOpts struct {
+ // The ID of the IKE policy
+ IKEPolicyID string `json:"ikepolicy_id"`
+
+ // The ID of the VPN Service
+ VPNServiceID string `json:"vpnservice_id"`
+
+ // The ID for the endpoint group that contains private subnets for the local side of the connection.
+ // You must specify this parameter with the peer_ep_group_id parameter unless
+ // in backward- compatible mode where peer_cidrs is provided with a subnet_id for the VPN service.
+ LocalEPGroupID string `json:"local_ep_group_id,omitempty"`
+
+ // The ID of the IPsec policy.
+ IPSecPolicyID string `json:"ipsecpolicy_id"`
+
+ // The peer router identity for authentication.
+ // A valid value is an IPv4 address, IPv6 address, e-mail address, key ID, or FQDN.
+ // Typically, this value matches the peer_address value.
+ PeerID string `json:"peer_id"`
+
+ // The ID of the project
+ TenantID string `json:"tenant_id,omitempty"`
+
+ // The ID for the endpoint group that contains private CIDRs in the form < net_address > / < prefix >
+ // for the peer side of the connection.
+ // You must specify this parameter with the local_ep_group_id parameter unless in backward-compatible mode
+ // where peer_cidrs is provided with a subnet_id for the VPN service.
+ PeerEPGroupID string `json:"peer_ep_group_id,omitempty"`
+
+ // An ID to be used instead of the external IP address for a virtual router used in traffic between instances on different networks in east-west traffic.
+ // Most often, local ID would be domain name, email address, etc.
+ // If this is not configured then the external IP address will be used as the ID.
+ LocalID string `json:"local_id,omitempty"`
+
+ // The human readable name of the connection.
+ // Does not have to be unique.
+ // Default is an empty string
+ Name string `json:"name,omitempty"`
+
+ // The human readable description of the connection.
+ // Does not have to be unique.
+ // Default is an empty string
+ Description string `json:"description,omitempty"`
+
+ // The peer gateway public IPv4 or IPv6 address or FQDN.
+ PeerAddress string `json:"peer_address"`
+
+ // The pre-shared key.
+ // A valid value is any string.
+ PSK string `json:"psk"`
+
+ // Indicates whether this VPN can only respond to connections or both respond to and initiate connections.
+ // A valid value is response-only or bi-directional. Default is bi-directional.
+ Initiator Initiator `json:"initiator,omitempty"`
+
+ // Unique list of valid peer private CIDRs in the form < net_address > / < prefix > .
+ PeerCIDRs []string `json:"peer_cidrs,omitempty"`
+
+ // The administrative state of the resource, which is up (true) or down (false).
+ // Default is false
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+
+ // A dictionary with dead peer detection (DPD) protocol controls.
+ DPD *DPDCreateOpts `json:"dpd,omitempty"`
+
+ // The maximum transmission unit (MTU) value to address fragmentation.
+ // Minimum value is 68 for IPv4, and 1280 for IPv6.
+ MTU int `json:"mtu,omitempty"`
+}
+
+// ToConnectionCreateMap casts a CreateOpts struct to a map.
+func (opts CreateOpts) ToConnectionCreateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "ipsec_site_connection")
+}
+
+// Create accepts a CreateOpts struct and uses the values to create a new
+// IPSec site connection.
+func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+ b, err := opts.ToConnectionCreateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
+
+ return
+}
+
+// Delete will permanently delete a particular IPSec site connection based on its
+// unique ID.
+func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
+ _, r.Err = c.Delete(resourceURL(c, id), nil)
+ return
+}
+
+// Get retrieves a particular IPSec site connection based on its unique ID.
+func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
+ _, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
+ return
+}
+
+// ListOptsBuilder allows extensions to add additional parameters to the
+// List request.
+type ListOptsBuilder interface {
+ ToConnectionListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing in struct field values that map to
+// the IPSec site connection attributes you want to see returned.
+type ListOpts struct {
+ IKEPolicyID string `q:"ikepolicy_id"`
+ VPNServiceID string `q:"vpnservice_id"`
+ LocalEPGroupID string `q:"local_ep_group_id"`
+ IPSecPolicyID string `q:"ipsecpolicy_id"`
+ PeerID string `q:"peer_id"`
+ TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
+ PeerEPGroupID string `q:"peer_ep_group_id"`
+ LocalID string `q:"local_id"`
+ Name string `q:"name"`
+ Description string `q:"description"`
+ PeerAddress string `q:"peer_address"`
+ PSK string `q:"psk"`
+ Initiator Initiator `q:"initiator"`
+ AdminStateUp *bool `q:"admin_state_up"`
+ MTU int `q:"mtu"`
+}
+
+// ToConnectionListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToConnectionListQuery() (string, error) {
+ q, err := gophercloud.BuildQueryString(opts)
+ return q.String(), err
+}
+
+// List returns a Pager which allows you to iterate over a collection of
+// IPSec site connections. It accepts a ListOpts struct, which allows you to filter
+// and sort the returned collection for greater efficiency.
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+ url := rootURL(c)
+ if opts != nil {
+ query, err := opts.ToConnectionListQuery()
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+ url += query
+ }
+ return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
+ return ConnectionPage{pagination.LinkedPageBase{PageResult: r}}
+ })
+}
+
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+ ToConnectionUpdateMap() (map[string]interface{}, error)
+}
+
+// UpdateOpts contains the values used when updating the DPD of an IPSec site connection
+type DPDUpdateOpts struct {
+ Action Action `json:"action,omitempty"`
+ Timeout int `json:"timeout,omitempty"`
+ Interval int `json:"interval,omitempty"`
+}
+
+// UpdateOpts contains the values used when updating an IPSec site connection
+type UpdateOpts struct {
+ Description *string `json:"description,omitempty"`
+ Name *string `json:"name,omitempty"`
+ LocalID string `json:"local_id,omitempty"`
+ PeerAddress string `json:"peer_address,omitempty"`
+ PeerID string `json:"peer_id,omitempty"`
+ PeerCIDRs []string `json:"peer_cidrs,omitempty"`
+ LocalEPGroupID string `json:"local_ep_group_id,omitempty"`
+ PeerEPGroupID string `json:"peer_ep_group_id,omitempty"`
+ MTU int `json:"mtu,omitempty"`
+ Initiator Initiator `json:"initiator,omitempty"`
+ PSK string `json:"psk,omitempty"`
+ DPD *DPDUpdateOpts `json:"dpd,omitempty"`
+ AdminStateUp *bool `json:"admin_state_up,omitempty"`
+}
+
+// ToConnectionUpdateMap casts an UpdateOpts struct to a map.
+func (opts UpdateOpts) ToConnectionUpdateMap() (map[string]interface{}, error) {
+ return gophercloud.BuildRequestBody(opts, "ipsec_site_connection")
+}
+
+// Update allows IPSec site connections to be updated.
+func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
+ b, err := opts.ToConnectionUpdateMap()
+ if err != nil {
+ r.Err = err
+ return
+ }
+ _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
+ OkCodes: []int{200},
+ })
+ return
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go
new file mode 100644
index 0000000000..3c09e4d074
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go
@@ -0,0 +1,163 @@
+package siteconnections
+
+import (
+ "github.com/gophercloud/gophercloud"
+ "github.com/gophercloud/gophercloud/pagination"
+)
+
+type DPD struct {
+ // Action is the dead peer detection (DPD) action.
+ Action string `json:"action"`
+
+ // Timeout is the dead peer detection (DPD) timeout in seconds.
+ Timeout int `json:"timeout"`
+
+ // Interval is the dead peer detection (DPD) interval in seconds.
+ Interval int `json:"interval"`
+}
+
+// Connection is an IPSec site connection
+type Connection struct {
+ // IKEPolicyID is the ID of the IKE policy.
+ IKEPolicyID string `json:"ikepolicy_id"`
+
+ // VPNServiceID is the ID of the VPN service.
+ VPNServiceID string `json:"vpnservice_id"`
+
+ // LocalEPGroupID is the ID for the endpoint group that contains private subnets for the local side of the connection.
+ LocalEPGroupID string `json:"local_ep_group_id"`
+
+ // IPSecPolicyID is the ID of the IPSec policy
+ IPSecPolicyID string `json:"ipsecpolicy_id"`
+
+ // PeerID is the peer router identity for authentication.
+ PeerID string `json:"peer_id"`
+
+ // TenantID is the ID of the project.
+ TenantID string `json:"tenant_id"`
+
+ // ProjectID is the ID of the project.
+ ProjectID string `json:"project_id"`
+
+ // PeerEPGroupID is the ID for the endpoint group that contains private CIDRs in the form < net_address > / < prefix >
+ // for the peer side of the connection.
+ PeerEPGroupID string `json:"peer_ep_group_id"`
+
+ // LocalID is an ID to be used instead of the external IP address for a virtual router used in traffic
+ // between instances on different networks in east-west traffic.
+ LocalID string `json:"local_id"`
+
+ // Name is the human readable name of the connection.
+ Name string `json:"name"`
+
+ // Description is the human readable description of the connection.
+ Description string `json:"description"`
+
+ // PeerAddress is the peer gateway public IPv4 or IPv6 address or FQDN.
+ PeerAddress string `json:"peer_address"`
+
+ // RouteMode is the route mode.
+ RouteMode string `json:"route_mode"`
+
+ // PSK is the pre-shared key.
+ PSK string `json:"psk"`
+
+ // Initiator indicates whether this VPN can only respond to connections or both respond to and initiate connections.
+ Initiator string `json:"initiator"`
+
+ // PeerCIDRs is a unique list of valid peer private CIDRs in the form < net_address > / < prefix > .
+ PeerCIDRs []string `json:"peer_cidrs"`
+
+ // AdminStateUp is the administrative state of the connection.
+ AdminStateUp bool `json:"admin_state_up"`
+
+ // DPD is the dead peer detection (DPD) protocol controls.
+ DPD DPD `json:"dpd"`
+
+ // AuthMode is the authentication mode.
+ AuthMode string `json:"auth_mode"`
+
+ // MTU is the maximum transmission unit (MTU) value to address fragmentation.
+ MTU int `json:"mtu"`
+
+ // Status indicates whether the IPsec connection is currently operational.
+ // Values are ACTIVE, DOWN, BUILD, ERROR, PENDING_CREATE, PENDING_UPDATE, or PENDING_DELETE.
+ Status string `json:"status"`
+
+ // ID is the id of the connection
+ ID string `json:"id"`
+}
+
+type commonResult struct {
+ gophercloud.Result
+}
+
+// ConnectionPage is the page returned by a pager when traversing over a
+// collection of IPSec site connections.
+type ConnectionPage struct {
+ pagination.LinkedPageBase
+}
+
+// NextPageURL is invoked when a paginated collection of IPSec site connections 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 (r ConnectionPage) NextPageURL() (string, error) {
+ var s struct {
+ Links []gophercloud.Link `json:"ipsec_site_connections_links"`
+ }
+ err := r.ExtractInto(&s)
+ if err != nil {
+ return "", err
+ }
+ return gophercloud.ExtractNextURL(s.Links)
+}
+
+// IsEmpty checks whether a ConnectionPage struct is empty.
+func (r ConnectionPage) IsEmpty() (bool, error) {
+ is, err := ExtractConnections(r)
+ return len(is) == 0, err
+}
+
+// ExtractConnections accepts a Page struct, specifically a Connection struct,
+// and extracts the elements into a slice of Connection structs. In other words,
+// a generic collection is mapped into a relevant slice.
+func ExtractConnections(r pagination.Page) ([]Connection, error) {
+ var s struct {
+ Connections []Connection `json:"ipsec_site_connections"`
+ }
+ err := (r.(ConnectionPage)).ExtractInto(&s)
+ return s.Connections, err
+}
+
+// Extract is a function that accepts a result and extracts an IPSec site connection.
+func (r commonResult) Extract() (*Connection, error) {
+ var s struct {
+ Connection *Connection `json:"ipsec_site_connection"`
+ }
+ err := r.ExtractInto(&s)
+ return s.Connection, err
+}
+
+// CreateResult represents the result of a create operation. Call its Extract
+// method to interpret it as a Connection.
+type CreateResult struct {
+ commonResult
+}
+
+// DeleteResult represents the result of a delete operation. Call its
+// ExtractErr method to determine if the operation succeeded or failed.
+type DeleteResult struct {
+ gophercloud.ErrResult
+}
+
+// GetResult represents the result of a get operation. Call its Extract
+// method to interpret it as a Connection.
+type GetResult struct {
+ commonResult
+}
+
+// UpdateResult represents the result of an update operation. Call its Extract
+// method to interpret it as a connection
+type UpdateResult struct {
+ commonResult
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go
new file mode 100644
index 0000000000..3db27364df
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go
@@ -0,0 +1,413 @@
+package testing
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/gophercloud/gophercloud"
+ fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/siteconnections"
+ "github.com/gophercloud/gophercloud/pagination"
+ th "github.com/gophercloud/gophercloud/testhelper"
+)
+
+func TestCreate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsec-site-connections", 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, `
+{
+
+ "ipsec_site_connection": {
+ "psk": "secret",
+ "initiator": "bi-directional",
+ "ipsecpolicy_id": "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ "admin_state_up": true,
+ "mtu": 1500,
+ "peer_ep_group_id": "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ "ikepolicy_id": "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ "vpnservice_id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "local_ep_group_id": "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ "peer_address": "172.24.4.233",
+ "peer_id": "172.24.4.233",
+ "name": "vpnconnection1"
+
+}
+} `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, `
+{
+ "ipsec_site_connection": {
+ "status": "PENDING_CREATE",
+ "psk": "secret",
+ "initiator": "bi-directional",
+ "name": "vpnconnection1",
+ "admin_state_up": true,
+ "project_id": "10039663455a446d8ba2cbb058b0f578",
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "auth_mode": "psk",
+ "peer_cidrs": [],
+ "mtu": 1500,
+ "peer_ep_group_id": "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ "ikepolicy_id": "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ "vpnservice_id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "dpd": {
+ "action": "hold",
+ "interval": 30,
+ "timeout": 120
+ },
+ "route_mode": "static",
+ "ipsecpolicy_id": "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ "local_ep_group_id": "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ "peer_address": "172.24.4.233",
+ "peer_id": "172.24.4.233",
+ "id": "851f280f-5639-4ea3-81aa-e298525ab74b",
+ "description": ""
+ }
+}
+ `)
+ })
+
+ options := siteconnections.CreateOpts{
+ Name: "vpnconnection1",
+ AdminStateUp: gophercloud.Enabled,
+ PSK: "secret",
+ Initiator: siteconnections.InitiatorBiDirectional,
+ IPSecPolicyID: "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ MTU: 1500,
+ PeerEPGroupID: "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ IKEPolicyID: "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ VPNServiceID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ LocalEPGroupID: "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ PeerAddress: "172.24.4.233",
+ PeerID: "172.24.4.233",
+ }
+ actual, err := siteconnections.Create(fake.ServiceClient(), options).Extract()
+ th.AssertNoErr(t, err)
+ expectedDPD := siteconnections.DPD{
+ Action: "hold",
+ Interval: 30,
+ Timeout: 120,
+ }
+ expected := siteconnections.Connection{
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ Name: "vpnconnection1",
+ AdminStateUp: true,
+ PSK: "secret",
+ Initiator: "bi-directional",
+ IPSecPolicyID: "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ MTU: 1500,
+ PeerEPGroupID: "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ IKEPolicyID: "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ VPNServiceID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ LocalEPGroupID: "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ PeerAddress: "172.24.4.233",
+ PeerID: "172.24.4.233",
+ Status: "PENDING_CREATE",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ AuthMode: "psk",
+ PeerCIDRs: []string{},
+ DPD: expectedDPD,
+ RouteMode: "static",
+ ID: "851f280f-5639-4ea3-81aa-e298525ab74b",
+ Description: "",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestDelete(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsec-site-connections/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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 := siteconnections.Delete(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828")
+ th.AssertNoErr(t, res.Err)
+}
+
+func TestGet(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsec-site-connections/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+{
+ "ipsec_site_connection": {
+ "status": "PENDING_CREATE",
+ "psk": "secret",
+ "initiator": "bi-directional",
+ "name": "vpnconnection1",
+ "admin_state_up": true,
+ "project_id": "10039663455a446d8ba2cbb058b0f578",
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "auth_mode": "psk",
+ "peer_cidrs": [],
+ "mtu": 1500,
+ "peer_ep_group_id": "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ "ikepolicy_id": "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ "vpnservice_id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "dpd": {
+ "action": "hold",
+ "interval": 30,
+ "timeout": 120
+ },
+ "route_mode": "static",
+ "ipsecpolicy_id": "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ "local_ep_group_id": "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ "peer_address": "172.24.4.233",
+ "peer_id": "172.24.4.233",
+ "id": "851f280f-5639-4ea3-81aa-e298525ab74b",
+ "description": ""
+ }
+}
+ `)
+ })
+
+ actual, err := siteconnections.Get(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828").Extract()
+ th.AssertNoErr(t, err)
+ expectedDPD := siteconnections.DPD{
+ Action: "hold",
+ Interval: 30,
+ Timeout: 120,
+ }
+ expected := siteconnections.Connection{
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ Name: "vpnconnection1",
+ AdminStateUp: true,
+ PSK: "secret",
+ Initiator: "bi-directional",
+ IPSecPolicyID: "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ MTU: 1500,
+ PeerEPGroupID: "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ IKEPolicyID: "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ VPNServiceID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ LocalEPGroupID: "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ PeerAddress: "172.24.4.233",
+ PeerID: "172.24.4.233",
+ Status: "PENDING_CREATE",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ AuthMode: "psk",
+ PeerCIDRs: []string{},
+ DPD: expectedDPD,
+ RouteMode: "static",
+ ID: "851f280f-5639-4ea3-81aa-e298525ab74b",
+ Description: "",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
+
+func TestList(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsec-site-connections", 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, `
+{
+ "ipsec_site_connections":[
+ {
+ "status": "PENDING_CREATE",
+ "psk": "secret",
+ "initiator": "bi-directional",
+ "name": "vpnconnection1",
+ "admin_state_up": true,
+ "project_id": "10039663455a446d8ba2cbb058b0f578",
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "auth_mode": "psk",
+ "peer_cidrs": [],
+ "mtu": 1500,
+ "peer_ep_group_id": "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ "ikepolicy_id": "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ "vpnservice_id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "dpd": {
+ "action": "hold",
+ "interval": 30,
+ "timeout": 120
+ },
+ "route_mode": "static",
+ "ipsecpolicy_id": "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ "local_ep_group_id": "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ "peer_address": "172.24.4.233",
+ "peer_id": "172.24.4.233",
+ "id": "851f280f-5639-4ea3-81aa-e298525ab74b",
+ "description": ""
+ }]
+}
+ `)
+ })
+
+ count := 0
+
+ siteconnections.List(fake.ServiceClient(), siteconnections.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := siteconnections.ExtractConnections(page)
+ if err != nil {
+ t.Errorf("Failed to extract members: %v", err)
+ return false, err
+ }
+
+ expectedDPD := siteconnections.DPD{
+ Action: "hold",
+ Interval: 30,
+ Timeout: 120,
+ }
+ expected := []siteconnections.Connection{
+ {
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ Name: "vpnconnection1",
+ AdminStateUp: true,
+ PSK: "secret",
+ Initiator: "bi-directional",
+ IPSecPolicyID: "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ MTU: 1500,
+ PeerEPGroupID: "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ IKEPolicyID: "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ VPNServiceID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ LocalEPGroupID: "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ PeerAddress: "172.24.4.233",
+ PeerID: "172.24.4.233",
+ Status: "PENDING_CREATE",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ AuthMode: "psk",
+ PeerCIDRs: []string{},
+ DPD: expectedDPD,
+ RouteMode: "static",
+ ID: "851f280f-5639-4ea3-81aa-e298525ab74b",
+ Description: "",
+ },
+ }
+
+ th.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
+ }
+}
+
+func TestUpdate(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ th.Mux.HandleFunc("/v2.0/vpn/ipsec-site-connections/5c561d9d-eaea-45f6-ae3e-08d1a7080828", 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, `
+ {
+ "ipsec_site_connection": {
+ "psk": "updatedsecret",
+ "initiator": "response-only",
+ "name": "updatedconnection",
+ "description": "updateddescription"
+ }
+ }
+ `)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, `
+
+ {
+ "ipsec_site_connection": {
+ "status": "ACTIVE",
+ "psk": "updatedsecret",
+ "initiator": "response-only",
+ "name": "updatedconnection",
+ "admin_state_up": true,
+ "project_id": "10039663455a446d8ba2cbb058b0f578",
+ "tenant_id": "10039663455a446d8ba2cbb058b0f578",
+ "auth_mode": "psk",
+ "peer_cidrs": [],
+ "mtu": 1500,
+ "peer_ep_group_id": "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ "ikepolicy_id": "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ "vpnservice_id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ "dpd": {
+ "action": "hold",
+ "interval": 30,
+ "timeout": 120
+ },
+ "route_mode": "static",
+ "ipsecpolicy_id": "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ "local_ep_group_id": "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ "peer_address": "172.24.4.233",
+ "peer_id": "172.24.4.233",
+ "id": "851f280f-5639-4ea3-81aa-e298525ab74b",
+ "description": "updateddescription"
+ }
+}
+}
+`)
+ })
+ updatedName := "updatedconnection"
+ updatedDescription := "updateddescription"
+ options := siteconnections.UpdateOpts{
+ Name: &updatedName,
+ Description: &updatedDescription,
+ Initiator: siteconnections.InitiatorResponseOnly,
+ PSK: "updatedsecret",
+ }
+
+ actual, err := siteconnections.Update(fake.ServiceClient(), "5c561d9d-eaea-45f6-ae3e-08d1a7080828", options).Extract()
+ th.AssertNoErr(t, err)
+
+ expectedDPD := siteconnections.DPD{
+ Action: "hold",
+ Interval: 30,
+ Timeout: 120,
+ }
+
+ expected := siteconnections.Connection{
+ TenantID: "10039663455a446d8ba2cbb058b0f578",
+ Name: "updatedconnection",
+ AdminStateUp: true,
+ PSK: "updatedsecret",
+ Initiator: "response-only",
+ IPSecPolicyID: "e6e23d0c-9519-4d52-8ea4-5b1f96d857b1",
+ MTU: 1500,
+ PeerEPGroupID: "9ad5a7e0-6dac-41b4-b20d-a7b8645fddf1",
+ IKEPolicyID: "9b00d6b0-6c93-4ca5-9747-b8ade7bb514f",
+ VPNServiceID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828",
+ LocalEPGroupID: "3e1815dd-e212-43d0-8f13-b494fa553e68",
+ PeerAddress: "172.24.4.233",
+ PeerID: "172.24.4.233",
+ Status: "ACTIVE",
+ ProjectID: "10039663455a446d8ba2cbb058b0f578",
+ AuthMode: "psk",
+ PeerCIDRs: []string{},
+ DPD: expectedDPD,
+ RouteMode: "static",
+ ID: "851f280f-5639-4ea3-81aa-e298525ab74b",
+ Description: "updateddescription",
+ }
+ th.AssertDeepEquals(t, expected, *actual)
+}
diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/urls.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/urls.go
new file mode 100644
index 0000000000..5c8ee9a364
--- /dev/null
+++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/urls.go
@@ -0,0 +1,16 @@
+package siteconnections
+
+import "github.com/gophercloud/gophercloud"
+
+const (
+ rootPath = "vpn"
+ resourcePath = "ipsec-site-connections"
+)
+
+func rootURL(c *gophercloud.ServiceClient) string {
+ return c.ServiceURL(rootPath, resourcePath)
+}
+
+func resourceURL(c *gophercloud.ServiceClient, id string) string {
+ return c.ServiceURL(rootPath, resourcePath, id)
+}
diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go
index 040f32183b..bc4460a065 100644
--- a/openstack/networking/v2/networks/requests.go
+++ b/openstack/networking/v2/networks/requests.go
@@ -21,6 +21,7 @@ type ListOpts struct {
Name string `q:"name"`
AdminStateUp *bool `q:"admin_state_up"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
Shared *bool `q:"shared"`
ID string `q:"id"`
Marker string `q:"marker"`
@@ -70,6 +71,7 @@ type CreateOpts struct {
Name string `json:"name,omitempty"`
Shared *bool `json:"shared,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
AvailabilityZoneHints []string `json:"availability_zone_hints,omitempty"`
}
diff --git a/openstack/networking/v2/networks/results.go b/openstack/networking/v2/networks/results.go
index c73f9e1a63..62f4b3c3a5 100644
--- a/openstack/networking/v2/networks/results.go
+++ b/openstack/networking/v2/networks/results.go
@@ -64,9 +64,12 @@ type Network struct {
// Subnets associated with this network.
Subnets []string `json:"subnets"`
- // Owner of network.
+ // TenantID is the project owner of the network.
TenantID string `json:"tenant_id"`
+ // ProjectID is the project owner of the network.
+ ProjectID string `json:"project_id"`
+
// Specifies whether the network resource can be accessed by any tenant.
Shared bool `json:"shared"`
diff --git a/openstack/networking/v2/networks/testing/fixtures.go b/openstack/networking/v2/networks/testing/fixtures.go
index 3edbe8f37a..9632d448a5 100644
--- a/openstack/networking/v2/networks/testing/fixtures.go
+++ b/openstack/networking/v2/networks/testing/fixtures.go
@@ -86,6 +86,32 @@ const CreateResponse = `
}
}`
+const CreatePortSecurityRequest = `
+{
+ "network": {
+ "name": "private",
+ "admin_state_up": true,
+ "port_security_enabled": false
+ }
+}`
+
+const CreatePortSecurityResponse = `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": ["08eae331-0402-425a-923c-34f7cfe39c1b"],
+ "name": "private",
+ "admin_state_up": true,
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "shared": false,
+ "id": "db193ab3-96e3-4cb3-8fc5-05f4296d0324",
+ "provider:segmentation_id": 9876543210,
+ "provider:physical_network": null,
+ "provider:network_type": "local",
+ "port_security_enabled": false
+ }
+}`
+
const CreateOptionalFieldsRequest = `
{
"network": {
@@ -122,6 +148,30 @@ const UpdateResponse = `
}
}`
+const UpdatePortSecurityRequest = `
+{
+ "network": {
+ "port_security_enabled": false
+ }
+}`
+
+const UpdatePortSecurityResponse = `
+{
+ "network": {
+ "status": "ACTIVE",
+ "subnets": ["08eae331-0402-425a-923c-34f7cfe39c1b"],
+ "name": "private",
+ "admin_state_up": true,
+ "tenant_id": "26a7980765d0414dbc1fc1f88cdb7e6e",
+ "shared": false,
+ "id": "4e8e5957-649f-477b-9e5b-f1f75b21c03c",
+ "provider:segmentation_id": 9876543210,
+ "provider:physical_network": null,
+ "provider:network_type": "local",
+ "port_security_enabled": false
+ }
+}`
+
var Network1 = networks.Network{
Status: "ACTIVE",
Subnets: []string{"54d6f61d-db07-451c-9ab3-b9609b6b6f0b"},
diff --git a/openstack/networking/v2/networks/testing/requests_test.go b/openstack/networking/v2/networks/testing/requests_test.go
index 0e028ead4e..231d7f087c 100644
--- a/openstack/networking/v2/networks/testing/requests_test.go
+++ b/openstack/networking/v2/networks/testing/requests_test.go
@@ -218,3 +218,78 @@ func TestDelete(t *testing.T) {
res := networks.Delete(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
th.AssertNoErr(t, res.Err)
}
+
+func TestCreatePortSecurity(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, CreatePortSecurityRequest)
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, CreatePortSecurityResponse)
+ })
+
+ var networkWithExtensions struct {
+ networks.Network
+ portsecurity.PortSecurityExt
+ }
+
+ iTrue := true
+ iFalse := false
+ networkCreateOpts := networks.CreateOpts{Name: "private", AdminStateUp: &iTrue}
+ createOpts := portsecurity.NetworkCreateOptsExt{
+ CreateOptsBuilder: networkCreateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := networks.Create(fake.ServiceClient(), createOpts).ExtractInto(&networkWithExtensions)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, networkWithExtensions.Status, "ACTIVE")
+ th.AssertEquals(t, networkWithExtensions.PortSecurityEnabled, false)
+}
+
+func TestUpdatePortSecurity(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, UpdatePortSecurityRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, UpdatePortSecurityResponse)
+ })
+
+ var networkWithExtensions struct {
+ networks.Network
+ portsecurity.PortSecurityExt
+ }
+
+ iFalse := false
+ networkUpdateOpts := networks.UpdateOpts{}
+ updateOpts := portsecurity.NetworkUpdateOptsExt{
+ UpdateOptsBuilder: networkUpdateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := networks.Update(fake.ServiceClient(), "4e8e5957-649f-477b-9e5b-f1f75b21c03c", updateOpts).ExtractInto(&networkWithExtensions)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, networkWithExtensions.Name, "private")
+ th.AssertEquals(t, networkWithExtensions.AdminStateUp, true)
+ th.AssertEquals(t, networkWithExtensions.Shared, false)
+ th.AssertEquals(t, networkWithExtensions.ID, "4e8e5957-649f-477b-9e5b-f1f75b21c03c")
+ th.AssertEquals(t, networkWithExtensions.PortSecurityEnabled, false)
+}
diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go
index fd1e972576..90416faa19 100644
--- a/openstack/networking/v2/ports/requests.go
+++ b/openstack/networking/v2/ports/requests.go
@@ -22,6 +22,7 @@ type ListOpts struct {
AdminStateUp *bool `q:"admin_state_up"`
NetworkID string `q:"network_id"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
DeviceOwner string `q:"device_owner"`
MACAddress string `q:"mac_address"`
ID string `q:"id"`
@@ -81,6 +82,7 @@ type CreateOpts struct {
DeviceID string `json:"device_id,omitempty"`
DeviceOwner string `json:"device_owner,omitempty"`
TenantID string `json:"tenant_id,omitempty"`
+ ProjectID string `json:"project_id,omitempty"`
SecurityGroups *[]string `json:"security_groups,omitempty"`
AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"`
}
diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go
index ebef98d5de..66937fd989 100644
--- a/openstack/networking/v2/ports/results.go
+++ b/openstack/networking/v2/ports/results.go
@@ -84,9 +84,12 @@ type Port struct {
// the subnets where the IP addresses are picked from
FixedIPs []IP `json:"fixed_ips"`
- // Owner of network.
+ // TenantID is the project owner of the port.
TenantID string `json:"tenant_id"`
+ // ProjectID is the project owner of the port.
+ ProjectID string `json:"project_id"`
+
// Identifies the entity (e.g.: dhcp agent) using this port.
DeviceOwner string `json:"device_owner"`
diff --git a/openstack/networking/v2/ports/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go
index dea77fe011..cfce9b0912 100644
--- a/openstack/networking/v2/ports/testing/fixtures.go
+++ b/openstack/networking/v2/ports/testing/fixtures.go
@@ -220,6 +220,62 @@ const CreateOmitSecurityGroupsResponse = `
}
`
+const CreatePortSecurityRequest = `
+{
+ "port": {
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "name": "private-port",
+ "admin_state_up": true,
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "security_groups": ["foo"],
+ "allowed_address_pairs": [
+ {
+ "ip_address": "10.0.0.4",
+ "mac_address": "fa:16:3e:c9:cb:f0"
+ }
+ ],
+ "port_security_enabled": false
+ }
+}
+`
+
+const CreatePortSecurityResponse = `
+{
+ "port": {
+ "status": "DOWN",
+ "name": "private-port",
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "allowed_address_pairs": [
+ {
+ "ip_address": "10.0.0.4",
+ "mac_address": "fa:16:3e:c9:cb:f0"
+ }
+ ],
+ "device_id": "",
+ "port_security_enabled": false
+ }
+}
+`
+
const UpdateRequest = `
{
"port": {
@@ -325,6 +381,46 @@ const UpdateOmitSecurityGroupsResponse = `
}
`
+const UpdatePortSecurityRequest = `
+{
+ "port": {
+ "port_security_enabled": false
+ }
+}
+`
+
+const UpdatePortSecurityResponse = `
+{
+ "port": {
+ "status": "DOWN",
+ "name": "private-port",
+ "admin_state_up": true,
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "security_groups": [
+ "f0ac4394-7e4a-4409-9701-ba8be283dbc3"
+ ],
+ "allowed_address_pairs": [
+ {
+ "ip_address": "10.0.0.4",
+ "mac_address": "fa:16:3e:c9:cb:f0"
+ }
+ ],
+ "device_id": "",
+ "port_security_enabled": false
+ }
+}
+`
+
const RemoveSecurityGroupRequest = `
{
"port": {
@@ -464,3 +560,151 @@ const DontUpdateAllowedAddressPairsResponse = `
}
}
`
+
+// GetWithExtraDHCPOptsResponse represents a raw port response with extra
+// DHCP options.
+const GetWithExtraDHCPOptsResponse = `
+{
+ "port": {
+ "status": "ACTIVE",
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "extra_dhcp_opts": [
+ {
+ "opt_name": "option1",
+ "opt_value": "value1",
+ "ip_version": 4
+ },
+ {
+ "opt_name": "option2",
+ "opt_value": "value2",
+ "ip_version": 4
+ }
+ ],
+ "admin_state_up": true,
+ "name": "port-with-extra-dhcp-opts",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.4"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "device_id": ""
+ }
+}
+`
+
+// CreateWithExtraDHCPOptsRequest represents a raw port creation request
+// with extra DHCP options.
+const CreateWithExtraDHCPOptsRequest = `
+{
+ "port": {
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "name": "port-with-extra-dhcp-opts",
+ "admin_state_up": true,
+ "fixed_ips": [
+ {
+ "ip_address": "10.0.0.2",
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2"
+ }
+ ],
+ "extra_dhcp_opts": [
+ {
+ "opt_name": "option1",
+ "opt_value": "value1"
+ }
+ ]
+ }
+}
+`
+
+// CreateWithExtraDHCPOptsResponse represents a raw port creation response
+// with extra DHCP options.
+const CreateWithExtraDHCPOptsResponse = `
+{
+ "port": {
+ "status": "DOWN",
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "extra_dhcp_opts": [
+ {
+ "opt_name": "option1",
+ "opt_value": "value1",
+ "ip_version": 4
+ }
+ ],
+ "admin_state_up": true,
+ "name": "port-with-extra-dhcp-opts",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.2"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "device_id": ""
+ }
+}
+`
+
+// UpdateWithExtraDHCPOptsRequest represents a raw port update request with
+// extra DHCP options.
+const UpdateWithExtraDHCPOptsRequest = `
+{
+ "port": {
+ "name": "updated-port-with-dhcp-opts",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.3"
+ }
+ ],
+ "extra_dhcp_opts": [
+ {
+ "opt_name": "option1",
+ "opt_value": null
+ },
+ {
+ "opt_name": "option2",
+ "opt_value": "value2"
+ }
+ ]
+ }
+}
+`
+
+// UpdateWithExtraDHCPOptsResponse represents a raw port update response with
+// extra DHCP options.
+const UpdateWithExtraDHCPOptsResponse = `
+{
+ "port": {
+ "status": "DOWN",
+ "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ "tenant_id": "d6700c0c9ffa4f1cb322cd4a1f3906fa",
+ "extra_dhcp_opts": [
+ {
+ "opt_name": "option2",
+ "opt_value": "value2",
+ "ip_version": 4
+ }
+ ],
+ "admin_state_up": true,
+ "name": "updated-port-with-dhcp-opts",
+ "device_owner": "",
+ "mac_address": "fa:16:3e:c9:cb:f0",
+ "fixed_ips": [
+ {
+ "subnet_id": "a0304c3a-4f08-4c43-88af-d796509c97d2",
+ "ip_address": "10.0.0.3"
+ }
+ ],
+ "id": "65c0ee9f-d634-4522-8954-51021b570b0d",
+ "device_id": ""
+ }
+}
+`
diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go
index aa6c74d64a..af91cf99d3 100644
--- a/openstack/networking/v2/ports/testing/requests_test.go
+++ b/openstack/networking/v2/ports/testing/requests_test.go
@@ -6,6 +6,7 @@ import (
"testing"
fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common"
+ "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/extradhcpopts"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsecurity"
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
"github.com/gophercloud/gophercloud/pagination"
@@ -311,6 +312,54 @@ func TestRequiredCreateOpts(t *testing.T) {
}
}
+func TestCreatePortSecurity(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, "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, CreatePortSecurityRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, CreatePortSecurityResponse)
+ })
+
+ var portWithExt struct {
+ ports.Port
+ portsecurity.PortSecurityExt
+ }
+
+ asu := true
+ iFalse := false
+ portCreateOpts := ports.CreateOpts{
+ Name: "private-port",
+ AdminStateUp: &asu,
+ NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ FixedIPs: []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ },
+ SecurityGroups: &[]string{"foo"},
+ AllowedAddressPairs: []ports.AddressPair{
+ {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"},
+ },
+ }
+ createOpts := portsecurity.PortCreateOptsExt{
+ CreateOptsBuilder: portCreateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := ports.Create(fake.ServiceClient(), createOpts).ExtractInto(&portWithExt)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, portWithExt.Status, "DOWN")
+ th.AssertEquals(t, portWithExt.PortSecurityEnabled, false)
+}
+
func TestUpdate(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -392,6 +441,43 @@ func TestUpdateOmitSecurityGroups(t *testing.T) {
th.AssertDeepEquals(t, s.SecurityGroups, []string{"f0ac4394-7e4a-4409-9701-ba8be283dbc3"})
}
+func TestUpdatePortSecurity(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, UpdatePortSecurityRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, UpdatePortSecurityResponse)
+ })
+
+ var portWithExt struct {
+ ports.Port
+ portsecurity.PortSecurityExt
+ }
+
+ iFalse := false
+ portUpdateOpts := ports.UpdateOpts{}
+ updateOpts := portsecurity.PortUpdateOptsExt{
+ UpdateOptsBuilder: portUpdateOpts,
+ PortSecurityEnabled: &iFalse,
+ }
+
+ err := ports.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", updateOpts).ExtractInto(&portWithExt)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, portWithExt.Status, "DOWN")
+ th.AssertEquals(t, portWithExt.Name, "private-port")
+ th.AssertEquals(t, portWithExt.PortSecurityEnabled, false)
+}
+
func TestRemoveSecurityGroups(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
@@ -521,3 +607,173 @@ func TestDelete(t *testing.T) {
res := ports.Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d")
th.AssertNoErr(t, res.Err)
}
+
+func TestGetWithExtraDHCPOpts(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", fake.TokenID)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, GetWithExtraDHCPOptsResponse)
+ })
+
+ var s struct {
+ ports.Port
+ extradhcpopts.ExtraDHCPOptsExt
+ }
+
+ err := ports.Get(fake.ServiceClient(), "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2").ExtractInto(&s)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Status, "ACTIVE")
+ th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+ th.AssertEquals(t, s.AdminStateUp, true)
+ th.AssertEquals(t, s.Name, "port-with-extra-dhcp-opts")
+ th.AssertEquals(t, s.DeviceOwner, "")
+ th.AssertEquals(t, s.MACAddress, "fa:16:3e:c9:cb:f0")
+ th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.4"},
+ })
+ th.AssertEquals(t, s.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertEquals(t, s.DeviceID, "")
+
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].OptName, "option1")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].OptValue, "value1")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].IPVersion, 4)
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[1].OptName, "option2")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[1].OptValue, "value2")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[1].IPVersion, 4)
+}
+
+func TestCreateWithExtraDHCPOpts(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, "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, CreateWithExtraDHCPOptsRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+
+ fmt.Fprintf(w, CreateWithExtraDHCPOptsResponse)
+ })
+
+ adminStateUp := true
+ portCreateOpts := ports.CreateOpts{
+ Name: "port-with-extra-dhcp-opts",
+ AdminStateUp: &adminStateUp,
+ NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7",
+ FixedIPs: []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ },
+ }
+
+ createOpts := extradhcpopts.CreateOptsExt{
+ CreateOptsBuilder: portCreateOpts,
+ ExtraDHCPOpts: []extradhcpopts.CreateExtraDHCPOpt{
+ {
+ OptName: "option1",
+ OptValue: "value1",
+ },
+ },
+ }
+
+ var s struct {
+ ports.Port
+ extradhcpopts.ExtraDHCPOptsExt
+ }
+
+ err := ports.Create(fake.ServiceClient(), createOpts).ExtractInto(&s)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Status, "DOWN")
+ th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+ th.AssertEquals(t, s.AdminStateUp, true)
+ th.AssertEquals(t, s.Name, "port-with-extra-dhcp-opts")
+ th.AssertEquals(t, s.DeviceOwner, "")
+ th.AssertEquals(t, s.MACAddress, "fa:16:3e:c9:cb:f0")
+ th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"},
+ })
+ th.AssertEquals(t, s.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertEquals(t, s.DeviceID, "")
+
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].OptName, "option1")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].OptValue, "value1")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].IPVersion, 4)
+}
+
+func TestUpdateWithExtraDHCPOpts(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+
+ 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", fake.TokenID)
+ th.TestHeader(t, r, "Content-Type", "application/json")
+ th.TestHeader(t, r, "Accept", "application/json")
+ th.TestJSONRequest(t, r, UpdateWithExtraDHCPOptsRequest)
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ fmt.Fprintf(w, UpdateWithExtraDHCPOptsResponse)
+ })
+
+ portUpdateOpts := ports.UpdateOpts{
+ Name: "updated-port-with-dhcp-opts",
+ FixedIPs: []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ },
+ }
+
+ edoValue2 := "value2"
+ updateOpts := extradhcpopts.UpdateOptsExt{
+ UpdateOptsBuilder: portUpdateOpts,
+ ExtraDHCPOpts: []extradhcpopts.UpdateExtraDHCPOpt{
+ {
+ OptName: "option1",
+ },
+ {
+ OptName: "option2",
+ OptValue: &edoValue2,
+ },
+ },
+ }
+
+ var s struct {
+ ports.Port
+ extradhcpopts.ExtraDHCPOptsExt
+ }
+
+ err := ports.Update(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d", updateOpts).ExtractInto(&s)
+ th.AssertNoErr(t, err)
+
+ th.AssertEquals(t, s.Status, "DOWN")
+ th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7")
+ th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa")
+ th.AssertEquals(t, s.AdminStateUp, true)
+ th.AssertEquals(t, s.Name, "updated-port-with-dhcp-opts")
+ th.AssertEquals(t, s.DeviceOwner, "")
+ th.AssertEquals(t, s.MACAddress, "fa:16:3e:c9:cb:f0")
+ th.AssertDeepEquals(t, s.FixedIPs, []ports.IP{
+ {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"},
+ })
+ th.AssertEquals(t, s.ID, "65c0ee9f-d634-4522-8954-51021b570b0d")
+ th.AssertEquals(t, s.DeviceID, "")
+
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].OptName, "option2")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].OptValue, "value2")
+ th.AssertDeepEquals(t, s.ExtraDHCPOpts[0].IPVersion, 4)
+}
diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go
index 403f692234..597a4e77f3 100644
--- a/openstack/networking/v2/subnets/requests.go
+++ b/openstack/networking/v2/subnets/requests.go
@@ -21,12 +21,14 @@ type ListOpts struct {
EnableDHCP *bool `q:"enable_dhcp"`
NetworkID string `q:"network_id"`
TenantID string `q:"tenant_id"`
+ ProjectID string `q:"project_id"`
IPVersion int `q:"ip_version"`
GatewayIP string `q:"gateway_ip"`
CIDR string `q:"cidr"`
IPv6AddressMode string `q:"ipv6_address_mode"`
IPv6RAMode string `q:"ipv6_ra_mode"`
ID string `q:"id"`
+ SubnetPoolID string `q:"subnetpool_id"`
Limit int `q:"limit"`
Marker string `q:"marker"`
SortKey string `q:"sort_key"`
@@ -83,10 +85,14 @@ type CreateOpts struct {
// Name is a human-readable name of the subnet.
Name string `json:"name,omitempty"`
- // The UUID of the tenant who owns the Subnet. Only administrative users
- // can specify a tenant UUID other than their own.
+ // The UUID of the project who owns the Subnet. Only administrative users
+ // can specify a project UUID other than their own.
TenantID string `json:"tenant_id,omitempty"`
+ // The UUID of the project who owns the Subnet. Only administrative users
+ // can specify a project UUID other than their own.
+ ProjectID string `json:"project_id,omitempty"`
+
// AllocationPools are IP Address pools that will be available for DHCP.
AllocationPools []AllocationPool `json:"allocation_pools,omitempty"`
@@ -114,6 +120,9 @@ type CreateOpts struct {
// The IPv6 router advertisement specifies whether the networking service
// should transmit ICMPv6 packets.
IPv6RAMode string `json:"ipv6_ra_mode,omitempty"`
+
+ // SubnetPoolID is the id of the subnet pool that subnet should be associated to.
+ SubnetPoolID string `json:"subnetpool_id,omitempty"`
}
// ToSubnetCreateMap builds a request body from CreateOpts.
diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go
index 743610f01e..493e5c042e 100644
--- a/openstack/networking/v2/subnets/results.go
+++ b/openstack/networking/v2/subnets/results.go
@@ -91,15 +91,21 @@ type Subnet struct {
// Specifies whether DHCP is enabled for this subnet or not.
EnableDHCP bool `json:"enable_dhcp"`
- // Owner of network.
+ // TenantID is the project owner of the subnet.
TenantID string `json:"tenant_id"`
+ // ProjectID is the project owner of the subnet.
+ ProjectID string `json:"project_id"`
+
// The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses.
IPv6AddressMode string `json:"ipv6_address_mode"`
// The IPv6 router advertisement specifies whether the networking service
// should transmit ICMPv6 packets.
IPv6RAMode string `json:"ipv6_ra_mode"`
+
+ // SubnetPoolID is the id of the subnet pool associated with the subnet.
+ SubnetPoolID string `json:"subnetpool_id"`
}
// SubnetPage is the page returned by a pager when traversing over a collection
diff --git a/openstack/networking/v2/subnets/testing/fixtures.go b/openstack/networking/v2/subnets/testing/fixtures.go
index 0dac09260f..619ea3e55e 100644
--- a/openstack/networking/v2/subnets/testing/fixtures.go
+++ b/openstack/networking/v2/subnets/testing/fixtures.go
@@ -60,6 +60,25 @@ const SubnetListResult = `
"gateway_ip": null,
"cidr": "192.168.1.0/24",
"id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0c"
+ },
+ {
+ "name": "my_subnet_with_subnetpool",
+ "enable_dhcp": false,
+ "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ "tenant_id": "4fd44f30292945e481c7b8a0c8908869",
+ "dns_nameservers": [],
+ "allocation_pools": [
+ {
+ "start": "10.11.12.2",
+ "end": "10.11.12.254"
+ }
+ ],
+ "host_routes": [],
+ "ip_version": 4,
+ "gateway_ip": null,
+ "cidr": "10.11.12.0/24",
+ "id": "38186a51-f373-4bbc-838b-6eaa1aa13eac",
+ "subnetpool_id": "b80340c7-9960-4f67-a99c-02501656284b"
}
]
}
@@ -122,6 +141,26 @@ var Subnet3 = subnets.Subnet{
ID: "54d6f61d-db07-451c-9ab3-b9609b6b6f0c",
}
+var Subnet4 = subnets.Subnet{
+ Name: "my_subnet_with_subnetpool",
+ EnableDHCP: false,
+ NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23",
+ TenantID: "4fd44f30292945e481c7b8a0c8908869",
+ DNSNameservers: []string{},
+ AllocationPools: []subnets.AllocationPool{
+ {
+ Start: "10.11.12.2",
+ End: "10.11.12.254",
+ },
+ },
+ HostRoutes: []subnets.HostRoute{},
+ IPVersion: 4,
+ GatewayIP: "",
+ CIDR: "10.11.12.0/24",
+ ID: "38186a51-f373-4bbc-838b-6eaa1aa13eac",
+ SubnetPoolID: "b80340c7-9960-4f67-a99c-02501656284b",
+}
+
const SubnetGetResult = `
{
"subnet": {
@@ -140,7 +179,8 @@ const SubnetGetResult = `
"ip_version": 4,
"gateway_ip": "192.0.0.1",
"cidr": "192.0.0.0/8",
- "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b"
+ "id": "54d6f61d-db07-451c-9ab3-b9609b6b6f0b",
+ "subnetpool_id": "b80340c7-9960-4f67-a99c-02501656284b"
}
}
`
@@ -159,7 +199,8 @@ const SubnetCreateRequest = `
"end": "192.168.199.254"
}
],
- "host_routes": [{"destination":"","nexthop": "bar"}]
+ "host_routes": [{"destination":"","nexthop": "bar"}],
+ "subnetpool_id": "b80340c7-9960-4f67-a99c-02501656284b"
}
}
`
@@ -182,7 +223,8 @@ const SubnetCreateResult = `
"ip_version": 4,
"gateway_ip": "192.168.199.1",
"cidr": "192.168.199.0/24",
- "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126"
+ "id": "3b80198d-4f7b-4f77-9ef5-774d54e17126",
+ "subnetpool_id": "b80340c7-9960-4f67-a99c-02501656284b"
}
}
`
diff --git a/openstack/networking/v2/subnets/testing/requests_test.go b/openstack/networking/v2/subnets/testing/requests_test.go
index 2dd4e029df..208fc608f9 100644
--- a/openstack/networking/v2/subnets/testing/requests_test.go
+++ b/openstack/networking/v2/subnets/testing/requests_test.go
@@ -39,6 +39,7 @@ func TestList(t *testing.T) {
Subnet1,
Subnet2,
Subnet3,
+ Subnet4,
}
th.CheckDeepEquals(t, expected, actual)
@@ -84,6 +85,7 @@ func TestGet(t *testing.T) {
th.AssertEquals(t, s.GatewayIP, "192.0.0.1")
th.AssertEquals(t, s.CIDR, "192.0.0.0/8")
th.AssertEquals(t, s.ID, "54d6f61d-db07-451c-9ab3-b9609b6b6f0b")
+ th.AssertEquals(t, s.SubnetPoolID, "b80340c7-9960-4f67-a99c-02501656284b")
}
func TestCreate(t *testing.T) {
@@ -119,6 +121,7 @@ func TestCreate(t *testing.T) {
HostRoutes: []subnets.HostRoute{
{NextHop: "bar"},
},
+ SubnetPoolID: "b80340c7-9960-4f67-a99c-02501656284b",
}
s, err := subnets.Create(fake.ServiceClient(), opts).Extract()
th.AssertNoErr(t, err)
@@ -139,6 +142,7 @@ func TestCreate(t *testing.T) {
th.AssertEquals(t, s.GatewayIP, "192.168.199.1")
th.AssertEquals(t, s.CIDR, "192.168.199.0/24")
th.AssertEquals(t, s.ID, "3b80198d-4f7b-4f77-9ef5-774d54e17126")
+ th.AssertEquals(t, s.SubnetPoolID, "b80340c7-9960-4f67-a99c-02501656284b")
}
func TestCreateNoGateway(t *testing.T) {
diff --git a/openstack/orchestration/v1/stacks/environment_test.go b/openstack/orchestration/v1/stacks/environment_test.go
index a7e3aaee19..6fcc230d43 100644
--- a/openstack/orchestration/v1/stacks/environment_test.go
+++ b/openstack/orchestration/v1/stacks/environment_test.go
@@ -138,7 +138,7 @@ service_db:
// handler for my_env.yaml
th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- w.Header().Set("Content-Type", "application/jason")
+ w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, environmentContent)
})
@@ -150,7 +150,7 @@ service_db:
// handler for my_db.yaml
th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- w.Header().Set("Content-Type", "application/jason")
+ w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, dbContent)
})
@@ -170,13 +170,20 @@ service_db:
th.AssertEquals(t, expectedEnvFilesContent, env.Files[fakeEnvURL])
th.AssertEquals(t, expectedDBFilesContent, env.Files[fakeDBURL])
+ // Update env's fileMaps to replace relative filenames by absolute URLs.
+ env.fileMaps = map[string]string{
+ "my_env.yaml": fakeEnvURL,
+ "my_db.yaml": fakeDBURL,
+ }
env.fixFileRefs()
+
expectedParsed := map[string]interface{}{
- "resource_registry": "2015-04-30",
- "My::WP::Server": fakeEnvURL,
- "resources": map[string]interface{}{
- "my_db_server": map[string]interface{}{
- "OS::DBInstance": fakeDBURL,
+ "resource_registry": map[string]interface{}{
+ "My::WP::Server": fakeEnvURL,
+ "resources": map[string]interface{}{
+ "my_db_server": map[string]interface{}{
+ "OS::DBInstance": fakeDBURL,
+ },
},
},
}
diff --git a/openstack/orchestration/v1/stacks/fixtures.go b/openstack/orchestration/v1/stacks/fixtures.go
index d6fd0750f3..58987d4bfd 100644
--- a/openstack/orchestration/v1/stacks/fixtures.go
+++ b/openstack/orchestration/v1/stacks/fixtures.go
@@ -6,7 +6,7 @@ const ValidJSONTemplate = `
"heat_template_version": "2014-10-16",
"parameters": {
"flavor": {
- "default": 4353,
+ "default": "debian2G",
"description": "Flavor for the server to be created",
"hidden": true,
"type": "string"
@@ -32,7 +32,7 @@ parameters:
flavor:
type: string
description: Flavor for the server to be created
- default: 4353
+ default: debian2G
hidden: true
resources:
test_server:
@@ -49,7 +49,7 @@ parameters:
flavor:
type: string
description: Flavor for the server to be created
- default: 4353
+ default: debian2G
hidden: true
resources:
test_server:
@@ -128,7 +128,7 @@ parameters:
flavor:
type: string
description: Flavor for the server to be created
- default: 4353
+ default: debian2G
hidden: true
resources:
test_server:
@@ -180,7 +180,7 @@ var ValidJSONTemplateParsed = map[string]interface{}{
"heat_template_version": "2014-10-16",
"parameters": map[string]interface{}{
"flavor": map[string]interface{}{
- "default": 4353,
+ "default": "debian2G",
"description": "Flavor for the server to be created",
"hidden": true,
"type": "string",
diff --git a/openstack/orchestration/v1/stacks/template.go b/openstack/orchestration/v1/stacks/template.go
index 4cf5aae41a..d8532ad01c 100644
--- a/openstack/orchestration/v1/stacks/template.go
+++ b/openstack/orchestration/v1/stacks/template.go
@@ -39,8 +39,8 @@ func (t *Template) Validate() error {
}
// GetFileContents recursively parses a template to search for urls. These urls
-// are assumed to point to other templates (known in OpenStack Heat as child
-// templates). The contents of these urls are fetched and stored in the `Files`
+// are assumed to point to other templates.
+// The contents of these urls are fetched and stored in the `Files`
// parameter of the template structure. This is the only way that a user can
// use child templates that are located in their filesystem; urls located on the
// web (e.g. on github or swift) can be fetched directly by Heat engine.
diff --git a/openstack/orchestration/v1/stacks/template_test.go b/openstack/orchestration/v1/stacks/template_test.go
index cbe99ed9cf..1ad9fc9c71 100644
--- a/openstack/orchestration/v1/stacks/template_test.go
+++ b/openstack/orchestration/v1/stacks/template_test.go
@@ -99,7 +99,7 @@ resources:
- {uuid: 11111111-1111-1111-1111-111111111111}`
th.Mux.HandleFunc(urlparsed.Path, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
- w.Header().Set("Content-Type", "application/jason")
+ w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, myNovaContent)
})
diff --git a/openstack/testing/endpoint_location_test.go b/openstack/testing/endpoint_location_test.go
index ea7bdd2bf0..5ac773eee6 100644
--- a/openstack/testing/endpoint_location_test.go
+++ b/openstack/testing/endpoint_location_test.go
@@ -178,6 +178,30 @@ var catalog3 = tokens3.ServiceCatalog{
},
},
},
+ tokens3.CatalogEntry{
+ Type: "someother",
+ Name: "someother",
+ Endpoints: []tokens3.Endpoint{
+ tokens3.Endpoint{
+ ID: "1",
+ Region: "someother",
+ Interface: "public",
+ URL: "https://public.correct.com/",
+ },
+ tokens3.Endpoint{
+ ID: "2",
+ RegionID: "someother",
+ Interface: "admin",
+ URL: "https://admin.correct.com/",
+ },
+ tokens3.Endpoint{
+ ID: "3",
+ RegionID: "someother",
+ Interface: "internal",
+ URL: "https://internal.correct.com/",
+ },
+ },
+ },
},
}
@@ -229,3 +253,22 @@ func TestV3EndpointBadAvailability(t *testing.T) {
})
th.CheckEquals(t, "Unexpected availability in endpoint query: wat", err.Error())
}
+
+func TestV3EndpointWithRegionID(t *testing.T) {
+ expectedURLs := map[gophercloud.Availability]string{
+ gophercloud.AvailabilityPublic: "https://public.correct.com/",
+ gophercloud.AvailabilityAdmin: "https://admin.correct.com/",
+ gophercloud.AvailabilityInternal: "https://internal.correct.com/",
+ }
+
+ for availability, expected := range expectedURLs {
+ actual, err := openstack.V3EndpointURL(&catalog3, gophercloud.EndpointOpts{
+ Type: "someother",
+ Name: "someother",
+ Region: "someother",
+ Availability: availability,
+ })
+ th.AssertNoErr(t, err)
+ th.CheckEquals(t, expected, actual)
+ }
+}
diff --git a/params.go b/params.go
index 687af3dc0c..28ad906856 100644
--- a/params.go
+++ b/params.go
@@ -115,10 +115,15 @@ func BuildRequestBody(opts interface{}, parent string) (map[string]interface{},
}
}
+ jsonTag := f.Tag.Get("json")
+ if jsonTag == "-" {
+ continue
+ }
+
if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
if zero {
//fmt.Printf("value before change: %+v\n", optsValue.Field(i))
- if jsonTag := f.Tag.Get("json"); jsonTag != "" {
+ if jsonTag != "" {
jsonTagPieces := strings.Split(jsonTag, ",")
if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" {
if v.CanSet() {
diff --git a/provider_client.go b/provider_client.go
index 72daeb0a3e..17e4512743 100644
--- a/provider_client.go
+++ b/provider_client.go
@@ -126,6 +126,36 @@ func (client *ProviderClient) SetToken(t string) {
client.TokenID = t
}
+//Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
+//called because of a 401 response, the caller may pass the previous token. In
+//this case, the reauthentication can be skipped if another thread has already
+//reauthenticated in the meantime. If no previous token is known, an empty
+//string should be passed instead to force unconditional reauthentication.
+func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
+ if client.ReauthFunc == nil {
+ return nil
+ }
+
+ if client.mut == nil {
+ return client.ReauthFunc()
+ }
+ client.mut.Lock()
+ defer client.mut.Unlock()
+
+ client.reauthmut.Lock()
+ client.reauthmut.reauthing = true
+ client.reauthmut.Unlock()
+
+ if previousToken == "" || client.TokenID == previousToken {
+ err = client.ReauthFunc()
+ }
+
+ client.reauthmut.Lock()
+ client.reauthmut.reauthing = false
+ client.reauthmut.Unlock()
+ return
+}
+
// RequestOpts customizes the behavior of the provider.Request() method.
type RequestOpts struct {
// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
@@ -254,21 +284,7 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
}
case http.StatusUnauthorized:
if client.ReauthFunc != nil {
- if client.mut != nil {
- client.mut.Lock()
- client.reauthmut.Lock()
- client.reauthmut.reauthing = true
- client.reauthmut.Unlock()
- if curtok := client.TokenID; curtok == prereqtok {
- err = client.ReauthFunc()
- }
- client.reauthmut.Lock()
- client.reauthmut.reauthing = false
- client.reauthmut.Unlock()
- client.mut.Unlock()
- } else {
- err = client.ReauthFunc()
- }
+ err = client.Reauthenticate(prereqtok)
if err != nil {
e := &ErrUnableToReauthenticate{}
e.ErrOriginal = respErr
@@ -298,6 +314,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
if error401er, ok := errType.(Err401er); ok {
err = error401er.Error401(respErr)
}
+ case http.StatusForbidden:
+ err = ErrDefault403{respErr}
+ if error403er, ok := errType.(Err403er); ok {
+ err = error403er.Error403(respErr)
+ }
case http.StatusNotFound:
err = ErrDefault404{respErr}
if error404er, ok := errType.(Err404er); ok {
diff --git a/script/format b/script/format
index 8ed602fde0..05645a8252 100755
--- a/script/format
+++ b/script/format
@@ -16,8 +16,26 @@ find_files() {
\) -name '*.go'
}
-diff=$(find_files | xargs ${goimports} -d -e 2>&1)
-if [[ -n "${diff}" ]]; then
- echo "${diff}"
- exit 1
+ignore_files=(
+ "./openstack/compute/v2/extensions/quotasets/testing/fixtures.go"
+ "./openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go"
+)
+
+bad_files=$(find_files | xargs ${goimports} -l)
+
+final_files=()
+for bad_file in $bad_files; do
+ found=
+ for ignore_file in "${ignore_files[@]}"; do
+ [[ "${bad_file}" == "${ignore_file}" ]] && { found=1; break; }
+ done
+ [[ -n $found ]] || final_files+=("$bad_file")
+done
+
+if [[ "${#final_files[@]}" -gt 0 ]]; then
+ diff=$(echo "${final_files[@]}" | xargs ${goimports} -d -e 2>&1)
+ if [[ -n "${diff}" ]]; then
+ echo "${diff}"
+ exit 1
+ fi
fi
diff --git a/service_client.go b/service_client.go
index d1a48fea35..145d932a6b 100644
--- a/service_client.go
+++ b/service_client.go
@@ -28,6 +28,10 @@ type ServiceClient struct {
// The microversion of the service to use. Set this to use a particular microversion.
Microversion string
+
+ // MoreHeaders allows users (or Gophercloud) to set service-wide headers on requests. Put another way,
+ // values set in this field will be set on all the HTTP requests the service client sends.
+ MoreHeaders map[string]string
}
// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
@@ -122,3 +126,16 @@ func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) {
opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion
}
}
+
+// Request carries out the HTTP operation for the service client
+func (client *ServiceClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
+ if len(client.MoreHeaders) > 0 {
+ if options == nil {
+ options = new(RequestOpts)
+ }
+ for k, v := range client.MoreHeaders {
+ options.MoreHeaders[k] = v
+ }
+ }
+ return client.ProviderClient.Request(method, url, options)
+}
diff --git a/testing/params_test.go b/testing/params_test.go
index acf392f2ab..18d6704d95 100644
--- a/testing/params_test.go
+++ b/testing/params_test.go
@@ -4,6 +4,7 @@ import (
"net/url"
"reflect"
"testing"
+ "time"
"github.com/gophercloud/gophercloud"
th "github.com/gophercloud/gophercloud/testhelper"
@@ -254,4 +255,22 @@ func TestBuildRequestBody(t *testing.T) {
_, err := gophercloud.BuildRequestBody(failCase.opts, "auth")
th.AssertDeepEquals(t, reflect.TypeOf(failCase.expected), reflect.TypeOf(err))
}
+
+ createdAt := time.Date(2018, 1, 4, 10, 00, 12, 0, time.UTC)
+ var complexFields = struct {
+ Username string `json:"username" required:"true"`
+ CreatedAt *time.Time `json:"-"`
+ }{
+ Username: "jdoe",
+ CreatedAt: &createdAt,
+ }
+
+ expectedComplexFields := map[string]interface{}{
+ "username": "jdoe",
+ }
+
+ actual, err := gophercloud.BuildRequestBody(complexFields, "")
+ th.AssertNoErr(t, err)
+ th.AssertDeepEquals(t, expectedComplexFields, actual)
+
}
diff --git a/testing/provider_client_test.go b/testing/provider_client_test.go
index 514147e727..15385beb0b 100644
--- a/testing/provider_client_test.go
+++ b/testing/provider_client_test.go
@@ -90,6 +90,9 @@ func TestConcurrentReauth(t *testing.T) {
wg := new(sync.WaitGroup)
reqopts := new(gophercloud.RequestOpts)
+ reqopts.MoreHeaders = map[string]string{
+ "X-Auth-Token": prereauthTok,
+ }
for i := 0; i < numconc; i++ {
wg.Add(1)
diff --git a/testing/service_client_test.go b/testing/service_client_test.go
index 904b303ee9..034fdc1d93 100644
--- a/testing/service_client_test.go
+++ b/testing/service_client_test.go
@@ -1,6 +1,8 @@
package testing
import (
+ "fmt"
+ "net/http"
"testing"
"github.com/gophercloud/gophercloud"
@@ -13,3 +15,20 @@ func TestServiceURL(t *testing.T) {
actual := c.ServiceURL("more", "parts", "here")
th.CheckEquals(t, expected, actual)
}
+
+func TestMoreHeaders(t *testing.T) {
+ th.SetupHTTP()
+ defer th.TeardownHTTP()
+ th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ c := new(gophercloud.ServiceClient)
+ c.MoreHeaders = map[string]string{
+ "custom": "header",
+ }
+ c.ProviderClient = new(gophercloud.ProviderClient)
+ resp, err := c.Get(fmt.Sprintf("%s/route", th.Endpoint()), nil, nil)
+ th.AssertNoErr(t, err)
+ th.AssertEquals(t, resp.Request.Header.Get("custom"), "header")
+}