From 641d0963dd6ae37409206139184c310d655ce058 Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Wed, 19 Jun 2024 22:01:56 +0200 Subject: [PATCH] add support for proxmox-api-go #339 --- docs/resources/vm_qemu.md | 1 + go.mod | 4 +- go.sum | 8 +- .../pxapi/dns/nameservers/nameservers.go | 41 +++ .../Internal/pxapi/guest/sshkeys/sshkeys.go | 35 +++ proxmox/heper_qemu.go | 34 +-- proxmox/heper_qemu_test.go | 103 +++++++- proxmox/resource_vm_qemu.go | 233 ++++++++++++------ proxmox/util.go | 15 ++ proxmox/util_test.go | 28 +++ 10 files changed, 383 insertions(+), 119 deletions(-) create mode 100644 proxmox/Internal/pxapi/dns/nameservers/nameservers.go create mode 100644 proxmox/Internal/pxapi/guest/sshkeys/sshkeys.go create mode 100644 proxmox/util_test.go diff --git a/docs/resources/vm_qemu.md b/docs/resources/vm_qemu.md index 39e8ea35..17cf8053 100644 --- a/docs/resources/vm_qemu.md +++ b/docs/resources/vm_qemu.md @@ -133,6 +133,7 @@ The following arguments are supported in the top level resource block. | `ciuser` | `str` | | Override the default cloud-init user for provisioning. | | `cipassword` | `str` | | Override the default cloud-init user's password. Sensitive. | | `cicustom` | `str` | | Instead specifying ciuser, cipasword, etc... you can specify the path to a custom cloud-init config file here. Grants more flexibility in configuring cloud-init. | +| `ciupgrade` | `bool` | `true` | Whether to upgrade the packages on the guest during provisioning. | | `searchdomain` | `str` | | Sets default DNS search domain suffix. | | `nameserver` | `str` | | Sets default DNS server for guest. | | `sshkeys` | `str` | | Newline delimited list of SSH public keys to add to authorized keys file for the cloud-init user. | diff --git a/go.mod b/go.mod index a52413d5..55b04842 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.21 toolchain go1.21.0 require ( - github.com/Telmate/proxmox-api-go v0.0.0-20240608213934-9d245a204c42 + github.com/Telmate/proxmox-api-go v0.0.0-20240616083239-78e131aa830b github.com/google/uuid v1.6.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 - github.com/rs/zerolog v1.33.0 + github.com/rs/zerolog v1.32.0 github.com/stretchr/testify v1.9.0 ) diff --git a/go.sum b/go.sum index bd0e423e..407ab0ce 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton h1:HKz85FwoXx86kVtTvFke7rgHvq/HoloSUvW5semjFWs= github.com/ProtonMail/go-crypto v1.1.0-alpha.2-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= -github.com/Telmate/proxmox-api-go v0.0.0-20240608213934-9d245a204c42 h1:wDo4xTIS+h2hF2m06Gz/Jw0F8fuczh4dZSYgDNEZ7BU= -github.com/Telmate/proxmox-api-go v0.0.0-20240608213934-9d245a204c42/go.mod h1:bscBzOUx0tJAdVGmQvcnoWPg5eI2eJ6anJKV1ueZ1oU= +github.com/Telmate/proxmox-api-go v0.0.0-20240616083239-78e131aa830b h1:uLi3eYDGVbTpowmiibl3elx/7kDONZopypHpmffcPYQ= +github.com/Telmate/proxmox-api-go v0.0.0-20240616083239-78e131aa830b/go.mod h1:O6yNUi0hG9GQLMBgpikSvbnuek1OMweFtbac1sfGuUs= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= @@ -132,8 +132,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= diff --git a/proxmox/Internal/pxapi/dns/nameservers/nameservers.go b/proxmox/Internal/pxapi/dns/nameservers/nameservers.go new file mode 100644 index 00000000..a23d9876 --- /dev/null +++ b/proxmox/Internal/pxapi/dns/nameservers/nameservers.go @@ -0,0 +1,41 @@ +package nameservers + +import ( + "net/netip" + "strings" +) + +func Split(rawNameServers string) *[]netip.Addr { + nameServers := make([]netip.Addr, 0) + if rawNameServers == "" { + return &nameServers + } + nameServerArrays := strings.Split(rawNameServers, " ") + for _, nameServer := range nameServerArrays { + nameServerSubArrays := strings.Split(nameServer, ",") + if len(nameServerSubArrays) > 1 { + tmpNameServers := make([]netip.Addr, len(nameServerSubArrays)) + for i, e := range nameServerSubArrays { + tmpNameServers[i], _ = netip.ParseAddr(e) + } + nameServers = append(nameServers, tmpNameServers...) + } else { + tmpNameServer, _ := netip.ParseAddr(nameServer) + nameServers = append(nameServers, tmpNameServer) + } + } + return &nameServers +} + +func String(nameServers *[]netip.Addr) string { + if nameServers != nil { + var rawNameServers string + for _, nameServer := range *nameServers { + rawNameServers += " " + nameServer.String() + } + if rawNameServers != "" { + return rawNameServers[1:] + } + } + return "" +} diff --git a/proxmox/Internal/pxapi/guest/sshkeys/sshkeys.go b/proxmox/Internal/pxapi/guest/sshkeys/sshkeys.go new file mode 100644 index 00000000..5ee08df7 --- /dev/null +++ b/proxmox/Internal/pxapi/guest/sshkeys/sshkeys.go @@ -0,0 +1,35 @@ +package sshkeys + +import ( + "crypto" + "regexp" + "strings" +) + +var regexMultipleSpaces = regexp.MustCompile(`\s+`) + +func Split(rawKeys string) *[]crypto.PublicKey { + tmpKeys := strings.Split(rawKeys, "\n") + keys := make([]crypto.PublicKey, len(tmpKeys)) + for i, e := range tmpKeys { + keys[i] = crypto.PublicKey(e) + } + return &keys +} + +func String(keys *[]crypto.PublicKey) string { + if keys != nil { + var rawKeys string + for _, key := range *keys { + rawKeys += "\n" + key.(string) + } + if rawKeys != "" { + return rawKeys[1:] + } + } + return "" +} + +func Trim(rawKeys string) string { + return regexMultipleSpaces.ReplaceAllString(strings.TrimSpace(rawKeys), " ") +} diff --git a/proxmox/heper_qemu.go b/proxmox/heper_qemu.go index e46953ac..16459d55 100644 --- a/proxmox/heper_qemu.go +++ b/proxmox/heper_qemu.go @@ -14,33 +14,23 @@ const ( errorGuestAgentNoIPv6Summary string = "Qemu Guest Agent is enabled but no IPv6 address is found" ) -func parseCloudInitInterface(ipConfig string, skipIPv4, skipIPv6 bool) (conn connectionInfo) { +func parseCloudInitInterface(ipConfig pxapi.CloudInitNetworkConfig, ciCustom, skipIPv4, skipIPv6 bool) (conn connectionInfo) { conn.SkipIPv4 = skipIPv4 conn.SkipIPv6 = skipIPv6 - var IPv4Set, IPv6Set bool - for _, e := range strings.Split(ipConfig, ",") { - if len(e) < 4 { - continue + if ipConfig.IPv4 != nil { + if ipConfig.IPv4.Address != nil { + splitCIDR := strings.Split(string(*ipConfig.IPv4.Address), "/") + conn.IPs.IPv4 = splitCIDR[0] } - if e[:3] == "ip=" { - IPv4Set = true - splitCIDR := strings.Split(e[3:], "/") - if len(splitCIDR) == 2 { - conn.IPs.IPv4 = splitCIDR[0] - } - } - if e[:4] == "ip6=" { - IPv6Set = true - splitCIDR := strings.Split(e[4:], "/") - if len(splitCIDR) == 2 { - conn.IPs.IPv6 = splitCIDR[0] - } - } - } - if !IPv4Set && conn.IPs.IPv4 == "" { + } else if !ciCustom { conn.SkipIPv4 = true } - if !IPv6Set && conn.IPs.IPv6 == "" { + if ipConfig.IPv6 != nil { + if ipConfig.IPv6.Address != nil { + splitCIDR := strings.Split(string(*ipConfig.IPv6.Address), "/") + conn.IPs.IPv6 = splitCIDR[0] + } + } else if !ciCustom { conn.SkipIPv6 = true } return diff --git a/proxmox/heper_qemu_test.go b/proxmox/heper_qemu_test.go index e7360315..79119cb9 100644 --- a/proxmox/heper_qemu_test.go +++ b/proxmox/heper_qemu_test.go @@ -151,7 +151,8 @@ func Test_HasRequiredIP(t *testing.T) { func Test_ParseCloudInitInterface(t *testing.T) { type testInput struct { - ci string + ci pxapi.CloudInitNetworkConfig + ciCustom bool skipIPv4 bool skipIPv6 bool } @@ -161,60 +162,138 @@ func Test_ParseCloudInitInterface(t *testing.T) { output connectionInfo }{ {name: `IPv4=DHCP`, - input: testInput{ci: "ip=dhcp"}, - output: connectionInfo{SkipIPv6: true}}, + input: testInput{ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + DHCP: true}}}, + output: connectionInfo{ + SkipIPv6: true}}, + {name: `IPv4=DHCP ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + DHCP: true}}, + ciCustom: true}}, {name: `IPv4=DHCP SkipIPv4`, input: testInput{ - ci: "ip=dhcp", + ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + DHCP: true}}, skipIPv4: true}, output: connectionInfo{ SkipIPv4: true, SkipIPv6: true}}, + {name: `IPv4=DHCP SkipIPv4 ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + DHCP: true}}, + ciCustom: true, + skipIPv4: true}, + output: connectionInfo{SkipIPv4: true}}, {name: `IPv4=Static`, - input: testInput{ci: "ip=192.168.1.1/24"}, + input: testInput{ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + Address: pointer(pxapi.IPv4CIDR("192.168.1.1/24"))}}}, output: connectionInfo{IPs: primaryIPs{ IPv4: "192.168.1.1"}, SkipIPv6: true}}, + {name: `IPv4=Static ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + Address: pointer(pxapi.IPv4CIDR("192.168.1.1/24"))}}, + ciCustom: true}, + output: connectionInfo{IPs: primaryIPs{IPv4: "192.168.1.1"}}}, {name: `IPv4=Static IPv6=Static`, - input: testInput{ci: "ip=192.168.1.1/24,ip6=2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"}, + input: testInput{ci: pxapi.CloudInitNetworkConfig{ + IPv4: &pxapi.CloudInitIPv4Config{ + Address: pointer(pxapi.IPv4CIDR("192.168.1.1/24"))}, + IPv6: &pxapi.CloudInitIPv6Config{ + Address: pointer(pxapi.IPv6CIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"))}}}, + output: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1", + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}}}, + {name: `IPv4=Static IPv6=Static ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{ + IPv4: &pxapi.CloudInitIPv4Config{ + Address: pointer(pxapi.IPv4CIDR("192.168.1.1/24"))}, + IPv6: &pxapi.CloudInitIPv6Config{ + Address: pointer(pxapi.IPv6CIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"))}}, + ciCustom: true}, output: connectionInfo{IPs: primaryIPs{ IPv4: "192.168.1.1", IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}}}, {name: `IPv4=Static SkipIPv4`, input: testInput{ - ci: "ip=192.168.1.1/24", + ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + Address: pointer(pxapi.IPv4CIDR("192.168.1.1/24"))}}, skipIPv4: true}, output: connectionInfo{IPs: primaryIPs{ IPv4: "192.168.1.1"}, SkipIPv4: true, SkipIPv6: true}}, + {name: `IPv4=Static SkipIPv4 ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv4: &pxapi.CloudInitIPv4Config{ + Address: pointer(pxapi.IPv4CIDR("192.168.1.1/24"))}}, + ciCustom: true, + skipIPv4: true}, + output: connectionInfo{IPs: primaryIPs{ + IPv4: "192.168.1.1"}, + SkipIPv4: true}}, {name: `IPv6=DHCP`, - input: testInput{ci: "ip6=dhcp"}, + input: testInput{ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + DHCP: true}}}, output: connectionInfo{SkipIPv4: true}}, + {name: `IPv6=DHCP ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + DHCP: true}}, + ciCustom: true}}, {name: `IPv6=DHCP SkipIPv6`, input: testInput{ - ci: "ip6=dhcp", + ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + DHCP: true}}, skipIPv6: true}, output: connectionInfo{ SkipIPv4: true, SkipIPv6: true}}, + {name: `IPv6=DHCP SkipIPv6 ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + DHCP: true}}, + ciCustom: true, + skipIPv6: true}, + output: connectionInfo{SkipIPv6: true}}, {name: `IPv6=Static`, - input: testInput{ci: "ip6=2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"}, + input: testInput{ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + Address: pointer(pxapi.IPv6CIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"))}}}, output: connectionInfo{IPs: primaryIPs{ IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, SkipIPv4: true}}, + {name: `IPv6=Static ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + Address: pointer(pxapi.IPv6CIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"))}}, + ciCustom: true}, + output: connectionInfo{IPs: primaryIPs{IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}}}, {name: `IPv6=Static SkipIPv6`, input: testInput{ - ci: "ip6=2001:0db8:85a3:0000:0000:8a2e:0370:7334/64", + ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + Address: pointer(pxapi.IPv6CIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"))}}, skipIPv6: true}, output: connectionInfo{IPs: primaryIPs{ IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, SkipIPv4: true, SkipIPv6: true}}, + {name: `IPv6=Static SkipIPv6 ciCustom`, + input: testInput{ + ci: pxapi.CloudInitNetworkConfig{IPv6: &pxapi.CloudInitIPv6Config{ + Address: pointer(pxapi.IPv6CIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"))}}, + ciCustom: true, + skipIPv6: true}, + output: connectionInfo{IPs: primaryIPs{ + IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}, + SkipIPv6: true}}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - require.Equal(t, test.output, parseCloudInitInterface(test.input.ci, test.input.skipIPv4, test.input.skipIPv6)) + require.Equal(t, test.output, parseCloudInitInterface(test.input.ci, test.input.ciCustom, test.input.skipIPv4, test.input.skipIPv6)) }) } } diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go index 83e1250f..6a5cb4cc 100755 --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -15,6 +15,8 @@ import ( "time" pxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/Telmate/terraform-provider-proxmox/v2/proxmox/Internal/pxapi/dns/nameservers" + "github.com/Telmate/terraform-provider-proxmox/v2/proxmox/Internal/pxapi/guest/sshkeys" "github.com/Telmate/terraform-provider-proxmox/v2/proxmox/Internal/pxapi/guest/tags" "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" @@ -671,6 +673,11 @@ func resourceVmQemu() *schema.Resource { return strings.TrimSpace(old) == strings.TrimSpace(new) }, }, + "ciupgrade": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, "ciuser": { Type: schema.TypeString, Optional: true, @@ -695,18 +702,16 @@ func resourceVmQemu() *schema.Resource { "searchdomain": { Type: schema.TypeString, Optional: true, - Computed: true, // could be pre-existing if we clone from a template with it defined }, "nameserver": { Type: schema.TypeString, Optional: true, - Computed: true, // could be pre-existing if we clone from a template with it defined }, "sshkeys": { Type: schema.TypeString, Optional: true, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return strings.TrimSpace(old) == strings.TrimSpace(new) + return sshkeys.Trim(old) == sshkeys.Trim(new) }, }, "ipconfig0": { @@ -902,21 +907,7 @@ func resourceVmQemuCreate(ctx context.Context, d *schema.ResourceData, meta inte QemuPCIDevices: qemuPCIDevices, QemuUsbs: qemuUsbs, Smbios1: BuildSmbiosArgs(d.Get("smbios").([]interface{})), - // Cloud-init. - CIuser: d.Get("ciuser").(string), - CIpassword: d.Get("cipassword").(string), - CIcustom: d.Get("cicustom").(string), - Searchdomain: d.Get("searchdomain").(string), - Nameserver: d.Get("nameserver").(string), - Sshkeys: d.Get("sshkeys").(string), - Ipconfig: pxapi.IpconfigMap{}, - } - // Populate Ipconfig map - for i := 0; i < 16; i++ { - iface := fmt.Sprintf("ipconfig%d", i) - if v, ok := d.GetOk(iface); ok { - config.Ipconfig[i] = v.(string) - } + CloudInit: mapToSDK_CloudInit(d), } config.Disks = mapToStruct_QemuStorages(d) @@ -1178,31 +1169,7 @@ func resourceVmQemuUpdate(ctx context.Context, d *schema.ResourceData, meta inte QemuPCIDevices: qemuPCIDevices, QemuUsbs: qemuUsbs, Smbios1: BuildSmbiosArgs(d.Get("smbios").([]interface{})), - // Cloud-init. - CIuser: d.Get("ciuser").(string), - CIpassword: d.Get("cipassword").(string), - CIcustom: d.Get("cicustom").(string), - Searchdomain: d.Get("searchdomain").(string), - Nameserver: d.Get("nameserver").(string), - Sshkeys: d.Get("sshkeys").(string), - Ipconfig: pxapi.IpconfigMap{ - 0: d.Get("ipconfig0").(string), - 1: d.Get("ipconfig1").(string), - 2: d.Get("ipconfig2").(string), - 3: d.Get("ipconfig3").(string), - 4: d.Get("ipconfig4").(string), - 5: d.Get("ipconfig5").(string), - 6: d.Get("ipconfig6").(string), - 7: d.Get("ipconfig7").(string), - 8: d.Get("ipconfig8").(string), - 9: d.Get("ipconfig9").(string), - 10: d.Get("ipconfig10").(string), - 11: d.Get("ipconfig11").(string), - 12: d.Get("ipconfig12").(string), - 13: d.Get("ipconfig13").(string), - 14: d.Get("ipconfig14").(string), - 15: d.Get("ipconfig15").(string), - }, + CloudInit: mapToSDK_CloudInit(d), } if len(qemuVgaList) > 0 { config.QemuVga = qemuVgaList[0].(map[string]interface{}) @@ -1437,7 +1404,7 @@ func resourceVmQemuRead(ctx context.Context, d *schema.ResourceData, meta interf } if err == nil && vmState["status"] == "running" { log.Printf("[DEBUG] VM is running, checking the IP") - diags = append(diags, initConnInfo(ctx, d, pconf, client, vmr, config, lock)...) + diags = append(diags, initConnInfo(d, client, vmr, config)...) } else { // Optional convenience attributes for provisioners err = d.Set("default_ipv4_address", nil) @@ -1479,36 +1446,11 @@ func resourceVmQemuRead(ctx context.Context, d *schema.ResourceData, meta interf d.Set("qemu_os", config.QemuOs) d.Set("tags", tags.String(config.Tags)) d.Set("args", config.Args) - // Cloud-init. - d.Set("ciuser", config.CIuser) - // we purposely use the password from the terraform config here - // because the proxmox api will always return "**********" leading to diff issues - d.Set("cipassword", d.Get("cipassword").(string)) - d.Set("cicustom", config.CIcustom) - d.Set("searchdomain", config.Searchdomain) - d.Set("nameserver", config.Nameserver) - d.Set("sshkeys", config.Sshkeys) - d.Set("ipconfig0", config.Ipconfig[0]) - d.Set("ipconfig1", config.Ipconfig[1]) - d.Set("ipconfig2", config.Ipconfig[2]) - d.Set("ipconfig3", config.Ipconfig[3]) - d.Set("ipconfig4", config.Ipconfig[4]) - d.Set("ipconfig5", config.Ipconfig[5]) - d.Set("ipconfig6", config.Ipconfig[6]) - d.Set("ipconfig7", config.Ipconfig[7]) - d.Set("ipconfig8", config.Ipconfig[8]) - d.Set("ipconfig9", config.Ipconfig[9]) - d.Set("ipconfig10", config.Ipconfig[10]) - d.Set("ipconfig11", config.Ipconfig[11]) - d.Set("ipconfig12", config.Ipconfig[12]) - d.Set("ipconfig13", config.Ipconfig[13]) - d.Set("ipconfig14", config.Ipconfig[14]) - d.Set("ipconfig15", config.Ipconfig[15]) - d.Set("smbios", ReadSmbiosArgs(config.Smbios1)) d.Set("linked_vmid", config.LinkedVmId) d.Set("disks", mapFromStruct_ConfigQemu(config.Disks)) mapFromStruct_QemuGuestAgent(d, config.Agent) + mapToTerraform_CloudInit(config.CloudInit, d) // Some dirty hacks to populate undefined keys with default values. checkedKeys := []string{"force_create", "define_connection_info"} @@ -1612,7 +1554,13 @@ func resourceVmQemuDelete(ctx context.Context, d *schema.ResourceData, meta inte return diag.FromErr(err) } if vmState["status"] != "stopped" { - if _, err := client.StopVm(vmr); err != nil { + var err error + if d.Get("agent") == 1 { + _, err = client.ShutdownVm(vmr) + } else { + _, err = client.StopVm(vmr) + } + if err != nil { return diag.FromErr(err) } @@ -1824,13 +1772,11 @@ func UpdateDevicesSet( return devicesSet } -func initConnInfo(ctx context.Context, +func initConnInfo( d *schema.ResourceData, - pconf *providerConfiguration, client *pxapi.Client, vmr *pxapi.VmRef, config *pxapi.ConfigQemu, - lock *pmApiLockHolder, ) diag.Diagnostics { logger, _ := CreateSubLogger("initConnInfo") var diags diag.Diagnostics @@ -1866,7 +1812,7 @@ func initConnInfo(ctx context.Context, 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) - IPs, agentDiags := getPrimaryIP(config, vmr, client, d, guestAgentWaitEnd, d.Get("additional_wait").(int), d.Get("agent_timeout").(int), ciAgentEnabled, d.Get("skip_ipv4").(bool), d.Get("skip_ipv6").(bool)) + IPs, agentDiags := getPrimaryIP(config, vmr, client, guestAgentWaitEnd, d.Get("additional_wait").(int), d.Get("agent_timeout").(int), ciAgentEnabled, d.Get("skip_ipv4").(bool), d.Get("skip_ipv6").(bool)) if len(agentDiags) > 0 { diags = append(diags, agentDiags...) } @@ -1897,7 +1843,7 @@ func initConnInfo(ctx context.Context, return diags } -func getPrimaryIP(config *pxapi.ConfigQemu, vmr *pxapi.VmRef, client *pxapi.Client, d *schema.ResourceData, endTime time.Time, additionalWait, agentTimeout int, ciAgentEnabled, skipIPv4 bool, skipIPv6 bool) (primaryIPs, diag.Diagnostics) { +func getPrimaryIP(config *pxapi.ConfigQemu, vmr *pxapi.VmRef, client *pxapi.Client, endTime time.Time, additionalWait, agentTimeout int, ciAgentEnabled, skipIPv4 bool, skipIPv6 bool) (primaryIPs, diag.Diagnostics) { logger, _ := CreateSubLogger("getPrimaryIP") // TODO allow the primary interface to be a different one than the first @@ -1906,11 +1852,14 @@ func getPrimaryIP(config *pxapi.ConfigQemu, vmr *pxapi.VmRef, client *pxapi.Clie SkipIPv6: skipIPv6, } // check if cloud init is enabled - if config.HasCloudInit() { + if config.CloudInit != nil { log.Print("[INFO][getPrimaryIP] vm has a cloud-init configuration") logger.Debug().Int("vmid", vmr.VmId()).Msgf(" vm has a cloud-init configuration") - CiInterface := d.Get("ipconfig0") - conn = parseCloudInitInterface(CiInterface.(string), conn.SkipIPv4, conn.SkipIPv6) + var cicustom bool + if config.CloudInit.Custom != nil && config.CloudInit.Custom.Network != nil { + cicustom = true + } + conn = parseCloudInitInterface(config.CloudInit.NetworkInterfaces[pxapi.QemuNetworkInterfaceID0], cicustom, conn.SkipIPv4, conn.SkipIPv6) // early return, we have all information we wanted if conn.hasRequiredIP() { if conn.IPs.IPv4 == "" && conn.IPs.IPv6 == "" { @@ -1992,6 +1941,48 @@ func mapFromStruct_ConfigQemu(config *pxapi.QemuStorages) []interface{} { } } +func mapToTerraform_CloudInit(config *pxapi.CloudInit, d *schema.ResourceData) { + if config == nil { + return + } + // we purposely use the password from the terraform config here + // because the proxmox api will always return "**********" leading to diff issues + d.Set("cipassword", d.Get("cipassword").(string)) + + d.Set("ciuser", config.Username) + if config.Custom != nil { + d.Set("cicustom", config.Custom.String()) + } + if config.DNS != nil { + d.Set("searchdomain", config.DNS.SearchDomain) + d.Set("nameserver", nameservers.String(config.DNS.NameServers)) + } + for i := pxapi.QemuNetworkInterfaceID(0); i < 16; i++ { + if v, isSet := config.NetworkInterfaces[i]; isSet { + d.Set("ipconfig"+strconv.Itoa(int(i)), mapToTerraform_CloudInitNetworkConfig(v)) + } + } + d.Set("sshkeys", sshkeys.String(config.PublicSSHkeys)) + if config.UpgradePackages != nil { + d.Set("ciupgrade", *config.UpgradePackages) + } +} + +func mapToTerraform_CloudInitNetworkConfig(config pxapi.CloudInitNetworkConfig) string { + if config.IPv4 != nil { + if config.IPv6 != nil { + return config.IPv4.String() + "," + config.IPv6.String() + } else { + return config.IPv4.String() + } + } else { + if config.IPv6 != nil { + return config.IPv6.String() + } + } + return "" +} + func mapFormStruct_IsoFile(config *pxapi.IsoFile) string { if config == nil { return "" @@ -2376,6 +2367,90 @@ func mapToStruct_IsoFile(iso string) *pxapi.IsoFile { return &pxapi.IsoFile{File: file, Storage: storage} } +func mapToSDK_CloudInit(d *schema.ResourceData) *pxapi.CloudInit { + ci := pxapi.CloudInit{ + Custom: &pxapi.CloudInitCustom{ + Meta: &pxapi.CloudInitSnippet{}, + Network: &pxapi.CloudInitSnippet{}, + User: &pxapi.CloudInitSnippet{}, + Vendor: &pxapi.CloudInitSnippet{}, + }, + DNS: &pxapi.GuestDNS{ + SearchDomain: pointer(d.Get("searchdomain").(string)), + NameServers: nameservers.Split(d.Get("nameserver").(string)), + }, + NetworkInterfaces: pxapi.CloudInitNetworkInterfaces{}, + PublicSSHkeys: sshkeys.Split(d.Get("sshkeys").(string)), + UpgradePackages: pointer(d.Get("ciupgrade").(bool)), + UserPassword: pointer(d.Get("cipassword").(string)), + Username: pointer(d.Get("ciuser").(string)), + } + params := splitStringOfSettings(d.Get("cicustom").(string)) + if v, isSet := params["meta"]; isSet { + ci.Custom.Meta = mapToSDK_CloudInitSnippet(v) + } + if v, isSet := params["network"]; isSet { + ci.Custom.Network = mapToSDK_CloudInitSnippet(v) + } + if v, isSet := params["user"]; isSet { + ci.Custom.User = mapToSDK_CloudInitSnippet(v) + } + if v, isSet := params["vendor"]; isSet { + ci.Custom.Vendor = mapToSDK_CloudInitSnippet(v) + } + for i := 0; i < 16; i++ { + ci.NetworkInterfaces[pxapi.QemuNetworkInterfaceID(i)] = mapToSDK_CloudInitNetworkConfig(d.Get("ipconfig" + strconv.Itoa(i)).(string)) + } + return &ci +} + +func mapToSDK_CloudInitNetworkConfig(param string) pxapi.CloudInitNetworkConfig { + config := pxapi.CloudInitNetworkConfig{ + IPv4: &pxapi.CloudInitIPv4Config{ + Address: pointer(pxapi.IPv4CIDR("")), + DHCP: false, + Gateway: pointer(pxapi.IPv4Address(""))}, + IPv6: &pxapi.CloudInitIPv6Config{ + Address: pointer(pxapi.IPv6CIDR("")), + DHCP: false, + Gateway: pointer(pxapi.IPv6Address("")), + SLAAC: false}} + params := splitStringOfSettings(param) + if v, isSet := params["ip"]; isSet { + if v == "dhcp" { + config.IPv4.DHCP = true + } else { + *config.IPv4.Address = pxapi.IPv4CIDR(v) + } + } + if v, isSet := params["gw"]; isSet { + *config.IPv4.Gateway = pxapi.IPv4Address(v) + } + if v, isSet := params["ip6"]; isSet { + if v == "dhcp" { + config.IPv6.DHCP = true + } else if v == "auto" { + config.IPv6.SLAAC = true + } else { + *config.IPv6.Address = pxapi.IPv6CIDR(v) + } + } + if v, isSet := params["gw6"]; isSet { + *config.IPv6.Gateway = pxapi.IPv6Address(v) + } + return config +} + +func mapToSDK_CloudInitSnippet(param string) *pxapi.CloudInitSnippet { + file := strings.SplitN(param, ":", 2) + if len(file) == 2 { + return &pxapi.CloudInitSnippet{ + Storage: file[0], + FilePath: pxapi.CloudInitSnippetPath(file[1])} + } + return nil +} + func mapToStruct_QemuCdRom(schema map[string]interface{}) (cdRom *pxapi.QemuCdRom) { schemaItem, ok := schema["cdrom"].([]interface{}) if !ok { diff --git a/proxmox/util.go b/proxmox/util.go index fc191092..08d13ef4 100644 --- a/proxmox/util.go +++ b/proxmox/util.go @@ -7,6 +7,7 @@ import ( "os" "regexp" "strconv" + "strings" "testing" "time" @@ -516,3 +517,17 @@ func ByteCountIEC(b int64) string { return fmt.Sprintf("%0.f%c", float64(b)/float64(div), "KMGTPE"[exp]) } + +func splitStringOfSettings(settings string) map[string]string { + settingValuePairs := strings.Split(settings, ",") + settingMap := map[string]string{} + for _, e := range settingValuePairs { + keyValuePair := strings.SplitN(e, "=", 2) + var value string + if len(keyValuePair) == 2 { + value = keyValuePair[1] + } + settingMap[keyValuePair[0]] = value + } + return settingMap +} diff --git a/proxmox/util_test.go b/proxmox/util_test.go new file mode 100644 index 00000000..2f2e1a60 --- /dev/null +++ b/proxmox/util_test.go @@ -0,0 +1,28 @@ +package proxmox + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_splitStringOfSettings(t *testing.T) { + testData := []struct { + Input string + Output map[string]string + }{ + { + Input: "setting=a,thing=b,randomString,doubleTest=value=equals,object=test", + Output: map[string]string{ + "setting": "a", + "thing": "b", + "randomString": "", + "doubleTest": "value=equals", + "object": "test", + }, + }, + } + for _, e := range testData { + require.Equal(t, e.Output, splitStringOfSettings(e.Input)) + } +}