From e83e6af1d4deb1010913fbd2392587ab38b1daaf Mon Sep 17 00:00:00 2001 From: Grant Gongaware Date: Fri, 13 Jul 2018 10:25:37 -0700 Subject: [PATCH] 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 }