Skip to content

[compute] add hostname option to compute instance#1910

Open
hops wants to merge 1 commit intoterraform-provider-openstack:mainfrom
HSLU-Lab-Services:compute-instance-hostname
Open

[compute] add hostname option to compute instance#1910
hops wants to merge 1 commit intoterraform-provider-openstack:mainfrom
HSLU-Lab-Services:compute-instance-hostname

Conversation

@hops
Copy link
Copy Markdown

@hops hops commented Jun 24, 2025

This pull request adds the optional hostname option to the compute_instance resource. I've marked this as a draft as I'd like to get your input on a couple of things.

The computeClient used in resourceComputeInstanceV2Read does not set any microversion and thus the OS-EXT-SRV-ATTR:hostname is not returned by the OpenStack API. The OS-EXT-SRV-ATTR:hostname is available from microversion 2.3 but until version 2.90 it was only returned to administrator users. That shouldn't be an issue because until microversion 2.90 the hostname could not be specified anyway and was generated from the instance's name.

I'm not sure what the best solution to this problem is. Maybe an additional parameter to pass the microversion which would mean we have to change the calls in resourceComputeInstanceV2Update and resourceComputeInstanceV2ImportState as well.
Another important detail is that with microversion 2.47 the response of the flavor changed from flavor.id and flavor.ilnks to an object with the individual fields of a flavor. However the flavor ID is not available anymore.

I've tested these changes against a DevStack 2025.1 installation and against a real OpenStack environment running 2024.1. On DevStack I ran all acceptance tests beginning with testAccCheckComputeV2Instance without errors. The tests on the OpenStack environment were done manually.

  • The added tests don't check the hostname (e.g. using TestCheckResourceAttr) as it isn't returned by the API.
  • data source changes are not included due to the dataSourceComputeInstanceV2Read function also not setting any microversion.

@kayrus
Copy link
Copy Markdown
Collaborator

kayrus commented Jun 24, 2025

@hops thanks for the PR. According to nova API it is possible to update the hostname. Can you implement the hostname update logic as well? https://docs.openstack.org/api-ref/compute/#update-server

@hops
Copy link
Copy Markdown
Author

hops commented Jun 24, 2025

@kayrus unless I'm missing something that would require adding update support in gophercloud as the hostname is missing in the UpdateOpts type. I can add this to gophercloud and push it upstream unless you see another solution?

@kayrus
Copy link
Copy Markdown
Collaborator

kayrus commented Jun 24, 2025

@hops feel free to push this to gophercloud

@hops
Copy link
Copy Markdown
Author

hops commented Jun 27, 2025

@kayrus I saw you already backported my change to the gophercloud v2 branch, thanks!
Does it matter that the Hostname in UpdateOpts is defined as a string pointer while the other fields are string? I'm fine either way but thought I'd asked before pushing the update implementation.

Should I bump the gophercloud version in this pull request as a separate commit or in a separate pull request?

@kayrus
Copy link
Copy Markdown
Collaborator

kayrus commented Jun 27, 2025

@hops no worries, this will be addressed in #1899. we can implement the remaining updates later without needing a gophercloud update. my goal was to cover all the caveats at once, although that hadn't been done previously.

Should I bump the gophercloud version in this pull request as a separate commit or in a separate pull request?

feel free to push it here, but please be prepared to wait longer for tests

@hops hops force-pushed the compute-instance-hostname branch from 67f8891 to ba6e856 Compare July 1, 2025 11:33
Copy link
Copy Markdown
Collaborator

@kayrus kayrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for your effort. see my comments regarding the code.

@kayrus
Copy link
Copy Markdown
Collaborator

kayrus commented Jul 1, 2025

in order to avoid long waiting, I'm gonna to update gophercloud in a separate PR.
UPD: new gophercloud tag is set and merged, you need to rebase your PR on main.

@hops hops force-pushed the compute-instance-hostname branch from ba6e856 to e44d3df Compare July 2, 2025 09:46
@hops
Copy link
Copy Markdown
Author

hops commented Jul 2, 2025

@kayrus I rebased my branch and changed your suggestions from the review.
I'm not particular happy with the naming of isValidHostname and validateHostname, I'm open for alternative ideas.

