diff --git a/.gitignore b/.gitignore index 422d55c8d..9e0c2d51d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ log.* # autogenerated fies *.autogenerated.* +.idea/ diff --git a/client/v3/v3_service.go b/client/v3/v3_service.go index 52c373c69..2070a2645 100644 --- a/client/v3/v3_service.go +++ b/client/v3/v3_service.go @@ -108,6 +108,18 @@ type Service interface { CreateRecoveryPlan(request *RecoveryPlanInput) (*RecoveryPlanResponse, error) UpdateRecoveryPlan(uuid string, body *RecoveryPlanInput) (*RecoveryPlanResponse, error) DeleteRecoveryPlan(uuid string) (*DeleteResponse, error) + GetServiceGroup(uuid string) (*ServiceGroupResponse, error) + ListServiceGroups(getEntitiesRequest *DSMetadata) (*ServiceGroupListResponse, error) + ListAllServiceGroups(filter string) (*ServiceGroupListResponse, error) + CreateServiceGroup(request *ServiceGroupInput) (*Reference, error) + UpdateServiceGroup(uuid string, body *ServiceGroupInput) error + DeleteServiceGroup(uuid string) error + GetAddressGroup(uuid string) (*AddressGroupResponse, error) + ListAddressGroups(getEntitiesRequest *DSMetadata) (*AddressGroupListResponse, error) + ListAllAddressGroups(filter string) (*AddressGroupListResponse, error) + DeleteAddressGroup(uuid string) error + CreateAddressGroup(request *AddressGroupInput) (*Reference, error) + UpdateAddressGroup(uuid string, body *AddressGroupInput) error } /*CreateVM Creates a VM @@ -2203,3 +2215,220 @@ func (op Operations) DeleteRecoveryPlan(uuid string) (*DeleteResponse, error) { return deleteResponse, op.client.Do(ctx, req, deleteResponse) } + +func (op Operations) GetServiceGroup(uuid string) (*ServiceGroupResponse, error) { + ctx := context.TODO() + + path := fmt.Sprintf("/service_groups/%s", uuid) + ServiceGroup := new(ServiceGroupResponse) + + req, err := op.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, err + } + + return ServiceGroup, op.client.Do(ctx, req, ServiceGroup) +} + +func (op Operations) CreateServiceGroup(request *ServiceGroupInput) (*Reference, error) { + ctx := context.TODO() + + req, err := op.client.NewRequest(ctx, http.MethodPost, "/service_groups", request) + ServiceGroup := new(Reference) + + if err != nil { + return nil, err + } + + return ServiceGroup, op.client.Do(ctx, req, ServiceGroup) +} + +func (op Operations) DeleteServiceGroup(uuid string) error { + ctx := context.TODO() + + path := fmt.Sprintf("/service_groups/%s", uuid) + + req, err := op.client.NewRequest(ctx, http.MethodDelete, path, nil) + + if err != nil { + return err + } + + return op.client.Do(ctx, req, nil) +} + +func (op Operations) ListAllServiceGroups(filter string) (*ServiceGroupListResponse, error) { + entities := make([]*ServiceGroupListEntry, 0) + + resp, err := op.ListServiceGroups(&DSMetadata{ + Filter: &filter, + Kind: utils.StringPtr("service_group"), + Length: utils.Int64Ptr(itemsPerPage), + }) + if err != nil { + return nil, err + } + + totalEntities := utils.Int64Value(resp.Metadata.TotalMatches) + remaining := totalEntities + offset := utils.Int64Value(resp.Metadata.Offset) + + if totalEntities > itemsPerPage { + for hasNext(&remaining) { + resp, err = op.ListServiceGroups(&DSMetadata{ + Filter: &filter, + Kind: utils.StringPtr("service_group"), + Length: utils.Int64Ptr(itemsPerPage), + Offset: utils.Int64Ptr(offset), + }) + + if err != nil { + return nil, err + } + + entities = append(entities, resp.Entities...) + + offset += itemsPerPage + log.Printf("[Debug] total=%d, remaining=%d, offset=%d len(entities)=%d\n", totalEntities, remaining, offset, len(entities)) + } + + resp.Entities = entities + } + + return resp, nil +} + +func (op Operations) ListServiceGroups(getEntitiesRequest *DSMetadata) (*ServiceGroupListResponse, error) { + ctx := context.TODO() + path := "/service_groups/list" + + list := new(ServiceGroupListResponse) + + req, err := op.client.NewRequest(ctx, http.MethodPost, path, getEntitiesRequest) + if err != nil { + return nil, err + } + + return list, op.client.Do(ctx, req, list) +} + +func (op Operations) UpdateServiceGroup(uuid string, body *ServiceGroupInput) error { + ctx := context.TODO() + + path := fmt.Sprintf("/service_groups/%s", uuid) + req, err := op.client.NewRequest(ctx, http.MethodPut, path, body) + + if err != nil { + return err + } + + return op.client.Do(ctx, req, nil) +} + +func (op Operations) GetAddressGroup(uuid string) (*AddressGroupResponse, error) { + ctx := context.TODO() + + path := fmt.Sprintf("/address_groups/%s", uuid) + AddressGroup := new(AddressGroupResponse) + + req, err := op.client.NewRequest(ctx, http.MethodGet, path, nil) + if err != nil { + return nil, err + } + + return AddressGroup, op.client.Do(ctx, req, AddressGroup) +} + +func (op Operations) ListAllAddressGroups(filter string) (*AddressGroupListResponse, error) { + entities := make([]*AddressGroupListEntry, 0) + + resp, err := op.ListAddressGroups(&DSMetadata{ + Filter: &filter, + Kind: utils.StringPtr("address_group"), + Length: utils.Int64Ptr(itemsPerPage), + }) + if err != nil { + return nil, err + } + + totalEntities := utils.Int64Value(resp.Metadata.TotalMatches) + remaining := totalEntities + offset := utils.Int64Value(resp.Metadata.Offset) + + if totalEntities > itemsPerPage { + for hasNext(&remaining) { + resp, err = op.ListAddressGroups(&DSMetadata{ + Filter: &filter, + Kind: utils.StringPtr("address_group"), + Length: utils.Int64Ptr(itemsPerPage), + Offset: utils.Int64Ptr(offset), + }) + + if err != nil { + return nil, err + } + + entities = append(entities, resp.Entities...) + + offset += itemsPerPage + log.Printf("[Debug] total=%d, remaining=%d, offset=%d len(entities)=%d\n", totalEntities, remaining, offset, len(entities)) + } + + resp.Entities = entities + } + + return resp, nil +} + +func (op Operations) ListAddressGroups(getEntitiesRequest *DSMetadata) (*AddressGroupListResponse, error) { + ctx := context.TODO() + path := "/address_groups/list" + + list := new(AddressGroupListResponse) + + req, err := op.client.NewRequest(ctx, http.MethodPost, path, getEntitiesRequest) + if err != nil { + return nil, err + } + + return list, op.client.Do(ctx, req, list) +} + +func (op Operations) DeleteAddressGroup(uuid string) error { + ctx := context.TODO() + + path := fmt.Sprintf("/address_groups/%s", uuid) + + req, err := op.client.NewRequest(ctx, http.MethodDelete, path, nil) + + if err != nil { + return err + } + + return op.client.Do(ctx, req, nil) +} + +func (op Operations) CreateAddressGroup(request *AddressGroupInput) (*Reference, error) { + ctx := context.TODO() + + req, err := op.client.NewRequest(ctx, http.MethodPost, "/address_groups", request) + AddressGroup := new(Reference) + + if err != nil { + return nil, err + } + + return AddressGroup, op.client.Do(ctx, req, AddressGroup) +} +func (op Operations) UpdateAddressGroup(uuid string, body *AddressGroupInput) error { + ctx := context.TODO() + + path := fmt.Sprintf("/address_groups/%s", uuid) + req, err := op.client.NewRequest(ctx, http.MethodPut, path, body) + + if err != nil { + return err + } + + return op.client.Do(ctx, req, nil) +} diff --git a/client/v3/v3_structs.go b/client/v3/v3_structs.go index f543d3d74..143917b21 100644 --- a/client/v3/v3_structs.go +++ b/client/v3/v3_structs.go @@ -2517,3 +2517,60 @@ type RecoveryPlanInput struct { Metadata *Metadata `json:"metadata,omitempty"` Spec *RecoveryPlanSpec `json:"spec,omitempty"` } + +type ServiceListEntry struct { + Protocol *string `json:"protocol,omitempty"` + TCPPortRangeList []*PortRange `json:"tcp_port_range_list,omitempty"` + UDPPortRangeList []*PortRange `json:"udp_port_range_list,omitempty"` + IcmpTypeCodeList []*NetworkRuleIcmpTypeCodeList `json:"icmp_type_code_list,omitempty"` +} + +type ServiceGroupListEntry struct { + UUID *string `json:"uuid,omitempty"` + ServiceGroup *ServiceGroupInput `json:"service_group,omitempty"` + AssociatedPoliciesList []*Reference `json:"associated_policies_list,omitempty"` +} + +type ServiceGroupInput struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + ServiceList []*ServiceListEntry `json:"service_list,omitempty"` + SystemDefined *bool `json:"is_system_defined,omitempty"` +} + +type ServiceGroupListResponse struct { + Metadata *ListMetadataOutput `json:"metadata,omitempty"` + Entities []*ServiceGroupListEntry `json:"entities,omitempty"` +} + +type ServiceGroupResponse struct { + ServiceGroup *ServiceGroupInput `json:"service_group,omitempty"` + UUID *string `json:"uuid,omitempty"` +} + +type IPAddressBlock struct { + IPAddress *string `json:"ip,omitempty"` + PrefixLength *int64 `json:"prefix_length,omitempty"` +} + +type AddressGroupInput struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + BlockList []*IPAddressBlock `json:"ip_address_block_list,omitempty"` + AddressGroupString *string `json:"address_group_string,omitempty"` +} + +type AddressGroupResponse struct { + UUID *string `json:"uuid,omitempty"` + AddressGroup *AddressGroupInput `json:"address_group,omitempty"` +} + +type AddressGroupListEntry struct { + AddressGroup *AddressGroupInput `json:"address_group,omitempty"` + AssociatedPoliciesList []*ReferenceValues `json:"associated_policies_list,omitempty"` +} + +type AddressGroupListResponse struct { + Metadata *ListMetadataOutput `json:"metadata,omitempty"` + Entities []*AddressGroupListEntry `json:"entities,omitempty"` +} diff --git a/nutanix/data_source_nutanix_address_group.go b/nutanix/data_source_nutanix_address_group.go new file mode 100644 index 000000000..0ed8cf97d --- /dev/null +++ b/nutanix/data_source_nutanix_address_group.go @@ -0,0 +1,85 @@ +package nutanix + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-nutanix/utils" +) + +func dataSourceNutanixAddressGroup() *schema.Resource { + return &schema.Resource{ + Read: dataSourceNutanixAddressGroupRead, + Schema: map[string]*schema.Schema{ + "uuid": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "ip_address_block_list": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + }, + "prefix_length": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "address_group_string": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceNutanixAddressGroupRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*Client).API + + if uuid, uuidOk := d.GetOk("uuid"); uuidOk { + group, reqErr := conn.V3.GetAddressGroup(uuid.(string)) + + if reqErr != nil { + if strings.Contains(fmt.Sprint(reqErr), "ENTITY_NOT_FOUND") { + d.SetId("") + } + return fmt.Errorf("error reading user with error %s", reqErr) + } + + if err := d.Set("name", utils.StringValue(group.AddressGroup.Name)); err != nil { + return err + } + + if err := d.Set("description", utils.StringValue(group.AddressGroup.Description)); err != nil { + return err + } + + if err := d.Set("address_group_string", utils.StringValue(group.AddressGroup.AddressGroupString)); err != nil { + return err + } + + if err := d.Set("ip_address_block_list", flattenAddressEntry(group.AddressGroup.BlockList)); err != nil { + return err + } + + d.SetId(uuid.(string)) + } else { + return fmt.Errorf("please provide `uuid`") + } + return nil +} diff --git a/nutanix/data_source_nutanix_address_group_test.go b/nutanix/data_source_nutanix_address_group_test.go new file mode 100644 index 000000000..12c9a0acd --- /dev/null +++ b/nutanix/data_source_nutanix_address_group_test.go @@ -0,0 +1,45 @@ +package nutanix + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccNutanixAddressGroupDataSource_basic(t *testing.T) { + rInt := acctest.RandInt() + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAddressGroupDataSourceConfig(rInt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nutanix_address_group.addr_group", "ip_address_block_list.#", "1"), + resource.TestCheckResourceAttr("data.nutanix_address_group.addr_group", "ip_address_block_list.0.prefix_length", "24"), + resource.TestCheckResourceAttr("data.nutanix_address_group.addr_group", "description", "test address group resource"), + ), + }, + }, + }) +} + +func testAccAddressGroupDataSourceConfig(r int) string { + return fmt.Sprintf(` + resource "nutanix_address_group" "test_address" { + name = "test-%[1]d" + description = "test address group resource" + + ip_address_block_list { + ip = "10.0.0.0" + prefix_length = 24 + } + } + + data "nutanix_address_group" "addr_group" { + uuid = "${nutanix_address_group.test_address.id}" + } + `, r) +} diff --git a/nutanix/data_source_nutanix_address_groups.go b/nutanix/data_source_nutanix_address_groups.go new file mode 100644 index 000000000..e45f76aea --- /dev/null +++ b/nutanix/data_source_nutanix_address_groups.go @@ -0,0 +1,163 @@ +package nutanix + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + v3 "github.com/terraform-providers/terraform-provider-nutanix/client/v3" + "github.com/terraform-providers/terraform-provider-nutanix/utils" +) + +func dataSourceNutanixAddressGroups() *schema.Resource { + return &schema.Resource{ + Read: dataSourceNutanixAddressGroupsRead, + Schema: map[string]*schema.Schema{ + "metadata": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "filter": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "kind": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "sort_order": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "offset": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "length": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "sort_attribute": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "entities": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "address_group": { + Type: schema.TypeList, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "ip_address_block_list": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + }, + "prefix_length": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "address_group_string": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "associated_policies_list": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "kind": { + Type: schema.TypeString, + Computed: true, + }, + "uuid": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourceNutanixAddressGroupsRead(d *schema.ResourceData, meta interface{}) error { + // Get client connection + conn := meta.(*Client).API + req := &v3.DSMetadata{} + + metadata, filtersOk := d.GetOk("metadata") + if filtersOk { + req = buildDataSourceListMetadata(metadata.(*schema.Set)) + } + + resp, err := conn.V3.ListAllAddressGroups(utils.StringValue(req.Filter)) + if err != nil { + return err + } + + if err := d.Set("entities", flattenAddressGroup(resp.Entities)); err != nil { + return err + } + + d.SetId(resource.UniqueId()) + return nil +} + +func flattenAddressGroup(entries []*v3.AddressGroupListEntry) interface{} { + entities := make([]map[string]interface{}, len(entries)) + + for i, entry := range entries { + entities[i] = map[string]interface{}{ + "address_group": []map[string]interface{}{ + { + "name": entry.AddressGroup.Name, + "description": entry.AddressGroup.Description, + "address_group_string": entry.AddressGroup.AddressGroupString, + "ip_address_block_list": flattenAddressEntry(entry.AddressGroup.BlockList), + }, + }, + "associated_policies_list": flattenReferenceList(entry.AssociatedPoliciesList), + } + } + return entities +} diff --git a/nutanix/data_source_nutanix_address_groups_test.go b/nutanix/data_source_nutanix_address_groups_test.go new file mode 100644 index 000000000..940e2e970 --- /dev/null +++ b/nutanix/data_source_nutanix_address_groups_test.go @@ -0,0 +1,49 @@ +package nutanix + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccNutanixAddressGroupsDataSource_basic(t *testing.T) { + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccAddressGroupsDataSourceConfig(rInt), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.nutanix_address_group.addr_group", "ip_address_block_list.#", "1"), + resource.TestCheckResourceAttr("data.nutanix_address_group.addr_group", "description", "test address groups resource"), + resource.TestCheckResourceAttr("data.nutanix_address_groups.addr_groups", "entities.#", "1"), + resource.TestCheckResourceAttr("data.nutanix_address_groups.addr_groups", "entities.0.address_group.#", "1"), + ), + }, + }, + }) +} + +func testAccAddressGroupsDataSourceConfig(r int) string { + return fmt.Sprintf(` + resource "nutanix_address_group" "test_address" { + name = "test-%[1]d" + description = "test address groups resource" + + ip_address_block_list { + ip = "10.0.0.0" + prefix_length = 24 + } + } + + data "nutanix_address_group" "addr_group" { + uuid = nutanix_address_group.test_address.id + } + + data "nutanix_address_groups" "addr_groups" {} + `, r) +} diff --git a/nutanix/provider.go b/nutanix/provider.go index 3e9c3d260..eac97925b 100644 --- a/nutanix/provider.go +++ b/nutanix/provider.go @@ -117,6 +117,8 @@ func Provider() terraform.ResourceProvider { "nutanix_protection_rules": dataSourceNutanixProtectionRules(), "nutanix_recovery_plan": dataSourceNutanixRecoveryPlan(), "nutanix_recovery_plans": dataSourceNutanixRecoveryPlans(), + "nutanix_address_groups": dataSourceNutanixAddressGroups(), + "nutanix_address_group": dataSourceNutanixAddressGroup(), }, ResourcesMap: map[string]*schema.Resource{ "nutanix_virtual_machine": resourceNutanixVirtualMachine(), @@ -133,6 +135,8 @@ func Provider() terraform.ResourceProvider { "nutanix_karbon_private_registry": resourceNutanixKarbonPrivateRegistry(), "nutanix_protection_rule": resourceNutanixProtectionRule(), "nutanix_recovery_plan": resourceNutanixRecoveryPlan(), + "nutanix_service_group": resourceNutanixServiceGroup(), + "nutanix_address_group": resourceNutanixAddressGroup(), }, ConfigureFunc: providerConfigure, } diff --git a/nutanix/resource_nutanix_address_group.go b/nutanix/resource_nutanix_address_group.go new file mode 100644 index 000000000..aa4cc485d --- /dev/null +++ b/nutanix/resource_nutanix_address_group.go @@ -0,0 +1,216 @@ +package nutanix + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + v3 "github.com/terraform-providers/terraform-provider-nutanix/client/v3" + "github.com/terraform-providers/terraform-provider-nutanix/utils" +) + +func resourceNutanixAddressGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceNutanixAddressGroupCreate, + Read: resourceNutanixAddressGroupRead, + Delete: resourceNutanixAddressGroupDelete, + Update: resourceNutanixAddressGroupUpdate, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "ip_address_block_list": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip": { + Type: schema.TypeString, + Required: true, + }, + "prefix_length": { + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + "address_group_string": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceNutanixAddressGroupUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*Client).API + id := d.Id() + response, err := conn.V3.GetAddressGroup(id) + + request := &v3.AddressGroupInput{} + + if err != nil { + if strings.Contains(fmt.Sprint(err), "ENTITY_NOT_FOUND") { + d.SetId("") + } + return fmt.Errorf("error retrieving for address group id (%s) :%+v", id, err) + } + + group := response.AddressGroup + + if d.HasChange("name") { + group.Name = utils.StringPtr(d.Get("name").(string)) + } + + if d.HasChange("description") { + group.Description = utils.StringPtr(d.Get("description").(string)) + } + + if d.HasChange("ip_address_block_list") { + blockList, err := expandAddressEntry(d) + + if err != nil { + return err + } + + group.BlockList = blockList + } + + request.Name = group.Name + request.Description = group.Description + request.BlockList = group.BlockList + + errUpdate := conn.V3.UpdateAddressGroup(d.Id(), request) + if errUpdate != nil { + return fmt.Errorf("error updating address group id %s): %s", d.Id(), errUpdate) + } + + return resourceNutanixAddressGroupRead(d, meta) +} + +func resourceNutanixAddressGroupDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*Client).API + + log.Printf("[Debug] Destroying the address group with the ID %s", d.Id()) + + if err := conn.V3.DeleteAddressGroup(d.Id()); err != nil { + return err + } + + d.SetId("") + return nil +} + +func resourceNutanixAddressGroupRead(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Reading AddressGroup: %s", d.Get("name").(string)) + + // Get client connection + conn := meta.(*Client).API + + // Make request to the API + resp, err := conn.V3.GetAddressGroup(d.Id()) + + if err != nil { + if strings.Contains(fmt.Sprint(err), "ENTITY_NOT_FOUND") { + d.SetId("") + return nil + } + return err + } + + if err := d.Set("ip_address_block_list", flattenAddressEntry(resp.AddressGroup.BlockList)); err != nil { + return err + } + + d.Set("name", utils.StringValue(resp.AddressGroup.Name)) + + return d.Set("description", utils.StringValue(resp.AddressGroup.Description)) +} + +func flattenAddressEntry(group []*v3.IPAddressBlock) []map[string]interface{} { + groupList := make([]map[string]interface{}, 0) + for _, v := range group { + groupItem := make(map[string]interface{}) + groupItem["ip"] = v.IPAddress + groupItem["prefix_length"] = v.PrefixLength + groupList = append(groupList, groupItem) + } + + return groupList +} + +func resourceNutanixAddressGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*Client).API + + request := &v3.AddressGroupInput{} + request.BlockList = make([]*v3.IPAddressBlock, 0) + + // Read Arguments and set request values + + if name, ok := d.GetOk("name"); ok { + request.Name = utils.StringPtr(name.(string)) + } + + if desc, ok := d.GetOk("description"); ok { + request.Description = utils.StringPtr(desc.(string)) + } + addressList, err := expandAddressEntry(d) + + if err != nil { + return err + } + + request.BlockList = addressList + + resp, err := conn.V3.CreateAddressGroup(request) + + if err != nil { + return err + } + + n := *resp.UUID + + // set terraform state + d.SetId(n) + + return resourceNutanixAddressGroupRead(d, meta) +} + +func expandAddressEntry(d *schema.ResourceData) ([]*v3.IPAddressBlock, error) { + if groups, ok := d.GetOk("ip_address_block_list"); ok { + set := groups.([]interface{}) + outbound := make([]*v3.IPAddressBlock, len(set)) + + for k, v := range set { + entry := v.(map[string]interface{}) + + block := &v3.IPAddressBlock{} + if ip, ipok := entry["ip"]; ipok { + block.IPAddress = utils.StringPtr(ip.(string)) + } else { + return nil, fmt.Errorf("error updating address group id %s): ip missing", d.Id()) + } + + if length, lengthok := entry["prefix_length"]; lengthok { + block.PrefixLength = utils.Int64Ptr(int64(length.(int))) + } else { + return nil, fmt.Errorf("error updating address group id %s): prefix_length missing", d.Id()) + } + + outbound[k] = block + } + return outbound, nil + } + + return nil, nil +} diff --git a/nutanix/resource_nutanix_address_group_test.go b/nutanix/resource_nutanix_address_group_test.go new file mode 100644 index 000000000..bbc33b77f --- /dev/null +++ b/nutanix/resource_nutanix_address_group_test.go @@ -0,0 +1,76 @@ +package nutanix + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +const resourcesAddressGroup = "nutanix_address_group.test" + +func TestAccNutanixAddressGroup(t *testing.T) { + name := acctest.RandomWithPrefix("nutanix_address_gr") + description := "this is nutanix address group" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNutanixAddressGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccNutanixAddressGroupConfig(name, description), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourcesAddressGroup, "name", name), + resource.TestCheckResourceAttr(resourcesAddressGroup, "description", description), + resource.TestCheckResourceAttr(resourcesAddressGroup, "ip_address_block_list.#", "1"), + resource.TestCheckResourceAttr(resourcesAddressGroup, "ip_address_block_list.0.prefix_length", "24"), + ), + }, + { + ResourceName: resourcesAddressGroup, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckNutanixAddressGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "nutanix_address_grp" { + continue + } + for { + _, err := conn.API.V3.GetVM(rs.Primary.ID) + if err != nil { + if strings.Contains(fmt.Sprint(err), "ENTITY_NOT_FOUND") { + return nil + } + return err + } + time.Sleep(3000 * time.Millisecond) + } + } + + return nil +} + +func testAccNutanixAddressGroupConfig(name, description string) string { + return fmt.Sprintf(` + resource "nutanix_address_group" "test" { + name = "%[1]s" + description = "%[2]s" + ip_address_block_list { + ip = "10.0.0.0" + prefix_length = 24 + } + } +`, name, description) +} diff --git a/nutanix/resource_nutanix_service_group.go b/nutanix/resource_nutanix_service_group.go new file mode 100644 index 000000000..215e6cc26 --- /dev/null +++ b/nutanix/resource_nutanix_service_group.go @@ -0,0 +1,339 @@ +package nutanix + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + v3 "github.com/terraform-providers/terraform-provider-nutanix/client/v3" + "github.com/terraform-providers/terraform-provider-nutanix/utils" +) + +func resourceNutanixServiceGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceNutanixServiceGroupCreate, + Read: resourceNutanixServiceGroupRead, + Delete: resourceNutanixServiceGroupDelete, + Update: resourceNutanixServiceGroupUpdate, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "system_defined": { + Type: schema.TypeBool, + Computed: true, + Optional: false, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "service_list": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": { + Type: schema.TypeString, + Optional: true, + }, + "icmp_type_code_list": { + Type: schema.TypeList, + + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "code": { + Type: schema.TypeString, + Optional: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "tcp_port_range_list": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "end_port": { + Type: schema.TypeInt, + Optional: true, + }, + "start_port": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "udp_port_range_list": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "end_port": { + Type: schema.TypeInt, + Optional: true, + }, + "start_port": { + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + }, + } +} + +func IsValidProtocol(category string) bool { + switch category { + case + "ALL", + "ICMP", + "TCP", + "UDP": + return true + } + return false +} + +func resourceNutanixServiceGroupUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*Client).API + id := d.Id() + response, err := conn.V3.GetServiceGroup(id) + + request := &v3.ServiceGroupInput{} + + if err != nil { + if strings.Contains(fmt.Sprint(err), "ENTITY_NOT_FOUND") { + d.SetId("") + } + return fmt.Errorf("error retrieving for access control policy id (%s) :%+v", id, err) + } + + group := response.ServiceGroup + + if d.HasChange("name") { + group.Name = utils.StringPtr(d.Get("name").(string)) + } + + if d.HasChange("description") { + group.Description = utils.StringPtr(d.Get("description").(string)) + } + + if d.HasChange("system_defined") { + group.SystemDefined = utils.BoolPtr(d.Get("system_defined").(bool)) + } + + if d.HasChange("service_list") { + serviceList, err := expandServiceEntry(d) + + if err != nil { + return err + } + + group.ServiceList = serviceList + } + + request.SystemDefined = group.SystemDefined + request.Name = group.Name + request.Description = group.Description + request.ServiceList = group.ServiceList + + errUpdate := conn.V3.UpdateServiceGroup(d.Id(), request) + if errUpdate != nil { + return fmt.Errorf("error updating service group id %s): %s", d.Id(), errUpdate) + } + + return resourceNutanixServiceGroupRead(d, meta) +} + +func flattenServiceEntry(group *v3.ServiceGroupInput) []map[string]interface{} { + groupList := make([]map[string]interface{}, 0) + + for _, v := range group.ServiceList { + groupItem := make(map[string]interface{}) + groupItem["protocol"] = utils.StringValue(v.Protocol) + + if v.TCPPortRangeList != nil { + tcpprl := v.TCPPortRangeList + tcpprList := make([]map[string]interface{}, len(tcpprl)) + for i, tcp := range tcpprl { + tcpItem := make(map[string]interface{}) + tcpItem["end_port"] = utils.Int64Value(tcp.EndPort) + tcpItem["start_port"] = utils.Int64Value(tcp.StartPort) + tcpprList[i] = tcpItem + } + groupItem["tcp_port_range_list"] = tcpprList + } + + if v.UDPPortRangeList != nil { + udpprl := v.UDPPortRangeList + udpprList := make([]map[string]interface{}, len(udpprl)) + for i, udp := range udpprl { + udpItem := make(map[string]interface{}) + udpItem["end_port"] = utils.Int64Value(udp.EndPort) + udpItem["start_port"] = utils.Int64Value(udp.StartPort) + udpprList[i] = udpItem + } + groupItem["udp_port_range_list"] = udpprList + } + + if v.IcmpTypeCodeList != nil { + icmptcl := v.IcmpTypeCodeList + icmptcList := make([]map[string]interface{}, len(icmptcl)) + for i, icmp := range icmptcl { + icmpItem := make(map[string]interface{}) + icmpItem["code"] = strconv.FormatInt(utils.Int64Value(icmp.Code), 10) + icmpItem["type"] = strconv.FormatInt(utils.Int64Value(icmp.Type), 10) + icmptcList[i] = icmpItem + } + groupItem["icmp_type_code_list"] = icmptcList + } + + groupList = append(groupList, groupItem) + } + return groupList +} + +func resourceNutanixServiceGroupRead(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Reading ServiceGroup: %s", d.Get("name").(string)) + + // Get client connection + conn := meta.(*Client).API + + // Make request to the API + resp, err := conn.V3.GetServiceGroup(d.Id()) + + if err != nil { + if strings.Contains(fmt.Sprint(err), "ENTITY_NOT_FOUND") { + d.SetId("") + return nil + } + return err + } + + if err := d.Set("service_list", flattenServiceEntry(resp.ServiceGroup)); err != nil { + return err + } + + d.Set("name", utils.StringValue(resp.ServiceGroup.Name)) + d.Set("description", utils.StringValue(resp.ServiceGroup.Description)) + + return d.Set("system_defined", utils.BoolValue(resp.ServiceGroup.SystemDefined)) +} + +func expandServiceEntry(d *schema.ResourceData) ([]*v3.ServiceListEntry, error) { + if services, ok := d.GetOk("service_list"); ok { + set := services.([]interface{}) + outbound := make([]*v3.ServiceListEntry, len(set)) + + for k, v := range set { + service := &v3.ServiceListEntry{} + + entry := v.(map[string]interface{}) + + if proto, pok := entry["protocol"]; pok && proto.(string) != "" { + if !IsValidProtocol(proto.(string)) { + return nil, fmt.Errorf("protocol needs to be one of 'ALL', 'ICMP', 'TCP', 'UDP'") + } + service.Protocol = utils.StringPtr(proto.(string)) + } + + if t, tcpok := entry["tcp_port_range_list"]; tcpok { + service.TCPPortRangeList = expandPortRangeList(t) + } + + if u, udpok := entry["udp_port_range_list"]; udpok { + service.UDPPortRangeList = expandPortRangeList(u) + } + + if icmp, icmpok := entry["icmp_type_code_list"]; icmpok { + service.IcmpTypeCodeList = expandIcmpTypeCodeList(icmp) + } + + outbound[k] = service + } + + return outbound, nil + } + return nil, nil +} + +func resourceNutanixServiceGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*Client).API + + request := &v3.ServiceGroupInput{} + request.ServiceList = make([]*v3.ServiceListEntry, 0) + + name, nameOK := d.GetOk("name") + + // Read Arguments and set request values + if desc, ok := d.GetOk("description"); ok { + request.Description = utils.StringPtr(desc.(string)) + } + + // validate required fields + if !nameOK { + return fmt.Errorf("please provide the required attribute name") + } + + request.Name = utils.StringPtr(name.(string)) + + serviceList, err := expandServiceEntry(d) + + if err != nil { + return err + } + + request.ServiceList = serviceList + + requestEnc, err := json.Marshal(request) + + if err != nil { + return err + } + + log.Printf("[DEBUG] %s", requestEnc) + + resp, err := conn.V3.CreateServiceGroup(request) + + if err != nil { + return err + } + + n := *resp.UUID + + // set terraform state + d.SetId(n) + + return resourceNutanixServiceGroupRead(d, meta) +} + +func resourceNutanixServiceGroupDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*Client).API + + log.Printf("[Debug] Destroying the service group with the ID %s", d.Id()) + + if err := conn.V3.DeleteServiceGroup(d.Id()); err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/nutanix/resource_nutanix_service_group_test.go b/nutanix/resource_nutanix_service_group_test.go new file mode 100644 index 000000000..ef17d203a --- /dev/null +++ b/nutanix/resource_nutanix_service_group_test.go @@ -0,0 +1,84 @@ +package nutanix + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" +) + +const resourceServiceGroup = "nutanix_service_group.test" + +func TestAccNutanixServiceGroup(t *testing.T) { + name := acctest.RandomWithPrefix("nutanix_service_gr") + description := "this is nutanix service group" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNutanixServiceGroupDestroy, + Steps: []resource.TestStep{ + { + Config: testAccNutanixServiceGroupConfig(name, description), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceServiceGroup, "name", name), + resource.TestCheckResourceAttr(resourceServiceGroup, "description", description), + resource.TestCheckResourceAttr(resourceServiceGroup, "service_list.#", "1"), + resource.TestCheckResourceAttr(resourceServiceGroup, "service_list.0.protocol", "TCP"), + ), + }, + { + ResourceName: resourceServiceGroup, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckNutanixServiceGroupDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "nutanix_service_grp" { + continue + } + for { + _, err := conn.API.V3.GetVM(rs.Primary.ID) + if err != nil { + if strings.Contains(fmt.Sprint(err), "ENTITY_NOT_FOUND") { + return nil + } + return err + } + time.Sleep(3000 * time.Millisecond) + } + } + + return nil +} + +func testAccNutanixServiceGroupConfig(name, description string) string { + return fmt.Sprintf(` + resource "nutanix_service_group" "test" { + name = "%[1]s" + description = "%[2]s" + + service_list { + protocol = "TCP" + tcp_port_range_list { + start_port = 22 + end_port = 22 + } + tcp_port_range_list { + start_port = 2222 + end_port = 2222 + } + } + } +`, name, description) +}