diff --git a/go.mod b/go.mod index ea67c1d9ab..5c94d8763b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.5 require ( github.com/IBM-Cloud/bluemix-go v0.0.0-20240926024252-81b3928fd062 github.com/IBM-Cloud/container-services-go-sdk v0.0.0-20240725064144-454a2ae23113 - github.com/IBM-Cloud/power-go-client v1.8.1 + github.com/IBM-Cloud/power-go-client v1.8.3 github.com/IBM/apigateway-go-sdk v0.0.0-20210714141226-a5d5d49caaca github.com/IBM/appconfiguration-go-admin-sdk v0.3.0 github.com/IBM/appid-management-go-sdk v0.0.0-20210908164609-dd0e0eaf732f diff --git a/go.sum b/go.sum index 6c3b571924..c40a3bf805 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,8 @@ github.com/IBM-Cloud/bluemix-go v0.0.0-20240926024252-81b3928fd062/go.mod h1:/7h github.com/IBM-Cloud/container-services-go-sdk v0.0.0-20240725064144-454a2ae23113 h1:f2Erqfea1dKpaTFagTJM6W/wnD3JGq/Vn9URh8nuRwk= github.com/IBM-Cloud/container-services-go-sdk v0.0.0-20240725064144-454a2ae23113/go.mod h1:xUQL9SGAjoZFd4GNjrjjtEpjpkgU7RFXRyHesbKTjiY= github.com/IBM-Cloud/ibm-cloud-cli-sdk v0.5.3/go.mod h1:RiUvKuHKTBmBApDMUQzBL14pQUGKcx/IioKQPIcRQjs= -github.com/IBM-Cloud/power-go-client v1.8.1 h1:tx1aPJmIQrNru1MD1VHGNasGx3eRIs0zzPZ0KvdFQrg= -github.com/IBM-Cloud/power-go-client v1.8.1/go.mod h1:N4RxrsMUvBQjSQ/qPk0iMZ8zK+fZPRTnHi/gTaASw0g= +github.com/IBM-Cloud/power-go-client v1.8.3 h1:QsBuIS6KvKsiEpe0yiHYKhWgXlqkcJ7XqFHtATj8Yh4= +github.com/IBM-Cloud/power-go-client v1.8.3/go.mod h1:UDyXeIKEp6r7yWUXYu3r0ZnFSlNZ2YeQTHwM2Tmlgv0= github.com/IBM-Cloud/softlayer-go v1.0.5-tf h1:koUAyF9b6X78lLLruGYPSOmrfY2YcGYKOj/Ug9nbKNw= github.com/IBM-Cloud/softlayer-go v1.0.5-tf/go.mod h1:6HepcfAXROz0Rf63krk5hPZyHT6qyx2MNvYyHof7ik4= github.com/IBM/apigateway-go-sdk v0.0.0-20210714141226-a5d5d49caaca h1:crniVcf+YcmgF03NmmfonXwSQ73oJF+IohFYBwknMxs= @@ -1653,8 +1653,10 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= diff --git a/ibm/provider/provider.go b/ibm/provider/provider.go index 2bf3bbb6bb..13a1e62c5c 100644 --- a/ibm/provider/provider.go +++ b/ibm/provider/provider.go @@ -1319,6 +1319,7 @@ func Provider() *schema.Provider { "ibm_pi_snapshot": power.ResourceIBMPISnapshot(), "ibm_pi_spp_placement_group": power.ResourceIBMPISPPPlacementGroup(), "ibm_pi_volume_attach": power.ResourceIBMPIVolumeAttach(), + "ibm_pi_volume_bulk": power.ResourceIBMPIVolumeBulk(), "ibm_pi_volume_clone": power.ResourceIBMPIVolumeClone(), "ibm_pi_volume_group_action": power.ResourceIBMPIVolumeGroupAction(), "ibm_pi_volume_group": power.ResourceIBMPIVolumeGroup(), diff --git a/ibm/service/power/ibm_pi_constants.go b/ibm/service/power/ibm_pi_constants.go index dc68945132..2981951400 100644 --- a/ibm/service/power/ibm_pi_constants.go +++ b/ibm/service/power/ibm_pi_constants.go @@ -16,6 +16,7 @@ const ( Arg_CloudConnectionName = "pi_cloud_connection_name" Arg_CloudInstanceID = "pi_cloud_instance_id" Arg_ConsistencyGroupName = "pi_consistency_group_name" + Arg_Count = "pi_count" Arg_Datacenter = "pi_datacenter" Arg_DatacenterZone = "pi_datacenter_zone" Arg_DeploymentTarget = "pi_deployment_target" diff --git a/ibm/service/power/resource_ibm_pi_volume.go b/ibm/service/power/resource_ibm_pi_volume.go index 365de4a32c..867d5e8fdf 100644 --- a/ibm/service/power/resource_ibm_pi_volume.go +++ b/ibm/service/power/resource_ibm_pi_volume.go @@ -124,7 +124,7 @@ func ResourceIBMPIVolume() *schema.Resource { Description: "The size of the volume in GB.", Required: true, Type: schema.TypeFloat, - ValidateFunc: validation.NoZeroValues, + ValidateFunc: validation.FloatAtLeast(1), }, Arg_VolumeType: { Computed: true, @@ -277,7 +277,7 @@ func resourceIBMPIVolumeCreate(ctx context.Context, d *schema.ResourceData, meta policy := ap.(string) body.AffinityPolicy = &policy - if policy == "affinity" { + if policy == Affinity { if av, ok := d.GetOk(Arg_AffinityVolume); ok { afvol := av.(string) body.AffinityVolume = &afvol @@ -317,10 +317,12 @@ func resourceIBMPIVolumeCreate(ctx context.Context, d *schema.ResourceData, meta } if _, ok := d.GetOk(Arg_UserTags); ok { - oldList, newList := d.GetChange(Arg_UserTags) - err := flex.UpdateGlobalTagsUsingCRN(oldList, newList, meta, string(vol.Crn), "", UserTagType) - if err != nil { - log.Printf("Error on update of volume (%s) pi_user_tags during creation: %s", volumeid, err) + if vol.Crn != "" { + oldList, newList := d.GetChange(Arg_UserTags) + err := flex.UpdateGlobalTagsUsingCRN(oldList, newList, meta, string(vol.Crn), "", UserTagType) + if err != nil { + log.Printf("Error on update of volume (%s) pi_user_tags during creation: %s", volumeid, err) + } } } diff --git a/ibm/service/power/resource_ibm_pi_volume_bulk.go b/ibm/service/power/resource_ibm_pi_volume_bulk.go new file mode 100644 index 0000000000..b5cb82a30f --- /dev/null +++ b/ibm/service/power/resource_ibm_pi_volume_bulk.go @@ -0,0 +1,524 @@ +// Copyright IBM Corp. 2024 All Rights Reserved. +// Licensed under the Mozilla Public License v2.0 + +package power + +import ( + "context" + "errors" + "log" + "time" + + "github.com/IBM-Cloud/power-go-client/clients/instance" + "github.com/IBM-Cloud/power-go-client/power/models" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/flex" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/validate" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func ResourceIBMPIVolumeBulk() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceIBMPIVolumeBulkCreate, + ReadContext: resourceIBMPIVolumeBulkRead, + DeleteContext: resourceIBMPIVolumeBulkDelete, + Importer: &schema.ResourceImporter{}, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + // Arguments + Arg_AffinityInstance: { + ConflictsWith: []string{Arg_AffinityVolume}, + Description: "PVM Instance (ID or Name) to base volume affinity policy against; required if requesting 'affinity' and 'pi_affinity_volume' is not provided.", + DiffSuppressFunc: flex.ApplyOnce, + ForceNew: true, + Optional: true, + Type: schema.TypeString, + }, + Arg_AffinityPolicy: { + Description: "Affinity policy for data volume being created; ignored if 'pi_volume_pool' provided; for policy 'affinity' requires one of 'pi_affinity_instance' or 'pi_affinity_volume' to be specified; for policy 'anti-affinity' requires one of 'pi_anti_affinity_instances' or 'pi_anti_affinity_volumes' to be specified; Allowable values: 'affinity', 'anti-affinity'.", + DiffSuppressFunc: flex.ApplyOnce, + ForceNew: true, + Optional: true, + Type: schema.TypeString, + ValidateFunc: validate.InvokeValidator("ibm_pi_volume", Arg_AffinityPolicy), + }, + Arg_AffinityVolume: { + ConflictsWith: []string{Arg_AffinityInstance}, + Description: "Volume (ID or Name) to base volume affinity policy against; required if requesting 'affinity' and 'pi_affinity_instance' is not provided.", + DiffSuppressFunc: flex.ApplyOnce, + ForceNew: true, + Optional: true, + Type: schema.TypeString, + }, + Arg_AntiAffinityInstances: { + ConflictsWith: []string{Arg_AntiAffinityVolumes}, + Description: "List of pvmInstances to base volume anti-affinity policy against; required if requesting 'anti-affinity' and 'pi_anti_affinity_volumes' is not provided.", + DiffSuppressFunc: flex.ApplyOnce, + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeList, + }, + Arg_AntiAffinityVolumes: { + ConflictsWith: []string{Arg_AntiAffinityInstances}, + Description: "List of volumes to base volume anti-affinity policy against; required if requesting 'anti-affinity' and 'pi_anti_affinity_instances' is not provided.", + DiffSuppressFunc: flex.ApplyOnce, + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeList, + }, + Arg_CloudInstanceID: { + Description: "The GUID of the service instance associated with an account.", + ForceNew: true, + Required: true, + Type: schema.TypeString, + ValidateFunc: validation.NoZeroValues, + }, + Arg_Count: { + Default: 1, + Description: "Number of volumes to create. Default 1. Maximum is 500 for public workspaces, and 250 for private workspaces.", + ForceNew: true, + Optional: true, + Type: schema.TypeInt, + ValidateFunc: validation.IntAtLeast(1), + }, + Arg_ReplicationEnabled: { + Computed: true, + Description: "Indicates if the volume should be replication enabled or not.", + Optional: true, + Type: schema.TypeBool, + }, + Arg_ReplicationSites: { + Description: "List of replication sites for volume replication.", + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Set: schema.HashString, + Type: schema.TypeSet, + }, + Arg_UserTags: { + Description: "The user tags attached to this resource.", + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Set: schema.HashString, + Type: schema.TypeSet, + }, + Arg_VolumeName: { + Description: "The shared prefix in the name of the volumes.", + ForceNew: true, + Required: true, + Type: schema.TypeString, + ValidateFunc: validation.NoZeroValues, + }, + Arg_VolumePool: { + Computed: true, + Description: "Volume pool where the volume will be created; if provided then 'pi_affinity_policy' values will be ignored.", + DiffSuppressFunc: flex.ApplyOnce, + Optional: true, + Type: schema.TypeString, + }, + Arg_VolumeShareable: { + Description: "If set to true, the volume can be shared across Power Systems Virtual Server instances. If set to false, you can attach it only to one instance.", + ForceNew: true, + Optional: true, + Type: schema.TypeBool, + }, + Arg_VolumeSize: { + Description: "The size of the volume in GB.", + ForceNew: true, + Required: true, + Type: schema.TypeFloat, + ValidateFunc: validation.FloatAtLeast(1), + }, + Arg_VolumeType: { + Computed: true, + Description: "Type of disk, if diskType is not provided the disk type will default to 'tier3'", + DiffSuppressFunc: flex.ApplyOnce, + Optional: true, + Type: schema.TypeString, + ValidateFunc: validate.ValidateAllowedStringValues([]string{"tier0", "tier1", "tier3", "tier5k"}), + }, + + // Attributes + Attr_Volumes: { + Computed: true, + Description: "List of volumes to create.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + Attr_Auxiliary: { + Computed: true, + Description: "Indicates if the volume is auxiliary or not.", + Type: schema.TypeBool, + }, + Attr_AuxiliaryVolumeName: { + Computed: true, + Description: "The auxiliary volume name.", + Type: schema.TypeString, + }, + Attr_ConsistencyGroupName: { + Computed: true, + Description: "The consistency group name if volume is a part of volume group.", + Type: schema.TypeString, + }, + Attr_CRN: { + Computed: true, + Description: "The CRN of this resource.", + Type: schema.TypeString, + }, + Attr_DeleteOnTermination: { + Computed: true, + Description: "Indicates if the volume should be deleted when the server terminates.", + Type: schema.TypeBool, + }, + Attr_GroupID: { + Computed: true, + Description: "The volume group id to which volume belongs.", + Type: schema.TypeString, + }, + Attr_IOThrottleRate: { + Computed: true, + Description: "Amount of iops assigned to the volume.", + Type: schema.TypeString, + }, + Attr_MasterVolumeName: { + Computed: true, + Description: "Indicates master volume name", + Type: schema.TypeString, + }, + Attr_MirroringState: { + Computed: true, + Description: "Mirroring state for replication enabled volume", + Type: schema.TypeString, + }, + Attr_PrimaryRole: { + Computed: true, + Description: "Indicates whether 'master'/'auxiliary' volume is playing the primary role.", + Type: schema.TypeString, + }, + Attr_ReplicationStatus: { + Computed: true, + Description: "The replication status of the volume.", + Type: schema.TypeString, + }, + Attr_ReplicationSites: { + Computed: true, + Description: "List of replication sites for volume replication.", + Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeList, + }, + Attr_ReplicationType: { + Computed: true, + Description: "The replication type of the volume 'metro' or 'global'.", + Type: schema.TypeString, + }, + Attr_VolumeID: { + Computed: true, + Description: "The unique identifier of the volume.", + Type: schema.TypeString, + }, + Attr_VolumeStatus: { + Computed: true, + Description: "The status of the volume.", + Type: schema.TypeString, + }, + Attr_WWN: { + Computed: true, + Description: "The world wide name of the volume.", + Type: schema.TypeString, + }, + }, + }, + Type: schema.TypeList, + }, + }, + } +} + +func resourceIBMPIVolumeBulkCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sess, err := meta.(conns.ClientSession).IBMPISession() + if err != nil { + return diag.FromErr(err) + } + + name := d.Get(Arg_VolumeName).(string) + size := int64(d.Get(Arg_VolumeSize).(float64)) + var shared bool + if v, ok := d.GetOk(Arg_VolumeShareable); ok { + shared = v.(bool) + } + cloudInstanceID := d.Get(Arg_CloudInstanceID).(string) + body := &models.MultiVolumesCreate{ + Name: &name, + Shareable: &shared, + Size: &size, + } + body.Count = int64(d.Get(Arg_Count).(int)) + if v, ok := d.GetOk(Arg_VolumeType); ok { + volType := v.(string) + body.DiskType = volType + } + if v, ok := d.GetOk(Arg_VolumePool); ok { + volumePool := v.(string) + body.VolumePool = volumePool + } + if v, ok := d.GetOk(Arg_ReplicationEnabled); ok { + replicationEnabled := v.(bool) + body.ReplicationEnabled = &replicationEnabled + } + if v, ok := d.GetOk(Arg_ReplicationSites); ok { + if d.Get(Arg_ReplicationEnabled).(bool) { + body.ReplicationSites = flex.FlattenSet(v.(*schema.Set)) + } else { + return diag.Errorf("Replication (%s) must be enabled if replication sites are specified.", Arg_ReplicationEnabled) + } + } + if ap, ok := d.GetOk(Arg_AffinityPolicy); ok { + policy := ap.(string) + body.AffinityPolicy = &policy + + if policy == Affinity { + if av, ok := d.GetOk(Arg_AffinityVolume); ok { + afvol := av.(string) + body.AffinityVolume = &afvol + } + if ai, ok := d.GetOk(Arg_AffinityInstance); ok { + afins := ai.(string) + body.AffinityPVMInstance = &afins + } + } else { + if avs, ok := d.GetOk(Arg_AntiAffinityVolumes); ok { + afvols := flex.ExpandStringList(avs.([]interface{})) + body.AntiAffinityVolumes = afvols + } + if ais, ok := d.GetOk(Arg_AntiAffinityInstances); ok { + afinss := flex.ExpandStringList(ais.([]interface{})) + body.AntiAffinityPVMInstances = afinss + } + } + + } + if v, ok := d.GetOk(Arg_UserTags); ok { + body.UserTags = flex.FlattenSet(v.(*schema.Set)) + } + + client := instance.NewIBMPIVolumeClient(ctx, sess, cloudInstanceID) + vols, err := client.CreateVolumeV2(body) + if err != nil { + return diag.FromErr(err) + } + + // id is a combination of the cloud instance id and all of the volume ids + id := cloudInstanceID + for _, vol := range vols.Volumes { + id += "/" + *vol.VolumeID + } + d.SetId(id) + + for _, vol := range vols.Volumes { + volumeid := *vol.VolumeID + _, err = isWaitForIBMPIVolumeBulkAvailable(ctx, client, volumeid, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return diag.FromErr(err) + } + if _, ok := d.GetOk(Arg_UserTags); ok { + if vol.Crn != "" { + oldList, newList := d.GetChange(Arg_UserTags) + err := flex.UpdateGlobalTagsUsingCRN(oldList, newList, meta, string(vol.Crn), "", UserTagType) + if err != nil { + log.Printf("Error on update of volume (%s) pi_user_tags during creation: %s", volumeid, err) + } + } + } + } + + return resourceIBMPIVolumeBulkRead(ctx, d, meta) +} + +func resourceIBMPIVolumeBulkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sess, err := meta.(conns.ClientSession).IBMPISession() + if err != nil { + return diag.FromErr(err) + } + + idArr, err := flex.IdParts(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + cloudInstanceID := idArr[0] + client := instance.NewIBMPIVolumeClient(ctx, sess, cloudInstanceID) + volumeIDs := idArr[1:] + + d.Set(Arg_CloudInstanceID, cloudInstanceID) + + // Set Arguments all volumes should have the same information + // so just get it from one + firstVolume, err := client.Get(volumeIDs[0]) + if err != nil { + return diag.FromErr(err) + } + d.Set(Arg_VolumePool, firstVolume.VolumePool) + if firstVolume.Shareable != nil { + d.Set(Arg_VolumeShareable, firstVolume.Shareable) + } + d.Set(Arg_VolumeSize, firstVolume.Size) + d.Set(Arg_VolumeType, firstVolume.DiskType) + d.Set(Arg_ReplicationEnabled, firstVolume.ReplicationEnabled) + + result := make([]map[string]interface{}, 0, len(volumeIDs)) + for _, volumeID := range volumeIDs { + vol, err := client.Get(volumeID) + if err != nil { + return diag.FromErr(err) + } + l := map[string]interface{}{ + Attr_CRN: vol.Crn, + Attr_Auxiliary: vol.Auxiliary, + Attr_AuxiliaryVolumeName: vol.AuxVolumeName, + Attr_ConsistencyGroupName: vol.ConsistencyGroupName, + Attr_GroupID: vol.GroupID, + Attr_IOThrottleRate: vol.IoThrottleRate, + Attr_MasterVolumeName: vol.MasterVolumeName, + Attr_MirroringState: vol.MirroringState, + Attr_PrimaryRole: vol.PrimaryRole, + Attr_ReplicationSites: vol.ReplicationSites, + Attr_ReplicationStatus: vol.ReplicationStatus, + Attr_ReplicationType: vol.ReplicationType, + Attr_VolumeStatus: vol.State, + Attr_WWN: vol.Wwn, + } + if vol.DeleteOnTermination != nil { + l[Attr_DeleteOnTermination] = vol.DeleteOnTermination + } + if vol.VolumeID != nil { + l[Attr_VolumeID] = vol.VolumeID + } + result = append(result, l) + } + + d.Set(Attr_Volumes, result) + + return nil +} + +func resourceIBMPIVolumeBulkDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + sess, err := meta.(conns.ClientSession).IBMPISession() + if err != nil { + return diag.FromErr(err) + } + + idArr, err := flex.IdParts(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + cloudInstanceID := idArr[0] + client := instance.NewIBMPIVolumeClient(ctx, sess, cloudInstanceID) + + volumeIDs := idArr[1:] + volumesDelete := models.VolumesDelete{ + VolumeIDs: volumeIDs, + } + + volInfo, err := client.BulkVolumeDelete(&volumesDelete) + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] Volumes delete accepted: %s", volInfo.Summary) + + _, err = isWaitForIBMPIVolumeBulkDeleted(ctx, client, volumeIDs, d.Timeout(schema.TimeoutDelete)) + if err != nil { + d.SetId(cloudInstanceID + err.Error()) + err = errors.New("error deleting all volumes") + return diag.FromErr(err) + } + + d.SetId("") + return nil +} + +func isWaitForIBMPIVolumeBulkAvailable(ctx context.Context, client *instance.IBMPIVolumeClient, id string, timeout time.Duration) (interface{}, error) { + log.Printf("Waiting for Volume (%s) to be available.", id) + + stateConf := &retry.StateChangeConf{ + Pending: []string{State_Creating}, + Target: []string{State_Available}, + Refresh: isIBMPIVolumeBulkRefreshFunc(client, id), + Delay: 10 * time.Second, + MinTimeout: 2 * time.Minute, + Timeout: timeout, + } + + return stateConf.WaitForStateContext(ctx) +} + +func isIBMPIVolumeBulkRefreshFunc(client *instance.IBMPIVolumeClient, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + vol, err := client.Get(id) + if err != nil { + return nil, "", err + } + + if vol.State == State_Available || vol.State == State_InUse { + return vol, State_Available, nil + } + + return vol, State_Creating, nil + } +} + +func isWaitForIBMPIVolumeBulkDeleted(ctx context.Context, client *instance.IBMPIVolumeClient, volumeIDs []string, timeout time.Duration) (interface{}, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{State_Deleting}, + Target: []string{State_Deleted, State_Error}, + Refresh: isIBMPIVolumeBulkDeleteRefreshFunc(client, volumeIDs), + Delay: 10 * time.Second, + MinTimeout: 2 * time.Minute, + Timeout: timeout, + } + return stateConf.WaitForStateContext(ctx) +} + +func isIBMPIVolumeBulkDeleteRefreshFunc(client *instance.IBMPIVolumeClient, volumeIDs []string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + // Every iteration find all volumes that were not deleted + var leftoverVolumeIDs []string + for _, volumeID := range volumeIDs { + vol, err := client.Get(volumeID) + if err == nil { + leftoverVolumeIDs = append(leftoverVolumeIDs, *vol.VolumeID) + } + } + + // If all volumes were deleted then there are no leftovers + if len(leftoverVolumeIDs) == 0 { + return leftoverVolumeIDs, State_Deleted, nil + } else { + // Every volume that has not been deleted will be retried. + volumesDelete := models.VolumesDelete{ + VolumeIDs: leftoverVolumeIDs, + } + _, err := client.BulkVolumeDelete(&volumesDelete) + if err != nil { + var leftoverIDs string + for _, leftoverVolumeID := range leftoverVolumeIDs { + leftoverIDs += "/" + leftoverVolumeID + } + leftoverError := errors.New(leftoverIDs) + return leftoverVolumeIDs, State_Error, leftoverError + } + return leftoverVolumeIDs, State_Deleting, nil + } + } +} diff --git a/ibm/service/power/resource_ibm_pi_volume_bulk_test.go b/ibm/service/power/resource_ibm_pi_volume_bulk_test.go new file mode 100644 index 0000000000..f67b03dc68 --- /dev/null +++ b/ibm/service/power/resource_ibm_pi_volume_bulk_test.go @@ -0,0 +1,119 @@ +package power_test + +import ( + "context" + "errors" + "fmt" + "log" + "testing" + + acc "github.com/IBM-Cloud/terraform-provider-ibm/ibm/acctest" + + "github.com/IBM-Cloud/power-go-client/clients/instance" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/conns" + "github.com/IBM-Cloud/terraform-provider-ibm/ibm/flex" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccIBMPIVolumeBulkbasic(t *testing.T) { + name := fmt.Sprintf("tf-pi-volume-%d", acctest.RandIntRange(10, 100)) + volumeRes := "ibm_pi_volume_bulk.power_volume" + userTagsString := `["env:dev","test_tag"]` + resource.Test(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + Providers: acc.TestAccProviders, + CheckDestroy: testAccCheckIBMPIVolumeBulkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckIBMPIVolumeBulkConfig(name, userTagsString), + Check: resource.ComposeTestCheckFunc( + testAccCheckIBMPIVolumeBulkExists(volumeRes), + resource.TestCheckResourceAttr(volumeRes, "pi_count", "5"), + resource.TestCheckResourceAttr(volumeRes, "pi_volume_name", name), + resource.TestCheckResourceAttr(volumeRes, "pi_user_tags.#", "2"), + resource.TestCheckTypeSetElemAttr(volumeRes, "pi_user_tags.*", "env:dev"), + resource.TestCheckTypeSetElemAttr(volumeRes, "pi_user_tags.*", "test_tag"), + ), + }, + }, + }) +} + +func testAccCheckIBMPIVolumeBulkConfig(name string, userTagsString string) string { + return fmt.Sprintf(` + resource "ibm_pi_volume_bulk" "power_volume" { + pi_cloud_instance_id = "%[2]s" + pi_count = 5 + pi_user_tags = %[3]s + pi_volume_name = "%[1]s" + pi_volume_shareable = true + pi_volume_size = 1 + pi_volume_type = "tier3" + }`, name, acc.Pi_cloud_instance_id, userTagsString) +} + +func testAccCheckIBMPIVolumeBulkExists(n 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 errors.New("No Record ID is set") + } + + sess, err := acc.TestAccProvider.Meta().(conns.ClientSession).IBMPISession() + if err != nil { + return err + } + + idArr, err := flex.IdParts(rs.Primary.ID) + if err != nil { + return err + } + + cloudInstanceID := idArr[0] + for _, volumeID := range idArr[1:] { + client := instance.NewIBMPIVolumeClient(context.Background(), sess, cloudInstanceID) + _, err := client.Get(volumeID) + if err != nil { + return err + } + } + return nil + } +} + +func testAccCheckIBMPIVolumeBulkDestroy(s *terraform.State) error { + sess, err := acc.TestAccProvider.Meta().(conns.ClientSession).IBMPISession() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ibm_pi_volume_bulk" { + continue + } + + idArr, err := flex.IdParts(rs.Primary.ID) + if err != nil { + return err + } + + cloudInstanceID := idArr[0] + for _, volumeID := range idArr[1:] { + client := instance.NewIBMPIVolumeClient(context.Background(), sess, cloudInstanceID) + volume, err := client.Get(volumeID) + if err == nil { + log.Println("volume*****", volume.State) + return fmt.Errorf("PI Volume still exists: %s", rs.Primary.ID) + } + } + } + + return nil +} diff --git a/website/docs/r/pi_volume.html.markdown b/website/docs/r/pi_volume.html.markdown index 6737608882..1341e91b11 100644 --- a/website/docs/r/pi_volume.html.markdown +++ b/website/docs/r/pi_volume.html.markdown @@ -8,7 +8,7 @@ description: |- # ibm_pi_volume -Create, update, or delete a volume to attach it to a Power Systems Virtual Server instance. For more information, about managing volume, see [cloning a volume](https://cloud.ibm.com/docs/power-iaas?topic=power-iaas-volume-snapshot-clone#cloning-volume). +Create, update, or delete a volume to attach it to a Power Systems Virtual Server instance. For more information, about managing volume, see [cloning a volume](https://cloud.ibm.com/docs/power-iaas?topic=power-iaas-snapshots-cloning). ## Example usage diff --git a/website/docs/r/pi_volume_bulk.html.markdown b/website/docs/r/pi_volume_bulk.html.markdown new file mode 100644 index 0000000000..54a613dc34 --- /dev/null +++ b/website/docs/r/pi_volume_bulk.html.markdown @@ -0,0 +1,106 @@ +--- +subcategory: "Power Systems" +layout: "ibm" +page_title: "IBM: pi_volume_bulk" +description: |- + Manages IBM Volume in the Power Virtual Server cloud. +--- + +# ibm_pi_volume_bulk + +Create, update, or delete a volume to attach it to a Power Systems Virtual Server instance. For more information, about managing volume, see [cloning a volume](https://cloud.ibm.com/docs/power-iaas?topic=power-iaas-snapshots-cloning). + +## Example usage + +The following example creates 3 20 GB volumes. + +```terraform +resource "ibm_pi_volume_bulk" "testacc_volume"{ + pi_cloud_instance_id = "" + pi_count = 3 + pi_volume_size = 20 + pi_volume_name = "test-volume" + pi_volume_type = "tier3" + pi_volume_shareable = true +} +``` + +### Notes + +- Please find [supported Regions](https://cloud.ibm.com/apidocs/power-cloud#endpoint) for endpoints. +- If a Power cloud instance is provisioned at `lon04`, The provider level attributes should be as follows: + - `region` - `lon` + - `zone` - `lon04` + +Example usage: + + ```terraform + provider "ibm" { + region = "lon" + zone = "lon04" + } + ``` + +## Timeouts + +ibm_pi_volume_bulk provides the following [timeouts](https://www.terraform.io/docs/language/resources/syntax.html) configuration options: + +- **create** - (Default 30 minutes) Used for creating volume. +- **delete** - (Default 30 minutes) Used for deleting volume. + +## Argument reference + +Review the argument references that you can specify for your resource. + +- `pi_affinity_instance` - (Optional, String) PVM Instance (ID or Name) to base volume affinity policy against; required if requesting `affinity` and `pi_affinity_volume` is not provided. +- `pi_affinity_policy` - (Optional, String) Affinity policy for data volume being created; ignored if `pi_volume_pool` provided; for policy 'affinity' requires one of `pi_affinity_instance` or `pi_affinity_volume` to be specified; for policy 'anti-affinity' requires one of `pi_anti_affinity_instances` or `pi_anti_affinity_volumes` to be specified; Allowable values: `affinity`, `anti-affinity`. +- `pi_affinity_volume`- (Optional, String) Volume (ID or Name) to base volume affinity policy against; required if requesting `affinity` and `pi_affinity_instance` is not provided. +- `pi_anti_affinity_instances` - (Optional, String) List of pvmInstances to base volume anti-affinity policy against; required if requesting `anti-affinity` and `pi_anti_affinity_volumes` is not provided. +- `pi_anti_affinity_volumes`- (Optional, String) List of volumes to base volume anti-affinity policy against; required if requesting `anti-affinity` and `pi_anti_affinity_instances` is not provided. +- `pi_cloud_instance_id` - (Required, String) The GUID of the service instance associated with an account. +- `pi_count` - (Optional, Integer) Number of volumes to create. Default 1. Maximum is 500 for public workspaces, and 250 for private workspaces. +- `pi_replication_enabled` - (Optional, Boolean) Indicates if the volume should be replication enabled or not. + + **Note:** `replication_sites` will be populated automatically with default sites if set to true and sites are not specified. + +- `pi_replication_sites` - (Optional, List) List of replication sites for volume replication. Must set `pi_replication_enabled` to true to use. +- `pi_user_tags` - (Optional, List) The user tags attached to this resource. +- `pi_volume_name` - (Required, String) The name of the volume. +- `pi_volume_pool` - (Optional, String) Volume pool where the volume will be created; if provided then `pi_affinity_policy` values will be ignored. +- `pi_volume_shareable` - (Required, Boolean) If set to **true**, the volume can be shared across Power Systems Virtual Server instances. If set to **false**, you can attach it only to one instance. +- `pi_volume_size` - (Required, Integer) The size of the volume in GB. +- `pi_volume_type` - (Optional, String) Type of volume, if this field is not provided, it will default to `tier3`. To get a list of available volume types, please use the [ibm_pi_storage_types_capacity](https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/data-sources/pi_storage_types_capacity) data source. + +## Attribute reference + +In addition to all argument reference list, you can access the following attribute reference after your resource is created. + +- `id` - (String) The unique identifier of the bulk volume resource. The ID is composed of `//.../`. +- `volumes` - (List) List of volumes to create. + + - `auxiliary` - (Boolean) Indicates if the volume is auxiliary or not. + - `auxiliary_volume_name` - (String) The auxiliary volume name. + - `consistency_group_name` - (String) The consistency group name if volume is a part of volume group. + - `crn` - (String) The CRN of this resource. + - `delete_on_termination` - (Boolean) Indicates if the volume should be deleted when the server terminates. + - `group_id` - (String) The volume group id to which volume belongs. + - `io_throttle_rate` - (String) Amount of iops assigned to the volume. + - `master_volume_name` - (String) The master volume name. + - `mirroring_state` - (String) Mirroring state for replication enabled volume. + - `primary_role` - (String) Indicates whether `master`/`auxiliary` volume is playing the primary role. + - `replication_status` - (String) The replication status of the volume. + - `replication_sites` - (List) List of replication sites for volume replication. + - `replication_type` - (String) The replication type of the volume `metro` or `global`. + - `volume_id` - (String) The unique identifier of the volume. + - `volume_status` - (String) The status of the volume. + - `wwn` - (String) The world wide name of the volume. + +## Import + +The `ibm_pi_volume_bulk` resource can be imported by using `pi_cloud_instance_id` and `volume_id`. + +### Example + +```bash +terraform import ibm_pi_volume_bulk.example d7bec597-4726-451f-8a63-e62e6f19c32c/cea6651a-bc0a-4438-9f8a-a0770bbf3ebb +```