mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 18:42:58 +00:00
fix(provider): fix insecure migration_type validation Signed-off-by: dgriswo <daniel@griswoldcomputing.com>
995 lines
31 KiB
Go
995 lines
31 KiB
Go
/*
|
|
* 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 fwprovider
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
|
"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/fwprovider/attribute"
|
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/config"
|
|
"github.com/bpg/terraform-provider-proxmox/fwprovider/validators"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/cluster"
|
|
)
|
|
|
|
const (
|
|
// ClusterOptionsNextIDLowerMaximum is the maximum number for the "lower" range for the next VM ID option.
|
|
// Note that this value is not documented in the section about the cluster options in the Proxmox VE API explorer but
|
|
// [in the sections about QEMU (POST)] as well as [the dedicated Proxmox VE documentations about QEMU/KVM].
|
|
//
|
|
// [in the sections about QEMU (POST)]: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu
|
|
// [the dedicated Proxmox VE documentations about QEMU/KVM]: https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_strong_qm_strong_qemu_kvm_virtual_machine_manager
|
|
ClusterOptionsNextIDLowerMaximum = 999999999
|
|
|
|
// ClusterOptionsNextIDLowerMinimum is the minimum number for the "lower" range for the next VM ID option.
|
|
// Note that this value is not documented in the section about the cluster options in the Proxmox VE API explorer but
|
|
// [in the sections about QEMU (POST)] as well as [the dedicated Proxmox VE documentations about QEMU/KVM].
|
|
//
|
|
// [in the sections about QEMU (POST)]: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu
|
|
// [the dedicated Proxmox VE documentations about QEMU/KVM]: https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_strong_qm_strong_qemu_kvm_virtual_machine_manager
|
|
ClusterOptionsNextIDLowerMinimum = 100
|
|
)
|
|
|
|
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"`
|
|
CrsHA types.String `tfsdk:"crs_ha"`
|
|
CrsHARebalanceOnStart types.Bool `tfsdk:"crs_ha_rebalance_on_start"`
|
|
Description types.String `tfsdk:"description"`
|
|
EmailFrom types.String `tfsdk:"email_from"`
|
|
HAShutdownPolicy types.String `tfsdk:"ha_shutdown_policy"`
|
|
HTTPProxy types.String `tfsdk:"http_proxy"`
|
|
Keyboard types.String `tfsdk:"keyboard"`
|
|
Language types.String `tfsdk:"language"`
|
|
MacPrefix types.String `tfsdk:"mac_prefix"`
|
|
MaxWorkers types.Int64 `tfsdk:"max_workers"`
|
|
MigrationNetwork types.String `tfsdk:"migration_cidr"`
|
|
MigrationType types.String `tfsdk:"migration_type"`
|
|
NextID *clusterOptionsNextIDModel `tfsdk:"next_id"`
|
|
Notify *clusterOptionsNotifyModel `tfsdk:"notify"`
|
|
}
|
|
|
|
type clusterOptionsNextIDModel struct {
|
|
Lower types.Int64 `tfsdk:"lower"`
|
|
Upper types.Int64 `tfsdk:"upper"`
|
|
}
|
|
|
|
type clusterOptionsNotifyModel struct {
|
|
HAFencingMode types.String `tfsdk:"ha_fencing_mode"`
|
|
HAFencingTarget types.String `tfsdk:"ha_fencing_target"`
|
|
PackageUpdates types.String `tfsdk:"package_updates"`
|
|
PackageUpdatesTarget types.String `tfsdk:"package_updates_target"`
|
|
Replication types.String `tfsdk:"replication"`
|
|
ReplicationTarget types.String `tfsdk:"replication_target"`
|
|
}
|
|
|
|
// haData returns HA settings parameter string for API, HA settings are
|
|
// defined, otherwise empty string is returned.
|
|
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 {
|
|
return strings.Join(haDataParams, ",")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// migrationData returns migration settings parameter string for API, if any of migration
|
|
// settings are defined, otherwise empty string is returned.
|
|
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 {
|
|
return strings.Join(migrationDataParams, ",")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// nextIDData returns settings for the "next-id" parameter string of the Proxmox VE API, if defined, otherwise an empty
|
|
// string is returned.
|
|
func (m *clusterOptionsModel) nextIDData() string {
|
|
var nextIDDataParams []string
|
|
|
|
if m.NextID == nil {
|
|
return ""
|
|
}
|
|
|
|
if !m.NextID.Lower.IsNull() {
|
|
nextIDDataParams = append(nextIDDataParams, fmt.Sprintf("lower=%d", m.NextID.Lower.ValueInt64()))
|
|
}
|
|
|
|
if !m.NextID.Upper.IsNull() {
|
|
nextIDDataParams = append(nextIDDataParams, fmt.Sprintf("upper=%d", m.NextID.Upper.ValueInt64()))
|
|
}
|
|
|
|
if len(nextIDDataParams) > 0 {
|
|
return strings.Join(nextIDDataParams, ",")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// notifyData returns settings for the "notify" parameter string of the Proxmox VE API, if defined, otherwise an empty
|
|
// string is returned.
|
|
func (m *clusterOptionsModel) notifyData() string {
|
|
var notifyDataParams []string
|
|
|
|
if m.Notify == nil {
|
|
return ""
|
|
}
|
|
|
|
if !m.Notify.HAFencingMode.IsNull() {
|
|
notifyDataParams = append(notifyDataParams, fmt.Sprintf("fencing=%s", m.Notify.HAFencingMode.ValueString()))
|
|
}
|
|
|
|
if !m.Notify.HAFencingTarget.IsNull() {
|
|
notifyDataParams = append(
|
|
notifyDataParams,
|
|
fmt.Sprintf("target-fencing=%s", m.Notify.HAFencingTarget.ValueString()),
|
|
)
|
|
}
|
|
|
|
if !m.Notify.PackageUpdates.IsNull() {
|
|
notifyDataParams = append(
|
|
notifyDataParams,
|
|
fmt.Sprintf("package-updates=%s", m.Notify.PackageUpdates.ValueString()),
|
|
)
|
|
}
|
|
|
|
if !m.Notify.PackageUpdatesTarget.IsNull() {
|
|
notifyDataParams = append(
|
|
notifyDataParams,
|
|
fmt.Sprintf("target-package-updates=%s", m.Notify.PackageUpdatesTarget.ValueString()),
|
|
)
|
|
}
|
|
|
|
if !m.Notify.Replication.IsNull() {
|
|
notifyDataParams = append(notifyDataParams, fmt.Sprintf("replication=%s", m.Notify.Replication.ValueString()))
|
|
}
|
|
|
|
if !m.Notify.ReplicationTarget.IsNull() {
|
|
notifyDataParams = append(
|
|
notifyDataParams,
|
|
fmt.Sprintf("target-replication=%s", m.Notify.ReplicationTarget.ValueString()),
|
|
)
|
|
}
|
|
|
|
if len(notifyDataParams) > 0 {
|
|
return strings.Join(notifyDataParams, ",")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// crsData returns cluster resource scheduling settings parameter string for API, if any of cluster resource scheduling
|
|
// settings are defined, otherwise empty string is returned.
|
|
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 {
|
|
return strings.Join(crsDataParams, ",")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// bandwidthData returns bandwidth limit settings parameter string for API, if any of bandwidth
|
|
// limit settings are defined, otherwise empty string is returned.
|
|
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 {
|
|
return strings.Join(bandwidthParams, ",")
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
nextIDData := m.nextIDData()
|
|
if nextIDData != "" {
|
|
body.NextID = &nextIDData
|
|
}
|
|
|
|
notifyData := m.notifyData()
|
|
if notifyData != "" {
|
|
body.Notify = ¬ifyData
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
haData := m.haData()
|
|
if haData != "" {
|
|
body.HASettings = &haData
|
|
}
|
|
|
|
bandwidthData := m.bandwidthData()
|
|
if bandwidthData != "" {
|
|
body.BandwidthLimit = &bandwidthData
|
|
}
|
|
|
|
crsData := m.crsData()
|
|
if crsData != "" {
|
|
body.ClusterResourceScheduling = &crsData
|
|
}
|
|
|
|
migrationData := m.migrationData()
|
|
if migrationData != "" {
|
|
body.Migration = &migrationData
|
|
}
|
|
|
|
return body
|
|
}
|
|
|
|
func (m *clusterOptionsModel) importFromOptionsAPI(_ context.Context, opts *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 opts.BandwidthLimit != nil {
|
|
for _, bandwidth := range strings.Split(*opts.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", *opts.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(opts.EmailFrom)
|
|
m.Keyboard = types.StringPointerValue(opts.Keyboard)
|
|
m.Language = types.StringPointerValue(opts.Language)
|
|
|
|
if opts.MaxWorkers != nil {
|
|
value := int64(*opts.MaxWorkers)
|
|
m.MaxWorkers = types.Int64PointerValue(&value)
|
|
} else {
|
|
m.MaxWorkers = types.Int64Null()
|
|
}
|
|
|
|
m.Console = types.StringPointerValue(opts.Console)
|
|
m.HTTPProxy = types.StringPointerValue(opts.HTTPProxy)
|
|
m.MacPrefix = types.StringPointerValue(opts.MacPrefix)
|
|
|
|
if opts.Description != nil && *opts.Description != "" {
|
|
m.Description = types.StringPointerValue(opts.Description)
|
|
} else {
|
|
m.Description = types.StringNull()
|
|
}
|
|
|
|
if opts.HASettings != nil {
|
|
m.HAShutdownPolicy = types.StringPointerValue(opts.HASettings.ShutdownPolicy)
|
|
} else {
|
|
m.HAShutdownPolicy = types.StringNull()
|
|
}
|
|
|
|
if opts.Migration != nil {
|
|
m.MigrationType = types.StringPointerValue(opts.Migration.Type)
|
|
m.MigrationNetwork = types.StringPointerValue(opts.Migration.Network)
|
|
} else {
|
|
m.MigrationType = types.StringNull()
|
|
m.MigrationNetwork = types.StringNull()
|
|
}
|
|
|
|
if opts.NextID != nil {
|
|
m.NextID = &clusterOptionsNextIDModel{}
|
|
m.NextID.Lower = types.Int64PointerValue(opts.NextID.Lower.PointerInt64())
|
|
m.NextID.Upper = types.Int64PointerValue(opts.NextID.Upper.PointerInt64())
|
|
}
|
|
|
|
if opts.Notify != nil {
|
|
m.Notify = &clusterOptionsNotifyModel{}
|
|
m.Notify.HAFencingMode = types.StringPointerValue(opts.Notify.HAFencingMode)
|
|
m.Notify.HAFencingTarget = types.StringPointerValue(opts.Notify.HAFencingTarget)
|
|
m.Notify.PackageUpdates = types.StringPointerValue(opts.Notify.PackageUpdates)
|
|
m.Notify.PackageUpdatesTarget = types.StringPointerValue(opts.Notify.PackageUpdatesTarget)
|
|
m.Notify.Replication = types.StringPointerValue(opts.Notify.Replication)
|
|
m.Notify.ReplicationTarget = types.StringPointerValue(opts.Notify.ReplicationTarget)
|
|
}
|
|
|
|
if opts.ClusterResourceScheduling != nil {
|
|
m.CrsHARebalanceOnStart = types.BoolPointerValue(opts.ClusterResourceScheduling.HaRebalanceOnStart.PointerBool())
|
|
m.CrsHA = types.StringPointerValue(opts.ClusterResourceScheduling.HA)
|
|
} else {
|
|
m.CrsHARebalanceOnStart = types.BoolNull()
|
|
m.CrsHA = types.StringNull()
|
|
}
|
|
|
|
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": attribute.ID(),
|
|
"email_from": schema.StringAttribute{
|
|
Description: "email address to send notification from (default is root@$hostname).",
|
|
Optional: 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,
|
|
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,
|
|
},
|
|
"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,
|
|
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,
|
|
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,
|
|
},
|
|
"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,
|
|
},
|
|
"ha_shutdown_policy": schema.StringAttribute{
|
|
Description: "Cluster wide HA shutdown policy.",
|
|
MarkdownDescription: "Cluster wide HA shutdown policy (). " +
|
|
"Must be `freeze` | `failover` | `migrate` | `conditional` (default is `conditional`).",
|
|
Optional: 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` | `insecure` " +
|
|
"(default is `secure`).",
|
|
Optional: true,
|
|
Validators: []validator.String{stringvalidator.OneOf([]string{
|
|
"secure",
|
|
"insecure",
|
|
}...)},
|
|
},
|
|
"migration_cidr": schema.StringAttribute{
|
|
Description: "Cluster wide migration network CIDR.",
|
|
Optional: true,
|
|
},
|
|
"next_id": schema.SingleNestedAttribute{
|
|
Attributes: map[string]schema.Attribute{
|
|
"lower": schema.Int64Attribute{
|
|
Description: "The minimum number for the next free VM ID.",
|
|
MarkdownDescription: "The minimum number for the next free VM ID. " +
|
|
fmt.Sprintf("Must be higher or equal to %d", ClusterOptionsNextIDLowerMinimum),
|
|
Optional: true,
|
|
Validators: []validator.Int64{
|
|
int64validator.AtLeast(ClusterOptionsNextIDLowerMinimum),
|
|
},
|
|
},
|
|
"upper": schema.Int64Attribute{
|
|
Description: "The maximum number for the next free VM ID.",
|
|
MarkdownDescription: "The maximum number for the next free VM ID. " +
|
|
fmt.Sprintf("Must be less or equal to %d", ClusterOptionsNextIDLowerMaximum),
|
|
Optional: true,
|
|
Validators: []validator.Int64{
|
|
int64validator.AtMost(ClusterOptionsNextIDLowerMaximum),
|
|
},
|
|
},
|
|
},
|
|
Description: "The ranges for the next free VM ID auto-selection pool.",
|
|
Optional: true,
|
|
},
|
|
"notify": schema.SingleNestedAttribute{
|
|
Attributes: map[string]schema.Attribute{
|
|
"ha_fencing_mode": schema.StringAttribute{
|
|
Description: "Cluster-wide notification settings for the HA fencing mode.",
|
|
MarkdownDescription: "Cluster-wide notification settings for the HA fencing mode. Must be `always` | `never`.",
|
|
Optional: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.OneOf(
|
|
[]string{
|
|
"always",
|
|
"never",
|
|
}...,
|
|
),
|
|
},
|
|
},
|
|
"ha_fencing_target": schema.StringAttribute{
|
|
Description: "Cluster-wide notification settings for the HA fencing target.",
|
|
MarkdownDescription: "Cluster-wide notification settings for the HA fencing target.",
|
|
Optional: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.UTF8LengthAtLeast(1),
|
|
stringvalidator.RegexMatches(regexp.MustCompile(`^\S|^$`), "must not start with whitespace"),
|
|
stringvalidator.RegexMatches(regexp.MustCompile(`\S$|^$`), "must not end with whitespace"),
|
|
},
|
|
},
|
|
"package_updates": schema.StringAttribute{
|
|
Description: "Cluster-wide notification settings for package updates.",
|
|
MarkdownDescription: "Cluster-wide notification settings for package updates. " +
|
|
"Must be `auto` | `always` | `never`. ",
|
|
Optional: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.OneOf(
|
|
[]string{
|
|
"auto",
|
|
"always",
|
|
"never",
|
|
}...,
|
|
),
|
|
},
|
|
},
|
|
"package_updates_target": schema.StringAttribute{
|
|
Description: "Cluster-wide notification settings for the package updates target.",
|
|
MarkdownDescription: "Cluster-wide notification settings for the package updates target.",
|
|
Optional: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.UTF8LengthAtLeast(1),
|
|
stringvalidator.RegexMatches(regexp.MustCompile(`^\S|^$`), "must not start with whitespace"),
|
|
stringvalidator.RegexMatches(regexp.MustCompile(`\S$|^$`), "must not end with whitespace"),
|
|
},
|
|
},
|
|
"replication": schema.StringAttribute{
|
|
Description: "Cluster-wide notification settings for replication.",
|
|
MarkdownDescription: "Cluster-wide notification settings for replication. " +
|
|
"Must be `always` | `never`. ",
|
|
Optional: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.OneOf(
|
|
[]string{
|
|
"always",
|
|
"never",
|
|
}...,
|
|
),
|
|
},
|
|
},
|
|
"replication_target": schema.StringAttribute{
|
|
Description: "Cluster-wide notification settings for the replication target.",
|
|
MarkdownDescription: "Cluster-wide notification settings for the replication target.",
|
|
Optional: true,
|
|
Validators: []validator.String{
|
|
stringvalidator.UTF8LengthAtLeast(1),
|
|
stringvalidator.RegexMatches(regexp.MustCompile(`^\S|^$`), "must not start with whitespace"),
|
|
stringvalidator.RegexMatches(regexp.MustCompile(`\S$|^$`), "must not end with whitespace"),
|
|
},
|
|
},
|
|
},
|
|
Description: "Cluster-wide notification settings.",
|
|
Optional: true,
|
|
},
|
|
"crs_ha": schema.StringAttribute{
|
|
Description: "Cluster resource scheduling setting for HA.",
|
|
MarkdownDescription: "Cluster resource scheduling setting for HA. Must be `static` | `basic` (default is `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,
|
|
},
|
|
"bandwidth_limit_clone": schema.Int64Attribute{
|
|
Description: "Clone I/O bandwidth limit in KiB/s.",
|
|
Optional: true,
|
|
},
|
|
"bandwidth_limit_default": schema.Int64Attribute{
|
|
Description: "Default I/O bandwidth limit in KiB/s.",
|
|
Optional: true,
|
|
},
|
|
"bandwidth_limit_migration": schema.Int64Attribute{
|
|
Description: "Migration I/O bandwidth limit in KiB/s.",
|
|
Optional: true,
|
|
},
|
|
"bandwidth_limit_move": schema.Int64Attribute{
|
|
Description: "Move I/O bandwidth limit in KiB/s.",
|
|
Optional: true,
|
|
},
|
|
"bandwidth_limit_restore": schema.Int64Attribute{
|
|
Description: "Restore I/O bandwidth limit in KiB/s.",
|
|
Optional: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (r *clusterOptionsResource) 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 *proxmox.Client, got: %T", req.ProviderData),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
r.client = cfg.Client
|
|
}
|
|
|
|
// Create update must-existing cluster options.
|
|
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",
|
|
"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 to a model",
|
|
"Could not import cluster options from API response, unexpected error: "+err.Error(),
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// Read reads cluster options.
|
|
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 cluster options.
|
|
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() != state.bandwidthData() && plan.bandwidthData() == "" {
|
|
toDelete = append(toDelete, "bwlimit")
|
|
}
|
|
|
|
if plan.crsData() != state.crsData() && plan.crsData() == "" {
|
|
toDelete = append(toDelete, "crs")
|
|
}
|
|
|
|
if plan.haData() != state.haData() && plan.haData() == "" {
|
|
toDelete = append(toDelete, "ha")
|
|
}
|
|
|
|
if plan.migrationData() != state.migrationData() && plan.migrationData() == "" {
|
|
toDelete = append(toDelete, "migration")
|
|
}
|
|
|
|
if plan.nextIDData() != state.nextIDData() && plan.nextIDData() == "" {
|
|
toDelete = append(toDelete, "next-id")
|
|
}
|
|
|
|
if plan.notifyData() != state.notifyData() && plan.notifyData() == "" {
|
|
toDelete = append(toDelete, "notify")
|
|
}
|
|
|
|
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",
|
|
"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 cluster options.
|
|
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...)
|
|
|
|
var toDelete []string
|
|
|
|
if !state.Keyboard.IsNull() && state.Keyboard.ValueString() != "" {
|
|
toDelete = append(toDelete, "keyboard")
|
|
}
|
|
|
|
if state.bandwidthData() != "" {
|
|
toDelete = append(toDelete, "bwlimit")
|
|
}
|
|
|
|
if state.crsData() != "" {
|
|
toDelete = append(toDelete, "crs")
|
|
}
|
|
|
|
if state.haData() != "" {
|
|
toDelete = append(toDelete, "ha")
|
|
}
|
|
|
|
if state.migrationData() != "" {
|
|
toDelete = append(toDelete, "migration")
|
|
}
|
|
|
|
if state.nextIDData() != "" {
|
|
toDelete = append(toDelete, "next-id")
|
|
}
|
|
|
|
if state.notifyData() != "" {
|
|
toDelete = append(toDelete, "notify")
|
|
}
|
|
|
|
if !state.EmailFrom.IsNull() && state.EmailFrom.ValueString() != "" {
|
|
toDelete = append(toDelete, "email_from")
|
|
}
|
|
|
|
if !state.Language.IsNull() && state.Language.ValueString() != "" {
|
|
toDelete = append(toDelete, "language")
|
|
}
|
|
|
|
if !state.Console.IsNull() && state.Console.ValueString() != "" {
|
|
toDelete = append(toDelete, "console")
|
|
}
|
|
|
|
if !state.HTTPProxy.IsNull() && state.HTTPProxy.ValueString() != "" {
|
|
toDelete = append(toDelete, "http_proxy")
|
|
}
|
|
|
|
if !state.MacPrefix.IsNull() && state.MacPrefix.ValueString() != "" {
|
|
toDelete = append(toDelete, "mac_prefix")
|
|
}
|
|
|
|
if !state.Description.IsNull() && state.Description.ValueString() != "" {
|
|
toDelete = append(toDelete, "description")
|
|
}
|
|
|
|
if !state.MaxWorkers.IsNull() && state.MaxWorkers.ValueInt64() != 0 {
|
|
toDelete = append(toDelete, "max_workers")
|
|
}
|
|
|
|
if len(toDelete) > 0 {
|
|
d := strings.Join(toDelete, ",")
|
|
body := &cluster.OptionsRequestData{
|
|
Delete: &d,
|
|
}
|
|
|
|
err := r.client.Cluster().CreateUpdateOptions(ctx, body)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error updating cluster options",
|
|
"Could not update cluster options, unexpected error: "+err.Error(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ImportState imports cluster options.
|
|
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...)
|
|
}
|