From 4d9fb629c60fddad0ebecde8538977079d3b9641 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Mon, 22 Mar 2021 11:48:31 +0100 Subject: [PATCH] Allow using API tokens for Proxmox authentication (#10797) --- builder/proxmox/clone/config.hcl2spec.go | 2 + builder/proxmox/common/builder.go | 11 +- builder/proxmox/common/client.go | 37 ++ builder/proxmox/common/client_test.go | 91 ++++ builder/proxmox/common/config.go | 8 +- builder/proxmox/common/config.hcl2spec.go | 2 + builder/proxmox/iso/config.hcl2spec.go | 2 + go.mod | 2 +- go.sum | 3 +- .../Telmate/proxmox-api-go/proxmox/client.go | 287 +++++++++++- .../proxmox-api-go/proxmox/config_lxc.go | 234 +++++----- .../proxmox-api-go/proxmox/config_qemu.go | 427 +++++++++++------- .../Telmate/proxmox-api-go/proxmox/session.go | 10 +- .../Telmate/proxmox-api-go/proxmox/util.go | 46 ++ vendor/modules.txt | 2 +- .../content/docs/builders/proxmox/clone.mdx | 10 + website/content/docs/builders/proxmox/iso.mdx | 10 + 17 files changed, 870 insertions(+), 314 deletions(-) create mode 100644 builder/proxmox/common/client.go create mode 100644 builder/proxmox/common/client_test.go diff --git a/builder/proxmox/clone/config.hcl2spec.go b/builder/proxmox/clone/config.hcl2spec.go index e20717cb9..32b6e64f3 100644 --- a/builder/proxmox/clone/config.hcl2spec.go +++ b/builder/proxmox/clone/config.hcl2spec.go @@ -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}, diff --git a/builder/proxmox/common/builder.go b/builder/proxmox/common/builder.go index 96937cf8f..0d4391afd 100644 --- a/builder/proxmox/common/builder.go +++ b/builder/proxmox/common/builder.go @@ -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 } diff --git a/builder/proxmox/common/client.go b/builder/proxmox/common/client.go new file mode 100644 index 000000000..e70f8af50 --- /dev/null +++ b/builder/proxmox/common/client.go @@ -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 +} diff --git a/builder/proxmox/common/client_test.go b/builder/proxmox/common/client_test.go new file mode 100644 index 000000000..afdf8ddf2 --- /dev/null +++ b/builder/proxmox/common/client_test.go @@ -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) +} diff --git a/builder/proxmox/common/config.go b/builder/proxmox/common/config.go index 33810c57d..19d7793cb 100644 --- a/builder/proxmox/common/config.go +++ b/builder/proxmox/common/config.go @@ -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")) diff --git a/builder/proxmox/common/config.hcl2spec.go b/builder/proxmox/common/config.hcl2spec.go index 96a5e09e8..f6d11221c 100644 --- a/builder/proxmox/common/config.hcl2spec.go +++ b/builder/proxmox/common/config.hcl2spec.go @@ -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}, diff --git a/builder/proxmox/iso/config.hcl2spec.go b/builder/proxmox/iso/config.hcl2spec.go index 7d420d277..88aa679bb 100644 --- a/builder/proxmox/iso/config.hcl2spec.go +++ b/builder/proxmox/iso/config.hcl2spec.go @@ -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}, diff --git a/go.mod b/go.mod index 4cb549450..e8950e8bc 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 807fb3dc9..d67880e44 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go b/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go index f7fa27da5..e69c467b0 100644 --- a/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go +++ b/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go @@ -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 diff --git a/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_lxc.go b/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_lxc.go index 5060d64ee..38df551bc 100644 --- a/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_lxc.go +++ b/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_lxc.go @@ -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, ¶mMap) - - // 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(¶mMap) + 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, ¶mMap) - - // 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, ¶mMap) + + // 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 +} diff --git a/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go b/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go index e57ee6bdf..2609ca9af 100644 --- a/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go +++ b/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go @@ -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:" 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) diff --git a/vendor/github.com/Telmate/proxmox-api-go/proxmox/session.go b/vendor/github.com/Telmate/proxmox-api-go/proxmox/session.go index 2b6f58c44..9b4787478 100644 --- a/vendor/github.com/Telmate/proxmox-api-go/proxmox/session.go +++ b/vendor/github.com/Telmate/proxmox-api-go/proxmox/session.go @@ -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) } diff --git a/vendor/github.com/Telmate/proxmox-api-go/proxmox/util.go b/vendor/github.com/Telmate/proxmox-api-go/proxmox/util.go index 5841c421f..636e69fd0 100644 --- a/vendor/github.com/Telmate/proxmox-api-go/proxmox/util.go +++ b/vendor/github.com/Telmate/proxmox-api-go/proxmox/util.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f5a55521e..3836bdf3a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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 diff --git a/website/content/docs/builders/proxmox/clone.mdx b/website/content/docs/builders/proxmox/clone.mdx index 5b8da5e02..487ec6606 100644 --- a/website/content/docs/builders/proxmox/clone.mdx +++ b/website/content/docs/builders/proxmox/clone.mdx @@ -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. diff --git a/website/content/docs/builders/proxmox/iso.mdx b/website/content/docs/builders/proxmox/iso.mdx index 2fe3f8f55..1d991c73c 100644 --- a/website/content/docs/builders/proxmox/iso.mdx +++ b/website/content/docs/builders/proxmox/iso.mdx @@ -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.