mirror of https://github.com/hashicorp/packer
parent
2f754c38f8
commit
65f38978f8
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@ -0,0 +1,597 @@
|
||||
package proxmox
|
||||
|
||||
// inspired by https://github.com/Telmate/vagrant-proxmox/blob/master/lib/vagrant-proxmox/proxmox/connection.rb
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TaskTimeout - default async task call timeout in seconds
|
||||
const TaskTimeout = 300
|
||||
|
||||
// TaskStatusCheckInterval - time between async checks in seconds
|
||||
const TaskStatusCheckInterval = 2
|
||||
|
||||
const exitStatusSuccess = "OK"
|
||||
|
||||
// Client - URL, user and password to specifc Proxmox node
|
||||
type Client struct {
|
||||
session *Session
|
||||
ApiUrl string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// VmRef - virtual machine ref parts
|
||||
// map[type:qemu node:proxmox1-xx id:qemu/132 diskread:5.57424738e+08 disk:0 netin:5.9297450593e+10 mem:3.3235968e+09 uptime:1.4567097e+07 vmid:132 template:0 maxcpu:2 netout:6.053310416e+09 maxdisk:3.4359738368e+10 maxmem:8.592031744e+09 diskwrite:1.49663619584e+12 status:running cpu:0.00386980694947209 name:appt-app1-dev.xxx.xx]
|
||||
type VmRef struct {
|
||||
vmId int
|
||||
node string
|
||||
vmType string
|
||||
}
|
||||
|
||||
func (vmr *VmRef) SetNode(node string) {
|
||||
vmr.node = node
|
||||
return
|
||||
}
|
||||
|
||||
func (vmr *VmRef) SetVmType(vmType string) {
|
||||
vmr.vmType = vmType
|
||||
return
|
||||
}
|
||||
|
||||
func (vmr *VmRef) VmId() int {
|
||||
return vmr.vmId
|
||||
}
|
||||
|
||||
func (vmr *VmRef) Node() string {
|
||||
return vmr.node
|
||||
}
|
||||
|
||||
func NewVmRef(vmId int) (vmr *VmRef) {
|
||||
vmr = &VmRef{vmId: vmId, node: "", vmType: ""}
|
||||
return
|
||||
}
|
||||
|
||||
func NewClient(apiUrl string, hclient *http.Client, tls *tls.Config) (client *Client, err error) {
|
||||
var sess *Session
|
||||
sess, err = NewSession(apiUrl, hclient, tls)
|
||||
if err == nil {
|
||||
client = &Client{session: sess, ApiUrl: apiUrl}
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
func (c *Client) Login(username string, password string) (err error) {
|
||||
c.Username = username
|
||||
c.Password = password
|
||||
return c.session.Login(username, password)
|
||||
}
|
||||
|
||||
func (c *Client) GetJsonRetryable(url string, data *map[string]interface{}, tries int) error {
|
||||
var statErr error
|
||||
for ii := 0; ii < tries; ii++ {
|
||||
_, statErr = c.session.GetJSON(url, nil, nil, data)
|
||||
if statErr == nil {
|
||||
return nil
|
||||
}
|
||||
// if statErr != io.ErrUnexpectedEOF { // don't give up on ErrUnexpectedEOF
|
||||
// return statErr
|
||||
// }
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
return statErr
|
||||
}
|
||||
|
||||
func (c *Client) GetNodeList() (list map[string]interface{}, err error) {
|
||||
err = c.GetJsonRetryable("/nodes", &list, 3)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetVmList() (list map[string]interface{}, err error) {
|
||||
err = c.GetJsonRetryable("/cluster/resources?type=vm", &list, 3)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) CheckVmRef(vmr *VmRef) (err error) {
|
||||
if vmr.node == "" || vmr.vmType == "" {
|
||||
_, err = c.GetVmInfo(vmr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetVmInfo(vmr *VmRef) (vmInfo map[string]interface{}, err error) {
|
||||
resp, err := c.GetVmList()
|
||||
vms := resp["data"].([]interface{})
|
||||
for vmii := range vms {
|
||||
vm := vms[vmii].(map[string]interface{})
|
||||
if int(vm["vmid"].(float64)) == vmr.vmId {
|
||||
vmInfo = vm
|
||||
vmr.node = vmInfo["node"].(string)
|
||||
vmr.vmType = vmInfo["type"].(string)
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("Vm '%d' not found", vmr.vmId))
|
||||
}
|
||||
|
||||
func (c *Client) GetVmRefByName(vmName string) (vmr *VmRef, err error) {
|
||||
resp, err := c.GetVmList()
|
||||
vms := resp["data"].([]interface{})
|
||||
for vmii := range vms {
|
||||
vm := vms[vmii].(map[string]interface{})
|
||||
if vm["name"] != nil && vm["name"].(string) == vmName {
|
||||
vmr = NewVmRef(int(vm["vmid"].(float64)))
|
||||
vmr.node = vm["node"].(string)
|
||||
vmr.vmType = vm["type"].(string)
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil, errors.New(fmt.Sprintf("Vm '%s' not found", vmName))
|
||||
}
|
||||
|
||||
func (c *Client) GetVmState(vmr *VmRef) (vmState 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/%s/%d/status/current", vmr.node, vmr.vmType, vmr.vmId)
|
||||
err = c.GetJsonRetryable(url, &data, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data["data"] == nil {
|
||||
return nil, errors.New("Vm STATE not readable")
|
||||
}
|
||||
vmState = data["data"].(map[string]interface{})
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetVmConfig(vmr *VmRef) (vmConfig 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/%s/%d/config", vmr.node, vmr.vmType, vmr.vmId)
|
||||
err = c.GetJsonRetryable(url, &data, 3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data["data"] == nil {
|
||||
return nil, errors.New("Vm CONFIG not readable")
|
||||
}
|
||||
vmConfig = data["data"].(map[string]interface{})
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) GetVmSpiceProxy(vmr *VmRef) (vmSpiceProxy 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/%s/%d/spiceproxy", vmr.node, vmr.vmType, vmr.vmId)
|
||||
_, err = c.session.PostJSON(url, nil, nil, nil, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data["data"] == nil {
|
||||
return nil, errors.New("Vm SpiceProxy not readable")
|
||||
}
|
||||
vmSpiceProxy = data["data"].(map[string]interface{})
|
||||
return
|
||||
}
|
||||
|
||||
type AgentNetworkInterface struct {
|
||||
MACAddress string
|
||||
IPAddresses []net.IP
|
||||
Name string
|
||||
Statistics map[string]int64
|
||||
}
|
||||
|
||||
func (a *AgentNetworkInterface) UnmarshalJSON(b []byte) error {
|
||||
var intermediate struct {
|
||||
HardwareAddress string `json:"hardware-address"`
|
||||
IPAddresses []struct {
|
||||
IPAddress string `json:"ip-address"`
|
||||
IPAddressType string `json:"ip-address-type"`
|
||||
Prefix int `json:"prefix"`
|
||||
} `json:"ip-addresses"`
|
||||
Name string `json:"name"`
|
||||
Statistics map[string]int64 `json:"statistics"`
|
||||
}
|
||||
err := json.Unmarshal(b, &intermediate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.IPAddresses = make([]net.IP, len(intermediate.IPAddresses))
|
||||
for idx, ip := range intermediate.IPAddresses {
|
||||
a.IPAddresses[idx] = net.ParseIP(ip.IPAddress)
|
||||
if a.IPAddresses[idx] == nil {
|
||||
return fmt.Errorf("Could not parse %s as IP", ip.IPAddress)
|
||||
}
|
||||
}
|
||||
a.MACAddress = intermediate.HardwareAddress
|
||||
a.Name = intermediate.Name
|
||||
a.Statistics = intermediate.Statistics
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetVmAgentNetworkInterfaces(vmr *VmRef) ([]AgentNetworkInterface, error) {
|
||||
var ifs []AgentNetworkInterface
|
||||
err := c.doAgentGet(vmr, "network-get-interfaces", &ifs)
|
||||
return ifs, err
|
||||
}
|
||||
|
||||
func (c *Client) doAgentGet(vmr *VmRef, command string, output interface{}) error {
|
||||
err := c.CheckVmRef(vmr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/nodes/%s/%s/%d/agent/%s", vmr.node, vmr.vmType, vmr.vmId, command)
|
||||
resp, err := c.session.Get(url, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return TypedResponse(resp, output)
|
||||
}
|
||||
|
||||
func (c *Client) CreateTemplate(vmr *VmRef) error {
|
||||
err := c.CheckVmRef(vmr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/nodes/%s/%s/%d/template", vmr.node, vmr.vmType, vmr.vmId)
|
||||
_, err = c.session.Post(url, nil, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) MonitorCmd(vmr *VmRef, command string) (monitorRes map[string]interface{}, err error) {
|
||||
err = c.CheckVmRef(vmr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqbody := ParamsToBody(map[string]interface{}{"command": command})
|
||||
url := fmt.Sprintf("/nodes/%s/%s/%d/monitor", vmr.node, vmr.vmType, vmr.vmId)
|
||||
resp, err := c.session.Post(url, nil, nil, &reqbody)
|
||||
monitorRes, err = ResponseJSON(resp)
|
||||
return
|
||||
}
|
||||
|
||||
// WaitForCompletion - poll the API for task completion
|
||||
func (c *Client) WaitForCompletion(taskResponse map[string]interface{}) (waitExitStatus string, err error) {
|
||||
if taskResponse["errors"] != nil {
|
||||
errJSON, _ := json.MarshalIndent(taskResponse["errors"], "", " ")
|
||||
return string(errJSON), errors.New("Error reponse")
|
||||
}
|
||||
if taskResponse["data"] == nil {
|
||||
return "", nil
|
||||
}
|
||||
waited := 0
|
||||
taskUpid := taskResponse["data"].(string)
|
||||
for waited < TaskTimeout {
|
||||
exitStatus, statErr := c.GetTaskExitstatus(taskUpid)
|
||||
if statErr != nil {
|
||||
if statErr != io.ErrUnexpectedEOF { // don't give up on ErrUnexpectedEOF
|
||||
return "", statErr
|
||||
}
|
||||
}
|
||||
if exitStatus != nil {
|
||||
waitExitStatus = exitStatus.(string)
|
||||
return
|
||||
}
|
||||
time.Sleep(TaskStatusCheckInterval * time.Second)
|
||||
waited = waited + TaskStatusCheckInterval
|
||||
}
|
||||
return "", errors.New("Wait timeout for:" + taskUpid)
|
||||
}
|
||||
|
||||
var rxTaskNode = regexp.MustCompile("UPID:(.*?):")
|
||||
|
||||
func (c *Client) GetTaskExitstatus(taskUpid string) (exitStatus interface{}, err error) {
|
||||
node := rxTaskNode.FindStringSubmatch(taskUpid)[1]
|
||||
url := fmt.Sprintf("/nodes/%s/tasks/%s/status", node, taskUpid)
|
||||
var data map[string]interface{}
|
||||
_, err = c.session.GetJSON(url, nil, nil, &data)
|
||||
if err == nil {
|
||||
exitStatus = data["data"].(map[string]interface{})["exitstatus"]
|
||||
}
|
||||
if exitStatus != nil && exitStatus != exitStatusSuccess {
|
||||
err = errors.New(exitStatus.(string))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) StatusChangeVm(vmr *VmRef, setStatus string) (exitStatus string, err error) {
|
||||
err = c.CheckVmRef(vmr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("/nodes/%s/%s/%d/status/%s", vmr.node, vmr.vmType, vmr.vmId, setStatus)
|
||||
var taskResponse map[string]interface{}
|
||||
for i := 0; i < 3; i++ {
|
||||
_, err = c.session.PostJSON(url, nil, nil, nil, &taskResponse)
|
||||
exitStatus, err = c.WaitForCompletion(taskResponse)
|
||||
if exitStatus == "" {
|
||||
time.Sleep(TaskStatusCheckInterval * time.Second)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) StartVm(vmr *VmRef) (exitStatus string, err error) {
|
||||
return c.StatusChangeVm(vmr, "start")
|
||||
}
|
||||
|
||||
func (c *Client) StopVm(vmr *VmRef) (exitStatus string, err error) {
|
||||
return c.StatusChangeVm(vmr, "stop")
|
||||
}
|
||||
|
||||
func (c *Client) ShutdownVm(vmr *VmRef) (exitStatus string, err error) {
|
||||
return c.StatusChangeVm(vmr, "shutdown")
|
||||
}
|
||||
|
||||
func (c *Client) ResetVm(vmr *VmRef) (exitStatus string, err error) {
|
||||
return c.StatusChangeVm(vmr, "reset")
|
||||
}
|
||||
|
||||
func (c *Client) SuspendVm(vmr *VmRef) (exitStatus string, err error) {
|
||||
return c.StatusChangeVm(vmr, "suspend")
|
||||
}
|
||||
|
||||
func (c *Client) ResumeVm(vmr *VmRef) (exitStatus string, err error) {
|
||||
return c.StatusChangeVm(vmr, "resume")
|
||||
}
|
||||
|
||||
func (c *Client) DeleteVm(vmr *VmRef) (exitStatus string, err error) {
|
||||
err = c.CheckVmRef(vmr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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)
|
||||
exitStatus, err = c.WaitForCompletion(taskResponse)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) CreateQemuVm(node string, vmParams map[string]interface{}) (exitStatus string, err error) {
|
||||
// Create VM disks first to ensure disks names.
|
||||
createdDisks, createdDisksErr := c.createVMDisks(node, vmParams)
|
||||
if createdDisksErr != nil {
|
||||
return "", createdDisksErr
|
||||
}
|
||||
|
||||
// Then create the VM itself.
|
||||
reqbody := ParamsToBody(vmParams)
|
||||
url := fmt.Sprintf("/nodes/%s/qemu", node)
|
||||
var resp *http.Response
|
||||
resp, err = c.session.Post(url, nil, nil, &reqbody)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
exitStatus = string(b)
|
||||
return
|
||||
}
|
||||
|
||||
taskResponse, err := ResponseJSON(resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
exitStatus, err = c.WaitForCompletion(taskResponse)
|
||||
// Delete VM disks if the VM didn't create.
|
||||
if exitStatus != "OK" {
|
||||
deleteDisksErr := c.DeleteVMDisks(node, createdDisks)
|
||||
if deleteDisksErr != nil {
|
||||
return "", deleteDisksErr
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) CloneQemuVm(vmr *VmRef, vmParams map[string]interface{}) (exitStatus string, err error) {
|
||||
reqbody := ParamsToBody(vmParams)
|
||||
url := fmt.Sprintf("/nodes/%s/qemu/%d/clone", vmr.node, 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) RollbackQemuVm(vmr *VmRef, snapshot string) (exitStatus string, err error) {
|
||||
err = c.CheckVmRef(vmr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
url := fmt.Sprintf("/nodes/%s/%s/%d/snapshot/%s/rollback", vmr.node, vmr.vmType, vmr.vmId, snapshot)
|
||||
var taskResponse map[string]interface{}
|
||||
_, err = c.session.PostJSON(url, nil, nil, nil, &taskResponse)
|
||||
exitStatus, err = c.WaitForCompletion(taskResponse)
|
||||
return
|
||||
}
|
||||
|
||||
// SetVmConfig - send config options
|
||||
func (c *Client) SetVmConfig(vmr *VmRef, vmParams map[string]interface{}) (exitStatus interface{}, err error) {
|
||||
reqbody := ParamsToBody(vmParams)
|
||||
url := fmt.Sprintf("/nodes/%s/%s/%d/config", 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) ResizeQemuDisk(vmr *VmRef, disk string, moreSizeGB int) (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)
|
||||
if err == nil {
|
||||
taskResponse, err := ResponseJSON(resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exitStatus, err = c.WaitForCompletion(taskResponse)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetNextID - Get next free VMID
|
||||
func (c *Client) GetNextID(currentID int) (nextID int, err error) {
|
||||
var data map[string]interface{}
|
||||
var url string
|
||||
if currentID >= 100 {
|
||||
url = fmt.Sprintf("/cluster/nextid?vmid=%d", currentID)
|
||||
} else {
|
||||
url = "/cluster/nextid"
|
||||
}
|
||||
_, err = c.session.GetJSON(url, nil, nil, &data)
|
||||
if err == nil {
|
||||
if data["errors"] != nil {
|
||||
if currentID >= 100 {
|
||||
return c.GetNextID(currentID + 1)
|
||||
} else {
|
||||
return -1, errors.New("error using /cluster/nextid")
|
||||
}
|
||||
}
|
||||
nextID, err = strconv.Atoi(data["data"].(string))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CreateVMDisk - Create single disk for VM on host node.
|
||||
func (c *Client) CreateVMDisk(
|
||||
nodeName string,
|
||||
storageName string,
|
||||
fullDiskName string,
|
||||
diskParams map[string]interface{},
|
||||
) error {
|
||||
|
||||
reqbody := ParamsToBody(diskParams)
|
||||
url := fmt.Sprintf("/nodes/%s/storage/%s/content", nodeName, storageName)
|
||||
resp, err := c.session.Post(url, nil, nil, &reqbody)
|
||||
if err == nil {
|
||||
taskResponse, err := ResponseJSON(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if diskName, containsData := taskResponse["data"]; !containsData || diskName != fullDiskName {
|
||||
return errors.New(fmt.Sprintf("Cannot create VM disk %s", fullDiskName))
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createVMDisks - Make disks parameters and create all VM disks on host node.
|
||||
func (c *Client) createVMDisks(
|
||||
node string,
|
||||
vmParams map[string]interface{},
|
||||
) (disks []string, err error) {
|
||||
var createdDisks []string
|
||||
vmID := vmParams["vmid"].(int)
|
||||
for deviceName, deviceConf := range vmParams {
|
||||
rxStorageModels := `(ide|sata|scsi|virtio)\d+`
|
||||
if matched, _ := regexp.MatchString(rxStorageModels, deviceName); matched {
|
||||
deviceConfMap := ParseConf(deviceConf.(string), ",", "=")
|
||||
// This if condition to differentiate between `disk` and `cdrom`.
|
||||
if media, containsFile := deviceConfMap["media"]; containsFile && media == "disk" {
|
||||
fullDiskName := deviceConfMap["file"].(string)
|
||||
storageName, volumeName := getStorageAndVolumeName(fullDiskName, ":")
|
||||
diskParams := map[string]interface{}{
|
||||
"vmid": vmID,
|
||||
"filename": volumeName,
|
||||
"size": deviceConfMap["size"],
|
||||
}
|
||||
err := c.CreateVMDisk(node, storageName, fullDiskName, diskParams)
|
||||
if err != nil {
|
||||
return createdDisks, err
|
||||
} else {
|
||||
createdDisks = append(createdDisks, fullDiskName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createdDisks, nil
|
||||
}
|
||||
|
||||
// DeleteVMDisks - Delete VM disks from host node.
|
||||
// By default the VM disks are deteled when the VM is deleted,
|
||||
// so mainly this is used to delete the disks in case VM creation didn't complete.
|
||||
func (c *Client) DeleteVMDisks(
|
||||
node string,
|
||||
disks []string,
|
||||
) error {
|
||||
for _, fullDiskName := range disks {
|
||||
storageName, volumeName := getStorageAndVolumeName(fullDiskName, ":")
|
||||
url := fmt.Sprintf("/nodes/%s/storage/%s/content/%s", node, storageName, volumeName)
|
||||
_, err := c.session.Post(url, nil, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getStorageAndVolumeName - Extract disk storage and disk volume, since disk name is saved
|
||||
// in Proxmox with its storage.
|
||||
func getStorageAndVolumeName(
|
||||
fullDiskName string,
|
||||
separator string,
|
||||
) (storageName string, diskName string) {
|
||||
storageAndVolumeName := strings.Split(fullDiskName, separator)
|
||||
storageName, volumeName := storageAndVolumeName[0], storageAndVolumeName[1]
|
||||
|
||||
// when disk type is dir, volumeName is `file=local:100/vm-100-disk-0.raw`
|
||||
re := regexp.MustCompile(`\d+/(?P<filename>\S+.\S+)`)
|
||||
match := re.FindStringSubmatch(volumeName)
|
||||
if len(match) == 2 {
|
||||
volumeName = match[1]
|
||||
}
|
||||
|
||||
return storageName, volumeName
|
||||
}
|
||||
@ -0,0 +1,706 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
QemuDevices map[int]map[string]interface{}
|
||||
QemuDevice map[string]interface{}
|
||||
QemuDeviceParam []string
|
||||
)
|
||||
|
||||
// ConfigQemu - Proxmox API QEMU options
|
||||
type ConfigQemu struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"desc"`
|
||||
Onboot bool `json:"onboot"`
|
||||
Agent string `json:"agent"`
|
||||
Memory int `json:"memory"`
|
||||
QemuOs string `json:"os"`
|
||||
QemuCores int `json:"cores"`
|
||||
QemuSockets int `json:"sockets"`
|
||||
QemuIso string `json:"iso"`
|
||||
FullClone *int `json:"fullclone"`
|
||||
QemuDisks QemuDevices `json:"disk"`
|
||||
QemuNetworks QemuDevices `json:"network"`
|
||||
|
||||
// Deprecated single disk.
|
||||
DiskSize float64 `json:"diskGB"`
|
||||
Storage string `json:"storage"`
|
||||
StorageType string `json:"storageType"` // virtio|scsi (cloud-init defaults to scsi)
|
||||
|
||||
// Deprecated single nic.
|
||||
QemuNicModel string `json:"nic"`
|
||||
QemuBrige string `json:"bridge"`
|
||||
QemuVlanTag int `json:"vlan"`
|
||||
QemuMacAddr string `json:"mac"`
|
||||
|
||||
// cloud-init options
|
||||
CIuser string `json:"ciuser"`
|
||||
CIpassword string `json:"cipassword"`
|
||||
|
||||
Searchdomain string `json:"searchdomain"`
|
||||
Nameserver string `json:"nameserver"`
|
||||
Sshkeys string `json:"sshkeys"`
|
||||
|
||||
// arrays are hard, support 2 interfaces for now
|
||||
Ipconfig0 string `json:"ipconfig0"`
|
||||
Ipconfig1 string `json:"ipconfig1"`
|
||||
}
|
||||
|
||||
// CreateVm - Tell Proxmox API to make the VM
|
||||
func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) {
|
||||
if config.HasCloudInit() {
|
||||
return errors.New("Cloud-init parameters only supported on clones or updates")
|
||||
}
|
||||
vmr.SetVmType("qemu")
|
||||
|
||||
params := map[string]interface{}{
|
||||
"vmid": vmr.vmId,
|
||||
"name": config.Name,
|
||||
"onboot": config.Onboot,
|
||||
"agent": config.Agent,
|
||||
"ide2": config.QemuIso + ",media=cdrom",
|
||||
"ostype": config.QemuOs,
|
||||
"sockets": config.QemuSockets,
|
||||
"cores": config.QemuCores,
|
||||
"cpu": "host",
|
||||
"memory": config.Memory,
|
||||
"description": config.Description,
|
||||
}
|
||||
|
||||
// Create disks config.
|
||||
config.CreateQemuDisksParams(vmr.vmId, params, false)
|
||||
|
||||
// Create networks config.
|
||||
config.CreateQemuNetworksParams(vmr.vmId, params)
|
||||
|
||||
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)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// HasCloudInit - are there cloud-init options?
|
||||
func (config ConfigQemu) HasCloudInit() bool {
|
||||
return config.CIuser != "" ||
|
||||
config.CIpassword != "" ||
|
||||
config.Searchdomain != "" ||
|
||||
config.Nameserver != "" ||
|
||||
config.Sshkeys != "" ||
|
||||
config.Ipconfig0 != "" ||
|
||||
config.Ipconfig1 != ""
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
CloneVm
|
||||
Example: Request
|
||||
|
||||
nodes/proxmox1-xx/qemu/1012/clone
|
||||
|
||||
newid:145
|
||||
name:tf-clone1
|
||||
target:proxmox1-xx
|
||||
full:1
|
||||
storage:xxx
|
||||
|
||||
*/
|
||||
func (config ConfigQemu) CloneVm(sourceVmr *VmRef, vmr *VmRef, client *Client) (err error) {
|
||||
vmr.SetVmType("qemu")
|
||||
fullclone := "1"
|
||||
if config.FullClone != nil {
|
||||
fullclone = strconv.Itoa(*config.FullClone)
|
||||
}
|
||||
storage := config.Storage
|
||||
if disk0Storage, ok := config.QemuDisks[0]["storage"].(string); ok && len(disk0Storage) > 0 {
|
||||
storage = disk0Storage
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"newid": vmr.vmId,
|
||||
"target": vmr.node,
|
||||
"name": config.Name,
|
||||
"storage": storage,
|
||||
"full": fullclone,
|
||||
}
|
||||
_, err = client.CloneQemuVm(sourceVmr, params)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return config.UpdateConfig(vmr, client)
|
||||
}
|
||||
|
||||
func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
|
||||
configParams := map[string]interface{}{
|
||||
"name": config.Name,
|
||||
"description": config.Description,
|
||||
"onboot": config.Onboot,
|
||||
"agent": config.Agent,
|
||||
"sockets": config.QemuSockets,
|
||||
"cores": config.QemuCores,
|
||||
"memory": config.Memory,
|
||||
}
|
||||
|
||||
// Create disks config.
|
||||
config.CreateQemuDisksParams(vmr.vmId, configParams, true)
|
||||
|
||||
// Create networks config.
|
||||
config.CreateQemuNetworksParams(vmr.vmId, configParams)
|
||||
|
||||
// cloud-init options
|
||||
if config.CIuser != "" {
|
||||
configParams["ciuser"] = config.CIuser
|
||||
}
|
||||
if config.CIpassword != "" {
|
||||
configParams["cipassword"] = config.CIpassword
|
||||
}
|
||||
if config.Searchdomain != "" {
|
||||
configParams["searchdomain"] = config.Searchdomain
|
||||
}
|
||||
if config.Nameserver != "" {
|
||||
configParams["nameserver"] = config.Nameserver
|
||||
}
|
||||
if config.Sshkeys != "" {
|
||||
sshkeyEnc := url.PathEscape(config.Sshkeys + "\n")
|
||||
sshkeyEnc = strings.Replace(sshkeyEnc, "+", "%2B", -1)
|
||||
sshkeyEnc = strings.Replace(sshkeyEnc, "@", "%40", -1)
|
||||
sshkeyEnc = strings.Replace(sshkeyEnc, "=", "%3D", -1)
|
||||
configParams["sshkeys"] = sshkeyEnc
|
||||
}
|
||||
if config.Ipconfig0 != "" {
|
||||
configParams["ipconfig0"] = config.Ipconfig0
|
||||
}
|
||||
if config.Ipconfig1 != "" {
|
||||
configParams["ipconfig1"] = config.Ipconfig1
|
||||
}
|
||||
_, err = client.SetVmConfig(vmr, configParams)
|
||||
return err
|
||||
}
|
||||
|
||||
func NewConfigQemuFromJson(io io.Reader) (config *ConfigQemu, err error) {
|
||||
config = &ConfigQemu{QemuVlanTag: -1}
|
||||
err = json.NewDecoder(io).Decode(config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
log.Println(config)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
rxIso = regexp.MustCompile(`(.*?),media`)
|
||||
rxDeviceID = regexp.MustCompile(`\d+`)
|
||||
rxDiskName = regexp.MustCompile(`(virtio|scsi)\d+`)
|
||||
rxDiskType = regexp.MustCompile(`\D+`)
|
||||
rxNicName = regexp.MustCompile(`net\d+`)
|
||||
)
|
||||
|
||||
func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err error) {
|
||||
var vmConfig map[string]interface{}
|
||||
for ii := 0; ii < 3; ii++ {
|
||||
vmConfig, err = client.GetVmConfig(vmr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
// this can happen:
|
||||
// {"data":{"lock":"clone","digest":"eb54fb9d9f120ba0c3bdf694f73b10002c375c38","description":" qmclone temporary file\n"}})
|
||||
if vmConfig["lock"] == nil {
|
||||
break
|
||||
} else {
|
||||
time.Sleep(8 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
if vmConfig["lock"] != nil {
|
||||
return nil, errors.New("vm locked, could not obtain config")
|
||||
}
|
||||
|
||||
// vmConfig Sample: map[ cpu:host
|
||||
// net0:virtio=62:DF:XX:XX:XX:XX,bridge=vmbr0
|
||||
// ide2:local:iso/xxx-xx.iso,media=cdrom memory:2048
|
||||
// smbios1:uuid=8b3bf833-aad8-4545-xxx-xxxxxxx digest:aa6ce5xxxxx1b9ce33e4aaeff564d4 sockets:1
|
||||
// name:terraform-ubuntu1404-template bootdisk:virtio0
|
||||
// virtio0:ProxmoxxxxISCSI:vm-1014-disk-2,size=4G
|
||||
// description:Base image
|
||||
// cores:2 ostype:l26
|
||||
|
||||
name := ""
|
||||
if _, isSet := vmConfig["name"]; isSet {
|
||||
name = vmConfig["name"].(string)
|
||||
}
|
||||
description := ""
|
||||
if _, isSet := vmConfig["description"]; isSet {
|
||||
description = vmConfig["description"].(string)
|
||||
}
|
||||
onboot := true
|
||||
if _, isSet := vmConfig["onboot"]; isSet {
|
||||
onboot = Itob(int(vmConfig["onboot"].(float64)))
|
||||
}
|
||||
agent := "1"
|
||||
if _, isSet := vmConfig["agent"]; isSet {
|
||||
agent = vmConfig["agent"].(string)
|
||||
}
|
||||
ostype := "other"
|
||||
if _, isSet := vmConfig["ostype"]; isSet {
|
||||
ostype = vmConfig["ostype"].(string)
|
||||
}
|
||||
memory := 0.0
|
||||
if _, isSet := vmConfig["memory"]; isSet {
|
||||
memory = vmConfig["memory"].(float64)
|
||||
}
|
||||
cores := 1.0
|
||||
if _, isSet := vmConfig["cores"]; isSet {
|
||||
cores = vmConfig["cores"].(float64)
|
||||
}
|
||||
sockets := 1.0
|
||||
if _, isSet := vmConfig["sockets"]; isSet {
|
||||
sockets = vmConfig["sockets"].(float64)
|
||||
}
|
||||
config = &ConfigQemu{
|
||||
Name: name,
|
||||
Description: strings.TrimSpace(description),
|
||||
Onboot: onboot,
|
||||
Agent: agent,
|
||||
QemuOs: ostype,
|
||||
Memory: int(memory),
|
||||
QemuCores: int(cores),
|
||||
QemuSockets: int(sockets),
|
||||
QemuVlanTag: -1,
|
||||
QemuDisks: QemuDevices{},
|
||||
QemuNetworks: QemuDevices{},
|
||||
}
|
||||
|
||||
if vmConfig["ide2"] != nil {
|
||||
isoMatch := rxIso.FindStringSubmatch(vmConfig["ide2"].(string))
|
||||
config.QemuIso = isoMatch[1]
|
||||
}
|
||||
|
||||
if _, isSet := vmConfig["ciuser"]; isSet {
|
||||
config.CIuser = vmConfig["ciuser"].(string)
|
||||
}
|
||||
if _, isSet := vmConfig["cipassword"]; isSet {
|
||||
config.CIpassword = vmConfig["cipassword"].(string)
|
||||
}
|
||||
if _, isSet := vmConfig["searchdomain"]; isSet {
|
||||
config.Searchdomain = vmConfig["searchdomain"].(string)
|
||||
}
|
||||
if _, isSet := vmConfig["sshkeys"]; isSet {
|
||||
config.Sshkeys, _ = url.PathUnescape(vmConfig["sshkeys"].(string))
|
||||
}
|
||||
if _, isSet := vmConfig["ipconfig0"]; isSet {
|
||||
config.Ipconfig0 = vmConfig["ipconfig0"].(string)
|
||||
}
|
||||
if _, isSet := vmConfig["ipconfig1"]; isSet {
|
||||
config.Ipconfig1 = vmConfig["ipconfig1"].(string)
|
||||
}
|
||||
|
||||
// Add disks.
|
||||
diskNames := []string{}
|
||||
|
||||
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), ",")
|
||||
|
||||
//
|
||||
id := rxDeviceID.FindStringSubmatch(diskName)
|
||||
diskID, _ := strconv.Atoi(id[0])
|
||||
diskType := rxDiskType.FindStringSubmatch(diskName)[0]
|
||||
storageName, fileName := ParseSubConf(diskConfList[0], ":")
|
||||
|
||||
//
|
||||
diskConfMap := QemuDevice{
|
||||
"type": diskType,
|
||||
"storage": storageName,
|
||||
"file": fileName,
|
||||
}
|
||||
|
||||
// Add rest of device config.
|
||||
diskConfMap.readDeviceConfig(diskConfList[1:])
|
||||
|
||||
// And device config to disks map.
|
||||
if len(diskConfMap) > 0 {
|
||||
config.QemuDisks[diskID] = diskConfMap
|
||||
}
|
||||
}
|
||||
|
||||
// Add networks.
|
||||
nicNameRe := regexp.MustCompile(`net\d+`)
|
||||
nicNames := []string{}
|
||||
|
||||
for k, _ := range vmConfig {
|
||||
if nicName := nicNameRe.FindStringSubmatch(k); len(nicName) > 0 {
|
||||
nicNames = append(nicNames, nicName[0])
|
||||
}
|
||||
}
|
||||
|
||||
for _, nicName := range nicNames {
|
||||
nicConfStr := vmConfig[nicName]
|
||||
nicConfList := strings.Split(nicConfStr.(string), ",")
|
||||
|
||||
//
|
||||
id := rxDeviceID.FindStringSubmatch(nicName)
|
||||
nicID, _ := strconv.Atoi(id[0])
|
||||
model, macaddr := ParseSubConf(nicConfList[0], "=")
|
||||
|
||||
// Add model and MAC address.
|
||||
nicConfMap := QemuDevice{
|
||||
"model": model,
|
||||
"macaddr": macaddr,
|
||||
}
|
||||
|
||||
// Add rest of device config.
|
||||
nicConfMap.readDeviceConfig(nicConfList[1:])
|
||||
|
||||
// And device config to networks.
|
||||
if len(nicConfMap) > 0 {
|
||||
config.QemuNetworks[nicID] = nicConfMap
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Useful waiting for ISO install to complete
|
||||
func WaitForShutdown(vmr *VmRef, client *Client) (err error) {
|
||||
for ii := 0; ii < 100; ii++ {
|
||||
vmState, err := client.GetVmState(vmr)
|
||||
if err != nil {
|
||||
log.Print("Wait error:")
|
||||
log.Println(err)
|
||||
} else if vmState["status"] == "stopped" {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
return errors.New("Not shutdown within wait time")
|
||||
}
|
||||
|
||||
// This is because proxmox create/config API won't let us make usernet devices
|
||||
func SshForwardUsernet(vmr *VmRef, client *Client) (sshPort string, err error) {
|
||||
vmState, err := client.GetVmState(vmr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if vmState["status"] == "stopped" {
|
||||
return "", errors.New("VM must be running first")
|
||||
}
|
||||
sshPort = strconv.Itoa(vmr.VmId() + 22000)
|
||||
_, err = client.MonitorCmd(vmr, "netdev_add user,id=net1,hostfwd=tcp::"+sshPort+"-:22")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = client.MonitorCmd(vmr, "device_add virtio-net-pci,id=net1,netdev=net1,addr=0x13")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// device_del net1
|
||||
// netdev_del net1
|
||||
func RemoveSshForwardUsernet(vmr *VmRef, client *Client) (err error) {
|
||||
vmState, err := client.GetVmState(vmr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if vmState["status"] == "stopped" {
|
||||
return errors.New("VM must be running first")
|
||||
}
|
||||
_, err = client.MonitorCmd(vmr, "device_del net1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = client.MonitorCmd(vmr, "netdev_del net1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MaxVmId(client *Client) (max int, err error) {
|
||||
resp, err := client.GetVmList()
|
||||
vms := resp["data"].([]interface{})
|
||||
max = 0
|
||||
for vmii := range vms {
|
||||
vm := vms[vmii].(map[string]interface{})
|
||||
vmid := int(vm["vmid"].(float64))
|
||||
if vmid > max {
|
||||
max = vmid
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SendKeysString(vmr *VmRef, client *Client, keys string) (err error) {
|
||||
vmState, err := client.GetVmState(vmr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if vmState["status"] == "stopped" {
|
||||
return errors.New("VM must be running first")
|
||||
}
|
||||
for _, r := range keys {
|
||||
c := string(r)
|
||||
lower := strings.ToLower(c)
|
||||
if c != lower {
|
||||
c = "shift-" + lower
|
||||
} else {
|
||||
switch c {
|
||||
case "!":
|
||||
c = "shift-1"
|
||||
case "@":
|
||||
c = "shift-2"
|
||||
case "#":
|
||||
c = "shift-3"
|
||||
case "$":
|
||||
c = "shift-4"
|
||||
case "%%":
|
||||
c = "shift-5"
|
||||
case "^":
|
||||
c = "shift-6"
|
||||
case "&":
|
||||
c = "shift-7"
|
||||
case "*":
|
||||
c = "shift-8"
|
||||
case "(":
|
||||
c = "shift-9"
|
||||
case ")":
|
||||
c = "shift-0"
|
||||
case "_":
|
||||
c = "shift-minus"
|
||||
case "+":
|
||||
c = "shift-equal"
|
||||
case " ":
|
||||
c = "spc"
|
||||
case "/":
|
||||
c = "slash"
|
||||
case "\\":
|
||||
c = "backslash"
|
||||
case ",":
|
||||
c = "comma"
|
||||
case "-":
|
||||
c = "minus"
|
||||
case "=":
|
||||
c = "equal"
|
||||
case ".":
|
||||
c = "dot"
|
||||
case "?":
|
||||
c = "shift-slash"
|
||||
}
|
||||
}
|
||||
_, err = client.MonitorCmd(vmr, "sendkey "+c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(100)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create parameters for each Nic device.
|
||||
func (c ConfigQemu) CreateQemuNetworksParams(vmID int, params map[string]interface{}) error {
|
||||
|
||||
// For backward compatibility.
|
||||
if len(c.QemuNetworks) == 0 && len(c.QemuNicModel) > 0 {
|
||||
deprecatedStyleMap := QemuDevice{
|
||||
"model": c.QemuNicModel,
|
||||
"bridge": c.QemuBrige,
|
||||
"macaddr": c.QemuMacAddr,
|
||||
}
|
||||
|
||||
if c.QemuVlanTag > 0 {
|
||||
deprecatedStyleMap["tag"] = strconv.Itoa(c.QemuVlanTag)
|
||||
}
|
||||
|
||||
c.QemuNetworks[0] = deprecatedStyleMap
|
||||
}
|
||||
|
||||
// For new style with multi net device.
|
||||
for nicID, nicConfMap := range c.QemuNetworks {
|
||||
|
||||
nicConfParam := QemuDeviceParam{}
|
||||
|
||||
// Set Nic name.
|
||||
qemuNicName := "net" + strconv.Itoa(nicID)
|
||||
|
||||
// Set Mac address.
|
||||
if nicConfMap["macaddr"] == nil || nicConfMap["macaddr"].(string) == "" {
|
||||
// Generate Mac based on VmID and NicID so it will be the same always.
|
||||
macaddr := make(net.HardwareAddr, 6)
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
rand.Read(macaddr)
|
||||
macaddr[0] = (macaddr[0] | 2) & 0xfe // fix from github issue #18
|
||||
macAddrUppr := strings.ToUpper(fmt.Sprintf("%v", macaddr))
|
||||
// use model=mac format for older proxmox compatability
|
||||
macAddr := fmt.Sprintf("%v=%v", nicConfMap["model"], macAddrUppr)
|
||||
|
||||
// Add Mac to source map so it will be returned. (useful for some use case like Terraform)
|
||||
nicConfMap["macaddr"] = macAddrUppr
|
||||
// and also add it to the parameters which will be sent to Proxmox API.
|
||||
nicConfParam = append(nicConfParam, macAddr)
|
||||
} else {
|
||||
macAddr := fmt.Sprintf("%v=%v", nicConfMap["model"], nicConfMap["macaddr"].(string))
|
||||
nicConfParam = append(nicConfParam, macAddr)
|
||||
}
|
||||
|
||||
// Set bridge if not nat.
|
||||
if nicConfMap["bridge"].(string) != "nat" {
|
||||
bridge := fmt.Sprintf("bridge=%v", nicConfMap["bridge"])
|
||||
nicConfParam = append(nicConfParam, bridge)
|
||||
}
|
||||
|
||||
// Keys that are not used as real/direct conf.
|
||||
ignoredKeys := []string{"id", "bridge", "macaddr", "model"}
|
||||
|
||||
// Rest of config.
|
||||
nicConfParam = nicConfParam.createDeviceParam(nicConfMap, ignoredKeys)
|
||||
|
||||
// Add nic to Qemu prams.
|
||||
params[qemuNicName] = strings.Join(nicConfParam, ",")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create parameters for each disk.
|
||||
func (c ConfigQemu) CreateQemuDisksParams(
|
||||
vmID int,
|
||||
params map[string]interface{},
|
||||
cloned bool,
|
||||
) error {
|
||||
|
||||
// For backward compatibility.
|
||||
if len(c.QemuDisks) == 0 && len(c.Storage) > 0 {
|
||||
|
||||
dType := c.StorageType
|
||||
if dType == "" {
|
||||
if c.HasCloudInit() {
|
||||
dType = "scsi"
|
||||
} else {
|
||||
dType = "virtio"
|
||||
}
|
||||
}
|
||||
deprecatedStyleMap := QemuDevice{
|
||||
"type": dType,
|
||||
"storage": c.Storage,
|
||||
"size": c.DiskSize,
|
||||
"storage_type": "lvm", // default old style
|
||||
"cache": "none", // default old value
|
||||
}
|
||||
|
||||
c.QemuDisks[0] = deprecatedStyleMap
|
||||
}
|
||||
|
||||
// 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, and Directory are considered.
|
||||
// Other formats are not verified, but could be added if they're needed.
|
||||
rxStorageTypes := `(zfspool|lvm)`
|
||||
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, ",")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create the parameters for each device that will be sent to Proxmox API.
|
||||
func (p QemuDeviceParam) createDeviceParam(
|
||||
deviceConfMap QemuDevice,
|
||||
ignoredKeys []string,
|
||||
) QemuDeviceParam {
|
||||
|
||||
for key, value := range deviceConfMap {
|
||||
if ignored := inArray(ignoredKeys, key); !ignored {
|
||||
var confValue interface{}
|
||||
if bValue, ok := value.(bool); ok && bValue {
|
||||
confValue = "1"
|
||||
} else if sValue, ok := value.(string); ok && len(sValue) > 0 {
|
||||
confValue = sValue
|
||||
} else if iValue, ok := value.(int); ok && iValue > 0 {
|
||||
confValue = iValue
|
||||
}
|
||||
if confValue != nil {
|
||||
deviceConf := fmt.Sprintf("%v=%v", key, confValue)
|
||||
p = append(p, deviceConf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// readDeviceConfig - get standard sub-conf strings where `key=value` and update conf map.
|
||||
func (confMap QemuDevice) readDeviceConfig(confList []string) error {
|
||||
// Add device config.
|
||||
for _, conf := range confList {
|
||||
key, value := ParseSubConf(conf, "=")
|
||||
confMap[key] = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ConfigQemu) String() string {
|
||||
jsConf, _ := json.Marshal(c)
|
||||
return string(jsConf)
|
||||
}
|
||||
@ -0,0 +1,319 @@
|
||||
package proxmox
|
||||
|
||||
// inspired by https://github.com/openstack/golang-client/blob/master/openstack/session.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
var Debug = new(bool)
|
||||
|
||||
type Response struct {
|
||||
Resp *http.Response
|
||||
Body []byte
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
httpClient *http.Client
|
||||
ApiUrl string
|
||||
AuthTicket string
|
||||
CsrfToken string
|
||||
Headers http.Header
|
||||
}
|
||||
|
||||
func NewSession(apiUrl string, hclient *http.Client, tls *tls.Config) (session *Session, err error) {
|
||||
if hclient == nil {
|
||||
// Only build a transport if we're also building the client
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: tls,
|
||||
DisableCompression: true,
|
||||
}
|
||||
hclient = &http.Client{Transport: tr}
|
||||
}
|
||||
session = &Session{
|
||||
httpClient: hclient,
|
||||
ApiUrl: apiUrl,
|
||||
AuthTicket: "",
|
||||
CsrfToken: "",
|
||||
Headers: http.Header{},
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func ParamsToBody(params map[string]interface{}) (body []byte) {
|
||||
vals := url.Values{}
|
||||
for k, intrV := range params {
|
||||
var v string
|
||||
switch intrV.(type) {
|
||||
// Convert true/false bool to 1/0 string where Proxmox API can understand it.
|
||||
case bool:
|
||||
if intrV.(bool) {
|
||||
v = "1"
|
||||
} else {
|
||||
v = "0"
|
||||
}
|
||||
default:
|
||||
v = fmt.Sprintf("%v", intrV)
|
||||
}
|
||||
vals.Set(k, v)
|
||||
}
|
||||
body = bytes.NewBufferString(vals.Encode()).Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func decodeResponse(resp *http.Response, v interface{}) error {
|
||||
if resp.Body == nil {
|
||||
return nil
|
||||
}
|
||||
rbody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading response body: %s", err)
|
||||
}
|
||||
if err = json.Unmarshal(rbody, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResponseJSON(resp *http.Response) (jbody map[string]interface{}, err error) {
|
||||
err = decodeResponse(resp, &jbody)
|
||||
return jbody, err
|
||||
}
|
||||
|
||||
func TypedResponse(resp *http.Response, v interface{}) error {
|
||||
var intermediate struct {
|
||||
Data struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
} `json:"data"`
|
||||
}
|
||||
err := decodeResponse(resp, &intermediate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading response envelope: %v", err)
|
||||
}
|
||||
if err = json.Unmarshal(intermediate.Data.Result, v); err != nil {
|
||||
return fmt.Errorf("error unmarshalling result %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) Login(username string, password string) (err error) {
|
||||
reqbody := ParamsToBody(map[string]interface{}{"username": username, "password": password})
|
||||
olddebug := *Debug
|
||||
*Debug = false // don't share passwords in debug log
|
||||
resp, err := s.Post("/access/ticket", nil, nil, &reqbody)
|
||||
*Debug = olddebug
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp == nil {
|
||||
return errors.New("Login error reading response")
|
||||
}
|
||||
dr, _ := httputil.DumpResponse(resp, true)
|
||||
jbody, err := ResponseJSON(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if jbody == nil || jbody["data"] == nil {
|
||||
return fmt.Errorf("Invalid login response:\n-----\n%s\n-----", dr)
|
||||
}
|
||||
dat := jbody["data"].(map[string]interface{})
|
||||
s.AuthTicket = dat["ticket"].(string)
|
||||
s.CsrfToken = dat["CSRFPreventionToken"].(string)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) NewRequest(method, url string, headers *http.Header, body io.Reader) (req *http.Request, err error) {
|
||||
req, err = http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if headers != nil {
|
||||
req.Header = *headers
|
||||
}
|
||||
if s.AuthTicket != "" {
|
||||
req.Header.Add("Cookie", "PVEAuthCookie="+s.AuthTicket)
|
||||
req.Header.Add("CSRFPreventionToken", s.CsrfToken)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Session) Do(req *http.Request) (*http.Response, error) {
|
||||
// Add session headers
|
||||
for k := range s.Headers {
|
||||
req.Header.Set(k, s.Headers.Get(k))
|
||||
}
|
||||
|
||||
if *Debug {
|
||||
d, _ := httputil.DumpRequestOut(req, true)
|
||||
log.Printf(">>>>>>>>>> REQUEST:\n", string(d))
|
||||
}
|
||||
|
||||
resp, err := s.httpClient.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *Debug {
|
||||
dr, _ := httputil.DumpResponse(resp, true)
|
||||
log.Printf("<<<<<<<<<< RESULT:\n", string(dr))
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
return resp, errors.New(resp.Status)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Perform a simple get to an endpoint
|
||||
func (s *Session) Request(
|
||||
method string,
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
body *[]byte,
|
||||
) (resp *http.Response, err error) {
|
||||
// add params to url here
|
||||
url = s.ApiUrl + url
|
||||
if params != nil {
|
||||
url = url + "?" + params.Encode()
|
||||
}
|
||||
|
||||
// Get the body if one is present
|
||||
var buf io.Reader
|
||||
if body != nil {
|
||||
buf = bytes.NewReader(*body)
|
||||
}
|
||||
|
||||
req, err := s.NewRequest(method, url, headers, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
return s.Do(req)
|
||||
}
|
||||
|
||||
// Perform a simple get to an endpoint and unmarshall returned JSON
|
||||
func (s *Session) RequestJSON(
|
||||
method string,
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
body interface{},
|
||||
responseContainer interface{},
|
||||
) (resp *http.Response, err error) {
|
||||
var bodyjson []byte
|
||||
if body != nil {
|
||||
bodyjson, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// if headers == nil {
|
||||
// headers = &http.Header{}
|
||||
// headers.Add("Content-Type", "application/json")
|
||||
// }
|
||||
|
||||
resp, err = s.Request(method, url, params, headers, &bodyjson)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// err = util.CheckHTTPResponseStatusCode(resp)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
rbody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp, errors.New("error reading response body")
|
||||
}
|
||||
if err = json.Unmarshal(rbody, &responseContainer); err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Session) Delete(
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
) (resp *http.Response, err error) {
|
||||
return s.Request("DELETE", url, params, headers, nil)
|
||||
}
|
||||
|
||||
func (s *Session) Get(
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
) (resp *http.Response, err error) {
|
||||
return s.Request("GET", url, params, headers, nil)
|
||||
}
|
||||
|
||||
func (s *Session) GetJSON(
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
responseContainer interface{},
|
||||
) (resp *http.Response, err error) {
|
||||
return s.RequestJSON("GET", url, params, headers, nil, responseContainer)
|
||||
}
|
||||
|
||||
func (s *Session) Head(
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
) (resp *http.Response, err error) {
|
||||
return s.Request("HEAD", url, params, headers, nil)
|
||||
}
|
||||
|
||||
func (s *Session) Post(
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
body *[]byte,
|
||||
) (resp *http.Response, err error) {
|
||||
if headers == nil {
|
||||
headers = &http.Header{}
|
||||
headers.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
return s.Request("POST", url, params, headers, body)
|
||||
}
|
||||
|
||||
func (s *Session) PostJSON(
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
body interface{},
|
||||
responseContainer interface{},
|
||||
) (resp *http.Response, err error) {
|
||||
return s.RequestJSON("POST", url, params, headers, body, responseContainer)
|
||||
}
|
||||
|
||||
func (s *Session) Put(
|
||||
url string,
|
||||
params *url.Values,
|
||||
headers *http.Header,
|
||||
body *[]byte,
|
||||
) (resp *http.Response, err error) {
|
||||
if headers == nil {
|
||||
headers = &http.Header{}
|
||||
headers.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
return s.Request("PUT", url, params, headers, body)
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func inArray(arr []string, str string) bool {
|
||||
for _, elem := range arr {
|
||||
if elem == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func Itob(i int) bool {
|
||||
if i == 1 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ParseSubConf - Parse standard sub-conf strings `key=value`.
|
||||
func ParseSubConf(
|
||||
element string,
|
||||
separator string,
|
||||
) (key string, value interface{}) {
|
||||
if strings.Contains(element, separator) {
|
||||
conf := strings.Split(element, separator)
|
||||
key, value := conf[0], conf[1]
|
||||
var interValue interface{}
|
||||
|
||||
// Make sure to add value in right type,
|
||||
// because all subconfig are returned as strings from Proxmox API.
|
||||
if iValue, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||||
interValue = int(iValue)
|
||||
} else if bValue, err := strconv.ParseBool(value); err == nil {
|
||||
interValue = bValue
|
||||
} else {
|
||||
interValue = value
|
||||
}
|
||||
return key, interValue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseConf - Parse standard device conf string `key1=val1,key2=val2`.
|
||||
func ParseConf(
|
||||
kvString string,
|
||||
confSeparator string,
|
||||
subConfSeparator string,
|
||||
) QemuDevice {
|
||||
var confMap = QemuDevice{}
|
||||
confList := strings.Split(kvString, confSeparator)
|
||||
for _, item := range confList {
|
||||
key, value := ParseSubConf(item, subConfSeparator)
|
||||
confMap[key] = value
|
||||
}
|
||||
return confMap
|
||||
}
|
||||
Loading…
Reference in new issue