From a2fee54a1379df5c98caa8e1bacb69c2c070cb58 Mon Sep 17 00:00:00 2001 From: Yousef Samarah <36555958+ysamarah@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:51:49 -0500 Subject: [PATCH] Fix cloudstack_nic ignoring project scope resourceCloudStackNICRead looks the parent instance up with GetVirtualMachineByID without passing a project. listVirtualMachines does not return guests that belong to a project unless projectid is supplied, so the lookup finds nothing, the NIC is treated as deleted, and Terraform removes it from state. Because Read runs immediately after Create, attaching a NIC to a guest in a project always fails the apply with "Provider produced inconsistent result after apply ... Root object was present, but now absent", even though the NIC was created correctly in CloudStack. Add an optional project argument and scope the read with cloudstack.WithProject, the same way resource_cloudstack_instance and resource_cloudstack_affinity_group already do. WithProject is a no-op for an empty value, so behaviour is unchanged when no project is set. Also add an importer. The parent instance cannot be derived from a NIC ID alone, so it takes /, or // for a guest in a project. This is the same class of bug as #319 (instance data source ignoring project scope) and #293 (userdata not found when added to a project). Co-authored-by: Cursor --- cloudstack/resource_cloudstack_nic.go | 42 +++++++++++++++++++++++++-- website/docs/r/nic.html.markdown | 19 ++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/cloudstack/resource_cloudstack_nic.go b/cloudstack/resource_cloudstack_nic.go index 36c25ecf..7f1abb9a 100644 --- a/cloudstack/resource_cloudstack_nic.go +++ b/cloudstack/resource_cloudstack_nic.go @@ -33,6 +33,9 @@ func resourceCloudStackNIC() *schema.Resource { Create: resourceCloudStackNICCreate, Read: resourceCloudStackNICRead, Delete: resourceCloudStackNICDelete, + Importer: &schema.ResourceImporter{ + State: importStateNIC, + }, Schema: map[string]*schema.Schema{ "network_id": { @@ -59,6 +62,12 @@ func resourceCloudStackNIC() *schema.Resource { Computed: true, ForceNew: true, }, + + "project": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, }, } } @@ -107,8 +116,13 @@ func resourceCloudStackNICCreate(d *schema.ResourceData, meta interface{}) error func resourceCloudStackNICRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) - // Get the virtual machine details - vm, count, err := cs.VirtualMachine.GetVirtualMachineByID(d.Get("virtual_machine_id").(string)) + // Get the virtual machine details. Instances that belong to a project are + // not returned by listVirtualMachines unless the project is passed along, + // so scope the lookup the same way the instance resource does. + vm, count, err := cs.VirtualMachine.GetVirtualMachineByID( + d.Get("virtual_machine_id").(string), + cloudstack.WithProject(d.Get("project").(string)), + ) if err != nil { if count == 0 { log.Printf("[DEBUG] Instance %s does no longer exist", d.Get("virtual_machine_id").(string)) @@ -165,6 +179,30 @@ func resourceCloudStackNICDelete(d *schema.ResourceData, meta interface{}) error return nil } +// importStateNIC imports a NIC using "/", or +// "//" when the instance belongs to a +// project. The parent instance cannot be derived from the NIC ID alone, so the +// generic passthrough importer is not usable here. +func importStateNIC(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + s := strings.Split(d.Id(), "/") + + switch len(s) { + case 2: + d.Set("virtual_machine_id", s[0]) + d.SetId(s[1]) + case 3: + d.Set("project", s[0]) + d.Set("virtual_machine_id", s[1]) + d.SetId(s[2]) + default: + return nil, fmt.Errorf( + "invalid import ID %q, expected / or "+ + "//", d.Id()) + } + + return []*schema.ResourceData{d}, nil +} + func retryableAddNicFunc(cs *cloudstack.CloudStackClient, p *cloudstack.AddNicToVirtualMachineParams) func() (interface{}, error) { return func() (interface{}, error) { r, err := cs.VirtualMachine.AddNicToVirtualMachine(p) diff --git a/website/docs/r/nic.html.markdown b/website/docs/r/nic.html.markdown index a000e703..14df34b3 100644 --- a/website/docs/r/nic.html.markdown +++ b/website/docs/r/nic.html.markdown @@ -50,6 +50,11 @@ The following arguments are supported: * `virtual_machine_id` - (Required) The ID of the virtual machine to which to attach the NIC. Changing this forces a new resource to be created. +* `project` - (Optional) The name or ID of the project the virtual machine + belongs to. Required when the virtual machine belongs to a project, + otherwise the NIC cannot be read back. Changing this forces a new resource + to be created. + ## Attributes Reference The following attributes are exported: @@ -57,3 +62,17 @@ The following attributes are exported: * `id` - The ID of the NIC. * `ip_address` - The assigned IP address. * `mac_address` - The assigned MAC address. + +## Import + +NICs can be imported using `/`, e.g. + +```shell +$ terraform import cloudstack_nic.test f8141e2f-4e7e-4c63-9362-986c908b7ea7/2b1a4b3c-5d6e-4f70-8192-a3b4c5d6e7f8 +``` + +When the virtual machine belongs to a project, prefix the project: + +```shell +$ terraform import cloudstack_nic.test my-project/f8141e2f-4e7e-4c63-9362-986c908b7ea7/2b1a4b3c-5d6e-4f70-8192-a3b4c5d6e7f8 +```