Skip to content

Commit

Permalink
Merge branch 'master' into qemu-tags
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinyblargon committed May 17, 2024
2 parents 7c7e09a + 3e88998 commit fae6ea0
Show file tree
Hide file tree
Showing 5 changed files with 460 additions and 154 deletions.
8 changes: 5 additions & 3 deletions docs/resources/vm_qemu.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,11 @@ The following arguments are supported in the top level resource block.
| `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. |
| `ipconfig0` | `str` | | The first IP address to assign to the guest. Format: `[gw=<GatewayIPv4>] [,gw6=<GatewayIPv6>] [,ip=<IPv4Format/CIDR>] [,ip6=<IPv6Format/CIDR>]`. |
| `ipconfig0` | `str` | `''` | The first IP address to assign to the guest. Format: `[gw=<GatewayIPv4>] [,gw6=<GatewayIPv6>] [,ip=<IPv4Format/CIDR>] [,ip6=<IPv6Format/CIDR>]`. When `os_type` is `cloud-init` not setting `ip=` is equivalent to `skip_ipv4` == `true` and `ip6=` to `skip_ipv4` == `true` .|
| `ipconfig1` to `ipconfig15` | `str` | | The second IP address to assign to the guest. Same format as `ipconfig0`. |
| `automatic_reboot` | `bool` | `true` | Automatically reboot the VM when parameter changes require this. If disabled the provider will emit a warning when the VM needs to be rebooted. |

| `skip_ipv4` | `bool` | `false` | Tells proxmox that acquiring an IPv4 address from the qemu guest agent isn't required, it will still return an ipv4 address if it could obtain one. Useful for reducing retries in environments without ipv4.|
| `skip_ipv6` | `bool` | `false` | Tells proxmox that acquiring an IPv6 address from the qemu guest agent isn't required, it will still return an ipv6 address if it could obtain one. Useful for reducing retries in environments without ipv6.|

### VGA Block

Expand Down Expand Up @@ -484,7 +485,8 @@ 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. The settings `ipconfig0` and `skip_ipv4` have influence on this.|
| `default_ipv6_address` | `str` | Read-only attribute. Only applies when `agent` is `1` and Proxmox can actually read the ip the vm has. The settings `ipconfig0` and `skip_ipv6` have influence on this.|

## Import

Expand Down
83 changes: 83 additions & 0 deletions proxmox/heper_qemu.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package proxmox

import (
"strings"

pxapi "github.com/Telmate/proxmox-api-go/proxmox"
)

const (
ErrorGuestAgentNotRunning string = "500 QEMU guest agent is not running"
)

func parseCloudInitInterface(ipConfig string, 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 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 == "" {
conn.SkipIPv4 = true
}
if !IPv6Set && conn.IPs.IPv6 == "" {
conn.SkipIPv6 = true
}
return
}

type primaryIPs struct {
IPv4 string
IPv6 string
}

type connectionInfo struct {
IPs primaryIPs
SkipIPv4 bool
SkipIPv6 bool
}

func (conn connectionInfo) hasRequiredIP() bool {
if conn.IPs.IPv4 == "" && !conn.SkipIPv4 || conn.IPs.IPv6 == "" && !conn.SkipIPv6 {
return false
}
return true
}

func (conn connectionInfo) parsePrimaryIPs(interfaces []pxapi.AgentNetworkInterface, mac string) connectionInfo {
lowerCaseMac := strings.ToLower(mac)
for _, iFace := range interfaces {
if iFace.MacAddress.String() == lowerCaseMac {
for _, addr := range iFace.IpAddresses {
if addr.IsGlobalUnicast() {
if addr.To4() != nil {
if conn.IPs.IPv4 == "" {
conn.IPs.IPv4 = addr.String()
}
} else {
if conn.IPs.IPv6 == "" {
conn.IPs.IPv6 = addr.String()
}
}
}
}
}
}
return conn
}
244 changes: 244 additions & 0 deletions proxmox/heper_qemu_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package proxmox

import (
"net"
"testing"

pxapi "github.com/Telmate/proxmox-api-go/proxmox"
"github.com/stretchr/testify/require"
)

func Test_HasRequiredIP(t *testing.T) {
tests := []struct {
name string
input connectionInfo
output bool
}{
{name: `IPv4`,
input: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1"}},
output: false},
{name: `IPv4 SkipIPv4`,
input: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1"},
SkipIPv4: true},
output: false},
{name: `SkipIPv4`,
input: connectionInfo{},
output: false},
{name: `IPv6`,
input: connectionInfo{IPs: primaryIPs{
IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}},
output: false},
{name: `IPv6 SkipIPv6`,
input: connectionInfo{IPs: primaryIPs{
IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
SkipIPv6: true},
output: false},
{name: `SkipIPv6`,
input: connectionInfo{},
output: false},
{name: `IPv4 IPv6`,
input: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1",
IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"}},
output: true},
{name: `IPv4 IPv6 SkipIPv4`,
input: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1",
IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
SkipIPv4: true},
output: true},
{name: `IPv4 IPv6 SkipIPv6`,
input: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1",
IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
SkipIPv6: true},
output: true},
{name: `IPv4 IPv6 SkipIPv4 SkipIPv6`,
input: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1",
IPv6: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
SkipIPv4: true,
SkipIPv6: true},
output: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.output, test.input.hasRequiredIP())
})
}
}

func Test_ParseCloudInitInterface(t *testing.T) {
type testInput struct {
ci string
skipIPv4 bool
skipIPv6 bool
}
tests := []struct {
name string
input testInput
output connectionInfo
}{
{name: `IPv4=DHCP`,
input: testInput{ci: "ip=dhcp"},
output: connectionInfo{SkipIPv6: true}},
{name: `IPv4=DHCP SkipIPv4`,
input: testInput{
ci: "ip=dhcp",
skipIPv4: true},
output: connectionInfo{
SkipIPv4: true,
SkipIPv6: true}},
{name: `IPv4=Static`,
input: testInput{ci: "ip=192.168.1.1/24"},
output: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1"},
SkipIPv6: true}},
{name: `IPv4=Static IPv6=Static`,
input: testInput{ci: "ip=192.168.1.1/24,ip6=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 SkipIPv4`,
input: testInput{
ci: "ip=192.168.1.1/24",
skipIPv4: true},
output: connectionInfo{IPs: primaryIPs{
IPv4: "192.168.1.1"},
SkipIPv4: true,
SkipIPv6: true}},
{name: `IPv6=DHCP`,
input: testInput{ci: "ip6=dhcp"},
output: connectionInfo{SkipIPv4: true}},
{name: `IPv6=DHCP SkipIPv6`,
input: testInput{
ci: "ip6=dhcp",
skipIPv6: true},
output: connectionInfo{
SkipIPv4: true,
SkipIPv6: true}},
{name: `IPv6=Static`,
input: testInput{ci: "ip6=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 SkipIPv6`,
input: testInput{
ci: "ip6=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}},
}
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))
})
}
}

