Allow using API tokens for Proxmox authentication (#10797)

This commit is contained in:
Marcus Weiner 2021-03-22 11:48:31 +01:00 committed by GitHub
parent 0993c976fa
commit 4d9fb629c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 870 additions and 314 deletions

View File

@ -81,6 +81,7 @@ type FlatConfig struct {
SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
Token *string `mapstructure:"token" cty:"token" hcl:"token"`
Node *string `mapstructure:"node" cty:"node" hcl:"node"`
Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
@ -190,6 +191,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
"node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false},
"pool": &hcldec.AttrSpec{Name: "pool", Type: cty.String, Required: false},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},

View File

@ -2,7 +2,6 @@ package proxmox
import (
"context"
"crypto/tls"
"errors"
"fmt"
@ -35,15 +34,7 @@ type Builder struct {
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook, state multistep.StateBag) (packersdk.Artifact, error) {
var err error
tlsConfig := &tls.Config{
InsecureSkipVerify: b.config.SkipCertValidation,
}
b.proxmoxClient, err = proxmox.NewClient(b.config.proxmoxURL.String(), nil, tlsConfig)
if err != nil {
return nil, err
}
err = b.proxmoxClient.Login(b.config.Username, b.config.Password, "")
b.proxmoxClient, err = newProxmoxClient(b.config)
if err != nil {
return nil, err
}

View File

@ -0,0 +1,37 @@
package proxmox
import (
"crypto/tls"
"log"
"time"
"github.com/Telmate/proxmox-api-go/proxmox"
)
const defaultTaskTimeout = 30 * time.Second
func newProxmoxClient(config Config) (*proxmox.Client, error) {
tlsConfig := &tls.Config{
InsecureSkipVerify: config.SkipCertValidation,
}
client, err := proxmox.NewClient(config.proxmoxURL.String(), nil, tlsConfig, int(defaultTaskTimeout.Seconds()))
if err != nil {
return nil, err
}
if config.Token != "" {
// configure token auth
log.Print("using token auth")
client.SetAPIToken(config.Username, config.Token)
} else {
// fallback to login if not using tokens
log.Print("using password auth")
err = client.Login(config.Username, config.Password, "")
if err != nil {
return nil, err
}
}
return client, nil
}

View File

@ -0,0 +1,91 @@
package proxmox
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/stretchr/testify/require"
)
func TestTokenAuth(t *testing.T) {
mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
if req.Header.Get("Authorization") != "PVEAPIToken=dummy@vmhost!test-token=ac5293bf-15e2-477f-b04c-a6dfa7a46b80" {
rw.WriteHeader(http.StatusUnauthorized)
return
}
}))
defer mockAPI.Close()
pmURL, _ := url.Parse(mockAPI.URL)
config := Config{
proxmoxURL: pmURL,
SkipCertValidation: false,
Username: "dummy@vmhost!test-token",
Password: "not-used",
Token: "ac5293bf-15e2-477f-b04c-a6dfa7a46b80",
}
client, err := newProxmoxClient(config)
require.NoError(t, err)
ref := proxmox.NewVmRef(110)
ref.SetNode("node1")
ref.SetVmType("qemu")
err = client.Sendkey(ref, "ping")
require.NoError(t, err)
}
func TestLogin(t *testing.T) {
mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
// mock ticketing api
if req.Method == http.MethodPost && req.URL.Path == "/access/ticket" {
body, _ := ioutil.ReadAll(req.Body)
values, _ := url.ParseQuery(string(body))
user := values.Get("username")
pass := values.Get("password")
if user != "dummy@vmhost" || pass != "correct-horse-battery-staple" {
rw.WriteHeader(http.StatusUnauthorized)
return
}
_ = json.NewEncoder(rw).Encode(map[string]interface{}{
"data": map[string]string{
"username": user,
"ticket": "dummy-ticket",
"CSRFPreventionToken": "random-token",
},
})
return
}
// validate ticket
if val, err := req.Cookie("PVEAuthCookie"); err != nil || val.Value != "dummy-ticket" {
rw.WriteHeader(http.StatusUnauthorized)
return
}
}))
defer mockAPI.Close()
pmURL, _ := url.Parse(mockAPI.URL)
config := Config{
proxmoxURL: pmURL,
SkipCertValidation: false,
Username: "dummy@vmhost",
Password: "correct-horse-battery-staple",
Token: "",
}
client, err := newProxmoxClient(config)
require.NoError(t, err)
ref := proxmox.NewVmRef(110)
ref.SetNode("node1")
ref.SetVmType("qemu")
err = client.Sendkey(ref, "ping")
require.NoError(t, err)
}

View File

