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 +}