You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by su...@apache.org on 2026/08/18 12:03:29 UTC

(cloudstack-terraform-provider) branch main updated: Add terraform import support for several cloudstack resources (#305)

This is an automated email from the ASF dual-hosted git repository.

sureshanaparti pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git


The following commit(s) were added to refs/heads/main by this push:
     new ae84c54  Add terraform import support for several cloudstack resources (#305)
ae84c54 is described below

commit ae84c54d1d7ed9b3a78b83ee7bc913cad8a1df3d
Author: Pearl Dsilva <pe...@gmail.com>
AuthorDate: Tue Aug 18 08:03:24 2026 -0400

    Add terraform import support for several cloudstack resources (#305)
---
 cloudstack/resource_cloudstack_autoscale_policy.go |  3 +
 .../resource_cloudstack_autoscale_vm_group.go      |  3 +
 .../resource_cloudstack_autoscale_vm_profile.go    |  3 +
 ...esource_cloudstack_autoscale_vm_profile_test.go | 21 +++++
 cloudstack/resource_cloudstack_condition.go        |  3 +
 cloudstack/resource_cloudstack_counter.go          |  3 +
 cloudstack/resource_cloudstack_host.go             | 22 +++--
 cloudstack/resource_cloudstack_host_test.go        | 26 ++++++
 cloudstack/resource_cloudstack_ipaddress.go        | 44 ++++++++++
 cloudstack/resource_cloudstack_ipaddress_test.go   | 19 +++++
 .../resource_cloudstack_loadbalancer_rule.go       |  3 +
 .../resource_cloudstack_loadbalancer_rule_test.go  | 21 +++++
 cloudstack/resource_cloudstack_network_offering.go | 20 +++++
 cloudstack/resource_cloudstack_port_forward.go     | 93 ++++++++++++++++++++++
 .../resource_cloudstack_port_forward_test.go       | 23 ++++++
 cloudstack/resource_cloudstack_service_offering.go | 20 +++++
 .../resource_cloudstack_service_offering_test.go   | 18 +++++
 cloudstack/resource_cloudstack_static_nat.go       |  4 +
 cloudstack/resource_cloudstack_static_nat_test.go  | 19 +++++
 cloudstack/resource_cloudstack_vpn_connection.go   |  3 +
 .../resource_cloudstack_vpn_connection_test.go     | 19 +++++
 website/docs/r/autoscale_policy.html.markdown      |  8 ++
 website/docs/r/autoscale_vm_profile.html.markdown  | 11 +++
 website/docs/r/condition.html.markdown             |  8 ++
 website/docs/r/counter.html.markdown               |  8 ++
 website/docs/r/ipaddress.html.markdown             | 15 ++++
 website/docs/r/loadbalancer_rule.html.markdown     | 18 +++++
 website/docs/r/network_offering.html.markdown      |  4 +
 website/docs/r/port_forward.html.markdown          | 20 +++++
 website/docs/r/service_offering.html.markdown      |  4 +
 website/docs/r/static_nat.html.markdown            | 15 ++++
 website/docs/r/vpn_connection.html.markdown        |  8 ++
 32 files changed, 503 insertions(+), 6 deletions(-)

diff --git a/cloudstack/resource_cloudstack_autoscale_policy.go b/cloudstack/resource_cloudstack_autoscale_policy.go
index fe390b5..8de316e 100644
--- a/cloudstack/resource_cloudstack_autoscale_policy.go
+++ b/cloudstack/resource_cloudstack_autoscale_policy.go
@@ -34,6 +34,9 @@ func resourceCloudStackAutoScalePolicy() *schema.Resource {
 		Read:   resourceCloudStackAutoScalePolicyRead,
 		Update: resourceCloudStackAutoScalePolicyUpdate,
 		Delete: resourceCloudStackAutoScalePolicyDelete,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"name": {
diff --git a/cloudstack/resource_cloudstack_autoscale_vm_group.go b/cloudstack/resource_cloudstack_autoscale_vm_group.go
index 8d0eb7e..d282032 100644
--- a/cloudstack/resource_cloudstack_autoscale_vm_group.go
+++ b/cloudstack/resource_cloudstack_autoscale_vm_group.go
@@ -35,6 +35,9 @@ func resourceCloudStackAutoScaleVMGroup() *schema.Resource {
 		Read:   resourceCloudStackAutoScaleVMGroupRead,
 		Update: resourceCloudStackAutoScaleVMGroupUpdate,
 		Delete: resourceCloudStackAutoScaleVMGroupDelete,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"lbrule_id": {
diff --git a/cloudstack/resource_cloudstack_autoscale_vm_profile.go b/cloudstack/resource_cloudstack_autoscale_vm_profile.go
index 0032b92..0bc71ae 100644
--- a/cloudstack/resource_cloudstack_autoscale_vm_profile.go
+++ b/cloudstack/resource_cloudstack_autoscale_vm_profile.go
@@ -35,6 +35,9 @@ func resourceCloudStackAutoScaleVMProfile() *schema.Resource {
 		Read:   resourceCloudStackAutoScaleVMProfileRead,
 		Update: resourceCloudStackAutoScaleVMProfileUpdate,
 		Delete: resourceCloudStackAutoScaleVMProfileDelete,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"service_offering": {
diff --git a/cloudstack/resource_cloudstack_autoscale_vm_profile_test.go b/cloudstack/resource_cloudstack_autoscale_vm_profile_test.go
index f417f45..97bb5b6 100644
--- a/cloudstack/resource_cloudstack_autoscale_vm_profile_test.go
+++ b/cloudstack/resource_cloudstack_autoscale_vm_profile_test.go
@@ -85,6 +85,27 @@ func TestAccCloudStackAutoscaleVMProfile_update(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackAutoscaleVMProfile_import(t *testing.T) {
+	t.Skip("Skipping due to bug in cloudstack-go library")
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:     func() { testAccPreCheck(t) },
+		Providers:    testAccProviders,
+		CheckDestroy: testAccCheckCloudStackAutoscaleVMProfileDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackAutoscaleVMProfile_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_autoscale_vm_profile.foo",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
 func testAccCheckResourceMetadata(vmProfile *cloudstack.AutoScaleVmProfile) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		cs := testAccProvider.Meta().(*cloudstack.CloudStackClient)
diff --git a/cloudstack/resource_cloudstack_condition.go b/cloudstack/resource_cloudstack_condition.go
index cd74e5c..be734ed 100644
--- a/cloudstack/resource_cloudstack_condition.go
+++ b/cloudstack/resource_cloudstack_condition.go
@@ -33,6 +33,9 @@ func resourceCloudStackCondition() *schema.Resource {
 		Read:   resourceCloudStackConditionRead,
 		Update: resourceCloudStackConditionUpdate,
 		Delete: resourceCloudStackConditionDelete,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"counter_id": {
diff --git a/cloudstack/resource_cloudstack_counter.go b/cloudstack/resource_cloudstack_counter.go
index ddd7e9e..ca78047 100644
--- a/cloudstack/resource_cloudstack_counter.go
+++ b/cloudstack/resource_cloudstack_counter.go
@@ -32,6 +32,9 @@ func resourceCloudStackCounter() *schema.Resource {
 		Create: resourceCloudStackCounterCreate,
 		Read:   resourceCloudStackCounterRead,
 		Delete: resourceCloudStackCounterDelete,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"name": {
diff --git a/cloudstack/resource_cloudstack_host.go b/cloudstack/resource_cloudstack_host.go
index 3e88d72..5f65639 100644
--- a/cloudstack/resource_cloudstack_host.go
+++ b/cloudstack/resource_cloudstack_host.go
@@ -37,6 +37,15 @@ func resourceCloudStackHost() *schema.Resource {
 		Update: resourceCloudStackHostUpdate,
 		Create: resourceCloudStackHostCreate,
 		Delete: resourceCloudStackHostDelete,
+		// NOTE: CloudStack's listHosts API never returns url, username, or
+		// password, so Read cannot repopulate them after import. url is also
+		// ForceNew, so leaving it unset in an imported config will plan a
+		// destroy/recreate of the host. After importing, set these fields
+		// explicitly in config (or use lifecycle.ignore_changes) to avoid
+		// unexpected replacement on the next apply.
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
 		Schema: map[string]*schema.Schema{
 			"hypervisor": {
 				Type:     schema.TypeString,
@@ -224,12 +233,13 @@ func resourceCloudStackHostRead(d *schema.ResourceData, meta interface{}) error
 	d.SetId(h.Id)
 
 	fields := map[string]interface{}{
-		"hypervisor":     h.Hypervisor,
-		"pod_id":         h.Podid,
-		"zone_id":        h.Zoneid,
-		"state":          h.State,
-		"resource_state": h.Resourcestate,
-		"name":           h.Name,
+		"hypervisor":       h.Hypervisor,
+		"pod_id":           h.Podid,
+		"zone_id":          h.Zoneid,
+		"state":            h.State,
+		"resource_state":   h.Resourcestate,
+		"allocation_state": h.Resourcestate,
+		"name":             h.Name,
 	}
 
 	for k, v := range fields {
diff --git a/cloudstack/resource_cloudstack_host_test.go b/cloudstack/resource_cloudstack_host_test.go
index da91b8a..25b5365 100644
--- a/cloudstack/resource_cloudstack_host_test.go
+++ b/cloudstack/resource_cloudstack_host_test.go
@@ -49,6 +49,32 @@ func TestAccCloudStackHost_basic(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackHost_import(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:  func() { testAccPreCheck(t) },
+		Providers: testAccProviders,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackHost_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_host.test",
+				ImportState:       true,
+				ImportStateVerify: true,
+				// CloudStack's listHosts API never returns the connection URL or
+				// credentials, and these timeouts/flags are provider-local knobs that
+				// aren't part of the host object, so Read can't populate any of them.
+				ImportStateVerifyIgnore: []string{
+					"url", "username", "password",
+					"create_timeout", "destroy_timeout",
+					"prevent_destroy", "force_destroy",
+				},
+			},
+		},
+	})
+}
+
 const testAccCloudStackHost_basic = `
 data "cloudstack_zone" "zone" {
 	filter {
diff --git a/cloudstack/resource_cloudstack_ipaddress.go b/cloudstack/resource_cloudstack_ipaddress.go
index 28663e2..1af251f 100644
--- a/cloudstack/resource_cloudstack_ipaddress.go
+++ b/cloudstack/resource_cloudstack_ipaddress.go
@@ -33,6 +33,9 @@ func resourceCloudStackIPAddress() *schema.Resource {
 		Create: resourceCloudStackIPAddressCreate,
 		Read:   resourceCloudStackIPAddressRead,
 		Delete: resourceCloudStackIPAddressDelete,
+		Importer: &schema.ResourceImporter{
+			State: resourceCloudStackIPAddressImport,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"is_portable": {
@@ -171,6 +174,41 @@ func resourceCloudStackIPAddressCreate(d *schema.ResourceData, meta interface{})
 	return resourceCloudStackIPAddressRead(d, meta)
 }
 
+func resourceCloudStackIPAddressImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
+	cs := meta.(*cloudstack.CloudStackClient)
+
+	// Try to split the ID to extract the optional project name.
+	s := strings.SplitN(d.Id(), "/", 2)
+	if len(s) == 2 {
+		d.Set("project", s[0])
+	}
+
+	ipAddressID := s[len(s)-1]
+	d.SetId(ipAddressID)
+
+	ip, count, err := cs.Address.GetPublicIpAddressByID(
+		ipAddressID,
+		cloudstack.WithProject(d.Get("project").(string)),
+	)
+	if err != nil {
+		if count == 0 {
+			return nil, fmt.Errorf("IP address with ID %s does not exist", ipAddressID)
+		}
+		return nil, err
+	}
+
+	// Seed whichever of network_id/vpc_id actually applies before Read runs:
+	// Read only refreshes these when already present in state, so import
+	// needs to set the right one here first.
+	if ip.Vpcid != "" {
+		d.Set("vpc_id", ip.Vpcid)
+	} else if ip.Associatednetworkid != "" {
+		d.Set("network_id", ip.Associatednetworkid)
+	}
+
+	return []*schema.ResourceData{d}, nil
+}
+
 func resourceCloudStackIPAddressRead(d *schema.ResourceData, meta interface{}) error {
 	cs := meta.(*cloudstack.CloudStackClient)
 
@@ -208,6 +246,12 @@ func resourceCloudStackIPAddressRead(d *schema.ResourceData, meta interface{}) e
 	// Updated the IP address
 	d.Set("ip_address", ip.Ipaddress)
 
+	// Only refresh network_id/vpc_id if already present in state: a plain
+	// zone-scoped IP (neither set) can still come back from the API with an
+	// associated network under the hood, and syncing that into an Optional,
+	// non-Computed, ForceNew field would create a permanent diff. The
+	// importer is responsible for seeding whichever one applies before this
+	// Read runs.
 	if _, ok := d.GetOk("network_id"); ok {
 		d.Set("network_id", ip.Associatednetworkid)
 	}
diff --git a/cloudstack/resource_cloudstack_ipaddress_test.go b/cloudstack/resource_cloudstack_ipaddress_test.go
index 26606ee..51faf18 100644
--- a/cloudstack/resource_cloudstack_ipaddress_test.go
+++ b/cloudstack/resource_cloudstack_ipaddress_test.go
@@ -49,6 +49,25 @@ func TestAccCloudStackIPAddress_basic(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackIPAddress_import(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:     func() { testAccPreCheck(t) },
+		Providers:    testAccProviders,
+		CheckDestroy: testAccCheckCloudStackIPAddressDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackIPAddress_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_ipaddress.foo",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
 func TestAccCloudStackIPAddress_vpc(t *testing.T) {
 	var ipaddr cloudstack.PublicIpAddress
 
diff --git a/cloudstack/resource_cloudstack_loadbalancer_rule.go b/cloudstack/resource_cloudstack_loadbalancer_rule.go
index 6ebf52b..1c53f17 100644
--- a/cloudstack/resource_cloudstack_loadbalancer_rule.go
+++ b/cloudstack/resource_cloudstack_loadbalancer_rule.go
@@ -37,6 +37,9 @@ func resourceCloudStackLoadBalancerRule() *schema.Resource {
 		Read:   resourceCloudStackLoadBalancerRuleRead,
 		Update: resourceCloudStackLoadBalancerRuleUpdate,
 		Delete: resourceCloudStackLoadBalancerRuleDelete,
+		Importer: &schema.ResourceImporter{
+			State: importStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"name": {
diff --git a/cloudstack/resource_cloudstack_loadbalancer_rule_test.go b/cloudstack/resource_cloudstack_loadbalancer_rule_test.go
index 8a9c792..f251152 100644
--- a/cloudstack/resource_cloudstack_loadbalancer_rule_test.go
+++ b/cloudstack/resource_cloudstack_loadbalancer_rule_test.go
@@ -53,6 +53,27 @@ func TestAccCloudStackLoadBalancerRule_basic(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackLoadBalancerRule_import(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:     func() { testAccPreCheck(t) },
+		Providers:    testAccProviders,
+		CheckDestroy: testAccCheckCloudStackLoadBalancerRuleDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackLoadBalancerRule_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_loadbalancer_rule.foo",
+				ImportState:       true,
+				ImportStateVerify: true,
+				// certificate_id can't be read back from the CloudStack API (write-only field)
+				ImportStateVerifyIgnore: []string{"certificate_id"},
+			},
+		},
+	})
+}
+
 func TestAccCloudStackLoadBalancerRule_update(t *testing.T) {
 	var id string
 
diff --git a/cloudstack/resource_cloudstack_network_offering.go b/cloudstack/resource_cloudstack_network_offering.go
index d6adf4b..f260e63 100644
--- a/cloudstack/resource_cloudstack_network_offering.go
+++ b/cloudstack/resource_cloudstack_network_offering.go
@@ -33,6 +33,9 @@ func resourceCloudStackNetworkOffering() *schema.Resource {
 		Read:   resourceCloudStackNetworkOfferingRead,
 		Update: resourceCloudStackNetworkOfferingUpdate,
 		Delete: resourceCloudStackNetworkOfferingDelete,
+		Importer: &schema.ResourceImporter{
+			State: resourceCloudStackNetworkOfferingImport,
+		},
 		Schema: map[string]*schema.Schema{
 			"name": {
 				Type:     schema.TypeString,
@@ -342,6 +345,23 @@ func resourceCloudStackNetworkOfferingDelete(d *schema.ResourceData, meta interf
 	return nil
 }
 
+func resourceCloudStackNetworkOfferingImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
+	cs := meta.(*cloudstack.CloudStackClient)
+
+	// Read looks the offering up by name, so resolve the name from the ID first
+	n, count, err := cs.NetworkOffering.GetNetworkOfferingByID(d.Id())
+	if err != nil {
+		if count == 0 {
+			return nil, fmt.Errorf("network offering with ID %s does not exist", d.Id())
+		}
+		return nil, err
+	}
+
+	d.Set("name", n.Name)
+
+	return []*schema.ResourceData{d}, nil
+}
+
 func resourceCloudStackNetworkOfferingRead(d *schema.ResourceData, meta interface{}) error {
 	cs := meta.(*cloudstack.CloudStackClient)
 	log.Printf("[DEBUG] Retrieving Network Offering %s", d.Get("name").(string))
diff --git a/cloudstack/resource_cloudstack_port_forward.go b/cloudstack/resource_cloudstack_port_forward.go
index 358b8a6..be7e1cc 100644
--- a/cloudstack/resource_cloudstack_port_forward.go
+++ b/cloudstack/resource_cloudstack_port_forward.go
@@ -38,6 +38,9 @@ func resourceCloudStackPortForward() *schema.Resource {
 		Read:   resourceCloudStackPortForwardRead,
 		Update: resourceCloudStackPortForwardUpdate,
 		Delete: resourceCloudStackPortForwardDelete,
+		Importer: &schema.ResourceImporter{
+			State: resourceCloudStackPortForwardImport,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"ip_address_id": {
@@ -245,6 +248,94 @@ func createPortForward(d *schema.ResourceData, meta interface{}, forward map[str
 	return nil
 }
 
+func resourceCloudStackPortForwardImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
+	cs := meta.(*cloudstack.CloudStackClient)
+
+	// Try to split the ID to extract the optional project name.
+	s := strings.SplitN(d.Id(), "/", 2)
+	if len(s) == 2 {
+		d.Set("project", s[0])
+	}
+
+	ipAddressID := s[len(s)-1]
+	d.SetId(ipAddressID)
+	d.Set("ip_address_id", ipAddressID)
+
+	// Make sure the IP address exists
+	if _, count, err := cs.Address.GetPublicIpAddressByID(
+		ipAddressID,
+		cloudstack.WithProject(d.Get("project").(string)),
+	); err != nil {
+		if count == 0 {
+			return nil, fmt.Errorf("IP address with ID %s does not exist", ipAddressID)
+		}
+		return nil, err
+	}
+
+	// Get all the forwards configured for this IP address
+	p := cs.Firewall.NewListPortForwardingRulesParams()
+	p.SetIpaddressid(ipAddressID)
+	p.SetListall(true)
+
+	if err := setProjectid(p, cs, d); err != nil {
+		return nil, err
+	}
+
+	l, err := cs.Firewall.ListPortForwardingRules(p)
+	if err != nil {
+		return nil, err
+	}
+
+	forwards := resourceCloudStackPortForward().Schema["forward"].ZeroValue().(*schema.Set)
+	for _, f := range l.PortForwardingRules {
+		privPort, err := strconv.Atoi(f.Privateport)
+		if err != nil {
+			return nil, err
+		}
+
+		pubPort, err := strconv.Atoi(f.Publicport)
+		if err != nil {
+			return nil, err
+		}
+
+		// Leave vm_guest_ip unset here, mirroring Read: it's only meant to
+		// reflect a value the user explicitly configured, not whatever
+		// CloudStack auto-selected. Populating it from the API on import
+		// would bake the auto-selected IP into the set element's hash,
+		// permanently diverging from a config that leaves it unset.
+		forward := map[string]interface{}{
+			"protocol":           f.Protocol,
+			"private_port":       privPort,
+			"public_port":        pubPort,
+			"virtual_machine_id": f.Virtualmachineid,
+			"uuid":               f.Id,
+		}
+
+		if f.Privateendport != "" && f.Privateendport != f.Privateport {
+			privEndPort, err := strconv.Atoi(f.Privateendport)
+			if err != nil {
+				return nil, err
+			}
+			forward["private_end_port"] = privEndPort
+		}
+
+		if f.Publicendport != "" && f.Publicendport != f.Publicport {
+			pubEndPort, err := strconv.Atoi(f.Publicendport)
+			if err != nil {
+				return nil, err
+			}
+			forward["public_end_port"] = pubEndPort
+		}
+
+		forwards.Add(forward)
+	}
+
+	d.Set("forward", forwards)
+	d.Set("managed", true)
+
+	return []*schema.ResourceData{d}, nil
+}
+
 func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) error {
 	cs := meta.(*cloudstack.CloudStackClient)
 
@@ -276,6 +367,8 @@ func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{})
 		return err
 	}
 
+	d.Set("ip_address_id", d.Id())
+
 	// Set the project if the IP address belongs to one
 	setValueOrID(d, "project", ip.Project, ip.Projectid)
 
diff --git a/cloudstack/resource_cloudstack_port_forward_test.go b/cloudstack/resource_cloudstack_port_forward_test.go
index c653ad1..e770425 100644
--- a/cloudstack/resource_cloudstack_port_forward_test.go
+++ b/cloudstack/resource_cloudstack_port_forward_test.go
@@ -57,6 +57,29 @@ func TestAccCloudStackPortForward_basic(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackPortForward_import(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:     func() { testAccPreCheck(t) },
+		Providers:    testAccProviders,
+		CheckDestroy: testAccCheckCloudStackPortForwardDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackPortForward_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_port_forward.foo",
+				ImportState:       true,
+				ImportStateVerify: true,
+				// The importer sets managed=true to bring all existing forwards for the
+				// IP address under this resource's control; the applied config leaves
+				// it at its default (false), so we ignore it for verification.
+				ImportStateVerifyIgnore: []string{"managed"},
+			},
+		},
+	})
+}
+
 func TestAccCloudStackPortForward_update(t *testing.T) {
 	resource.Test(t, resource.TestCase{
 		PreCheck:     func() { testAccPreCheck(t) },
diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go
index 323e3b8..7c427d7 100644
--- a/cloudstack/resource_cloudstack_service_offering.go
+++ b/cloudstack/resource_cloudstack_service_offering.go
@@ -34,6 +34,9 @@ func resourceCloudStackServiceOffering() *schema.Resource {
 		Read:   resourceCloudStackServiceOfferingRead,
 		Update: resourceCloudStackServiceOfferingUpdate,
 		Delete: resourceCloudStackServiceOfferingDelete,
+		Importer: &schema.ResourceImporter{
+			State: resourceCloudStackServiceOfferingImport,
+		},
 		Schema: map[string]*schema.Schema{
 			"name": {
 				Type:     schema.TypeString,
@@ -248,6 +251,23 @@ func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interf
 	return resourceCloudStackServiceOfferingRead(d, meta)
 }
 
+func resourceCloudStackServiceOfferingImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
+	cs := meta.(*cloudstack.CloudStackClient)
+
+	// Read looks the offering up by name, so resolve the name from the ID first
+	s, count, err := cs.ServiceOffering.GetServiceOfferingByID(d.Id())
+	if err != nil {
+		if count == 0 {
+			return nil, fmt.Errorf("service offering with ID %s does not exist", d.Id())
+		}
+		return nil, err
+	}
+
+	d.Set("name", s.Name)
+
+	return []*schema.ResourceData{d}, nil
+}
+
 func resourceCloudStackServiceOfferingRead(d *schema.ResourceData, meta interface{}) error {
 	cs := meta.(*cloudstack.CloudStackClient)
 	log.Printf("[DEBUG] Retrieving Service Offering %s", d.Get("name").(string))
diff --git a/cloudstack/resource_cloudstack_service_offering_test.go b/cloudstack/resource_cloudstack_service_offering_test.go
index c403428..9bb34f3 100644
--- a/cloudstack/resource_cloudstack_service_offering_test.go
+++ b/cloudstack/resource_cloudstack_service_offering_test.go
@@ -47,6 +47,24 @@ func TestAccCloudStackServiceOffering_basic(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackServiceOffering_import(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:  func() { testAccPreCheck(t) },
+		Providers: testAccProviders,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackServiceOffering_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_service_offering.test1",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
 const testAccCloudStackServiceOffering_basic = `
 resource "cloudstack_service_offering" "test1" {
   name 			= "service_offering_1"
diff --git a/cloudstack/resource_cloudstack_static_nat.go b/cloudstack/resource_cloudstack_static_nat.go
index 2b2c402..0e6e2a8 100644
--- a/cloudstack/resource_cloudstack_static_nat.go
+++ b/cloudstack/resource_cloudstack_static_nat.go
@@ -34,6 +34,9 @@ func resourceCloudStackStaticNAT() *schema.Resource {
 		Exists: resourceCloudStackStaticNATExists,
 		Read:   resourceCloudStackStaticNATRead,
 		Delete: resourceCloudStackStaticNATDelete,
+		Importer: &schema.ResourceImporter{
+			State: importStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"ip_address_id": {
@@ -160,6 +163,7 @@ func resourceCloudStackStaticNATRead(d *schema.ResourceData, meta interface{}) e
 
 	d.Set("virtual_machine_id", ip.Virtualmachineid)
 	d.Set("vm_guest_ip", ip.Vmipaddress)
+	d.Set("ip_address_id", ip.Id)
 
 	setValueOrID(d, "project", ip.Project, ip.Projectid)
 
diff --git a/cloudstack/resource_cloudstack_static_nat_test.go b/cloudstack/resource_cloudstack_static_nat_test.go
index eebebcb..8004718 100644
--- a/cloudstack/resource_cloudstack_static_nat_test.go
+++ b/cloudstack/resource_cloudstack_static_nat_test.go
@@ -48,6 +48,25 @@ func TestAccCloudStackStaticNAT_basic(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackStaticNAT_import(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:     func() { testAccPreCheck(t) },
+		Providers:    testAccProviders,
+		CheckDestroy: testAccCheckCloudStackStaticNATDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackStaticNAT_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_static_nat.foo",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
 func testAccCheckCloudStackStaticNATExists(
 	n string, ipaddr *cloudstack.PublicIpAddress) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
diff --git a/cloudstack/resource_cloudstack_vpn_connection.go b/cloudstack/resource_cloudstack_vpn_connection.go
index a493e16..d1bf4d8 100644
--- a/cloudstack/resource_cloudstack_vpn_connection.go
+++ b/cloudstack/resource_cloudstack_vpn_connection.go
@@ -33,6 +33,9 @@ func resourceCloudStackVPNConnection() *schema.Resource {
 		Create: resourceCloudStackVPNConnectionCreate,
 		Read:   resourceCloudStackVPNConnectionRead,
 		Delete: resourceCloudStackVPNConnectionDelete,
+		Importer: &schema.ResourceImporter{
+			State: schema.ImportStatePassthrough,
+		},
 
 		Schema: map[string]*schema.Schema{
 			"customer_gateway_id": {
diff --git a/cloudstack/resource_cloudstack_vpn_connection_test.go b/cloudstack/resource_cloudstack_vpn_connection_test.go
index 55d42a7..1eef989 100644
--- a/cloudstack/resource_cloudstack_vpn_connection_test.go
+++ b/cloudstack/resource_cloudstack_vpn_connection_test.go
@@ -49,6 +49,25 @@ func TestAccCloudStackVPNConnection_basic(t *testing.T) {
 	})
 }
 
+func TestAccCloudStackVPNConnection_import(t *testing.T) {
+	resource.Test(t, resource.TestCase{
+		PreCheck:     func() { testAccPreCheck(t) },
+		Providers:    testAccProviders,
+		CheckDestroy: testAccCheckCloudStackVPNConnectionDestroy,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccCloudStackVPNConnection_basic,
+			},
+
+			{
+				ResourceName:      "cloudstack_vpn_connection.foo-bar",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
 func testAccCheckCloudStackVPNConnectionExists(
 	n string, vpnConnection *cloudstack.VpnConnection) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
diff --git a/website/docs/r/autoscale_policy.html.markdown b/website/docs/r/autoscale_policy.html.markdown
index 98da742..3abe3c2 100644
--- a/website/docs/r/autoscale_policy.html.markdown
+++ b/website/docs/r/autoscale_policy.html.markdown
@@ -61,3 +61,11 @@ The following arguments are supported:
 The following attributes are exported:
 
 * `id` - The autoscale policy ID.
+
+## Import
+
+Autoscale policies can be imported using the `id`, e.g.
+
+```shell
+$ terraform import cloudstack_autoscale_policy.default eb22f91-7454-4107-89f4-36afcdf33021
+```
diff --git a/website/docs/r/autoscale_vm_profile.html.markdown b/website/docs/r/autoscale_vm_profile.html.markdown
index 4213ccc..2a55b74 100644
--- a/website/docs/r/autoscale_vm_profile.html.markdown
+++ b/website/docs/r/autoscale_vm_profile.html.markdown
@@ -74,3 +74,14 @@ The following arguments are supported:
 The following attributes are exported:
 
 * `id` - The autoscale VM profile ID.
+
+## Import
+
+Autoscale VM profiles can be imported using the `id`, e.g.
+
+```shell
+$ terraform import cloudstack_autoscale_vm_profile.default eb22f91-7454-4107-89f4-36afcdf33021
+```
+
+*NOTE: Import acceptance test coverage is currently skipped due to an upstream
+cloudstack-go issue.*
diff --git a/website/docs/r/condition.html.markdown b/website/docs/r/condition.html.markdown
index 68f80d6..23956a3 100644
--- a/website/docs/r/condition.html.markdown
+++ b/website/docs/r/condition.html.markdown
@@ -59,3 +59,11 @@ The following arguments are supported:
 The following attributes are exported:
 
 * `id` - The condition ID.
+
+## Import
+
+Conditions can be imported using the `id`, e.g.
+
+```shell
+$ terraform import cloudstack_condition.default eb22f91-7454-4107-89f4-36afcdf33021
+```
diff --git a/website/docs/r/counter.html.markdown b/website/docs/r/counter.html.markdown
index 9699df9..0dada3f 100644
--- a/website/docs/r/counter.html.markdown
+++ b/website/docs/r/counter.html.markdown
@@ -41,3 +41,11 @@ The following arguments are supported:
 The following attributes are exported:
 
 * `id` - The counter ID.
+
+## Import
+
+Counters can be imported using the `id`, e.g.
+
+```shell
+$ terraform import cloudstack_counter.default eb22f91-7454-4107-89f4-36afcdf33021
+```
diff --git a/website/docs/r/ipaddress.html.markdown b/website/docs/r/ipaddress.html.markdown
index 2a059eb..19c1898 100644
--- a/website/docs/r/ipaddress.html.markdown
+++ b/website/docs/r/ipaddress.html.markdown
@@ -105,3 +105,18 @@ The following attributes are exported:
 
 * `id` - The ID of the acquired and associated IP address.
 * `ip_address` - The IP address that was acquired and associated.
+
+## Import
+
+IP addresses can be imported; use `<IPADDRESSID>` as the import ID. For
+example:
+
+```shell
+$ terraform import cloudstack_ipaddress.default 6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
+
+When importing into a project you need to prefix the import ID with the project name:
+
+```shell
+$ terraform import cloudstack_ipaddress.default my-project/6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
diff --git a/website/docs/r/loadbalancer_rule.html.markdown b/website/docs/r/loadbalancer_rule.html.markdown
index 890d044..00cd968 100644
--- a/website/docs/r/loadbalancer_rule.html.markdown
+++ b/website/docs/r/loadbalancer_rule.html.markdown
@@ -70,3 +70,21 @@ The following attributes are exported:
 
 * `id` - The load balancer rule ID.
 * `description` - The description of the load balancer rule.
+
+## Import
+
+Load balancer rules can be imported; use `<LOADBALANCERRULEID>` as the import ID. For
+example:
+
+```shell
+$ terraform import cloudstack_loadbalancer_rule.default 6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
+
+When importing into a project you need to prefix the import ID with the project name:
+
+```shell
+$ terraform import cloudstack_loadbalancer_rule.default my-project/6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
+
+*NOTE: `certificate_id` cannot be read back from the CloudStack API, so it will
+always be empty after an import.*
diff --git a/website/docs/r/network_offering.html.markdown b/website/docs/r/network_offering.html.markdown
index 8449299..e302b41 100644
--- a/website/docs/r/network_offering.html.markdown
+++ b/website/docs/r/network_offering.html.markdown
@@ -78,3 +78,7 @@ Network offerings can be imported; use `<NETWORKOFFERINGID>` as the import ID. F
 ```shell
 $ terraform import cloudstack_network_offering.example <NETWORKOFFERINGID>
 ```
+
+*NOTE: The importer looks up the network offering by ID and resolves the
+required `name` attribute from it, so it does not need to be set in the
+configuration beforehand.*
diff --git a/website/docs/r/port_forward.html.markdown b/website/docs/r/port_forward.html.markdown
index 7b3d753..273cb18 100644
--- a/website/docs/r/port_forward.html.markdown
+++ b/website/docs/r/port_forward.html.markdown
@@ -115,3 +115,23 @@ The following attributes are exported:
 * `id` - The ID of the IP address for which the port forwards are created.
 * `vm_guest_ip` - The IP address of the virtual machine that is used
     for the port forwarding rule.
+
+## Import
+
+Port forwards can be imported; use the `<IPADDRESSID>` for which the rules
+are configured as the import ID. For example:
+
+```shell
+$ terraform import cloudstack_port_forward.default 6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
+
+When importing into a project you need to prefix the import ID with the project name:
+
+```shell
+$ terraform import cloudstack_port_forward.default my-project/6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
+
+*NOTE: All existing port forwarding rules for the IP address are imported into
+a single `forward` set. Review and copy the imported rules into your config
+before running `terraform apply`, otherwise missing rules may be deleted if
+`managed` is `true`.*
diff --git a/website/docs/r/service_offering.html.markdown b/website/docs/r/service_offering.html.markdown
index 57cfe84..6a5754f 100644
--- a/website/docs/r/service_offering.html.markdown
+++ b/website/docs/r/service_offering.html.markdown
@@ -105,3 +105,7 @@ Service offerings can be imported; use `<SERVICEOFFERINGID>` as the import ID. F
 ```shell
 $ terraform import cloudstack_service_offering.example <SERVICEOFFERINGID>
 ```
+
+*NOTE: The importer looks up the service offering by ID and resolves the
+required `name` attribute from it, so it does not need to be set in the
+configuration beforehand.*
diff --git a/website/docs/r/static_nat.html.markdown b/website/docs/r/static_nat.html.markdown
index 7484782..f62bf94 100644
--- a/website/docs/r/static_nat.html.markdown
+++ b/website/docs/r/static_nat.html.markdown
@@ -43,3 +43,18 @@ The following attributes are exported:
 * `id` - The static nat ID.
 * `vm_guest_ip` - The IP address of the virtual machine that is used
     to forward the static NAT traffic to.
+
+## Import
+
+Static NAT can be imported; use the `<IPADDRESSID>` for which static NAT is
+enabled as the import ID. For example:
+
+```shell
+$ terraform import cloudstack_static_nat.default 6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
+
+When importing into a project you need to prefix the import ID with the project name:
+
+```shell
+$ terraform import cloudstack_static_nat.default my-project/6226ea4d-9cbe-4cc9-b30c-b9532146da5b
+```
diff --git a/website/docs/r/vpn_connection.html.markdown b/website/docs/r/vpn_connection.html.markdown
index 7a2f701..56896ac 100644
--- a/website/docs/r/vpn_connection.html.markdown
+++ b/website/docs/r/vpn_connection.html.markdown
@@ -36,3 +36,11 @@ The following arguments are supported:
 The following attributes are exported:
 
 * `id` - The ID of the VPN Connection.
+
+## Import
+
+VPN Connections can be imported using the `id`, e.g.
+
+```shell
+$ terraform import cloudstack_vpn_connection.default eb22f91-7454-4107-89f4-36afcdf33021
+```