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

Initial support for custom cloud-init user data

This commit is contained in:
Dan Petersen 2019-12-29 06:58:35 +01:00
parent 4780318bd4
commit 1176ef9ee4
12 changed files with 679 additions and 283 deletions

View File

@ -240,13 +240,22 @@ This data source doesn't accept arguments.
##### File (proxmox_virtual_environment_file)
###### Arguments
* `content_type` - (Optional) The content type (`backup`, `images`, `iso` or `vztmpl`)
* `content_type` - (Optional) The content type
* `backup`
* `iso`
* `snippets`
* `vztmpl`
* `datastore_id` - (Required) The datastore id
* `node_name` - (Required) The node name
* `override_file_name` - (Optional) The file name to use instead of the source file name
* `source` - (Required) A path to a local file or a URL
* `source_checksum` - (Optional) The SHA256 checksum of the source file
* `source_insecure` - (Optional) Whether to skip the TLS verification step for HTTPS sources (defaults to `false`)
* `source_file` - (Optional) The source file (conflicts with `source_raw`)
* `checksum` - (Optional) The SHA256 checksum of the source file
* `file_name` - (Optional) The file name to use instead of the source file name
* `insecure` - (Optional) Whether to skip the TLS verification step for HTTPS sources (defaults to `false`)
* `path` - (Required) A path to a local file or a URL
* `source_raw` - (Optional) The raw source (conflicts with `source_file`)
* `data` - (Required) The raw data
* `file_name` - (Required) The file name
* `resize` - (Optional) The number of bytes to resize the file to
###### Attributes
* `file_modification_date` - The file modification date (RFC 3339)
@ -334,10 +343,11 @@ This resource doesn't expose any additional attributes.
* `ipv6` - (Optional) The IPv4 configuration
* `address` - (Optional) The IPv6 address (use `dhcp` for autodiscovery)
* `gateway` - (Optional) The IPv6 gateway (must be omitted when `dhcp` is used as the address)
* `user_account` - (Required) The user account configuration
* `user_account` - (Required) The user account configuration (conflicts with `user_data_file_id`)
* `keys` - (Required) The SSH keys
* `password` - (Optional) The SSH password
* `username` - (Required) The SSH username
* `user_data_file_id` - (Optional) The ID of a file containing custom user data (conflicts with `user_account`)
* `cpu` - (Optional) The CPU configuration
* `cores` - (Optional) The number of CPU cores (defaults to `1`)
* `hotplugged` - (Optional) The number of hotplugged vCPUs (defaults to `0`)

View File

@ -1,8 +1,40 @@
resource "proxmox_virtual_environment_file" "ubuntu_cloud_image" {
content_type = "iso"
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"
content_type = "iso"
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_file {
path = "https://cloud-images.ubuntu.com/bionic/current/bionic-server-cloudimg-amd64.img"
}
}
resource "proxmox_virtual_environment_file" "cloud_init_config" {
content_type = "snippets"
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_raw {
data = <<EOF
#cloud-config
chpasswd:
list: |
ubuntu:example
expire: False
hostname: terraform-provider-proxmox-example
packages:
- qemu-guest-agent
users:
- default
- name: ubuntu
groups: sudo
shell: /bin/bash
ssh-authorized-keys:
- ${trimspace(tls_private_key.example.public_key_openssh)}
sudo: ALL=(ALL) NOPASSWD:ALL
EOF
file_name = "terraform-provider-proxmox-example-cloud-init.yaml"
}
}
output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_content_type" {
@ -37,6 +69,6 @@ output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_node_name"
value = "${proxmox_virtual_environment_file.ubuntu_cloud_image.node_name}"
}
output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_source" {
value = "${proxmox_virtual_environment_file.ubuntu_cloud_image.source}"
output "resource_proxmox_virtual_environment_file_ubuntu_cloud_image_source_file" {
value = "${proxmox_virtual_environment_file.ubuntu_cloud_image.source_file}"
}

View File