func Test_ParsePrimaryIPs(t *testing.T) {
parseMac := func(mac string) net.HardwareAddr {
realMac, _ := net.ParseMAC(mac)
return realMac
}
parseIP := func(ip string) net.IP {
realIP, _, _ := net.ParseCIDR(ip)
return realIP
}
formatIP := func(ip string) string {
return net.ParseIP(ip).String()
}
type testInput struct {
interfaces []pxapi.AgentNetworkInterface
mac string
conn connectionInfo
}
tests := []struct {
name string
input testInput
output connectionInfo
}{
{name: `Only Loopback`,
input: testInput{
mac: "9c:7a:1b:4f:3e:a2",
interfaces: []pxapi.AgentNetworkInterface{
{
MacAddress: parseMac("9C:7A:1B:4F:3E:A2"),
Name: "eth1",
IpAddresses: []net.IP{
parseIP("127.0.0.1/8"),
parseIP("::1/128")}}}}},
{name: `Only IPv4`,
input: testInput{
mac: "3A:7E:9D:1F:5B:8C",
interfaces: []pxapi.AgentNetworkInterface{
{MacAddress: parseMac("3A:7E:9D:1F:5B:8C"),
Name: "eth1",
IpAddresses: []net.IP{
parseIP("127.0.0.1/8"),
parseIP("192.168.1.1/24"),
parseIP("::1/128")}}}},
output: connectionInfo{IPs: primaryIPs{IPv4: formatIP("192.168.1.1")}}},
{name: `Only IPv6`,
input: testInput{
mac: "6F:2C:4A:8E:7D:1B",
interfaces: []pxapi.AgentNetworkInterface{
{MacAddress: parseMac("6F:2C:4A:8E:7D:1B"),
Name: "eth1",
IpAddresses: []net.IP{
parseIP("127.0.0.1/8"),
parseIP("::1/128"),
parseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64")}}}},
output: connectionInfo{IPs: primaryIPs{IPv6: formatIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}}},
{name: `Full test`,
input: testInput{
mac: "3A:7E:9D:1F:5B:8C",
interfaces: []pxapi.AgentNetworkInterface{
{MacAddress: parseMac("6F:2C:4A:8E:7D:1B"),
Name: "lo",
IpAddresses: []net.IP{
parseIP("127.0.0.1/8"),
parseIP("::1/128")}},
{MacAddress: parseMac("9C:7A:1B:4F:3E:A2"),
Name: "eth0",
IpAddresses: []net.IP{
parseIP("192.168.1.1/24"),
parseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64")}},
{MacAddress: parseMac("3A:7E:9D:1F:5B:8C"),
Name: "wth1",
IpAddresses: []net.IP{
parseIP("10.10.10.1/16"),
parseIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf/64")}},
}},
output: connectionInfo{IPs: primaryIPs{
IPv4: formatIP("10.10.10.1"),
IPv6: formatIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf")},
},
},
{name: `IPv4 Already Set`,
input: testInput{
mac: "3A:7E:9D:1F:5B:8C",
interfaces: []pxapi.AgentNetworkInterface{
{MacAddress: parseMac("3A:7E:9D:1F:5B:8C"),
IpAddresses: []net.IP{parseIP("192.168.1.1/24")}}},
conn: connectionInfo{IPs: primaryIPs{IPv4: formatIP("10.10.1.1")}}},
output: connectionInfo{IPs: primaryIPs{IPv4: formatIP("10.10.1.1")}}},
{name: `IPv6 Already Set`,
input: testInput{
mac: "3A:7E:9D:1F:5B:8C",
interfaces: []pxapi.AgentNetworkInterface{
{MacAddress: parseMac("3A:7E:9D:1F:5B:8C"),
IpAddresses: []net.IP{parseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64")}}},
conn: connectionInfo{IPs: primaryIPs{IPv6: formatIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf")}}},
output: connectionInfo{IPs: primaryIPs{IPv6: formatIP("3ffe:1900:4545:3:200:f8ff:fe21:67cf")}}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.output, test.input.conn.parsePrimaryIPs(test.input.interfaces, test.input.mac))
})
}
}
Loading

0 comments on commit fae6ea0

Please sign in to comment.