diff --git a/docs/resources/virtual_environment_storage_directory.md b/docs/resources/virtual_environment_storage_directory.md new file mode 100644 index 000000000..8fbd6d31d --- /dev/null +++ b/docs/resources/virtual_environment_storage_directory.md @@ -0,0 +1,45 @@ +--- +layout: page +title: proxmox_virtual_environment_storage_directory +parent: Resources +subcategory: Virtual Environment +description: |- + Manages directory-based storage in Proxmox VE. +--- + +# Resource: proxmox_virtual_environment_storage_directory + +Manages directory-based storage in Proxmox VE. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the storage. +- `path` (String) The path to the directory on the Proxmox node. + +### Optional + +- `backups` (Block, Optional) Configure backup retention settings for the storage type. (see [below for nested schema](#nestedblock--backups)) +- `content` (Set of String) The content types that can be stored on this storage. +- `disable` (Boolean) Whether the storage is disabled. +- `nodes` (Set of String) A list of nodes where this storage is available. +- `preallocation` (String) The preallocation mode for raw and qcow2 images. +- `shared` (Boolean) Whether the storage is shared across all nodes. + + +### Nested Schema for `backups` + +Optional: + +- `keep_all` (Boolean) Specifies if all backups should be kept, regardless of their age. +- `keep_daily` (Number) The number of daily backups to keep. Older backups will be removed. +- `keep_hourly` (Number) The number of hourly backups to keep. Older backups will be removed. +- `keep_last` (Number) Specifies the number of the most recent backups to keep, regardless of their age. +- `keep_monthly` (Number) The number of monthly backups to keep. Older backups will be removed. +- `keep_weekly` (Number) The number of weekly backups to keep. Older backups will be removed. +- `keep_yearly` (Number) The number of yearly backups to keep. Older backups will be removed. +- `max_protected_backups` (Number) The maximum number of protected backups per guest. Use '-1' for unlimited. diff --git a/docs/resources/virtual_environment_storage_lvm.md b/docs/resources/virtual_environment_storage_lvm.md new file mode 100644 index 000000000..64ccc8f22 --- /dev/null +++ b/docs/resources/virtual_environment_storage_lvm.md @@ -0,0 +1,30 @@ +--- +layout: page +title: proxmox_virtual_environment_storage_lvm +parent: Resources +subcategory: Virtual Environment +description: |- + Manages LVM-based storage in Proxmox VE. +--- + +# Resource: proxmox_virtual_environment_storage_lvm + +Manages LVM-based storage in Proxmox VE. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the storage. +- `volume_group` (String) The name of the volume group to use. + +### Optional + +- `content` (Set of String) The content types that can be stored on this storage. +- `disable` (Boolean) Whether the storage is disabled. +- `nodes` (Set of String) A list of nodes where this storage is available. +- `shared` (Boolean) Whether the storage is shared across all nodes. +- `wipe_removed_volumes` (Boolean) Whether to zero-out data when removing LVMs. diff --git a/docs/resources/virtual_environment_storage_lvmthin.md b/docs/resources/virtual_environment_storage_lvmthin.md new file mode 100644 index 000000000..ad9277e54 --- /dev/null +++ b/docs/resources/virtual_environment_storage_lvmthin.md @@ -0,0 +1,30 @@ +--- +layout: page +title: proxmox_virtual_environment_storage_lvmthin +parent: Resources +subcategory: Virtual Environment +description: |- + Manages thin LVM-based storage in Proxmox VE. +--- + +# Resource: proxmox_virtual_environment_storage_lvmthin + +Manages thin LVM-based storage in Proxmox VE. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the storage. +- `thin_pool` (String) The name of the LVM thin pool to use. +- `volume_group` (String) The name of the volume group to use. + +### Optional + +- `content` (Set of String) The content types that can be stored on this storage. +- `disable` (Boolean) Whether the storage is disabled. +- `nodes` (Set of String) A list of nodes where this storage is available. +- `shared` (Boolean) Whether the storage is shared across all nodes. diff --git a/docs/resources/virtual_environment_storage_nfs.md b/docs/resources/virtual_environment_storage_nfs.md new file mode 100644 index 000000000..188786f65 --- /dev/null +++ b/docs/resources/virtual_environment_storage_nfs.md @@ -0,0 +1,36 @@ +--- +layout: page +title: proxmox_virtual_environment_storage_nfs +parent: Resources +subcategory: Virtual Environment +description: |- + Manages an NFS-based storage in Proxmox VE. +--- + +# Resource: proxmox_virtual_environment_storage_nfs + +Manages an NFS-based storage in Proxmox VE. + + + + +## Schema + +### Required + +- `export` (String) The path of the NFS export. +- `id` (String) The unique identifier of the storage. +- `server` (String) The IP address or DNS name of the NFS server. + +### Optional + +- `content` (Set of String) The content types that can be stored on this storage. +- `disable` (Boolean) Whether the storage is disabled. +- `nodes` (Set of String) A list of nodes where this storage is available. +- `options` (String) The options to pass to the NFS service. +- `preallocation` (String) The preallocation mode for raw and qcow2 images. +- `snapshot_as_volume_chain` (Boolean) Enable support for creating snapshots through volume backing-chains. + +### Read-Only + +- `shared` (Boolean) Whether the storage is shared across all nodes. diff --git a/docs/resources/virtual_environment_storage_pbs.md b/docs/resources/virtual_environment_storage_pbs.md new file mode 100644 index 000000000..90a5f3c55 --- /dev/null +++ b/docs/resources/virtual_environment_storage_pbs.md @@ -0,0 +1,41 @@ +--- +layout: page +title: proxmox_virtual_environment_storage_pbs +parent: Resources +subcategory: Virtual Environment +description: |- + Manages a Proxmox Backup Server (PBS) storage in Proxmox VE. +--- + +# Resource: proxmox_virtual_environment_storage_pbs + +Manages a Proxmox Backup Server (PBS) storage in Proxmox VE. + + + + +## Schema + +### Required + +- `datastore` (String) The name of the datastore on the Proxmox Backup Server. +- `id` (String) The unique identifier of the storage. +- `password` (String, Sensitive) The password for authenticating with the Proxmox Backup Server. +- `server` (String) The IP address or DNS name of the Proxmox Backup Server. +- `username` (String) The username for authenticating with the Proxmox Backup Server. + +### Optional + +- `content` (Set of String) The content types that can be stored on this storage. +- `disable` (Boolean) Whether the storage is disabled. +- `encryption_key` (String, Sensitive) An existing encryption key for the datastore. This is a sensitive value. Conflicts with `generate_encryption_key`. +- `fingerprint` (String) The SHA256 fingerprint of the Proxmox Backup Server's certificate. +- `generate_encryption_key` (Boolean) If set to true, Proxmox will generate a new encryption key. The key will be stored in the `generated_encryption_key` attribute. Conflicts with `encryption_key`. +- `namespace` (String) The namespace to use on the Proxmox Backup Server. +- `nodes` (Set of String) A list of nodes where this storage is available. + +### Read-Only + +- `encryption_key_fingerprint` (String) The SHA256 fingerprint of the encryption key currently in use. +- `generated_encryption_key` (String, Sensitive) The encryption key returned by Proxmox when `generate_encryption_key` is true. +- `shared` (Boolean) Whether the storage is shared across all nodes. diff --git a/docs/resources/virtual_environment_storage_smb.md b/docs/resources/virtual_environment_storage_smb.md new file mode 100644 index 000000000..1af007a64 --- /dev/null +++ b/docs/resources/virtual_environment_storage_smb.md @@ -0,0 +1,39 @@ +--- +layout: page +title: proxmox_virtual_environment_storage_smb +parent: Resources +subcategory: Virtual Environment +description: |- + Manages an SMB/CIFS based storage server in Proxmox VE. +--- + +# Resource: proxmox_virtual_environment_storage_smb + +Manages an SMB/CIFS based storage server in Proxmox VE. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the storage. +- `password` (String, Sensitive) The password for authenticating with the SMB/CIFS server. +- `server` (String) The IP address or DNS name of the SMB/CIFS server. +- `share` (String) The name of the SMB/CIFS share. +- `username` (String) The username for authenticating with the SMB/CIFS server. + +### Optional + +- `content` (Set of String) The content types that can be stored on this storage. +- `disable` (Boolean) Whether the storage is disabled. +- `domain` (String) The SMB/CIFS domain. +- `nodes` (Set of String) A list of nodes where this storage is available. +- `preallocation` (String) The preallocation mode for raw and qcow2 images. +- `snapshot_as_volume_chain` (Boolean) Enable support for creating snapshots through volume backing-chains. +- `subdirectory` (String) A subdirectory to mount within the share. + +### Read-Only + +- `shared` (Boolean) Whether the storage is shared across all nodes. diff --git a/docs/resources/virtual_environment_storage_zfspool.md b/docs/resources/virtual_environment_storage_zfspool.md new file mode 100644 index 000000000..90b0c977b --- /dev/null +++ b/docs/resources/virtual_environment_storage_zfspool.md @@ -0,0 +1,34 @@ +--- +layout: page +title: proxmox_virtual_environment_storage_zfspool +parent: Resources +subcategory: Virtual Environment +description: |- + Manages ZFS-based storage in Proxmox VE. +--- + +# Resource: proxmox_virtual_environment_storage_zfspool + +Manages ZFS-based storage in Proxmox VE. + + + + +## Schema + +### Required + +- `id` (String) The unique identifier of the storage. +- `zfs_pool` (String) The name of the ZFS storage pool to use (e.g. `tank`, `rpool/data`). + +### Optional + +- `blocksize` (String) Block size for newly created volumes (e.g. `4k`, `8k`, `16k`). Larger values may improve throughput for large I/O, while smaller values optimize space efficiency. +- `content` (Set of String) The content types that can be stored on this storage. +- `disable` (Boolean) Whether the storage is disabled. +- `nodes` (Set of String) A list of nodes where this storage is available. +- `thin_provision` (Boolean) Whether to enable thin provisioning (`on` or `off`). Thin provisioning allows flexible disk allocation without pre-allocating full space. + +### Read-Only + +- `shared` (Boolean) Whether the storage is shared across all nodes. diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 5238d23ea..348c3ea47 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -38,6 +38,7 @@ import ( "github.com/bpg/terraform-provider-proxmox/fwprovider/nodes/datastores" "github.com/bpg/terraform-provider-proxmox/fwprovider/nodes/network" "github.com/bpg/terraform-provider-proxmox/fwprovider/nodes/vm" + "github.com/bpg/terraform-provider-proxmox/fwprovider/storage" "github.com/bpg/terraform-provider-proxmox/proxmox" "github.com/bpg/terraform-provider-proxmox/proxmox/api" "github.com/bpg/terraform-provider-proxmox/proxmox/cluster" @@ -534,7 +535,14 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc sdnzone.NewQinQResource, sdnzone.NewVXLANResource, sdnzone.NewEVPNResource, - sdnapplier.NewResource, + sdnapplier.NewResource, + storage.NewDirectoryStorageResource, + storage.NewLVMPoolStorageResource, + storage.NewLVMThinPoolStorageResource, + storage.NewNFSStorageResource, + storage.NewProxmoxBackupServerStorageResource, + storage.NewCIFSStorageResource, + storage.NewZFSPoolStorageResource, } } diff --git a/fwprovider/storage/model_backups.go b/fwprovider/storage/model_backups.go new file mode 100644 index 000000000..d0cff4023 --- /dev/null +++ b/fwprovider/storage/model_backups.go @@ -0,0 +1,23 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// BackupModel maps the backup block schema. +type BackupModel struct { + MaxProtectedBackups types.Int64 `tfsdk:"max_protected_backups"` + KeepAll types.Bool `tfsdk:"keep_all"` + KeepLast types.Int64 `tfsdk:"keep_last"` + KeepHourly types.Int64 `tfsdk:"keep_hourly"` + KeepDaily types.Int64 `tfsdk:"keep_daily"` + KeepWeekly types.Int64 `tfsdk:"keep_weekly"` + KeepMonthly types.Int64 `tfsdk:"keep_monthly"` + KeepYearly types.Int64 `tfsdk:"keep_yearly"` +} diff --git a/fwprovider/storage/model_base.go b/fwprovider/storage/model_base.go new file mode 100644 index 000000000..f431044c1 --- /dev/null +++ b/fwprovider/storage/model_base.go @@ -0,0 +1,109 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + "fmt" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + proxmox_types "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// StorageModelBase contains the common fields for all storage models. +type StorageModelBase struct { + ID types.String `tfsdk:"id"` + Nodes types.Set `tfsdk:"nodes"` + ContentTypes types.Set `tfsdk:"content"` + Disable types.Bool `tfsdk:"disable"` + Shared types.Bool `tfsdk:"shared"` +} + +// GetID returns the storage identifier from the base model. +func (m *StorageModelBase) GetID() types.String { + return m.ID +} + +// populateBaseFromAPI is a helper to populate the common fields from an API response. +func (m *StorageModelBase) populateBaseFromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + m.ID = types.StringValue(*datastore.ID) + + if datastore.Nodes != nil { + nodes, diags := types.SetValueFrom(ctx, types.StringType, *datastore.Nodes) + if diags.HasError() { + return fmt.Errorf("cannot parse nodes from datastore: %s", diags) + } + + m.Nodes = nodes + } else { + m.Nodes = types.SetValueMust(types.StringType, []attr.Value{}) + } + + if datastore.ContentTypes != nil { + contentTypes, diags := types.SetValueFrom(ctx, types.StringType, *datastore.ContentTypes) + if diags.HasError() { + return fmt.Errorf("cannot parse content from datastore: %s", diags) + } + + m.ContentTypes = contentTypes + } + + if datastore.Disable != nil { + m.Disable = datastore.Disable.ToValue() + } + + if datastore.Shared != nil { + m.Shared = datastore.Shared.ToValue() + } + + return nil +} + +// populateCreateFields is a helper to populate the common fields for a create request. +func (m *StorageModelBase) populateCreateFields( + ctx context.Context, + immutableReq *storage.DataStoreCommonImmutableFields, + mutableReq *storage.DataStoreCommonMutableFields, +) error { + var nodes proxmox_types.CustomCommaSeparatedList + if diags := m.Nodes.ElementsAs(ctx, &nodes, false); diags.HasError() { + return fmt.Errorf("cannot convert nodes: %s", diags) + } + + var contentTypes proxmox_types.CustomCommaSeparatedList + if diags := m.ContentTypes.ElementsAs(ctx, &contentTypes, false); diags.HasError() { + return fmt.Errorf("cannot convert content-types: %s", diags) + } + + immutableReq.ID = m.ID.ValueStringPointer() + mutableReq.Nodes = &nodes + mutableReq.ContentTypes = &contentTypes + mutableReq.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) + + return nil +} + +// populateUpdateFields is a helper to populate the common fields for an update request. +func (m *StorageModelBase) populateUpdateFields(ctx context.Context, mutableReq *storage.DataStoreCommonMutableFields) error { + var nodes proxmox_types.CustomCommaSeparatedList + if diags := m.Nodes.ElementsAs(ctx, &nodes, false); diags.HasError() { + return fmt.Errorf("cannot convert nodes: %s", diags) + } + + var contentTypes proxmox_types.CustomCommaSeparatedList + if diags := m.ContentTypes.ElementsAs(ctx, &contentTypes, false); diags.HasError() { + return fmt.Errorf("cannot convert content-types: %s", diags) + } + + mutableReq.Nodes = &nodes + mutableReq.ContentTypes = &contentTypes + mutableReq.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) + + return nil +} diff --git a/fwprovider/storage/model_cifs.go b/fwprovider/storage/model_cifs.go new file mode 100644 index 000000000..d4d1f448d --- /dev/null +++ b/fwprovider/storage/model_cifs.go @@ -0,0 +1,97 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + proxmox_types "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// CIFSStorageModel maps the Terraform schema for CIFS storage. +type CIFSStorageModel struct { + StorageModelBase + + Server types.String `tfsdk:"server"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + Share types.String `tfsdk:"share"` + Domain types.String `tfsdk:"domain"` + SubDirectory types.String `tfsdk:"subdirectory"` + Preallocation types.String `tfsdk:"preallocation"` + SnapshotsAsVolumeChain types.Bool `tfsdk:"snapshot_as_volume_chain"` +} + +func (m *CIFSStorageModel) GetStorageType() types.String { + return types.StringValue("cifs") +} + +func (m *CIFSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.CIFSStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() + + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Server = m.Server.ValueStringPointer() + request.Username = m.Username.ValueStringPointer() + request.Password = m.Password.ValueStringPointer() + request.Share = m.Share.ValueStringPointer() + request.Domain = m.Domain.ValueStringPointer() + request.Subdirectory = m.SubDirectory.ValueStringPointer() + request.Preallocation = m.Preallocation.ValueStringPointer() + request.SnapshotsAsVolumeChain = proxmox_types.CustomBool(m.SnapshotsAsVolumeChain.ValueBool()) + + return request, nil +} + +func (m *CIFSStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.CIFSStorageUpdateRequest{} + + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Preallocation = m.Preallocation.ValueStringPointer() + + return request, nil +} + +func (m *CIFSStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + if err := m.populateBaseFromAPI(ctx, datastore); err != nil { + return err + } + + if datastore.Server != nil { + m.Server = types.StringValue(*datastore.Server) + } + + if datastore.Username != nil { + m.Username = types.StringValue(*datastore.Username) + } + + if datastore.Share != nil { + m.Share = types.StringValue(*datastore.Share) + } + + if datastore.Domain != nil { + m.Domain = types.StringValue(*datastore.Domain) + } + + if datastore.SubDirectory != nil { + m.SubDirectory = types.StringValue(*datastore.SubDirectory) + } + + if datastore.Preallocation != nil { + m.Preallocation = types.StringValue(*datastore.Preallocation) + } + + return nil +} diff --git a/fwprovider/storage/model_directory.go b/fwprovider/storage/model_directory.go new file mode 100644 index 000000000..ca27756e2 --- /dev/null +++ b/fwprovider/storage/model_directory.go @@ -0,0 +1,69 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// DirectoryStorageModel maps the Terraform schema for directory storage. +type DirectoryStorageModel struct { + StorageModelBase + + Path types.String `tfsdk:"path"` + Preallocation types.String `tfsdk:"preallocation"` + Backups *BackupModel `tfsdk:"backups"` +} + +func (m *DirectoryStorageModel) GetStorageType() types.String { + return types.StringValue("dir") +} + +func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.DirectoryStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() + + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Path = m.Path.ValueStringPointer() + request.Preallocation = m.Preallocation.ValueStringPointer() + + return request, nil +} + +func (m *DirectoryStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.DirectoryStorageUpdateRequest{} + + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Preallocation = m.Preallocation.ValueStringPointer() + + return request, nil +} + +func (m *DirectoryStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + if err := m.populateBaseFromAPI(ctx, datastore); err != nil { + return err + } + + if datastore.Path != nil { + m.Path = types.StringValue(*datastore.Path) + } + + if datastore.Preallocation != nil { + m.Preallocation = types.StringValue(*datastore.Preallocation) + } + + return nil +} diff --git a/fwprovider/storage/model_lvm.go b/fwprovider/storage/model_lvm.go new file mode 100644 index 000000000..e5a7f4b53 --- /dev/null +++ b/fwprovider/storage/model_lvm.go @@ -0,0 +1,73 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + proxmox_types "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// LVMStorageModel maps the Terraform schema for LVM storage. +type LVMStorageModel struct { + StorageModelBase + + VolumeGroup types.String `tfsdk:"volume_group"` + WipeRemovedVolumes types.Bool `tfsdk:"wipe_removed_volumes"` +} + +// GetStorageType returns the storage type identifier. +func (m *LVMStorageModel) GetStorageType() types.String { + return types.StringValue("lvm") +} + +// toCreateAPIRequest converts the Terraform model to a Proxmox API request body. +func (m *LVMStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.LVMStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() + + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.VolumeGroup = m.VolumeGroup.ValueStringPointer() + request.WipeRemovedVolumes = proxmox_types.CustomBool(m.WipeRemovedVolumes.ValueBool()) + + return request, nil +} + +// toUpdateAPIRequest converts the Terraform model to a Proxmox API request body for updates. +func (m *LVMStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.LVMStorageUpdateRequest{} + + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.WipeRemovedVolumes = proxmox_types.CustomBool(m.WipeRemovedVolumes.ValueBool()) + + return request, nil +} + +// fromAPI populates the Terraform model from a Proxmox API response. +func (m *LVMStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + if err := m.populateBaseFromAPI(ctx, datastore); err != nil { + return err + } + + if datastore.VolumeGroup != nil { + m.VolumeGroup = types.StringValue(*datastore.VolumeGroup) + } + + if datastore.WipeRemovedVolumes != nil { + m.WipeRemovedVolumes = types.BoolValue(*datastore.WipeRemovedVolumes.PointerBool()) + } + + return nil +} diff --git a/fwprovider/storage/model_lvm_thin.go b/fwprovider/storage/model_lvm_thin.go new file mode 100644 index 000000000..39482b0f1 --- /dev/null +++ b/fwprovider/storage/model_lvm_thin.go @@ -0,0 +1,70 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// LVMThinStorageModel maps the Terraform schema for LVM storage. +type LVMThinStorageModel struct { + StorageModelBase + + VolumeGroup types.String `tfsdk:"volume_group"` + ThinPool types.String `tfsdk:"thin_pool"` +} + +// GetStorageType returns the storage type identifier. +func (m *LVMThinStorageModel) GetStorageType() types.String { + return types.StringValue("lvmthin") +} + +// toCreateAPIRequest converts the Terraform model to a Proxmox API request body. +func (m *LVMThinStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.LVMThinStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() + + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.VolumeGroup = m.VolumeGroup.ValueStringPointer() + request.ThinPool = m.ThinPool.ValueStringPointer() + + return request, nil +} + +// toUpdateAPIRequest converts the Terraform model to a Proxmox API request body for updates. +func (m *LVMThinStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.LVMThinStorageUpdateRequest{} + + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + return request, nil +} + +// fromAPI populates the Terraform model from a Proxmox API response. +func (m *LVMThinStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + if err := m.populateBaseFromAPI(ctx, datastore); err != nil { + return err + } + + if datastore.VolumeGroup != nil { + m.VolumeGroup = types.StringValue(*datastore.VolumeGroup) + } + + if datastore.ThinPool != nil { + m.ThinPool = types.StringValue(*datastore.ThinPool) + } + + return nil +} diff --git a/fwprovider/storage/model_nfs.go b/fwprovider/storage/model_nfs.go new file mode 100644 index 000000000..56c000778 --- /dev/null +++ b/fwprovider/storage/model_nfs.go @@ -0,0 +1,83 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + proxmox_types "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// NFSStorageModel maps the Terraform schema for NFS storage. +type NFSStorageModel struct { + StorageModelBase + + Server types.String `tfsdk:"server"` + Export types.String `tfsdk:"export"` + Options types.String `tfsdk:"options"` + Preallocation types.String `tfsdk:"preallocation"` + SnapshotsAsVolumeChain types.Bool `tfsdk:"snapshot_as_volume_chain"` +} + +func (m *NFSStorageModel) GetStorageType() types.String { + return types.StringValue("nfs") +} + +func (m *NFSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.NFSStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() + + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Server = m.Server.ValueStringPointer() + request.Export = m.Export.ValueStringPointer() + request.Options = m.Options.ValueStringPointer() + request.Preallocation = m.Preallocation.ValueStringPointer() + request.SnapshotsAsVolumeChain = proxmox_types.CustomBool(m.SnapshotsAsVolumeChain.ValueBool()) + + return request, nil +} + +func (m *NFSStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.NFSStorageUpdateRequest{} + + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Options = m.Options.ValueStringPointer() + + return request, nil +} + +func (m *NFSStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + if err := m.populateBaseFromAPI(ctx, datastore); err != nil { + return err + } + + if datastore.Server != nil { + m.Server = types.StringValue(*datastore.Server) + } + + if datastore.Export != nil { + m.Export = types.StringValue(*datastore.Export) + } + + if datastore.Options != nil { + m.Options = types.StringValue(*datastore.Options) + } + + if datastore.Preallocation != nil { + m.Preallocation = types.StringValue(*datastore.Preallocation) + } + + return nil +} diff --git a/fwprovider/storage/model_pbs.go b/fwprovider/storage/model_pbs.go new file mode 100644 index 000000000..d09e5950e --- /dev/null +++ b/fwprovider/storage/model_pbs.go @@ -0,0 +1,113 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// PBSStorageModel maps the Terraform schema for PBS storage. +type PBSStorageModel struct { + StorageModelBase + + Server types.String `tfsdk:"server"` + Datastore types.String `tfsdk:"datastore"` + Username types.String `tfsdk:"username"` + Password types.String `tfsdk:"password"` + Namespace types.String `tfsdk:"namespace"` + Fingerprint types.String `tfsdk:"fingerprint"` + EncryptionKey types.String `tfsdk:"encryption_key"` + EncryptionKeyFingerprint types.String `tfsdk:"encryption_key_fingerprint"` + GenerateEncryptionKey types.Bool `tfsdk:"generate_encryption_key"` + GeneratedEncryptionKey types.String `tfsdk:"generated_encryption_key"` +} + +// GetStorageType returns the storage type identifier. +func (m *PBSStorageModel) GetStorageType() types.String { + return types.StringValue("pbs") +} + +// toCreateAPIRequest converts the Terraform model to a Proxmox API request body. +func (m *PBSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.PBSStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() + + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Username = m.Username.ValueStringPointer() + request.Password = m.Password.ValueStringPointer() + request.Namespace = m.Namespace.ValueStringPointer() + request.Server = m.Server.ValueStringPointer() + request.Datastore = m.Datastore.ValueStringPointer() + request.Fingerprint = m.Fingerprint.ValueStringPointer() + + if !m.GenerateEncryptionKey.IsNull() && m.GenerateEncryptionKey.ValueBool() { + request.Encryption = types.StringValue("autogen").ValueStringPointer() + } else if !m.EncryptionKey.IsNull() && m.EncryptionKey.ValueString() != "" { + request.Encryption = m.EncryptionKey.ValueStringPointer() + } + + return request, nil +} + +// toUpdateAPIRequest converts the Terraform model to a Proxmox API request body for updates. +func (m *PBSStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.PBSStorageUpdateRequest{} + + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.Fingerprint = m.Fingerprint.ValueStringPointer() + + if !m.GenerateEncryptionKey.IsNull() && m.GenerateEncryptionKey.ValueBool() { + request.Encryption = types.StringValue("autogen").ValueStringPointer() + } else if !m.EncryptionKey.IsNull() && m.EncryptionKey.ValueString() != "" { + request.Encryption = m.EncryptionKey.ValueStringPointer() + } + + return request, nil +} + +// fromAPI populates the Terraform model from a Proxmox API response. +// Password is not returned by the API so we leave it as is in the state. +func (m *PBSStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + if err := m.populateBaseFromAPI(ctx, datastore); err != nil { + return err + } + + if datastore.Server != nil { + m.Server = types.StringValue(*datastore.Server) + } + + if datastore.Datastore != nil { + m.Datastore = types.StringValue(*datastore.Datastore) + } + + if datastore.Username != nil { + m.Username = types.StringValue(*datastore.Username) + } + + if datastore.Namespace != nil { + m.Namespace = types.StringValue(*datastore.Namespace) + } + + if datastore.Fingerprint != nil { + m.Fingerprint = types.StringValue(*datastore.Fingerprint) + } + + if datastore.Shared != nil { + m.Shared = types.BoolValue(*datastore.Shared.PointerBool()) + } + + return nil +} diff --git a/fwprovider/storage/model_zfs.go b/fwprovider/storage/model_zfs.go new file mode 100644 index 000000000..e935d2f9a --- /dev/null +++ b/fwprovider/storage/model_zfs.go @@ -0,0 +1,83 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + proxmox_types "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// ZFSStorageModel maps the Terraform schema for ZFS storage. +type ZFSStorageModel struct { + StorageModelBase + + ZFSPool types.String `tfsdk:"zfs_pool"` + ThinProvision types.Bool `tfsdk:"thin_provision"` + Blocksize types.String `tfsdk:"blocksize"` +} + +// GetStorageType returns the storage type identifier. +func (m *ZFSStorageModel) GetStorageType() types.String { + return types.StringValue("zfspool") +} + +// toCreateAPIRequest converts the Terraform model to a Proxmox API request body. +func (m *ZFSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.ZFSStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() + + if err := m.populateCreateFields(ctx, + &request.DataStoreCommonImmutableFields, + &request.DataStoreCommonMutableFields, + ); err != nil { + return nil, err + } + + request.ZFSPool = m.ZFSPool.ValueStringPointer() + request.ThinProvision = proxmox_types.CustomBool(m.ThinProvision.ValueBool()) + request.Blocksize = m.Blocksize.ValueStringPointer() + + return request, nil +} + +// toUpdateAPIRequest converts the Terraform model to a Proxmox API request body for updates. +func (m *ZFSStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.ZFSStorageUpdateRequest{} + + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err + } + + request.ThinProvision = proxmox_types.CustomBool(m.ThinProvision.ValueBool()) + request.Blocksize = m.Blocksize.ValueStringPointer() + + return request, nil +} + +// fromAPI populates the Terraform model from a Proxmox API response. +func (m *ZFSStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + if err := m.populateBaseFromAPI(ctx, datastore); err != nil { + return err + } + + if datastore.ZFSPool != nil { + m.ZFSPool = types.StringValue(*datastore.ZFSPool) + } + + if datastore.ThinProvision != nil { + m.ThinProvision = types.BoolValue(*datastore.ThinProvision.PointerBool()) + } + + if datastore.Blocksize != nil { + m.Blocksize = types.StringValue(*datastore.Blocksize) + } + + return nil +} diff --git a/fwprovider/storage/resource_cifs.go b/fwprovider/storage/resource_cifs.go new file mode 100644 index 000000000..dbbe206b1 --- /dev/null +++ b/fwprovider/storage/resource_cifs.go @@ -0,0 +1,98 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &smbStorageResource{} + +// NewCIFSStorageResource is a helper function to simplify the provider implementation. +func NewCIFSStorageResource() resource.Resource { + return &smbStorageResource{ + storageResource: &storageResource[ + *CIFSStorageModel, + CIFSStorageModel, + ]{ + storageType: "smb", + resourceName: "proxmox_virtual_environment_storage_smb", + }, + } +} + +// smbStorageResource is the resource implementation. +type smbStorageResource struct { + *storageResource[*CIFSStorageModel, CIFSStorageModel] +} + +// Metadata returns the resource type name. +func (r *smbStorageResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.resourceName +} + +// Schema defines the schema for the SMB storage resource. +func (r *smbStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "server": schema.StringAttribute{ + Description: "The IP address or DNS name of the SMB/CIFS server.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "username": schema.StringAttribute{ + Description: "The username for authenticating with the SMB/CIFS server.", + Required: true, + }, + "password": schema.StringAttribute{ + Description: "The password for authenticating with the SMB/CIFS server.", + Required: true, + Sensitive: true, + }, + "share": schema.StringAttribute{ + Description: "The name of the SMB/CIFS share.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "domain": schema.StringAttribute{ + Description: "The SMB/CIFS domain.", + Optional: true, + }, + "subdirectory": schema.StringAttribute{ + Description: "A subdirectory to mount within the share.", + Optional: true, + }, + "preallocation": schema.StringAttribute{ + Description: "The preallocation mode for raw and qcow2 images.", + Optional: true, + }, + "snapshot_as_volume_chain": schema.BoolAttribute{ + Description: "Enable support for creating snapshots through volume backing-chains.", + Optional: true, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Computed: true, + Default: booldefault.StaticBool(true), + }, + } + + factory := NewStorageSchemaFactory() + factory.WithAttributes(attributes) + factory.WithDescription("Manages an SMB/CIFS based storage server in Proxmox VE.") + resp.Schema = *factory.Schema +} diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go new file mode 100644 index 000000000..aad1ce30e --- /dev/null +++ b/fwprovider/storage/resource_directory.go @@ -0,0 +1,92 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + "fmt" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/config" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &directoryStorageResource{} + +// NewDirectoryStorageResource is a helper function to simplify the provider implementation. +func NewDirectoryStorageResource() resource.Resource { + return &directoryStorageResource{ + storageResource: &storageResource[ + *DirectoryStorageModel, + DirectoryStorageModel, + ]{ + storageType: "dir", + resourceName: "proxmox_virtual_environment_storage_directory", + }, + } +} + +// directoryStorageResource is the resource implementation. +type directoryStorageResource struct { + *storageResource[*DirectoryStorageModel, DirectoryStorageModel] +} + +// Metadata returns the resource type name. +func (r *directoryStorageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.resourceName +} + +func (r *directoryStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "path": schema.StringAttribute{ + Description: "The path to the directory on the Proxmox node.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "preallocation": schema.StringAttribute{ + Description: "The preallocation mode for raw and qcow2 images.", + Optional: true, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Optional: true, + Default: booldefault.StaticBool(true), + Computed: true, + }, + } + + factory := NewStorageSchemaFactory() + factory.WithAttributes(attributes) + factory.WithDescription("Manages directory-based storage in Proxmox VE.") + factory.WithBackupBlock() + resp.Schema = *factory.Schema +} + +// Configure adds the provider configured client to the resource. +func (r *directoryStorageResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + cfg, ok := req.ProviderData.(config.Resource) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData), + ) + + return + } + + r.client = cfg.Client +} diff --git a/fwprovider/storage/resource_generic.go b/fwprovider/storage/resource_generic.go new file mode 100644 index 000000000..b4f9ffc1c --- /dev/null +++ b/fwprovider/storage/resource_generic.go @@ -0,0 +1,162 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + "fmt" + + "github.com/bpg/terraform-provider-proxmox/fwprovider/config" + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// storageModel is an interface that all storage resource models must implement. +// This allows a generic resource implementation to handle the CRUD operations. +type storageModel interface { + // GetID returns the storage identifier from the model. + GetID() types.String + + // toCreateAPIRequest converts the Terraform model to the specific API request body for creation. + toCreateAPIRequest(ctx context.Context) (interface{}, error) + + // toUpdateAPIRequest converts the Terraform model to the specific API request body for updates. + toUpdateAPIRequest(ctx context.Context) (interface{}, error) + + // fromAPI populates the model from the Proxmox API response. + fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error +} + +// storageResource is a generic implementation for all storage resources. +// It uses a generic type parameter 'T' which must be a pointer to a struct +// that implements the storageModel interface. +type storageResource[T interface { + *M + storageModel +}, M any] struct { + client proxmox.Client + storageType string + resourceName string +} + +// Configure is the generic configuration function. +func (r *storageResource[T, M]) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + cfg, ok := req.ProviderData.(config.Resource) + if !ok { + resp.Diagnostics.AddError("Unexpected Resource Configure Type", fmt.Sprintf("Expected config.Resource, got: %T", req.ProviderData)) + return + } + + r.client = cfg.Client +} + +// Create is the generic create function. +func (r *storageResource[T, M]) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan T = new(M) + + diags := req.Plan.Get(ctx, plan) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + requestBody, err := plan.toCreateAPIRequest(ctx) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error creating API request for %s storage", r.storageType), err.Error()) + return + } + + _, err = r.client.Storage().CreateDatastore(ctx, requestBody) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error creating %s storage", r.storageType), err.Error()) + return + } + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +// Read is the generic read function. +func (r *storageResource[T, M]) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state T = new(M) + + diags := req.State.Get(ctx, state) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + datastoreID := state.GetID().ValueString() + + datastore, err := r.client.Storage().GetDatastore(ctx, &storage.DatastoreGetRequest{ID: &datastoreID}) + if err != nil { + resp.State.RemoveResource(ctx) + return + } + + err = state.fromAPI(ctx, datastore) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error reading %s storage", r.storageType), err.Error()) + return + } + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} + +// Update is the generic update function. +func (r *storageResource[T, M]) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan T = new(M) + + diags := req.Plan.Get(ctx, plan) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + requestBody, err := plan.toUpdateAPIRequest(ctx) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error creating API request for %s storage", r.storageType), err.Error()) + return + } + + err = r.client.Storage().UpdateDatastore(ctx, plan.GetID().ValueString(), requestBody) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error updating %s storage", r.storageType), err.Error()) + return + } + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +// Delete is the generic delete function. +func (r *storageResource[T, M]) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state T = new(M) + + diags := req.State.Get(ctx, state) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + err := r.client.Storage().DeleteDatastore(ctx, state.GetID().ValueString()) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error deleting %s storage", r.storageType), err.Error()) + return + } +} diff --git a/fwprovider/storage/resource_lvm.go b/fwprovider/storage/resource_lvm.go new file mode 100644 index 000000000..b5d8497c2 --- /dev/null +++ b/fwprovider/storage/resource_lvm.go @@ -0,0 +1,72 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &lvmPoolStorageResource{} + +// NewLVMPoolStorageResource is a helper function to simplify the provider implementation. +func NewLVMPoolStorageResource() resource.Resource { + return &lvmPoolStorageResource{ + storageResource: &storageResource[ + *LVMStorageModel, + LVMStorageModel, + ]{ + storageType: "lvm", + resourceName: "proxmox_virtual_environment_storage_lvm", + }, + } +} + +// lvmPoolStorageResource is the resource implementation. +type lvmPoolStorageResource struct { + *storageResource[*LVMStorageModel, LVMStorageModel] +} + +// Metadata returns the resource type name. +func (r *lvmPoolStorageResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.resourceName +} + +// Schema defines the schema for the NFS storage resource. +func (r *lvmPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "volume_group": schema.StringAttribute{ + Description: "The name of the volume group to use.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "wipe_removed_volumes": schema.BoolAttribute{ + Description: "Whether to zero-out data when removing LVMs.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Optional: true, + Default: booldefault.StaticBool(false), + Computed: true, + }, + } + factory := NewStorageSchemaFactory() + factory.WithAttributes(attributes) + factory.WithDescription("Manages LVM-based storage in Proxmox VE.") + resp.Schema = *factory.Schema +} diff --git a/fwprovider/storage/resource_lvm_thin.go b/fwprovider/storage/resource_lvm_thin.go new file mode 100644 index 000000000..e5311dac3 --- /dev/null +++ b/fwprovider/storage/resource_lvm_thin.go @@ -0,0 +1,70 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &lvmThinPoolStorageResource{} + +// NewLVMThinPoolStorageResource is a helper function to simplify the provider implementation. +func NewLVMThinPoolStorageResource() resource.Resource { + return &lvmThinPoolStorageResource{ + storageResource: &storageResource[ + *LVMThinStorageModel, + LVMThinStorageModel, + ]{ + storageType: "lvmthin", + resourceName: "proxmox_virtual_environment_storage_lvmthin", + }, + } +} + +// lvmThinPoolStorageResource is the resource implementation. +type lvmThinPoolStorageResource struct { + *storageResource[*LVMThinStorageModel, LVMThinStorageModel] +} + +// Metadata returns the resource type name. +func (r *lvmThinPoolStorageResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.resourceName +} + +// Schema defines the schema for the NFS storage resource. +func (r *lvmThinPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "volume_group": schema.StringAttribute{ + Description: "The name of the volume group to use.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "thin_pool": schema.StringAttribute{ + Description: "The name of the LVM thin pool to use.", + Required: true, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Optional: true, + Default: booldefault.StaticBool(false), + Computed: true, + }, + } + factory := NewStorageSchemaFactory() + factory.WithAttributes(attributes) + factory.WithDescription("Manages thin LVM-based storage in Proxmox VE.") + resp.Schema = *factory.Schema +} diff --git a/fwprovider/storage/resource_nfs.go b/fwprovider/storage/resource_nfs.go new file mode 100644 index 000000000..a484ade57 --- /dev/null +++ b/fwprovider/storage/resource_nfs.go @@ -0,0 +1,92 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &nfsStorageResource{} + +// NewNFSStorageResource is a helper function to simplify the provider implementation. +func NewNFSStorageResource() resource.Resource { + return &nfsStorageResource{ + storageResource: &storageResource[ + *NFSStorageModel, + NFSStorageModel, + ]{ + storageType: "nfs", + resourceName: "proxmox_virtual_environment_storage_nfs", + }, + } +} + +// nfsStorageResource is the resource implementation. +type nfsStorageResource struct { + *storageResource[*NFSStorageModel, NFSStorageModel] +} + +// Metadata returns the resource type name. +func (r *nfsStorageResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.resourceName +} + +// Schema defines the schema for the NFS storage resource. +func (r *nfsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "server": schema.StringAttribute{ + Description: "The IP address or DNS name of the NFS server.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "export": schema.StringAttribute{ + Description: "The path of the NFS export.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "preallocation": schema.StringAttribute{ + Description: "The preallocation mode for raw and qcow2 images.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "options": schema.StringAttribute{ + Description: "The options to pass to the NFS service.", + Optional: true, + }, + "snapshot_as_volume_chain": schema.BoolAttribute{ + Description: "Enable support for creating snapshots through volume backing-chains.", + Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Computed: true, + Default: booldefault.StaticBool(true), + }, + } + + factory := NewStorageSchemaFactory() + factory.WithAttributes(attributes) + factory.WithDescription("Manages an NFS-based storage in Proxmox VE.") + resp.Schema = *factory.Schema +} diff --git a/fwprovider/storage/resource_pbs.go b/fwprovider/storage/resource_pbs.go new file mode 100644 index 000000000..cc6b47020 --- /dev/null +++ b/fwprovider/storage/resource_pbs.go @@ -0,0 +1,184 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &pbsStorageResource{} + +// NewProxmoxBackupServerStorageResource is a helper function to simplify the provider implementation. +func NewProxmoxBackupServerStorageResource() resource.Resource { + return &pbsStorageResource{ + storageResource: &storageResource[ + *PBSStorageModel, + PBSStorageModel, + ]{ + storageType: "pbs", + resourceName: "proxmox_virtual_environment_storage_pbs", + }, + } +} + +// pbsStorageResource is the resource implementation. +type pbsStorageResource struct { + *storageResource[*PBSStorageModel, PBSStorageModel] +} + +// Metadata returns the resource type name. +func (r *pbsStorageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.resourceName +} + +// Create is the generic create function. +func (r *pbsStorageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan PBSStorageModel + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + requestBody, err := plan.toCreateAPIRequest(ctx) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error creating API request for %s storage", r.storageType), err.Error()) + return + } + + responseData, err := r.client.Storage().CreateDatastore(ctx, requestBody) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error creating %s storage", r.storageType), err.Error()) + return + } + + plan.Shared = types.BoolValue(false) + + if !plan.GenerateEncryptionKey.IsNull() && plan.GenerateEncryptionKey.ValueBool() { + var encryptionKey storage.EncryptionKey + + err := json.Unmarshal([]byte(*responseData.Config.EncryptionKey), &encryptionKey) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error unmarshaling encryption key for %s storage", r.storageType), err.Error()) + return + } + + plan.GeneratedEncryptionKey = types.StringValue(*responseData.Config.EncryptionKey) + plan.EncryptionKeyFingerprint = types.StringValue(encryptionKey.Fingerprint) + } else { + plan.GeneratedEncryptionKey = types.StringNull() + } + + if !plan.EncryptionKey.IsNull() { + var encryptionKey storage.EncryptionKey + + err := json.Unmarshal([]byte(*responseData.Config.EncryptionKey), &encryptionKey) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Error unmarshaling encryption key for %s storage", r.storageType), err.Error()) + return + } + + plan.EncryptionKey = types.StringValue(*responseData.Config.EncryptionKey) + plan.EncryptionKeyFingerprint = types.StringValue(encryptionKey.Fingerprint) + } + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +// Schema defines the schema for the Proxmox Backup Server storage resource. +func (r *pbsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "server": schema.StringAttribute{ + Description: "The IP address or DNS name of the Proxmox Backup Server.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "datastore": schema.StringAttribute{ + Description: "The name of the datastore on the Proxmox Backup Server.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "username": schema.StringAttribute{ + Description: "The username for authenticating with the Proxmox Backup Server.", + Required: true, + }, + "password": schema.StringAttribute{ + Description: "The password for authenticating with the Proxmox Backup Server.", + Required: true, + Sensitive: true, + }, + "namespace": schema.StringAttribute{ + Description: "The namespace to use on the Proxmox Backup Server.", + Optional: true, + }, + "fingerprint": schema.StringAttribute{ + Description: "The SHA256 fingerprint of the Proxmox Backup Server's certificate.", + Optional: true, + }, + "encryption_key": schema.StringAttribute{ + Description: "An existing encryption key for the datastore. This is a sensitive value. Conflicts with `generate_encryption_key`.", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("generate_encryption_key")), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "encryption_key_fingerprint": schema.StringAttribute{ + Description: "The SHA256 fingerprint of the encryption key currently in use.", + Computed: true, + }, + "generate_encryption_key": schema.BoolAttribute{ + Description: "If set to true, Proxmox will generate a new encryption key. The key will be stored in the `generated_encryption_key` attribute. " + + "Conflicts with `encryption_key`.", + Optional: true, + Validators: []validator.Bool{ + boolvalidator.ConflictsWith(path.MatchRoot("encryption_key")), + }, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + "generated_encryption_key": schema.StringAttribute{ + Description: "The encryption key returned by Proxmox when `generate_encryption_key` is true.", + Computed: true, + Sensitive: true, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Computed: true, + }, + } + factory := NewStorageSchemaFactory() + factory.WithAttributes(attributes) + factory.WithDescription("Manages a Proxmox Backup Server (PBS) storage in Proxmox VE.") + resp.Schema = *factory.Schema +} diff --git a/fwprovider/storage/resource_zfs.go b/fwprovider/storage/resource_zfs.go new file mode 100644 index 000000000..65fab392d --- /dev/null +++ b/fwprovider/storage/resource_zfs.go @@ -0,0 +1,75 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ resource.Resource = &zfsPoolStorageResource{} + +// NewZFSPoolStorageResource is a helper function to simplify the provider implementation. +func NewZFSPoolStorageResource() resource.Resource { + return &zfsPoolStorageResource{ + storageResource: &storageResource[ + *ZFSStorageModel, + ZFSStorageModel, + ]{ + storageType: "zfspool", + resourceName: "proxmox_virtual_environment_storage_zfspool", + }, + } +} + +// zfsPoolStorageResource is the resource implementation. +type zfsPoolStorageResource struct { + *storageResource[*ZFSStorageModel, ZFSStorageModel] +} + +// Metadata returns the resource type name. +func (r *zfsPoolStorageResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = r.resourceName +} + +// Schema defines the schema for the NFS storage resource. +func (r *zfsPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + attributes := map[string]schema.Attribute{ + "zfs_pool": schema.StringAttribute{ + Description: "The name of the ZFS storage pool to use (e.g. `tank`, `rpool/data`).", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "thin_provision": schema.BoolAttribute{ + Description: "Whether to enable thin provisioning (`on` or `off`). Thin provisioning allows flexible disk allocation without " + + "pre-allocating full space.", + Optional: true, + }, + "blocksize": schema.StringAttribute{ + Description: "Block size for newly created volumes (e.g. `4k`, `8k`, `16k`). Larger values may improve throughput for large I/O, " + + "while smaller values optimize space efficiency.", + Optional: true, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Computed: true, + Default: booldefault.StaticBool(false), + }, + } + factory := NewStorageSchemaFactory() + factory.WithAttributes(attributes) + factory.WithDescription("Manages ZFS-based storage in Proxmox VE.") + resp.Schema = *factory.Schema +} diff --git a/fwprovider/storage/schema_factory.go b/fwprovider/storage/schema_factory.go new file mode 100644 index 000000000..3ae2b519a --- /dev/null +++ b/fwprovider/storage/schema_factory.go @@ -0,0 +1,149 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type StorageSchemaFactory struct { + Schema *schema.Schema +} + +func NewStorageSchemaFactory() *StorageSchemaFactory { + s := &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier of the storage.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "nodes": schema.SetAttribute{ + Description: "A list of nodes where this storage is available.", + ElementType: types.StringType, + Optional: true, + Computed: true, + Default: setdefault.StaticValue( + types.SetValueMust(types.StringType, []attr.Value{}), + ), + }, + "content": schema.SetAttribute{ + Description: "The content types that can be stored on this storage.", + ElementType: types.StringType, + Optional: true, + Computed: true, + Default: setdefault.StaticValue( + types.SetValueMust(types.StringType, []attr.Value{}), + ), + }, + "disable": schema.BoolAttribute{ + Description: "Whether the storage is disabled.", + Optional: true, + Default: booldefault.StaticBool(false), + Computed: true, + }, + }, + Blocks: map[string]schema.Block{}, + } + + return &StorageSchemaFactory{ + Schema: s, + } +} + +func (s *StorageSchemaFactory) WithDescription(description string) *StorageSchemaFactory { + s.Schema.Description = description + return s +} + +func (s *StorageSchemaFactory) WithAttributes(attributes map[string]schema.Attribute) *StorageSchemaFactory { + for k, v := range attributes { + s.Schema.Attributes[k] = v + } + + return s +} + +func (s *StorageSchemaFactory) WithBlocks(blocks map[string]schema.Block) *StorageSchemaFactory { + for k, v := range blocks { + s.Schema.Blocks[k] = v + } + + return s +} + +func (s *StorageSchemaFactory) WithBackupBlock() *StorageSchemaFactory { + return s.WithBlocks(map[string]schema.Block{ + "backups": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "max_protected_backups": schema.Int64Attribute{ + Description: "The maximum number of protected backups per guest. Use '-1' for unlimited.", + Optional: true, + }, + "keep_last": schema.Int64Attribute{ + Description: "Specifies the number of the most recent backups to keep, regardless of their age.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "keep_hourly": schema.Int64Attribute{ + Description: "The number of hourly backups to keep. Older backups will be removed.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "keep_daily": schema.Int64Attribute{ + Description: "The number of daily backups to keep. Older backups will be removed.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "keep_weekly": schema.Int64Attribute{ + Description: "The number of weekly backups to keep. Older backups will be removed.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "keep_monthly": schema.Int64Attribute{ + Description: "The number of monthly backups to keep. Older backups will be removed.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "keep_yearly": schema.Int64Attribute{ + Description: "The number of yearly backups to keep. Older backups will be removed.", + Optional: true, + Validators: []validator.Int64{ + int64validator.AtLeast(0), + }, + }, + "keep_all": schema.BoolAttribute{ + Description: "Specifies if all backups should be kept, regardless of their age.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + }, + Description: "Configure backup retention settings for the storage type.", + }, + }) +} diff --git a/main.go b/main.go index 720c18c68..28a776382 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,13 @@ import ( //go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_qinq.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_vxlan.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_sdn_zone_evpn.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_storage_directory.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_storage_lvmthin.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_storage_lvm.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_storage_nfs.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_storage_pbs.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_storage_smb.md ./docs/resources/ +//go:generate cp ./build/docs-gen/resources/virtual_environment_storage_zfspool.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_user_token.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_vm2.md ./docs/resources/ //go:generate cp ./build/docs-gen/resources/virtual_environment_metrics_server.md ./docs/resources/ diff --git a/proxmox/storage/cifs_types.go b/proxmox/storage/cifs_types.go new file mode 100644 index 000000000..f30ea6026 --- /dev/null +++ b/proxmox/storage/cifs_types.go @@ -0,0 +1,33 @@ +package storage + +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + +// CIFSStorageMutableFields defines specific options for 'smb'/'cifs' type storage. +type CIFSStorageMutableFields struct { + DataStoreCommonMutableFields + DataStoreWithBackups + + Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` +} + +type CIFSStorageImmutableFields struct { + Server *string `json:"server" url:"server"` + Username *string `json:"username" url:"username"` + Password *string `json:"password" url:"password"` + Share *string `json:"share" url:"share"` + Domain *string `json:"domain,omitempty" url:"domain,omitempty"` + Subdirectory *string `json:"subdir,omitempty" url:"subdir,omitempty"` + SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` +} + +// CIFSStorageCreateRequest defines the request body for creating a new SMB/CIFS storage. +type CIFSStorageCreateRequest struct { + DataStoreCommonImmutableFields + CIFSStorageMutableFields + CIFSStorageImmutableFields +} + +// CIFSStorageUpdateRequest defines the request body for updating an existing SMB/CIFS storage. +type CIFSStorageUpdateRequest struct { + CIFSStorageMutableFields +} diff --git a/proxmox/storage/client.go b/proxmox/storage/client.go index e66b25853..f87968a71 100644 --- a/proxmox/storage/client.go +++ b/proxmox/storage/client.go @@ -7,10 +7,36 @@ package storage import ( + "fmt" + "github.com/bpg/terraform-provider-proxmox/proxmox/api" + "github.com/bpg/terraform-provider-proxmox/proxmox/nodes/tasks" ) -// Client is an interface for accessing the Proxmox storage API. +// Client is an interface for accessing the Proxmox node storage API. type Client struct { api.Client + + StorageName string +} + +func (c *Client) basePath() string { + return c.Client.ExpandPath("storage") +} + +// ExpandPath expands a relative path to a full node storage API path. +func (c *Client) ExpandPath(path string) string { + ep := fmt.Sprintf("%s/%s", c.basePath(), c.StorageName) + if path != "" { + ep = fmt.Sprintf("%s/%s", ep, path) + } + + return ep +} + +// Tasks returns a client for managing node storage tasks. +func (c *Client) Tasks() *tasks.Client { + return &tasks.Client{ + Client: c.Client, + } } diff --git a/proxmox/storage/directory_types.go b/proxmox/storage/directory_types.go new file mode 100644 index 000000000..de89c5f0c --- /dev/null +++ b/proxmox/storage/directory_types.go @@ -0,0 +1,29 @@ +package storage + +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + +// DirectoryStorageMutableFields defines the mutable attributes for 'dir' type storage. +type DirectoryStorageMutableFields struct { + DataStoreCommonMutableFields + DataStoreWithBackups + + Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` + SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` + Shared *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"` +} + +// DirectoryStorageImmutableFields defines the immutable attributes for 'dir' type storage. +type DirectoryStorageImmutableFields struct { + Path *string `json:"path,omitempty" url:"path,omitempty"` +} + +// DirectoryStorageCreateRequest defines options for 'dir' type storage. +type DirectoryStorageCreateRequest struct { + DataStoreCommonImmutableFields + DirectoryStorageMutableFields + DirectoryStorageImmutableFields +} + +type DirectoryStorageUpdateRequest struct { + DirectoryStorageMutableFields +} diff --git a/proxmox/storage/lvm_thin_types.go b/proxmox/storage/lvm_thin_types.go new file mode 100644 index 000000000..03f3c396c --- /dev/null +++ b/proxmox/storage/lvm_thin_types.go @@ -0,0 +1,24 @@ +package storage + +// LVMThinStorageMutableFields defines options for 'lvmthin' type storage. +type LVMThinStorageMutableFields struct { + DataStoreCommonMutableFields +} + +// LVMThinStorageImmutableFields defines options for 'lvmthin' type storage. +type LVMThinStorageImmutableFields struct { + VolumeGroup *string `json:"vgname" url:"vgname"` + ThinPool *string `json:"thinpool,omitempty" url:"thinpool,omitempty"` +} + +// LVMThinStorageCreateRequest defines the request body for creating a new LVM thin storage. +type LVMThinStorageCreateRequest struct { + DataStoreCommonImmutableFields + LVMThinStorageMutableFields + LVMThinStorageImmutableFields +} + +// LVMThinStorageUpdateRequest defines the request body for updating an existing LVM thin storage. +type LVMThinStorageUpdateRequest struct { + LVMThinStorageMutableFields +} diff --git a/proxmox/storage/lvm_types.go b/proxmox/storage/lvm_types.go new file mode 100644 index 000000000..f403db855 --- /dev/null +++ b/proxmox/storage/lvm_types.go @@ -0,0 +1,27 @@ +package storage + +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + +// LVMStorageMutableFields defines options for 'lvm' type storage. +type LVMStorageMutableFields struct { + DataStoreCommonMutableFields + + WipeRemovedVolumes types.CustomBool `json:"saferemove" url:"saferemove,int"` +} + +// LVMStorageImmutableFields defines options for 'lvm' type storage. +type LVMStorageImmutableFields struct { + VolumeGroup *string `json:"vgname" url:"vgname"` +} + +// LVMStorageCreateRequest defines the request body for creating a new LVM storage. +type LVMStorageCreateRequest struct { + DataStoreCommonImmutableFields + LVMStorageMutableFields + LVMStorageImmutableFields +} + +// LVMStorageUpdateRequest defines the request body for updating an existing LVM storage. +type LVMStorageUpdateRequest struct { + LVMStorageMutableFields +} diff --git a/proxmox/storage/nfs_types.go b/proxmox/storage/nfs_types.go new file mode 100644 index 000000000..af8c9e13e --- /dev/null +++ b/proxmox/storage/nfs_types.go @@ -0,0 +1,31 @@ +package storage + +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + +// NFSStorageMutableFields defines the mutable attributes for 'nfs' type storage. +type NFSStorageMutableFields struct { + DataStoreCommonMutableFields + DataStoreWithBackups + + Options *string `json:"options,omitempty" url:"options,omitempty"` +} + +// NFSStorageImmutableFields defines the immutable attributes for 'nfs' type storage. +type NFSStorageImmutableFields struct { + Server *string `json:"server,omitempty" url:"server,omitempty"` + Export *string `json:"export,omitempty" url:"export,omitempty"` + Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` + SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` +} + +// NFSStorageCreateRequest defines the request body for creating a new NFS storage. +type NFSStorageCreateRequest struct { + DataStoreCommonImmutableFields + NFSStorageMutableFields + NFSStorageImmutableFields +} + +// NFSStorageUpdateRequest defines the request body for updating an existing NFS storage. +type NFSStorageUpdateRequest struct { + NFSStorageMutableFields +} diff --git a/proxmox/storage/pbs_types.go b/proxmox/storage/pbs_types.go new file mode 100644 index 000000000..4c1c8d042 --- /dev/null +++ b/proxmox/storage/pbs_types.go @@ -0,0 +1,78 @@ +package storage + +import "time" + +// PBSStorageMutableFields defines the mutable attributes for 'pbs' type storage. +type PBSStorageMutableFields struct { + DataStoreCommonMutableFields + DataStoreWithBackups + + Fingerprint *string `json:"fingerprint,omitempty" url:"fingerprint,omitempty"` + Encryption *string `json:"encryption-key,omitempty" url:"encryption-key,omitempty"` +} + +// PBSStorageImmutableFields defines the immutable attributes for 'pbs' type storage. +type PBSStorageImmutableFields struct { + Username *string `json:"username,omitempty" url:"username,omitempty"` + Password *string `json:"password,omitempty" url:"password,omitempty"` + Namespace *string `json:"namespace,omitempty" url:"namespace,omitempty"` + Server *string `json:"server,omitempty" url:"server,omitempty"` + Datastore *string `json:"datastore,omitempty" url:"datastore,omitempty"` +} + +// PBSStorageCreateRequest defines the request body for creating a new PBS storage. +type PBSStorageCreateRequest struct { + DataStoreCommonImmutableFields + PBSStorageMutableFields + PBSStorageImmutableFields +} + +// PBSStorageUpdateRequest defines the request body for updating an existing PBS storage. +type PBSStorageUpdateRequest struct { + PBSStorageMutableFields +} + +// EncryptionKey represents a Proxmox Backup Server encryption key object. +// Keys are stored as JSON and may include optional KDF (Key Derivation Function) +// parameters, creation/modification metadata, the key data itself, and its fingerprint. +// +// Example JSON: +// +// { +// "kdf": { "Scrypt": { "n": 32768, "r": 8, "p": 1, "salt": "..." } }, +// "created": "2025-08-18T15:04:05Z", +// "modified": "2025-08-18T15:04:05Z", +// "data": "base64-encoded-key", +// "fingerprint": "sha256:abcdef..." +// } +type EncryptionKey struct { + KDF *KDF `json:"kdf"` + Created time.Time `json:"created"` + Modified time.Time `json:"modified"` + Data string `json:"data"` + Fingerprint string `json:"fingerprint"` +} + +// KDF defines the Key Derivation Function configuration for an encryption key. +// Only one algorithm may be set at a time. If no KDF is used, this field is nil. +type KDF struct { + Scrypt *ScryptParams `json:"Scrypt,omitempty"` + PBKDF2 *PBKDF2Params `json:"PBKDF2,omitempty"` +} + +// ScryptParams defines parameters for the scrypt key derivation function. +// The values control CPU/memory cost (N), block size (r), parallelization (p), +// and the random salt used to derive the key. +type ScryptParams struct { + N int `json:"n"` + R int `json:"r"` + P int `json:"p"` + Salt string `json:"salt"` +} + +// PBKDF2Params defines parameters for the PBKDF2 key derivation function. +// It includes the iteration count (Iter) and the random salt value. +type PBKDF2Params struct { + Iter int `json:"iter"` + Salt string `json:"salt"` +} diff --git a/proxmox/storage/storage.go b/proxmox/storage/storage.go index c988c5930..d79875499 100644 --- a/proxmox/storage/storage.go +++ b/proxmox/storage/storage.go @@ -10,45 +10,97 @@ import ( "context" "fmt" "net/http" - "net/url" + "sort" + + "github.com/bpg/terraform-provider-proxmox/proxmox/api" ) -// GetDatastore retrieves information about a datastore. -/* -Using undocumented API endpoints is not recommended, but sometimes it's the only way to get things done. -$ pvesh get /storage/local -┌─────────┬───────────────────────────────────────────┐ -│ key │ value │ -╞═════════╪═══════════════════════════════════════════╡ -│ content │ images,vztmpl,iso,backup,snippets,rootdir │ -├─────────┼───────────────────────────────────────────┤ -│ digest │ 5b65ede80f34631d6039e6922845cfa4abc956be │ -├─────────┼───────────────────────────────────────────┤ -│ path │ /var/lib/vz │ -├─────────┼───────────────────────────────────────────┤ -│ shared │ 0 │ -├─────────┼───────────────────────────────────────────┤ -│ storage │ local │ -├─────────┼───────────────────────────────────────────┤ -│ type │ dir │ -└─────────┴───────────────────────────────────────────┘. -*/ -func (c *Client) GetDatastore( - ctx context.Context, - datastoreID string, -) (*DatastoreGetResponseData, error) { - resBody := &DatastoreGetResponseBody{} +// ListDatastore retrieves a list of the cluster. +func (c *Client) ListDatastore(ctx context.Context, d *DatastoreListRequest) ([]*DatastoreGetResponseData, error) { + resBody := &DatastoreListResponse{} + + err := c.DoRequest( + ctx, + http.MethodGet, + c.basePath(), + d, + resBody, + ) + if err != nil { + return nil, fmt.Errorf("error retrieving datastores: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + sort.Slice(resBody.Data, func(i, j int) bool { + return *(resBody.Data[i]).ID < *(resBody.Data[j]).ID + }) + + return resBody.Data, nil +} + +func (c *Client) GetDatastore(ctx context.Context, d *DatastoreGetRequest) (*DatastoreGetResponseData, error) { + resBody := &DatastoreGetResponse{} err := c.DoRequest( ctx, http.MethodGet, - fmt.Sprintf("storage/%s", url.PathEscape(datastoreID)), + c.ExpandPath(*d.ID), nil, resBody, ) if err != nil { - return nil, fmt.Errorf("error retrieving datastore %s: %w", datastoreID, err) + return nil, fmt.Errorf("error reading datastore: %w", err) } return resBody.Data, nil } + +func (c *Client) CreateDatastore(ctx context.Context, d interface{}) (*DatastoreCreateResponseData, error) { + resBody := &DatastoreCreateResponse{} + + err := c.DoRequest( + ctx, + http.MethodPost, + c.basePath(), + d, + resBody, + ) + if err != nil { + return nil, fmt.Errorf("error creating datastore: %w", err) + } + + return resBody.Data, nil +} + +func (c *Client) UpdateDatastore(ctx context.Context, storeID string, d interface{}) error { + err := c.DoRequest( + ctx, + http.MethodPut, + c.ExpandPath(storeID), + d, + nil, + ) + if err != nil { + return fmt.Errorf("error updating datastore: %w", err) + } + + return nil +} + +func (c *Client) DeleteDatastore(ctx context.Context, storeID string) error { + err := c.DoRequest( + ctx, + http.MethodDelete, + c.ExpandPath(storeID), + nil, + nil, + ) + if err != nil { + return fmt.Errorf("error deleting datastore: %w", err) + } + + return nil +} diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index bf2231727..700cfaf16 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -7,20 +7,144 @@ package storage import ( + "fmt" + "net/url" + "strconv" + "strings" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) -// DatastoreGetResponseBody contains the body from a datastore get response. -type DatastoreGetResponseBody struct { +type DatastoreGetRequest struct { + ID *string `json:"storage" url:"storage"` +} + +type DatastoreGetResponse struct { Data *DatastoreGetResponseData `json:"data,omitempty"` } -// DatastoreGetResponseData contains the data from a datastore get response. +// DatastoreListRequest contains the body for a datastore list request. +type DatastoreListRequest struct { + Type *string `json:"type,omitempty" url:"type,omitempty,omitempty"` +} + +// DatastoreListResponse contains the body from a datastore list response. +type DatastoreListResponse struct { + Data []*DatastoreGetResponseData `json:"data,omitempty"` +} + type DatastoreGetResponseData struct { - Content types.CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"` - Digest *string `json:"digest,omitempty"` - Path *string `json:"path,omitempty"` - Shared *types.CustomBool `json:"shared,omitempty"` - Storage *string `json:"storage,omitempty"` - Type *string `json:"type,omitempty"` + ID *string `json:"storage" url:"storage"` + Type *string `json:"type" url:"type"` + ContentTypes *types.CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"` + Path *string `json:"path,omitempty" url:"path,omitempty"` + Nodes *types.CustomCommaSeparatedList `json:"nodes,omitempty" url:"nodes,omitempty,comma"` + Disable *types.CustomBool `json:"disable,omitempty" url:"disable,omitempty,int"` + Shared *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"` + Server *string `json:"server,omitempty" url:"server,omitempty"` + Export *string `json:"export,omitempty" url:"export,omitempty"` + Options *string `json:"options,omitempty" url:"options,omitempty"` + Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` + Datastore *string `json:"datastore,omitempty" url:"datastore,omitempty"` + Username *string `json:"username,omitempty" url:"username,omitempty"` + Password *string `json:"password,omitempty" url:"password,omitempty"` + Namespace *string `json:"namespace,omitempty" url:"namespace,omitempty"` + Fingerprint *string `json:"fingerprint,omitempty" url:"fingerprint,omitempty"` + EncryptionKey *string `json:"keyring,omitempty" url:"keyring,omitempty"` + ZFSPool *string `json:"pool,omitempty" url:"pool,omitempty"` + ThinProvision *types.CustomBool `json:"sparse,omitempty" url:"sparse,omitempty,int"` + Blocksize *string `json:"blocksize,omitempty" url:"blocksize,omitempty"` + VolumeGroup *string `json:"vgname,omitempty" url:"vgname,omitempty"` + WipeRemovedVolumes *types.CustomBool `json:"saferemove,omitempty" url:"saferemove,omitempty,int"` + ThinPool *string `json:"thinpool,omitempty" url:"thinpool,omitempty"` + Share *string `json:"share,omitempty" url:"share,omitempty"` + Domain *string `json:"domain,omitempty" url:"domain,omitempty"` + SubDirectory *string `json:"subdir,omitempty" url:"subdir,omitempty"` +} + +type DatastoreCreateResponse struct { + Data *DatastoreCreateResponseData `json:"data,omitempty" url:"data,omitempty"` +} + +type DatastoreCreateResponseData struct { + Type *string `json:"type" url:"type"` + Storage *string `json:"storage,omitempty" url:"storage,omitempty"` + Config DatastoreCreateResponseConfigData `json:"config,omitempty" url:"config,omitempty"` +} + +type DatastoreCreateResponseConfigData struct { + EncryptionKey *string `json:"encryption-key,omitempty" url:"encryption-key,omitempty"` +} + +type DataStoreCommonImmutableFields struct { + ID *string `json:"storage" url:"storage"` + Type *string `json:"type,omitempty" url:"type,omitempty"` +} + +type DataStoreCommonMutableFields struct { + ContentTypes *types.CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"` + Nodes *types.CustomCommaSeparatedList `json:"nodes,omitempty" url:"nodes,omitempty,comma"` + Disable *types.CustomBool `json:"disable,omitempty" url:"disable,omitempty,int"` + Shared *bool `json:"shared,omitempty" url:"shared,omitempty,int"` +} + +// DataStoreWithBackups holds optional retention settings for backups. +type DataStoreWithBackups struct { + MaxProtectedBackups *types.CustomInt64 `json:"max-protected-backups,omitempty" url:"max,omitempty"` + KeepAll *types.CustomBool `json:"-" url:"-"` + KeepDaily *int `json:"-" url:"-"` + KeepHourly *int `json:"-" url:"-"` + KeepLast *int `json:"-" url:"-"` + KeepMonthly *int `json:"-" url:"-"` + KeepWeekly *int `json:"-" url:"-"` + KeepYearly *int `json:"-" url:"-"` +} + +// String serializes DataStoreWithBackups into the Proxmox "key=value,key=value" format. +// Only defined (non-nil) fields will be included. +func (b *DataStoreWithBackups) String() string { + var parts []string + + if b.KeepAll != nil { + return "keep-all=1" + } + + if b.KeepLast != nil { + parts = append(parts, fmt.Sprintf("keep-last=%d", *b.KeepLast)) + } + + if b.KeepHourly != nil { + parts = append(parts, fmt.Sprintf("keep-hourly=%d", *b.KeepHourly)) + } + + if b.KeepDaily != nil { + parts = append(parts, fmt.Sprintf("keep-daily=%d", *b.KeepDaily)) + } + + if b.KeepWeekly != nil { + parts = append(parts, fmt.Sprintf("keep-weekly=%d", *b.KeepWeekly)) + } + + if b.KeepMonthly != nil { + parts = append(parts, fmt.Sprintf("keep-monthly=%d", *b.KeepMonthly)) + } + + if b.KeepYearly != nil { + parts = append(parts, fmt.Sprintf("keep-yearly=%d", *b.KeepYearly)) + } + + return strings.Join(parts, ",") +} + +func (b *DataStoreWithBackups) EncodeValues(key string, v *url.Values) error { + if b.MaxProtectedBackups != nil { + v.Set("max-protected-backups", strconv.FormatInt(int64(*b.MaxProtectedBackups), 10)) + } + + backupString := b.String() + if backupString != "" { + v.Set("prune-backups", backupString) + } + + return nil } diff --git a/proxmox/storage/storage_types_test.go b/proxmox/storage/storage_types_test.go new file mode 100644 index 000000000..8fc375344 --- /dev/null +++ b/proxmox/storage/storage_types_test.go @@ -0,0 +1,105 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package storage + +import ( + "testing" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/stretchr/testify/assert" +) + +func intPtr(i int) *int { + return &i +} + +func customInt64Ptr(i int64) *types.CustomInt64 { + c := types.CustomInt64(i) + return &c +} + +// TestDataStoreWithBackups_String tests backup settings are encoded correctly into a string. +func TestDataStoreWithBackups_String(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input DataStoreWithBackups + expected string + }{ + { + name: "Empty struct", + input: DataStoreWithBackups{}, + expected: "", + }, + { + name: "KeepLast only", + input: DataStoreWithBackups{KeepLast: intPtr(5)}, + expected: "keep-last=5", + }, + { + name: "KeepHourly only", + input: DataStoreWithBackups{KeepHourly: intPtr(24)}, + expected: "keep-hourly=24", + }, + { + name: "KeepDaily only", + input: DataStoreWithBackups{KeepDaily: intPtr(7)}, + expected: "keep-daily=7", + }, + { + name: "KeepWeekly only", + input: DataStoreWithBackups{KeepWeekly: intPtr(4)}, + expected: "keep-weekly=4", + }, + { + name: "KeepMonthly only", + input: DataStoreWithBackups{KeepMonthly: intPtr(12)}, + expected: "keep-monthly=12", + }, + { + name: "KeepYearly only", + input: DataStoreWithBackups{KeepYearly: intPtr(3)}, + expected: "keep-yearly=3", + }, + { + name: "Multiple values", + input: DataStoreWithBackups{ + KeepDaily: intPtr(30), + KeepWeekly: intPtr(8), + KeepYearly: intPtr(10), + }, + expected: "keep-daily=30,keep-weekly=8,keep-yearly=10", + }, + { + name: "All values set", + input: DataStoreWithBackups{ + KeepLast: intPtr(1), + KeepHourly: intPtr(2), + KeepDaily: intPtr(3), + KeepWeekly: intPtr(4), + KeepMonthly: intPtr(5), + KeepYearly: intPtr(6), + }, + expected: "keep-last=1,keep-hourly=2,keep-daily=3,keep-weekly=4,keep-monthly=5,keep-yearly=6", + }, + { + name: "MaxProtectedBackups should be ignored", + input: DataStoreWithBackups{MaxProtectedBackups: customInt64Ptr(10)}, + expected: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + result := tc.input.String() + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/proxmox/storage/zfs_types.go b/proxmox/storage/zfs_types.go new file mode 100644 index 000000000..24165e435 --- /dev/null +++ b/proxmox/storage/zfs_types.go @@ -0,0 +1,27 @@ +package storage + +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + +// ZFSStorageMutableFields defines options for 'zfspool' type storage. +type ZFSStorageMutableFields struct { + DataStoreCommonMutableFields + + ThinProvision types.CustomBool `json:"sparse,omitempty" url:"sparse,omitempty,int"` + Blocksize *string `json:"blocksize,omitempty" url:"blocksize,omitempty"` +} + +type ZFSStorageImmutableFields struct { + ZFSPool *string `json:"pool" url:"pool"` +} + +// ZFSStorageCreateRequest defines the request body for creating a new ZFS storage. +type ZFSStorageCreateRequest struct { + DataStoreCommonImmutableFields + ZFSStorageMutableFields + ZFSStorageImmutableFields +} + +// ZFSStorageUpdateRequest defines the request body for updating an existing ZFS storage. +type ZFSStorageUpdateRequest struct { + ZFSStorageMutableFields +} diff --git a/proxmoxtf/resource/file.go b/proxmoxtf/resource/file.go index 4b5649e0a..c85894c21 100644 --- a/proxmoxtf/resource/file.go +++ b/proxmoxtf/resource/file.go @@ -23,6 +23,7 @@ import ( "strings" "time" + "github.com/bpg/terraform-provider-proxmox/proxmox/storage" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -566,7 +567,9 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag default: // For all other content types, we need to upload the file to the node's // datastore using SFTP. - datastore, err2 := capi.Storage().GetDatastore(ctx, datastoreID) + req := &storage.DatastoreGetRequest{ID: &datastoreID} + + datastore, err2 := capi.Storage().GetDatastore(ctx, req) if err2 != nil { return diag.Errorf("failed to get datastore: %s", err2) } @@ -575,15 +578,17 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag return diag.Errorf("failed to determine the datastore path") } - sort.Strings(datastore.Content) + contentTypes := []string(*datastore.ContentTypes) + + sort.Strings(contentTypes) - _, found := slices.BinarySearch(datastore.Content, *contentType) + _, found := slices.BinarySearch(contentTypes, *contentType) if !found { diags = append(diags, diag.Diagnostics{ diag.Diagnostic{ Severity: diag.Warning, Summary: fmt.Sprintf("the datastore %q does not support content type %q; supported content types are: %v", - *datastore.Storage, *contentType, datastore.Content, + *datastore.ID, *contentType, contentTypes, ), }, }...)