From fe438f6d3ecf9253ea3b0db1dcf4448ca8795742 Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Tue, 2 Jan 2018 18:31:32 +0300 Subject: [PATCH 001/120] Add Neutron subnetpool get support Add support to show a Neutron subnetpool through a GET request on /v2.0/subnetpools/{subnetpool_id}. The same command with OpenStack CLI is done with: openstack subnet pool show or neutron subnetpool-show --- .../v2/extensions/subnetpools/doc.go | 8 ++++ .../v2/extensions/subnetpools/requests.go | 6 +++ .../v2/extensions/subnetpools/results.go | 19 ++++++++++ .../subnetpools/testing/fixtures.go | 26 +++++++++++++ .../subnetpools/testing/requests_test.go | 38 +++++++++++++++++++ .../v2/extensions/subnetpools/urls.go | 8 ++++ 6 files changed, 105 insertions(+) diff --git a/openstack/networking/v2/extensions/subnetpools/doc.go b/openstack/networking/v2/extensions/subnetpools/doc.go index 82d09a320d..9d2f46ac9c 100644 --- a/openstack/networking/v2/extensions/subnetpools/doc.go +++ b/openstack/networking/v2/extensions/subnetpools/doc.go @@ -20,5 +20,13 @@ 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) + } */ package subnetpools diff --git a/openstack/networking/v2/extensions/subnetpools/requests.go b/openstack/networking/v2/extensions/subnetpools/requests.go index 506a867d5c..c7471d5927 100644 --- a/openstack/networking/v2/extensions/subnetpools/requests.go +++ b/openstack/networking/v2/extensions/subnetpools/requests.go @@ -66,3 +66,9 @@ 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 +} diff --git a/openstack/networking/v2/extensions/subnetpools/results.go b/openstack/networking/v2/extensions/subnetpools/results.go index b4eb8fb2db..bef3d6ea08 100644 --- a/openstack/networking/v2/extensions/subnetpools/results.go +++ b/openstack/networking/v2/extensions/subnetpools/results.go @@ -9,6 +9,25 @@ import ( "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 +} + // SubnetPool represents a Neutron subnetpool. // A subnetpool is a pool of addresses from which subnets can be allocated. type SubnetPool struct { diff --git a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go index d22074c148..f736493df0 100644 --- a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go +++ b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go @@ -143,3 +143,29 @@ var SubnetPool3 = subnetpools.SubnetPool{ Shared: true, UpdatedAt: "2017-12-28T07:21:27Z", } + +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:01", + "prefixes": [ + "2001:db8::a3/64" + ], + "updated_at": "2018-01-01T00:10:10", + "ip_version": 6, + "shared": false, + "description": "ipv6 prefixes", + "revision_number": 2 + } +} +` diff --git a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go index 9624bf4420..92487c7e60 100644 --- a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go +++ b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go @@ -50,3 +50,41 @@ 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, "2018-01-01T00:00:01") + th.AssertEquals(t, s.UpdatedAt, "2018-01-01T00:10:10") + 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) +} diff --git a/openstack/networking/v2/extensions/subnetpools/urls.go b/openstack/networking/v2/extensions/subnetpools/urls.go index c8fc5cb13e..cdd345a45c 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,7 @@ 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) +} From f05feab319e784a98d4ae5e7292011536c7cc3d4 Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Thu, 4 Jan 2018 06:28:44 +0300 Subject: [PATCH 002/120] Add Neutron subnetpool creation support (#702) * Add Neutron subnetpool creation support Add support to create a Neutron subnetpool through a POST request on /v2.0/subnetpools. The same command with OpenStack CLI is done with: openstack subnet pool create or neutron subnetpool-create * Add acceptance test for the subnetpool creation Add the TestCreateSubnetPool acceptance test and a generic CreateSubnetPool function inside the acceptance tests for sharing with other components. * Rename TestCreateSubnetPool to TestSubnetPoolsCRUD * Fix Subnetpools CreateOpts comments Fix DefaultPrefixLen comment and remove empty line at the beginning of a struct. --- .../v2/extensions/subnetpools/subnetpools.go | 32 ++++++++ .../subnetpools/subnetpools_test.go | 15 ++++ .../v2/extensions/subnetpools/doc.go | 17 +++++ .../v2/extensions/subnetpools/requests.go | 73 +++++++++++++++++++ .../v2/extensions/subnetpools/results.go | 6 ++ .../subnetpools/testing/fixtures.go | 43 +++++++++++ .../subnetpools/testing/requests_test.go | 41 +++++++++++ .../v2/extensions/subnetpools/urls.go | 4 + 8 files changed, 231 insertions(+) create mode 100644 acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go 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..028bb3d2d9 --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go @@ -0,0 +1,32 @@ +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 +} diff --git a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go index 9884a70a9c..d419ee9bea 100644 --- a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go +++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go @@ -10,6 +10,21 @@ 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) + } + + tools.PrintResource(t, subnetPool) +} + func TestSubnetPoolsList(t *testing.T) { client, err := clients.NewNetworkV2Client() if err != nil { diff --git a/openstack/networking/v2/extensions/subnetpools/doc.go b/openstack/networking/v2/extensions/subnetpools/doc.go index 9d2f46ac9c..bef874546f 100644 --- a/openstack/networking/v2/extensions/subnetpools/doc.go +++ b/openstack/networking/v2/extensions/subnetpools/doc.go @@ -28,5 +28,22 @@ Example to Get a Subnetpool 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) + } */ package subnetpools diff --git a/openstack/networking/v2/extensions/subnetpools/requests.go b/openstack/networking/v2/extensions/subnetpools/requests.go index c7471d5927..4828e05aa1 100644 --- a/openstack/networking/v2/extensions/subnetpools/requests.go +++ b/openstack/networking/v2/extensions/subnetpools/requests.go @@ -72,3 +72,76 @@ 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 +} diff --git a/openstack/networking/v2/extensions/subnetpools/results.go b/openstack/networking/v2/extensions/subnetpools/results.go index bef3d6ea08..ff3dc38009 100644 --- a/openstack/networking/v2/extensions/subnetpools/results.go +++ b/openstack/networking/v2/extensions/subnetpools/results.go @@ -28,6 +28,12 @@ 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 +} + // SubnetPool represents a Neutron subnetpool. // A subnetpool is a pool of addresses from which subnets can be allocated. type SubnetPool struct { diff --git a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go index f736493df0..fd279f1d38 100644 --- a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go +++ b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go @@ -169,3 +169,46 @@ const SubnetPoolGetResult = ` } } ` + +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" + } +} +` diff --git a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go index 92487c7e60..b80d7ef032 100644 --- a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go +++ b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go @@ -88,3 +88,44 @@ func TestGet(t *testing.T) { 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") +} diff --git a/openstack/networking/v2/extensions/subnetpools/urls.go b/openstack/networking/v2/extensions/subnetpools/urls.go index cdd345a45c..5ba98ae359 100644 --- a/openstack/networking/v2/extensions/subnetpools/urls.go +++ b/openstack/networking/v2/extensions/subnetpools/urls.go @@ -19,3 +19,7 @@ func listURL(c *gophercloud.ServiceClient) string { func getURL(c *gophercloud.ServiceClient, id string) string { return resourceURL(c, id) } + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} From 800a4c0d57fbe8403b0bb6f13a8340c8fc990ad5 Mon Sep 17 00:00:00 2001 From: David Lyle Date: Wed, 3 Jan 2018 21:57:46 -0700 Subject: [PATCH 003/120] Flavor Extra Spec Delete --- acceptance/openstack/compute/v2/flavors_test.go | 6 ++++++ openstack/compute/v2/flavors/doc.go | 7 +++++++ openstack/compute/v2/flavors/requests.go | 9 +++++++++ openstack/compute/v2/flavors/results.go | 6 ++++++ openstack/compute/v2/flavors/testing/fixtures.go | 9 +++++++++ openstack/compute/v2/flavors/testing/requests_test.go | 9 +++++++++ openstack/compute/v2/flavors/urls.go | 4 ++++ 7 files changed, 50 insertions(+) diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go index bb6c5c41d9..817fcc5c58 100644 --- a/acceptance/openstack/compute/v2/flavors_test.go +++ b/acceptance/openstack/compute/v2/flavors_test.go @@ -176,6 +176,11 @@ func TestFlavorExtraSpecsCRUD(t *testing.T) { } tools.PrintResource(t, createdExtraSpecs) + err = flavors.DeleteExtraSpec(client, flavor.ID, "hw:cpu_policy").ExtractErr() + if err != nil { + t.Fatalf("Unable to delete ExtraSpec: %v\n", err) + } + allExtraSpecs, err := flavors.ListExtraSpecs(client, flavor.ID).Extract() if err != nil { t.Fatalf("Unable to get flavor extra_specs: %v", err) @@ -189,4 +194,5 @@ func TestFlavorExtraSpecsCRUD(t *testing.T) { } tools.PrintResource(t, spec) } + } diff --git a/openstack/compute/v2/flavors/doc.go b/openstack/compute/v2/flavors/doc.go index 867d53a819..31e8e2733f 100644 --- a/openstack/compute/v2/flavors/doc.go +++ b/openstack/compute/v2/flavors/doc.go @@ -99,5 +99,12 @@ Example to Get Extra Specs for a Flavor fmt.Printf("%+v", extraSpecs) +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..6edae4c9ce 100644 --- a/openstack/compute/v2/flavors/requests.go +++ b/openstack/compute/v2/flavors/requests.go @@ -231,6 +231,15 @@ func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts C 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..e0ab64bf37 100644 --- a/openstack/compute/v2/flavors/results.go +++ b/openstack/compute/v2/flavors/results.go @@ -223,6 +223,12 @@ type GetExtraSpecResult 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..0c5486b152 100644 --- a/openstack/compute/v2/flavors/testing/fixtures.go +++ b/openstack/compute/v2/flavors/testing/fixtures.go @@ -78,3 +78,12 @@ func HandleExtraSpecsCreateSuccessfully(t *testing.T) { fmt.Fprintf(w, ExtraSpecsGetBody) }) } + +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..f34cb3c1a3 100644 --- a/openstack/compute/v2/flavors/testing/requests_test.go +++ b/openstack/compute/v2/flavors/testing/requests_test.go @@ -336,3 +336,12 @@ func TestFlavorExtraSpecsCreate(t *testing.T) { 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..c9d43c5b46 100644 --- a/openstack/compute/v2/flavors/urls.go +++ b/openstack/compute/v2/flavors/urls.go @@ -39,3 +39,7 @@ 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 extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} From b2bf8a613b41baa2a8b56a6a8483321a84520dfa Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy Date: Mon, 8 Jan 2018 07:05:15 +0300 Subject: [PATCH 004/120] Compute availability zones info support (#704) * Availability zones info support (#320) Add ability to get list and detailed AZs info * Style fixes * Change test fixture to proper * Delete useless method of `ServerAvailabilityZoneExt` * Code improvements * Change `AvailabilityZoneInfo` type from `struct` to `slice` * Add custom `UnmarshalJSON` to `AvailabilityZoneInfo` * Get rid of 'OS' prefixes in variable's names * Update doc.go * Code refactoring * Rename `ServiceOfState` to `ServiceState` * Change type `Services` from `struct` to `map[string]ServiceState` * Delete useless type `AvailabilityZoneInfo` * Update doc.go * Acceptance tests for #704 * Change struct tag `updated_at` to `-` --- .../compute/v2/availabilityzones_test.go | 53 +++++ .../v2/extensions/availabilityzones/doc.go | 43 +++- .../extensions/availabilityzones/requests.go | 20 ++ .../extensions/availabilityzones/results.go | 66 +++++- .../availabilityzones/testing/doc.go | 2 + .../availabilityzones/testing/fixtures.go | 197 ++++++++++++++++++ .../testing/requests_test.go | 41 ++++ .../v2/extensions/availabilityzones/urls.go | 11 + 8 files changed, 425 insertions(+), 8 deletions(-) create mode 100644 acceptance/openstack/compute/v2/availabilityzones_test.go create mode 100644 openstack/compute/v2/extensions/availabilityzones/requests.go create mode 100644 openstack/compute/v2/extensions/availabilityzones/testing/doc.go create mode 100644 openstack/compute/v2/extensions/availabilityzones/testing/fixtures.go create mode 100644 openstack/compute/v2/extensions/availabilityzones/testing/requests_test.go create mode 100644 openstack/compute/v2/extensions/availabilityzones/urls.go diff --git a/acceptance/openstack/compute/v2/availabilityzones_test.go b/acceptance/openstack/compute/v2/availabilityzones_test.go new file mode 100644 index 0000000000..3e82128422 --- /dev/null +++ b/acceptance/openstack/compute/v2/availabilityzones_test.go @@ -0,0 +1,53 @@ +// +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" +) + +func TestAvailabilityZonesList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := availabilityzones.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list availability zones info: %v", err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + t.Fatalf("Unable to extract availability zones info: %v", err) + } + + for _, zoneInfo := range availabilityZoneInfo { + tools.PrintResource(t, zoneInfo) + } +} + +func TestAvailabilityZonesListDetail(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := availabilityzones.ListDetail(client).AllPages() + if err != nil { + t.Fatalf("Unable to list availability zones detailed info: %v", err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + t.Fatalf("Unable to extract availability zones detailed info: %v", err) + } + + for _, zoneInfo := range availabilityZoneInfo { + tools.PrintResource(t, zoneInfo) + } +} diff --git a/openstack/compute/v2/extensions/availabilityzones/doc.go b/openstack/compute/v2/extensions/availabilityzones/doc.go index 80464ba399..c5429add0a 100644 --- a/openstack/compute/v2/extensions/availabilityzones/doc.go +++ b/openstack/compute/v2/extensions/availabilityzones/doc.go @@ -1,26 +1,57 @@ /* -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 availabilityzones.ServerAvailabilityZoneExt } - var allServers []ServerWithAZ - allPages, err := servers.List(client, nil).AllPages() if err != nil { panic("Unable to retrieve servers: %s", err) } - err = servers.ExtractServersInto(allPages, &allServers) if err != nil { panic("Unable to extract servers: %s", err) } - 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..82ee42e480 100644 --- a/openstack/compute/v2/extensions/availabilityzones/results.go +++ b/openstack/compute/v2/extensions/availabilityzones/results.go @@ -1,8 +1,70 @@ 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" +) + +// ServerExt 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"` } + +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 +} + +type Services map[string]ServiceState + +type Hosts map[string]Services + +// 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 +} + +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") +} From 28db6d7217b6079cf27b97a8484c64a00e48452e Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Mon, 8 Jan 2018 04:10:10 +0000 Subject: [PATCH 005/120] Compute v2: Update docs for Availability Zones --- .../compute/v2/extensions/availabilityzones/doc.go | 4 ++++ .../compute/v2/extensions/availabilityzones/results.go | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openstack/compute/v2/extensions/availabilityzones/doc.go b/openstack/compute/v2/extensions/availabilityzones/doc.go index c5429add0a..29b554d213 100644 --- a/openstack/compute/v2/extensions/availabilityzones/doc.go +++ b/openstack/compute/v2/extensions/availabilityzones/doc.go @@ -9,15 +9,19 @@ Example of Extend server result with Availability Zone Information: servers.Server availabilityzones.ServerAvailabilityZoneExt } + var allServers []ServerWithAZ + allPages, err := servers.List(client, nil).AllPages() if err != nil { panic("Unable to retrieve servers: %s", err) } + err = servers.ExtractServersInto(allPages, &allServers) if err != nil { panic("Unable to extract servers: %s", err) } + for _, server := range allServers { fmt.Println(server.AvailabilityZone) } diff --git a/openstack/compute/v2/extensions/availabilityzones/results.go b/openstack/compute/v2/extensions/availabilityzones/results.go index 82ee42e480..d48a0ea858 100644 --- a/openstack/compute/v2/extensions/availabilityzones/results.go +++ b/openstack/compute/v2/extensions/availabilityzones/results.go @@ -8,12 +8,13 @@ import ( "github.com/gophercloud/gophercloud/pagination" ) -// ServerExt is an extension to the base Server object +// 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"` @@ -38,11 +39,14 @@ func (r *ServiceState) UnmarshalJSON(b []byte) error { 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 -// The current state of the availability zone +// ZoneState represents the current state of the availability zone. type ZoneState struct { // Returns true if the availability zone is available Available bool `json:"available"` @@ -61,6 +65,8 @@ 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"` From 4f997f57855b1849b8a1508b605111e3aef39d74 Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Thu, 11 Jan 2018 06:42:52 +0300 Subject: [PATCH 006/120] Fix Orchestration TestGetRRFileContents (#709) * Fix "Content-Type" in orchestration env tests We need to use "application/json" instead of "application/jason" * Fix Orchestration's TestGetRRFileContents Fix environments unit test "TestGetRRFileContents" by updating the "expectedParsed" to be a valid representation of a Heat's resource_registry definition. Also update environment's fileMaps to sucessfully apply the "fixFileRefs" method. * Fix Orchestration TestGetRRFileContents comments --- .../v1/stacks/environment_test.go | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) 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, + }, }, }, } From 993bca9e3fac8efeaa6cfe74b0b7b00468d8fcf5 Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Thu, 11 Jan 2018 06:43:56 +0300 Subject: [PATCH 007/120] Fix Orchestration TestTemplateParsing (#710) * Fix "Content-Type" in orchestration template tests We need to use "application/json" instead of "application/jason" * Fix flavor field in orchestration fixtures Set "default" field to be a string in "ValidJSONTemplate", "ValidYAMLTemplate", "InvalidTemplateNoVersion", "InvalidEnvironment", "ValidJSONTemplateParsed" orchestration templates test fixtures. That also fixes "TestTemplateParsing" orchestration template unit test. --- openstack/orchestration/v1/stacks/fixtures.go | 10 +++++----- openstack/orchestration/v1/stacks/template_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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_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) }) From 35ab3f13f69349f99ba8b9c9c36a7031ae2963dd Mon Sep 17 00:00:00 2001 From: David Lyle Date: Tue, 9 Jan 2018 23:24:51 -0700 Subject: [PATCH 008/120] Flavor Extra Spec Update --- .../openstack/compute/v2/flavors_test.go | 9 +++++ openstack/compute/v2/flavors/doc.go | 14 +++++++ openstack/compute/v2/flavors/requests.go | 37 +++++++++++++++++++ openstack/compute/v2/flavors/results.go | 6 +++ .../compute/v2/flavors/testing/fixtures.go | 29 ++++++++++++++- .../v2/flavors/testing/requests_test.go | 14 +++++++ openstack/compute/v2/flavors/urls.go | 4 ++ 7 files changed, 112 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go index 817fcc5c58..b20a5555a0 100644 --- a/acceptance/openstack/compute/v2/flavors_test.go +++ b/acceptance/openstack/compute/v2/flavors_test.go @@ -181,6 +181,15 @@ func TestFlavorExtraSpecsCRUD(t *testing.T) { t.Fatalf("Unable to delete ExtraSpec: %v\n", err) } + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-BETTER", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(client, flavor.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update flavor extra_specs: %v", err) + } + tools.PrintResource(t, updatedExtraSpec) + allExtraSpecs, err := flavors.ListExtraSpecs(client, flavor.ID).Extract() if err != nil { t.Fatalf("Unable to get flavor extra_specs: %v", err) diff --git a/openstack/compute/v2/flavors/doc.go b/openstack/compute/v2/flavors/doc.go index 31e8e2733f..0edc478098 100644 --- a/openstack/compute/v2/flavors/doc.go +++ b/openstack/compute/v2/flavors/doc.go @@ -99,6 +99,20 @@ 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" diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go index 6edae4c9ce..8144dde846 100644 --- a/openstack/compute/v2/flavors/requests.go +++ b/openstack/compute/v2/flavors/requests.go @@ -231,6 +231,43 @@ func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts C return } +// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateExtraSpecOptsBuilder interface { + ToExtraSpecUpdateMap() (map[string]string, string, error) +} + +// ToExtraSpecUpdateMap assembles a body for an Update request based on the +// contents of a ExtraSpecOpts. +func (opts ExtraSpecsOpts) ToExtraSpecUpdateMap() (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.ToExtraSpecUpdateMap() + 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) { diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go index e0ab64bf37..49e3bed936 100644 --- a/openstack/compute/v2/flavors/results.go +++ b/openstack/compute/v2/flavors/results.go @@ -223,6 +223,12 @@ 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 { diff --git a/openstack/compute/v2/flavors/testing/fixtures.go b/openstack/compute/v2/flavors/testing/fixtures.go index 0c5486b152..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") @@ -79,6 +91,21 @@ func HandleExtraSpecsCreateSuccessfully(t *testing.T) { }) } +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") diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go index f34cb3c1a3..7ad667c482 100644 --- a/openstack/compute/v2/flavors/testing/requests_test.go +++ b/openstack/compute/v2/flavors/testing/requests_test.go @@ -337,6 +337,20 @@ func TestFlavorExtraSpecsCreate(t *testing.T) { 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() diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go index c9d43c5b46..8620dd78ad 100644 --- a/openstack/compute/v2/flavors/urls.go +++ b/openstack/compute/v2/flavors/urls.go @@ -40,6 +40,10 @@ 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) } From 57f1c660ea6fe700cde9d9c964c820ec985c731e Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Fri, 12 Jan 2018 06:17:04 +0300 Subject: [PATCH 009/120] Add Neutron subnetpool update support (#706) * Add Neutron subnetpool update support Add support to update a Neutron subnetpool through a PUT request on /v2.0/subnetpools. The same command with OpenStack CLI is done with: openstack subnet pool set or neutron subnetpool-update * Add a documentation about subnetpool updating * Add subnetpool update to acceptance tests * Cleanup Neutron subnetpools docs Remove some garbage dots from titles because we don't really need them in the documentation. * Update Neutron subnetpools update fileds types We need to use a "*string" to "AddressScopeID" and "Description" fileds, "*bool" to a IsDefault field and "*int" to a DefaultQuota field. That is because we wan't to have a possibility to update those fields to empty or false or zero values. * Fix TestSubnetPoolsCRUD update test Variable newSubnetPool is new so it should be initialized. --- .../subnetpools/subnetpools_test.go | 12 ++++ .../v2/extensions/subnetpools/doc.go | 19 ++++- .../v2/extensions/subnetpools/requests.go | 71 +++++++++++++++++++ .../v2/extensions/subnetpools/results.go | 6 ++ .../subnetpools/testing/fixtures.go | 43 +++++++++++ .../subnetpools/testing/requests_test.go | 46 ++++++++++++ .../v2/extensions/subnetpools/urls.go | 4 ++ 7 files changed, 199 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go index d419ee9bea..8dc50b6a39 100644 --- a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go +++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go @@ -23,6 +23,18 @@ func TestSubnetPoolsCRUD(t *testing.T) { } tools.PrintResource(t, subnetPool) + + newName := tools.RandomString("TESTACC-", 8) + updateOpts := &subnetpools.UpdateOpts{ + Name: newName, + } + + newSubnetPool, err := subnetpools.Update(client, subnetPool.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update the subnetpool: %v", err) + } + + tools.PrintResource(t, newSubnetPool) } func TestSubnetPoolsList(t *testing.T) { diff --git a/openstack/networking/v2/extensions/subnetpools/doc.go b/openstack/networking/v2/extensions/subnetpools/doc.go index bef874546f..3e8bae90df 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, @@ -29,7 +29,7 @@ Example to Get a Subnetpool panic(err) } -Example to Create a new Subnetpool. +Example to Create a new Subnetpool subnetPoolName := "private_pool" subnetPoolPrefixes := []string{ @@ -45,5 +45,20 @@ Example to Create a new Subnetpool. 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) + } */ package subnetpools diff --git a/openstack/networking/v2/extensions/subnetpools/requests.go b/openstack/networking/v2/extensions/subnetpools/requests.go index 4828e05aa1..9bbd3957e9 100644 --- a/openstack/networking/v2/extensions/subnetpools/requests.go +++ b/openstack/networking/v2/extensions/subnetpools/requests.go @@ -145,3 +145,74 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create }) 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 +} diff --git a/openstack/networking/v2/extensions/subnetpools/results.go b/openstack/networking/v2/extensions/subnetpools/results.go index ff3dc38009..0d60ff3e20 100644 --- a/openstack/networking/v2/extensions/subnetpools/results.go +++ b/openstack/networking/v2/extensions/subnetpools/results.go @@ -34,6 +34,12 @@ 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 +} + // SubnetPool represents a Neutron subnetpool. // A subnetpool is a pool of addresses from which subnets can be allocated. type SubnetPool struct { diff --git a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go index fd279f1d38..f70b41773e 100644 --- a/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go +++ b/openstack/networking/v2/extensions/subnetpools/testing/fixtures.go @@ -212,3 +212,46 @@ const SubnetPoolCreateResult = ` } } ` + +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 b80d7ef032..ec02227a79 100644 --- a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go +++ b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go @@ -129,3 +129,49 @@ func TestCreate(t *testing.T) { 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, "") +} diff --git a/openstack/networking/v2/extensions/subnetpools/urls.go b/openstack/networking/v2/extensions/subnetpools/urls.go index 5ba98ae359..2b02dd6a93 100644 --- a/openstack/networking/v2/extensions/subnetpools/urls.go +++ b/openstack/networking/v2/extensions/subnetpools/urls.go @@ -23,3 +23,7 @@ func getURL(c *gophercloud.ServiceClient, id string) string { func createURL(c *gophercloud.ServiceClient) string { return rootURL(c) } + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} From df0b021b1d39cc2f8f12a333615200a3234e9f41 Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Sat, 13 Jan 2018 23:50:34 +0300 Subject: [PATCH 010/120] Add get request in subnetpools acceptance tests --- .../v2/extensions/subnetpools/subnetpools_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go index 8dc50b6a39..eeb3e19afb 100644 --- a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go +++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go @@ -29,11 +29,16 @@ func TestSubnetPoolsCRUD(t *testing.T) { Name: newName, } - newSubnetPool, err := subnetpools.Update(client, subnetPool.ID, updateOpts).Extract() + _, 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) } From 9c7c97cb37e9f6724d09dad32e681d97dca37d6d Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Sun, 14 Jan 2018 00:08:39 +0300 Subject: [PATCH 011/120] Implement Neutron subnetpools delete support Add a Delete request, unit test and acceptance tests with acceptance reusable "DeleteSubnetPool" function. --- .../v2/extensions/subnetpools/subnetpools.go | 13 +++++++++++++ .../v2/extensions/subnetpools/subnetpools_test.go | 1 + .../networking/v2/extensions/subnetpools/doc.go | 8 ++++++++ .../v2/extensions/subnetpools/requests.go | 6 ++++++ .../v2/extensions/subnetpools/results.go | 6 ++++++ .../subnetpools/testing/requests_test.go | 14 ++++++++++++++ .../networking/v2/extensions/subnetpools/urls.go | 4 ++++ 7 files changed, 52 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go index 028bb3d2d9..fdf6318d52 100644 --- a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go +++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools.go @@ -30,3 +30,16 @@ func CreateSubnetPool(t *testing.T, client *gophercloud.ServiceClient) (*subnetp 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 8dc50b6a39..6d51f19091 100644 --- a/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go +++ b/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go @@ -21,6 +21,7 @@ func TestSubnetPoolsCRUD(t *testing.T) { if err != nil { t.Fatalf("Unable to create a subnetpool: %v", err) } + defer DeleteSubnetPool(t, client, subnetPool.ID) tools.PrintResource(t, subnetPool) diff --git a/openstack/networking/v2/extensions/subnetpools/doc.go b/openstack/networking/v2/extensions/subnetpools/doc.go index 3e8bae90df..2a9fe63dd4 100644 --- a/openstack/networking/v2/extensions/subnetpools/doc.go +++ b/openstack/networking/v2/extensions/subnetpools/doc.go @@ -60,5 +60,13 @@ Example to Update a Subnetpool 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 9bbd3957e9..61f660d5ab 100644 --- a/openstack/networking/v2/extensions/subnetpools/requests.go +++ b/openstack/networking/v2/extensions/subnetpools/requests.go @@ -216,3 +216,9 @@ func Update(c *gophercloud.ServiceClient, subnetPoolID string, opts UpdateOptsBu }) 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 0d60ff3e20..a490f05401 100644 --- a/openstack/networking/v2/extensions/subnetpools/results.go +++ b/openstack/networking/v2/extensions/subnetpools/results.go @@ -40,6 +40,12 @@ 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 { diff --git a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go index ec02227a79..d4f62319ad 100644 --- a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go +++ b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go @@ -175,3 +175,17 @@ func TestUpdate(t *testing.T) { 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 2b02dd6a93..a05062c96d 100644 --- a/openstack/networking/v2/extensions/subnetpools/urls.go +++ b/openstack/networking/v2/extensions/subnetpools/urls.go @@ -27,3 +27,7 @@ func createURL(c *gophercloud.ServiceClient) string { func updateURL(c *gophercloud.ServiceClient, id string) string { return resourceURL(c, id) } + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} From 7e80bca56edbda494d00ab082547c16086269bac Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy Date: Sun, 14 Jan 2018 05:48:30 +0300 Subject: [PATCH 012/120] Compute v2: Show Hypervisor Statistics (#720) * Hypervisors statistics support * Add ability to get hypervisors stats * Add unit tests * Update doc.go * Add acceptance tests * Style fixes * Rename few vars --- .../openstack/compute/v2/hypervisors_test.go | 14 +++++ .../compute/v2/extensions/hypervisors/doc.go | 14 ++++- .../v2/extensions/hypervisors/requests.go | 8 +++ .../v2/extensions/hypervisors/results.go | 54 +++++++++++++++++++ .../hypervisors/testing/fixtures.go | 43 +++++++++++++++ .../hypervisors/testing/requests_test.go | 12 +++++ .../compute/v2/extensions/hypervisors/urls.go | 4 ++ 7 files changed, 147 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/compute/v2/hypervisors_test.go b/acceptance/openstack/compute/v2/hypervisors_test.go index 627dc76345..c9e9071358 100644 --- a/acceptance/openstack/compute/v2/hypervisors_test.go +++ b/acceptance/openstack/compute/v2/hypervisors_test.go @@ -30,3 +30,17 @@ func TestHypervisorsList(t *testing.T) { tools.PrintResource(t, h) } } + +func TestHypervisorsStatistics(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + hypervisorsStats, err := hypervisors.GetStatistics(client).Extract() + if err != nil { + t.Fatalf("Unable to get hypervisors statistics: %v", err) + } + + tools.PrintResource(t, hypervisorsStats) +} diff --git a/openstack/compute/v2/extensions/hypervisors/doc.go b/openstack/compute/v2/extensions/hypervisors/doc.go index cf603a9f32..05c8c5df5b 100644 --- a/openstack/compute/v2/extensions/hypervisors/doc.go +++ b/openstack/compute/v2/extensions/hypervisors/doc.go @@ -1,6 +1,6 @@ /* -Package hypervisors returns details about the hypervisors in the OpenStack -cloud. +Package hypervisors returns details about the hypervisors and shows +summary statistics for all hypervisors over all compute nodes in the OpenStack cloud. Example of Retrieving Details of All Hypervisors @@ -17,5 +17,15 @@ 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) + */ package hypervisors diff --git a/openstack/compute/v2/extensions/hypervisors/requests.go b/openstack/compute/v2/extensions/hypervisors/requests.go index 57cc19a71f..e8151c06d6 100644 --- a/openstack/compute/v2/extensions/hypervisors/requests.go +++ b/openstack/compute/v2/extensions/hypervisors/requests.go @@ -11,3 +11,11 @@ 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 +} diff --git a/openstack/compute/v2/extensions/hypervisors/results.go b/openstack/compute/v2/extensions/hypervisors/results.go index d4e87de083..c8b1cbaa91 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,56 @@ func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) { err := (p.(HypervisorPage)).ExtractInto(&h) return h.Hypervisors, 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 +} diff --git a/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go b/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go index 45a32de18d..faeea676b7 100644 --- a/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go +++ b/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go @@ -83,6 +83,25 @@ 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 + } +} +` + var ( HypervisorFake = hypervisors.Hypervisor{ CPUInfo: hypervisors.CPUInfo{ @@ -123,8 +142,32 @@ 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, + } ) +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") diff --git a/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go b/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go index 1da3b1de50..0c200ab6cd 100644 --- a/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go +++ b/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go @@ -51,3 +51,15 @@ 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) +} diff --git a/openstack/compute/v2/extensions/hypervisors/urls.go b/openstack/compute/v2/extensions/hypervisors/urls.go index 5e6f679e96..5c51db96e3 100644 --- a/openstack/compute/v2/extensions/hypervisors/urls.go +++ b/openstack/compute/v2/extensions/hypervisors/urls.go @@ -5,3 +5,7 @@ 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") +} From 050e933d542f7a12e43034f0f456f770d4c475b3 Mon Sep 17 00:00:00 2001 From: Ildar Svetlov Date: Tue, 16 Jan 2018 07:21:11 +0400 Subject: [PATCH 013/120] Compute service list support (#716) * Add Compute service list support * Remove ForcedDown field, add UpdatedAt field --- .../openstack/compute/v2/services_test.go | 32 +++++ .../compute/v2/extensions/services/doc.go | 22 ++++ .../v2/extensions/services/requests.go | 13 ++ .../compute/v2/extensions/services/results.go | 73 +++++++++++ .../extensions/services/testing/fixtures.go | 123 ++++++++++++++++++ .../services/testing/requests_test.go | 42 ++++++ .../compute/v2/extensions/services/urls.go | 7 + 7 files changed, 312 insertions(+) create mode 100644 acceptance/openstack/compute/v2/services_test.go create mode 100644 openstack/compute/v2/extensions/services/doc.go create mode 100644 openstack/compute/v2/extensions/services/requests.go create mode 100644 openstack/compute/v2/extensions/services/results.go create mode 100644 openstack/compute/v2/extensions/services/testing/fixtures.go create mode 100644 openstack/compute/v2/extensions/services/testing/requests_test.go create mode 100644 openstack/compute/v2/extensions/services/urls.go diff --git a/acceptance/openstack/compute/v2/services_test.go b/acceptance/openstack/compute/v2/services_test.go new file mode 100644 index 0000000000..b949b70fa2 --- /dev/null +++ b/acceptance/openstack/compute/v2/services_test.go @@ -0,0 +1,32 @@ +// +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" +) + +func TestServicesList(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + allPages, err := services.List(client).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/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") +} From 936e48acce6798b2efd69db8ac3c8ffe7f67784f Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy Date: Wed, 17 Jan 2018 05:52:53 +0300 Subject: [PATCH 014/120] Compute v2: Get Hypervisor Details (#722) * Add support to get specific hypervisor by ID * Add unit tests * Update doc.go * Add acceptance tests * Fix conversion `int` -> `string` --- .../openstack/compute/v2/hypervisors_test.go | 39 +++++++++++++ .../compute/v2/extensions/hypervisors/doc.go | 14 ++++- .../v2/extensions/hypervisors/requests.go | 11 ++++ .../v2/extensions/hypervisors/results.go | 13 +++++ .../hypervisors/testing/fixtures.go | 56 +++++++++++++++++++ .../hypervisors/testing/requests_test.go | 12 ++++ .../compute/v2/extensions/hypervisors/urls.go | 4 ++ 7 files changed, 147 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/compute/v2/hypervisors_test.go b/acceptance/openstack/compute/v2/hypervisors_test.go index c9e9071358..567cfaac30 100644 --- a/acceptance/openstack/compute/v2/hypervisors_test.go +++ b/acceptance/openstack/compute/v2/hypervisors_test.go @@ -3,8 +3,10 @@ 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" @@ -31,6 +33,25 @@ func TestHypervisorsList(t *testing.T) { } } +func TestHypervisorsGet(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + hypervisorID, err := getHypervisorID(t, client) + if err != nil { + t.Fatal(err) + } + + hypervisor, err := hypervisors.Get(client, hypervisorID).Extract() + if err != nil { + t.Fatalf("Unable to get hypervisor: %v", err) + } + + tools.PrintResource(t, hypervisor) +} + func TestHypervisorsStatistics(t *testing.T) { client, err := clients.NewComputeV2Client() if err != nil { @@ -44,3 +65,21 @@ func TestHypervisorsStatistics(t *testing.T) { tools.PrintResource(t, hypervisorsStats) } + +func getHypervisorID(t *testing.T, client *gophercloud.ServiceClient) (int, error) { + allPages, err := hypervisors.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list hypervisors: %v", err) + } + + allHypervisors, err := hypervisors.ExtractHypervisors(allPages) + if err != nil { + t.Fatalf("Unable to extract hypervisors") + } + + for _, h := range allHypervisors { + return h.ID, nil + } + + return 0, fmt.Errorf("Unable to get hypervisor ID") +} diff --git a/openstack/compute/v2/extensions/hypervisors/doc.go b/openstack/compute/v2/extensions/hypervisors/doc.go index 05c8c5df5b..9d72f8fe61 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 and shows -summary statistics for all hypervisors over all compute nodes 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 diff --git a/openstack/compute/v2/extensions/hypervisors/requests.go b/openstack/compute/v2/extensions/hypervisors/requests.go index e8151c06d6..f5efff85c7 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" ) @@ -19,3 +21,12 @@ func GetStatistics(client *gophercloud.ServiceClient) (r StatisticsResult) { }) 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 +} diff --git a/openstack/compute/v2/extensions/hypervisors/results.go b/openstack/compute/v2/extensions/hypervisors/results.go index c8b1cbaa91..a35a0341ce 100644 --- a/openstack/compute/v2/extensions/hypervisors/results.go +++ b/openstack/compute/v2/extensions/hypervisors/results.go @@ -191,6 +191,19 @@ func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) { 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 { diff --git a/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go b/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go index faeea676b7..d261fa658f 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" @@ -102,6 +103,50 @@ const HypervisorsStatisticsBody = ` } ` +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 + } +} +` + var ( HypervisorFake = hypervisors.Hypervisor{ CPUInfo: hypervisors.CPUInfo{ @@ -177,3 +222,14 @@ 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) + }) +} diff --git a/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go b/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go index 0c200ab6cd..b89f6e76c8 100644 --- a/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go +++ b/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go @@ -63,3 +63,15 @@ func TestHypervisorsStatistics(t *testing.T) { 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) +} diff --git a/openstack/compute/v2/extensions/hypervisors/urls.go b/openstack/compute/v2/extensions/hypervisors/urls.go index 5c51db96e3..19107fe53d 100644 --- a/openstack/compute/v2/extensions/hypervisors/urls.go +++ b/openstack/compute/v2/extensions/hypervisors/urls.go @@ -9,3 +9,7 @@ func hypervisorsListDetailURL(c *gophercloud.ServiceClient) string { 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) +} From 1a43566306cb8cebad8cae85c67b15b3c254f316 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Thu, 18 Jan 2018 04:45:06 +0000 Subject: [PATCH 015/120] Prevent Recursive BuildRequestBody This commit prevents Gophercloud from performing a recursive BuildRequestBody when the field's JSON tag is "-". --- params.go | 7 ++++++- testing/params_test.go | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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/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) + } From 8a6dfa8264e8b64523272c7a205e5f08bb6c118f Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Thu, 18 Jan 2018 20:43:55 -0700 Subject: [PATCH 016/120] Compute v2: Flavor Access Remove (#688) * Compute v2: Flavor Access Remove * Correcting method names --- .../openstack/compute/v2/flavors_test.go | 13 +++++++ openstack/compute/v2/flavors/doc.go | 13 +++++++ openstack/compute/v2/flavors/requests.go | 38 +++++++++++++++++-- openstack/compute/v2/flavors/results.go | 8 +++- .../v2/flavors/testing/requests_test.go | 38 +++++++++++++++++++ 5 files changed, 105 insertions(+), 5 deletions(-) diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go index b20a5555a0..b7768b380a 100644 --- a/acceptance/openstack/compute/v2/flavors_test.go +++ b/acceptance/openstack/compute/v2/flavors_test.go @@ -152,6 +152,19 @@ func TestFlavorAccessCRUD(t *testing.T) { for _, access := range accessList { tools.PrintResource(t, access) } + + removeAccessOpts := flavors.RemoveAccessOpts{ + Tenant: project.ID, + } + + accessList, err = flavors.RemoveAccess(client, flavor.ID, removeAccessOpts).Extract() + if err != nil { + t.Fatalf("Unable to remove access to flavor: %v", err) + } + + for _, access := range accessList { + tools.PrintResource(t, access) + } } func TestFlavorExtraSpecsCRUD(t *testing.T) { diff --git a/openstack/compute/v2/flavors/doc.go b/openstack/compute/v2/flavors/doc.go index 0edc478098..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" diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go index 8144dde846..e7041df059 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 diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go index 49e3bed936..5fe4e6cfef 100644 --- a/openstack/compute/v2/flavors/results.go +++ b/openstack/compute/v2/flavors/results.go @@ -158,12 +158,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) { diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go index 7ad667c482..990d75d495 100644 --- a/openstack/compute/v2/flavors/testing/requests_test.go +++ b/openstack/compute/v2/flavors/testing/requests_test.go @@ -300,6 +300,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() From 8ca2d18cc1409027815e2a5085152d8a224ffa00 Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Fri, 19 Jan 2018 06:47:02 +0300 Subject: [PATCH 017/120] Compute v2: Get Hypervisor Uptime (#724) * Add support to get hypervisor uptime specified by ID * Add unit test * Add acceptance test --- .../openstack/compute/v2/hypervisors_test.go | 21 +++++++++++- .../compute/v2/extensions/hypervisors/doc.go | 10 ++++++ .../v2/extensions/hypervisors/requests.go | 9 ++++++ .../v2/extensions/hypervisors/results.go | 32 +++++++++++++++++++ .../hypervisors/testing/fixtures.go | 30 +++++++++++++++++ .../hypervisors/testing/requests_test.go | 12 +++++++ .../compute/v2/extensions/hypervisors/urls.go | 4 +++ 7 files changed, 117 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/compute/v2/hypervisors_test.go b/acceptance/openstack/compute/v2/hypervisors_test.go index 567cfaac30..15aff3de74 100644 --- a/acceptance/openstack/compute/v2/hypervisors_test.go +++ b/acceptance/openstack/compute/v2/hypervisors_test.go @@ -52,7 +52,7 @@ func TestHypervisorsGet(t *testing.T) { tools.PrintResource(t, hypervisor) } -func TestHypervisorsStatistics(t *testing.T) { +func TestHypervisorsGetStatistics(t *testing.T) { client, err := clients.NewComputeV2Client() if err != nil { t.Fatalf("Unable to create a compute client: %v", err) @@ -66,6 +66,25 @@ func TestHypervisorsStatistics(t *testing.T) { tools.PrintResource(t, hypervisorsStats) } +func TestHypervisorsGetUptime(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + hypervisorID, err := getHypervisorID(t, client) + if err != nil { + t.Fatal(err) + } + + hypervisor, err := hypervisors.GetUptime(client, hypervisorID).Extract() + if err != nil { + t.Fatalf("Unable to hypervisor uptime: %v", err) + } + + tools.PrintResource(t, hypervisor) +} + func getHypervisorID(t *testing.T, client *gophercloud.ServiceClient) (int, error) { allPages, err := hypervisors.List(client).AllPages() if err != nil { diff --git a/openstack/compute/v2/extensions/hypervisors/doc.go b/openstack/compute/v2/extensions/hypervisors/doc.go index 9d72f8fe61..b8eb699edb 100644 --- a/openstack/compute/v2/extensions/hypervisors/doc.go +++ b/openstack/compute/v2/extensions/hypervisors/doc.go @@ -37,5 +37,15 @@ Example of Show Hypervisor Statistics 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 f5efff85c7..b6f1c541cb 100644 --- a/openstack/compute/v2/extensions/hypervisors/requests.go +++ b/openstack/compute/v2/extensions/hypervisors/requests.go @@ -30,3 +30,12 @@ func Get(client *gophercloud.ServiceClient, hypervisorID int) (r HypervisorResul }) 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 a35a0341ce..7f3fafe1aa 100644 --- a/openstack/compute/v2/extensions/hypervisors/results.go +++ b/openstack/compute/v2/extensions/hypervisors/results.go @@ -256,3 +256,35 @@ func (r StatisticsResult) Extract() (*Statistics, error) { 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 d261fa658f..1dc05fb9b0 100644 --- a/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go +++ b/openstack/compute/v2/extensions/hypervisors/testing/fixtures.go @@ -147,6 +147,18 @@ const HypervisorGetBody = ` } ` +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{ @@ -201,6 +213,13 @@ var ( 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) { @@ -233,3 +252,14 @@ func HandleHypervisorGetSuccessfully(t *testing.T) { 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 b89f6e76c8..95f9636c0c 100644 --- a/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go +++ b/openstack/compute/v2/extensions/hypervisors/testing/requests_test.go @@ -75,3 +75,15 @@ func TestGetHypervisor(t *testing.T) { 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 19107fe53d..4c18ed43c4 100644 --- a/openstack/compute/v2/extensions/hypervisors/urls.go +++ b/openstack/compute/v2/extensions/hypervisors/urls.go @@ -13,3 +13,7 @@ func hypervisorsStatisticsURL(c *gophercloud.ServiceClient) string { 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") +} From a13ccee8008ac7ac09a11b361812fa2163cabb82 Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Sun, 21 Jan 2018 05:18:45 +0300 Subject: [PATCH 018/120] Compute v2: Add Live Migration Action (#728) * Add support for server live-migration * Add unit test * Fix 'Host' type `string` -> `*string` to be able to set it to nil * Add acceptance test * Add ability to turn-on/off acceptance test for server live-migration * Get rid of 'strconv', add description when skip migrate test --- acceptance/clients/clients.go | 9 +++++ .../openstack/compute/v2/migrate_test.go | 37 ++++++++++++++++++ .../compute/v2/extensions/migrate/doc.go | 19 ++++++++- .../compute/v2/extensions/migrate/requests.go | 39 +++++++++++++++++++ .../v2/extensions/migrate/testing/fixtures.go | 15 +++++++ .../migrate/testing/requests_test.go | 20 ++++++++++ 6 files changed, 138 insertions(+), 1 deletion(-) diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index fcf22a7e8d..d5c9cccdf1 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -42,6 +42,9 @@ type AcceptanceTestChoices struct { // DBDatastoreTypeID is the datastore type version for DB tests. DBDatastoreVersion string + + // LiveMigrate indicates ability to run multi-node migration tests + LiveMigrate bool } // AcceptanceTestChoicesFromEnv populates a ComputeChoices struct from environment variables. @@ -57,6 +60,11 @@ func AcceptanceTestChoicesFromEnv() (*AcceptanceTestChoices, error) { dbDatastoreType := os.Getenv("OS_DB_DATASTORE_TYPE") dbDatastoreVersion := os.Getenv("OS_DB_DATASTORE_VERSION") + var liveMigrate bool + if v := os.Getenv("OS_LIVE_MIGRATE"); v != "" { + liveMigrate = true + } + missing := make([]string, 0, 3) if imageID == "" { missing = append(missing, "OS_IMAGE_ID") @@ -106,6 +114,7 @@ func AcceptanceTestChoicesFromEnv() (*AcceptanceTestChoices, error) { ShareNetworkID: shareNetworkID, DBDatastoreType: dbDatastoreType, DBDatastoreVersion: dbDatastoreVersion, + LiveMigrate: liveMigrate, }, nil } diff --git a/acceptance/openstack/compute/v2/migrate_test.go b/acceptance/openstack/compute/v2/migrate_test.go index 4d03350100..954716bda0 100644 --- a/acceptance/openstack/compute/v2/migrate_test.go +++ b/acceptance/openstack/compute/v2/migrate_test.go @@ -28,3 +28,40 @@ func TestMigrate(t *testing.T) { t.Fatalf("Error during migration: %v", err) } } + +func TestLiveMigrate(t *testing.T) { + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + if !choices.LiveMigrate { + t.Skip("Testing of live migration is disabled") + } + + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", 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() + if err != nil { + t.Fatalf("Error during live migration: %v", err) + } +} 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) +} From 4a3f5ae58624b68283375060dad06a214b05a32b Mon Sep 17 00:00:00 2001 From: Olivier Gagnon Date: Tue, 23 Jan 2018 13:50:12 -0500 Subject: [PATCH 019/120] add simple tenant usage (#624) * add simple tenant usage * tenantusage time fields changed to time.Time * removed multi, kept single * lint * fixed naming * address comments in simpletenantusage * renamed simpletenantusage to usage * added acceptance test * updated usage/doc.go to reflect package name * renamed usage to tenantUsage to prevent shadowing * renamed symbols in extension/usage --- acceptance/openstack/compute/v2/usage_test.go | 34 +++++ openstack/compute/v2/extensions/usage/doc.go | 27 ++++ .../compute/v2/extensions/usage/requests.go | 54 +++++++ .../compute/v2/extensions/usage/results.go | 137 ++++++++++++++++++ .../v2/extensions/usage/testing/doc.go | 2 + .../v2/extensions/usage/testing/fixtures.go | 137 ++++++++++++++++++ .../extensions/usage/testing/requests_test.go | 21 +++ openstack/compute/v2/extensions/usage/urls.go | 13 ++ 8 files changed, 425 insertions(+) create mode 100644 acceptance/openstack/compute/v2/usage_test.go create mode 100644 openstack/compute/v2/extensions/usage/doc.go create mode 100644 openstack/compute/v2/extensions/usage/requests.go create mode 100644 openstack/compute/v2/extensions/usage/results.go create mode 100644 openstack/compute/v2/extensions/usage/testing/doc.go create mode 100644 openstack/compute/v2/extensions/usage/testing/fixtures.go create mode 100644 openstack/compute/v2/extensions/usage/testing/requests_test.go create mode 100644 openstack/compute/v2/extensions/usage/urls.go diff --git a/acceptance/openstack/compute/v2/usage_test.go b/acceptance/openstack/compute/v2/usage_test.go new file mode 100644 index 0000000000..1537fda0cb --- /dev/null +++ b/acceptance/openstack/compute/v2/usage_test.go @@ -0,0 +1,34 @@ +// +build acceptance compute usage + +package v2 + +import ( + "strings" + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" +) + +func TestUsageSingleTenant(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + endpointParts := strings.Split(client.Endpoint, "/") + tenantID := endpointParts[4] + + page, err := usage.SingleTenant(client, tenantID, nil).AllPages() + if err != nil { + t.Fatal(err) + } + + tenantUsage, err := usage.ExtractSingleTenant(page) + if err != nil { + t.Fatal(err) + } + + tools.PrintResource(t, tenantUsage) +} 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) +} From 0b8b348f5ad19aa4513ad9f8ad24f766a6623ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Knecht?= Date: Wed, 24 Jan 2018 15:39:50 +0100 Subject: [PATCH 020/120] compute: flavors: add Ephemeral attribute Nova returns the amount of ephemeral storage (in GB) associated with a flavor as `OS-FLV-EXT-DATA:ephemeral`. --- openstack/compute/v2/flavors/results.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go index 5fe4e6cfef..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 { From a3e3dd5688a0be7260fa9887c91f66b5543ef203 Mon Sep 17 00:00:00 2001 From: Ildar Svetlov Date: Mon, 29 Jan 2018 01:40:20 +0400 Subject: [PATCH 021/120] Blockstorage service list support (#733) * Add Blockstorage service list support * Add optional fields into List query --- .../blockstorage/extensions/services_test.go | 32 ++++++ .../blockstorage/extensions/services/doc.go | 22 +++++ .../extensions/services/requests.go | 42 ++++++++ .../extensions/services/results.go | 84 ++++++++++++++++ .../extensions/services/testing/fixtures.go | 97 +++++++++++++++++++ .../services/testing/requests_test.go | 41 ++++++++ .../blockstorage/extensions/services/urls.go | 7 ++ 7 files changed, 325 insertions(+) create mode 100644 acceptance/openstack/blockstorage/extensions/services_test.go create mode 100644 openstack/blockstorage/extensions/services/doc.go create mode 100644 openstack/blockstorage/extensions/services/requests.go create mode 100644 openstack/blockstorage/extensions/services/results.go create mode 100644 openstack/blockstorage/extensions/services/testing/fixtures.go create mode 100644 openstack/blockstorage/extensions/services/testing/requests_test.go create mode 100644 openstack/blockstorage/extensions/services/urls.go 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/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") +} From 1db95d798aa72ec12a6e60e40749cea56073d2fb Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sun, 28 Jan 2018 21:48:51 +0000 Subject: [PATCH 022/120] Compute v2: Add unit tests for Ephemeral field --- .../compute/v2/flavors/testing/requests_test.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go index 990d75d495..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) { From f0a5d284c0791cb8e0aa93882560076f9371ea9e Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Sun, 28 Jan 2018 21:59:35 +0100 Subject: [PATCH 023/120] add ProviderClient.Reauthenticate() function If a user wants to do their own HTTP requests and reauthenticate in case of 401 responses, they can already use ProviderClient.ReauthFunc(), but that function is not thread-safe. This commit provides a safer alternative by pulling the relevant piece of code out of ProviderClient.Request(). --- provider_client.go | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/provider_client.go b/provider_client.go index 72daeb0a3e..3b8e9cb8d4 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 From a924af7658c466bf641b255397766fd4f4889189 Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Mon, 29 Jan 2018 22:18:43 +0300 Subject: [PATCH 024/120] Compute v2: Create aggregate (#739) * Add create host aggregate support * Update doc.go * Add `required:true` to struct field --- .../compute/v2/extensions/aggregates/doc.go | 16 +++++- .../v2/extensions/aggregates/requests.go | 28 ++++++++++ .../v2/extensions/aggregates/results.go | 51 ++++++++++++++++++- .../extensions/aggregates/testing/fixtures.go | 39 ++++++++++++++ .../aggregates/testing/requests_test.go | 30 ++++++++--- .../compute/v2/extensions/aggregates/urls.go | 4 ++ 6 files changed, 160 insertions(+), 8 deletions(-) diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go index 43699cb916..506fa81fef 100644 --- a/openstack/compute/v2/extensions/aggregates/doc.go +++ b/openstack/compute/v2/extensions/aggregates/doc.go @@ -1,7 +1,21 @@ /* -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 an 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 Retrieving list of all aggregates allPages, err := aggregates.List(computeClient).AllPages() diff --git a/openstack/compute/v2/extensions/aggregates/requests.go b/openstack/compute/v2/extensions/aggregates/requests.go index 5f31136c52..af5c5de09a 100644 --- a/openstack/compute/v2/extensions/aggregates/requests.go +++ b/openstack/compute/v2/extensions/aggregates/requests.go @@ -11,3 +11,31 @@ 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 +} diff --git a/openstack/compute/v2/extensions/aggregates/results.go b/openstack/compute/v2/extensions/aggregates/results.go index 19fc4443d6..f11463855b 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,33 @@ 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:"-"` +} + +// 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"` + } + 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) + + return nil } // AggregatesPage represents a single page of all Aggregates from a List @@ -40,3 +73,19 @@ func ExtractAggregates(p pagination.Page) ([]Aggregate, error) { err := (p.(AggregatesPage)).ExtractInto(&a) return a.Aggregates, err } + +type aggregatesResult struct { + gophercloud.Result +} + +type CreateResult struct { + aggregatesResult +} + +func (r CreateResult) Extract() (*Aggregate, error) { + var s struct { + Aggregate *Aggregate `json:"aggregate"` + } + err := r.ExtractInto(&s) + return s.Aggregate, err +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go index 758da2fb00..b4c53202cd 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/aggregates" th "github.com/gophercloud/gophercloud/testhelper" @@ -44,6 +45,20 @@ const AggregateListBody = ` } ` +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 + } +} +` + // First aggregate from the AggregateListBody var FirstFakeAggregate = aggregates.Aggregate{ AvailabilityZone: "", @@ -51,6 +66,8 @@ var FirstFakeAggregate = aggregates.Aggregate{ ID: 1, Metadata: map[string]string{}, Name: "test-aggregate1", + CreatedAt: time.Date(2017, 12, 22, 10, 12, 6, 0, time.UTC), + UpdatedAt: time.Time{}, } // Second aggregate from the AggregateListBody @@ -60,6 +77,18 @@ var SecondFakeAggregate = aggregates.Aggregate{ 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{}, +} + +var 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{}, } // HandleListSuccessfully configures the test server to respond to a List request. @@ -72,3 +101,13 @@ 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) + }) +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go index 903b675c9f..7a59b0da0f 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,33 @@ 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) +} diff --git a/openstack/compute/v2/extensions/aggregates/urls.go b/openstack/compute/v2/extensions/aggregates/urls.go index 88b15009fa..80a37b8169 100644 --- a/openstack/compute/v2/extensions/aggregates/urls.go +++ b/openstack/compute/v2/extensions/aggregates/urls.go @@ -5,3 +5,7 @@ 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") +} From 8128df2865bb9d40a9d09f901483b6cd0436e53c Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Tue, 30 Jan 2018 22:37:20 +0300 Subject: [PATCH 025/120] Compute v2: Delete aggregate (#740) * Add delete aggregate support * Change expected status code * Style fix --- openstack/compute/v2/extensions/aggregates/doc.go | 9 ++++++++- .../compute/v2/extensions/aggregates/requests.go | 11 +++++++++++ .../compute/v2/extensions/aggregates/results.go | 4 ++++ .../v2/extensions/aggregates/testing/fixtures.go | 13 +++++++++++++ .../extensions/aggregates/testing/requests_test.go | 9 +++++++++ openstack/compute/v2/extensions/aggregates/urls.go | 4 ++++ 6 files changed, 49 insertions(+), 1 deletion(-) diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go index 506fa81fef..f5c1f640d3 100644 --- a/openstack/compute/v2/extensions/aggregates/doc.go +++ b/openstack/compute/v2/extensions/aggregates/doc.go @@ -13,9 +13,16 @@ Example of Create an aggregate if err != nil { panic(err) } - fmt.Printf("%+v\n", aggregate) +Example of Delete an aggregate + + aggregateID := 32 + err := aggregates.Delete(computeClient, aggregateID).ExtractErr() + if err != nil { + panic(err) + } + Example of Retrieving list of all aggregates allPages, err := aggregates.List(computeClient).AllPages() diff --git a/openstack/compute/v2/extensions/aggregates/requests.go b/openstack/compute/v2/extensions/aggregates/requests.go index af5c5de09a..22de32b4b2 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" ) @@ -39,3 +41,12 @@ func Create(client *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) }) 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 +} diff --git a/openstack/compute/v2/extensions/aggregates/results.go b/openstack/compute/v2/extensions/aggregates/results.go index f11463855b..fa62b75140 100644 --- a/openstack/compute/v2/extensions/aggregates/results.go +++ b/openstack/compute/v2/extensions/aggregates/results.go @@ -89,3 +89,7 @@ func (r CreateResult) Extract() (*Aggregate, error) { err := r.ExtractInto(&s) return s.Aggregate, err } + +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go index b4c53202cd..c6214516e4 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -3,6 +3,7 @@ package testing import ( "fmt" "net/http" + "strconv" "testing" "time" @@ -91,6 +92,8 @@ var CreatedAggregate = aggregates.Aggregate{ UpdatedAt: time.Time{}, } +var AggregateIDtoDelete = 1 + // HandleListSuccessfully configures the test server to respond to a List request. func HandleListSuccessfully(t *testing.T) { th.Mux.HandleFunc("/os-aggregates", func(w http.ResponseWriter, r *http.Request) { @@ -111,3 +114,13 @@ func HandleCreateSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go index 7a59b0da0f..123fcfcb42 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go +++ b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go @@ -56,3 +56,12 @@ func TestCreateAggregates(t *testing.T) { 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) +} diff --git a/openstack/compute/v2/extensions/aggregates/urls.go b/openstack/compute/v2/extensions/aggregates/urls.go index 80a37b8169..9a991e951d 100644 --- a/openstack/compute/v2/extensions/aggregates/urls.go +++ b/openstack/compute/v2/extensions/aggregates/urls.go @@ -9,3 +9,7 @@ func aggregatesListURL(c *gophercloud.ServiceClient) string { 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) +} From b2667db96072181f10f4ef7af905a34c078e91f6 Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Wed, 31 Jan 2018 11:36:09 +0300 Subject: [PATCH 026/120] Compute v2: Get aggregate (#746) * Add get aggregates support * Update doc.go --- .../compute/v2/extensions/aggregates/doc.go | 13 ++- .../v2/extensions/aggregates/requests.go | 9 ++ .../v2/extensions/aggregates/results.go | 14 ++- .../extensions/aggregates/testing/fixtures.go | 96 +++++++++++++------ .../aggregates/testing/requests_test.go | 13 +++ .../compute/v2/extensions/aggregates/urls.go | 4 + 6 files changed, 113 insertions(+), 36 deletions(-) diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go index f5c1f640d3..140ac1acbe 100644 --- a/openstack/compute/v2/extensions/aggregates/doc.go +++ b/openstack/compute/v2/extensions/aggregates/doc.go @@ -2,7 +2,7 @@ Package aggregates manages information about the host aggregates in the OpenStack cloud. -Example of Create an aggregate +Example of Create Aggregate opts := aggregates.CreateOpts{ Name: "name", @@ -15,7 +15,16 @@ Example of Create an aggregate } fmt.Printf("%+v\n", aggregate) -Example of Delete an 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() diff --git a/openstack/compute/v2/extensions/aggregates/requests.go b/openstack/compute/v2/extensions/aggregates/requests.go index 22de32b4b2..d08cbdb939 100644 --- a/openstack/compute/v2/extensions/aggregates/requests.go +++ b/openstack/compute/v2/extensions/aggregates/requests.go @@ -50,3 +50,12 @@ func Delete(client *gophercloud.ServiceClient, aggregateID int) (r DeleteResult) }) return } + +// Get makes a request against the API to get details for an 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 +} diff --git a/openstack/compute/v2/extensions/aggregates/results.go b/openstack/compute/v2/extensions/aggregates/results.go index fa62b75140..bbd60440d9 100644 --- a/openstack/compute/v2/extensions/aggregates/results.go +++ b/openstack/compute/v2/extensions/aggregates/results.go @@ -78,11 +78,7 @@ type aggregatesResult struct { gophercloud.Result } -type CreateResult struct { - aggregatesResult -} - -func (r CreateResult) Extract() (*Aggregate, error) { +func (r aggregatesResult) Extract() (*Aggregate, error) { var s struct { Aggregate *Aggregate `json:"aggregate"` } @@ -90,6 +86,14 @@ func (r CreateResult) Extract() (*Aggregate, error) { return s.Aggregate, err } +type CreateResult struct { + aggregatesResult +} + +type GetResult struct { + aggregatesResult +} + type DeleteResult struct { gophercloud.ErrResult } diff --git a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go index c6214516e4..818ca2ba66 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -60,39 +60,66 @@ const AggregateCreateBody = ` } ` -// First aggregate from the AggregateListBody -var 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{}, +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" + } + } } +` -// 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", - CreatedAt: time.Date(2017, 12, 22, 10, 16, 7, 0, time.UTC), - UpdatedAt: time.Time{}, -} +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{}, + } -var 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{}, -} + // 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{}, + } -var AggregateIDtoDelete = 1 + // 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{}, + } + + // Aggregate ID to delete + AggregateIDtoDelete = 1 + + // Aggregate ID to get, from the AggregateGetBody + AggregateIDtoGet = SecondFakeAggregate.ID +) // HandleListSuccessfully configures the test server to respond to a List request. func HandleListSuccessfully(t *testing.T) { @@ -124,3 +151,14 @@ func HandleDeleteSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go index 123fcfcb42..b838a1907c 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go +++ b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go @@ -65,3 +65,16 @@ func TestDeleteAggregates(t *testing.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) +} diff --git a/openstack/compute/v2/extensions/aggregates/urls.go b/openstack/compute/v2/extensions/aggregates/urls.go index 9a991e951d..0b681f11f7 100644 --- a/openstack/compute/v2/extensions/aggregates/urls.go +++ b/openstack/compute/v2/extensions/aggregates/urls.go @@ -13,3 +13,7 @@ func aggregatesCreateURL(c *gophercloud.ServiceClient) string { 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) +} From 6e3bdd38004d86c8dd0e4acab69e91123751a86e Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy Date: Wed, 31 Jan 2018 16:27:27 +0300 Subject: [PATCH 027/120] Add missing fields `Deleted` and `DeletedAt` --- openstack/compute/v2/extensions/aggregates/results.go | 10 ++++++++++ .../v2/extensions/aggregates/testing/fixtures.go | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/openstack/compute/v2/extensions/aggregates/results.go b/openstack/compute/v2/extensions/aggregates/results.go index bbd60440d9..3b69b2b4d4 100644 --- a/openstack/compute/v2/extensions/aggregates/results.go +++ b/openstack/compute/v2/extensions/aggregates/results.go @@ -31,6 +31,14 @@ type Aggregate struct { // 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 @@ -40,6 +48,7 @@ func (r *Aggregate) UnmarshalJSON(b []byte) error { 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 { @@ -49,6 +58,7 @@ func (r *Aggregate) UnmarshalJSON(b []byte) error { r.CreatedAt = time.Time(s.CreatedAt) r.UpdatedAt = time.Time(s.UpdatedAt) + r.DeletedAt = time.Time(s.DeletedAt) return nil } diff --git a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go index 818ca2ba66..9760710b1d 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -90,6 +90,8 @@ var ( 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 @@ -101,6 +103,8 @@ var ( 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 @@ -112,6 +116,8 @@ var ( 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 From 1861349f37e119f911d170838a9d766b16e274e5 Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Sat, 3 Feb 2018 00:34:13 +0300 Subject: [PATCH 028/120] Acceptance test for create, get and delete aggregate (#745) * Add acceptance test for create and delete aggregate #739, #740 * Add reusable functions for Create/Delete aggregates * Fix acceptance test --- .../openstack/compute/v2/aggregates_test.go | 20 ++++++++++++ acceptance/openstack/compute/v2/compute.go | 32 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/acceptance/openstack/compute/v2/aggregates_test.go b/acceptance/openstack/compute/v2/aggregates_test.go index d0771155ce..cad2379902 100644 --- a/acceptance/openstack/compute/v2/aggregates_test.go +++ b/acceptance/openstack/compute/v2/aggregates_test.go @@ -30,3 +30,23 @@ func TestAggregatesList(t *testing.T) { tools.PrintResource(t, h) } } + +func TestAggregatesCreateGetDelete(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + createdAggregate, err := CreateAggregate(t, client) + if err != nil { + t.Fatalf("Unable to create an aggregate: %v", err) + } + defer DeleteAggregate(t, client, createdAggregate) + + aggregate, err := aggregates.Get(client, createdAggregate.ID).Extract() + if err != nil { + t.Fatalf("Unable to get an aggregate: %v", err) + } + + tools.PrintResource(t, aggregate) +} diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go index fad4673b42..5cb86185d3 100644 --- a/acceptance/openstack/compute/v2/compute.go +++ b/acceptance/openstack/compute/v2/compute.go @@ -26,6 +26,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/aggregates" "golang.org/x/crypto/ssh" ) @@ -570,6 +571,37 @@ func CreateVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blo return volumeAttachment, 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) + availableZone := tools.RandomString("zone_", 5) + t.Logf("Attempting to create aggregate %s", aggregateName) + + createOpts := aggregates.CreateOpts{Name: aggregateName, AvailabilityZone: availableZone} + + aggregate, err := aggregates.Create(client, createOpts).Extract() + if err != nil { + return nil, err + } + + t.Logf("Successfully created aggregate %d", aggregate.ID) + + return aggregate, 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. From 3b177406222d4b76d93e90d6ab412e7b7dc160f4 Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Sat, 3 Feb 2018 02:12:48 +0300 Subject: [PATCH 029/120] Compute v2: Update aggregate (#748) * Add aggregate update support * Change field to optional * Add acceptance test for Update --- .../openstack/compute/v2/aggregates_test.go | 42 ++++++++++++++++- .../compute/v2/extensions/aggregates/doc.go | 14 ++++++ .../v2/extensions/aggregates/requests.go | 29 ++++++++++++ .../v2/extensions/aggregates/results.go | 4 ++ .../extensions/aggregates/testing/fixtures.go | 45 +++++++++++++++++++ .../aggregates/testing/requests_test.go | 18 ++++++++ .../compute/v2/extensions/aggregates/urls.go | 4 ++ 7 files changed, 155 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/compute/v2/aggregates_test.go b/acceptance/openstack/compute/v2/aggregates_test.go index cad2379902..8a85e401ad 100644 --- a/acceptance/openstack/compute/v2/aggregates_test.go +++ b/acceptance/openstack/compute/v2/aggregates_test.go @@ -31,7 +31,22 @@ func TestAggregatesList(t *testing.T) { } } -func TestAggregatesCreateGetDelete(t *testing.T) { +func TestAggregatesCreateDelete(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + createdAggregate, err := CreateAggregate(t, client) + if err != nil { + t.Fatalf("Unable to create an aggregate: %v", err) + } + defer DeleteAggregate(t, client, createdAggregate) + + tools.PrintResource(t, createdAggregate) +} + +func TestAggregatesGet(t *testing.T) { client, err := clients.NewComputeV2Client() if err != nil { t.Fatalf("Unable to create a compute client: %v", err) @@ -50,3 +65,28 @@ func TestAggregatesCreateGetDelete(t *testing.T) { tools.PrintResource(t, aggregate) } + +func TestAggregatesUpdate(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + createdAggregate, err := CreateAggregate(t, client) + if err != nil { + t.Fatalf("Unable to create an aggregate: %v", err) + } + defer DeleteAggregate(t, client, createdAggregate) + + updateOpts := aggregates.UpdateOpts{ + Name: "new_aggregate_name", + AvailabilityZone: "new_azone", + } + + updatedAggregate, err := aggregates.Update(client, createdAggregate.ID, updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update an aggregate: %v", err) + } + + tools.PrintResource(t, updatedAggregate) +} diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go index 140ac1acbe..3e353952ba 100644 --- a/openstack/compute/v2/extensions/aggregates/doc.go +++ b/openstack/compute/v2/extensions/aggregates/doc.go @@ -32,6 +32,20 @@ Example of Delete Aggregate 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() diff --git a/openstack/compute/v2/extensions/aggregates/requests.go b/openstack/compute/v2/extensions/aggregates/requests.go index d08cbdb939..a26f0ca0f4 100644 --- a/openstack/compute/v2/extensions/aggregates/requests.go +++ b/openstack/compute/v2/extensions/aggregates/requests.go @@ -59,3 +59,32 @@ func Get(client *gophercloud.ServiceClient, aggregateID int) (r GetResult) { }) 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") +} + +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 +} diff --git a/openstack/compute/v2/extensions/aggregates/results.go b/openstack/compute/v2/extensions/aggregates/results.go index 3b69b2b4d4..b6e613e35e 100644 --- a/openstack/compute/v2/extensions/aggregates/results.go +++ b/openstack/compute/v2/extensions/aggregates/results.go @@ -107,3 +107,7 @@ type GetResult struct { type DeleteResult struct { gophercloud.ErrResult } + +type UpdateResult struct { + aggregatesResult +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go index 9760710b1d..e11e2b537d 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -80,6 +80,24 @@ const AggregateGetBody = ` } ` +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" + } + } +} +` + var ( // First aggregate from the AggregateListBody FirstFakeAggregate = aggregates.Aggregate{ @@ -125,6 +143,22 @@ var ( // 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, + } ) // HandleListSuccessfully configures the test server to respond to a List request. @@ -168,3 +202,14 @@ func HandleGetSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go index b838a1907c..e19e66e348 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go +++ b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go @@ -78,3 +78,21 @@ func TestGetAggregates(t *testing.T) { 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) +} diff --git a/openstack/compute/v2/extensions/aggregates/urls.go b/openstack/compute/v2/extensions/aggregates/urls.go index 0b681f11f7..6e45bd13d7 100644 --- a/openstack/compute/v2/extensions/aggregates/urls.go +++ b/openstack/compute/v2/extensions/aggregates/urls.go @@ -17,3 +17,7 @@ func aggregatesDeleteURL(c *gophercloud.ServiceClient, aggregateID string) strin 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) +} From fe85f9519740a5b5c09ecb0909609b58e59e5dec Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Sun, 4 Feb 2018 21:36:31 +0300 Subject: [PATCH 030/120] Compute v2: Add host to aggregate (#751) * Add host to aggregate support * Add unit test * Update doc.go * Add acceptance test * Fix acceptance test --- .../openstack/compute/v2/aggregates_test.go | 50 +++++++++++++++++++ .../compute/v2/extensions/aggregates/doc.go | 14 ++++++ .../v2/extensions/aggregates/requests.go | 27 +++++++++- .../v2/extensions/aggregates/results.go | 4 ++ .../extensions/aggregates/testing/fixtures.go | 44 ++++++++++++++++ .../aggregates/testing/requests_test.go | 17 +++++++ .../compute/v2/extensions/aggregates/urls.go | 4 ++ 7 files changed, 159 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/compute/v2/aggregates_test.go b/acceptance/openstack/compute/v2/aggregates_test.go index 8a85e401ad..5bb002d520 100644 --- a/acceptance/openstack/compute/v2/aggregates_test.go +++ b/acceptance/openstack/compute/v2/aggregates_test.go @@ -3,11 +3,14 @@ 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" ) func TestAggregatesList(t *testing.T) { @@ -90,3 +93,50 @@ func TestAggregatesUpdate(t *testing.T) { tools.PrintResource(t, updatedAggregate) } + +func TestAggregatesAddHost(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + hostToAdd, err := getHypervisor(t, client) + if err != nil { + t.Fatal(err) + } + + createdAggregate, err := CreateAggregate(t, client) + if err != nil { + t.Fatalf("Unable to create an aggregate: %v", err) + } + defer DeleteAggregate(t, client, createdAggregate) + + addHostOpts := aggregates.AddHostOpts{ + Host: hostToAdd.HypervisorHostname, + } + + aggregateWithNewHost, err := aggregates.AddHost(client, createdAggregate.ID, addHostOpts).Extract() + if err != nil { + t.Fatalf("Unable to add host to aggregate: %v", err) + } + + tools.PrintResource(t, aggregateWithNewHost) +} + +func getHypervisor(t *testing.T, client *gophercloud.ServiceClient) (*hypervisors.Hypervisor, error) { + allPages, err := hypervisors.List(client).AllPages() + if err != nil { + t.Fatalf("Unable to list hypervisors: %v", err) + } + + allHypervisors, err := hypervisors.ExtractHypervisors(allPages) + if err != nil { + t.Fatal("Unable to extract hypervisors") + } + + for _, h := range allHypervisors { + return &h, nil + } + + return nil, fmt.Errorf("Unable to get hypervisor") +} diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go index 3e353952ba..d7fc339bf7 100644 --- a/openstack/compute/v2/extensions/aggregates/doc.go +++ b/openstack/compute/v2/extensions/aggregates/doc.go @@ -61,5 +61,19 @@ 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) + */ package aggregates diff --git a/openstack/compute/v2/extensions/aggregates/requests.go b/openstack/compute/v2/extensions/aggregates/requests.go index a26f0ca0f4..2bf2ec92b6 100644 --- a/openstack/compute/v2/extensions/aggregates/requests.go +++ b/openstack/compute/v2/extensions/aggregates/requests.go @@ -51,7 +51,7 @@ func Delete(client *gophercloud.ServiceClient, aggregateID int) (r DeleteResult) return } -// Get makes a request against the API to get details for an specific aggregate. +// 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{ @@ -75,6 +75,7 @@ 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) @@ -88,3 +89,27 @@ func Update(client *gophercloud.ServiceClient, aggregateID int, opts UpdateOpts) }) 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 +} diff --git a/openstack/compute/v2/extensions/aggregates/results.go b/openstack/compute/v2/extensions/aggregates/results.go index b6e613e35e..2ab0cf22f0 100644 --- a/openstack/compute/v2/extensions/aggregates/results.go +++ b/openstack/compute/v2/extensions/aggregates/results.go @@ -111,3 +111,7 @@ type DeleteResult struct { 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 e11e2b537d..f0fe61bf09 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -98,6 +98,27 @@ const AggregateUpdateBody = ` } ` +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" + } + } +} +` + var ( // First aggregate from the AggregateListBody FirstFakeAggregate = aggregates.Aggregate{ @@ -159,6 +180,18 @@ var ( 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, + } ) // HandleListSuccessfully configures the test server to respond to a List request. @@ -213,3 +246,14 @@ func HandleUpdateSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go index e19e66e348..b154957d50 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go +++ b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go @@ -96,3 +96,20 @@ func TestUpdateAggregate(t *testing.T) { 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) +} diff --git a/openstack/compute/v2/extensions/aggregates/urls.go b/openstack/compute/v2/extensions/aggregates/urls.go index 6e45bd13d7..ed10604bd7 100644 --- a/openstack/compute/v2/extensions/aggregates/urls.go +++ b/openstack/compute/v2/extensions/aggregates/urls.go @@ -21,3 +21,7 @@ func aggregatesGetURL(c *gophercloud.ServiceClient, aggregateID string) string { 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") +} From debc1adf8e41fb5c5b7e2021a1be0b4d0c78318a Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sun, 4 Feb 2018 20:34:22 +0000 Subject: [PATCH 031/120] Networking v2: Create Floating IP with Subnet This commit enables the creation of floating IPs by specifying a subnet ID. This is useful when a floating IP pool has multiple subnets. --- .../v2/extensions/layer3/floatingips_test.go | 47 +++++++++++++++++ .../extensions/layer3/floatingips/requests.go | 1 + .../floatingips/testing/requests_test.go | 52 +++++++++++++++++++ 3 files changed, 100 insertions(+) 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/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/openstack/networking/v2/extensions/layer3/floatingips/requests.go index 1c8a8b2f13..0a6eb62cb6 100644 --- a/openstack/networking/v2/extensions/layer3/floatingips/requests.go +++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -53,6 +53,7 @@ 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"` } 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() From 104e2578924bb3b211150c19414d0144b82165bb Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Tue, 6 Feb 2018 19:55:54 +0300 Subject: [PATCH 032/120] Compute v2: Remove host (#754) * Add `remove-host` support * Add unit test * Update doc.go * Add acceptance test --- .../openstack/compute/v2/aggregates_test.go | 13 +++++- .../compute/v2/extensions/aggregates/doc.go | 13 ++++++ .../v2/extensions/aggregates/requests.go | 24 +++++++++++ .../extensions/aggregates/testing/fixtures.go | 41 +++++++++++++++++++ .../aggregates/testing/requests_test.go | 17 ++++++++ .../compute/v2/extensions/aggregates/urls.go | 4 ++ 6 files changed, 111 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/compute/v2/aggregates_test.go b/acceptance/openstack/compute/v2/aggregates_test.go index 5bb002d520..24ba7a8ff2 100644 --- a/acceptance/openstack/compute/v2/aggregates_test.go +++ b/acceptance/openstack/compute/v2/aggregates_test.go @@ -94,7 +94,7 @@ func TestAggregatesUpdate(t *testing.T) { tools.PrintResource(t, updatedAggregate) } -func TestAggregatesAddHost(t *testing.T) { +func TestAggregatesAddRemoveHost(t *testing.T) { client, err := clients.NewComputeV2Client() if err != nil { t.Fatalf("Unable to create a compute client: %v", err) @@ -121,6 +121,17 @@ func TestAggregatesAddHost(t *testing.T) { } tools.PrintResource(t, aggregateWithNewHost) + + removeHostOpts := aggregates.RemoveHostOpts{ + Host: hostToAdd.HypervisorHostname, + } + + aggregateWithRemovedHost, err := aggregates.RemoveHost(client, createdAggregate.ID, removeHostOpts).Extract() + if err != nil { + t.Fatalf("Unable to remove host from aggregate: %v", err) + } + + tools.PrintResource(t, aggregateWithRemovedHost) } func getHypervisor(t *testing.T, client *gophercloud.ServiceClient) (*hypervisors.Hypervisor, error) { diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go index d7fc339bf7..8f15289997 100644 --- a/openstack/compute/v2/extensions/aggregates/doc.go +++ b/openstack/compute/v2/extensions/aggregates/doc.go @@ -75,5 +75,18 @@ Example of Add Host } 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) + */ package aggregates diff --git a/openstack/compute/v2/extensions/aggregates/requests.go b/openstack/compute/v2/extensions/aggregates/requests.go index 2bf2ec92b6..2f433f645f 100644 --- a/openstack/compute/v2/extensions/aggregates/requests.go +++ b/openstack/compute/v2/extensions/aggregates/requests.go @@ -113,3 +113,27 @@ func AddHost(client *gophercloud.ServiceClient, aggregateID int, opts AddHostOpt }) 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 +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go index f0fe61bf09..5d1d802548 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -119,6 +119,24 @@ const AggregateAddHostBody = ` } ` +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" + } + } +} +` + var ( // First aggregate from the AggregateListBody FirstFakeAggregate = aggregates.Aggregate{ @@ -192,6 +210,18 @@ var ( 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, + } ) // HandleListSuccessfully configures the test server to respond to a List request. @@ -257,3 +287,14 @@ func HandleAddHostSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go index b154957d50..ddf7eb8625 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go +++ b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go @@ -113,3 +113,20 @@ func TestAddHostAggregate(t *testing.T) { th.AssertDeepEquals(t, &expected, actual) } + +func TestRemoveHostAggregate(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAddHostSuccessfully(t) + + expected := AggregateWithAddedHost + + opts := aggregates.RemoveHostOpts{ + Host: "cmp1", + } + + actual, err := aggregates.RemoveHost(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 ed10604bd7..fb1090019a 100644 --- a/openstack/compute/v2/extensions/aggregates/urls.go +++ b/openstack/compute/v2/extensions/aggregates/urls.go @@ -25,3 +25,7 @@ func aggregatesUpdateURL(c *gophercloud.ServiceClient, aggregateID string) strin 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") +} From 6da026c32e2d622cc242d32984259c77237aefe1 Mon Sep 17 00:00:00 2001 From: Daniil Rutskiy <10889589+dstdfx@users.noreply.github.com> Date: Sat, 10 Feb 2018 05:43:43 +0300 Subject: [PATCH 033/120] Compute v2: Create Or Update Aggregate Metadata (#756) * Add support create/update metadata * Add unit test * Update doc.go * Add acceptance test * Fix doc.go * Change `Metadata` field type to map[string]interface{} * Add delete key to acceptance test --- .../openstack/compute/v2/aggregates_test.go | 35 +++++++++++++++ .../compute/v2/extensions/aggregates/doc.go | 17 ++++++- .../v2/extensions/aggregates/requests.go | 23 ++++++++++ .../extensions/aggregates/testing/fixtures.go | 44 +++++++++++++++++++ .../aggregates/testing/requests_test.go | 21 ++++++++- .../compute/v2/extensions/aggregates/urls.go | 4 ++ 6 files changed, 140 insertions(+), 4 deletions(-) diff --git a/acceptance/openstack/compute/v2/aggregates_test.go b/acceptance/openstack/compute/v2/aggregates_test.go index 24ba7a8ff2..7209831c3d 100644 --- a/acceptance/openstack/compute/v2/aggregates_test.go +++ b/acceptance/openstack/compute/v2/aggregates_test.go @@ -134,6 +134,41 @@ func TestAggregatesAddRemoveHost(t *testing.T) { tools.PrintResource(t, aggregateWithRemovedHost) } +func TestAggregatesSetRemoveMetadata(t *testing.T) { + client, err := clients.NewComputeV2Client() + if err != nil { + t.Fatalf("Unable to create a compute client: %v", err) + } + + createdAggregate, err := CreateAggregate(t, client) + if err != nil { + t.Fatalf("Unable to create an aggregate: %v", err) + } + defer DeleteAggregate(t, client, createdAggregate) + + opts := aggregates.SetMetadataOpts{ + Metadata: map[string]interface{}{"key": "value"}, + } + + aggregateWithMetadata, err := aggregates.SetMetadata(client, createdAggregate.ID, opts).Extract() + if err != nil { + t.Fatalf("Unable to set metadata to aggregate: %v", err) + } + + tools.PrintResource(t, aggregateWithMetadata) + + optsToRemove := aggregates.SetMetadataOpts{ + Metadata: map[string]interface{}{"key": nil}, + } + + aggregateWithRemovedKey, err := aggregates.SetMetadata(client, createdAggregate.ID, optsToRemove).Extract() + if err != nil { + t.Fatalf("Unable to set metadata to aggregate: %v", err) + } + + tools.PrintResource(t, aggregateWithRemovedKey) +} + func getHypervisor(t *testing.T, client *gophercloud.ServiceClient) (*hypervisors.Hypervisor, error) { allPages, err := hypervisors.List(client).AllPages() if err != nil { diff --git a/openstack/compute/v2/extensions/aggregates/doc.go b/openstack/compute/v2/extensions/aggregates/doc.go index 8f15289997..97f1b033da 100644 --- a/openstack/compute/v2/extensions/aggregates/doc.go +++ b/openstack/compute/v2/extensions/aggregates/doc.go @@ -66,7 +66,7 @@ Example of Add Host aggregateID := 22 opts := aggregates.AddHostOpts{ - Host: "newhost-cmp1" + Host: "newhost-cmp1", } aggregate, err := aggregates.AddHost(computeClient, aggregateID, opts).Extract() @@ -79,7 +79,7 @@ Example of Remove Host aggregateID := 22 opts := aggregates.RemoveHostOpts{ - Host: "newhost-cmp1" + Host: "newhost-cmp1", } aggregate, err := aggregates.RemoveHost(computeClient, aggregateID, opts).Extract() @@ -88,5 +88,18 @@ Example of Remove Host } 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 2f433f645f..c37531c56a 100644 --- a/openstack/compute/v2/extensions/aggregates/requests.go +++ b/openstack/compute/v2/extensions/aggregates/requests.go @@ -137,3 +137,26 @@ func RemoveHost(client *gophercloud.ServiceClient, aggregateID int, opts RemoveH }) 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/testing/fixtures.go b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go index 5d1d802548..9ae71d2230 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/fixtures.go +++ b/openstack/compute/v2/extensions/aggregates/testing/fixtures.go @@ -137,6 +137,27 @@ const AggregateRemoveHostBody = ` } ` +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{ @@ -222,6 +243,18 @@ var ( 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. @@ -298,3 +331,14 @@ func HandleRemoveHostSuccessfully(t *testing.T) { 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 ddf7eb8625..bfd18614cc 100644 --- a/openstack/compute/v2/extensions/aggregates/testing/requests_test.go +++ b/openstack/compute/v2/extensions/aggregates/testing/requests_test.go @@ -117,9 +117,9 @@ func TestAddHostAggregate(t *testing.T) { func TestRemoveHostAggregate(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() - HandleAddHostSuccessfully(t) + HandleRemoveHostSuccessfully(t) - expected := AggregateWithAddedHost + expected := AggregateWithRemovedHost opts := aggregates.RemoveHostOpts{ Host: "cmp1", @@ -130,3 +130,20 @@ func TestRemoveHostAggregate(t *testing.T) { 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 fb1090019a..bb30c7fc90 100644 --- a/openstack/compute/v2/extensions/aggregates/urls.go +++ b/openstack/compute/v2/extensions/aggregates/urls.go @@ -29,3 +29,7 @@ func aggregatesAddHostURL(c *gophercloud.ServiceClient, aggregateID string) stri 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") +} From 197afc8d119485289f060e34e53a15ca97bf2743 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 14 Feb 2018 03:10:44 +0000 Subject: [PATCH 034/120] Compute v2: Fix flavor extra spec builder names --- openstack/compute/v2/flavors/requests.go | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go index e7041df059..4b406df957 100644 --- a/openstack/compute/v2/flavors/requests.go +++ b/openstack/compute/v2/flavors/requests.go @@ -236,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 @@ -261,15 +262,15 @@ func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts C return } -// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to the -// Update request. +// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to +// the Update request. type UpdateExtraSpecOptsBuilder interface { - ToExtraSpecUpdateMap() (map[string]string, string, error) + ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) } -// ToExtraSpecUpdateMap assembles a body for an Update request based on the -// contents of a ExtraSpecOpts. -func (opts ExtraSpecsOpts) ToExtraSpecUpdateMap() (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" @@ -285,9 +286,10 @@ func (opts ExtraSpecsOpts) ToExtraSpecUpdateMap() (map[string]string, string, er return opts, key, nil } -// UpdateExtraSpec will updates the value of the specified flavor's extra spec for the key in opts. +// 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.ToExtraSpecUpdateMap() + b, key, err := opts.ToFlavorExtraSpecUpdateMap() if err != nil { r.Err = err return From bd6e51250a76aa01959ffc4f6fbbddcfb48a9afb Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Thu, 15 Feb 2018 05:45:49 +0300 Subject: [PATCH 035/120] Add SubnetPoolID into the Subnet struct and GET requests (#764) * Add SubnetPoolID into the Subnet struct Add ability to populate subnet's subnetpool_id field from GET requests. Also add this field to GET and LIST unit tests. * Add SubnetPoolID as a query parameter to Subnets Add SubnetPoolID to the subnet List request. --- openstack/networking/v2/subnets/requests.go | 1 + openstack/networking/v2/subnets/results.go | 3 ++ .../networking/v2/subnets/testing/fixtures.go | 42 ++++++++++++++++++- .../v2/subnets/testing/requests_test.go | 2 + 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go index 403f692234..bef04f077f 100644 --- a/openstack/networking/v2/subnets/requests.go +++ b/openstack/networking/v2/subnets/requests.go @@ -27,6 +27,7 @@ type ListOpts struct { 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"` diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go index 743610f01e..8cc4dfac46 100644 --- a/openstack/networking/v2/subnets/results.go +++ b/openstack/networking/v2/subnets/results.go @@ -100,6 +100,9 @@ type Subnet struct { // 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..89afa625f3 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" } } ` diff --git a/openstack/networking/v2/subnets/testing/requests_test.go b/openstack/networking/v2/subnets/testing/requests_test.go index 2dd4e029df..dbe86a6d0e 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) { From 3efdea1595d59f73812eb09a6d77b5d991ceb4b0 Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Wed, 14 Feb 2018 22:17:44 +0300 Subject: [PATCH 036/120] Add the SubnetPoolID to the Subnet Create request Add the "subnetpool_id" field to the subnet creation request with an acceptance test. Also update the unit test. --- .../openstack/networking/v2/networking.go | 26 ++++++++++++++ .../openstack/networking/v2/subnets_test.go | 35 +++++++++++++++++++ openstack/networking/v2/subnets/requests.go | 3 ++ .../networking/v2/subnets/testing/fixtures.go | 6 ++-- .../v2/subnets/testing/requests_test.go | 2 ++ 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go index bc463dde93..d19fb06262 100644 --- a/acceptance/openstack/networking/v2/networking.go +++ b/acceptance/openstack/networking/v2/networking.go @@ -188,6 +188,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. 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/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go index bef04f077f..32f5c78c67 100644 --- a/openstack/networking/v2/subnets/requests.go +++ b/openstack/networking/v2/subnets/requests.go @@ -115,6 +115,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/testing/fixtures.go b/openstack/networking/v2/subnets/testing/fixtures.go index 89afa625f3..619ea3e55e 100644 --- a/openstack/networking/v2/subnets/testing/fixtures.go +++ b/openstack/networking/v2/subnets/testing/fixtures.go @@ -199,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" } } ` @@ -222,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 dbe86a6d0e..208fc608f9 100644 --- a/openstack/networking/v2/subnets/testing/requests_test.go +++ b/openstack/networking/v2/subnets/testing/requests_test.go @@ -121,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) @@ -141,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) { From b922c6e3f157de4ad6e715842d5afd4c4256950d Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Sun, 18 Feb 2018 03:52:47 +0100 Subject: [PATCH 037/120] Vpnaas: Create VPN service (#760) * Added create method for vpn services * Added urls to urls file * Added results file for create method * Created unit test for vpn service creation * Added missing extract method to results file * Made parameter in create API compliant * Made flavorId parameter optional, fixed unit tests * Removed space in struct definition * removed comma in struct * formatting * added documentation file doc.go * added descriptive comments to fields * renamed ToVPNServiceCreateMap to ToServiceCreateMap * removed faulty description of subnet ID * removed field project_id * Added acceptance test for vpn service creation * Removed unnecessary print statement * Formatting * formatting doc.go file * Changed example in doc.go to verified working example. Also edited some formatting on inline comments * Fixed inline formatting for results.go * Added comparison to created object at the end * Changed individual attribute comparisons in unit test to struct comparison. Also changed AdminStateUp type from *bool to bool --- .../v2/extensions/vpnaas/service_test.go | 31 ++++++++ .../networking/v2/extensions/vpnaas/vpnaas.go | 32 ++++++++ .../v2/extensions/vpnaas/services/doc.go | 21 +++++ .../v2/extensions/vpnaas/services/requests.go | 52 +++++++++++++ .../v2/extensions/vpnaas/services/results.go | 62 +++++++++++++++ .../vpnaas/services/testing/requests_test.go | 77 +++++++++++++++++++ .../v2/extensions/vpnaas/services/urls.go | 16 ++++ 7 files changed, 291 insertions(+) create mode 100644 acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go create mode 100644 acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go create mode 100644 openstack/networking/v2/extensions/vpnaas/services/doc.go create mode 100644 openstack/networking/v2/extensions/vpnaas/services/requests.go create mode 100644 openstack/networking/v2/extensions/vpnaas/services/results.go create mode 100644 openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/services/urls.go 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..b002f1b5ae --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go @@ -0,0 +1,31 @@ +// +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" +) + +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) + } + + tools.PrintResource(t, service) +} 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..c50c3b796d --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -0,0 +1,32 @@ +package vpnaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/services" +) + +// 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 +} 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..7cddfcaf64 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/services/doc.go @@ -0,0 +1,21 @@ +/* +Package services allows management and retrieval of VPN services in the +OpenStack Networking 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) + } + +*/ +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..7feeb7baec --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/services/requests.go @@ -0,0 +1,52 @@ +package services + +import "github.com/gophercloud/gophercloud" + +// 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 +} 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..73aad3fc17 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/services/results.go @@ -0,0 +1,62 @@ +package services + +import ( + "github.com/gophercloud/gophercloud" +) + +// Service is a VPN Service +type Service struct { + // TenantID is the ID of the project. + TenantID string `json:"tenant_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 +} + +// 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 +} 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..e7548ed722 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go @@ -0,0 +1,77 @@ +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" + 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" + } +} + `) + }) + + 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", + ExternalV4IP: "172.32.1.11", + ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828", + Description: "OpenStack VPN 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) +} From 1858d1f75923be847d3a98198dcc4756dc177beb Mon Sep 17 00:00:00 2001 From: PriyankaJ77 <35596310+PriyankaJ77@users.noreply.github.com> Date: Tue, 20 Feb 2018 05:56:48 +0530 Subject: [PATCH 038/120] Network v2: RBAC-Create (#755) * [wip] Network v2: RBAC-Create * Added Acceptance Tests for RBAC. * Addressed some nits. * Addressed Review Comments * Resolved build failure * Addressed doc nits, RBAC to RBACPolicy naming convention, and restricting the acceptance test to only be run by admin user. --- .../v2/extensions/rbacpolicies/pkg.go | 1 + .../extensions/rbacpolicies/rbacpolicies.go | 29 ++++++++++ .../rbacpolicies/rbacpolicies_test.go | 56 +++++++++++++++++++ .../v2/extensions/rbacpolicies/doc.go | 31 ++++++++++ .../v2/extensions/rbacpolicies/requests.go | 51 +++++++++++++++++ .../v2/extensions/rbacpolicies/results.go | 53 ++++++++++++++++++ .../v2/extensions/rbacpolicies/testing/doc.go | 2 + .../rbacpolicies/testing/fixtures.go | 40 +++++++++++++ .../rbacpolicies/testing/requests_test.go | 39 +++++++++++++ .../v2/extensions/rbacpolicies/urls.go | 11 ++++ 10 files changed, 313 insertions(+) create mode 100644 acceptance/openstack/networking/v2/extensions/rbacpolicies/pkg.go create mode 100644 acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go create mode 100644 acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go create mode 100644 openstack/networking/v2/extensions/rbacpolicies/doc.go create mode 100644 openstack/networking/v2/extensions/rbacpolicies/requests.go create mode 100644 openstack/networking/v2/extensions/rbacpolicies/results.go create mode 100644 openstack/networking/v2/extensions/rbacpolicies/testing/doc.go create mode 100644 openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go create mode 100644 openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go create mode 100644 openstack/networking/v2/extensions/rbacpolicies/urls.go 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..27d3df7f8b --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go @@ -0,0 +1,29 @@ +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 +} 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..9c0b4bd515 --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go @@ -0,0 +1,56 @@ +// +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" +) + +func TestRBACPolicyCreate(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) + } + + tools.PrintResource(t, rbacPolicy) +} diff --git a/openstack/networking/v2/extensions/rbacpolicies/doc.go b/openstack/networking/v2/extensions/rbacpolicies/doc.go new file mode 100644 index 0000000000..4a3aec1a59 --- /dev/null +++ b/openstack/networking/v2/extensions/rbacpolicies/doc.go @@ -0,0 +1,31 @@ +/* +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) + } + +*/ +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..286c02a94e --- /dev/null +++ b/openstack/networking/v2/extensions/rbacpolicies/requests.go @@ -0,0 +1,51 @@ +package rbacpolicies + +import ( + "github.com/gophercloud/gophercloud" +) + +// 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 +} diff --git a/openstack/networking/v2/extensions/rbacpolicies/results.go b/openstack/networking/v2/extensions/rbacpolicies/results.go new file mode 100644 index 0000000000..c2d120efe3 --- /dev/null +++ b/openstack/networking/v2/extensions/rbacpolicies/results.go @@ -0,0 +1,53 @@ +package rbacpolicies + +import ( + "github.com/gophercloud/gophercloud" +) + +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 +} + +// 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"` +} 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..cd26e0139b --- /dev/null +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go @@ -0,0 +1,40 @@ +package testing + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/rbacpolicies" +) + +// 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" + } +}` + +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", +} 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..50d1ac4063 --- /dev/null +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go @@ -0,0 +1,39 @@ +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" + 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) +} diff --git a/openstack/networking/v2/extensions/rbacpolicies/urls.go b/openstack/networking/v2/extensions/rbacpolicies/urls.go new file mode 100644 index 0000000000..542b38b58c --- /dev/null +++ b/openstack/networking/v2/extensions/rbacpolicies/urls.go @@ -0,0 +1,11 @@ +package rbacpolicies + +import "github.com/gophercloud/gophercloud" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("rbac-policies") +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} From bfeb72b9527bf840b4bffd2f7e082d6ab974e7d3 Mon Sep 17 00:00:00 2001 From: Manjunath Patil Date: Wed, 21 Feb 2018 08:06:37 +0530 Subject: [PATCH 039/120] Network v2: RBAC-GET-BY-ID (#770) * [wip] Network v2: RBAC-Create * Added Acceptance Tests for RBAC. * Addressed some nits. * Addressed Review Comments * Resolved build failure * Addressed doc nits, RBAC to RBACPolicy naming convention, and restricting the acceptance test to only be run by admin user. * Network v2: RBAC-GET-BY-ID --- .../rbacpolicies/rbacpolicies_test.go | 10 ++++++++++ .../v2/extensions/rbacpolicies/requests.go | 6 ++++++ .../v2/extensions/rbacpolicies/results.go | 6 ++++++ .../rbacpolicies/testing/fixtures.go | 14 ++++++++++++++ .../rbacpolicies/testing/requests_test.go | 19 +++++++++++++++++++ .../v2/extensions/rbacpolicies/urls.go | 8 ++++++++ 6 files changed, 63 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go index 9c0b4bd515..de944c0c08 100644 --- a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go +++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go @@ -10,6 +10,7 @@ import ( 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 TestRBACPolicyCreate(t *testing.T) { @@ -53,4 +54,13 @@ func TestRBACPolicyCreate(t *testing.T) { } tools.PrintResource(t, rbacPolicy) + + // 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) } diff --git a/openstack/networking/v2/extensions/rbacpolicies/requests.go b/openstack/networking/v2/extensions/rbacpolicies/requests.go index 286c02a94e..7332fce9ad 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/requests.go +++ b/openstack/networking/v2/extensions/rbacpolicies/requests.go @@ -4,6 +4,12 @@ import ( "github.com/gophercloud/gophercloud" ) +// 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 diff --git a/openstack/networking/v2/extensions/rbacpolicies/results.go b/openstack/networking/v2/extensions/rbacpolicies/results.go index c2d120efe3..0e85a66798 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/results.go +++ b/openstack/networking/v2/extensions/rbacpolicies/results.go @@ -25,6 +25,12 @@ 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 +} + // RBACPolicy represents a RBAC policy. type RBACPolicy struct { // UUID of the RBAC policy. diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go index cd26e0139b..4328baac40 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go @@ -29,6 +29,20 @@ const CreateResponse = ` } }` +// 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" + } +}` + var rbacPolicy1 = rbacpolicies.RBACPolicy{ ID: "2cf7523a-93b5-4e69-9360-6c6bf986bb7c", Action: rbacpolicies.ActionAccessShared, diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go index 50d1ac4063..ccd433098b 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go @@ -37,3 +37,22 @@ func TestCreate(t *testing.T) { 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) +} diff --git a/openstack/networking/v2/extensions/rbacpolicies/urls.go b/openstack/networking/v2/extensions/rbacpolicies/urls.go index 542b38b58c..068daa1998 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/urls.go +++ b/openstack/networking/v2/extensions/rbacpolicies/urls.go @@ -2,6 +2,10 @@ 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") } @@ -9,3 +13,7 @@ func rootURL(c *gophercloud.ServiceClient) string { func createURL(c *gophercloud.ServiceClient) string { return rootURL(c) } + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} From d693a2e15df35aba7480c4c719d63066bc59adee Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 21 Feb 2018 04:03:51 +0100 Subject: [PATCH 040/120] Vpnaas: Delete service (#769) * Added delete request * Added delete result * added unit and acceptance tests for delete function * added delete Service to acceptancetest * Added documentation in doc.go file --- .../v2/extensions/vpnaas/service_test.go | 1 + .../networking/v2/extensions/vpnaas/vpnaas.go | 14 ++++++++++++++ .../v2/extensions/vpnaas/services/doc.go | 8 ++++++++ .../v2/extensions/vpnaas/services/requests.go | 7 +++++++ .../v2/extensions/vpnaas/services/results.go | 6 ++++++ .../vpnaas/services/testing/requests_test.go | 14 ++++++++++++++ 6 files changed, 50 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go index b002f1b5ae..658f0840d5 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go @@ -26,6 +26,7 @@ func TestServiceCRUD(t *testing.T) { if err != nil { t.Fatalf("Unable to create service: %v", err) } + defer DeleteService(t, client, service.ID) tools.PrintResource(t, service) } diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index c50c3b796d..5a5ec1de3f 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -30,3 +30,17 @@ func CreateService(t *testing.T, client *gophercloud.ServiceClient, routerID str 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) +} diff --git a/openstack/networking/v2/extensions/vpnaas/services/doc.go b/openstack/networking/v2/extensions/vpnaas/services/doc.go index 7cddfcaf64..2060bcb086 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/services/doc.go @@ -17,5 +17,13 @@ Example to Create a Service panic(err) } +Example to Delete a Service + + serviceID := "38aee955-6283-4279-b091-8b9c828000ec" + err := policies.Delete(networkClient, serviceID).ExtractErr() + 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 index 7feeb7baec..7073508f9f 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/services/requests.go @@ -50,3 +50,10 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul _, 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/services/results.go b/openstack/networking/v2/extensions/vpnaas/services/results.go index 73aad3fc17..77bc02b76c 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/results.go +++ b/openstack/networking/v2/extensions/vpnaas/services/results.go @@ -60,3 +60,9 @@ func (r commonResult) Extract() (*Service, error) { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go index e7548ed722..450a56ad26 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go @@ -75,3 +75,17 @@ func TestCreate(t *testing.T) { } 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) +} From d4d605f09f27442acc49fb097f5432501c82ef98 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 21 Feb 2018 04:30:35 +0000 Subject: [PATCH 041/120] Identity v3: Add support for RegionID in token catalog endpoints --- openstack/endpoint_location.go | 2 +- openstack/identity/v3/tokens/results.go | 1 + .../identity/v3/tokens/testing/fixtures.go | 6 +++ openstack/testing/endpoint_location_test.go | 43 +++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) 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/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/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) + } +} From 94986f2ad4599240265d6d225acd894cb1b41c5d Mon Sep 17 00:00:00 2001 From: PriyankaJ77 <35596310+PriyankaJ77@users.noreply.github.com> Date: Thu, 22 Feb 2018 04:35:07 +0530 Subject: [PATCH 042/120] Network v2: RBAC-Delete (#781) --- .../v2/extensions/rbacpolicies/rbacpolicies.go | 14 ++++++++++++++ .../extensions/rbacpolicies/rbacpolicies_test.go | 1 + .../networking/v2/extensions/rbacpolicies/doc.go | 8 ++++++++ .../v2/extensions/rbacpolicies/requests.go | 6 ++++++ .../v2/extensions/rbacpolicies/results.go | 6 ++++++ .../rbacpolicies/testing/requests_test.go | 14 ++++++++++++++ .../networking/v2/extensions/rbacpolicies/urls.go | 4 ++++ 7 files changed, 53 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go index 27d3df7f8b..46903dff78 100644 --- a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go +++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies.go @@ -27,3 +27,17 @@ func CreateRBACPolicy(t *testing.T, client *gophercloud.ServiceClient, tenantID, 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 index de944c0c08..d898c7db40 100644 --- a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go +++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go @@ -52,6 +52,7 @@ func TestRBACPolicyCreate(t *testing.T) { if err != nil { t.Fatalf("Unable to create rbac-policy: %v", err) } + defer DeleteRBACPolicy(t, client, rbacPolicy.ID) tools.PrintResource(t, rbacPolicy) diff --git a/openstack/networking/v2/extensions/rbacpolicies/doc.go b/openstack/networking/v2/extensions/rbacpolicies/doc.go index 4a3aec1a59..9c61d00027 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/doc.go +++ b/openstack/networking/v2/extensions/rbacpolicies/doc.go @@ -27,5 +27,13 @@ Example to Create a RBAC Policy panic(err) } +Example to Delete a RBAC Policy + + rbacPolicyID := "94fe107f-da78-4d92-a9d7-5611b06dad8d" + err := rbacpolicies.Delete(rbacClient, rbacPolicyID).ExtractErr() + 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 index 7332fce9ad..9b0658a9a1 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/requests.go +++ b/openstack/networking/v2/extensions/rbacpolicies/requests.go @@ -55,3 +55,9 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul _, 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 +} diff --git a/openstack/networking/v2/extensions/rbacpolicies/results.go b/openstack/networking/v2/extensions/rbacpolicies/results.go index 0e85a66798..22d91edaac 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/results.go +++ b/openstack/networking/v2/extensions/rbacpolicies/results.go @@ -31,6 +31,12 @@ 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 +} + // RBACPolicy represents a RBAC policy. type RBACPolicy struct { // UUID of the RBAC policy. diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go index ccd433098b..2e50c1feec 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go @@ -56,3 +56,17 @@ func TestGet(t *testing.T) { th.AssertNoErr(t, err) th.CheckDeepEquals(t, &rbacPolicy1, n) } + +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) +} diff --git a/openstack/networking/v2/extensions/rbacpolicies/urls.go b/openstack/networking/v2/extensions/rbacpolicies/urls.go index 068daa1998..9954f65629 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/urls.go +++ b/openstack/networking/v2/extensions/rbacpolicies/urls.go @@ -17,3 +17,7 @@ func createURL(c *gophercloud.ServiceClient) string { func getURL(c *gophercloud.ServiceClient, id string) string { return resourceURL(c, id) } + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} From 6c823a6861b8e5dcf93a9a5a6937d592370aacb6 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Thu, 22 Feb 2018 03:46:25 +0100 Subject: [PATCH 043/120] Vpnaas: Create IPSec Policy (#768) * Added requests.go file and necessary methods for ipsecpolicy creation * created results file * Updated results description * Created urls.go file * Put correct root and resource urls * Added unit test for ipsecpolicy creation. Also renamed LifetimeName to LifetimeValue * Formatted inline comments, put LifetimeCreateOpts and Lifetime into their own structs * Added acceptance test, formatted struct fields * Added doc.go file * Added comparison between expected and actual struct to tests * Removed Lifetime prefix before Units and Value fields. Updated unit test to compare struct instead of fields * added types to pass into createopts * Added missing 'id' field to results struct, fixed typo in comment to TransformProtocol * fixed typo --- .../v2/extensions/vpnaas/ipsecpolicy_test.go | 24 ++++ .../networking/v2/extensions/vpnaas/vpnaas.go | 22 ++++ .../v2/extensions/vpnaas/ipsecpolicies/doc.go | 16 +++ .../vpnaas/ipsecpolicies/requests.go | 110 ++++++++++++++++++ .../vpnaas/ipsecpolicies/results.go | 67 +++++++++++ .../ipsecpolicies/testing/requests_test.go | 97 +++++++++++++++ .../extensions/vpnaas/ipsecpolicies/urls.go | 16 +++ 7 files changed, 352 insertions(+) create mode 100644 acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ipsecpolicies/urls.go 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..a3e4d7c600 --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go @@ -0,0 +1,24 @@ +// +build acceptance networking vpnaas + +package vpnaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" +) + +func TestPolicyCRUD(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 policy: %v", err) + } + + tools.PrintResource(t, policy) +} diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index 5a5ec1de3f..71b0b98763 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -5,6 +5,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/services" ) @@ -44,3 +45,24 @@ func DeleteService(t *testing.T, client *gophercloud.ServiceClient, serviceID st 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 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 +} 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..64ff17eec4 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go @@ -0,0 +1,16 @@ +/* +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) + } +*/ +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..eea9333005 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go @@ -0,0 +1,110 @@ +package ipsecpolicies + +import "github.com/gophercloud/gophercloud" + +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 +} 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..278c0b1560 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go @@ -0,0 +1,67 @@ +package ipsecpolicies + +import ( + "github.com/gophercloud/gophercloud" +) + +// Policy is an IPSec Policy +type Policy struct { + // TenantID is the ID of the project + TenantID string `json:"tenant_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 +} 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..715e1d8f9d --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go @@ -0,0 +1,97 @@ +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" + 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", + "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", + } + 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) +} From c06af9d18942a83da79436ee0b415a496458347b Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Thu, 22 Feb 2018 04:48:55 +0000 Subject: [PATCH 044/120] Networking v2: Port Security Create --- .../openstack/networking/v2/networking.go | 66 +++++++++++++++++++ .../openstack/networking/v2/networks_test.go | 26 ++++++++ .../openstack/networking/v2/ports_test.go | 41 ++++++++++++ .../v2/extensions/portsecurity/requests.go | 55 ++++++++++++++++ .../v2/networks/testing/fixtures.go | 26 ++++++++ .../v2/networks/testing/requests_test.go | 36 ++++++++++ .../networking/v2/ports/testing/fixtures.go | 56 ++++++++++++++++ .../v2/ports/testing/requests_test.go | 48 ++++++++++++++ 8 files changed, 354 insertions(+) create mode 100644 openstack/networking/v2/extensions/portsecurity/requests.go diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go index d19fb06262..e889267674 100644 --- a/acceptance/openstack/networking/v2/networking.go +++ b/acceptance/openstack/networking/v2/networking.go @@ -6,6 +6,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/acceptance/tools" + "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 +32,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 +126,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) { diff --git a/acceptance/openstack/networking/v2/networks_test.go b/acceptance/openstack/networking/v2/networks_test.go index c100bd4160..2ff00172bc 100644 --- a/acceptance/openstack/networking/v2/networks_test.go +++ b/acceptance/openstack/networking/v2/networks_test.go @@ -71,3 +71,29 @@ 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) +} diff --git a/acceptance/openstack/networking/v2/ports_test.go b/acceptance/openstack/networking/v2/ports_test.go index eddddf64f2..89ac3b5382 100644 --- a/acceptance/openstack/networking/v2/ports_test.go +++ b/acceptance/openstack/networking/v2/ports_test.go @@ -8,6 +8,7 @@ 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/portsecurity" "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" ) @@ -347,3 +348,43 @@ 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) +} diff --git a/openstack/networking/v2/extensions/portsecurity/requests.go b/openstack/networking/v2/extensions/portsecurity/requests.go new file mode 100644 index 0000000000..781353ee37 --- /dev/null +++ b/openstack/networking/v2/extensions/portsecurity/requests.go @@ -0,0 +1,55 @@ +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 +} + +// 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 +} diff --git a/openstack/networking/v2/networks/testing/fixtures.go b/openstack/networking/v2/networks/testing/fixtures.go index 3edbe8f37a..e4f6b6bd02 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": { diff --git a/openstack/networking/v2/networks/testing/requests_test.go b/openstack/networking/v2/networks/testing/requests_test.go index 0e028ead4e..1bfafaae8d 100644 --- a/openstack/networking/v2/networks/testing/requests_test.go +++ b/openstack/networking/v2/networks/testing/requests_test.go @@ -218,3 +218,39 @@ 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) +} diff --git a/openstack/networking/v2/ports/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go index dea77fe011..d8e57cbbae 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": { diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go index aa6c74d64a..87a18864d4 100644 --- a/openstack/networking/v2/ports/testing/requests_test.go +++ b/openstack/networking/v2/ports/testing/requests_test.go @@ -311,6 +311,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() From 8f1afb16493e82ad2cabafc23843cb128e64ee97 Mon Sep 17 00:00:00 2001 From: Manjunath Patil Date: Thu, 22 Feb 2018 22:05:32 +0530 Subject: [PATCH 045/120] Network v2: RBAC-LIST (#758) * [wip] Network v2: RBAC-Create * Added Acceptance Tests for RBAC. * Addressed some nits. * Addressed Review Comments * Resolved build failure * Addressed doc nits, RBAC to RBACPolicy naming convention, and restricting the acceptance test to only be run by admin user. * Network v2: RBAC-GET-ALL List all the RBAC policies. * Doc Comment addressed --- .../rbacpolicies/rbacpolicies_test.go | 27 +++++++ .../v2/extensions/rbacpolicies/doc.go | 39 ++++++++-- .../v2/extensions/rbacpolicies/requests.go | 48 +++++++++++++ .../v2/extensions/rbacpolicies/results.go | 27 +++++++ .../rbacpolicies/testing/fixtures.go | 36 ++++++++++ .../rbacpolicies/testing/requests_test.go | 72 +++++++++++++++++++ .../v2/extensions/rbacpolicies/urls.go | 4 ++ 7 files changed, 248 insertions(+), 5 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go index d898c7db40..6b3a72f141 100644 --- a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go +++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go @@ -65,3 +65,30 @@ func TestRBACPolicyCreate(t *testing.T) { 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/openstack/networking/v2/extensions/rbacpolicies/doc.go b/openstack/networking/v2/extensions/rbacpolicies/doc.go index 9c61d00027..4eabbec7aa 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/doc.go +++ b/openstack/networking/v2/extensions/rbacpolicies/doc.go @@ -27,13 +27,42 @@ Example to Create a RBAC Policy 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) - } + 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) */ package rbacpolicies diff --git a/openstack/networking/v2/extensions/rbacpolicies/requests.go b/openstack/networking/v2/extensions/rbacpolicies/requests.go index 9b0658a9a1..b3c89f80d1 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/requests.go +++ b/openstack/networking/v2/extensions/rbacpolicies/requests.go @@ -2,8 +2,56 @@ 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"` + 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) diff --git a/openstack/networking/v2/extensions/rbacpolicies/results.go b/openstack/networking/v2/extensions/rbacpolicies/results.go index 22d91edaac..8baa5a8feb 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/results.go +++ b/openstack/networking/v2/extensions/rbacpolicies/results.go @@ -2,6 +2,7 @@ package rbacpolicies import ( "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" ) type commonResult struct { @@ -63,3 +64,29 @@ type RBACPolicy struct { // 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/fixtures.go b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go index 4328baac40..0b3772ddd4 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go @@ -4,6 +4,30 @@ 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 = ` { @@ -52,3 +76,15 @@ var rbacPolicy1 = rbacpolicies.RBACPolicy{ 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 index 2e50c1feec..98654616cc 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go @@ -7,6 +7,7 @@ import ( 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" ) @@ -57,6 +58,77 @@ func TestGet(t *testing.T) { 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() diff --git a/openstack/networking/v2/extensions/rbacpolicies/urls.go b/openstack/networking/v2/extensions/rbacpolicies/urls.go index 9954f65629..258c207304 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/urls.go +++ b/openstack/networking/v2/extensions/rbacpolicies/urls.go @@ -14,6 +14,10 @@ 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) } From b57885588035b78f5f12a11ed087709518f65e0f Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Fri, 23 Feb 2018 03:15:24 +0000 Subject: [PATCH 046/120] Networking v2: Fix subnetpools The following fixes have been applied: * Removal of Prefixes from ListOpts * Changing ListOpts Shared to *bool * Changing ListOpts IsDefault to *bool * Changing the result CreatedAt to time.Time * Changing the result UpdatedAt to time.Time --- .../v2/extensions/subnetpools/requests.go | 39 +++++++++---------- .../v2/extensions/subnetpools/results.go | 5 ++- .../subnetpools/testing/fixtures.go | 18 +++++---- .../subnetpools/testing/requests_test.go | 5 ++- 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/openstack/networking/v2/extensions/subnetpools/requests.go b/openstack/networking/v2/extensions/subnetpools/requests.go index 61f660d5ab..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. diff --git a/openstack/networking/v2/extensions/subnetpools/results.go b/openstack/networking/v2/extensions/subnetpools/results.go index a490f05401..e761eac44d 100644 --- a/openstack/networking/v2/extensions/subnetpools/results.go +++ b/openstack/networking/v2/extensions/subnetpools/results.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "strconv" + "time" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" @@ -66,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 f70b41773e..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,7 +143,7 @@ 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 = ` @@ -157,11 +159,11 @@ const SubnetPoolGetResult = ` "is_default": true, "project_id": "1e2b9857295a4a3e841809ef492812c5", "tenant_id": "1e2b9857295a4a3e841809ef492812c5", - "created_at": "2018-01-01T00:00:01", + "created_at": "2018-01-01T00:00:01Z", "prefixes": [ "2001:db8::a3/64" ], - "updated_at": "2018-01-01T00:10:10", + "updated_at": "2018-01-01T00:10:10Z", "ip_version": 6, "shared": false, "description": "ipv6 prefixes", diff --git a/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go b/openstack/networking/v2/extensions/subnetpools/testing/requests_test.go index d4f62319ad..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" @@ -73,8 +74,8 @@ func TestGet(t *testing.T) { th.AssertEquals(t, s.DefaultQuota, 2) th.AssertEquals(t, s.TenantID, "1e2b9857295a4a3e841809ef492812c5") th.AssertEquals(t, s.ProjectID, "1e2b9857295a4a3e841809ef492812c5") - th.AssertEquals(t, s.CreatedAt, "2018-01-01T00:00:01") - th.AssertEquals(t, s.UpdatedAt, "2018-01-01T00:10:10") + 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", }) From 44ab6a048b83d9042103347ba28a1513274f72f7 Mon Sep 17 00:00:00 2001 From: PriyankaJ77 <35596310+PriyankaJ77@users.noreply.github.com> Date: Sat, 24 Feb 2018 09:11:01 +0530 Subject: [PATCH 047/120] Network v2: RBAC-Update (#782) --- .../rbacpolicies/rbacpolicies_test.go | 21 ++++++++++++- .../v2/extensions/rbacpolicies/doc.go | 11 +++++++ .../v2/extensions/rbacpolicies/requests.go | 30 +++++++++++++++++++ .../v2/extensions/rbacpolicies/results.go | 6 ++++ .../rbacpolicies/testing/fixtures.go | 22 ++++++++++++++ .../rbacpolicies/testing/requests_test.go | 25 ++++++++++++++++ .../v2/extensions/rbacpolicies/urls.go | 4 +++ 7 files changed, 118 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go index 6b3a72f141..db838d2816 100644 --- a/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go +++ b/acceptance/openstack/networking/v2/extensions/rbacpolicies/rbacpolicies_test.go @@ -13,7 +13,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/rbacpolicies" ) -func TestRBACPolicyCreate(t *testing.T) { +func TestRBACPolicyCRUD(t *testing.T) { username := os.Getenv("OS_USERNAME") if username != "admin" { t.Skip("must be admin to run this test") @@ -56,6 +56,25 @@ func TestRBACPolicyCreate(t *testing.T) { 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() diff --git a/openstack/networking/v2/extensions/rbacpolicies/doc.go b/openstack/networking/v2/extensions/rbacpolicies/doc.go index 4eabbec7aa..f0ddbc0f67 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/doc.go +++ b/openstack/networking/v2/extensions/rbacpolicies/doc.go @@ -64,5 +64,16 @@ Example to Get RBAC Policy by ID } 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 index b3c89f80d1..8ce85a54c8 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/requests.go +++ b/openstack/networking/v2/extensions/rbacpolicies/requests.go @@ -109,3 +109,33 @@ 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 index 8baa5a8feb..a62facc0dc 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/results.go +++ b/openstack/networking/v2/extensions/rbacpolicies/results.go @@ -38,6 +38,12 @@ 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. diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go index 0b3772ddd4..f63fa2b89f 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/fixtures.go @@ -67,6 +67,28 @@ const GetResponse = ` } }` +// 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, diff --git a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go index 98654616cc..8aad843459 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/rbacpolicies/testing/requests_test.go @@ -142,3 +142,28 @@ func TestDelete(t *testing.T) { 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 index 258c207304..8beeed9a5f 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/urls.go +++ b/openstack/networking/v2/extensions/rbacpolicies/urls.go @@ -25,3 +25,7 @@ func getURL(c *gophercloud.ServiceClient, id string) string { func deleteURL(c *gophercloud.ServiceClient, id string) string { return resourceURL(c, id) } + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} From 47e78c6bba46f5484da66c8e9ba0ecb94093641c Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Sat, 24 Feb 2018 05:26:33 +0100 Subject: [PATCH 048/120] Vpnaas: Delete IPSec policy (#775) * Added delete unit test * Added delete function and result * Added comment to delete result * Added documentation and acceptance tests --- .../v2/extensions/vpnaas/ipsecpolicy_test.go | 1 + .../networking/v2/extensions/vpnaas/vpnaas.go | 14 ++++++++++++++ .../v2/extensions/vpnaas/ipsecpolicies/doc.go | 7 +++++++ .../v2/extensions/vpnaas/ipsecpolicies/requests.go | 7 +++++++ .../v2/extensions/vpnaas/ipsecpolicies/results.go | 6 ++++++ .../vpnaas/ipsecpolicies/testing/requests_test.go | 14 ++++++++++++++ 6 files changed, 49 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go index a3e4d7c600..e46e7150bc 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go @@ -19,6 +19,7 @@ func TestPolicyCRUD(t *testing.T) { if err != nil { t.Fatalf("Unable to create policy: %v", err) } + defer DeleteIPSecPolicy(t, client, policy.ID) tools.PrintResource(t, policy) } diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index 71b0b98763..b899c624f4 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -66,3 +66,17 @@ func CreateIPSecPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ipsecp 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 policy: %s", policyID) + + err := ipsecpolicies.Delete(client, policyID).ExtractErr() + if err != nil { + t.Fatalf("Unable to delete policy %s: %v", policyID, err) + } + + t.Logf("Deleted policy: %s", policyID) +} diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go index 64ff17eec4..4da16abeba 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go @@ -12,5 +12,12 @@ Example to Create a Policy 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) + } */ package ipsecpolicies diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go index eea9333005..d9c38c2dec 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go @@ -108,3 +108,10 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul _, 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go index 278c0b1560..e011f4670b 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go @@ -65,3 +65,9 @@ func (r commonResult) Extract() (*Policy, error) { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go index 715e1d8f9d..42907e6220 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go @@ -95,3 +95,17 @@ func TestCreate(t *testing.T) { } 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) +} From cb23b19a1ee57a4bca31c797a0af490260a4a870 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 24 Feb 2018 05:26:49 +0000 Subject: [PATCH 049/120] Networking v2: Support both TenantID and ProjectID where applicable --- .../v2/extensions/fwaas/firewalls/requests.go | 2 ++ .../v2/extensions/fwaas/firewalls/results.go | 1 + .../v2/extensions/fwaas/policies/requests.go | 2 ++ .../v2/extensions/fwaas/policies/results.go | 1 + .../v2/extensions/fwaas/rules/requests.go | 2 ++ .../v2/extensions/fwaas/rules/results.go | 1 + .../extensions/layer3/floatingips/requests.go | 2 ++ .../extensions/layer3/floatingips/results.go | 7 ++++-- .../v2/extensions/layer3/routers/requests.go | 2 ++ .../v2/extensions/layer3/routers/results.go | 7 ++++-- .../extensions/lbaas_v2/listeners/requests.go | 7 +++++- .../lbaas_v2/loadbalancers/requests.go | 9 ++++++-- .../extensions/lbaas_v2/monitors/requests.go | 9 ++++++-- .../v2/extensions/lbaas_v2/pools/requests.go | 17 ++++++++++---- .../v2/extensions/rbacpolicies/requests.go | 1 + .../v2/extensions/security/groups/requests.go | 23 +++++++++++-------- .../v2/extensions/security/groups/results.go | 5 +++- .../v2/extensions/security/rules/requests.go | 7 +++--- .../v2/extensions/security/rules/results.go | 5 +++- openstack/networking/v2/networks/requests.go | 2 ++ openstack/networking/v2/networks/results.go | 5 +++- openstack/networking/v2/ports/requests.go | 2 ++ openstack/networking/v2/ports/results.go | 5 +++- openstack/networking/v2/subnets/requests.go | 9 ++++++-- openstack/networking/v2/subnets/results.go | 5 +++- 25 files changed, 106 insertions(+), 32 deletions(-) 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 0a6eb62cb6..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"` @@ -55,6 +56,7 @@ type CreateOpts struct { 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/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..49ec9ecac3 100644 --- a/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go +++ b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go @@ -20,6 +20,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"` @@ -81,10 +82,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"` 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/rbacpolicies/requests.go b/openstack/networking/v2/extensions/rbacpolicies/requests.go index 8ce85a54c8..532a2f23d2 100644 --- a/openstack/networking/v2/extensions/rbacpolicies/requests.go +++ b/openstack/networking/v2/extensions/rbacpolicies/requests.go @@ -21,6 +21,7 @@ type ListOpts struct { 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"` 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/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/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/subnets/requests.go b/openstack/networking/v2/subnets/requests.go index 32f5c78c67..597a4e77f3 100644 --- a/openstack/networking/v2/subnets/requests.go +++ b/openstack/networking/v2/subnets/requests.go @@ -21,6 +21,7 @@ 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"` @@ -84,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"` diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go index 8cc4dfac46..493e5c042e 100644 --- a/openstack/networking/v2/subnets/results.go +++ b/openstack/networking/v2/subnets/results.go @@ -91,9 +91,12 @@ 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"` From fbf9a3c4ef4f5ea3ab16fbe095e532934f8b00e1 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Tue, 27 Feb 2018 05:29:00 +0100 Subject: [PATCH 050/120] Vpnaas: Show IPSec Policy Details (#773) * Added get function for IPSec Policies * Added GetResult * added unit test for Get function * Added acceptance test * fixed C&P error * added projectID --- .../v2/extensions/vpnaas/ipsecpolicy_test.go | 9 ++- .../vpnaas/ipsecpolicies/requests.go | 6 ++ .../vpnaas/ipsecpolicies/results.go | 9 +++ .../ipsecpolicies/testing/requests_test.go | 57 +++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go index e46e7150bc..e4ee3d4063 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go @@ -7,6 +7,7 @@ import ( "github.com/gophercloud/gophercloud/acceptance/clients" "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies" ) func TestPolicyCRUD(t *testing.T) { @@ -17,9 +18,15 @@ func TestPolicyCRUD(t *testing.T) { policy, err := CreateIPSecPolicy(t, client) if err != nil { - t.Fatalf("Unable to create policy: %v", err) + t.Fatalf("Unable to create IPSec policy: %v", err) } defer DeleteIPSecPolicy(t, client, policy.ID) 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/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go index d9c38c2dec..0fc1813afd 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go @@ -115,3 +115,9 @@ 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go index e011f4670b..dffab6444a 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go @@ -9,6 +9,9 @@ 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"` @@ -71,3 +74,9 @@ type CreateResult struct { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go index 42907e6220..03bec8d070 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go @@ -49,6 +49,7 @@ func TestCreate(t *testing.T) { "encryption_algorithm": "aes-128", "pfs": "group5", "tenant_id": "b4eedccc6fb74fa8a7ad6b08382b852b", + "project_id": "b4eedccc6fb74fa8a7ad6b08382b852b", "lifetime": { "units": "seconds", "value": 7200 @@ -92,6 +93,62 @@ func TestCreate(t *testing.T) { 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) } From eedbafadaa1aecde95e4c14c3bebb401275b4d5d Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Tue, 27 Feb 2018 05:32:27 +0100 Subject: [PATCH 051/120] Vpnaas: Create IKE policy (#785) * Added result struct and file structure for IKE policies. Also added unit test for ike policy creation * added create function and unit test for ikepolicies * Added acceptance test for IKE policy creation * Added documentation * Fixed typo * formatting * renamed Acceptance tests, renamed variables, removed invalid fields * Added projectID field and removed line --- .../v2/extensions/vpnaas/ikepolicy_test.go | 24 ++++ .../v2/extensions/vpnaas/ipsecpolicy_test.go | 2 +- .../networking/v2/extensions/vpnaas/vpnaas.go | 24 ++++ .../v2/extensions/vpnaas/ikepolicies/doc.go | 21 ++++ .../extensions/vpnaas/ikepolicies/requests.go | 107 ++++++++++++++++++ .../extensions/vpnaas/ikepolicies/results.go | 65 +++++++++++ .../ikepolicies/testing/requests_test.go | 85 ++++++++++++++ .../v2/extensions/vpnaas/ikepolicies/urls.go | 16 +++ 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/ikepolicies/urls.go 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..365745a9d7 --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go @@ -0,0 +1,24 @@ +// +build acceptance networking vpnaas + +package vpnaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" +) + +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) + } + + tools.PrintResource(t, policy) +} diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go index e4ee3d4063..2d7b4dac30 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go @@ -10,7 +10,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies" ) -func TestPolicyCRUD(t *testing.T) { +func TestIPSecPolicyCRUD(t *testing.T) { client, err := clients.NewNetworkV2Client() if err != nil { t.Fatalf("Unable to create a network client: %v", err) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index b899c624f4..385597ed42 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -5,6 +5,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/acceptance/tools" + "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" ) @@ -67,6 +68,29 @@ func CreateIPSecPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ipsecp 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 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. 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..6b65ff9985 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -0,0 +1,21 @@ +/* +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) + } + +*/ +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..ed42ddf70e --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go @@ -0,0 +1,107 @@ +package ikepolicies + +import "github.com/gophercloud/gophercloud" + +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 +} 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..01ea95e429 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go @@ -0,0 +1,65 @@ +package ikepolicies + +import "github.com/gophercloud/gophercloud" + +// 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 +} + +type CreateResult 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..f8485e4256 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go @@ -0,0 +1,85 @@ +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" + 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) +} 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) +} From 7cab6edf6ad120e9a3a8b84724e1fdb0a8158bdb Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 28 Feb 2018 04:08:15 +0100 Subject: [PATCH 052/120] Vpnaas: Show IKE policy details (#786) * Added unit test for ike policy get operation * Added get request * fixed unit test * Added acceptance test * added project id to test * added documentation --- .../v2/extensions/vpnaas/ikepolicy_test.go | 9 ++- .../v2/extensions/vpnaas/ikepolicies/doc.go | 7 +++ .../extensions/vpnaas/ikepolicies/requests.go | 6 ++ .../extensions/vpnaas/ikepolicies/results.go | 8 +++ .../ikepolicies/testing/requests_test.go | 55 +++++++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go index 365745a9d7..c721007bea 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go @@ -7,6 +7,7 @@ import ( "github.com/gophercloud/gophercloud/acceptance/clients" "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ikepolicies" ) func TestIKEPolicyCRUD(t *testing.T) { @@ -19,6 +20,12 @@ func TestIKEPolicyCRUD(t *testing.T) { if err != nil { t.Fatalf("Unable to create IKE policy: %v", err) } - 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) + } diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go index 6b65ff9985..9d4803de67 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -17,5 +17,12 @@ Example to Create an IKE policy 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) + } + */ package ikepolicies diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go index ed42ddf70e..be7fbe2f5f 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go @@ -105,3 +105,9 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul _, 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go index 01ea95e429..411366efd9 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go @@ -60,6 +60,14 @@ func (r commonResult) Extract() (*Policy, error) { 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 } + +// GetResult represents the result of a Get operation. Call its Extract method to +// interpret it as a Policy. +type GetResult 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 index f8485e4256..5f864e94a8 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go @@ -83,3 +83,58 @@ func TestCreate(t *testing.T) { } 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) +} From 3ffde9a2861d6120fa02fcfc6fe70dc24aa10742 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 28 Feb 2018 04:25:30 +0100 Subject: [PATCH 053/120] Vpnaas: List IPSec Policies (#772) * Added unit test for list function * Added required parts to results.go for listing function * Added list request * Added acceptance test for list policy * removed id field from Listopts, removed Lifetime field from ListOpts, added projectID to unit test * Added documentation --- .../v2/extensions/vpnaas/ipsecpolicy_test.go | 21 ++++++ .../v2/extensions/vpnaas/ipsecpolicies/doc.go | 13 ++++ .../vpnaas/ipsecpolicies/requests.go | 49 +++++++++++- .../vpnaas/ipsecpolicies/results.go | 38 ++++++++++ .../ipsecpolicies/testing/requests_test.go | 75 +++++++++++++++++++ 5 files changed, 195 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go index 2d7b4dac30..4a9905bb93 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go @@ -10,6 +10,27 @@ import ( "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies" ) +func TestPolicyList(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 { diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go index 4da16abeba..26ce7b5980 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go @@ -19,5 +19,18 @@ Example to Delete a Policy if err != nil { panic(err) } + +Example to List IPSec policies + + 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) + } + */ package ipsecpolicies diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go index 0fc1813afd..d7c2ddc45b 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go @@ -1,6 +1,9 @@ package ipsecpolicies -import "github.com/gophercloud/gophercloud" +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) type TransformProtocol string type AuthAlgorithm string @@ -121,3 +124,47 @@ 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}} + }) +} diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go index dffab6444a..780ecbab8d 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go @@ -2,6 +2,7 @@ package ipsecpolicies import ( "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" ) // Policy is an IPSec Policy @@ -80,3 +81,40 @@ type DeleteResult struct { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go index 03bec8d070..d6148b59d6 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go @@ -7,6 +7,7 @@ import ( 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" ) @@ -166,3 +167,77 @@ func TestDelete(t *testing.T) { 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) + } +} From 95b324abc9c478b454cc572eec6c2b4852716764 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 28 Feb 2018 03:27:28 +0000 Subject: [PATCH 054/120] Networking v2: VPNaaS Doc fixes --- .../networking/v2/extensions/vpnaas/ikepolicies/doc.go | 6 +++--- .../networking/v2/extensions/vpnaas/ipsecpolicies/doc.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go index 9d4803de67..c1affed9ac 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -6,8 +6,8 @@ OpenStack Networking Service. Example to Create an IKE policy createOpts := ikepolicies.CreateOpts{ - Name: "ikepolicy1", - Description: "Description of ikepolicy1", + Name: "ikepolicy1", + Description: "Description of ikepolicy1", EncryptionAlgorithm: ikepolicies.EncryptionAlgorithm3DES, PFS: ikepolicies.PFSGroup5, } @@ -20,7 +20,7 @@ Example to Create an IKE policy 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 { + if err != nil { panic(err) } diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go index 26ce7b5980..d2d227f466 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go @@ -24,12 +24,12 @@ Example to List IPSec policies allPages, err := ipsecpolicies.List(client, nil).AllPages() if err != nil { - t.Fatalf("Unable to list IPSec policies: %v", err) + panic(err) } allPolicies, err := ipsecpolicies.ExtractPolicies(allPages) if err != nil { - t.Fatalf("Unable to extract policies: %v", err) + panic(err) } */ From a3a5eb426a650a1184b3964e2c5c6c78a03021e7 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 28 Feb 2018 17:36:07 +0100 Subject: [PATCH 055/120] Vpnaas: Service Get/List (#771) * unit test for List and Get * request functions for list and get added to requests.go * edited comment for accuracy * Added relevant parts for service listing to results.go * Made test compare structs instead of values * Added acceptance test * Added documentation in doc.go file * Added missing parameters * Added project_id as a field to result struct * removed id field from ListOpts --- .../v2/extensions/vpnaas/service_test.go | 28 +++++ .../v2/extensions/vpnaas/services/doc.go | 19 +++ .../v2/extensions/vpnaas/services/requests.go | 57 ++++++++- .../v2/extensions/vpnaas/services/results.go | 47 ++++++++ .../vpnaas/services/testing/requests_test.go | 111 +++++++++++++++++- 5 files changed, 260 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go index 658f0840d5..f88aa7611d 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go @@ -8,8 +8,30 @@ import ( "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 { @@ -28,5 +50,11 @@ func TestServiceCRUD(t *testing.T) { } 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/openstack/networking/v2/extensions/vpnaas/services/doc.go b/openstack/networking/v2/extensions/vpnaas/services/doc.go index 2060bcb086..bf4302a5aa 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/services/doc.go @@ -2,6 +2,25 @@ 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 diff --git a/openstack/networking/v2/extensions/vpnaas/services/requests.go b/openstack/networking/v2/extensions/vpnaas/services/requests.go index 7073508f9f..6cfd656a25 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/services/requests.go @@ -1,6 +1,9 @@ package services -import "github.com/gophercloud/gophercloud" +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) // CreateOptsBuilder allows extensions to add additional parameters to the // Create request. @@ -57,3 +60,55 @@ 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 { + 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 index 77bc02b76c..df30884e21 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/results.go +++ b/openstack/networking/v2/extensions/vpnaas/services/results.go @@ -2,6 +2,7 @@ package services import ( "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" ) // Service is a VPN Service @@ -9,6 +10,9 @@ 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"` @@ -46,6 +50,49 @@ 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 { diff --git a/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go index 450a56ad26..0bfa1ded67 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -46,7 +47,8 @@ func TestCreate(t *testing.T) { "tenant_id": "10039663455a446d8ba2cbb058b0f578", "external_v4_ip": "172.32.1.11", "id": "5c561d9d-eaea-45f6-ae3e-08d1a7080828", - "description": "OpenStack VPN service" + "description": "OpenStack VPN service", + "project_id": "10039663455a446d8ba2cbb058b0f578" } } `) @@ -69,6 +71,7 @@ func TestCreate(t *testing.T) { AdminStateUp: true, SubnetID: "", TenantID: "10039663455a446d8ba2cbb058b0f578", + ProjectID: "10039663455a446d8ba2cbb058b0f578", ExternalV4IP: "172.32.1.11", ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828", Description: "OpenStack VPN service", @@ -76,6 +79,112 @@ func TestCreate(t *testing.T) { 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() From 43de125425b9d90b4d5eb67fa9b568493d61416a Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 28 Feb 2018 17:43:44 +0100 Subject: [PATCH 056/120] Vpnaas: Delete IKE policy (#792) * Added ike policy delete operation * Added documentation --- .../v2/extensions/vpnaas/ikepolicy_test.go | 2 ++ .../networking/v2/extensions/vpnaas/vpnaas.go | 22 +++++++++++++++---- .../v2/extensions/vpnaas/ikepolicies/doc.go | 7 ++++++ .../extensions/vpnaas/ikepolicies/requests.go | 7 ++++++ .../extensions/vpnaas/ikepolicies/results.go | 6 +++++ .../ikepolicies/testing/requests_test.go | 14 ++++++++++++ 6 files changed, 54 insertions(+), 4 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go index c721007bea..c411ef68ec 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go @@ -20,6 +20,8 @@ func TestIKEPolicyCRUD(t *testing.T) { 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() diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index 385597ed42..f5f9a110d3 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -73,7 +73,7 @@ func CreateIPSecPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ipsecp func CreateIKEPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ikepolicies.Policy, error) { policyName := tools.RandomString("TESTACC-", 8) - t.Logf("Attempting to create policy %s", policyName) + t.Logf("Attempting to create IKE policy %s", policyName) createOpts := ikepolicies.CreateOpts{ Name: policyName, @@ -95,12 +95,26 @@ func CreateIKEPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ikepolic // 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 policy: %s", policyID) + t.Logf("Attempting to delete IPSec policy: %s", policyID) err := ipsecpolicies.Delete(client, policyID).ExtractErr() if err != nil { - t.Fatalf("Unable to delete policy %s: %v", policyID, err) + 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 policy: %s", policyID) + t.Logf("Deleted IKE policy: %s", policyID) } diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go index c1affed9ac..94fdbdd485 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -24,5 +24,12 @@ Example to Show the details of a specific IKE policy by ID panic(err) } +Example to Delete a Policy + + err := ikepolicies.Delete(client, "5291b189-fd84-46e5-84bd-78f40c05d69c").ExtractErr() + 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 index be7fbe2f5f..89c73c7e4e 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go @@ -111,3 +111,10 @@ 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go index 411366efd9..eb5bd8857b 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go @@ -71,3 +71,9 @@ type CreateResult struct { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go index 5f864e94a8..f5f1f9dc9b 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go @@ -138,3 +138,17 @@ func TestGet(t *testing.T) { } 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) +} From afbf0422412f5dc726fa12be280fa0c3cb31fcbd Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 28 Feb 2018 17:50:54 +0100 Subject: [PATCH 057/120] Vpnaas: Create Endpoint Group (#794) * Added request, result, unit test and acceptance test for endpoint group creation * Added documentation * switched acronyms to uppercase, changed ToGroupCreateMap to ToEndpointGroupCreateMap --- .../v2/extensions/vpnaas/group_test.go | 24 ++++++ .../networking/v2/extensions/vpnaas/vpnaas.go | 26 +++++++ .../extensions/vpnaas/endpointgroups/doc.go | 19 +++++ .../vpnaas/endpointgroups/requests.go | 58 ++++++++++++++ .../vpnaas/endpointgroups/results.go | 48 ++++++++++++ .../endpointgroups/testing/requests_test.go | 78 +++++++++++++++++++ .../extensions/vpnaas/endpointgroups/urls.go | 16 ++++ 7 files changed, 269 insertions(+) create mode 100644 acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go create mode 100644 openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go create mode 100644 openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go create mode 100644 openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/endpointgroups/urls.go 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..7305fcf8c6 --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go @@ -0,0 +1,24 @@ +// +build acceptance networking vpnaas + +package vpnaas + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" +) + +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) + } + + tools.PrintResource(t, group) +} diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index f5f9a110d3..a770895457 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -5,6 +5,7 @@ import ( "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" @@ -118,3 +119,28 @@ func DeleteIKEPolicy(t *testing.T, client *gophercloud.ServiceClient, policyID s 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 +} 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..3915124555 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go @@ -0,0 +1,19 @@ +/* +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 + } +*/ +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..47b9855894 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go @@ -0,0 +1,58 @@ +package endpointgroups + +import "github.com/gophercloud/gophercloud" + +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 +} 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..c4a6ade334 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go @@ -0,0 +1,48 @@ +package endpointgroups + +import ( + "github.com/gophercloud/gophercloud" +) + +// 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 +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as an endpoint group. +type CreateResult 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..f4fe3eb634 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go @@ -0,0 +1,78 @@ +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" + 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) +} 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) +} From 32083cdce4c64eff52ec57f82b2d3b522d1ba48a Mon Sep 17 00:00:00 2001 From: Colin Nolan Date: Fri, 2 Mar 2018 19:53:23 +0000 Subject: [PATCH 058/120] Corrects example to create a server with a key pair (#799) --- openstack/compute/v2/extensions/keypairs/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", } From f7e32fe391ee0d82498d64d244443936604f0e5d Mon Sep 17 00:00:00 2001 From: Fabian Ruff Date: Mon, 5 Mar 2018 17:52:58 +0100 Subject: [PATCH 059/120] Add support for custom 403 error handling (#714) * Add support for custom 403 error handling * Include full response body in error string --- errors.go | 18 ++++++++++++++++++ provider_client.go | 5 +++++ 2 files changed, 23 insertions(+) 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/provider_client.go b/provider_client.go index 72daeb0a3e..e93c236e17 100644 --- a/provider_client.go +++ b/provider_client.go @@ -298,6 +298,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 { From aa1f387e28bd82a1d4294b3ea48d7bf274437820 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Tue, 6 Mar 2018 02:27:45 +0100 Subject: [PATCH 060/120] Vpnaas: List IKE policies (#797) * Added list function for IKE policies(with unit and acceptance tests * Added documentation for IKE policy list function --- .../v2/extensions/vpnaas/ikepolicy_test.go | 21 ++++++ .../v2/extensions/vpnaas/ipsecpolicy_test.go | 2 +- .../v2/extensions/vpnaas/ikepolicies/doc.go | 14 ++++ .../extensions/vpnaas/ikepolicies/requests.go | 50 ++++++++++++- .../extensions/vpnaas/ikepolicies/results.go | 42 ++++++++++- .../ikepolicies/testing/requests_test.go | 75 +++++++++++++++++++ 6 files changed, 201 insertions(+), 3 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go index c411ef68ec..891eea81f5 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go @@ -10,6 +10,27 @@ import ( "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 { diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go index 4a9905bb93..87f3aa490f 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go @@ -10,7 +10,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/ipsecpolicies" ) -func TestPolicyList(t *testing.T) { +func TestIPSecPolicyList(t *testing.T) { client, err := clients.NewNetworkV2Client() if err != nil { t.Fatalf("Unable to create a network client: %v", err) diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go index 94fdbdd485..23630a17f4 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -24,6 +24,7 @@ Example to Show the details of a specific IKE policy by ID panic(err) } + Example to Delete a Policy err := ikepolicies.Delete(client, "5291b189-fd84-46e5-84bd-78f40c05d69c").ExtractErr() @@ -31,5 +32,18 @@ Example to Delete a Policy 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 index 89c73c7e4e..f6dfcdd46f 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go @@ -1,6 +1,9 @@ package ikepolicies -import "github.com/gophercloud/gophercloud" +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) type AuthAlgorithm string type EncryptionAlgorithm string @@ -118,3 +121,48 @@ 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}} + }) +} diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go index eb5bd8857b..d298bc53a5 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go @@ -1,6 +1,9 @@ package ikepolicies -import "github.com/gophercloud/gophercloud" +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) // Policy is an IKE Policy type Policy struct { @@ -60,6 +63,43 @@ func (r commonResult) Extract() (*Policy, error) { 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 { diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go index f5f1f9dc9b..8f32432f94 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go @@ -7,6 +7,7 @@ import ( 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" ) @@ -152,3 +153,77 @@ func TestDelete(t *testing.T) { 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) + } +} From 8eb2fcc5bd524d966c59efb5db1cbbdc8c6a82fd Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Tue, 6 Mar 2018 02:30:03 +0100 Subject: [PATCH 061/120] Vpnaas: Get Endpoint Group (#798) * Added endpointgroup get operation * Fixed wrong constant in doc --- .../v2/extensions/vpnaas/group_test.go | 9 +++- .../extensions/vpnaas/endpointgroups/doc.go | 9 +++- .../vpnaas/endpointgroups/requests.go | 6 +++ .../vpnaas/endpointgroups/results.go | 6 +++ .../endpointgroups/testing/requests_test.go | 46 +++++++++++++++++++ 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go index 7305fcf8c6..ce80011be1 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go @@ -7,6 +7,7 @@ import ( "github.com/gophercloud/gophercloud/acceptance/clients" "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/vpnaas/endpointgroups" ) func TestGroupCRUD(t *testing.T) { @@ -19,6 +20,12 @@ func TestGroupCRUD(t *testing.T) { if err != nil { t.Fatalf("Unable to create Endpoint group: %v", err) } - 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) + } diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go index 3915124555..bdd986bdc9 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go @@ -5,7 +5,7 @@ Example to create an Endpoint Group createOpts := endpointgroups.CreateOpts{ Name: groupName, - Type: endpointgroups.TypeCidr, + Type: endpointgroups.TypeCIDR, Endpoints: []string{ "10.2.0.0/24", "10.3.0.0/24", @@ -15,5 +15,12 @@ Example to create an Endpoint Group 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) + } */ package endpointgroups diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go index 47b9855894..5279f26aca 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go @@ -56,3 +56,9 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul _, 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go index c4a6ade334..6804d53757 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go @@ -46,3 +46,9 @@ func (r commonResult) Extract() (*EndpointGroup, error) { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go index f4fe3eb634..ba8d33c458 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go @@ -76,3 +76,49 @@ func TestCreate(t *testing.T) { } 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) +} From 9b604ddaf1a607170b72aa50c34e1977edffbfea Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Tue, 6 Mar 2018 02:36:37 +0100 Subject: [PATCH 062/120] Vpnaas: Update IPSec Policy (#774) * Added update methods to requests.go * Added updateresult to results.go * Added unit test for update function * Added acceptance test * fixed typo * Changed name and description to pointers in updateOpts * Adjusted the pointers for updateOpts * fixed indentation and log message --- .../v2/extensions/vpnaas/ipsecpolicy_test.go | 10 +++ .../vpnaas/ipsecpolicies/requests.go | 41 ++++++++++ .../vpnaas/ipsecpolicies/results.go | 6 ++ .../ipsecpolicies/testing/requests_test.go | 78 +++++++++++++++++++ 4 files changed, 135 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go index 87f3aa490f..2fdee7dd92 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go @@ -42,7 +42,17 @@ func TestIPSecPolicyCRUD(t *testing.T) { 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() diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go index d7c2ddc45b..9496365ca4 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go @@ -168,3 +168,44 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { 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 index 780ecbab8d..eda4a1bd23 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/results.go @@ -118,3 +118,9 @@ func ExtractPolicies(r pagination.Page) ([]Policy, error) { 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 index d6148b59d6..702bd38355 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/testing/requests_test.go @@ -241,3 +241,81 @@ func TestList(t *testing.T) { 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) +} From d7d64b9b261b431428e50b460c2b52dea74c999f Mon Sep 17 00:00:00 2001 From: Andrei Ozerov Date: Tue, 6 Mar 2018 05:57:07 +0300 Subject: [PATCH 063/120] Add networking port extra DHCP opts Update call (#804) * Add networking port extra DHCP opts GET support Add a new networking v2 extension to provide extra DHCP configuration support for ports. Add basic structures with a unit test. * Fix networking port extra DHCP structs names Use "ExtraDHCPOptsExt" and "ExtraDHCPOpts" for basic structs names of the extra dhcp options extenstion. Upgrade unit-test and fix spacing in the unit test fixture. * Move network port extra DHCP tests Move the extra DHCP options GET unit test to the standard networking port package. * Add networking port extra DHCP opts Create call Add a Create request to the networking v2 extra DHCP options extension. Add unit and acceptance tests. * Move network port extra DHCP creation tests Move the extra DHCP options Create call unit and acceptance tests to the standard networking port package. * Fix inline comments for the extra DHCP creation Rename comment for the CreateOptsExt.ExtraDHCPOpts field. * Add networking port extra DHCP opts Update call Add an Update request to the networking v2 extra DHCP options extension. Add unit and acceptance tests. --- .../openstack/networking/v2/networking.go | 50 ++++++ .../openstack/networking/v2/ports_test.go | 54 ++++++ .../v2/extensions/extradhcpopts/doc.go | 75 ++++++++ .../v2/extensions/extradhcpopts/requests.go | 75 ++++++++ .../v2/extensions/extradhcpopts/results.go | 32 ++++ .../networking/v2/ports/testing/fixtures.go | 139 +++++++++++++++ .../v2/ports/testing/requests_test.go | 168 ++++++++++++++++++ 7 files changed, 593 insertions(+) create mode 100644 openstack/networking/v2/extensions/extradhcpopts/doc.go create mode 100644 openstack/networking/v2/extensions/extradhcpopts/requests.go create mode 100644 openstack/networking/v2/extensions/extradhcpopts/results.go diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go index e889267674..e8bd67c046 100644 --- a/acceptance/openstack/networking/v2/networking.go +++ b/acceptance/openstack/networking/v2/networking.go @@ -6,6 +6,7 @@ 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" @@ -336,3 +337,52 @@ func WaitForPortToCreate(client *gophercloud.ServiceClient, portID string, secs return false, nil }) } + +// PortWithDHCPOpts represents a port with extra DHCP options configuration. +type PortWithDHCPOpts struct { + ports.Port + extradhcpopts.ExtraDHCPOptsExt +} + +// CreatePortWithDHCPOpts will create a port with DHCP options on the specified subnet. +// An error will be returned if the port could not be created. +func CreatePortWithDHCPOpts(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID string) (*PortWithDHCPOpts, 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.ExtraDHCPOpts{ + { + OptName: "test_option_1", + OptValue: "test_value_1", + }, + }, + } + port := &PortWithDHCPOpts{} + + 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/ports_test.go b/acceptance/openstack/networking/v2/ports_test.go index 89ac3b5382..6466aadfb0 100644 --- a/acceptance/openstack/networking/v2/ports_test.go +++ b/acceptance/openstack/networking/v2/ports_test.go @@ -8,6 +8,7 @@ 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" ) @@ -388,3 +389,56 @@ func TestPortsPortSecurityCRUD(t *testing.T) { tools.PrintResource(t, portWithExt) } + +func TestPortsWithDHCPOptsCRUD(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 := CreatePortWithDHCPOpts(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, + } + updateOpts := extradhcpopts.UpdateOptsExt{ + UpdateOptsBuilder: portUpdateOpts, + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + { + OptName: "test_option_2", + OptValue: "test_value_2", + }, + }, + } + + newPort := &PortWithDHCPOpts{} + 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/openstack/networking/v2/extensions/extradhcpopts/doc.go b/openstack/networking/v2/extensions/extradhcpopts/doc.go new file mode 100644 index 0000000000..d36b9025c1 --- /dev/null +++ b/openstack/networking/v2/extensions/extradhcpopts/doc.go @@ -0,0 +1,75 @@ +/* +Package extradhcpopts allow to work with extra DHCP functionality of Neutron ports. + +Example to Get a Port with DHCP opts + + 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 DHCP opts + + 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.ExtraDHCPOpts{ + { + OptName: "optionA", + OptValue: "valueA", + }, + }, + } + var s struct { + ports.Port + extradhcpopts.ExtraDHCPOptsExt + } + + err := ports.Create(networkClient, createOpts).ExtractInto(&s) + if err != nil { + panic(err) + } + +Example to Update a Port with DHCP opts + + portUpdateOpts := ports.UpdateOpts{ + Name: "updated-dhcp-conf-port", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + } + updateOpts := extradhcpopts.UpdateOptsExt{ + UpdateOptsBuilder: portUpdateOpts, + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + { + OptName: "optionB", + OptValue: "valueB", + }, + }, + } + var s struct { + ports.Port + extradhcpopts.ExtraDHCPOptsExt + } + 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..ead0950312 --- /dev/null +++ b/openstack/networking/v2/extensions/extradhcpopts/requests.go @@ -0,0 +1,75 @@ +package extradhcpopts + +import ( + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" +) + +// CreateOptsExt adds port 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 []ExtraDHCPOpts `json:"extra_dhcp_opts,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.DHCPOpts to a slice of maps. + if opts.ExtraDHCPOpts != nil { + extraDHCPOpts := make([]map[string]interface{}, len(opts.ExtraDHCPOpts)) + for i, opt := range opts.ExtraDHCPOpts { + extraDHCPOptMap, err := opt.ToMap() + if err != nil { + return nil, err + } + extraDHCPOpts[i] = extraDHCPOptMap + } + port["extra_dhcp_opts"] = extraDHCPOpts + } + + return base, nil +} + +// UpdateOptsExt adds port 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 []ExtraDHCPOpts `json:"extra_dhcp_opts,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 { + extraDHCPOptMap, err := opt.ToMap() + if err != nil { + return nil, err + } + extraDHCPOpts[i] = extraDHCPOptMap + } + 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..0a79ebd81c --- /dev/null +++ b/openstack/networking/v2/extensions/extradhcpopts/results.go @@ -0,0 +1,32 @@ +package extradhcpopts + +import "github.com/gophercloud/gophercloud" + +// ExtraDHCPOptsExt is a struct that contains different DHCP options for a single port. +type ExtraDHCPOptsExt struct { + ExtraDHCPOpts []ExtraDHCPOpts `json:"extra_dhcp_opts"` +} + +// ExtraDHCPOpts represents a single set of extra DHCP options for a single port. +type ExtraDHCPOpts struct { + // Name is the name of a single DHCP option. + OptName string `json:"opt_name"` + + // Value 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,omitempty"` +} + +// ToMap is a helper function to convert an individual DHCPOpts structure +// into a sub-map. +func (opts ExtraDHCPOpts) ToMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/openstack/networking/v2/ports/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go index d8e57cbbae..96d70a706d 100644 --- a/openstack/networking/v2/ports/testing/fixtures.go +++ b/openstack/networking/v2/ports/testing/fixtures.go @@ -520,3 +520,142 @@ const DontUpdateAllowedAddressPairsResponse = ` } } ` + +// GetWithDHCPOptsResponse represents a raw port response with extra DHCP options. +const GetWithDHCPOptsResponse = ` +{ + "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": "" + } +} +` + +// CreateWithDHCPOptsRequest represents a raw port creation request with extra DHCP options. +const CreateWithDHCPOptsRequest = ` +{ + "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" + } + ] + } +} +` + +// CreateWithDHCPOptsResponse represents a raw port creation response with extra DHCP options. +const CreateWithDHCPOptsResponse = ` +{ + "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": "" + } +} +` + +// UpdateWithDHCPOptsRequest represents a raw port update request with extra DHCP options. +const UpdateWithDHCPOptsRequest = ` +{ + "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": "option2", + "opt_value": "value2" + } + ] + } +} +` + +// UpdateWithDHCPOptsResponse represents a raw port update response with extra DHCP options. +const UpdateWithDHCPOptsResponse = ` +{ + "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 87a18864d4..a58badb08e 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" @@ -569,3 +570,170 @@ func TestDelete(t *testing.T) { res := ports.Delete(fake.ServiceClient(), "65c0ee9f-d634-4522-8954-51021b570b0d") th.AssertNoErr(t, res.Err) } + +func TestGetWithDHCPOpts(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, GetWithDHCPOptsResponse) + }) + + 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.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + {OptName: "option1", OptValue: "value1", IPVersion: 4}, + {OptName: "option2", OptValue: "value2", IPVersion: 4}, + }, + }) + 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, "") +} + +func TestCreateWithDHCPOpts(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, CreateWithDHCPOptsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + fmt.Fprintf(w, CreateWithDHCPOptsResponse) + }) + + 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.ExtraDHCPOpts{ + { + 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.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + {OptName: "option1", OptValue: "value1", IPVersion: 4}, + }, + }) + 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, "") +} + +func TestUpdateWithDHCPOpts(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, UpdateWithDHCPOptsRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, UpdateWithDHCPOptsResponse) + }) + + portUpdateOpts := ports.UpdateOpts{ + Name: "updated-port-with-dhcp-opts", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.3"}, + }, + } + + updateOpts := extradhcpopts.UpdateOptsExt{ + UpdateOptsBuilder: portUpdateOpts, + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + { + OptName: "option2", + OptValue: "value2", + }, + }, + } + + 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.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + {OptName: "option2", OptValue: "value2", IPVersion: 4}, + }, + }) + 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, "") +} From 282b17ffd963a1a5e81c3b2c9f52622ad33612a8 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Tue, 6 Mar 2018 02:49:42 +0000 Subject: [PATCH 064/120] Rename ExtraDHCPOpts to ExtraDHCPOpt. Doc cleanup --- .../openstack/networking/v2/networking.go | 14 +++++----- .../openstack/networking/v2/ports_test.go | 8 +++--- .../v2/extensions/extradhcpopts/doc.go | 10 +++---- .../v2/extensions/extradhcpopts/requests.go | 10 +++---- .../v2/extensions/extradhcpopts/results.go | 13 +++++----- .../networking/v2/ports/testing/fixtures.go | 25 +++++++++++------- .../v2/ports/testing/requests_test.go | 26 +++++++++---------- 7 files changed, 56 insertions(+), 50 deletions(-) diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go index e8bd67c046..58a59f48ae 100644 --- a/acceptance/openstack/networking/v2/networking.go +++ b/acceptance/openstack/networking/v2/networking.go @@ -338,15 +338,15 @@ func WaitForPortToCreate(client *gophercloud.ServiceClient, portID string, secs }) } -// PortWithDHCPOpts represents a port with extra DHCP options configuration. -type PortWithDHCPOpts struct { +// PortWithExtraDHCPOpts represents a port with extra DHCP options configuration. +type PortWithExtraDHCPOpts struct { ports.Port extradhcpopts.ExtraDHCPOptsExt } -// CreatePortWithDHCPOpts will create a port with DHCP options on the specified subnet. -// An error will be returned if the port could not be created. -func CreatePortWithDHCPOpts(t *testing.T, client *gophercloud.ServiceClient, networkID, subnetID string) (*PortWithDHCPOpts, error) { +// 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) @@ -359,14 +359,14 @@ func CreatePortWithDHCPOpts(t *testing.T, client *gophercloud.ServiceClient, net } createOpts := extradhcpopts.CreateOptsExt{ CreateOptsBuilder: portCreateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ { OptName: "test_option_1", OptValue: "test_value_1", }, }, } - port := &PortWithDHCPOpts{} + port := &PortWithExtraDHCPOpts{} err := ports.Create(client, createOpts).ExtractInto(port) if err != nil { diff --git a/acceptance/openstack/networking/v2/ports_test.go b/acceptance/openstack/networking/v2/ports_test.go index 6466aadfb0..d47bc3a196 100644 --- a/acceptance/openstack/networking/v2/ports_test.go +++ b/acceptance/openstack/networking/v2/ports_test.go @@ -390,7 +390,7 @@ func TestPortsPortSecurityCRUD(t *testing.T) { tools.PrintResource(t, portWithExt) } -func TestPortsWithDHCPOptsCRUD(t *testing.T) { +func TestPortsWithExtraDHCPOptsCRUD(t *testing.T) { client, err := clients.NewNetworkV2Client() if err != nil { t.Fatalf("Unable to create a network client: %v", err) @@ -411,7 +411,7 @@ func TestPortsWithDHCPOptsCRUD(t *testing.T) { defer DeleteSubnet(t, client, subnet.ID) // Create a port with extra DHCP options. - port, err := CreatePortWithDHCPOpts(t, client, network.ID, subnet.ID) + port, err := CreatePortWithExtraDHCPOpts(t, client, network.ID, subnet.ID) if err != nil { t.Fatalf("Unable to create a port: %v", err) } @@ -426,7 +426,7 @@ func TestPortsWithDHCPOptsCRUD(t *testing.T) { } updateOpts := extradhcpopts.UpdateOptsExt{ UpdateOptsBuilder: portUpdateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ { OptName: "test_option_2", OptValue: "test_value_2", @@ -434,7 +434,7 @@ func TestPortsWithDHCPOptsCRUD(t *testing.T) { }, } - newPort := &PortWithDHCPOpts{} + newPort := &PortWithExtraDHCPOpts{} err = ports.Update(client, port.ID, updateOpts).ExtractInto(newPort) if err != nil { t.Fatalf("Could not update port: %v", err) diff --git a/openstack/networking/v2/extensions/extradhcpopts/doc.go b/openstack/networking/v2/extensions/extradhcpopts/doc.go index d36b9025c1..487fdd6cbe 100644 --- a/openstack/networking/v2/extensions/extradhcpopts/doc.go +++ b/openstack/networking/v2/extensions/extradhcpopts/doc.go @@ -1,7 +1,7 @@ /* Package extradhcpopts allow to work with extra DHCP functionality of Neutron ports. -Example to Get a Port with DHCP opts +Example to Get a Port with Extra DHCP Options portID := "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2" var s struct { @@ -14,7 +14,7 @@ Example to Get a Port with DHCP opts panic(err) } -Example to Create a Port with DHCP opts +Example to Create a Port with Extra DHCP Options adminStateUp := true portCreateOpts := ports.CreateOpts{ @@ -27,7 +27,7 @@ Example to Create a Port with DHCP opts } createOpts := extradhcpopts.CreateOptsExt{ CreateOptsBuilder: portCreateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ { OptName: "optionA", OptValue: "valueA", @@ -44,7 +44,7 @@ Example to Create a Port with DHCP opts panic(err) } -Example to Update a Port with DHCP opts +Example to Update a Port with Extra DHCP Options portUpdateOpts := ports.UpdateOpts{ Name: "updated-dhcp-conf-port", @@ -54,7 +54,7 @@ Example to Update a Port with DHCP opts } updateOpts := extradhcpopts.UpdateOptsExt{ UpdateOptsBuilder: portUpdateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ { OptName: "optionB", OptValue: "valueB", diff --git a/openstack/networking/v2/extensions/extradhcpopts/requests.go b/openstack/networking/v2/extensions/extradhcpopts/requests.go index ead0950312..9893f84d84 100644 --- a/openstack/networking/v2/extensions/extradhcpopts/requests.go +++ b/openstack/networking/v2/extensions/extradhcpopts/requests.go @@ -4,14 +4,14 @@ import ( "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" ) -// CreateOptsExt adds port DHCP options to the base ports.CreateOpts. +// 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 []ExtraDHCPOpts `json:"extra_dhcp_opts,omitempty"` + ExtraDHCPOpts []ExtraDHCPOpt `json:"extra_dhcp_opts,omitempty"` } // ToPortCreateMap casts a CreateOptsExt struct to a map. @@ -23,7 +23,7 @@ func (opts CreateOptsExt) ToPortCreateMap() (map[string]interface{}, error) { port := base["port"].(map[string]interface{}) - // Convert opts.DHCPOpts to a slice of maps. + // 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 { @@ -39,14 +39,14 @@ func (opts CreateOptsExt) ToPortCreateMap() (map[string]interface{}, error) { return base, nil } -// UpdateOptsExt adds port DHCP options to the base ports.UpdateOpts +// 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 []ExtraDHCPOpts `json:"extra_dhcp_opts,omitempty"` + ExtraDHCPOpts []ExtraDHCPOpt `json:"extra_dhcp_opts,omitempty"` } // ToPortUpdateMap casts an UpdateOpts struct to a map. diff --git a/openstack/networking/v2/extensions/extradhcpopts/results.go b/openstack/networking/v2/extensions/extradhcpopts/results.go index 0a79ebd81c..042d425a58 100644 --- a/openstack/networking/v2/extensions/extradhcpopts/results.go +++ b/openstack/networking/v2/extensions/extradhcpopts/results.go @@ -2,13 +2,14 @@ package extradhcpopts import "github.com/gophercloud/gophercloud" -// ExtraDHCPOptsExt is a struct that contains different DHCP options for a single port. +// ExtraDHCPOptsExt is a struct that contains different DHCP options for a +// single port. type ExtraDHCPOptsExt struct { - ExtraDHCPOpts []ExtraDHCPOpts `json:"extra_dhcp_opts"` + ExtraDHCPOpts []ExtraDHCPOpt `json:"extra_dhcp_opts"` } -// ExtraDHCPOpts represents a single set of extra DHCP options for a single port. -type ExtraDHCPOpts struct { +// ExtraDHCPOpt represents a single set of extra DHCP options for a single port. +type ExtraDHCPOpt struct { // Name is the name of a single DHCP option. OptName string `json:"opt_name"` @@ -20,9 +21,9 @@ type ExtraDHCPOpts struct { IPVersion int `json:"ip_version,omitempty"` } -// ToMap is a helper function to convert an individual DHCPOpts structure +// ToMap is a helper function to convert an individual ExtraDHCPOpt structure // into a sub-map. -func (opts ExtraDHCPOpts) ToMap() (map[string]interface{}, error) { +func (opts ExtraDHCPOpt) ToMap() (map[string]interface{}, error) { b, err := gophercloud.BuildRequestBody(opts, "") if err != nil { return nil, err diff --git a/openstack/networking/v2/ports/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go index 96d70a706d..870572cb64 100644 --- a/openstack/networking/v2/ports/testing/fixtures.go +++ b/openstack/networking/v2/ports/testing/fixtures.go @@ -521,8 +521,9 @@ const DontUpdateAllowedAddressPairsResponse = ` } ` -// GetWithDHCPOptsResponse represents a raw port response with extra DHCP options. -const GetWithDHCPOptsResponse = ` +// GetWithExtraDHCPOptsResponse represents a raw port response with extra +// DHCP options. +const GetWithExtraDHCPOptsResponse = ` { "port": { "status": "ACTIVE", @@ -556,8 +557,9 @@ const GetWithDHCPOptsResponse = ` } ` -// CreateWithDHCPOptsRequest represents a raw port creation request with extra DHCP options. -const CreateWithDHCPOptsRequest = ` +// CreateWithExtraDHCPOptsRequest represents a raw port creation request +// with extra DHCP options. +const CreateWithExtraDHCPOptsRequest = ` { "port": { "network_id": "a87cc70a-3e15-4acf-8205-9b711a3531b7", @@ -579,8 +581,9 @@ const CreateWithDHCPOptsRequest = ` } ` -// CreateWithDHCPOptsResponse represents a raw port creation response with extra DHCP options. -const CreateWithDHCPOptsResponse = ` +// CreateWithExtraDHCPOptsResponse represents a raw port creation response +// with extra DHCP options. +const CreateWithExtraDHCPOptsResponse = ` { "port": { "status": "DOWN", @@ -609,8 +612,9 @@ const CreateWithDHCPOptsResponse = ` } ` -// UpdateWithDHCPOptsRequest represents a raw port update request with extra DHCP options. -const UpdateWithDHCPOptsRequest = ` +// UpdateWithExtraDHCPOptsRequest represents a raw port update request with +// extra DHCP options. +const UpdateWithExtraDHCPOptsRequest = ` { "port": { "name": "updated-port-with-dhcp-opts", @@ -630,8 +634,9 @@ const UpdateWithDHCPOptsRequest = ` } ` -// UpdateWithDHCPOptsResponse represents a raw port update response with extra DHCP options. -const UpdateWithDHCPOptsResponse = ` +// UpdateWithExtraDHCPOptsResponse represents a raw port update response with +// extra DHCP options. +const UpdateWithExtraDHCPOptsResponse = ` { "port": { "status": "DOWN", diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go index a58badb08e..7df21c7a35 100644 --- a/openstack/networking/v2/ports/testing/requests_test.go +++ b/openstack/networking/v2/ports/testing/requests_test.go @@ -571,7 +571,7 @@ func TestDelete(t *testing.T) { th.AssertNoErr(t, res.Err) } -func TestGetWithDHCPOpts(t *testing.T) { +func TestGetWithExtraDHCPOpts(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() @@ -582,7 +582,7 @@ func TestGetWithDHCPOpts(t *testing.T) { w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, GetWithDHCPOptsResponse) + fmt.Fprintf(w, GetWithExtraDHCPOptsResponse) }) var s struct { @@ -597,7 +597,7 @@ func TestGetWithDHCPOpts(t *testing.T) { th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") th.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ {OptName: "option1", OptValue: "value1", IPVersion: 4}, {OptName: "option2", OptValue: "value2", IPVersion: 4}, }, @@ -613,7 +613,7 @@ func TestGetWithDHCPOpts(t *testing.T) { th.AssertEquals(t, s.DeviceID, "") } -func TestCreateWithDHCPOpts(t *testing.T) { +func TestCreateWithExtraDHCPOpts(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() @@ -622,12 +622,12 @@ func TestCreateWithDHCPOpts(t *testing.T) { 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, CreateWithDHCPOptsRequest) + th.TestJSONRequest(t, r, CreateWithExtraDHCPOptsRequest) w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - fmt.Fprintf(w, CreateWithDHCPOptsResponse) + fmt.Fprintf(w, CreateWithExtraDHCPOptsResponse) }) adminStateUp := true @@ -642,7 +642,7 @@ func TestCreateWithDHCPOpts(t *testing.T) { createOpts := extradhcpopts.CreateOptsExt{ CreateOptsBuilder: portCreateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ { OptName: "option1", OptValue: "value1", @@ -662,7 +662,7 @@ func TestCreateWithDHCPOpts(t *testing.T) { th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") th.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ {OptName: "option1", OptValue: "value1", IPVersion: 4}, }, }) @@ -677,7 +677,7 @@ func TestCreateWithDHCPOpts(t *testing.T) { th.AssertEquals(t, s.DeviceID, "") } -func TestUpdateWithDHCPOpts(t *testing.T) { +func TestUpdateWithExtraDHCPOpts(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() @@ -686,12 +686,12 @@ func TestUpdateWithDHCPOpts(t *testing.T) { 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, UpdateWithDHCPOptsRequest) + th.TestJSONRequest(t, r, UpdateWithExtraDHCPOptsRequest) w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, UpdateWithDHCPOptsResponse) + fmt.Fprintf(w, UpdateWithExtraDHCPOptsResponse) }) portUpdateOpts := ports.UpdateOpts{ @@ -703,7 +703,7 @@ func TestUpdateWithDHCPOpts(t *testing.T) { updateOpts := extradhcpopts.UpdateOptsExt{ UpdateOptsBuilder: portUpdateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ { OptName: "option2", OptValue: "value2", @@ -723,7 +723,7 @@ func TestUpdateWithDHCPOpts(t *testing.T) { th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") th.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpts{ + ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ {OptName: "option2", OptValue: "value2", IPVersion: 4}, }, }) From fe864ba585e153c296567b9ab4bbb1345d05900a Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Mon, 5 Mar 2018 21:08:46 -0700 Subject: [PATCH 065/120] Image Service v2: Add Missing Fields to ListOpts (#780) * Add date filtering fields to list operation * Changing Tag to Tags * adding container_format and disk_format fields to list operation * fix FilterNE/FilterEQ typo * Add comment about filter behavior when no filter is applied * Add ID to ListOpts * Change status to a string * Add descriptive comments about the in operator * Re-add comment that was accidentaly removed * This commit makes the following changes: * Changes the ListOpts.Status type back to images.ImageStatus * Renames the ListOpts.CreatedAt field to CreatedAtQuery * Renames the ListOpts.UpdatedAt field to UpdatedAtQuery --- .../openstack/imageservice/v2/images_test.go | 89 +++++++++++++++++++ .../openstack/imageservice/v2/imageservice.go | 1 + openstack/imageservice/v2/images/requests.go | 55 +++++++++++- .../v2/images/testing/fixtures.go | 41 +++++++++ .../v2/images/testing/requests_test.go | 88 ++++++++++++++++++ openstack/imageservice/v2/images/types.go | 25 ++++++ 6 files changed, 298 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/imageservice/v2/images_test.go b/acceptance/openstack/imageservice/v2/images_test.go index c2a8987319..04926109f6 100644 --- a/acceptance/openstack/imageservice/v2/images_test.go +++ b/acceptance/openstack/imageservice/v2/images_test.go @@ -4,6 +4,7 @@ package v2 import ( "testing" + "time" "github.com/gophercloud/gophercloud/acceptance/clients" "github.com/gophercloud/gophercloud/acceptance/tools" @@ -78,3 +79,91 @@ func TestImagesCreateDestroyEmptyImage(t *testing.T) { tools.PrintResource(t, image) } + +func TestImagesListByDate(t *testing.T) { + client, err := clients.NewImageServiceV2Client() + if err != nil { + t.Fatalf("Unable to create an image service client: %v", err) + } + + date := time.Date(2014, 1, 1, 1, 1, 1, 0, time.UTC) + listOpts := images.ListOpts{ + Limit: 1, + CreatedAt: &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) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + t.Fatalf("Unable to extract images: %v", err) + } + + 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, + CreatedAt: &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) + } + + allImages, err = images.ExtractImages(allPages) + if err != nil { + t.Fatalf("Unable to extract images: %v", err) + } + + if len(allImages) > 0 { + t.Fatalf("Expected 0 images, got %d", len(allImages)) + } +} + +func TestImagesFilter(t *testing.T) { + client, err := clients.NewImageServiceV2Client() + if err != nil { + t.Fatalf("Unable to create an image service client: %v", err) + } + + image, err := CreateEmptyImage(t, client) + if err != nil { + t.Fatalf("Unable to create empty image: %v", err) + } + + defer DeleteImage(t, client, image) + + listOpts := images.ListOpts{ + Tags: []string{"foo", "bar"}, + ContainerFormat: "bare", + DiskFormat: "qcow2", + } + + allPages, err := images.List(client, listOpts).AllPages() + if err != nil { + t.Fatalf("Unable to retrieve all images: %v", err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + t.Fatalf("Unable to extract images: %v", err) + } + + 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..18af05ab3b 100644 --- a/acceptance/openstack/imageservice/v2/imageservice.go +++ b/acceptance/openstack/imageservice/v2/imageservice.go @@ -31,6 +31,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() 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/testing/fixtures.go b/openstack/imageservice/v2/images/testing/fixtures.go index 33177c23f4..29757d203b 100644 --- a/openstack/imageservice/v2/images/testing/fixtures.go +++ b/openstack/imageservice/v2/images/testing/fixtures.go @@ -345,3 +345,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..487247b110 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" @@ -291,3 +292,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 +} From 55324a00c82e9e71110e0ef947a4654fc9f849c3 Mon Sep 17 00:00:00 2001 From: sreinkemeier Date: Tue, 6 Mar 2018 14:15:10 +0100 Subject: [PATCH 066/120] Added delete endpoint group operation --- .../networking/v2/extensions/vpnaas/group_test.go | 1 + .../networking/v2/extensions/vpnaas/vpnaas.go | 14 ++++++++++++++ .../v2/extensions/vpnaas/endpointgroups/doc.go | 8 ++++++++ .../extensions/vpnaas/endpointgroups/requests.go | 7 +++++++ .../v2/extensions/vpnaas/endpointgroups/results.go | 6 ++++++ .../vpnaas/endpointgroups/testing/requests_test.go | 14 ++++++++++++++ 6 files changed, 50 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go index ce80011be1..ce02a97493 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go @@ -20,6 +20,7 @@ func TestGroupCRUD(t *testing.T) { 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() diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index a770895457..0c6616538b 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -144,3 +144,17 @@ func CreateEndpointGroup(t *testing.T, client *gophercloud.ServiceClient) (*endp 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) +} diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go index bdd986bdc9..250dcd2a17 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go @@ -22,5 +22,13 @@ Example to retrieve an Endpoint Group 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) + } + */ package endpointgroups diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go index 5279f26aca..d75c252aee 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go @@ -62,3 +62,10 @@ 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 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go index 6804d53757..fa687f40c8 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go @@ -52,3 +52,9 @@ type CreateResult struct { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go index ba8d33c458..d21c7ca33d 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go @@ -122,3 +122,17 @@ func TestGet(t *testing.T) { } th.AssertDeepEquals(t, expected, *actual) } + +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) +} From c2736130e84eb237aaae7a461b8c0f8fe8a19051 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 7 Mar 2018 03:39:17 +0100 Subject: [PATCH 067/120] Vpnaas: Update IKE policy (#793) * Added unit test for ike policy update * Added update request * Added acceptance test for update operation, added documentation * Changed Name and Description to pointers in UpdateOpts so updating to empty string is possible * Added phase1negotiationmode and ikeversion as updateable fields --- .../v2/extensions/vpnaas/ikepolicy_test.go | 15 ++++ .../v2/extensions/vpnaas/ikepolicies/doc.go | 13 ++++ .../extensions/vpnaas/ikepolicies/requests.go | 41 ++++++++++ .../extensions/vpnaas/ikepolicies/results.go | 6 ++ .../ikepolicies/testing/requests_test.go | 77 ++++++++++++++++++- 5 files changed, 151 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go index 891eea81f5..2efa1e1b65 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go @@ -51,4 +51,19 @@ func TestIKEPolicyCRUD(t *testing.T) { } 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/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go index 23630a17f4..649027103d 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -30,6 +30,19 @@ 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 + + updateOpts := ikepolicies.UpdateOpts{ + Name: "updatedname", + Description: "updated policy", + Lifetime: &ikepolicies.LifetimeUpdateOpts{ + Value: 7000, + }, + } + updatedPolicy, err := ikepolicies.Update(client, "5c561d9d-eaea-45f6-ae3e-08d1a7080828", updateOpts).Extract() + if err != nil { + t.Fatalf("Unable to update IKE policy: %v", err) } diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go index f6dfcdd46f..6b084ff624 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go @@ -166,3 +166,44 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { 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 index d298bc53a5..b825f5754f 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/results.go @@ -117,3 +117,9 @@ type GetResult struct { 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 index 8f32432f94..9c3b08f120 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go @@ -161,7 +161,6 @@ func TestList(t *testing.T) { 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) @@ -227,3 +226,79 @@ func TestList(t *testing.T) { 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) +} From 24d38e255f73b6eac52312031a9450f57e0c6b60 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Thu, 8 Mar 2018 03:20:18 +0100 Subject: [PATCH 068/120] Vpnaas: Create IPSec site connection (#810) * Added file structure, methods and structs for IPSec site connection creation * Added unit test * Added acceptance test * Got acceptance test to work by adding link between subnet and router * Added documentation * removed print statement * renamed AuthenticationMode to AuthMode to match json string, deleted '(Deprecated)' in comment * Removed AuthMode and RouteMode from request * fixed typos --- .../extensions/vpnaas/siteconnection_test.go | 82 +++++++++++ .../networking/v2/extensions/vpnaas/vpnaas.go | 60 +++++++- .../extensions/vpnaas/siteconnections/doc.go | 27 ++++ .../vpnaas/siteconnections/requests.go | 129 ++++++++++++++++++ .../vpnaas/siteconnections/results.go | 107 +++++++++++++++ .../siteconnections/testing/requests_test.go | 124 +++++++++++++++++ .../extensions/vpnaas/siteconnections/urls.go | 16 +++ 7 files changed, 544 insertions(+), 1 deletion(-) create mode 100644 acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go create mode 100644 openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go create mode 100644 openstack/networking/v2/extensions/vpnaas/siteconnections/results.go create mode 100644 openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go create mode 100644 openstack/networking/v2/extensions/vpnaas/siteconnections/urls.go 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..264cb54db9 --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go @@ -0,0 +1,82 @@ +// +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" +) + +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) + } + + // Create Subnet + subnet, err := networks.CreateSubnet(t, client, network.ID) + if err != nil { + t.Fatalf("Unable to create subnet: %v", err) + } + + router, err := layer3.CreateExternalRouter(t, client) + if err != nil { + t.Fatalf("Unable to create router: %v", err) + } + + // 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) + } + + // 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) + } + + ikepolicy, err := CreateIKEPolicy(t, client) + if err != nil { + t.Fatalf("Unable to create IKE policy: %v", err) + } + + ipsecpolicy, err := CreateIPSecPolicy(t, client) + if err != nil { + t.Fatalf("Unable to create IPSec Policy: %v", err) + } + + peerEPGroup, err := CreateEndpointGroup(t, client) + if err != nil { + t.Fatalf("Unable to create Endpoint Group with CIDR endpoints: %v", err) + } + + localEPGroup, err := CreateEndpointGroupWithSubnet(t, client, subnet.ID) + if err != nil { + t.Fatalf("Unable to create Endpoint Group with subnet endpoints: %v", err) + } + + 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) + } + + tools.PrintResource(t, conn) + +} diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index 0c6616538b..e38625a6b3 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -9,6 +9,7 @@ import ( "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 @@ -53,7 +54,7 @@ func DeleteService(t *testing.T, client *gophercloud.ServiceClient, serviceID st func CreateIPSecPolicy(t *testing.T, client *gophercloud.ServiceClient) (*ipsecpolicies.Policy, error) { policyName := tools.RandomString("TESTACC-", 8) - t.Logf("Attempting to create policy %s", policyName) + t.Logf("Attempting to create IPSec policy %s", policyName) createOpts := ipsecpolicies.CreateOpts{ Name: policyName, @@ -157,4 +158,61 @@ func DeleteEndpointGroup(t *testing.T, client *gophercloud.ServiceClient, epGrou } 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 } 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..b90cca8e57 --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go @@ -0,0 +1,27 @@ +/* +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) + } +*/ +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..3b2e66c3fa --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go @@ -0,0 +1,129 @@ +package siteconnections + +import ( + "github.com/gophercloud/gophercloud" +) + +// 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"` +} + +// ToServiceCreateMap 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 +} 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..420ab8ca2f --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go @@ -0,0 +1,107 @@ +package siteconnections + +import ( + "github.com/gophercloud/gophercloud" +) + +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 +} + +// 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 +} 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..f99290526a --- /dev/null +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go @@ -0,0 +1,124 @@ +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" + 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) +} 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) +} From dd34b9036dd655fd414389304277d49b347e1cbd Mon Sep 17 00:00:00 2001 From: sreinkemeier Date: Wed, 7 Mar 2018 17:07:12 +0100 Subject: [PATCH 069/120] Added delete request, result, unit test and acceptance test --- .../v2/extensions/vpnaas/siteconnection_test.go | 15 +++++++++++++++ .../networking/v2/extensions/vpnaas/vpnaas.go | 15 +++++++++++++++ .../extensions/vpnaas/siteconnections/requests.go | 7 +++++++ .../extensions/vpnaas/siteconnections/results.go | 6 ++++++ .../siteconnections/testing/requests_test.go | 14 ++++++++++++++ 5 files changed, 57 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go index 264cb54db9..a631522aac 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go @@ -24,17 +24,20 @@ func TestConnectionCRUD(t *testing.T) { 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{ @@ -45,37 +48,49 @@ func TestConnectionCRUD(t *testing.T) { 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) tools.PrintResource(t, conn) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index e38625a6b3..f42aa50450 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -216,3 +216,18 @@ func CreateSiteConnection(t *testing.T, client *gophercloud.ServiceClient, ikepo 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/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go index 3b2e66c3fa..280f53ee8f 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go @@ -127,3 +127,10 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul _, 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go index 420ab8ca2f..1a7e0dd2fe 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go @@ -105,3 +105,9 @@ func (r commonResult) Extract() (*Connection, error) { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go index f99290526a..ab68720904 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go @@ -122,3 +122,17 @@ func TestCreate(t *testing.T) { } 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) +} From d2fe5bf4e65410df3bcca179f80aa84a3fc7fb98 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Fri, 9 Mar 2018 04:24:54 +0100 Subject: [PATCH 070/120] Vpnaas: List Endpoint groups (#813) * Added List function for Endpoint groups * Added documentation * Removed Endpoints parameter from list function * Added projectID to ListOpts --- .../v2/extensions/vpnaas/group_test.go | 21 ++++++ .../extensions/vpnaas/endpointgroups/doc.go | 11 ++++ .../vpnaas/endpointgroups/requests.go | 45 ++++++++++++- .../vpnaas/endpointgroups/results.go | 38 +++++++++++ .../endpointgroups/testing/requests_test.go | 64 +++++++++++++++++++ 5 files changed, 178 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go index ce02a97493..f793b3428c 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go @@ -10,6 +10,27 @@ import ( "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 { diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go index 250dcd2a17..f8dff3b66c 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go @@ -30,5 +30,16 @@ Example to Delete an Endpoint Group 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) + } */ package endpointgroups diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go index d75c252aee..cd9ca9ce8c 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go @@ -1,6 +1,9 @@ package endpointgroups -import "github.com/gophercloud/gophercloud" +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) type EndpointType string @@ -63,6 +66,46 @@ func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { 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) { diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go index fa687f40c8..c3d7dfcf5d 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go @@ -2,6 +2,7 @@ package endpointgroups import ( "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" ) // EndpointGroup is an endpoint group. @@ -41,6 +42,43 @@ func (r commonResult) Extract() (*EndpointGroup, error) { 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 { diff --git a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go index d21c7ca33d..1a3814d47b 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go @@ -7,6 +7,7 @@ import ( 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" ) @@ -123,6 +124,69 @@ func TestGet(t *testing.T) { 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() From 4a1a047deff21d19687a0e1a21786a93f4d25707 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Fri, 9 Mar 2018 04:09:15 +0000 Subject: [PATCH 071/120] Compute v2: Fix EOF errors in compute secgroups --- acceptance/openstack/compute/v2/compute.go | 10 +++++++- .../openstack/compute/v2/secgroup_test.go | 24 ++++++++++++------- .../v2/extensions/secgroups/requests.go | 4 ++-- .../extensions/secgroups/testing/fixtures.go | 2 -- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go index 5cb86185d3..77b3d7d2f8 100644 --- a/acceptance/openstack/compute/v2/compute.go +++ b/acceptance/openstack/compute/v2/compute.go @@ -681,7 +681,15 @@ func DeleteServer(t *testing.T, client *gophercloud.ServiceClient, server *serve t.Fatalf("Unable to delete server %s: %s", server.ID, err) } - t.Logf("Deleted server: %s", server.ID) + 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) + } + + t.Fatalf("Could not delete server: %s", server.ID) } // DeleteServerGroup will delete a server group. A fatal error will occur if diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go index c0d023037d..d77c4ace86 100644 --- a/acceptance/openstack/compute/v2/secgroup_test.go +++ b/acceptance/openstack/compute/v2/secgroup_test.go @@ -8,6 +8,7 @@ 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" ) func TestSecGroupsList(t *testing.T) { @@ -105,12 +106,6 @@ func TestSecGroupsAddGroupToServer(t *testing.T) { t.Fatalf("Unable to create a compute client: %v", err) } - server, err := CreateServer(t, client) - if err != nil { - t.Fatalf("Unable to create server: %v", err) - } - defer DeleteServer(t, client, server) - securityGroup, err := CreateSecurityGroup(t, client) if err != nil { t.Fatalf("Unable to create security group: %v", err) @@ -123,15 +118,28 @@ func TestSecGroupsAddGroupToServer(t *testing.T) { } defer DeleteSecurityGroupRule(t, client, rule) + server, err := CreateServer(t, client) + if err != nil { + t.Fatalf("Unable to create server: %v", err) + } + defer DeleteServer(t, client, server) + 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" { + if err != nil { t.Fatalf("Unable to add group %s to server %s: %s", securityGroup.ID, server.ID, err) } + server, err = servers.Get(client, server.ID).Extract() + if err != nil { + t.Fatalf("Unable to get server %s: %s", server.ID, err) + } + + tools.PrintResource(t, server) + 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" { + if err != nil { t.Fatalf("Unable to remove group %s from server %s: %s", securityGroup.ID, server.ID, err) } } 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, `{}`) }) } From fd83de6f9a5581d9bfb2fbc5a96287f8cec6ad71 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Mon, 12 Mar 2018 03:02:53 +0000 Subject: [PATCH 072/120] Networking v2: Fix Extra DHCP Options Updates This commit fixes updating of extra DHCP options on a Networking port. The fix entails creating two new structs: one for create and one for update. The update struct is able to take a string pointer as a dhcp value so a nil/null string can be provided. This nil/null value will cause a dhcp option to be remove if requested. --- .../openstack/networking/v2/networking.go | 3 +- .../openstack/networking/v2/ports_test.go | 12 +++++- .../v2/extensions/extradhcpopts/doc.go | 29 +++++++------ .../v2/extensions/extradhcpopts/requests.go | 39 +++++++++++++++--- .../v2/extensions/extradhcpopts/results.go | 19 ++------- .../networking/v2/ports/testing/fixtures.go | 4 ++ .../v2/ports/testing/requests_test.go | 41 ++++++++++--------- 7 files changed, 91 insertions(+), 56 deletions(-) diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go index 58a59f48ae..b5dcb6d2c7 100644 --- a/acceptance/openstack/networking/v2/networking.go +++ b/acceptance/openstack/networking/v2/networking.go @@ -357,9 +357,10 @@ func CreatePortWithExtraDHCPOpts(t *testing.T, client *gophercloud.ServiceClient AdminStateUp: gophercloud.Enabled, FixedIPs: []ports.IP{ports.IP{SubnetID: subnetID}}, } + createOpts := extradhcpopts.CreateOptsExt{ CreateOptsBuilder: portCreateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ + ExtraDHCPOpts: []extradhcpopts.CreateExtraDHCPOpt{ { OptName: "test_option_1", OptValue: "test_value_1", diff --git a/acceptance/openstack/networking/v2/ports_test.go b/acceptance/openstack/networking/v2/ports_test.go index d47bc3a196..96faa09a2c 100644 --- a/acceptance/openstack/networking/v2/ports_test.go +++ b/acceptance/openstack/networking/v2/ports_test.go @@ -424,12 +424,20 @@ func TestPortsWithExtraDHCPOptsCRUD(t *testing.T) { portUpdateOpts := ports.UpdateOpts{ Name: newPortName, } + + existingOpt := port.ExtraDHCPOpts[0] + newOptValue := "test_value_2" + updateOpts := extradhcpopts.UpdateOptsExt{ UpdateOptsBuilder: portUpdateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ + ExtraDHCPOpts: []extradhcpopts.UpdateExtraDHCPOpt{ + { + OptName: existingOpt.OptName, + OptValue: nil, + }, { OptName: "test_option_2", - OptValue: "test_value_2", + OptValue: &newOptValue, }, }, } diff --git a/openstack/networking/v2/extensions/extradhcpopts/doc.go b/openstack/networking/v2/extensions/extradhcpopts/doc.go index 487fdd6cbe..ec5d6181d6 100644 --- a/openstack/networking/v2/extensions/extradhcpopts/doc.go +++ b/openstack/networking/v2/extensions/extradhcpopts/doc.go @@ -16,6 +16,11 @@ Example to Get a Port with Extra DHCP Options 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", @@ -25,19 +30,16 @@ Example to Create a Port with Extra DHCP Options {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, }, } + createOpts := extradhcpopts.CreateOptsExt{ CreateOptsBuilder: portCreateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ + ExtraDHCPOpts: []extradhcpopts.CreateExtraDHCPOpt{ { OptName: "optionA", OptValue: "valueA", }, }, } - var s struct { - ports.Port - extradhcpopts.ExtraDHCPOptsExt - } err := ports.Create(networkClient, createOpts).ExtractInto(&s) if err != nil { @@ -46,27 +48,30 @@ Example to Create a Port with Extra DHCP Options 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.ExtraDHCPOpt{ + ExtraDHCPOpts: []extradhcpopts.UpdateExtraDHCPOpt{ { OptName: "optionB", - OptValue: "valueB", + OptValue: &value, }, }, } - var s struct { - ports.Port - extradhcpopts.ExtraDHCPOptsExt - } - portID := "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2" + portID := "46d4bfb9-b26e-41f3-bd2e-e6dcc1ccedb2" err := ports.Update(networkClient, portID, updateOpts).ExtractInto(&s) if err != nil { panic(err) diff --git a/openstack/networking/v2/extensions/extradhcpopts/requests.go b/openstack/networking/v2/extensions/extradhcpopts/requests.go index 9893f84d84..f3eb9bc450 100644 --- a/openstack/networking/v2/extensions/extradhcpopts/requests.go +++ b/openstack/networking/v2/extensions/extradhcpopts/requests.go @@ -1,6 +1,7 @@ package extradhcpopts import ( + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" ) @@ -11,7 +12,20 @@ type CreateOptsExt struct { ports.CreateOptsBuilder // ExtraDHCPOpts field is a set of DHCP options for a single port. - ExtraDHCPOpts []ExtraDHCPOpt `json:"extra_dhcp_opts,omitempty"` + 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. @@ -27,11 +41,11 @@ func (opts CreateOptsExt) ToPortCreateMap() (map[string]interface{}, error) { if opts.ExtraDHCPOpts != nil { extraDHCPOpts := make([]map[string]interface{}, len(opts.ExtraDHCPOpts)) for i, opt := range opts.ExtraDHCPOpts { - extraDHCPOptMap, err := opt.ToMap() + b, err := gophercloud.BuildRequestBody(opt, "") if err != nil { return nil, err } - extraDHCPOpts[i] = extraDHCPOptMap + extraDHCPOpts[i] = b } port["extra_dhcp_opts"] = extraDHCPOpts } @@ -46,7 +60,20 @@ type UpdateOptsExt struct { ports.UpdateOptsBuilder // ExtraDHCPOpts field is a set of DHCP options for a single port. - ExtraDHCPOpts []ExtraDHCPOpt `json:"extra_dhcp_opts,omitempty"` + 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. @@ -62,11 +89,11 @@ func (opts UpdateOptsExt) ToPortUpdateMap() (map[string]interface{}, error) { if opts.ExtraDHCPOpts != nil { extraDHCPOpts := make([]map[string]interface{}, len(opts.ExtraDHCPOpts)) for i, opt := range opts.ExtraDHCPOpts { - extraDHCPOptMap, err := opt.ToMap() + b, err := gophercloud.BuildRequestBody(opt, "") if err != nil { return nil, err } - extraDHCPOpts[i] = extraDHCPOptMap + extraDHCPOpts[i] = b } port["extra_dhcp_opts"] = extraDHCPOpts } diff --git a/openstack/networking/v2/extensions/extradhcpopts/results.go b/openstack/networking/v2/extensions/extradhcpopts/results.go index 042d425a58..8e3132ea4a 100644 --- a/openstack/networking/v2/extensions/extradhcpopts/results.go +++ b/openstack/networking/v2/extensions/extradhcpopts/results.go @@ -1,7 +1,5 @@ package extradhcpopts -import "github.com/gophercloud/gophercloud" - // ExtraDHCPOptsExt is a struct that contains different DHCP options for a // single port. type ExtraDHCPOptsExt struct { @@ -10,24 +8,13 @@ type ExtraDHCPOptsExt struct { // ExtraDHCPOpt represents a single set of extra DHCP options for a single port. type ExtraDHCPOpt struct { - // Name is the name of a single DHCP option. + // OptName is the name of a single DHCP option. OptName string `json:"opt_name"` - // Value is the value of a single DHCP option. + // 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,omitempty"` -} - -// ToMap is a helper function to convert an individual ExtraDHCPOpt structure -// into a sub-map. -func (opts ExtraDHCPOpt) ToMap() (map[string]interface{}, error) { - b, err := gophercloud.BuildRequestBody(opts, "") - if err != nil { - return nil, err - } - - return b, nil + IPVersion int `json:"ip_version"` } diff --git a/openstack/networking/v2/ports/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go index 870572cb64..94b5a64a21 100644 --- a/openstack/networking/v2/ports/testing/fixtures.go +++ b/openstack/networking/v2/ports/testing/fixtures.go @@ -625,6 +625,10 @@ const UpdateWithExtraDHCPOptsRequest = ` } ], "extra_dhcp_opts": [ + { + "opt_name": "option1", + "opt_value": null + }, { "opt_name": "option2", "opt_value": "value2" diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go index 7df21c7a35..7b04bb1e5e 100644 --- a/openstack/networking/v2/ports/testing/requests_test.go +++ b/openstack/networking/v2/ports/testing/requests_test.go @@ -596,12 +596,6 @@ func TestGetWithExtraDHCPOpts(t *testing.T) { th.AssertEquals(t, s.Status, "ACTIVE") th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") - th.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ - {OptName: "option1", OptValue: "value1", IPVersion: 4}, - {OptName: "option2", OptValue: "value2", IPVersion: 4}, - }, - }) th.AssertEquals(t, s.AdminStateUp, true) th.AssertEquals(t, s.Name, "port-with-extra-dhcp-opts") th.AssertEquals(t, s.DeviceOwner, "") @@ -611,6 +605,13 @@ func TestGetWithExtraDHCPOpts(t *testing.T) { }) 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) { @@ -642,7 +643,7 @@ func TestCreateWithExtraDHCPOpts(t *testing.T) { createOpts := extradhcpopts.CreateOptsExt{ CreateOptsBuilder: portCreateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ + ExtraDHCPOpts: []extradhcpopts.CreateExtraDHCPOpt{ { OptName: "option1", OptValue: "value1", @@ -661,11 +662,6 @@ func TestCreateWithExtraDHCPOpts(t *testing.T) { th.AssertEquals(t, s.Status, "DOWN") th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") - th.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ - {OptName: "option1", OptValue: "value1", IPVersion: 4}, - }, - }) th.AssertEquals(t, s.AdminStateUp, true) th.AssertEquals(t, s.Name, "port-with-extra-dhcp-opts") th.AssertEquals(t, s.DeviceOwner, "") @@ -675,6 +671,10 @@ func TestCreateWithExtraDHCPOpts(t *testing.T) { }) 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) { @@ -701,12 +701,16 @@ func TestUpdateWithExtraDHCPOpts(t *testing.T) { }, } + edoValue2 := "value2" updateOpts := extradhcpopts.UpdateOptsExt{ UpdateOptsBuilder: portUpdateOpts, - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ + ExtraDHCPOpts: []extradhcpopts.UpdateExtraDHCPOpt{ + { + OptName: "option1", + }, { OptName: "option2", - OptValue: "value2", + OptValue: &edoValue2, }, }, } @@ -722,11 +726,6 @@ func TestUpdateWithExtraDHCPOpts(t *testing.T) { th.AssertEquals(t, s.Status, "DOWN") th.AssertEquals(t, s.NetworkID, "a87cc70a-3e15-4acf-8205-9b711a3531b7") th.AssertEquals(t, s.TenantID, "d6700c0c9ffa4f1cb322cd4a1f3906fa") - th.AssertDeepEquals(t, s.ExtraDHCPOptsExt, extradhcpopts.ExtraDHCPOptsExt{ - ExtraDHCPOpts: []extradhcpopts.ExtraDHCPOpt{ - {OptName: "option2", OptValue: "value2", IPVersion: 4}, - }, - }) th.AssertEquals(t, s.AdminStateUp, true) th.AssertEquals(t, s.Name, "updated-port-with-dhcp-opts") th.AssertEquals(t, s.DeviceOwner, "") @@ -736,4 +735,8 @@ func TestUpdateWithExtraDHCPOpts(t *testing.T) { }) 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) } From 81bac724d837c187b5f9d364fc70fa92f20f2803 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Mon, 12 Mar 2018 03:35:00 +0000 Subject: [PATCH 073/120] Acc: Add Debug Logging This commit adds the ability to view the OpenStack API JSON requests and responses during acceptance test execution by setting the OS_DEBUG environment variable to 1/true/yes. --- acceptance/clients/clients.go | 49 ++++++++++ acceptance/clients/http.go | 170 ++++++++++++++++++++++++++++++++++ 2 files changed, 219 insertions(+) create mode 100644 acceptance/clients/http.go diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index d5c9cccdf1..3a7717d6a9 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -5,6 +5,7 @@ package clients import ( "fmt" + "net/http" "os" "strings" @@ -132,6 +133,8 @@ func NewBlockStorageV1Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewBlockStorageV1(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -151,6 +154,8 @@ func NewBlockStorageV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewBlockStorageV2(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -170,6 +175,8 @@ func NewBlockStorageV3Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewBlockStorageV3(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -187,6 +194,8 @@ func NewBlockStorageV2NoAuthClient() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return noauth.NewBlockStorageNoAuth(client, noauth.EndpointOpts{ CinderEndpoint: os.Getenv("CINDER_ENDPOINT"), }) @@ -204,6 +213,8 @@ func NewBlockStorageV3NoAuthClient() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return noauth.NewBlockStorageNoAuth(client, noauth.EndpointOpts{ CinderEndpoint: os.Getenv("CINDER_ENDPOINT"), }) @@ -223,6 +234,8 @@ func NewComputeV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewComputeV2(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -242,6 +255,8 @@ func NewDBV1Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewDBV1(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -261,6 +276,8 @@ func NewDNSV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewDNSV2(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -280,6 +297,8 @@ func NewIdentityV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -299,6 +318,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, @@ -319,6 +340,8 @@ func NewIdentityV2UnauthenticatedClient() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewIdentityV2(client, gophercloud.EndpointOpts{}) } @@ -336,6 +359,8 @@ func NewIdentityV3Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -355,6 +380,8 @@ func NewIdentityV3UnauthenticatedClient() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewIdentityV3(client, gophercloud.EndpointOpts{}) } @@ -372,6 +399,8 @@ func NewImageServiceV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewImageServiceV2(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -391,6 +420,8 @@ func NewNetworkV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewNetworkV2(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -410,6 +441,8 @@ func NewObjectStorageV1Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -429,7 +462,23 @@ func NewSharedFileSystemV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewSharedFileSystemV2(client, 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 +} 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") +} From bc0882a09253c768051d9201257640794415c5a9 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Wed, 14 Mar 2018 01:23:02 +0100 Subject: [PATCH 074/120] Vpnaas: Show IPSec site connection details (#821) * Added unit and acceptance tests * Added request and result for Get operation * Added documentation for Get operation * Added various missing bits of documentation --- .../extensions/vpnaas/siteconnection_test.go | 7 ++ .../v2/extensions/vpnaas/ikepolicies/doc.go | 2 +- .../v2/extensions/vpnaas/ipsecpolicies/doc.go | 18 +++++ .../v2/extensions/vpnaas/services/doc.go | 7 ++ .../extensions/vpnaas/siteconnections/doc.go | 15 ++++ .../vpnaas/siteconnections/requests.go | 6 ++ .../vpnaas/siteconnections/results.go | 6 ++ .../siteconnections/testing/requests_test.go | 77 +++++++++++++++++++ 8 files changed, 137 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go index a631522aac..c063dc24f5 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go @@ -11,6 +11,7 @@ import ( "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 TestConnectionCRUD(t *testing.T) { @@ -92,6 +93,12 @@ func TestConnectionCRUD(t *testing.T) { } 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/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go index 649027103d..2285aabd3b 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -42,7 +42,7 @@ Example to Update an IKE policy } updatedPolicy, err := ikepolicies.Update(client, "5c561d9d-eaea-45f6-ae3e-08d1a7080828", updateOpts).Extract() if err != nil { - t.Fatalf("Unable to update IKE policy: %v", err) + panic(err) } diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go index d2d227f466..13a1636807 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go @@ -20,6 +20,24 @@ Example to Delete a Policy 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 + + updateOpts := ipsecpolicies.UpdateOpts{ + Name: "updatedname", + Description: "updated policy", + } + 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() diff --git a/openstack/networking/v2/extensions/vpnaas/services/doc.go b/openstack/networking/v2/extensions/vpnaas/services/doc.go index bf4302a5aa..cb1ef8cabf 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/services/doc.go @@ -44,5 +44,12 @@ Example to Delete a Service 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/siteconnections/doc.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go index b90cca8e57..9311ec61a2 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go @@ -23,5 +23,20 @@ createOpts := siteconnections.CreateOpts{ 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, serviceID).ExtractErr() + 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 index 280f53ee8f..6d79dc1395 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go @@ -134,3 +134,9 @@ 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go index 1a7e0dd2fe..351aa0d29a 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go @@ -111,3 +111,9 @@ type CreateResult struct { 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 +} diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go index ab68720904..c552091cce 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go @@ -136,3 +136,80 @@ func TestDelete(t *testing.T) { 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) +} From ab6985d3fcf97fff7583a8813f7d5d9c72b02219 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 14 Mar 2018 16:07:16 -0600 Subject: [PATCH 075/120] Docs: Update CONTRIBUTING Guide (#826) * Docs: Update CONTRIBUTING Guide This commit updates the CONTRIBUTING guide so it suggests creating an issue before submitting a PR. * Amending CONTRIBUTING --- .github/CONTRIBUTING.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3092511a3b..d6c894637e 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 From de4b788e8f384b917d879d1d788765f1c752d3e9 Mon Sep 17 00:00:00 2001 From: Jon Perritt Date: Wed, 14 Mar 2018 17:49:47 -0400 Subject: [PATCH 076/120] add cascade option to loadbalancers.Delete --- acceptance/clients/clients.go | 19 +++ .../extensions/lbaas_v2/loadbalancers_test.go | 149 ++++++++++++++++++ .../lbaas_v2/loadbalancers/requests.go | 18 ++- .../lbaas_v2/loadbalancers/results.go | 4 +- .../loadbalancers/testing/requests_test.go | 17 ++ 5 files changed, 204 insertions(+), 3 deletions(-) diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index 3a7717d6a9..6c548bfb59 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -482,3 +482,22 @@ func configureDebug(client *gophercloud.ProviderClient) *gophercloud.ProviderCli return client } + +// 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 + } + + return openstack.NewLoadBalancerV2(client, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +} diff --git a/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go index 650eb2cc49..26064f0c28 100644 --- a/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go +++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go @@ -176,3 +176,152 @@ func TestLoadbalancersCRUD(t *testing.T) { tools.PrintResource(t, newMonitor) } + +func TestOctaviaLoadbalancersCRUD(t *testing.T) { + netClient, err := clients.NewNetworkV2Client() + if err != nil { + t.Fatalf("Unable to create a network client: %v", err) + } + + lbClient, err := clients.NewLoadBalancerV2Client() + if err != nil { + t.Fatalf("Unable to create a network 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 func() { + t.Logf("Running cascading delete on Octavia LB...") + err := loadbalancers.CascadingDelete(lbClient, lb.ID).ExtractErr() + if err != nil { + t.Fatalf("Error running cascading delete: %v", err) + } + }() + + 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/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go b/openstack/networking/v2/extensions/lbaas_v2/loadbalancers/requests.go index 49ec9ecac3..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" ) @@ -36,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 @@ -175,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) +} From 08084679db8635a7b53f445cdfd1414024b4938d Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 14 Mar 2018 20:44:07 -0600 Subject: [PATCH 077/120] Networking v2: Port Security Update (#808) * Networking v2: Port Security Update * Networking v2: Port Security Create and Update Docs --- .../openstack/networking/v2/networks_test.go | 14 +++ .../openstack/networking/v2/ports_test.go | 14 +++ .../v2/extensions/portsecurity/doc.go | 103 +++++++++++++++++- .../v2/extensions/portsecurity/requests.go | 49 +++++++++ .../v2/networks/testing/fixtures.go | 24 ++++ .../v2/networks/testing/requests_test.go | 39 +++++++ .../networking/v2/ports/testing/fixtures.go | 40 +++++++ .../v2/ports/testing/requests_test.go | 37 +++++++ 8 files changed, 317 insertions(+), 3 deletions(-) diff --git a/acceptance/openstack/networking/v2/networks_test.go b/acceptance/openstack/networking/v2/networks_test.go index 2ff00172bc..ab4c2b1ce9 100644 --- a/acceptance/openstack/networking/v2/networks_test.go +++ b/acceptance/openstack/networking/v2/networks_test.go @@ -96,4 +96,18 @@ func TestNetworksPortSecurityCRUD(t *testing.T) { } 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 d47bc3a196..1d4a9d757b 100644 --- a/acceptance/openstack/networking/v2/ports_test.go +++ b/acceptance/openstack/networking/v2/ports_test.go @@ -388,6 +388,20 @@ func TestPortsPortSecurityCRUD(t *testing.T) { } 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) { 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 index 781353ee37..c80f47cf61 100644 --- a/openstack/networking/v2/extensions/portsecurity/requests.go +++ b/openstack/networking/v2/extensions/portsecurity/requests.go @@ -29,6 +29,30 @@ func (opts PortCreateOptsExt) ToPortCreateMap() (map[string]interface{}, error) 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 { @@ -53,3 +77,28 @@ func (opts NetworkCreateOptsExt) ToNetworkCreateMap() (map[string]interface{}, e 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/networks/testing/fixtures.go b/openstack/networking/v2/networks/testing/fixtures.go index e4f6b6bd02..9632d448a5 100644 --- a/openstack/networking/v2/networks/testing/fixtures.go +++ b/openstack/networking/v2/networks/testing/fixtures.go @@ -148,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 1bfafaae8d..231d7f087c 100644 --- a/openstack/networking/v2/networks/testing/requests_test.go +++ b/openstack/networking/v2/networks/testing/requests_test.go @@ -254,3 +254,42 @@ func TestCreatePortSecurity(t *testing.T) { 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/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go index 870572cb64..28c37f7237 100644 --- a/openstack/networking/v2/ports/testing/fixtures.go +++ b/openstack/networking/v2/ports/testing/fixtures.go @@ -381,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": { diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go index 7df21c7a35..7abeb5f965 100644 --- a/openstack/networking/v2/ports/testing/requests_test.go +++ b/openstack/networking/v2/ports/testing/requests_test.go @@ -441,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() From 3aef8e417612ddbdd8f7a76004af4307296cd1b6 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Tue, 20 Mar 2018 18:01:40 -0600 Subject: [PATCH 078/120] Acc Compute v2: Updating the Compute v2 Acceptance Tests (#820) * Acc: Adding condition to require admin user * Acc Compute v2: Updating aggregates tests * Acc Compute v2: Updating attachinterfaces tests * Acc Compute v2: Updating availabilityzone tests * Acc Compute v2: Removing short test check in compute.go * Acc Compute v2: Updating bootfromvolume tests * Acc Compute v2: Updating defsecrules tests * Acc Compute v2: Updating extensions tests * Acc Compute v2: Updating flavors tests * Acc Compute v2: Updating floatingips tests * Acc Compute v2: Updating hypervisors tests * Acc Compute v2: Updating images tests * Acc Compute v2: Updating keypairs tests * Acc Compute v2: Updating limits tests * Acc Compute v2: Updating migration tests * Acc Compute v2: Updating network tests * Acc Compute v2: Updating quotaset tests * Acc Compute v2: Simplifying assertions * Acc Compute v2: Updating servers tests * Acc Compute v2: Updating services tests * Acc Compute v2: Updating tenantnetworks tests * Acc Compute v2: Updating usage tests * Acc Compute v2: Updating volumeattach tests * Acc Compute v2: Adding RequireLong convenience function * Acc Compute v2: Cleaning up convenience functions * Acc Compute v2: Suppressing error message verbosity * Acc Compute v2: Updating secgroups tests --- acceptance/clients/clients.go | 9 - acceptance/clients/conditions.go | 44 +++ .../openstack/blockstorage/v2/blockstorage.go | 15 +- .../openstack/compute/v2/aggregates_test.go | 152 +++----- .../compute/v2/attachinterfaces_test.go | 59 ++- .../compute/v2/availabilityzones_test.go | 41 +- .../compute/v2/bootfromvolume_test.go | 169 ++++---- acceptance/openstack/compute/v2/compute.go | 278 +++++++++----- .../openstack/compute/v2/defsecrules_test.go | 42 +- .../openstack/compute/v2/extension_test.go | 32 +- .../openstack/compute/v2/flavors_test.go | 188 ++++----- .../openstack/compute/v2/floatingip_test.go | 105 ++--- .../openstack/compute/v2/hypervisors_test.go | 73 ++-- .../openstack/compute/v2/images_test.go | 37 +- .../openstack/compute/v2/keypairs_test.go | 83 ++-- .../openstack/compute/v2/limits_test.go | 30 +- .../openstack/compute/v2/migrate_test.go | 39 +- .../openstack/compute/v2/network_test.go | 41 +- .../openstack/compute/v2/quotaset_test.go | 99 ++--- .../openstack/compute/v2/secgroup_test.go | 143 ++++--- .../openstack/compute/v2/servergroup_test.go | 80 ++-- .../openstack/compute/v2/servers_test.go | 363 +++++++----------- .../openstack/compute/v2/services_test.go | 22 +- .../compute/v2/tenantnetworks_test.go | 39 +- acceptance/openstack/compute/v2/usage_test.go | 31 +- .../openstack/compute/v2/volumeattach_test.go | 64 +-- 26 files changed, 1064 insertions(+), 1214 deletions(-) create mode 100644 acceptance/clients/conditions.go diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index 6c548bfb59..eee80005ef 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -43,9 +43,6 @@ type AcceptanceTestChoices struct { // DBDatastoreTypeID is the datastore type version for DB tests. DBDatastoreVersion string - - // LiveMigrate indicates ability to run multi-node migration tests - LiveMigrate bool } // AcceptanceTestChoicesFromEnv populates a ComputeChoices struct from environment variables. @@ -61,11 +58,6 @@ func AcceptanceTestChoicesFromEnv() (*AcceptanceTestChoices, error) { dbDatastoreType := os.Getenv("OS_DB_DATASTORE_TYPE") dbDatastoreVersion := os.Getenv("OS_DB_DATASTORE_VERSION") - var liveMigrate bool - if v := os.Getenv("OS_LIVE_MIGRATE"); v != "" { - liveMigrate = true - } - missing := make([]string, 0, 3) if imageID == "" { missing = append(missing, "OS_IMAGE_ID") @@ -115,7 +107,6 @@ func AcceptanceTestChoicesFromEnv() (*AcceptanceTestChoices, error) { ShareNetworkID: shareNetworkID, DBDatastoreType: dbDatastoreType, DBDatastoreVersion: dbDatastoreVersion, - LiveMigrate: liveMigrate, }, nil } diff --git a/acceptance/clients/conditions.go b/acceptance/clients/conditions.go new file mode 100644 index 0000000000..9c62c29c11 --- /dev/null +++ b/acceptance/clients/conditions.go @@ -0,0 +1,44 @@ +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") + } +} + +// 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") + } +} + +// 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/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/compute/v2/aggregates_test.go b/acceptance/openstack/compute/v2/aggregates_test.go index 7209831c3d..352adb38e3 100644 --- a/acceptance/openstack/compute/v2/aggregates_test.go +++ b/acceptance/openstack/compute/v2/aggregates_test.go @@ -11,174 +11,132 @@ import ( "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 _, h := range allAggregates { - tools.PrintResource(t, h) + for _, v := range allAggregates { + tools.PrintResource(t, v) } } -func TestAggregatesCreateDelete(t *testing.T) { - client, err := clients.NewComputeV2Client() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } +func TestAggregatesCRUD(t *testing.T) { + clients.RequireAdmin(t) - createdAggregate, err := CreateAggregate(t, client) - if err != nil { - t.Fatalf("Unable to create an aggregate: %v", err) - } - defer DeleteAggregate(t, client, createdAggregate) - - tools.PrintResource(t, createdAggregate) -} - -func TestAggregatesGet(t *testing.T) { client, err := clients.NewComputeV2Client() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) - createdAggregate, err := CreateAggregate(t, client) - if err != nil { - t.Fatalf("Unable to create an aggregate: %v", err) - } - defer DeleteAggregate(t, client, createdAggregate) + aggregate, err := CreateAggregate(t, client) + th.AssertNoErr(t, err) - aggregate, err := aggregates.Get(client, createdAggregate.ID).Extract() - if err != nil { - t.Fatalf("Unable to get an aggregate: %v", err) - } + defer DeleteAggregate(t, client, aggregate) tools.PrintResource(t, aggregate) -} - -func TestAggregatesUpdate(t *testing.T) { - client, err := clients.NewComputeV2Client() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } - - createdAggregate, err := CreateAggregate(t, client) - if err != nil { - t.Fatalf("Unable to create an aggregate: %v", err) - } - defer DeleteAggregate(t, client, createdAggregate) updateOpts := aggregates.UpdateOpts{ Name: "new_aggregate_name", AvailabilityZone: "new_azone", } - updatedAggregate, err := aggregates.Update(client, createdAggregate.ID, updateOpts).Extract() - if err != nil { - t.Fatalf("Unable to update an aggregate: %v", err) - } + updatedAggregate, err := aggregates.Update(client, aggregate.ID, updateOpts).Extract() + th.AssertNoErr(t, err) - tools.PrintResource(t, updatedAggregate) + 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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) hostToAdd, err := getHypervisor(t, client) - if err != nil { - t.Fatal(err) - } + th.AssertNoErr(t, err) - createdAggregate, err := CreateAggregate(t, client) - if err != nil { - t.Fatalf("Unable to create an aggregate: %v", err) - } - defer DeleteAggregate(t, client, createdAggregate) + 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, createdAggregate.ID, addHostOpts).Extract() - if err != nil { - t.Fatalf("Unable to add host to aggregate: %v", err) - } + 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, createdAggregate.ID, removeHostOpts).Extract() - if err != nil { - t.Fatalf("Unable to remove host from aggregate: %v", err) - } + 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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) - createdAggregate, err := CreateAggregate(t, client) - if err != nil { - t.Fatalf("Unable to create an aggregate: %v", err) - } - defer DeleteAggregate(t, client, createdAggregate) + 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, createdAggregate.ID, opts).Extract() - if err != nil { - t.Fatalf("Unable to set metadata to aggregate: %v", err) - } + 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, createdAggregate.ID, optsToRemove).Extract() - if err != nil { - t.Fatalf("Unable to set metadata to aggregate: %v", err) - } + 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() - if err != nil { - t.Fatalf("Unable to list hypervisors: %v", err) - } + th.AssertNoErr(t, err) allHypervisors, err := hypervisors.ExtractHypervisors(allPages) - if err != nil { - t.Fatal("Unable to extract hypervisors") - } + th.AssertNoErr(t, err) for _, h := range allHypervisors { return &h, nil 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 index 3e82128422..4d030c2968 100644 --- a/acceptance/openstack/compute/v2/availabilityzones_test.go +++ b/acceptance/openstack/compute/v2/availabilityzones_test.go @@ -8,46 +8,51 @@ import ( "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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) allPages, err := availabilityzones.List(client).AllPages() - if err != nil { - t.Fatalf("Unable to list availability zones info: %v", err) - } + th.AssertNoErr(t, err) availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) - if err != nil { - t.Fatalf("Unable to extract availability zones info: %v", err) - } + 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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) allPages, err := availabilityzones.ListDetail(client).AllPages() - if err != nil { - t.Fatalf("Unable to list availability zones detailed info: %v", err) - } + th.AssertNoErr(t, err) availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) - if err != nil { - t.Fatalf("Unable to extract availability zones detailed info: %v", err) - } + 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 77b3d7d2f8..cdcbf0bb9e 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,6 +26,7 @@ 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" @@ -64,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() @@ -113,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 } @@ -160,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 } @@ -216,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 } @@ -225,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() @@ -268,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 } @@ -293,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. @@ -340,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) @@ -353,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) @@ -361,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, @@ -384,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. @@ -396,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) @@ -409,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) @@ -417,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, @@ -432,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 @@ -445,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) @@ -473,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) @@ -497,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) @@ -521,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) @@ -536,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 @@ -571,25 +676,6 @@ func CreateVolumeAttachment(t *testing.T, client *gophercloud.ServiceClient, blo return volumeAttachment, 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) - availableZone := tools.RandomString("zone_", 5) - t.Logf("Attempting to create aggregate %s", aggregateName) - - createOpts := aggregates.CreateOpts{Name: aggregateName, AvailabilityZone: availableZone} - - aggregate, err := aggregates.Create(client, createOpts).Extract() - if err != nil { - return nil, err - } - - t.Logf("Successfully created aggregate %d", aggregate.ID) - - return aggregate, 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. @@ -651,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. @@ -720,6 +806,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. @@ -802,6 +902,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 b7768b380a..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,9 +131,11 @@ 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) @@ -158,25 +146,19 @@ func TestFlavorAccessCRUD(t *testing.T) { } accessList, err = flavors.RemoveAccess(client, flavor.ID, removeAccessOpts).Extract() - if err != nil { - t.Fatalf("Unable to remove access to flavor: %v", err) - } + th.AssertNoErr(t, err) - for _, access := range accessList { - tools.PrintResource(t, access) - } + 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{ @@ -184,37 +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) + 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() - if err != nil { - t.Fatalf("Unable to delete ExtraSpec: %v\n", err) - } + th.AssertNoErr(t, err) updateOpts := flavors.ExtraSpecsOpts{ "hw:cpu_thread_policy": "CPU-THREAD-POLICY-BETTER", } updatedExtraSpec, err := flavors.UpdateExtraSpec(client, flavor.ID, updateOpts).Extract() - if err != nil { - t.Fatalf("Unable to update flavor extra_specs: %v", err) - } + th.AssertNoErr(t, err) + tools.PrintResource(t, updatedExtraSpec) allExtraSpecs, err := flavors.ListExtraSpecs(client, flavor.ID).Extract() - if err != nil { - t.Fatalf("Unable to get flavor extra_specs: %v", err) - } + 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 15aff3de74..29d49a277e 100644 --- a/acceptance/openstack/compute/v2/hypervisors_test.go +++ b/acceptance/openstack/compute/v2/hypervisors_test.go @@ -10,23 +10,20 @@ import ( "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) @@ -34,70 +31,64 @@ func TestHypervisorsList(t *testing.T) { } func TestHypervisorsGet(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) hypervisorID, err := getHypervisorID(t, client) - if err != nil { - t.Fatal(err) - } + th.AssertNoErr(t, err) hypervisor, err := hypervisors.Get(client, hypervisorID).Extract() - if err != nil { - t.Fatalf("Unable to get hypervisor: %v", err) - } + 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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) hypervisorsStats, err := hypervisors.GetStatistics(client).Extract() - if err != nil { - t.Fatalf("Unable to get hypervisors statistics: %v", err) - } + 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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) hypervisorID, err := getHypervisorID(t, client) - if err != nil { - t.Fatal(err) - } + th.AssertNoErr(t, err) hypervisor, err := hypervisors.GetUptime(client, hypervisorID).Extract() - if err != nil { - t.Fatalf("Unable to hypervisor uptime: %v", err) - } + 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() - 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 { - return h.ID, nil + 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 954716bda0..3f61188ae4 100644 --- a/acceptance/openstack/compute/v2/migrate_test.go +++ b/acceptance/openstack/compute/v2/migrate_test.go @@ -7,47 +7,36 @@ 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) { - choices, err := clients.AcceptanceTestChoicesFromEnv() - if err != nil { - t.Fatal(err) - } - - if !choices.LiveMigrate { - t.Skip("Testing of live migration is disabled") - } + clients.RequireLong(t) + clients.RequireAdmin(t) + clients.RequireLiveMigration(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) @@ -61,7 +50,5 @@ func TestLiveMigrate(t *testing.T) { } err = migrate.LiveMigrate(client, server.ID, liveMigrateOpts).ExtractErr() - if err != nil { - t.Fatalf("Error during live migration: %v", err) - } + 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 d77c4ace86..0d69a18fd0 100644 --- a/acceptance/openstack/compute/v2/secgroup_test.go +++ b/acceptance/openstack/compute/v2/secgroup_test.go @@ -9,137 +9,136 @@ import ( "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) + 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) - server, err := CreateServer(t, client) - if err != nil { - t.Fatalf("Unable to create server: %v", err) - } + server, err = CreateServer(t, client) + th.AssertNoErr(t, err) defer DeleteServer(t, client, server) t.Logf("Adding group %s to server %s", securityGroup.ID, server.ID) err = secgroups.AddServer(client, server.ID, securityGroup.Name).ExtractErr() - if err != nil { - 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() - if err != nil { - t.Fatalf("Unable to get server %s: %s", server.ID, err) - } + 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 { - 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 index b949b70fa2..5c6484e0e6 100644 --- a/acceptance/openstack/compute/v2/services_test.go +++ b/acceptance/openstack/compute/v2/services_test.go @@ -8,25 +8,29 @@ import ( "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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + th.AssertNoErr(t, err) allPages, err := services.List(client).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") - } + 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 index 1537fda0cb..0511f8937c 100644 --- a/acceptance/openstack/compute/v2/usage_test.go +++ b/acceptance/openstack/compute/v2/usage_test.go @@ -5,30 +5,43 @@ 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() - if err != nil { - t.Fatalf("Unable to create a compute client: %v", err) - } + 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] - page, err := usage.SingleTenant(client, tenantID, nil).AllPages() - if err != nil { - t.Fatal(err) + 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) - if err != nil { - t.Fatal(err) - } + 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) } From c7ca48da8eafab13e1835090a1147e64cfc10174 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Sun, 25 Mar 2018 23:12:57 +0200 Subject: [PATCH 079/120] Vpnaas: List IPSec site connections (#842) * Added list function for site connections * Added acceptance test and documentation --- .../extensions/vpnaas/siteconnection_test.go | 21 ++++ .../extensions/vpnaas/siteconnections/doc.go | 15 ++- .../vpnaas/siteconnections/requests.go | 54 ++++++++++- .../vpnaas/siteconnections/results.go | 38 ++++++++ .../siteconnections/testing/requests_test.go | 97 +++++++++++++++++++ 5 files changed, 223 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go index c063dc24f5..5bf7560747 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go @@ -14,6 +14,27 @@ import ( "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 { diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go index 9311ec61a2..76d71553a2 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go @@ -34,9 +34,22 @@ Example to Show the details of a specific IPSec site connection by ID Example to Delete a site connection connID := "38aee955-6283-4279-b091-8b9c828000ec" - err := siteconnections.Delete(networkClient, serviceID).ExtractErr() + 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) + } + */ package siteconnections diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go index 6d79dc1395..70fd9d1152 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go @@ -2,6 +2,7 @@ package siteconnections import ( "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" ) // CreateOptsBuilder allows extensions to add additional parameters to the @@ -111,7 +112,7 @@ type CreateOpts struct { MTU int `json:"mtu,omitempty"` } -// ToServiceCreateMap casts a CreateOpts struct to a map. +// ToConnectionCreateMap casts a CreateOpts struct to a map. func (opts CreateOpts) ToConnectionCreateMap() (map[string]interface{}, error) { return gophercloud.BuildRequestBody(opts, "ipsec_site_connection") } @@ -140,3 +141,54 @@ 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}} + }) +} diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go index 351aa0d29a..c8e21d5f71 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go @@ -2,6 +2,7 @@ package siteconnections import ( "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" ) type DPD struct { @@ -91,6 +92,43 @@ 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 { diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go index c552091cce..710f713664 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go @@ -8,6 +8,7 @@ import ( "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" ) @@ -213,3 +214,99 @@ func TestGet(t *testing.T) { } 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) + } +} From 2b0354debf8c26defaa09cad57d2e148b7dcaf3a Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Tue, 27 Mar 2018 03:58:43 +0200 Subject: [PATCH 080/120] Vpnaas: Update endpoint groups (#841) * Added endpoint group update function * doc fixes(changed strings to pointers) --- .../v2/extensions/vpnaas/group_test.go | 12 ++++ .../extensions/vpnaas/endpointgroups/doc.go | 13 ++++ .../vpnaas/endpointgroups/requests.go | 30 +++++++++ .../vpnaas/endpointgroups/results.go | 6 ++ .../endpointgroups/testing/requests_test.go | 63 +++++++++++++++++++ 5 files changed, 124 insertions(+) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go index f793b3428c..853065d219 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go @@ -50,4 +50,16 @@ func TestGroupCRUD(t *testing.T) { } 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/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go index f8dff3b66c..5f49bd1da4 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/doc.go @@ -41,5 +41,18 @@ Example to List Endpoint groups 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 index cd9ca9ce8c..c12d0a8004 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/requests.go @@ -112,3 +112,33 @@ 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 index c3d7dfcf5d..822b70002c 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/results.go @@ -96,3 +96,9 @@ type GetResult struct { 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 index 1a3814d47b..7feac37f78 100644 --- a/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/endpointgroups/testing/requests_test.go @@ -200,3 +200,66 @@ func TestDelete(t *testing.T) { 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) +} From 3b8ddacec5a2308dc30446f82c6bab8337555b67 Mon Sep 17 00:00:00 2001 From: sreinkemeier Date: Tue, 20 Feb 2018 14:03:27 +0100 Subject: [PATCH 081/120] added update request --- .../networking/v2/extensions/vpnaas/vpnaas.go | 25 +++++++ .../v2/extensions/vpnaas/services/doc.go | 15 +++- .../v2/extensions/vpnaas/services/requests.go | 36 ++++++++++ .../v2/extensions/vpnaas/services/results.go | 6 ++ .../vpnaas/services/testing/requests_test.go | 69 ++++++++++++++++++- .../vpnaas/siteconnections/requests.go | 1 + 6 files changed, 150 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go index f42aa50450..2194a4c70e 100644 --- a/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go +++ b/acceptance/openstack/networking/v2/extensions/vpnaas/vpnaas.go @@ -146,6 +146,31 @@ func CreateEndpointGroup(t *testing.T, client *gophercloud.ServiceClient) (*endp 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. diff --git a/openstack/networking/v2/extensions/vpnaas/services/doc.go b/openstack/networking/v2/extensions/vpnaas/services/doc.go index cb1ef8cabf..6bd3236c84 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/services/doc.go @@ -36,10 +36,23 @@ Example to Create a Service 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 := policies.Delete(networkClient, serviceID).ExtractErr() + err := services.Delete(networkClient, serviceID).ExtractErr() if err != nil { panic(err) } diff --git a/openstack/networking/v2/extensions/vpnaas/services/requests.go b/openstack/networking/v2/extensions/vpnaas/services/requests.go index 6cfd656a25..8d642197e0 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/services/requests.go @@ -61,6 +61,42 @@ func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { 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 { diff --git a/openstack/networking/v2/extensions/vpnaas/services/results.go b/openstack/networking/v2/extensions/vpnaas/services/results.go index df30884e21..5e555699fc 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/results.go +++ b/openstack/networking/v2/extensions/vpnaas/services/results.go @@ -113,3 +113,9 @@ type CreateResult struct { 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 index 0bfa1ded67..ca7adf327c 100644 --- a/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/services/testing/requests_test.go @@ -194,7 +194,74 @@ func TestDelete(t *testing.T) { 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/siteconnections/requests.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go index 70fd9d1152..9906e67b2a 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go @@ -126,6 +126,7 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul return } _, r.Err = c.Post(rootURL(c), b, &r.Body, nil) + return } From 6fb25bc8ee20976989ee1db3b9d7a7b08a1e1a49 Mon Sep 17 00:00:00 2001 From: Simon Reinkemeier Date: Tue, 27 Mar 2018 21:38:32 +0200 Subject: [PATCH 082/120] Vpnaas: Update Site connection (#847) * Added update function and unit test * Changed strings to pointers in documentation --- .../v2/extensions/vpnaas/ikepolicies/doc.go | 6 +- .../v2/extensions/vpnaas/ipsecpolicies/doc.go | 6 +- .../extensions/vpnaas/siteconnections/doc.go | 13 +++ .../vpnaas/siteconnections/requests.go | 48 +++++++++ .../vpnaas/siteconnections/results.go | 6 ++ .../siteconnections/testing/requests_test.go | 101 ++++++++++++++++++ 6 files changed, 176 insertions(+), 4 deletions(-) diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go index 2285aabd3b..ee44279afa 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/doc.go @@ -33,9 +33,11 @@ Example to Delete a Policy Example to Update an IKE policy + name := "updatedname" + description := "updated policy" updateOpts := ikepolicies.UpdateOpts{ - Name: "updatedname", - Description: "updated policy", + Name: &name, + Description: &description, Lifetime: &ikepolicies.LifetimeUpdateOpts{ Value: 7000, }, diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go index 13a1636807..91d5451a6e 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/doc.go @@ -29,9 +29,11 @@ Example to Show the details of a specific IPSec policy by ID Example to Update an IPSec policy + name := "updatedname" + description := "updated policy" updateOpts := ipsecpolicies.UpdateOpts{ - Name: "updatedname", - Description: "updated policy", + Name: &name, + Description: &description, } updatedPolicy, err := ipsecpolicies.Update(client, "5c561d9d-eaea-45f6-ae3e-08d1a7080828", updateOpts).Extract() if err != nil { diff --git a/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go index 76d71553a2..66befd3ba2 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/doc.go @@ -51,5 +51,18 @@ Example to List site connections 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 index 70fd9d1152..bc2a911f22 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/requests.go @@ -192,3 +192,51 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { 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 index c8e21d5f71..3c09e4d074 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/results.go @@ -155,3 +155,9 @@ type DeleteResult struct { 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 index 710f713664..3db27364df 100644 --- a/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/siteconnections/testing/requests_test.go @@ -310,3 +310,104 @@ func TestList(t *testing.T) { 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) +} From 781450b3c4fcb4f5182bcc5133adb4b2e4a09d1d Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Fri, 30 Mar 2018 09:58:14 -0700 Subject: [PATCH 083/120] Clustering policytype list (#860) * Clustering PolicyType list implementation * ran gofmt * fix policytype list example * add policytype list acceptance test * remove admin required * Fix review comments * add acceptance test for microversion 1.5 * Fix wrong method name --- acceptance/clients/clients.go | 19 +++ acceptance/openstack/clustering/v1/pkg.go | 2 + .../clustering/v1/policytypes_test.go | 43 ++++++ openstack/client.go | 6 + openstack/clustering/v1/policytypes/doc.go | 21 +++ .../clustering/v1/policytypes/requests.go | 14 ++ .../clustering/v1/policytypes/results.go | 38 +++++ .../clustering/v1/policytypes/testing/doc.go | 2 + .../v1/policytypes/testing/fixtures.go | 139 ++++++++++++++++++ .../v1/policytypes/testing/requests_test.go | 37 +++++ openstack/clustering/v1/policytypes/urls.go | 12 ++ 11 files changed, 333 insertions(+) create mode 100644 acceptance/openstack/clustering/v1/pkg.go create mode 100644 acceptance/openstack/clustering/v1/policytypes_test.go create mode 100644 openstack/clustering/v1/policytypes/doc.go create mode 100644 openstack/clustering/v1/policytypes/requests.go create mode 100644 openstack/clustering/v1/policytypes/results.go create mode 100644 openstack/clustering/v1/policytypes/testing/doc.go create mode 100644 openstack/clustering/v1/policytypes/testing/fixtures.go create mode 100644 openstack/clustering/v1/policytypes/testing/requests_test.go create mode 100644 openstack/clustering/v1/policytypes/urls.go diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index eee80005ef..4c916309a0 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -492,3 +492,22 @@ func NewLoadBalancerV2Client() (*gophercloud.ServiceClient, error) { 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"), + }) +} 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/policytypes_test.go b/acceptance/openstack/clustering/v1/policytypes_test.go new file mode 100644 index 0000000000..a3bb9c7c77 --- /dev/null +++ b/acceptance/openstack/clustering/v1/policytypes_test.go @@ -0,0 +1,43 @@ +// +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) + } +} diff --git a/openstack/client.go b/openstack/client.go index 5a52e57914..85705d2126 100644 --- a/openstack/client.go +++ b/openstack/client.go @@ -394,3 +394,9 @@ func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi sc.ResourceBase = sc.Endpoint + "v2.0/" return sc, err } + +// 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") +} diff --git a/openstack/clustering/v1/policytypes/doc.go b/openstack/clustering/v1/policytypes/doc.go new file mode 100644 index 0000000000..d89641c014 --- /dev/null +++ b/openstack/clustering/v1/policytypes/doc.go @@ -0,0 +1,21 @@ +/* +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) + } +*/ +package policytypes diff --git a/openstack/clustering/v1/policytypes/requests.go b/openstack/clustering/v1/policytypes/requests.go new file mode 100644 index 0000000000..87ef56a049 --- /dev/null +++ b/openstack/clustering/v1/policytypes/requests.go @@ -0,0 +1,14 @@ +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)} + }) +} diff --git a/openstack/clustering/v1/policytypes/results.go b/openstack/clustering/v1/policytypes/results.go new file mode 100644 index 0000000000..2be8b3df09 --- /dev/null +++ b/openstack/clustering/v1/policytypes/results.go @@ -0,0 +1,38 @@ +package policytypes + +import ( + "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 +} 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..4854d70321 --- /dev/null +++ b/openstack/clustering/v1/policytypes/testing/fixtures.go @@ -0,0 +1,139 @@ +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 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" + } + ] + } + } + ] +} +` + +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", + }, + }, + }, + }, + } +) + +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) + }) +} 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..2714c9876f --- /dev/null +++ b/openstack/clustering/v1/policytypes/testing/requests_test.go @@ -0,0 +1,37 @@ +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) + } +} diff --git a/openstack/clustering/v1/policytypes/urls.go b/openstack/clustering/v1/policytypes/urls.go new file mode 100644 index 0000000000..3cc0fec8fb --- /dev/null +++ b/openstack/clustering/v1/policytypes/urls.go @@ -0,0 +1,12 @@ +package policytypes + +import "github.com/gophercloud/gophercloud" + +const ( + apiVersion = "v1" + apiName = "policy-types" +) + +func policyTypeListURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL(apiVersion, apiName) +} From 91d0b48a086c7d21bb0235009c0c877c5523b262 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sun, 1 Apr 2018 16:31:25 -0600 Subject: [PATCH 084/120] Docs: Contributor Tutorial (#845) * Move assets to docs directory * Move FAQ to docs * Move styleguide to docs * Move migration guide to docs * Adding contributor tutorial --- .github/CONTRIBUTING.md | 4 +- README.md | 6 +- FAQ.md => docs/FAQ.md | 0 MIGRATING.md => docs/MIGRATING.md | 0 STYLEGUIDE.md => docs/STYLEGUIDE.md | 0 {assets => docs/assets}/openlab.png | Bin {assets => docs/assets}/vexxhost.png | Bin docs/contributor-tutorial/README.md | 12 ++ .../step-01-introduction.md | 16 ++ docs/contributor-tutorial/step-02-issues.md | 124 ++++++++++++ .../step-03-code-hunting.md | 104 ++++++++++ .../step-04-acceptance-testing.md | 27 +++ .../step-05-pull-requests.md | 183 ++++++++++++++++++ .../step-06-code-review.md | 93 +++++++++ .../step-07-congratulations.md | 9 + 15 files changed, 573 insertions(+), 5 deletions(-) rename FAQ.md => docs/FAQ.md (100%) rename MIGRATING.md => docs/MIGRATING.md (100%) rename STYLEGUIDE.md => docs/STYLEGUIDE.md (100%) rename {assets => docs/assets}/openlab.png (100%) rename {assets => docs/assets}/vexxhost.png (100%) create mode 100644 docs/contributor-tutorial/README.md create mode 100644 docs/contributor-tutorial/step-01-introduction.md create mode 100644 docs/contributor-tutorial/step-02-issues.md create mode 100644 docs/contributor-tutorial/step-03-code-hunting.md create mode 100644 docs/contributor-tutorial/step-04-acceptance-testing.md create mode 100644 docs/contributor-tutorial/step-05-pull-requests.md create mode 100644 docs/contributor-tutorial/step-06-code-review.md create mode 100644 docs/contributor-tutorial/step-07-congratulations.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d6c894637e..0d511dbf9f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -101,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). @@ -247,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/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/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. From d38bdaf7706c833e9f32c6b1cac3de2191266ff0 Mon Sep 17 00:00:00 2001 From: "zhang.zujian" Date: Mon, 2 Apr 2018 14:17:39 +0800 Subject: [PATCH 085/120] fix Fatalf(): format %v reads arg #1, but call has only 0 args --- acceptance/openstack/identity/v3/endpoint_test.go | 8 ++++---- acceptance/openstack/identity/v3/projects_test.go | 8 ++++---- acceptance/openstack/identity/v3/service_test.go | 2 +- acceptance/openstack/identity/v3/token_test.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/acceptance/openstack/identity/v3/endpoint_test.go b/acceptance/openstack/identity/v3/endpoint_test.go index a589970606..68f5a351d4 100644 --- a/acceptance/openstack/identity/v3/endpoint_test.go +++ b/acceptance/openstack/identity/v3/endpoint_test.go @@ -15,7 +15,7 @@ import ( func TestEndpointsList(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { - t.Fatalf("Unable to obtain an identity client: %v") + t.Fatalf("Unable to obtain an identity client: %v", err) } allPages, err := endpoints.List(client, nil).AllPages() @@ -36,7 +36,7 @@ func TestEndpointsList(t *testing.T) { func TestEndpointsNavigateCatalog(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { - t.Fatalf("Unable to obtain an identity client: %v") + t.Fatalf("Unable to obtain an identity client: %v", err) } // Discover the service we're interested in. @@ -51,7 +51,7 @@ func TestEndpointsNavigateCatalog(t *testing.T) { allServices, err := services.ExtractServices(allPages) if err != nil { - t.Fatalf("Unable to extract service: %v") + t.Fatalf("Unable to extract service: %v", err) } if len(allServices) != 1 { @@ -74,7 +74,7 @@ func TestEndpointsNavigateCatalog(t *testing.T) { allEndpoints, err := endpoints.ExtractEndpoints(allPages) if err != nil { - t.Fatalf("Unable to extract endpoint: %v") + t.Fatalf("Unable to extract endpoint: %v", err) } if len(allEndpoints) != 1 { diff --git a/acceptance/openstack/identity/v3/projects_test.go b/acceptance/openstack/identity/v3/projects_test.go index 08a5cfdad4..326b4ad034 100644 --- a/acceptance/openstack/identity/v3/projects_test.go +++ b/acceptance/openstack/identity/v3/projects_test.go @@ -64,7 +64,7 @@ func TestProjectsGet(t *testing.T) { func TestProjectsCRUD(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { - t.Fatalf("Unable to obtain an identity client: %v") + t.Fatalf("Unable to obtain an identity client: %v", err) } project, err := CreateProject(t, client, nil) @@ -91,7 +91,7 @@ func TestProjectsCRUD(t *testing.T) { func TestProjectsDomain(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { - t.Fatalf("Unable to obtain an identity client: %v") + t.Fatalf("Unable to obtain an identity client: %v", err) } var iTrue = true @@ -126,14 +126,14 @@ func TestProjectsDomain(t *testing.T) { _, err = projects.Update(client, projectDomain.ID, updateOpts).Extract() if err != nil { - t.Fatalf("Unable to disable domain: %v") + t.Fatalf("Unable to disable domain: %v", err) } } func TestProjectsNested(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { - t.Fatalf("Unable to obtain an identity client: %v") + t.Fatalf("Unable to obtain an identity client: %v", err) } projectMain, err := CreateProject(t, client, nil) diff --git a/acceptance/openstack/identity/v3/service_test.go b/acceptance/openstack/identity/v3/service_test.go index ffd7e4d1fb..ed9d3855df 100644 --- a/acceptance/openstack/identity/v3/service_test.go +++ b/acceptance/openstack/identity/v3/service_test.go @@ -13,7 +13,7 @@ import ( func TestServicesList(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { - t.Fatalf("Unable to obtain an identity client: %v") + t.Fatalf("Unable to obtain an identity client: %v", err) } listOpts := services.ListOpts{ diff --git a/acceptance/openstack/identity/v3/token_test.go b/acceptance/openstack/identity/v3/token_test.go index 0f471f776b..b426116f4d 100644 --- a/acceptance/openstack/identity/v3/token_test.go +++ b/acceptance/openstack/identity/v3/token_test.go @@ -14,7 +14,7 @@ import ( func TestGetToken(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { - t.Fatalf("Unable to obtain an identity client: %v") + t.Fatalf("Unable to obtain an identity client: %v", err) } ao, err := openstack.AuthOptionsFromEnv() From fd734d98d0a24dc7e7e14861563eef154dec560b Mon Sep 17 00:00:00 2001 From: Rui Chen Date: Mon, 2 Apr 2018 15:38:50 +0800 Subject: [PATCH 086/120] Support acceptance test against OpenStack Queens release in OpenLab Input comment "recheck stable/queens" in pull request comments, that will trigger acceptance tests against OpenStack Queens release. OpenLab will report test result in followint comments automatically. And add support for Newton and Ocata release - "recheck stable/newton" -> test against OpenStack Newton release - "recheck stable/ocata" -> test against OpenStack Ocata release Related-Bug: theopenlab/openlab#38 --- .zuul.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From d2426f82c33d910620b0a6a34ecc5fa57a693f6c Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Mon, 2 Apr 2018 11:49:58 -0700 Subject: [PATCH 087/120] Clustering PolicyType Get implementation (#861) * Add Clustering PolicyType Get implementation * Changed PolicyTypeDetail Schema to map[string]interface{} * Updated test fixtures to use real response from API --- .../clustering/v1/policytypes_test.go | 21 ++++ openstack/clustering/v1/policytypes/doc.go | 9 ++ .../clustering/v1/policytypes/requests.go | 7 ++ .../clustering/v1/policytypes/results.go | 25 +++++ .../v1/policytypes/testing/fixtures.go | 104 ++++++++++++++++-- .../v1/policytypes/testing/requests_test.go | 12 ++ openstack/clustering/v1/policytypes/urls.go | 4 + 7 files changed, 175 insertions(+), 7 deletions(-) diff --git a/acceptance/openstack/clustering/v1/policytypes_test.go b/acceptance/openstack/clustering/v1/policytypes_test.go index a3bb9c7c77..fdb42a3153 100644 --- a/acceptance/openstack/clustering/v1/policytypes_test.go +++ b/acceptance/openstack/clustering/v1/policytypes_test.go @@ -41,3 +41,24 @@ func TestPolicyTypeList_v_1_5(t *testing.T) { 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/openstack/clustering/v1/policytypes/doc.go b/openstack/clustering/v1/policytypes/doc.go index d89641c014..1886939f39 100644 --- a/openstack/clustering/v1/policytypes/doc.go +++ b/openstack/clustering/v1/policytypes/doc.go @@ -17,5 +17,14 @@ Example to list policy types 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 index 87ef56a049..bb8a48d40f 100644 --- a/openstack/clustering/v1/policytypes/requests.go +++ b/openstack/clustering/v1/policytypes/requests.go @@ -12,3 +12,10 @@ func List(client *gophercloud.ServiceClient) pagination.Pager { 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 index 2be8b3df09..4d5d8e1c3f 100644 --- a/openstack/clustering/v1/policytypes/results.go +++ b/openstack/clustering/v1/policytypes/results.go @@ -1,6 +1,7 @@ package policytypes import ( + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" ) @@ -36,3 +37,27 @@ 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/fixtures.go b/openstack/clustering/v1/policytypes/testing/fixtures.go index 4854d70321..3db56cdadc 100644 --- a/openstack/clustering/v1/policytypes/testing/fixtures.go +++ b/openstack/clustering/v1/policytypes/testing/fixtures.go @@ -10,6 +10,8 @@ import ( fake "github.com/gophercloud/gophercloud/testhelper/client" ) +const FakePolicyTypetoGet = "fake-policytype" + const PolicyTypeBody = ` { "policy_types": [ @@ -69,6 +71,45 @@ const PolicyTypeBody = ` } ` +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{ { @@ -124,16 +165,65 @@ var ( }, }, } + + 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) + 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) + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, PolicyTypeBody) - }) + fmt.Fprintf(w, PolicyTypeDetailBody) + }) } diff --git a/openstack/clustering/v1/policytypes/testing/requests_test.go b/openstack/clustering/v1/policytypes/testing/requests_test.go index 2714c9876f..87e42fab1a 100644 --- a/openstack/clustering/v1/policytypes/testing/requests_test.go +++ b/openstack/clustering/v1/policytypes/testing/requests_test.go @@ -35,3 +35,15 @@ func TestListPolicyTypes(t *testing.T) { 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 index 3cc0fec8fb..b291a95c70 100644 --- a/openstack/clustering/v1/policytypes/urls.go +++ b/openstack/clustering/v1/policytypes/urls.go @@ -10,3 +10,7 @@ const ( 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) +} From 3ff109d28762ddc1560994a051fab6b1146c6dbf Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Mon, 2 Apr 2018 13:14:32 -0600 Subject: [PATCH 088/120] Octavia (#854) * LBaaS v2: Initial commit of dedicated lbaas packages * LBaaS v2: Customizing the loadbalancer packages * Networking v2: Removing old Octavia acceptance test * LBaaS v2: gofmt fixes * LBaaS v2: fix acceptance test client * LBaaS v2: Support Cascading delete by way of a DeleteOpts parameter --- acceptance/clients/clients.go | 30 +- .../loadbalancer/v2/listeners_test.go | 32 ++ .../openstack/loadbalancer/v2/loadbalancer.go | 309 ++++++++++++++ .../loadbalancer/v2/loadbalancers_test.go | 326 +++++++++++++++ .../loadbalancer/v2/monitors_test.go | 32 ++ acceptance/openstack/loadbalancer/v2/pkg.go | 1 + .../openstack/loadbalancer/v2/pools_test.go | 32 ++ .../extensions/lbaas_v2/loadbalancers_test.go | 149 ------- openstack/loadbalancer/v2/doc.go | 3 + openstack/loadbalancer/v2/listeners/doc.go | 63 +++ .../loadbalancer/v2/listeners/requests.go | 199 +++++++++ .../loadbalancer/v2/listeners/results.go | 131 ++++++ .../loadbalancer/v2/listeners/testing/doc.go | 2 + .../v2/listeners/testing/fixtures.go | 213 ++++++++++ .../v2/listeners/testing/requests_test.go | 137 +++++++ openstack/loadbalancer/v2/listeners/urls.go | 16 + .../loadbalancer/v2/loadbalancers/doc.go | 76 ++++ .../loadbalancer/v2/loadbalancers/requests.go | 210 ++++++++++ .../loadbalancer/v2/loadbalancers/results.go | 149 +++++++ .../v2/loadbalancers/testing/doc.go | 2 + .../v2/loadbalancers/testing/fixtures.go | 284 +++++++++++++ .../v2/loadbalancers/testing/requests_test.go | 162 ++++++++ .../loadbalancer/v2/loadbalancers/urls.go | 21 + openstack/loadbalancer/v2/monitors/doc.go | 69 ++++ .../loadbalancer/v2/monitors/requests.go | 257 ++++++++++++ openstack/loadbalancer/v2/monitors/results.go | 149 +++++++ .../loadbalancer/v2/monitors/testing/doc.go | 2 + .../v2/monitors/testing/fixtures.go | 215 ++++++++++ .../v2/monitors/testing/requests_test.go | 154 +++++++ openstack/loadbalancer/v2/monitors/urls.go | 16 + openstack/loadbalancer/v2/pools/doc.go | 124 ++++++ openstack/loadbalancer/v2/pools/requests.go | 356 ++++++++++++++++ openstack/loadbalancer/v2/pools/results.go | 273 ++++++++++++ .../loadbalancer/v2/pools/testing/doc.go | 2 + .../loadbalancer/v2/pools/testing/fixtures.go | 388 ++++++++++++++++++ .../v2/pools/testing/requests_test.go | 262 ++++++++++++ openstack/loadbalancer/v2/pools/urls.go | 25 ++ .../loadbalancer/v2/testhelper/client.go | 14 + 38 files changed, 4722 insertions(+), 163 deletions(-) create mode 100644 acceptance/openstack/loadbalancer/v2/listeners_test.go create mode 100644 acceptance/openstack/loadbalancer/v2/loadbalancer.go create mode 100644 acceptance/openstack/loadbalancer/v2/loadbalancers_test.go create mode 100644 acceptance/openstack/loadbalancer/v2/monitors_test.go create mode 100644 acceptance/openstack/loadbalancer/v2/pkg.go create mode 100644 acceptance/openstack/loadbalancer/v2/pools_test.go create mode 100644 openstack/loadbalancer/v2/doc.go create mode 100644 openstack/loadbalancer/v2/listeners/doc.go create mode 100644 openstack/loadbalancer/v2/listeners/requests.go create mode 100644 openstack/loadbalancer/v2/listeners/results.go create mode 100644 openstack/loadbalancer/v2/listeners/testing/doc.go create mode 100644 openstack/loadbalancer/v2/listeners/testing/fixtures.go create mode 100644 openstack/loadbalancer/v2/listeners/testing/requests_test.go create mode 100644 openstack/loadbalancer/v2/listeners/urls.go create mode 100644 openstack/loadbalancer/v2/loadbalancers/doc.go create mode 100644 openstack/loadbalancer/v2/loadbalancers/requests.go create mode 100644 openstack/loadbalancer/v2/loadbalancers/results.go create mode 100644 openstack/loadbalancer/v2/loadbalancers/testing/doc.go create mode 100644 openstack/loadbalancer/v2/loadbalancers/testing/fixtures.go create mode 100644 openstack/loadbalancer/v2/loadbalancers/testing/requests_test.go create mode 100644 openstack/loadbalancer/v2/loadbalancers/urls.go create mode 100644 openstack/loadbalancer/v2/monitors/doc.go create mode 100644 openstack/loadbalancer/v2/monitors/requests.go create mode 100644 openstack/loadbalancer/v2/monitors/results.go create mode 100644 openstack/loadbalancer/v2/monitors/testing/doc.go create mode 100644 openstack/loadbalancer/v2/monitors/testing/fixtures.go create mode 100644 openstack/loadbalancer/v2/monitors/testing/requests_test.go create mode 100644 openstack/loadbalancer/v2/monitors/urls.go create mode 100644 openstack/loadbalancer/v2/pools/doc.go create mode 100644 openstack/loadbalancer/v2/pools/requests.go create mode 100644 openstack/loadbalancer/v2/pools/results.go create mode 100644 openstack/loadbalancer/v2/pools/testing/doc.go create mode 100644 openstack/loadbalancer/v2/pools/testing/fixtures.go create mode 100644 openstack/loadbalancer/v2/pools/testing/requests_test.go create mode 100644 openstack/loadbalancer/v2/pools/urls.go create mode 100644 openstack/loadbalancer/v2/testhelper/client.go diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index 4c916309a0..93e852418a 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -460,20 +460,6 @@ func NewSharedFileSystemV2Client() (*gophercloud.ServiceClient, error) { }) } -// 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 -} - // 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. @@ -488,6 +474,8 @@ func NewLoadBalancerV2Client() (*gophercloud.ServiceClient, error) { return nil, err } + client = configureDebug(client) + return openstack.NewLoadBalancerV2(client, gophercloud.EndpointOpts{ Region: os.Getenv("OS_REGION_NAME"), }) @@ -511,3 +499,17 @@ func NewClusteringV1Client() (*gophercloud.ServiceClient, error) { 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 +} 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..0a811d69c3 --- /dev/null +++ b/acceptance/openstack/loadbalancer/v2/loadbalancer.go @@ -0,0 +1,309 @@ +package v2 + +import ( + "fmt" + "strings" + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/acceptance/tools" + "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 +} + +// 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..b18b975c35 --- /dev/null +++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go @@ -0,0 +1,326 @@ +// +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/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) + + // 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/networking/v2/extensions/lbaas_v2/loadbalancers_test.go b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go index 26064f0c28..650eb2cc49 100644 --- a/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go +++ b/acceptance/openstack/networking/v2/extensions/lbaas_v2/loadbalancers_test.go @@ -176,152 +176,3 @@ func TestLoadbalancersCRUD(t *testing.T) { tools.PrintResource(t, newMonitor) } - -func TestOctaviaLoadbalancersCRUD(t *testing.T) { - netClient, err := clients.NewNetworkV2Client() - if err != nil { - t.Fatalf("Unable to create a network client: %v", err) - } - - lbClient, err := clients.NewLoadBalancerV2Client() - if err != nil { - t.Fatalf("Unable to create a network 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 func() { - t.Logf("Running cascading delete on Octavia LB...") - err := loadbalancers.CascadingDelete(lbClient, lb.ID).ExtractErr() - if err != nil { - t.Fatalf("Error running cascading delete: %v", err) - } - }() - - 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/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/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 +} From 6190a9d8261cd133ffde3c6abca0a09a5ec50191 Mon Sep 17 00:00:00 2001 From: zhangzujian Date: Wed, 4 Apr 2018 01:45:11 +0800 Subject: [PATCH 089/120] Identity V3: Change user password (#863) * Identity V3: change password for user * add testing * Identity V3 users: some renaming * fix JSON tags * fix doc and testing * fix doc * add acceptance testing for users.ChangePassword() --- .../openstack/identity/v3/users_test.go | 40 +++++++++++++++++++ openstack/identity/v3/users/doc.go | 16 ++++++++ openstack/identity/v3/users/requests.go | 39 ++++++++++++++++++ openstack/identity/v3/users/results.go | 6 +++ .../identity/v3/users/testing/fixtures.go | 24 ++++++++++- .../v3/users/testing/requests_test.go | 14 +++++++ openstack/identity/v3/users/urls.go | 4 ++ 7 files changed, 142 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/identity/v3/users_test.go b/acceptance/openstack/identity/v3/users_test.go index 3ba1e87cf5..7abeb9a887 100644 --- a/acceptance/openstack/identity/v3/users_test.go +++ b/acceptance/openstack/identity/v3/users_test.go @@ -123,6 +123,46 @@ func TestUserCRUD(t *testing.T) { tools.PrintResource(t, newUser.Extra) } +func TestUserChangePassword(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + createOpts := users.CreateOpts{ + Password: "secretsecret", + DomainID: "default", + Options: map[users.Option]interface{}{ + users.IgnorePasswordExpiry: true, + users.MultiFactorAuthRules: []interface{}{ + []string{"password", "totp"}, + []string{"password", "custom-auth-method"}, + }, + }, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, + } + + user, err := CreateUser(t, client, &createOpts) + if err != nil { + t.Fatalf("Unable to create user: %v", err) + } + defer DeleteUser(t, client, user.ID) + + tools.PrintResource(t, user) + tools.PrintResource(t, user.Extra) + + changePasswordOpts := users.ChangePasswordOpts{ + OriginalPassword: "secretsecret", + Password: "new_secretsecret", + } + err = users.ChangePassword(client, user.ID, changePasswordOpts).ExtractErr() + if err != nil { + t.Fatalf("Unable to change password for user: %v", err) + } +} + func TestUsersListGroups(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { diff --git a/openstack/identity/v3/users/doc.go b/openstack/identity/v3/users/doc.go index aa7ec196f5..282d0f9ca2 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" diff --git a/openstack/identity/v3/users/requests.go b/openstack/identity/v3/users/requests.go index 779d116fcc..b8eb7b0628 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) diff --git a/openstack/identity/v3/users/results.go b/openstack/identity/v3/users/results.go index c474e882b9..12ab05eaba 100644 --- a/openstack/identity/v3/users/results.go +++ b/openstack/identity/v3/users/results.go @@ -98,6 +98,12 @@ 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 { diff --git a/openstack/identity/v3/users/testing/fixtures.go b/openstack/identity/v3/users/testing/fixtures.go index 8d8e6df642..820da2ee5e 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) { diff --git a/openstack/identity/v3/users/testing/requests_test.go b/openstack/identity/v3/users/testing/requests_test.go index 15314ca61c..884c31d239 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() diff --git a/openstack/identity/v3/users/urls.go b/openstack/identity/v3/users/urls.go index 1db2831b5e..3d2bde85a7 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) } From 5cc045ce480e5a23394538be9ce931343248607b Mon Sep 17 00:00:00 2001 From: zhangzujian Date: Wed, 4 Apr 2018 10:28:58 +0800 Subject: [PATCH 090/120] Identity V3: add user to group (#867) * add: Identity V3: add user to group * add acceptance testing for users.AddToGroup() * fix acceptance testing --- .../openstack/identity/v3/users_test.go | 54 +++++++++++++++++++ openstack/identity/v3/users/doc.go | 10 ++++ openstack/identity/v3/users/requests.go | 9 ++++ openstack/identity/v3/users/results.go | 6 +++ .../identity/v3/users/testing/fixtures.go | 11 ++++ .../v3/users/testing/requests_test.go | 8 +++ openstack/identity/v3/users/urls.go | 4 ++ 7 files changed, 102 insertions(+) diff --git a/acceptance/openstack/identity/v3/users_test.go b/acceptance/openstack/identity/v3/users_test.go index 7abeb9a887..e21bfaf03e 100644 --- a/acceptance/openstack/identity/v3/users_test.go +++ b/acceptance/openstack/identity/v3/users_test.go @@ -196,6 +196,60 @@ func TestUsersListGroups(t *testing.T) { } } +func TestUserAddToGroup(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + createOpts := users.CreateOpts{ + Password: "foobar", + DomainID: "default", + Options: map[users.Option]interface{}{ + users.IgnorePasswordExpiry: true, + users.MultiFactorAuthRules: []interface{}{ + []string{"password", "totp"}, + []string{"password", "custom-auth-method"}, + }, + }, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, + } + + user, err := CreateUser(t, client, &createOpts) + if err != nil { + t.Fatalf("Unable to create user: %v", err) + } + defer DeleteUser(t, client, user.ID) + + tools.PrintResource(t, user) + tools.PrintResource(t, user.Extra) + + createGroupOpts := groups.CreateOpts{ + Name: "testgroup", + DomainID: "default", + Extra: map[string]interface{}{ + "email": "testgroup@example.com", + }, + } + + // Create Group in the default domain + group, err := CreateGroup(t, client, &createGroupOpts) + if err != nil { + t.Fatalf("Unable to create group: %v", err) + } + defer DeleteGroup(t, client, group.ID) + + tools.PrintResource(t, group) + tools.PrintResource(t, group.Extra) + + err = users.AddToGroup(client, group.ID, user.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to add user to group: %v", err) + } +} + func TestUsersListProjects(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { diff --git a/openstack/identity/v3/users/doc.go b/openstack/identity/v3/users/doc.go index 282d0f9ca2..ca9e4b4285 100644 --- a/openstack/identity/v3/users/doc.go +++ b/openstack/identity/v3/users/doc.go @@ -96,6 +96,16 @@ 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 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 b8eb7b0628..ec6e90669c 100644 --- a/openstack/identity/v3/users/requests.go +++ b/openstack/identity/v3/users/requests.go @@ -257,6 +257,15 @@ 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 +} + // 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 12ab05eaba..d8a55612e4 100644 --- a/openstack/identity/v3/users/results.go +++ b/openstack/identity/v3/users/results.go @@ -110,6 +110,12 @@ 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 +} + // 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 820da2ee5e..0f97c14854 100644 --- a/openstack/identity/v3/users/testing/fixtures.go +++ b/openstack/identity/v3/users/testing/fixtures.go @@ -470,6 +470,17 @@ 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) + }) +} + // 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 884c31d239..b372d98913 100644 --- a/openstack/identity/v3/users/testing/requests_test.go +++ b/openstack/identity/v3/users/testing/requests_test.go @@ -162,6 +162,14 @@ 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 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 3d2bde85a7..900a435c62 100644 --- a/openstack/identity/v3/users/urls.go +++ b/openstack/identity/v3/users/urls.go @@ -30,6 +30,10 @@ 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 listProjectsURL(client *gophercloud.ServiceClient, userID string) string { return client.ServiceURL("users", userID, "projects") } From bc178df6d64d999e4a5520b601bc3e26bb38f446 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Wed, 4 Apr 2018 16:27:41 +1200 Subject: [PATCH 091/120] LBaaS v2 l7 policy support [Part 1]: create l7policy (#834) * LBaaS v2 l7 policy support [Part 1]: create l7policy For #832 L7policy functionality in Octavia is backward compatible with Neutron-LBaaS. Octavia L7 policy create API implementation: https://github.com/openstack/octavia/blob/master/octavia/api/v2/controllers/l7policy.py#L140 Neutron-LBaaS L7 policy create API implementation: https://github.com/openstack/neutron-lbaas/blob/8e8a6c47c7d38fa7b61850ff9ea2cf130718ded3/neutron_lbaas/services/loadbalancer/plugin.py#L913 Octavia L7 policy create API doc: https://developer.openstack.org/api-ref/load-balancer/v2/index.html#create-an-l7-policy * Use original type for the response fields * Change to use lbClient for octavia acceptance test --- .../openstack/loadbalancer/v2/loadbalancer.go | 30 ++++++ .../loadbalancer/v2/loadbalancers_test.go | 6 ++ openstack/loadbalancer/v2/l7policies/doc.go | 16 ++++ .../loadbalancer/v2/l7policies/requests.go | 84 +++++++++++++++++ .../loadbalancer/v2/l7policies/results.go | 93 +++++++++++++++++++ .../loadbalancer/v2/l7policies/testing/doc.go | 2 + .../v2/l7policies/testing/fixtures.go | 67 +++++++++++++ .../v2/l7policies/testing/requests_test.go | 42 +++++++++ openstack/loadbalancer/v2/l7policies/urls.go | 12 +++ 9 files changed, 352 insertions(+) create mode 100644 openstack/loadbalancer/v2/l7policies/doc.go create mode 100644 openstack/loadbalancer/v2/l7policies/requests.go create mode 100644 openstack/loadbalancer/v2/l7policies/results.go create mode 100644 openstack/loadbalancer/v2/l7policies/testing/doc.go create mode 100644 openstack/loadbalancer/v2/l7policies/testing/fixtures.go create mode 100644 openstack/loadbalancer/v2/l7policies/testing/requests_test.go create mode 100644 openstack/loadbalancer/v2/l7policies/urls.go diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancer.go b/acceptance/openstack/loadbalancer/v2/loadbalancer.go index 0a811d69c3..ba1b64fb7e 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancer.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancer.go @@ -7,6 +7,7 @@ import ( "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" @@ -172,6 +173,35 @@ func CreatePool(t *testing.T, client *gophercloud.ServiceClient, lb *loadbalance 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 +} + // 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. diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go index b18b975c35..866f9b852a 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go @@ -100,6 +100,12 @@ func TestLoadbalancersCRUD(t *testing.T) { tools.PrintResource(t, newListener) + // L7 policy + _, err = CreateL7Policy(t, lbClient, listener, lb) + if err != nil { + t.Fatalf("Unable to create l7 policy: %v", err) + } + // Pool pool, err := CreatePool(t, lbClient, lb) if err != nil { diff --git a/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go new file mode 100644 index 0000000000..7d9418f355 --- /dev/null +++ b/openstack/loadbalancer/v2/l7policies/doc.go @@ -0,0 +1,16 @@ +/* +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(networkClient, 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..1d59b4a601 --- /dev/null +++ b/openstack/loadbalancer/v2/l7policies/requests.go @@ -0,0 +1,84 @@ +package l7policies + +import ( + "github.com/gophercloud/gophercloud" +) + +// 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"` + + // TenantID is the UUID of the project who owns the L7 policy in neutron-lbaas. + // 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 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/results.go b/openstack/loadbalancer/v2/l7policies/results.go new file mode 100644 index 0000000000..827fd59f69 --- /dev/null +++ b/openstack/loadbalancer/v2/l7policies/results.go @@ -0,0 +1,93 @@ +package l7policies + +import ( + "github.com/gophercloud/gophercloud" +) + +// 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"` + + // TenantID is the UUID of the project who owns the L7 policy in neutron-lbaas. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_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"` + + // TenantID is the UUID of the project who owns the rule in neutron-lbaas. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_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 +} 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..f64dde2e66 --- /dev/null +++ b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go @@ -0,0 +1,67 @@ +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, + "tenant_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: "", + TenantID: "e3cd678b11784734bc366148aa37580e", + RedirectPoolID: "", + RedirectURL: "http://www.example.com", + AdminStateUp: true, + Rules: []l7policies.Rule{}, + } +) + +// 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) + }) +} 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..fe0278c197 --- /dev/null +++ b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go @@ -0,0 +1,42 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies" + fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" + 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") + } +} diff --git a/openstack/loadbalancer/v2/l7policies/urls.go b/openstack/loadbalancer/v2/l7policies/urls.go new file mode 100644 index 0000000000..fd898e9b06 --- /dev/null +++ b/openstack/loadbalancer/v2/l7policies/urls.go @@ -0,0 +1,12 @@ +package l7policies + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "lbaas" + resourcePath = "l7policies" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} From 30ecef204a3177eb9d3746bb23d9ef52ca91e96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=A5=96=E5=BB=BA?= Date: Wed, 4 Apr 2018 12:35:17 +0800 Subject: [PATCH 092/120] Identity V3: remove user from group (#882) * Identity V3 users: add function RemoveFromGroup() * fix acceptance testing --- .../openstack/identity/v3/users_test.go | 61 ++++++++++++++++++- openstack/identity/v3/users/doc.go | 10 +++ openstack/identity/v3/users/requests.go | 9 +++ openstack/identity/v3/users/results.go | 6 ++ .../identity/v3/users/testing/fixtures.go | 11 ++++ .../v3/users/testing/requests_test.go | 8 +++ openstack/identity/v3/users/urls.go | 4 ++ 7 files changed, 108 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/identity/v3/users_test.go b/acceptance/openstack/identity/v3/users_test.go index e21bfaf03e..1eb15456d6 100644 --- a/acceptance/openstack/identity/v3/users_test.go +++ b/acceptance/openstack/identity/v3/users_test.go @@ -196,7 +196,7 @@ func TestUsersListGroups(t *testing.T) { } } -func TestUserAddToGroup(t *testing.T) { +func TestUsersAddToGroup(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { t.Fatalf("Unable to obtain an identity client: %v", err) @@ -250,6 +250,65 @@ func TestUserAddToGroup(t *testing.T) { } } +func TestUsersRemoveFromGroup(t *testing.T) { + client, err := clients.NewIdentityV3Client() + if err != nil { + t.Fatalf("Unable to obtain an identity client: %v", err) + } + + createOpts := users.CreateOpts{ + Password: "foobar", + DomainID: "default", + Options: map[users.Option]interface{}{ + users.IgnorePasswordExpiry: true, + users.MultiFactorAuthRules: []interface{}{ + []string{"password", "totp"}, + []string{"password", "custom-auth-method"}, + }, + }, + Extra: map[string]interface{}{ + "email": "jsmith@example.com", + }, + } + + user, err := CreateUser(t, client, &createOpts) + if err != nil { + t.Fatalf("Unable to create user: %v", err) + } + defer DeleteUser(t, client, user.ID) + + tools.PrintResource(t, user) + tools.PrintResource(t, user.Extra) + + createGroupOpts := groups.CreateOpts{ + Name: "testgroup", + DomainID: "default", + Extra: map[string]interface{}{ + "email": "testgroup@example.com", + }, + } + + // Create Group in the default domain + group, err := CreateGroup(t, client, &createGroupOpts) + if err != nil { + t.Fatalf("Unable to create group: %v", err) + } + defer DeleteGroup(t, client, group.ID) + + tools.PrintResource(t, group) + tools.PrintResource(t, group.Extra) + + err = users.AddToGroup(client, group.ID, user.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to add user to group: %v", err) + } + + err = users.RemoveFromGroup(client, group.ID, user.ID).ExtractErr() + if err != nil { + t.Fatalf("Unable to remove user from group: %v", err) + } +} + func TestUsersListProjects(t *testing.T) { client, err := clients.NewIdentityV3Client() if err != nil { diff --git a/openstack/identity/v3/users/doc.go b/openstack/identity/v3/users/doc.go index ca9e4b4285..c51a3fb607 100644 --- a/openstack/identity/v3/users/doc.go +++ b/openstack/identity/v3/users/doc.go @@ -106,6 +106,16 @@ Example to Add a User to a Group 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 ec6e90669c..e1be94e7d9 100644 --- a/openstack/identity/v3/users/requests.go +++ b/openstack/identity/v3/users/requests.go @@ -266,6 +266,15 @@ func AddToGroup(client *gophercloud.ServiceClient, groupID, userID string) (r Ad 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 d8a55612e4..00a1a00062 100644 --- a/openstack/identity/v3/users/results.go +++ b/openstack/identity/v3/users/results.go @@ -116,6 +116,12 @@ 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 0f97c14854..73e6acb767 100644 --- a/openstack/identity/v3/users/testing/fixtures.go +++ b/openstack/identity/v3/users/testing/fixtures.go @@ -481,6 +481,17 @@ func HandleAddToGroupSuccessfully(t *testing.T) { }) } +// 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 b372d98913..8cd4c47ac5 100644 --- a/openstack/identity/v3/users/testing/requests_test.go +++ b/openstack/identity/v3/users/testing/requests_test.go @@ -170,6 +170,14 @@ func TestAddToGroup(t *testing.T) { 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 900a435c62..35468ad28e 100644 --- a/openstack/identity/v3/users/urls.go +++ b/openstack/identity/v3/users/urls.go @@ -34,6 +34,10 @@ func addToGroupURL(client *gophercloud.ServiceClient, groupID, userID string) st 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") } From 041bfb8d7fd5d76f57e2af41ba9c292f7b09e9e8 Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Wed, 4 Apr 2018 08:34:37 -0700 Subject: [PATCH 093/120] Clustering Policy List implementation (#870) * Add Clustering Policy List Implementation * add metadata * Fix review comments * Changed to non-pointer time.Time for CreatedAt and UpdatedAt fields * Changed to pointer bool for GlobalProject * Fixed Domain and Project field names * Removed Metadata field * change UserUUID to User * remove debug error messages --- .../openstack/clustering/v1/policies_test.go | 36 +++++ openstack/clustering/v1/policies/doc.go | 26 +++ openstack/clustering/v1/policies/requests.go | 56 +++++++ openstack/clustering/v1/policies/results.go | 74 +++++++++ .../clustering/v1/policies/testing/doc.go | 2 + .../v1/policies/testing/fixtures.go | 152 ++++++++++++++++++ .../v1/policies/testing/requests_test.go | 41 +++++ openstack/clustering/v1/policies/urls.go | 12 ++ 8 files changed, 399 insertions(+) create mode 100644 acceptance/openstack/clustering/v1/policies_test.go create mode 100644 openstack/clustering/v1/policies/doc.go create mode 100644 openstack/clustering/v1/policies/requests.go create mode 100644 openstack/clustering/v1/policies/results.go create mode 100644 openstack/clustering/v1/policies/testing/doc.go create mode 100644 openstack/clustering/v1/policies/testing/fixtures.go create mode 100644 openstack/clustering/v1/policies/testing/requests_test.go create mode 100644 openstack/clustering/v1/policies/urls.go diff --git a/acceptance/openstack/clustering/v1/policies_test.go b/acceptance/openstack/clustering/v1/policies_test.go new file mode 100644 index 0000000000..b3343c3ac3 --- /dev/null +++ b/acceptance/openstack/clustering/v1/policies_test.go @@ -0,0 +1,36 @@ +// +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()) + } + } +} diff --git a/openstack/clustering/v1/policies/doc.go b/openstack/clustering/v1/policies/doc.go new file mode 100644 index 0000000000..053c380551 --- /dev/null +++ b/openstack/clustering/v1/policies/doc.go @@ -0,0 +1,26 @@ +/* +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) + } + +*/ +package policies diff --git a/openstack/clustering/v1/policies/requests.go b/openstack/clustering/v1/policies/requests.go new file mode 100644 index 0000000000..4f50a0ce3c --- /dev/null +++ b/openstack/clustering/v1/policies/requests.go @@ -0,0 +1,56 @@ +package policies + +import ( + "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 + }) +} diff --git a/openstack/clustering/v1/policies/results.go b/openstack/clustering/v1/policies/results.go new file mode 100644 index 0000000000..044e58d99e --- /dev/null +++ b/openstack/clustering/v1/policies/results.go @@ -0,0 +1,74 @@ +package policies + +import ( + "encoding/json" + "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 map[string]interface{} `json:"spec"` + Type string `json:"type"` + UpdatedAt time.Time `json:"-"` + User string `json:"user"` +} + +// 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 +} + +func (r *Policy) UnmarshalJSON(b []byte) error { + type tmp Policy + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at,omitempty"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at,omitempty"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Policy(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} 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..30db27b88d --- /dev/null +++ b/openstack/clustering/v1/policies/testing/fixtures.go @@ -0,0 +1,152 @@ +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": null, + "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 + }, + "type": "senlin.policy.deletion-1.0", + "updated_at": null, + "user": "fe43e41739154b72818565e0d2580819" + } + ] +} +` + +var ( + ExpectedPolicyCreatedAt1, _ = time.Parse(time.RFC3339, "2018-04-02T21:43:30.000000Z") + ExpectedPolicyCreatedAt2, _ = time.Parse(time.RFC3339, "2018-04-02T22:29:36.000000Z") + ZeroTime, _ = time.Parse(time.RFC3339, "1-01-01T00:00:00.000000Z") + + ExpectedPolicies = [][]policies.Policy{ + { + { + CreatedAt: ExpectedPolicyCreatedAt1, + Data: map[string]interface{}{}, + Domain: "", + ID: "PolicyListBodyID1", + Name: "delpol", + Project: "018cd0909fb44cd5bc9b7a3cd664920e", + + Spec: map[string]interface{}{ + "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": float64(1), + }, + Type: "senlin.policy.deletion-1.0", + User: "fe43e41739154b72818565e0d2580819", + UpdatedAt: ZeroTime, + }, + }, + { + { + CreatedAt: ExpectedPolicyCreatedAt2, + Data: map[string]interface{}{}, + Domain: "", + ID: "PolicyListBodyID2", + Name: "delpol2", + Project: "018cd0909fb44cd5bc9b7a3cd664920e", + + Spec: map[string]interface{}{ + "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": float64(1), + }, + Type: "senlin.policy.deletion-1.0", + 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) + } + }) +} 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..0f110a52f0 --- /dev/null +++ b/openstack/clustering/v1/policies/testing/requests_test.go @@ -0,0 +1,41 @@ +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) + } +} diff --git a/openstack/clustering/v1/policies/urls.go b/openstack/clustering/v1/policies/urls.go new file mode 100644 index 0000000000..9e2349ea8b --- /dev/null +++ b/openstack/clustering/v1/policies/urls.go @@ -0,0 +1,12 @@ +package policies + +import "github.com/gophercloud/gophercloud" + +const ( + apiVersion = "v1" + apiName = "policies" +) + +func policyListURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL(apiVersion, apiName) +} From 407d3c61d7efbe73af3fc82489b8be46f09b8a7e Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 4 Apr 2018 20:10:29 +0000 Subject: [PATCH 094/120] Acc tests: Fix TestSecGroupsAddGroupToServer --- acceptance/openstack/compute/v2/secgroup_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/acceptance/openstack/compute/v2/secgroup_test.go b/acceptance/openstack/compute/v2/secgroup_test.go index 0d69a18fd0..6f3028432a 100644 --- a/acceptance/openstack/compute/v2/secgroup_test.go +++ b/acceptance/openstack/compute/v2/secgroup_test.go @@ -101,10 +101,6 @@ func TestSecGroupsAddGroupToServer(t *testing.T) { th.AssertNoErr(t, err) defer DeleteSecurityGroupRule(t, client, rule.ID) - server, err = CreateServer(t, client) - th.AssertNoErr(t, err) - defer DeleteServer(t, client, server) - t.Logf("Adding group %s to server %s", securityGroup.ID, server.ID) err = secgroups.AddServer(client, server.ID, securityGroup.Name).ExtractErr() th.AssertNoErr(t, err) From 54a8a6b7fcccae909f78aee14f7ecddf70f775e7 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Wed, 4 Apr 2018 18:07:26 -0600 Subject: [PATCH 095/120] Fix go tip gofmt errors (#892) * Fix go tip gofmt errors * Modify format script to ignore certain files * Update Travis to test with Go 1.10 * Fix yaml parsing in .travis.yml --- .travis.yml | 4 +- .../extensions/quotasets/testing/fixtures.go | 2 +- .../ikepolicies/testing/requests_test.go | 38 +++++++++---------- script/format | 26 +++++++++++-- 4 files changed, 44 insertions(+), 26 deletions(-) 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/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/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go index 9c3b08f120..b3ec548da7 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/testing/requests_test.go @@ -74,13 +74,13 @@ func TestCreate(t *testing.T) { 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", + 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) } @@ -130,12 +130,12 @@ func TestGet(t *testing.T) { TenantID: "9145d91459d248b1b02fdaca97c6a75d", ProjectID: "9145d91459d248b1b02fdaca97c6a75d", Phase1NegotiationMode: "main", - PFS: "Group5", - EncryptionAlgorithm: "aes-128", - Description: "IKE policy", - Name: "policy", - ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828", - Lifetime: expectedLifetime, + PFS: "Group5", + EncryptionAlgorithm: "aes-128", + Description: "IKE policy", + Name: "policy", + ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828", + Lifetime: expectedLifetime, } th.AssertDeepEquals(t, expected, *actual) } @@ -208,12 +208,12 @@ func TestList(t *testing.T) { TenantID: "9145d91459d248b1b02fdaca97c6a75d", ProjectID: "9145d91459d248b1b02fdaca97c6a75d", Phase1NegotiationMode: "main", - PFS: "Group5", - EncryptionAlgorithm: "aes-128", - Description: "IKE policy", - Name: "policy", - ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828", - Lifetime: expectedLifetime, + PFS: "Group5", + EncryptionAlgorithm: "aes-128", + Description: "IKE policy", + Name: "policy", + ID: "5c561d9d-eaea-45f6-ae3e-08d1a7080828", + Lifetime: expectedLifetime, }, } 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 From 4b59796743b1afab8785ff215be229b3a81c5f9e Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Wed, 4 Apr 2018 19:14:51 +1200 Subject: [PATCH 096/120] LBaaS v2 l7 policy support - part 2: list l7policy For #832 Neutron-LBaaS l7 policy list API implementation: https://github.com/openstack/neutron-lbaas/blob/8e8a6c47c7d38fa7b61850ff9ea2cf130718ded3/neutron_lbaas/services/loadbalancer/plugin.py#L967 Octavia l7 policy list API implementation: https://github.com/openstack/octavia/blob/54a4cf00cf304b108013ca4487c138c13f30983d/octavia/api/v2/controllers/l7policy.py#L61 --- .../loadbalancer/v2/l7policies_test.go | 32 +++++++++ openstack/loadbalancer/v2/l7policies/doc.go | 16 +++++ .../loadbalancer/v2/l7policies/requests.go | 49 ++++++++++++++ .../loadbalancer/v2/l7policies/results.go | 38 +++++++++++ .../v2/l7policies/testing/fixtures.go | 66 +++++++++++++++++++ .../v2/l7policies/testing/requests_test.go | 44 +++++++++++++ 6 files changed, 245 insertions(+) create mode 100644 acceptance/openstack/loadbalancer/v2/l7policies_test.go 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/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go index 7d9418f355..98e074b876 100644 --- a/openstack/loadbalancer/v2/l7policies/doc.go +++ b/openstack/loadbalancer/v2/l7policies/doc.go @@ -12,5 +12,21 @@ Example to Create a L7Policy if err != nil { panic(err) } + +Example to List L7Policies + listOpts := l7policies.ListOpts{ + ListenerID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + allPages, err := l7policies.List(networkClient, 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) + } */ package l7policies diff --git a/openstack/loadbalancer/v2/l7policies/requests.go b/openstack/loadbalancer/v2/l7policies/requests.go index 1d59b4a601..48a197b146 100644 --- a/openstack/loadbalancer/v2/l7policies/requests.go +++ b/openstack/loadbalancer/v2/l7policies/requests.go @@ -2,6 +2,7 @@ package l7policies import ( "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" ) // CreateOptsBuilder allows extensions to add additional parameters to the @@ -82,3 +83,51 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul _, 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"` + TenantID string `q:"tenant_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}} + }) +} diff --git a/openstack/loadbalancer/v2/l7policies/results.go b/openstack/loadbalancer/v2/l7policies/results.go index 827fd59f69..a9f3f1405e 100644 --- a/openstack/loadbalancer/v2/l7policies/results.go +++ b/openstack/loadbalancer/v2/l7policies/results.go @@ -2,6 +2,7 @@ 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 @@ -91,3 +92,40 @@ func (r commonResult) Extract() (*L7Policy, error) { 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go index f64dde2e66..35cd2129cb 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go +++ b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go @@ -43,6 +43,19 @@ var ( 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: "", + TenantID: "c1f7910086964990847dc6c8b128f63c", + RedirectPoolID: "bac433c6-5bea-4311-80da-bd1cd90fbd25", + RedirectURL: "", + AdminStateUp: true, + Rules: []l7policies.Rule{}, + } ) // HandleL7PolicyCreationSuccessfully sets up the test server to respond to a l7policy creation request @@ -65,3 +78,56 @@ func HandleL7PolicyCreationSuccessfully(t *testing.T, response string) { 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": [], + "tenant_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": [], + "tenant_id": "c1f7910086964990847dc6c8b128f63c", + "listener_id": "be3138a3-5cf7-4513-a4c2-bb137e668bab", + "action": "REDIRECT_TO_POOL", + "position": 1, + "id": "964f4ba4-f6cd-405c-bebd-639460af7231", + "name": "redirect-pool" + } + ] +} +` + +// 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) + } + }) +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go index fe0278c197..4dc64a638c 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go +++ b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -40,3 +41,46 @@ func TestRequiredL7PolicyCreateOpts(t *testing.T) { 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]) +} From 66b52f4d8afd5e54a01a58311dbe37273ab7dd74 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Thu, 5 Apr 2018 17:55:14 +1200 Subject: [PATCH 097/120] LBaaS v2 l7 policy support - part 3: get l7policy For #832 Neutron-LBaaS l7 policy get API implementation: https://github.com/openstack/neutron-lbaas/blob/8e8a6c47c7d38fa7b61850ff9ea2cf130718ded3/neutron_lbaas/services/loadbalancer/plugin.py#L971 Octavia l7 policy get API implementation: https://github.com/openstack/octavia/blob/54a4cf00cf304b108013ca4487c138c13f30983d/octavia/api/v2/controllers/l7policy.py#L47 --- .../loadbalancer/v2/loadbalancers_test.go | 10 +++++++++- openstack/loadbalancer/v2/l7policies/doc.go | 14 ++++++++++++-- openstack/loadbalancer/v2/l7policies/requests.go | 6 ++++++ openstack/loadbalancer/v2/l7policies/results.go | 6 ++++++ .../loadbalancer/v2/l7policies/testing/fixtures.go | 11 +++++++++++ .../v2/l7policies/testing/requests_test.go | 14 ++++++++++++++ openstack/loadbalancer/v2/l7policies/urls.go | 4 ++++ 7 files changed, 62 insertions(+), 3 deletions(-) diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go index 866f9b852a..ee6885bb86 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go @@ -8,6 +8,7 @@ import ( "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" @@ -101,11 +102,18 @@ func TestLoadbalancersCRUD(t *testing.T) { tools.PrintResource(t, newListener) // L7 policy - _, err = CreateL7Policy(t, lbClient, listener, lb) + policy, err := CreateL7Policy(t, lbClient, listener, lb) if err != nil { t.Fatalf("Unable to create l7 policy: %v", err) } + newPolicy, err := l7policies.Get(lbClient, policy.ID).Extract() + if err != nil { + t.Fatalf("Unable to get l7 policy: %v", err) + } + + tools.PrintResource(t, newPolicy) + // Pool pool, err := CreatePool(t, lbClient, lb) if err != nil { diff --git a/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go index 98e074b876..c1fe28eccf 100644 --- a/openstack/loadbalancer/v2/l7policies/doc.go +++ b/openstack/loadbalancer/v2/l7policies/doc.go @@ -1,23 +1,26 @@ /* 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(networkClient, createOpts).Extract() + 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(networkClient, listOpts).AllPages() + allPages, err := l7policies.List(lbClient, listOpts).AllPages() if err != nil { panic(err) } @@ -28,5 +31,12 @@ Example to List L7Policies 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) + } */ package l7policies diff --git a/openstack/loadbalancer/v2/l7policies/requests.go b/openstack/loadbalancer/v2/l7policies/requests.go index 48a197b146..95853aac10 100644 --- a/openstack/loadbalancer/v2/l7policies/requests.go +++ b/openstack/loadbalancer/v2/l7policies/requests.go @@ -131,3 +131,9 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/results.go b/openstack/loadbalancer/v2/l7policies/results.go index a9f3f1405e..566185e17c 100644 --- a/openstack/loadbalancer/v2/l7policies/results.go +++ b/openstack/loadbalancer/v2/l7policies/results.go @@ -129,3 +129,9 @@ func ExtractL7Policies(r pagination.Page) ([]L7Policy, error) { 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go index 35cd2129cb..8b5a2e022f 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go +++ b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go @@ -131,3 +131,14 @@ func HandleL7PolicyListSuccessfully(t *testing.T) { } }) } + +// 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) + }) +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go index 4dc64a638c..b13a3fb138 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go +++ b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go @@ -84,3 +84,17 @@ func TestListAllL7Policies(t *testing.T) { 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) +} diff --git a/openstack/loadbalancer/v2/l7policies/urls.go b/openstack/loadbalancer/v2/l7policies/urls.go index fd898e9b06..7a87a187f9 100644 --- a/openstack/loadbalancer/v2/l7policies/urls.go +++ b/openstack/loadbalancer/v2/l7policies/urls.go @@ -10,3 +10,7 @@ const ( 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) +} From 04ada7564b59ad6efffa284c370c805c531fcb5d Mon Sep 17 00:00:00 2001 From: Jon Perritt Date: Fri, 6 Apr 2018 08:15:17 -0500 Subject: [PATCH 098/120] add MoreHeaders field to ServiceClient --- service_client.go | 17 +++++++++++++++++ testing/service_client_test.go | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) 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/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") +} From 4f9015244d4a6d3267a602a2786d758b1bed156d Mon Sep 17 00:00:00 2001 From: Jon Perritt Date: Fri, 6 Apr 2018 09:24:46 -0500 Subject: [PATCH 099/120] simulate previous token existing in reauth test --- testing/provider_client_test.go | 3 +++ 1 file changed, 3 insertions(+) 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) From b4ed2b8f377486b0339992fdf8f6ee22278adae4 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Sat, 7 Apr 2018 07:31:49 +1200 Subject: [PATCH 100/120] LBaaS v2 l7 policy support - part 4: delete l7policy (#839) * LBaaS v2 l7 policy support - part 3: get l7policy For #832 Neutron-LBaaS l7 policy get API implementation: https://github.com/openstack/neutron-lbaas/blob/8e8a6c47c7d38fa7b61850ff9ea2cf130718ded3/neutron_lbaas/services/loadbalancer/plugin.py#L971 Octavia l7 policy get API implementation: https://github.com/openstack/octavia/blob/54a4cf00cf304b108013ca4487c138c13f30983d/octavia/api/v2/controllers/l7policy.py#L47 * LBaaS v2 l7 policy support - part 4: delete l7policy For #832 Neutron-LBaaS l7 policy delete API implementation: https://github.com/openstack/neutron-lbaas/blob/8e8a6c47c7d38fa7b61850ff9ea2cf130718ded3/neutron_lbaas/services/loadbalancer/plugin.py#L954 Octavia l7 policy delete API implementation: https://github.com/openstack/octavia/blob/54a4cf00cf304b108013ca4487c138c13f30983d/octavia/api/v2/controllers/l7policy.py#L253 * Add missing parameter --- .../openstack/loadbalancer/v2/loadbalancer.go | 17 +++++++++++++++++ .../loadbalancer/v2/loadbalancers_test.go | 1 + openstack/loadbalancer/v2/l7policies/doc.go | 8 ++++++++ .../loadbalancer/v2/l7policies/requests.go | 6 ++++++ openstack/loadbalancer/v2/l7policies/results.go | 6 ++++++ .../v2/l7policies/testing/fixtures.go | 10 ++++++++++ .../v2/l7policies/testing/requests_test.go | 9 +++++++++ 7 files changed, 57 insertions(+) diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancer.go b/acceptance/openstack/loadbalancer/v2/loadbalancer.go index ba1b64fb7e..10bbcc28c1 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancer.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancer.go @@ -202,6 +202,23 @@ func CreateL7Policy(t *testing.T, client *gophercloud.ServiceClient, listener *l return policy, 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. diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go index ee6885bb86..02d4136a5d 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go @@ -106,6 +106,7 @@ func TestLoadbalancersCRUD(t *testing.T) { if err != nil { t.Fatalf("Unable to create l7 policy: %v", err) } + defer DeleteL7Policy(t, lbClient, lb.ID, policy.ID) newPolicy, err := l7policies.Get(lbClient, policy.ID).Extract() if err != nil { diff --git a/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go index c1fe28eccf..9c9a40471b 100644 --- a/openstack/loadbalancer/v2/l7policies/doc.go +++ b/openstack/loadbalancer/v2/l7policies/doc.go @@ -38,5 +38,13 @@ Example to Get a L7Policy 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) + } */ package l7policies diff --git a/openstack/loadbalancer/v2/l7policies/requests.go b/openstack/loadbalancer/v2/l7policies/requests.go index 95853aac10..c565da5f67 100644 --- a/openstack/loadbalancer/v2/l7policies/requests.go +++ b/openstack/loadbalancer/v2/l7policies/requests.go @@ -137,3 +137,9 @@ 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/results.go b/openstack/loadbalancer/v2/l7policies/results.go index 566185e17c..8ab9493baf 100644 --- a/openstack/loadbalancer/v2/l7policies/results.go +++ b/openstack/loadbalancer/v2/l7policies/results.go @@ -135,3 +135,9 @@ func ExtractL7Policies(r pagination.Page) ([]L7Policy, error) { 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go index 8b5a2e022f..3c9bf6cc74 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go +++ b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go @@ -142,3 +142,13 @@ func HandleL7PolicyGetSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go index b13a3fb138..e350bc8541 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go +++ b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go @@ -98,3 +98,12 @@ func TestGetL7Policy(t *testing.T) { 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) +} From 0ff48d79bb747dcbb1c9899592b31ab82892079d Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Fri, 6 Apr 2018 21:14:32 -0600 Subject: [PATCH 101/120] Add Scope to AuthOptions (#896) * Add Scope to AuthOptions This commit adds a Scope field to the base AuthOptions struct. This allows an explicit scope to be passed to AuthOptions which will enable detailed scoping configurations. * Use new() --- auth_options.go | 75 +++++++++++++----------- openstack/identity/v3/tokens/requests.go | 71 +++------------------- 2 files changed, 49 insertions(+), 97 deletions(-) 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/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 { From 94190ef1db5858af6bfd9c302c2e295227269cc6 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 7 Apr 2018 04:45:00 +0000 Subject: [PATCH 102/120] Acc Tests: Update Identity v2 Tests --- acceptance/clients/conditions.go | 8 +++ .../openstack/identity/v2/extension_test.go | 33 +++++----- acceptance/openstack/identity/v2/identity.go | 8 ++- acceptance/openstack/identity/v2/role_test.go | 61 +++++++++---------- .../openstack/identity/v2/tenant_test.go | 44 ++++++------- .../openstack/identity/v2/token_test.go | 39 +++++------- acceptance/openstack/identity/v2/user_test.go | 42 ++++++------- 7 files changed, 122 insertions(+), 113 deletions(-) diff --git a/acceptance/clients/conditions.go b/acceptance/clients/conditions.go index 9c62c29c11..a858468669 100644 --- a/acceptance/clients/conditions.go +++ b/acceptance/clients/conditions.go @@ -20,6 +20,14 @@ func RequireGuestAgent(t *testing.T) { } } +// 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) { 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) } From 1eccea2fadf173e8022ad57ec0344a8aef0fe139 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sat, 7 Apr 2018 05:31:40 +0000 Subject: [PATCH 103/120] Acc Tests: Update Identity v3 Tests --- .../openstack/identity/v3/domains_test.go | 65 ++-- .../openstack/identity/v3/endpoint_test.go | 54 ++- .../openstack/identity/v3/groups_test.go | 27 +- acceptance/openstack/identity/v3/identity.go | 15 + .../openstack/identity/v3/projects_test.go | 88 +++-- .../openstack/identity/v3/regions_test.go | 49 ++- .../openstack/identity/v3/roles_test.go | 269 +++++++-------- .../openstack/identity/v3/service_test.go | 37 +- .../openstack/identity/v3/token_test.go | 33 +- .../openstack/identity/v3/users_test.go | 315 ++++++------------ 10 files changed, 413 insertions(+), 539 deletions(-) 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 68f5a351d4..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", err) - } + 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", err) - } + 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", err) - } + 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", err) - } + 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 326b4ad034..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", 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) @@ -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", err) - } + 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", err) - } + 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", err) - } + 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..0955e3415f 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,256 @@ 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 TestRolesAssignToUserOnProject(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) - 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 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 ed9d3855df..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", err) - } + 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 b426116f4d..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", err) - } + 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 1eb15456d6..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,39 +111,27 @@ 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 TestUserChangePassword(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 := users.CreateOpts{ Password: "secretsecret", DomainID: "default", - Options: map[users.Option]interface{}{ - users.IgnorePasswordExpiry: true, - users.MultiFactorAuthRules: []interface{}{ - []string{"password", "totp"}, - []string{"password", "custom-auth-method"}, - }, - }, - Extra: map[string]interface{}{ - "email": "jsmith@example.com", - }, } 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) @@ -158,69 +142,22 @@ func TestUserChangePassword(t *testing.T) { Password: "new_secretsecret", } err = users.ChangePassword(client, user.ID, changePasswordOpts).ExtractErr() - if err != nil { - t.Fatalf("Unable to change password for user: %v", err) - } + th.AssertNoErr(t, err) } -func TestUsersListGroups(t *testing.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) - } +func TestUsersGroups(t *testing.T) { + clients.RequireAdmin(t) - allUsers, err := users.ExtractUsers(allUserPages) - if err != nil { - t.Fatalf("Unable to extract users: %v", err) - } - - user := allUsers[0] - - allGroupPages, err := users.ListGroups(client, user.ID).AllPages() - if err != nil { - t.Fatalf("Unable to list groups: %v", err) - } - - allGroups, err := groups.ExtractGroups(allGroupPages) - if err != nil { - t.Fatalf("Unable to extract groups: %v", err) - } - - for _, group := range allGroups { - tools.PrintResource(t, group) - tools.PrintResource(t, group.Extra) - } -} - -func TestUsersAddToGroup(t *testing.T) { client, err := clients.NewIdentityV3Client() - if err != nil { - t.Fatalf("Unable to obtain an identity client: %v", err) - } + th.AssertNoErr(t, err) createOpts := users.CreateOpts{ Password: "foobar", DomainID: "default", - Options: map[users.Option]interface{}{ - users.IgnorePasswordExpiry: true, - users.MultiFactorAuthRules: []interface{}{ - []string{"password", "totp"}, - []string{"password", "custom-auth-method"}, - }, - }, - Extra: map[string]interface{}{ - "email": "jsmith@example.com", - }, } 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) @@ -229,147 +166,117 @@ func TestUsersAddToGroup(t *testing.T) { createGroupOpts := groups.CreateOpts{ Name: "testgroup", DomainID: "default", - Extra: map[string]interface{}{ - "email": "testgroup@example.com", - }, } // Create Group in the default domain group, err := CreateGroup(t, client, &createGroupOpts) - 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) tools.PrintResource(t, group.Extra) err = users.AddToGroup(client, group.ID, user.ID).ExtractErr() - if err != nil { - t.Fatalf("Unable to add user to group: %v", err) - } -} + th.AssertNoErr(t, err) -func TestUsersRemoveFromGroup(t *testing.T) { - client, err := clients.NewIdentityV3Client() - if err != nil { - t.Fatalf("Unable to obtain an identity client: %v", err) - } + allGroupPages, err := users.ListGroups(client, user.ID).AllPages() + th.AssertNoErr(t, err) - createOpts := users.CreateOpts{ - Password: "foobar", - DomainID: "default", - Options: map[users.Option]interface{}{ - users.IgnorePasswordExpiry: true, - users.MultiFactorAuthRules: []interface{}{ - []string{"password", "totp"}, - []string{"password", "custom-auth-method"}, - }, - }, - Extra: map[string]interface{}{ - "email": "jsmith@example.com", - }, - } + allGroups, err := groups.ExtractGroups(allGroupPages) + th.AssertNoErr(t, err) - user, err := CreateUser(t, client, &createOpts) - if err != nil { - t.Fatalf("Unable to create user: %v", err) + var found bool + for _, g := range allGroups { + tools.PrintResource(t, g) + tools.PrintResource(t, g.Extra) + + if g.ID == group.ID { + found = true + } } - defer DeleteUser(t, client, user.ID) - tools.PrintResource(t, user) - tools.PrintResource(t, user.Extra) + th.AssertEquals(t, found, true) - createGroupOpts := groups.CreateOpts{ - Name: "testgroup", - DomainID: "default", - Extra: map[string]interface{}{ - "email": "testgroup@example.com", - }, - } + found = false + allUserPages, err := users.ListInGroup(client, group.ID, nil).AllPages() + th.AssertNoErr(t, err) - // Create Group in the default domain - group, err := CreateGroup(t, client, &createGroupOpts) - if err != nil { - t.Fatalf("Unable to create group: %v", err) - } - defer DeleteGroup(t, client, group.ID) + allUsers, err := users.ExtractUsers(allUserPages) + th.AssertNoErr(t, err) - tools.PrintResource(t, group) - tools.PrintResource(t, group.Extra) + for _, u := range allUsers { + tools.PrintResource(t, user) + tools.PrintResource(t, user.Extra) - err = users.AddToGroup(client, group.ID, user.ID).ExtractErr() - if err != nil { - t.Fatalf("Unable to add user to group: %v", err) + if u.ID == user.ID { + found = true + } } + th.AssertEquals(t, found, true) + err = users.RemoveFromGroup(client, group.ID, user.ID).ExtractErr() - if err != nil { - t.Fatalf("Unable to remove user from group: %v", err) + 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() - if err != nil { - t.Fatalf("Unable to obtain an identity client: %v", err) - } + th.AssertNoErr(t, 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) - } + th.AssertNoErr(t, err) user := allUsers[0] allProjectPages, err := users.ListProjects(client, user.ID).AllPages() - if err != nil { - t.Fatalf("Unable to list projects: %v", err) - } + th.AssertNoErr(t, err) allProjects, err := projects.ExtractProjects(allProjectPages) - if err != nil { - t.Fatalf("Unable to extract projects: %v", err) - } + th.AssertNoErr(t, err) for _, project := range allProjects { tools.PrintResource(t, project) } } - -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) - } - - allGroups, err := groups.ExtractGroups(allGroupPages) - if err != nil { - t.Fatalf("Unable to extract groups: %v", err) - } - - group := allGroups[0] - - allUserPages, err := users.ListInGroup(client, group.ID, nil).AllPages() - if err != nil { - t.Fatalf("Unable to list users: %v", err) - } - - allUsers, err := users.ExtractUsers(allUserPages) - if err != nil { - t.Fatalf("Unable to extract users: %v", err) - } - - for _, user := range allUsers { - tools.PrintResource(t, user) - tools.PrintResource(t, user.Extra) - } -} From 8873eb140010928dfbfb1e4a7f9cadab94de3ce2 Mon Sep 17 00:00:00 2001 From: Jude C Date: Sat, 7 Apr 2018 14:13:36 -0700 Subject: [PATCH 104/120] Messaging Queue Create (#846) * Add NewMessagingV2 client * Add create function for queues * Add unit tests for create queues * Remove queueName from function signature & fix createOpts metadata * Fix unit tests to use Extra and proper function signature * Fix documentation * Update urls to use contants * Fix Extra comments * Fix formatting * Fix doc formatting * Remove Client-ID from create function * Add clientID parameter to NewMessagingV2 --- openstack/client.go | 8 ++ openstack/messaging/v2/queues/doc.go | 24 ++++++ openstack/messaging/v2/queues/requests.go | 80 +++++++++++++++++++ openstack/messaging/v2/queues/results.go | 10 +++ openstack/messaging/v2/queues/testing/doc.go | 2 + .../messaging/v2/queues/testing/fixtures.go | 37 +++++++++ .../v2/queues/testing/requests_test.go | 29 +++++++ openstack/messaging/v2/queues/urls.go | 10 +++ 8 files changed, 200 insertions(+) create mode 100644 openstack/messaging/v2/queues/doc.go create mode 100644 openstack/messaging/v2/queues/requests.go create mode 100644 openstack/messaging/v2/queues/results.go create mode 100644 openstack/messaging/v2/queues/testing/doc.go create mode 100644 openstack/messaging/v2/queues/testing/fixtures.go create mode 100644 openstack/messaging/v2/queues/testing/requests_test.go create mode 100644 openstack/messaging/v2/queues/urls.go diff --git a/openstack/client.go b/openstack/client.go index 85705d2126..b9e187ddaa 100644 --- a/openstack/client.go +++ b/openstack/client.go @@ -400,3 +400,11 @@ func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi 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/messaging/v2/queues/doc.go b/openstack/messaging/v2/queues/doc.go new file mode 100644 index 0000000000..7f89a01688 --- /dev/null +++ b/openstack/messaging/v2/queues/doc.go @@ -0,0 +1,24 @@ +/* +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 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) + } +*/ +package queues diff --git a/openstack/messaging/v2/queues/requests.go b/openstack/messaging/v2/queues/requests.go new file mode 100644 index 0000000000..02551907f0 --- /dev/null +++ b/openstack/messaging/v2/queues/requests.go @@ -0,0 +1,80 @@ +package queues + +import ( + "github.com/gophercloud/gophercloud" +) + +// 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 +} diff --git a/openstack/messaging/v2/queues/results.go b/openstack/messaging/v2/queues/results.go new file mode 100644 index 0000000000..3273e82d96 --- /dev/null +++ b/openstack/messaging/v2/queues/results.go @@ -0,0 +1,10 @@ +package queues + +import ( + "github.com/gophercloud/gophercloud" +) + +// CreateResult is the response of a Create operation. +type CreateResult struct { + gophercloud.ErrResult +} 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..b118048f93 --- /dev/null +++ b/openstack/messaging/v2/queues/testing/fixtures.go @@ -0,0 +1,37 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + 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." +}` + +// 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) + }) +} 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..37c1f899ab --- /dev/null +++ b/openstack/messaging/v2/queues/testing/requests_test.go @@ -0,0 +1,29 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/messaging/v2/queues" + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +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) +} diff --git a/openstack/messaging/v2/queues/urls.go b/openstack/messaging/v2/queues/urls.go new file mode 100644 index 0000000000..b29fb7a9d7 --- /dev/null +++ b/openstack/messaging/v2/queues/urls.go @@ -0,0 +1,10 @@ +package queues + +import "github.com/gophercloud/gophercloud" + +const ApiVersion = "v2" +const ApiName = "queues" + +func createURL(client *gophercloud.ServiceClient, queueName string) string { + return client.ServiceURL(ApiVersion, ApiName, queueName) +} From 4e791fb6e668d1a194eeb627af5e56d46431b9b4 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sun, 8 Apr 2018 02:13:57 +0000 Subject: [PATCH 105/120] Test Acc: Updating DNS Tests --- acceptance/clients/conditions.go | 8 ++ acceptance/openstack/dns/v2/dns.go | 15 +++- .../openstack/dns/v2/recordsets_test.go | 83 +++++++------------ acceptance/openstack/dns/v2/zones_test.go | 54 ++++++------ 4 files changed, 77 insertions(+), 83 deletions(-) diff --git a/acceptance/clients/conditions.go b/acceptance/clients/conditions.go index 9c62c29c11..1f7043c9ec 100644 --- a/acceptance/clients/conditions.go +++ b/acceptance/clients/conditions.go @@ -12,6 +12,14 @@ func RequireAdmin(t *testing.T) { } } +// 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) { 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") } From 71a0b3389c74fd3bb489511e4ab3a33fa7b11bd1 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sun, 8 Apr 2018 03:10:40 +0000 Subject: [PATCH 106/120] Image Service v2: Fix unmarshaling of empty images --- openstack/imageservice/v2/images/results.go | 2 +- openstack/imageservice/v2/images/testing/fixtures.go | 2 ++ .../imageservice/v2/images/testing/requests_test.go | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) 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 29757d203b..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, diff --git a/openstack/imageservice/v2/images/testing/requests_test.go b/openstack/imageservice/v2/images/testing/requests_test.go index 487247b110..ac71db86d6 100644 --- a/openstack/imageservice/v2/images/testing/requests_test.go +++ b/openstack/imageservice/v2/images/testing/requests_test.go @@ -132,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) @@ -145,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", @@ -166,6 +173,8 @@ func TestCreateImageNulls(t *testing.T) { CreatedAt: createdDate, UpdatedAt: lastUpdate, Schema: schema, + Properties: properties, + SizeBytes: sizeBytes, } th.AssertDeepEquals(t, &expectedImage, actualImage) From 38714be079fc3bd9b3b3cab12c2d1f074a09c1e3 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Sun, 8 Apr 2018 03:10:59 +0000 Subject: [PATCH 107/120] Test Acc: Updating Image Service Tests --- .../openstack/imageservice/v2/images_test.go | 93 +++++++------------ .../openstack/imageservice/v2/imageservice.go | 13 ++- 2 files changed, 43 insertions(+), 63 deletions(-) diff --git a/acceptance/openstack/imageservice/v2/images_test.go b/acceptance/openstack/imageservice/v2/images_test.go index 04926109f6..9c6cf32a06 100644 --- a/acceptance/openstack/imageservice/v2/images_test.go +++ b/acceptance/openstack/imageservice/v2/images_test.go @@ -10,13 +10,12 @@ import ( "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, @@ -40,69 +39,54 @@ 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) - listOpts := images.ListOpts{ - Limit: 1, - } + image, err := CreateEmptyImage(t, client) + th.AssertNoErr(t, err) + defer DeleteImage(t, client, image) + + listOpts := images.ListOpts{} 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) - } - - for _, image := range allImages { - tools.PrintResource(t, image) - tools.PrintResource(t, image.Properties) - } -} + th.AssertNoErr(t, err) -func TestImagesCreateDestroyEmptyImage(t *testing.T) { - client, err := clients.NewImageServiceV2Client() - if err != nil { - t.Fatalf("Unable to create an image service client: %v", err) - } + var found bool + for _, i := range allImages { + tools.PrintResource(t, i) + tools.PrintResource(t, i.Properties) - image, err := CreateEmptyImage(t, client) - if err != nil { - t.Fatalf("Unable to create empty image: %v", err) + if i.Name == image.Name { + found = true + } } - defer DeleteImage(t, client, image) - - tools.PrintResource(t, image) + th.AssertEquals(t, found, true) } func TestImagesListByDate(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) date := time.Date(2014, 1, 1, 1, 1, 1, 0, time.UTC) listOpts := images.ListOpts{ Limit: 1, - CreatedAt: &images.ImageDateQuery{ + 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 { @@ -113,21 +97,17 @@ func TestImagesListByDate(t *testing.T) { date = time.Date(2049, 1, 1, 1, 1, 1, 0, time.UTC) listOpts = images.ListOpts{ Limit: 1, - CreatedAt: &images.ImageDateQuery{ + 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("Expected 0 images, got %d", len(allImages)) @@ -136,15 +116,10 @@ func TestImagesListByDate(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{ @@ -154,14 +129,10 @@ func TestImagesFilter(t *testing.T) { } 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") diff --git a/acceptance/openstack/imageservice/v2/imageservice.go b/acceptance/openstack/imageservice/v2/imageservice.go index 18af05ab3b..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. @@ -39,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. From e09b6c86177fba6c539e66dc198752d26c759c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E7=A5=96=E5=BB=BA?= Date: Tue, 10 Apr 2018 09:22:52 +0800 Subject: [PATCH 108/120] Identity V3: list role assignments for user on project (#885) * Identity V3 roles: add function ListAssignmentsForUserOnProject() * fix error * fix JSON output * create function ListAssignmentsOnResource() * fix unit testing * fix unit testing * fix acceptance testing --- .../openstack/identity/v3/roles_test.go | 238 ++++++++++++++++++ openstack/identity/v3/roles/doc.go | 23 ++ openstack/identity/v3/roles/requests.go | 56 +++++ .../identity/v3/roles/testing/fixtures.go | 56 +++++ .../v3/roles/testing/requests_test.go | 70 ++++++ openstack/identity/v3/roles/urls.go | 4 + 6 files changed, 447 insertions(+) diff --git a/acceptance/openstack/identity/v3/roles_test.go b/acceptance/openstack/identity/v3/roles_test.go index 0955e3415f..f442439086 100644 --- a/acceptance/openstack/identity/v3/roles_test.go +++ b/acceptance/openstack/identity/v3/roles_test.go @@ -86,6 +86,244 @@ func TestRolesCRUD(t *testing.T) { th.AssertEquals(t, newRole.Extra["description"], "updated test role description") } +func TestRoleListAssignmentForUserOnProject(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) + + 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) + 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) 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) } From ff8db8da249eb162535238257b472e32570aee27 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Tue, 10 Apr 2018 13:28:21 +1200 Subject: [PATCH 109/120] LBaaS v2 l7 policy support - part 5: update l7policy (#905) * LBaaS v2 l7 policy support - part 5: update l7policy For #832 Neutron-LBaaS l7 policy update API implementation: https://github.com/openstack/neutron-lbaas/blob/ac720b2a49720fb99e4189f93d5a83cfb295ccb3/neutron_lbaas/services/loadbalancer/plugin.py#L931 Octavia l7 policy update API implementation: https://github.com/openstack/octavia/blob/06bf5c58d5845f684fcaf933605ed112586eefc3/octavia/api/v2/controllers/l7policy.py#L204 * Allow empty string for name and description fields --- .../loadbalancer/v2/loadbalancers_test.go | 13 +++++ openstack/loadbalancer/v2/l7policies/doc.go | 11 ++++ .../loadbalancer/v2/l7policies/requests.go | 44 ++++++++++++++++ .../loadbalancer/v2/l7policies/results.go | 6 +++ .../v2/l7policies/testing/fixtures.go | 51 +++++++++++++++++++ .../v2/l7policies/testing/requests_test.go | 20 ++++++++ 6 files changed, 145 insertions(+) diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go index 02d4136a5d..080f215ef0 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go @@ -108,6 +108,19 @@ func TestLoadbalancersCRUD(t *testing.T) { } 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) diff --git a/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go index 9c9a40471b..7165dd522a 100644 --- a/openstack/loadbalancer/v2/l7policies/doc.go +++ b/openstack/loadbalancer/v2/l7policies/doc.go @@ -46,5 +46,16 @@ Example to Delete a L7Policy if err != nil { panic(err) } + +Example to Update a L7Policy + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + updateOpts := l7policies.UpdateOpts{ + Name: "new-name", + } + l7policy, err := l7policies.Update(lbClient, l7policyID, updateOpts).Extract() + if err != nil { + panic(err) + } */ package l7policies diff --git a/openstack/loadbalancer/v2/l7policies/requests.go b/openstack/loadbalancer/v2/l7policies/requests.go index c565da5f67..b64c14b863 100644 --- a/openstack/loadbalancer/v2/l7policies/requests.go +++ b/openstack/loadbalancer/v2/l7policies/requests.go @@ -143,3 +143,47 @@ 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/results.go b/openstack/loadbalancer/v2/l7policies/results.go index 8ab9493baf..f4907a3f59 100644 --- a/openstack/loadbalancer/v2/l7policies/results.go +++ b/openstack/loadbalancer/v2/l7policies/results.go @@ -141,3 +141,9 @@ type GetResult struct { 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 +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go index 3c9bf6cc74..193e728097 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go +++ b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go @@ -56,6 +56,19 @@ var ( 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", + TenantID: "e3cd678b11784734bc366148aa37580e", + RedirectPoolID: "", + RedirectURL: "http://www.new-example.com", + AdminStateUp: true, + Rules: []l7policies.Rule{}, + } ) // HandleL7PolicyCreationSuccessfully sets up the test server to respond to a l7policy creation request @@ -112,6 +125,25 @@ const L7PoliciesListBody = ` } ` +// 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, + "tenant_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) { @@ -152,3 +184,22 @@ func HandleL7PolicyDeletionSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go index e350bc8541..f9c47d665c 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go +++ b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go @@ -107,3 +107,23 @@ func TestDeleteL7Policy(t *testing.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) +} From 72c94d28d050ef138da464bbf94f73e057bd1d50 Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Mon, 9 Apr 2018 19:29:45 -0600 Subject: [PATCH 110/120] LBaaS v2: Fix doc example for updating l7policies --- openstack/loadbalancer/v2/l7policies/doc.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go index 7165dd522a..95ac5e17f0 100644 --- a/openstack/loadbalancer/v2/l7policies/doc.go +++ b/openstack/loadbalancer/v2/l7policies/doc.go @@ -50,8 +50,9 @@ Example to Delete a L7Policy Example to Update a L7Policy l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + name := "new-name" updateOpts := l7policies.UpdateOpts{ - Name: "new-name", + Name: &name, } l7policy, err := l7policies.Update(lbClient, l7policyID, updateOpts).Extract() if err != nil { From aaee2285afde23ac71e3b8ac0d20c19d0056e88d Mon Sep 17 00:00:00 2001 From: Joe Topjian Date: Tue, 10 Apr 2018 02:17:54 +0000 Subject: [PATCH 111/120] Acc Tests: Fix Server Delete There might be occasions when the API will return an actual DELETED status. This is a short window of time, but it could happen. --- acceptance/openstack/compute/v2/compute.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go index cdcbf0bb9e..570378ddfd 100644 --- a/acceptance/openstack/compute/v2/compute.go +++ b/acceptance/openstack/compute/v2/compute.go @@ -775,7 +775,9 @@ func DeleteServer(t *testing.T, client *gophercloud.ServiceClient, server *serve t.Fatalf("Error deleting server %s: %s", server.ID, err) } - t.Fatalf("Could not delete server: %s", server.ID) + // 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) } // DeleteServerGroup will delete a server group. A fatal error will occur if From 760543a1f14f91a19a9ae133321e4f6857de509c Mon Sep 17 00:00:00 2001 From: Jude C Date: Mon, 9 Apr 2018 20:15:27 -0700 Subject: [PATCH 112/120] Messaging Queue List (#848) * Add create function for queues * Add list function for queues * Add unit tests for list queues * Fix documentation * Update commonURL to use constants * Fix QueueDetails to properly handle misc key/values and update unit tests * Remove Client-ID from list function * Implement NextPageURL for list queues --- openstack/messaging/v2/queues/doc.go | 20 ++++ openstack/messaging/v2/queues/requests.go | 43 +++++++ openstack/messaging/v2/queues/results.go | 111 ++++++++++++++++++ .../messaging/v2/queues/testing/fixtures.go | 108 +++++++++++++++++ .../v2/queues/testing/requests_test.go | 25 ++++ openstack/messaging/v2/queues/urls.go | 27 ++++- 6 files changed, 333 insertions(+), 1 deletion(-) diff --git a/openstack/messaging/v2/queues/doc.go b/openstack/messaging/v2/queues/doc.go index 7f89a01688..2bf91491bf 100644 --- a/openstack/messaging/v2/queues/doc.go +++ b/openstack/messaging/v2/queues/doc.go @@ -4,6 +4,26 @@ 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{ diff --git a/openstack/messaging/v2/queues/requests.go b/openstack/messaging/v2/queues/requests.go index 02551907f0..05fe91a779 100644 --- a/openstack/messaging/v2/queues/requests.go +++ b/openstack/messaging/v2/queues/requests.go @@ -2,8 +2,51 @@ 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 { diff --git a/openstack/messaging/v2/queues/results.go b/openstack/messaging/v2/queues/results.go index 3273e82d96..55927eb31a 100644 --- a/openstack/messaging/v2/queues/results.go +++ b/openstack/messaging/v2/queues/results.go @@ -1,10 +1,121 @@ package queues import ( + "encoding/json" + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/internal" + "github.com/gophercloud/gophercloud/pagination" ) +// 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 } + +// 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"` +} + +// 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/fixtures.go b/openstack/messaging/v2/queues/testing/fixtures.go index b118048f93..58cbe60a93 100644 --- a/openstack/messaging/v2/queues/testing/fixtures.go +++ b/openstack/messaging/v2/queues/testing/fixtures.go @@ -5,6 +5,7 @@ import ( "net/http" "testing" + "github.com/gophercloud/gophercloud/openstack/messaging/v2/queues" th "github.com/gophercloud/gophercloud/testhelper" fake "github.com/gophercloud/gophercloud/testhelper/client" ) @@ -24,6 +25,113 @@ const CreateQueueRequest = ` "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" + } + ] +}` + +// 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}} + +// 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), diff --git a/openstack/messaging/v2/queues/testing/requests_test.go b/openstack/messaging/v2/queues/testing/requests_test.go index 37c1f899ab..e85a6462e5 100644 --- a/openstack/messaging/v2/queues/testing/requests_test.go +++ b/openstack/messaging/v2/queues/testing/requests_test.go @@ -4,10 +4,35 @@ 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() diff --git a/openstack/messaging/v2/queues/urls.go b/openstack/messaging/v2/queues/urls.go index b29fb7a9d7..8be0d8a7a1 100644 --- a/openstack/messaging/v2/queues/urls.go +++ b/openstack/messaging/v2/queues/urls.go @@ -1,10 +1,35 @@ package queues -import "github.com/gophercloud/gophercloud" +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 +} From 8bbbdbd42a0315234ae3cc2e3136a5c50794edad Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Tue, 10 Apr 2018 18:52:49 -0700 Subject: [PATCH 113/120] Clustering Policy Create Implementation (#895) * Clustering Policy Create Implementation * Fix review comments * Created Spec type * Changed Policy unmarshal to handle both time with and without Z * Added custom unmarshal to handle Version as either string or float * Change UpdatedAt value in fixtures to test different code path * fix variable name * Fix review comments - Removed custom Version type and unmarshal function - Added Version assignment to policy unmarshal with custom logic * added custom unmarshal for spec --- .../openstack/clustering/v1/policies_test.go | 34 ++++++ openstack/clustering/v1/policies/doc.go | 26 +++++ openstack/clustering/v1/policies/requests.go | 29 +++++ openstack/clustering/v1/policies/results.go | 81 ++++++++++++- .../v1/policies/testing/fixtures.go | 108 +++++++++++++++--- .../v1/policies/testing/requests_test.go | 19 +++ openstack/clustering/v1/policies/urls.go | 4 + 7 files changed, 278 insertions(+), 23 deletions(-) diff --git a/acceptance/openstack/clustering/v1/policies_test.go b/acceptance/openstack/clustering/v1/policies_test.go index b3343c3ac3..6fd9cffa35 100644 --- a/acceptance/openstack/clustering/v1/policies_test.go +++ b/acceptance/openstack/clustering/v1/policies_test.go @@ -34,3 +34,37 @@ func TestPolicyList(t *testing.T) { } } } + +func TestPolicyCreate(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) + + 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/openstack/clustering/v1/policies/doc.go b/openstack/clustering/v1/policies/doc.go index 053c380551..90830c8dcf 100644 --- a/openstack/clustering/v1/policies/doc.go +++ b/openstack/clustering/v1/policies/doc.go @@ -22,5 +22,31 @@ Example to List Policies 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 index 4f50a0ce3c..cd6f6c9759 100644 --- a/openstack/clustering/v1/policies/requests.go +++ b/openstack/clustering/v1/policies/requests.go @@ -54,3 +54,32 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa 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 +} diff --git a/openstack/clustering/v1/policies/results.go b/openstack/clustering/v1/policies/results.go index 044e58d99e..2e82deb53c 100644 --- a/openstack/clustering/v1/policies/results.go +++ b/openstack/clustering/v1/policies/results.go @@ -2,6 +2,8 @@ package policies import ( "encoding/json" + "fmt" + "strconv" "time" "github.com/gophercloud/gophercloud" @@ -16,12 +18,19 @@ type Policy struct { ID string `json:"id"` Name string `json:"name"` Project string `json:"project"` - Spec map[string]interface{} `json:"spec"` + 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 { @@ -54,12 +63,14 @@ func (r PolicyPage) LastMarker() (string, error) { 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 gophercloud.JSONRFC3339MilliNoZ `json:"created_at,omitempty"` - UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` } err := json.Unmarshal(b, &s) if err != nil { @@ -67,8 +78,68 @@ func (r *Policy) UnmarshalJSON(b []byte) error { } *r = Policy(s.tmp) - r.CreatedAt = time.Time(s.CreatedAt) - r.UpdatedAt = time.Time(s.UpdatedAt) + 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 policyResult) Extract() (*Policy, error) { + var s struct { + Policy *Policy `json:"policy"` + } + err := r.ExtractInto(&s) + + return s.Policy, err +} + +type CreateResult struct { + policyResult +} diff --git a/openstack/clustering/v1/policies/testing/fixtures.go b/openstack/clustering/v1/policies/testing/fixtures.go index 30db27b88d..6f2bad66f9 100644 --- a/openstack/clustering/v1/policies/testing/fixtures.go +++ b/openstack/clustering/v1/policies/testing/fixtures.go @@ -33,7 +33,7 @@ const PolicyListBody1 = ` "version": 1 }, "type": "senlin.policy.deletion-1.0", - "updated_at": null, + "updated_at": "2018-04-02T00:19:12Z", "user": "fe43e41739154b72818565e0d2580819" } ] @@ -59,20 +59,53 @@ const PolicyListBody2 = ` "reduce_desired_capacity": false }, "type": "senlin.policy.deletion", - "version": 1 + "version": "1.0" }, "type": "senlin.policy.deletion-1.0", - "updated_at": null, + "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" + } +} +` + var ( - ExpectedPolicyCreatedAt1, _ = time.Parse(time.RFC3339, "2018-04-02T21:43:30.000000Z") - ExpectedPolicyCreatedAt2, _ = time.Parse(time.RFC3339, "2018-04-02T22:29:36.000000Z") - ZeroTime, _ = time.Parse(time.RFC3339, "1-01-01T00:00:00.000000Z") + 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") ExpectedPolicies = [][]policies.Policy{ { @@ -84,20 +117,20 @@ var ( Name: "delpol", Project: "018cd0909fb44cd5bc9b7a3cd664920e", - Spec: map[string]interface{}{ - "description": "A policy for choosing victim node(s) from a cluster for deletion.", - "properties": map[string]interface{}{ + 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": float64(1), + Type: "senlin.policy.deletion", + Version: "1.0", }, Type: "senlin.policy.deletion-1.0", User: "fe43e41739154b72818565e0d2580819", - UpdatedAt: ZeroTime, + UpdatedAt: ExpectedPolicyUpdatedAt1, }, }, { @@ -109,23 +142,50 @@ var ( Name: "delpol2", Project: "018cd0909fb44cd5bc9b7a3cd664920e", - Spec: map[string]interface{}{ - "description": "A policy for choosing victim node(s) from a cluster for deletion.", - "properties": map[string]interface{}{ + 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": float64(1), + Type: "senlin.policy.deletion", + Version: "1.0", }, Type: "senlin.policy.deletion-1.0", User: "fe43e41739154b72818565e0d2580819", - UpdatedAt: ZeroTime, + 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) { @@ -150,3 +210,15 @@ func HandlePolicyList(t *testing.T) { } }) } + +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) + }) +} diff --git a/openstack/clustering/v1/policies/testing/requests_test.go b/openstack/clustering/v1/policies/testing/requests_test.go index 0f110a52f0..17fef6ce76 100644 --- a/openstack/clustering/v1/policies/testing/requests_test.go +++ b/openstack/clustering/v1/policies/testing/requests_test.go @@ -39,3 +39,22 @@ func TestListPolicies(t *testing.T) { 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) +} diff --git a/openstack/clustering/v1/policies/urls.go b/openstack/clustering/v1/policies/urls.go index 9e2349ea8b..e0d0c8de7f 100644 --- a/openstack/clustering/v1/policies/urls.go +++ b/openstack/clustering/v1/policies/urls.go @@ -10,3 +10,7 @@ const ( func policyListURL(client *gophercloud.ServiceClient) string { return client.ServiceURL(apiVersion, apiName) } + +func policyCreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL(apiVersion, apiName) +} From 92596e817fe740fd67363d63ca4e1e9993c71dcd Mon Sep 17 00:00:00 2001 From: Jude C Date: Tue, 10 Apr 2018 18:56:20 -0700 Subject: [PATCH 114/120] Messaging Queue Update (#849) * Add create function for queues * Fix documentation * Add list function for queues * Add unit tests for list queues * Fix QueueDetails to properly handle misc key/values and update unit tests * Add update function for queues * Add unit tests for update queue * Fix formatting * Update updateURL to use constants * Fix UpdateQueueBody to use UpdateOp instead of a string * Remove Client-ID from update function * Fix unit tests to use proper path in update * Remove Client-ID referneces in docs & fix unit tests * Fix Update to use BatchUpdate --- openstack/messaging/v2/queues/doc.go | 23 +++++++- openstack/messaging/v2/queues/requests.go | 52 +++++++++++++++++++ openstack/messaging/v2/queues/results.go | 17 ++++++ .../messaging/v2/queues/testing/fixtures.go | 30 ++++++++++- .../v2/queues/testing/requests_test.go | 21 ++++++++ openstack/messaging/v2/queues/urls.go | 4 ++ 6 files changed, 145 insertions(+), 2 deletions(-) diff --git a/openstack/messaging/v2/queues/doc.go b/openstack/messaging/v2/queues/doc.go index 2bf91491bf..0b214d46cb 100644 --- a/openstack/messaging/v2/queues/doc.go +++ b/openstack/messaging/v2/queues/doc.go @@ -11,7 +11,8 @@ Example to List Queues } pager := queues.List(client, listOpts) - err = pager.EachPage(func(page pagination.Page) (bool, error) { + + err = pager.EachPage(func(page pagination.Page) (bool, error) { queues, err := queues.ExtractQueues(page) if err != nil { panic(err) @@ -40,5 +41,25 @@ Example to Create a Queue 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) + } */ package queues diff --git a/openstack/messaging/v2/queues/requests.go b/openstack/messaging/v2/queues/requests.go index 05fe91a779..5ed51d0061 100644 --- a/openstack/messaging/v2/queues/requests.go +++ b/openstack/messaging/v2/queues/requests.go @@ -121,3 +121,55 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create }) 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 +} diff --git a/openstack/messaging/v2/queues/results.go b/openstack/messaging/v2/queues/results.go index 55927eb31a..f31608e6b5 100644 --- a/openstack/messaging/v2/queues/results.go +++ b/openstack/messaging/v2/queues/results.go @@ -8,6 +8,11 @@ import ( "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 @@ -18,6 +23,11 @@ type CreateResult struct { gophercloud.ErrResult } +// UpdateResult is the response of a Update operation. +type UpdateResult struct { + commonResult +} + // Queue represents a messaging queue. type Queue struct { Href string `json:"href"` @@ -56,6 +66,13 @@ type QueueDetails struct { 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) { diff --git a/openstack/messaging/v2/queues/testing/fixtures.go b/openstack/messaging/v2/queues/testing/fixtures.go index 58cbe60a93..1010fa2f07 100644 --- a/openstack/messaging/v2/queues/testing/fixtures.go +++ b/openstack/messaging/v2/queues/testing/fixtures.go @@ -77,6 +77,22 @@ const ListQueuesResponse2 = ` ] }` +// 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" +}` + // FirstQueue is the first result in a List. var FirstQueue = queues.Queue{ Href: "/v2/queues/london", @@ -128,7 +144,6 @@ func HandleListSuccessfully(t *testing.T) { case "/v2/queues?marker=beijing": fmt.Fprint(w, `{ "queues": [] }`) } - }) } @@ -143,3 +158,16 @@ func HandleCreateSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/messaging/v2/queues/testing/requests_test.go b/openstack/messaging/v2/queues/testing/requests_test.go index e85a6462e5..408ad9a4f3 100644 --- a/openstack/messaging/v2/queues/testing/requests_test.go +++ b/openstack/messaging/v2/queues/testing/requests_test.go @@ -52,3 +52,24 @@ func TestCreate(t *testing.T) { 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) +} diff --git a/openstack/messaging/v2/queues/urls.go b/openstack/messaging/v2/queues/urls.go index 8be0d8a7a1..38238133f1 100644 --- a/openstack/messaging/v2/queues/urls.go +++ b/openstack/messaging/v2/queues/urls.go @@ -33,3 +33,7 @@ func nextPageURL(currentURL string, next string) (string, error) { } return base.ResolveReference(rel).String(), nil } + +func updateURL(client *gophercloud.ServiceClient, queueName string) string { + return client.ServiceURL(ApiVersion, ApiName, queueName) +} From dadd6cfbab32d7b31a94108835e04517925ff369 Mon Sep 17 00:00:00 2001 From: Jude Cross Date: Mon, 26 Mar 2018 22:52:38 -0700 Subject: [PATCH 115/120] Add get function for queues --- openstack/messaging/v2/queues/doc.go | 7 +++++ openstack/messaging/v2/queues/requests.go | 8 ++++++ openstack/messaging/v2/queues/results.go | 5 ++++ .../messaging/v2/queues/testing/fixtures.go | 27 +++++++++++++++++++ .../v2/queues/testing/requests_test.go | 10 +++++++ openstack/messaging/v2/queues/urls.go | 4 +++ 6 files changed, 61 insertions(+) diff --git a/openstack/messaging/v2/queues/doc.go b/openstack/messaging/v2/queues/doc.go index 0b214d46cb..8c83b13bed 100644 --- a/openstack/messaging/v2/queues/doc.go +++ b/openstack/messaging/v2/queues/doc.go @@ -61,5 +61,12 @@ Example to Update a Queue if err != nil { panic(err) } + +Example to Get a Queue + + queue, err := queues.Get(client, queueName).Extract() + if err != nil { + panic(err) + } */ package queues diff --git a/openstack/messaging/v2/queues/requests.go b/openstack/messaging/v2/queues/requests.go index 5ed51d0061..fd98530d1b 100644 --- a/openstack/messaging/v2/queues/requests.go +++ b/openstack/messaging/v2/queues/requests.go @@ -173,3 +173,11 @@ func Update(client *gophercloud.ServiceClient, queueName string, opts UpdateOpts }) 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 +} diff --git a/openstack/messaging/v2/queues/results.go b/openstack/messaging/v2/queues/results.go index f31608e6b5..84ac48acc2 100644 --- a/openstack/messaging/v2/queues/results.go +++ b/openstack/messaging/v2/queues/results.go @@ -28,6 +28,11 @@ type UpdateResult struct { commonResult } +// GetResult is the response of a Get operation. +type GetResult struct { + commonResult +} + // Queue represents a messaging queue. type Queue struct { Href string `json:"href"` diff --git a/openstack/messaging/v2/queues/testing/fixtures.go b/openstack/messaging/v2/queues/testing/fixtures.go index 1010fa2f07..bbea7b90fc 100644 --- a/openstack/messaging/v2/queues/testing/fixtures.go +++ b/openstack/messaging/v2/queues/testing/fixtures.go @@ -93,6 +93,14 @@ 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", @@ -126,6 +134,13 @@ var SecondQueue = queues.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", @@ -171,3 +186,15 @@ func HandleUpdateSuccessfully(t *testing.T) { 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) + }) +} diff --git a/openstack/messaging/v2/queues/testing/requests_test.go b/openstack/messaging/v2/queues/testing/requests_test.go index 408ad9a4f3..3a7a6f92ab 100644 --- a/openstack/messaging/v2/queues/testing/requests_test.go +++ b/openstack/messaging/v2/queues/testing/requests_test.go @@ -73,3 +73,13 @@ func TestUpdate(t *testing.T) { 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) +} diff --git a/openstack/messaging/v2/queues/urls.go b/openstack/messaging/v2/queues/urls.go index 38238133f1..76ce7efbce 100644 --- a/openstack/messaging/v2/queues/urls.go +++ b/openstack/messaging/v2/queues/urls.go @@ -37,3 +37,7 @@ func nextPageURL(currentURL string, next string) (string, error) { 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) +} From 3e7c3510d8a3ff8c7f14fadb30bfd361b2d78013 Mon Sep 17 00:00:00 2001 From: Jude Cross Date: Mon, 26 Mar 2018 22:52:38 -0700 Subject: [PATCH 116/120] Add delete function for queues --- openstack/messaging/v2/queues/doc.go | 7 +++++++ openstack/messaging/v2/queues/requests.go | 8 ++++++++ openstack/messaging/v2/queues/results.go | 6 ++++++ openstack/messaging/v2/queues/testing/fixtures.go | 10 ++++++++++ openstack/messaging/v2/queues/testing/requests_test.go | 9 +++++++++ openstack/messaging/v2/queues/urls.go | 4 ++++ 6 files changed, 44 insertions(+) diff --git a/openstack/messaging/v2/queues/doc.go b/openstack/messaging/v2/queues/doc.go index 8c83b13bed..c24e8d94d9 100644 --- a/openstack/messaging/v2/queues/doc.go +++ b/openstack/messaging/v2/queues/doc.go @@ -68,5 +68,12 @@ Example to Get a Queue 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 index fd98530d1b..327f6f887e 100644 --- a/openstack/messaging/v2/queues/requests.go +++ b/openstack/messaging/v2/queues/requests.go @@ -181,3 +181,11 @@ func Get(client *gophercloud.ServiceClient, queueName string) (r GetResult) { }) 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 index 84ac48acc2..6800ec7bce 100644 --- a/openstack/messaging/v2/queues/results.go +++ b/openstack/messaging/v2/queues/results.go @@ -33,6 +33,12 @@ 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"` diff --git a/openstack/messaging/v2/queues/testing/fixtures.go b/openstack/messaging/v2/queues/testing/fixtures.go index bbea7b90fc..eb05dae826 100644 --- a/openstack/messaging/v2/queues/testing/fixtures.go +++ b/openstack/messaging/v2/queues/testing/fixtures.go @@ -198,3 +198,13 @@ func HandleGetSuccessfully(t *testing.T) { 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 index 3a7a6f92ab..5b8252e43f 100644 --- a/openstack/messaging/v2/queues/testing/requests_test.go +++ b/openstack/messaging/v2/queues/testing/requests_test.go @@ -83,3 +83,12 @@ func TestGet(t *testing.T) { 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 index 76ce7efbce..efea4528b2 100644 --- a/openstack/messaging/v2/queues/urls.go +++ b/openstack/messaging/v2/queues/urls.go @@ -41,3 +41,7 @@ func updateURL(client *gophercloud.ServiceClient, queueName string) string { 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) +} From 7314aaab5fec29673689a4df85654d6b7b68c88f Mon Sep 17 00:00:00 2001 From: Jude Cross Date: Tue, 10 Apr 2018 15:58:29 -0700 Subject: [PATCH 117/120] Add acceptance tests for queues --- acceptance/clients/clients.go | 21 +++++ .../openstack/messaging/v2/messaging.go | 54 ++++++++++++ .../openstack/messaging/v2/queue_test.go | 86 +++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 acceptance/openstack/messaging/v2/messaging.go create mode 100644 acceptance/openstack/messaging/v2/queue_test.go diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index 93e852418a..666564ad5f 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -500,6 +500,27 @@ func NewClusteringV1Client() (*gophercloud.ServiceClient, error) { }) } +// 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 { 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 + }) +} From 76e673f99f92ac10f73aeaa5dc0401b2d742780d Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Wed, 11 Apr 2018 18:44:46 -0700 Subject: [PATCH 118/120] Clustering Policy Delete Implementation (#917) * initial commit * fix X-OpenStack-Request-ID header * fix review comments --- .../openstack/clustering/v1/policies_test.go | 4 +++- openstack/clustering/v1/policies/requests.go | 12 ++++++++++++ openstack/clustering/v1/policies/results.go | 17 ++++++++++++++++- .../clustering/v1/policies/testing/fixtures.go | 15 +++++++++++++++ .../v1/policies/testing/requests_test.go | 12 ++++++++++++ openstack/clustering/v1/policies/urls.go | 4 ++++ 6 files changed, 62 insertions(+), 2 deletions(-) diff --git a/acceptance/openstack/clustering/v1/policies_test.go b/acceptance/openstack/clustering/v1/policies_test.go index 6fd9cffa35..8fff7a5f54 100644 --- a/acceptance/openstack/clustering/v1/policies_test.go +++ b/acceptance/openstack/clustering/v1/policies_test.go @@ -35,7 +35,7 @@ func TestPolicyList(t *testing.T) { } } -func TestPolicyCreate(t *testing.T) { +func TestPolicyCreateAndDelete(t *testing.T) { client, err := clients.NewClusteringV1Client() th.AssertNoErr(t, err) @@ -57,6 +57,8 @@ func TestPolicyCreate(t *testing.T) { 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() { diff --git a/openstack/clustering/v1/policies/requests.go b/openstack/clustering/v1/policies/requests.go index cd6f6c9759..6a6f1657b7 100644 --- a/openstack/clustering/v1/policies/requests.go +++ b/openstack/clustering/v1/policies/requests.go @@ -1,6 +1,8 @@ package policies import ( + "net/http" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" ) @@ -83,3 +85,13 @@ func Create(client *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) }) 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 index 2e82deb53c..a0a0356fa9 100644 --- a/openstack/clustering/v1/policies/results.go +++ b/openstack/clustering/v1/policies/results.go @@ -131,7 +131,7 @@ type policyResult struct { gophercloud.Result } -func (r policyResult) Extract() (*Policy, error) { +func (r CreateResult) Extract() (*Policy, error) { var s struct { Policy *Policy `json:"policy"` } @@ -143,3 +143,18 @@ func (r policyResult) Extract() (*Policy, error) { 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/fixtures.go b/openstack/clustering/v1/policies/testing/fixtures.go index 6f2bad66f9..7f2aaa92bb 100644 --- a/openstack/clustering/v1/policies/testing/fixtures.go +++ b/openstack/clustering/v1/policies/testing/fixtures.go @@ -99,6 +99,8 @@ const PolicyCreateBody = ` } ` +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") @@ -107,6 +109,9 @@ var ( 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{ { { @@ -222,3 +227,13 @@ func HandlePolicyCreate(t *testing.T) { 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 index 17fef6ce76..f077a5459a 100644 --- a/openstack/clustering/v1/policies/testing/requests_test.go +++ b/openstack/clustering/v1/policies/testing/requests_test.go @@ -58,3 +58,15 @@ func TestCreatePolicy(t *testing.T) { 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 index e0d0c8de7f..d705e8a3fc 100644 --- a/openstack/clustering/v1/policies/urls.go +++ b/openstack/clustering/v1/policies/urls.go @@ -14,3 +14,7 @@ func policyListURL(client *gophercloud.ServiceClient) string { 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) +} From 4d8fee31efb25411f1588e71c8347570a3df4ad2 Mon Sep 17 00:00:00 2001 From: JackKuei <37349764+JackKuei@users.noreply.github.com> Date: Wed, 11 Apr 2018 18:56:12 -0700 Subject: [PATCH 119/120] Senlin: Webhook Trigger (#824) * Senlin: Webhook Trigger (#823) Add webhooks trigger for senlin clustering * Fixed example code in doc Improved/cleanup comments to make it more readable Refactored Webhook struct for Extract() to be cleaner and more readable * Fixed trigger opts to properly construct the query strings for "V" and "params" Added test cases for nil, empty, and params to webhook trigger Added test case for testing query string construction * Added acceptance test, but commented out because in order to invoke webhook trigger, requires cluster, profiles, and receiver to be created. * Changed doc.go to use V param * Merged from upstream with go fmt fix * Fixed formatting for doc as well for testing for triggerOpts --- acceptance/clients/clients.go | 6 +- .../clustering/v1/webhooktrigger_test.go | 32 +++++ openstack/clustering/v1/webhooks/doc.go | 15 ++ openstack/clustering/v1/webhooks/requests.go | 53 +++++++ openstack/clustering/v1/webhooks/results.go | 22 +++ .../clustering/v1/webhooks/testing/doc.go | 2 + .../v1/webhooks/testing/requests_test.go | 136 ++++++++++++++++++ openstack/clustering/v1/webhooks/urls.go | 7 + 8 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 acceptance/openstack/clustering/v1/webhooktrigger_test.go create mode 100644 openstack/clustering/v1/webhooks/doc.go create mode 100644 openstack/clustering/v1/webhooks/requests.go create mode 100644 openstack/clustering/v1/webhooks/results.go create mode 100644 openstack/clustering/v1/webhooks/testing/doc.go create mode 100644 openstack/clustering/v1/webhooks/testing/requests_test.go create mode 100644 openstack/clustering/v1/webhooks/urls.go diff --git a/acceptance/clients/clients.go b/acceptance/clients/clients.go index 666564ad5f..d664f90109 100644 --- a/acceptance/clients/clients.go +++ b/acceptance/clients/clients.go @@ -481,9 +481,9 @@ func NewLoadBalancerV2Client() (*gophercloud.ServiceClient, error) { }) } -// 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. +// 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 { 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/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") +} From 3c0e6bd2622193314406205c231ca623528832c9 Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Thu, 12 Apr 2018 17:13:20 +1200 Subject: [PATCH 120/120] Octavia l7 rule support - part 1: create (#924) * Octavia l7 rule support - part 1: create For #832 Octavia l7 rule create API implementation: https://github.com/openstack/octavia/blob/06bf5c58d5845f684fcaf933605ed112586eefc3/octavia/api/v2/controllers/l7rule.py#L146 * Use project_id instead of tenant_id --- .../openstack/loadbalancer/v2/loadbalancer.go | 24 ++++++++ .../loadbalancer/v2/loadbalancers_test.go | 6 ++ openstack/loadbalancer/v2/l7policies/doc.go | 13 ++++ .../loadbalancer/v2/l7policies/requests.go | 46 ++++++++++++-- .../loadbalancer/v2/l7policies/results.go | 27 +++++++-- .../v2/l7policies/testing/fixtures.go | 60 ++++++++++++++++--- .../v2/l7policies/testing/requests_test.go | 47 +++++++++++++++ openstack/loadbalancer/v2/l7policies/urls.go | 5 ++ 8 files changed, 212 insertions(+), 16 deletions(-) diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancer.go b/acceptance/openstack/loadbalancer/v2/loadbalancer.go index 10bbcc28c1..d9c8ce86c0 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancer.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancer.go @@ -202,6 +202,30 @@ func CreateL7Policy(t *testing.T, client *gophercloud.ServiceClient, listener *l 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. diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go index 080f215ef0..0149b19ea4 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go @@ -128,6 +128,12 @@ func TestLoadbalancersCRUD(t *testing.T) { 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 { diff --git a/openstack/loadbalancer/v2/l7policies/doc.go b/openstack/loadbalancer/v2/l7policies/doc.go index 95ac5e17f0..86135e3d37 100644 --- a/openstack/loadbalancer/v2/l7policies/doc.go +++ b/openstack/loadbalancer/v2/l7policies/doc.go @@ -58,5 +58,18 @@ Example to Update a L7Policy 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 index b64c14b863..16a655947b 100644 --- a/openstack/loadbalancer/v2/l7policies/requests.go +++ b/openstack/loadbalancer/v2/l7policies/requests.go @@ -51,10 +51,6 @@ type CreateOpts struct { // A human-readable description for the resource. Description string `json:"description,omitempty"` - // TenantID is the UUID of the project who owns the L7 policy in neutron-lbaas. - // 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 L7 policy in octavia. // Only administrative users can specify a project UUID other than their own. ProjectID string `json:"project_id,omitempty"` @@ -96,7 +92,7 @@ type ListOpts struct { Name string `q:"name"` ListenerID string `q:"listener_id"` Action string `q:"action"` - TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` RedirectPoolID string `q:"redirect_pool_id"` RedirectURL string `q:"redirect_url"` ID string `q:"id"` @@ -187,3 +183,43 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r }) 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 index f4907a3f59..78a8e83db0 100644 --- a/openstack/loadbalancer/v2/l7policies/results.go +++ b/openstack/loadbalancer/v2/l7policies/results.go @@ -26,9 +26,9 @@ type L7Policy struct { // A human-readable description for the resource. Description string `json:"description"` - // TenantID is the UUID of the project who owns the L7 policy in neutron-lbaas. + // 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. - TenantID string `json:"tenant_id"` + 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. @@ -59,9 +59,9 @@ type Rule struct { // The value to use for the comparison. For example, the file type to compare. Value string `json:"value"` - // TenantID is the UUID of the project who owns the rule in neutron-lbaas. + // 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. - TenantID string `json:"tenant_id"` + 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"` @@ -147,3 +147,22 @@ type DeleteResult struct { 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/fixtures.go b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go index 193e728097..a4d2e13144 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/fixtures.go +++ b/openstack/loadbalancer/v2/l7policies/testing/fixtures.go @@ -21,7 +21,7 @@ const SingleL7PolicyBody = ` "redirect_url": "http://www.example.com", "action": "REDIRECT_TO_URL", "position": 1, - "tenant_id": "e3cd678b11784734bc366148aa37580e", + "project_id": "e3cd678b11784734bc366148aa37580e", "id": "8a1412f0-4c32-4257-8b07-af4770b604fd", "name": "redirect-example.com", "rules": [] @@ -37,7 +37,7 @@ var ( Action: "REDIRECT_TO_URL", Position: 1, Description: "", - TenantID: "e3cd678b11784734bc366148aa37580e", + ProjectID: "e3cd678b11784734bc366148aa37580e", RedirectPoolID: "", RedirectURL: "http://www.example.com", AdminStateUp: true, @@ -50,7 +50,7 @@ var ( Action: "REDIRECT_TO_POOL", Position: 1, Description: "", - TenantID: "c1f7910086964990847dc6c8b128f63c", + ProjectID: "c1f7910086964990847dc6c8b128f63c", RedirectPoolID: "bac433c6-5bea-4311-80da-bd1cd90fbd25", RedirectURL: "", AdminStateUp: true, @@ -63,12 +63,22 @@ var ( Action: "REDIRECT_TO_URL", Position: 1, Description: "Redirect requests to example.com", - TenantID: "e3cd678b11784734bc366148aa37580e", + 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 @@ -101,7 +111,7 @@ const L7PoliciesListBody = ` "description": "", "admin_state_up": true, "rules": [], - "tenant_id": "e3cd678b11784734bc366148aa37580e", + "project_id": "e3cd678b11784734bc366148aa37580e", "listener_id": "023f2e34-7806-443b-bfae-16c324569a3d", "redirect_url": "http://www.example.com", "action": "REDIRECT_TO_URL", @@ -114,7 +124,7 @@ const L7PoliciesListBody = ` "description": "", "admin_state_up": true, "rules": [], - "tenant_id": "c1f7910086964990847dc6c8b128f63c", + "project_id": "c1f7910086964990847dc6c8b128f63c", "listener_id": "be3138a3-5cf7-4513-a4c2-bb137e668bab", "action": "REDIRECT_TO_POOL", "position": 1, @@ -136,7 +146,7 @@ const PostUpdateL7PolicyBody = ` "redirect_url": "http://www.new-example.com", "action": "REDIRECT_TO_URL", "position": 1, - "tenant_id": "e3cd678b11784734bc366148aa37580e", + "project_id": "e3cd678b11784734bc366148aa37580e", "id": "8a1412f0-4c32-4257-8b07-af4770b604fd", "name": "NewL7PolicyName", "rules": [] @@ -203,3 +213,39 @@ func HandleL7PolicyUpdateSuccessfully(t *testing.T) { 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 index f9c47d665c..9fac83960b 100644 --- a/openstack/loadbalancer/v2/l7policies/testing/requests_test.go +++ b/openstack/loadbalancer/v2/l7policies/testing/requests_test.go @@ -127,3 +127,50 @@ func TestUpdateL7Policy(t *testing.T) { 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 index 7a87a187f9..44ebdd444f 100644 --- a/openstack/loadbalancer/v2/l7policies/urls.go +++ b/openstack/loadbalancer/v2/l7policies/urls.go @@ -5,6 +5,7 @@ import "github.com/gophercloud/gophercloud" const ( rootPath = "lbaas" resourcePath = "l7policies" + rulePath = "rules" ) func rootURL(c *gophercloud.ServiceClient) string { @@ -14,3 +15,7 @@ func rootURL(c *gophercloud.ServiceClient) string { 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) +}