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) ##### File (proxmox_virtual_environment_file)
###### Arguments ###### 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 * `datastore_id` - (Required) The datastore id
* `node_name` - (Required) The node name * `node_name` - (Required) The node name
* `override_file_name` - (Optional) The file name to use instead of the source file name * `source_file` - (Optional) The source file (conflicts with `source_raw`)
* `source` - (Required) A path to a local file or a URL * `checksum` - (Optional) The SHA256 checksum of the source file
* `source_checksum` - (Optional) The SHA256 checksum of the source file * `file_name` - (Optional) The file name to use instead of the source file name
* `source_insecure` - (Optional) Whether to skip the TLS verification step for HTTPS sources (defaults to `false`) * `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 ###### Attributes
* `file_modification_date` - The file modification date (RFC 3339) * `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 * `ipv6` - (Optional) The IPv4 configuration
* `address` - (Optional) The IPv6 address (use `dhcp` for autodiscovery) * `address` - (Optional) The IPv6 address (use `dhcp` for autodiscovery)
* `gateway` - (Optional) The IPv6 gateway (must be omitted when `dhcp` is used as the address) * `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 * `keys` - (Required) The SSH keys
* `password` - (Optional) The SSH password * `password` - (Optional) The SSH password
* `username` - (Required) The SSH username * `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 * `cpu` - (Optional) The CPU configuration
* `cores` - (Optional) The number of CPU cores (defaults to `1`) * `cores` - (Optional) The number of CPU cores (defaults to `1`)
* `hotplugged` - (Optional) The number of hotplugged vCPUs (defaults to `0`) * `hotplugged` - (Optional) The number of hotplugged vCPUs (defaults to `0`)

View File

@ -1,8 +1,40 @@
resource "proxmox_virtual_environment_file" "ubuntu_cloud_image" { resource "proxmox_virtual_environment_file" "ubuntu_cloud_image" {
content_type = "iso" content_type = "iso"
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_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" { 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}" value = "${proxmox_virtual_environment_file.ubuntu_cloud_image.node_name}"
} }
output "resource_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}" value = "${proxmox_virtual_environment_file.ubuntu_cloud_image.source_file}"
} }

View File

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

4
go.mod
View File