The update hostname acceptance test only checks if the instance ID hasn't changed as we're still missing OS-EXT-SRV-ATTR:hostname due to resourceComputeInstanceV2Read not setting the minimum required microversion 2.90. Do you have an idea how on how to best pass the required microversion?

Last but not least, thank you for your fast response time and sorry that I can't keep up with your pace.

@hops hops marked this pull request as ready for review July 2, 2025 10:46
@kayrus
Copy link
Copy Markdown
Collaborator

kayrus commented Jul 9, 2025

@hops please fix the golangci issues
as for the microversion, try this approach

if err != nil {
// reset microversion to 2.1 and try again
computeClient.Microversion = "2.1"
flavor, err = flavors.Get(ctx, computeClient, v).Extract()
if err != nil {
if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
return diag.Errorf("No Flavor found")
}
return diag.Errorf("Unable to retrieve OpenStack %s flavor: %s", v, err)
}
}

@hops hops force-pushed the compute-instance-hostname branch 2 times, most recently from e44d3df to 5643982 Compare July 17, 2025 09:38
@hops
Copy link
Copy Markdown
Author

hops commented Jul 17, 2025

@kayrus I've fixed the golangci issues and added the microversion handling for flavors in resourceComputeInstanceV2Read.
I do struggled with the acceptance tests though. I've added a resource.TestCheckResourceAttrPtr step in the ComposeTestCheckFunc to test for the value of the hostname of the instance. However I always get a nil pointer dereference error. I think this is due the hostname only being returned when microverison 2.90+ is used so I tried to add it in the testAccCheckComputeV2InstanceExists but still got the same errors. I've commented these test steps out in this commit.
Am I doing something wrong? Is there something else calling the OpenStack API while running the acceptance tests? Or can you give me some pointers on how to debug this?
Should I squash the commits once this has been accepted?

Config: testAccComputeInstanceV2HostnameFqdnConfig(),
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeV2InstanceExists(t.Context(), "openstack_compute_instance_v2.instance_1", &instance),
// resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please try this

Suggested change
// resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname),
resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", &instance.Hostname),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the testAccCheckComputeV2InstanceExists doesn't use microversion fetching the server resource. you need to create a new testAccCheckComputeV2InstanceHostnameExists and set service client microversion to fetch the hostname.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tested this as well by adding the computeV2InstanceCreateServerWithHostnameMicroversion here with the same result (nil pointer dereference). I've also added it as a test to testAccCheckComputeV2InstanceDestroy without luck.
Do you know if there's something else in the test suite that calls GET requests to the instance?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kayrus I've attached the Terraform debug and trace log from running the TestAccComputeV2Instance_hostnameFqdn acceptance test. I do see GET requests to the /compute/v2.1/servers/<UUID> with the header X-Openstack-Nova-Api-Version: 2.1 but I can't figure out who's sending these. I hope you have an idea or maybe a pointer on how to track down the caller of these requests. Also here's the terminal output of the test run.

$ make testacc TEST=./openstack TESTARGS="-run=TestAccComputeV2Instance_hostnameFqdn -count=1"
==> Checking that code complies with gofmt requirements...
TF_ACC=1 go test ./openstack -v -run=TestAccComputeV2Instance_hostnameFqdn -count=1 -timeout 120m
=== RUN   TestAccComputeV2Instance_hostnameFqdn
--- FAIL: TestAccComputeV2Instance_hostnameFqdn (33.28s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
	panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x102e29f20]

goroutine 4 [running]:
testing.tRunner.func1.2({0x10344e520, 0x10419f4a0})
	/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1734 +0x1ac
testing.tRunner.func1()
	/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1737 +0x334
panic({0x10344e520?, 0x10419f4a0?})
	/opt/homebrew/Cellar/go/1.24.5/libexec/src/runtime/panic.go:792 +0x124
github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack.TestAccComputeV2Instance_hostnameFqdn.TestCheckResourceAttrPtr.func4(0x140005db9d0)
	/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing.go:1746 +0x30
github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack.TestAccComputeV2Instance_hostnameFqdn.ComposeTestCheckFunc.func5(0x140005db9d0)
	/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing.go:1020 +0x64
github.com/hashicorp/terraform-plugin-testing/helper/resource.testStepNewConfig({_, _}, {_, _}, {0x0, 0x1400043b8a0, {0x0, 0x0, 0x0}, 0x140004277a0, ...}, ...)
	/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing_new_config.go:219 +0xd80
