From de8b4ec41ada527b5a14883b5dcacdab2684fc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= <51059348+rafsaf@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:49:03 +0200 Subject: [PATCH] feat(cluster): add cluster options resource (#548) --- .../virtual_environment_hagroup.md | 2 +- .../virtual_environment_hagroups.md | 2 +- .../virtual_environment_haresource.md | 2 +- .../virtual_environment_haresources.md | 2 +- .../virtual_environment_cluster_options.md | 65 ++ docs/resources/virtual_environment_hagroup.md | 2 +- .../virtual_environment_haresource.md | 2 +- ...rce_virtual_environment_cluster_options.tf | 10 + .../import.sh | 3 + .../resource.tf | 10 + internal/cluster/resource_options.go | 663 ++++++++++++++++++ internal/provider/provider.go | 1 + internal/structure/attribute.go | 2 +- internal/test/cluster/cluster_options_test.go | 97 +++ internal/validators/i18n.go | 28 + proxmox/cluster/options.go | 41 ++ proxmox/cluster/options_types.go | 92 +++ tools/tools.go | 7 +- 18 files changed, 1021 insertions(+), 10 deletions(-) create mode 100644 docs/resources/virtual_environment_cluster_options.md create mode 100644 example/resource_virtual_environment_cluster_options.tf create mode 100644 examples/resources/proxmox_virtual_environment_cluster_options/import.sh create mode 100644 examples/resources/proxmox_virtual_environment_cluster_options/resource.tf create mode 100644 internal/cluster/resource_options.go create mode 100644 internal/test/cluster/cluster_options_test.go create mode 100644 internal/validators/i18n.go create mode 100644 proxmox/cluster/options.go create mode 100644 proxmox/cluster/options_types.go diff --git a/docs/data-sources/virtual_environment_hagroup.md b/docs/data-sources/virtual_environment_hagroup.md index 43776c03..c3614821 100644 --- a/docs/data-sources/virtual_environment_hagroup.md +++ b/docs/data-sources/virtual_environment_hagroup.md @@ -38,7 +38,7 @@ output "proxmox_virtual_environment_hagroups_full" { ### Read-Only - `comment` (String) The comment associated with this group -- `id` (String) The ID of this resource. +- `id` (String) The unique identifier of this resource. - `no_failback` (Boolean) A flag that indicates that failing back to a higher priority node is disabled for this HA group. - `nodes` (Map of Number) The member nodes for this group. They are provided as a map, where the keys are the node names and the values represent their priority: integers for known priorities or `null` for unset priorities. - `restricted` (Boolean) A flag that indicates that other nodes may not be used to run resources associated to this HA group. diff --git a/docs/data-sources/virtual_environment_hagroups.md b/docs/data-sources/virtual_environment_hagroups.md index 214203be..a1b005df 100644 --- a/docs/data-sources/virtual_environment_hagroups.md +++ b/docs/data-sources/virtual_environment_hagroups.md @@ -27,4 +27,4 @@ output "data_proxmox_virtual_environment_hagroups" { ### Read-Only - `group_ids` (Set of String) The identifiers of the High Availability groups. -- `id` (String) The ID of this resource. +- `id` (String) The unique identifier of this resource. diff --git a/docs/data-sources/virtual_environment_haresource.md b/docs/data-sources/virtual_environment_haresource.md index a4d58a61..33d11423 100644 --- a/docs/data-sources/virtual_environment_haresource.md +++ b/docs/data-sources/virtual_environment_haresource.md @@ -39,7 +39,7 @@ output "proxmox_virtual_environment_haresources_full" { - `comment` (String) The comment associated with this resource. - `group` (String) The identifier of the High Availability group this resource is a member of. -- `id` (String) The ID of this resource. +- `id` (String) The unique identifier of this resource. - `max_relocate` (Number) The maximal number of relocation attempts. - `max_restart` (Number) The maximal number of restart attempts. - `state` (String) The desired state of the resource. diff --git a/docs/data-sources/virtual_environment_haresources.md b/docs/data-sources/virtual_environment_haresources.md index d6aeb75d..16e86b87 100644 --- a/docs/data-sources/virtual_environment_haresources.md +++ b/docs/data-sources/virtual_environment_haresources.md @@ -39,5 +39,5 @@ output "data_proxmox_virtual_environment_haresources" { ### Read-Only -- `id` (String) The ID of this resource. +- `id` (String) The unique identifier of this resource. - `resource_ids` (Set of String) The identifiers of the High Availability resources. diff --git a/docs/resources/virtual_environment_cluster_options.md b/docs/resources/virtual_environment_cluster_options.md new file mode 100644 index 00000000..5b5132a8 --- /dev/null +++ b/docs/resources/virtual_environment_cluster_options.md @@ -0,0 +1,65 @@ +--- +layout: page +title: proxmox_virtual_environment_cluster_options +parent: Resources +subcategory: Virtual Environment +description: |- + Manages Proxmox VE Cluster Datacenter options. +--- + +# Resource: proxmox_virtual_environment_cluster_options + +Manages Proxmox VE Cluster Datacenter options. + +## Example Usage + +```terraform +resource "proxmox_virtual_environment_cluster_options" "options" { + language = "en" + keyboard = "pl" + email_from = "ged@gont.earthsea" + bandwidth_limit_migration = 555555 + bandwidth_limit_default = 666666 + max_workers = 5 + migration_cidr = "10.0.0.0/8" + migration_type = "secure" +} +``` + + +## Schema + +### Optional + +- `bandwidth_limit_clone` (Number) Clone I/O bandwidth limit in KiB/s. +- `bandwidth_limit_default` (Number) Default I/O bandwidth limit in KiB/s. +- `bandwidth_limit_migration` (Number) Migration I/O bandwidth limit in KiB/s. +- `bandwidth_limit_move` (Number) Move I/O bandwidth limit in KiB/s. +- `bandwidth_limit_restore` (Number) Restore I/O bandwidth limit in KiB/s. +- `console` (String) Select the default Console viewer. Must be `applet` | `vv`| `html5` | `xtermjs`. You can either use the builtin java applet (VNC; deprecated and maps to html5), an external virt-viewer compatible application (SPICE), an HTML5 based vnc viewer (noVNC), or an HTML5 based console client (xtermjs). If the selected viewer is not available (e.g. SPICE not activated for the VM), the fallback is noVNC. +- `crs_ha` (String) Cluster resource scheduling setting for HA. Must be `static` | `basic`. +- `crs_ha_rebalance_on_start` (Boolean) Cluster resource scheduling setting for HA rebalance on start. +- `description` (String) Datacenter description. Shown in the web-interface datacenter notes panel. This is saved as comment inside the configuration file. +- `email_from` (String) email address to send notification from (default is root@$hostname). +- `ha_shutdown_policy` (String) Cluster wide HA shutdown policy. Must be `freeze` | `failover` | `migrate` | `conditional`. +- `http_proxy` (String) Specify external http proxy which is used for downloads (example: `http://username:password@host:port/`). +- `keyboard` (String) Default keyboard layout for vnc server. Must be `de` | `de-ch` | `da` | `en-gb` | `en-us` | `es` | `fi` | `fr` | `fr-be` | `fr-ca` | `fr-ch` | `hu` | `is` | `it` | `ja` | `lt` | `mk` | `nl` | `no` | `pl` | `pt` | `pt-br` | `sv` | `sl` | `tr`. +- `language` (String) Default GUI language. Must be `ca` | `da` | `de` | `en` | `es` | `eu` | `fa` | `fr` | `he` | `it` | `ja` | `nb` | `nn` | `pl` | `pt_BR` | `ru` | `sl` | `sv` | `tr` | `zh_CN` | `zh_TW`. +- `mac_prefix` (String) Prefix for autogenerated MAC addresses. +- `max_workers` (Number) Defines how many workers (per node) are maximal started on actions like 'stopall VMs' or task from the ha-manager. +- `migration_cidr` (String) Cluster wide migration network CIDR. +- `migration_type` (String) Cluster wide migration type. Must be `secure` | `unsecure`. + +### Read-Only + +- `id` (String) The unique identifier of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +#!/usr/bin/env sh +# Cluster options are global and can be imported using e.g.: +terraform import proxmox_virtual_environment_cluster_options.options cluster +``` diff --git a/docs/resources/virtual_environment_hagroup.md b/docs/resources/virtual_environment_hagroup.md index 83b176c0..c431848f 100644 --- a/docs/resources/virtual_environment_hagroup.md +++ b/docs/resources/virtual_environment_hagroup.md @@ -46,7 +46,7 @@ resource "proxmox_virtual_environment_hagroup" "example" { ### Read-Only -- `id` (String) The ID of this resource. +- `id` (String) The unique identifier of this resource. ## Import diff --git a/docs/resources/virtual_environment_haresource.md b/docs/resources/virtual_environment_haresource.md index fdaaf4e0..28d364be 100644 --- a/docs/resources/virtual_environment_haresource.md +++ b/docs/resources/virtual_environment_haresource.md @@ -43,7 +43,7 @@ resource "proxmox_virtual_environment_haresource" "example" { ### Read-Only -- `id` (String) The ID of this resource. +- `id` (String) The unique identifier of this resource. ## Import diff --git a/example/resource_virtual_environment_cluster_options.tf b/example/resource_virtual_environment_cluster_options.tf new file mode 100644 index 00000000..edc4a226 --- /dev/null +++ b/example/resource_virtual_environment_cluster_options.tf @@ -0,0 +1,10 @@ +resource "proxmox_virtual_environment_cluster_options" "options" { + language = "en" + keyboard = "pl" + email_from = "ged@gont.earthsea" + bandwidth_limit_migration = 555555 + bandwidth_limit_default = 666666 + max_workers = 5 + migration_cidr = "10.0.0.0/8" + migration_type = "secure" +} diff --git a/examples/resources/proxmox_virtual_environment_cluster_options/import.sh b/examples/resources/proxmox_virtual_environment_cluster_options/import.sh new file mode 100644 index 00000000..34883483 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_cluster_options/import.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh +# Cluster options are global and can be imported using e.g.: +terraform import proxmox_virtual_environment_cluster_options.options cluster diff --git a/examples/resources/proxmox_virtual_environment_cluster_options/resource.tf b/examples/resources/proxmox_virtual_environment_cluster_options/resource.tf new file mode 100644 index 00000000..edc4a226 --- /dev/null +++ b/examples/resources/proxmox_virtual_environment_cluster_options/resource.tf @@ -0,0 +1,10 @@ +resource "proxmox_virtual_environment_cluster_options" "options" { + language = "en" + keyboard = "pl" + email_from = "ged@gont.earthsea" + bandwidth_limit_migration = 555555 + bandwidth_limit_default = 666666 + max_workers = 5 + migration_cidr = "10.0.0.0/8" + migration_type = "secure" +} diff --git a/internal/cluster/resource_options.go b/internal/cluster/resource_options.go new file mode 100644 index 00000000..623766de --- /dev/null +++ b/internal/cluster/resource_options.go @@ -0,0 +1,663 @@ +/* + * 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 cluster + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/bpg/terraform-provider-proxmox/internal/structure" + "github.com/bpg/terraform-provider-proxmox/internal/validators" + "github.com/bpg/terraform-provider-proxmox/proxmox" + "github.com/bpg/terraform-provider-proxmox/proxmox/cluster" +) + +var ( + _ resource.Resource = &clusterOptionsResource{} + _ resource.ResourceWithConfigure = &clusterOptionsResource{} + _ resource.ResourceWithImportState = &clusterOptionsResource{} +) + +type clusterOptionsModel struct { + ID types.String `tfsdk:"id"` + BandwidthLimitClone types.Int64 `tfsdk:"bandwidth_limit_clone"` + BandwidthLimitDefault types.Int64 `tfsdk:"bandwidth_limit_default"` + BandwidthLimitMigration types.Int64 `tfsdk:"bandwidth_limit_migration"` + BandwidthLimitMove types.Int64 `tfsdk:"bandwidth_limit_move"` + BandwidthLimitRestore types.Int64 `tfsdk:"bandwidth_limit_restore"` + Console types.String `tfsdk:"console"` + HTTPProxy types.String `tfsdk:"http_proxy"` + MacPrefix types.String `tfsdk:"mac_prefix"` + Description types.String `tfsdk:"description"` + HAShutdownPolicy types.String `tfsdk:"ha_shutdown_policy"` + MigrationType types.String `tfsdk:"migration_type"` + MigrationNetwork types.String `tfsdk:"migration_cidr"` + CrsHA types.String `tfsdk:"crs_ha"` + CrsHARebalanceOnStart types.Bool `tfsdk:"crs_ha_rebalance_on_start"` + EmailFrom types.String `tfsdk:"email_from"` + Keyboard types.String `tfsdk:"keyboard"` + Language types.String `tfsdk:"language"` + MaxWorkers types.Int64 `tfsdk:"max_workers"` +} + +func (m *clusterOptionsModel) haData() *string { + var haDataParams []string + + if !m.HAShutdownPolicy.IsNull() && m.HAShutdownPolicy.ValueString() != "" { + haDataParams = append(haDataParams, fmt.Sprintf("shutdown_policy=%s", m.HAShutdownPolicy.ValueString())) + } + + if len(haDataParams) > 0 { + haDataValue := strings.Join(haDataParams, ",") + + return &haDataValue + } + + return nil +} + +func (m *clusterOptionsModel) migrationData() *string { + var migrationDataParams []string + + if !m.MigrationType.IsNull() && m.MigrationType.ValueString() != "" { + migrationDataParams = append(migrationDataParams, fmt.Sprintf("type=%s", m.MigrationType.ValueString())) + } + + if !m.MigrationNetwork.IsNull() && m.MigrationNetwork.ValueString() != "" { + migrationDataParams = append(migrationDataParams, fmt.Sprintf("network=%s", m.MigrationNetwork.ValueString())) + } + + if len(migrationDataParams) > 0 { + migrationDataValue := strings.Join(migrationDataParams, ",") + + return &migrationDataValue + } + + return nil +} + +func (m *clusterOptionsModel) crsData() *string { + var crsDataParams []string + + if !m.CrsHA.IsNull() && m.CrsHA.ValueString() != "" { + crsDataParams = append(crsDataParams, fmt.Sprintf("ha=%s", m.CrsHA.ValueString())) + } + + if !m.CrsHARebalanceOnStart.IsNull() { + var haRebalanceOnStart string + if m.CrsHARebalanceOnStart.ValueBool() { + haRebalanceOnStart = "1" + } else { + haRebalanceOnStart = "0" + } + + crsDataParams = append(crsDataParams, fmt.Sprintf("ha-rebalance-on-start=%s", haRebalanceOnStart)) + } + + if len(crsDataParams) > 0 { + crsDataValue := strings.Join(crsDataParams, ",") + + return &crsDataValue + } + + return nil +} + +func (m *clusterOptionsModel) bandwidthData() *string { + var bandwidthParams []string + + if !m.BandwidthLimitClone.IsNull() && m.BandwidthLimitClone.ValueInt64() != 0 { + bandwidthParams = append(bandwidthParams, fmt.Sprintf("clone=%d", m.BandwidthLimitClone.ValueInt64())) + } + + if !m.BandwidthLimitDefault.IsNull() && m.BandwidthLimitDefault.ValueInt64() != 0 { + bandwidthParams = append(bandwidthParams, fmt.Sprintf("default=%d", m.BandwidthLimitDefault.ValueInt64())) + } + + if !m.BandwidthLimitMigration.IsNull() && m.BandwidthLimitMigration.ValueInt64() != 0 { + bandwidthParams = append(bandwidthParams, fmt.Sprintf("migration=%d", m.BandwidthLimitMigration.ValueInt64())) + } + + if !m.BandwidthLimitMove.IsNull() && m.BandwidthLimitMove.ValueInt64() != 0 { + bandwidthParams = append(bandwidthParams, fmt.Sprintf("move=%d", m.BandwidthLimitMove.ValueInt64())) + } + + if !m.BandwidthLimitRestore.IsNull() && m.BandwidthLimitRestore.ValueInt64() != 0 { + bandwidthParams = append(bandwidthParams, fmt.Sprintf("restore=%d", m.BandwidthLimitRestore.ValueInt64())) + } + + if len(bandwidthParams) > 0 { + bandwithDataValue := strings.Join(bandwidthParams, ",") + + return &bandwithDataValue + } + + return nil +} + +func (m *clusterOptionsModel) toOptionsRequestBody() *cluster.OptionsRequestData { + body := &cluster.OptionsRequestData{} + + if !m.EmailFrom.IsUnknown() { + body.EmailFrom = m.EmailFrom.ValueStringPointer() + } + + if !m.Keyboard.IsUnknown() { + body.Keyboard = m.Keyboard.ValueStringPointer() + } + + if !m.Language.IsUnknown() { + body.Language = m.Language.ValueStringPointer() + } + + if !m.MaxWorkers.IsUnknown() { + body.MaxWorkers = m.MaxWorkers.ValueInt64Pointer() + } + + if !m.Console.IsUnknown() { + body.Console = m.Console.ValueStringPointer() + } + + if !m.HTTPProxy.IsUnknown() { + body.HTTPProxy = m.HTTPProxy.ValueStringPointer() + } + + if !m.MacPrefix.IsUnknown() { + body.MacPrefix = m.MacPrefix.ValueStringPointer() + } + + if !m.MacPrefix.IsUnknown() { + body.Description = m.Description.ValueStringPointer() + } + + body.HASettings = m.haData() + body.BandwidthLimit = m.bandwidthData() + body.ClusterResourceScheduling = m.crsData() + body.Migration = m.migrationData() + + return body +} + +func (m *clusterOptionsModel) importFromOptionsAPI( + _ context.Context, + iface *cluster.OptionsResponseData, +) error { + m.BandwidthLimitClone = types.Int64Null() + m.BandwidthLimitDefault = types.Int64Null() + m.BandwidthLimitMigration = types.Int64Null() + m.BandwidthLimitMove = types.Int64Null() + m.BandwidthLimitRestore = types.Int64Null() + + //nolint:nestif + if iface.BandwidthLimit != nil { + for _, bandwidth := range strings.Split(*iface.BandwidthLimit, ",") { + bandwidthData := strings.SplitN(bandwidth, "=", 2) + bandwidthName := bandwidthData[0] + + bandwidthLimit, err := strconv.ParseInt(bandwidthData[1], 10, 64) + if err != nil { + return fmt.Errorf("failed to parse bandwidth limit: %s", *iface.BandwidthLimit) + } + + if bandwidthName == "clone" { + m.BandwidthLimitClone = types.Int64Value(bandwidthLimit) + } + + if bandwidthName == "default" { + m.BandwidthLimitDefault = types.Int64Value(bandwidthLimit) + } + + if bandwidthName == "migration" { + m.BandwidthLimitMigration = types.Int64Value(bandwidthLimit) + } + + if bandwidthName == "move" { + m.BandwidthLimitMove = types.Int64Value(bandwidthLimit) + } + + if bandwidthName == "restore" { + m.BandwidthLimitRestore = types.Int64Value(bandwidthLimit) + } + } + } + + m.EmailFrom = types.StringPointerValue(iface.EmailFrom) + m.Keyboard = types.StringPointerValue(iface.Keyboard) + m.Language = types.StringPointerValue(iface.Language) + + if iface.MaxWorkers != nil { + m.MaxWorkers = types.Int64Value(int64(*iface.MaxWorkers)) + } + + m.Console = types.StringPointerValue(iface.Console) + m.HTTPProxy = types.StringPointerValue(iface.HTTPProxy) + m.MacPrefix = types.StringPointerValue(iface.MacPrefix) + m.Description = types.StringPointerValue(iface.Description) + + if iface.HASettings != nil { + m.HAShutdownPolicy = types.StringPointerValue(iface.HASettings.ShutdownPolicy) + } else { + m.HAShutdownPolicy = types.StringPointerValue(nil) + } + + if iface.Migration != nil { + m.MigrationType = types.StringPointerValue(iface.Migration.Type) + m.MigrationNetwork = types.StringPointerValue(iface.Migration.Network) + } else { + m.MigrationType = types.StringPointerValue(nil) + m.MigrationNetwork = types.StringPointerValue(nil) + } + + if iface.ClusterResourceScheduling != nil { + m.CrsHARebalanceOnStart = types.BoolValue(bool(*iface.ClusterResourceScheduling.HaRebalanceOnStart)) + m.CrsHA = types.StringPointerValue(iface.ClusterResourceScheduling.HA) + } else { + m.CrsHARebalanceOnStart = types.BoolPointerValue(nil) + m.CrsHA = types.StringPointerValue(nil) + } + + return nil +} + +// NewClusterOptionsResource manages cluster options resource. +func NewClusterOptionsResource() resource.Resource { + return &clusterOptionsResource{} +} + +type clusterOptionsResource struct { + client proxmox.Client +} + +func (r *clusterOptionsResource) Metadata( + _ context.Context, + req resource.MetadataRequest, + resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_cluster_options" +} + +// Schema defines the schema for the resource. +func (r *clusterOptionsResource) Schema( + _ context.Context, + _ resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = schema.Schema{ + Description: "Manages Proxmox VE Cluster Datacenter options.", + Attributes: map[string]schema.Attribute{ + "id": structure.IDAttribute(), + "email_from": schema.StringAttribute{ + Description: "email address to send notification from (default is root@$hostname).", + Optional: true, + Computed: true, + }, + "keyboard": schema.StringAttribute{ + Description: "Default keyboard layout for vnc server.", + MarkdownDescription: "Default keyboard layout for vnc server. Must be `de` | " + + "`de-ch` | `da` | `en-gb` | `en-us` | `es` | `fi` | `fr` | `fr-be` | `fr-ca` " + + "| `fr-ch` | `hu` | `is` | `it` | `ja` | `lt` | `mk` | `nl` | `no` | `pl` | " + + "`pt` | `pt-br` | `sv` | `sl` | `tr`.", + Optional: true, + Computed: true, + Validators: []validator.String{validators.KeyboardLayoutValidator()}, + }, + "max_workers": schema.Int64Attribute{ + Description: "Defines how many workers (per node) are maximal started on" + + " actions like 'stopall VMs' or task from the ha-manager.", + Optional: true, + Computed: true, + }, + "language": schema.StringAttribute{ + Description: "Default GUI language.", + MarkdownDescription: "Default GUI language. Must be `ca` | `da` | `de` " + + "| `en` | `es` | `eu` | `fa` | `fr` | `he` | `it` | `ja` | `nb` | " + + "`nn` | `pl` | `pt_BR` | `ru` | `sl` | `sv` | `tr` | `zh_CN` | `zh_TW`.", + Optional: true, + Computed: true, + Validators: []validator.String{validators.LanguageValidator()}, + }, + "console": schema.StringAttribute{ + Description: "Select the default Console viewer.", + MarkdownDescription: "Select the default Console viewer. " + + "Must be `applet` | `vv`| `html5` | `xtermjs`. " + + "You can either use the builtin java applet (VNC; deprecated and maps to html5), " + + "an external virt-viewer compatible application (SPICE), " + + "an HTML5 based vnc viewer (noVNC), " + + "or an HTML5 based console client (xtermjs). " + + "If the selected viewer is not available " + + "(e.g. SPICE not activated for the VM), " + + "the fallback is noVNC.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.OneOf([]string{ + "applet", + "vv", + "html5", + "xtermjs", + }...)}, + }, + "http_proxy": schema.StringAttribute{ + Description: "Specify external http proxy which is used for downloads.", + MarkdownDescription: "Specify external http proxy which is used for downloads " + + "(example: `http://username:password@host:port/`).", + Optional: true, + Computed: true, + }, + "mac_prefix": schema.StringAttribute{ + Description: "Prefix for autogenerated MAC addresses.", + Optional: true, + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Datacenter description. Shown in the web-interface datacenter notes panel. " + + "This is saved as comment inside the configuration file.", + Optional: true, + Computed: true, + }, + "ha_shutdown_policy": schema.StringAttribute{ + Description: "Cluster wide HA shutdown policy.", + MarkdownDescription: "Cluster wide HA shutdown policy. " + + "Must be `freeze` | `failover` | `migrate` | `conditional`.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.OneOf([]string{ + "freeze", + "failover", + "migrate", + "conditional", + }...)}, + }, + "migration_type": schema.StringAttribute{ + Description: "Cluster wide migration type.", + MarkdownDescription: "Cluster wide migration type. Must be `secure` | `unsecure`.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.OneOf([]string{ + "secure", + "unsecure", + }...)}, + }, + "migration_cidr": schema.StringAttribute{ + Description: "Cluster wide migration network CIDR.", + Optional: true, + Computed: true, + }, + "crs_ha": schema.StringAttribute{ + Description: "Cluster resource scheduling setting for HA.", + MarkdownDescription: "Cluster resource scheduling setting for HA. Must be `static` | `basic`.", + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.OneOf([]string{ + "static", + "basic", + }...)}, + }, + "crs_ha_rebalance_on_start": schema.BoolAttribute{ + Description: "Cluster resource scheduling setting for HA rebalance on start.", + Optional: true, + Computed: true, + }, + "bandwidth_limit_clone": schema.Int64Attribute{ + Description: "Clone I/O bandwidth limit in KiB/s.", + Optional: true, + Computed: true, + }, + "bandwidth_limit_default": schema.Int64Attribute{ + Description: "Default I/O bandwidth limit in KiB/s.", + Optional: true, + Computed: true, + }, + "bandwidth_limit_migration": schema.Int64Attribute{ + Description: "Migration I/O bandwidth limit in KiB/s.", + Optional: true, + Computed: true, + }, + "bandwidth_limit_move": schema.Int64Attribute{ + Description: "Move I/O bandwidth limit in KiB/s.", + Optional: true, + Computed: true, + }, + "bandwidth_limit_restore": schema.Int64Attribute{ + Description: "Restore I/O bandwidth limit in KiB/s.", + Optional: true, + Computed: true, + }, + }, + } +} + +func (r *clusterOptionsResource) Configure( + _ context.Context, + req resource.ConfigureRequest, + resp *resource.ConfigureResponse, +) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(proxmox.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *proxmox.Client, got: %T. Please report this issue to the provider developers.", + req.ProviderData), + ) + + return + } + + r.client = client +} + +// Create update must-existing cluster options interface. +// +//nolint:lll +func (r *clusterOptionsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan clusterOptionsModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + body := plan.toOptionsRequestBody() + + err := r.client.Cluster().CreateUpdateOptions(ctx, body) + if err != nil { + resp.Diagnostics.AddError( + "Error creating cluster options interface", + "Could not create cluster options, unexpected error: "+err.Error(), + ) + + return + } + + plan.ID = types.StringValue("cluster") + + r.read(ctx, &plan, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) +} + +func (r *clusterOptionsResource) read(ctx context.Context, model *clusterOptionsModel, diags *diag.Diagnostics) { + options, err := r.client.Cluster().GetOptions(ctx) + if err != nil { + diags.AddError( + "Error get cluster options", + "Could not get cluster options, unexpected error: "+err.Error(), + ) + + return + } + + err = model.importFromOptionsAPI(ctx, options) + + if err != nil { + diags.AddError( + "Error converting cluster options interface to a model", + "Could not import cluster options from API response, unexpected error: "+err.Error(), + ) + + return + } +} + +// Read reads a cluster options interface. +func (r *clusterOptionsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // Get current state + var state clusterOptionsModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + r.read(ctx, &state, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} + +// Update updates a cluster options interface. +// +//nolint:lll +func (r *clusterOptionsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state clusterOptionsModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + body := plan.toOptionsRequestBody() + + var toDelete []string + + if !plan.Keyboard.Equal(state.Keyboard) && plan.Keyboard.ValueString() == "" { + toDelete = append(toDelete, "keyboard") + } + + if (plan.bandwidthData() == nil && state.bandwidthData() != nil) || (*plan.bandwidthData() != *state.bandwidthData() && *plan.bandwidthData() == "") { + toDelete = append(toDelete, "bwlimit") + } + + if (plan.crsData() == nil && state.crsData() != nil) || (*plan.crsData() != *state.crsData() && *plan.crsData() == "") { + toDelete = append(toDelete, "crs") + } + + if (plan.haData() == nil && state.haData() != nil) || (*plan.haData() != *state.haData() && *plan.haData() == "") { + toDelete = append(toDelete, "ha") + } + + if (plan.migrationData() == nil && state.migrationData() != nil) || (*plan.migrationData() != *state.migrationData() && *plan.migrationData() == "") { + toDelete = append(toDelete, "migration") + } + + if !plan.EmailFrom.Equal(state.EmailFrom) && plan.EmailFrom.ValueString() == "" { + toDelete = append(toDelete, "email_from") + } + + if !plan.Language.Equal(state.Language) && plan.Language.ValueString() == "" { + toDelete = append(toDelete, "language") + } + + if !plan.Console.Equal(state.Console) && plan.Console.ValueString() == "" { + toDelete = append(toDelete, "console") + } + + if !plan.HTTPProxy.Equal(state.HTTPProxy) && plan.HTTPProxy.ValueString() == "" { + toDelete = append(toDelete, "http_proxy") + } + + if !plan.MacPrefix.Equal(state.MacPrefix) && plan.MacPrefix.ValueString() == "" { + toDelete = append(toDelete, "mac_prefix") + } + + if !plan.Description.Equal(state.Description) && plan.Description.ValueString() == "" { + toDelete = append(toDelete, "description") + } + + if !plan.MaxWorkers.Equal(state.MaxWorkers) && plan.MaxWorkers.ValueInt64() == 0 { + toDelete = append(toDelete, "max_workers") + } + + if len(toDelete) > 0 { + d := strings.Join(toDelete, ",") + body.Delete = &d + } + + err := r.client.Cluster().CreateUpdateOptions(ctx, body) + if err != nil { + resp.Diagnostics.AddError( + "Error updating cluster options interface", + "Could not update cluster options, unexpected error: "+err.Error(), + ) + + return + } + + r.read(ctx, &plan, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +// Delete deletes a cluster options interface. +// +//nolint:lll +func (r *clusterOptionsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state clusterOptionsModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } +} + +// Imports a cluster options interface. +func (r *clusterOptionsResource) ImportState( + ctx context.Context, + req resource.ImportStateRequest, + resp *resource.ImportStateResponse, +) { + state := clusterOptionsModel{ID: types.StringValue(req.ID)} + r.read(ctx, &state, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + diags := resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9ebabfb0..da9e69f3 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -367,6 +367,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc return []func() resource.Resource{ cluster.NewHAGroupResource, cluster.NewHAResourceResource, + cluster.NewClusterOptionsResource, network.NewLinuxBridgeResource, network.NewLinuxVLANResource, } diff --git a/internal/structure/attribute.go b/internal/structure/attribute.go index c02052aa..462141fc 100644 --- a/internal/structure/attribute.go +++ b/internal/structure/attribute.go @@ -19,7 +19,7 @@ func IDAttribute(desc ...string) schema.StringAttribute { PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, - Description: "The unique identifier for the resource.", + Description: "The unique identifier of this resource.", } if len(desc) > 0 { diff --git a/internal/test/cluster/cluster_options_test.go b/internal/test/cluster/cluster_options_test.go new file mode 100644 index 00000000..a7d39f98 --- /dev/null +++ b/internal/test/cluster/cluster_options_test.go @@ -0,0 +1,97 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package cluster + +import ( + "context" + "testing" + + "github.com/bpg/terraform-provider-proxmox/internal/test" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestClusterOptionsResource(t *testing.T) { + t.Parallel() + + accProviders := test.AccMuxProviders(context.Background(), t) + + resourceName := "proxmox_virtual_environment_cluster_options.test_options" + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: accProviders, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: test.ProviderConfig + ` +resource "proxmox_virtual_environment_cluster_options" "test_options" { + language = "en" + keyboard = "pl" + email_from = "example@example.com" + bandwidth_limit_migration = 555554 + bandwidth_limit_default = 666666 + max_workers = 5 + crs_ha = "static" + ha_shutdown_policy = "freeze" + migration_cidr = "10.0.0.0/8" + migration_type = "secure" +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "language", "en"), + resource.TestCheckResourceAttr(resourceName, "keyboard", "pl"), + resource.TestCheckResourceAttr(resourceName, "email_from", "example@example.com"), + resource.TestCheckResourceAttr(resourceName, "bandwidth_limit_migration", "555554"), + resource.TestCheckResourceAttr(resourceName, "bandwidth_limit_default", "666666"), + resource.TestCheckResourceAttr(resourceName, "max_workers", "5"), + resource.TestCheckResourceAttr(resourceName, "crs_ha", "static"), + resource.TestCheckResourceAttr(resourceName, "ha_shutdown_policy", "freeze"), + resource.TestCheckResourceAttr(resourceName, "migration_cidr", "10.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "migration_type", "secure"), + resource.TestCheckResourceAttr(resourceName, "id", "cluster"), + resource.TestCheckNoResourceAttr(resourceName, "bandwidth_limit_restore"), + resource.TestCheckNoResourceAttr(resourceName, "bandwidth_limit_move"), + ), + }, + // ImportState testing + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // Update testing + { + Config: test.ProviderConfig + ` +resource "proxmox_virtual_environment_cluster_options" "test_options" { + language = "en" + keyboard = "pl" + email_from = "ged@gont.earthsea" + bandwidth_limit_migration = 111111 + bandwidth_limit_default = 666666 + max_workers = 6 + migration_cidr = "10.0.0.0/8" + migration_type = "secure" +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "language", "en"), + resource.TestCheckResourceAttr(resourceName, "keyboard", "pl"), + resource.TestCheckResourceAttr(resourceName, "email_from", "ged@gont.earthsea"), + resource.TestCheckResourceAttr(resourceName, "bandwidth_limit_migration", "111111"), + resource.TestCheckResourceAttr(resourceName, "bandwidth_limit_default", "666666"), + resource.TestCheckResourceAttr(resourceName, "max_workers", "6"), + resource.TestCheckResourceAttr(resourceName, "migration_cidr", "10.0.0.0/8"), + resource.TestCheckResourceAttr(resourceName, "migration_type", "secure"), + resource.TestCheckResourceAttr(resourceName, "id", "cluster"), + resource.TestCheckNoResourceAttr(resourceName, "bandwidth_limit_restore"), + resource.TestCheckNoResourceAttr(resourceName, "bandwidth_limit_move"), + resource.TestCheckNoResourceAttr(resourceName, "crs_ha"), + resource.TestCheckNoResourceAttr(resourceName, "ha_shutdown_policy"), + ), + }, + }, + }) +} diff --git a/internal/validators/i18n.go b/internal/validators/i18n.go new file mode 100644 index 00000000..00ef6015 --- /dev/null +++ b/internal/validators/i18n.go @@ -0,0 +1,28 @@ +/* + * 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 validators + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" +) + +// LanguageValidator returns a new validator for language codes. +func LanguageValidator() validator.String { + return stringvalidator.OneOf([]string{ + `ca`, `da`, `de`, `en`, `es`, `eu`, `fa`, `fr`, `he`, `it`, `ja`, `nb`, + `nn`, `pl`, `pt_BR`, `ru`, `sl`, `sv`, `tr`, `zh_CN`, `zh_TW`, + }...) +} + +// KeyboardLayoutValidator returns a new validator for keyboard layouts. +func KeyboardLayoutValidator() validator.String { + return stringvalidator.OneOf([]string{ + `de`, `de-ch`, `da`, `en-gb`, `en-us`, `es`, `fi`, `fr`, `fr-be`, `fr-ca`, `fr-ch`, + `hu`, `is`, `it`, `ja`, `lt`, `mk`, `nl`, `no`, `pl`, `pt`, `pt-br`, `sv`, `sl`, `tr`, + }...) +} diff --git a/proxmox/cluster/options.go b/proxmox/cluster/options.go new file mode 100644 index 00000000..b636995e --- /dev/null +++ b/proxmox/cluster/options.go @@ -0,0 +1,41 @@ +/* + * 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 cluster + +import ( + "context" + "fmt" + "net/http" + + "github.com/bpg/terraform-provider-proxmox/proxmox/api" +) + +// GetOptions retrieves the cluster options. +func (c *Client) GetOptions(ctx context.Context) (*OptionsResponseData, error) { + resBody := &OptionsResponseBody{} + + err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("options"), nil, resBody) + if err != nil { + return nil, fmt.Errorf("error reading Cluster options: %w", err) + } + + if resBody.Data == nil { + return nil, api.ErrNoDataObjectInResponse + } + + return resBody.Data, nil +} + +// CreateUpdateOptions updates the cluster options. +func (c *Client) CreateUpdateOptions(ctx context.Context, data *OptionsRequestData) error { + err := c.DoRequest(ctx, http.MethodPut, c.ExpandPath("options"), data, nil) + if err != nil { + return fmt.Errorf("error updating Cluster resource: %w", err) + } + + return nil +} diff --git a/proxmox/cluster/options_types.go b/proxmox/cluster/options_types.go new file mode 100644 index 00000000..8de486bb --- /dev/null +++ b/proxmox/cluster/options_types.go @@ -0,0 +1,92 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package cluster + +import ( + "github.com/bpg/terraform-provider-proxmox/proxmox/types" +) + +type userTagAccess struct { + UserAllowList *[]string `json:"user-allow-list,omitempty"` + UserAllow *string `json:"user-allow,omitempty"` +} +type tagStyle struct { + Shape *string `json:"shape,omitempty"` + CaseSensitive *types.CustomBool `json:"case-sensitive,omitempty"` + Ordering *string `json:"ordering,omitempty"` + ColorMap *string `json:"color-map,omitempty"` +} +type crs struct { + HaRebalanceOnStart *types.CustomBool `json:"ha-rebalance-on-start,omitempty"` + HA *string `json:"ha,omitempty"` +} +type notify struct { + PackageUpdates *string `json:"package-updates,omitempty"` +} +type migration struct { + Network *string `json:"network,omitempty"` + Type *string `json:"type,omitempty"` +} + +type haSettings struct { + ShutdownPolicy *string `json:"shutdown_policy,omitempty"` +} +type nextID struct { + Upper *string `json:"upper,omitempty"` + Lower *string `json:"lower,omitempty"` +} +type webauthn struct { + ID *string `json:"id,omitempty"` + Origin *string `json:"origin,omitempty"` + RP *string `json:"rp,omitempty"` +} +type optionsBaseData struct { + BandwidthLimit *string `json:"bwlimit,omitempty" url:"bwlimit,omitempty"` + EmailFrom *string `json:"email_from,omitempty" url:"email_from,omitempty"` + Description *string `json:"description,omitempty" url:"description,omitempty"` + Console *string `json:"console,omitempty" url:"console,omitempty"` + HTTPProxy *string `json:"http_proxy,omitempty" url:"http_proxy,omitempty"` + MacPrefix *string `json:"mac_prefix,omitempty" url:"mac_prefix,omitempty"` + Keyboard *string `json:"keyboard,omitempty" url:"keyboard,omitempty"` + Language *string `json:"language,omitempty" url:"language,omitempty"` +} + +// OptionsResponseBody contains the body from a cluster options response. +type OptionsResponseBody struct { + Data *OptionsResponseData `json:"data,omitempty"` +} + +// OptionsResponseData contains the data from a cluster options response. +type OptionsResponseData struct { + optionsBaseData + MaxWorkers *types.CustomInt `json:"max_workers,omitempty"` + ClusterResourceScheduling *crs `json:"crs,omitempty"` + HASettings *haSettings `json:"ha,omitempty"` + TagStyle *tagStyle `json:"tag-style,omitempty"` + Migration *migration `json:"migration,omitempty"` + Webauthn *webauthn `json:"webauthn,omitempty"` + NextID *nextID `json:"next-id,omitempty"` + Notify *notify `json:"notify,omitempty"` + UserTagAccess *userTagAccess `json:"user-tag-access,omitempty"` + RegisteredTags *[]string `json:"registered-tags,omitempty"` +} + +// OptionsRequestData contains the body for cluster options request. +type OptionsRequestData struct { + optionsBaseData + MaxWorkers *int64 `json:"max_workers,omitempty" url:"max_workers,omitempty"` + Delete *string `json:"delete,omitempty" url:"delete,omitempty"` + ClusterResourceScheduling *string `json:"crs,omitempty" url:"crs,omitempty"` + HASettings *string `json:"ha,omitempty" url:"ha,omitempty"` + TagStyle *string `json:"tag-style,omitempty" url:"tag-style,omitempty"` + Migration *string `json:"migration,omitempty" url:"migration,omitempty"` + Webauthn *string `json:"webauthn,omitempty" url:"webauthn,omitempty"` + NextID *string `json:"next-id,omitempty" url:"next-id,omitempty"` + Notify *string `json:"notify,omitempty" url:"notify,omitempty"` + UserTagAccess *string `json:"user-tag-access,omitempty" url:"user-tag-access,omitempty"` + RegisteredTags *string `json:"registered-tags,omitempty" url:"registered-tags,omitempty"` +} diff --git a/tools/tools.go b/tools/tools.go index 235b4c44..a519932e 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -1,12 +1,12 @@ +//go:build tools +// +build tools + /* * 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/. */ -//go:build tools -// +build tools - package tools // Manage tool dependencies via go.mod. @@ -37,3 +37,4 @@ import ( //go:generate cp ../build/docs-gen/resources/virtual_environment_network_linux_vlan.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_hagroup.md ../docs/resources/ //go:generate cp ../build/docs-gen/resources/virtual_environment_haresource.md ../docs/resources/ +//go:generate cp ../build/docs-gen/resources/virtual_environment_cluster_options.md ../docs/resources/