From 04654a1388fe5d7da395785c7d2addc9a7e9921a Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Tue, 10 Jul 2018 10:25:46 -0700 Subject: [PATCH 1/9] suppress diff on sensitive data when empty --- proxmox/resource_vm_qemu.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index e46d8be..f3b0432 100644 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -2,13 +2,14 @@ package proxmox import ( "fmt" - pxapi "github.com/Telmate/proxmox-api-go/proxmox" - "github.com/hashicorp/terraform/helper/schema" "log" "path" "strconv" "strings" "time" + + pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/terraform/helper/schema" ) func resourceVmQemu() *schema.Resource { @@ -110,6 +111,9 @@ func resourceVmQemu() *schema.Resource { Optional: true, Sensitive: true, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "" { + return true + } return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, From 6cb6133735f57a4ce0b515ffbc0ba52321d88759 Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Tue, 10 Jul 2018 10:37:45 -0700 Subject: [PATCH 2/9] auto-formatted updates --- proxmox/preprovision.go | 4 ++-- proxmox/provider.go | 5 +++-- proxmox/provisioner.go | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/proxmox/preprovision.go b/proxmox/preprovision.go index 3669dde..f83f0e9 100644 --- a/proxmox/preprovision.go +++ b/proxmox/preprovision.go @@ -2,11 +2,11 @@ package proxmox import ( "fmt" + "github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator/remote" "github.com/hashicorp/terraform/helper/schema" - // "github.com/hashicorp/terraform/terraform" - // "github.com/mitchellh/go-linereader" + "io" "log" "strconv" diff --git a/proxmox/provider.go b/proxmox/provider.go index 3933300..cce6b12 100644 --- a/proxmox/provider.go +++ b/proxmox/provider.go @@ -2,11 +2,12 @@ package proxmox import ( "fmt" - pxapi "github.com/Telmate/proxmox-api-go/proxmox" - "github.com/hashicorp/terraform/helper/schema" "regexp" "strconv" "sync" + + pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/terraform/helper/schema" ) type providerConfiguration struct { diff --git a/proxmox/provisioner.go b/proxmox/provisioner.go index 55a6743..b295831 100644 --- a/proxmox/provisioner.go +++ b/proxmox/provisioner.go @@ -3,10 +3,11 @@ package proxmox import ( "context" "fmt" + "time" + pxapi "github.com/Telmate/proxmox-api-go/proxmox" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" - "time" ) func Provisioner() terraform.ResourceProvisioner { From 90d504fa0de28c16f4a240417f84e6b862ba4caf Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Tue, 10 Jul 2018 14:50:35 -0700 Subject: [PATCH 3/9] -support optional insecure TLS proxmox nodes -restore previous ssh key behavior (works in 0.11.x) --- README.md | 1 + proxmox/provider.go | 16 +++++++++++++--- proxmox/provisioner.go | 2 +- proxmox/resource_vm_qemu.go | 20 +++++++++----------- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2774ab0..8a92fb2 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ terraform apply main.tf: ``` provider "proxmox" { + pm_tls_insecure = true } resource "proxmox_vm_qemu" "test" { diff --git a/proxmox/provider.go b/proxmox/provider.go index cce6b12..960777f 100644 --- a/proxmox/provider.go +++ b/proxmox/provider.go @@ -1,6 +1,7 @@ package proxmox import ( + "crypto/tls" "fmt" "regexp" "strconv" @@ -47,6 +48,11 @@ func Provider() *schema.Provider { Optional: true, Default: 4, }, + "pm_tls_insecure": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, }, ResourcesMap: map[string]*schema.Resource{ @@ -61,7 +67,7 @@ func Provider() *schema.Provider { } func providerConfigure(d *schema.ResourceData) (interface{}, error) { - client, err := getClient(d.Get("pm_api_url").(string), d.Get("pm_user").(string), d.Get("pm_password").(string)) + client, err := getClient(d.Get("pm_api_url").(string), d.Get("pm_user").(string), d.Get("pm_password").(string), d.Get("pm_tls_insecure").(bool)) if err != nil { return nil, err } @@ -76,8 +82,12 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { }, nil } -func getClient(pm_api_url string, pm_user string, pm_password string) (*pxapi.Client, error) { - client, _ := pxapi.NewClient(pm_api_url, nil, nil) +func getClient(pm_api_url string, pm_user string, pm_password string, pm_tls_insecure bool) (*pxapi.Client, error) { + tlsconf := &tls.Config{InsecureSkipVerify: true} + if !pm_tls_insecure { + tlsconf = nil + } + client, _ := pxapi.NewClient(pm_api_url, nil, tlsconf) err := client.Login(pm_user, pm_password) if err != nil { return nil, err diff --git a/proxmox/provisioner.go b/proxmox/provisioner.go index b295831..462a7d5 100644 --- a/proxmox/provisioner.go +++ b/proxmox/provisioner.go @@ -44,7 +44,7 @@ func applyFn(ctx context.Context) error { vmr.SetNode(targetNode) client := currentClient if client == nil { - client, err = getClient(connInfo["pm_api_url"], connInfo["pm_user"], connInfo["pm_password"]) + client, err = getClient(connInfo["pm_api_url"], connInfo["pm_user"], connInfo["pm_password"], connInfo["pm_tls_insecure"] == "true") if err != nil { return err } diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index f3b0432..d14d968 100644 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -111,9 +111,6 @@ func resourceVmQemu() *schema.Resource { Optional: true, Sensitive: true, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if old == "" { - return true - } return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, @@ -244,14 +241,15 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { pmParallelEnd(pconf) d.SetConnInfo(map[string]string{ - "type": "ssh", - "host": d.Get("ssh_forward_ip").(string), - "port": sshPort, - "user": d.Get("ssh_user").(string), - "private_key": d.Get("ssh_private_key").(string), - "pm_api_url": client.ApiUrl, - "pm_user": client.Username, - "pm_password": client.Password, + "type": "ssh", + "host": d.Get("ssh_forward_ip").(string), + "port": sshPort, + "user": d.Get("ssh_user").(string), + "private_key": d.Get("ssh_private_key").(string), + "pm_api_url": client.ApiUrl, + "pm_user": client.Username, + "pm_password": client.Password, + "pm_tls_insecure": "true", // TODO - pass pm_tls_insecure state around, but if we made it this far, default insecure }) switch d.Get("os_type").(string) { From 9291c20a84238d479de6ab18ed26dbabba209b15 Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Wed, 11 Jul 2018 15:14:26 -0700 Subject: [PATCH 4/9] switch to /cluster/nextid instead of maxid --- proxmox/provider.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/proxmox/provider.go b/proxmox/provider.go index 960777f..a3d6fa1 100644 --- a/proxmox/provider.go +++ b/proxmox/provider.go @@ -76,7 +76,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { Client: client, MaxParallel: d.Get("pm_parallel").(int), CurrentParallel: 0, - MaxVmId: 0, + MaxVmId: -1, Mutex: &mut, Cond: sync.NewCond(&mut), }, nil @@ -97,13 +97,10 @@ func getClient(pm_api_url string, pm_user string, pm_password string, pm_tls_ins func nextVmId(pconf *providerConfiguration) (nextId int, err error) { pconf.Mutex.Lock() - if pconf.MaxVmId == 0 { - pconf.MaxVmId, err = pxapi.MaxVmId(pconf.Client) - if err != nil { - return 0, err - } + pconf.MaxVmId, err = pconf.Client.GetNextID(pconf.MaxVmId + 1) + if err != nil { + return 0, err } - pconf.MaxVmId++ nextId = pconf.MaxVmId pconf.Mutex.Unlock() return nextId, nil From e83e6af1d4deb1010913fbd2392587ab38b1daaf Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Fri, 13 Jul 2018 10:25:37 -0700 Subject: [PATCH 5/9] untested cloud-init support --- README.md | 61 +++++++++++++++-- proxmox/provider.go | 9 +-- proxmox/provisioner.go | 8 +-- proxmox/resource_vm_qemu.go | 133 +++++++++++++++++++++++++++++++----- 4 files changed, 182 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8a92fb2..335a9bd 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,45 @@ provider "proxmox" { pm_tls_insecure = true } -resource "proxmox_vm_qemu" "test" { +/* Uses cloud-init options from Proxmox 5.2 */ +resource "proxmox_vm_qemu" "cloudinit-test" { + name = "tftest1.xyz.com" + desc = "tf description" + target_node = "proxmox1-xx" + + clone = "ci-ubuntu-template" + storage = "local" + cores = 3 + sockets = 1 + memory = 2560 + disk_gb = 4 + nic = "virtio" + bridge = "vmbr0" + + ssh_user = "root" + ssh_private_key = <] [,gw6=] [,ip=] [,ip6=] +* ipconfig1 - optional, same as ipconfig0 format + +### Preprovision (internal alternative to Cloud-Init) There is a pre-provision phase which is used to set a hostname, intialize eth0, and resize the VM disk to available space. This is done over SSH with the ssh_forward_ip, ssh_user and ssh_private_key. diff --git a/proxmox/provider.go b/proxmox/provider.go index a3d6fa1..ac372ca 100644 --- a/proxmox/provider.go +++ b/proxmox/provider.go @@ -15,11 +15,12 @@ type providerConfiguration struct { Client *pxapi.Client MaxParallel int CurrentParallel int - MaxVmId int + MaxVMID int Mutex *sync.Mutex Cond *sync.Cond } +// Provider - Terrafrom properties for proxmox func Provider() *schema.Provider { return &schema.Provider{ @@ -76,7 +77,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { Client: client, MaxParallel: d.Get("pm_parallel").(int), CurrentParallel: 0, - MaxVmId: -1, + MaxVMID: -1, Mutex: &mut, Cond: sync.NewCond(&mut), }, nil @@ -97,11 +98,11 @@ func getClient(pm_api_url string, pm_user string, pm_password string, pm_tls_ins func nextVmId(pconf *providerConfiguration) (nextId int, err error) { pconf.Mutex.Lock() - pconf.MaxVmId, err = pconf.Client.GetNextID(pconf.MaxVmId + 1) + pconf.MaxVMID, err = pconf.Client.GetNextID(pconf.MaxVMID + 1) if err != nil { return 0, err } - nextId = pconf.MaxVmId + nextId = pconf.MaxVMID pconf.Mutex.Unlock() return nextId, nil } diff --git a/proxmox/provisioner.go b/proxmox/provisioner.go index 462a7d5..0c7a7db 100644 --- a/proxmox/provisioner.go +++ b/proxmox/provisioner.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform/terraform" ) +// Provisioner - Terrafrom properties for proxmox-provisioner func Provisioner() terraform.ResourceProvisioner { return &schema.Provisioner{ Schema: map[string]*schema.Schema{ @@ -27,7 +28,7 @@ func Provisioner() terraform.ResourceProvisioner { } } -var currentClient *pxapi.Client = nil +var currentClient *pxapi.Client func applyFn(ctx context.Context) error { data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) @@ -36,11 +37,11 @@ func applyFn(ctx context.Context) error { connInfo := state.Ephemeral.ConnInfo act := data.Get("action").(string) - targetNode, _, vmId, err := parseResourceId(state.ID) + targetNode, _, vmID, err := parseResourceId(state.ID) if err != nil { return err } - vmr := pxapi.NewVmRef(vmId) + vmr := pxapi.NewVmRef(vmID) vmr.SetNode(targetNode) client := currentClient if client == nil { @@ -69,5 +70,4 @@ func applyFn(ctx context.Context) error { default: return fmt.Errorf("Unkown action: %s", act) } - return nil } diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index d14d968..bee0d97 100644 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "path" + "regexp" "strconv" "strings" "time" @@ -38,6 +39,7 @@ func resourceVmQemu() *schema.Resource { "target_node": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "ssh_forward_ip": { Type: schema.TypeString, @@ -46,15 +48,22 @@ func resourceVmQemu() *schema.Resource { "iso": { Type: schema.TypeString, Optional: true, + ForceNew: true, }, "clone": { Type: schema.TypeString, Optional: true, + ForceNew: true, }, "storage": { Type: schema.TypeString, Required: true, }, + "storage_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, "qemu_os": { Type: schema.TypeString, Optional: true, @@ -119,16 +128,61 @@ func resourceVmQemu() *schema.Resource { Optional: true, Default: false, }, + "mac": { + Type: schema.TypeString, + Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "" { + return false // macaddr auto-generates and its ok + } + return strings.TrimSpace(old) == strings.TrimSpace(new) + }, + }, + "ci_wait": { // how long to wait before provision + Type: schema.TypeInt, + Optional: true, + Default: 30, + }, + "ciuser": { + Type: schema.TypeString, + Optional: true, + }, + "cipassword": { + Type: schema.TypeString, + Optional: true, + }, + "searchdomain": { + Type: schema.TypeString, + Optional: true, + }, + "nameserver": { + Type: schema.TypeString, + Optional: true, + }, + "sshkeys": { + Type: schema.TypeString, + Optional: true, + }, + "ipconfig0": { + Type: schema.TypeString, + Optional: true, + }, + "ipconfig1": { + Type: schema.TypeString, + Optional: true, + }, }, } } +var rxIPconfig = regexp.MustCompile("ip6?=([0-9a-fA-F:\\.]+)") + func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { pconf := meta.(*providerConfiguration) pmParallelBegin(pconf) client := pconf.Client vmName := d.Get("name").(string) - disk_gb := d.Get("disk_gb").(float64) + diskGB := d.Get("disk_gb").(float64) config := pxapi.ConfigQemu{ Name: vmName, Description: d.Get("desc").(string), @@ -136,11 +190,19 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { Memory: d.Get("memory").(int), QemuCores: d.Get("cores").(int), QemuSockets: d.Get("sockets").(int), - DiskSize: disk_gb, + DiskSize: diskGB, QemuOs: d.Get("qemu_os").(string), QemuNicModel: d.Get("nic").(string), QemuBrige: d.Get("bridge").(string), QemuVlanTag: d.Get("vlan").(int), + QemuMacAddr: d.Get("mac").(string), + CIuser: d.Get("ciuser").(string), + CIpassword: d.Get("cipassword").(string), + Searchdomain: d.Get("searchdomain").(string), + Nameserver: d.Get("nameserver").(string), + Sshkeys: d.Get("sshkeys").(string), + Ipconfig0: d.Get("ipconfig0").(string), + Ipconfig1: d.Get("ipconfig1").(string), } log.Print("[DEBUG] checking for duplicate name") dupVmr, _ := client.GetVmRefByName(vmName) @@ -185,7 +247,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { // give sometime to proxmox to catchup time.Sleep(5 * time.Second) - err = prepareDiskSize(client, vmr, disk_gb) + err = prepareDiskSize(client, vmr, diskGB) if err != nil { pmParallelEnd(pconf) return err @@ -213,7 +275,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { // give sometime to proxmox to catchup time.Sleep(5 * time.Second) - err = prepareDiskSize(client, vmr, disk_gb) + err = prepareDiskSize(client, vmr, diskGB) if err != nil { pmParallelEnd(pconf) return err @@ -230,11 +292,25 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { pmParallelEnd(pconf) return err } - log.Print("[DEBUG] setting up SSH forward") - sshPort, err := pxapi.SshForwardUsernet(vmr, client) - if err != nil { - pmParallelEnd(pconf) - return err + + sshPort := "22" + sshHost := "" + if config.HasCloudInit() { + if d.Get("ssh_forward_ip") != nil { + sshHost = d.Get("ssh_forward_ip").(string) + } else { + // parse IP address out of ipconfig0 + ipMatch := rxIPconfig.FindStringSubmatch(d.Get("ipconfig0").(string)) + sshHost = ipMatch[1] + } + } else { + log.Print("[DEBUG] setting up SSH forward") + sshPort, err = pxapi.SshForwardUsernet(vmr, client) + if err != nil { + pmParallelEnd(pconf) + return err + } + sshHost = d.Get("ssh_forward_ip").(string) } // Done with proxmox API, end parallel and do the SSH things @@ -242,7 +318,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { d.SetConnInfo(map[string]string{ "type": "ssh", - "host": d.Get("ssh_forward_ip").(string), + "host": sshHost, "port": sshPort, "user": d.Get("ssh_user").(string), "private_key": d.Get("ssh_private_key").(string), @@ -270,6 +346,10 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { return err } + case "cloud-init": + // wait for OS too boot awhile... + time.Sleep(time.Duration(d.Get("ci_wait").(int)) * time.Second) + default: return fmt.Errorf("Unknown os_type: %s", d.Get("os_type").(string)) } @@ -287,7 +367,7 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { return err } vmName := d.Get("name").(string) - disk_gb := d.Get("disk_gb").(float64) + diskGB := d.Get("disk_gb").(float64) config := pxapi.ConfigQemu{ Name: vmName, Description: d.Get("desc").(string), @@ -295,11 +375,19 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { Memory: d.Get("memory").(int), QemuCores: d.Get("cores").(int), QemuSockets: d.Get("sockets").(int), - DiskSize: disk_gb, + DiskSize: diskGB, QemuOs: d.Get("qemu_os").(string), QemuNicModel: d.Get("nic").(string), QemuBrige: d.Get("bridge").(string), QemuVlanTag: d.Get("vlan").(int), + QemuMacAddr: d.Get("mac").(string), + CIuser: d.Get("ciuser").(string), + CIpassword: d.Get("cipassword").(string), + Searchdomain: d.Get("searchdomain").(string), + Nameserver: d.Get("nameserver").(string), + Sshkeys: d.Get("sshkeys").(string), + Ipconfig0: d.Get("ipconfig0").(string), + Ipconfig1: d.Get("ipconfig1").(string), } err = config.UpdateConfig(vmr, client) @@ -311,7 +399,7 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { // give sometime to proxmox to catchup time.Sleep(5 * time.Second) - prepareDiskSize(client, vmr, disk_gb) + prepareDiskSize(client, vmr, diskGB) // give sometime to proxmox to catchup time.Sleep(5 * time.Second) @@ -361,6 +449,7 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error { d.Set("name", config.Name) d.Set("desc", config.Description) d.Set("storage", config.Storage) + d.Set("storage_type", config.StorageType) d.Set("memory", config.Memory) d.Set("cores", config.QemuCores) d.Set("sockets", config.QemuSockets) @@ -369,6 +458,16 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error { d.Set("nic", config.QemuNicModel) d.Set("bridge", config.QemuBrige) d.Set("vlan", config.QemuVlanTag) + d.Set("mac", config.QemuMacAddr) + + d.Set("ciuser", config.CIuser) + d.Set("cipassword", config.CIpassword) + d.Set("searchdomain", config.Searchdomain) + d.Set("nameserver", config.Nameserver) + d.Set("sshkeys", config.Sshkeys) + d.Set("ipconfig0", config.Ipconfig0) + d.Set("ipconfig1", config.Ipconfig1) + pmParallelEnd(pconf) return nil } @@ -397,14 +496,14 @@ func resourceVmQemuDelete(d *schema.ResourceData, meta interface{}) error { return err } -func prepareDiskSize(client *pxapi.Client, vmr *pxapi.VmRef, disk_gb float64) error { +func prepareDiskSize(client *pxapi.Client, vmr *pxapi.VmRef, diskGB float64) error { clonedConfig, err := pxapi.NewConfigQemuFromApi(vmr, client) if err != nil { return err } - if disk_gb > clonedConfig.DiskSize { - log.Print("[DEBUG] resizing disk") - _, err = client.ResizeQemuDisk(vmr, "virtio0", int(disk_gb-clonedConfig.DiskSize)) + if diskGB > clonedConfig.DiskSize { + log.Print("[DEBUG] resizing disk " + clonedConfig.StorageType) + _, err = client.ResizeQemuDisk(vmr, clonedConfig.StorageType+"0", int(diskGB-clonedConfig.DiskSize)) if err != nil { return err } From a61e8d5a39526836a4458fc3d038da1656994ee6 Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Mon, 16 Jul 2018 09:10:23 -0700 Subject: [PATCH 6/9] ssh_forward_ip optional with cloud-init --- proxmox/resource_vm_qemu.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index bee0d97..bb2fcfb 100644 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -43,7 +43,7 @@ func resourceVmQemu() *schema.Resource { }, "ssh_forward_ip": { Type: schema.TypeString, - Required: true, + Optional: true, }, "iso": { Type: schema.TypeString, @@ -348,6 +348,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { case "cloud-init": // wait for OS too boot awhile... + log.Print("[DEBUG] sleeping for OS bootup...") time.Sleep(time.Duration(d.Get("ci_wait").(int)) * time.Second) default: From fdd21abd51e5d6b412d2a4d1e8d31c4b49982dd6 Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Mon, 16 Jul 2018 11:57:46 -0700 Subject: [PATCH 7/9] fix sshHost, disk resize, and allow sparse mac, storage_type & qemu_os-type parameters --- proxmox/resource_vm_qemu.go | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index bb2fcfb..864c241 100644 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -3,6 +3,7 @@ package proxmox import ( "fmt" "log" + "math" "path" "regexp" "strconv" @@ -62,12 +63,24 @@ func resourceVmQemu() *schema.Resource { "storage_type": { Type: schema.TypeString, Optional: true, - ForceNew: true, + ForceNew: false, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if new == "" { + return true // empty template ok + } + return strings.TrimSpace(old) == strings.TrimSpace(new) + }, }, "qemu_os": { Type: schema.TypeString, Optional: true, Default: "l26", + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "" { + return true // reading empty ok + } + return strings.TrimSpace(old) == strings.TrimSpace(new) + }, }, "memory": { Type: schema.TypeInt, @@ -84,6 +97,12 @@ func resourceVmQemu() *schema.Resource { "disk_gb": { Type: schema.TypeFloat, Required: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // bigger ok + oldf, _ := strconv.ParseFloat(old, 64) + newf, _ := strconv.ParseFloat(new, 64) + return oldf >= newf + }, }, "nic": { Type: schema.TypeString, @@ -132,8 +151,8 @@ func resourceVmQemu() *schema.Resource { Type: schema.TypeString, Optional: true, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if old == "" { - return false // macaddr auto-generates and its ok + if new == "" { + return true // macaddr auto-generates and its ok } return strings.TrimSpace(old) == strings.TrimSpace(new) }, @@ -162,6 +181,9 @@ func resourceVmQemu() *schema.Resource { "sshkeys": { Type: schema.TypeString, Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return strings.TrimSpace(old) == strings.TrimSpace(new) + }, }, "ipconfig0": { Type: schema.TypeString, @@ -298,7 +320,8 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { if config.HasCloudInit() { if d.Get("ssh_forward_ip") != nil { sshHost = d.Get("ssh_forward_ip").(string) - } else { + } + if sshHost == "" { // parse IP address out of ipconfig0 ipMatch := rxIPconfig.FindStringSubmatch(d.Get("ipconfig0").(string)) sshHost = ipMatch[1] @@ -504,7 +527,8 @@ func prepareDiskSize(client *pxapi.Client, vmr *pxapi.VmRef, diskGB float64) err } if diskGB > clonedConfig.DiskSize { log.Print("[DEBUG] resizing disk " + clonedConfig.StorageType) - _, err = client.ResizeQemuDisk(vmr, clonedConfig.StorageType+"0", int(diskGB-clonedConfig.DiskSize)) + diffSize := int(math.Ceil(diskGB - clonedConfig.DiskSize)) + _, err = client.ResizeQemuDisk(vmr, clonedConfig.StorageType+"0", diffSize) if err != nil { return err } From 6d51c5bf558ff5a560a4da203db3dce25aee59d8 Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Thu, 19 Jul 2018 09:53:00 -0700 Subject: [PATCH 8/9] suppress ci_wait change to default for backward compatibility --- proxmox/resource_vm_qemu.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index 864c241..dfc7acd 100644 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -161,6 +161,12 @@ func resourceVmQemu() *schema.Resource { Type: schema.TypeInt, Optional: true, Default: 30, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "" { + return true // old empty ok + } + return strings.TrimSpace(old) == strings.TrimSpace(new) + }, }, "ciuser": { Type: schema.TypeString, From 217a5cf262f7f5914541acf4a7e4db33b3908764 Mon Sep 17 00:00:00 2001 From: AAbouZaid Date: Sun, 29 Jul 2018 03:39:11 +0200 Subject: [PATCH 9/9] add onboot, multi disks, and multi nic --- README.md | 26 +- proxmox/provisioner.go | 2 +- proxmox/resource_vm_qemu.go | 555 ++++++++++++++++++++++++++---------- 3 files changed, 425 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 335a9bd..a855cd4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Proxmox 4 Terraform +# Proxmox 4 Terraform Terraform provider plugin for proxmox @@ -78,13 +78,27 @@ resource "proxmox_vm_qemu" "prepprovision-test" { target_node = "proxmox1-xx" clone = "terraform-ubuntu1404-template" - storage = "local" cores = 3 sockets = 1 memory = 2560 - disk_gb = 4 - nic = "virtio" - bridge = "vmbr1" + network { + id = 0 + model = "virtio" + } + network { + id = 1 + model = "virtio" + bridge = "vmbr1" + } + disk { + id = 0 + type = virtio + storage = local-lvm + storage_type = lvm + size = 4G + backup = true + } + preprovision = true ssh_forward_ip = "10.0.0.1" ssh_user = "terraform" ssh_private_key = <= newf }, }, + "storage": { + Type: schema.TypeString, + Deprecated: "Use `disk.storage` instead", + Optional: true, + }, + "storage_type": { + Type: schema.TypeString, + Deprecated: "Use `disk.type` instead", + Optional: true, + ForceNew: false, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if new == "" { + return true // empty template ok + } + return strings.TrimSpace(old) == strings.TrimSpace(new) + }, + }, + // Deprecated single nic config. "nic": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Deprecated: "Use `network` instead", + Optional: true, }, "bridge": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Deprecated: "Use `network.bridge` instead", + Optional: true, }, "vlan": { - Type: schema.TypeInt, - Optional: true, - Default: -1, + Type: schema.TypeInt, + Deprecated: "Use `network.tag` instead", + Optional: true, + Default: -1, + }, + "mac": { + Type: schema.TypeString, + Deprecated: "Use `network.macaddr` to access the auto generated MAC address", + Optional: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if new == "" { + return true // macaddr auto-generates and its ok + } + return strings.TrimSpace(old) == strings.TrimSpace(new) + }, }, "os_type": { Type: schema.TypeString, @@ -129,6 +252,10 @@ func resourceVmQemu() *schema.Resource { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, + "ssh_forward_ip": { + Type: schema.TypeString, + Optional: true, + }, "ssh_user": { Type: schema.TypeString, Optional: true, @@ -147,16 +274,6 @@ func resourceVmQemu() *schema.Resource { Optional: true, Default: false, }, - "mac": { - Type: schema.TypeString, - Optional: true, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if new == "" { - return true // macaddr auto-generates and its ok - } - return strings.TrimSpace(old) == strings.TrimSpace(new) - }, - }, "ci_wait": { // how long to wait before provision Type: schema.TypeInt, Optional: true, @@ -199,6 +316,12 @@ func resourceVmQemu() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "preprovision": { + Type: schema.TypeBool, + Optional: true, + Default: true, + ConflictsWith: []string{"ssh_forward_ip", "ssh_user", "ssh_private_key", "os_type", "os_network_config"}, + }, }, } } @@ -210,20 +333,23 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { pmParallelBegin(pconf) client := pconf.Client vmName := d.Get("name").(string) + networks := d.Get("network").(*schema.Set) + qemuNetworks := devicesSetToMap(networks) + disks := d.Get("disk").(*schema.Set) + qemuDisks := devicesSetToMap(disks) diskGB := d.Get("disk_gb").(float64) + config := pxapi.ConfigQemu{ Name: vmName, Description: d.Get("desc").(string), - Storage: d.Get("storage").(string), + Onboot: d.Get("onboot").(bool), Memory: d.Get("memory").(int), QemuCores: d.Get("cores").(int), QemuSockets: d.Get("sockets").(int), - DiskSize: diskGB, QemuOs: d.Get("qemu_os").(string), - QemuNicModel: d.Get("nic").(string), - QemuBrige: d.Get("bridge").(string), - QemuVlanTag: d.Get("vlan").(int), - QemuMacAddr: d.Get("mac").(string), + QemuNetworks: qemuNetworks, + QemuDisks: qemuDisks, + // Cloud-init. CIuser: d.Get("ciuser").(string), CIpassword: d.Get("cipassword").(string), Searchdomain: d.Get("searchdomain").(string), @@ -231,6 +357,14 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { Sshkeys: d.Get("sshkeys").(string), Ipconfig0: d.Get("ipconfig0").(string), Ipconfig1: d.Get("ipconfig1").(string), + // Deprecated single disk config. + Storage: d.Get("storage").(string), + DiskSize: diskGB, + // Deprecated single nic config. + QemuNicModel: d.Get("nic").(string), + QemuBrige: d.Get("bridge").(string), + QemuVlanTag: d.Get("vlan").(int), + QemuMacAddr: d.Get("mac").(string), } log.Print("[DEBUG] checking for duplicate name") dupVmr, _ := client.GetVmRefByName(vmName) @@ -275,7 +409,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { // give sometime to proxmox to catchup time.Sleep(5 * time.Second) - err = prepareDiskSize(client, vmr, diskGB) + err = prepareDiskSize(client, vmr, qemuDisks) if err != nil { pmParallelEnd(pconf) return err @@ -303,7 +437,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { // give sometime to proxmox to catchup time.Sleep(5 * time.Second) - err = prepareDiskSize(client, vmr, diskGB) + err = prepareDiskSize(client, vmr, qemuDisks) if err != nil { pmParallelEnd(pconf) return err @@ -321,68 +455,8 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error { return err } - sshPort := "22" - sshHost := "" - if config.HasCloudInit() { - if d.Get("ssh_forward_ip") != nil { - sshHost = d.Get("ssh_forward_ip").(string) - } - if sshHost == "" { - // parse IP address out of ipconfig0 - ipMatch := rxIPconfig.FindStringSubmatch(d.Get("ipconfig0").(string)) - sshHost = ipMatch[1] - } - } else { - log.Print("[DEBUG] setting up SSH forward") - sshPort, err = pxapi.SshForwardUsernet(vmr, client) - if err != nil { - pmParallelEnd(pconf) - return err - } - sshHost = d.Get("ssh_forward_ip").(string) - } - - // Done with proxmox API, end parallel and do the SSH things - pmParallelEnd(pconf) - - d.SetConnInfo(map[string]string{ - "type": "ssh", - "host": sshHost, - "port": sshPort, - "user": d.Get("ssh_user").(string), - "private_key": d.Get("ssh_private_key").(string), - "pm_api_url": client.ApiUrl, - "pm_user": client.Username, - "pm_password": client.Password, - "pm_tls_insecure": "true", // TODO - pass pm_tls_insecure state around, but if we made it this far, default insecure - }) - - switch d.Get("os_type").(string) { - - case "ubuntu": - // give sometime to bootup - time.Sleep(9 * time.Second) - err = preProvisionUbuntu(d) - if err != nil { - return err - } - - case "centos": - // give sometime to bootup - time.Sleep(9 * time.Second) - err = preProvisionCentos(d) - if err != nil { - return err - } - - case "cloud-init": - // wait for OS too boot awhile... - log.Print("[DEBUG] sleeping for OS bootup...") - time.Sleep(time.Duration(d.Get("ci_wait").(int)) * time.Second) - - default: - return fmt.Errorf("Unknown os_type: %s", d.Get("os_type").(string)) - } + // Apply pre-provision if enabled. + preprovision(d, pconf, client, vmr, true) return nil } @@ -397,20 +471,23 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { return err } vmName := d.Get("name").(string) + configDisksSet := d.Get("disk").(*schema.Set) + qemuDisks := devicesSetToMap(configDisksSet) + configNetworksSet := d.Get("network").(*schema.Set) + qemuNetworks := devicesSetToMap(configNetworksSet) diskGB := d.Get("disk_gb").(float64) + config := pxapi.ConfigQemu{ Name: vmName, Description: d.Get("desc").(string), - Storage: d.Get("storage").(string), + Onboot: d.Get("onboot").(bool), Memory: d.Get("memory").(int), QemuCores: d.Get("cores").(int), QemuSockets: d.Get("sockets").(int), - DiskSize: diskGB, QemuOs: d.Get("qemu_os").(string), - QemuNicModel: d.Get("nic").(string), - QemuBrige: d.Get("bridge").(string), - QemuVlanTag: d.Get("vlan").(int), - QemuMacAddr: d.Get("mac").(string), + QemuNetworks: qemuNetworks, + QemuDisks: qemuDisks, + // Cloud-init. CIuser: d.Get("ciuser").(string), CIpassword: d.Get("cipassword").(string), Searchdomain: d.Get("searchdomain").(string), @@ -418,6 +495,14 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { Sshkeys: d.Get("sshkeys").(string), Ipconfig0: d.Get("ipconfig0").(string), Ipconfig1: d.Get("ipconfig1").(string), + // Deprecated single disk config. + Storage: d.Get("storage").(string), + DiskSize: diskGB, + // Deprecated single nic config. + QemuNicModel: d.Get("nic").(string), + QemuBrige: d.Get("bridge").(string), + QemuVlanTag: d.Get("vlan").(int), + QemuMacAddr: d.Get("mac").(string), } err = config.UpdateConfig(vmr, client) @@ -429,31 +514,24 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { // give sometime to proxmox to catchup time.Sleep(5 * time.Second) - prepareDiskSize(client, vmr, diskGB) + prepareDiskSize(client, vmr, qemuDisks) // give sometime to proxmox to catchup time.Sleep(5 * time.Second) - log.Print("[DEBUG] starting VM") - _, err = client.StartVm(vmr) - - if err != nil { + // Start VM only if it wasn't running. + vmState, err := client.GetVmState(vmr) + if err == nil && vmState["status"] == "stopped" { + log.Print("[DEBUG] starting VM") + _, err = client.StartVm(vmr) + } else if err != nil { pmParallelEnd(pconf) return err } - sshPort, err := pxapi.SshForwardUsernet(vmr, client) - if err != nil { - pmParallelEnd(pconf) - return err - } - d.SetConnInfo(map[string]string{ - "type": "ssh", - "host": d.Get("ssh_forward_ip").(string), - "port": sshPort, - "user": d.Get("ssh_user").(string), - "private_key": d.Get("ssh_private_key").(string), - }) + // Apply pre-provision if enabled. + preprovision(d, pconf, client, vmr, false) + pmParallelEnd(pconf) // give sometime to bootup @@ -478,18 +556,12 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error { d.Set("target_node", vmr.Node()) d.Set("name", config.Name) d.Set("desc", config.Description) - d.Set("storage", config.Storage) - d.Set("storage_type", config.StorageType) + d.Set("onboot", config.Onboot) d.Set("memory", config.Memory) d.Set("cores", config.QemuCores) d.Set("sockets", config.QemuSockets) - d.Set("disk_gb", config.DiskSize) d.Set("qemu_os", config.QemuOs) - d.Set("nic", config.QemuNicModel) - d.Set("bridge", config.QemuBrige) - d.Set("vlan", config.QemuVlanTag) - d.Set("mac", config.QemuMacAddr) - + // Cloud-init. d.Set("ciuser", config.CIuser) d.Set("cipassword", config.CIpassword) d.Set("searchdomain", config.Searchdomain) @@ -497,6 +569,23 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error { d.Set("sshkeys", config.Sshkeys) d.Set("ipconfig0", config.Ipconfig0) d.Set("ipconfig1", config.Ipconfig1) + // Disks. + configDisksSet := d.Get("disk").(*schema.Set) + activeDisksSet := updateDevicesSet(configDisksSet, config.QemuDisks) + d.Set("disk", activeDisksSet) + // Networks. + configNetworksSet := d.Get("network").(*schema.Set) + activeNetworksSet := updateDevicesSet(configNetworksSet, config.QemuNetworks) + d.Set("network", activeNetworksSet) + // Deprecated single disk config. + d.Set("storage", config.Storage) + d.Set("disk_gb", config.DiskSize) + d.Set("storage_type", config.StorageType) + // Deprecated single nic config. + d.Set("nic", config.QemuNicModel) + d.Set("bridge", config.QemuBrige) + d.Set("vlan", config.QemuVlanTag) + d.Set("mac", config.QemuMacAddr) pmParallelEnd(pconf) return nil @@ -526,18 +615,182 @@ func resourceVmQemuDelete(d *schema.ResourceData, meta interface{}) error { return err } -func prepareDiskSize(client *pxapi.Client, vmr *pxapi.VmRef, diskGB float64) error { +// Increase disk size if original disk was smaller than new disk. +func prepareDiskSize( + client *pxapi.Client, + vmr *pxapi.VmRef, + diskConfMap pxapi.QemuDevices, +) error { clonedConfig, err := pxapi.NewConfigQemuFromApi(vmr, client) - if err != nil { - return err - } - if diskGB > clonedConfig.DiskSize { - log.Print("[DEBUG] resizing disk " + clonedConfig.StorageType) - diffSize := int(math.Ceil(diskGB - clonedConfig.DiskSize)) - _, err = client.ResizeQemuDisk(vmr, clonedConfig.StorageType+"0", diffSize) + for _, diskConf := range diskConfMap { + diskID := diskConf["id"].(int) + diskName := fmt.Sprintf("%v%v", diskConf["type"], diskID) + + diskSizeGB := diskConf["size"].(string) + diskSize, _ := strconv.ParseFloat(strings.Trim(diskSizeGB, "G"), 64) if err != nil { return err } + + if _, diskExists := clonedConfig.QemuDisks[diskID]; !diskExists { + return err + } + clonedDiskSizeGB := clonedConfig.QemuDisks[diskID]["size"].(string) + clonedDiskSize, _ := strconv.ParseFloat(strings.Trim(clonedDiskSizeGB, "G"), 64) + + if err != nil { + return err + } + + diffSize := int(math.Ceil(diskSize - clonedDiskSize)) + if diskSize > clonedDiskSize { + log.Print("[DEBUG] resizing disk " + diskName) + _, err = client.ResizeQemuDisk(vmr, diskName, diffSize) + if err != nil { + return err + } + } } return nil } + +// Converting from schema.TypeSet to map of id and conf for each device, +// which will be sent to Proxmox API. +func devicesSetToMap(devicesSet *schema.Set) pxapi.QemuDevices { + + devicesMap := pxapi.QemuDevices{} + + for _, set := range devicesSet.List() { + setMap, isMap := set.(map[string]interface{}) + if isMap { + setID := setMap["id"].(int) + devicesMap[setID] = setMap + } + } + return devicesMap +} + +// Update schema.TypeSet with new values comes from Proxmox API. +// TODO: Maybe it's better to create a new Set instead add to current one. +func updateDevicesSet( + devicesSet *schema.Set, + devicesMap pxapi.QemuDevices, +) *schema.Set { + + configDevicesMap := devicesSetToMap(devicesSet) + activeDevicesMap := updateDevicesDefaults(devicesMap, configDevicesMap) + + for _, setConf := range devicesSet.List() { + devicesSet.Remove(setConf) + setConfMap := setConf.(map[string]interface{}) + deviceID := setConfMap["id"].(int) + // Value type should be one of types allowed by Terraform schema types. + for key, value := range activeDevicesMap[deviceID] { + // This nested switch is used for nested config like in `net[n]`, + // where Proxmox uses `key=<0|1>` in string" at the same time + // a boolean could be used in ".tf" files. + switch setConfMap[key].(type) { + case bool: + switch value.(type) { + // If the key is bool and value is int (which comes from Proxmox API), + // should be converted to bool (as in ".tf" conf). + case int: + sValue := strconv.Itoa(value.(int)) + bValue, err := strconv.ParseBool(sValue) + if err == nil { + setConfMap[key] = bValue + } + // If value is bool, which comes from Terraform conf, add it directly. + case bool: + setConfMap[key] = value + } + // Anything else will be added as it is. + default: + setConfMap[key] = value + } + devicesSet.Add(setConfMap) + } + } + + return devicesSet +} + +// Because default values are not stored in Proxmox, so the API returns only active values. +// So to prevent Terraform doing unnecessary diffs, this function reads default values +// from Terraform itself, and fill empty fields. +func updateDevicesDefaults( + activeDevicesMap pxapi.QemuDevices, + configDevicesMap pxapi.QemuDevices, +) pxapi.QemuDevices { + + for deviceID, deviceConf := range configDevicesMap { + if _, ok := activeDevicesMap[deviceID]; !ok { + activeDevicesMap[deviceID] = configDevicesMap[deviceID] + } + for key, value := range deviceConf { + if _, ok := activeDevicesMap[deviceID][key]; !ok { + activeDevicesMap[deviceID][key] = value + } + } + } + return activeDevicesMap +} + +// Internal pre-provision. +func preprovision( + d *schema.ResourceData, + pconf *providerConfiguration, + client *pxapi.Client, + vmr *pxapi.VmRef, + systemPreProvision bool, +) error { + + if d.Get("preprovision").(bool) { + log.Print("[DEBUG] setting up SSH forward") + sshPort, err := pxapi.SshForwardUsernet(vmr, client) + if err != nil { + pmParallelEnd(pconf) + return err + } + + // Done with proxmox API, end parallel and do the SSH things + pmParallelEnd(pconf) + + d.SetConnInfo(map[string]string{ + "type": "ssh", + "host": d.Get("ssh_forward_ip").(string), + "port": sshPort, + "user": d.Get("ssh_user").(string), + "private_key": d.Get("ssh_private_key").(string), + "pm_api_url": client.ApiUrl, + "pm_user": client.Username, + "pm_password": client.Password, + }) + + if systemPreProvision { + switch d.Get("os_type").(string) { + + case "ubuntu": + // give sometime to bootup + time.Sleep(9 * time.Second) + err = preProvisionUbuntu(d) + if err != nil { + return err + } + + case "centos": + // give sometime to bootup + time.Sleep(9 * time.Second) + err = preProvisionCentos(d) + if err != nil { + return err + } + + default: + return fmt.Errorf("Unknown os_type: %s", d.Get("os_type").(string)) + } + } + } + + return nil +}