github.com/hashicorp/terraform-plugin-testing/helper/resource.runNewTest({0x103649648, 0x140002a28d0}, {0x103652b20, 0x14000003a40}, {0x0, 0x1400043b8a0, {0x0, 0x0, 0x0}, 0x140004277a0, ...}, ...)
	/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing_new.go:362 +0x1dcc
github.com/hashicorp/terraform-plugin-testing/helper/resource.Test({0x103652b20, 0x14000003a40}, {0x0, 0x1400043b8a0, {0x0, 0x0, 0x0}, 0x140004277a0, 0x0, 0x0, ...})
	/Users/user/go/pkg/mod/github.com/hashicorp/terraform-plugin-testing@v1.13.2/helper/resource/testing.go:979 +0x510
github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack.TestAccComputeV2Instance_hostnameFqdn(0x14000003a40)
	/Users/user/src/labservices/ls-terraform-openstack-provider/openstack/resource_openstack_compute_instance_v2_test.go:901 +0x3c8
testing.tRunner(0x14000003a40, 0x10362e148)
	/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1792 +0xe4
created by testing.(*T).Run in goroutine 1
	/opt/homebrew/Cellar/go/1.24.5/libexec/src/testing/testing.go:1851 +0x374
FAIL	github.com/terraform-provider-openstack/terraform-provider-openstack/v3/openstack	33.641s
FAIL
make: *** [testacc] Error 1

The microversion on the compute client in testAccCheckComputeV2InstanceExists and testAccCheckComputeV2InstanceDestroy are only temporary and I'll create proper functions once the culprit of the error has been identified.

Copy link
Copy Markdown
Collaborator

@kayrus kayrus Jul 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname), you're passing a nil pointer.

in testAccCheckComputeV2InstanceExists(t.Context(), "openstack_compute_instance_v2.instance_1", &instance), you're passing a pointer to a struct and then populate this struct with new values. the nil get overwritten with a new pointer, but old nil pointer was already passed to a TestCheckResourceAttrPtr func.

try this approach:

diff --git a/openstack/resource_openstack_compute_instance_v2_test.go b/openstack/resource_openstack_compute_instance_v2_test.go
index 7cc7fc87..06b0bced 100644
--- a/openstack/resource_openstack_compute_instance_v2_test.go
+++ b/openstack/resource_openstack_compute_instance_v2_test.go
@@ -910,13 +910,25 @@ func TestAccComputeV2Instance_hostnameFqdn(t *testing.T) {
                                Config: testAccComputeInstanceV2HostnameFqdnConfig(),
                                Check: resource.ComposeTestCheckFunc(
                                        testAccCheckComputeV2InstanceExists(t.Context(), "openstack_compute_instance_v2.instance_1", &instance),
-                                       resource.TestCheckResourceAttrPtr("openstack_compute_instance_v2.instance_1", "hostname", instance.Hostname),
+                                       resource.TestCheckResourceAttrWith("openstack_compute_instance_v2.instance_1", "hostname", testAccCheckComputeV2InstanceHostname(&instance)),
                                ),
                        },
                },
        })
 }
 