@ -5,5 +5,7 @@ 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 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/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/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.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/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/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= 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/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/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/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/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/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/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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/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/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.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/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.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI= 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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/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/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= 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-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-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-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-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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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= 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-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-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-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/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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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, " - ")) status = fmt.Sprintf("%s (%s)", status, strings.Join(errList, " - "))
} }
switch res.StatusCode { return fmt.Errorf("Received an HTTP %d response - Reason: %s", res.StatusCode, status)
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 nil return nil

View File

@ -13,6 +13,9 @@ import (
"net/url" "net/url"
"os" "os"
"sort" "sort"
"strings"
"github.com/pkg/sftp"
) )
// DeleteDatastoreFile deletes a file in a datastore. // 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. // UploadFileToDatastore uploads a file to a datastore.
func (c *VirtualEnvironmentClient) UploadFileToDatastore(d *VirtualEnvironmentDatastoreUploadRequestBody) (*VirtualEnvironmentDatastoreUploadResponseBody, error) { 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() { go func() {
defer w.Close() defer w.Close()
defer m.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 { 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 { 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. defer os.Remove(tempMultipartFileName)
// 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 { // Now that the multipart data is stored in a file, we can go ahead and do a HTTP POST request.
return nil, err 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. // ExecuteNodeCommands executes commands on a given node.
func (c *VirtualEnvironmentClient) ExecuteNodeCommands(nodeName string, commands []string) error { 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. sshClient, err := c.OpenNodeShell(nodeName)
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 { if err != nil {
return err return err
@ -62,7 +32,7 @@ func (c *VirtualEnvironmentClient) ExecuteNodeCommands(nodeName string, commands
defer sshSession.Close() defer sshSession.Close()
_, err = sshSession.CombinedOutput( output, err := sshSession.CombinedOutput(
fmt.Sprintf( fmt.Sprintf(
"/bin/bash -c '%s'", "/bin/bash -c '%s'",
strings.ReplaceAll(strings.Join(commands, " && "), "'", "'\"'\"'"), strings.ReplaceAll(strings.Join(commands, " && "), "'", "'\"'\"'"),
@ -70,12 +40,36 @@ func (c *VirtualEnvironmentClient) ExecuteNodeCommands(nodeName string, commands
) )
if err != nil { if err != nil {
return err return errors.New(string(output))
} }
return nil 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. // ListNodeNetworkDevices retrieves a list of network devices for a specific nodes.
func (c *VirtualEnvironmentClient) ListNodeNetworkDevices(nodeName string) ([]*VirtualEnvironmentNodeNetworkDeviceListResponseData, error) { func (c *VirtualEnvironmentClient) ListNodeNetworkDevices(nodeName string) ([]*VirtualEnvironmentNodeNetworkDeviceListResponseData, error) {
resBody := &VirtualEnvironmentNodeNetworkDeviceListResponseBody{} resBody := &VirtualEnvironmentNodeNetworkDeviceListResponseBody{}
@ -115,3 +109,28 @@ func (c *VirtualEnvironmentClient) ListNodes() ([]*VirtualEnvironmentNodeListRes
return resBody.Data, nil 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 package proxmoxtf
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -24,11 +24,13 @@ import (
) )
const ( const (
dvResourceVirtualEnvironmentFileContentType = "" dvResourceVirtualEnvironmentFileContentType = ""
dvResourceVirtualEnvironmentFileOverrideFileName = "" dvResourceVirtualEnvironmentFileSourceData = ""
dvResourceVirtualEnvironmentFileSourceChanged = false dvResourceVirtualEnvironmentFileSourceFileChanged = false
dvResourceVirtualEnvironmentFileSourceChecksum = "" dvResourceVirtualEnvironmentFileSourceFileChecksum = ""
dvResourceVirtualEnvironmentFileSourceInsecure = false dvResourceVirtualEnvironmentFileSourceFileFileName = ""
dvResourceVirtualEnvironmentFileSourceFileInsecure = false
dvResourceVirtualEnvironmentFileSourceRawResize = 0
mkResourceVirtualEnvironmentFileContentType = "content_type" mkResourceVirtualEnvironmentFileContentType = "content_type"
mkResourceVirtualEnvironmentFileDatastoreID = "datastore_id" mkResourceVirtualEnvironmentFileDatastoreID = "datastore_id"
@ -36,23 +38,29 @@ const (
mkResourceVirtualEnvironmentFileFileName = "file_name" mkResourceVirtualEnvironmentFileFileName = "file_name"
mkResourceVirtualEnvironmentFileFileSize = "file_size" mkResourceVirtualEnvironmentFileFileSize = "file_size"
mkResourceVirtualEnvironmentFileFileTag = "file_tag" mkResourceVirtualEnvironmentFileFileTag = "file_tag"
mkResourceVirtualEnvironmentFileOverrideFileName = "override_file_name"
mkResourceVirtualEnvironmentFileNodeName = "node_name" mkResourceVirtualEnvironmentFileNodeName = "node_name"
mkResourceVirtualEnvironmentFileSource = "source" mkResourceVirtualEnvironmentFileSourceFile = "source_file"
mkResourceVirtualEnvironmentFileSourceChanged = "source_changed" mkResourceVirtualEnvironmentFileSourceFilePath = "path"
mkResourceVirtualEnvironmentFileSourceChecksum = "source_checksum" mkResourceVirtualEnvironmentFileSourceFileChanged = "changed"
mkResourceVirtualEnvironmentFileSourceInsecure = "source_insecure" mkResourceVirtualEnvironmentFileSourceFileChecksum = "checksum"
mkResourceVirtualEnvironmentFileSourceFileFileName = "file_name"
mkResourceVirtualEnvironmentFileSourceFileInsecure = "insecure"
mkResourceVirtualEnvironmentFileSourceRaw = "source_raw"
mkResourceVirtualEnvironmentFileSourceRawData = "data"
mkResourceVirtualEnvironmentFileSourceRawFileName = "file_name"
mkResourceVirtualEnvironmentFileSourceRawResize = "resize"
) )
func resourceVirtualEnvironmentFile() *schema.Resource { func resourceVirtualEnvironmentFile() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
mkResourceVirtualEnvironmentFileContentType: &schema.Schema{ mkResourceVirtualEnvironmentFileContentType: &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Description: "The content type", Description: "The content type",
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
Default: dvResourceVirtualEnvironmentFileContentType, Default: dvResourceVirtualEnvironmentFileContentType,
ValidateFunc: getContentTypeValidator(),
}, },
mkResourceVirtualEnvironmentFileDatastoreID: &schema.Schema{ mkResourceVirtualEnvironmentFileDatastoreID: &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -83,45 +91,94 @@ func resourceVirtualEnvironmentFile() *schema.Resource {
Computed: true, Computed: true,
ForceNew: 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{ mkResourceVirtualEnvironmentFileNodeName: &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Description: "The node name", Description: "The node name",
Required: true, Required: true,
ForceNew: true, ForceNew: true,
}, },
mkResourceVirtualEnvironmentFileSource: &schema.Schema{ mkResourceVirtualEnvironmentFileSourceFile: &schema.Schema{
Type: schema.TypeString, Type: schema.TypeList,
Description: "A path to a local file or a URL", Description: "The source file",
Required: true,
ForceNew: true,
},
mkResourceVirtualEnvironmentFileSourceChanged: &schema.Schema{
Type: schema.TypeBool,
Description: "Whether the source has changed since the last run",
Optional: true, Optional: true,
ForceNew: 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{ mkResourceVirtualEnvironmentFileSourceRaw: &schema.Schema{
Type: schema.TypeString, Type: schema.TypeList,
Description: "The SHA256 checksum of the source file", Description: "The raw source",
Optional: true, Optional: true,
ForceNew: true, ForceNew: true,
Default: dvResourceVirtualEnvironmentFileSourceChecksum, DefaultFunc: func() (interface{}, error) {
}, return make([]interface{}, 1), nil
mkResourceVirtualEnvironmentFileSourceInsecure: &schema.Schema{ },
Type: schema.TypeBool, Elem: &schema.Resource{
Description: "Whether to skip the TLS verification step for HTTPS sources", Schema: map[string]*schema.Schema{
Optional: true, mkResourceVirtualEnvironmentFileSourceRawData: &schema.Schema{
ForceNew: true, Type: schema.TypeString,
Default: dvResourceVirtualEnvironmentFileSourceInsecure, 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, Create: resourceVirtualEnvironmentFileCreate,
@ -152,87 +209,144 @@ func resourceVirtualEnvironmentFileCreate(d *schema.ResourceData, m interface{})
} }
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string) nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string) sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
sourceChecksum := d.Get(mkResourceVirtualEnvironmentFileSourceChecksum).(string) sourceRaw := d.Get(mkResourceVirtualEnvironmentFileSourceRaw).([]interface{})
sourceInsecure := d.Get(mkResourceVirtualEnvironmentFileSourceInsecure).(bool)
sourceFile := "" sourceFilePathLocal := ""
// Download the source file, if it's not available locally. // Determine if both source_data and source_file is specified as this is not supported.
if resourceVirtualEnvironmentFileIsURL(d, m) { if len(sourceFile) > 0 && len(sourceRaw) > 0 {
log.Printf("[DEBUG] Downloading file from '%s'", source) return fmt.Errorf(
"Please specify \"%s.%s\" or \"%s\" - not both",
httpClient := http.Client{ mkResourceVirtualEnvironmentFileSourceFile,
Transport: &http.Transport{ mkResourceVirtualEnvironmentFileSourceFilePath,
TLSClientConfig: &tls.Config{ mkResourceVirtualEnvironmentFileSourceRaw,
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
} }
// Calculate the checksum of the source file now that it's available locally. // Determine if we're dealing with raw file data or a reference to a file or URL.
if sourceChecksum != "" { // In case of a URL, we must first download the file before proceeding.
file, err := os.Open(sourceFile) // 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 { if resourceVirtualEnvironmentFileIsURL(d, m) {
return err 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() // Calculate the checksum of the source file now that it's available locally.
_, err = io.Copy(h, file) 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() 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 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) return err
if sourceChecksum != calculatedChecksum {
return fmt.Errorf("The calculated SHA256 checksum \"%s\" does not match source checksum \"%s\"", calculatedChecksum, sourceChecksum)
} }
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. // Open the source file for reading in order to upload it.
file, err := os.Open(sourceFile) file, err := os.Open(sourceFilePathLocal)
if err != nil { if err != nil {
return err return err
@ -267,69 +381,101 @@ func resourceVirtualEnvironmentFileCreate(d *schema.ResourceData, m interface{})
func resourceVirtualEnvironmentFileGetContentType(d *schema.ResourceData, m interface{}) (*string, error) { func resourceVirtualEnvironmentFileGetContentType(d *schema.ResourceData, m interface{}) (*string, error) {
contentType := d.Get(mkResourceVirtualEnvironmentFileContentType).(string) contentType := d.Get(mkResourceVirtualEnvironmentFileContentType).(string)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string) sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
supportedTypes := []string{"backup", "images", "iso", "vztmpl"} 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 contentType == "" {
if strings.HasSuffix(source, ".tar.xz") { if strings.HasSuffix(sourceFilePath, ".tar.xz") {
contentType = "vztmpl" contentType = "vztmpl"
} else { } else {
ext := strings.TrimLeft(strings.ToLower(filepath.Ext(source)), ".") ext := strings.TrimLeft(strings.ToLower(filepath.Ext(sourceFilePath)), ".")
switch ext { switch ext {
case "img", "iso": case "img", "iso":
contentType = "iso" contentType = "iso"
case "yaml", "yml":
contentType = "snippets"
} }
} }
if contentType == "" { if contentType == "" {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"Cannot determine the content type of source \"%s\" - Please manually define the \"%s\" argument (supported: %s)", "Cannot determine the content type of source \"%s\" - Please manually define the \"%s\" argument",
source, sourceFilePath,
mkResourceVirtualEnvironmentFileContentType, mkResourceVirtualEnvironmentFileContentType,
strings.Join(supportedTypes, " or "),
) )
} }
} }
for _, v := range supportedTypes { ctValidator := getContentTypeValidator()
if v == contentType { _, errs := ctValidator(contentType, mkResourceVirtualEnvironmentFileContentType)
return &contentType, nil
} if len(errs) > 0 {
return nil, errs[0]
} }
return nil, fmt.Errorf( return &contentType, nil
"Unsupported content type \"%s\" for source \"%s\" (supported: %s)",
contentType,
source,
strings.Join(supportedTypes, " or "),
)
} }
func resourceVirtualEnvironmentFileGetFileName(d *schema.ResourceData, m interface{}) (*string, error) { func resourceVirtualEnvironmentFileGetFileName(d *schema.ResourceData, m interface{}) (*string, error) {
fileName := d.Get(mkResourceVirtualEnvironmentFileOverrideFileName).(string) sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string) 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) { if resourceVirtualEnvironmentFileIsURL(d, m) {
downloadURL, err := url.ParseRequestURI(source) downloadURL, err := url.ParseRequestURI(sourceFilePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
path := strings.Split(downloadURL.Path, "/") path := strings.Split(downloadURL.Path, "/")
fileName = path[len(path)-1] sourceFileFileName = path[len(path)-1]
if fileName == "" { if sourceFileFileName == "" {
return nil, errors.New("Failed to determine file name from source URL") return nil, fmt.Errorf("Failed to determine file name from the URL \"%s\"", sourceFilePath)
} }
} else { } else {
fileName = filepath.Base(source) sourceFileFileName = filepath.Base(sourceFilePath)
} }
} }
return &fileName, nil return &sourceFileFileName, nil
} }
func resourceVirtualEnvironmentFileGetVolumeID(d *schema.ResourceData, m interface{}) (*string, error) { 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 { 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 { 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) datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string)
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(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) list, err := veClient.ListDatastoreFiles(nodeName, datastoreID)
@ -374,17 +537,21 @@ func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) e
return err return err
} }
fileIsURL := resourceVirtualEnvironmentFileIsURL(d, m)
fileName, err := resourceVirtualEnvironmentFileGetFileName(d, m)
if err != nil {
return err
}
for _, v := range list { for _, v := range list {
if v.VolumeID == d.Id() { if v.VolumeID == d.Id() {
fileName, _ := resourceVirtualEnvironmentFileGetFileName(d, m)
source := d.Get(mkResourceVirtualEnvironmentFileSource).(string)
var fileModificationDate string var fileModificationDate string
var fileSize int64 var fileSize int64
var fileTag string var fileTag string
if resourceVirtualEnvironmentFileIsURL(d, m) { if fileIsURL {
res, err := http.Head(source) res, err := http.Head(sourceFilePath)
if err != nil { if err != nil {
return err return err
@ -425,7 +592,7 @@ func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) e
fileTag = "" fileTag = ""
} }
} else { } else {
f, err := os.Open(source) f, err := os.Open(sourceFilePath)
if err != nil { if err != nil {
return err return err
@ -452,7 +619,7 @@ func resourceVirtualEnvironmentFileRead(d *schema.ResourceData, m interface{}) e
d.Set(mkResourceVirtualEnvironmentFileFileName, *fileName) d.Set(mkResourceVirtualEnvironmentFileFileName, *fileName)
d.Set(mkResourceVirtualEnvironmentFileFileSize, fileSize) d.Set(mkResourceVirtualEnvironmentFileFileSize, fileSize)
d.Set(mkResourceVirtualEnvironmentFileFileTag, fileTag) d.Set(mkResourceVirtualEnvironmentFileFileTag, fileTag)
d.Set(mkResourceVirtualEnvironmentFileSourceChanged, lastFileModificationDate != fileModificationDate || lastFileSize != fileSize || lastFileTag != fileTag) d.Set(mkResourceVirtualEnvironmentFileSourceFileChanged, lastFileModificationDate != fileModificationDate || lastFileSize != fileSize || lastFileTag != fileTag)
return nil return nil
} }

View File

@ -26,15 +26,12 @@ func TestResourceVirtualEnvironmentFileSchema(t *testing.T) {
testRequiredArguments(t, s, []string{ testRequiredArguments(t, s, []string{
mkResourceVirtualEnvironmentFileDatastoreID, mkResourceVirtualEnvironmentFileDatastoreID,
mkResourceVirtualEnvironmentFileNodeName, mkResourceVirtualEnvironmentFileNodeName,
mkResourceVirtualEnvironmentFileSource,
}) })
testOptionalArguments(t, s, []string{ testOptionalArguments(t, s, []string{
mkResourceVirtualEnvironmentFileContentType, mkResourceVirtualEnvironmentFileContentType,
mkResourceVirtualEnvironmentFileOverrideFileName, mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceChanged, mkResourceVirtualEnvironmentFileSourceRaw,
mkResourceVirtualEnvironmentFileSourceChecksum,
mkResourceVirtualEnvironmentFileSourceInsecure,
}) })
testComputedAttributes(t, s, []string{ testComputedAttributes(t, s, []string{
@ -51,12 +48,9 @@ func TestResourceVirtualEnvironmentFileSchema(t *testing.T) {
mkResourceVirtualEnvironmentFileFileName, mkResourceVirtualEnvironmentFileFileName,
mkResourceVirtualEnvironmentFileFileSize, mkResourceVirtualEnvironmentFileFileSize,
mkResourceVirtualEnvironmentFileFileTag, mkResourceVirtualEnvironmentFileFileTag,
mkResourceVirtualEnvironmentFileOverrideFileName,
mkResourceVirtualEnvironmentFileSourceChanged,
mkResourceVirtualEnvironmentFileNodeName, mkResourceVirtualEnvironmentFileNodeName,
mkResourceVirtualEnvironmentFileSource, mkResourceVirtualEnvironmentFileSourceFile,
mkResourceVirtualEnvironmentFileSourceChecksum, mkResourceVirtualEnvironmentFileSourceRaw,
mkResourceVirtualEnvironmentFileSourceInsecure,
}, []schema.ValueType{ }, []schema.ValueType{
schema.TypeString, schema.TypeString,
schema.TypeString, schema.TypeString,
@ -65,10 +59,55 @@ func TestResourceVirtualEnvironmentFileSchema(t *testing.T) {
schema.TypeInt, schema.TypeInt,
schema.TypeString, schema.TypeString,
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.TypeBool,
schema.TypeString, schema.TypeString,
schema.TypeString, schema.TypeString,
schema.TypeString,
schema.TypeBool, 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 = "" dvResourceVirtualEnvironmentVMCloudInitDNSDomain = ""
dvResourceVirtualEnvironmentVMCloudInitDNSServer = "" dvResourceVirtualEnvironmentVMCloudInitDNSServer = ""
dvResourceVirtualEnvironmentVMCloudInitUserAccountPassword = "" dvResourceVirtualEnvironmentVMCloudInitUserAccountPassword = ""
dvResourceVirtualEnvironmentVMCloudInitUserDataFileID = ""
dvResourceVirtualEnvironmentVMCPUCores = 1 dvResourceVirtualEnvironmentVMCPUCores = 1
dvResourceVirtualEnvironmentVMCPUHotplugged = 0 dvResourceVirtualEnvironmentVMCPUHotplugged = 0
dvResourceVirtualEnvironmentVMCPUSockets = 1 dvResourceVirtualEnvironmentVMCPUSockets = 1
@ -72,6 +73,7 @@ const (
mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys = "keys" mkResourceVirtualEnvironmentVMCloudInitUserAccountKeys = "keys"
mkResourceVirtualEnvironmentVMCloudInitUserAccountPassword = "password" mkResourceVirtualEnvironmentVMCloudInitUserAccountPassword = "password"
mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername = "username" mkResourceVirtualEnvironmentVMCloudInitUserAccountUsername = "username"
mkResourceVirtualEnvironmentVMCloudInitUserDataFileID = "user_data_file_id"
mkResourceVirtualEnvironmentVMCPU = "cpu" mkResourceVirtualEnvironmentVMCPU = "cpu"
mkResourceVirtualEnvironmentVMCPUCores = "cores" mkResourceVirtualEnvironmentVMCPUCores = "cores"
mkResourceVirtualEnvironmentVMCPUHotplugged = "hotplugged" mkResourceVirtualEnvironmentVMCPUHotplugged = "hotplugged"
@ -318,6 +320,14 @@ func resourceVirtualEnvironmentVM() *schema.Resource {
MaxItems: 1, MaxItems: 1,
MinItems: 0, 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, MaxItems: 1,
@ -770,6 +780,14 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e
cloudInitConfig.Username = &username cloudInitConfig.Username = &username
} }
cloudInitUserDataFileID := cloudInitBlock[mkResourceVirtualEnvironmentVMCloudInitUserDataFileID].(string)
if cloudInitUserDataFileID != "" {
cloudInitConfig.Files = &proxmox.CustomCloudInitFiles{
UserVolume: &cloudInitUserDataFileID,
}
}
} }
cpu := d.Get(mkResourceVirtualEnvironmentVMCPU).([]interface{}) cpu := d.Get(mkResourceVirtualEnvironmentVMCPU).([]interface{})
@ -1075,7 +1093,9 @@ func resourceVirtualEnvironmentVMCreateImportedDisks(d *schema.ResourceData, m i
speedBlock := speed[0].(map[string]interface{}) speedBlock := speed[0].(map[string]interface{})
speedLimitRead := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedRead].(int) speedLimitRead := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedRead].(int)
speedLimitReadBurstable := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedReadBurstable].(int)
speedLimitWrite := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedWrite].(int) speedLimitWrite := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedWrite].(int)
speedLimitWriteBurstable := speedBlock[mkResourceVirtualEnvironmentVMDiskSpeedWriteBurstable].(int)
diskOptions := "" diskOptions := ""
@ -1083,21 +1103,37 @@ func resourceVirtualEnvironmentVMCreateImportedDisks(d *schema.ResourceData, m i
diskOptions += fmt.Sprintf(",mbps_rd=%d", speedLimitRead) diskOptions += fmt.Sprintf(",mbps_rd=%d", speedLimitRead)
} }
if speedLimitReadBurstable > 0 {
diskOptions += fmt.Sprintf(",mbps_rd_max=%d", speedLimitReadBurstable)
}
if speedLimitWrite > 0 { if speedLimitWrite > 0 {
diskOptions += fmt.Sprintf(",mbps_wr=%d", speedLimitWrite) diskOptions += fmt.Sprintf(",mbps_wr=%d", speedLimitWrite)
} }
if speedLimitWriteBurstable > 0 {
diskOptions += fmt.Sprintf(",mbps_wr_max=%d", speedLimitWriteBurstable)
}
fileIDParts := strings.Split(fileID, ":") 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) filePathTmp := fmt.Sprintf("/tmp/vm-%d-disk-%d.%s", vmID, diskCount+importedDiskCount, fileFormat)
commands = append( commands = append(
commands, commands,
fmt.Sprintf("cp %s %s", filePath, filePathTmp), `set -e`,
fmt.Sprintf("qemu-img resize %s %dG", filePathTmp, size), 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("qm importdisk %d %s %s -format qcow2", vmID, filePathTmp, datastoreID), fmt.Sprintf(`qemu-img resize %s %dG`, filePathTmp, size),
fmt.Sprintf("qm set %d -scsi%d %s:vm-%d-disk-%d%s", vmID, i, datastoreID, vmID, diskCount+importedDiskCount, diskOptions), fmt.Sprintf(`qm importdisk %d %s %s -format qcow2`, vmID, filePathTmp, datastoreID),
fmt.Sprintf("rm -f %s", filePathTmp), 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++ importedDiskCount++

View File

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