Allow using API tokens for Proxmox authentication (#10797)
This commit is contained in:
parent
0993c976fa
commit
4d9fb629c6
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"))
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
2
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
|
||||
|
|
3
go.sum
3
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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue