diff --git a/docs/resources/vm_qemu.md b/docs/resources/vm_qemu.md index b569e906..5208b239 100644 --- a/docs/resources/vm_qemu.md +++ b/docs/resources/vm_qemu.md @@ -121,7 +121,7 @@ The following arguments are supported in the top level resource block. | `scsihw` | `str` | `"lsi"` | The SCSI controller to emulate. Options: `lsi`, `lsi53c810`, `megasas`, `pvscsi`, `virtio-scsi-pci`, `virtio-scsi-single`. | | `pool` | `str` | | The resource pool to which the VM will be added. | | `tags` | `str` | | Tags of the VM. This is only meta information. | -| `force_create` | `bool` | `false` | If `false`, and a vm of the same name, on the same node exists, terraform will attempt to reconfigure that VM with these settings. Set to true to always create a new VM (note, the name of the VM must still be unique, otherwise an error will be produced.) | +| `force_create` | `bool` | `false` | If `false`, and a VM of the same name, on the same node exists, terraform will attempt to reconfigure that VM with these settings. Set to true to always create a new VM (note, the name of the VM must still be unique, otherwise an error will be produced.) | | `os_type` | `str` | | Which provisioning method to use, based on the OS type. Options: `ubuntu`, `centos`, `cloud-init`. | | `force_recreate_on_change_of` | `str` | | If the value of this string changes, the VM will be recreated. Useful for allowing this resource to be recreated when arbitrary attributes change. An example where this is useful is a cloudinit configuration (as the `cicustom` attribute points to a file not the content). | | `os_network_config` | `str` | | Only applies when `define_connection_info` is true. Network configuration to be copied into the VM when preprovisioning `ubuntu` or `centos` guests. The specified configuration is added to `/etc/network/interfaces` for Ubuntu, or `/etc/sysconfig/network-scripts/ifcfg-eth0` for CentOS. Forces re-creation on change. | @@ -369,7 +369,7 @@ See the [docs about disks](https://pve.proxmox.com/pve-docs/chapter-qm.html#qm_h | `iops_wr_burst_length` | `int` | `0` | `all` | Length of the write burst duration in seconds. `0` means the default duration dictated by proxmox. | | `iops_wr_concurrent` | `int` | `0` | `all` | Maximum number of iops while writing concurrently. `0` means unlimited. | | `iothread` | `bool` | `false` | `scsi`, `virtio` | Whether to use iothreads for this drive. Only effective when the the emulated controller type (`scsihw` top level block argument) is `virtio-scsi-single`. | -| `linked_disk_id` | `int` | | `all` | **Computed** The `vmid` of the linked vm this disk was cloned from. | +| `linked_disk_id` | `int` | | `all` | **Computed** The `vmid` of the linked VM this disk was cloned from. | | `mbps_r_burst` | `float` | `0.0` | `all` | Maximum read speed in megabytes per second. `0` means unlimited. | | `mbps_r_concurrent` | `float` | `0.0` | `all` | Maximum read speed in megabytes per second. `0` means unlimited. | | `mbps_wr_burst` | `float` | `0.0` | `all` | Maximum write speed in megabytes per second. `0` means unlimited. | @@ -483,7 +483,7 @@ In addition to the arguments above, the following attributes can be referenced f | ---------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ssh_host` | `str` | Read-only attribute. Only applies when `define_connection_info` is true. The hostname or IP to use to connect to the VM for preprovisioning. This can be overridden by defining `ssh_forward_ip`, but if you're using cloud-init and `ipconfig0=dhcp`, the IP reported by qemu-guest-agent is used, otherwise the IP defined in `ipconfig0` is used. | | `ssh_port` | `str` | Read-only attribute. Only applies when `define_connection_info` is true. The port to connect to the VM over SSH for preprovisioning. If using cloud-init and a port is not specified in `ssh_forward_ip`, then 22 is used. If not using cloud-init, a port on the `target_node` will be forwarded to port 22 in the guest, and this attribute will be set to the forwarded port. | -| `default_ipv4_address` | `str` | Read-only attribute. Only applies when `agent` is `1` and Proxmox can actually read the ip the vm has. | +| `default_ipv4_address` | `str` | Read-only attribute. Only applies when `agent` is `1` and Proxmox can actually read the ip the VM has. | ## Import diff --git a/examples/cloudinit_example.tf b/examples/cloudinit_example.tf index 70c61a09..938f1b49 100644 --- a/examples/cloudinit_example.tf +++ b/examples/cloudinit_example.tf @@ -1,59 +1,59 @@ provider "proxmox" { - pm_tls_insecure = true - pm_api_url = "https://proxmox-server01.example.com:8006/api2/json" - pm_password = "secret" - pm_user = "terraform-prov@pve" - pm_otp = "" + pm_tls_insecure = true + pm_api_url = "https://proxmox-server01.example.com:8006/api2/json" + pm_password = "secret" + pm_user = "terraform-prov@pve" + pm_otp = "" } resource "proxmox_vm_qemu" "cloudinit-test" { - name = "terraform-test-vm" - desc = "A test for using terraform and cloudinit" - - # Node name has to be the same name as within the cluster - # this might not include the FQDN - target_node = "proxmox-server02" - - # The destination resource pool for the new VM - pool = "pool0" - - # The template name to clone this vm from - clone = "linux-cloudinit-template" - - # Activate QEMU agent for this VM - agent = 1 - - os_type = "cloud-init" - cores = 2 - sockets = 1 - vcpus = 0 - cpu = "host" - memory = 2048 - scsihw = "lsi" - - # Setup the disk - disk { - size = 32 - type = "virtio" - storage = "ceph-storage-pool" - storage_type = "rbd" - iothread = 1 - ssd = 1 - discard = "on" - } - - # Setup the network interface and assign a vlan tag: 256 - network { - model = "virtio" - bridge = "vmbr0" - tag = 256 - } - - # Setup the ip address using cloud-init. - # Keep in mind to use the CIDR notation for the ip. - ipconfig0 = "ip=192.168.10.20/24,gw=192.168.10.1" - - sshkeys = < 0 { logger.Debug().Int("vmid", vmID).Msgf("VM Net Config '%+v' from '%+v' set as '%+v' type of '%T'", config.QemuNetworks, flatNetworks, d.Get("network"), flatNetworks[0]["macaddr"]) } - logger.Debug().Int("vmid", vmID).Msgf("Finished VM read resulting in data: '%+v'", string(jsonString)) - + logger.Debug().Int("vmid", vmID).Msgf("Finished VM read resulting in data: `%+v`", string(jsonString)) + lock.unlock() return diags } @@ -1584,7 +1567,7 @@ func resourceVmQemuDelete(ctx context.Context, d *schema.ResourceData, meta inte return diag.FromErr(err) } - // Wait until vm is stopped. Otherwise, deletion will fail. + // Wait until VM is stopped. Otherwise, deletion will fail. // ugly way to wait 5 minutes(300s) waited := 0 for waited < 300 { @@ -1601,6 +1584,7 @@ func resourceVmQemuDelete(ctx context.Context, d *schema.ResourceData, meta inte } _, err = client.DeleteVm(vmr) + lock.unlock() return diag.FromErr(err) } @@ -1798,7 +1782,6 @@ func initConnInfo(ctx context.Context, client *pxapi.Client, vmr *pxapi.VmRef, config *pxapi.ConfigQemu, - lock *pmApiLockHolder, ) diag.Diagnostics { logger, _ := CreateSubLogger("initConnInfo") @@ -1809,7 +1792,6 @@ func initConnInfo(ctx context.Context, var diags diag.Diagnostics // allow user to opt-out of setting the connection info for the resource if !d.Get("define_connection_info").(bool) { - log.Printf("[INFO][initConnInfo] define_connection_info is %t, no further action", d.Get("define_connection_info").(bool)) logger.Info().Int("vmid", vmr.VmId()).Msgf("define_connection_info is %t, no further action", d.Get("define_connection_info").(bool)) diags = append(diags, diag.Diagnostic{ @@ -1822,7 +1804,6 @@ func initConnInfo(ctx context.Context, } // allow user to opt-out of setting the connection info for the resource if d.Get("agent") != 1 { - log.Printf("[INFO][initConnInfo] qemu agent is disabled from proxmox config, cant communicate with vm.") logger.Info().Int("vmid", vmr.VmId()).Msgf("qemu agent is disabled from proxmox config, cant communicate with vm.") diags = append(diags, diag.Diagnostic{ Severity: diag.Warning, @@ -1832,9 +1813,7 @@ func initConnInfo(ctx context.Context, }) return diags } - - log.Print("[INFO][initConnInfo] trying to get vm ip address for provisioner") - logger.Info().Int("vmid", vmr.VmId()).Msgf("trying to get vm ip address for provisioner") + logger.Info().Str("vmid", d.Id()).Msgf("trying to get VM IP address for provisioner") sshPort := "22" sshHost := "" // assume guest agent not running yet or not enabled @@ -1843,8 +1822,6 @@ func initConnInfo(ctx context.Context, // wait until the os has started the guest agent guestAgentTimeout := d.Timeout(schema.TimeoutCreate) guestAgentWaitEnd := time.Now().Add(time.Duration(guestAgentTimeout)) - log.Printf("[DEBUG][initConnInfo] retrying for at most %v minutes before giving up", guestAgentTimeout) - log.Printf("[DEBUG][initConnInfo] retries will end at %s", guestAgentWaitEnd) logger.Debug().Int("vmid", vmr.VmId()).Msgf("retrying for at most %v minutes before giving up", guestAgentTimeout) logger.Debug().Int("vmid", vmr.VmId()).Msgf("retries will end at %s", guestAgentWaitEnd) @@ -1852,12 +1829,9 @@ func initConnInfo(ctx context.Context, interfaces, err = client.GetVmAgentNetworkInterfaces(vmr) lasterr = err if err != nil { - log.Printf("[DEBUG][initConnInfo] check ip result error %s", err.Error()) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("check ip result error %s", err.Error()) + logger.Debug().Int("vmid", vmr.VmId()).Msgf("check ip result error: `%s`", err.Error()) } else if err == nil { lasterr = nil - log.Print("[INFO][initConnInfo] found working QEMU Agent") - log.Printf("[DEBUG][initConnInfo] interfaces found: %v", interfaces) logger.Info().Int("vmid", vmr.VmId()).Msgf("found working QEMU Agent") logger.Debug().Int("vmid", vmr.VmId()).Msgf("interfaces found: %v", interfaces) @@ -1872,7 +1846,6 @@ func initConnInfo(ctx context.Context, time.Sleep(time.Duration(d.Get("additional_wait").(int)) * time.Second) } if lasterr != nil { - log.Printf("[INFO][initConnInfo] error from PVE: \"%s\"\n, QEMU Agent is enabled in you configuration but non installed/not working on your vm", lasterr) logger.Info().Int("vmid", vmr.VmId()).Msgf("error from PVE: \"%s\"\n, QEMU Agent is enabled in you configuration but non installed/not working on your vm", lasterr) return diag.FromErr(fmt.Errorf("error from PVE: \"%s\"\n, QEMU Agent is enabled in you configuration but non installed/not working on your vm", lasterr)) } @@ -1880,28 +1853,22 @@ func initConnInfo(ctx context.Context, if err != nil { return diag.FromErr(err) } - log.Print("[INFO][initConnInfo] trying to find IP address of first network card") logger.Info().Int("vmid", vmr.VmId()).Msgf("trying to find IP address of first network card") // wait until we find a valid ipv4 address - log.Printf("[DEBUG][initConnInfo] checking network card...") - logger.Debug().Int("vmid", vmr.VmId()).Msgf("checking network card...") + logger.Debug().Int("vmid", vmr.VmId()).Msgf("checking network card") for guestAgentRunning && time.Now().Before(guestAgentWaitEnd) { - log.Printf("[DEBUG][initConnInfo] checking network card...") interfaces, err = client.GetVmAgentNetworkInterfaces(vmr) net0MacAddress := macAddressRegex.FindString(vmConfig["net0"].(string)) if err != nil { - log.Printf("[DEBUG][initConnInfo] checking network card error %s", err.Error()) logger.Debug().Int("vmid", vmr.VmId()).Msgf("checking network card error %s", err.Error()) // return err } else { - log.Printf("[DEBUG][initConnInfo] checking network card loop") logger.Debug().Int("vmid", vmr.VmId()).Msgf("checking network card loop") for _, iface := range interfaces { if strings.EqualFold(strings.ToUpper(iface.MACAddress), strings.ToUpper(net0MacAddress)) { for _, addr := range iface.IPAddresses { if addr.IsGlobalUnicast() && strings.Count(addr.String(), ":") < 2 { - log.Printf("[DEBUG][initConnInfo] Found IP address: %s", addr.String()) logger.Debug().Int("vmid", vmr.VmId()).Msgf("Found IP address: %s", addr.String()) sshHost = addr.String() } @@ -1909,7 +1876,6 @@ func initConnInfo(ctx context.Context, } } if sshHost != "" { - log.Printf("[DEBUG][initConnInfo] sshHost not empty: %s", sshHost) logger.Debug().Int("vmid", vmr.VmId()).Msgf("sshHost not empty: %s", sshHost) break } @@ -1920,15 +1886,12 @@ func initConnInfo(ctx context.Context, // todo - log a warning if we couldn't get an IP if config.HasCloudInit() { - log.Print("[DEBUG][initConnInfo] vm has a cloud-init configuration") - logger.Debug().Int("vmid", vmr.VmId()).Msgf(" vm has a cloud-init configuration") + logger.Debug().Int("vmid", vmr.VmId()).Msgf("VM has a cloud-init configuration") _, ipconfig0Set := d.GetOk("ipconfig0") if ipconfig0Set { vmState, err := client.GetVmState(vmr) - log.Printf("[DEBUG][initConnInfo] cloudinitcheck vm state %v", vmState) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("cloudinitcheck vm state %v", vmState) + logger.Debug().Int("vmid", vmr.VmId()).Msgf("cloud-init VM state %v", vmState) if err != nil { - log.Printf("[DEBUG][initConnInfo] vmstate error %s", err.Error()) logger.Debug().Int("vmid", vmr.VmId()).Msgf("vmstate error %s", err.Error()) return diag.FromErr(err) } @@ -1940,7 +1903,6 @@ func initConnInfo(ctx context.Context, } ipconfig0 := net.ParseIP(strings.Split(ipMatch[1], ":")[0]) interfaces, err = client.GetVmAgentNetworkInterfaces(vmr) - log.Printf("[DEBUG][initConnInfo] ipconfig0 interfaces: %v", interfaces) logger.Debug().Int("vmid", vmr.VmId()).Msgf("ipconfig0 interfaces %v", interfaces) if err != nil { return diag.FromErr(err) @@ -1960,8 +1922,7 @@ func initConnInfo(ctx context.Context, } } - log.Print("[DEBUG][initConnInfo] found an ip configuration") - logger.Debug().Int("vmid", vmr.VmId()).Msgf("Found an ip configuration") + logger.Debug().Int("vmid", vmr.VmId()).Msgf("found an ip configuration") // Check if we got a specified port if strings.Contains(sshHost, ":") { sshParts := strings.Split(sshHost, ":") @@ -1970,13 +1931,11 @@ func initConnInfo(ctx context.Context, } } if sshHost == "" { - log.Print("[DEBUG][initConnInfo] Cannot find any IP address") - logger.Debug().Int("vmid", vmr.VmId()).Msgf("Cannot find any IP address") + logger.Debug().Int("vmid", vmr.VmId()).Msgf("cannot find any IP address") return diag.FromErr(fmt.Errorf("cannot find any IP address")) } - log.Printf("[DEBUG][initConnInfo] this is the vm configuration: %s %s", sshHost, sshPort) - logger.Debug().Int("vmid", vmr.VmId()).Msgf("this is the vm configuration: %s %s", sshHost, sshPort) + logger.Debug().Int("vmid", vmr.VmId()).Msgf("this is the VM configuration: %s %s", sshHost, sshPort) // Optional convenience attributes for provisioners err = d.Set("default_ipv4_address", sshHost) @@ -1998,7 +1957,7 @@ func initConnInfo(ctx context.Context, func setCloudInitDisk(d *schema.ResourceData, config *pxapi.ConfigQemu) { storage := d.Get("cloudinit_cdrom_storage").(string) if storage != "" { - config.Disks.Ide.Disk_3 = &pxapi.QemuIdeStorage{CloudInit: &pxapi.QemuCloudInitDisk{ + config.Disks.Ide.Disk_2 = &pxapi.QemuIdeStorage{CloudInit: &pxapi.QemuCloudInitDisk{ Format: pxapi.QemuDiskFormat_Raw, Storage: storage, }} @@ -2006,8 +1965,8 @@ func setCloudInitDisk(d *schema.ResourceData, config *pxapi.ConfigQemu) { } func getCloudInitDisk(config *pxapi.QemuStorages) string { - if config != nil && config.Ide != nil && config.Ide.Disk_3 != nil && config.Ide.Disk_3.CloudInit != nil { - return config.Ide.Disk_3.CloudInit.Storage + if config != nil && config.Ide != nil && config.Ide.Disk_2 != nil && config.Ide.Disk_2.CloudInit != nil { + return config.Ide.Disk_2.CloudInit.Storage } return "" } @@ -2083,15 +2042,16 @@ func mapFromStruct_QemuIdeDisks(config *pxapi.QemuIdeDisks) []interface{} { } ide_0 := mapFromStruct_QemuIdeStorage(config.Disk_0, "ide0") ide_1 := mapFromStruct_QemuIdeStorage(config.Disk_1, "ide1") - ide_2 := mapFromStruct_QemuIdeStorage(config.Disk_2, "ide2") - if ide_0 == nil && ide_1 == nil && ide_2 == nil { + //ide_2 := mapFromStruct_QemuIdeStorage(config.Disk_2, "ide2") + ide_3 := mapFromStruct_QemuIdeStorage(config.Disk_3, "ide3") + if ide_0 == nil && ide_1 == nil && ide_3 == nil { return nil } return []interface{}{ map[string]interface{}{ "ide0": ide_0, "ide1": ide_1, - "ide2": ide_2, + "ide3": ide_3, }, } } @@ -2444,7 +2404,7 @@ func mapToStruct_QemuIdeDisks(ide *pxapi.QemuIdeDisks, schema map[string]interfa disks := schemaItem[0].(map[string]interface{}) mapToStruct_QemuIdeStorage(ide.Disk_0, "ide0", disks) mapToStruct_QemuIdeStorage(ide.Disk_1, "ide1", disks) - mapToStruct_QemuIdeStorage(ide.Disk_2, "ide2", disks) + mapToStruct_QemuIdeStorage(ide.Disk_3, "ide3", disks) } func mapToStruct_QemuIdeStorage(ide *pxapi.QemuIdeStorage, key string, schema map[string]interface{}) { diff --git a/proxmox/resource_vm_qemu_test.go b/proxmox/resource_vm_qemu_test.go index f57fc230..dbe49dde 100644 --- a/proxmox/resource_vm_qemu_test.go +++ b/proxmox/resource_vm_qemu_test.go @@ -75,7 +75,7 @@ func testAccProxmoxProviderFactory() map[string]*schema.Provider { //} // testAccExampleQemuBasic generates the most simplistic VM we're able to make -// this confirms we can spin up a vm using just default values +// this confirms we can spin up a VM using just default values func testAccExampleQemuBasic(name string, targetNode string) string { return fmt.Sprintf(` resource "proxmox_vm_qemu" "%s" { @@ -127,7 +127,7 @@ resource "proxmox_vm_qemu" "%s" { // testAccExampleResource generates a configured VM with a 1G disk // the goal with this resource is to make a "basic" but "standard" virtual machine -// using a configuration that would apply to a usable vm (but NOT a cloud config'd one) +// using a configuration that would apply to a usable VM (but NOT a cloud config'd one) func testAccExampleQemuStandard(name string, targetNode string) string { return fmt.Sprintf(` resource "proxmox_vm_qemu" "%s" { @@ -243,7 +243,7 @@ func TestAccProxmoxVmQemu_StandardCreate(t *testing.T) { } // TODO: this test FAILS - it looks like the api library isn't actually sending the slot request to proxmox? needs further investigation. -// TestAccProxmoxVmQemu_DiskSlot tests we can correctly create a vm disk assigned to a particular disk slot +// TestAccProxmoxVmQemu_DiskSlot tests we can correctly create a VM disk assigned to a particular disk slot //func TestAccProxmoxVmQemu_DiskSlot(t *testing.T) { // resourceName := acctest.RandStringFromCharSet(10, acctest.CharSetAlpha) // resourcePath := fmt.Sprintf("proxmox_vm_qemu.%s", resourceName) diff --git a/proxmox/util.go b/proxmox/util.go index 1e766806..d516897b 100644 --- a/proxmox/util.go +++ b/proxmox/util.go @@ -27,6 +27,17 @@ var macAddressRegex = regexp.MustCompile(`([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}`) var machineModelsRegex = regexp.MustCompile(`(^pc|^q35|^virt)`) +// a global variable (but package scoped) to allow us to log stuff happening with style +// IMPORTANT: this logger is created by the ConfigureLogger function. Be sure that has run +// before using this logger otherwise you'll probably crash stuff. +var rootLogger zerolog.Logger + +// a supporting global to keep track of our configured logLevels +// IMPORTANT: this variable is set by the ConfigureLogger function. Be sure that it has run. +var logLevels map[string]string + +type KeyedDeviceMap map[interface{}]pxapi.QemuDevice + // given a string, return the appropriate zerolog level func levelStringToZerologLevel(logLevel string) (zerolog.Level, error) { conversionMap := map[string]zerolog.Level{ @@ -46,15 +57,6 @@ func levelStringToZerologLevel(logLevel string) (zerolog.Level, error) { return foundResult, nil } -// a global variable (but package scoped) to allow us to log stuff happening with style -// IMPORTANT: this logger is created by the ConfigureLogger function. Be sure that has run -// before using this logger otherwise you'll probably crash stuff. -var rootLogger zerolog.Logger - -// a supporting global to keep track of our configured logLevels -// IMPORTANT: this variable is set by the ConfigureLogger function. Be sure that it has run. -var logLevels map[string]string - // Configure the debug logger for this provider. The goal here is to enable selective amounts // of output for targetted debugging without overwhelming with data from sources the user/developer // doesn't care about. @@ -105,12 +107,12 @@ func ConfigureLogger(enableOutput bool, logPath string, inputLogLevels map[strin // using a multi-writer here so we can easily add additional log destination (like a json file) // for now though using just the console writer because it makes pretty logs - consoleWriter := zerolog.ConsoleWriter{Out: f, TimeFormat: time.RFC1123Z} + consoleWriter := zerolog.ConsoleWriter{Out: f, TimeFormat: time.RFC3339, NoColor: true} multi := zerolog.MultiLevelWriter(consoleWriter) // create an init logger for logging just stuff before the root logger can get going // this has a hard coded set of information to ensure we can log stuff before the root logger is live - initLogger := zerolog.New(multi).With().Timestamp().Caller().Logger().Level(zerolog.InfoLevel) + initLogger := zerolog.New(multi).With().Timestamp().Logger().Level(zerolog.InfoLevel) // look to see if there is a default level we should be using defaultLevelString, ok := logLevels["_default"] @@ -134,7 +136,7 @@ func ConfigureLogger(enableOutput bool, logPath string, inputLogLevels map[strin // create the root logger // note there is no initialization here. we WANT this to be set to the global logger - rootLogger = zerolog.New(multi).With().Timestamp().Caller().Logger().Level(rootLevel) + rootLogger = zerolog.New(multi).With().Timestamp().Logger().Level(rootLevel) // mirror Stdout to the debug log file as well // useful as we can debug the communication to/from the plugin and terraform @@ -262,8 +264,6 @@ func DevicesSetToMapWithoutId(devicesSet *schema.Set) pxapi.QemuDevices { return devicesMap } -type KeyedDeviceMap map[interface{}]pxapi.QemuDevice - func DevicesListToMapByKey(devicesList []interface{}, key string) KeyedDeviceMap { devicesMap := KeyedDeviceMap{} for i, set := range devicesList { @@ -441,7 +441,6 @@ func schemaListToFlatValues(schemaList []interface{}, resource *schema.Resource) // func getIP(ifs pxapi.AgentNetworkInterface, macaddr string) string { // for _, iface := range ifs { // if strings.ToUpper(iface.MACAddress) == strings.ToUpper(macaddr) { - // } // } // return ""