@ -1,4 +1,8 @@
resource "proxmox_virtual_environment_vm" "example" {
agent {
enabled = true
}
cloud_init {
dns {
server = "1.1.1.1"
@ -15,6 +19,8 @@ resource "proxmox_virtual_environment_vm" "example" {
password = "proxmoxtf"
username = "ubuntu"
}
user_data_file_id = "${proxmox_virtual_environment_file.cloud_init_config.id}"
}
disk {

4
go.mod
View File

@ -5,5 +5,7 @@ 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
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/pkg/sftp v1.10.1
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586
)

13
go.sum
View File

@ -77,7 +77,6 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
@ -195,8 +194,11 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -254,6 +256,10 @@ github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1 h1:VasscCm72135zRysgrJDKsntdmPN+OuU3+nnHYA9wyc=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI=
@ -281,6 +287,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/svanharmelen/jsonapi v0.0.0-20180618144545-0c0828c3f16d/go.mod h1:BSTlc8jOjh0niykqEGVXOLXdi9o0r0kR8tCYiMvjFgw=
github.com/terraform-providers/terraform-provider-openstack v1.15.0/go.mod h1:2aQ6n/BtChAl1y2S60vebhyJyZXBsuAI5G4+lHrT1Ew=
github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@ -310,8 +317,9 @@ golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
@ -365,7 +373,6 @@ golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M=
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -177,20 +177,7 @@ func (c *VirtualEnvironmentClient) ValidateResponseCode(res *http.Response) erro
status = fmt.Sprintf("%s (%s)", status, strings.Join(errList, " - "))
}
switch res.StatusCode {
case 400, 500:
return fmt.Errorf("Received an HTTP %d response - Reason: %s", res.StatusCode, status)
case 401:
return fmt.Errorf("Received an HTTP %d response - Please verify that the specified credentials are valid", res.StatusCode)
case 403:
return fmt.Errorf("Received an HTTP %d response - Please verify that the user account has the necessary permissions", res.StatusCode)
case 404:
return fmt.Errorf("Received an HTTP %d response - Please verify that the endpoint refers to a supported version of the Proxmox Virtual Environment API", res.StatusCode)
case 501, 502, 503:
return fmt.Errorf("Received an HTTP %d response - Please verify that the Proxmox Virtual Environment API is healthy", res.StatusCode)
default:
return fmt.Errorf("Received an HTTP %d response", res.StatusCode)
}
return fmt.Errorf("Received an HTTP %d response - Reason: %s", res.StatusCode, status)
}
return nil

View File