+func testAccCheckComputeV2InstanceHostname(instance *servers.Server) resource.CheckResourceAttrWithFunc {
+       return func(v string) error {
+               if instance.Hostname == nil {
+                       return fmt.Errorf("empty hostname")
+               }
+               if *instance.Hostname != v {
+                       return fmt.Errorf("unexpected hostname: %s", *instance.Hostname)
+               }
+               return nil
+       }
+}
+
 func testAccCheckComputeV2InstanceDestroy(ctx context.Context) resource.TestCheckFunc {
        return func(s *terraform.State) error {
                config := testAccProvider.Meta().(*Config)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that's embarrassing but totally makes sense. Thank you for your guidance!
I've fixed the acceptance tests with your suggestion and they passed on a DevStack 2025.1 environment. I also squashed my commits.

@hops hops force-pushed the compute-instance-hostname branch 2 times, most recently from b4a5337 to e3e2b22 Compare July 24, 2025 08:33
Copy link
Copy Markdown
Collaborator

@kayrus kayrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your effort. See my PR comments.

Comment on lines +526 to +530
hostname = v.(string)
if isValidHostname(hostname) {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion
} else {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hostname = v.(string)
if isValidHostname(hostname) {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion
} else {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion
}
hostname = v.(string)
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion
if isValidHostname(hostname) {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion
}

server, err := servers.Get(ctx, computeClient, d.Id()).Extract()
if err != nil {
return diag.FromErr(CheckDeleted(d, err, "server"))
log.Printf("[DEBUG] Falling back to the default API call due to: %#v", err)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
log.Printf("[DEBUG] Falling back to the default API call due to: %#v", err)
log.Printf("[DEBUG] Falling back to the default API microversion due to: %#v", err)

Comment on lines +861 to +867
// Set the required microversion.
if isValidHostname(*updateOpts.Hostname) {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion
} else {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Set the required microversion.
if isValidHostname(*updateOpts.Hostname) {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion
} else {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion
}
// Set the required microversion.
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameIsFqdnMicroversion
if isValidHostname(*updateOpts.Hostname) {
computeClient.Microversion = computeV2InstanceCreateServerWithHostnameMicroversion
}

Comment on lines +769 to +770
var e gophercloud.ErrResourceNotFound
if errors.Is(err, &e) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't the if gophercloud.ResponseCodeIs(err, http.StatusNotFound) { work here?

and how about this? let's omit info about microversions here

        flavorName := server.Flavor["original_name"].(string)
        flavorID := server.Flavor["id"].(string)
        if flavorName != "" {
                // Starting with microversion 2.47 Nova returns the flavor name but not the flavor ID anymore
                d.Set("flavor_name", flavorName)
                v, err := flavorsutils.IDFromName(ctx, computeClient, flavorName)
                if err != nil {
                        if !gophercloud.ResponseCodeIs(err, http.StatusNotFound){
                                return diag.FromErr(err)
                        }
                        // Original flavor was deleted, but it is possible that instance started
                        // with this flavor is still running
                        log.Printf("[DEBUG] Original flavor of instance with id %s could not be found", d.Id())
                }
                if v != "" {
                        d.Set("flavor_id", v)
                }
        } else if flavorID != "" {
                // handle case, when microversion was not set
                d.Set("flavor_id", flavorID)
                v, err := flavors.Get(ctx, computeClient, flavorID).Extract()
                if err != nil {
                        if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
                                return diag.FromErr(err)
                        }
                        // Original flavor was deleted, but it is possible that instance started
                        // with this flavor is still running
                        log.Printf("[DEBUG] Original flavor of instance with id %s could not be found", d.Id())
                }
                if v != nil && v.Name != "" {
                        d.Set("flavor_name", v.Name)
                }
        }
        if flavorName == "" || flavorID == "" {
                return diag.Errorf("Error setting OpenStack server's flavor: %v", server.Flavor)
        }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No if gophercloud.ResponseCodeIs(err, http.StatusNotFound) doesn't work because IDFromName() doesn't return a ErrUnexpectedResponsecode like flavors.Get() does.
IDFromName() from gophercloud utils returns a ErrResourceNotFound{Name: name, ResourceType: "flavor"} with the name being the flavor's name.

Generally I like your approach more but have a couple of remarks.

        flavorName := server.Flavor["original_name"].(string)
        flavorID := server.Flavor["id"].(string)

This will always cause a type assertion error because either we have the ID (microversion <= 2.46) or the name (microversion >= 2.47) but never both at the same time. I'd suggest to leave it as is (flavorID, ok := server.Flavor["id"].(string)) but I'm open to other suggestions.

        if flavorName == "" || flavorID == "" {
                return diag.Errorf("Error setting OpenStack server's flavor: %v", server.Flavor)
        }

Same issue as above. Did you mean to write if flavorName == "" && flavorID == ""?

Further I think we have to keep the reset of flavor_id and flavor_name in case the flavor got deleted but the instance is still running. Otherwise it wouldn't behave the same as before this change.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest to leave it as is (flavorID, ok := server.Flavor["id"].(string))

agree

Did you mean to write if flavorName == "" && flavorID == ""?

yes

Further I think we have to keep the reset of flavor_id and flavor_name in case the flavor got deleted but the instance is still running. Otherwise it wouldn't behave the same as before this change.

ok, I'll check this again in the next review cycle.

@hops hops force-pushed the compute-instance-hostname branch from e3e2b22 to 856d514 Compare August 1, 2025 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants