package proxmox import ( "fmt" "log" "math" "path" "regexp" "strconv" "strings" "time" pxapi "github.com/Telmate/proxmox-api-go/proxmox" "github.com/hashicorp/terraform/helper/schema" ) func resourceVmQemu() *schema.Resource { *pxapi.Debug = true return &schema.Resource{ Create: resourceVmQemuCreate, Read: resourceVmQemuRead, Update: resourceVmQemuUpdate, Delete: resourceVmQemuDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, Required: true, }, "desc": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, "target_node": { Type: schema.TypeString, Required: true, }, "onboot": { Type: schema.TypeBool, Optional: true, Default: true, }, "boot": { Type: schema.TypeString, Optional: true, Default: "cdn", }, "bootdisk": { Type: schema.TypeString, Optional: true, }, "agent": { Type: schema.TypeInt, Optional: true, Default: 0, }, "iso": { Type: schema.TypeString, Optional: true, ForceNew: true, }, "clone": { Type: schema.TypeString, Optional: true, ForceNew: true, }, "full_clone": { Type: schema.TypeBool, Optional: true, ForceNew: true, Default: true, }, "hastate": { Type: schema.TypeString, Optional: true, }, "qemu_os": { Type: schema.TypeString, Optional: true, Default: "l26", DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { if new == "l26" { return len(d.Get("clone").(string)) > 0 // the cloned source may have a different os, which we shoud leave alone } return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, "memory": { Type: schema.TypeInt, Optional: true, Default: 512, }, "cores": { Type: schema.TypeInt, Optional: true, Default: 1, }, "sockets": { Type: schema.TypeInt, Optional: true, Default: 1, }, "vcpus": { Type: schema.TypeInt, Optional: true, Default: 0, }, "cpu": { Type: schema.TypeString, Optional: true, Default: "host", }, "numa": { Type: schema.TypeBool, Optional: true, Default: false, }, "hotplug": { Type: schema.TypeString, Optional: true, Default: "network,disk,usb", }, "scsihw": { Type: schema.TypeString, Optional: true, Default: "", }, "network": &schema.Schema{ Type: schema.TypeSet, Optional: true, ConflictsWith: []string{"nic", "bridge", "vlan", "mac"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": &schema.Schema{ Type: schema.TypeInt, Required: true, }, "model": &schema.Schema{ Type: schema.TypeString, Required: true, }, "macaddr": &schema.Schema{ // TODO: Find a way to set MAC address in .tf config. 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) }, }, "bridge": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "nat", }, "tag": &schema.Schema{ Type: schema.TypeInt, Optional: true, Description: "VLAN tag.", Default: -1, }, "firewall": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "rate": &schema.Schema{ Type: schema.TypeInt, Optional: true, Default: -1, }, "queues": &schema.Schema{ Type: schema.TypeInt, Optional: true, Default: -1, }, "link_down": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, }, }, }, "disk": &schema.Schema{ Type: schema.TypeSet, Optional: true, ConflictsWith: []string{"disk_gb", "storage", "storage_type"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": &schema.Schema{ Type: schema.TypeInt, Required: true, }, "type": &schema.Schema{ Type: schema.TypeString, Required: true, }, "storage": &schema.Schema{ Type: schema.TypeString, Required: true, }, "storage_type": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "dir", Description: "One of PVE types as described: https://pve.proxmox.com/wiki/Storage", }, "size": &schema.Schema{ Type: schema.TypeString, Required: true, }, "format": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "raw", }, "cache": &schema.Schema{ Type: schema.TypeString, Optional: true, Default: "none", }, "backup": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "iothread": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, "replicate": &schema.Schema{ Type: schema.TypeBool, Optional: true, Default: false, }, }, }, }, // Deprecated single disk config. "disk_gb": { Type: schema.TypeFloat, Deprecated: "Use `disk.size` instead", Optional: 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 }, }, "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, Deprecated: "Use `network` instead", Optional: true, }, "bridge": { Type: schema.TypeString, Deprecated: "Use `network.bridge` instead", Optional: true, }, "vlan": { 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) }, }, "serial": &schema.Schema{ Type: schema.TypeSet, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": &schema.Schema{ Type: schema.TypeInt, Required: true, }, "type": &schema.Schema{ Type: schema.TypeString, Required: true, }, }, }, }, "os_type": { Type: schema.TypeString, Optional: true, }, "os_network_config": { Type: schema.TypeString, Optional: true, ForceNew: true, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, "ssh_forward_ip": { Type: schema.TypeString, Optional: true, }, "ssh_user": { Type: schema.TypeString, Optional: true, ForceNew: true, }, "ssh_private_key": { Type: schema.TypeString, Optional: true, Sensitive: true, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, "force_create": { Type: schema.TypeBool, Optional: true, Default: false, }, "clone_wait": { Type: schema.TypeInt, Optional: true, Default: 15, }, "ci_wait": { // how long to wait before provision 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, 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, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, "ipconfig0": { Type: schema.TypeString, Optional: true, }, "ipconfig1": { Type: schema.TypeString, Optional: true, }, "ipconfig2": { 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"}, }, "pool": { Type: schema.TypeString, Optional: true, }, "ssh_host": { Type: schema.TypeString, Computed: true, }, "ssh_port": { Type: schema.TypeString, Computed: 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) networks := d.Get("network").(*schema.Set) qemuNetworks := DevicesSetToMap(networks) disks := d.Get("disk").(*schema.Set) qemuDisks := DevicesSetToMap(disks) serials := d.Get("serial").(*schema.Set) qemuSerials := DevicesSetToMap(serials) config := pxapi.ConfigQemu{ Name: vmName, Description: d.Get("desc").(string), Pool: d.Get("pool").(string), Onboot: d.Get("onboot").(bool), Boot: d.Get("boot").(string), BootDisk: d.Get("bootdisk").(string), Agent: d.Get("agent").(int), Memory: d.Get("memory").(int), QemuCores: d.Get("cores").(int), QemuSockets: d.Get("sockets").(int), QemuVcpus: d.Get("vcpus").(int), QemuCpu: d.Get("cpu").(string), QemuNuma: d.Get("numa").(bool), Hotplug: d.Get("hotplug").(string), Scsihw: d.Get("scsihw").(string), HaState: d.Get("hastate").(string), QemuOs: d.Get("qemu_os").(string), QemuNetworks: qemuNetworks, QemuDisks: qemuDisks, QemuSerials: qemuSerials, // Cloud-init. 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), Ipconfig2: d.Get("ipconfig2").(string), // Deprecated single disk config. Storage: d.Get("storage").(string), DiskSize: d.Get("disk_gb").(float64), // 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) forceCreate := d.Get("force_create").(bool) targetNode := d.Get("target_node").(string) pool := d.Get("pool").(string) if dupVmr != nil && forceCreate { pmParallelEnd(pconf) return fmt.Errorf("Duplicate VM name (%s) with vmId: %d. Set force_create=false to recycle", vmName, dupVmr.VmId()) } else if dupVmr != nil && dupVmr.Node() != targetNode { pmParallelEnd(pconf) return fmt.Errorf("Duplicate VM name (%s) with vmId: %d on different target_node=%s", vmName, dupVmr.VmId(), dupVmr.Node()) } vmr := dupVmr if vmr == nil { // get unique id nextid, err := nextVmId(pconf) if err != nil { pmParallelEnd(pconf) return err } vmr = pxapi.NewVmRef(nextid) // set target node and pool vmr.SetNode(targetNode) if pool != "" { vmr.SetPool(pool) } // check if ISO or clone if d.Get("clone").(string) != "" { fullClone := 1 if !d.Get("full_clone").(bool) { fullClone = 0 } config.FullClone = &fullClone sourceVmr, err := client.GetVmRefByName(d.Get("clone").(string)) if err != nil { pmParallelEnd(pconf) return err } log.Print("[DEBUG] cloning VM") err = config.CloneVm(sourceVmr, vmr, client) if err != nil { // Set the id because when update config fail the vm is still created d.SetId(resourceId(targetNode, "qemu", vmr.VmId())) pmParallelEnd(pconf) return err } // give sometime to proxmox to catchup time.Sleep(time.Duration(d.Get("clone_wait").(int)) * time.Second) err = prepareDiskSize(client, vmr, qemuDisks) if err != nil { pmParallelEnd(pconf) return err } } else if d.Get("iso").(string) != "" { config.QemuIso = d.Get("iso").(string) err := config.CreateVm(vmr, client) if err != nil { pmParallelEnd(pconf) return err } } else { return fmt.Errorf("Either clone or iso must be set") } } else { log.Printf("[DEBUG] recycling VM vmId: %d", vmr.VmId()) client.StopVm(vmr) err := config.UpdateConfig(vmr, client) if err != nil { // Set the id because when update config fail the vm is still created d.SetId(resourceId(targetNode, "qemu", vmr.VmId())) pmParallelEnd(pconf) return err } // give sometime to proxmox to catchup time.Sleep(5 * time.Second) err = prepareDiskSize(client, vmr, qemuDisks) if err != nil { pmParallelEnd(pconf) return err } } d.SetId(resourceId(targetNode, "qemu", vmr.VmId())) // give sometime to proxmox to catchup time.Sleep(15 * time.Second) log.Print("[DEBUG] starting VM") _, err := client.StartVm(vmr) if err != nil { pmParallelEnd(pconf) return err } err = initConnInfo(d, pconf, client, vmr, &config) if err != nil { return err } // Apply pre-provision if enabled. preprovision(d, pconf, client, vmr, true) return nil } func resourceVmQemuUpdate(d *schema.ResourceData, meta interface{}) error { pconf := meta.(*providerConfiguration) pmParallelBegin(pconf) client := pconf.Client _, _, vmID, err := parseResourceId(d.Id()) if err != nil { pmParallelEnd(pconf) return err } vmr := pxapi.NewVmRef(vmID) _, err = client.GetVmInfo(vmr) if err != nil { pmParallelEnd(pconf) return err } configDisksSet := d.Get("disk").(*schema.Set) qemuDisks := DevicesSetToMap(configDisksSet) configNetworksSet := d.Get("network").(*schema.Set) qemuNetworks := DevicesSetToMap(configNetworksSet) serials := d.Get("serial").(*schema.Set) qemuSerials := DevicesSetToMap(serials) d.Partial(true) if d.HasChange("target_node") { _, err := client.MigrateNode(vmr, d.Get("target_node").(string), true) if err != nil { pmParallelEnd(pconf) return err } d.SetPartial("target_node") vmr.SetNode(d.Get("target_node").(string)) } d.Partial(false) config := pxapi.ConfigQemu{ Name: d.Get("name").(string), Description: d.Get("desc").(string), Pool: d.Get("pool").(string), Onboot: d.Get("onboot").(bool), Boot: d.Get("boot").(string), BootDisk: d.Get("bootdisk").(string), Agent: d.Get("agent").(int), Memory: d.Get("memory").(int), QemuCores: d.Get("cores").(int), QemuSockets: d.Get("sockets").(int), QemuVcpus: d.Get("vcpus").(int), QemuCpu: d.Get("cpu").(string), QemuNuma: d.Get("numa").(bool), Hotplug: d.Get("hotplug").(string), Scsihw: d.Get("scsihw").(string), HaState: d.Get("hastate").(string), QemuOs: d.Get("qemu_os").(string), QemuNetworks: qemuNetworks, QemuDisks: qemuDisks, QemuSerials: qemuSerials, // Cloud-init. 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), Ipconfig2: d.Get("ipconfig2").(string), // Deprecated single disk config. Storage: d.Get("storage").(string), DiskSize: d.Get("disk_gb").(float64), // 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) if err != nil { pmParallelEnd(pconf) return err } // give sometime to proxmox to catchup time.Sleep(5 * time.Second) prepareDiskSize(client, vmr, qemuDisks) // give sometime to proxmox to catchup time.Sleep(15 * time.Second) // 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 } err = initConnInfo(d, pconf, client, vmr, &config) if err != nil { return err } // Apply pre-provision if enabled. preprovision(d, pconf, client, vmr, false) // give sometime to bootup time.Sleep(9 * time.Second) return nil } func resourceVmQemuRead(d *schema.ResourceData, meta interface{}) error { pconf := meta.(*providerConfiguration) pmParallelBegin(pconf) client := pconf.Client _, _, vmID, err := parseResourceId(d.Id()) if err != nil { pmParallelEnd(pconf) d.SetId("") return err } vmr := pxapi.NewVmRef(vmID) _, err = client.GetVmInfo(vmr) if err != nil { pmParallelEnd(pconf) return err } config, err := pxapi.NewConfigQemuFromApi(vmr, client) if err != nil { pmParallelEnd(pconf) return err } d.SetId(resourceId(vmr.Node(), "qemu", vmr.VmId())) d.Set("target_node", vmr.Node()) d.Set("name", config.Name) d.Set("desc", config.Description) d.Set("pool", config.Pool) d.Set("onboot", config.Onboot) d.Set("boot", config.Boot) d.Set("bootdisk", config.BootDisk) d.Set("agent", config.Agent) d.Set("memory", config.Memory) d.Set("cores", config.QemuCores) d.Set("sockets", config.QemuSockets) d.Set("vcpus", config.QemuVcpus) d.Set("cpu", config.QemuCpu) d.Set("numa", config.QemuNuma) d.Set("hotplug", config.Hotplug) d.Set("scsihw", config.Scsihw) d.Set("hastate", vmr.HaState()) d.Set("qemu_os", config.QemuOs) // Cloud-init. 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) d.Set("ipconfig2", config.Ipconfig2) // 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) d.Set("pool", vmr.Pool()) //Serials configSerialsSet := d.Get("serial").(*schema.Set) activeSerialSet := UpdateDevicesSet(configSerialsSet, config.QemuSerials) d.Set("serial", activeSerialSet) pmParallelEnd(pconf) return nil } func resourceVmQemuDelete(d *schema.ResourceData, meta interface{}) error { pconf := meta.(*providerConfiguration) pmParallelBegin(pconf) client := pconf.Client vmId, _ := strconv.Atoi(path.Base(d.Id())) vmr := pxapi.NewVmRef(vmId) _, err := client.StopVm(vmr) if err != nil { pmParallelEnd(pconf) return err } // give sometime to proxmox to catchup time.Sleep(2 * time.Second) _, err = client.DeleteVm(vmr) pmParallelEnd(pconf) return err } // 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 } //log.Printf("%s", clonedConfig) for diskID, diskConf := range diskConfMap { diskName := fmt.Sprintf("%v%v", diskConf["type"], diskID) diskSize := diskSizeGB(diskConf["size"]) if _, diskExists := clonedConfig.QemuDisks[diskID]; !diskExists { return err } clonedDiskSize := diskSizeGB(clonedConfig.QemuDisks[diskID]["size"]) 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 } func diskSizeGB(dcSize interface{}) float64 { var diskSize float64 switch dcSize.(type) { case string: diskString := strings.ToUpper(dcSize.(string)) re := regexp.MustCompile("([0-9]+)([A-Z]*)") diskArray := re.FindStringSubmatch(diskString) diskSize, _ = strconv.ParseFloat(diskArray[1], 64) if len(diskArray) >= 3 { switch diskArray[2] { case "G", "GB": //Nothing to do case "M", "MB": diskSize /= 1000 case "K", "KB": diskSize /= 1000000 } } case float64: diskSize = dcSize.(float64) } return diskSize } // 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 } func initConnInfo( d *schema.ResourceData, pconf *providerConfiguration, client *pxapi.Client, vmr *pxapi.VmRef, config *pxapi.ConfigQemu) error { sshPort := "22" sshHost := "" var err error 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] } // Check if we got a speficied port if strings.Contains(sshHost, ":") { sshParts := strings.Split(sshHost, ":") sshHost = sshParts[0] sshPort = sshParts[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) // Optional convience attributes for provisioners d.Set("ssh_host", sshHost) d.Set("ssh_port", sshPort) // This connection INFO is longer shared up to the providers :-( 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_otp": client.Otp, "pm_tls_insecure": "true", // TODO - pass pm_tls_insecure state around, but if we made it this far, default insecure }) return nil } // Internal pre-provision. func preprovision( d *schema.ResourceData, pconf *providerConfiguration, client *pxapi.Client, vmr *pxapi.VmRef, systemPreProvision bool, ) error { if d.Get("preprovision").(bool) { 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 } 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)) } } } return nil }