From ef79b92df4bb70f41ed0ed1e454294ae9c3af9fd Mon Sep 17 00:00:00 2001 From: chuliang Date: Mon, 4 Sep 2023 13:56:18 +0000 Subject: [PATCH 01/15] identity: Add Get endpoint by ID Co-Authored-By: chuliang Co-Authored-By: Pierre Prinetti Co-Authored-By: Stephen Finucane --- .../openstack/identity/v3/endpoint_test.go | 23 +++ openstack/identity/v3/endpoints/requests.go | 7 + openstack/identity/v3/endpoints/results.go | 6 + .../v3/endpoints/testing/requests_test.go | 169 +++++++++++------- openstack/identity/v3/projects/doc.go | 2 +- 5 files changed, 137 insertions(+), 70 deletions(-) diff --git a/internal/acceptance/openstack/identity/v3/endpoint_test.go b/internal/acceptance/openstack/identity/v3/endpoint_test.go index ae2254102a..8dd9e5bad1 100644 --- a/internal/acceptance/openstack/identity/v3/endpoint_test.go +++ b/internal/acceptance/openstack/identity/v3/endpoint_test.go @@ -39,6 +39,29 @@ func TestEndpointsList(t *testing.T) { th.AssertEquals(t, found, true) } +func TestEndpointsGet(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewIdentityV3Client() + th.AssertNoErr(t, err) + + allPages, err := endpoints.List(client, nil).AllPages(context.TODO()) + th.AssertNoErr(t, err) + + allEndpoints, err := endpoints.ExtractEndpoints(allPages) + th.AssertNoErr(t, err) + + endpoint := allEndpoints[0] + e, err := endpoints.Get(context.TODO(), client, endpoint.ID).Extract() + if err != nil { + t.Fatalf("Unable to get endpoint: %v", err) + } + + tools.PrintResource(t, e) + + th.AssertEquals(t, e.Name, e.Name) +} + func TestEndpointsNavigateCatalog(t *testing.T) { clients.RequireAdmin(t) diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go index 35eb966077..4386bb9bc9 100644 --- a/openstack/identity/v3/endpoints/requests.go +++ b/openstack/identity/v3/endpoints/requests.go @@ -90,6 +90,13 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa }) } +// Get retrieves details on a single endpoint, by ID. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, endpointURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + // UpdateOptsBuilder allows extensions to add parameters to the Update request. type UpdateOptsBuilder interface { ToEndpointUpdateMap() (map[string]any, error) diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go index 9efec30af2..19d279ebc6 100644 --- a/openstack/identity/v3/endpoints/results.go +++ b/openstack/identity/v3/endpoints/results.go @@ -19,6 +19,12 @@ func (r commonResult) Extract() (*Endpoint, error) { return s.Endpoint, err } +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as an Endpoint. +type GetResult struct { + commonResult +} + // CreateResult is the response from a Create operation. Call its Extract // method to interpret it as an Endpoint. type CreateResult struct { diff --git a/openstack/identity/v3/endpoints/testing/requests_test.go b/openstack/identity/v3/endpoints/testing/requests_test.go index 348a1f1c6e..ad138acd49 100644 --- a/openstack/identity/v3/endpoints/testing/requests_test.go +++ b/openstack/identity/v3/endpoints/testing/requests_test.go @@ -20,35 +20,31 @@ func TestCreateSuccessful(t *testing.T) { th.Mux.HandleFunc("/endpoints", 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, ` - { - "endpoint": { - "interface": "public", - "name": "the-endiest-of-points", - "region": "underground", - "url": "https://1.2.3.4:9000/", - "service_id": "asdfasdfasdfasdf" - } - } - `) + th.TestJSONRequest(t, r, `{ + "endpoint": { + "interface": "public", + "name": "the-endiest-of-points", + "region": "underground", + "url": "https://1.2.3.4:9000/", + "service_id": "asdfasdfasdfasdf" + } + }`) w.WriteHeader(http.StatusCreated) - fmt.Fprint(w, ` - { - "endpoint": { - "id": "12", - "interface": "public", - "enabled": true, - "links": { - "self": "https://localhost:5000/v3/endpoints/12" - }, - "name": "the-endiest-of-points", - "region": "underground", - "service_id": "asdfasdfasdfasdf", - "url": "https://1.2.3.4:9000/" - } - } - `) + fmt.Fprint(w, `{ + "endpoint": { + "id": "12", + "interface": "public", + "enabled": true, + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + }`) }) actual, err := endpoints.Create(context.TODO(), client.ServiceClient(), endpoints.CreateOpts{ @@ -82,40 +78,38 @@ func TestListEndpoints(t *testing.T) { th.TestHeader(t, r, "X-Auth-Token", client.TokenID) w.Header().Add("Content-Type", "application/json") - fmt.Fprint(w, ` - { - "endpoints": [ - { - "id": "12", - "interface": "public", - "enabled": true, - "links": { - "self": "https://localhost:5000/v3/endpoints/12" - }, - "name": "the-endiest-of-points", - "region": "underground", - "service_id": "asdfasdfasdfasdf", - "url": "https://1.2.3.4:9000/" + fmt.Fprint(w, `{ + "endpoints": [ + { + "id": "12", + "interface": "public", + "enabled": true, + "links": { + "self": "https://localhost:5000/v3/endpoints/12" }, - { - "id": "13", - "interface": "internal", - "enabled": false, - "links": { - "self": "https://localhost:5000/v3/endpoints/13" - }, - "name": "shhhh", - "region": "underground", - "service_id": "asdfasdfasdfasdf", - "url": "https://1.2.3.4:9001/" - } - ], - "links": { - "next": null, - "previous": null + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + }, + { + "id": "13", + "interface": "internal", + "enabled": false, + "links": { + "self": "https://localhost:5000/v3/endpoints/13" + }, + "name": "shhhh", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9001/" } + ], + "links": { + "next": null, + "previous": null } - `) + }`) }) count := 0 @@ -154,6 +148,47 @@ func TestListEndpoints(t *testing.T) { th.AssertEquals(t, 1, count) } +func TestGetEndpoint(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + fmt.Fprint(w, `{ + "endpoint": { + "id": "12", + "interface": "public", + "enabled": true, + "links": { + "self": "https://localhost:5000/v3/endpoints/12" + }, + "name": "the-endiest-of-points", + "region": "underground", + "service_id": "asdfasdfasdfasdf", + "url": "https://1.2.3.4:9000/" + } + }`) + }) + + actual, err := endpoints.Get(context.TODO(), client.ServiceClient(), "12").Extract() + if err != nil { + t.Fatalf("Unexpected error from Get: %v", err) + } + + expected := &endpoints.Endpoint{ + ID: "12", + Availability: gophercloud.AvailabilityPublic, + Enabled: true, + Name: "the-endiest-of-points", + Region: "underground", + ServiceID: "asdfasdfasdfasdf", + URL: "https://1.2.3.4:9000/", + } + th.AssertDeepEquals(t, expected, actual) +} + func TestUpdateEndpoint(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() @@ -161,17 +196,14 @@ func TestUpdateEndpoint(t *testing.T) { th.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) { th.TestMethod(t, r, "PATCH") th.TestHeader(t, r, "X-Auth-Token", client.TokenID) - th.TestJSONRequest(t, r, ` - { - "endpoint": { - "name": "renamed", + th.TestJSONRequest(t, r, `{ + "endpoint": { + "name": "renamed", "region": "somewhere-else" - } - } - `) + } + }`) - fmt.Fprint(w, ` - { + fmt.Fprint(w, `{ "endpoint": { "id": "12", "interface": "public", @@ -184,8 +216,7 @@ func TestUpdateEndpoint(t *testing.T) { "service_id": "asdfasdfasdfasdf", "url": "https://1.2.3.4:9000/" } - } - `) + }`) }) actual, err := endpoints.Update(context.TODO(), client.ServiceClient(), "12", endpoints.UpdateOpts{ diff --git a/openstack/identity/v3/projects/doc.go b/openstack/identity/v3/projects/doc.go index 6aea466a51..6100327397 100644 --- a/openstack/identity/v3/projects/doc.go +++ b/openstack/identity/v3/projects/doc.go @@ -73,7 +73,7 @@ Example to List all tags of a Project panic(err) } -Example to modify all tags of a Project +Example to modify all tags of a Project projectID := "966b3c7d36a24facaf20b7e458bf2192" tags := ["foo", "bar"] From 2a76f50e59d764a3ad682be0c367d25f3005729b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Mon, 10 Mar 2025 09:46:12 +0100 Subject: [PATCH 02/15] Switch to a version of gocovmerge compatible with go 1.22 The original `gocovmerge` repository [1] has not seen a commit in 9 years and should be considered unmaintained. Switch to fork has a go.mod pinned to go 1.22. Please review the diff [2] between the two repos to check if it's safe to use in gophercloud. [1] https://github.com/wadey/gocovmerge [2] https://github.com/wadey/gocovmerge/compare/master...alexfalkowski:gocovmerge:v1.4.0 --- .github/workflows/unit.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml index 11fddb5f68..0ae498baf7 100644 --- a/.github/workflows/unit.yaml +++ b/.github/workflows/unit.yaml @@ -30,7 +30,7 @@ jobs: cd "$(mktemp -d)" go mod init unit_tests - go install github.com/wadey/gocovmerge@master + go install github.com/alexfalkowski/gocovmerge@v1.4.0 - name: Run unit tests run: | From cfa7409cb2d1a8c4098968d31b4ca92338ab4bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Thu, 6 Mar 2025 13:53:09 +0100 Subject: [PATCH 03/15] Improve support for network `standard-attr-revisions` extension Consistently add support for the neutron `Resource revision numbers` extension [1] for more resources that support it. It adds a `RevisionNumber` field to all responses, and allows filtering via the `RevisionNumber` list option. It was only partially implemented. Also support the `If-Match` constraints, and add test for it. [1] https://docs.openstack.org/api-ref/network/v2/#revisions --- .../v2/extensions/layer3/floatingips_test.go | 60 +++++ .../v2/extensions/layer3/routers_test.go | 66 ++++++ .../extensions/qos/policies/policies_test.go | 66 ++++++ .../networking/v2/extensions/security_test.go | 63 ++++++ .../subnetpools/subnetpools_test.go | 61 +++++ .../v2/extensions/trunks/trunks_test.go | 212 ++++++++++-------- .../extensions/layer3/floatingips/requests.go | 20 +- .../extensions/layer3/floatingips/results.go | 3 + .../v2/extensions/layer3/routers/requests.go | 52 +++-- .../v2/extensions/layer3/routers/results.go | 3 + .../v2/extensions/qos/policies/requests.go | 21 +- .../v2/extensions/qos/policies/results.go | 2 +- .../v2/extensions/security/groups/requests.go | 48 ++-- .../v2/extensions/security/groups/results.go | 3 + .../v2/extensions/security/rules/requests.go | 1 + .../v2/extensions/security/rules/results.go | 3 + .../v2/extensions/subnetpools/requests.go | 22 +- .../v2/extensions/trunks/requests.go | 48 ++-- .../v2/extensions/trunks/results.go | 1 + openstack/networking/v2/networks/requests.go | 33 +-- openstack/networking/v2/ports/requests.go | 1 + openstack/networking/v2/subnets/requests.go | 1 + 22 files changed, 628 insertions(+), 162 deletions(-) diff --git a/internal/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go b/internal/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go index fee8cacd38..5f4d771614 100644 --- a/internal/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go +++ b/internal/acceptance/openstack/networking/v2/extensions/layer3/floatingips_test.go @@ -4,6 +4,7 @@ package layer3 import ( "context" + "strings" "testing" "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" @@ -203,3 +204,62 @@ func TestLayer3FloatingIPsCreateDeleteBySubnetID(t *testing.T) { DeleteFloatingIP(t, client, fip.ID) } + +func TestLayer3FloatingIPsRevision(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + choices, err := clients.AcceptanceTestChoicesFromEnv() + th.AssertNoErr(t, err) + + fip, err := CreateFloatingIP(t, client, choices.ExternalNetworkID, "") + th.AssertNoErr(t, err) + defer DeleteFloatingIP(t, client, fip.ID) + + tools.PrintResource(t, fip) + + // Store the current revision number. + oldRevisionNumber := fip.RevisionNumber + + // Update the fip without revision number. + // This should work. + newDescription := "" + updateOpts := &floatingips.UpdateOpts{ + Description: &newDescription, + } + fip, err = floatingips.Update(context.TODO(), client, fip.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, fip) + + // This should fail due to an old revision number. + newDescription = "new description" + updateOpts = &floatingips.UpdateOpts{ + Description: &newDescription, + RevisionNumber: &oldRevisionNumber, + } + _, err = floatingips.Update(context.TODO(), client, fip.ID, updateOpts).Extract() + th.AssertErr(t, err) + if !strings.Contains(err.Error(), "RevisionNumberConstraintFailed") { + t.Fatalf("expected to see an error of type RevisionNumberConstraintFailed, but got the following error instead: %v", err) + } + + // Reread the fip to show that it did not change. + fip, err = floatingips.Get(context.TODO(), client, fip.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, fip) + + // This should work because now we do provide a valid revision number. + newDescription = "new description" + updateOpts = &floatingips.UpdateOpts{ + Description: &newDescription, + RevisionNumber: &fip.RevisionNumber, + } + fip, err = floatingips.Update(context.TODO(), client, fip.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, fip) + + th.AssertEquals(t, fip.Description, newDescription) +} diff --git a/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go b/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go index 8d1800f719..1df4375164 100644 --- a/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go +++ b/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go @@ -4,6 +4,7 @@ package layer3 import ( "context" + "strings" "testing" "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" @@ -213,3 +214,68 @@ func TestLayer3RouterAgents(t *testing.T) { th.AssertEquals(t, found, true) } + +func TestLayer3RouterRevision(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + network, err := networking.CreateNetwork(t, client) + th.AssertNoErr(t, err) + defer networking.DeleteNetwork(t, client, network.ID) + + router, err := CreateRouter(t, client, network.ID) + th.AssertNoErr(t, err) + defer DeleteRouter(t, client, router.ID) + + tools.PrintResource(t, router) + + // Store the current revision number. + oldRevisionNumber := router.RevisionNumber + + // Update the router without revision number. + // This should work. + newName := tools.RandomString("TESTACC-", 8) + newDescription := "" + updateOpts := &routers.UpdateOpts{ + Name: newName, + Description: &newDescription, + } + router, err = routers.Update(context.TODO(), client, router.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, router) + + // This should fail due to an old revision number. + newDescription = "new description" + updateOpts = &routers.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &oldRevisionNumber, + } + _, err = routers.Update(context.TODO(), client, router.ID, updateOpts).Extract() + th.AssertErr(t, err) + if !strings.Contains(err.Error(), "RevisionNumberConstraintFailed") { + t.Fatalf("expected to see an error of type RevisionNumberConstraintFailed, but got the following error instead: %v", err) + } + + // Reread the router to show that it did not change. + router, err = routers.Get(context.TODO(), client, router.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, router) + + // This should work because now we do provide a valid revision number. + newDescription = "new description" + updateOpts = &routers.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &router.RevisionNumber, + } + router, err = routers.Update(context.TODO(), client, router.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, router) + + th.AssertEquals(t, router.Name, newName) + th.AssertEquals(t, router.Description, newDescription) +} diff --git a/internal/acceptance/openstack/networking/v2/extensions/qos/policies/policies_test.go b/internal/acceptance/openstack/networking/v2/extensions/qos/policies/policies_test.go index fc9563df38..9ee2304c75 100644 --- a/internal/acceptance/openstack/networking/v2/extensions/qos/policies/policies_test.go +++ b/internal/acceptance/openstack/networking/v2/extensions/qos/policies/policies_test.go @@ -4,6 +4,7 @@ package policies import ( "context" + "strings" "testing" "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" @@ -59,3 +60,68 @@ func TestPoliciesCRUD(t *testing.T) { th.AssertEquals(t, found, true) } + +func TestPoliciesRevision(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + // Skip these tests if we don't have the required extension + v2.RequireNeutronExtension(t, client, "qos") + + // Create a policy + policy, err := CreateQoSPolicy(t, client) + th.AssertNoErr(t, err) + defer DeleteQoSPolicy(t, client, policy.ID) + + tools.PrintResource(t, policy) + + // Store the current revision number. + oldRevisionNumber := policy.RevisionNumber + + // Update the policy without revision number. + // This should work. + newName := tools.RandomString("TESTACC-", 8) + newDescription := "" + updateOpts := &policies.UpdateOpts{ + Name: newName, + Description: &newDescription, + } + policy, err = policies.Update(context.TODO(), client, policy.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, policy) + + // This should fail due to an old revision number. + newDescription = "new description" + updateOpts = &policies.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &oldRevisionNumber, + } + _, err = policies.Update(context.TODO(), client, policy.ID, updateOpts).Extract() + th.AssertErr(t, err) + if !strings.Contains(err.Error(), "RevisionNumberConstraintFailed") { + t.Fatalf("expected to see an error of type RevisionNumberConstraintFailed, but got the following error instead: %v", err) + } + + // Reread the policy to show that it did not change. + policy, err = policies.Get(context.TODO(), client, policy.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, policy) + + // This should work because now we do provide a valid revision number. + newDescription = "new description" + updateOpts = &policies.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &policy.RevisionNumber, + } + policy, err = policies.Update(context.TODO(), client, policy.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, policy) + + th.AssertEquals(t, policy.Name, newName) + th.AssertEquals(t, policy.Description, newDescription) +} diff --git a/internal/acceptance/openstack/networking/v2/extensions/security_test.go b/internal/acceptance/openstack/networking/v2/extensions/security_test.go index 8d855bbb0a..33fc3f176d 100644 --- a/internal/acceptance/openstack/networking/v2/extensions/security_test.go +++ b/internal/acceptance/openstack/networking/v2/extensions/security_test.go @@ -4,6 +4,7 @@ package extensions import ( "context" + "strings" "testing" "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" @@ -93,3 +94,65 @@ func TestSecurityGroupsPort(t *testing.T) { tools.PrintResource(t, port) } + +func TestSecurityGroupsRevision(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + // Create a group + group, err := CreateSecurityGroup(t, client) + th.AssertNoErr(t, err) + defer DeleteSecurityGroup(t, client, group.ID) + + tools.PrintResource(t, group) + + // Store the current revision number. + oldRevisionNumber := group.RevisionNumber + + // Update the group without revision number. + // This should work. + newName := tools.RandomString("TESTACC-", 8) + newDescription := "" + updateOpts := &groups.UpdateOpts{ + Name: newName, + Description: &newDescription, + } + group, err = groups.Update(context.TODO(), client, group.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, group) + + // This should fail due to an old revision number. + newDescription = "new description" + updateOpts = &groups.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &oldRevisionNumber, + } + _, err = groups.Update(context.TODO(), client, group.ID, updateOpts).Extract() + th.AssertErr(t, err) + if !strings.Contains(err.Error(), "RevisionNumberConstraintFailed") { + t.Fatalf("expected to see an error of type RevisionNumberConstraintFailed, but got the following error instead: %v", err) + } + + // Reread the group to show that it did not change. + group, err = groups.Get(context.TODO(), client, group.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, group) + + // This should work because now we do provide a valid revision number. + newDescription = "new description" + updateOpts = &groups.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &group.RevisionNumber, + } + group, err = groups.Update(context.TODO(), client, group.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, group) + + th.AssertEquals(t, group.Name, newName) + th.AssertEquals(t, group.Description, newDescription) +} diff --git a/internal/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go b/internal/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go index 8a0575d8fe..8c9cb3120b 100644 --- a/internal/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go +++ b/internal/acceptance/openstack/networking/v2/extensions/subnetpools/subnetpools_test.go @@ -4,6 +4,7 @@ package v2 import ( "context" + "strings" "testing" "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" @@ -55,3 +56,63 @@ func TestSubnetPoolsCRUD(t *testing.T) { th.AssertEquals(t, found, true) } + +func TestSubnetPoolsRevision(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + // Create a subnetpool + subnetPool, err := CreateSubnetPool(t, client) + th.AssertNoErr(t, err) + defer DeleteSubnetPool(t, client, subnetPool.ID) + + // Store the current revision number. + oldRevisionNumber := subnetPool.RevisionNumber + + // Update the subnet pool without revision number. + // This should work. + newName := tools.RandomString("TESTACC-", 8) + newDescription := "" + updateOpts := &subnetpools.UpdateOpts{ + Name: newName, + Description: &newDescription, + } + subnetPool, err = subnetpools.Update(context.TODO(), client, subnetPool.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, subnetPool) + + // This should fail due to an old revision number. + newDescription = "new description" + updateOpts = &subnetpools.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &oldRevisionNumber, + } + _, err = subnetpools.Update(context.TODO(), client, subnetPool.ID, updateOpts).Extract() + th.AssertErr(t, err) + if !strings.Contains(err.Error(), "RevisionNumberConstraintFailed") { + t.Fatalf("expected to see an error of type RevisionNumberConstraintFailed, but got the following error instead: %v", err) + } + + // Reread the subnet pool to show that it did not change. + subnetPool, err = subnetpools.Get(context.TODO(), client, subnetPool.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, subnetPool) + + // This should work because now we do provide a valid revision number. + newDescription = "new description" + updateOpts = &subnetpools.UpdateOpts{ + Name: newName, + Description: &newDescription, + RevisionNumber: &subnetPool.RevisionNumber, + } + subnetPool, err = subnetpools.Update(context.TODO(), client, subnetPool.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, subnetPool) + + th.AssertEquals(t, subnetPool.Name, newName) + th.AssertEquals(t, subnetPool.Description, newDescription) +} diff --git a/internal/acceptance/openstack/networking/v2/extensions/trunks/trunks_test.go b/internal/acceptance/openstack/networking/v2/extensions/trunks/trunks_test.go index 65ebd90a78..3e0af63175 100644 --- a/internal/acceptance/openstack/networking/v2/extensions/trunks/trunks_test.go +++ b/internal/acceptance/openstack/networking/v2/extensions/trunks/trunks_test.go @@ -5,6 +5,7 @@ package trunks import ( "context" "sort" + "strings" "testing" "github.com/gophercloud/gophercloud/v2/internal/acceptance/clients" @@ -17,56 +18,40 @@ import ( func TestTrunkCRUD(t *testing.T) { client, err := clients.NewNetworkV2Client() - if err != nil { - t.Fatalf("Unable to create a network client: %v", err) - } + th.AssertNoErr(t, err) // Skip these tests if we don't have the required extension v2.RequireNeutronExtension(t, client, "trunk") // Create Network network, err := v2.CreateNetwork(t, client) - if err != nil { - t.Fatalf("Unable to create network: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeleteNetwork(t, client, network.ID) // Create Subnet subnet, err := v2.CreateSubnet(t, client, network.ID) - if err != nil { - t.Fatalf("Unable to create subnet: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeleteSubnet(t, client, subnet.ID) // Create port parentPort, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, parentPort.ID) subport1, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, subport1.ID) subport2, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, subport2.ID) trunk, err := CreateTrunk(t, client, parentPort.ID, subport1.ID, subport2.ID) - if err != nil { - t.Fatalf("Unable to create trunk: %v", err) - } + th.AssertNoErr(t, err) defer DeleteTrunk(t, client, trunk.ID) _, err = trunks.Get(context.TODO(), client, trunk.ID).Extract() - if err != nil { - t.Fatalf("Unable to get trunk: %v", err) - } + th.AssertNoErr(t, err) // Update Trunk name := "" @@ -76,9 +61,7 @@ func TestTrunkCRUD(t *testing.T) { Description: &description, } updatedTrunk, err := trunks.Update(context.TODO(), client, trunk.ID, updateOpts).Extract() - if err != nil { - t.Fatalf("Unable to update trunk: %v", err) - } + th.AssertNoErr(t, err) if trunk.Name == updatedTrunk.Name { t.Fatalf("Trunk name was not updated correctly") @@ -93,9 +76,7 @@ func TestTrunkCRUD(t *testing.T) { // Get subports subports, err := trunks.GetSubports(context.TODO(), client, trunk.ID).Extract() - if err != nil { - t.Fatalf("Unable to get subports from the Trunk: %v", err) - } + th.AssertNoErr(t, err) th.AssertDeepEquals(t, trunk.Subports[0], subports[0]) th.AssertDeepEquals(t, trunk.Subports[1], subports[1]) @@ -104,22 +85,16 @@ func TestTrunkCRUD(t *testing.T) { func TestTrunkList(t *testing.T) { client, err := clients.NewNetworkV2Client() - if err != nil { - t.Fatalf("Unable to create a network client: %v", err) - } + th.AssertNoErr(t, err) // Skip these tests if we don't have the required extension v2.RequireNeutronExtension(t, client, "trunk") allPages, err := trunks.List(client, nil).AllPages(context.TODO()) - if err != nil { - t.Fatalf("Unable to list trunks: %v", err) - } + th.AssertNoErr(t, err) allTrunks, err := trunks.ExtractTrunks(allPages) - if err != nil { - t.Fatalf("Unable to extract trunks: %v", err) - } + th.AssertNoErr(t, err) for _, trunk := range allTrunks { tools.PrintResource(t, trunk) @@ -128,50 +103,36 @@ func TestTrunkList(t *testing.T) { func TestTrunkSubportOperation(t *testing.T) { client, err := clients.NewNetworkV2Client() - if err != nil { - t.Fatalf("Unable to create a network client: %v", err) - } + th.AssertNoErr(t, err) // Skip these tests if we don't have the required extension v2.RequireNeutronExtension(t, client, "trunk") // Create Network network, err := v2.CreateNetwork(t, client) - if err != nil { - t.Fatalf("Unable to create network: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeleteNetwork(t, client, network.ID) // Create Subnet subnet, err := v2.CreateSubnet(t, client, network.ID) - if err != nil { - t.Fatalf("Unable to create subnet: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeleteSubnet(t, client, subnet.ID) // Create port parentPort, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, parentPort.ID) subport1, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, subport1.ID) subport2, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, subport2.ID) trunk, err := CreateTrunk(t, client, parentPort.ID) - if err != nil { - t.Fatalf("Unable to create trunk: %v", err) - } + th.AssertNoErr(t, err) defer DeleteTrunk(t, client, trunk.ID) // Add subports to the trunk @@ -190,9 +151,7 @@ func TestTrunkSubportOperation(t *testing.T) { }, } updatedTrunk, err := trunks.AddSubports(context.TODO(), client, trunk.ID, addSubportsOpts).Extract() - if err != nil { - t.Fatalf("Unable to add subports to the Trunk: %v", err) - } + th.AssertNoErr(t, err) th.AssertEquals(t, 2, len(updatedTrunk.Subports)) th.AssertDeepEquals(t, addSubportsOpts.Subports[0], updatedTrunk.Subports[0]) th.AssertDeepEquals(t, addSubportsOpts.Subports[1], updatedTrunk.Subports[1]) @@ -205,58 +164,42 @@ func TestTrunkSubportOperation(t *testing.T) { }, } updatedAgainTrunk, err := trunks.RemoveSubports(context.TODO(), client, trunk.ID, subRemoveOpts).Extract() - if err != nil { - t.Fatalf("Unable to remove subports from the Trunk: %v", err) - } + th.AssertNoErr(t, err) th.AssertDeepEquals(t, trunk.Subports, updatedAgainTrunk.Subports) } func TestTrunkTags(t *testing.T) { client, err := clients.NewNetworkV2Client() - if err != nil { - t.Fatalf("Unable to create a network client: %v", err) - } + th.AssertNoErr(t, err) // Skip these tests if we don't have the required extension v2.RequireNeutronExtension(t, client, "trunk") // Create Network network, err := v2.CreateNetwork(t, client) - if err != nil { - t.Fatalf("Unable to create network: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeleteNetwork(t, client, network.ID) // Create Subnet subnet, err := v2.CreateSubnet(t, client, network.ID) - if err != nil { - t.Fatalf("Unable to create subnet: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeleteSubnet(t, client, subnet.ID) // Create port parentPort, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, parentPort.ID) subport1, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, subport1.ID) subport2, err := v2.CreatePort(t, client, network.ID, subnet.ID) - if err != nil { - t.Fatalf("Unable to create port: %v", err) - } + th.AssertNoErr(t, err) defer v2.DeletePort(t, client, subport2.ID) trunk, err := CreateTrunk(t, client, parentPort.ID, subport1.ID, subport2.ID) - if err != nil { - t.Fatalf("Unable to create trunk: %v", err) - } + th.AssertNoErr(t, err) defer DeleteTrunk(t, client, trunk.ID) tagReplaceAllOpts := attributestags.ReplaceAllOpts{ @@ -264,14 +207,10 @@ func TestTrunkTags(t *testing.T) { Tags: []string{"a", "b", "c"}, } _, err = attributestags.ReplaceAll(context.TODO(), client, "trunks", trunk.ID, tagReplaceAllOpts).Extract() - if err != nil { - t.Fatalf("Unable to set trunk tags: %v", err) - } + th.AssertNoErr(t, err) gtrunk, err := trunks.Get(context.TODO(), client, trunk.ID).Extract() - if err != nil { - t.Fatalf("Unable to get trunk: %v", err) - } + th.AssertNoErr(t, err) tags := gtrunk.Tags sort.Strings(tags) // Ensure ordering, older OpenStack versions aren't sorted... th.AssertDeepEquals(t, []string{"a", "b", "c"}, tags) @@ -297,3 +236,90 @@ func TestTrunkTags(t *testing.T) { th.AssertNoErr(t, err) th.AssertEquals(t, 0, len(tags)) } + +func TestTrunkRevision(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + // Skip these tests if we don't have the required extension + v2.RequireNeutronExtension(t, client, "trunk") + + // Create Network + network, err := v2.CreateNetwork(t, client) + th.AssertNoErr(t, err) + defer v2.DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := v2.CreateSubnet(t, client, network.ID) + th.AssertNoErr(t, err) + defer v2.DeleteSubnet(t, client, subnet.ID) + + // Create port + parentPort, err := v2.CreatePort(t, client, network.ID, subnet.ID) + th.AssertNoErr(t, err) + defer v2.DeletePort(t, client, parentPort.ID) + + subport1, err := v2.CreatePort(t, client, network.ID, subnet.ID) + th.AssertNoErr(t, err) + defer v2.DeletePort(t, client, subport1.ID) + + subport2, err := v2.CreatePort(t, client, network.ID, subnet.ID) + th.AssertNoErr(t, err) + defer v2.DeletePort(t, client, subport2.ID) + + trunk, err := CreateTrunk(t, client, parentPort.ID, subport1.ID, subport2.ID) + th.AssertNoErr(t, err) + defer DeleteTrunk(t, client, trunk.ID) + + tools.PrintResource(t, trunk) + + // Store the current revision number. + oldRevisionNumber := trunk.RevisionNumber + + // Update the trunk without revision number. + // This should work. + newName := tools.RandomString("TESTACC-", 8) + newDescription := "" + updateOpts := &trunks.UpdateOpts{ + Name: &newName, + Description: &newDescription, + } + trunk, err = trunks.Update(context.TODO(), client, trunk.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, trunk) + + // This should fail due to an old revision number. + newDescription = "new description" + updateOpts = &trunks.UpdateOpts{ + Name: &newName, + Description: &newDescription, + RevisionNumber: &oldRevisionNumber, + } + _, err = trunks.Update(context.TODO(), client, trunk.ID, updateOpts).Extract() + th.AssertErr(t, err) + if !strings.Contains(err.Error(), "RevisionNumberConstraintFailed") { + t.Fatalf("expected to see an error of type RevisionNumberConstraintFailed, but got the following error instead: %v", err) + } + + // Reread the trunk to show that it did not change. + trunk, err = trunks.Get(context.TODO(), client, trunk.ID).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, trunk) + + // This should work because now we do provide a valid revision number. + newDescription = "new description" + updateOpts = &trunks.UpdateOpts{ + Name: &newName, + Description: &newDescription, + RevisionNumber: &trunk.RevisionNumber, + } + trunk, err = trunks.Update(context.TODO(), client, trunk.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, trunk) + + th.AssertEquals(t, trunk.Name, newName) + th.AssertEquals(t, trunk.Description, newDescription) +} diff --git a/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/openstack/networking/v2/extensions/layer3/floatingips/requests.go index a3afb0403c..be8949d693 100644 --- a/openstack/networking/v2/extensions/layer3/floatingips/requests.go +++ b/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -2,6 +2,7 @@ package floatingips import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -37,6 +38,7 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // ToNetworkListQuery formats a ListOpts into a query string. @@ -144,6 +146,11 @@ type UpdateOpts struct { Description *string `json:"description,omitempty"` PortID *string `json:"port_id,omitempty"` FixedIP string `json:"fixed_ip_address,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder @@ -171,8 +178,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/openstack/networking/v2/extensions/layer3/floatingips/results.go b/openstack/networking/v2/extensions/layer3/floatingips/results.go index 50740ebf30..7ea6160032 100644 --- a/openstack/networking/v2/extensions/layer3/floatingips/results.go +++ b/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -56,6 +56,9 @@ type FloatingIP struct { // Tags optionally set via extensions/attributestags Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` } func (r *FloatingIP) UnmarshalJSON(b []byte) error { diff --git a/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go index dcc7977a46..f6ca654841 100644 --- a/openstack/networking/v2/extensions/layer3/routers/requests.go +++ b/openstack/networking/v2/extensions/layer3/routers/requests.go @@ -2,6 +2,7 @@ package routers import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -13,22 +14,23 @@ 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"` - Description string `q:"description"` - AdminStateUp *bool `q:"admin_state_up"` - 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"` - SortDir string `q:"sort_dir"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + 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"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // List returns a Pager which allows you to iterate over a collection of @@ -112,6 +114,11 @@ type UpdateOpts struct { Distributed *bool `json:"distributed,omitempty"` GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` Routes *[]Route `json:"routes,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToRouterUpdateMap builds an update body based on UpdateOpts. @@ -130,8 +137,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go index 25e8ab7b59..cb2d444034 100644 --- a/openstack/networking/v2/extensions/layer3/routers/results.go +++ b/openstack/networking/v2/extensions/layer3/routers/results.go @@ -77,6 +77,9 @@ type Router struct { // Tags optionally set via extensions/attributestags Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` } // RouterPage is the page returned by a pager when traversing over a diff --git a/openstack/networking/v2/extensions/qos/policies/requests.go b/openstack/networking/v2/extensions/qos/policies/requests.go index 88d9f85fb8..832ea2d73e 100644 --- a/openstack/networking/v2/extensions/qos/policies/requests.go +++ b/openstack/networking/v2/extensions/qos/policies/requests.go @@ -2,6 +2,7 @@ package policies import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" @@ -134,7 +135,6 @@ type ListOpts struct { ProjectID string `q:"project_id"` Name string `q:"name"` Description string `q:"description"` - RevisionNumber *int `q:"revision_number"` IsDefault *bool `q:"is_default"` Shared *bool `q:"shared"` Limit int `q:"limit"` @@ -145,6 +145,7 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // ToPolicyListQuery formats a ListOpts into a query string. @@ -243,6 +244,11 @@ type UpdateOpts struct { // IsDefault indicates if this QoS policy is default policy or not. IsDefault *bool `json:"is_default,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToPolicyUpdateMap builds a request body from UpdateOpts. @@ -258,8 +264,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, policyID string, r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, updateURL(c, policyID), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/openstack/networking/v2/extensions/qos/policies/results.go b/openstack/networking/v2/extensions/qos/policies/results.go index 70c70379d7..c35853bac8 100644 --- a/openstack/networking/v2/extensions/qos/policies/results.go +++ b/openstack/networking/v2/extensions/qos/policies/results.go @@ -79,7 +79,7 @@ type Policy struct { // Shared indicates whether this policy is shared across all projects. Shared bool `json:"shared"` - // RevisionNumber represents revision number of the policy. + // RevisionNumber optionally set via extensions/standard-attr-revisions RevisionNumber int `json:"revision_number"` // Rules represents QoS rules of the policy. diff --git a/openstack/networking/v2/extensions/security/groups/requests.go b/openstack/networking/v2/extensions/security/groups/requests.go index fabb744114..95f7bfabdf 100644 --- a/openstack/networking/v2/extensions/security/groups/requests.go +++ b/openstack/networking/v2/extensions/security/groups/requests.go @@ -2,6 +2,7 @@ package groups import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -13,20 +14,21 @@ 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"` - Description string `q:"description"` - Stateful *bool `q:"stateful"` - 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"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + Stateful *bool `q:"stateful"` + 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"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // List returns a Pager which allows you to iterate over a collection of @@ -104,6 +106,11 @@ type UpdateOpts struct { // Stateful indicates if the security group is stateful or stateless. Stateful *bool `json:"stateful,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToSecGroupUpdateMap builds a request body from UpdateOpts. @@ -118,9 +125,20 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/openstack/networking/v2/extensions/security/groups/results.go b/openstack/networking/v2/extensions/security/groups/results.go index b3aa2efb48..6a8a16fa86 100644 --- a/openstack/networking/v2/extensions/security/groups/results.go +++ b/openstack/networking/v2/extensions/security/groups/results.go @@ -41,6 +41,9 @@ type SecGroup struct { // Tags optionally set via extensions/attributestags Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` } func (r *SecGroup) UnmarshalJSON(b []byte) error { diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go index 8976224b42..77768a3dac 100644 --- a/openstack/networking/v2/extensions/security/rules/requests.go +++ b/openstack/networking/v2/extensions/security/rules/requests.go @@ -29,6 +29,7 @@ type ListOpts struct { Marker string `q:"marker"` SortKey string `q:"sort_key"` SortDir string `q:"sort_dir"` + RevisionNumber *int `q:"revision_number"` } // List returns a Pager which allows you to iterate over a collection of diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go index 0901ced578..9aa91dbe97 100644 --- a/openstack/networking/v2/extensions/security/rules/results.go +++ b/openstack/networking/v2/extensions/security/rules/results.go @@ -56,6 +56,9 @@ type SecGroupRule struct { // ProjectID is the project owner of this security group rule. ProjectID string `json:"project_id"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` } // SecGroupRulePage is the page returned by a pager when traversing over a diff --git a/openstack/networking/v2/extensions/subnetpools/requests.go b/openstack/networking/v2/extensions/subnetpools/requests.go index 8823e94b2f..b18427a6e7 100644 --- a/openstack/networking/v2/extensions/subnetpools/requests.go +++ b/openstack/networking/v2/extensions/subnetpools/requests.go @@ -2,6 +2,7 @@ package subnetpools import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -33,7 +34,6 @@ type ListOpts struct { 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"` @@ -42,6 +42,8 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + // type int does not allow to filter with revision_number=0 + RevisionNumber int `q:"revision_number"` } // ToSubnetPoolListQuery formats a ListOpts into a query string. @@ -201,6 +203,11 @@ type UpdateOpts struct { // IsDefault indicates if the subnetpool is default pool or not. IsDefault *bool `json:"is_default,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToSubnetPoolUpdateMap builds a request body from UpdateOpts. @@ -216,8 +223,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, subnetPoolID stri r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, updateURL(c, subnetPoolID), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/openstack/networking/v2/extensions/trunks/requests.go b/openstack/networking/v2/extensions/trunks/requests.go index a58d81ac17..ea4dba2507 100644 --- a/openstack/networking/v2/extensions/trunks/requests.go +++ b/openstack/networking/v2/extensions/trunks/requests.go @@ -2,6 +2,7 @@ package trunks import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -63,21 +64,22 @@ type ListOptsBuilder interface { // by a particular trunk attribute. SortDir sets the direction, and is either // `asc' or `desc'. Marker and Limit are used for pagination. type ListOpts struct { - AdminStateUp *bool `q:"admin_state_up"` - Description string `q:"description"` - ID string `q:"id"` - Name string `q:"name"` - PortID string `q:"port_id"` + AdminStateUp *bool `q:"admin_state_up"` + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + PortID string `q:"port_id"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + SortDir string `q:"sort_dir"` + SortKey string `q:"sort_key"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + // TODO change type to *int for consistency RevisionNumber string `q:"revision_number"` - Status string `q:"status"` - TenantID string `q:"tenant_id"` - ProjectID string `q:"project_id"` - SortDir string `q:"sort_dir"` - SortKey string `q:"sort_key"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` } // ToTrunkListQuery formats a ListOpts into a query string. @@ -122,6 +124,11 @@ type UpdateOpts struct { AdminStateUp *bool `json:"admin_state_up,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } func (opts UpdateOpts) ToTrunkUpdateMap() (map[string]any, error) { @@ -134,8 +141,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, updateURL(c, id), body, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/openstack/networking/v2/extensions/trunks/results.go b/openstack/networking/v2/extensions/trunks/results.go index 20edcf9a88..72efd636fb 100644 --- a/openstack/networking/v2/extensions/trunks/results.go +++ b/openstack/networking/v2/extensions/trunks/results.go @@ -81,6 +81,7 @@ type Trunk struct { // if the resource has not been updated, this field will show as null. UpdatedAt time.Time `json:"updated_at"` + // RevisionNumber optionally set via extensions/standard-attr-revisions RevisionNumber int `json:"revision_number"` // UUID of the trunk's parent port diff --git a/openstack/networking/v2/networks/requests.go b/openstack/networking/v2/networks/requests.go index 29d14187ab..d4dd64ff93 100644 --- a/openstack/networking/v2/networks/requests.go +++ b/openstack/networking/v2/networks/requests.go @@ -20,22 +20,23 @@ type ListOptsBuilder interface { // by a particular network attribute. SortDir sets the direction, and is either // `asc' or `desc'. Marker and Limit are used for pagination. type ListOpts struct { - Status string `q:"status"` - Name string `q:"name"` - Description string `q:"description"` - 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"` - Limit int `q:"limit"` - SortKey string `q:"sort_key"` - SortDir string `q:"sort_dir"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` + Status string `q:"status"` + Name string `q:"name"` + Description string `q:"description"` + 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"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // ToNetworkListQuery formats a ListOpts into a query string. diff --git a/openstack/networking/v2/ports/requests.go b/openstack/networking/v2/ports/requests.go index 218c2897f7..bfff2dffb2 100644 --- a/openstack/networking/v2/ports/requests.go +++ b/openstack/networking/v2/ports/requests.go @@ -41,6 +41,7 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` SecurityGroups []string `q:"security_groups"` FixedIPs []FixedIPOpts } diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go index db597d6864..150afd7394 100644 --- a/openstack/networking/v2/subnets/requests.go +++ b/openstack/networking/v2/subnets/requests.go @@ -42,6 +42,7 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // ToSubnetListQuery formats a ListOpts into a query string. From 99b0a51a254c4e3332a0117a6f2ed4461bdc99fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Thu, 6 Mar 2025 17:27:50 +0100 Subject: [PATCH 04/15] Improve support for network standard-attr-timestamp extension Add a few missing `CreatedAt` and `UpdatedAt` fields for networking resources. --- .../networking/v2/extensions/layer3/routers/results.go | 6 ++++++ .../networking/v2/extensions/security/rules/results.go | 8 ++++++++ openstack/networking/v2/subnets/results.go | 8 ++++++++ 3 files changed, 22 insertions(+) diff --git a/openstack/networking/v2/extensions/layer3/routers/results.go b/openstack/networking/v2/extensions/layer3/routers/results.go index cb2d444034..d75615b773 100644 --- a/openstack/networking/v2/extensions/layer3/routers/results.go +++ b/openstack/networking/v2/extensions/layer3/routers/results.go @@ -80,6 +80,12 @@ type Router struct { // RevisionNumber optionally set via extensions/standard-attr-revisions RevisionNumber int `json:"revision_number"` + + // Timestamp when the router was created + CreatedAt time.Time `json:"created_at"` + + // Timestamp when the router was last updated + UpdatedAt time.Time `json:"updated_at"` } // RouterPage is the page returned by a pager when traversing over a diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go index 9aa91dbe97..8a3355dfe0 100644 --- a/openstack/networking/v2/extensions/security/rules/results.go +++ b/openstack/networking/v2/extensions/security/rules/results.go @@ -1,6 +1,8 @@ package rules import ( + "time" + "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" ) @@ -59,6 +61,12 @@ type SecGroupRule struct { // RevisionNumber optionally set via extensions/standard-attr-revisions RevisionNumber int `json:"revision_number"` + + // Timestamp when the rule was created + CreatedAt time.Time `json:"created_at"` + + // Timestamp when the rule was last updated + UpdatedAt time.Time `json:"updated_at"` } // SecGroupRulePage is the page returned by a pager when traversing over a diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go index 7d5ba13cc5..01c6acc070 100644 --- a/openstack/networking/v2/subnets/results.go +++ b/openstack/networking/v2/subnets/results.go @@ -1,6 +1,8 @@ package subnets import ( + "time" + "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" ) @@ -121,6 +123,12 @@ type Subnet struct { // RevisionNumber optionally set via extensions/standard-attr-revisions RevisionNumber int `json:"revision_number"` + + // Timestamp when the subnet was created + CreatedAt time.Time `json:"created_at"` + + // Timestamp when the subnet was last updated + UpdatedAt time.Time `json:"updated_at"` } // SubnetPage is the page returned by a pager when traversing over a collection From 302bf61b00ffc2de5c4275296e6bf4a52645854d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Tue, 11 Mar 2025 08:26:17 +0100 Subject: [PATCH 05/15] CI: skip TestLayer3RouterRevision on bobcat Due to https://bugs.launchpad.net/neutron/+bug/2101871, which causes neutron to return a 500 error. --- .../openstack/networking/v2/extensions/layer3/routers_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go b/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go index 1df4375164..5930be2a1e 100644 --- a/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go +++ b/internal/acceptance/openstack/networking/v2/extensions/layer3/routers_test.go @@ -216,6 +216,8 @@ func TestLayer3RouterAgents(t *testing.T) { } func TestLayer3RouterRevision(t *testing.T) { + // https://bugs.launchpad.net/neutron/+bug/2101871 + clients.SkipRelease(t, "stable/2023.2") client, err := clients.NewNetworkV2Client() th.AssertNoErr(t, err) From 9e7a3d4942b49dfa736c8808314956be3733ce42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Tue, 11 Mar 2025 09:52:44 +0100 Subject: [PATCH 06/15] CI: Fix SkipRelease for stable branches Since 5819dc0c7b6c72a3d6cddb1584e8f28ad42a2cc8, `getReleaseFromEnv()` trims the `stable/` prefix, which caused the condition in `SkipRelease()` to fail if we looked for a release name including the prefix. Fix `SkipRelease()` to account for release names with a `stable/` prefix. --- internal/acceptance/clients/conditions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/acceptance/clients/conditions.go b/internal/acceptance/clients/conditions.go index 619d31a94e..c48ac208a9 100644 --- a/internal/acceptance/clients/conditions.go +++ b/internal/acceptance/clients/conditions.go @@ -119,7 +119,7 @@ func getReleaseFromEnv(t *testing.T) string { // release. Releases are named such as 'stable/mitaka', master, etc. func SkipRelease(t *testing.T, release string) { current := getReleaseFromEnv(t) - if current == release { + if current == strings.TrimPrefix(release, "stable/") { t.Skipf("this is not supported in %s", release) } } From e0b454daad41a88f229a56aa7f1a2af13cb2cb58 Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Fri, 29 Nov 2024 10:26:22 -0500 Subject: [PATCH 07/15] Merge pull request #3209 from shiftstack/proper-service-discovery Configure and respect service type aliases --- endpoint_search.go | 61 ++++++++++++++++++++++- openstack/blockstorage/noauth/requests.go | 4 +- openstack/client.go | 16 +++--- openstack/endpoint_location.go | 6 ++- service_client.go | 10 ++-- testing/endpoint_search_test.go | 4 +- 6 files changed, 83 insertions(+), 18 deletions(-) diff --git a/endpoint_search.go b/endpoint_search.go index 2fbc3c97f1..8818e769b8 100644 --- a/endpoint_search.go +++ b/endpoint_search.go @@ -1,5 +1,7 @@ package gophercloud +import "slices" + // Availability indicates to whom a specific service endpoint is accessible: // the internet at large, internal networks only, or only to administrators. // Different identity services use different terminology for these. Identity v2 @@ -22,6 +24,31 @@ const ( AvailabilityInternal Availability = "internal" ) +// ServiceTypeAliases contains a mapping of service types to any aliases, as +// defined by the OpenStack Service Types Authority. Only service types that +// we support are included. +var ServiceTypeAliases = map[string][]string{ + "application-container": {"container"}, + "baremetal": {"bare-metal"}, + "baremetal-introspection": {}, + "block-storage": {"block-store", "volume", "volumev2", "volumev3"}, + "compute": {}, + "container-infrastructure-management": {"container-infrastructure", "container-infra"}, + "database": {}, + "dns": {}, + "identity": {}, + "image": {}, + "key-manager": {}, + "load-balancer": {}, + "message": {"messaging"}, + "networking": {}, + "object-store": {}, + "orchestration": {}, + "placement": {}, + "shared-file-system": {"sharev2", "share"}, + "workflow": {"workflowv2"}, +} + // EndpointOpts specifies search criteria used by queries against an // OpenStack service catalog. The options must contain enough information to // unambiguously identify one, and only one, endpoint within the catalog. @@ -30,8 +57,9 @@ const ( // package, like "openstack.NewComputeV2()". type EndpointOpts struct { // Type [required] is the service type for the client (e.g., "compute", - // "object-store"). Generally, this will be supplied by the service client - // function, but a user-given value will be honored if provided. + // "object-store"), as defined by the OpenStack Service Types Authority. + // This will generally be supplied by the service client function, but a + // user-given value will be honored if provided. Type string // Name [optional] is the service name for the client (e.g., "nova") as it @@ -39,6 +67,13 @@ type EndpointOpts struct { // different Name, which is why both Type and Name are sometimes needed. Name string + // Aliases [optional] is the set of aliases of the service type (e.g. + // "volumev2"/"volumev3", "volume" and "block-store" for the + // "block-storage" service type), as defined by the OpenStack Service Types + // Authority. As with Type, this will generally be supplied by the service + // client function, but a user-given value will be honored if provided. + Aliases []string + // Region [required] is the geographic region in which the endpoint resides, // generally specifying which datacenter should house your resources. // Required only for services that span multiple regions. @@ -73,4 +108,26 @@ func (eo *EndpointOpts) ApplyDefaults(t string) { if eo.Availability == "" { eo.Availability = AvailabilityPublic } + if len(eo.Aliases) == 0 { + if aliases, ok := ServiceTypeAliases[eo.Type]; ok { + // happy path: user requested a service type by its official name + eo.Aliases = aliases + } else { + // unhappy path: user requested a service type by its alias or an + // invalid/unsupported service type + // TODO(stephenfin): This should probably be an error in v3 + for t, aliases := range ServiceTypeAliases { + if slices.Contains(aliases, eo.Type) { + // we intentionally override the service type, even if it + // was explicitly requested by the user + eo.Type = t + eo.Aliases = aliases + } + } + } + } +} + +func (eo *EndpointOpts) Types() []string { + return append([]string{eo.Type}, eo.Aliases...) } diff --git a/openstack/blockstorage/noauth/requests.go b/openstack/blockstorage/noauth/requests.go index 87f2d9b7f9..cf887553f0 100644 --- a/openstack/blockstorage/noauth/requests.go +++ b/openstack/blockstorage/noauth/requests.go @@ -51,10 +51,10 @@ func initClientOpts(client *gophercloud.ProviderClient, eo EndpointOpts, clientT // NewBlockStorageNoAuthV2 creates a ServiceClient that may be used to access "noauth" v2 block storage service. func NewBlockStorageNoAuthV2(client *gophercloud.ProviderClient, eo EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "volumev2") + return initClientOpts(client, eo, "block-storage") } // NewBlockStorageNoAuthV3 creates a ServiceClient that may be used to access "noauth" v3 block storage service. func NewBlockStorageNoAuthV3(client *gophercloud.ProviderClient, eo EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "volumev3") + return initClientOpts(client, eo, "block-storage") } diff --git a/openstack/client.go b/openstack/client.go index 43b569d3b4..122a3ee699 100644 --- a/openstack/client.go +++ b/openstack/client.go @@ -344,6 +344,7 @@ func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp }, nil } +// TODO(stephenfin): Allow passing aliases to all New${SERVICE}V${VERSION} methods in v3 func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { sc := new(gophercloud.ServiceClient) eo.ApplyDefaults(clientType) @@ -393,6 +394,7 @@ func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpt return sc, err } +// TODO(stephenfin): Remove this in v3. We no longer support the V1 Block Storage service. // NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 // block storage service. func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { @@ -402,17 +404,17 @@ func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoi // NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 // block storage service. func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "volumev2") + return initClientOpts(client, eo, "block-storage") } // NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service. func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "volumev3") + return initClientOpts(client, eo, "block-storage") } // NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "sharev2") + return initClientOpts(client, eo, "shared-file-system") } // NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 @@ -457,14 +459,14 @@ func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi // 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, err := initClientOpts(client, eo, "message") sc.MoreHeaders = map[string]string{"Client-ID": clientID} return sc, err } // NewContainerV1 creates a ServiceClient that may be used with v1 container package func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "container") + return initClientOpts(client, eo, "application-container") } // NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key @@ -478,12 +480,12 @@ func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoint // NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management // package. func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "container-infra") + return initClientOpts(client, eo, "container-infrastructure-management") } // NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "workflowv2") + return initClientOpts(client, eo, "workflow") } // NewPlacementV1 creates a ServiceClient that may be used with the placement package. diff --git a/openstack/endpoint_location.go b/openstack/endpoint_location.go index 2cdbd3e7f7..14cff0d755 100644 --- a/openstack/endpoint_location.go +++ b/openstack/endpoint_location.go @@ -1,6 +1,8 @@ package openstack import ( + "slices" + "github.com/gophercloud/gophercloud/v2" tokens2 "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens" tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" @@ -20,7 +22,7 @@ func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpt // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. var endpoints = make([]tokens2.Endpoint, 0, 1) for _, entry := range catalog.Entries { - if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { for _, endpoint := range entry.Endpoints { if opts.Region == "" || endpoint.Region == opts.Region { endpoints = append(endpoints, endpoint) @@ -74,7 +76,7 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt // Name if provided, and Region if provided. var endpoints = make([]tokens3.Endpoint, 0, 1) for _, entry := range catalog.Entries { - if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { for _, endpoint := range entry.Endpoints { if opts.Availability != gophercloud.AvailabilityAdmin && opts.Availability != gophercloud.AvailabilityPublic && diff --git a/service_client.go b/service_client.go index 11b80108c3..c1f9f41d4d 100644 --- a/service_client.go +++ b/service_client.go @@ -115,13 +115,17 @@ func (client *ServiceClient) Head(ctx context.Context, url string, opts *Request } func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { + serviceType := client.Type + switch client.Type { case "compute": opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion - case "sharev2": + case "shared-file-system", "sharev2", "share": opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion - case "volume": + case "block-storage", "block-store", "volume", "volumev3": opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion + // cinder should accept block-storage but (as of Dalmatian) does not + serviceType = "volume" case "baremetal": opts.MoreHeaders["X-OpenStack-Ironic-API-Version"] = client.Microversion case "baremetal-introspection": @@ -129,7 +133,7 @@ func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { } if client.Type != "" { - opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion + opts.MoreHeaders["OpenStack-API-Version"] = serviceType + " " + client.Microversion } } diff --git a/testing/endpoint_search_test.go b/testing/endpoint_search_test.go index 278955ecdf..c1ae9b92db 100644 --- a/testing/endpoint_search_test.go +++ b/testing/endpoint_search_test.go @@ -10,11 +10,11 @@ import ( func TestApplyDefaultsToEndpointOpts(t *testing.T) { eo := gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic} eo.ApplyDefaults("compute") - expected := gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute"} + expected := gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute", Aliases: []string{}} th.CheckDeepEquals(t, expected, eo) eo = gophercloud.EndpointOpts{Type: "compute"} eo.ApplyDefaults("object-store") - expected = gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute"} + expected = gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic, Type: "compute", Aliases: []string{}} th.CheckDeepEquals(t, expected, eo) } From 88b963b4c7880b8533edc58abb4985bc521e2e39 Mon Sep 17 00:00:00 2001 From: dkokkino Date: Thu, 20 Feb 2025 12:36:13 +0100 Subject: [PATCH 08/15] Enhance Snapshot struct and add ListDetail function in V3 blockstorage - Extended Snapshot struct to include additional fields: Progress, ProjectID, GroupSnapshotID, UserID, ConsumesQuota - Implemented ListDetail function to retrieve detailed snapshot information from the snapshots/detail endpoint - Added new test to validate the inclusion of additional fields and the functionality of the ListDetails function Fixes: #2403 --- .../blockstorage/v3/snapshots_test.go | 17 ++++++ .../blockstorage/v3/snapshots/requests.go | 15 +++++ .../blockstorage/v3/snapshots/results.go | 15 +++++ .../v3/snapshots/testing/fixtures_test.go | 56 +++++++++++++++++++ .../v3/snapshots/testing/requests_test.go | 56 +++++++++++++++++++ openstack/blockstorage/v3/snapshots/urls.go | 4 ++ 6 files changed, 163 insertions(+) diff --git a/internal/acceptance/openstack/blockstorage/v3/snapshots_test.go b/internal/acceptance/openstack/blockstorage/v3/snapshots_test.go index bdb4c8fed3..18f045fc30 100644 --- a/internal/acceptance/openstack/blockstorage/v3/snapshots_test.go +++ b/internal/acceptance/openstack/blockstorage/v3/snapshots_test.go @@ -73,7 +73,24 @@ func TestSnapshots(t *testing.T) { return true, nil }) + th.AssertNoErr(t, err) + + err = snapshots.ListDetail(client, listOpts).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) { + actual, err := snapshots.ExtractSnapshots(page) + th.AssertNoErr(t, err) + th.AssertEquals(t, 1, len(actual)) + + var found bool + for _, v := range actual { + if v.ID == snapshot1.ID || v.ID == snapshot2.ID { + found = true + } + } + th.AssertEquals(t, found, true) + + return true, nil + }) th.AssertNoErr(t, err) } diff --git a/openstack/blockstorage/v3/snapshots/requests.go b/openstack/blockstorage/v3/snapshots/requests.go index 644e9cb1d3..85b67df879 100644 --- a/openstack/blockstorage/v3/snapshots/requests.go +++ b/openstack/blockstorage/v3/snapshots/requests.go @@ -122,6 +122,21 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa }) } +// ListDetail returns Snapshots with additional details optionally limited by the conditions provided in ListOpts. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailsURL(client) + if opts != nil { + query, err := opts.ToSnapshotListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SnapshotPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + // UpdateOptsBuilder allows extensions to add additional parameters to the // Update request. type UpdateOptsBuilder interface { diff --git a/openstack/blockstorage/v3/snapshots/results.go b/openstack/blockstorage/v3/snapshots/results.go index 8a1440da42..87a2dec2c6 100644 --- a/openstack/blockstorage/v3/snapshots/results.go +++ b/openstack/blockstorage/v3/snapshots/results.go @@ -36,6 +36,21 @@ type Snapshot struct { // User-defined key-value pairs. Metadata map[string]string `json:"metadata"` + + // Progress of the snapshot creation. + Progress string `json:"os-extended-snapshot-attributes:progress"` + + // Project ID that owns the snapshot. + ProjectID string `json:"os-extended-snapshot-attributes:project_id"` + + // ID of the group snapshot, if applicable. + GroupSnapshotID string `json:"group_snapshot_id"` + + // User ID that created the snapshot. + UserID string `json:"user_id"` + + // Indicates whether the snapshot consumes quota. + ConsumesQuota bool `json:"consumes_quota"` } // CreateResult contains the response body and error from a Create request. diff --git a/openstack/blockstorage/v3/snapshots/testing/fixtures_test.go b/openstack/blockstorage/v3/snapshots/testing/fixtures_test.go index 25e54b9b3e..e1bfd37584 100644 --- a/openstack/blockstorage/v3/snapshots/testing/fixtures_test.go +++ b/openstack/blockstorage/v3/snapshots/testing/fixtures_test.go @@ -61,6 +61,62 @@ func MockListResponse(t *testing.T) { }) } +func MockListDetailsResponse(t *testing.T) { + th.Mux.HandleFunc("/snapshots/detail", 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) + + if err := r.ParseForm(); err != nil { + t.Errorf("Failed to parse request form %v", err) + } + marker := r.Form.Get("marker") + switch marker { + case "": + fmt.Fprint(w, ` + { + "snapshots": [ + { + "id": "289da7f8-6440-407c-9fb4-7db01ec49164", + "name": "snapshot-001", + "volume_id": "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + "description": "Daily Backup", + "status": "available", + "size": 30, + "created_at": "2017-05-30T03:35:03.000000", + "os-extended-snapshot-attributes:progress": "100%", + "os-extended-snapshot-attributes:project_id": "84b8950a-8594-4e5b-8dce-0dfa9c696357", + "group_snapshot_id": null, + "user_id": "075da7f8-6440-407c-9fb4-7db01ec49531", + "consumes_quota": true + }, + { + "id": "96c3bda7-c82a-4f50-be73-ca7621794835", + "name": "snapshot-002", + "volume_id": "76b8950a-8594-4e5b-8dce-0dfa9c696358", + "description": "Weekly Backup", + "status": "available", + "size": 25, + "created_at": "2017-05-30T03:35:03.000000", + "os-extended-snapshot-attributes:progress": "50%", + "os-extended-snapshot-attributes:project_id": "84b8950a-8594-4e5b-8dce-0dfa9c696357", + "group_snapshot_id": "865da7f8-6440-407c-9fb4-7db01ec40876", + "user_id": "075da7f8-6440-407c-9fb4-7db01ec49531", + "consumes_quota": false + } + ] + } + `) + case "1": + fmt.Fprint(w, `{"snapshots": []}`) + default: + t.Fatalf("Unexpected marker: [%s]", marker) + } + }) +} + // MockGetResponse provides mock response for get snapshot API call func MockGetResponse(t *testing.T) { th.Mux.HandleFunc("/snapshots/d32019d3-bc6e-4319-9c1d-6722fc136a22", func(w http.ResponseWriter, r *http.Request) { diff --git a/openstack/blockstorage/v3/snapshots/testing/requests_test.go b/openstack/blockstorage/v3/snapshots/testing/requests_test.go index e0ca0b8e0f..aca65ba7a0 100644 --- a/openstack/blockstorage/v3/snapshots/testing/requests_test.go +++ b/openstack/blockstorage/v3/snapshots/testing/requests_test.go @@ -59,6 +59,62 @@ func TestList(t *testing.T) { } } +func TestDetailList(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockListDetailsResponse(t) + + count := 0 + + err := snapshots.ListDetail(client.ServiceClient(), &snapshots.ListOpts{}).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) { + count++ + actual, err := snapshots.ExtractSnapshots(page) + if err != nil { + t.Errorf("Failed to extract snapshots: %v", err) + return false, err + } + expected := []snapshots.Snapshot{ + { + ID: "289da7f8-6440-407c-9fb4-7db01ec49164", + Name: "snapshot-001", + VolumeID: "521752a6-acf6-4b2d-bc7a-119f9148cd8c", + Status: "available", + Size: 30, + CreatedAt: time.Date(2017, 5, 30, 3, 35, 3, 0, time.UTC), + Description: "Daily Backup", + Progress: "100%", + ProjectID: "84b8950a-8594-4e5b-8dce-0dfa9c696357", + GroupSnapshotID: "", + UserID: "075da7f8-6440-407c-9fb4-7db01ec49531", + ConsumesQuota: true, + }, + { + ID: "96c3bda7-c82a-4f50-be73-ca7621794835", + Name: "snapshot-002", + VolumeID: "76b8950a-8594-4e5b-8dce-0dfa9c696358", + Status: "available", + Size: 25, + CreatedAt: time.Date(2017, 5, 30, 3, 35, 3, 0, time.UTC), + Description: "Weekly Backup", + Progress: "50%", + ProjectID: "84b8950a-8594-4e5b-8dce-0dfa9c696357", + GroupSnapshotID: "865da7f8-6440-407c-9fb4-7db01ec40876", + UserID: "075da7f8-6440-407c-9fb4-7db01ec49531", + ConsumesQuota: false, + }, + } + th.CheckDeepEquals(t, expected, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + func TestGet(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/openstack/blockstorage/v3/snapshots/urls.go b/openstack/blockstorage/v3/snapshots/urls.go index b68cee9ccf..de6a83cc32 100644 --- a/openstack/blockstorage/v3/snapshots/urls.go +++ b/openstack/blockstorage/v3/snapshots/urls.go @@ -18,6 +18,10 @@ func listURL(c *gophercloud.ServiceClient) string { return createURL(c) } +func listDetailsURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("snapshots", "detail") +} + func updateURL(c *gophercloud.ServiceClient, id string) string { return deleteURL(c, id) } From b76a690afa47c92e990fefa9f75f4583d91ea95d Mon Sep 17 00:00:00 2001 From: Bodo Petermann Date: Wed, 26 Feb 2025 13:55:45 +0100 Subject: [PATCH 09/15] vpnaas: add support for more ciphers (auth, encryption, pfs modes) For VPNaaS IKE policies and IPSEC policies several choices for encryption algorithms, auth algorithms and PFS modes have been added. Encryption algorithms: add AES CCM mode and AES GCM mode variants for 128/192/256 bit keys and 8/12/16 octet ICVs. Add AES CTR modes for 128/192/256 bit keys. Auth algorithms: add aes-xcbc and aes-cmac. PFS: add Diffie Hellman groups 15 to 31. Closes: #3314 --- .../extensions/vpnaas/ikepolicies/requests.go | 72 ++++++++++++++---- .../vpnaas/ipsecpolicies/requests.go | 76 ++++++++++++++----- 2 files changed, 114 insertions(+), 34 deletions(-) diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go index 7d5c81e13b..f0845d6a60 100644 --- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go @@ -15,22 +15,62 @@ 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" + AuthAlgorithmSHA1 AuthAlgorithm = "sha1" + AuthAlgorithmSHA256 AuthAlgorithm = "sha256" + AuthAlgorithmSHA384 AuthAlgorithm = "sha384" + AuthAlgorithmSHA512 AuthAlgorithm = "sha512" + AuthAlgorithmAESXCBC AuthAlgorithm = "aes-xcbc" + AuthAlgorithmAESCMAC AuthAlgorithm = "aes-cmac" + EncryptionAlgorithm3DES EncryptionAlgorithm = "3des" + EncryptionAlgorithmAES128 EncryptionAlgorithm = "aes-128" + EncryptionAlgorithmAES192 EncryptionAlgorithm = "aes-192" + EncryptionAlgorithmAES256 EncryptionAlgorithm = "aes-256" + EncryptionAlgorithmAES128CTR EncryptionAlgorithm = "aes-128-ctr" + EncryptionAlgorithmAES192CTR EncryptionAlgorithm = "aes-192-ctr" + EncryptionAlgorithmAES256CTR EncryptionAlgorithm = "aes-256-ctr" + EncryptionAlgorithmAES128CCM8 EncryptionAlgorithm = "aes-128-ccm-8" + EncryptionAlgorithmAES128CCM12 EncryptionAlgorithm = "aes-128-ccm-12" + EncryptionAlgorithmAES128CCM16 EncryptionAlgorithm = "aes-128-ccm-16" + EncryptionAlgorithmAES192CCM8 EncryptionAlgorithm = "aes-192-ccm-8" + EncryptionAlgorithmAES192CCM12 EncryptionAlgorithm = "aes-192-ccm-12" + EncryptionAlgorithmAES192CCM16 EncryptionAlgorithm = "aes-192-ccm-16" + EncryptionAlgorithmAES256CCM8 EncryptionAlgorithm = "aes-256-ccm-8" + EncryptionAlgorithmAES256CCM12 EncryptionAlgorithm = "aes-256-ccm-12" + EncryptionAlgorithmAES256CCM16 EncryptionAlgorithm = "aes-256-ccm-16" + EncryptionAlgorithmAES128GCM8 EncryptionAlgorithm = "aes-128-gcm-8" + EncryptionAlgorithmAES128GCM12 EncryptionAlgorithm = "aes-128-gcm-12" + EncryptionAlgorithmAES128GCM16 EncryptionAlgorithm = "aes-128-gcm-16" + EncryptionAlgorithmAES192GCM8 EncryptionAlgorithm = "aes-192-gcm-8" + EncryptionAlgorithmAES192GCM12 EncryptionAlgorithm = "aes-192-gcm-12" + EncryptionAlgorithmAES192GCM16 EncryptionAlgorithm = "aes-192-gcm-16" + EncryptionAlgorithmAES256GCM8 EncryptionAlgorithm = "aes-256-gcm-8" + EncryptionAlgorithmAES256GCM12 EncryptionAlgorithm = "aes-256-gcm-12" + EncryptionAlgorithmAES256GCM16 EncryptionAlgorithm = "aes-256-gcm-16" + UnitSeconds Unit = "seconds" + UnitKilobytes Unit = "kilobytes" + PFSGroup2 PFS = "group2" + PFSGroup5 PFS = "group5" + PFSGroup14 PFS = "group14" + PFSGroup15 PFS = "group15" + PFSGroup16 PFS = "group16" + PFSGroup17 PFS = "group17" + PFSGroup18 PFS = "group18" + PFSGroup19 PFS = "group19" + PFSGroup20 PFS = "group20" + PFSGroup21 PFS = "group21" + PFSGroup22 PFS = "group22" + PFSGroup23 PFS = "group23" + PFSGroup24 PFS = "group24" + PFSGroup25 PFS = "group25" + PFSGroup26 PFS = "group26" + PFSGroup27 PFS = "group27" + PFSGroup28 PFS = "group28" + PFSGroup29 PFS = "group29" + PFSGroup30 PFS = "group30" + PFSGroup31 PFS = "group31" + IKEVersionv1 IKEVersion = "v1" + IKEVersionv2 IKEVersion = "v2" + Phase1NegotiationModeMain Phase1NegotiationMode = "main" ) // CreateOptsBuilder allows extensions to add additional parameters to the diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go index 4397c0587b..9f34895c15 100644 --- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go +++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go @@ -15,24 +15,64 @@ 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" + TransformProtocolESP TransformProtocol = "esp" + TransformProtocolAH TransformProtocol = "ah" + TransformProtocolAHESP TransformProtocol = "ah-esp" + AuthAlgorithmSHA1 AuthAlgorithm = "sha1" + AuthAlgorithmSHA256 AuthAlgorithm = "sha256" + AuthAlgorithmSHA384 AuthAlgorithm = "sha384" + AuthAlgorithmSHA512 AuthAlgorithm = "sha512" + AuthAlgorithmAESXCBC AuthAlgorithm = "aes-xcbc" + AuthAlgorithmAESCMAC AuthAlgorithm = "aes-cmac" + EncryptionAlgorithm3DES EncryptionAlgorithm = "3des" + EncryptionAlgorithmAES128 EncryptionAlgorithm = "aes-128" + EncryptionAlgorithmAES192 EncryptionAlgorithm = "aes-192" + EncryptionAlgorithmAES256 EncryptionAlgorithm = "aes-256" + EncryptionAlgorithmAES128CTR EncryptionAlgorithm = "aes-128-ctr" + EncryptionAlgorithmAES192CTR EncryptionAlgorithm = "aes-192-ctr" + EncryptionAlgorithmAES256CTR EncryptionAlgorithm = "aes-256-ctr" + EncryptionAlgorithmAES128CCM8 EncryptionAlgorithm = "aes-128-ccm-8" + EncryptionAlgorithmAES128CCM12 EncryptionAlgorithm = "aes-128-ccm-12" + EncryptionAlgorithmAES128CCM16 EncryptionAlgorithm = "aes-128-ccm-16" + EncryptionAlgorithmAES192CCM8 EncryptionAlgorithm = "aes-192-ccm-8" + EncryptionAlgorithmAES192CCM12 EncryptionAlgorithm = "aes-192-ccm-12" + EncryptionAlgorithmAES192CCM16 EncryptionAlgorithm = "aes-192-ccm-16" + EncryptionAlgorithmAES256CCM8 EncryptionAlgorithm = "aes-256-ccm-8" + EncryptionAlgorithmAES256CCM12 EncryptionAlgorithm = "aes-256-ccm-12" + EncryptionAlgorithmAES256CCM16 EncryptionAlgorithm = "aes-256-ccm-16" + EncryptionAlgorithmAES128GCM8 EncryptionAlgorithm = "aes-128-gcm-8" + EncryptionAlgorithmAES128GCM12 EncryptionAlgorithm = "aes-128-gcm-12" + EncryptionAlgorithmAES128GCM16 EncryptionAlgorithm = "aes-128-gcm-16" + EncryptionAlgorithmAES192GCM8 EncryptionAlgorithm = "aes-192-gcm-8" + EncryptionAlgorithmAES192GCM12 EncryptionAlgorithm = "aes-192-gcm-12" + EncryptionAlgorithmAES192GCM16 EncryptionAlgorithm = "aes-192-gcm-16" + EncryptionAlgorithmAES256GCM8 EncryptionAlgorithm = "aes-256-gcm-8" + EncryptionAlgorithmAES256GCM12 EncryptionAlgorithm = "aes-256-gcm-12" + EncryptionAlgorithmAES256GCM16 EncryptionAlgorithm = "aes-256-gcm-16" + EncapsulationModeTunnel EncapsulationMode = "tunnel" + EncapsulationModeTransport EncapsulationMode = "transport" + UnitSeconds Unit = "seconds" + UnitKilobytes Unit = "kilobytes" + PFSGroup2 PFS = "group2" + PFSGroup5 PFS = "group5" + PFSGroup14 PFS = "group14" + PFSGroup15 PFS = "group15" + PFSGroup16 PFS = "group16" + PFSGroup17 PFS = "group17" + PFSGroup18 PFS = "group18" + PFSGroup19 PFS = "group19" + PFSGroup20 PFS = "group20" + PFSGroup21 PFS = "group21" + PFSGroup22 PFS = "group22" + PFSGroup23 PFS = "group23" + PFSGroup24 PFS = "group24" + PFSGroup25 PFS = "group25" + PFSGroup26 PFS = "group26" + PFSGroup27 PFS = "group27" + PFSGroup28 PFS = "group28" + PFSGroup29 PFS = "group29" + PFSGroup30 PFS = "group30" + PFSGroup31 PFS = "group31" ) // CreateOptsBuilder allows extensions to add additional parameters to the From c9240e7aa89fd0782b7fb8e651e0a4cace68edca Mon Sep 17 00:00:00 2001 From: Sharpz7 Date: Mon, 24 Feb 2025 21:57:36 +0000 Subject: [PATCH 10/15] Added support for VIF's in Baremetal --- .../openstack/baremetal/v1/nodes_test.go | 47 +++++++++++ openstack/baremetal/v1/nodes/requests.go | 58 ++++++++++++++ openstack/baremetal/v1/nodes/results.go | 37 +++++++++ .../v1/nodes/testing/fixtures_test.go | 75 +++++++++++++++++ .../v1/nodes/testing/requests_test.go | 80 +++++++++++++++++++ openstack/baremetal/v1/nodes/urls.go | 8 ++ 6 files changed, 305 insertions(+) diff --git a/internal/acceptance/openstack/baremetal/v1/nodes_test.go b/internal/acceptance/openstack/baremetal/v1/nodes_test.go index 669ea69cdc..b3a2923fbf 100644 --- a/internal/acceptance/openstack/baremetal/v1/nodes_test.go +++ b/internal/acceptance/openstack/baremetal/v1/nodes_test.go @@ -272,3 +272,50 @@ func TestNodesServicingHold(t *testing.T) { }, nodes.Active) th.AssertNoErr(t, err) } + +func TestNodesVirtualInterfaces(t *testing.T) { + clients.SkipReleasesBelow(t, "stable/2023.2") // Adjust based on when this feature was added + clients.RequireLong(t) + + client, err := clients.NewBareMetalV1Client() + th.AssertNoErr(t, err) + client.Microversion = "1.28" // VIFs were added in API version 1.28 + + node, err := CreateNode(t, client) + th.AssertNoErr(t, err) + defer DeleteNode(t, client, node) + + // First, list VIFs (should be empty initially) + vifs, err := nodes.ListVirtualInterfaces(context.TODO(), client, node.UUID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, 0, len(vifs)) + + // For a real test, we would need a valid VIF ID from the networking service + // Since this is difficult in a test environment, we can test the API call + // with a fake ID and expect it to fail with a specific error + fakeVifID := "1974dcfa-836f-41b2-b541-686c100900e5" + + // Try to attach a VIF (this will likely fail with a 404 Not Found since the VIF doesn't exist) + err = nodes.AttachVirtualInterface(context.TODO(), client, node.UUID, nodes.VirtualInterfaceOpts{ + ID: fakeVifID, + }).ExtractErr() + + // We expect this to fail, but we're testing the API call itself + // In a real environment with valid VIFs, you would check for success instead + if err == nil { + t.Logf("Warning: Expected error when attaching non-existent VIF, but got success. This might indicate the test environment has a VIF with ID %s", fakeVifID) + } + + // Try to detach a VIF (this will likely fail with a 404 Not Found) + err = nodes.DetachVirtualInterface(context.TODO(), client, node.UUID, fakeVifID).ExtractErr() + + // Again, we expect this to fail in most test environments + if err == nil { + t.Logf("Warning: Expected error when detaching non-existent VIF, but got success. This might indicate the test environment has a VIF with ID %s", fakeVifID) + } + + // List VIFs again to confirm state hasn't changed + vifs, err = nodes.ListVirtualInterfaces(context.TODO(), client, node.UUID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, 0, len(vifs)) +} diff --git a/openstack/baremetal/v1/nodes/requests.go b/openstack/baremetal/v1/nodes/requests.go index 8b2eb4b218..cddd269a3d 100644 --- a/openstack/baremetal/v1/nodes/requests.go +++ b/openstack/baremetal/v1/nodes/requests.go @@ -1002,3 +1002,61 @@ func DetachVirtualMedia(ctx context.Context, client *gophercloud.ServiceClient, _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return } + +// VirtualInterfaceOpts defines options for attaching a VIF to a node +type VirtualInterfaceOpts struct { + // The UUID or name of the VIF + ID string `json:"id" required:"true"` + // The UUID of a port to attach the VIF to. Cannot be specified with PortgroupUUID + PortUUID string `json:"port_uuid,omitempty"` + // The UUID of a portgroup to attach the VIF to. Cannot be specified with PortUUID + PortgroupUUID string `json:"portgroup_uuid,omitempty"` +} + +// VirtualInterfaceOptsBuilder allows extensions to add additional parameters to the +// AttachVirtualInterface request. +type VirtualInterfaceOptsBuilder interface { + ToVirtualInterfaceMap() (map[string]any, error) +} + +// ToVirtualInterfaceMap assembles a request body based on the contents of a VirtualInterfaceOpts. +func (opts VirtualInterfaceOpts) ToVirtualInterfaceMap() (map[string]any, error) { + if opts.PortUUID != "" && opts.PortgroupUUID != "" { + return nil, fmt.Errorf("cannot specify both port_uuid and portgroup_uuid") + } + + return gophercloud.BuildRequestBody(opts, "") +} + +// ListVirtualInterfaces returns a list of VIFs that are attached to the node. +func ListVirtualInterfaces(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ListVirtualInterfacesResult) { + resp, err := client.Get(ctx, virtualInterfaceURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// AttachVirtualInterface attaches a VIF to a node. +func AttachVirtualInterface(ctx context.Context, client *gophercloud.ServiceClient, id string, opts VirtualInterfaceOptsBuilder) (r VirtualInterfaceAttachResult) { + reqBody, err := opts.ToVirtualInterfaceMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(ctx, virtualInterfaceURL(client, id), reqBody, nil, &gophercloud.RequestOpts{ + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DetachVirtualInterface detaches a VIF from a node. +func DetachVirtualInterface(ctx context.Context, client *gophercloud.ServiceClient, id string, vifID string) (r VirtualInterfaceDetachResult) { + resp, err := client.Delete(ctx, virtualInterfaceDeleteURL(client, id, vifID), &gophercloud.RequestOpts{ + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/baremetal/v1/nodes/results.go b/openstack/baremetal/v1/nodes/results.go index 507e0d6a4f..3e1976d3ef 100644 --- a/openstack/baremetal/v1/nodes/results.go +++ b/openstack/baremetal/v1/nodes/results.go @@ -710,3 +710,40 @@ type VirtualMediaAttachResult struct { type VirtualMediaDetachResult struct { gophercloud.ErrResult } + +// VirtualInterfaceAttachResult is the response from an AttachVirtualInterface operation. +type VirtualInterfaceAttachResult struct { + gophercloud.ErrResult +} + +// VirtualInterfaceDetachResult is the response from a DetachVirtualInterface operation. +type VirtualInterfaceDetachResult struct { + gophercloud.ErrResult +} + +// VIF represents a virtual interface attached to a node. +type VIF struct { + // The UUID or name of the VIF + ID string `json:"id"` +} + +// ListVirtualInterfacesResult is the response from a ListVirtualInterfaces operation. +type ListVirtualInterfacesResult struct { + gophercloud.Result + gophercloud.HeaderResult +} + +// Extract interprets any ListVirtualInterfacesResult as a list of VIFs. +func (r ListVirtualInterfacesResult) Extract() ([]VIF, error) { + var s struct { + VIFs []VIF `json:"vifs"` + } + + err := r.Result.ExtractInto(&s) + return s.VIFs, err +} + +// ExtractHeader interprets any ListVirtualInterfacesResult as a HeaderResult. +func (r ListVirtualInterfacesResult) ExtractHeader() (gophercloud.HeaderResult, error) { + return r.HeaderResult, nil +} diff --git a/openstack/baremetal/v1/nodes/testing/fixtures_test.go b/openstack/baremetal/v1/nodes/testing/fixtures_test.go index 88abebcebb..282d5dbc7f 100644 --- a/openstack/baremetal/v1/nodes/testing/fixtures_test.go +++ b/openstack/baremetal/v1/nodes/testing/fixtures_test.go @@ -1849,3 +1849,78 @@ func HandleDetachVirtualMediaSuccessfully(t *testing.T, withType bool) { w.WriteHeader(http.StatusNoContent) }) } + +// HandleListVirtualInterfacesSuccessfully sets up the test server to respond to a ListVirtualInterfaces request +func HandleListVirtualInterfacesSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/nodes/1234asdf/vifs", + 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") + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ` +{ + "vifs": [ + { + "id": "1974dcfa-836f-41b2-b541-686c100900e5" + } + ] +}`) + }) +} + +// HandleAttachVirtualInterfaceSuccessfully sets up the test server to respond to an AttachVirtualInterface request +func HandleAttachVirtualInterfaceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/nodes/1234asdf/vifs", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5"}`) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleAttachVirtualInterfaceWithPortSuccessfully sets up the test server to respond to an AttachVirtualInterface request with port +func HandleAttachVirtualInterfaceWithPortSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/nodes/1234asdf/vifs", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5","port_uuid":"b2f96298-5172-45e9-b174-8d1ba936ab47"}`) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleAttachVirtualInterfaceWithPortgroupSuccessfully sets up the test server to respond to an AttachVirtualInterface request with portgroup +func HandleAttachVirtualInterfaceWithPortgroupSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/nodes/1234asdf/vifs", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5","portgroup_uuid":"c24944b5-a52e-4c5c-9c0a-52a0235a08a2"}`) + + w.WriteHeader(http.StatusNoContent) + }) +} + +// HandleDetachVirtualInterfaceSuccessfully sets up the test server to respond to a DetachVirtualInterface request +func HandleDetachVirtualInterfaceSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/nodes/1234asdf/vifs/1974dcfa-836f-41b2-b541-686c100900e5", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.WriteHeader(http.StatusNoContent) + }) +} diff --git a/openstack/baremetal/v1/nodes/testing/requests_test.go b/openstack/baremetal/v1/nodes/testing/requests_test.go index 5529e795ad..487c350175 100644 --- a/openstack/baremetal/v1/nodes/testing/requests_test.go +++ b/openstack/baremetal/v1/nodes/testing/requests_test.go @@ -829,3 +829,83 @@ func TestVirtualMediaDetachWithTypes(t *testing.T) { err := nodes.DetachVirtualMedia(context.TODO(), c, "1234asdf", opts).ExtractErr() th.AssertNoErr(t, err) } + +func TestListVirtualInterfaces(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListVirtualInterfacesSuccessfully(t) + + c := client.ServiceClient() + actual, err := nodes.ListVirtualInterfaces(context.TODO(), c, "1234asdf").Extract() + th.AssertNoErr(t, err) + + expected := []nodes.VIF{ + { + ID: "1974dcfa-836f-41b2-b541-686c100900e5", + }, + } + + th.CheckDeepEquals(t, expected, actual) +} + +func TestAttachVirtualInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAttachVirtualInterfaceSuccessfully(t) + + c := client.ServiceClient() + opts := nodes.VirtualInterfaceOpts{ + ID: "1974dcfa-836f-41b2-b541-686c100900e5", + } + err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAttachVirtualInterfaceWithPort(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAttachVirtualInterfaceWithPortSuccessfully(t) + + c := client.ServiceClient() + opts := nodes.VirtualInterfaceOpts{ + ID: "1974dcfa-836f-41b2-b541-686c100900e5", + PortUUID: "b2f96298-5172-45e9-b174-8d1ba936ab47", + } + err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestAttachVirtualInterfaceWithPortgroup(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleAttachVirtualInterfaceWithPortgroupSuccessfully(t) + + c := client.ServiceClient() + opts := nodes.VirtualInterfaceOpts{ + ID: "1974dcfa-836f-41b2-b541-686c100900e5", + PortgroupUUID: "c24944b5-a52e-4c5c-9c0a-52a0235a08a2", + } + err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestDetachVirtualInterface(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleDetachVirtualInterfaceSuccessfully(t) + + c := client.ServiceClient() + err := nodes.DetachVirtualInterface(context.TODO(), c, "1234asdf", "1974dcfa-836f-41b2-b541-686c100900e5").ExtractErr() + th.AssertNoErr(t, err) +} + +func TestVirtualInterfaceOptsValidation(t *testing.T) { + opts := nodes.VirtualInterfaceOpts{ + ID: "1974dcfa-836f-41b2-b541-686c100900e5", + PortUUID: "b2f96298-5172-45e9-b174-8d1ba936ab47", + PortgroupUUID: "c24944b5-a52e-4c5c-9c0a-52a0235a08a2", + } + + _, err := opts.ToVirtualInterfaceMap() + th.AssertEquals(t, err.Error(), "cannot specify both port_uuid and portgroup_uuid") +} diff --git a/openstack/baremetal/v1/nodes/urls.go b/openstack/baremetal/v1/nodes/urls.go index 2948bb659e..b86e4820e5 100644 --- a/openstack/baremetal/v1/nodes/urls.go +++ b/openstack/baremetal/v1/nodes/urls.go @@ -89,3 +89,11 @@ func firmwareListURL(client *gophercloud.ServiceClient, id string) string { func virtualMediaURL(client *gophercloud.ServiceClient, id string) string { return client.ServiceURL("nodes", id, "vmedia") } + +func virtualInterfaceURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("nodes", id, "vifs") +} + +func virtualInterfaceDeleteURL(client *gophercloud.ServiceClient, id string, vifID string) string { + return client.ServiceURL("nodes", id, "vifs", vifID) +} From cf7f9d959a08af748051b5f118970ecdba68f412 Mon Sep 17 00:00:00 2001 From: Sharpz7 Date: Fri, 28 Feb 2025 05:43:17 +0000 Subject: [PATCH 11/15] Added bugfix for functional tests --- internal/acceptance/openstack/baremetal/v1/nodes_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/acceptance/openstack/baremetal/v1/nodes_test.go b/internal/acceptance/openstack/baremetal/v1/nodes_test.go index b3a2923fbf..1151135e62 100644 --- a/internal/acceptance/openstack/baremetal/v1/nodes_test.go +++ b/internal/acceptance/openstack/baremetal/v1/nodes_test.go @@ -279,7 +279,8 @@ func TestNodesVirtualInterfaces(t *testing.T) { client, err := clients.NewBareMetalV1Client() th.AssertNoErr(t, err) - client.Microversion = "1.28" // VIFs were added in API version 1.28 + // VIFs were added in API version 1.28, but at least 1.38 is needed for tests to pass + client.Microversion = "1.38" node, err := CreateNode(t, client) th.AssertNoErr(t, err) From 18d7f8d8bf0b99e4e80c80e031e0f62c866f28c5 Mon Sep 17 00:00:00 2001 From: Himanshu Roy Date: Mon, 3 Feb 2025 19:04:34 +0530 Subject: [PATCH 12/15] add support to get virtual media for a node --- openstack/baremetal/v1/nodes/requests.go | 12 ++++++++++++ openstack/baremetal/v1/nodes/results.go | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/openstack/baremetal/v1/nodes/requests.go b/openstack/baremetal/v1/nodes/requests.go index cddd269a3d..6268f5037e 100644 --- a/openstack/baremetal/v1/nodes/requests.go +++ b/openstack/baremetal/v1/nodes/requests.go @@ -1003,6 +1003,18 @@ func DetachVirtualMedia(ctx context.Context, client *gophercloud.ServiceClient, return } +// Request the list of virtual media devices attached to the Node. +// Requires microversion 1.93 or later. +func GetVirtualMedia(ctx context.Context, client *gophercloud.ServiceClient, id string) (r VirtualMediaGetResult) { + + resp, err := client.Get(ctx, virtualMediaURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + // VirtualInterfaceOpts defines options for attaching a VIF to a node type VirtualInterfaceOpts struct { // The UUID or name of the VIF diff --git a/openstack/baremetal/v1/nodes/results.go b/openstack/baremetal/v1/nodes/results.go index 3e1976d3ef..b33517a388 100644 --- a/openstack/baremetal/v1/nodes/results.go +++ b/openstack/baremetal/v1/nodes/results.go @@ -711,6 +711,11 @@ type VirtualMediaDetachResult struct { gophercloud.ErrResult } +// Requires microversion 1.93 or later. +type VirtualMediaGetResult struct { + gophercloud.Result +} + // VirtualInterfaceAttachResult is the response from an AttachVirtualInterface operation. type VirtualInterfaceAttachResult struct { gophercloud.ErrResult From 7ffac16b10a06a23814ff6b14a786d0b5b39683a Mon Sep 17 00:00:00 2001 From: Himanshu Roy Date: Fri, 14 Feb 2025 17:19:45 +0530 Subject: [PATCH 13/15] virtual media get api tests Signed-off-by: Himanshu Roy --- .../openstack/baremetal/v1/nodes_test.go | 5 ++- .../v1/nodes/testing/fixtures_test.go | 35 +++++++++++++++++++ .../v1/nodes/testing/requests_test.go | 20 +++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/internal/acceptance/openstack/baremetal/v1/nodes_test.go b/internal/acceptance/openstack/baremetal/v1/nodes_test.go index 1151135e62..a4865783a1 100644 --- a/internal/acceptance/openstack/baremetal/v1/nodes_test.go +++ b/internal/acceptance/openstack/baremetal/v1/nodes_test.go @@ -219,7 +219,7 @@ func TestNodesVirtualMedia(t *testing.T) { client, err := clients.NewBareMetalV1Client() th.AssertNoErr(t, err) - client.Microversion = "1.89" + client.Microversion = "1.93" node, err := CreateNode(t, client) th.AssertNoErr(t, err) @@ -241,6 +241,9 @@ func TestNodesVirtualMedia(t *testing.T) { err = nodes.DetachVirtualMedia(context.TODO(), client, node.UUID, nodes.DetachVirtualMediaOpts{}).ExtractErr() th.AssertNoErr(t, err) + + err = nodes.GetVirtualMedia(context.TODO(), client, node.UUID).Err + th.AssertNoErr(t, err) } func TestNodesServicingHold(t *testing.T) { diff --git a/openstack/baremetal/v1/nodes/testing/fixtures_test.go b/openstack/baremetal/v1/nodes/testing/fixtures_test.go index 282d5dbc7f..371a4f00a9 100644 --- a/openstack/baremetal/v1/nodes/testing/fixtures_test.go +++ b/openstack/baremetal/v1/nodes/testing/fixtures_test.go @@ -900,6 +900,28 @@ const NodeVirtualMediaAttachBodyWithSource = ` } ` +const NodeVirtualMediaGetBodyAttached = ` +{ + "image": "https://example.com/image", + "inserted": true, + "media_types": [ + "CD", + "DVD" + ] +} +` + +const NodeVirtualMediaGetBodyNotAttached = ` +{ + "image": "", + "inserted": false, + "media_types": [ + "CD", + "DVD" + ] +} +` + var ( createdAtFoo, _ = time.Parse(time.RFC3339, "2019-01-31T19:59:28+00:00") createdAtBar, _ = time.Parse(time.RFC3339, "2019-01-31T19:59:29+00:00") @@ -1850,6 +1872,19 @@ func HandleDetachVirtualMediaSuccessfully(t *testing.T, withType bool) { }) } +func HandleGetVirtualMediaSuccessfully(t *testing.T, attached bool) { + th.Mux.HandleFunc("/nodes/1234asdf/vmedia", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + w.WriteHeader(http.StatusOK) + if attached { + fmt.Fprint(w, NodeVirtualMediaGetBodyAttached) + } else { + fmt.Fprint(w, NodeVirtualMediaGetBodyNotAttached) + } + }) +} + // HandleListVirtualInterfacesSuccessfully sets up the test server to respond to a ListVirtualInterfaces request func HandleListVirtualInterfacesSuccessfully(t *testing.T) { th.Mux.HandleFunc("/nodes/1234asdf/vifs", diff --git a/openstack/baremetal/v1/nodes/testing/requests_test.go b/openstack/baremetal/v1/nodes/testing/requests_test.go index 487c350175..24452be0c2 100644 --- a/openstack/baremetal/v1/nodes/testing/requests_test.go +++ b/openstack/baremetal/v1/nodes/testing/requests_test.go @@ -830,6 +830,26 @@ func TestVirtualMediaDetachWithTypes(t *testing.T) { th.AssertNoErr(t, err) } +func TestVirtualMediaGetAttached(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetVirtualMediaSuccessfully(t, true) + + c := client.ServiceClient() + err := nodes.GetVirtualMedia(context.TODO(), c, "1234asdf").Err + th.AssertNoErr(t, err) +} + +func TestVirtualMediaGetNotAttached(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetVirtualMediaSuccessfully(t, false) + + c := client.ServiceClient() + err := nodes.GetVirtualMedia(context.TODO(), c, "1234asdf").Err + th.AssertNoErr(t, err) +} + func TestListVirtualInterfaces(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() From b33345d7170b81c89d13ecb08e10560367b592e0 Mon Sep 17 00:00:00 2001 From: Himanshu Roy Date: Fri, 7 Mar 2025 10:44:15 +0530 Subject: [PATCH 14/15] ignore error for vmedia get api for ipmi driver Signed-off-by: Himanshu Roy --- internal/acceptance/openstack/baremetal/v1/nodes_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/acceptance/openstack/baremetal/v1/nodes_test.go b/internal/acceptance/openstack/baremetal/v1/nodes_test.go index a4865783a1..be48ee9197 100644 --- a/internal/acceptance/openstack/baremetal/v1/nodes_test.go +++ b/internal/acceptance/openstack/baremetal/v1/nodes_test.go @@ -243,7 +243,13 @@ func TestNodesVirtualMedia(t *testing.T) { th.AssertNoErr(t, err) err = nodes.GetVirtualMedia(context.TODO(), client, node.UUID).Err - th.AssertNoErr(t, err) + // Since Virtual Media GET api call is synchronous, we get a HTTP 400 + // response as CreateNode has ipmi driver hardcoded, but the api is + // only supported by the redfish driver + // (TODO: hroyrh) fix this once redfish driver is used in the tests + if node.Driver == "redfish" { + th.AssertNoErr(t, err) + } } func TestNodesServicingHold(t *testing.T) { From 11349b4c3f71c79bf2322189f13da3033a678dfd Mon Sep 17 00:00:00 2001 From: Pierre Prinetti Date: Thu, 3 Apr 2025 11:39:47 +0200 Subject: [PATCH 15/15] Prepare v2.7.0 --- CHANGELOG.md | 11 +++++++++++ provider_client.go | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cffb759590..73fe513468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## v2.7.0 (2025-04-03) + +* [GH-3306](https://github.com/gophercloud/gophercloud/pull/3306) [v2] identity: Add Get endpoint by ID +* [GH-3325](https://github.com/gophercloud/gophercloud/pull/3325) [v2] Switch to a version of gocovmerge compatible with go 1.22 +* [GH-3327](https://github.com/gophercloud/gophercloud/pull/3327) Merge pull request #3209 from shiftstack/proper-service-discovery +* [GH-3328](https://github.com/gophercloud/gophercloud/pull/3328) [v2] Improve support for `network standard-attr-*` extensions +* [GH-3330](https://github.com/gophercloud/gophercloud/pull/3330) [v2] Enhance Snapshot struct and add ListDetail function in V3 blockstorage +* [GH-3333](https://github.com/gophercloud/gophercloud/pull/3333) [v2] vpnaas: add support for more ciphers (auth, encryption, pfs modes) +* [GH-3334](https://github.com/gophercloud/gophercloud/pull/3334) [v2] Added support for VIF's in Baremetal +* [GH-3335](https://github.com/gophercloud/gophercloud/pull/3335) [v2] Baremetal virtual media Get API + ## v2.6.0 (2025-03-03) * [GH-3309](https://github.com/gophercloud/gophercloud/pull/3309) Backport: Added support for hypervisor_hostname to v2 diff --git a/provider_client.go b/provider_client.go index 8dac878eb9..52fcd38ab3 100644 --- a/provider_client.go +++ b/provider_client.go @@ -13,7 +13,7 @@ import ( // DefaultUserAgent is the default User-Agent string set in the request header. const ( - DefaultUserAgent = "gophercloud/v2.6.0" + DefaultUserAgent = "gophercloud/v2.7.0" DefaultMaxBackoffRetries = 60 )