0
0
mirror of https://github.com/bpg/terraform-provider-proxmox.git synced 2025-08-22 19:38:35 +00:00

Continued work on VM resource

This commit is contained in:
Dan Petersen 2019-12-27 02:48:27 +01:00
parent 96d139fcb4
commit c69cabc57a
10 changed files with 367 additions and 3 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
/website/node_modules /website/node_modules
/website/vendor /website/vendor
autogenerated/
bin/ bin/
dist/ dist/
modules-dev/ modules-dev/

View File

@ -3,7 +3,6 @@ resource "proxmox_virtual_environment_file" "ubuntu_cloud_image" {
datastore_id = "${element(data.proxmox_virtual_environment_datastores.example.datastore_ids, index(data.proxmox_virtual_environment_datastores.example.datastore_ids, "local"))}" datastore_id = "${element(data.proxmox_virtual_environment_datastores.example.datastore_ids, index(data.proxmox_virtual_environment_datastores.example.datastore_ids, "local"))}"
node_name = "${data.proxmox_virtual_environment_datastores.example.node_name}" node_name = "${data.proxmox_virtual_environment_datastores.example.node_name}"
source = "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img" source = "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img"
source_checksum = "6afb97af96b671572389935d6579557357ad7bbf2c2dd2cb52879c957c85dbee"
} }
output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_content_type" { output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_content_type" {

View File

@ -16,9 +16,25 @@ resource "proxmox_virtual_environment_vm" "example" {
} }
} }
disk {
datastore_id = "${element(data.proxmox_virtual_environment_datastores.example.datastore_ids, index(data.proxmox_virtual_environment_datastores.example.datastore_ids, "local-lvm"))}"
file_id = "${proxmox_virtual_environment_file.ubuntu_cloud_image.id}"
}
network_device {} network_device {}
node_name = "${data.proxmox_virtual_environment_nodes.example.names[0]}" node_name = "${data.proxmox_virtual_environment_nodes.example.names[0]}"
os_type = "l26"
}
resource "local_file" "example_ssh_private_key" {
filename = "${path.module}/autogenerated/id_rsa"
sensitive_content = "${tls_private_key.example.private_key_pem}"
}
resource "local_file" "example_ssh_public_key" {
filename = "${path.module}/autogenerated/id_rsa.pub"
sensitive_content = "${tls_private_key.example.public_key_openssh}"
} }
resource "tls_private_key" "example" { resource "tls_private_key" "example" {

1
go.mod
View File

@ -5,4 +5,5 @@ go 1.13
require ( require (
github.com/google/go-querystring v1.0.0 github.com/google/go-querystring v1.0.0
github.com/hashicorp/terraform v0.12.18 github.com/hashicorp/terraform v0.12.18
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
) )

View File

@ -11,6 +11,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
@ -150,6 +151,9 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in
if err != nil { if err != nil {
return fmt.Errorf("Failed to decode HTTP %s response (path: %s) - Reason: %s", method, modifiedPath, err.Error()) return fmt.Errorf("Failed to decode HTTP %s response (path: %s) - Reason: %s", method, modifiedPath, err.Error())
} }
} else {
data, _ := ioutil.ReadAll(res.Body)
log.Printf("[DEBUG] Unhandled HTTP response body: %s", string(data))
} }
return nil return nil

View File

