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") +}