untested cloud-init support

This commit is contained in:
Grant Gongaware 2018-07-13 10:25:37 -07:00
parent 9291c20a84
commit e83e6af1d4
4 changed files with 182 additions and 29 deletions

View File

@ -34,7 +34,45 @@ provider "proxmox" {
pm_tls_insecure = true 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 = <<EOF
-----BEGIN RSA PRIVATE KEY-----
private ssh key root
-----END RSA PRIVATE KEY-----
EOF
os_type = "cloud-init"
ipconfig0 = "ip=10.0.2.99, gw=10.0.2.2"
sshkeys = <<EOF
ssh-rsa AAAAB3NzaC1kj...key1
ssh-rsa AAAAB3NzaC1kj...key2
EOF
provisioner "remote-exec" {
inline = [
"ip a"
]
}
}
/* Uses custom eth1 user-net SSH portforward */
resource "proxmox_vm_qemu" "prepprovision-test" {
name = "tftest1.xyz.com" name = "tftest1.xyz.com"
desc = "tf description" desc = "tf description"
target_node = "proxmox1-xx" target_node = "proxmox1-xx"
@ -80,12 +118,27 @@ Optimally, you could create a VM resource you will use a clone base with an ISO,
Interesting parameters: Interesting parameters:
**ssh_forward_ip** - should be the IP or hostname of the target node or bridge IP. This is where proxmox will create a port forward to your VM with via a user_net. **os_type** -
* cloud-init - from Proxmox 5.2
* ubuntu -(https://github.com/Telmate/terraform-ubuntu-proxmox-iso)
* centos - (TODO: centos iso template)
**os_type** - ubuntu (https://github.com/Telmate/terraform-ubuntu-proxmox-iso) or centos (TODO: centos iso template) **ssh_forward_ip** - should be the IP or hostname of the target node or bridge IP. This is where proxmox will create a port forward to your VM with via a user_net. (for pre-cloud-init provisioning)
### Cloud-Init
### Preprovision (internal) Cloud-init VMs must be cloned from a cloud-init ready template.
See: https://pve.proxmox.com/wiki/Cloud-Init_Support
* ciuser - User name to change ssh keys and password for instead of the images configured default user.
* cipassword - Password to assign the user.
* searchdomain - Sets DNS search domains for a container.
* nameserver - Sets DNS server IP address for a container.
* sshkeys - public ssh keys, one per line
* ipconfig0 - [gw=<GatewayIPv4>] [,gw6=<GatewayIPv6>] [,ip=<IPv4Format/CIDR>] [,ip6=<IPv6Format/CIDR>]
* 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. 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.

View File

@ -15,11 +15,12 @@ type providerConfiguration struct {
Client *pxapi.Client Client *pxapi.Client
MaxParallel int MaxParallel int
CurrentParallel int CurrentParallel int
MaxVmId int MaxVMID int
Mutex *sync.Mutex Mutex *sync.Mutex
Cond *sync.Cond Cond *sync.Cond
} }
// Provider - Terrafrom properties for proxmox
func Provider() *schema.Provider { func Provider() *schema.Provider {
return &schema.Provider{ return &schema.Provider{
@ -76,7 +77,7 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
Client: client, Client: client,
MaxParallel: d.Get("pm_parallel").(int), MaxParallel: d.Get("pm_parallel").(int),
CurrentParallel: 0, CurrentParallel: 0,
MaxVmId: -1, MaxVMID: -1,
Mutex: &mut, Mutex: &mut,
Cond: sync.NewCond(&mut), Cond: sync.NewCond(&mut),
}, nil }, 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) { func nextVmId(pconf *providerConfiguration) (nextId int, err error) {
pconf.Mutex.Lock() pconf.Mutex.Lock()
pconf.MaxVmId, err = pconf.Client.GetNextID(pconf.MaxVmId + 1) pconf.MaxVMID, err = pconf.Client.GetNextID(pconf.MaxVMID + 1)
if err != nil { if err != nil {
return 0, err return 0, err
} }
nextId = pconf.MaxVmId nextId = pconf.MaxVMID
pconf.Mutex.Unlock() pconf.Mutex.Unlock()
return nextId, nil return nextId, nil
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
// Provisioner - Terrafrom properties for proxmox-provisioner
func Provisioner() terraform.ResourceProvisioner { func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{ return &schema.Provisioner{
Schema: map[string]*schema.Schema{ 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 { func applyFn(ctx context.Context) error {
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
@ -36,11 +37,11 @@ func applyFn(ctx context.Context) error {
connInfo := state.Ephemeral.ConnInfo connInfo := state.Ephemeral.ConnInfo
act := data.Get("action").(string) act := data.Get("action").(string)
targetNode, _, vmId, err := parseResourceId(state.ID) targetNode, _, vmID, err := parseResourceId(state.ID)
if err != nil { if err != nil {
return err return err
} }
vmr := pxapi.NewVmRef(vmId) vmr := pxapi.NewVmRef(vmID)
vmr.SetNode(targetNode) vmr.SetNode(targetNode)
client := currentClient client := currentClient
if client == nil { if client == nil {
@ -69,5 +70,4 @@ func applyFn(ctx context.Context) error {
default: default:
return fmt.Errorf("Unkown action: %s", act) return fmt.Errorf("Unkown action: %s", act)
} }
return nil
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -38,6 +39,7 @@ func resourceVmQemu() *schema.Resource {
"target_node": { "target_node": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
ForceNew: true,
}, },
"ssh_forward_ip": { "ssh_forward_ip": {
Type: schema.TypeString, Type: schema.TypeString,
@ -46,15 +48,22 @@ func resourceVmQemu() *schema.Resource {
"iso": { "iso": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true,
}, },
"clone": { "clone": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
ForceNew: true,
}, },
"storage": { "storage": {
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Required: true,
}, },
"storage_type": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"qemu_os": { "qemu_os": {
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
@ -119,16 +128,61 @@ func resourceVmQemu() *schema.Resource {
Optional: true, Optional: true,
Default: false, 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 { func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
pconf := meta.(*providerConfiguration) pconf := meta.(*providerConfiguration)
pmParallelBegin(pconf) pmParallelBegin(pconf)
client := pconf.Client client := pconf.Client
vmName := d.Get("name").(string) vmName := d.Get("name").(string)
disk_gb := d.Get("disk_gb").(float64) diskGB := d.Get("disk_gb").(float64)
config := pxapi.ConfigQemu{ config := pxapi.ConfigQemu{
Name: vmName, Name: vmName,
Description: d.Get("desc").(string), Description: d.Get("desc").(string),
@ -136,11 +190,19 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
Memory: d.Get("memory").(int), Memory: d.Get("memory").(int),
QemuCores: d.Get("cores").(int), QemuCores: d.Get("cores").(int),
QemuSockets: d.Get("sockets").(int), QemuSockets: d.Get("sockets").(int),
DiskSize: disk_gb, DiskSize: diskGB,
QemuOs: d.Get("qemu_os").(string), QemuOs: d.Get("qemu_os").(string),
QemuNicModel: d.Get("nic").(string), QemuNicModel: d.Get("nic").(string),
QemuBrige: d.Get("bridge").(string), QemuBrige: d.Get("bridge").(string),
QemuVlanTag: d.Get("vlan").(int), 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") log.Print("[DEBUG] checking for duplicate name")
dupVmr, _ := client.GetVmRefByName(vmName) dupVmr, _ := client.GetVmRefByName(vmName)
@ -185,7 +247,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
// give sometime to proxmox to catchup // give sometime to proxmox to catchup
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
err = prepareDiskSize(client, vmr, disk_gb) err = prepareDiskSize(client, vmr, diskGB)
if err != nil { if err != nil {
pmParallelEnd(pconf) pmParallelEnd(pconf)
return err return err
@ -213,7 +275,7 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
// give sometime to proxmox to catchup // give sometime to proxmox to catchup
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
err = prepareDiskSize(client, vmr, disk_gb) err = prepareDiskSize(client, vmr, diskGB)
if err != nil { if err != nil {
pmParallelEnd(pconf) pmParallelEnd(pconf)
return err return err
@ -230,11 +292,25 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
pmParallelEnd(pconf) pmParallelEnd(pconf)
return err return err
} }
log.Print("[DEBUG] setting up SSH forward")
sshPort, err := pxapi.SshForwardUsernet(vmr, client) sshPort := "22"
if err != nil { sshHost := ""
pmParallelEnd(pconf) if config.HasCloudInit() {
return err 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 // 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{ d.SetConnInfo(map[string]string{
"type": "ssh", "type": "ssh",
"host": d.Get("ssh_forward_ip").(string), "host": sshHost,
"port": sshPort, "port": sshPort,
"user": d.Get("ssh_user").(string), "user": d.Get("ssh_user").(string),
"private_key": d.Get("ssh_private_key").(string), "private_key": d.Get("ssh_private_key").(string),
@ -270,6 +346,10 @@ func resourceVmQemuCreate(d *schema.ResourceData, meta interface{}) error {
return err return err
} }
case "cloud-init":
// wait for OS too boot awhile...
time.Sleep(time.Duration(d.Get("ci_wait").(int)) * time.Second)
default: default:
return fmt.Errorf("Unknown os_type: %s", d.Get("os_type").(string)) 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 return err
} }
vmName := d.Get("name").(string) vmName := d.Get("name").(string)
disk_gb := d.Get("disk_gb").(float64) diskGB := d.Get("disk_gb").(float64)
config := pxapi.ConfigQemu{ config := pxapi.ConfigQemu{
Name: vmName, Name: vmName,
Description: d.Get("desc").(string), Description: d.Get("desc").(string),
@ -295,11 +375,19 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error {
Memory: d.Get("memory").(int), Memory: d.Get("memory").(int),
QemuCores: d.Get("cores").(int), QemuCores: d.Get("cores").(int),
QemuSockets: d.Get("sockets").(int), QemuSockets: d.Get("sockets").(int),
DiskSize: disk_gb, DiskSize: diskGB,
QemuOs: d.Get("qemu_os").(string), QemuOs: d.Get("qemu_os").(string),
QemuNicModel: d.Get("nic").(string), QemuNicModel: d.Get("nic").(string),
QemuBrige: d.Get("bridge").(string), QemuBrige: d.Get("bridge").(string),
QemuVlanTag: d.Get("vlan").(int), 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) err = config.UpdateConfig(vmr, client)
@ -311,7 +399,7 @@ func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error {
// give sometime to proxmox to catchup // give sometime to proxmox to catchup
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
prepareDiskSize(client, vmr, disk_gb) prepareDiskSize(client, vmr, diskGB)
// give sometime to proxmox to catchup // give sometime to proxmox to catchup
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
@ -361,6 +449,7 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error {
d.Set("name", config.Name) d.Set("name", config.Name)
d.Set("desc", config.Description) d.Set("desc", config.Description)
d.Set("storage", config.Storage) d.Set("storage", config.Storage)
d.Set("storage_type", config.StorageType)
d.Set("memory", config.Memory) d.Set("memory", config.Memory)
d.Set("cores", config.QemuCores) d.Set("cores", config.QemuCores)
d.Set("sockets", config.QemuSockets) d.Set("sockets", config.QemuSockets)
@ -369,6 +458,16 @@ func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error {
d.Set("nic", config.QemuNicModel) d.Set("nic", config.QemuNicModel)
d.Set("bridge", config.QemuBrige) d.Set("bridge", config.QemuBrige)
d.Set("vlan", config.QemuVlanTag) 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) pmParallelEnd(pconf)
return nil return nil
} }
@ -397,14 +496,14 @@ func resourceVmQemuDelete(d *schema.ResourceData, meta interface{}) error {
return err 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) clonedConfig, err := pxapi.NewConfigQemuFromApi(vmr, client)
if err != nil { if err != nil {
return err return err
} }
if disk_gb > clonedConfig.DiskSize { if diskGB > clonedConfig.DiskSize {
log.Print("[DEBUG] resizing disk") log.Print("[DEBUG] resizing disk " + clonedConfig.StorageType)
_, err = client.ResizeQemuDisk(vmr, "virtio0", int(disk_gb-clonedConfig.DiskSize)) _, err = client.ResizeQemuDisk(vmr, clonedConfig.StorageType+"0", int(diskGB-clonedConfig.DiskSize))
if err != nil { if err != nil {
return err return err
} }