@ -6,9 +6,96 @@ package proxmox
import ( import (
"errors" "errors"
"fmt"
"net/url"
"sort" "sort"
"strings"
"golang.org/x/crypto/ssh"
) )
// ExecuteNodeCommands executes commands on a given node.
func (c *VirtualEnvironmentClient) ExecuteNodeCommands(nodeName string, commands []string) error {
// We must first retrieve the IP address of the node as we need to bypass the API and use SSH instead.
networkDevices, err := c.ListNodeNetworkDevices(nodeName)
if err != nil {
return err
}
nodeAddress := ""
for _, d := range networkDevices {
if d.Address != nil {
nodeAddress = *d.Address
break
}
}
if nodeAddress == "" {
return fmt.Errorf("Failed to determine the IP address of node \"%s\"", nodeName)
}
// We can now go ahead and execute the commands using SSH.
// Hopefully, the developers will add this feature to the REST API at some point.
ur := strings.Split(c.Username, "@")
sshConfig := &ssh.ClientConfig{
User: ur[0],
Auth: []ssh.AuthMethod{ssh.Password(c.Password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
sshClient, err := ssh.Dial("tcp", nodeAddress+":22", sshConfig)
if err != nil {
return err
}
defer sshClient.Close()
sshSession, err := sshClient.NewSession()
if err != nil {
return err
}
defer sshSession.Close()
_, err = sshSession.CombinedOutput(
fmt.Sprintf(
"/bin/bash -c '%s'",
strings.ReplaceAll(strings.Join(commands, " && "), "'", "'\"'\"'"),
),
)
if err != nil {
return err
}
return nil
}
// ListNodeNetworkDevices retrieves a list of network devices for a specific nodes.
func (c *VirtualEnvironmentClient) ListNodeNetworkDevices(nodeName string) ([]*VirtualEnvironmentNodeNetworkDeviceListResponseData, error) {
resBody := &VirtualEnvironmentNodeNetworkDeviceListResponseBody{}
err := c.DoRequest(hmGET, fmt.Sprintf("nodes/%s/network", url.PathEscape(nodeName)), nil, resBody)
if err != nil {
return nil, err
}
if resBody.Data == nil {
return nil, errors.New("The server did not include a data object in the response")
}
sort.Slice(resBody.Data, func(i, j int) bool {
return resBody.Data[i].Priority < resBody.Data[j].Priority
})
return resBody.Data, nil
}
// ListNodes retrieves a list of nodes. // ListNodes retrieves a list of nodes.
func (c *VirtualEnvironmentClient) ListNodes() ([]*VirtualEnvironmentNodeListResponseData, error) { func (c *VirtualEnvironmentClient) ListNodes() ([]*VirtualEnvironmentNodeListResponseData, error) {
resBody := &VirtualEnvironmentNodeListResponseBody{} resBody := &VirtualEnvironmentNodeListResponseBody{}

View File

@ -4,6 +4,19 @@
package proxmox package proxmox
import (
"encoding/json"
"net/url"
)
// CustomNodeCommands contains an array of commands to execute.
type CustomNodeCommands []string
// VirtualEnvironmentNodeExecuteRequestBody contains the data for a node execute request.
type VirtualEnvironmentNodeExecuteRequestBody struct {
Commands CustomNodeCommands `json:"commands" url:"commands"`
}
// VirtualEnvironmentNodeListResponseBody contains the body from a node list response. // VirtualEnvironmentNodeListResponseBody contains the body from a node list response.
type VirtualEnvironmentNodeListResponseBody struct { type VirtualEnvironmentNodeListResponseBody struct {
Data []*VirtualEnvironmentNodeListResponseData `json:"data,omitempty"` Data []*VirtualEnvironmentNodeListResponseData `json:"data,omitempty"`
@ -21,3 +34,41 @@ type VirtualEnvironmentNodeListResponseData struct {
SupportLevel *string `json:"level,omitempty"` SupportLevel *string `json:"level,omitempty"`
Uptime *int `json:"uptime"` Uptime *int `json:"uptime"`
} }
// VirtualEnvironmentNodeNetworkDeviceListResponseBody contains the body from a node network device list response.
type VirtualEnvironmentNodeNetworkDeviceListResponseBody struct {
Data []*VirtualEnvironmentNodeNetworkDeviceListResponseData `json:"data,omitempty"`
}
// VirtualEnvironmentNodeNetworkDeviceListResponseData contains the data from a node network device list response.
type VirtualEnvironmentNodeNetworkDeviceListResponseData struct {
Active *CustomBool `json:"active,omitempty"`
Address *string `json:"address,omitempty"`
Autostart *CustomBool `json:"autostart,omitempty"`
BridgeFD *string `json:"bridge_fd,omitempty"`
BridgePorts *string `json:"bridge_ports,omitempty"`
BridgeSTP *string `json:"bridge_stp,omitempty"`
CIDR *string `json:"cidr,omitempty"`
Exists *CustomBool `json:"exists,omitempty"`
Families *[]string `json:"families,omitempty"`
Gateway *string `json:"gateway,omitempty"`
Iface string `json:"iface"`
MethodIPv4 *string `json:"method,omitempty"`
MethodIPv6 *string `json:"method6,omitempty"`
Netmask *string `json:"netmask,omitempty"`
Priority int `json:"priority"`
Type string `json:"type"`
}
// EncodeValues converts a CustomNodeCommands array to a JSON encoded URL vlaue.
func (r CustomNodeCommands) EncodeValues(key string, v *url.Values) error {
jsonArrayBytes, err := json.Marshal(r)
if err != nil {
return err
}
v.Add(key, string(jsonArrayBytes))
return nil
}

View File

@ -227,7 +227,7 @@ type VirtualEnvironmentVMCreateRequestBody struct {
PCIDevices CustomPCIDevices `json:"hostpci,omitempty" url:"hostpci,omitempty"` PCIDevices CustomPCIDevices `json:"hostpci,omitempty" url:"hostpci,omitempty"`
Revert *string `json:"revert,omitempty" url:"revert,omitempty"` Revert *string `json:"revert,omitempty" url:"revert,omitempty"`
SATADevices CustomStorageDevices `json:"sata,omitempty" url:"sata,omitempty"` SATADevices CustomStorageDevices `json:"sata,omitempty" url:"sata,omitempty"`
SCSIDevices CustomStorageDevices `json:"scsi,omitempty" url:"sata,omitempty"` SCSIDevices CustomStorageDevices `json:"scsi,omitempty" url:"scsi,omitempty"`
SCSIHardware *string `json:"scsihw,omitempty" url:"scsihw,omitempty"` SCSIHardware *string `json:"scsihw,omitempty" url:"scsihw,omitempty"`
SerialDevices CustomSerialDevices `json:"serial,omitempty" url:"serial,omitempty"` SerialDevices CustomSerialDevices `json:"serial,omitempty" url:"serial,omitempty"`
SharedMemory *CustomSharedMemory `json:"ivshmem,omitempty" url:"ivshmem,omitempty"` SharedMemory *CustomSharedMemory `json:"ivshmem,omitempty" url:"ivshmem,omitempty"`

View File

@ -6,6 +6,7 @@ package proxmoxtf
import ( import (
"fmt" "fmt"
"log"
"strconv" "strconv"
"strings" "strings"
@ -610,6 +611,30 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e
cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(int) cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(int)
cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int) cpuSockets := cpuBlock[mkResourceVirtualEnvironmentVMCPUSockets].(int)
disk := d.Get(mkResourceVirtualEnvironmentVMDisk).([]interface{})
scsiDevices := make(proxmox.CustomStorageDevices, len(disk))
for i, d := range disk {
block := d.(map[string]interface{})
datastoreID, _ := block[mkResourceVirtualEnvironmentVMDiskDatastoreID].(string)
enabled, _ := block[mkResourceVirtualEnvironmentVMDiskEnabled].(bool)
fileID, _ := block[mkResourceVirtualEnvironmentVMDiskFileID].(string)
size, _ := block[mkResourceVirtualEnvironmentVMDiskSize].(int)
diskDevice := proxmox.CustomStorageDevice{
Enabled: enabled,
}
if fileID != "" {
diskDevice.Enabled = false
} else {
diskDevice.FileVolume = fmt.Sprintf("%s:%d", datastoreID, size)
}
scsiDevices[i] = diskDevice
}
keyboardLayout := d.Get(mkResourceVirtualEnvironmentVMKeyboardLayout).(string) keyboardLayout := d.Get(mkResourceVirtualEnvironmentVMKeyboardLayout).(string)
memory := d.Get(mkResourceVirtualEnvironmentVMMemory).([]interface{}) memory := d.Get(mkResourceVirtualEnvironmentVMMemory).([]interface{})
@ -679,6 +704,7 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e
var memorySharedObject *proxmox.CustomSharedMemory var memorySharedObject *proxmox.CustomSharedMemory
agentEnabled := proxmox.CustomBool(true) agentEnabled := proxmox.CustomBool(true)
bootDisk := "scsi0"
bootOrder := "c" bootOrder := "c"
if cdromEnabled { if cdromEnabled {
@ -713,6 +739,7 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e
body := &proxmox.VirtualEnvironmentVMCreateRequestBody{ body := &proxmox.VirtualEnvironmentVMCreateRequestBody{
Agent: &proxmox.CustomAgent{Enabled: &agentEnabled}, Agent: &proxmox.CustomAgent{Enabled: &agentEnabled},
BootDisk: &bootDisk,
BootOrder: &bootOrder, BootOrder: &bootOrder,
CloudInitConfig: cloudInitConfig, CloudInitConfig: cloudInitConfig,
CPUCores: &cpuCores, CPUCores: &cpuCores,
@ -723,11 +750,11 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e
KeyboardLayout: &keyboardLayout, KeyboardLayout: &keyboardLayout,
NetworkDevices: networkDeviceObjects, NetworkDevices: networkDeviceObjects,
OSType: &osType, OSType: &osType,
SCSIDevices: scsiDevices,
SCSIHardware: &scsiHardware, SCSIHardware: &scsiHardware,
SerialDevices: []string{"socket"}, SerialDevices: []string{"socket"},
SharedMemory: memorySharedObject, SharedMemory: memorySharedObject,
TabletDeviceEnabled: &tabletDeviceEnabled, TabletDeviceEnabled: &tabletDeviceEnabled,
VGADevice: &proxmox.CustomVGADevice{Type: "serial0"},
VMID: &vmID, VMID: &vmID,
} }
@ -743,6 +770,85 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e
d.SetId(strconv.Itoa(vmID)) d.SetId(strconv.Itoa(vmID))
return resourceVirtualEnvironmentVMImportDisks(d, m)
}
func resourceVirtualEnvironmentVMImportDisks(d *schema.ResourceData, m interface{}) error {
config := m.(providerConfiguration)
veClient, err := config.GetVEClient()
if err != nil {
return err
}
nodeName := d.Get(mkResourceVirtualEnvironmentVMNodeName).(string)
vmID, err := strconv.Atoi(d.Id())
if err != nil {
return err
}
commands := []string{}
// Determine the ID of the next disk.
disk := d.Get(mkResourceVirtualEnvironmentVMDisk).([]interface{})
diskCount := 0
for _, d := range disk {
block := d.(map[string]interface{})
fileID, _ := block[mkResourceVirtualEnvironmentVMDiskFileID].(string)
if fileID == "" {
diskCount++
}
}
// Generate the commands required to import the specified disks.
importedDiskCount := 0
for i, d := range disk {
block := d.(map[string]interface{})
datastoreID, _ := block[mkResourceVirtualEnvironmentVMDiskDatastoreID].(string)
enabled, _ := block[mkResourceVirtualEnvironmentVMDiskEnabled].(bool)
fileFormat, _ := block[mkResourceVirtualEnvironmentVMDiskFileFormat].(string)
fileID, _ := block[mkResourceVirtualEnvironmentVMDiskFileID].(string)
size, _ := block[mkResourceVirtualEnvironmentVMDiskSize].(int)
if !enabled || fileID == "" {
continue
}
fileIDParts := strings.Split(fileID, ":")
filePath := fmt.Sprintf("/var/lib/vz/template/%s", fileIDParts[1])
filePathTmp := fmt.Sprintf("/tmp/vm-%d-disk-%d.%s", vmID, diskCount+importedDiskCount, fileFormat)
commands = append(
commands,
fmt.Sprintf("cp %s %s", filePath, filePathTmp),
fmt.Sprintf("qemu-img resize %s %dG", filePathTmp, size),
fmt.Sprintf("qm importdisk %d %s %s -format qcow2", vmID, filePathTmp, datastoreID),
fmt.Sprintf("qm set %d --scsi%d %s:vm-%d-disk-%d", vmID, i, datastoreID, vmID, diskCount+importedDiskCount),
fmt.Sprintf("rm -f %s", filePathTmp),
)
importedDiskCount++
}
// Execute the commands on the node and wait for the result.
// This is a highly experimental approach to disk imports and is not recommended by Proxmox.
if len(commands) > 0 {
for _, cmd := range commands {
log.Printf("[DEBUG] Node command: %s", cmd)
}
err = veClient.ExecuteNodeCommands(nodeName, commands)
if err != nil {
return err
}
}
return resourceVirtualEnvironmentVMRead(d, m) return resourceVirtualEnvironmentVMRead(d, m)
} }

View File

@ -29,6 +29,7 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) {
testOptionalArguments(t, s, []string{ testOptionalArguments(t, s, []string{
mkResourceVirtualEnvironmentVMCDROM, mkResourceVirtualEnvironmentVMCDROM,
mkResourceVirtualEnvironmentVMCloudInit,
mkResourceVirtualEnvironmentVMCPU, mkResourceVirtualEnvironmentVMCPU,
mkResourceVirtualEnvironmentVMDisk, mkResourceVirtualEnvironmentVMDisk,
mkResourceVirtualEnvironmentVMKeyboardLayout, mkResourceVirtualEnvironmentVMKeyboardLayout,
@ -41,6 +42,7 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) {
testSchemaValueTypes(t, s, []string{ testSchemaValueTypes(t, s, []string{
mkResourceVirtualEnvironmentVMCDROM, mkResourceVirtualEnvironmentVMCDROM,
mkResourceVirtualEnvironmentVMCloudInit,
mkResourceVirtualEnvironmentVMCPU, mkResourceVirtualEnvironmentVMCPU,
mkResourceVirtualEnvironmentVMDisk, mkResourceVirtualEnvironmentVMDisk,
mkResourceVirtualEnvironmentVMKeyboardLayout, mkResourceVirtualEnvironmentVMKeyboardLayout,
@ -53,6 +55,7 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) {
schema.TypeList, schema.TypeList,
schema.TypeList, schema.TypeList,
schema.TypeList, schema.TypeList,
schema.TypeList,
schema.TypeString, schema.TypeString,
schema.TypeList, schema.TypeList,
schema.TypeString, schema.TypeString,
@ -76,6 +79,102 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) {
schema.TypeString, schema.TypeString,
}) })
cloudInitSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMCloudInit)
testRequiredArguments(t, cloudInitSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitUserAccount,
})
testOptionalArguments(t, cloudInitSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitDNS,
mkResourceVirtualEnvironmentVMCloudInitIPConfig,
})
testSchemaValueTypes(t, cloudInitSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitDNS,
mkResourceVirtualEnvironmentVMCloudInitIPConfig,
mkResourceVirtualEnvironmentVMCloudInitUserAccount,
}, []schema.ValueType{
schema.TypeList,
schema.TypeList,
schema.TypeList,
})
cloudInitDNSSchema := testNestedSchemaExistence(t, cloudInitSchema, mkResourceVirtualEnvironmentVMCloudInitDNS)
testOptionalArguments(t, cloudInitDNSSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitDNSDomain,
mkResourceVirtualEnvironmentVMCloudInitDNSServer,
})
testSchemaValueTypes(t, cloudInitDNSSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitDNSDomain,
mkResourceVirtualEnvironmentVMCloudInitDNSServer,
}, []schema.ValueType{
schema.TypeString,
schema.TypeString,
})
cloudInitIPConfigSchema := testNestedSchemaExistence(t, cloudInitSchema, mkResourceVirtualEnvironmentVMCloudInitIPConfig)
testOptionalArguments(t, cloudInitIPConfigSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4,
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6,
})
testSchemaValueTypes(t, cloudInitIPConfigSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4,
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6,
}, []schema.ValueType{
schema.TypeList,
schema.TypeList,
})
cloudInitIPConfigIPv4Schema := testNestedSchemaExistence(t, cloudInitIPConfigSchema, mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4)
testOptionalArguments(t, cloudInitIPConfigIPv4Schema, []string{
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Address,
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Gateway,
})
testSchemaValueTypes(t, cloudInitIPConfigIPv4Schema, []string{
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Address,
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv4Gateway,
}, []schema.ValueType{
schema.TypeString,
schema.TypeString,
})
cloudInitIPConfigIPv6Schema := testNestedSchemaExistence(t, cloudInitIPConfigSchema, mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6)
testOptionalArguments(t, cloudInitIPConfigIPv6Schema, []string{
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Address,
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Gateway,
})
testSchemaValueTypes(t, cloudInitIPConfigIPv6Schema, []string{
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Address,
mkResourceVirtualEnvironmentVMCloudInitIPConfigIPv6Gateway,
}, []schema.ValueType{
schema.TypeString,
schema.TypeString,
})
cloudInitUserAccountSchema := testNestedSchemaExistence(t, cloudInitSchema, mkResourceVirtualEnvironmentVMCloudInitUserAccount)
testRequiredArguments(t, cloudInitUserAccountSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys,
mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername,
})
testSchemaValueTypes(t, cloudInitUserAccountSchema, []string{
mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys,
mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername,
}, []schema.ValueType{
schema.TypeList,
schema.TypeString,
})
cpuSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMCPU) cpuSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMCPU)
testOptionalArguments(t, cpuSchema, []string{ testOptionalArguments(t, cpuSchema, []string{