@ -13,6 +13,9 @@ import (
"net/url"
"os"
"sort"
"strings"
"github.com/pkg/sftp"
)
// DeleteDatastoreFile deletes a file in a datastore.
@ -68,80 +71,155 @@ func (c *VirtualEnvironmentClient) ListDatastores(nodeName string, d *VirtualEnv
// UploadFileToDatastore uploads a file to a datastore.
func (c *VirtualEnvironmentClient) UploadFileToDatastore(d *VirtualEnvironmentDatastoreUploadRequestBody) (*VirtualEnvironmentDatastoreUploadResponseBody, error) {
r, w := io.Pipe()
switch d.ContentType {
case "iso", "vztmpl":
r, w := io.Pipe()
defer r.Close()
defer r.Close()
m := multipart.NewWriter(w)
m := multipart.NewWriter(w)
go func() {
defer w.Close()
defer m.Close()
go func() {
defer w.Close()
defer m.Close()
m.WriteField("content", d.ContentType)
m.WriteField("content", d.ContentType)
part, err := m.CreateFormFile("filename", d.FileName)
part, err := m.CreateFormFile("filename", d.FileName)
if err != nil {
return
}
_, err = io.Copy(part, d.FileReader)
if err != nil {
return
}
}()
// We need to store the multipart content in a temporary file to avoid using high amounts of memory.
// This is necessary due to Proxmox VE not supporting chunked transfers in v6.1 and earlier versions.
tempMultipartFile, err := ioutil.TempFile("", "multipart")
if err != nil {
return
return nil, err
}
_, err = io.Copy(part, d.FileReader)
tempMultipartFileName := tempMultipartFile.Name()
io.Copy(tempMultipartFile, r)
err = tempMultipartFile.Close()
if err != nil {
return
return nil, err
}
}()
// We need to store the multipart content in a temporary file to avoid using high amounts of memory.
// This is necessary due to Proxmox VE not supporting chunked transfers in v6.1 and earlier versions.
tempMultipartFile, err := ioutil.TempFile("", "multipart")
defer os.Remove(tempMultipartFileName)
if err != nil {
return nil, err
// Now that the multipart data is stored in a file, we can go ahead and do a HTTP POST request.
fileReader, err := os.Open(tempMultipartFileName)
if err != nil {
return nil, err
}
defer fileReader.Close()
fileInfo, err := fileReader.Stat()
if err != nil {
return nil, err
}
fileSize := fileInfo.Size()
reqBody := &VirtualEnvironmentMultiPartData{
Boundary: m.Boundary(),
Reader: fileReader,
Size: &fileSize,
}
resBody := &VirtualEnvironmentDatastoreUploadResponseBody{}
err = c.DoRequest(hmPOST, fmt.Sprintf("nodes/%s/storage/%s/upload", url.PathEscape(d.NodeName), url.PathEscape(d.DatastoreID)), reqBody, resBody)
if err != nil {
return nil, err
}
return resBody, nil
default:
// We need to upload all other files using SFTP due to API limitations.
// Hopefully, this will not be required in future releases of Proxmox VE.
sshClient, err := c.OpenNodeShell(d.NodeName)
if err != nil {
return nil, err
}
defer sshClient.Close()
sshSession, err := sshClient.NewSession()
if err != nil {
return nil, err
}
buf, err := sshSession.CombinedOutput(
fmt.Sprintf(`grep -Pzo ': %s\s+path\s+[^\s]+' /etc/pve/storage.cfg | grep -Pzo '/[^\s]*' | tr -d '\000'`, d.DatastoreID),
)
if err != nil {
sshSession.Close()
return nil, err
}
sshSession.Close()
datastorePath := strings.Trim(string(buf), "\000")
if datastorePath == "" {
return nil, errors.New("Failed to determine the datastore path")
}
remoteFileDir := datastorePath
switch d.ContentType {
default:
remoteFileDir += fmt.Sprintf("/%s", d.ContentType)
}
remoteFilePath := fmt.Sprintf("%s/%s", remoteFileDir, d.FileName)
sftpClient, err := sftp.NewClient(sshClient)
if err != nil {
return nil, err
}
defer sftpClient.Close()
err = sftpClient.MkdirAll(remoteFileDir)
if err != nil {
return nil, err
}
remoteFile, err := sftpClient.Create(remoteFilePath)
if err != nil {
return nil, err
}
defer remoteFile.Close()
_, err = remoteFile.ReadFrom(d.FileReader)
if err != nil {
return nil, err
}
return &VirtualEnvironmentDatastoreUploadResponseBody{}, nil
}
tempMultipartFileName := tempMultipartFile.Name()
io.Copy(tempMultipartFile, r)
err = tempMultipartFile.Close()
if err != nil {
return nil, err
}
defer os.Remove(tempMultipartFileName)
// Now that the multipart data is stored in a file, we can go ahead and do a HTTP POST request.
fileReader, err := os.Open(tempMultipartFileName)
if err != nil {
return nil, err
}
defer fileReader.Close()
fileInfo, err := fileReader.Stat()
if err != nil {
return nil, err
}
fileSize := fileInfo.Size()
reqBody := &VirtualEnvironmentMultiPartData{
Boundary: m.Boundary(),
Reader: fileReader,
Size: &fileSize,
}
resBody := &VirtualEnvironmentDatastoreUploadResponseBody{}
err = c.DoRequest(hmPOST, fmt.Sprintf("nodes/%s/storage/%s/upload", url.PathEscape(d.NodeName), url.PathEscape(d.DatastoreID)), reqBody, resBody)
if err != nil {
return nil, err
}
return resBody, nil
}

View File

@ -16,37 +16,7 @@ import (
// 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)
sshClient, err := c.OpenNodeShell(nodeName)
if err != nil {
return err
@ -62,7 +32,7 @@ func (c *VirtualEnvironmentClient) ExecuteNodeCommands(nodeName string, commands
defer sshSession.Close()
_, err = sshSession.CombinedOutput(
output, err := sshSession.CombinedOutput(
fmt.Sprintf(
"/bin/bash -c '%s'",
strings.ReplaceAll(strings.Join(commands, " && "), "'", "'\"'\"'"),
@ -70,12 +40,36 @@ func (c *VirtualEnvironmentClient) ExecuteNodeCommands(nodeName string, commands
)
if err != nil {
return err
return errors.New(string(output))
}
return nil
}
// GetNodeIP retrieves the IP address of a node.
func (c *VirtualEnvironmentClient) GetNodeIP(nodeName string) (*string, error) {
networkDevices, err := c.ListNodeNetworkDevices(nodeName)
if err != nil {
return nil, err
}
nodeAddress := ""
for _, d := range networkDevices {
if d.Address != nil {
nodeAddress = *d.Address
break
}
}
if nodeAddress == "" {
return nil, fmt.Errorf("Failed to determine the IP address of node \"%s\"", nodeName)
}
return &nodeAddress, nil
}
// ListNodeNetworkDevices retrieves a list of network devices for a specific nodes.
func (c *VirtualEnvironmentClient) ListNodeNetworkDevices(nodeName string) ([]*VirtualEnvironmentNodeNetworkDeviceListResponseData, error) {
resBody := &VirtualEnvironmentNodeNetworkDeviceListResponseBody{}
@ -115,3 +109,28 @@ func (c *VirtualEnvironmentClient) ListNodes() ([]*VirtualEnvironmentNodeListRes
return resBody.Data, nil
}
// OpenNodeShell establishes a new SSH connection to a node.
func (c *VirtualEnvironmentClient) OpenNodeShell(nodeName string) (*ssh.Client, error) {
nodeAddress, err := c.GetNodeIP(nodeName)
if err != nil {
return nil, err
}
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 nil, err
}
return sshClient, nil
}

View File

@ -5,9 +5,9 @@
package proxmoxtf
import (
"bytes"
"crypto/sha256"
"crypto/tls"
"errors"
"fmt"
"io"
"io/ioutil"
@ -24,11 +24,13 @@ import (
)
const (
dvResourceVirtualEnvironmentFileContentType = ""
dvResourceVirtualEnvironmentFileOverrideFileName = ""
dvResourceVirtualEnvironmentFileSourceChanged = false
dvResourceVirtualEnvironmentFileSourceChecksum = ""
dvResourceVirtualEnvironmentFileSourceInsecure = false
dvResourceVirtualEnvironmentFileContentType = ""
dvResourceVirtualEnvironmentFileSourceData = ""
dvResourceVirtualEnvironmentFileSourceFileChanged = false
dvResourceVirtualEnvironmentFileSourceFileChecksum = ""
dvResourceVirtualEnvironmentFileSourceFileFileName = ""
dvResourceVirtualEnvironmentFileSourceFileInsecure = false
dvResourceVirtualEnvironmentFileSourceRawResize = 0
mkResourceVirtualEnvironmentFileContentType = "content_type"
mkResourceVirtualEnvironmentFileDatastoreID = "datastore_id"
@ -36,23 +38,29 @@ const (
mkResourceVirtualEnvironmentFileFileName = "file_name"
mkResourceVirtualEnvironmentFileFileSize = "file_size"
mkResourceVirtualEnvironmentFileFileTag = "file_tag"
mkResourceVirtualEnvironmentFileOverrideFileName = "override_file_name"
mkResourceVirtualEnvironmentFileNodeName = "node_name"
mkResourceVirtualEnvironmentFileSource = "source"
mkResourceVirtualEnvironmentFileSourceChanged = "source_changed"
mkResourceVirtualEnvironmentFileSourceChecksum = "source_checksum"
mkResourceVirtualEnvironmentFileSourceInsecure = "source_insecure"
mkResourceVirtualEnvironmentFileSourceFile = "source_file"
mkResourceVirtualEnvironmentFileSourceFilePath = "path"
mkResourceVirtualEnvironmentFileSourceFileChanged = "changed"
mkResourceVirtualEnvironmentFileSourceFileChecksum = "checksum"
mkResourceVirtualEnvironmentFileSourceFileFileName = "file_name"
mkResourceVirtualEnvironmentFileSourceFileInsecure = "insecure"
mkResourceVirtualEnvironmentFileSourceRaw = "source_raw"
mkResourceVirtualEnvironmentFileSourceRawData = "data"
mkResourceVirtualEnvironmentFileSourceRawFileName = "file_name"
mkResourceVirtualEnvironmentFileSourceRawResize = "resize"
)
func resourceVirtualEnvironmentFile() *schema.Resource {
return &schema.Resource{
Schema: map[string]*schema.Schema{
mkResourceVirtualEnvironmentFileContentType: &schema.Schema{
Type: schema.TypeString,
Description: "The content type",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileContentType,
Type: schema.TypeString,
Description: "The content type",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileContentType,
ValidateFunc: getContentTypeValidator(),
},
mkResourceVirtualEnvironmentFileDatastoreID: &schema.Schema{
Type: schema.TypeString,
@ -83,45 +91,94 @@ func resourceVirtualEnvironmentFile() *schema.Resource {
Computed: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileOverrideFileName: &schema.Schema{
Type: schema.TypeString,
Description: "The file name to use instead of the source file name",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileOverrideFileName,
},
mkResourceVirtualEnvironmentFileNodeName: &schema.Schema{
Type: schema.TypeString,
Description: "The node name",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileSource: &schema.Schema{
Type: schema.TypeString,
Description: "A path to a local file or a URL",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileSourceChanged: &schema.Schema{
Type: schema.TypeBool,
Description: "Whether the source has changed since the last run",
mkResourceVirtualEnvironmentFileSourceFile: &schema.Schema{
Type: schema.TypeList,
Description: "The source file",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceChanged,
DefaultFunc: func() (interface{}, error) {
return make([]interface{}, 1), nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkResourceVirtualEnvironmentFileSourceFilePath: &schema.Schema{
Type: schema.TypeString,
Description: "A path to a local file or a URL",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileSourceFileChanged: &schema.Schema{
Type: schema.TypeBool,
Description: "Whether the source file has changed since the last run",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceFileChanged,
},
mkResourceVirtualEnvironmentFileSourceFileChecksum: &schema.Schema{
Type: schema.TypeString,
Description: "The SHA256 checksum of the source file",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceFileChecksum,
},
mkResourceVirtualEnvironmentFileSourceFileFileName: &schema.Schema{
Type: schema.TypeString,
Description: "The file name to use instead of the source file name",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceFileFileName,
},
mkResourceVirtualEnvironmentFileSourceFileInsecure: &schema.Schema{
Type: schema.TypeBool,
Description: "Whether to skip the TLS verification step for HTTPS sources",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceFileInsecure,
},
},
},
MaxItems: 1,
MinItems: 0,
},
mkResourceVirtualEnvironmentFileSourceChecksum: &schema.Schema{
Type: schema.TypeString,
Description: "The SHA256 checksum of the source file",
mkResourceVirtualEnvironmentFileSourceRaw: &schema.Schema{
Type: schema.TypeList,
Description: "The raw source",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceChecksum,
},
mkResourceVirtualEnvironmentFileSourceInsecure: &schema.Schema{
Type: schema.TypeBool,
Description: "Whether to skip the TLS verification step for HTTPS sources",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceInsecure,
DefaultFunc: func() (interface{}, error) {
return make([]interface{}, 1), nil
},
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
mkResourceVirtualEnvironmentFileSourceRawData: &schema.Schema{
Type: schema.TypeString,
Description: "The raw data",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileSourceRawFileName: &schema.Schema{
Type: schema.TypeString,
Description: "The file name",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileSourceRawResize: &schema.Schema{
Type: schema.TypeInt,
Description: "The number of bytes to resize the file to",
Optional: true,
ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceRawResize,
},
},
},
MaxItems: 1,
MinItems: 0,
},
},
Create: resourceVirtualEnvironmentFileCreate,
@ -152,87 +209,144 @@ func resourceVirtualEnvironmentFileCreate(d *schema.ResourceData, m interface{})
}
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string)
sourceChecksum := d.Get(mkResourceVirtualEnvironmentFileSourceChecksum).(string)
sourceInsecure := d.Get(mkResourceVirtualEnvironmentFileSourceInsecure).(bool)
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
sourceRaw := d.Get(mkResourceVirtualEnvironmentFileSourceRaw).([]interface{})
sourceFile := ""
sourceFilePathLocal := ""
// Download the source file, if it's not available locally.
if resourceVirtualEnvironmentFileIsURL(d, m) {
log.Printf("[DEBUG] Downloading file from '%s'", source)
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: sourceInsecure,
},
},
}
res, err := httpClient.Get(source)
if err != nil {
return err
}
defer res.Body.Close()
tempDownloadedFile, err := ioutil.TempFile("", "download")
if err != nil {
return err
}
tempDownloadedFileName := tempDownloadedFile.Name()
_, err = io.Copy(tempDownloadedFile, res.Body)
if err != nil {
tempDownloadedFile.Close()
return err
}
tempDownloadedFile.Close()
defer os.Remove(tempDownloadedFileName)
sourceFile = tempDownloadedFileName
} else {
sourceFile = source
// Determine if both source_data and source_file is specified as this is not supported.
if len(sourceFile) > 0 && len(sourceRaw) > 0 {
return fmt.Errorf(
"Please specify \"%s.%s\" or \"%s\" - not both",
mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceFilePath,
mkResourceVirtualEnvironmentFileSourceRaw,
)
}
// Calculate the checksum of the source file now that it's available locally.
if sourceChecksum != "" {
file, err := os.Open(sourceFile)
// Determine if we're dealing with raw file data or a reference to a file or URL.
// In case of a URL, we must first download the file before proceeding.
// This is due to lack of support for chunked transfers in the Proxmox VE API.
if len(sourceFile) > 0 {
sourceFileBlock := sourceFile[0].(map[string]interface{})
sourceFilePath := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
sourceFileChecksum := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileChecksum].(string)
sourceFileInsecure := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileInsecure].(bool)
if err != nil {
return err
if resourceVirtualEnvironmentFileIsURL(d, m) {
log.Printf("[DEBUG] Downloading file from '%s'", sourceFilePath)
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: sourceFileInsecure,
},
},
}
res, err := httpClient.Get(sourceFilePath)
if err != nil {
return err
}
defer res.Body.Close()
tempDownloadedFile, err := ioutil.TempFile("", "download")
if err != nil {
return err
}
tempDownloadedFileName := tempDownloadedFile.Name()
_, err = io.Copy(tempDownloadedFile, res.Body)
if err != nil {
tempDownloadedFile.Close()
return err
}
tempDownloadedFile.Close()
defer os.Remove(tempDownloadedFileName)
sourceFilePathLocal = tempDownloadedFileName
} else {
sourceFilePathLocal = sourceFilePath
}
h := sha256.New()
_, err = io.Copy(h, file)
// Calculate the checksum of the source file now that it's available locally.
if sourceFileChecksum != "" {
file, err := os.Open(sourceFilePathLocal)
if err != nil {
return err
}
h := sha256.New()
_, err = io.Copy(h, file)
if err != nil {
file.Close()
return err
}
if err != nil {
file.Close()
calculatedChecksum := fmt.Sprintf("%x", h.Sum(nil))
log.Printf("[DEBUG] The calculated SHA256 checksum for source \"%s\" is \"%s\"", sourceFilePath, calculatedChecksum)
if sourceFileChecksum != calculatedChecksum {
return fmt.Errorf("The calculated SHA256 checksum \"%s\" does not match source checksum \"%s\"", calculatedChecksum, sourceFileChecksum)
}
}
} else if len(sourceRaw) > 0 {
sourceRawBlock := sourceRaw[0].(map[string]interface{})
sourceRawData := sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawData].(string)
sourceRawResize := sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawResize].(int)
if sourceRawResize > 0 {
if len(sourceRawData) <= sourceRawResize {
sourceRawData = fmt.Sprintf(fmt.Sprintf("%%-%dv", sourceRawResize), sourceRawData)
} else {
return fmt.Errorf("Cannot resize %d bytes to %d bytes", len(sourceRawData), sourceRawResize)
}
}
tempRawFile, err := ioutil.TempFile("", "raw")
if err != nil {
return err
}
file.Close()
tempRawFileName := tempRawFile.Name()
_, err = io.Copy(tempRawFile, bytes.NewBufferString(sourceRawData))
calculatedChecksum := fmt.Sprintf("%x", h.Sum(nil))
if err != nil {
tempRawFile.Close()
log.Printf("[DEBUG] The calculated SHA256 checksum for source \"%s\" is \"%s\"", source, calculatedChecksum)
if sourceChecksum != calculatedChecksum {
return fmt.Errorf("The calculated SHA256 checksum \"%s\" does not match source checksum \"%s\"", calculatedChecksum, sourceChecksum)
return err
}
tempRawFile.Close()
defer os.Remove(tempRawFileName)
sourceFilePathLocal = tempRawFileName
} else {
return fmt.Errorf(
"Please specify either \"%s.%s\" or \"%s\"",
mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceFilePath,
mkResourceVirtualEnvironmentFileSourceRaw,
)
}
// Open the source file for reading in order to upload it.
file, err := os.Open(sourceFile)
file, err := os.Open(sourceFilePathLocal)
if err != nil {
return err
@ -267,69 +381,101 @@ func resourceVirtualEnvironmentFileCreate(d *schema.ResourceData, m interface{})
func resourceVirtualEnvironmentFileGetContentType(d *schema.ResourceData, m interface{}) (*string, error) {
contentType := d.Get(mkResourceVirtualEnvironmentFileContentType).(string)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string)
supportedTypes := []string{"backup", "images", "iso", "vztmpl"}
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
sourceRaw := d.Get(mkResourceVirtualEnvironmentFileSourceRaw).([]interface{})
sourceFilePath := ""
if len(sourceFile) > 0 {
sourceFileBlock := sourceFile[0].(map[string]interface{})
sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
} else if len(sourceRaw) > 0 {
sourceRawBlock := sourceRaw[0].(map[string]interface{})
sourceFilePath = sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawFileName].(string)
} else {
return nil, fmt.Errorf(
"Missing argument \"%s.%s\" or \"%s\"",
mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceFilePath,
mkResourceVirtualEnvironmentFileSourceRaw,
)
}
if contentType == "" {
if strings.HasSuffix(source, ".tar.xz") {
if strings.HasSuffix(sourceFilePath, ".tar.xz") {
contentType = "vztmpl"
} else {
ext := strings.TrimLeft(strings.ToLower(filepath.Ext(source)), ".")
ext := strings.TrimLeft(strings.ToLower(filepath.Ext(sourceFilePath)), ".")
switch ext {
case "img", "iso":
contentType = "iso"
case "yaml", "yml":
contentType = "snippets"
}
}
if contentType == "" {
return nil, fmt.Errorf(
"Cannot determine the content type of source \"%s\" - Please manually define the \"%s\" argument (supported: %s)",
source,
"Cannot determine the content type of source \"%s\" - Please manually define the \"%s\" argument",
sourceFilePath,
mkResourceVirtualEnvironmentFileContentType,
strings.Join(supportedTypes, " or "),
)
}
}
for _, v := range supportedTypes {
if v == contentType {
return &contentType, nil
}
ctValidator := getContentTypeValidator()
_, errs := ctValidator(contentType, mkResourceVirtualEnvironmentFileContentType)
if len(errs) > 0 {
return nil, errs[0]
}
return nil, fmt.Errorf(
"Unsupported content type \"%s\" for source \"%s\" (supported: %s)",
contentType,
source,
strings.Join(supportedTypes, " or "),
)
return &contentType, nil
}
func resourceVirtualEnvironmentFileGetFileName(d *schema.ResourceData, m interface{}) (*string, error) {
fileName := d.Get(mkResourceVirtualEnvironmentFileOverrideFileName).(string)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string)
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
sourceRaw := d.Get(mkResourceVirtualEnvironmentFileSourceRaw).([]interface{})
if fileName == "" {
sourceFileFileName := ""
sourceFilePath := ""
if len(sourceFile) > 0 {
sourceFileBlock := sourceFile[0].(map[string]interface{})
sourceFileFileName = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileFileName].(string)
sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
} else if len(sourceRaw) > 0 {
sourceRawBlock := sourceRaw[0].(map[string]interface{})
sourceFileFileName = sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawFileName].(string)
} else {
return nil, fmt.Errorf(
"Missing argument \"%s.%s\"",
mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceFilePath,
)
}
if sourceFileFileName == "" {
if resourceVirtualEnvironmentFileIsURL(d, m) {
downloadURL, err := url.ParseRequestURI(source)
downloadURL, err := url.ParseRequestURI(sourceFilePath)
if err != nil {
return nil, err
}
path := strings.Split(downloadURL.Path, "/")
fileName = path[len(path)-1]
sourceFileFileName = path[len(path)-1]
if fileName == "" {
return nil, errors.New("Failed to determine file name from source URL")
if sourceFileFileName == "" {
return nil, fmt.Errorf("Failed to determine file name from the URL \"%s\"", sourceFilePath)
}
} else {
fileName = filepath.Base(source)
sourceFileFileName = filepath.Base(sourceFilePath)
}
}
return &fileName, nil
return &sourceFileFileName, nil
}
func resourceVirtualEnvironmentFileGetVolumeID(d *schema.ResourceData, m interface{}) (*string, error) {
@ -352,9 +498,17 @@ func resourceVirtualEnvironmentFileGetVolumeID(d *schema.ResourceData, m interfa
}
func resourceVirtualEnvironmentFileIsURL(d *schema.ResourceData, m interface{}) bool {
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string)
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
sourceFilePath := ""
return strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://")
if len(sourceFile) > 0 {
sourceFileBlock := sourceFile[0].(map[string]interface{})
sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
} else {
return false
}
return strings.HasPrefix(sourceFilePath, "http://") || strings.HasPrefix(sourceFilePath, "https://")
}
func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) error {
@ -367,6 +521,15 @@ func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) e
datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string)
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string)
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
sourceFilePath := ""
if len(sourceFile) == 0 {
return nil
}
sourceFileBlock := sourceFile[0].(map[string]interface{})
sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
list, err := veClient.ListDatastoreFiles(nodeName, datastoreID)
@ -374,17 +537,21 @@ func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) e
return err
}
fileIsURL := resourceVirtualEnvironmentFileIsURL(d, m)
fileName, err := resourceVirtualEnvironmentFileGetFileName(d, m)
if err != nil {
return err
}
for _, v := range list {
if v.VolumeID == d.Id() {
fileName, _ := resourceVirtualEnvironmentFileGetFileName(d, m)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string)
var fileModificationDate string
var fileSize int64
var fileTag string
if resourceVirtualEnvironmentFileIsURL(d, m) {
res, err := http.Head(source)
if fileIsURL {
res, err := http.Head(sourceFilePath)
if err != nil {
return err
@ -425,7 +592,7 @@ func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) e
fileTag = ""
}
} else {
f, err := os.Open(source)
f, err := os.Open(sourceFilePath)
if err != nil {
return err
@ -452,7 +619,7 @@ func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) e
d.Set(mkResourceVirtualEnvironmentFileFileName, *fileName)
d.Set(mkResourceVirtualEnvironmentFileFileSize, fileSize)
d.Set(mkResourceVirtualEnvironmentFileFileTag, fileTag)
d.Set(mkResourceVirtualEnvironmentFileSourceChanged, lastFileModificationDate != fileModificationDate || lastFileSize != fileSize || lastFileTag != fileTag)
d.Set(mkResourceVirtualEnvironmentFileSourceFileChanged, lastFileModificationDate != fileModificationDate || lastFileSize != fileSize || lastFileTag != fileTag)
return nil
}

View File

@ -26,15 +26,12 @@ func TestResourceVirtualEnvironmentFileSchema(t *testing.T) {
testRequiredArguments(t, s, []string{
mkResourceVirtualEnvironmentFileDatastoreID,
mkResourceVirtualEnvironmentFileNodeName,
mkResourceVirtualEnvironmentFileSource,
})
testOptionalArguments(t, s, []string{
mkResourceVirtualEnvironmentFileContentType,
mkResourceVirtualEnvironmentFileOverrideFileName,
mkResourceVirtualEnvironmentFileSourceChanged,
mkResourceVirtualEnvironmentFileSourceChecksum,
mkResourceVirtualEnvironmentFileSourceInsecure,
mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceRaw,
})
testComputedAttributes(t, s, []string{
@ -51,12 +48,9 @@ func TestResourceVirtualEnvironmentFileSchema(t *testing.T) {
mkResourceVirtualEnvironmentFileFileName,
mkResourceVirtualEnvironmentFileFileSize,
mkResourceVirtualEnvironmentFileFileTag,
mkResourceVirtualEnvironmentFileOverrideFileName,
mkResourceVirtualEnvironmentFileSourceChanged,
mkResourceVirtualEnvironmentFileNodeName,
mkResourceVirtualEnvironmentFileSource,
mkResourceVirtualEnvironmentFileSourceChecksum,
mkResourceVirtualEnvironmentFileSourceInsecure,
mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceRaw,
}, []schema.ValueType{
schema.TypeString,
schema.TypeString,
@ -65,10 +59,55 @@ func TestResourceVirtualEnvironmentFileSchema(t *testing.T) {
schema.TypeInt,
schema.TypeString,
schema.TypeString,
schema.TypeList,
schema.TypeList,
})
sourceFileSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentFileSourceFile)
testRequiredArguments(t, sourceFileSchema, []string{
mkResourceVirtualEnvironmentFileSourceFilePath,
})
testOptionalArguments(t, sourceFileSchema, []string{
mkResourceVirtualEnvironmentFileSourceFileChanged,
mkResourceVirtualEnvironmentFileSourceFileChecksum,
mkResourceVirtualEnvironmentFileSourceFileFileName,
mkResourceVirtualEnvironmentFileSourceFileInsecure,
})
testSchemaValueTypes(t, sourceFileSchema, []string{
mkResourceVirtualEnvironmentFileSourceFileChanged,
mkResourceVirtualEnvironmentFileSourceFileChecksum,
mkResourceVirtualEnvironmentFileSourceFileFileName,
mkResourceVirtualEnvironmentFileSourceFileInsecure,
mkResourceVirtualEnvironmentFileSourceFilePath,
}, []schema.ValueType{
schema.TypeBool,
schema.TypeString,
schema.TypeString,
schema.TypeString,
schema.TypeBool,
schema.TypeString,
})
sourceRawSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentFileSourceRaw)
testRequiredArguments(t, sourceRawSchema, []string{
mkResourceVirtualEnvironmentFileSourceRawData,
mkResourceVirtualEnvironmentFileSourceRawFileName,
})
testOptionalArguments(t, sourceRawSchema, []string{
mkResourceVirtualEnvironmentFileSourceRawResize,
})
testSchemaValueTypes(t, sourceRawSchema, []string{
mkResourceVirtualEnvironmentFileSourceRawData,
mkResourceVirtualEnvironmentFileSourceRawFileName,
mkResourceVirtualEnvironmentFileSourceRawResize,
}, []schema.ValueType{
schema.TypeString,
schema.TypeString,
schema.TypeInt,
})
}

View File

@ -24,6 +24,7 @@ const (
dvResourceVirtualEnvironmentVMCloudInitDNSDomain = ""
dvResourceVirtualEnvironmentVMCloudInitDNSServer = ""
dvResourceVirtualEnvironmentVMCloudInitUserAccountPassword = ""
dvResourceVirtualEnvironmentVMCloudInitUserDataFileID = ""
dvResourceVirtualEnvironmentVMCPUCores = 1
dvResourceVirtualEnvironmentVMCPUHotplugged = 0
dvResourceVirtualEnvironmentVMCPUSockets = 1
@ -72,6 +73,7 @@ const (
mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys = "keys"
mkResourceVirtualEnvironmentVMCloudInitUserAccountPassword = "password"
mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername = "username"
mkResourceVirtualEnvironmentVMCloudInitUserDataFileID = "user_data_file_id"
mkResourceVirtualEnvironmentVMCPU = "cpu"
mkResourceVirtualEnvironmentVMCPUCores = "cores"
mkResourceVirtualEnvironmentVMCPUHotplugged = "hotplugged"
@ -318,6 +320,14 @@ func resourceVirtualEnvironmentVM() *schema.Resource {
MaxItems: 1,
MinItems: 0,
},
mkResourceVirtualEnvironmentVMCloudInitUserDataFileID: {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Description: "The ID of a file containing custom user data",
Default: dvResourceVirtualEnvironmentVMCloudInitUserDataFileID,
ValidateFunc: getFileIDValidator(),
},
},
},
MaxItems: 1,
@ -770,6 +780,14 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e
cloudInitConfig.Username = &username
}
cloudInitUserDataFileID := cloudInitBlock[mkResourceVirtualEnvironmentVMCloudInitUserDataFileID].(string)
if cloudInitUserDataFileID != "" {
cloudInitConfig.Files = &proxmox.CustomCloudInitFiles{
UserVolume: &cloudInitUserDataFileID,
}
}
}
cpu := d.Get(mkResourceVirtualEnvironmentVMCPU).([]interface{})
@ -1075,7 +1093,9 @@ func resourceVirtualEnvironmentVMCreateImportedDisks(d *schema.ResourceData, m i
speedBlock := speed[0].(map[string]interface{})
speedLimitRead := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedRead].(int)
speedLimitReadBurstable := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedReadBurstable].(int)
speedLimitWrite := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedWrite].(int)
speedLimitWriteBurstable := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedWriteBurstable].(int)
diskOptions := ""
@ -1083,21 +1103,37 @@ func resourceVirtualEnvironmentVMCreateImportedDisks(d *schema.ResourceData, m i
diskOptions += fmt.Sprintf(",mbps_rd=%d", speedLimitRead)
}
if speedLimitReadBurstable > 0 {
diskOptions += fmt.Sprintf(",mbps_rd_max=%d", speedLimitReadBurstable)
}
if speedLimitWrite > 0 {
diskOptions += fmt.Sprintf(",mbps_wr=%d", speedLimitWrite)
}
if speedLimitWriteBurstable > 0 {
diskOptions += fmt.Sprintf(",mbps_wr_max=%d", speedLimitWriteBurstable)
}
fileIDParts := strings.Split(fileID, ":")
filePath := fmt.Sprintf("/var/lib/vz/template/%s", fileIDParts[1])
filePath := ""
if strings.HasPrefix(fileIDParts[1], "iso/") {
filePath = fmt.Sprintf("/template/%s", fileIDParts[1])
} else {
filePath = fmt.Sprintf("/%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%s", vmID, i, datastoreID, vmID, diskCount+importedDiskCount, diskOptions),
fmt.Sprintf("rm -f %s", filePathTmp),
`set -e`,
fmt.Sprintf(`cp "$(grep -Pzo ': %s\s+path\s+[^\s]+' /etc/pve/storage.cfg | grep -Pzo '/[^\s]*' | tr -d '\000')%s" %s`, fileIDParts[0], 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%s`, vmID, i, datastoreID, vmID, diskCount+importedDiskCount, diskOptions),
fmt.Sprintf(`rm -f %s`, filePathTmp),
)
importedDiskCount++

View File

@ -13,8 +13,21 @@ import (
"github.com/hashicorp/terraform/helper/validation"
)
func getContentTypeValidator() schema.SchemaValidateFunc {
return validation.StringInSlice([]string{
"backup",
"iso",
"snippets",
"vztmpl",
}, false)
}
func getFileFormatValidator() schema.SchemaValidateFunc {
return validation.StringInSlice([]string{"qcow2", "raw", "vmdk"}, false)
return validation.StringInSlice([]string{
"qcow2",
"raw",
"vmdk",
}, false)
}
func getFileIDValidator() schema.SchemaValidateFunc {