diff --git a/.gitignore b/.gitignore index b8a4b91f..3f913c16 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /website/node_modules /website/vendor +autogenerated/ bin/ dist/ modules-dev/ diff --git a/example/resource_virtual_environment_file.tf b/example/resource_virtual_environment_file.tf index 686067be..d078bc13 100644 --- a/example/resource_virtual_environment_file.tf +++ b/example/resource_virtual_environment_file.tf @@ -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"))}" node_name = "${data.proxmox_virtual_environment_datastores.example.node_name}" 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" { diff --git a/example/resource_virtual_environment_vm.tf b/example/resource_virtual_environment_vm.tf index f02ceaff..698c589b 100644 --- a/example/resource_virtual_environment_vm.tf +++ b/example/resource_virtual_environment_vm.tf @@ -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 {} 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" { diff --git a/go.mod b/go.mod index f1a7f5cb..f1fbd6fd 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.13 require ( github.com/google/go-querystring v1.0.0 github.com/hashicorp/terraform v0.12.18 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 ) diff --git a/proxmox/virtual_environment_client.go b/proxmox/virtual_environment_client.go index 316f26fd..483da79b 100644 --- a/proxmox/virtual_environment_client.go +++ b/proxmox/virtual_environment_client.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "io/ioutil" "log" "net/http" "net/url" @@ -150,6 +151,9 @@ func (c *VirtualEnvironmentClient) DoRequest(method, path string, requestBody in if err != nil { 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 diff --git a/proxmox/virtual_environment_nodes.go b/proxmox/virtual_environment_nodes.go index d0b5387e..ec95f0a8 100644 --- a/proxmox/virtual_environment_nodes.go +++ b/proxmox/virtual_environment_nodes.go @@ -6,9 +6,96 @@ package proxmox import ( "errors" + "fmt" + "net/url" "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. func (c *VirtualEnvironmentClient) ListNodes() ([]*VirtualEnvironmentNodeListResponseData, error) { resBody := &VirtualEnvironmentNodeListResponseBody{} diff --git a/proxmox/virtual_environment_nodes_types.go b/proxmox/virtual_environment_nodes_types.go index c6ee725a..3a869efa 100644 --- a/proxmox/virtual_environment_nodes_types.go +++ b/proxmox/virtual_environment_nodes_types.go @@ -4,6 +4,19 @@ 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. type VirtualEnvironmentNodeListResponseBody struct { Data []*VirtualEnvironmentNodeListResponseData `json:"data,omitempty"` @@ -21,3 +34,41 @@ type VirtualEnvironmentNodeListResponseData struct { SupportLevel *string `json:"level,omitempty"` 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 +} diff --git a/proxmox/virtual_environment_vm_types.go b/proxmox/virtual_environment_vm_types.go index f22ea6ea..49ca22f6 100644 --- a/proxmox/virtual_environment_vm_types.go +++ b/proxmox/virtual_environment_vm_types.go @@ -227,7 +227,7 @@ type VirtualEnvironmentVMCreateRequestBody struct { PCIDevices CustomPCIDevices `json:"hostpci,omitempty" url:"hostpci,omitempty"` Revert *string `json:"revert,omitempty" url:"revert,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"` SerialDevices CustomSerialDevices `json:"serial,omitempty" url:"serial,omitempty"` SharedMemory *CustomSharedMemory `json:"ivshmem,omitempty" url:"ivshmem,omitempty"` diff --git a/proxmoxtf/resource_virtual_environment_vm.go b/proxmoxtf/resource_virtual_environment_vm.go index 346652b4..0e9e5c48 100644 --- a/proxmoxtf/resource_virtual_environment_vm.go +++ b/proxmoxtf/resource_virtual_environment_vm.go @@ -6,6 +6,7 @@ package proxmoxtf import ( "fmt" + "log" "strconv" "strings" @@ -610,6 +611,30 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e cpuCores := cpuBlock[mkResourceVirtualEnvironmentVMCPUCores].(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) memory := d.Get(mkResourceVirtualEnvironmentVMMemory).([]interface{}) @@ -679,6 +704,7 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e var memorySharedObject *proxmox.CustomSharedMemory agentEnabled := proxmox.CustomBool(true) + bootDisk := "scsi0" bootOrder := "c" if cdromEnabled { @@ -713,6 +739,7 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e body := &proxmox.VirtualEnvironmentVMCreateRequestBody{ Agent: &proxmox.CustomAgent{Enabled: &agentEnabled}, + BootDisk: &bootDisk, BootOrder: &bootOrder, CloudInitConfig: cloudInitConfig, CPUCores: &cpuCores, @@ -723,11 +750,11 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e KeyboardLayout: &keyboardLayout, NetworkDevices: networkDeviceObjects, OSType: &osType, + SCSIDevices: scsiDevices, SCSIHardware: &scsiHardware, SerialDevices: []string{"socket"}, SharedMemory: memorySharedObject, TabletDeviceEnabled: &tabletDeviceEnabled, - VGADevice: &proxmox.CustomVGADevice{Type: "serial0"}, VMID: &vmID, } @@ -743,6 +770,85 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e 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) } diff --git a/proxmoxtf/resource_virtual_environment_vm_test.go b/proxmoxtf/resource_virtual_environment_vm_test.go index c39cf38d..5bd23093 100644 --- a/proxmoxtf/resource_virtual_environment_vm_test.go +++ b/proxmoxtf/resource_virtual_environment_vm_test.go @@ -29,6 +29,7 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { testOptionalArguments(t, s, []string{ mkResourceVirtualEnvironmentVMCDROM, + mkResourceVirtualEnvironmentVMCloudInit, mkResourceVirtualEnvironmentVMCPU, mkResourceVirtualEnvironmentVMDisk, mkResourceVirtualEnvironmentVMKeyboardLayout, @@ -41,6 +42,7 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { testSchemaValueTypes(t, s, []string{ mkResourceVirtualEnvironmentVMCDROM, + mkResourceVirtualEnvironmentVMCloudInit, mkResourceVirtualEnvironmentVMCPU, mkResourceVirtualEnvironmentVMDisk, mkResourceVirtualEnvironmentVMKeyboardLayout, @@ -53,6 +55,7 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { schema.TypeList, schema.TypeList, schema.TypeList, + schema.TypeList, schema.TypeString, schema.TypeList, schema.TypeString, @@ -76,6 +79,102 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { 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) testOptionalArguments(t, cpuSchema, []string{