mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-30 02:31:10 +00:00
chore(vm2): initial experimental VM resource implementation using Plugin Framework (#1230)
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
5a606ec18e
commit
d8202dd7a1
39
docs/resources/virtual_environment_vm2.md
Normal file
39
docs/resources/virtual_environment_vm2.md
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
layout: page
|
||||
title: proxmox_virtual_environment_vm2
|
||||
parent: Resources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
This is an experimental implementation of a Proxmox VM resource using Plugin Framework.It is a Proof of Concept, highly experimental and will change in future. It does not support all features of the Proxmox API for VMs and MUST NOT be used in production.
|
||||
---
|
||||
|
||||
# Resource: proxmox_virtual_environment_vm2
|
||||
|
||||
~> **DO NOT USE**
|
||||
This is an experimental implementation of a Proxmox VM resource using Plugin Framework.<br><br>It is a Proof of Concept, highly experimental and **will** change in future. It does not support all features of the Proxmox API for VMs and **MUST NOT** be used in production.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `node_name` (String) The name of the node where the VM is provisioned.
|
||||
|
||||
### Optional
|
||||
|
||||
- `description` (String) The description of the VM.
|
||||
- `id` (Number) The unique identifier of the VM in the Proxmox cluster.
|
||||
- `name` (String) The name of the VM. Doesn't have to be unique.
|
||||
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))
|
||||
|
||||
<a id="nestedatt--timeouts"></a>
|
||||
### Nested Schema for `timeouts`
|
||||
|
||||
Optional:
|
||||
|
||||
- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
|
||||
- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs.
|
||||
- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled.
|
||||
- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours).
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/ha"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/hardwaremapping"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/network"
|
||||
"github.com/bpg/terraform-provider-proxmox/fwprovider/vm"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes"
|
||||
@ -445,6 +446,7 @@ func (p *proxmoxProvider) Resources(_ context.Context) []func() resource.Resourc
|
||||
hardwaremapping.NewResourceUSB,
|
||||
network.NewLinuxBridgeResource,
|
||||
network.NewLinuxVLANResource,
|
||||
vm.NewVMResource,
|
||||
NewClusterOptionsResource,
|
||||
NewDownloadFileResource,
|
||||
}
|
||||
|
134
fwprovider/tests/resource_vm2_test.go
Normal file
134
fwprovider/tests/resource_vm2_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 tests
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v7"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
func TestAccResourcePVEVM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
te := initTestEnvironment(t)
|
||||
vmID := gofakeit.IntRange(90000, 100000)
|
||||
te.addTemplateVars(map[string]any{
|
||||
"VMID": vmID,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []resource.TestStep
|
||||
}{
|
||||
{"create minimal vm", []resource.TestStep{{
|
||||
Config: te.renderConfig(`
|
||||
resource "proxmox_virtual_environment_vm2" "test_vm" {
|
||||
node_name = "{{.NodeName}}"
|
||||
}`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testResourceAttributes("proxmox_virtual_environment_vm2.test_vm", map[string]string{
|
||||
"node_name": te.nodeName,
|
||||
}),
|
||||
testResourceAttributesSet("proxmox_virtual_environment_vm2.test_vm", []string{
|
||||
"id",
|
||||
}),
|
||||
),
|
||||
}}},
|
||||
{"create minimal vm with ID", []resource.TestStep{{
|
||||
Config: te.renderConfig(`
|
||||
resource "proxmox_virtual_environment_vm2" "test_vm" {
|
||||
node_name = "{{.NodeName}}"
|
||||
|
||||
id = {{.VMID}}
|
||||
}`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testResourceAttributes("proxmox_virtual_environment_vm2.test_vm", map[string]string{
|
||||
"node_name": te.nodeName,
|
||||
"id": strconv.Itoa(vmID),
|
||||
}),
|
||||
),
|
||||
}}},
|
||||
{"set and invalid VM name", []resource.TestStep{{
|
||||
Config: te.renderConfig(`
|
||||
resource "proxmox_virtual_environment_vm2" "test_vm" {
|
||||
node_name = "{{.NodeName}}"
|
||||
|
||||
name = "not a valid DNS name"
|
||||
|
||||
}`),
|
||||
ExpectError: regexp.MustCompile(`name must be a valid DNS name`),
|
||||
}}},
|
||||
{"set, update, import with primitive fields", []resource.TestStep{
|
||||
{
|
||||
Config: te.renderConfig(`
|
||||
resource "proxmox_virtual_environment_vm2" "test_vm" {
|
||||
node_name = "{{.NodeName}}"
|
||||
|
||||
name = "test-vm"
|
||||
description = "test description"
|
||||
}`),
|
||||
Check: testResourceAttributes("proxmox_virtual_environment_vm2.test_vm", map[string]string{
|
||||
"name": "test-vm",
|
||||
"description": "test description",
|
||||
}),
|
||||
},
|
||||
{
|
||||
Config: te.renderConfig(`
|
||||
resource "proxmox_virtual_environment_vm2" "test_vm" {
|
||||
node_name = "{{.NodeName}}"
|
||||
|
||||
name = "test-vm"
|
||||
}`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testResourceAttributes("proxmox_virtual_environment_vm2.test_vm", map[string]string{
|
||||
"name": "test-vm",
|
||||
}),
|
||||
testNoResourceAttributesSet("proxmox_virtual_environment_vm2.test_vm", []string{
|
||||
"description",
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
ResourceName: "proxmox_virtual_environment_vm2.test_vm",
|
||||
ImportState: true,
|
||||
ImportStateVerify: true,
|
||||
ImportStateIdPrefix: te.nodeName + "/",
|
||||
},
|
||||
}},
|
||||
{"multiline description", []resource.TestStep{{
|
||||
Config: te.renderConfig(`
|
||||
resource "proxmox_virtual_environment_vm2" "test_vm" {
|
||||
node_name = "{{.NodeName}}"
|
||||
|
||||
description = trimspace(<<-EOT
|
||||
my
|
||||
description
|
||||
value
|
||||
EOT
|
||||
)
|
||||
}`),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testResourceAttributes("proxmox_virtual_environment_vm2.test_vm", map[string]string{
|
||||
"description": "my\ndescription\nvalue",
|
||||
}),
|
||||
),
|
||||
}}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resource.ParallelTest(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: te.accProviders,
|
||||
Steps: tt.steps,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
431
fwprovider/vm/resource.go
Normal file
431
fwprovider/vm/resource.go
Normal file
@ -0,0 +1,431 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms"
|
||||
proxmoxtypes "github.com/bpg/terraform-provider-proxmox/proxmox/types"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCreateTimeout = 30 * time.Minute
|
||||
defaultReadTimeout = 5 * time.Minute
|
||||
defaultUpdateTimeout = 30 * time.Minute
|
||||
defaultDeleteTimeout = 10 * time.Minute
|
||||
|
||||
// these timeouts are for individual PVE operations.
|
||||
defaultShutdownTimeout = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
_ resource.Resource = &vmResource{}
|
||||
_ resource.ResourceWithConfigure = &vmResource{}
|
||||
_ resource.ResourceWithImportState = &vmResource{}
|
||||
)
|
||||
|
||||
type vmResource struct {
|
||||
client proxmox.Client
|
||||
}
|
||||
|
||||
// NewVMResource creates a new resource for managing VMs.
|
||||
func NewVMResource() resource.Resource {
|
||||
return &vmResource{}
|
||||
}
|
||||
|
||||
// Metadata defines the name of the resource.
|
||||
func (r *vmResource) Metadata(
|
||||
_ context.Context,
|
||||
req resource.MetadataRequest,
|
||||
resp *resource.MetadataResponse,
|
||||
) {
|
||||
resp.TypeName = req.ProviderTypeName + "_vm2"
|
||||
}
|
||||
|
||||
func (r *vmResource) 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", req.ProviderData),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r.client = client
|
||||
}
|
||||
|
||||
func (r *vmResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var plan vmModel
|
||||
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
timeout, d := plan.Timeouts.Create(ctx, defaultCreateTimeout)
|
||||
resp.Diagnostics.Append(d...)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
vmID := int(plan.ID.ValueInt64())
|
||||
if vmID == 0 {
|
||||
id, err := r.client.Cluster().GetVMID(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to get VM ID", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
vmID = *id
|
||||
}
|
||||
|
||||
vmAPI := r.client.Node(plan.NodeName.ValueString()).VM(vmID)
|
||||
|
||||
createBody := &vms.CreateRequestBody{
|
||||
Description: plan.Description.ValueStringPointer(),
|
||||
Name: plan.Name.ValueStringPointer(),
|
||||
VMID: &vmID,
|
||||
}
|
||||
|
||||
err := vmAPI.CreateVM(ctx, createBody)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to create VM", err.Error())
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// read back the VM from the PVE API to populate computed fields
|
||||
exists, err := r.read(ctx, vmAPI, &plan)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to read VM", err.Error())
|
||||
}
|
||||
|
||||
if !exists {
|
||||
resp.Diagnostics.AddError("VM does not exist after creation", "")
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// set state to the updated plan data
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
|
||||
}
|
||||
|
||||
func (r *vmResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var state vmModel
|
||||
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
timeout, d := state.Timeouts.Read(ctx, defaultReadTimeout)
|
||||
resp.Diagnostics.Append(d...)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
vmAPI := r.client.Node(state.NodeName.ValueString()).VM(int(state.ID.ValueInt64()))
|
||||
|
||||
exists, err := r.read(ctx, vmAPI, &state)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to read VM", err.Error())
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if !exists {
|
||||
tflog.Info(ctx, "VM does not exist, removing from the state", map[string]interface{}{
|
||||
"id": state.ID.ValueInt64(),
|
||||
})
|
||||
resp.State.RemoveResource(ctx)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// store updated state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
|
||||
}
|
||||
|
||||
func (r *vmResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var plan, state vmModel
|
||||
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
timeout, d := plan.Timeouts.Update(ctx, defaultUpdateTimeout)
|
||||
resp.Diagnostics.Append(d...)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
vmAPI := r.client.Node(plan.NodeName.ValueString()).VM(int(plan.ID.ValueInt64()))
|
||||
|
||||
updateBody := &vms.UpdateRequestBody{
|
||||
VMID: proxmoxtypes.Int64PtrToIntPtr(plan.ID.ValueInt64Pointer()),
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
del := func(field string) {
|
||||
errs = append(errs, updateBody.ToDelete(field))
|
||||
}
|
||||
|
||||
if !plan.Description.Equal(state.Description) {
|
||||
if plan.Description.IsNull() {
|
||||
del("Description")
|
||||
} else {
|
||||
updateBody.Description = plan.Description.ValueStringPointer()
|
||||
}
|
||||
}
|
||||
|
||||
if !plan.Name.Equal(state.Name) {
|
||||
if plan.Name.IsNull() {
|
||||
del("Name")
|
||||
} else {
|
||||
updateBody.Name = plan.Name.ValueStringPointer()
|
||||
}
|
||||
}
|
||||
|
||||
err := vmAPI.UpdateVM(ctx, updateBody)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to update VM", err.Error())
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// read back the VM from the PVE API to populate computed fields
|
||||
exists, err := r.read(ctx, vmAPI, &plan)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to read VM", err.Error())
|
||||
}
|
||||
|
||||
if !exists {
|
||||
resp.Diagnostics.AddError("VM does not exist after update", "")
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// set state to the updated plan data
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
|
||||
}
|
||||
|
||||
func (r *vmResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var state vmModel
|
||||
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
timeout, d := state.Timeouts.Delete(ctx, defaultDeleteTimeout)
|
||||
resp.Diagnostics.Append(d...)
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
vmAPI := r.client.Node(state.NodeName.ValueString()).VM(int(state.ID.ValueInt64()))
|
||||
|
||||
// Stop or shut down the virtual machine before deleting it.
|
||||
status, err := vmAPI.GetVMStatus(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to get VM status", err.Error())
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// stop := d.Get(mkStopOnDestroy).(bool)
|
||||
stop := false
|
||||
|
||||
if status.Status != "stopped" {
|
||||
if stop {
|
||||
if e := vmStop(ctx, vmAPI); e != nil {
|
||||
resp.Diagnostics.AddWarning("Failed to stop VM", e.Error())
|
||||
}
|
||||
} else {
|
||||
if e := vmShutdown(ctx, vmAPI); e != nil {
|
||||
resp.Diagnostics.AddWarning("Failed to shut down VM", e.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = vmAPI.DeleteVM(ctx)
|
||||
if err != nil && !errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
resp.Diagnostics.AddError("Failed to delete VM", err.Error())
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
resp.State.RemoveResource(ctx)
|
||||
}
|
||||
|
||||
func (r *vmResource) ImportState(
|
||||
ctx context.Context,
|
||||
req resource.ImportStateRequest,
|
||||
resp *resource.ImportStateResponse,
|
||||
) {
|
||||
ctx, cancel := context.WithTimeout(ctx, defaultReadTimeout)
|
||||
defer cancel()
|
||||
|
||||
nodeName, vmid, found := strings.Cut(req.ID, "/")
|
||||
|
||||
id, err := strconv.Atoi(vmid)
|
||||
if !found || err != nil || id == 0 {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Import Identifier",
|
||||
fmt.Sprintf("Expected import identifier with format: `node_name/id`. Got: %q", req.ID),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
vmAPI := r.client.Node(nodeName).VM(id)
|
||||
|
||||
var ts timeouts.Value
|
||||
|
||||
resp.Diagnostics.Append(resp.State.GetAttribute(ctx, path.Root("timeouts"), &ts)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
state := vmModel{
|
||||
ID: types.Int64Value(int64(id)),
|
||||
NodeName: types.StringValue(nodeName),
|
||||
Timeouts: ts,
|
||||
}
|
||||
|
||||
exists, err := r.read(ctx, vmAPI, &state)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Failed to read VM", err.Error())
|
||||
}
|
||||
|
||||
if !exists {
|
||||
resp.Diagnostics.AddError(fmt.Sprintf("VM %d does not exist on node %s", id, nodeName), "")
|
||||
}
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
diags := resp.State.Set(ctx, state)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
}
|
||||
|
||||
// read retrieves the current state of the resource from the API and updates the state.
|
||||
// Returns false if the resource does not exist, so the caller can remove it from the state if necessary.
|
||||
func (r *vmResource) read(ctx context.Context, vmAPI *vms.Client, model *vmModel) (bool, error) {
|
||||
// Retrieve the entire configuration in order to compare it to the state.
|
||||
vmConfig, err := vmAPI.GetVM(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, api.ErrResourceDoesNotExist) {
|
||||
tflog.Info(ctx, "VM does not exist, removing from the state", map[string]interface{}{
|
||||
"vm_id": vmAPI.VMID,
|
||||
})
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("failed to get VM: %w", err)
|
||||
}
|
||||
|
||||
vmStatus, err := vmAPI.GetVMStatus(ctx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get VM status: %w", err)
|
||||
}
|
||||
|
||||
err = model.updateFromAPI(*vmConfig, *vmStatus)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to update VM from API: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Shutdown the VM, then wait for it to actually shut down (it may not be shut down immediately if
|
||||
// running in HA mode).
|
||||
func vmShutdown(ctx context.Context, vmAPI *vms.Client) error {
|
||||
tflog.Debug(ctx, "Shutting down VM")
|
||||
|
||||
shutdownTimeoutSec := int(defaultShutdownTimeout.Seconds())
|
||||
|
||||
if dl, ok := ctx.Deadline(); ok {
|
||||
time.Until(dl)
|
||||
shutdownTimeoutSec = int(time.Until(dl).Seconds())
|
||||
}
|
||||
|
||||
err := vmAPI.ShutdownVM(ctx, &vms.ShutdownRequestBody{
|
||||
ForceStop: proxmoxtypes.CustomBool(true).Pointer(),
|
||||
Timeout: &shutdownTimeoutSec,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate shut down of VM: %w", err)
|
||||
}
|
||||
|
||||
err = vmAPI.WaitForVMStatus(ctx, "stopped")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for VM to shut down: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Forcefully stop the VM, then wait for it to actually stop.
|
||||
func vmStop(ctx context.Context, vmAPI *vms.Client) error {
|
||||
tflog.Debug(ctx, "Stopping VM")
|
||||
|
||||
err := vmAPI.StopVM(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate stop of VM: %w", err)
|
||||
}
|
||||
|
||||
err = vmAPI.WaitForVMStatus(ctx, "stopped")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to wait for VM to stop: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
32
fwprovider/vm/resource_model.go
Normal file
32
fwprovider/vm/resource_model.go
Normal file
@ -0,0 +1,32 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
"github.com/bpg/terraform-provider-proxmox/proxmox/nodes/vms"
|
||||
)
|
||||
|
||||
type vmModel struct {
|
||||
Description types.String `tfsdk:"description"`
|
||||
ID types.Int64 `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
NodeName types.String `tfsdk:"node_name"`
|
||||
Timeouts timeouts.Value `tfsdk:"timeouts"`
|
||||
}
|
||||
|
||||
func (m *vmModel) updateFromAPI(config vms.GetResponseData, status vms.GetStatusResponseData) error {
|
||||
if status.VMID == nil {
|
||||
return errors.New("VM ID is missing in status API response")
|
||||
}
|
||||
|
||||
m.ID = types.Int64Value(int64(*status.VMID))
|
||||
|
||||
// Optional fields can be removed from the model, use StringPointerValue to handle removal on nil
|
||||
m.Description = types.StringPointerValue(config.Description)
|
||||
m.Name = types.StringPointerValue(config.Name)
|
||||
|
||||
return nil
|
||||
}
|
65
fwprovider/vm/resource_schema.go
Normal file
65
fwprovider/vm/resource_schema.go
Normal file
@ -0,0 +1,65 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
)
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *vmResource) Schema(
|
||||
ctx context.Context,
|
||||
_ resource.SchemaRequest,
|
||||
resp *resource.SchemaResponse,
|
||||
) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "This is an experimental implementation of a Proxmox VM resource using Plugin Framework.",
|
||||
MarkdownDescription: "This is an experimental implementation of a Proxmox VM resource using Plugin Framework." +
|
||||
"<br><br>It is a Proof of Concept, highly experimental and **will** change in future. " +
|
||||
"It does not support all features of the Proxmox API for VMs and **MUST NOT** be used in production.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.Int64Attribute{
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
int64planmodifier.RequiresReplace(),
|
||||
},
|
||||
Description: "The unique identifier of the VM in the Proxmox cluster.",
|
||||
},
|
||||
|
||||
"description": schema.StringAttribute{
|
||||
Description: "The description of the VM.",
|
||||
Optional: true,
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the VM.",
|
||||
MarkdownDescription: "The name of the VM. Doesn't have to be unique.",
|
||||
Optional: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$`),
|
||||
"must be a valid DNS name",
|
||||
),
|
||||
},
|
||||
},
|
||||
"node_name": schema.StringAttribute{
|
||||
Description: "The name of the node where the VM is provisioned.",
|
||||
Required: true,
|
||||
},
|
||||
"timeouts": timeouts.Attributes(ctx, timeouts.Opts{
|
||||
Create: true,
|
||||
Read: true,
|
||||
Update: true,
|
||||
Delete: true,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
1
go.mod
1
go.mod
@ -12,6 +12,7 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
|
||||
github.com/hashicorp/terraform-plugin-framework v1.8.0
|
||||
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
|
||||
github.com/hashicorp/terraform-plugin-go v0.22.2
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0
|
||||
|
2
go.sum
2
go.sum
@ -85,6 +85,8 @@ github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRy
|
||||
github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.8.0 h1:P07qy8RKLcoBkCrY2RHJer5AEvJnDuXomBgou6fD8kI=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.8.0/go.mod h1:/CpTukO88PcL/62noU7cuyaSJ4Rsim+A/pa+3rUVufY=
|
||||
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E=
|
||||
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg=
|
||||
github.com/hashicorp/terraform-plugin-go v0.22.2 h1:5o8uveu6eZUf5J7xGPV0eY0TPXg3qpmwX9sce03Bxnc=
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -667,6 +668,23 @@ type UpdateAsyncResponseBody struct {
|
||||
// UpdateRequestBody contains the data for an virtual machine update request.
|
||||
type UpdateRequestBody CreateRequestBody
|
||||
|
||||
// ToDelete adds a field to the delete list.
|
||||
func (u *UpdateRequestBody) ToDelete(fieldName string) error {
|
||||
if u == nil {
|
||||
return errors.New("update request body is nil")
|
||||
}
|
||||
|
||||
if field, ok := reflect.TypeOf(*u).FieldByName(fieldName); ok {
|
||||
fieldTag := field.Tag.Get("url")
|
||||
name := strings.Split(fieldTag, ",")[0]
|
||||
u.Delete = append(u.Delete, name)
|
||||
} else {
|
||||
return fmt.Errorf("field %s not found in struct %s", fieldName, reflect.TypeOf(u).Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeValues converts a CustomAgent struct to a URL vlaue.
|
||||
func (r CustomAgent) EncodeValues(key string, v *url.Values) error {
|
||||
var values []string
|
||||
|
@ -39,3 +39,12 @@ func CopyInt(i *int) *int {
|
||||
|
||||
return IntPtr(*i)
|
||||
}
|
||||
|
||||
// Int64PtrToIntPtr converts an int64 pointer to an int pointer.
|
||||
func Int64PtrToIntPtr(i *int64) *int {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return IntPtr(int(*i))
|
||||
}
|
||||
|
29
templates/resources/virtual_environment_vm2.md.tmpl
Normal file
29
templates/resources/virtual_environment_vm2.md.tmpl
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
layout: page
|
||||
title: {{.Name}}
|
||||
parent: Resources
|
||||
subcategory: Virtual Environment
|
||||
description: |-
|
||||
{{ .Description | plainmarkdown | trimspace | prefixlines " " }}
|
||||
---
|
||||
|
||||
# {{.Type}}: {{.Name}}
|
||||
|
||||
~> **DO NOT USE**
|
||||
{{ .Description | trimspace }}
|
||||
|
||||
{{ if .HasExample -}}
|
||||
## Example Usage
|
||||
|
||||
{{ codefile "terraform" .ExampleFile }}
|
||||
{{- end }}
|
||||
|
||||
{{ .SchemaMarkdown | trimspace }}
|
||||
{{- if .HasImport }}
|
||||
|
||||
## Import
|
||||
|
||||
Import is supported using the following syntax:
|
||||
|
||||
{{ codefile "shell" .ImportFile }}
|
||||
{{- end }}
|
@ -45,3 +45,4 @@ import (
|
||||
//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/
|
||||
//go:generate cp ../build/docs-gen/resources/virtual_environment_download_file.md ../docs/resources/
|
||||
//go:generate cp ../build/docs-gen/resources/virtual_environment_vm2.md ../docs/resources/
|
||||
|
Loading…
Reference in New Issue
Block a user