From e3304387bdb96e7c082db4004ee985f177adb80f Mon Sep 17 00:00:00 2001 From: James Neill Date: Sun, 17 Aug 2025 21:00:06 +0200 Subject: [PATCH 01/25] feat(core): add support for interacting with the `/storage` endpoint - implement interfaces for creating/updating each type of storage - common base fields are extracted and type embedded - as are the backup fields, as not all storage methods support backups Signed-off-by: James Neill --- proxmox/storage/client.go | 28 +++- proxmox/storage/storage.go | 103 ++++++++++---- proxmox/storage/storage_types.go | 237 +++++++++++++++++++++++++++++-- 3 files changed, 328 insertions(+), 40 deletions(-) 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/storage.go b/proxmox/storage/storage.go index c988c5930..8991b2ad0 100644 --- a/proxmox/storage/storage.go +++ b/proxmox/storage/storage.go @@ -10,45 +10,92 @@ 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( +// ListDatastores retrieves a list of the cluster. +func (c *Client) ListDatastores( ctx context.Context, - datastoreID string, -) (*DatastoreGetResponseData, error) { - resBody := &DatastoreGetResponseBody{} + d *DatastoreListRequestBody, +) ([]*DatastoreListResponseData, error) { + resBody := &DatastoreListResponseBody{} err := c.DoRequest( ctx, http.MethodGet, - fmt.Sprintf("storage/%s", url.PathEscape(datastoreID)), - nil, + c.basePath(), + d, resBody, ) if err != nil { - return nil, fmt.Errorf("error retrieving datastore %s: %w", datastoreID, err) + 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) CreateDatastore( + ctx context.Context, + d interface{}, +) error { + err := c.DoRequest( + ctx, + http.MethodPost, + c.basePath(), + d, + nil, + ) + if err != nil { + return fmt.Errorf("error creating datastore: %w", err) + } + + return nil +} + +func (c *Client) UpdateDatastore( + ctx context.Context, + d interface{}, +) error { + + err := c.DoRequest( + ctx, + http.MethodPost, + c.ExpandPath(d.(DataStoreBase).Storage), + d, + nil, + ) + if err != nil { + return fmt.Errorf("error updating datastore: %w", err) + } + + return nil +} + +func (c *Client) DeleteDatastore( + ctx context.Context, + d interface{}, +) error { + + err := c.DoRequest( + ctx, + http.MethodDelete, + c.ExpandPath(d.(DataStoreBase).Storage), + 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..0eafe8c09 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -7,20 +7,235 @@ package storage import ( + "encoding/json" + "fmt" + "strings" + "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) -// DatastoreGetResponseBody contains the body from a datastore get response. -type DatastoreGetResponseBody struct { - Data *DatastoreGetResponseData `json:"data,omitempty"` +// DatastoreListRequestBody contains the body for a datastore list request. +type DatastoreListRequestBody struct { + ContentTypes types.CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"` + Enabled *types.CustomBool `json:"enabled,omitempty" url:"enabled,omitempty,int"` + Format *types.CustomBool `json:"format,omitempty" url:"format,omitempty,int"` + ID *string `json:"storage,omitempty" url:"storage,omitempty"` + Target *string `json:"target,omitempty" url:"target,omitempty"` +} + +// DatastoreListResponseBody contains the body from a datastore list response. +type DatastoreListResponseBody struct { + Data []*DatastoreListResponseData `json:"data,omitempty"` +} + +// DatastoreListResponseData contains the data from a datastore list response. +type DatastoreListResponseData struct { + Active *types.CustomBool `json:"active,omitempty"` + ContentTypes *types.CustomCommaSeparatedList `json:"content,omitempty"` + Enabled *types.CustomBool `json:"enabled,omitempty"` + ID string `json:"storage,omitempty"` + Shared *types.CustomBool `json:"shared,omitempty"` + SpaceAvailable *types.CustomInt64 `json:"avail,omitempty"` + SpaceTotal *types.CustomInt64 `json:"total,omitempty"` + SpaceUsed *types.CustomInt64 `json:"used,omitempty"` + SpaceUsedPercentage *types.CustomFloat64 `json:"used_fraction,omitempty"` + Type string `json:"type,omitempty"` +} + +// DataStoreBase contains the common fields for all storage types. +type DataStoreBase struct { + Storage string `json:"storage"` + Nodes string `json:"nodes,omitempty"` + Content string `json:"content,omitempty"` + Enable bool `json:"enable,omitempty"` + Shared bool `json:"shared,omitempty"` +} + +// DataStoreWithBackups holds optional retention settings for backups. +type DataStoreWithBackups struct { + MaxProtectedBackups *types.CustomInt64 `json:"max-protected-backups,omitempty"` + KeepDaily *int `json:"-"` + KeepHourly *int `json:"-"` + KeepLast *int `json:"-"` + KeepMonthly *int `json:"-"` + KeepWeekly *int `json:"-"` + KeepYearly *int `json:"-"` +} + +// 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.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, ",") +} + +// MarshalJSON ensures DataStoreWithBackups is encoded into a JSON field "prune-backups". +func (b DataStoreWithBackups) MarshalJSON() ([]byte, error) { + str := b.String() + + // Special case; nothing defined so we omit the field + if str == "" && b.MaxProtectedBackups == nil { + return []byte(`{}`), nil + } + + type Alias DataStoreWithBackups + aux := struct { + *Alias + PruneBackups string `json:"prune-backups,omitempty"` + }{ + Alias: (*Alias)(&b), + PruneBackups: str, + } + return json.Marshal(aux) +} + +// DirectoryStorageRequestBody defines options for 'dir' type storage. +type DirectoryStorageRequestBody struct { + DataStoreBase + DataStoreWithBackups + Path string `json:"path"` + Preallocation string `json:"preallocation,omitempty"` + SnapshotsAsVolumeChain bool `json:"snapshot,omitempty"` +} + +// LVMStorageRequestBody defines options for 'lvm' type storage. +type LVMStorageRequestBody struct { + DataStoreBase + VolumeGroup string `json:"volume_group"` + WipeRemovedVolumes bool `json:"wipe_removed_volumes,omitempty"` +} + +// LVMThinStorageRequestBody defines options for 'lvmthin' type storage. +type LVMThinStorageRequestBody struct { + DataStoreBase + VolumeGroup string `json:"volume_group"` + ThinPool string `json:"thin_pool,omitempty"` +} + +// BTRFSStorageRequestBody defines options for 'btrfs' type storage. +type BTRFSStorageRequestBody struct { + DataStoreBase + DataStoreWithBackups + Path string `json:"path"` + Preallocation string `json:"preallocation,omitempty"` +} + +// NFSStorageRequestBody defines specific options for 'nfs' type storage. +type NFSStorageRequestBody struct { + DataStoreBase + Export string `json:"export"` + NFSVersion string `json:"nfs_version,omitempty"` + Server string `json:"server"` + Preallocation string `json:"preallocation,omitempty"` + SnapshotsAsVolumeChain bool `json:"snapshot-as-volume-chain,omitempty"` +} + +// SMBStorageRequestBody defines specific options for 'smb'/'cifs' type storage. +type SMBStorageRequestBody struct { + DataStoreBase + DataStoreWithBackups + Username string `json:"username"` + Password string `json:"password"` + Share string `json:"share"` + Domain string `json:"domain,omitempty"` + Subdirectory string `json:"subdirectory,omitempty"` + Server string `json:"server"` + Preallocation string `json:"preallocation,omitempty"` + SnapshotsAsVolumeChain bool `json:"snapshot-as-volume-chain,omitempty"` +} + +// ISCSIStorageRequestBody defines options for 'iscsi' type storage. +type ISCSIStorageRequestBody struct { + DataStoreBase + Portal string `json:"portal"` + Target string `json:"target"` + UseLUNsDirectly bool `json:"use_luns_directly,omitempty"` +} + +// CephFSStorageRequestBody defines options for 'cephfs' type storage. +type CephFSStorageRequestBody struct { + DataStoreBase + DataStoreWithBackups + Monitors string `json:"monhost"` + Username string `json:"username,omitempty"` + FSName string `json:"fs_name,omitempty"` + SecretKey string `json:"keyring,omitempty"` + Managed bool `json:"managed,omitempty"` +} + +// RBDStorageRequestBody defines options for 'rbd' type storage. +type RBDStorageRequestBody struct { + DataStoreBase + Pool string `json:"pool"` + Monitors string `json:"monhost"` + Username string `json:"username,omitempty"` + KRBD bool `json:"krbd,omitempty"` + SecretKey string `json:"keyring"` + Managed bool `json:"managed,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + +// ZFSStorageRequestBody defines options for 'zfs' type storage. +type ZFSStorageRequestBody struct { + DataStoreBase + ZFSPool string `json:"zfs_pool"` + ThinProvision bool `json:"thin_provision,omitempty"` + Blocksize string `json:"blocksize,omitempty"` +} + +// ZFSOverISCSIOptions defines options for 'zfs over iscsi' type storage. +type ZFSOverISCSIOptions struct { + DataStoreBase + Portal string `json:"portal"` + Pool string `json:"pool"` + Blocksize string `json:"blocksize,omitempty"` + Target string `json:"target"` + TargetGroup string `json:"target_group,omitempty"` + ISCSIProvider string `json:"iscsi_provider"` + ThinProvision bool `json:"thin_provision,omitempty"` + WriteCache bool `json:"write_cache,omitempty"` + HostGroup string `json:"host_group,omitempty"` + TargetPortalGroup string `json:"target_portal_group,omitempty"` +} + +// PBSStorageRequestBody defines options for 'pbs' (Proxmox Backup Server) type storage. +type PBSStorageRequestBody struct { + DataStoreBase + DataStoreWithBackups + Server string `json:"server"` + Username string `json:"username"` + Password string `json:"password"` + Datastore string `json:"datastore"` + Namespace string `json:"namespace,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + Encryption string `json:"encryption-key,omitempty"` } -// DatastoreGetResponseData contains the data from a datastore get response. -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"` +// ESXiStorageRequestBody defines options for 'esxi' type storage. +type ESXiStorageRequestBody struct { + DataStoreBase + Server string `json:"server"` + Username string `json:"username"` + Password string `json:"password"` + SkipCertVerification bool `json:"skip_cert_verification,omitempty"` } From 0a8067eadc73fd48fc47572b1fa62388000aa635 Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 17:35:48 +0200 Subject: [PATCH 02/25] feat(core): implement resource for creating directory storage types - still need to implement adding the backup definitions but the models are implemented - this breaks `proxmoxtf/resource/file.go` as it was using the wrong storage client (should've been the node specific storage endpoint) Signed-off-by: James Neill --- fwprovider/provider.go | 2 + fwprovider/storage/model_backups.go | 16 ++ fwprovider/storage/model_directory.go | 102 ++++++++++ fwprovider/storage/resource_directory.go | 226 +++++++++++++++++++++++ proxmox/storage/storage.go | 50 ++--- proxmox/storage/storage_types.go | 202 +++++--------------- 6 files changed, 420 insertions(+), 178 deletions(-) create mode 100644 fwprovider/storage/model_backups.go create mode 100644 fwprovider/storage/model_directory.go create mode 100644 fwprovider/storage/resource_directory.go diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 98faa4ebf..9752c2a5e 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -37,6 +37,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" @@ -533,6 +534,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc sdnzone.NewQinQResource, sdnzone.NewVXLANResource, sdnzone.NewEVPNResource, + storage.NewDirectoryStorageResource, } } diff --git a/fwprovider/storage/model_backups.go b/fwprovider/storage/model_backups.go new file mode 100644 index 000000000..bfe1ceeb6 --- /dev/null +++ b/fwprovider/storage/model_backups.go @@ -0,0 +1,16 @@ +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"` + 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_directory.go b/fwprovider/storage/model_directory.go new file mode 100644 index 000000000..911c89b02 --- /dev/null +++ b/fwprovider/storage/model_directory.go @@ -0,0 +1,102 @@ +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" +) + +// DirectoryStorageModel maps the Terraform schema for directory storage. +type DirectoryStorageModel struct { + ID types.String `tfsdk:"id" json:"storage"` + Type types.String `tfsdk:"type" json:"type"` + Path types.String `tfsdk:"path" json:"path"` + Nodes types.Set `tfsdk:"nodes" json:"nodes"` + ContentTypes types.Set `tfsdk:"content" json:"content"` + Disable types.Bool `tfsdk:"disable" json:"disable"` + Shared types.Bool `tfsdk:"shared" json:"shared"` + Preallocation types.String `tfsdk:"preallocation" json:"preallocation"` +} + +// toCreateAPIRequest converts the Terraform model to a Proxmox API request body. +func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (storage.DirectoryStorageCreateRequest, error) { + storageType := "dir" + request := storage.DirectoryStorageCreateRequest{} + + nodes := proxmox_types.CustomCommaSeparatedList{} + diags := m.Nodes.ElementsAs(ctx, &nodes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) + } + contentTypes := proxmox_types.CustomCommaSeparatedList{} + diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + } + + request.ID = m.ID.ValueStringPointer() + request.Type = &storageType + request.Nodes = &nodes + request.ContentTypes = &contentTypes + request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) + request.Shared = proxmox_types.CustomBoolPtr(m.Shared.ValueBoolPointer()) + request.Path = m.Path.ValueStringPointer() + + return request, nil +} + +func (m *DirectoryStorageModel) toUpdateAPIRequest(ctx context.Context) (storage.DirectoryStorageUpdateRequest, error) { + request := storage.DirectoryStorageUpdateRequest{} + + nodes := proxmox_types.CustomCommaSeparatedList{} + diags := m.Nodes.ElementsAs(ctx, &nodes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) + } + + contentTypes := proxmox_types.CustomCommaSeparatedList{} + diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + } + + request.ContentTypes = &contentTypes + request.Nodes = &nodes + request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) + request.Shared = proxmox_types.CustomBoolPtr(m.Shared.ValueBoolPointer()) + + return request, nil +} + +func (m *DirectoryStorageModel) importFromAPI(ctx context.Context, datastore storage.DatastoreGetResponseData) error { + m.ID = types.StringValue(*datastore.ID) + m.Type = types.StringValue(*datastore.Type) + 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 +} diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go new file mode 100644 index 000000000..4393e093f --- /dev/null +++ b/fwprovider/storage/resource_directory.go @@ -0,0 +1,226 @@ +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-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/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" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &directoryStorageResource{} + _ resource.ResourceWithConfigure = &directoryStorageResource{} +) + +var allowedStorageTypes = []string{ + "btrfs", "cephfs", "cifs", "dir", "esxi", "iscsi", "iscsidirect", + "lvm", "lvmthin", "nfs", "pbs", "rbd", "zfs", "zfspool", +} + +// NewDirectoryStorageResource is a helper function to simplify the provider implementation. +func NewDirectoryStorageResource() resource.Resource { + return &directoryStorageResource{} +} + +// directoryStorageResource is the resource implementation. +type directoryStorageResource struct { + client proxmox.Client +} + +func (r *directoryStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Manages a directory-based storage in Proxmox VE.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier of the storage.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "type": schema.StringAttribute{ + Description: "The type of storage to create.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(allowedStorageTypes...), + }, + }, + "path": schema.StringAttribute{ + Description: "The path to the directory on the Proxmox node.", + 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, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, + "preallocation": schema.StringAttribute{ + Description: "The preallocation mode for raw and qcow2 images.", + Optional: true, + }, + }, + } +} + +// Create creates the resource and sets the initial state. +func (r *directoryStorageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan DirectoryStorageModel + 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("Error creating create request for directory storage", err.Error()) + return + } + + err = r.client.Storage().CreateDatastore(ctx, &requestBody) + if err != nil { + resp.Diagnostics.AddError("Error creating directory storage", err.Error()) + return + } + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +// Read refreshes the resource state from the Proxmox API. +func (r *directoryStorageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state DirectoryStorageModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + requestBody := &storage.DatastoreGetRequest{ID: state.ID.ValueStringPointer()} + datastore, err := r.client.Storage().GetDatastore(ctx, requestBody) + + if err != nil { + resp.Diagnostics.AddError( + "Error Reading Proxmox Storage", + "Could not read storage ("+state.ID.ValueString()+"): "+err.Error(), + ) + return + } + + state.importFromAPI(ctx, *datastore) + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) +} + +// Update updates the resource and sets the new state. +func (r *directoryStorageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan DirectoryStorageModel + 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("Error creating update request for directory storage", err.Error()) + return + } + + err = r.client.Storage().UpdateDatastore(ctx, plan.ID.ValueString(), &requestBody) + if err != nil { + resp.Diagnostics.AddError("Error updating directory storage", err.Error()) + return + } + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) +} + +// Delete deletes the resource and removes it from the state. +func (r *directoryStorageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state DirectoryStorageModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.Storage().DeleteDatastore(ctx, state.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting directory storage", + "Could not delete directory storage, unexpected error: "+err.Error(), + ) + return + } +} + +// Metadata returns the resource type name. +func (r *directoryStorageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_storage_directory" +} + +// 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/proxmox/storage/storage.go b/proxmox/storage/storage.go index 8991b2ad0..b0e5754e0 100644 --- a/proxmox/storage/storage.go +++ b/proxmox/storage/storage.go @@ -15,12 +15,9 @@ import ( "github.com/bpg/terraform-provider-proxmox/proxmox/api" ) -// ListDatastores retrieves a list of the cluster. -func (c *Client) ListDatastores( - ctx context.Context, - d *DatastoreListRequestBody, -) ([]*DatastoreListResponseData, error) { - resBody := &DatastoreListResponseBody{} +// ListDatastore retrieves a list of the cluster. +func (c *Client) ListDatastore(ctx context.Context, d *DatastoreListRequest) ([]*DatastoreGetResponseData, error) { + resBody := &DatastoreListResponse{} err := c.DoRequest( ctx, @@ -38,16 +35,29 @@ func (c *Client) ListDatastores( } sort.Slice(resBody.Data, func(i, j int) bool { - return resBody.Data[i].ID < resBody.Data[j].ID + return *(resBody.Data[i]).ID < *(resBody.Data[j]).ID }) return resBody.Data, nil } -func (c *Client) CreateDatastore( - ctx context.Context, - d interface{}, -) error { +func (c *Client) GetDatastore(ctx context.Context, d *DatastoreGetRequest) (*DatastoreGetResponseData, error) { + resBody := &DatastoreGetResponseBody{} + err := c.DoRequest( + ctx, + http.MethodGet, + c.ExpandPath(*d.ID), + nil, + resBody, + ) + if err != nil { + return nil, fmt.Errorf("error reading datastore: %w", err) + } + + return resBody.Data, nil +} + +func (c *Client) CreateDatastore(ctx context.Context, d interface{}) error { err := c.DoRequest( ctx, http.MethodPost, @@ -62,15 +72,11 @@ func (c *Client) CreateDatastore( return nil } -func (c *Client) UpdateDatastore( - ctx context.Context, - d interface{}, -) error { - +func (c *Client) UpdateDatastore(ctx context.Context, storeID string, d interface{}) error { err := c.DoRequest( ctx, - http.MethodPost, - c.ExpandPath(d.(DataStoreBase).Storage), + http.MethodPut, + c.ExpandPath(storeID), d, nil, ) @@ -81,15 +87,11 @@ func (c *Client) UpdateDatastore( return nil } -func (c *Client) DeleteDatastore( - ctx context.Context, - d interface{}, -) error { - +func (c *Client) DeleteDatastore(ctx context.Context, storeID string) error { err := c.DoRequest( ctx, http.MethodDelete, - c.ExpandPath(d.(DataStoreBase).Storage), + c.ExpandPath(storeID), nil, nil, ) diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 0eafe8c09..29da8e805 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -14,46 +14,48 @@ import ( "github.com/bpg/terraform-provider-proxmox/proxmox/types" ) -// DatastoreListRequestBody contains the body for a datastore list request. -type DatastoreListRequestBody struct { - ContentTypes types.CustomCommaSeparatedList `json:"content,omitempty" url:"content,omitempty,comma"` - Enabled *types.CustomBool `json:"enabled,omitempty" url:"enabled,omitempty,int"` - Format *types.CustomBool `json:"format,omitempty" url:"format,omitempty,int"` - ID *string `json:"storage,omitempty" url:"storage,omitempty"` - Target *string `json:"target,omitempty" url:"target,omitempty"` +type DatastoreGetRequest struct { + ID *string `json:"storage" url:"storage"` } -// DatastoreListResponseBody contains the body from a datastore list response. -type DatastoreListResponseBody struct { - Data []*DatastoreListResponseData `json:"data,omitempty"` +type DatastoreGetResponseBody struct { + Data *DatastoreGetResponseData `json:"data,omitempty"` } -// DatastoreListResponseData contains the data from a datastore list response. -type DatastoreListResponseData struct { - Active *types.CustomBool `json:"active,omitempty"` - ContentTypes *types.CustomCommaSeparatedList `json:"content,omitempty"` - Enabled *types.CustomBool `json:"enabled,omitempty"` - ID string `json:"storage,omitempty"` - Shared *types.CustomBool `json:"shared,omitempty"` - SpaceAvailable *types.CustomInt64 `json:"avail,omitempty"` - SpaceTotal *types.CustomInt64 `json:"total,omitempty"` - SpaceUsed *types.CustomInt64 `json:"used,omitempty"` - SpaceUsedPercentage *types.CustomFloat64 `json:"used_fraction,omitempty"` - Type string `json:"type,omitempty"` +// DatastoreListRequest contains the body for a datastore list request. +type DatastoreListRequest struct { + Type *string `json:"type,omitempty" url:"type,omitempty,omitempty"` } -// DataStoreBase contains the common fields for all storage types. -type DataStoreBase struct { - Storage string `json:"storage"` - Nodes string `json:"nodes,omitempty"` - Content string `json:"content,omitempty"` - Enable bool `json:"enable,omitempty"` - Shared bool `json:"shared,omitempty"` +// DatastoreListResponse contains the body from a datastore list response. +type DatastoreListResponse struct { + Data []*DatastoreGetResponseData `json:"data,omitempty"` +} + +type DatastoreGetResponseData struct { + ID *string `json:"storage" url:"storage"` + Type *string `json:"type" url:"type"` + 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 *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"` +} + +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 *types.CustomBool `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"` + MaxProtectedBackups *types.CustomInt64 `json:"max-protected-backups,omitempty" url:"max,omitempty"` KeepDaily *int `json:"-"` KeepHourly *int `json:"-"` KeepLast *int `json:"-"` @@ -101,7 +103,7 @@ func (b DataStoreWithBackups) MarshalJSON() ([]byte, error) { type Alias DataStoreWithBackups aux := struct { *Alias - PruneBackups string `json:"prune-backups,omitempty"` + PruneBackups string `json:"prune-backups,omitempty" url:"prune,omitempty"` }{ Alias: (*Alias)(&b), PruneBackups: str, @@ -109,133 +111,25 @@ func (b DataStoreWithBackups) MarshalJSON() ([]byte, error) { return json.Marshal(aux) } -// DirectoryStorageRequestBody defines options for 'dir' type storage. -type DirectoryStorageRequestBody struct { - DataStoreBase - DataStoreWithBackups - Path string `json:"path"` - Preallocation string `json:"preallocation,omitempty"` - SnapshotsAsVolumeChain bool `json:"snapshot,omitempty"` -} - -// LVMStorageRequestBody defines options for 'lvm' type storage. -type LVMStorageRequestBody struct { - DataStoreBase - VolumeGroup string `json:"volume_group"` - WipeRemovedVolumes bool `json:"wipe_removed_volumes,omitempty"` -} - -// LVMThinStorageRequestBody defines options for 'lvmthin' type storage. -type LVMThinStorageRequestBody struct { - DataStoreBase - VolumeGroup string `json:"volume_group"` - ThinPool string `json:"thin_pool,omitempty"` -} - -// BTRFSStorageRequestBody defines options for 'btrfs' type storage. -type BTRFSStorageRequestBody struct { - DataStoreBase - DataStoreWithBackups - Path string `json:"path"` - Preallocation string `json:"preallocation,omitempty"` -} - -// NFSStorageRequestBody defines specific options for 'nfs' type storage. -type NFSStorageRequestBody struct { - DataStoreBase - Export string `json:"export"` - NFSVersion string `json:"nfs_version,omitempty"` - Server string `json:"server"` - Preallocation string `json:"preallocation,omitempty"` - SnapshotsAsVolumeChain bool `json:"snapshot-as-volume-chain,omitempty"` -} - -// SMBStorageRequestBody defines specific options for 'smb'/'cifs' type storage. -type SMBStorageRequestBody struct { - DataStoreBase - DataStoreWithBackups - Username string `json:"username"` - Password string `json:"password"` - Share string `json:"share"` - Domain string `json:"domain,omitempty"` - Subdirectory string `json:"subdirectory,omitempty"` - Server string `json:"server"` - Preallocation string `json:"preallocation,omitempty"` - SnapshotsAsVolumeChain bool `json:"snapshot-as-volume-chain,omitempty"` -} - -// ISCSIStorageRequestBody defines options for 'iscsi' type storage. -type ISCSIStorageRequestBody struct { - DataStoreBase - Portal string `json:"portal"` - Target string `json:"target"` - UseLUNsDirectly bool `json:"use_luns_directly,omitempty"` -} - -// CephFSStorageRequestBody defines options for 'cephfs' type storage. -type CephFSStorageRequestBody struct { - DataStoreBase - DataStoreWithBackups - Monitors string `json:"monhost"` - Username string `json:"username,omitempty"` - FSName string `json:"fs_name,omitempty"` - SecretKey string `json:"keyring,omitempty"` - Managed bool `json:"managed,omitempty"` -} - -// RBDStorageRequestBody defines options for 'rbd' type storage. -type RBDStorageRequestBody struct { - DataStoreBase - Pool string `json:"pool"` - Monitors string `json:"monhost"` - Username string `json:"username,omitempty"` - KRBD bool `json:"krbd,omitempty"` - SecretKey string `json:"keyring"` - Managed bool `json:"managed,omitempty"` - Namespace string `json:"namespace,omitempty"` -} - -// ZFSStorageRequestBody defines options for 'zfs' type storage. -type ZFSStorageRequestBody struct { - DataStoreBase - ZFSPool string `json:"zfs_pool"` - ThinProvision bool `json:"thin_provision,omitempty"` - Blocksize string `json:"blocksize,omitempty"` +// DirectoryStorageMutableFields defines the mutable attributes for 'dir' type storage. +type DirectoryStorageMutableFields struct { + DataStoreCommonMutableFields + Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` + SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` } -// ZFSOverISCSIOptions defines options for 'zfs over iscsi' type storage. -type ZFSOverISCSIOptions struct { - DataStoreBase - Portal string `json:"portal"` - Pool string `json:"pool"` - Blocksize string `json:"blocksize,omitempty"` - Target string `json:"target"` - TargetGroup string `json:"target_group,omitempty"` - ISCSIProvider string `json:"iscsi_provider"` - ThinProvision bool `json:"thin_provision,omitempty"` - WriteCache bool `json:"write_cache,omitempty"` - HostGroup string `json:"host_group,omitempty"` - TargetPortalGroup string `json:"target_portal_group,omitempty"` +// DirectoryStorageImmutableFields defines the immutable attributes for 'dir' type storage. +type DirectoryStorageImmutableFields struct { + Path *string `json:"path,omitempty" url:"path,omitempty"` } -// PBSStorageRequestBody defines options for 'pbs' (Proxmox Backup Server) type storage. -type PBSStorageRequestBody struct { - DataStoreBase - DataStoreWithBackups - Server string `json:"server"` - Username string `json:"username"` - Password string `json:"password"` - Datastore string `json:"datastore"` - Namespace string `json:"namespace,omitempty"` - Fingerprint string `json:"fingerprint,omitempty"` - Encryption string `json:"encryption-key,omitempty"` +// DirectoryStorageCreateRequest defines options for 'dir' type storage. +type DirectoryStorageCreateRequest struct { + DataStoreCommonImmutableFields + DirectoryStorageMutableFields + DirectoryStorageImmutableFields } -// ESXiStorageRequestBody defines options for 'esxi' type storage. -type ESXiStorageRequestBody struct { - DataStoreBase - Server string `json:"server"` - Username string `json:"username"` - Password string `json:"password"` - SkipCertVerification bool `json:"skip_cert_verification,omitempty"` +type DirectoryStorageUpdateRequest struct { + DirectoryStorageMutableFields } From 5314b343cc39cfbcd630e3b71e9f5ed11949dc96 Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 17:47:13 +0200 Subject: [PATCH 03/25] fix(core): file upload to use new storage structs & interface Signed-off-by: James Neill --- proxmox/storage/storage_types.go | 1 + proxmoxtf/resource/file.go | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 29da8e805..48c724eac 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -36,6 +36,7 @@ type DatastoreGetResponseData struct { 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"` diff --git a/proxmoxtf/resource/file.go b/proxmoxtf/resource/file.go index 4b5649e0a..488357ac7 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,8 @@ 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 +577,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) - _, found := slices.BinarySearch(datastore.Content, *contentType) + sort.Strings(contentTypes) + + _, 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, ), }, }...) From 14d6014eb7a29e6b63a360819344a9ba278f638e Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 18:15:13 +0200 Subject: [PATCH 04/25] refactor(storage): introduce schema factory for adding new storage resources Signed-off-by: James Neill --- fwprovider/storage/resource_directory.go | 79 ++++-------------------- fwprovider/storage/schema_factory.go | 65 +++++++++++++++++++ 2 files changed, 78 insertions(+), 66 deletions(-) create mode 100644 fwprovider/storage/schema_factory.go diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go index 4393e093f..68b903b88 100644 --- a/fwprovider/storage/resource_directory.go +++ b/fwprovider/storage/resource_directory.go @@ -7,16 +7,10 @@ import ( "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-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" "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/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" ) // Ensure the implementation satisfies the expected interfaces. @@ -41,69 +35,22 @@ type directoryStorageResource struct { } func (r *directoryStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Manages a directory-based storage in Proxmox VE.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "The unique identifier of the storage.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "type": schema.StringAttribute{ - Description: "The type of storage to create.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.OneOf(allowedStorageTypes...), - }, - }, - "path": schema.StringAttribute{ - Description: "The path to the directory on the Proxmox node.", - 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, - }, - "shared": schema.BoolAttribute{ - Description: "Whether the storage is shared across all nodes.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - "preallocation": schema.StringAttribute{ - Description: "The preallocation mode for raw and qcow2 images.", - Optional: true, + specificAttributes := 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, + }, } + + resp.Schema = storageSchemaFactory(specificAttributes) + resp.Schema.Description = "Manages a directory-based storage in Proxmox VE." } // Create creates the resource and sets the initial state. diff --git a/fwprovider/storage/schema_factory.go b/fwprovider/storage/schema_factory.go new file mode 100644 index 000000000..d44622d38 --- /dev/null +++ b/fwprovider/storage/schema_factory.go @@ -0,0 +1,65 @@ +package storage + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// storageSchemaFactory generates the schema for a storage resource. +func storageSchemaFactory(specificAttributes map[string]schema.Attribute) schema.Schema { + attributes := map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The unique identifier of the storage.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "type": schema.StringAttribute{ + Description: "The type of storage to create.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(allowedStorageTypes...), + }, + }, + "nodes": schema.SetAttribute{ + Description: "A list of nodes where this storage is available.", + ElementType: types.StringType, + Optional: true, + }, + "content": schema.SetAttribute{ + Description: "The content types that can be stored on this storage.", + ElementType: types.StringType, + Required: true, + }, + "disable": schema.BoolAttribute{ + Description: "Whether the storage is disabled.", + Optional: true, + Default: booldefault.StaticBool(false), + Computed: true, + }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Optional: true, + Default: booldefault.StaticBool(false), + Computed: true, + }, + } + + // Merge provided attributes for the given storage type + for k, v := range specificAttributes { + attributes[k] = v + } + + return schema.Schema{ + Attributes: attributes, + } +} From 71ada4bfe7a445e45bd807298beaced9a002b953 Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 18:21:52 +0200 Subject: [PATCH 05/25] refactor(storage): split out types into base & storage specific files Signed-off-by: James Neill --- proxmox/storage/directory_types.go | 26 ++++++++++++++++++++++++++ proxmox/storage/storage_types.go | 23 ----------------------- 2 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 proxmox/storage/directory_types.go diff --git a/proxmox/storage/directory_types.go b/proxmox/storage/directory_types.go new file mode 100644 index 000000000..112cf02d3 --- /dev/null +++ b/proxmox/storage/directory_types.go @@ -0,0 +1,26 @@ +package storage + +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + +// DirectoryStorageMutableFields defines the mutable attributes for 'dir' type storage. +type DirectoryStorageMutableFields struct { + DataStoreCommonMutableFields + Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` + SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` +} + +// 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/storage_types.go b/proxmox/storage/storage_types.go index 48c724eac..629f1acfa 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -111,26 +111,3 @@ func (b DataStoreWithBackups) MarshalJSON() ([]byte, error) { } return json.Marshal(aux) } - -// DirectoryStorageMutableFields defines the mutable attributes for 'dir' type storage. -type DirectoryStorageMutableFields struct { - DataStoreCommonMutableFields - Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` - SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` -} - -// 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 -} From 599112ab0cd862e7f58604cb9f8c74b492cafea7 Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 19:25:59 +0200 Subject: [PATCH 06/25] refactor(storage): remove type from schema as it's implied by each resource type Signed-off-by: James Neill --- fwprovider/storage/model_directory.go | 9 ++++++--- fwprovider/storage/schema_factory.go | 11 ----------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/fwprovider/storage/model_directory.go b/fwprovider/storage/model_directory.go index 911c89b02..b0cda4567 100644 --- a/fwprovider/storage/model_directory.go +++ b/fwprovider/storage/model_directory.go @@ -22,9 +22,12 @@ type DirectoryStorageModel struct { Preallocation types.String `tfsdk:"preallocation" json:"preallocation"` } +func (m *DirectoryStorageModel) GetStorageType() types.String { + return types.StringValue("dir") +} + // toCreateAPIRequest converts the Terraform model to a Proxmox API request body. func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (storage.DirectoryStorageCreateRequest, error) { - storageType := "dir" request := storage.DirectoryStorageCreateRequest{} nodes := proxmox_types.CustomCommaSeparatedList{} @@ -39,7 +42,7 @@ func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (storage } request.ID = m.ID.ValueStringPointer() - request.Type = &storageType + request.Type = m.GetStorageType().ValueStringPointer() request.Nodes = &nodes request.ContentTypes = &contentTypes request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) @@ -74,7 +77,7 @@ func (m *DirectoryStorageModel) toUpdateAPIRequest(ctx context.Context) (storage func (m *DirectoryStorageModel) importFromAPI(ctx context.Context, datastore storage.DatastoreGetResponseData) error { m.ID = types.StringValue(*datastore.ID) - m.Type = types.StringValue(*datastore.Type) + m.Type = m.GetStorageType() if datastore.Nodes != nil { nodes, diags := types.SetValueFrom(ctx, types.StringType, *datastore.Nodes) if diags.HasError() { diff --git a/fwprovider/storage/schema_factory.go b/fwprovider/storage/schema_factory.go index d44622d38..4ee8f9256 100644 --- a/fwprovider/storage/schema_factory.go +++ b/fwprovider/storage/schema_factory.go @@ -6,7 +6,6 @@ import ( "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" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -20,16 +19,6 @@ func storageSchemaFactory(specificAttributes map[string]schema.Attribute) schema stringplanmodifier.RequiresReplace(), }, }, - "type": schema.StringAttribute{ - Description: "The type of storage to create.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.OneOf(allowedStorageTypes...), - }, - }, "nodes": schema.SetAttribute{ Description: "A list of nodes where this storage is available.", ElementType: types.StringType, From 6b158efa60db708b58e5671cb16d41a271738faf Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 19:26:41 +0200 Subject: [PATCH 07/25] fix(storage): defaults for nodes and content fields after move Signed-off-by: James Neill --- fwprovider/storage/schema_factory.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/fwprovider/storage/schema_factory.go b/fwprovider/storage/schema_factory.go index 4ee8f9256..b567b4418 100644 --- a/fwprovider/storage/schema_factory.go +++ b/fwprovider/storage/schema_factory.go @@ -1,10 +1,11 @@ package storage import ( - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/types" ) @@ -23,20 +24,22 @@ func storageSchemaFactory(specificAttributes map[string]schema.Attribute) schema 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, - Required: true, - }, - "disable": schema.BoolAttribute{ - Description: "Whether the storage is disabled.", Optional: true, - Default: booldefault.StaticBool(false), Computed: true, + Default: setdefault.StaticValue( + types.SetValueMust(types.StringType, []attr.Value{}), + ), }, - "shared": schema.BoolAttribute{ - Description: "Whether the storage is shared across all nodes.", + "disable": schema.BoolAttribute{ + Description: "Whether the storage is disabled.", Optional: true, Default: booldefault.StaticBool(false), Computed: true, From 7bd1ffa77733d9d6aac40618db871a18ced1562b Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 19:27:57 +0200 Subject: [PATCH 08/25] fix(storage): make shared no longer a common immutable field, not supported by nfs Signed-off-by: James Neill --- fwprovider/storage/model_directory.go | 3 +++ fwprovider/storage/resource_directory.go | 7 +++++++ proxmox/storage/directory_types.go | 5 +++-- proxmox/storage/storage_types.go | 1 - 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/fwprovider/storage/model_directory.go b/fwprovider/storage/model_directory.go index b0cda4567..d718554cb 100644 --- a/fwprovider/storage/model_directory.go +++ b/fwprovider/storage/model_directory.go @@ -100,6 +100,9 @@ func (m *DirectoryStorageModel) importFromAPI(ctx context.Context, datastore sto if datastore.Shared != nil { m.Shared = datastore.Shared.ToValue() } + if datastore.Path != nil { + m.Path = types.StringValue(*datastore.Path) + } return nil } diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go index 68b903b88..0a6a61ebd 100644 --- a/fwprovider/storage/resource_directory.go +++ b/fwprovider/storage/resource_directory.go @@ -9,6 +9,7 @@ import ( "github.com/bpg/terraform-provider-proxmox/proxmox/storage" "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" ) @@ -47,6 +48,12 @@ func (r *directoryStorageResource) Schema(_ context.Context, _ resource.SchemaRe 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, + }, } resp.Schema = storageSchemaFactory(specificAttributes) diff --git a/proxmox/storage/directory_types.go b/proxmox/storage/directory_types.go index 112cf02d3..abe00b301 100644 --- a/proxmox/storage/directory_types.go +++ b/proxmox/storage/directory_types.go @@ -5,8 +5,9 @@ import "github.com/bpg/terraform-provider-proxmox/proxmox/types" // DirectoryStorageMutableFields defines the mutable attributes for 'dir' type storage. type DirectoryStorageMutableFields struct { DataStoreCommonMutableFields - Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` - SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` + 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. diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 629f1acfa..264f952f3 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -51,7 +51,6 @@ 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 *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"` } // DataStoreWithBackups holds optional retention settings for backups. From f15fd7d2eab05e36c6fb53faed5e72746824aa75 Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 19:28:18 +0200 Subject: [PATCH 09/25] refactor(storage): remove unused supported type variable Signed-off-by: James Neill --- fwprovider/storage/resource_directory.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go index 0a6a61ebd..c564f152d 100644 --- a/fwprovider/storage/resource_directory.go +++ b/fwprovider/storage/resource_directory.go @@ -20,11 +20,6 @@ var ( _ resource.ResourceWithConfigure = &directoryStorageResource{} ) -var allowedStorageTypes = []string{ - "btrfs", "cephfs", "cifs", "dir", "esxi", "iscsi", "iscsidirect", - "lvm", "lvmthin", "nfs", "pbs", "rbd", "zfs", "zfspool", -} - // NewDirectoryStorageResource is a helper function to simplify the provider implementation. func NewDirectoryStorageResource() resource.Resource { return &directoryStorageResource{} From 261c4dcd67eb40af1966603194aea5ef490086fe Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 19:29:42 +0200 Subject: [PATCH 10/25] feat(storage): implement generic resource and add NFS support Signed-off-by: James Neill --- fwprovider/provider.go | 1 + fwprovider/storage/model_nfs.go | 119 +++++++++++++++++++++ fwprovider/storage/resource_generic.go | 140 +++++++++++++++++++++++++ fwprovider/storage/resource_nfs.go | 70 +++++++++++++ proxmox/storage/nfs_types.go | 29 +++++ proxmox/storage/storage_types.go | 3 + 6 files changed, 362 insertions(+) create mode 100644 fwprovider/storage/model_nfs.go create mode 100644 fwprovider/storage/resource_generic.go create mode 100644 fwprovider/storage/resource_nfs.go create mode 100644 proxmox/storage/nfs_types.go diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 9752c2a5e..9151b543b 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -535,6 +535,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc sdnzone.NewVXLANResource, sdnzone.NewEVPNResource, storage.NewDirectoryStorageResource, + storage.NewNFSStorageResource, } } diff --git a/fwprovider/storage/model_nfs.go b/fwprovider/storage/model_nfs.go new file mode 100644 index 000000000..f744f5641 --- /dev/null +++ b/fwprovider/storage/model_nfs.go @@ -0,0 +1,119 @@ +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" +) + +// NFSStorageModel maps the Terraform schema for NFS storage. +type NFSStorageModel struct { + ID types.String `tfsdk:"id" json:"storage"` + Type types.String `tfsdk:"type" json:"type"` + Nodes types.Set `tfsdk:"nodes" json:"nodes"` + ContentTypes types.Set `tfsdk:"content" json:"content"` + Disable types.Bool `tfsdk:"disable" json:"disable"` + Server types.String `tfsdk:"server" json:"server"` + Export types.String `tfsdk:"export" json:"export"` + Options types.String `tfsdk:"options" json:"options"` + Preallocation types.String `tfsdk:"preallocation" json:"preallocation"` + SnapshotsAsVolumeChain types.Bool `tfsdk:"snapshot_as_volume_chain" json:"snapshot-as-volume-chain"` +} + +func (m *NFSStorageModel) GetID() types.String { + return m.ID +} + +func (m *NFSStorageModel) GetStorageType() types.String { + return types.StringValue("nfs") +} + +func (m *NFSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.NFSStorageCreateRequest{} + + nodes := proxmox_types.CustomCommaSeparatedList{} + diags := m.Nodes.ElementsAs(ctx, &nodes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) + } + contentTypes := proxmox_types.CustomCommaSeparatedList{} + diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + } + + request.ID = m.ID.ValueStringPointer() + request.Type = m.GetStorageType().ValueStringPointer() + request.Nodes = &nodes + request.ContentTypes = &contentTypes + request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) + request.Server = m.Server.ValueStringPointer() + request.Export = m.Export.ValueStringPointer() + request.Options = m.Options.ValueStringPointer() + + return request, nil +} + +func (m *NFSStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { + request := storage.NFSStorageUpdateRequest{} + + nodes := proxmox_types.CustomCommaSeparatedList{} + diags := m.Nodes.ElementsAs(ctx, &nodes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) + } + contentTypes := proxmox_types.CustomCommaSeparatedList{} + diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) + if diags.HasError() { + return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + } + + request.Nodes = &nodes + request.ContentTypes = &contentTypes + request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) + request.Options = m.Options.ValueStringPointer() + + return request, nil +} + +func (m *NFSStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { + m.ID = types.StringValue(*datastore.ID) + m.Type = m.GetStorageType() + 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.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.Disable != nil { + m.Disable = datastore.Disable.ToValue() + } + + 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) + } + + return nil +} diff --git a/fwprovider/storage/resource_generic.go b/fwprovider/storage/resource_generic.go new file mode 100644 index 000000000..84d139c02 --- /dev/null +++ b/fwprovider/storage/resource_generic.go @@ -0,0 +1,140 @@ +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 + } + + state.fromAPI(ctx, datastore) + 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_nfs.go b/fwprovider/storage/resource_nfs.go new file mode 100644 index 000000000..ad545fe3f --- /dev/null +++ b/fwprovider/storage/resource_nfs.go @@ -0,0 +1,70 @@ +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/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, // The pointer to our model + NFSStorageModel, // The struct type of our model + ]{ + 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, req 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) { + s := storageSchemaFactory(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, + }, + "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, + }, + }) + s.Description = "Manages an NFS-based storage in Proxmox VE." + resp.Schema = s +} diff --git a/proxmox/storage/nfs_types.go b/proxmox/storage/nfs_types.go new file mode 100644 index 000000000..9fcd72684 --- /dev/null +++ b/proxmox/storage/nfs_types.go @@ -0,0 +1,29 @@ +package storage + +import "github.com/bpg/terraform-provider-proxmox/proxmox/types" + +// NFSStorageMutableFields defines the mutable attributes for 'nfs' type storage. +type NFSStorageMutableFields struct { + DataStoreCommonMutableFields + Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` + SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` + 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"` +} + +// 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/storage_types.go b/proxmox/storage/storage_types.go index 264f952f3..421d428c0 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -40,6 +40,9 @@ type DatastoreGetResponseData struct { 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"` } type DataStoreCommonImmutableFields struct { From d5a76c919acc60d0eb7db73af07af7704946e56e Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 19:41:11 +0200 Subject: [PATCH 11/25] refactor(storage): update directory resource to use generic resource Signed-off-by: James Neill --- fwprovider/storage/model_directory.go | 12 ++- fwprovider/storage/model_nfs.go | 2 - fwprovider/storage/resource_directory.go | 124 +++-------------------- 3 files changed, 23 insertions(+), 115 deletions(-) diff --git a/fwprovider/storage/model_directory.go b/fwprovider/storage/model_directory.go index d718554cb..824277b5c 100644 --- a/fwprovider/storage/model_directory.go +++ b/fwprovider/storage/model_directory.go @@ -13,7 +13,6 @@ import ( // DirectoryStorageModel maps the Terraform schema for directory storage. type DirectoryStorageModel struct { ID types.String `tfsdk:"id" json:"storage"` - Type types.String `tfsdk:"type" json:"type"` Path types.String `tfsdk:"path" json:"path"` Nodes types.Set `tfsdk:"nodes" json:"nodes"` ContentTypes types.Set `tfsdk:"content" json:"content"` @@ -22,12 +21,16 @@ type DirectoryStorageModel struct { Preallocation types.String `tfsdk:"preallocation" json:"preallocation"` } +func (m *DirectoryStorageModel) GetID() types.String { + return m.ID +} + func (m *DirectoryStorageModel) GetStorageType() types.String { return types.StringValue("dir") } // toCreateAPIRequest converts the Terraform model to a Proxmox API request body. -func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (storage.DirectoryStorageCreateRequest, error) { +func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { request := storage.DirectoryStorageCreateRequest{} nodes := proxmox_types.CustomCommaSeparatedList{} @@ -52,7 +55,7 @@ func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (storage return request, nil } -func (m *DirectoryStorageModel) toUpdateAPIRequest(ctx context.Context) (storage.DirectoryStorageUpdateRequest, error) { +func (m *DirectoryStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { request := storage.DirectoryStorageUpdateRequest{} nodes := proxmox_types.CustomCommaSeparatedList{} @@ -75,9 +78,8 @@ func (m *DirectoryStorageModel) toUpdateAPIRequest(ctx context.Context) (storage return request, nil } -func (m *DirectoryStorageModel) importFromAPI(ctx context.Context, datastore storage.DatastoreGetResponseData) error { +func (m *DirectoryStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { m.ID = types.StringValue(*datastore.ID) - m.Type = m.GetStorageType() if datastore.Nodes != nil { nodes, diags := types.SetValueFrom(ctx, types.StringType, *datastore.Nodes) if diags.HasError() { diff --git a/fwprovider/storage/model_nfs.go b/fwprovider/storage/model_nfs.go index f744f5641..8281b4d72 100644 --- a/fwprovider/storage/model_nfs.go +++ b/fwprovider/storage/model_nfs.go @@ -13,7 +13,6 @@ import ( // NFSStorageModel maps the Terraform schema for NFS storage. type NFSStorageModel struct { ID types.String `tfsdk:"id" json:"storage"` - Type types.String `tfsdk:"type" json:"type"` Nodes types.Set `tfsdk:"nodes" json:"nodes"` ContentTypes types.Set `tfsdk:"content" json:"content"` Disable types.Bool `tfsdk:"disable" json:"disable"` @@ -82,7 +81,6 @@ func (m *NFSStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, func (m *NFSStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { m.ID = types.StringValue(*datastore.ID) - m.Type = m.GetStorageType() if datastore.ContentTypes != nil { contentTypes, diags := types.SetValueFrom(ctx, types.StringType, *datastore.ContentTypes) if diags.HasError() { diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go index c564f152d..960810f2f 100644 --- a/fwprovider/storage/resource_directory.go +++ b/fwprovider/storage/resource_directory.go @@ -5,8 +5,6 @@ import ( "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/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" @@ -15,19 +13,29 @@ import ( ) // Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &directoryStorageResource{} - _ resource.ResourceWithConfigure = &directoryStorageResource{} -) +var _ resource.Resource = &directoryStorageResource{} // NewDirectoryStorageResource is a helper function to simplify the provider implementation. func NewDirectoryStorageResource() resource.Resource { - return &directoryStorageResource{} + return &directoryStorageResource{ + storageResource: &storageResource[ + *DirectoryStorageModel, // The pointer to our model + DirectoryStorageModel, // The struct type of our model + ]{ + storageType: "dir", + resourceName: "proxmox_virtual_environment_storage_directory", + }, + } } // directoryStorageResource is the resource implementation. type directoryStorageResource struct { - client proxmox.Client + *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) { @@ -55,106 +63,6 @@ func (r *directoryStorageResource) Schema(_ context.Context, _ resource.SchemaRe resp.Schema.Description = "Manages a directory-based storage in Proxmox VE." } -// Create creates the resource and sets the initial state. -func (r *directoryStorageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var plan DirectoryStorageModel - 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("Error creating create request for directory storage", err.Error()) - return - } - - err = r.client.Storage().CreateDatastore(ctx, &requestBody) - if err != nil { - resp.Diagnostics.AddError("Error creating directory storage", err.Error()) - return - } - - diags = resp.State.Set(ctx, &plan) - resp.Diagnostics.Append(diags...) -} - -// Read refreshes the resource state from the Proxmox API. -func (r *directoryStorageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state DirectoryStorageModel - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - requestBody := &storage.DatastoreGetRequest{ID: state.ID.ValueStringPointer()} - datastore, err := r.client.Storage().GetDatastore(ctx, requestBody) - - if err != nil { - resp.Diagnostics.AddError( - "Error Reading Proxmox Storage", - "Could not read storage ("+state.ID.ValueString()+"): "+err.Error(), - ) - return - } - - state.importFromAPI(ctx, *datastore) - - diags = resp.State.Set(ctx, &state) - resp.Diagnostics.Append(diags...) -} - -// Update updates the resource and sets the new state. -func (r *directoryStorageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var plan DirectoryStorageModel - 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("Error creating update request for directory storage", err.Error()) - return - } - - err = r.client.Storage().UpdateDatastore(ctx, plan.ID.ValueString(), &requestBody) - if err != nil { - resp.Diagnostics.AddError("Error updating directory storage", err.Error()) - return - } - - diags = resp.State.Set(ctx, &plan) - resp.Diagnostics.Append(diags...) -} - -// Delete deletes the resource and removes it from the state. -func (r *directoryStorageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state DirectoryStorageModel - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err := r.client.Storage().DeleteDatastore(ctx, state.ID.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "Error deleting directory storage", - "Could not delete directory storage, unexpected error: "+err.Error(), - ) - return - } -} - -// Metadata returns the resource type name. -func (r *directoryStorageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_storage_directory" -} - // 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 { From ca4b52866f9eda68525a2b3ede929cb622622bc8 Mon Sep 17 00:00:00 2001 From: James Neill Date: Mon, 18 Aug 2025 20:50:37 +0200 Subject: [PATCH 12/25] refactor(storage): introduce a base storage model to handle setting common attrs - shared field is always included, just some it's always enabled, so handle that now in the schema factory by denoting it as computed with a default of true Signed-off-by: James Neill --- fwprovider/storage/model_base.go | 96 +++++++++++++++++++++++++++ fwprovider/storage/model_directory.go | 82 +++++------------------ fwprovider/storage/model_nfs.go | 80 +++++----------------- fwprovider/storage/resource_nfs.go | 17 ++++- fwprovider/storage/schema_factory.go | 25 ++++++- proxmox/storage/nfs_types.go | 10 +-- proxmox/storage/storage_types.go | 21 +++--- 7 files changed, 184 insertions(+), 147 deletions(-) create mode 100644 fwprovider/storage/model_base.go diff --git a/fwprovider/storage/model_base.go b/fwprovider/storage/model_base.go new file mode 100644 index 000000000..84817753d --- /dev/null +++ b/fwprovider/storage/model_base.go @@ -0,0 +1,96 @@ +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_directory.go b/fwprovider/storage/model_directory.go index 824277b5c..fc9f5d1f3 100644 --- a/fwprovider/storage/model_directory.go +++ b/fwprovider/storage/model_directory.go @@ -2,55 +2,32 @@ 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" ) // DirectoryStorageModel maps the Terraform schema for directory storage. type DirectoryStorageModel struct { - ID types.String `tfsdk:"id" json:"storage"` - Path types.String `tfsdk:"path" json:"path"` - Nodes types.Set `tfsdk:"nodes" json:"nodes"` - ContentTypes types.Set `tfsdk:"content" json:"content"` - Disable types.Bool `tfsdk:"disable" json:"disable"` - Shared types.Bool `tfsdk:"shared" json:"shared"` - Preallocation types.String `tfsdk:"preallocation" json:"preallocation"` -} - -func (m *DirectoryStorageModel) GetID() types.String { - return m.ID + StorageModelBase + Path types.String `tfsdk:"path"` + Preallocation types.String `tfsdk:"preallocation"` } func (m *DirectoryStorageModel) GetStorageType() types.String { return types.StringValue("dir") } -// toCreateAPIRequest converts the Terraform model to a Proxmox API request body. func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { request := storage.DirectoryStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() - nodes := proxmox_types.CustomCommaSeparatedList{} - diags := m.Nodes.ElementsAs(ctx, &nodes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) - } - contentTypes := proxmox_types.CustomCommaSeparatedList{} - diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err } - request.ID = m.ID.ValueStringPointer() - request.Type = m.GetStorageType().ValueStringPointer() - request.Nodes = &nodes - request.ContentTypes = &contentTypes - request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) - request.Shared = proxmox_types.CustomBoolPtr(m.Shared.ValueBoolPointer()) request.Path = m.Path.ValueStringPointer() + request.Preallocation = m.Preallocation.ValueStringPointer() return request, nil } @@ -58,53 +35,26 @@ func (m *DirectoryStorageModel) toCreateAPIRequest(ctx context.Context) (interfa func (m *DirectoryStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { request := storage.DirectoryStorageUpdateRequest{} - nodes := proxmox_types.CustomCommaSeparatedList{} - diags := m.Nodes.ElementsAs(ctx, &nodes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) - } - - contentTypes := proxmox_types.CustomCommaSeparatedList{} - diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err } - request.ContentTypes = &contentTypes - request.Nodes = &nodes - request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) - request.Shared = proxmox_types.CustomBoolPtr(m.Shared.ValueBoolPointer()) + request.Preallocation = m.Preallocation.ValueStringPointer() return request, nil } func (m *DirectoryStorageModel) fromAPI(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() + 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_nfs.go b/fwprovider/storage/model_nfs.go index 8281b4d72..52e8920dc 100644 --- a/fwprovider/storage/model_nfs.go +++ b/fwprovider/storage/model_nfs.go @@ -2,29 +2,20 @@ 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" ) // NFSStorageModel maps the Terraform schema for NFS storage. type NFSStorageModel struct { - ID types.String `tfsdk:"id" json:"storage"` - Nodes types.Set `tfsdk:"nodes" json:"nodes"` - ContentTypes types.Set `tfsdk:"content" json:"content"` - Disable types.Bool `tfsdk:"disable" json:"disable"` - Server types.String `tfsdk:"server" json:"server"` - Export types.String `tfsdk:"export" json:"export"` - Options types.String `tfsdk:"options" json:"options"` - Preallocation types.String `tfsdk:"preallocation" json:"preallocation"` - SnapshotsAsVolumeChain types.Bool `tfsdk:"snapshot_as_volume_chain" json:"snapshot-as-volume-chain"` -} - -func (m *NFSStorageModel) GetID() types.String { - return m.ID + 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 { @@ -33,26 +24,17 @@ func (m *NFSStorageModel) GetStorageType() types.String { func (m *NFSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, error) { request := storage.NFSStorageCreateRequest{} + request.Type = m.GetStorageType().ValueStringPointer() - nodes := proxmox_types.CustomCommaSeparatedList{} - diags := m.Nodes.ElementsAs(ctx, &nodes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) - } - contentTypes := proxmox_types.CustomCommaSeparatedList{} - diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { + return nil, err } - request.ID = m.ID.ValueStringPointer() - request.Type = m.GetStorageType().ValueStringPointer() - request.Nodes = &nodes - request.ContentTypes = &contentTypes - request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) 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 } @@ -60,58 +42,32 @@ func (m *NFSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, func (m *NFSStorageModel) toUpdateAPIRequest(ctx context.Context) (interface{}, error) { request := storage.NFSStorageUpdateRequest{} - nodes := proxmox_types.CustomCommaSeparatedList{} - diags := m.Nodes.ElementsAs(ctx, &nodes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert nodes to directory storage: %s", diags) - } - contentTypes := proxmox_types.CustomCommaSeparatedList{} - diags = m.ContentTypes.ElementsAs(ctx, &contentTypes, false) - if diags.HasError() { - return request, fmt.Errorf("cannot convert content-types to directory storage: %s", diags) + if err := m.populateUpdateFields(ctx, &request.DataStoreCommonMutableFields); err != nil { + return nil, err } - request.Nodes = &nodes - request.ContentTypes = &contentTypes - request.Disable = proxmox_types.CustomBoolPtr(m.Disable.ValueBoolPointer()) request.Options = m.Options.ValueStringPointer() return request, nil } func (m *NFSStorageModel) fromAPI(ctx context.Context, datastore *storage.DatastoreGetResponseData) error { - m.ID = types.StringValue(*datastore.ID) - 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.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.Disable != nil { - m.Disable = datastore.Disable.ToValue() + 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/resource_nfs.go b/fwprovider/storage/resource_nfs.go index ad545fe3f..37000ca8e 100644 --- a/fwprovider/storage/resource_nfs.go +++ b/fwprovider/storage/resource_nfs.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -31,13 +32,16 @@ type nfsStorageResource struct { } // Metadata returns the resource type name. -func (r *nfsStorageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { +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) { - s := storageSchemaFactory(map[string]schema.Attribute{ + factoryOptions := &schemaFactoryOptions{ + IsSharedByDefault: true, + } + attributes := map[string]schema.Attribute{ "server": schema.StringAttribute{ Description: "The IP address or DNS name of the NFS server.", Required: true, @@ -55,6 +59,9 @@ func (r *nfsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, "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.", @@ -63,8 +70,12 @@ func (r *nfsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, "snapshot_as_volume_chain": schema.BoolAttribute{ Description: "Enable support for creating snapshots through volume backing-chains.", Optional: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, }, - }) + } + s := storageSchemaFactory(attributes, factoryOptions) s.Description = "Manages an NFS-based storage in Proxmox VE." resp.Schema = s } diff --git a/fwprovider/storage/schema_factory.go b/fwprovider/storage/schema_factory.go index b567b4418..3c68e7027 100644 --- a/fwprovider/storage/schema_factory.go +++ b/fwprovider/storage/schema_factory.go @@ -10,8 +10,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +type schemaFactoryOptions struct { + IsSharedByDefault bool +} + // storageSchemaFactory generates the schema for a storage resource. -func storageSchemaFactory(specificAttributes map[string]schema.Attribute) schema.Schema { +func storageSchemaFactory(specificAttributes map[string]schema.Attribute, opt ...*schemaFactoryOptions) schema.Schema { + options := &schemaFactoryOptions{} + if opt != nil && len(opt) > 0 { + options = opt[0] + } attributes := map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "The unique identifier of the storage.", @@ -46,6 +54,21 @@ func storageSchemaFactory(specificAttributes map[string]schema.Attribute) schema }, } + if options.IsSharedByDefault { + // For types like NFS, 'shared' is a computed, read-only attribute. The user cannot set it. + attributes["shared"] = schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes. This is inherent to the storage type.", + Computed: true, + Default: booldefault.StaticBool(true), + } + } else { + attributes["shared"] = schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Optional: true, + Computed: true, + } + } + // Merge provided attributes for the given storage type for k, v := range specificAttributes { attributes[k] = v diff --git a/proxmox/storage/nfs_types.go b/proxmox/storage/nfs_types.go index 9fcd72684..f4bb885ee 100644 --- a/proxmox/storage/nfs_types.go +++ b/proxmox/storage/nfs_types.go @@ -5,15 +5,15 @@ import "github.com/bpg/terraform-provider-proxmox/proxmox/types" // NFSStorageMutableFields defines the mutable attributes for 'nfs' type storage. type NFSStorageMutableFields struct { DataStoreCommonMutableFields - Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` - SnapshotsAsVolumeChain types.CustomBool `json:"snapshot-as-volume-chain,omitempty" url:"snapshot-as-volume-chain,omitempty"` - Options *string `json:"options,omitempty" url:"options,omitempty"` + 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"` + 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. diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 421d428c0..775e1f4de 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -33,16 +33,17 @@ type DatastoreListResponse struct { } type DatastoreGetResponseData struct { - 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"` + 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"` } type DataStoreCommonImmutableFields struct { From 9087a3ee7f99729f2333061166173c1c03b9cde2 Mon Sep 17 00:00:00 2001 From: James Neill Date: Tue, 19 Aug 2025 01:06:06 +0200 Subject: [PATCH 13/25] feat(storage): add Proxmox Backup Server support - supports automatic generation and management of encryption keys Signed-off-by: James Neill --- fwprovider/provider.go | 1 + fwprovider/storage/model_pbs.go | 99 +++++++++++++++ fwprovider/storage/resource_generic.go | 2 +- fwprovider/storage/resource_pbs.go | 166 +++++++++++++++++++++++++ proxmox/storage/pbs_types.go | 77 ++++++++++++ proxmox/storage/storage.go | 11 +- proxmox/storage/storage_types.go | 22 +++- 7 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 fwprovider/storage/model_pbs.go create mode 100644 fwprovider/storage/resource_pbs.go create mode 100644 proxmox/storage/pbs_types.go diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 9151b543b..e3098b759 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -536,6 +536,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc sdnzone.NewEVPNResource, storage.NewDirectoryStorageResource, storage.NewNFSStorageResource, + storage.NewProxmoxBackupServerStorageResource, } } diff --git a/fwprovider/storage/model_pbs.go b/fwprovider/storage/model_pbs.go new file mode 100644 index 000000000..448543097 --- /dev/null +++ b/fwprovider/storage/model_pbs.go @@ -0,0 +1,99 @@ +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.PBSStorageMutableFields.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) + } + + return nil +} diff --git a/fwprovider/storage/resource_generic.go b/fwprovider/storage/resource_generic.go index 84d139c02..bf84b38ee 100644 --- a/fwprovider/storage/resource_generic.go +++ b/fwprovider/storage/resource_generic.go @@ -67,7 +67,7 @@ func (r *storageResource[T, M]) Create(ctx context.Context, req resource.CreateR return } - err = r.client.Storage().CreateDatastore(ctx, requestBody) + _, err = r.client.Storage().CreateDatastore(ctx, requestBody) if err != nil { resp.Diagnostics.AddError(fmt.Sprintf("Error creating %s storage", r.storageType), err.Error()) return diff --git a/fwprovider/storage/resource_pbs.go b/fwprovider/storage/resource_pbs.go new file mode 100644 index 000000000..419143bf3 --- /dev/null +++ b/fwprovider/storage/resource_pbs.go @@ -0,0 +1,166 @@ +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 + } + + 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) { + factoryOptions := &schemaFactoryOptions{ + IsSharedByDefault: true, + } + 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, + }, + } + resp.Schema = storageSchemaFactory(attributes, factoryOptions) + resp.Schema.Description = "Manages a Proxmox Backup Server (PBS) storage in Proxmox VE." +} diff --git a/proxmox/storage/pbs_types.go b/proxmox/storage/pbs_types.go new file mode 100644 index 000000000..094ff6d09 --- /dev/null +++ b/proxmox/storage/pbs_types.go @@ -0,0 +1,77 @@ +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 b0e5754e0..ec580ed31 100644 --- a/proxmox/storage/storage.go +++ b/proxmox/storage/storage.go @@ -42,7 +42,7 @@ func (c *Client) ListDatastore(ctx context.Context, d *DatastoreListRequest) ([] } func (c *Client) GetDatastore(ctx context.Context, d *DatastoreGetRequest) (*DatastoreGetResponseData, error) { - resBody := &DatastoreGetResponseBody{} + resBody := &DatastoreGetResponse{} err := c.DoRequest( ctx, http.MethodGet, @@ -57,19 +57,20 @@ func (c *Client) GetDatastore(ctx context.Context, d *DatastoreGetRequest) (*Dat return resBody.Data, nil } -func (c *Client) CreateDatastore(ctx context.Context, d interface{}) error { +func (c *Client) CreateDatastore(ctx context.Context, d interface{}) (*DatastoreCreateResponseData, error) { + resBody := &DatastoreCreateResponse{} err := c.DoRequest( ctx, http.MethodPost, c.basePath(), d, - nil, + resBody, ) if err != nil { - return fmt.Errorf("error creating datastore: %w", err) + return nil, fmt.Errorf("error creating datastore: %w", err) } - return nil + return resBody.Data, nil } func (c *Client) UpdateDatastore(ctx context.Context, storeID string, d interface{}) error { diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 775e1f4de..99874f3e4 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -18,7 +18,7 @@ type DatastoreGetRequest struct { ID *string `json:"storage" url:"storage"` } -type DatastoreGetResponseBody struct { +type DatastoreGetResponse struct { Data *DatastoreGetResponseData `json:"data,omitempty"` } @@ -44,6 +44,26 @@ type DatastoreGetResponseData struct { 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"` +} + +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 { From 4d2ec08ff64251bfb575ddedb3f8a6605c2e3890 Mon Sep 17 00:00:00 2001 From: James Neill Date: Tue, 19 Aug 2025 01:44:35 +0200 Subject: [PATCH 14/25] feat(storage): add ZFS pool support Signed-off-by: James Neill --- fwprovider/provider.go | 1 + fwprovider/storage/model_zfs.go | 71 ++++++++++++++++++++++++++++++ fwprovider/storage/resource_zfs.go | 63 ++++++++++++++++++++++++++ proxmox/storage/storage_types.go | 3 ++ proxmox/storage/zfs_types.go | 27 ++++++++++++ 5 files changed, 165 insertions(+) create mode 100644 fwprovider/storage/model_zfs.go create mode 100644 fwprovider/storage/resource_zfs.go create mode 100644 proxmox/storage/zfs_types.go diff --git a/fwprovider/provider.go b/fwprovider/provider.go index e3098b759..1d0fd1314 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -537,6 +537,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc storage.NewDirectoryStorageResource, storage.NewNFSStorageResource, storage.NewProxmoxBackupServerStorageResource, + storage.NewZFSPoolStorageResource, } } diff --git a/fwprovider/storage/model_zfs.go b/fwprovider/storage/model_zfs.go new file mode 100644 index 000000000..fc984b50b --- /dev/null +++ b/fwprovider/storage/model_zfs.go @@ -0,0 +1,71 @@ +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.ZFSStorageMutableFields.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_zfs.go b/fwprovider/storage/resource_zfs.go new file mode 100644 index 000000000..a605e3e0c --- /dev/null +++ b/fwprovider/storage/resource_zfs.go @@ -0,0 +1,63 @@ +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/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, // The pointer to our model + ZFSStorageModel, // The struct type of our model + ]{ + 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) { + factoryOptions := &schemaFactoryOptions{ + IsSharedByDefault: true, + } + 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, + }, + } + s := storageSchemaFactory(attributes, factoryOptions) + s.Description = "Manages ZFS-based storage in Proxmox VE." + resp.Schema = s +} diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 99874f3e4..faf439511 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -50,6 +50,9 @@ type DatastoreGetResponseData struct { 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"` + Blocksize *string `json:"blocksize,omitempty" url:"blocksize,omitempty"` } type DatastoreCreateResponse struct { diff --git a/proxmox/storage/zfs_types.go b/proxmox/storage/zfs_types.go new file mode 100644 index 000000000..7d7016c10 --- /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 { + DataStoreCommonMutableFields + 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 +} From b10b62a0b3bd15d726d9ed8b16e9c3e14cf02a41 Mon Sep 17 00:00:00 2001 From: James Neill Date: Tue, 19 Aug 2025 02:58:04 +0200 Subject: [PATCH 15/25] feat(storage): add LVM support Signed-off-by: James Neill --- fwprovider/provider.go | 1 + fwprovider/storage/model_lvm.go | 65 ++++++++++++++++++++++++++++++ fwprovider/storage/resource_lvm.go | 62 ++++++++++++++++++++++++++++ proxmox/storage/lvm_types.go | 26 ++++++++++++ proxmox/storage/storage_types.go | 42 ++++++++++--------- 5 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 fwprovider/storage/model_lvm.go create mode 100644 fwprovider/storage/resource_lvm.go create mode 100644 proxmox/storage/lvm_types.go diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 1d0fd1314..2aaeffd34 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -535,6 +535,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc sdnzone.NewVXLANResource, sdnzone.NewEVPNResource, storage.NewDirectoryStorageResource, + storage.NewLVMPoolStorageResource, storage.NewNFSStorageResource, storage.NewProxmoxBackupServerStorageResource, storage.NewZFSPoolStorageResource, diff --git a/fwprovider/storage/model_lvm.go b/fwprovider/storage/model_lvm.go new file mode 100644 index 000000000..0fa3dbd87 --- /dev/null +++ b/fwprovider/storage/model_lvm.go @@ -0,0 +1,65 @@ +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.LVMStorageMutableFields.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/resource_lvm.go b/fwprovider/storage/resource_lvm.go new file mode 100644 index 000000000..41bdb1c9e --- /dev/null +++ b/fwprovider/storage/resource_lvm.go @@ -0,0 +1,62 @@ +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, // The pointer to our model + LVMStorageModel, // The struct type of our model + ]{ + 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) { + factoryOptions := &schemaFactoryOptions{ + IsSharedByDefault: true, + } + 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), + }, + } + s := storageSchemaFactory(attributes, factoryOptions) + s.Description = "Manages LVM-based storage in Proxmox VE." + resp.Schema = s +} diff --git a/proxmox/storage/lvm_types.go b/proxmox/storage/lvm_types.go new file mode 100644 index 000000000..cd58a7165 --- /dev/null +++ b/proxmox/storage/lvm_types.go @@ -0,0 +1,26 @@ +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/storage_types.go b/proxmox/storage/storage_types.go index faf439511..c2f09c152 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -33,26 +33,28 @@ type DatastoreListResponse struct { } type DatastoreGetResponseData struct { - 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"` - Blocksize *string `json:"blocksize,omitempty" url:"blocksize,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"` + 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"` } type DatastoreCreateResponse struct { From e9c869361fdc475209c1ed64948288307e8b6a64 Mon Sep 17 00:00:00 2001 From: James Neill Date: Tue, 19 Aug 2025 03:30:12 +0200 Subject: [PATCH 16/25] feat(storage): add LVM thin support Signed-off-by: James Neill --- fwprovider/provider.go | 1 + fwprovider/storage/model_lvm_thin.go | 62 +++++++++++++++++++++++++ fwprovider/storage/resource_lvm_thin.go | 59 +++++++++++++++++++++++ proxmox/storage/lvm_thin_types.go | 24 ++++++++++ proxmox/storage/storage_types.go | 1 + 5 files changed, 147 insertions(+) create mode 100644 fwprovider/storage/model_lvm_thin.go create mode 100644 fwprovider/storage/resource_lvm_thin.go create mode 100644 proxmox/storage/lvm_thin_types.go diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 2aaeffd34..0322024e6 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -536,6 +536,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc sdnzone.NewEVPNResource, storage.NewDirectoryStorageResource, storage.NewLVMPoolStorageResource, + storage.NewLVMThinPoolStorageResource, storage.NewNFSStorageResource, storage.NewProxmoxBackupServerStorageResource, storage.NewZFSPoolStorageResource, diff --git a/fwprovider/storage/model_lvm_thin.go b/fwprovider/storage/model_lvm_thin.go new file mode 100644 index 000000000..97403eb3b --- /dev/null +++ b/fwprovider/storage/model_lvm_thin.go @@ -0,0 +1,62 @@ +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.LVMThinStorageMutableFields.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/resource_lvm_thin.go b/fwprovider/storage/resource_lvm_thin.go new file mode 100644 index 000000000..cf55ac0ae --- /dev/null +++ b/fwprovider/storage/resource_lvm_thin.go @@ -0,0 +1,59 @@ +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/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, // The pointer to our model + LVMThinStorageModel, // The struct type of our model + ]{ + 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) { + factoryOptions := &schemaFactoryOptions{ + IsSharedByDefault: true, + } + 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, + }, + } + s := storageSchemaFactory(attributes, factoryOptions) + s.Description = "Manages LVM-based storage in Proxmox VE." + resp.Schema = s +} diff --git a/proxmox/storage/lvm_thin_types.go b/proxmox/storage/lvm_thin_types.go new file mode 100644 index 000000000..ad067d7dd --- /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/storage_types.go b/proxmox/storage/storage_types.go index c2f09c152..41a0d4ac1 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -55,6 +55,7 @@ type DatastoreGetResponseData struct { 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"` + ThinPool *string `json:"thinpool,omitempty" url:"thinpool,omitempty"` } type DatastoreCreateResponse struct { From 2cd3461704ab398d12123292ae9486170bbc58f7 Mon Sep 17 00:00:00 2001 From: James Neill Date: Fri, 22 Aug 2025 20:55:03 +0200 Subject: [PATCH 17/25] refactor(storage): remove schema factory options variadic Signed-off-by: James Neill --- fwprovider/storage/model_pbs.go | 5 +++-- fwprovider/storage/resource_lvm.go | 5 +---- fwprovider/storage/resource_lvm_thin.go | 5 +---- fwprovider/storage/resource_nfs.go | 11 +++++++---- fwprovider/storage/resource_pbs.go | 11 +++++++---- fwprovider/storage/resource_zfs.go | 11 +++++++---- fwprovider/storage/schema_factory.go | 25 +------------------------ proxmox/storage/storage_types.go | 1 + 8 files changed, 28 insertions(+), 46 deletions(-) diff --git a/fwprovider/storage/model_pbs.go b/fwprovider/storage/model_pbs.go index 448543097..4a01555ff 100644 --- a/fwprovider/storage/model_pbs.go +++ b/fwprovider/storage/model_pbs.go @@ -35,13 +35,11 @@ func (m *PBSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.PBSStorageMutableFields.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() { @@ -94,6 +92,9 @@ func (m *PBSStorageModel) fromAPI(ctx context.Context, datastore *storage.Datast 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/resource_lvm.go b/fwprovider/storage/resource_lvm.go index 41bdb1c9e..c568e8447 100644 --- a/fwprovider/storage/resource_lvm.go +++ b/fwprovider/storage/resource_lvm.go @@ -38,9 +38,6 @@ func (r *lvmPoolStorageResource) Metadata(_ context.Context, _ resource.Metadata // Schema defines the schema for the NFS storage resource. func (r *lvmPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - factoryOptions := &schemaFactoryOptions{ - IsSharedByDefault: true, - } attributes := map[string]schema.Attribute{ "volume_group": schema.StringAttribute{ Description: "The name of the volume group to use.", @@ -56,7 +53,7 @@ func (r *lvmPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequ Default: booldefault.StaticBool(false), }, } - s := storageSchemaFactory(attributes, factoryOptions) + s := storageSchemaFactory(attributes) s.Description = "Manages LVM-based storage in Proxmox VE." resp.Schema = s } diff --git a/fwprovider/storage/resource_lvm_thin.go b/fwprovider/storage/resource_lvm_thin.go index cf55ac0ae..21dad50ba 100644 --- a/fwprovider/storage/resource_lvm_thin.go +++ b/fwprovider/storage/resource_lvm_thin.go @@ -37,9 +37,6 @@ func (r *lvmThinPoolStorageResource) Metadata(_ context.Context, _ resource.Meta // Schema defines the schema for the NFS storage resource. func (r *lvmThinPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - factoryOptions := &schemaFactoryOptions{ - IsSharedByDefault: true, - } attributes := map[string]schema.Attribute{ "volume_group": schema.StringAttribute{ Description: "The name of the volume group to use.", @@ -53,7 +50,7 @@ func (r *lvmThinPoolStorageResource) Schema(_ context.Context, _ resource.Schema Required: true, }, } - s := storageSchemaFactory(attributes, factoryOptions) + s := storageSchemaFactory(attributes) s.Description = "Manages LVM-based storage in Proxmox VE." resp.Schema = s } diff --git a/fwprovider/storage/resource_nfs.go b/fwprovider/storage/resource_nfs.go index 37000ca8e..2f1806db4 100644 --- a/fwprovider/storage/resource_nfs.go +++ b/fwprovider/storage/resource_nfs.go @@ -5,6 +5,7 @@ import ( "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" @@ -38,9 +39,6 @@ func (r *nfsStorageResource) Metadata(_ context.Context, _ resource.MetadataRequ // Schema defines the schema for the NFS storage resource. func (r *nfsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - factoryOptions := &schemaFactoryOptions{ - IsSharedByDefault: true, - } attributes := map[string]schema.Attribute{ "server": schema.StringAttribute{ Description: "The IP address or DNS name of the NFS server.", @@ -74,8 +72,13 @@ func (r *nfsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, boolplanmodifier.RequiresReplace(), }, }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Computed: true, + Default: booldefault.StaticBool(true), + }, } - s := storageSchemaFactory(attributes, factoryOptions) + s := storageSchemaFactory(attributes) s.Description = "Manages an NFS-based storage in Proxmox VE." resp.Schema = s } diff --git a/fwprovider/storage/resource_pbs.go b/fwprovider/storage/resource_pbs.go index 419143bf3..b642f6cc2 100644 --- a/fwprovider/storage/resource_pbs.go +++ b/fwprovider/storage/resource_pbs.go @@ -65,6 +65,8 @@ func (r *pbsStorageResource) Create(ctx context.Context, req resource.CreateRequ 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) @@ -95,9 +97,6 @@ func (r *pbsStorageResource) Create(ctx context.Context, req resource.CreateRequ // Schema defines the schema for the Proxmox Backup Server storage resource. func (r *pbsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - factoryOptions := &schemaFactoryOptions{ - IsSharedByDefault: true, - } attributes := map[string]schema.Attribute{ "server": schema.StringAttribute{ Description: "The IP address or DNS name of the Proxmox Backup Server.", @@ -160,7 +159,11 @@ func (r *pbsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, Computed: true, Sensitive: true, }, + "shared": schema.BoolAttribute{ + Description: "Whether the storage is shared across all nodes.", + Computed: true, + }, } - resp.Schema = storageSchemaFactory(attributes, factoryOptions) + resp.Schema = storageSchemaFactory(attributes) resp.Schema.Description = "Manages a Proxmox Backup Server (PBS) storage in Proxmox VE." } diff --git a/fwprovider/storage/resource_zfs.go b/fwprovider/storage/resource_zfs.go index a605e3e0c..c0c843cf4 100644 --- a/fwprovider/storage/resource_zfs.go +++ b/fwprovider/storage/resource_zfs.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -37,9 +38,6 @@ func (r *zfsPoolStorageResource) Metadata(_ context.Context, _ resource.Metadata // Schema defines the schema for the NFS storage resource. func (r *zfsPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - factoryOptions := &schemaFactoryOptions{ - IsSharedByDefault: true, - } attributes := map[string]schema.Attribute{ "zfs_pool": schema.StringAttribute{ Description: "The name of the ZFS storage pool to use (e.g. `tank`, `rpool/data`).", @@ -56,8 +54,13 @@ func (r *zfsPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequ 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), + }, } - s := storageSchemaFactory(attributes, factoryOptions) + s := storageSchemaFactory(attributes) s.Description = "Manages ZFS-based storage in Proxmox VE." resp.Schema = s } diff --git a/fwprovider/storage/schema_factory.go b/fwprovider/storage/schema_factory.go index 3c68e7027..b567b4418 100644 --- a/fwprovider/storage/schema_factory.go +++ b/fwprovider/storage/schema_factory.go @@ -10,16 +10,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -type schemaFactoryOptions struct { - IsSharedByDefault bool -} - // storageSchemaFactory generates the schema for a storage resource. -func storageSchemaFactory(specificAttributes map[string]schema.Attribute, opt ...*schemaFactoryOptions) schema.Schema { - options := &schemaFactoryOptions{} - if opt != nil && len(opt) > 0 { - options = opt[0] - } +func storageSchemaFactory(specificAttributes map[string]schema.Attribute) schema.Schema { attributes := map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "The unique identifier of the storage.", @@ -54,21 +46,6 @@ func storageSchemaFactory(specificAttributes map[string]schema.Attribute, opt .. }, } - if options.IsSharedByDefault { - // For types like NFS, 'shared' is a computed, read-only attribute. The user cannot set it. - attributes["shared"] = schema.BoolAttribute{ - Description: "Whether the storage is shared across all nodes. This is inherent to the storage type.", - Computed: true, - Default: booldefault.StaticBool(true), - } - } else { - attributes["shared"] = schema.BoolAttribute{ - Description: "Whether the storage is shared across all nodes.", - Optional: true, - Computed: true, - } - } - // Merge provided attributes for the given storage type for k, v := range specificAttributes { attributes[k] = v diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 41a0d4ac1..b636e596f 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -81,6 +81,7 @@ 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. From 9025a70c335b18c79976dcfecf22d40a031f6650 Mon Sep 17 00:00:00 2001 From: James Neill Date: Fri, 22 Aug 2025 22:53:53 +0200 Subject: [PATCH 18/25] fix(storage): add missing shared fields to lvm resources Signed-off-by: James Neill --- fwprovider/storage/resource_lvm.go | 6 ++++++ fwprovider/storage/resource_lvm_thin.go | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/fwprovider/storage/resource_lvm.go b/fwprovider/storage/resource_lvm.go index c568e8447..a9d252dd4 100644 --- a/fwprovider/storage/resource_lvm.go +++ b/fwprovider/storage/resource_lvm.go @@ -52,6 +52,12 @@ func (r *lvmPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequ 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, + }, } s := storageSchemaFactory(attributes) s.Description = "Manages LVM-based storage in Proxmox VE." diff --git a/fwprovider/storage/resource_lvm_thin.go b/fwprovider/storage/resource_lvm_thin.go index 21dad50ba..827af9313 100644 --- a/fwprovider/storage/resource_lvm_thin.go +++ b/fwprovider/storage/resource_lvm_thin.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -49,6 +50,12 @@ func (r *lvmThinPoolStorageResource) Schema(_ context.Context, _ resource.Schema 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, + }, } s := storageSchemaFactory(attributes) s.Description = "Manages LVM-based storage in Proxmox VE." From ebd353a07729314f9f312fce60d3e2fda29057db Mon Sep 17 00:00:00 2001 From: James Neill Date: Sat, 23 Aug 2025 11:49:38 +0200 Subject: [PATCH 19/25] feat(storage): integrate backup's block Signed-off-by: James Neill --- fwprovider/storage/model_backups.go | 1 + fwprovider/storage/model_directory.go | 1 + fwprovider/storage/resource_directory.go | 9 +- fwprovider/storage/resource_lvm.go | 7 +- fwprovider/storage/resource_lvm_thin.go | 7 +- fwprovider/storage/resource_nfs.go | 8 +- fwprovider/storage/resource_pbs.go | 6 +- fwprovider/storage/resource_zfs.go | 7 +- fwprovider/storage/schema_factory.go | 159 +++++++++++++++++------ proxmox/storage/directory_types.go | 1 + proxmox/storage/nfs_types.go | 1 + proxmox/storage/pbs_types.go | 2 +- proxmox/storage/storage_types.go | 44 +++---- 13 files changed, 175 insertions(+), 78 deletions(-) diff --git a/fwprovider/storage/model_backups.go b/fwprovider/storage/model_backups.go index bfe1ceeb6..80f33fc40 100644 --- a/fwprovider/storage/model_backups.go +++ b/fwprovider/storage/model_backups.go @@ -7,6 +7,7 @@ import ( // 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"` diff --git a/fwprovider/storage/model_directory.go b/fwprovider/storage/model_directory.go index fc9f5d1f3..04716cddb 100644 --- a/fwprovider/storage/model_directory.go +++ b/fwprovider/storage/model_directory.go @@ -12,6 +12,7 @@ type DirectoryStorageModel struct { StorageModelBase Path types.String `tfsdk:"path"` Preallocation types.String `tfsdk:"preallocation"` + Backups *BackupModel `tfsdk:"backups"` } func (m *DirectoryStorageModel) GetStorageType() types.String { diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go index 960810f2f..5099ac05f 100644 --- a/fwprovider/storage/resource_directory.go +++ b/fwprovider/storage/resource_directory.go @@ -39,7 +39,7 @@ func (r *directoryStorageResource) Metadata(_ context.Context, req resource.Meta } func (r *directoryStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - specificAttributes := map[string]schema.Attribute{ + attributes := map[string]schema.Attribute{ "path": schema.StringAttribute{ Description: "The path to the directory on the Proxmox node.", Required: true, @@ -59,8 +59,11 @@ func (r *directoryStorageResource) Schema(_ context.Context, _ resource.SchemaRe }, } - resp.Schema = storageSchemaFactory(specificAttributes) - resp.Schema.Description = "Manages a directory-based storage in Proxmox VE." + 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. diff --git a/fwprovider/storage/resource_lvm.go b/fwprovider/storage/resource_lvm.go index a9d252dd4..cb66dda91 100644 --- a/fwprovider/storage/resource_lvm.go +++ b/fwprovider/storage/resource_lvm.go @@ -59,7 +59,8 @@ func (r *lvmPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequ Computed: true, }, } - s := storageSchemaFactory(attributes) - s.Description = "Manages LVM-based storage in Proxmox VE." - resp.Schema = s + 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 index 827af9313..f8ebfca95 100644 --- a/fwprovider/storage/resource_lvm_thin.go +++ b/fwprovider/storage/resource_lvm_thin.go @@ -57,7 +57,8 @@ func (r *lvmThinPoolStorageResource) Schema(_ context.Context, _ resource.Schema Computed: true, }, } - s := storageSchemaFactory(attributes) - s.Description = "Manages LVM-based storage in Proxmox VE." - resp.Schema = s + 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 index 2f1806db4..e1dee8e3a 100644 --- a/fwprovider/storage/resource_nfs.go +++ b/fwprovider/storage/resource_nfs.go @@ -78,7 +78,9 @@ func (r *nfsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, Default: booldefault.StaticBool(true), }, } - s := storageSchemaFactory(attributes) - s.Description = "Manages an NFS-based storage in Proxmox VE." - resp.Schema = s + + 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 index b642f6cc2..418d1ebb8 100644 --- a/fwprovider/storage/resource_pbs.go +++ b/fwprovider/storage/resource_pbs.go @@ -164,6 +164,8 @@ func (r *pbsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, Computed: true, }, } - resp.Schema = storageSchemaFactory(attributes) - resp.Schema.Description = "Manages a Proxmox Backup Server (PBS) storage in Proxmox VE." + 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 index c0c843cf4..3c7eeffb9 100644 --- a/fwprovider/storage/resource_zfs.go +++ b/fwprovider/storage/resource_zfs.go @@ -60,7 +60,8 @@ func (r *zfsPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequ Default: booldefault.StaticBool(false), }, } - s := storageSchemaFactory(attributes) - s.Description = "Manages ZFS-based storage in Proxmox VE." - resp.Schema = s + 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 index b567b4418..c8a2922ef 100644 --- a/fwprovider/storage/schema_factory.go +++ b/fwprovider/storage/schema_factory.go @@ -1,57 +1,142 @@ 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" ) -// storageSchemaFactory generates the schema for a storage resource. -func storageSchemaFactory(specificAttributes map[string]schema.Attribute) schema.Schema { - attributes := map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "The unique identifier of the storage.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), +type StorageSchemaFactory struct { + Schema *schema.Schema + + description string +} + +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, }, }, - "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, + } +} - // Merge provided attributes for the given storage type - for k, v := range specificAttributes { - attributes[k] = v +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 +} - return schema.Schema{ - Attributes: attributes, +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/proxmox/storage/directory_types.go b/proxmox/storage/directory_types.go index abe00b301..7032e044c 100644 --- a/proxmox/storage/directory_types.go +++ b/proxmox/storage/directory_types.go @@ -5,6 +5,7 @@ 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"` diff --git a/proxmox/storage/nfs_types.go b/proxmox/storage/nfs_types.go index f4bb885ee..b96f69591 100644 --- a/proxmox/storage/nfs_types.go +++ b/proxmox/storage/nfs_types.go @@ -5,6 +5,7 @@ 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"` } diff --git a/proxmox/storage/pbs_types.go b/proxmox/storage/pbs_types.go index 094ff6d09..ad2dc343a 100644 --- a/proxmox/storage/pbs_types.go +++ b/proxmox/storage/pbs_types.go @@ -5,7 +5,7 @@ import "time" // PBSStorageMutableFields defines the mutable attributes for 'pbs' type storage. type PBSStorageMutableFields struct { DataStoreCommonMutableFields - //DataStoreWithBackups + DataStoreWithBackups Fingerprint *string `json:"fingerprint,omitempty" url:"fingerprint,omitempty"` Encryption *string `json:"encryption-key,omitempty" url:"encryption-key,omitempty"` } diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index b636e596f..c0a4232b2 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -7,8 +7,8 @@ package storage import ( - "encoding/json" "fmt" + "net/url" "strings" "github.com/bpg/terraform-provider-proxmox/proxmox/types" @@ -87,19 +87,24 @@ type DataStoreCommonMutableFields struct { // DataStoreWithBackups holds optional retention settings for backups. type DataStoreWithBackups struct { MaxProtectedBackups *types.CustomInt64 `json:"max-protected-backups,omitempty" url:"max,omitempty"` - KeepDaily *int `json:"-"` - KeepHourly *int `json:"-"` - KeepLast *int `json:"-"` - KeepMonthly *int `json:"-"` - KeepWeekly *int `json:"-"` - KeepYearly *int `json:"-"` + 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 { +func (b *DataStoreWithBackups) String() string { var parts []string + if b.KeepLast != nil { + return fmt.Sprintf("keep-all=1", *b.KeepLast) + } + if b.KeepLast != nil { parts = append(parts, fmt.Sprintf("keep-last=%d", *b.KeepLast)) } @@ -122,22 +127,15 @@ func (b DataStoreWithBackups) String() string { return strings.Join(parts, ",") } -// MarshalJSON ensures DataStoreWithBackups is encoded into a JSON field "prune-backups". -func (b DataStoreWithBackups) MarshalJSON() ([]byte, error) { - str := b.String() - - // Special case; nothing defined so we omit the field - if str == "" && b.MaxProtectedBackups == nil { - return []byte(`{}`), nil +func (b *DataStoreWithBackups) EncodeValues(key string, v *url.Values) error { + if b.MaxProtectedBackups != nil { + v.Set("max-protected-backups", string(*b.MaxProtectedBackups)) } - type Alias DataStoreWithBackups - aux := struct { - *Alias - PruneBackups string `json:"prune-backups,omitempty" url:"prune,omitempty"` - }{ - Alias: (*Alias)(&b), - PruneBackups: str, + backupString := b.String() + if backupString != "" { + v.Set("prune-backups", backupString) } - return json.Marshal(aux) + + return nil } From 747a7082c7db5982bf12d460ad02057aadb1bf99 Mon Sep 17 00:00:00 2001 From: James Neill Date: Sat, 23 Aug 2025 11:50:55 +0200 Subject: [PATCH 20/25] test(storage): test prune backups string representation Signed-off-by: James Neill --- proxmox/storage/storage_types_test.go | 147 ++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 proxmox/storage/storage_types_test.go diff --git a/proxmox/storage/storage_types_test.go b/proxmox/storage/storage_types_test.go new file mode 100644 index 000000000..aabb627ef --- /dev/null +++ b/proxmox/storage/storage_types_test.go @@ -0,0 +1,147 @@ +package storage + +import ( + "encoding/json" + "testing" + + "github.com/bpg/terraform-provider-proxmox/proxmox/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +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) { + 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) { + result := tc.input.String() + assert.Equal(t, tc.expected, result) + }) + } +} + +// TestDataStoreWithBackups_MarshalJSON tests the MarshalJSON() method. +func TestDataStoreWithBackups_MarshalJSON(t *testing.T) { + testCases := []struct { + name string + input DataStoreWithBackups + expected string + }{ + { + name: "Empty struct", + input: DataStoreWithBackups{}, + expected: `{}`, + }, + { + name: "Only MaxProtectedBackups", + input: DataStoreWithBackups{MaxProtectedBackups: customInt64Ptr(10)}, + expected: `{"max-protected-backups":10}`, + }, + { + name: "Only prune-backups (single)", + input: DataStoreWithBackups{KeepDaily: intPtr(7)}, + expected: `{"prune-backups":"keep-daily=7"}`, + }, + { + name: "Only prune-backups (multiple)", + input: DataStoreWithBackups{ + KeepWeekly: intPtr(4), + KeepMonthly: intPtr(12), + }, + expected: `{"prune-backups":"keep-weekly=4,keep-monthly=12"}`, + }, + { + name: "Both MaxProtectedBackups and prune-backups", + input: DataStoreWithBackups{ + MaxProtectedBackups: customInt64Ptr(5), + KeepLast: intPtr(3), + KeepYearly: intPtr(1), + }, + expected: `{"max-protected-backups":5,"prune-backups":"keep-last=3,keep-yearly=1"}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := json.Marshal(tc.input) + require.NoError(t, err) + assert.JSONEq(t, tc.expected, string(result)) + }) + } +} From 57c60909ed165cec8b379991cdfb425a71e56a54 Mon Sep 17 00:00:00 2001 From: James Neill Date: Sat, 23 Aug 2025 12:30:06 +0200 Subject: [PATCH 21/25] docs(storage): remove irrelevant docstring Signed-off-by: James Neill --- fwprovider/storage/resource_directory.go | 4 ++-- fwprovider/storage/resource_lvm.go | 4 ++-- fwprovider/storage/resource_lvm_thin.go | 4 ++-- fwprovider/storage/resource_nfs.go | 4 ++-- fwprovider/storage/resource_zfs.go | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go index 5099ac05f..bed417984 100644 --- a/fwprovider/storage/resource_directory.go +++ b/fwprovider/storage/resource_directory.go @@ -19,8 +19,8 @@ var _ resource.Resource = &directoryStorageResource{} func NewDirectoryStorageResource() resource.Resource { return &directoryStorageResource{ storageResource: &storageResource[ - *DirectoryStorageModel, // The pointer to our model - DirectoryStorageModel, // The struct type of our model + *DirectoryStorageModel, + DirectoryStorageModel, ]{ storageType: "dir", resourceName: "proxmox_virtual_environment_storage_directory", diff --git a/fwprovider/storage/resource_lvm.go b/fwprovider/storage/resource_lvm.go index cb66dda91..9fcab9373 100644 --- a/fwprovider/storage/resource_lvm.go +++ b/fwprovider/storage/resource_lvm.go @@ -17,8 +17,8 @@ var _ resource.Resource = &lvmPoolStorageResource{} func NewLVMPoolStorageResource() resource.Resource { return &lvmPoolStorageResource{ storageResource: &storageResource[ - *LVMStorageModel, // The pointer to our model - LVMStorageModel, // The struct type of our model + *LVMStorageModel, + LVMStorageModel, ]{ storageType: "lvm", resourceName: "proxmox_virtual_environment_storage_lvm", diff --git a/fwprovider/storage/resource_lvm_thin.go b/fwprovider/storage/resource_lvm_thin.go index f8ebfca95..f69641071 100644 --- a/fwprovider/storage/resource_lvm_thin.go +++ b/fwprovider/storage/resource_lvm_thin.go @@ -17,8 +17,8 @@ var _ resource.Resource = &lvmThinPoolStorageResource{} func NewLVMThinPoolStorageResource() resource.Resource { return &lvmThinPoolStorageResource{ storageResource: &storageResource[ - *LVMThinStorageModel, // The pointer to our model - LVMThinStorageModel, // The struct type of our model + *LVMThinStorageModel, + LVMThinStorageModel, ]{ storageType: "lvmthin", resourceName: "proxmox_virtual_environment_storage_lvmthin", diff --git a/fwprovider/storage/resource_nfs.go b/fwprovider/storage/resource_nfs.go index e1dee8e3a..c8712d3f0 100644 --- a/fwprovider/storage/resource_nfs.go +++ b/fwprovider/storage/resource_nfs.go @@ -18,8 +18,8 @@ var _ resource.Resource = &nfsStorageResource{} func NewNFSStorageResource() resource.Resource { return &nfsStorageResource{ storageResource: &storageResource[ - *NFSStorageModel, // The pointer to our model - NFSStorageModel, // The struct type of our model + *NFSStorageModel, + NFSStorageModel, ]{ storageType: "nfs", resourceName: "proxmox_virtual_environment_storage_nfs", diff --git a/fwprovider/storage/resource_zfs.go b/fwprovider/storage/resource_zfs.go index 3c7eeffb9..087a1411c 100644 --- a/fwprovider/storage/resource_zfs.go +++ b/fwprovider/storage/resource_zfs.go @@ -17,8 +17,8 @@ var _ resource.Resource = &zfsPoolStorageResource{} func NewZFSPoolStorageResource() resource.Resource { return &zfsPoolStorageResource{ storageResource: &storageResource[ - *ZFSStorageModel, // The pointer to our model - ZFSStorageModel, // The struct type of our model + *ZFSStorageModel, + ZFSStorageModel, ]{ storageType: "zfspool", resourceName: "proxmox_virtual_environment_storage_zfspool", From a9d125009c88b13e9aa53dd474398a88d840a0b8 Mon Sep 17 00:00:00 2001 From: James Neill Date: Sat, 23 Aug 2025 12:30:39 +0200 Subject: [PATCH 22/25] feat(storage): add support for SMB/CIFS shares Signed-off-by: James Neill --- fwprovider/provider.go | 1 + fwprovider/storage/model_cifs.go | 85 ++++++++++++++++++++++++++ fwprovider/storage/resource_cifs.go | 92 +++++++++++++++++++++++++++++ proxmox/storage/cifs_types.go | 32 ++++++++++ proxmox/storage/storage_types.go | 7 ++- 5 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 fwprovider/storage/model_cifs.go create mode 100644 fwprovider/storage/resource_cifs.go create mode 100644 proxmox/storage/cifs_types.go diff --git a/fwprovider/provider.go b/fwprovider/provider.go index 0322024e6..a1371292b 100644 --- a/fwprovider/provider.go +++ b/fwprovider/provider.go @@ -539,6 +539,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc storage.NewLVMThinPoolStorageResource, storage.NewNFSStorageResource, storage.NewProxmoxBackupServerStorageResource, + storage.NewCIFSStorageResource, storage.NewZFSPoolStorageResource, } } diff --git a/fwprovider/storage/model_cifs.go b/fwprovider/storage/model_cifs.go new file mode 100644 index 000000000..f230a2ef4 --- /dev/null +++ b/fwprovider/storage/model_cifs.go @@ -0,0 +1,85 @@ +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/resource_cifs.go b/fwprovider/storage/resource_cifs.go new file mode 100644 index 000000000..a98688a4e --- /dev/null +++ b/fwprovider/storage/resource_cifs.go @@ -0,0 +1,92 @@ +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/proxmox/storage/cifs_types.go b/proxmox/storage/cifs_types.go new file mode 100644 index 000000000..20f701647 --- /dev/null +++ b/proxmox/storage/cifs_types.go @@ -0,0 +1,32 @@ +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/storage_types.go b/proxmox/storage/storage_types.go index c0a4232b2..121b4deea 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -51,11 +51,14 @@ type DatastoreGetResponseData struct { 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"` + 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"` + 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 { From b3a396a002fd4accec76c717039e87bef7eafd5e Mon Sep 17 00:00:00 2001 From: James Neill Date: Sat, 23 Aug 2025 13:15:39 +0200 Subject: [PATCH 23/25] test(storage): remove irrelevant JSON Marshalling tests for backups block Signed-off-by: James Neill --- proxmox/storage/storage_types.go | 7 ++-- proxmox/storage/storage_types_test.go | 52 --------------------------- 2 files changed, 4 insertions(+), 55 deletions(-) diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 121b4deea..6d23762ec 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -9,6 +9,7 @@ package storage import ( "fmt" "net/url" + "strconv" "strings" "github.com/bpg/terraform-provider-proxmox/proxmox/types" @@ -104,8 +105,8 @@ type DataStoreWithBackups struct { func (b *DataStoreWithBackups) String() string { var parts []string - if b.KeepLast != nil { - return fmt.Sprintf("keep-all=1", *b.KeepLast) + if b.KeepAll != nil { + return fmt.Sprintf("keep-all=1") } if b.KeepLast != nil { @@ -132,7 +133,7 @@ func (b *DataStoreWithBackups) String() string { func (b *DataStoreWithBackups) EncodeValues(key string, v *url.Values) error { if b.MaxProtectedBackups != nil { - v.Set("max-protected-backups", string(*b.MaxProtectedBackups)) + v.Set("max-protected-backups", strconv.FormatInt(int64(*b.MaxProtectedBackups), 10)) } backupString := b.String() diff --git a/proxmox/storage/storage_types_test.go b/proxmox/storage/storage_types_test.go index aabb627ef..7e401b347 100644 --- a/proxmox/storage/storage_types_test.go +++ b/proxmox/storage/storage_types_test.go @@ -1,12 +1,10 @@ package storage import ( - "encoding/json" "testing" "github.com/bpg/terraform-provider-proxmox/proxmox/types" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func intPtr(i int) *int { @@ -95,53 +93,3 @@ func TestDataStoreWithBackups_String(t *testing.T) { }) } } - -// TestDataStoreWithBackups_MarshalJSON tests the MarshalJSON() method. -func TestDataStoreWithBackups_MarshalJSON(t *testing.T) { - testCases := []struct { - name string - input DataStoreWithBackups - expected string - }{ - { - name: "Empty struct", - input: DataStoreWithBackups{}, - expected: `{}`, - }, - { - name: "Only MaxProtectedBackups", - input: DataStoreWithBackups{MaxProtectedBackups: customInt64Ptr(10)}, - expected: `{"max-protected-backups":10}`, - }, - { - name: "Only prune-backups (single)", - input: DataStoreWithBackups{KeepDaily: intPtr(7)}, - expected: `{"prune-backups":"keep-daily=7"}`, - }, - { - name: "Only prune-backups (multiple)", - input: DataStoreWithBackups{ - KeepWeekly: intPtr(4), - KeepMonthly: intPtr(12), - }, - expected: `{"prune-backups":"keep-weekly=4,keep-monthly=12"}`, - }, - { - name: "Both MaxProtectedBackups and prune-backups", - input: DataStoreWithBackups{ - MaxProtectedBackups: customInt64Ptr(5), - KeepLast: intPtr(3), - KeepYearly: intPtr(1), - }, - expected: `{"max-protected-backups":5,"prune-backups":"keep-last=3,keep-yearly=1"}`, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result, err := json.Marshal(tc.input) - require.NoError(t, err) - assert.JSONEq(t, tc.expected, string(result)) - }) - } -} From 6c23a8e57615e96aad2609d280ed1e0118a4085b Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:02:06 -0400 Subject: [PATCH 24/25] fix: linter errors Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- fwprovider/storage/model_backups.go | 6 ++ fwprovider/storage/model_base.go | 15 ++++- fwprovider/storage/model_cifs.go | 12 ++++ fwprovider/storage/model_directory.go | 8 +++ fwprovider/storage/model_lvm.go | 10 ++- fwprovider/storage/model_lvm_thin.go | 10 ++- fwprovider/storage/model_nfs.go | 10 +++ fwprovider/storage/model_pbs.go | 15 ++++- fwprovider/storage/model_zfs.go | 14 +++- fwprovider/storage/resource_cifs.go | 6 ++ fwprovider/storage/resource_directory.go | 6 ++ fwprovider/storage/resource_generic.go | 24 ++++++- fwprovider/storage/resource_lvm.go | 6 ++ fwprovider/storage/resource_lvm_thin.go | 6 ++ fwprovider/storage/resource_nfs.go | 6 ++ fwprovider/storage/resource_pbs.go | 17 ++++- fwprovider/storage/resource_zfs.go | 16 +++-- fwprovider/storage/schema_factory.go | 11 +++- proxmox/storage/cifs_types.go | 13 ++-- proxmox/storage/directory_types.go | 5 +- proxmox/storage/lvm_thin_types.go | 2 +- proxmox/storage/lvm_types.go | 1 + proxmox/storage/nfs_types.go | 7 +- proxmox/storage/pbs_types.go | 9 +-- proxmox/storage/storage.go | 2 + proxmox/storage/storage_types.go | 81 +++++++++++++----------- proxmox/storage/storage_types_test.go | 10 +++ proxmox/storage/zfs_types.go | 4 +- proxmoxtf/resource/file.go | 1 + 29 files changed, 263 insertions(+), 70 deletions(-) diff --git a/fwprovider/storage/model_backups.go b/fwprovider/storage/model_backups.go index 80f33fc40..d0cff4023 100644 --- a/fwprovider/storage/model_backups.go +++ b/fwprovider/storage/model_backups.go @@ -1,3 +1,9 @@ +/* + * 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 ( diff --git a/fwprovider/storage/model_base.go b/fwprovider/storage/model_base.go index 84817753d..f431044c1 100644 --- a/fwprovider/storage/model_base.go +++ b/fwprovider/storage/model_base.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -33,6 +39,7 @@ func (m *StorageModelBase) populateBaseFromAPI(ctx context.Context, datastore *s 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{}) @@ -43,12 +50,14 @@ func (m *StorageModelBase) populateBaseFromAPI(ctx context.Context, datastore *s 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() } @@ -57,7 +66,11 @@ func (m *StorageModelBase) populateBaseFromAPI(ctx context.Context, datastore *s } // 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 { +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) diff --git a/fwprovider/storage/model_cifs.go b/fwprovider/storage/model_cifs.go index f230a2ef4..d4d1f448d 100644 --- a/fwprovider/storage/model_cifs.go +++ b/fwprovider/storage/model_cifs.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -11,6 +17,7 @@ import ( // 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"` @@ -65,18 +72,23 @@ func (m *CIFSStorageModel) fromAPI(ctx context.Context, datastore *storage.Datas 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) } diff --git a/fwprovider/storage/model_directory.go b/fwprovider/storage/model_directory.go index 04716cddb..ca27756e2 100644 --- a/fwprovider/storage/model_directory.go +++ b/fwprovider/storage/model_directory.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -10,6 +16,7 @@ import ( // 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"` @@ -53,6 +60,7 @@ func (m *DirectoryStorageModel) fromAPI(ctx context.Context, datastore *storage. if datastore.Path != nil { m.Path = types.StringValue(*datastore.Path) } + if datastore.Preallocation != nil { m.Preallocation = types.StringValue(*datastore.Preallocation) } diff --git a/fwprovider/storage/model_lvm.go b/fwprovider/storage/model_lvm.go index 0fa3dbd87..e5a7f4b53 100644 --- a/fwprovider/storage/model_lvm.go +++ b/fwprovider/storage/model_lvm.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -11,6 +17,7 @@ import ( // 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"` } @@ -25,7 +32,7 @@ func (m *LVMStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, request := storage.LVMStorageCreateRequest{} request.Type = m.GetStorageType().ValueStringPointer() - if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.LVMStorageMutableFields.DataStoreCommonMutableFields); err != nil { + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { return nil, err } @@ -57,6 +64,7 @@ func (m *LVMStorageModel) fromAPI(ctx context.Context, datastore *storage.Datast if datastore.VolumeGroup != nil { m.VolumeGroup = types.StringValue(*datastore.VolumeGroup) } + if datastore.WipeRemovedVolumes != nil { m.WipeRemovedVolumes = types.BoolValue(*datastore.WipeRemovedVolumes.PointerBool()) } diff --git a/fwprovider/storage/model_lvm_thin.go b/fwprovider/storage/model_lvm_thin.go index 97403eb3b..39482b0f1 100644 --- a/fwprovider/storage/model_lvm_thin.go +++ b/fwprovider/storage/model_lvm_thin.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -10,6 +16,7 @@ import ( // LVMThinStorageModel maps the Terraform schema for LVM storage. type LVMThinStorageModel struct { StorageModelBase + VolumeGroup types.String `tfsdk:"volume_group"` ThinPool types.String `tfsdk:"thin_pool"` } @@ -24,7 +31,7 @@ func (m *LVMThinStorageModel) toCreateAPIRequest(ctx context.Context) (interface request := storage.LVMThinStorageCreateRequest{} request.Type = m.GetStorageType().ValueStringPointer() - if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.LVMThinStorageMutableFields.DataStoreCommonMutableFields); err != nil { + if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.DataStoreCommonMutableFields); err != nil { return nil, err } @@ -54,6 +61,7 @@ func (m *LVMThinStorageModel) fromAPI(ctx context.Context, datastore *storage.Da if datastore.VolumeGroup != nil { m.VolumeGroup = types.StringValue(*datastore.VolumeGroup) } + if datastore.ThinPool != nil { m.ThinPool = types.StringValue(*datastore.ThinPool) } diff --git a/fwprovider/storage/model_nfs.go b/fwprovider/storage/model_nfs.go index 52e8920dc..56c000778 100644 --- a/fwprovider/storage/model_nfs.go +++ b/fwprovider/storage/model_nfs.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -11,6 +17,7 @@ import ( // 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"` @@ -59,12 +66,15 @@ func (m *NFSStorageModel) fromAPI(ctx context.Context, datastore *storage.Datast 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) } diff --git a/fwprovider/storage/model_pbs.go b/fwprovider/storage/model_pbs.go index 4a01555ff..d09e5950e 100644 --- a/fwprovider/storage/model_pbs.go +++ b/fwprovider/storage/model_pbs.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -10,6 +16,7 @@ import ( // 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"` @@ -32,9 +39,10 @@ func (m *PBSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, request := storage.PBSStorageCreateRequest{} request.Type = m.GetStorageType().ValueStringPointer() - if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.PBSStorageMutableFields.DataStoreCommonMutableFields); err != nil { + 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() @@ -80,18 +88,23 @@ func (m *PBSStorageModel) fromAPI(ctx context.Context, datastore *storage.Datast 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()) } diff --git a/fwprovider/storage/model_zfs.go b/fwprovider/storage/model_zfs.go index fc984b50b..e935d2f9a 100644 --- a/fwprovider/storage/model_zfs.go +++ b/fwprovider/storage/model_zfs.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -11,6 +17,7 @@ import ( // 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"` @@ -26,7 +33,10 @@ func (m *ZFSStorageModel) toCreateAPIRequest(ctx context.Context) (interface{}, request := storage.ZFSStorageCreateRequest{} request.Type = m.GetStorageType().ValueStringPointer() - if err := m.populateCreateFields(ctx, &request.DataStoreCommonImmutableFields, &request.ZFSStorageMutableFields.DataStoreCommonMutableFields); err != nil { + if err := m.populateCreateFields(ctx, + &request.DataStoreCommonImmutableFields, + &request.DataStoreCommonMutableFields, + ); err != nil { return nil, err } @@ -60,9 +70,11 @@ func (m *ZFSStorageModel) fromAPI(ctx context.Context, datastore *storage.Datast 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) } diff --git a/fwprovider/storage/resource_cifs.go b/fwprovider/storage/resource_cifs.go index a98688a4e..dbbe206b1 100644 --- a/fwprovider/storage/resource_cifs.go +++ b/fwprovider/storage/resource_cifs.go @@ -1,3 +1,9 @@ +/* + * 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 ( diff --git a/fwprovider/storage/resource_directory.go b/fwprovider/storage/resource_directory.go index bed417984..aad1ce30e 100644 --- a/fwprovider/storage/resource_directory.go +++ b/fwprovider/storage/resource_directory.go @@ -1,3 +1,9 @@ +/* + * 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 ( diff --git a/fwprovider/storage/resource_generic.go b/fwprovider/storage/resource_generic.go index bf84b38ee..b4f9ffc1c 100644 --- a/fwprovider/storage/resource_generic.go +++ b/fwprovider/storage/resource_generic.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -44,19 +50,23 @@ func (r *storageResource[T, M]) Configure(ctx context.Context, req resource.Conf 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 } @@ -80,20 +90,28 @@ func (r *storageResource[T, M]) Create(ctx context.Context, req resource.CreateR // 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 } - state.fromAPI(ctx, datastore) + 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...) } @@ -101,8 +119,10 @@ func (r *storageResource[T, M]) Read(ctx context.Context, req resource.ReadReque // 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 } @@ -126,8 +146,10 @@ func (r *storageResource[T, M]) Update(ctx context.Context, req resource.UpdateR // 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 } diff --git a/fwprovider/storage/resource_lvm.go b/fwprovider/storage/resource_lvm.go index 9fcab9373..b5d8497c2 100644 --- a/fwprovider/storage/resource_lvm.go +++ b/fwprovider/storage/resource_lvm.go @@ -1,3 +1,9 @@ +/* + * 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 ( diff --git a/fwprovider/storage/resource_lvm_thin.go b/fwprovider/storage/resource_lvm_thin.go index f69641071..e5311dac3 100644 --- a/fwprovider/storage/resource_lvm_thin.go +++ b/fwprovider/storage/resource_lvm_thin.go @@ -1,3 +1,9 @@ +/* + * 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 ( diff --git a/fwprovider/storage/resource_nfs.go b/fwprovider/storage/resource_nfs.go index c8712d3f0..a484ade57 100644 --- a/fwprovider/storage/resource_nfs.go +++ b/fwprovider/storage/resource_nfs.go @@ -1,3 +1,9 @@ +/* + * 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 ( diff --git a/fwprovider/storage/resource_pbs.go b/fwprovider/storage/resource_pbs.go index 418d1ebb8..cc6b47020 100644 --- a/fwprovider/storage/resource_pbs.go +++ b/fwprovider/storage/resource_pbs.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -47,8 +53,10 @@ func (r *pbsStorageResource) Metadata(_ context.Context, req resource.MetadataRe // 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 } @@ -69,11 +77,13 @@ func (r *pbsStorageResource) Create(ctx context.Context, req resource.CreateRequ 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 { @@ -82,11 +92,13 @@ func (r *pbsStorageResource) Create(ctx context.Context, req resource.CreateRequ 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) } @@ -145,8 +157,9 @@ func (r *pbsStorageResource) Schema(_ context.Context, _ resource.SchemaRequest, 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, + 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")), }, diff --git a/fwprovider/storage/resource_zfs.go b/fwprovider/storage/resource_zfs.go index 087a1411c..65fab392d 100644 --- a/fwprovider/storage/resource_zfs.go +++ b/fwprovider/storage/resource_zfs.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -47,12 +53,14 @@ func (r *zfsPoolStorageResource) Schema(_ context.Context, _ resource.SchemaRequ }, }, "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, + 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, + 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.", diff --git a/fwprovider/storage/schema_factory.go b/fwprovider/storage/schema_factory.go index c8a2922ef..3ae2b519a 100644 --- a/fwprovider/storage/schema_factory.go +++ b/fwprovider/storage/schema_factory.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -14,8 +20,6 @@ import ( type StorageSchemaFactory struct { Schema *schema.Schema - - description string } func NewStorageSchemaFactory() *StorageSchemaFactory { @@ -55,6 +59,7 @@ func NewStorageSchemaFactory() *StorageSchemaFactory { }, Blocks: map[string]schema.Block{}, } + return &StorageSchemaFactory{ Schema: s, } @@ -69,6 +74,7 @@ func (s *StorageSchemaFactory) WithAttributes(attributes map[string]schema.Attri for k, v := range attributes { s.Schema.Attributes[k] = v } + return s } @@ -76,6 +82,7 @@ func (s *StorageSchemaFactory) WithBlocks(blocks map[string]schema.Block) *Stora for k, v := range blocks { s.Schema.Blocks[k] = v } + return s } diff --git a/proxmox/storage/cifs_types.go b/proxmox/storage/cifs_types.go index 20f701647..f30ea6026 100644 --- a/proxmox/storage/cifs_types.go +++ b/proxmox/storage/cifs_types.go @@ -6,16 +6,17 @@ import "github.com/bpg/terraform-provider-proxmox/proxmox/types" 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"` + 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"` } diff --git a/proxmox/storage/directory_types.go b/proxmox/storage/directory_types.go index 7032e044c..de89c5f0c 100644 --- a/proxmox/storage/directory_types.go +++ b/proxmox/storage/directory_types.go @@ -6,9 +6,10 @@ import "github.com/bpg/terraform-provider-proxmox/proxmox/types" type DirectoryStorageMutableFields struct { DataStoreCommonMutableFields DataStoreWithBackups - Preallocation *string `json:"preallocation,omitempty" url:"preallocation,omitempty"` + + 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"` + Shared *types.CustomBool `json:"shared,omitempty" url:"shared,omitempty,int"` } // DirectoryStorageImmutableFields defines the immutable attributes for 'dir' type storage. diff --git a/proxmox/storage/lvm_thin_types.go b/proxmox/storage/lvm_thin_types.go index ad067d7dd..03f3c396c 100644 --- a/proxmox/storage/lvm_thin_types.go +++ b/proxmox/storage/lvm_thin_types.go @@ -7,7 +7,7 @@ type LVMThinStorageMutableFields struct { // LVMThinStorageImmutableFields defines options for 'lvmthin' type storage. type LVMThinStorageImmutableFields struct { - VolumeGroup *string `json:"vgname" url:"vgname"` + VolumeGroup *string `json:"vgname" url:"vgname"` ThinPool *string `json:"thinpool,omitempty" url:"thinpool,omitempty"` } diff --git a/proxmox/storage/lvm_types.go b/proxmox/storage/lvm_types.go index cd58a7165..f403db855 100644 --- a/proxmox/storage/lvm_types.go +++ b/proxmox/storage/lvm_types.go @@ -5,6 +5,7 @@ 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"` } diff --git a/proxmox/storage/nfs_types.go b/proxmox/storage/nfs_types.go index b96f69591..af8c9e13e 100644 --- a/proxmox/storage/nfs_types.go +++ b/proxmox/storage/nfs_types.go @@ -6,14 +6,15 @@ import "github.com/bpg/terraform-provider-proxmox/proxmox/types" 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"` + 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"` } diff --git a/proxmox/storage/pbs_types.go b/proxmox/storage/pbs_types.go index ad2dc343a..4c1c8d042 100644 --- a/proxmox/storage/pbs_types.go +++ b/proxmox/storage/pbs_types.go @@ -6,16 +6,17 @@ import "time" type PBSStorageMutableFields struct { DataStoreCommonMutableFields DataStoreWithBackups - Fingerprint *string `json:"fingerprint,omitempty" url:"fingerprint,omitempty"` + + 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"` + 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"` + Server *string `json:"server,omitempty" url:"server,omitempty"` Datastore *string `json:"datastore,omitempty" url:"datastore,omitempty"` } diff --git a/proxmox/storage/storage.go b/proxmox/storage/storage.go index ec580ed31..d79875499 100644 --- a/proxmox/storage/storage.go +++ b/proxmox/storage/storage.go @@ -43,6 +43,7 @@ func (c *Client) ListDatastore(ctx context.Context, d *DatastoreListRequest) ([] func (c *Client) GetDatastore(ctx context.Context, d *DatastoreGetRequest) (*DatastoreGetResponseData, error) { resBody := &DatastoreGetResponse{} + err := c.DoRequest( ctx, http.MethodGet, @@ -59,6 +60,7 @@ func (c *Client) GetDatastore(ctx context.Context, d *DatastoreGetRequest) (*Dat func (c *Client) CreateDatastore(ctx context.Context, d interface{}) (*DatastoreCreateResponseData, error) { resBody := &DatastoreCreateResponse{} + err := c.DoRequest( ctx, http.MethodPost, diff --git a/proxmox/storage/storage_types.go b/proxmox/storage/storage_types.go index 6d23762ec..700cfaf16 100644 --- a/proxmox/storage/storage_types.go +++ b/proxmox/storage/storage_types.go @@ -34,32 +34,32 @@ type DatastoreListResponse struct { } type DatastoreGetResponseData struct { - 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"` + 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"` + 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 { @@ -67,9 +67,9 @@ type DatastoreCreateResponse struct { } type DatastoreCreateResponseData struct { - Type *string `json:"type" url:"type"` + Type *string `json:"type" url:"type"` Storage *string `json:"storage,omitempty" url:"storage,omitempty"` - Config DatastoreCreateResponseConfigData `json:"config,omitempty" url:"config,omitempty"` + Config DatastoreCreateResponseConfigData `json:"config,omitempty" url:"config,omitempty"` } type DatastoreCreateResponseConfigData struct { @@ -77,27 +77,27 @@ type DatastoreCreateResponseConfigData struct { } type DataStoreCommonImmutableFields struct { - ID *string `json:"storage" url:"storage"` + 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"` + 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"` + 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:"-"` + 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. @@ -106,24 +106,29 @@ func (b *DataStoreWithBackups) String() string { var parts []string if b.KeepAll != nil { - return fmt.Sprintf("keep-all=1") + 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)) } diff --git a/proxmox/storage/storage_types_test.go b/proxmox/storage/storage_types_test.go index 7e401b347..8fc375344 100644 --- a/proxmox/storage/storage_types_test.go +++ b/proxmox/storage/storage_types_test.go @@ -1,3 +1,9 @@ +/* + * 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 ( @@ -18,6 +24,8 @@ func customInt64Ptr(i int64) *types.CustomInt64 { // 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 @@ -88,6 +96,8 @@ func TestDataStoreWithBackups_String(t *testing.T) { 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 index 7d7016c10..24165e435 100644 --- a/proxmox/storage/zfs_types.go +++ b/proxmox/storage/zfs_types.go @@ -5,12 +5,12 @@ 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"` + + ThinProvision types.CustomBool `json:"sparse,omitempty" url:"sparse,omitempty,int"` Blocksize *string `json:"blocksize,omitempty" url:"blocksize,omitempty"` } type ZFSStorageImmutableFields struct { - DataStoreCommonMutableFields ZFSPool *string `json:"pool" url:"pool"` } diff --git a/proxmoxtf/resource/file.go b/proxmoxtf/resource/file.go index 488357ac7..c85894c21 100644 --- a/proxmoxtf/resource/file.go +++ b/proxmoxtf/resource/file.go @@ -568,6 +568,7 @@ func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag // For all other content types, we need to upload the file to the node's // datastore using SFTP. req := &storage.DatastoreGetRequest{ID: &datastoreID} + datastore, err2 := capi.Storage().GetDatastore(ctx, req) if err2 != nil { return diag.Errorf("failed to get datastore: %s", err2) From 212b174b2b9d67da0768d7235dcd2e613f3a82d6 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:29:21 -0400 Subject: [PATCH 25/25] feat: add docs Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- .../virtual_environment_storage_directory.md | 45 +++++++++++++++++++ .../virtual_environment_storage_lvm.md | 30 +++++++++++++ .../virtual_environment_storage_lvmthin.md | 30 +++++++++++++ .../virtual_environment_storage_nfs.md | 36 +++++++++++++++ .../virtual_environment_storage_pbs.md | 41 +++++++++++++++++ .../virtual_environment_storage_smb.md | 39 ++++++++++++++++ .../virtual_environment_storage_zfspool.md | 34 ++++++++++++++ main.go | 7 +++ 8 files changed, 262 insertions(+) create mode 100644 docs/resources/virtual_environment_storage_directory.md create mode 100644 docs/resources/virtual_environment_storage_lvm.md create mode 100644 docs/resources/virtual_environment_storage_lvmthin.md create mode 100644 docs/resources/virtual_environment_storage_nfs.md create mode 100644 docs/resources/virtual_environment_storage_pbs.md create mode 100644 docs/resources/virtual_environment_storage_smb.md create mode 100644 docs/resources/virtual_environment_storage_zfspool.md 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/main.go b/main.go index 7ab7d54a5..570ebee5a 100644 --- a/main.go +++ b/main.go @@ -76,6 +76,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/