@ -35,6 +35,7 @@ type Config struct {
SkipCertValidation bool `mapstructure:"insecure_skip_tls_verify"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Token string `mapstructure:"token"`
Node string `mapstructure:"node"`
Pool string `mapstructure:"pool"`
@ -135,6 +136,9 @@ func (c *Config) Prepare(upper interface{}, raws ...interface{}) ([]string, []st
if c.Password == "" {
c.Password = os.Getenv("PROXMOX_PASSWORD")
}
if c.Token == "" {
c.Token = os.Getenv("PROXMOX_TOKEN")
}
if c.BootKeyInterval == 0 && os.Getenv(bootcommand.PackerKeyEnv) != "" {
var err error
c.BootKeyInterval, err = time.ParseDuration(os.Getenv(bootcommand.PackerKeyEnv))
@ -220,8 +224,8 @@ func (c *Config) Prepare(upper interface{}, raws ...interface{}) ([]string, []st
if c.Username == "" {
errs = packersdk.MultiErrorAppend(errs, errors.New("username must be specified"))
}
if c.Password == "" {
errs = packersdk.MultiErrorAppend(errs, errors.New("password must be specified"))
if c.Password == "" && c.Token == "" {
errs = packersdk.MultiErrorAppend(errs, errors.New("password or token must be specified"))
}
if c.ProxmoxURLRaw == "" {
errs = packersdk.MultiErrorAppend(errs, errors.New("proxmox_url must be specified"))

View File

@ -80,6 +80,7 @@ type FlatConfig struct {
SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
Token *string `mapstructure:"token" cty:"token" hcl:"token"`
Node *string `mapstructure:"node" cty:"node" hcl:"node"`
Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
@ -187,6 +188,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
"node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false},
"pool": &hcldec.AttrSpec{Name: "pool", Type: cty.String, Required: false},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},

View File

@ -81,6 +81,7 @@ type FlatConfig struct {
SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
Token *string `mapstructure:"token" cty:"token" hcl:"token"`
Node *string `mapstructure:"node" cty:"node" hcl:"node"`
Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
@ -196,6 +197,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
"node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false},
"pool": &hcldec.AttrSpec{Name: "pool", Type: cty.String, Required: false},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},

2
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/Azure/go-autorest/autorest/to v0.3.0
github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022
github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.1.0
github.com/Telmate/proxmox-api-go v0.0.0-20200715182505-ec97c70ba887
github.com/Telmate/proxmox-api-go v0.0.0-20210320143302-fea68269e6b0
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190418113227-25233c783f4e
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20170113022742-e6dbea820a9f
github.com/antihax/optional v1.0.0

3
go.sum
View File

@ -81,8 +81,9 @@ github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.1.0 h1:0nxjOH7NurPGUWNG5BCrASW
github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.1.0/go.mod h1:P+3VS0ETiQPyWOx3vB/oeC8J3qd7jnVZLYAFwWgGRt8=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/Telmate/proxmox-api-go v0.0.0-20200715182505-ec97c70ba887 h1:Q65o4V0g/KR1sSUZIMf4m1rShb7f1tVHuEt30hfnh2A=
github.com/Telmate/proxmox-api-go v0.0.0-20200715182505-ec97c70ba887/go.mod h1:OGWyIMJ87/k/GCz8CGiWB2HOXsOVDM6Lpe/nFPkC4IQ=
github.com/Telmate/proxmox-api-go v0.0.0-20210320143302-fea68269e6b0 h1:LeBf+Ex12uqA6dWZp73Qf3dzpV/LvB9SRmHgPBwnXrQ=
github.com/Telmate/proxmox-api-go v0.0.0-20210320143302-fea68269e6b0/go.mod h1:ayPkdmEKnlssqLQ9K1BE1jlsaYhXVwkoduXI30oQF0I=
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14=
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=

View File

@ -20,9 +20,6 @@ import (
"time"
)
// TaskTimeout - default async task call timeout in seconds
const TaskTimeout = 300
// TaskStatusCheckInterval - time between async checks in seconds
const TaskStatusCheckInterval = 2
@ -30,11 +27,12 @@ const exitStatusSuccess = "OK"
// Client - URL, user and password to specifc Proxmox node
type Client struct {
session *Session
ApiUrl string
Username string
Password string
Otp string
session *Session
ApiUrl string
Username string
Password string
Otp string
TaskTimeout int
}
// VmRef - virtual machine ref parts
@ -86,15 +84,27 @@ func NewVmRef(vmId int) (vmr *VmRef) {
return
}
func NewClient(apiUrl string, hclient *http.Client, tls *tls.Config) (client *Client, err error) {
func NewClient(apiUrl string, hclient *http.Client, tls *tls.Config, taskTimeout int) (client *Client, err error) {
var sess *Session
sess, err = NewSession(apiUrl, hclient, tls)
if err == nil {
client = &Client{session: sess, ApiUrl: apiUrl}
client = &Client{session: sess, ApiUrl: apiUrl, TaskTimeout: taskTimeout}
}
return client, err
}
// SetAPIToken specifies a pair of user identifier and token UUID to use
// for authenticating API calls.
// If this is set, a ticket from calling `Login` will not be used.
//
// - `userID` is expected to be in the form `USER@REALM!TOKENID`
// - `token` is just the UUID you get when initially creating the token
//
// See https://pve.proxmox.com/wiki/User_Management#pveum_tokens
func (c *Client) SetAPIToken(userID, token string) {
c.session.SetAPIToken(userID, token)
}
func (c *Client) Login(username string, password string, otp string) (err error) {
c.Username = username
c.Password = password
@ -214,6 +224,40 @@ func (c *Client) GetVmConfig(vmr *VmRef) (vmConfig map[string]interface{}, err e
return
}
func (c *Client) GetStorageStatus(vmr *VmRef, storageName string) (storageStatus map[string]interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
var data map[string]interface{}
url := fmt.Sprintf("/nodes/%s/storage/%s/status", vmr.node, storageName)
err = c.GetJsonRetryable(url, &data, 3)
if err != nil {
return nil, err
}
if data["data"] == nil {
return nil, errors.New("Storage STATUS not readable")
}
storageStatus = data["data"].(map[string]interface{})
return
}
func (c *Client) GetStorageContent(vmr *VmRef, storageName string) (data map[string]interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
url := fmt.Sprintf("/nodes/%s/storage/%s/content", vmr.node, storageName)
err = c.GetJsonRetryable(url, &data, 3)
if err != nil {
return nil, err
}
if data["data"] == nil {
return nil, errors.New("Storage Content not readable")
}
return
}
func (c *Client) GetVmSpiceProxy(vmr *VmRef) (vmSpiceProxy map[string]interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
@ -343,7 +387,7 @@ func (c *Client) WaitForCompletion(taskResponse map[string]interface{}) (waitExi
}
waited := 0
taskUpid := taskResponse["data"].(string)
for waited < TaskTimeout {
for waited < c.TaskTimeout {
exitStatus, statErr := c.GetTaskExitstatus(taskUpid)
if statErr != nil {
if statErr != io.ErrUnexpectedEOF { // don't give up on ErrUnexpectedEOF
@ -421,6 +465,10 @@ func (c *Client) ResumeVm(vmr *VmRef) (exitStatus string, err error) {
}
func (c *Client) DeleteVm(vmr *VmRef) (exitStatus string, err error) {
return c.DeleteVmParams(vmr, nil)
}
func (c *Client) DeleteVmParams(vmr *VmRef, params map[string]interface{}) (exitStatus string, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return "", err
@ -442,9 +490,10 @@ func (c *Client) DeleteVm(vmr *VmRef) (exitStatus string, err error) {
}
}
reqbody := ParamsToBody(params)
url := fmt.Sprintf("/nodes/%s/%s/%d", vmr.node, vmr.vmType, vmr.vmId)
var taskResponse map[string]interface{}
_, err = c.session.RequestJSON("DELETE", url, nil, nil, nil, &taskResponse)
_, err = c.session.RequestJSON("DELETE", url, nil, nil, &reqbody, &taskResponse)
exitStatus, err = c.WaitForCompletion(taskResponse)
return
}
@ -523,6 +572,61 @@ func (c *Client) CloneQemuVm(vmr *VmRef, vmParams map[string]interface{}) (exitS
return
}
func (c *Client) CreateQemuSnapshot(vmr *VmRef, snapshotName string) (exitStatus string, err error) {
err = c.CheckVmRef(vmr)
snapshotParams := map[string]interface{}{
"snapname": snapshotName,
}
reqbody := ParamsToBody(snapshotParams)
if err != nil {
return "", err
}
url := fmt.Sprintf("/nodes/%s/%s/%d/snapshot/", vmr.node, vmr.vmType, vmr.vmId)
resp, err := c.session.Post(url, nil, nil, &reqbody)
if err == nil {
taskResponse, err := ResponseJSON(resp)
if err != nil {
return "", err
}
exitStatus, err = c.WaitForCompletion(taskResponse)
}
return
}
func (c *Client) DeleteQemuSnapshot(vmr *VmRef, snapshotName string) (exitStatus string, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return "", err
}
url := fmt.Sprintf("/nodes/%s/%s/%d/snapshot/%s", vmr.node, vmr.vmType, vmr.vmId, snapshotName)
resp, err := c.session.Delete(url, nil, nil)
if err == nil {
taskResponse, err := ResponseJSON(resp)
if err != nil {
return "", err
}
exitStatus, err = c.WaitForCompletion(taskResponse)
}
return
}
func (c *Client) ListQemuSnapshot(vmr *VmRef) (taskResponse map[string]interface{}, exitStatus string, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, "", err
}
url := fmt.Sprintf("/nodes/%s/%s/%d/snapshot/", vmr.node, vmr.vmType, vmr.vmId)
resp, err := c.session.Get(url, nil, nil)
if err == nil {
taskResponse, err := ResponseJSON(resp)
if err != nil {
return nil, "", err
}
return taskResponse, "", nil
}
return
}
func (c *Client) RollbackQemuVm(vmr *VmRef, snapshot string) (exitStatus string, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
@ -581,14 +685,24 @@ func (c *Client) MigrateNode(vmr *VmRef, newTargetNode string, online bool) (exi
return nil, err
}
// ResizeQemuDisk allows the caller to increase the size of a disk by the indicated number of gigabytes
func (c *Client) ResizeQemuDisk(vmr *VmRef, disk string, moreSizeGB int) (exitStatus interface{}, err error) {
size := fmt.Sprintf("+%dG", moreSizeGB)
return c.ResizeQemuDiskRaw(vmr, disk, size)
}
// ResizeQemuDiskRaw allows the caller to provide the raw resize string to be send to proxmox.
// See the proxmox API documentation for full information, but the short version is if you prefix
// your desired size with a '+' character it will ADD size to the disk. If you just specify the size by
// itself it will do an absolute resizing to the specified size. Permitted suffixes are K, M, G, T
// to indicate order of magnitude (kilobyte, megabyte, etc). Decrease of disk size is not permitted.
func (c *Client) ResizeQemuDiskRaw(vmr *VmRef, disk string, size string) (exitStatus interface{}, err error) {
// PUT
//disk:virtio0
//size:+2G
if disk == "" {
disk = "virtio0"
}
size := fmt.Sprintf("+%dG", moreSizeGB)
reqbody := ParamsToBody(map[string]interface{}{"disk": disk, "size": size})
url := fmt.Sprintf("/nodes/%s/%s/%d/resize", vmr.node, vmr.vmType, vmr.vmId)
resp, err := c.session.Put(url, nil, nil, &reqbody)
@ -602,6 +716,20 @@ func (c *Client) ResizeQemuDisk(vmr *VmRef, disk string, moreSizeGB int) (exitSt
return
}
func (c *Client) MoveLxcDisk(vmr *VmRef, disk string, storage string) (exitStatus interface{}, err error) {
reqbody := ParamsToBody(map[string]interface{}{"disk": disk, "storage": storage, "delete": true})
url := fmt.Sprintf("/nodes/%s/%s/%d/move_volume", vmr.node, vmr.vmType, vmr.vmId)
resp, err := c.session.Post(url, nil, nil, &reqbody)
if err == nil {
taskResponse, err := ResponseJSON(resp)
if err != nil {
return nil, err
}
exitStatus, err = c.WaitForCompletion(taskResponse)
}
return
}
func (c *Client) MoveQemuDisk(vmr *VmRef, disk string, storage string) (exitStatus interface{}, err error) {
if disk == "" {
disk = "virtio0"
@ -661,7 +789,7 @@ func (c *Client) CreateVMDisk(
return err
}
if diskName, containsData := taskResponse["data"]; !containsData || diskName != fullDiskName {
return errors.New(fmt.Sprintf("Cannot create VM disk %s", fullDiskName))
return errors.New(fmt.Sprintf("Cannot create VM disk %s - %s", fullDiskName, diskName))
}
} else {
return err
@ -680,7 +808,7 @@ func (c *Client) createVMDisks(
for deviceName, deviceConf := range vmParams {
rxStorageModels := `(ide|sata|scsi|virtio)\d+`
if matched, _ := regexp.MatchString(rxStorageModels, deviceName); matched {
deviceConfMap := ParseConf(deviceConf.(string), ",", "=")
deviceConfMap := ParsePMConf(deviceConf.(string), "")
// This if condition to differentiate between `disk` and `cdrom`.
if media, containsFile := deviceConfMap["media"]; containsFile && media == "disk" {
fullDiskName := deviceConfMap["file"].(string)
@ -722,6 +850,135 @@ func (c *Client) DeleteVMDisks(
return nil
}
// VzDump - Create backup
func (c *Client) VzDump(vmr *VmRef, params map[string]interface{}) (exitStatus interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
reqbody := ParamsToBody(params)
url := fmt.Sprintf("/nodes/%s/vzdump", vmr.node)
resp, err := c.session.Post(url, nil, nil, &reqbody)
if err == nil {
taskResponse, err := ResponseJSON(resp)
if err != nil {
return nil, err
}
exitStatus, err = c.WaitForCompletion(taskResponse)
}
return
}
// CreateVNCProxy - Creates a TCP VNC proxy connections
func (c *Client) CreateVNCProxy(vmr *VmRef, params map[string]interface{}) (vncProxyRes map[string]interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
reqbody := ParamsToBody(params)
url := fmt.Sprintf("/nodes/%s/qemu/%d/vncproxy", vmr.node, vmr.vmId)
resp, err := c.session.Post(url, nil, nil, &reqbody)
if err != nil {
return nil, err
}
vncProxyRes, err = ResponseJSON(resp)
if err != nil {
return nil, err
}
if vncProxyRes["data"] == nil {
return nil, errors.New("VNC Proxy not readable")
}
vncProxyRes = vncProxyRes["data"].(map[string]interface{})
return
}
// GetExecStatus - Gets the status of the given pid started by the guest-agent
func (c *Client) GetExecStatus(vmr *VmRef, pid string) (status map[string]interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
err = c.GetJsonRetryable(fmt.Sprintf("/nodes/%s/%s/%d/agent/exec-status?pid=%s", vmr.node, vmr.vmType, vmr.vmId, pid), &status, 3)
if err == nil {
status = status["data"].(map[string]interface{})
}
return
}
// SetQemuFirewallOptions - Set Firewall options.
func (c *Client) SetQemuFirewallOptions(vmr *VmRef, fwOptions map[string]interface{}) (exitStatus interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
reqbody := ParamsToBody(fwOptions)
url := fmt.Sprintf("/nodes/%s/qemu/%d/firewall/options", vmr.node, vmr.vmId)
resp, err := c.session.Put(url, nil, nil, &reqbody)
if err == nil {
taskResponse, err := ResponseJSON(resp)
if err != nil {
return nil, err
}
exitStatus, err = c.WaitForCompletion(taskResponse)
}
return
}
// GetQemuFirewallOptions - Get VM firewall options.
func (c *Client) GetQemuFirewallOptions(vmr *VmRef) (firewallOptions map[string]interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
url := fmt.Sprintf("/nodes/%s/qemu/%d/firewall/options", vmr.node, vmr.vmId)
resp, err := c.session.Get(url, nil, nil)
if err == nil {
firewallOptions, err := ResponseJSON(resp)
if err != nil {
return nil, err
}
return firewallOptions, nil
}
return
}
// CreateQemuIPSet - Create new IPSet
func (c *Client) CreateQemuIPSet(vmr *VmRef, params map[string]interface{}) (exitStatus interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
reqbody := ParamsToBody(params)
url := fmt.Sprintf("/nodes/%s/qemu/%d/firewall/ipset", vmr.node, vmr.vmId)
resp, err := c.session.Post(url, nil, nil, &reqbody)
if err == nil {
taskResponse, err := ResponseJSON(resp)
if err != nil {
return nil, err
}
exitStatus, err = c.WaitForCompletion(taskResponse)
}
return
}
// GetQemuIPSet - List IPSets
func (c *Client) GetQemuIPSet(vmr *VmRef) (ipsets map[string]interface{}, err error) {
err = c.CheckVmRef(vmr)
if err != nil {
return nil, err
}
url := fmt.Sprintf("/nodes/%s/qemu/%d/firewall/ipset", vmr.node, vmr.vmId)
resp, err := c.session.Get(url, nil, nil)
if err == nil {
ipsets, err := ResponseJSON(resp)
if err != nil {
return nil, err
}
return ipsets, nil
}
return
}
func (c *Client) Upload(node string, storage string, contentType string, filename string, file io.Reader) error {
var doStreamingIO bool
var fileSize int64

View File

@ -4,7 +4,6 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"strconv"
"strings"
)
@ -36,7 +35,7 @@ type configLxc struct {
Pool string `json:"pool,omitempty"`
Protection bool `json:"protection"`
Restore bool `json:"restore,omitempty"`
RootFs string `json:"rootfs,omitempty"`
RootFs QemuDevice `json:"rootfs,omitempty"`
SearchDomain string `json:"searchdomain,omitempty"`
SSHPublicKeys string `json:"ssh-public-keys,omitempty"`
Start bool `json:"start"`
@ -47,6 +46,7 @@ type configLxc struct {
Tty int `json:"tty"`
Unique bool `json:"unique,omitempty"`
Unprivileged bool `json:"unprivileged"`
Tags string `json:"tags"`
Unused []string `json:"unused,omitempty"`
}
@ -72,12 +72,7 @@ func NewConfigLxc() configLxc {
func NewConfigLxcFromJson(io io.Reader) (config configLxc, err error) {
config = NewConfigLxc()
err = json.NewDecoder(io).Decode(config)
if err != nil {
log.Fatal(err)
return config, err
}
log.Println(config)
return
return config, err
}
func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err error) {
@ -85,7 +80,6 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
var lxcConfig map[string]interface{}
lxcConfig, err = client.GetVmConfig(vmr)
if err != nil {
log.Fatal(err)
return nil, err
}
@ -106,7 +100,7 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
if _, isSet := lxcConfig["console"]; isSet {
console = Itob(int(lxcConfig["console"].(float64)))
}
cores := 1
cores := 0
if _, isSet := lxcConfig["cores"]; isSet {
cores = int(lxcConfig["cores"].(float64))
}
@ -158,6 +152,12 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
memory = int(lxcConfig["memory"].(float64))
}
// add rootfs
rootfs := QemuDevice{}
if rootfsStr, isSet := lxcConfig["rootfs"]; isSet {
rootfs = ParsePMConf(rootfsStr.(string), "volume")
}
// add mountpoints
mpNames := []string{}
@ -168,17 +168,14 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
}
for _, mpName := range mpNames {
mpConfStr := lxcConfig[mpName]
mpConfList := strings.Split(mpConfStr.(string), ",")
mpConfStr := lxcConfig[mpName].(string)
mpConfMap := ParseLxcDisk(mpConfStr)
// add mp id
id := rxDeviceID.FindStringSubmatch(mpName)
mpID, _ := strconv.Atoi(id[0])
// add mp id
mpConfMap := QemuDevice{
"id": mpID,
}
// add rest of device config
mpConfMap.readDeviceConfig(mpConfList)
mpConfMap["slot"] = mpID
// prepare empty mountpoint map
if config.Mountpoints == nil {
config.Mountpoints = QemuDevices{}
@ -215,6 +212,13 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
}
// add rest of device config
nicConfMap.readDeviceConfig(nicConfList)
if nicConfMap["firewall"] == 1 {
nicConfMap["firewall"] = true
} else if nicConfMap["firewall"] == 0 {
nicConfMap["firewall"] = false
}
// prepare empty network map
if config.Networks == nil {
config.Networks = QemuDevices{}
@ -237,10 +241,6 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
if _, isSet := lxcConfig["protection"]; isSet {
protection = Itob(int(lxcConfig["protection"].(float64)))
}
rootfs := ""
if _, isSet := lxcConfig["rootfs"]; isSet {
rootfs = lxcConfig["rootfs"].(string)
}
searchdomain := ""
if _, isSet := lxcConfig["searchdomain"]; isSet {
searchdomain = lxcConfig["searchdomain"].(string)
@ -265,6 +265,10 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
if _, isset := lxcConfig["unprivileged"]; isset {
unprivileged = Itob(int(lxcConfig["unprivileged"].(float64)))
}
tags := ""
if _, isSet := lxcConfig["tags"]; isSet {
tags = lxcConfig["tags"].(string)
}
var unused []string
if _, isset := lxcConfig["unused"]; isset {
unused = lxcConfig["unused"].([]string)
@ -294,6 +298,7 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
config.Tty = tty
config.Unprivileged = unprivileged
config.Unused = unused
config.Tags = tags
return
}
@ -301,123 +306,21 @@ func NewConfigLxcFromApi(vmr *VmRef, client *Client) (config *configLxc, err err
// create LXC container using the Proxmox API
func (config configLxc) CreateLxc(vmr *VmRef, client *Client) (err error) {
vmr.SetVmType("lxc")
// convert config to map
params, _ := json.Marshal(&config)
var paramMap map[string]interface{}
json.Unmarshal(params, &paramMap)
// build list of features
// add features as parameter list to lxc parameters
// this overwrites the orginal formatting with a
// comma separated list of "key=value" pairs
featuresParam := QemuDeviceParam{}
featuresParam = featuresParam.createDeviceParam(config.Features, nil)
if len(featuresParam) > 0 {
paramMap["features"] = strings.Join(featuresParam, ",")
}
// build list of mountpoints
// this does the same as for the feature list
// except that there can be multiple of these mountpoint sets
// and each mountpoint set comes with a new id
for mpID, mpConfMap := range config.Mountpoints {
mpConfParam := QemuDeviceParam{}
mpConfParam = mpConfParam.createDeviceParam(mpConfMap, nil)
// add mp to lxc parameters
mpName := fmt.Sprintf("mp%v", mpID)
paramMap[mpName] = strings.Join(mpConfParam, ",")
}
// build list of network parameters
for nicID, nicConfMap := range config.Networks {
nicConfParam := QemuDeviceParam{}
nicConfParam = nicConfParam.createDeviceParam(nicConfMap, nil)
// add nic to lxc parameters
nicName := fmt.Sprintf("net%v", nicID)
paramMap[nicName] = strings.Join(nicConfParam, ",")
}
// build list of unused volumes for sake of completenes,
// even if it is not recommended to change these volumes manually
for volID, vol := range config.Unused {
// add volume to lxc parameters
volName := fmt.Sprintf("unused%v", volID)
paramMap[volName] = vol
}
// now that we concatenated the key value parameter
// list for the networks, mountpoints and unused volumes,
// remove the original keys, since the Proxmox API does
// not know how to handle this key
delete(paramMap, "networks")
delete(paramMap, "mountpoints")
delete(paramMap, "unused")
paramMap := config.mapToAPIParams()
// amend vmid
paramMap["vmid"] = vmr.vmId
exitStatus, err := client.CreateLxcContainer(vmr.node, paramMap)
if err != nil {
return fmt.Errorf("Error creating LXC container: %v, error status: %s (params: %v)", err, exitStatus, params)
params, _ := json.Marshal(&paramMap)
return fmt.Errorf("Error creating LXC container: %v, error status: %s (params: %v)", err, exitStatus, string(params))
}
return
}
func (config configLxc) UpdateConfig(vmr *VmRef, client *Client) (err error) {
// convert config to map
params, _ := json.Marshal(&config)
var paramMap map[string]interface{}
json.Unmarshal(params, &paramMap)
// build list of features
// add features as parameter list to lxc parameters
// this overwrites the orginal formatting with a
// comma separated list of "key=value" pairs
featuresParam := QemuDeviceParam{}
featuresParam = featuresParam.createDeviceParam(config.Features, nil)
paramMap["features"] = strings.Join(featuresParam, ",")
// build list of mountpoints
// this does the same as for the feature list
// except that there can be multiple of these mountpoint sets
// and each mountpoint set comes with a new id
for mpID, mpConfMap := range config.Mountpoints {
mpConfParam := QemuDeviceParam{}
mpConfParam = mpConfParam.createDeviceParam(mpConfMap, nil)
// add mp to lxc parameters
mpName := fmt.Sprintf("mp%v", mpID)
paramMap[mpName] = strings.Join(mpConfParam, ",")
}
// build list of network parameters
for nicID, nicConfMap := range config.Networks {
nicConfParam := QemuDeviceParam{}
nicConfParam = nicConfParam.createDeviceParam(nicConfMap, nil)
// add nic to lxc parameters
nicName := fmt.Sprintf("net%v", nicID)
paramMap[nicName] = strings.Join(nicConfParam, ",")
}
// build list of unused volumes for sake of completenes,
// even if it is not recommended to change these volumes manually
for volID, vol := range config.Unused {
// add volume to lxc parameters
volName := fmt.Sprintf("unused%v", volID)
paramMap[volName] = vol
}
// now that we concatenated the key value parameter
// list for the networks, mountpoints and unused volumes,
// remove the original keys, since the Proxmox API does
// not know how to handle this key
delete(paramMap, "networks")
delete(paramMap, "mountpoints")
delete(paramMap, "unused")
paramMap := config.mapToAPIParams()
// delete parameters wich are not supported in updated operations
delete(paramMap, "pool")
@ -425,6 +328,7 @@ func (config configLxc) UpdateConfig(vmr *VmRef, client *Client) (err error) {
delete(paramMap, "password")
delete(paramMap, "ostemplate")
delete(paramMap, "start")
// even though it is listed as a PUT option in the API documentation
// we remove it here because "it should not be modified manually";
// also, error "500 unable to modify read-only option: 'unprivileged'"
@ -433,3 +337,77 @@ func (config configLxc) UpdateConfig(vmr *VmRef, client *Client) (err error) {
_, err = client.SetLxcConfig(vmr, paramMap)
return err
}
func ParseLxcDisk(diskStr string) QemuDevice {
disk := ParsePMConf(diskStr, "volume")
// add features, if any
if mountoptions, isSet := disk["mountoptions"]; isSet {
moList := strings.Split(mountoptions.(string), ";")
moMap := map[string]bool{}
for _, mo := range moList {
moMap[mo] = true
}
disk["mountoptions"] = moMap
}
storageName, fileName := ParseSubConf(disk["volume"].(string), ":")
disk["storage"] = storageName
disk["file"] = fileName
return disk
}
func (config configLxc) mapToAPIParams() map[string]interface{} {
// convert config to map
params, _ := json.Marshal(&config)
var paramMap map[string]interface{}
json.Unmarshal(params, &paramMap)
// build list of features
// add features as parameter list to lxc parameters
// this overwrites the orginal formatting with a
// comma separated list of "key=value" pairs
paramMap["features"] = formatDeviceParam(config.Features)
// format rootfs params as expected
if rootfs := config.RootFs; rootfs != nil {
paramMap["rootfs"] = FormatDiskParam(rootfs)
}
// build list of mountpoints
// this does the same as for the feature list
// except that there can be multiple of these mountpoint sets
// and each mountpoint set comes with a new id
for _, mpConfMap := range config.Mountpoints {
// add mp to lxc parameters
mpID := mpConfMap["slot"]
mpName := fmt.Sprintf("mp%v", mpID)
paramMap[mpName] = FormatDiskParam(mpConfMap)
}
// build list of network parameters
for nicID, nicConfMap := range config.Networks {
// add nic to lxc parameters
nicName := fmt.Sprintf("net%v", nicID)
paramMap[nicName] = formatDeviceParam(nicConfMap)
}
// build list of unused volumes for sake of completeness,
// even if it is not recommended to change these volumes manually
for volID, vol := range config.Unused {
// add volume to lxc parameters
volName := fmt.Sprintf("unused%v", volID)
paramMap[volName] = vol
}
// now that we concatenated the key value parameter
// list for the networks, mountpoints and unused volumes,
// remove the original keys, since the Proxmox API does
// not know how to handle this key
delete(paramMap, "networks")
delete(paramMap, "mountpoints")
delete(paramMap, "unused")
return paramMap
}

View File

@ -15,6 +15,10 @@ import (
"time"
)
// Currently ZFS local, LVM, Ceph RBD, CephFS, Directory and virtio-scsi-pci are considered.
// Other formats are not verified, but could be added if they're needed.
const rxStorageTypes = `(zfspool|lvm|rbd|cephfs|dir|virtio-scsi-pci)`
type (
QemuDevices map[int]map[string]interface{}
QemuDevice map[string]interface{}
@ -23,33 +27,35 @@ type (
// ConfigQemu - Proxmox API QEMU options
type ConfigQemu struct {
VmID int `json:"vmid"`
Name string `json:"name"`
Description string `json:"desc"`
Pool string `json:"pool,omitempty"`
Bios string `json:"bios"`
Onboot bool `json:"onboot"`
Agent int `json:"agent"`
Memory int `json:"memory"`
Balloon int `json:"balloon"`
QemuOs string `json:"os"`
QemuCores int `json:"cores"`
QemuSockets int `json:"sockets"`
QemuVcpus int `json:"vcpus"`
QemuCpu string `json:"cpu"`
QemuNuma bool `json:"numa"`
QemuKVM bool `json:"kvm"`
Hotplug string `json:"hotplug"`
QemuIso string `json:"iso"`
FullClone *int `json:"fullclone"`
Boot string `json:"boot"`
BootDisk string `json:"bootdisk,omitempty"`
Scsihw string `json:"scsihw,omitempty"`
QemuDisks QemuDevices `json:"disk"`
QemuVga QemuDevice `json:"vga,omitempty"`
QemuNetworks QemuDevices `json:"network"`
QemuSerials QemuDevices `json:"serial,omitempty"`
HaState string `json:"hastate,omitempty"`
VmID int `json:"vmid"`
Name string `json:"name"`
Description string `json:"desc"`
Pool string `json:"pool,omitempty"`
Bios string `json:"bios"`
Onboot bool `json:"onboot"`
Agent int `json:"agent"`
Memory int `json:"memory"`
Balloon int `json:"balloon"`
QemuOs string `json:"os"`
QemuCores int `json:"cores"`
QemuSockets int `json:"sockets"`
QemuVcpus int `json:"vcpus"`
QemuCpu string `json:"cpu"`
QemuNuma bool `json:"numa"`
QemuKVM bool `json:"kvm"`
Hotplug string `json:"hotplug"`
QemuIso string `json:"iso"`
FullClone *int `json:"fullclone"`
Boot string `json:"boot"`
BootDisk string `json:"bootdisk,omitempty"`
Scsihw string `json:"scsihw,omitempty"`
QemuDisks QemuDevices `json:"disk"`
QemuUnusedDisks QemuDevices `json:"unused_disk"`
QemuVga QemuDevice `json:"vga,omitempty"`
QemuNetworks QemuDevices `json:"network"`
QemuSerials QemuDevices `json:"serial,omitempty"`
HaState string `json:"hastate,omitempty"`
Tags string `json:"tags"`
// Deprecated single disk.
DiskSize float64 `json:"diskGB"`
@ -100,6 +106,7 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) {
"memory": config.Memory,
"boot": config.Boot,
"description": config.Description,
"tags": config.Tags,
}
if config.Bios != "" {
@ -127,7 +134,10 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) {
}
// Create disks config.
config.CreateQemuDisksParams(vmr.vmId, params, false)
err = config.CreateQemuDisksParams(vmr.vmId, params, false)
if err != nil {
log.Printf("[ERROR] %q", err)
}
// Create vga config.
vgaParam := QemuDeviceParam{}
@ -137,17 +147,26 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) {
}
// Create networks config.
config.CreateQemuNetworksParams(vmr.vmId, params)
err = config.CreateQemuNetworksParams(vmr.vmId, params)
if err != nil {
log.Printf("[ERROR] %q", err)
}
// Create serial interfaces
config.CreateQemuSerialsParams(vmr.vmId, params)
err = config.CreateQemuSerialsParams(vmr.vmId, params)
if err != nil {
log.Printf("[ERROR] %q", err)
}
exitStatus, err := client.CreateQemuVm(vmr.node, params)
if err != nil {
return fmt.Errorf("Error creating VM: %v, error status: %s (params: %v)", err, exitStatus, params)
}
client.UpdateVMHA(vmr, config.HaState)
_, err = client.UpdateVMHA(vmr, config.HaState)
if err != nil {
log.Printf("[ERROR] %q", err)
}
return
}
@ -210,6 +229,7 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
configParams := map[string]interface{}{
"name": config.Name,
"description": config.Description,
"tags": config.Tags,
"onboot": config.Onboot,
"agent": config.Agent,
"sockets": config.QemuSockets,
@ -253,8 +273,16 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
configParamsDisk := map[string]interface{}{
"vmid": vmr.vmId,
}
config.CreateQemuDisksParams(vmr.vmId, configParamsDisk, false)
client.createVMDisks(vmr.node, configParamsDisk)
// TODO keep going if error=
err = config.CreateQemuDisksParams(vmr.vmId, configParamsDisk, false)
if err != nil {
log.Printf("[ERROR] %q", err)
}
// TODO keep going if error=
_, err = client.createVMDisks(vmr.node, configParamsDisk)
if err != nil {
log.Printf("[ERROR] %q", err)
}
//Copy the disks to the global configParams
for key, value := range configParamsDisk {
//vmid is only required in createVMDisks
@ -264,7 +292,10 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
}
// Create networks config.
config.CreateQemuNetworksParams(vmr.vmId, configParams)
err = config.CreateQemuNetworksParams(vmr.vmId, configParams)
if err != nil {
log.Printf("[ERROR] %q", err)
}
// Create vga config.
vgaParam := QemuDeviceParam{}
@ -276,7 +307,10 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
}
// Create serial interfaces
config.CreateQemuSerialsParams(vmr.vmId, configParams)
err = config.CreateQemuSerialsParams(vmr.vmId, configParams)
if err != nil {
log.Printf("[ERROR] %q", err)
}
// cloud-init options
if config.CIuser != "" {
@ -321,7 +355,10 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
return err
}
client.UpdateVMHA(vmr, config.HaState)
_, err = client.UpdateVMHA(vmr, config.HaState)
if err != nil {
log.Printf("[ERROR] %q", err)
}
_, err = client.UpdateVMPool(vmr, config.Pool)
@ -340,13 +377,14 @@ func NewConfigQemuFromJson(io io.Reader) (config *ConfigQemu, err error) {
}
var (
rxIso = regexp.MustCompile(`(.*?),media`)
rxDeviceID = regexp.MustCompile(`\d+`)
rxDiskName = regexp.MustCompile(`(virtio|scsi)\d+`)
rxDiskType = regexp.MustCompile(`\D+`)
rxNicName = regexp.MustCompile(`net\d+`)
rxMpName = regexp.MustCompile(`mp\d+`)
rxSerialName = regexp.MustCompile(`serial\d+`)
rxIso = regexp.MustCompile(`(.*?),media`)
rxDeviceID = regexp.MustCompile(`\d+`)
rxDiskName = regexp.MustCompile(`(virtio|scsi)\d+`)
rxDiskType = regexp.MustCompile(`\D+`)
rxUnusedDiskName = regexp.MustCompile(`^(unused)\d+`)
rxNicName = regexp.MustCompile(`net\d+`)
rxMpName = regexp.MustCompile(`mp\d+`)
rxSerialName = regexp.MustCompile(`serial\d+`)
)
func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err error) {
@ -387,6 +425,10 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
if _, isSet := vmConfig["description"]; isSet {
description = vmConfig["description"].(string)
}
tags := ""
if _, isSet := vmConfig["tags"]; isSet {
tags = vmConfig["tags"].(string)
}
bios := "seabios"
if _, isSet := vmConfig["bios"]; isSet {
bios = vmConfig["bios"].(string)
@ -466,28 +508,30 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
}
config = &ConfigQemu{
Name: name,
Description: strings.TrimSpace(description),
Bios: bios,
Onboot: onboot,
Agent: agent,
QemuOs: ostype,
Memory: int(memory),
QemuCores: int(cores),
QemuSockets: int(sockets),
QemuCpu: cpu,
QemuNuma: numa,
QemuKVM: kvm,
Hotplug: hotplug,
QemuVlanTag: -1,
Boot: boot,
BootDisk: bootdisk,
Scsihw: scsihw,
HaState: hastate,
QemuDisks: QemuDevices{},
QemuVga: QemuDevice{},
QemuNetworks: QemuDevices{},
QemuSerials: QemuDevices{},
Name: name,
Description: strings.TrimSpace(description),
Tags: strings.TrimSpace(tags),
Bios: bios,
Onboot: onboot,
Agent: agent,
QemuOs: ostype,
Memory: int(memory),
QemuCores: int(cores),
QemuSockets: int(sockets),
QemuCpu: cpu,
QemuNuma: numa,
QemuKVM: kvm,
Hotplug: hotplug,
QemuVlanTag: -1,
Boot: boot,
BootDisk: bootdisk,
Scsihw: scsihw,
HaState: hastate,
QemuDisks: QemuDevices{},
QemuUnusedDisks: QemuDevices{},
QemuVga: QemuDevice{},
QemuNetworks: QemuDevices{},
QemuSerials: QemuDevices{},
}
if balloon >= 1 {
@ -533,32 +577,66 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
// Add disks.
diskNames := []string{}
for k, _ := range vmConfig {
for k := range vmConfig {
if diskName := rxDiskName.FindStringSubmatch(k); len(diskName) > 0 {
diskNames = append(diskNames, diskName[0])
}
}
for _, diskName := range diskNames {
diskConfStr := vmConfig[diskName]
diskConfList := strings.Split(diskConfStr.(string), ",")
diskConfStr := vmConfig[diskName].(string)
//
id := rxDeviceID.FindStringSubmatch(diskName)
diskID, _ := strconv.Atoi(id[0])
diskType := rxDiskType.FindStringSubmatch(diskName)[0]
storageName, fileName := ParseSubConf(diskConfList[0], ":")
//
diskConfMap := QemuDevice{
"id": diskID,
"type": diskType,
"storage": storageName,
"file": fileName,
diskConfMap := ParsePMConf(diskConfStr, "volume")
diskConfMap["slot"] = diskID
diskConfMap["type"] = diskType
storageName, fileName := ParseSubConf(diskConfMap["volume"].(string), ":")
diskConfMap["storage"] = storageName
diskConfMap["file"] = fileName
filePath := diskConfMap["volume"]
// Get disk format
storageContent, err := client.GetStorageContent(vmr, storageName)
if err != nil {
log.Fatal(err)
return nil, err
}
var storageFormat string
contents := storageContent["data"].([]interface{})
for content := range contents {
storageContentMap := contents[content].(map[string]interface{})
if storageContentMap["volid"] == filePath {
storageFormat = storageContentMap["format"].(string)
break
}
}
diskConfMap["format"] = storageFormat
// Add rest of device config.
diskConfMap.readDeviceConfig(diskConfList[1:])
// Get storage type for disk
var storageStatus map[string]interface{}
storageStatus, err = client.GetStorageStatus(vmr, storageName)
if err != nil {
log.Fatal(err)
return nil, err
}
storageType := storageStatus["type"]
diskConfMap["storage_type"] = storageType
// Convert to gigabytes if disk size was received in terabytes
sizeIsInTerabytes, err := regexp.MatchString("[0-9]+T", diskConfMap["size"].(string))
if err != nil {
log.Fatal(err)
return nil, err
}
if sizeIsInTerabytes {
diskConfMap["size"] = fmt.Sprintf("%.0fG", DiskSizeGB(diskConfMap["size"]))
}
// And device config to disks map.
if len(diskConfMap) > 0 {
@ -566,11 +644,49 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
}
}
// Add unused disks
// unused0:local:100/vm-100-disk-1.qcow2
unusedDiskNames := []string{}
for k := range vmConfig {
// look for entries from the config in the format "unusedX:<storagepath>" where X is an integer
if unusedDiskName := rxUnusedDiskName.FindStringSubmatch(k); len(unusedDiskName) > 0 {
unusedDiskNames = append(unusedDiskNames, unusedDiskName[0])
}
}
fmt.Println(fmt.Sprintf("unusedDiskNames: %v", unusedDiskNames))
for _, unusedDiskName := range unusedDiskNames {
unusedDiskConfStr := vmConfig[unusedDiskName].(string)
finalDiskConfMap := QemuDevice{}
// parse "unused0" to get the id '0' as an int
id := rxDeviceID.FindStringSubmatch(unusedDiskName)
diskID, err := strconv.Atoi(id[0])
if err != nil {
return nil, errors.New(fmt.Sprintf("Unable to parse unused disk id from input string '%v' tried to convert '%v' to integer.", unusedDiskName, diskID))
}
finalDiskConfMap["slot"] = diskID
// parse the attributes from the unused disk
// extract the storage and file path from the unused disk entry
parsedUnusedDiskMap := ParsePMConf(unusedDiskConfStr, "storage+file")
storageName, fileName := ParseSubConf(parsedUnusedDiskMap["storage+file"].(string), ":")
finalDiskConfMap["storage"] = storageName
finalDiskConfMap["file"] = fileName
config.QemuUnusedDisks[diskID] = finalDiskConfMap
}
//Display
if vga, isSet := vmConfig["vga"]; isSet {
vgaList := strings.Split(vga.(string), ",")
vgaMap := QemuDevice{}
vgaMap.readDeviceConfig(vgaList)
// TODO: keep going if error?
err = vgaMap.readDeviceConfig(vgaList)
if err != nil {
log.Printf("[ERROR] %q", err)
}
if len(vgaMap) > 0 {
config.QemuVga = vgaMap
}
@ -579,7 +695,7 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
// Add networks.
nicNames := []string{}
for k, _ := range vmConfig {
for k := range vmConfig {
if nicName := rxNicName.FindStringSubmatch(k); len(nicName) > 0 {
nicNames = append(nicNames, nicName[0])
}
@ -601,7 +717,15 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
}
// Add rest of device config.
nicConfMap.readDeviceConfig(nicConfList[1:])
err = nicConfMap.readDeviceConfig(nicConfList[1:])
if err != nil {
log.Printf("[ERROR] %q", err)
}
if nicConfMap["firewall"] == 1 {
nicConfMap["firewall"] = true
} else if nicConfMap["firewall"] == 0 {
nicConfMap["firewall"] = false
}
// And device config to networks.
if len(nicConfMap) > 0 {
@ -612,7 +736,7 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
// Add serials
serialNames := []string{}
for k, _ := range vmConfig {
for k := range vmConfig {
if serialName := rxSerialName.FindStringSubmatch(k); len(serialName) > 0 {
serialNames = append(serialNames, serialName[0])
}
@ -773,6 +897,52 @@ func SendKeysString(vmr *VmRef, client *Client, keys string) (err error) {
return nil
}
// Given a QemuDevice, return a param string to give to ProxMox
func formatDeviceParam(device QemuDevice) string {
deviceConfParams := QemuDeviceParam{}
deviceConfParams = deviceConfParams.createDeviceParam(device, nil)
return strings.Join(deviceConfParams, ",")
}
// Given a QemuDevice (represesting a disk), return a param string to give to ProxMox
func FormatDiskParam(disk QemuDevice) string {
diskConfParam := QemuDeviceParam{}
if volume, ok := disk["volume"]; ok && volume != "" {
diskConfParam = append(diskConfParam, volume.(string))
diskConfParam = append(diskConfParam, fmt.Sprintf("size=%v", disk["size"]))
} else {
volumeInit := fmt.Sprintf("%v:%v", disk["storage"], DiskSizeGB(disk["size"]))
diskConfParam = append(diskConfParam, volumeInit)
}
// Set cache if not none (default).
if cache, ok := disk["cache"]; ok && cache != "none" {
diskCache := fmt.Sprintf("cache=%v", disk["cache"])
diskConfParam = append(diskConfParam, diskCache)
}
// Mountoptions
if mountoptions, ok := disk["mountoptions"]; ok {
options := []string{}
for opt, enabled := range mountoptions.(map[string]interface{}) {
if enabled.(bool) {
options = append(options, opt)
}
}
diskMountOpts := fmt.Sprintf("mountoptions=%v", strings.Join(options, ";"))
diskConfParam = append(diskConfParam, diskMountOpts)
}
// Keys that are not used as real/direct conf.
ignoredKeys := []string{"key", "slot", "type", "storage", "file", "size", "cache", "volume", "container", "vm", "mountoptions", "storage_type", "format"}
// Rest of config.
diskConfParam = diskConfParam.createDeviceParam(disk, ignoredKeys)
return strings.Join(diskConfParam, ",")
}
// Create parameters for each Nic device.
func (c ConfigQemu) CreateQemuNetworksParams(vmID int, params map[string]interface{}) error {
@ -873,57 +1043,41 @@ func (c ConfigQemu) CreateQemuDisksParams(
// For new style with multi disk device.
for diskID, diskConfMap := range c.QemuDisks {
// skip the first disk for clones (may not always be right, but a template probably has at least 1 disk)
if diskID == 0 && cloned {
continue
}
diskConfParam := QemuDeviceParam{
"media=disk",
}
// Device name.
deviceType := diskConfMap["type"].(string)
qemuDiskName := deviceType + strconv.Itoa(diskID)
// Set disk storage.
// Disk size.
diskSizeGB := fmt.Sprintf("size=%v", diskConfMap["size"])
diskConfParam = append(diskConfParam, diskSizeGB)
// Disk name.
var diskFile string
// Currently ZFS local, LVM, Ceph RBD, CephFS and Directory are considered.
// Other formats are not verified, but could be added if they're needed.
rxStorageTypes := `(zfspool|lvm|rbd|cephfs)`
storageType := diskConfMap["storage_type"].(string)
if matched, _ := regexp.MatchString(rxStorageTypes, storageType); matched {
diskFile = fmt.Sprintf("file=%v:vm-%v-disk-%v", diskConfMap["storage"], vmID, diskID)
} else {
diskFile = fmt.Sprintf("file=%v:%v/vm-%v-disk-%v.%v", diskConfMap["storage"], vmID, vmID, diskID, diskConfMap["format"])
}
diskConfParam = append(diskConfParam, diskFile)
// Set cache if not none (default).
if diskConfMap["cache"].(string) != "none" {
diskCache := fmt.Sprintf("cache=%v", diskConfMap["cache"])
diskConfParam = append(diskConfParam, diskCache)
}
// Keys that are not used as real/direct conf.
ignoredKeys := []string{"id", "type", "storage", "storage_type", "size", "cache"}
// Rest of config.
diskConfParam = diskConfParam.createDeviceParam(diskConfMap, ignoredKeys)
// Add back to Qemu prams.
params[qemuDiskName] = strings.Join(diskConfParam, ",")
params[qemuDiskName] = FormatDiskParam(diskConfMap)
}
return nil
}
// Create parameters for serial interface
func (c ConfigQemu) CreateQemuSerialsParams(
vmID int,
params map[string]interface{},
) error {
// For new style with multi disk device.
for serialID, serialConfMap := range c.QemuSerials {
// Device name.
deviceType := serialConfMap["type"].(string)
qemuSerialName := "serial" + strconv.Itoa(serialID)
// Add back to Qemu prams.
params[qemuSerialName] = deviceType
}
return nil
}
// Create the parameters for each device that will be sent to Proxmox API.
func (p QemuDeviceParam) createDeviceParam(
deviceConfMap QemuDevice,
ignoredKeys []string,
@ -964,43 +1118,6 @@ func (c ConfigQemu) String() string {
return string(jsConf)
}
// Create parameters for serial interface
func (c ConfigQemu) CreateQemuSerialsParams(
vmID int,
params map[string]interface{},
) error {
// For new style with multi disk device.
for serialID, serialConfMap := range c.QemuSerials {
// Device name.
deviceType := serialConfMap["type"].(string)
qemuSerialName := "serial" + strconv.Itoa(serialID)
// Add back to Qemu prams.
params[qemuSerialName] = deviceType
}
return nil
}
// NextId - Get next free VMID
func (c *Client) NextId() (id int, err error) {
var data map[string]interface{}
_, err = c.session.GetJSON("/cluster/nextid", nil, nil, &data)
if err != nil {
return -1, err
}
if data["data"] == nil || data["errors"] != nil {
return -1, fmt.Errorf(data["errors"].(string))
}
i, err := strconv.Atoi(data["data"].(string))
if err != nil {
return -1, err
}
return i, nil
}
// VMIdExists - If you pass an VMID that exists it will raise an error otherwise it will return the vmID
func (c *Client) VMIdExists(vmID int) (id int, err error) {
_, err = c.session.Get(fmt.Sprintf("/cluster/nextid?vmid=%d", vmID), nil, nil)

View File

@ -28,6 +28,7 @@ type Session struct {
ApiUrl string
AuthTicket string
CsrfToken string
AuthToken string // Combination of user, realm, token ID and UUID
Headers http.Header
}
@ -108,6 +109,11 @@ func TypedResponse(resp *http.Response, v interface{}) error {
return nil
}
func (s *Session) SetAPIToken(userID, token string) {
auth := fmt.Sprintf("%s=%s", userID, token)
s.AuthToken = auth
}
func (s *Session) Login(username string, password string, otp string) (err error) {
reqUser := map[string]interface{}{"username": username, "password": password}
if otp != "" {
@ -150,7 +156,9 @@ func (s *Session) NewRequest(method, url string, headers *http.Header, body io.R
if headers != nil {
req.Header = *headers
}
if s.AuthTicket != "" {
if s.AuthToken != "" {
req.Header.Add("Authorization", "PVEAPIToken="+s.AuthToken)
} else if s.AuthTicket != "" {
req.Header.Add("Cookie", "PVEAuthCookie="+s.AuthTicket)
req.Header.Add("CSRFPreventionToken", s.CsrfToken)
}

View File

@ -1,6 +1,7 @@
package proxmox
import (
"regexp"
"strconv"
"strings"
)
@ -51,12 +52,57 @@ func ParseConf(
kvString string,
confSeparator string,
subConfSeparator string,
implicitFirstKey string,
) QemuDevice {
var confMap = QemuDevice{}
confList := strings.Split(kvString, confSeparator)
if implicitFirstKey != "" {
if !strings.Contains(confList[0], "=") {
confMap[implicitFirstKey] = confList[0]
confList = confList[1:]
}
}
for _, item := range confList {
key, value := ParseSubConf(item, subConfSeparator)
confMap[key] = value
}
return confMap
}
func ParsePMConf(
kvString string,
implicitFirstKey string,
) QemuDevice {
return ParseConf(kvString, ",", "=", implicitFirstKey)
}
// Convert a disk-size string to a GB float
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 "T", "TB":
diskSize *= 1024
case "G", "GB":
//Nothing to do
case "M", "MB":
diskSize /= 1024
case "K", "KB":
diskSize /= 1048576
}
}
case float64:
diskSize = dcSize.(float64)
}
return diskSize
}

2
vendor/modules.txt vendored
View File

@ -78,7 +78,7 @@ github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud
github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/server
# github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d
github.com/StackExchange/wmi
# github.com/Telmate/proxmox-api-go v0.0.0-20200715182505-ec97c70ba887
# github.com/Telmate/proxmox-api-go v0.0.0-20210320143302-fea68269e6b0
## explicit
github.com/Telmate/proxmox-api-go/proxmox
# github.com/agext/levenshtein v1.2.1

View File

@ -43,10 +43,20 @@ in the image's Cloud-Init settings for provisioning.
- `username` (string) - Username when authenticating to Proxmox, including
the realm. For example `user@pve` to use the local Proxmox realm.
When used with `token`, it would look like this: `user@pve!token`
Can also be set via the `PROXMOX_USERNAME` environment variable.
- `password` (string) - Password for the user.
For API tokens please use `token`.
Can also be set via the `PROXMOX_PASSWORD` environment variable.
Either `password` or `token` must be specifed. If both are set,
`token` takes precedence.
- `token` (string) - Token for authenticating API calls.
This allows the API client to work with API tokens instead of user passwords.
Can also be set via the `PROXMOX_TOKEN` environment variable.
Either `password` or `token` must be specifed. If both are set,
`token` takes precedence.
- `node` (string) - Which node in the Proxmox cluster to start the virtual
machine on during creation.

View File

@ -40,10 +40,20 @@ builder.
- `username` (string) - Username when authenticating to Proxmox, including
the realm. For example `user@pve` to use the local Proxmox realm.
When used with `token`, it would look like this: `user@pve!token`
Can also be set via the `PROXMOX_USERNAME` environment variable.
- `password` (string) - Password for the user.
For API tokens please use `token`.
Can also be set via the `PROXMOX_PASSWORD` environment variable.
Either `password` or `token` must be specifed. If both are set,
`token` takes precedence.
- `token` (string) - Token for authenticating API calls.
This allows the API client to work with API tokens instead of user passwords.
Can also be set via the `PROXMOX_TOKEN` environment variable.
Either `password` or `token` must be specifed. If both are set,
`token` takes precedence.
- `node` (string) - Which node in the Proxmox cluster to start the virtual
machine on during creation.