Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Proxmox provider #169

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function.
* Triton [Config options](https://github.com/hashicorp/go-discover/blob/8b3ddf4/provider/triton/triton_discover.go#L17-L27)
* vSphere [Config options](https://github.com/hashicorp/go-discover/blob/8b3ddf4/provider/vsphere/vsphere_discover.go#L145-L157)
* Packet [Config options](https://github.com/hashicorp/go-discover/blob/8b3ddf4/provider/packet/packet_discover.go#L25-L40)
* Proxmox [Config options](https://github.com/hashicorp/go-discover/blob/master/provider/proxmox/proxmox_discover.go#L14-L23)

The following providers are implemented in the go-discover/provider subdirectory
but aren't automatically registered. If you want to support these providers,
Expand Down Expand Up @@ -91,6 +92,9 @@ provider=vsphere category_name=consul-role tag_name=consul-server host=... user=
# Packet
provider=packet auth_token=token project=uuid url=... address_type=...

# Proxmox
provider=proxmox api_host=... api_token_id=... api_token_secret=... api_skip_tls_verify=[skip|verify] pool_name=...

# Kubernetes
provider=k8s label_selector="app = consul-server"
```
Expand Down
2 changes: 2 additions & 0 deletions discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/hashicorp/go-discover/provider/mdns"
"github.com/hashicorp/go-discover/provider/os"
"github.com/hashicorp/go-discover/provider/packet"
"github.com/hashicorp/go-discover/provider/proxmox"
"github.com/hashicorp/go-discover/provider/scaleway"
"github.com/hashicorp/go-discover/provider/softlayer"
"github.com/hashicorp/go-discover/provider/tencentcloud"
Expand Down Expand Up @@ -59,6 +60,7 @@ var Providers = map[string]Provider{
"triton": &triton.Provider{},
"vsphere": &vsphere.Provider{},
"packet": &packet.Provider{},
"proxmox": &proxmox.Provider{},
}

// Discover looks up metadata in different cloud environments.
Expand Down
127 changes: 127 additions & 0 deletions provider/proxmox/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package proxmox

import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
)

// MakeRequest sends a GET request to the Proxmox API
func MakeRequest(args map[string]string, apiPath string) (*http.Response, error) {
apiBase := "/api2/json"
apiURL, err := url.Parse(args["api_host"] + apiBase + apiPath)
if err != nil {
return nil, err
}

// Allow skipping certificate since many Proxmox users use self-signed and untrusted certs
var transport *http.Transport = &http.Transport{}
if args["api_skip_tls_verify"] == "skip" {
transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}

httpClient := &http.Client{
Transport: transport,
}

req, err := http.NewRequest("GET", apiURL.String(), nil)
if err != nil {
return nil, err
}

req.Header.Add("Authorization", fmt.Sprintf("PVEAPIToken=%s=%s", args["api_token_id"], args["api_token_secret"]))

res, err := httpClient.Do(req)
if err != nil {
return nil, err
}

return res, nil
}

// Member represents the record of an entity in a Proxmox pool
type Member struct {
ID string `json:"id"`
Node string `json:"node"`
Name string `json:"name"`
Status string `json:"status"`
Type string `json:"type"`
VMID int `json:"vmid"`
}

type poolData struct {
Members []Member `json:"members"`
}

type nodesAPIResponse struct {
Data poolData `json:"data"`
}

// GetPoolMembers fetches the members of a pool from the Proxmox API
func GetPoolMembers(args map[string]string) ([]Member, error) {
res, err := MakeRequest(args, "/pools/"+args["pool_name"])
if err != nil {
return nil, err
}

var nodes = new(nodesAPIResponse)
jsonErr := json.NewDecoder(res.Body).Decode(&nodes)
if jsonErr != nil {
return nil, err
}

return nodes.Data.Members, nil
}

type ipAddresses struct {
IPAddress string `json:"ip-address"`
IPAddressType string `json:"ip-address-type"`
Prefix int `json:"prefix"`
}

type statistics struct {
RxBytes int `json:"rx-bytes"`
RxDropped int `json:"rx-dropped"`
RxErrs int `json:"rx-errs"`
RxPackets int `json:"rx-packets"`
TxBytes int `json:"tx-bytes"`
TxDropped int `json:"tx-dropped"`
TxErrs int `json:"tx-errs"`
TxPackets int `json:"tx-packets"`
}

// NetworkInterface represents a network interface fetched from the Proxmox API
type NetworkInterface struct {
HardwareAddress string `json:"hardware-address"`
IPAddresses []ipAddresses `json:"ip-addresses"`
Name string `json:"name"`
Statistics statistics `json:"statistics"`
}

type data struct {
Result []NetworkInterface `json:"result"`
}

type getNetworkInterfacesResponse struct {
Data data `json:"data"`
}

// GetNetworkInterfaces fetches the network interfaces of a specific VM from the Proxmox API
func GetNetworkInterfaces(args map[string]string, node string, vmID string) ([]NetworkInterface, error) {
res, err := MakeRequest(args, "/nodes/"+node+"/qemu/"+vmID+"/agent/network-get-interfaces")
if err != nil {
return nil, err
}

var interfaces = new(getNetworkInterfacesResponse)
jsonErr := json.NewDecoder(res.Body).Decode(&interfaces)
if jsonErr != nil {
return nil, err
}

return interfaces.Data.Result, nil
}
67 changes: 67 additions & 0 deletions provider/proxmox/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package proxmox_test

import (
"fmt"
"os"
"testing"

discover "github.com/hashicorp/go-discover"
"github.com/hashicorp/go-discover/provider/proxmox"
)

func TestGetPoolMembers(t *testing.T) {
args := discover.Config{
"provider": "proxmox",
"api_host": os.Getenv("PROXMOX_API_HOST"),
"api_token_id": os.Getenv("PROXMOX_API_ID"),
"api_token_secret": os.Getenv("PROXMOX_API_SECRET"),
"api_skip_tls_verify": "skip",
"pool_name": os.Getenv("PROXMOX_POOL_NAME"),
}

if args["api_host"] == "" || args["api_token_id"] == "" || args["api_token_secret"] == "" || args["pool_name"] == "" {
t.Skip("Proxmox credentials missing")
}

members, err := proxmox.GetPoolMembers(args)
if err != nil {
t.Fatalf("bad: %v", err)
}

// Assume the pool has at least one member
if len(members) <= 0 {
t.Fatal("Zero members retrieved")
}
}

func TestGetNetworkInterfaces(t *testing.T) {
args := discover.Config{
"provider": "proxmox",
"api_host": os.Getenv("PROXMOX_API_HOST"),
"api_token_id": os.Getenv("PROXMOX_API_ID"),
"api_token_secret": os.Getenv("PROXMOX_API_SECRET"),
"api_skip_tls_verify": "skip",
"pool_name": os.Getenv("PROXMOX_POOL_NAME"),
}

if args["api_host"] == "" || args["api_token_id"] == "" || args["api_token_secret"] == "" || args["pool_name"] == "" {
t.Skip("Proxmox credentials missing")
}

members, err := proxmox.GetPoolMembers(args)
if err != nil {
t.Fatalf("bad: %v", err)
}

for _, member := range members {
interfaces, err := proxmox.GetNetworkInterfaces(args, member.Node, fmt.Sprint(member.VMID))
if err != nil {
t.Fatalf("bad: %v", err)
}

// Assume each member has at least one network interface
if len(interfaces) <= 0 {
t.Fatal("Zero network interfaces retrieved")
}
}
}
102 changes: 102 additions & 0 deletions provider/proxmox/proxmox_discover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package proxmox

import (
"fmt"
"io/ioutil"
"log"
)

// Provider for Proxmox
type Provider struct{}

// Help message generator
func (p *Provider) Help() string {
return `Proxmox:

provider: "proxmox"
api_host: The address of the Proxmox node
api_token_id: The ID of the API token
api_token_secret: The secret of the API token
api_skip_tls_verify: "skip" or "verify". Defaults to "verify"
addr_type: "v4" or "v6". Defaults to "v4".
pool_name Pool to get VMs from
`
}

// Addrs function to retrieve IP addresses from Proxmox
func (p *Provider) Addrs(args map[string]string, l *log.Logger) ([]string, error) {
if args["provider"] != "proxmox" {
return nil, fmt.Errorf("discover-proxmox: invalid provider " + args["provider"])
}

if l == nil {
l = log.New(ioutil.Discard, "", 0)
}

if args["api_skip_tls_verify"] != "skip" && args["api_skip_tls_verify"] != "verify" {
l.Printf("[INFO] discover-proxmox: api_skip_tls_verify %s is not supported. Valid values are 'skip' or 'verify'. Falling back to 'verify'", args["api_skip_tls_verify"])
args["api_skip_tls_verify"] = "verify"
}

if args["addr_type"] != "v4" && args["addr_type"] != "v6" {
l.Printf("[INFO] discover-proxmox: addr_type %s is not supported. Valid values are 'v4' or 'v6'. Falling back to 'v4'", args["addr_type"])
args["addr_type"] = "v4"
}

// Get all the members of the pool
l.Printf("[DEBUG] discover-proxmox: retrieveing members of pool: %s", args["pool_name"])
members, err := GetPoolMembers(args)
l.Printf("[DEBUG] discover-proxmox: got %d members", len(members))
if err != nil {
return nil, fmt.Errorf("discover-proxmox: could not list pool members: %s", err)
}

// Get the network interfaces from just the members that at QEMU vm's
l.Print("[DEBUG] discover-proxmox: retrieveing network interfaces from members")
var interfaces []NetworkInterface
for _, member := range members {
if member.Type == "qemu" {
memberInterfaces, err := GetNetworkInterfaces(args, member.Node, fmt.Sprint(member.VMID))
if err != nil {
return nil, fmt.Errorf(
"discover-proxmox: could not get interfaces from pool member '%s' (ID: %d): %s",
member.Name,
member.VMID,
err,
)
}

// Add the first non loopback interface to the output list
for _, memberInterface := range memberInterfaces {
// Ignore loopback interfaces
if memberInterface.HardwareAddress == "00:00:00:00:00:00" {
continue
}

interfaces = append(interfaces, memberInterface)
break
}
}
}
l.Printf("[DEBUG] discover-proxmox: got %d network interfaces", len(interfaces))

// Collect the correct (ipv4 or ipv6) IP addresses from the interface
l.Printf("[DEBUG] discover-proxmox: filtering ip addresses by type (%s)", args["addr_type"])
var addresses []string
for _, netInterface := range interfaces {
for _, ipAddress := range netInterface.IPAddresses {
if args["addr_type"] == "v4" && ipAddress.IPAddressType == "ipv4" {
addresses = append(addresses, ipAddress.IPAddress)
break
}

if args["addr_type"] == "v6" && ipAddress.IPAddressType == "ipv6" {
addresses = append(addresses, ipAddress.IPAddress)
break
}
}
}
l.Printf("[DEBUG] discover-proxmox: got %d ip addresses", len(addresses))

return addresses, nil
}
Loading