diff --git a/examples/tencentcloud-eni/main.tf b/examples/tencentcloud-eni/main.tf new file mode 100644 index 0000000000..4af8cff709 --- /dev/null +++ b/examples/tencentcloud-eni/main.tf @@ -0,0 +1,89 @@ +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "${var.availability_zone}" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.0.0/16" + is_multicast = false +} + +resource "tencentcloud_security_group" "foo" { + name = "test-ci-eni-sg1" +} + +resource "tencentcloud_security_group" "bar" { + name = "test-ci-eni-sg2" +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + security_groups = ["${tencentcloud_security_group.foo.id}", "${tencentcloud_security_group.bar.id}"] + + ipv4s { + ip = "10.0.0.10" + primary = true + description = "new desc" + } + + ipv4s { + ip = "10.0.0.11" + primary = false + } + + ipv4s { + ip = "10.0.0.12" + primary = false + } + + tags = { + "test" = "test" + } +} + +data "tencentcloud_image" "my_favorite_image" { + os_name = "centos" + + filter { + name = "image-type" + values = ["PUBLIC_IMAGE"] + } +} + +data "tencentcloud_instance_types" "my_favorite_instance_types" { + filter { + name = "instance-family" + values = ["S3"] + } + + cpu_core_count = 4 + memory_size = 8 +} + +resource "tencentcloud_instance" "foo" { + instance_name = "ci-test-eni-attach" + availability_zone = "ap-guangzhou-3" + image_id = "${data.tencentcloud_image.my_favorite_image.image_id}" + instance_type = "${data.tencentcloud_instance_types.my_favorite_instance_types.instance_types.0.instance_type}" + system_disk_type = "CLOUD_PREMIUM" + disable_security_service = true + disable_monitor_service = true + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" +} + +resource "tencentcloud_eni_attachment" "foo" { + eni_id = "${tencentcloud_eni.foo.id}" + instance_id = "${tencentcloud_instance.foo.id}" +} + +data "tencentcloud_enis" "subnet" { + subnet_id = "${tencentcloud_eni.foo.subnet_id}" + security_group = "${tencentcloud_security_group.foo.id}" +} diff --git a/examples/tencentcloud-eni/variable.tf b/examples/tencentcloud-eni/variable.tf new file mode 100644 index 0000000000..8c0f4e325c --- /dev/null +++ b/examples/tencentcloud-eni/variable.tf @@ -0,0 +1,3 @@ +variable "availability_zone" { + default = "ap-guangzhou-3" +} diff --git a/tencentcloud/data_source_tc_enis.go b/tencentcloud/data_source_tc_enis.go new file mode 100644 index 0000000000..ba0a75188b --- /dev/null +++ b/tencentcloud/data_source_tc_enis.go @@ -0,0 +1,306 @@ +/* +Use this data source to query query ENIs. + +Example Usage + +```hcl +data "tencentcloud_enis" "name" { + name = "test eni" +} +``` +*/ +package tencentcloud + +import ( + "context" + "log" + + "github.com/hashicorp/terraform/helper/schema" + vpc "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vpc/v20170312" +) + +func dataSourceTencentCloudEnis() *schema.Resource { + return &schema.Resource{ + Read: dataSourceTencentCloudEnisRead, + Schema: map[string]*schema.Schema{ + "ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + ConflictsWith: []string{"vpc_id", "subnet_id", "instance_id", "security_group", "name", "description", "ipv4", "tags"}, + Description: "ID of the ENIs to be queried. Conflict with `vpc_id`,`subnet_id`,`instance_id`,`security_group`,`name`,`ipv4` and `tags`.", + }, + "vpc_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "ID of the vpc to be queried. Conflict with `ids`.", + }, + "subnet_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "ID of the subnet within this vpc to be queried. Conflict with `ids`.", + }, + "instance_id": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "ID of the instance which bind the ENI. Conflict with `ids`.", + }, + "security_group": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "A set of security group IDs which bind the ENI. Conflict with `ids`.", + }, + "name": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "Name of the ENI to be queried. Conflict with `ids`.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "Description of the ENI. Conflict with `ids`.", + }, + "ipv4": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "Intranet IP of the ENI. Conflict with `ids`.", + }, + "tags": { + Type: schema.TypeMap, + Optional: true, + ConflictsWith: []string{"ids"}, + Description: "Tags of the ENI. Conflict with `ids`.", + }, + "result_output_file": { + Type: schema.TypeString, + Optional: true, + Description: "Used to save results.", + }, + + // computed + "enis": { + Type: schema.TypeList, + Computed: true, + Description: "An information list of ENIs. Each element contains the following attributes:", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the ENI.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "Name of the ENI.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "Description of the ENI.", + }, + "vpc_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the vpc.", + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the subnet within this vpc.", + }, + "security_groups": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Description: "A set of security group IDs which bind the ENI.", + }, + "primary": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates whether the IP is primary.", + }, + "mac": { + Type: schema.TypeString, + Computed: true, + Description: "MAC address.", + }, + "state": { + Type: schema.TypeString, + Computed: true, + Description: "States of the ENI.", + }, + "ipv4s": { + Type: schema.TypeList, + Computed: true, + Description: "A set of intranet IPv4s.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Computed: true, + Description: "Intranet IP.", + }, + "primary": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates whether the IP is primary.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "Description of the IP.", + }, + }, + }, + }, + "instance_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the instance which bind the ENI.", + }, + "tags": { + Type: schema.TypeMap, + Computed: true, + Description: "Tags of the ENI.", + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: "Creation time of the ENI.", + }, + }, + }, + }, + }, + } +} + +func dataSourceTencentCloudEnisRead(d *schema.ResourceData, m interface{}) error { + defer logElapsed("data_source.tencentcloud_enis.read")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + service := VpcService{client: m.(*TencentCloudClient).apiV3Conn} + + var ( + ids []string + vpcId *string + subnetId *string + cvmId *string + sgId *string + name *string + desc *string + ipv4 *string + ) + + if raw, ok := d.GetOk("ids"); ok { + ids = expandStringList(raw.(*schema.Set).List()) + } + + if raw, ok := d.GetOk("vpc_id"); ok { + vpcId = stringToPointer(raw.(string)) + } + if raw, ok := d.GetOk("subnet_id"); ok { + subnetId = stringToPointer(raw.(string)) + } + if raw, ok := d.GetOk("instance_id"); ok { + cvmId = stringToPointer(raw.(string)) + } + if raw, ok := d.GetOk("security_group"); ok { + sgId = stringToPointer(raw.(string)) + } + if raw, ok := d.GetOk("name"); ok { + name = stringToPointer(raw.(string)) + } + if raw, ok := d.GetOk("description"); ok { + desc = stringToPointer(raw.(string)) + } + if raw, ok := d.GetOk("ipv4"); ok { + ipv4 = stringToPointer(raw.(string)) + } + tags := getTags(d, "tags") + + var ( + respEnis []*vpc.NetworkInterface + err error + ) + + if len(ids) > 0 { + respEnis, err = service.DescribeEniById(ctx, ids) + } else { + respEnis, err = service.DescribeEniByFilters(ctx, vpcId, subnetId, cvmId, sgId, name, desc, ipv4, tags) + } + + if err != nil { + return err + } + + enis := make([]map[string]interface{}, 0, len(respEnis)) + eniIds := make([]string, 0, len(respEnis)) + + for _, eni := range respEnis { + ipv4s := make([]map[string]interface{}, 0, len(eni.PrivateIpAddressSet)) + for _, ipv4 := range eni.PrivateIpAddressSet { + ipv4s = append(ipv4s, map[string]interface{}{ + "ip": ipv4.PrivateIpAddress, + "primary": ipv4.Primary, + "description": eni.NetworkInterfaceDescription, + }) + } + + sgs := make([]string, 0, len(eni.GroupSet)) + for _, sg := range eni.GroupSet { + sgs = append(sgs, *sg) + } + + respTags := make(map[string]string, len(eni.TagSet)) + for _, tag := range eni.TagSet { + respTags[*tag.Key] = *tag.Value + } + + eniIds = append(eniIds, *eni.NetworkInterfaceId) + + m := map[string]interface{}{ + "id": eni.NetworkInterfaceId, + "name": eni.NetworkInterfaceName, + "description": eni.NetworkInterfaceDescription, + "vpc_id": eni.VpcId, + "subnet_id": eni.SubnetId, + "primary": eni.Primary, + "mac": eni.MacAddress, + "state": eni.State, + "create_time": eni.CreatedTime, + "ipv4s": ipv4s, + "security_groups": sgs, + "tags": respTags, + } + + if eni.Attachment != nil { + m["instance_id"] = eni.Attachment.InstanceId + } + + enis = append(enis, m) + } + + d.Set("enis", enis) + d.SetId(dataResourceIdsHash(eniIds)) + + if output, ok := d.GetOk("result_output_file"); ok && output.(string) != "" { + if err := writeToFile(output.(string), enis); err != nil { + log.Printf("[CRITAL]%s output file[%s] fail, reason[%v]", + logId, output.(string), err) + return err + } + } + + return nil +} diff --git a/tencentcloud/data_source_tc_enis_test.go b/tencentcloud/data_source_tc_enis_test.go new file mode 100644 index 0000000000..2e57c4c552 --- /dev/null +++ b/tencentcloud/data_source_tc_enis_test.go @@ -0,0 +1,239 @@ +package tencentcloud + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccDataSourceTencentCloudEnis_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: TestAccDataSourceTencentCloudEnisBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckTencentCloudDataSourceID("data.tencentcloud_enis.foo"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "ids.#", "1"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.#", "1"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.foo", "enis.0.id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.name", "ci-test-eni"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.description", "eni desc"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.foo", "enis.0.vpc_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.foo", "enis.0.subnet_id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.security_groups.#", "1"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.primary", "false"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.foo", "enis.0.mac"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.foo", "enis.0.create_time"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.ipv4s.#", "1"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.foo", "enis.0.ipv4s.0.ip"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.ipv4s.0.primary", "true"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.foo", "enis.0.ipv4s.0.description", "eni desc"), + ), + }, + }, + }) +} + +func TestAccDataSourceTencentCloudEnis_filter(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: TestAccDataSourceTencentCloudEnisFilter, + Check: resource.ComposeTestCheckFunc( + testAccCheckTencentCloudDataSourceID("data.tencentcloud_enis.vpc"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.vpc", "vpc_id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.#", "1"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.vpc", "enis.0.id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.name", "ci-test-eni"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.description", "eni desc"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.vpc", "enis.0.vpc_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.vpc", "enis.0.subnet_id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.security_groups.#", "1"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.primary", "false"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.vpc", "enis.0.mac"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.vpc", "enis.0.create_time"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.tags.test", "test"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.ipv4s.#", "1"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.vpc", "enis.0.ipv4s.0.ip"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.ipv4s.0.primary", "true"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.vpc", "enis.0.ipv4s.0.description", "eni desc"), + + testAccCheckTencentCloudDataSourceID("data.tencentcloud_enis.subnet"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "subnet_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "security_group"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.#", "1"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "enis.0.id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.name", "ci-test-eni"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.description", "eni desc"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "enis.0.vpc_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "enis.0.subnet_id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.security_groups.#", "1"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.primary", "false"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "enis.0.mac"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "enis.0.create_time"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.tags.test", "test"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.ipv4s.#", "1"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.subnet", "enis.0.ipv4s.0.ip"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.ipv4s.0.primary", "true"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.subnet", "enis.0.ipv4s.0.description", "eni desc"), + + testAccCheckTencentCloudDataSourceID("data.tencentcloud_enis.name"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.name", "name", "ci-test-eni"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.name", "enis.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.id"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.name", "enis.0.name", "ci-test-eni"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.description"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.vpc_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.subnet_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.primary"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.mac"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.name", "enis.0.state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.create_time"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.name", "enis.0.ipv4s.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.ipv4s.0.ip"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.name", "enis.0.ipv4s.0.primary"), + + testAccCheckTencentCloudDataSourceID("data.tencentcloud_enis.description"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "description"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.description", "enis.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.name"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.description", "enis.0.description", "eni desc"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.vpc_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.subnet_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.primary"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.mac"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.description", "enis.0.state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.create_time"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.description", "enis.0.ipv4s.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.ipv4s.0.ip"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.description", "enis.0.ipv4s.0.primary"), + + testAccCheckTencentCloudDataSourceID("data.tencentcloud_enis.ipv4"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "ipv4"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.ipv4", "enis.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.name"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.description"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.vpc_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.subnet_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.primary"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.mac"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.ipv4", "enis.0.state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.create_time"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.ipv4", "enis.0.ipv4s.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.ipv4s.0.ip"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.ipv4", "enis.0.ipv4s.0.primary"), + + testAccCheckTencentCloudDataSourceID("data.tencentcloud_enis.tags"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.tags", "enis.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.name"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.description"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.vpc_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.subnet_id"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.primary"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.mac"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.tags", "enis.0.state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.create_time"), + resource.TestCheckResourceAttr("data.tencentcloud_enis.tags", "enis.0.tags.test", "test"), + resource.TestMatchResourceAttr("data.tencentcloud_enis.tags", "enis.0.ipv4s.#", regexp.MustCompile(`^[1-9]\d*$`)), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.ipv4s.0.ip"), + resource.TestCheckResourceAttrSet("data.tencentcloud_enis.tags", "enis.0.ipv4s.0.primary"), + ), + }, + }, + }) +} + +const testAccEnisVpc = ` +variable "availability_zone" { + default = "ap-guangzhou-3" +} + +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "${var.availability_zone}" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.0.0/16" + is_multicast = false +} +` + +const TestAccDataSourceTencentCloudEnisBasic = testAccEnisVpc + ` + +resource "tencentcloud_security_group" "foo" { + name = "test-ci-eni-sg1" +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + security_groups = ["${tencentcloud_security_group.foo.id}"] + ipv4_count = 1 +} + +data "tencentcloud_enis" "foo" { + ids = ["${tencentcloud_eni.foo.id}"] +} +` + +const TestAccDataSourceTencentCloudEnisFilter = testAccEnisVpc + ` + +resource "tencentcloud_security_group" "foo" { + name = "test-ci-eni-sg1" +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + security_groups = ["${tencentcloud_security_group.foo.id}"] + ipv4_count = 1 + + tags = { + "test" = "test" + } +} + +data "tencentcloud_enis" "vpc" { + vpc_id = "${tencentcloud_eni.foo.vpc_id}" +} + +data "tencentcloud_enis" "subnet" { + subnet_id = "${tencentcloud_eni.foo.subnet_id}" + security_group = "${tencentcloud_security_group.foo.id}" +} + +data "tencentcloud_enis" "name" { + name = "${tencentcloud_eni.foo.name}" +} + +data "tencentcloud_enis" "description" { + description = "${tencentcloud_eni.foo.description}" +} + +data "tencentcloud_enis" "ipv4" { + ipv4 = "${tencentcloud_eni.foo.ipv4_info.0.ip}" +} + +data "tencentcloud_enis" "tags" { + tags = "${tencentcloud_eni.foo.tags}" +} +` diff --git a/tencentcloud/extension_vpc.go b/tencentcloud/extension_vpc.go index 520335e7b2..e8e90fdde5 100644 --- a/tencentcloud/extension_vpc.go +++ b/tencentcloud/extension_vpc.go @@ -35,3 +35,24 @@ const ( EIP_STATUS_OFFLINING = "OFFLINING" EIP_STATUS_BIND_ENI = "BIND_ENI" ) + +// ENI +const ( + ENI_DESCRIBE_LIMIT = 100 +) + +const ( + ENI_STATE_PENDING = "PENDING" + ENI_STATE_AVAILABLE = "AVAILABLE" + ENI_STATE_ATTACHING = "ATTACHING" + ENI_STATE_DETACHING = "DETACHING" + ENI_STATE_DELETING = "DELETING" +) + +const ( + ENI_IP_PENDING = "PENDING" + ENI_IP_AVAILABLE = "AVAILABLE" + ENI_IP_ATTACHING = "ATTACHING" + ENI_IP_DETACHING = "DETACHING" + ENI_IP_DELETING = "DELETING" +) diff --git a/tencentcloud/provider.go b/tencentcloud/provider.go index 57c165cc35..012746f4e9 100644 --- a/tencentcloud/provider.go +++ b/tencentcloud/provider.go @@ -44,6 +44,7 @@ Data Sources tencentcloud_dnats tencentcloud_eip tencentcloud_eips + tencentcloud_enis tencentcloud_gaap_certificates tencentcloud_gaap_http_domains tencentcloud_gaap_http_rules @@ -164,6 +165,8 @@ SSL Resources tencentcloud_ssl_certificate VPC Resources + tencentcloud_eni + tencentcloud_eni_attachment tencentcloud_vpc tencentcloud_subnet tencentcloud_security_group @@ -274,6 +277,7 @@ func Provider() *schema.Provider { "tencentcloud_placement_groups": dataSourceTencentCloudPlacementGroups(), "tencentcloud_eips": dataSourceTencentCloudEips(), "tencentcloud_key_pairs": dataSourceTencentCloudKeyPairs(), + "tencentcloud_enis": dataSourceTencentCloudEnis(), }, ResourcesMap: map[string]*schema.Resource{ @@ -341,6 +345,8 @@ func Provider() *schema.Provider { "tencentcloud_ssl_certificate": resourceTencentCloudSslCertificate(), "tencentcloud_security_group_lite_rule": resourceTencentCloudSecurityGroupLiteRule(), "tencentcloud_placement_group": resourceTencentCloudPlacementGroup(), + "tencentcloud_eni": resourceTencentCloudEni(), + "tencentcloud_eni_attachment": resourceTencentCloudEniAttachment(), }, ConfigureFunc: providerConfigure, diff --git a/tencentcloud/resource_tc_eni.go b/tencentcloud/resource_tc_eni.go new file mode 100644 index 0000000000..286bd14704 --- /dev/null +++ b/tencentcloud/resource_tc_eni.go @@ -0,0 +1,674 @@ +/* +Provides a resource to create an ENI. + +Example Usage + +```hcl +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "ap-guangzhou-3" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.0.0/16" + is_multicast = false +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 1 +} +``` + +Import + +ENI can be imported using the id, e.g. + +``` + $ terraform import tencentcloud_eni.foo eni-qka182br +``` +*/ +package tencentcloud + +import ( + "context" + "errors" + "fmt" + "net" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" +) + +func eniIpOutputResource() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Computed: true, + Description: "Intranet IP.", + }, + "primary": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates whether the IP is primary.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "Description of the IP.", + }, + }, + } +} + +func resourceTencentCloudEni() *schema.Resource { + return &schema.Resource{ + Create: resourceTencentCloudEniCreate, + Read: resourceTencentCloudEniRead, + Update: resourceTencentCloudEniUpdate, + Delete: resourceTencentCloudEniDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateStringLengthInRange(0, 60), + Description: "Name of the ENI, maximum length 60.", + }, + "vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the vpc.", + }, + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the subnet within this vpc.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + ValidateFunc: validateStringLengthInRange(0, 60), + Description: "Description of the ENI, maximum length 60.", + }, + "security_groups": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Description: "A set of security group IDs.", + }, + "ipv4s": { + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{"ipv4_count"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + Description: "Intranet IP.", + }, + "primary": { + Type: schema.TypeBool, + Required: true, + Description: "Indicates whether the IP is primary.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "Description of the IP, maximum length 25.", + ValidateFunc: validateStringLengthInRange(0, 25), + }, + }, + }, + MaxItems: 30, + Description: "Applying for intranet IPv4s collection, conflict with `ipv4_count`. When there are multiple ipv4s, can only be one primary IP, and the maximum length of the array is 30. Each element contains the following attributes:", + }, + "ipv4_count": { + Type: schema.TypeInt, + Optional: true, + ConflictsWith: []string{"ipv4s"}, + ValidateFunc: validateIntegerInRange(1, 30), + Description: "The number of intranet IPv4s. When it is greater than 1, there is only one primary intranet IP. The others are auxiliary intranet IPs, which conflict with `ipv4s`.", + }, + "tags": { + Type: schema.TypeMap, + Optional: true, + Description: "Tags of the ENI.", + }, + + // computed + "mac": { + Type: schema.TypeString, + Computed: true, + Description: "MAC address.", + }, + "state": { + Type: schema.TypeString, + Computed: true, + Description: "State of the ENI.", + }, + "primary": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates whether the IP is primary.", + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: "Creation time of the ENI.", + }, + "ipv4_info": { + Type: schema.TypeList, + Elem: eniIpOutputResource(), + Computed: true, + Description: "An information list of IPv4s. Each element contains the following attributes:", + }, + }, + } +} + +func resourceTencentCloudEniCreate(d *schema.ResourceData, m interface{}) error { + defer logElapsed("resource.tencentcloud_eni.create")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + name := d.Get("name").(string) + vpcId := d.Get("vpc_id").(string) + subnetId := d.Get("subnet_id").(string) + desc := d.Get("description").(string) + + var ( + securityGroups []string + ipv4s []VpcEniIP + ipv4Count *int + ) + + if raw, ok := d.GetOk("security_groups"); ok { + securityGroups = expandStringList(raw.(*schema.Set).List()) + } + + if raw, ok := d.GetOk("ipv4s"); ok { + set := raw.(*schema.Set) + ipv4s = make([]VpcEniIP, 0, set.Len()) + var hasPrimary bool + + for _, v := range set.List() { + m := v.(map[string]interface{}) + + ipStr := m["ip"].(string) + ip := net.ParseIP(ipStr) + if ip == nil { + return fmt.Errorf("ip %s is invalid", ipStr) + } + + primary := m["primary"].(bool) + + switch { + case !hasPrimary && primary: + hasPrimary = true + + case hasPrimary && primary: + return errors.New("only can have a primary ipv4") + } + + ipv4 := VpcEniIP{ + ip: ip, + primary: primary, + } + + ipv4.desc = stringToPointer(m["description"].(string)) + + ipv4s = append(ipv4s, ipv4) + } + + if !hasPrimary { + return errors.New("need a primary ipv4") + } + } + + if raw, ok := d.GetOk("ipv4_count"); ok { + ipv4Count = common.IntPtr(raw.(int)) + } + + if len(ipv4s) == 0 && ipv4Count == nil { + return errors.New("ipv4s or ipv4_count must be set") + } + + tags := getTags(d, "tags") + + client := m.(*TencentCloudClient).apiV3Conn + vpcService := VpcService{client: client} + tagService := TagService{client: client} + region := client.Region + + var ( + id string + err error + ) + + switch { + case len(ipv4s) > 0 && len(ipv4s) <= 10: + id, err = vpcService.CreateEni(ctx, name, vpcId, subnetId, desc, securityGroups, nil, ipv4s) + if err != nil { + return err + } + + d.SetId(id) + + case len(ipv4s) > 0: + // eni should create with primary ipv4 + for i := 0; i < len(ipv4s); i++ { + if ipv4s[i].primary { + if i < 10 { + break + } + + // move primary ip to the first + ipv4s[0], ipv4s[i] = ipv4s[i], ipv4s[0] + break + } + } + + ipv4ss := chunkEniIP(ipv4s) + withPrimaryIpv4s := ipv4ss[0] + + id, err = vpcService.CreateEni(ctx, name, vpcId, subnetId, desc, securityGroups, nil, withPrimaryIpv4s) + if err != nil { + return err + } + + d.SetId(id) + + for _, ipv4s := range ipv4ss[1:] { + if err = vpcService.AssignIpv4ToEni(ctx, id, ipv4s, nil); err != nil { + return err + } + } + + case ipv4Count != nil && *ipv4Count <= 10: + id, err = vpcService.CreateEni(ctx, name, vpcId, subnetId, desc, securityGroups, ipv4Count, nil) + if err != nil { + return err + } + + d.SetId(id) + + case ipv4Count != nil: + count := *ipv4Count + + id, err = vpcService.CreateEni(ctx, name, vpcId, subnetId, desc, securityGroups, common.IntPtr(10), nil) + if err != nil { + return err + } + + d.SetId(id) + + count -= 10 + for count > 10 { + if err = vpcService.AssignIpv4ToEni(ctx, id, nil, common.IntPtr(10)); err != nil { + return err + } + count -= 10 + } + // assign last ip + if count > 0 { + if err = vpcService.AssignIpv4ToEni(ctx, id, nil, &count); err != nil { + return err + } + } + } + + if len(tags) > 0 { + resourceName := BuildTagResourceName("vpc", "eni", region, id) + if err := tagService.ModifyTags(ctx, resourceName, tags, nil); err != nil { + return err + } + } + + return resourceTencentCloudEniRead(d, m) +} + +func resourceTencentCloudEniRead(d *schema.ResourceData, m interface{}) error { + defer logElapsed("resource.tencentcloud_eni.read")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + id := d.Id() + + service := VpcService{client: m.(*TencentCloudClient).apiV3Conn} + + enis, err := service.DescribeEniById(ctx, []string{id}) + if err != nil { + return err + } + + if len(enis) < 1 { + d.SetId("") + return nil + } + + eni := enis[0] + + d.Set("name", eni.NetworkInterfaceName) + d.Set("vpc_id", eni.VpcId) + d.Set("subnet_id", eni.SubnetId) + d.Set("description", eni.NetworkInterfaceDescription) + d.Set("mac", eni.MacAddress) + d.Set("state", eni.State) + d.Set("primary", eni.Primary) + d.Set("create_time", eni.CreatedTime) + + sgs := make([]string, 0, len(eni.GroupSet)) + for _, sg := range eni.GroupSet { + sgs = append(sgs, *sg) + } + d.Set("security_groups", sgs) + + ipv4s := make([]map[string]interface{}, 0, len(eni.PrivateIpAddressSet)) + for _, ipv4 := range eni.PrivateIpAddressSet { + ipv4s = append(ipv4s, map[string]interface{}{ + "ip": ipv4.PrivateIpAddress, + "primary": ipv4.Primary, + "description": ipv4.Description, + }) + } + d.Set("ipv4_info", ipv4s) + + _, manually := d.GetOk("ipv4s") + _, count := d.GetOk("ipv4_count") + if !manually && !count { + // import mode + d.Set("ipv4_count", len(ipv4s)) + } + + tags := make(map[string]string, len(eni.TagSet)) + for _, tag := range eni.TagSet { + tags[*tag.Key] = *tag.Value + } + d.Set("tags", tags) + + return nil +} + +func resourceTencentCloudEniUpdate(d *schema.ResourceData, m interface{}) error { + defer logElapsed("resource.tencentcloud_eni.update")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + id := d.Id() + + d.Partial(true) + + client := m.(*TencentCloudClient).apiV3Conn + vpcService := VpcService{client: client} + tagService := TagService{client: client} + region := client.Region + + var ( + name *string + desc *string + sgs []string + updateAttrs []string + ) + + if d.HasChange("name") { + updateAttrs = append(updateAttrs, "name") + name = stringToPointer(d.Get("name").(string)) + } + + if d.HasChange("description") { + updateAttrs = append(updateAttrs, "description") + desc = stringToPointer(d.Get("description").(string)) + } + + if d.HasChange("security_groups") { + updateAttrs = append(updateAttrs, "security_groups") + } + sgs = expandStringList(d.Get("security_groups").(*schema.Set).List()) + + if len(updateAttrs) > 0 { + if err := vpcService.ModifyEniAttribute(ctx, id, name, desc, sgs); err != nil { + return err + } + + for _, attr := range updateAttrs { + d.SetPartial(attr) + } + } + + // if ipv4 set manually + if _, ok := d.GetOk("ipv4s"); ok && d.HasChange("ipv4s") { + oldRaw, newRaw := d.GetChange("ipv4s") + oldSet := oldRaw.(*schema.Set) + newSet := newRaw.(*schema.Set) + + if newSet.Len() == 0 { + return errors.New("can't remove all ipv4s") + } + + removeSet := oldSet.Difference(newSet).List() + addSet := newSet.Difference(oldSet).List() + + var modifyPrimaryIpv4 *VpcEniIP + + removeIpv4 := make([]string, 0, len(removeSet)) + for _, v := range removeSet { + m := v.(map[string]interface{}) + if m["primary"].(bool) { + // check if only modify primary description + modifyPrimaryIpv4 = &VpcEniIP{ + ip: net.ParseIP(m["ip"].(string)), + } + continue + } + + removeIpv4 = append(removeIpv4, m["ip"].(string)) + } + + addIpv4 := make([]VpcEniIP, 0, len(addSet)) + newPrimaryCount := 0 + for _, v := range addSet { + m := v.(map[string]interface{}) + + ipStr := m["ip"].(string) + ip := net.ParseIP(ipStr) + if ip == nil { + return fmt.Errorf("ip %s is invalid", ipStr) + } + + if m["primary"].(bool) { + if modifyPrimaryIpv4 == nil { + return errors.New("can't set more than one primary ipv4") + } + + // if newPrimaryCount > 1, means new ipv4s have more than one primary ipv4, + // if only one, maybe just update primary ipv4 description + newPrimaryCount++ + if newPrimaryCount > 1 { + return errors.New("can't set more than one primary ipv4") + } + + // only can update primary ipv4 description + if modifyPrimaryIpv4.ip.String() != ipStr { + return errors.New("can't change primary ipv4") + } + + modifyPrimaryIpv4.desc = stringToPointer(m["description"].(string)) + continue + } + + ipv4 := VpcEniIP{ + ip: ip, + primary: m["primary"].(bool), + desc: stringToPointer(m["description"].(string)), + } + + addIpv4 = append(addIpv4, ipv4) + } + + if modifyPrimaryIpv4 != nil { + // if desc is nil, means remove primary ipv4 but not add same primary ipv4, + // that means not just update primary ipv4 description, user remove primary ipv4 + if modifyPrimaryIpv4.desc == nil { + return errors.New("can't remove primary ipv4") + } + + if err := vpcService.ModifyEniPrimaryIpv4Desc(ctx, id, modifyPrimaryIpv4.ip.String(), modifyPrimaryIpv4.desc); err != nil { + return err + } + } + + if len(removeIpv4) > 0 { + if len(removeIpv4) <= 10 { + if err := vpcService.UnAssignIpv4FromEni(ctx, id, removeIpv4); err != nil { + return err + } + } else { + for _, remove := range chunkRemoveIpv4(removeIpv4) { + if err := vpcService.UnAssignIpv4FromEni(ctx, id, remove); err != nil { + return err + } + } + } + } + + if len(addIpv4) > 0 { + if len(addIpv4) <= 10 { + if err := vpcService.AssignIpv4ToEni(ctx, id, addIpv4, nil); err != nil { + return err + } + } else { + for _, add := range chunkEniIP(addIpv4) { + if err := vpcService.AssignIpv4ToEni(ctx, id, add, nil); err != nil { + return err + } + } + } + } + + d.SetPartial("ipv4s") + } + + if _, ok := d.GetOk("ipv4_count"); ok { + if d.HasChange("ipv4_count") { + oldRaw, newRaw := d.GetChange("ipv4_count") + oldCount := oldRaw.(int) + newCount := newRaw.(int) + + if newCount > oldCount { + count := newCount - oldCount + + if count <= 10 { + if err := vpcService.AssignIpv4ToEni(ctx, id, nil, &count); err != nil { + return err + } + } else { + for count > 10 { + if err := vpcService.AssignIpv4ToEni(ctx, id, nil, common.IntPtr(10)); err != nil { + return err + } + count -= 10 + } + // assign last ip + if count > 0 { + if err := vpcService.AssignIpv4ToEni(ctx, id, nil, &count); err != nil { + return err + } + } + } + } else { + removeCount := oldCount - newCount + list := d.Get("ipv4_info").([]interface{}) + removeIpv4 := make([]string, 0, removeCount) + for _, v := range list { + if removeCount == 0 { + break + } + m := v.(map[string]interface{}) + if m["primary"].(bool) { + continue + } + removeIpv4 = append(removeIpv4, m["ip"].(string)) + removeCount-- + } + + if len(removeIpv4) <= 10 { + if err := vpcService.UnAssignIpv4FromEni(ctx, id, removeIpv4); err != nil { + return err + } + } else { + for _, remove := range chunkRemoveIpv4(removeIpv4) { + if err := vpcService.UnAssignIpv4FromEni(ctx, id, remove); err != nil { + return err + } + } + } + + d.SetPartial("ipv4_count") + } + } + } + + if d.HasChange("tags") { + oldTags, newTags := d.GetChange("tags") + replaceTags, deleteTags := diffTags(oldTags.(map[string]interface{}), newTags.(map[string]interface{})) + + resourceName := BuildTagResourceName("vpc", "eni", region, id) + + if err := tagService.ModifyTags(ctx, resourceName, replaceTags, deleteTags); err != nil { + return err + } + + d.SetPartial("tags") + } + + d.Partial(false) + + return resourceTencentCloudEniRead(d, m) +} + +func resourceTencentCloudEniDelete(d *schema.ResourceData, m interface{}) error { + defer logElapsed("resource.tencentcloud_eni.delete")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + id := d.Id() + + service := VpcService{client: m.(*TencentCloudClient).apiV3Conn} + + return service.DeleteEni(ctx, id) +} + +func chunkEniIP(ipv4s []VpcEniIP) [][]VpcEniIP { + if len(ipv4s) <= 10 { + return [][]VpcEniIP{ipv4s} + } + + first := ipv4s[:10] + return append([][]VpcEniIP{first}, chunkEniIP(ipv4s[10:])...) +} + +func chunkRemoveIpv4(ss []string) [][]string { + if len(ss) <= 10 { + return [][]string{ss} + } + + s := ss[:10] + return append([][]string{s}, chunkRemoveIpv4(ss[10:])...) +} diff --git a/tencentcloud/resource_tc_eni_attachment.go b/tencentcloud/resource_tc_eni_attachment.go new file mode 100644 index 0000000000..60a5457b63 --- /dev/null +++ b/tencentcloud/resource_tc_eni_attachment.go @@ -0,0 +1,185 @@ +/* +Provides a resource to detailed information of attached backend server to an ENI. + +Example Usage + +```hcl +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "ap-guangzhou-3" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.0.0/16" + is_multicast = false +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 1 +} + +data "tencentcloud_image" "my_favorite_image" { + os_name = "centos" + filter { + name = "image-type" + values = ["PUBLIC_IMAGE"] + } +} + +data "tencentcloud_instance_types" "my_favorite_instance_types" { + filter { + name = "instance-family" + values = ["S2"] + } + cpu_core_count = 1 + memory_size = 1 +} + +resource "tencentcloud_instance" "foo" { + instance_name = "ci-test-eni-attach" + availability_zone = "ap-guangzhou-3" + image_id = "${data.tencentcloud_image.my_favorite_image.image_id}" + instance_type = "${data.tencentcloud_instance_types.my_favorite_instance_types.instance_types.0.instance_type}" + system_disk_type = "CLOUD_PREMIUM" + disable_security_service = true + disable_monitor_service = true + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" +} + +resource "tencentcloud_eni_attachment" "foo" { + eni_id = "${tencentcloud_eni.foo.id}" + instance_id = "${tencentcloud_instance.foo.id}" +} +``` + +Import + +ENI attachment can be imported using the id, e.g. + +``` + $ terraform import tencentcloud_eni_attachment.foo eni-gtlvkjvz+ins-0h3a5new +``` +*/ +package tencentcloud + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceTencentCloudEniAttachment() *schema.Resource { + return &schema.Resource{ + Create: resourceTencentCloudEniAttachmentCreate, + Read: resourceTencentCloudEniAttachmentRead, + Delete: resourceTencentCloudEniAttachmentDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "eni_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the ENI.", + }, + "instance_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "ID of the instance which bind the ENI.", + }, + }, + } +} + +func resourceTencentCloudEniAttachmentCreate(d *schema.ResourceData, m interface{}) error { + defer logElapsed("resource.tencentcloud_eni_attachment.create")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + eniId := d.Get("eni_id").(string) + cvmId := d.Get("instance_id").(string) + + service := VpcService{client: m.(*TencentCloudClient).apiV3Conn} + + if err := service.AttachEniToCvm(ctx, eniId, cvmId); err != nil { + return err + } + + d.SetId(fmt.Sprintf("%s+%s", eniId, cvmId)) + + return resourceTencentCloudEniAttachmentRead(d, m) +} + +func resourceTencentCloudEniAttachmentRead(d *schema.ResourceData, m interface{}) error { + defer logElapsed("resource.tencentcloud_eni_attachment.read")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + id := d.Id() + split := strings.Split(id, "+") + if len(split) != 2 { + log.Printf("[CRITAL]%s id %s is invalid", logId, id) + d.SetId("") + return nil + } + + eniId := split[0] + + service := VpcService{client: m.(*TencentCloudClient).apiV3Conn} + + enis, err := service.DescribeEniById(ctx, []string{eniId}) + if err != nil { + return err + } + + if len(enis) < 1 { + d.SetId("") + return nil + } + + eni := enis[0] + + if eni.Attachment == nil { + d.SetId("") + return nil + } + + d.Set("eni_id", eni.NetworkInterfaceId) + d.Set("instance_id", eni.Attachment.InstanceId) + + return nil +} + +func resourceTencentCloudEniAttachmentDelete(d *schema.ResourceData, m interface{}) error { + defer logElapsed("resource.tencentcloud_eni_attachment.delete")() + logId := getLogId(contextNil) + ctx := context.WithValue(context.TODO(), "logId", logId) + + id := d.Id() + split := strings.Split(id, "+") + if len(split) != 2 { + log.Printf("[CRITAL]%s id %s is invalid", logId, id) + d.SetId("") + return nil + } + + eniId, cvmId := split[0], split[1] + + service := VpcService{client: m.(*TencentCloudClient).apiV3Conn} + + return service.DetachEniFromCvm(ctx, eniId, cvmId) +} diff --git a/tencentcloud/resource_tc_eni_attachment_test.go b/tencentcloud/resource_tc_eni_attachment_test.go new file mode 100644 index 0000000000..ecae4bb895 --- /dev/null +++ b/tencentcloud/resource_tc_eni_attachment_test.go @@ -0,0 +1,166 @@ +package tencentcloud + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccTencentCloudEniAttachment_basic(t *testing.T) { + var ( + eniId string + cvmId string + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEniAttachmentDestroy(&eniId), + Steps: []resource.TestStep{ + { + Config: testAccEniAttachmentBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniAttachmentExists("tencentcloud_eni_attachment.foo", &eniId, &cvmId), + resource.TestCheckResourceAttrSet("tencentcloud_eni_attachment.foo", "eni_id"), + resource.TestCheckResourceAttrSet("tencentcloud_eni_attachment.foo", "instance_id"), + ), + }, + { + ResourceName: "tencentcloud_eni_attachment.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckEniAttachmentExists(n string, eniId, cvmId *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no eni attachment id is set") + } + + split := strings.Split(rs.Primary.ID, "+") + *eniId, *cvmId = split[0], split[1] + + service := VpcService{client: testAccProvider.Meta().(*TencentCloudClient).apiV3Conn} + + enis, err := service.DescribeEniById(context.TODO(), []string{*eniId}) + if err != nil { + return err + } + + for _, e := range enis { + if e.NetworkInterfaceId == nil { + return errors.New("eni id is nil") + } + + if *e.NetworkInterfaceId == *eniId { + if e.Attachment == nil { + return errors.New("eni attachment is nil") + } + + if e.Attachment.InstanceId == nil { + return errors.New("eni attach instance id is nil") + } + + if *e.Attachment.InstanceId != *cvmId { + return errors.New("eni attach instance id is not right") + } + + return nil + } + } + + return fmt.Errorf("eni attachment not found: %s", rs.Primary.ID) + } +} + +func testAccCheckEniAttachmentDestroy(eniId *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*TencentCloudClient).apiV3Conn + service := VpcService{client: client} + + enis, err := service.DescribeEniById(context.TODO(), []string{*eniId}) + if err != nil { + return err + } + + if len(enis) > 0 { + return errors.New("eni still exists") + } + + return nil + } +} + +const testAccEniAttachmentBasic = ` +variable "availability_zone" { + default = "ap-guangzhou-3" +} + +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "${var.availability_zone}" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.20.0/28" + is_multicast = false +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 1 +} + +data "tencentcloud_image" "my_favorite_image" { + os_name = "centos" + filter { + name = "image-type" + values = ["PUBLIC_IMAGE"] + } +} + +data "tencentcloud_instance_types" "my_favorite_instance_types" { + filter { + name = "instance-family" + values = ["S2"] + } + cpu_core_count = 1 + memory_size = 1 +} + +resource "tencentcloud_instance" "foo" { + instance_name = "ci-test-eni-attach" + availability_zone = "ap-guangzhou-3" + image_id = "${data.tencentcloud_image.my_favorite_image.image_id}" + instance_type = "${data.tencentcloud_instance_types.my_favorite_instance_types.instance_types.0.instance_type}" + system_disk_type = "CLOUD_PREMIUM" + disable_security_service = true + disable_monitor_service = true + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" +} + +resource "tencentcloud_eni_attachment" "foo" { + eni_id = "${tencentcloud_eni.foo.id}" + instance_id = "${tencentcloud_instance.foo.id}" +} +` diff --git a/tencentcloud/resource_tc_eni_test.go b/tencentcloud/resource_tc_eni_test.go new file mode 100644 index 0000000000..a470c4b412 --- /dev/null +++ b/tencentcloud/resource_tc_eni_test.go @@ -0,0 +1,618 @@ +package tencentcloud + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccTencentCloudEni_basic(t *testing.T) { + var eniId string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEniDestroy(&eniId), + Steps: []resource.TestStep{ + { + Config: testAccEniBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "name", "ci-test-eni"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "description", "eni desc"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "security_groups.#", "0"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "vpc_id"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "subnet_id"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_count", "1"), + resource.TestCheckNoResourceAttr("tencentcloud_eni.foo", "tags"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "mac"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "primary", "false"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "create_time"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "1"), + ), + }, + { + ResourceName: "tencentcloud_eni.foo", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccTencentCloudEni_updateAttr(t *testing.T) { + var eniId string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEniDestroy(&eniId), + Steps: []resource.TestStep{ + { + Config: testAccEniBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "name", "ci-test-eni"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "description", "eni desc"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "security_groups.#", "0"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "vpc_id"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "subnet_id"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_count", "1"), + resource.TestCheckNoResourceAttr("tencentcloud_eni.foo", "tags"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "mac"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "primary", "false"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "create_time"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "1"), + ), + }, + { + Config: testAccEniUpdateAttr, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "name", "ci-test-eni-new"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "description", "eni desc new"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "security_groups.#", "2"), + ), + }, + { + Config: testAccEniUpdateTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "tags.test", "test"), + ), + }, + }, + }) +} + +func TestAccTencentCloudEni_updateCount(t *testing.T) { + var eniId string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEniDestroy(&eniId), + Steps: []resource.TestStep{ + { + Config: testAccEniBasic, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "name", "ci-test-eni"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "description", "eni desc"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "security_groups.#", "0"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "vpc_id"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "subnet_id"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_count", "1"), + resource.TestCheckNoResourceAttr("tencentcloud_eni.foo", "tags"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "mac"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "primary", "false"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "create_time"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "1"), + ), + }, + { + Config: testAccEniUpdateCountAdd, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_count", "30"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "30"), + ), + }, + { + Config: testAccEniUpdateCountSub, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_count", "20"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "20"), + ), + }, + }, + }) +} + +func TestAccTencentCloudEni_updateManually(t *testing.T) { + var eniId string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckEniDestroy(&eniId), + Steps: []resource.TestStep{ + { + Config: testAccEniManually, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "name", "ci-test-eni"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "description", "eni desc"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "security_groups.#", "0"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "vpc_id"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "subnet_id"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4s.#", "1"), + resource.TestCheckNoResourceAttr("tencentcloud_eni.foo", "ipv4_count"), + resource.TestCheckNoResourceAttr("tencentcloud_eni.foo", "tags"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "mac"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "state", ENI_STATE_AVAILABLE), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "primary", "false"), + resource.TestCheckResourceAttrSet("tencentcloud_eni.foo", "create_time"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "1"), + ), + }, + { + Config: testAccEniManuallyUpdatePrimaryDesc, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "1"), + ), + }, + { + Config: testAccEniManuallyUpdateAdd, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4s.#", "30"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "30"), + ), + }, + { + Config: testAccEniManuallyUpdateSub, + Check: resource.ComposeTestCheckFunc( + testAccCheckEniExists("tencentcloud_eni.foo", &eniId), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4s.#", "15"), + resource.TestCheckResourceAttr("tencentcloud_eni.foo", "ipv4_info.#", "15"), + ), + }, + }, + }) +} + +func testAccCheckEniExists(n string, id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no eni id is set") + } + + service := VpcService{client: testAccProvider.Meta().(*TencentCloudClient).apiV3Conn} + + enis, err := service.DescribeEniById(context.TODO(), []string{rs.Primary.ID}) + if err != nil { + return err + } + + for _, e := range enis { + if e.NetworkInterfaceId == nil { + return errors.New("eni id is nil") + } + + if *e.NetworkInterfaceId == rs.Primary.ID { + *id = rs.Primary.ID + return nil + } + } + + return fmt.Errorf("eni not found: %s", rs.Primary.ID) + } +} + +func testAccCheckEniDestroy(id *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*TencentCloudClient).apiV3Conn + service := VpcService{client: client} + + enis, err := service.DescribeEniById(context.TODO(), []string{*id}) + if err != nil { + return err + } + + if len(enis) > 0 { + return errors.New("eni still exists") + } + + return nil + } +} + +const testAccEniVpc = ` +variable "availability_zone" { + default = "ap-guangzhou-3" +} + +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "${var.availability_zone}" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.0.0/16" + is_multicast = false +} +` + +const testAccEniBasic = testAccEniVpc + ` + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 1 +} +` + +const testAccEniUpdateAttr = testAccEniVpc + ` + +resource "tencentcloud_security_group" "foo" { + name = "test-ci-eni-sg1" +} + +resource "tencentcloud_security_group" "bar" { + name = "test-ci-eni-sg2" +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni-new" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc new" + security_groups = ["${tencentcloud_security_group.foo.id}", "${tencentcloud_security_group.bar.id}"] + ipv4_count = 1 +} +` + +const testAccEniUpdateTags = testAccEniVpc + ` + +resource "tencentcloud_security_group" "foo" { + name = "test-ci-eni-sg1" +} + +resource "tencentcloud_security_group" "bar" { + name = "test-ci-eni-sg2" +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni-new" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc new" + security_groups = ["${tencentcloud_security_group.foo.id}", "${tencentcloud_security_group.bar.id}"] + ipv4_count = 1 + + tags = { + "test" = "test" + } +} +` + +const testAccEniUpdateCountAdd = testAccEniVpc + ` + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 30 +} +` + +const testAccEniUpdateCountSub = testAccEniVpc + ` + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 20 +} +` + +const testAccEniManually = testAccEniVpc + ` + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + + ipv4s { + ip = "10.0.0.10" + primary = true + description = "desc" + } +} +` + +const testAccEniManuallyUpdatePrimaryDesc = testAccEniVpc + ` + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + + ipv4s { + ip = "10.0.0.10" + primary = true + description = "" + } +} +` + +const testAccEniManuallyUpdateAdd = testAccEniVpc + ` + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + + ipv4s { + ip = "10.0.0.10" + primary = true + description = "" + } + + ipv4s { + ip = "10.0.0.11" + primary = false + } + + ipv4s { + ip = "10.0.0.12" + primary = false + } + + ipv4s { + ip = "10.0.0.13" + primary = false + } + + ipv4s { + ip = "10.0.0.14" + primary = false + } + + ipv4s { + ip = "10.0.0.15" + primary = false + } + + ipv4s { + ip = "10.0.0.16" + primary = false + } + + ipv4s { + ip = "10.0.0.17" + primary = false + } + + ipv4s { + ip = "10.0.0.18" + primary = false + } + + ipv4s { + ip = "10.0.0.19" + primary = false + } + + ipv4s { + ip = "10.0.0.21" + primary = false + } + + ipv4s { + ip = "10.0.0.22" + primary = false + } + + ipv4s { + ip = "10.0.0.23" + primary = false + } + + ipv4s { + ip = "10.0.0.24" + primary = false + } + + ipv4s { + ip = "10.0.0.25" + primary = false + } + + ipv4s { + ip = "10.0.0.26" + primary = false + } + + ipv4s { + ip = "10.0.0.27" + primary = false + } + + ipv4s { + ip = "10.0.0.28" + primary = false + } + + ipv4s { + ip = "10.0.0.29" + primary = false + } + + ipv4s { + ip = "10.0.0.30" + primary = false + } + + ipv4s { + ip = "10.0.0.31" + primary = false + } + + ipv4s { + ip = "10.0.0.32" + primary = false + } + + ipv4s { + ip = "10.0.0.33" + primary = false + } + + ipv4s { + ip = "10.0.0.34" + primary = false + } + + ipv4s { + ip = "10.0.0.35" + primary = false + } + + ipv4s { + ip = "10.0.0.36" + primary = false + } + + ipv4s { + ip = "10.0.0.37" + primary = false + } + + ipv4s { + ip = "10.0.0.38" + primary = false + } + + ipv4s { + ip = "10.0.0.39" + primary = false + } + + ipv4s { + ip = "10.0.0.40" + primary = false + } +} +` + +const testAccEniManuallyUpdateSub = testAccEniVpc + ` + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + + ipv4s { + ip = "10.0.0.10" + primary = true + description = "" // set empty desc to test if SDK can set private IP desc empty or not + } + + ipv4s { + ip = "10.0.0.11" + primary = false + } + + ipv4s { + ip = "10.0.0.12" + primary = false + } + + ipv4s { + ip = "10.0.0.13" + primary = false + } + + ipv4s { + ip = "10.0.0.14" + primary = false + } + + ipv4s { + ip = "10.0.0.15" + primary = false + } + + ipv4s { + ip = "10.0.0.16" + primary = false + } + + ipv4s { + ip = "10.0.0.17" + primary = false + } + + ipv4s { + ip = "10.0.0.18" + primary = false + } + + ipv4s { + ip = "10.0.0.19" + primary = false + } + + ipv4s { + ip = "10.0.0.21" + primary = false + } + + ipv4s { + ip = "10.0.0.22" + primary = false + } + + ipv4s { + ip = "10.0.0.23" + primary = false + } + + ipv4s { + ip = "10.0.0.24" + primary = false + } + + ipv4s { + ip = "10.0.0.25" + primary = false + } +} +` diff --git a/tencentcloud/service_tencentcloud_vpc.go b/tencentcloud/service_tencentcloud_vpc.go index 539e7f83ac..e6facd3761 100644 --- a/tencentcloud/service_tencentcloud_vpc.go +++ b/tencentcloud/service_tencentcloud_vpc.go @@ -76,6 +76,12 @@ type VpcSecurityGroupLiteRule struct { nestedSecurityGroupId string // if rule is a nested security group, other attrs will be ignored } +type VpcEniIP struct { + ip net.IP + primary bool + desc *string +} + func (rule VpcSecurityGroupLiteRule) String() string { if rule.nestedSecurityGroupId != "" { return rule.nestedSecurityGroupId @@ -1739,3 +1745,748 @@ func (me *VpcService) UnattachEip(ctx context.Context, eipId string) error { return nil } + +func (me *VpcService) CreateEni( + ctx context.Context, + name, vpcId, subnetId, desc string, + securityGroups []string, + ipv4Count *int, + ipv4s []VpcEniIP, +) (id string, err error) { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + createRequest := vpc.NewCreateNetworkInterfaceRequest() + createRequest.NetworkInterfaceName = &name + createRequest.VpcId = &vpcId + createRequest.SubnetId = &subnetId + createRequest.NetworkInterfaceDescription = &desc + + if len(securityGroups) > 0 { + createRequest.SecurityGroupIds = common.StringPtrs(securityGroups) + } + + if ipv4Count != nil { + // create will assign a primary ip, secondary ip count is *ipv4Count-1 + createRequest.SecondaryPrivateIpAddressCount = intToPointer(*ipv4Count - 1) + } + + var wantIpv4 []string + + for _, ipv4 := range ipv4s { + wantIpv4 = append(wantIpv4, ipv4.ip.String()) + createRequest.PrivateIpAddresses = append(createRequest.PrivateIpAddresses, &vpc.PrivateIpAddressSpecification{ + PrivateIpAddress: stringToPointer(ipv4.ip.String()), + Primary: boolToPointer(ipv4.primary), + Description: ipv4.desc, + }) + } + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(createRequest.GetAction()) + + response, err := client.CreateNetworkInterface(createRequest) + if err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, createRequest.GetAction(), createRequest.ToJsonString(), err) + return retryError(err) + } + + eni := response.Response.NetworkInterface + + if eni == nil { + err := fmt.Errorf("api[%s] eni is nil", createRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if eni.NetworkInterfaceId == nil { + err := fmt.Errorf("api[%s] eni id is nil", createRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + ipv4Set := eni.PrivateIpAddressSet + + if len(wantIpv4) > 0 { + checkMap := make(map[string]bool, len(wantIpv4)) + for _, ipv4 := range wantIpv4 { + checkMap[ipv4] = false + } + + for _, ipv4 := range ipv4Set { + if ipv4.PrivateIpAddress == nil { + err := fmt.Errorf("api[%s] eni ipv4 ip is nil", createRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + checkMap[*ipv4.PrivateIpAddress] = true + } + + for ipv4, checked := range checkMap { + if !checked { + err := fmt.Errorf("api[%s] doesn't assign %s ip", createRequest.GetAction(), ipv4) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + } + } else { + if len(ipv4Set) != *ipv4Count { + err := fmt.Errorf("api[%s] doesn't assign enough ip", createRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + wantIpv4 = make([]string, 0, *ipv4Count) + for _, ipv4 := range ipv4Set { + if ipv4.PrivateIpAddress == nil { + err := fmt.Errorf("api[%s] eni ipv4 ip is nil", createRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + wantIpv4 = append(wantIpv4, *ipv4.PrivateIpAddress) + } + } + + id = *eni.NetworkInterfaceId + + return nil + }); err != nil { + log.Printf("[CRITAL]%s create eni failed, reason: %v", logId, err) + return "", err + } + + if err := waitEniReady(ctx, id, client, wantIpv4, nil); err != nil { + log.Printf("[CRITAL]%s create eni failed, reason: %v", logId, err) + return "", err + } + + return +} + +func (me *VpcService) describeEnis( + ctx context.Context, + ids []string, + vpcId, subnetId, id, cvmId, sgId, name, desc, ipv4 *string, + tags map[string]string, +) (enis []*vpc.NetworkInterface, err error) { + logId := getLogId(ctx) + + request := vpc.NewDescribeNetworkInterfacesRequest() + + if len(ids) > 0 { + request.NetworkInterfaceIds = common.StringPtrs(ids) + } + + if vpcId != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("vpc-id"), + Values: []*string{vpcId}, + }) + } + + if subnetId != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("subnet-id"), + Values: []*string{subnetId}, + }) + } + + if id != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("network-interface-id"), + Values: []*string{id}, + }) + } + + if cvmId != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("attachment.instance-id"), + Values: []*string{cvmId}, + }) + } + + if sgId != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("groups.security-group-id"), + Values: []*string{sgId}, + }) + } + + if name != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("network-interface-name"), + Values: []*string{name}, + }) + } + + if desc != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("network-interface-description"), + Values: []*string{desc}, + }) + } + + if ipv4 != nil { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("address-ip"), + Values: []*string{ipv4}, + }) + } + + for k, v := range tags { + request.Filters = append(request.Filters, &vpc.Filter{ + Name: stringToPointer("tag:" + k), + Values: []*string{stringToPointer(v)}, + }) + } + + var offset uint64 + request.Offset = &offset + request.Limit = intToPointer(ENI_DESCRIBE_LIMIT) + + count := ENI_DESCRIBE_LIMIT + for count == ENI_DESCRIBE_LIMIT { + if err := resource.Retry(readRetryTimeout, func() *resource.RetryError { + ratelimit.Check(request.GetAction()) + + response, err := me.client.UseVpcClient().DescribeNetworkInterfaces(request) + if err != nil { + count = 0 + + if sdkError, ok := err.(*sdkErrors.TencentCloudSDKError); ok { + if sdkError.Code == "ResourceNotFound" { + return nil + } + } + + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, request.GetAction(), request.ToJsonString(), err) + return retryError(err) + } + + eniSet := response.Response.NetworkInterfaceSet + count = len(eniSet) + enis = append(enis, eniSet...) + + return nil + }); err != nil { + log.Printf("[CRITAL]%s read eni list failed, reason: %v", logId, err) + return nil, err + } + + offset += uint64(count) + } + + return +} + +func (me *VpcService) DescribeEniById(ctx context.Context, ids []string) (enis []*vpc.NetworkInterface, err error) { + return me.describeEnis(ctx, ids, nil, nil, nil, nil, nil, nil, nil, nil, nil) +} + +func (me *VpcService) ModifyEniAttribute(ctx context.Context, id string, name, desc *string, sgs []string) error { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + request := vpc.NewModifyNetworkInterfaceAttributeRequest() + request.NetworkInterfaceId = &id + request.NetworkInterfaceName = name + request.NetworkInterfaceDescription = desc + request.SecurityGroupIds = common.StringPtrs(sgs) + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(request.GetAction()) + + if _, err := client.ModifyNetworkInterfaceAttribute(request); err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, request.GetAction(), request.ToJsonString(), err) + return retryError(err) + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s modify eni attribute failed, reason: %v", logId, err) + return err + } + + if err := waitEniReady(ctx, id, client, nil, nil); err != nil { + log.Printf("[CRITAL]%s modify eni attribute failed, reason: %v", logId, err) + return err + } + + return nil +} + +func (me *VpcService) UnAssignIpv4FromEni(ctx context.Context, id string, ipv4s []string) error { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + request := vpc.NewUnassignPrivateIpAddressesRequest() + request.NetworkInterfaceId = &id + request.PrivateIpAddresses = make([]*vpc.PrivateIpAddressSpecification, 0, len(ipv4s)) + for _, ipv4 := range ipv4s { + request.PrivateIpAddresses = append(request.PrivateIpAddresses, &vpc.PrivateIpAddressSpecification{ + PrivateIpAddress: stringToPointer(ipv4), + }) + } + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(request.GetAction()) + + if _, err := client.UnassignPrivateIpAddresses(request); err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, request.GetAction(), request.ToJsonString(), err) + return retryError(err) + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s unassign ipv4 from eni failed, reason: %v", logId, err) + return err + } + + if err := waitEniReady(ctx, id, client, nil, ipv4s); err != nil { + log.Printf("[CRITAL]%s unassign ipv4 from eni failed, reason: %v", logId, err) + return err + } + + return nil +} + +func (me *VpcService) AssignIpv4ToEni(ctx context.Context, id string, ipv4s []VpcEniIP, ipv4Count *int) error { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + request := vpc.NewAssignPrivateIpAddressesRequest() + request.NetworkInterfaceId = &id + + if ipv4Count != nil { + request.SecondaryPrivateIpAddressCount = intToPointer(*ipv4Count) + } + + var wantIpv4 []string + + if len(ipv4s) > 0 { + request.PrivateIpAddresses = make([]*vpc.PrivateIpAddressSpecification, 0, len(ipv4s)) + wantIpv4 = make([]string, 0, len(ipv4s)) + + for _, ipv4 := range ipv4s { + wantIpv4 = append(wantIpv4, ipv4.ip.String()) + request.PrivateIpAddresses = append(request.PrivateIpAddresses, &vpc.PrivateIpAddressSpecification{ + PrivateIpAddress: stringToPointer(ipv4.ip.String()), + Primary: boolToPointer(ipv4.primary), + Description: ipv4.desc, + }) + } + } + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(request.GetAction()) + + response, err := client.AssignPrivateIpAddresses(request) + if err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, request.GetAction(), request.ToJsonString(), err) + return retryError(err) + } + + ipv4Set := response.Response.PrivateIpAddressSet + + if len(wantIpv4) > 0 { + checkMap := make(map[string]bool, len(wantIpv4)) + for _, ipv4 := range wantIpv4 { + checkMap[ipv4] = false + } + + for _, ipv4 := range ipv4Set { + if ipv4.PrivateIpAddress == nil { + err := fmt.Errorf("api[%s] eni ipv4 ip is nil", request.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + checkMap[*ipv4.PrivateIpAddress] = true + } + + for ipv4, checked := range checkMap { + if !checked { + err := fmt.Errorf("api[%s] doesn't assign %s ip", request.GetAction(), ipv4) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + } + } else { + if len(ipv4Set) != *ipv4Count { + err := fmt.Errorf("api[%s] doesn't assign enough ip", request.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + wantIpv4 = make([]string, 0, *ipv4Count) + for _, ipv4 := range ipv4Set { + if ipv4.PrivateIpAddress == nil { + err := fmt.Errorf("api[%s] eni ipv4 ip is nil", request.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + wantIpv4 = append(wantIpv4, *ipv4.PrivateIpAddress) + } + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s assign ipv4 to eni failed, reason: %v", logId, err) + return err + } + + if err := waitEniReady(ctx, id, client, wantIpv4, nil); err != nil { + log.Printf("[CRITAL]%s assign ipv4 to eni failed, reason: %v", logId, err) + return err + } + + return nil +} + +func (me *VpcService) DeleteEni(ctx context.Context, id string) error { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + deleteRequest := vpc.NewDeleteNetworkInterfaceRequest() + deleteRequest.NetworkInterfaceId = &id + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(deleteRequest.GetAction()) + + if _, err := client.DeleteNetworkInterface(deleteRequest); err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, deleteRequest.GetAction(), deleteRequest.ToJsonString(), err) + return retryError(err) + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s delete eni failed, reason: %v", logId, err) + return err + } + + describeRequest := vpc.NewDescribeNetworkInterfacesRequest() + describeRequest.NetworkInterfaceIds = []*string{&id} + + if err := resource.Retry(readRetryTimeout, func() *resource.RetryError { + ratelimit.Check(describeRequest.GetAction()) + + response, err := client.DescribeNetworkInterfaces(describeRequest) + if err != nil { + if sdkError, ok := err.(*sdkErrors.TencentCloudSDKError); ok { + if sdkError.Code == "ResourceNotFound" { + return nil + } + } + + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, describeRequest.GetAction(), describeRequest.ToJsonString(), err) + return retryError(err) + } + + for _, eni := range response.Response.NetworkInterfaceSet { + if eni.NetworkInterfaceId == nil { + err := fmt.Errorf("api[%s] eni id is nil", describeRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if *eni.NetworkInterfaceId == id { + err := errors.New("eni still exists") + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s delete eni failed, reason: %v", logId, err) + return err + } + + return nil +} + +func (me *VpcService) AttachEniToCvm(ctx context.Context, eniId, cvmId string) error { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + attachRequest := vpc.NewAttachNetworkInterfaceRequest() + attachRequest.NetworkInterfaceId = &eniId + attachRequest.InstanceId = &cvmId + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(attachRequest.GetAction()) + + if _, err := client.AttachNetworkInterface(attachRequest); err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, attachRequest.GetAction(), attachRequest.ToJsonString(), err) + return retryError(err) + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s attach eni to instance failed, reason: %v", logId, err) + return err + } + + describeRequest := vpc.NewDescribeNetworkInterfacesRequest() + describeRequest.NetworkInterfaceIds = []*string{&eniId} + + if err := resource.Retry(readRetryTimeout, func() *resource.RetryError { + ratelimit.Check(describeRequest.GetAction()) + + response, err := client.DescribeNetworkInterfaces(describeRequest) + if err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, describeRequest.GetAction(), describeRequest.ToJsonString(), err) + return retryError(err) + } + + var eni *vpc.NetworkInterface + for _, e := range response.Response.NetworkInterfaceSet { + if e.NetworkInterfaceId == nil { + err := fmt.Errorf("api[%s] eni id is nil", describeRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if *e.NetworkInterfaceId == eniId { + eni = e + break + } + } + + if eni == nil { + err := fmt.Errorf("api[%s] eni not found", describeRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if eni.Attachment == nil { + err := fmt.Errorf("api[%s] eni attachment is not ready", describeRequest.GetAction()) + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + + if eni.Attachment.InstanceId == nil { + err := fmt.Errorf("api[%s] eni attach instance id is nil", describeRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if *eni.Attachment.InstanceId != cvmId { + err := fmt.Errorf("api[%s] eni attach instance id is not right", describeRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if eni.State == nil { + err := fmt.Errorf("api[%s] eni state is nil", describeRequest.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if *eni.State != ENI_STATE_AVAILABLE { + err := errors.New("eni is not ready") + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s attach eni to instance failed, reason: %v", logId, err) + return err + } + + return nil +} + +func (me *VpcService) DetachEniFromCvm(ctx context.Context, eniId, cvmId string) error { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + request := vpc.NewDetachNetworkInterfaceRequest() + request.NetworkInterfaceId = &eniId + request.InstanceId = &cvmId + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(request.GetAction()) + + if _, err := client.DetachNetworkInterface(request); err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, request.GetAction(), request.ToJsonString(), err) + return retryError(err) + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s detach eni from instance failed, reason: %v", logId, err) + return err + } + + if err := waitEniReady(ctx, eniId, client, nil, nil); err != nil { + log.Printf("[CRITAL]%s detach eni from instance failed, reason: %v", logId, err) + return err + } + + return nil +} + +func (me *VpcService) ModifyEniPrimaryIpv4Desc(ctx context.Context, id, ip string, desc *string) error { + logId := getLogId(ctx) + client := me.client.UseVpcClient() + + request := vpc.NewModifyPrivateIpAddressesAttributeRequest() + request.NetworkInterfaceId = &id + request.PrivateIpAddresses = []*vpc.PrivateIpAddressSpecification{ + { + PrivateIpAddress: &ip, + Description: desc, + }, + } + + if err := resource.Retry(writeRetryTimeout, func() *resource.RetryError { + ratelimit.Check(request.GetAction()) + + if _, err := client.ModifyPrivateIpAddressesAttribute(request); err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, request.GetAction(), request.ToJsonString(), err) + return retryError(err) + } + return nil + }); err != nil { + log.Printf("[CRITAL]%s modify eni primary ipv4 description failed, reason: %v", logId, err) + return err + } + + if err := waitEniReady(ctx, id, client, []string{ip}, nil); err != nil { + log.Printf("[CRITAL]%s modify eni primary ipv4 description failed, reason: %v", logId, err) + return err + } + + return nil +} + +func (me *VpcService) DescribeEniByFilters( + ctx context.Context, + vpcId, subnetId, cvmId, sgId, name, desc, ipv4 *string, + tags map[string]string, +) (enis []*vpc.NetworkInterface, err error) { + return me.describeEnis(ctx, nil, vpcId, subnetId, nil, cvmId, sgId, name, desc, ipv4, tags) +} + +func waitEniReady(ctx context.Context, id string, client *vpc.Client, wantIpv4s []string, dropIpv4s []string) error { + logId := getLogId(ctx) + + wantCheckMap := make(map[string]bool, len(wantIpv4s)) + for _, ipv4 := range wantIpv4s { + wantCheckMap[ipv4] = false + } + + dropCheckMap := make(map[string]struct{}, len(dropIpv4s)) + for _, ipv4 := range dropIpv4s { + dropCheckMap[ipv4] = struct{}{} + } + + request := vpc.NewDescribeNetworkInterfacesRequest() + request.NetworkInterfaceIds = []*string{stringToPointer(id)} + + if err := resource.Retry(readRetryTimeout, func() *resource.RetryError { + ratelimit.Check(request.GetAction()) + + response, err := client.DescribeNetworkInterfaces(request) + if err != nil { + log.Printf("[CRITAL]%s api[%s] fail, request body [%s], reason[%v]", + logId, request.GetAction(), request.ToJsonString(), err) + return retryError(err) + } + + var eni *vpc.NetworkInterface + for _, networkInterface := range response.Response.NetworkInterfaceSet { + if networkInterface.NetworkInterfaceId == nil { + err := fmt.Errorf("api[%s] eni id is nil", request.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if *networkInterface.NetworkInterfaceId == id { + eni = networkInterface + break + } + } + + if eni == nil { + err := fmt.Errorf("api[%s] eni not exist", request.GetAction()) + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + + if eni.State == nil { + err := fmt.Errorf("api[%s] eni state is nil", request.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if *eni.State != ENI_STATE_AVAILABLE { + err := errors.New("eni is not available") + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + + for _, ipv4 := range eni.PrivateIpAddressSet { + if ipv4.PrivateIpAddress == nil { + err := fmt.Errorf("api[%s] eni ipv4 ip is nil", request.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + // check drop + if _, ok := dropCheckMap[*ipv4.PrivateIpAddress]; ok { + err := fmt.Errorf("api[%s] drop ip %s still exists", request.GetAction(), *ipv4.PrivateIpAddress) + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + + // check want + if _, ok := wantCheckMap[*ipv4.PrivateIpAddress]; ok { + wantCheckMap[*ipv4.PrivateIpAddress] = true + } + + if ipv4.State == nil { + err := fmt.Errorf("api[%s] eni ipv4 state is nil", request.GetAction()) + log.Printf("[CRITAL]%s %v", logId, err) + return resource.NonRetryableError(err) + } + + if *ipv4.State != ENI_IP_AVAILABLE { + err := errors.New("eni ipv4 is not available") + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + } + + for ipv4, checked := range wantCheckMap { + if !checked { + err := fmt.Errorf("api[%s] ipv4 %s is no ready", request.GetAction(), ipv4) + log.Printf("[DEBUG]%s %v", logId, err) + return resource.RetryableError(err) + } + } + + return nil + }); err != nil { + log.Printf("[CRITAL]%s eni is not available failed, reason: %v", logId, err) + return err + } + + return nil +} diff --git a/website/docs/d/enis.html.markdown b/website/docs/d/enis.html.markdown new file mode 100644 index 0000000000..745632bc28 --- /dev/null +++ b/website/docs/d/enis.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "tencentcloud" +page_title: "TencentCloud: tencentcloud_enis" +sidebar_current: "docs-tencentcloud-datasource-enis" +description: |- + Use this data source to query query ENIs. +--- + +# tencentcloud_enis + +Use this data source to query query ENIs. + +## Example Usage + +```hcl +data "tencentcloud_enis" "name" { + name = "test eni" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `description` - (Optional) Description of the ENI. Conflict with `ids`. +* `ids` - (Optional) ID of the ENIs to be queried. Conflict with `vpc_id`,`subnet_id`,`instance_id`,`security_group`,`name`,`ipv4` and `tags`. +* `instance_id` - (Optional) ID of the instance which bind the ENI. Conflict with `ids`. +* `ipv4` - (Optional) Intranet IP of the ENI. Conflict with `ids`. +* `name` - (Optional) Name of the ENI to be queried. Conflict with `ids`. +* `result_output_file` - (Optional) Used to save results. +* `security_group` - (Optional) A set of security group IDs which bind the ENI. Conflict with `ids`. +* `subnet_id` - (Optional) ID of the subnet within this vpc to be queried. Conflict with `ids`. +* `tags` - (Optional) Tags of the ENI. Conflict with `ids`. +* `vpc_id` - (Optional) ID of the vpc to be queried. Conflict with `ids`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `enis` - An information list of ENIs. Each element contains the following attributes: + * `create_time` - Creation time of the ENI. + * `description` - Description of the ENI. + * `id` - ID of the ENI. + * `instance_id` - ID of the instance which bind the ENI. + * `ipv4s` - A set of intranet IPv4s. + * `description` - Description of the IP. + * `ip` - Intranet IP. + * `primary` - Indicates whether the IP is primary. + * `mac` - MAC address. + * `name` - Name of the ENI. + * `primary` - Indicates whether the IP is primary. + * `security_groups` - A set of security group IDs which bind the ENI. + * `state` - States of the ENI. + * `subnet_id` - ID of the subnet within this vpc. + * `tags` - Tags of the ENI. + * `vpc_id` - ID of the vpc. + + diff --git a/website/docs/r/eni.html.markdown b/website/docs/r/eni.html.markdown new file mode 100644 index 0000000000..90ea101271 --- /dev/null +++ b/website/docs/r/eni.html.markdown @@ -0,0 +1,78 @@ +--- +layout: "tencentcloud" +page_title: "TencentCloud: tencentcloud_eni" +sidebar_current: "docs-tencentcloud-resource-eni" +description: |- + Provides a resource to create an ENI. +--- + +# tencentcloud_eni + +Provides a resource to create an ENI. + +## Example Usage + +```hcl +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "ap-guangzhou-3" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.0.0/16" + is_multicast = false +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 1 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Name of the ENI, maximum length 60. +* `subnet_id` - (Required, ForceNew) ID of the subnet within this vpc. +* `vpc_id` - (Required, ForceNew) ID of the vpc. +* `description` - (Optional) Description of the ENI, maximum length 60. +* `ipv4_count` - (Optional) The number of intranet IPv4s. When it is greater than 1, there is only one primary intranet IP. The others are auxiliary intranet IPs, which conflict with `ipv4s`. +* `ipv4s` - (Optional) Applying for intranet IPv4s collection, conflict with `ipv4_count`. When there are multiple ipv4s, can only be one primary IP, and the maximum length of the array is 30. Each element contains the following attributes: +* `security_groups` - (Optional) A set of security group IDs. +* `tags` - (Optional) Tags of the ENI. + +The `ipv4s` object supports the following: + +* `ip` - (Required) Intranet IP. +* `primary` - (Required) Indicates whether the IP is primary. +* `description` - (Optional) Description of the IP, maximum length 25. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `create_time` - Creation time of the ENI. +* `ipv4_info` - An information list of IPv4s. Each element contains the following attributes: + * `description` - Description of the IP. + * `ip` - Intranet IP. + * `primary` - Indicates whether the IP is primary. +* `mac` - MAC address. +* `primary` - Indicates whether the IP is primary. +* `state` - State of the ENI. + + +## Import + +ENI can be imported using the id, e.g. + +``` + $ terraform import tencentcloud_eni.foo eni-qka182br +``` + diff --git a/website/docs/r/eni_attachment.html.markdown b/website/docs/r/eni_attachment.html.markdown new file mode 100644 index 0000000000..d5adf72ec7 --- /dev/null +++ b/website/docs/r/eni_attachment.html.markdown @@ -0,0 +1,87 @@ +--- +layout: "tencentcloud" +page_title: "TencentCloud: tencentcloud_eni_attachment" +sidebar_current: "docs-tencentcloud-resource-eni_attachment" +description: |- + Provides a resource to detailed information of attached backend server to an ENI. +--- + +# tencentcloud_eni_attachment + +Provides a resource to detailed information of attached backend server to an ENI. + +## Example Usage + +```hcl +resource "tencentcloud_vpc" "foo" { + name = "ci-test-eni-vpc" + cidr_block = "10.0.0.0/16" +} + +resource "tencentcloud_subnet" "foo" { + availability_zone = "ap-guangzhou-3" + name = "ci-test-eni-subnet" + vpc_id = "${tencentcloud_vpc.foo.id}" + cidr_block = "10.0.0.0/16" + is_multicast = false +} + +resource "tencentcloud_eni" "foo" { + name = "ci-test-eni" + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" + description = "eni desc" + ipv4_count = 1 +} + +data "tencentcloud_image" "my_favorite_image" { + os_name = "centos" + filter { + name = "image-type" + values = ["PUBLIC_IMAGE"] + } +} + +data "tencentcloud_instance_types" "my_favorite_instance_types" { + filter { + name = "instance-family" + values = ["S2"] + } + cpu_core_count = 1 + memory_size = 1 +} + +resource "tencentcloud_instance" "foo" { + instance_name = "ci-test-eni-attach" + availability_zone = "ap-guangzhou-3" + image_id = "${data.tencentcloud_image.my_favorite_image.image_id}" + instance_type = "${data.tencentcloud_instance_types.my_favorite_instance_types.instance_types.0.instance_type}" + system_disk_type = "CLOUD_PREMIUM" + disable_security_service = true + disable_monitor_service = true + vpc_id = "${tencentcloud_vpc.foo.id}" + subnet_id = "${tencentcloud_subnet.foo.id}" +} + +resource "tencentcloud_eni_attachment" "foo" { + eni_id = "${tencentcloud_eni.foo.id}" + instance_id = "${tencentcloud_instance.foo.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `eni_id` - (Required, ForceNew) ID of the ENI. +* `instance_id` - (Required, ForceNew) ID of the instance which bind the ENI. + + +## Import + +ENI attachment can be imported using the id, e.g. + +``` + $ terraform import tencentcloud_eni_attachment.foo eni-gtlvkjvz+ins-0h3a5new +``` + diff --git a/website/tencentcloud.erb b/website/tencentcloud.erb index fbd6c2e33e..f2d9d4eec1 100644 --- a/website/tencentcloud.erb +++ b/website/tencentcloud.erb @@ -86,6 +86,9 @@ > tencentcloud_eips + > + tencentcloud_enis + > tencentcloud_gaap_certificates @@ -462,6 +465,12 @@ VPC Resources