diff --git a/azurerm/internal/services/maintenance/client/client.go b/azurerm/internal/services/maintenance/client/client.go index 5fb54aed666c..4205000e7e4e 100644 --- a/azurerm/internal/services/maintenance/client/client.go +++ b/azurerm/internal/services/maintenance/client/client.go @@ -6,14 +6,19 @@ import ( ) type Client struct { - ConfigurationsClient *maintenance.ConfigurationsClient + ConfigurationsClient *maintenance.ConfigurationsClient + ConfigurationAssignmentsClient *maintenance.ConfigurationAssignmentsClient } func NewClient(o *common.ClientOptions) *Client { configurationsClient := maintenance.NewConfigurationsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&configurationsClient.Client, o.ResourceManagerAuthorizer) + configurationAssignmentsClient := maintenance.NewConfigurationAssignmentsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&configurationAssignmentsClient.Client, o.ResourceManagerAuthorizer) + return &Client{ - ConfigurationsClient: &configurationsClient, + ConfigurationsClient: &configurationsClient, + ConfigurationAssignmentsClient: &configurationAssignmentsClient, } } diff --git a/azurerm/internal/services/maintenance/maintenance_assignment_dedicated_host_resource.go b/azurerm/internal/services/maintenance/maintenance_assignment_dedicated_host_resource.go new file mode 100644 index 000000000000..17e111878a50 --- /dev/null +++ b/azurerm/internal/services/maintenance/maintenance_assignment_dedicated_host_resource.go @@ -0,0 +1,180 @@ +package maintenance + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/maintenance/mgmt/2018-06-01-preview/maintenance" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/location" + parseCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" + validateCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/maintenance/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/maintenance/validate" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmMaintenanceAssignmentDedicatedHost() *schema.Resource { + return &schema.Resource{ + Create: resourceArmMaintenanceAssignmentDedicatedHostCreate, + Read: resourceArmMaintenanceAssignmentDedicatedHostRead, + Delete: resourceArmMaintenanceAssignmentDedicatedHostDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.MaintenanceAssignmentDedicatedHostID(id) + return err + }), + + Schema: map[string]*schema.Schema{ + "location": azure.SchemaLocation(), + + "maintenance_configuration_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.MaintenanceConfigurationID, + }, + + "dedicated_host_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateCompute.DedicatedHostID, + DiffSuppressFunc: suppress.CaseDifference, + }, + }, + } +} + +func resourceArmMaintenanceAssignmentDedicatedHostCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + dedicatedHostIdRaw := d.Get("dedicated_host_id").(string) + dedicatedHostId, _ := parseCompute.DedicatedHostID(dedicatedHostIdRaw) + + existing, err := getMaintenanceAssignmentDedicatedHost(ctx, client, dedicatedHostId, dedicatedHostIdRaw) + if err != nil { + return err + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_maintenance_assignment_dedicated_host", *existing.ID) + } + + maintenanceConfigurationID := d.Get("maintenance_configuration_id").(string) + configurationId, _ := parse.MaintenanceConfigurationID(maintenanceConfigurationID) + + // set assignment name to configuration name + assignmentName := configurationId.Name + assignment := maintenance.ConfigurationAssignment{ + Name: utils.String(assignmentName), + Location: utils.String(location.Normalize(d.Get("location").(string))), + ConfigurationAssignmentProperties: &maintenance.ConfigurationAssignmentProperties{ + MaintenanceConfigurationID: utils.String(maintenanceConfigurationID), + ResourceID: utils.String(dedicatedHostIdRaw), + }, + } + + // It may take a few minutes after starting a VM for it to become available to assign to a configuration + err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + if _, err := client.CreateOrUpdateParent(ctx, dedicatedHostId.ResourceGroup, "Microsoft.Compute", "hostGroups", dedicatedHostId.HostGroup, "hosts", dedicatedHostId.Name, assignmentName, assignment); err != nil { + if strings.Contains(err.Error(), "It may take a few minutes after starting a VM for it to become available to assign to a configuration") { + return resource.RetryableError(fmt.Errorf("expected VM is available to assign to a configuration but was in pending state, retrying")) + } + return resource.NonRetryableError(fmt.Errorf("issuing creating request for Maintenance Assignment (Dedicated Host ID %q): %+v", dedicatedHostIdRaw, err)) + } + + return nil + }) + if err != nil { + return err + } + + resp, err := getMaintenanceAssignmentDedicatedHost(ctx, client, dedicatedHostId, dedicatedHostIdRaw) + if err != nil { + return err + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("empty or nil ID of Maintenance Assignment (Dedicated Host ID %q)", dedicatedHostIdRaw) + } + + d.SetId(*resp.ID) + return resourceArmMaintenanceAssignmentDedicatedHostRead(d, meta) +} + +func resourceArmMaintenanceAssignmentDedicatedHostRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.MaintenanceAssignmentDedicatedHostID(d.Id()) + if err != nil { + return err + } + + assignment, err := getMaintenanceAssignmentDedicatedHost(ctx, client, id.DedicatedHostId, id.DedicatedHostIdRaw) + if err != nil { + return err + } + if assignment.ID == nil || *assignment.ID == "" { + return fmt.Errorf("empty or nil ID of Maintenance Assignment (Dedicated Host ID: %q", id.DedicatedHostIdRaw) + } + + // in list api, `ResourceID` returned is always nil + d.Set("dedicated_host_id", id.DedicatedHostIdRaw) + if props := assignment.ConfigurationAssignmentProperties; props != nil { + d.Set("maintenance_configuration_id", props.MaintenanceConfigurationID) + } + return nil +} + +func resourceArmMaintenanceAssignmentDedicatedHostDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.MaintenanceAssignmentDedicatedHostID(d.Id()) + if err != nil { + return err + } + + if _, err := client.DeleteParent(ctx, id.DedicatedHostId.ResourceGroup, "Microsoft.Compute", "hostGroups", id.DedicatedHostId.HostGroup, "hosts", id.DedicatedHostId.Name, id.Name); err != nil { + return fmt.Errorf("deleting Maintenance Assignment to resource %q: %+v", id.DedicatedHostIdRaw, err) + } + + return nil +} + +func getMaintenanceAssignmentDedicatedHost(ctx context.Context, client *maintenance.ConfigurationAssignmentsClient, id *parseCompute.DedicatedHostId, dedicatedHostId string) (result maintenance.ConfigurationAssignment, err error) { + resp, err := client.ListParent(ctx, id.ResourceGroup, "Microsoft.Compute", "hostGroups", id.HostGroup, "hosts", id.Name) + + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + err = fmt.Errorf("checking for presence of existing Maintenance assignment (Dedicated Host ID %q): %+v", dedicatedHostId, err) + return + } + return result, nil + } + if resp.Value == nil || len(*resp.Value) == 0 { + err = fmt.Errorf("could not find Maintenance assignment (Dedicated Host ID %q)", dedicatedHostId) + return + } + + return (*resp.Value)[0], nil +} diff --git a/azurerm/internal/services/maintenance/maintenance_assignment_virtual_machine_resource.go b/azurerm/internal/services/maintenance/maintenance_assignment_virtual_machine_resource.go new file mode 100644 index 000000000000..27ed274b9bd3 --- /dev/null +++ b/azurerm/internal/services/maintenance/maintenance_assignment_virtual_machine_resource.go @@ -0,0 +1,180 @@ +package maintenance + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/services/preview/maintenance/mgmt/2018-06-01-preview/maintenance" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/suppress" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/location" + parseCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" + validateCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/maintenance/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/maintenance/validate" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmMaintenanceAssignmentVirtualMachine() *schema.Resource { + return &schema.Resource{ + Create: resourceArmMaintenanceAssignmentVirtualMachineCreate, + Read: resourceArmMaintenanceAssignmentVirtualMachineRead, + Delete: resourceArmMaintenanceAssignmentVirtualMachineDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.MaintenanceAssignmentVirtualMachineID(id) + return err + }), + + Schema: map[string]*schema.Schema{ + "location": azure.SchemaLocation(), + + "maintenance_configuration_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.MaintenanceConfigurationID, + }, + + "virtual_machine_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateCompute.VirtualMachineID, + DiffSuppressFunc: suppress.CaseDifference, + }, + }, + } +} + +func resourceArmMaintenanceAssignmentVirtualMachineCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + virtualMachineIdRaw := d.Get("virtual_machine_id").(string) + virtualMachineId, _ := parseCompute.VirtualMachineID(virtualMachineIdRaw) + + existing, err := getMaintenanceAssignmentVirtualMachine(ctx, client, virtualMachineId, virtualMachineIdRaw) + if err != nil { + return err + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_maintenance_assignment_virtual_machine", *existing.ID) + } + + maintenanceConfigurationID := d.Get("maintenance_configuration_id").(string) + configurationId, _ := parse.MaintenanceConfigurationID(maintenanceConfigurationID) + + // set assignment name to configuration name + assignmentName := configurationId.Name + assignment := maintenance.ConfigurationAssignment{ + Name: utils.String(assignmentName), + Location: utils.String(location.Normalize(d.Get("location").(string))), + ConfigurationAssignmentProperties: &maintenance.ConfigurationAssignmentProperties{ + MaintenanceConfigurationID: utils.String(maintenanceConfigurationID), + ResourceID: utils.String(virtualMachineIdRaw), + }, + } + + // It may take a few minutes after starting a VM for it to become available to assign to a configuration + err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + if _, err := client.CreateOrUpdate(ctx, virtualMachineId.ResourceGroup, "Microsoft.Compute", "virtualMachines", virtualMachineId.Name, assignmentName, assignment); err != nil { + if strings.Contains(err.Error(), "It may take a few minutes after starting a VM for it to become available to assign to a configuration") { + return resource.RetryableError(fmt.Errorf("expected VM is available to assign to a configuration but was in pending state, retrying")) + } + return resource.NonRetryableError(fmt.Errorf("issuing creating request for Maintenance Assignment (virtual machine ID %q): %+v", virtualMachineIdRaw, err)) + } + + return nil + }) + if err != nil { + return err + } + + resp, err := getMaintenanceAssignmentVirtualMachine(ctx, client, virtualMachineId, virtualMachineIdRaw) + if err != nil { + return err + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("empty or nil ID of Maintenance Assignment (virtual machine ID %q)", virtualMachineIdRaw) + } + + d.SetId(*resp.ID) + return resourceArmMaintenanceAssignmentVirtualMachineRead(d, meta) +} + +func resourceArmMaintenanceAssignmentVirtualMachineRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.MaintenanceAssignmentVirtualMachineID(d.Id()) + if err != nil { + return err + } + + assignment, err := getMaintenanceAssignmentVirtualMachine(ctx, client, id.VirtualMachineId, id.VirtualMachineIdRaw) + if err != nil { + return err + } + if assignment.ID == nil || *assignment.ID == "" { + return fmt.Errorf("empty or nil ID of Maintenance Assignment (virtual machine ID id: %q", id.VirtualMachineIdRaw) + } + + // in list api, `ResourceID` returned is always nil + d.Set("virtual_machine_id", id.VirtualMachineIdRaw) + if props := assignment.ConfigurationAssignmentProperties; props != nil { + d.Set("maintenance_configuration_id", props.MaintenanceConfigurationID) + } + return nil +} + +func resourceArmMaintenanceAssignmentVirtualMachineDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.MaintenanceAssignmentVirtualMachineID(d.Id()) + if err != nil { + return err + } + + if _, err := client.Delete(ctx, id.VirtualMachineId.ResourceGroup, "Microsoft.Compute", "virtualMachines", id.VirtualMachineId.Name, id.Name); err != nil { + return fmt.Errorf("deleting Maintenance Assignment to resource %q: %+v", id.VirtualMachineIdRaw, err) + } + + return nil +} + +func getMaintenanceAssignmentVirtualMachine(ctx context.Context, client *maintenance.ConfigurationAssignmentsClient, id *parseCompute.VirtualMachineId, virtualMachineId string) (result maintenance.ConfigurationAssignment, err error) { + resp, err := client.List(ctx, id.ResourceGroup, "Microsoft.Compute", "virtualMachines", id.Name) + + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + err = fmt.Errorf("checking for presence of existing Maintenance assignment (virtual machine ID: %q): %+v", virtualMachineId, err) + return + } + return result, nil + } + if resp.Value == nil || len(*resp.Value) == 0 { + err = fmt.Errorf("could not find Maintenance assignment (virtual machine ID: %q)", virtualMachineId) + return + } + + return (*resp.Value)[0], nil +} diff --git a/azurerm/internal/services/maintenance/parse/maintenance_assignment_dedicated_host.go b/azurerm/internal/services/maintenance/parse/maintenance_assignment_dedicated_host.go new file mode 100644 index 000000000000..e9861ad9da0d --- /dev/null +++ b/azurerm/internal/services/maintenance/parse/maintenance_assignment_dedicated_host.go @@ -0,0 +1,33 @@ +package parse + +import ( + "fmt" + "regexp" + + parseCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" +) + +type MaintenanceAssignmentDedicatedHostId struct { + DedicatedHostId *parseCompute.DedicatedHostId + DedicatedHostIdRaw string + Name string +} + +func MaintenanceAssignmentDedicatedHostID(input string) (*MaintenanceAssignmentDedicatedHostId, error) { + groups := regexp.MustCompile(`^(.+)/providers/Microsoft\.Maintenance/configurationAssignments/([^/]+)$`).FindStringSubmatch(input) + if len(groups) != 3 { + return nil, fmt.Errorf("parsing Maintenance Assignment Dedicated Host ID (%q)", input) + } + + targetResourceId, name := groups[1], groups[2] + dedicatedHostID, err := parseCompute.DedicatedHostID(targetResourceId) + if err != nil { + return nil, fmt.Errorf("parsing Maintenance Assignment Dedicated Host ID: %q: Expected valid Dedicated Host ID", input) + } + + return &MaintenanceAssignmentDedicatedHostId{ + DedicatedHostId: dedicatedHostID, + DedicatedHostIdRaw: targetResourceId, + Name: name, + }, nil +} diff --git a/azurerm/internal/services/maintenance/parse/maintenance_assignment_dedicated_host_test.go b/azurerm/internal/services/maintenance/parse/maintenance_assignment_dedicated_host_test.go new file mode 100644 index 000000000000..fee1bb78261b --- /dev/null +++ b/azurerm/internal/services/maintenance/parse/maintenance_assignment_dedicated_host_test.go @@ -0,0 +1,96 @@ +package parse + +import ( + "reflect" + "testing" + + parseCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" +) + +func TestMaintenanceAssignmentDedicatedHostID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *MaintenanceAssignmentDedicatedHostId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "No target resource type", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/", + Error: true, + }, + { + Name: "No target resource name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/hostGroups/group1/hosts", + Error: true, + }, + { + Name: "No Maintenance Assignment Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/hostGroups/group1/hosts/host1/providers/Microsoft.Maintenance/", + Error: true, + }, + { + Name: "No Maintenance Assignment name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/hostGroups/group1/hosts/host1/providers/Microsoft.Maintenance/configurationAssignments/", + Error: true, + }, + { + Name: "ID of Maintenance Assignment to dedicated host", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/hostGroups/group1/hosts/host1/providers/Microsoft.Maintenance/configurationAssignments/assign1", + Error: false, + Expect: &MaintenanceAssignmentDedicatedHostId{ + DedicatedHostIdRaw: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/hostGroups/group1/hosts/host1", + DedicatedHostId: &parseCompute.DedicatedHostId{ + ResourceGroup: "resGroup1", + HostGroup: "group1", + Name: "host1", + }, + Name: "assign1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/hostGroups/group1/hosts/host1/providers/Microsoft.Maintenance/ConfigurationAssignments/assign1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.Name) + + actual, err := MaintenanceAssignmentDedicatedHostID(v.Input) + if err != nil { + if v.Expect == nil { + continue + } + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + + if actual.DedicatedHostIdRaw != v.Expect.DedicatedHostIdRaw { + t.Fatalf("Expected %q but got %q for DedicatedHostIdRaw", v.Expect.DedicatedHostIdRaw, actual.DedicatedHostIdRaw) + } + + if !reflect.DeepEqual(v.Expect.DedicatedHostId, actual.DedicatedHostId) { + t.Fatalf("Expected %+v but got %+v", v.Expect.DedicatedHostId, actual.DedicatedHostId) + } + } +} diff --git a/azurerm/internal/services/maintenance/parse/maintenance_assignment_virtual_machine.go b/azurerm/internal/services/maintenance/parse/maintenance_assignment_virtual_machine.go new file mode 100644 index 000000000000..bcc30c0c0a23 --- /dev/null +++ b/azurerm/internal/services/maintenance/parse/maintenance_assignment_virtual_machine.go @@ -0,0 +1,33 @@ +package parse + +import ( + "fmt" + "regexp" + + parseCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" +) + +type MaintenanceAssignmentVirtualMachineId struct { + VirtualMachineId *parseCompute.VirtualMachineId + VirtualMachineIdRaw string + Name string +} + +func MaintenanceAssignmentVirtualMachineID(input string) (*MaintenanceAssignmentVirtualMachineId, error) { + groups := regexp.MustCompile(`^(.+)/providers/Microsoft\.Maintenance/configurationAssignments/([^/]+)$`).FindStringSubmatch(input) + if len(groups) != 3 { + return nil, fmt.Errorf("parsing Maintenance Assignment Virtual Machine ID (%q)", input) + } + + targetResourceId, name := groups[1], groups[2] + virtualMachineId, err := parseCompute.VirtualMachineID(targetResourceId) + if err != nil { + return nil, fmt.Errorf("parsing Maintenance Assignment Virtual Machine ID: %q: Expected valid virtual machine ID", input) + } + + return &MaintenanceAssignmentVirtualMachineId{ + VirtualMachineId: virtualMachineId, + VirtualMachineIdRaw: targetResourceId, + Name: name, + }, nil +} diff --git a/azurerm/internal/services/maintenance/parse/maintenance_assignment_virtual_machine_test.go b/azurerm/internal/services/maintenance/parse/maintenance_assignment_virtual_machine_test.go new file mode 100644 index 000000000000..8ec69413caae --- /dev/null +++ b/azurerm/internal/services/maintenance/parse/maintenance_assignment_virtual_machine_test.go @@ -0,0 +1,95 @@ +package parse + +import ( + "reflect" + "testing" + + parseCompute "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/compute/parse" +) + +func TestMaintenanceAssignmentVirtualMachineID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *MaintenanceAssignmentVirtualMachineId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "No target resource type", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/", + Error: true, + }, + { + Name: "No target resource name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/virtualMachines/", + Error: true, + }, + { + Name: "No Maintenance Assignment Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/virtualMachines/vm1/providers/Microsoft.Maintenance/", + Error: true, + }, + { + Name: "No Maintenance Assignment name", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/virtualMachines/vm1/providers/Microsoft.Maintenance/configurationAssignments/", + Error: true, + }, + { + Name: "ID of Maintenance Assignment to vm", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/virtualMachines/vm1/providers/Microsoft.Maintenance/configurationAssignments/assign1", + Error: false, + Expect: &MaintenanceAssignmentVirtualMachineId{ + VirtualMachineIdRaw: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/virtualMachines/vm1", + VirtualMachineId: &parseCompute.VirtualMachineId{ + ResourceGroup: "resGroup1", + Name: "vm1", + }, + Name: "assign1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resGroup1/providers/microsoft.compute/virtualMachines/vm1/providers/Microsoft.Maintenance/ConfigurationAssignments/assign1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q..", v.Name) + + actual, err := MaintenanceAssignmentVirtualMachineID(v.Input) + if err != nil { + if v.Expect == nil { + continue + } + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + + if actual.VirtualMachineIdRaw != v.Expect.VirtualMachineIdRaw { + t.Fatalf("Expected %q but got %q for VirtualMachineIdRaw", v.Expect.VirtualMachineIdRaw, actual.VirtualMachineIdRaw) + } + + if !reflect.DeepEqual(v.Expect.VirtualMachineId, actual.VirtualMachineId) { + t.Fatalf("Expected %+v but got %+v", v.Expect.VirtualMachineId, actual.VirtualMachineId) + } + } +} diff --git a/azurerm/internal/services/maintenance/registration.go b/azurerm/internal/services/maintenance/registration.go index d461b1a485d8..b703a69a9e51 100644 --- a/azurerm/internal/services/maintenance/registration.go +++ b/azurerm/internal/services/maintenance/registration.go @@ -24,6 +24,8 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azurerm_maintenance_configuration": resourceArmMaintenanceConfiguration(), + "azurerm_maintenance_assignment_dedicated_host": resourceArmMaintenanceAssignmentDedicatedHost(), + "azurerm_maintenance_assignment_virtual_machine": resourceArmMaintenanceAssignmentVirtualMachine(), + "azurerm_maintenance_configuration": resourceArmMaintenanceConfiguration(), } } diff --git a/azurerm/internal/services/maintenance/tests/maintenance_assignment_dedicated_host_resource_test.go b/azurerm/internal/services/maintenance/tests/maintenance_assignment_dedicated_host_resource_test.go new file mode 100644 index 000000000000..d50876ac685b --- /dev/null +++ b/azurerm/internal/services/maintenance/tests/maintenance_assignment_dedicated_host_resource_test.go @@ -0,0 +1,171 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/maintenance/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMMaintenanceAssignmentDedicatedHost_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_maintenance_assignment_dedicated_host", "test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMaintenanceAssignmentDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMaintenanceAssignmentDedicatedHost_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMaintenanceAssignmentDedicatedHostExists(data.ResourceName), + ), + }, + data.ImportStep("location"), + }, + }) +} + +func TestAccAzureRMMaintenanceAssignmentDedicatedHost_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_maintenance_assignment_dedicated_host", "test") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMaintenanceAssignmentDedicatedHostDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMaintenanceAssignmentDedicatedHost_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMaintenanceAssignmentDedicatedHostExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMMaintenanceAssignmentDedicatedHost_requiresImport), + }, + }) +} + +func testCheckAzureRMMaintenanceAssignmentDedicatedHostDestroy(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_maintenance_assignment_dedicated_host" { + continue + } + + id, err := parse.MaintenanceAssignmentDedicatedHostID(rs.Primary.ID) + if err != nil { + return err + } + + listResp, err := conn.ListParent(ctx, id.DedicatedHostId.ResourceGroup, "Microsoft.Compute", "hostGroups", id.DedicatedHostId.HostGroup, "hosts", id.DedicatedHostId.Name) + if err != nil { + if !utils.ResponseWasNotFound(listResp.Response) { + return err + } + return nil + } + if listResp.Value != nil && len(*listResp.Value) > 0 { + return fmt.Errorf("maintenance assignment (Dedicated Host ID: %q) still exists", id.DedicatedHostIdRaw) + } + + return nil + } + + return nil +} + +func testCheckAzureRMMaintenanceAssignmentDedicatedHostExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + id, err := parse.MaintenanceAssignmentDedicatedHostID(rs.Primary.ID) + if err != nil { + return err + } + + listResp, err := conn.ListParent(ctx, id.DedicatedHostId.ResourceGroup, "Microsoft.Compute", "hostGroups", id.DedicatedHostId.HostGroup, "hosts", id.DedicatedHostId.Name) + if err != nil { + return fmt.Errorf("bad: list on ConfigurationAssignmentsClient: %+v", err) + } + if listResp.Value == nil || len(*listResp.Value) == 0 { + return fmt.Errorf("could not find Maintenance Assignment (target resource id: %q)", id.DedicatedHostIdRaw) + } + + return nil + } +} + +func testAccAzureRMMaintenanceAssignmentDedicatedHost_basic(data acceptance.TestData) string { + template := testAccAzureRMMaintenanceAssignmentDedicatedHost_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_maintenance_assignment_dedicated_host" "test" { + location = azurerm_resource_group.test.location + maintenance_configuration_id = azurerm_maintenance_configuration.test.id + dedicated_host_id = azurerm_dedicated_host.test.id +} +`, template) +} + +func testAccAzureRMMaintenanceAssignmentDedicatedHost_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMMaintenanceAssignmentDedicatedHost_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_maintenance_assignment_dedicated_host" "import" { + location = azurerm_maintenance_assignment_dedicated_host.test.location + maintenance_configuration_id = azurerm_maintenance_assignment_dedicated_host.test.maintenance_configuration_id + dedicated_host_id = azurerm_maintenance_assignment_dedicated_host.test.dedicated_host_id +} +`, template) +} + +func testAccAzureRMMaintenanceAssignmentDedicatedHost_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-maint-%[1]d" + location = "%[2]s" +} + +resource "azurerm_maintenance_configuration" "test" { + name = "acctest-MC%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + scope = "All" +} + +resource "azurerm_dedicated_host_group" "test" { + name = "acctest-DHG-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + platform_fault_domain_count = 2 +} + +resource "azurerm_dedicated_host" "test" { + name = "acctest-DH-%[1]d" + location = azurerm_resource_group.test.location + dedicated_host_group_id = azurerm_dedicated_host_group.test.id + sku_name = "DSv3-Type1" + platform_fault_domain = 1 +} +`, data.RandomInteger, data.Locations.Primary) +} diff --git a/azurerm/internal/services/maintenance/tests/maintenance_assignment_virtual_machine_resource_test.go b/azurerm/internal/services/maintenance/tests/maintenance_assignment_virtual_machine_resource_test.go new file mode 100644 index 000000000000..f059f6db73f4 --- /dev/null +++ b/azurerm/internal/services/maintenance/tests/maintenance_assignment_virtual_machine_resource_test.go @@ -0,0 +1,210 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/maintenance/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMMaintenanceAssignmentVirtualMachine_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_maintenance_assignment_virtual_machine", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMaintenanceAssignmentVirtualMachineDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMaintenanceAssignmentVirtualMachine_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMaintenanceAssignmentVirtualMachineExists(data.ResourceName), + ), + }, + // location not returned by list rest api + data.ImportStep("location"), + }, + }) +} + +func TestAccAzureRMMaintenanceAssignmentVirtualMachine_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_maintenance_assignment_virtual_machine", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMMaintenanceAssignmentVirtualMachineDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMMaintenanceAssignmentVirtualMachine_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMMaintenanceAssignmentVirtualMachineExists(data.ResourceName), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMMaintenanceAssignmentVirtualMachine_requiresImport), + }, + }) +} + +func testCheckAzureRMMaintenanceAssignmentVirtualMachineDestroy(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_maintenance_assignment_virtual_machine" { + continue + } + + id, err := parse.MaintenanceAssignmentVirtualMachineID(rs.Primary.ID) + if err != nil { + return err + } + + listResp, err := conn.List(ctx, id.VirtualMachineId.ResourceGroup, "Microsoft.Compute", "virtualMachines", id.VirtualMachineId.Name) + if err != nil { + if !utils.ResponseWasNotFound(listResp.Response) { + return err + } + return nil + } + if listResp.Value != nil && len(*listResp.Value) > 0 { + return fmt.Errorf("maintenance assignment (Virtual Machine id: %q) still exists", id.VirtualMachineIdRaw) + } + + return nil + } + + return nil +} + +func testCheckAzureRMMaintenanceAssignmentVirtualMachineExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acceptance.AzureProvider.Meta().(*clients.Client).Maintenance.ConfigurationAssignmentsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + id, err := parse.MaintenanceAssignmentVirtualMachineID(rs.Primary.ID) + if err != nil { + return err + } + + listResp, err := conn.List(ctx, id.VirtualMachineId.ResourceGroup, "Microsoft.Compute", "virtualMachines", id.VirtualMachineId.Name) + if err != nil { + return fmt.Errorf("bad: list on ConfigurationAssignmentsClient: %+v", err) + } + if listResp.Value == nil || len(*listResp.Value) == 0 { + return fmt.Errorf("could not find Maintenance Assignment (Virtual Machine id: %q)", id.VirtualMachineIdRaw) + } + + return nil + } +} + +func testAccAzureRMMaintenanceAssignmentVirtualMachine_basic(data acceptance.TestData) string { + template := testAccAzureRMMaintenanceAssignmentVirtualMachine_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_maintenance_assignment_virtual_machine" "test" { + location = azurerm_resource_group.test.location + maintenance_configuration_id = azurerm_maintenance_configuration.test.id + virtual_machine_id = azurerm_linux_virtual_machine.test.id +} +`, template) +} + +func testAccAzureRMMaintenanceAssignmentVirtualMachine_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMMaintenanceAssignmentVirtualMachine_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_maintenance_assignment_virtual_machine" "import" { + location = azurerm_maintenance_assignment_virtual_machine.test.location + maintenance_configuration_id = azurerm_maintenance_assignment_virtual_machine.test.maintenance_configuration_id + virtual_machine_id = azurerm_maintenance_assignment_virtual_machine.test.virtual_machine_id +} +`, template) +} + +func testAccAzureRMMaintenanceAssignmentVirtualMachine_template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-maint-%[1]d" + location = "%[2]s" +} + +resource "azurerm_maintenance_configuration" "test" { + name = "acctest-MC%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + scope = "All" +} + +resource "azurerm_virtual_network" "test" { + name = "acctestnw-%[1]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "internal" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "10.0.2.0/24" +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + ip_configuration { + name = "testconfiguration1" + subnet_id = azurerm_subnet.test.id + private_ip_address_allocation = "Dynamic" + } +} + +resource "azurerm_linux_virtual_machine" "test" { + name = "acctestVM-%[1]d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_D15_v2" + admin_username = "adminuser" + admin_password = "P@$$w0rd1234!" + + disable_password_authentication = false + + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-LTS" + version = "latest" + } +} +`, data.RandomInteger, data.Locations.Primary) +} diff --git a/azurerm/internal/services/maintenance/validate/maintenance_configuration.go b/azurerm/internal/services/maintenance/validate/maintenance_configuration.go new file mode 100644 index 000000000000..aec653423d20 --- /dev/null +++ b/azurerm/internal/services/maintenance/validate/maintenance_configuration.go @@ -0,0 +1,22 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/maintenance/parse" +) + +func MaintenanceConfigurationID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := parse.MaintenanceConfigurationID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 2bd263ca1e84..21d367a6950e 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -1704,6 +1704,14 @@
  • Maintenance Resources