Initial commit

This commit is contained in:
2023-02-14 15:48:24 +01:00
commit 6dba571338
58 changed files with 7755 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
package common
import (
"fmt"
"github.com/hashicorp/packer-plugin-sdk/packer"
"os"
"path/filepath"
)
// This is the common builder ID to all of these artifacts.
const BuilderId = "packer.xenserver"
type LocalArtifact struct {
dir string
f []string
}
func NewArtifact(dir string) (packer.Artifact, error) {
files := make([]string, 0, 1)
visit := func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
files = append(files, path)
}
return err
}
if err := filepath.Walk(dir, visit); err != nil {
return nil, err
}
return &LocalArtifact{
dir: dir,
f: files,
}, nil
}
func (*LocalArtifact) BuilderId() string {
return BuilderId
}
func (a *LocalArtifact) Files() []string {
return a.f
}
func (*LocalArtifact) Id() string {
return "VM"
}
func (a *LocalArtifact) String() string {
return fmt.Sprintf("VM files in directory: %s", a.dir)
}
func (a *LocalArtifact) State(name string) interface{} {
return nil
}
func (a *LocalArtifact) Destroy() error {
return os.RemoveAll(a.dir)
}

View File

@@ -0,0 +1,974 @@
package common
import (
"encoding/xml"
"errors"
"fmt"
"log"
xmlrpc "github.com/amfranz/go-xmlrpc-client"
xenapi "github.com/terra-farm/go-xen-api-client"
)
type XenAPIClient struct {
Session interface{}
Host string
Url string
Username string
Password string
RPC *xmlrpc.Client
}
type APIResult struct {
Status string
Value interface{}
ErrorDescription string
}
type XenAPIObject struct {
Ref string
Client *XenAPIClient
}
type Host XenAPIObject
type VM XenAPIObject
type SR XenAPIObject
type VDI XenAPIObject
type Network XenAPIObject
type VBD XenAPIObject
type VIF XenAPIObject
type PIF XenAPIObject
type Pool XenAPIObject
type Task XenAPIObject
type VDIType int
const (
_ VDIType = iota
Disk
CD
Floppy
)
type TaskStatusType int
const (
_ TaskStatusType = iota
Pending
Success
Failure
Cancelling
Cancelled
)
func (c *XenAPIClient) RPCCall(result interface{}, method string, params []interface{}) (err error) {
fmt.Println(params)
p := xmlrpc.Params{Params: params}
err = c.RPC.Call(method, p, result)
return err
}
func (client *XenAPIClient) Login() (err error) {
//Do loging call
result := xmlrpc.Struct{}
params := make([]interface{}, 2)
params[0] = client.Username
params[1] = client.Password
err = client.RPCCall(&result, "session.login_with_password", params)
client.Session = result["Value"]
return err
}
func (client *XenAPIClient) APICall(result *APIResult, method string, params ...interface{}) (err error) {
if client.Session == nil {
fmt.Println("Error: no session")
return fmt.Errorf("No session. Unable to make call")
}
//Make a params slice which will include the session
p := make([]interface{}, len(params)+1)
p[0] = client.Session
if params != nil {
for idx, element := range params {
p[idx+1] = element
}
}
res := xmlrpc.Struct{}
err = client.RPCCall(&res, method, p)
if err != nil {
return err
}
result.Status = res["Status"].(string)
if result.Status != "Success" {
fmt.Println("Encountered an API error: ", result.Status)
fmt.Println(res["ErrorDescription"])
return fmt.Errorf("API Error: %s", res["ErrorDescription"])
} else {
result.Value = res["Value"]
}
return
}
func (client *XenAPIClient) GetHosts() (hosts []*Host, err error) {
hosts = make([]*Host, 0)
result := APIResult{}
_ = client.APICall(&result, "host.get_all")
for _, elem := range result.Value.([]interface{}) {
host := new(Host)
host.Ref = elem.(string)
host.Client = client
hosts = append(hosts, host)
}
return
}
func (client *XenAPIClient) GetPools() (pools []*Pool, err error) {
pools = make([]*Pool, 0)
result := APIResult{}
err = client.APICall(&result, "pool.get_all")
if err != nil {
return pools, err
}
for _, elem := range result.Value.([]interface{}) {
pool := new(Pool)
pool.Ref = elem.(string)
pool.Client = client
pools = append(pools, pool)
}
return pools, nil
}
func (client *XenAPIClient) GetDefaultSR() (sr *SR, err error) {
pools, err := client.GetPools()
if err != nil {
return nil, err
}
pool_rec, err := pools[0].GetRecord()
if err != nil {
return nil, err
}
if pool_rec["default_SR"] == "" {
return nil, errors.New("No default_SR specified for the pool.")
}
sr = new(SR)
sr.Ref = pool_rec["default_SR"].(string)
sr.Client = client
return sr, nil
}
func (client *XenAPIClient) GetVMByUuid(vm_uuid string) (vm *VM, err error) {
vm = new(VM)
result := APIResult{}
err = client.APICall(&result, "VM.get_by_uuid", vm_uuid)
if err != nil {
return nil, err
}
vm.Ref = result.Value.(string)
vm.Client = client
return
}
func (client *XenAPIClient) GetVMByNameLabel(name_label string) (vms []*VM, err error) {
vms = make([]*VM, 0)
result := APIResult{}
err = client.APICall(&result, "VM.get_by_name_label", name_label)
if err != nil {
return vms, err
}
for _, elem := range result.Value.([]interface{}) {
vm := new(VM)
vm.Ref = elem.(string)
vm.Client = client
vms = append(vms, vm)
}
return vms, nil
}
func (client *XenAPIClient) GetNetworkByUuid(network_uuid string) (network *Network, err error) {
network = new(Network)
result := APIResult{}
err = client.APICall(&result, "network.get_by_uuid", network_uuid)
if err != nil {
return nil, err
}
network.Ref = result.Value.(string)
network.Client = client
return
}
func (client *XenAPIClient) GetNetworkByNameLabel(name_label string) (networks []*Network, err error) {
networks = make([]*Network, 0)
result := APIResult{}
err = client.APICall(&result, "network.get_by_name_label", name_label)
if err != nil {
return networks, err
}
for _, elem := range result.Value.([]interface{}) {
network := new(Network)
network.Ref = elem.(string)
network.Client = client
networks = append(networks, network)
}
return networks, nil
}
func (client *XenAPIClient) GetVdiByNameLabel(name_label string) (vdis []*VDI, err error) {
vdis = make([]*VDI, 0)
result := APIResult{}
err = client.APICall(&result, "VDI.get_by_name_label", name_label)
if err != nil {
return vdis, err
}
for _, elem := range result.Value.([]interface{}) {
vdi := new(VDI)
vdi.Ref = elem.(string)
vdi.Client = client
vdis = append(vdis, vdi)
}
return vdis, nil
}
func (client *XenAPIClient) GetVdiByUuid(vdi_uuid string) (vdi *VDI, err error) {
vdi = new(VDI)
result := APIResult{}
err = client.APICall(&result, "VDI.get_by_uuid", vdi_uuid)
if err != nil {
return nil, err
}
vdi.Ref = result.Value.(string)
vdi.Client = client
return
}
func (client *XenAPIClient) GetPIFs() (pifs []*PIF, err error) {
pifs = make([]*PIF, 0)
result := APIResult{}
err = client.APICall(&result, "PIF.get_all")
if err != nil {
return pifs, err
}
for _, elem := range result.Value.([]interface{}) {
pif := new(PIF)
pif.Ref = elem.(string)
pif.Client = client
pifs = append(pifs, pif)
}
return pifs, nil
}
// Host associated functions
func (self *Host) GetSoftwareVersion() (versions map[string]interface{}, err error) {
versions = make(map[string]interface{})
result := APIResult{}
err = self.Client.APICall(&result, "host.get_software_version", self.Ref)
if err != nil {
return nil, err
}
for k, v := range result.Value.(xmlrpc.Struct) {
versions[k] = v.(string)
}
return
}
func (self *Host) CallPlugin(plugin, function string, args map[string]string) (res string, err error) {
args_rec := make(xmlrpc.Struct)
for key, value := range args {
args_rec[key] = value
}
result := APIResult{}
err = self.Client.APICall(&result, "host.call_plugin", self.Ref, plugin, function, args_rec)
if err != nil {
return "", err
}
// The plugin should return a string value
res = result.Value.(string)
return
}
// VM associated functions
func (self *VM) Clone(label string) (new_instance *VM, err error) {
new_instance = new(VM)
result := APIResult{}
err = self.Client.APICall(&result, "VM.clone", self.Ref, label)
if err != nil {
return nil, err
}
new_instance.Ref = result.Value.(string)
new_instance.Client = self.Client
return
}
func (self *VM) Destroy() (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.destroy", self.Ref)
if err != nil {
return err
}
return
}
func (self *VM) Start(paused, force bool) (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.start", self.Ref, paused, force)
if err != nil {
return err
}
return
}
func (self *VM) CleanShutdown() (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.clean_shutdown", self.Ref)
if err != nil {
return err
}
return
}
func Unpause(c *Connection, vmRef xenapi.VMRef) (err error) {
err = c.client.VM.Unpause(c.session, vmRef)
if err != nil {
return err
}
return
}
func (self *VM) SetHVMBoot(policy, bootOrder string) (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.set_HVM_boot_policy", self.Ref, policy)
if err != nil {
return err
}
result = APIResult{}
params := make(xmlrpc.Struct)
params["order"] = bootOrder
err = self.Client.APICall(&result, "VM.set_HVM_boot_params", self.Ref, params)
if err != nil {
return err
}
return
}
func (self *VM) SetPVBootloader(pv_bootloader, pv_args string) (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.set_PV_bootloader", self.Ref, pv_bootloader)
if err != nil {
return err
}
result = APIResult{}
err = self.Client.APICall(&result, "VM.set_PV_bootloader_args", self.Ref, pv_args)
if err != nil {
return err
}
return
}
func (self *VM) GetDomainId() (domid string, err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.get_domid", self.Ref)
if err != nil {
return "", err
}
domid = result.Value.(string)
return domid, nil
}
func (self *VM) GetPowerState() (state string, err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.get_power_state", self.Ref)
if err != nil {
return "", err
}
state = result.Value.(string)
return state, nil
}
func (self *VM) GetUuid() (uuid string, err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.get_uuid", self.Ref)
if err != nil {
return "", err
}
uuid = result.Value.(string)
return uuid, nil
}
func (self *VM) GetVBDs() (vbds []VBD, err error) {
vbds = make([]VBD, 0)
result := APIResult{}
err = self.Client.APICall(&result, "VM.get_VBDs", self.Ref)
if err != nil {
return vbds, err
}
for _, elem := range result.Value.([]interface{}) {
vbd := VBD{}
vbd.Ref = elem.(string)
vbd.Client = self.Client
vbds = append(vbds, vbd)
}
return vbds, nil
}
func GetDisks(c *Connection, vmRef xenapi.VMRef) (vdis []xenapi.VDIRef, err error) {
// Return just data disks (non-isos)
vdis = make([]xenapi.VDIRef, 0)
vbds, err := c.client.VM.GetVBDs(c.session, vmRef)
if err != nil {
return nil, err
}
for _, vbd := range vbds {
rec, err := c.client.VBD.GetRecord(c.session, vbd)
if err != nil {
return nil, err
}
if rec.Type == "Disk" {
vdi, err := c.client.VBD.GetVDI(c.session, vbd)
if err != nil {
return nil, err
}
vdis = append(vdis, vdi)
}
}
return vdis, nil
}
func (self *VM) GetGuestMetricsRef() (ref string, err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.get_guest_metrics", self.Ref)
if err != nil {
return "", nil
}
ref = result.Value.(string)
return ref, err
}
func (self *VM) GetGuestMetrics() (metrics map[string]interface{}, err error) {
metrics_ref, err := self.GetGuestMetricsRef()
if err != nil {
return nil, err
}
if metrics_ref == "OpaqueRef:NULL" {
return nil, nil
}
result := APIResult{}
err = self.Client.APICall(&result, "VM_guest_metrics.get_record", metrics_ref)
if err != nil {
return nil, err
}
return result.Value.(xmlrpc.Struct), nil
}
func (self *VM) SetStaticMemoryRange(min, max uint) (err error) {
result := APIResult{}
strMin := fmt.Sprintf("%d", min)
strMax := fmt.Sprintf("%d", max)
err = self.Client.APICall(&result, "VM.set_memory_limits", self.Ref, strMin, strMax, strMin, strMax)
if err != nil {
return err
}
return
}
func ConnectVdi(c *Connection, vmRef xenapi.VMRef, vdiRef xenapi.VDIRef, vbdType xenapi.VbdType) (err error) {
var mode xenapi.VbdMode
var unpluggable bool
var bootable bool
var t xenapi.VbdType
switch vbdType {
case xenapi.VbdTypeCD:
mode = xenapi.VbdModeRO
bootable = true
unpluggable = false
t = xenapi.VbdTypeCD
case xenapi.VbdTypeDisk:
mode = xenapi.VbdModeRW
bootable = false
unpluggable = false
t = xenapi.VbdTypeDisk
case xenapi.VbdTypeFloppy:
mode = xenapi.VbdModeRW
bootable = false
unpluggable = true
t = xenapi.VbdTypeFloppy
}
vbd_ref, err := c.client.VBD.Create(c.session, xenapi.VBDRecord{
VM: xenapi.VMRef(vmRef),
VDI: xenapi.VDIRef(vdiRef),
Userdevice: "autodetect",
Empty: false,
// OtherConfig: map[string]interface{{}},
QosAlgorithmType: "",
// QosAlgorithmParams: map[string]interface{{}},
Mode: mode,
Unpluggable: unpluggable,
Bootable: bootable,
Type: t,
})
if err != nil {
return err
}
fmt.Println("VBD Ref:", vbd_ref)
uuid, err := c.client.VBD.GetUUID(c.session, vbd_ref)
fmt.Println("VBD UUID: ", uuid)
/*
// 2. Plug VBD (Non need - the VM hasn't booted.
// @todo - check VM state
result = APIResult{}
err = self.Client.APICall(&result, "VBD.plug", vbd_ref)
if err != nil {
return err
}
*/
return
}
func DisconnectVdi(c *Connection, vmRef xenapi.VMRef, vdi xenapi.VDIRef) error {
vbds, err := c.client.VM.GetVBDs(c.session, vmRef)
if err != nil {
return fmt.Errorf("Unable to get VM VBDs: %s", err.Error())
}
for _, vbd := range vbds {
rec, err := c.client.VBD.GetRecord(c.session, vbd)
if err != nil {
return fmt.Errorf("Could not get record for VBD '%s': %s", vbd, err.Error())
}
recVdi := rec.VDI
if recVdi == vdi {
_ = c.client.VBD.Unplug(c.session, vbd)
err = c.client.VBD.Destroy(c.session, vbd)
if err != nil {
return fmt.Errorf("Could not destroy VBD '%s': %s", vbd, err.Error())
}
return nil
} else {
log.Printf("Could not find VDI record in VBD '%s'", vbd)
}
}
return fmt.Errorf("Could not find VBD for VDI '%s'", vdi)
}
func (self *VM) SetPlatform(params map[string]string) (err error) {
result := APIResult{}
platform_rec := make(xmlrpc.Struct)
for key, value := range params {
platform_rec[key] = value
}
err = self.Client.APICall(&result, "VM.set_platform", self.Ref, platform_rec)
if err != nil {
return err
}
return
}
func ConnectNetwork(c *Connection, networkRef xenapi.NetworkRef, vmRef xenapi.VMRef, device string) (*xenapi.VIFRef, error) {
vif, err := c.client.VIF.Create(c.session, xenapi.VIFRecord{
Network: networkRef,
VM: vmRef,
Device: device,
LockingMode: xenapi.VifLockingModeNetworkDefault,
})
if err != nil {
return nil, err
}
log.Printf("Created the following VIF: %s", vif)
return &vif, nil
}
// Setters
func (self *VM) SetIsATemplate(is_a_template bool) (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VM.set_is_a_template", self.Ref, is_a_template)
if err != nil {
return err
}
return
}
// SR associated functions
func (self *SR) CreateVdi(name_label string, size int64) (vdi *VDI, err error) {
vdi = new(VDI)
vdi_rec := make(xmlrpc.Struct)
vdi_rec["name_label"] = name_label
vdi_rec["SR"] = self.Ref
vdi_rec["virtual_size"] = fmt.Sprintf("%d", size)
vdi_rec["type"] = "user"
vdi_rec["sharable"] = false
vdi_rec["read_only"] = false
oc := make(xmlrpc.Struct)
oc["temp"] = "temp"
vdi_rec["other_config"] = oc
result := APIResult{}
err = self.Client.APICall(&result, "VDI.create", vdi_rec)
if err != nil {
return nil, err
}
vdi.Ref = result.Value.(string)
vdi.Client = self.Client
return
}
// Network associated functions
func (self *Network) GetAssignedIPs() (ip_map map[string]string, err error) {
ip_map = make(map[string]string, 0)
result := APIResult{}
err = self.Client.APICall(&result, "network.get_assigned_ips", self.Ref)
if err != nil {
return ip_map, err
}
for k, v := range result.Value.(xmlrpc.Struct) {
ip_map[k] = v.(string)
}
return ip_map, nil
}
// PIF associated functions
func (self *PIF) GetRecord() (record map[string]interface{}, err error) {
record = make(map[string]interface{})
result := APIResult{}
err = self.Client.APICall(&result, "PIF.get_record", self.Ref)
if err != nil {
return record, err
}
for k, v := range result.Value.(xmlrpc.Struct) {
record[k] = v
}
return record, nil
}
// Pool associated functions
func (self *Pool) GetRecord() (record map[string]interface{}, err error) {
record = make(map[string]interface{})
result := APIResult{}
err = self.Client.APICall(&result, "pool.get_record", self.Ref)
if err != nil {
return record, err
}
for k, v := range result.Value.(xmlrpc.Struct) {
record[k] = v
}
return record, nil
}
// VBD associated functions
func (self *VBD) GetRecord() (record map[string]interface{}, err error) {
record = make(map[string]interface{})
result := APIResult{}
err = self.Client.APICall(&result, "VBD.get_record", self.Ref)
if err != nil {
return record, err
}
for k, v := range result.Value.(xmlrpc.Struct) {
record[k] = v
}
return record, nil
}
func (self *VBD) GetVDI() (vdi *VDI, err error) {
vbd_rec, err := self.GetRecord()
if err != nil {
return nil, err
}
vdi = new(VDI)
vdi.Ref = vbd_rec["VDI"].(string)
vdi.Client = self.Client
return vdi, nil
}
func (self *VBD) Eject() (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VBD.eject", self.Ref)
if err != nil {
return err
}
return nil
}
func (self *VBD) Unplug() (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VBD.unplug", self.Ref)
if err != nil {
return err
}
return nil
}
func (self *VBD) Destroy() (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VBD.destroy", self.Ref)
if err != nil {
return err
}
return nil
}
// VIF associated functions
func (self *VIF) Destroy() (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VIF.destroy", self.Ref)
if err != nil {
return err
}
return nil
}
// VDI associated functions
func (self *VDI) GetUuid() (vdi_uuid string, err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VDI.get_uuid", self.Ref)
if err != nil {
return "", err
}
vdi_uuid = result.Value.(string)
return vdi_uuid, nil
}
func (self *VDI) GetVBDs() (vbds []VBD, err error) {
vbds = make([]VBD, 0)
result := APIResult{}
err = self.Client.APICall(&result, "VDI.get_VBDs", self.Ref)
if err != nil {
return vbds, err
}
for _, elem := range result.Value.([]interface{}) {
vbd := VBD{}
vbd.Ref = elem.(string)
vbd.Client = self.Client
vbds = append(vbds, vbd)
}
return vbds, nil
}
func (self *VDI) Destroy() (err error) {
result := APIResult{}
err = self.Client.APICall(&result, "VDI.destroy", self.Ref)
if err != nil {
return err
}
return
}
// Expose a VDI using the Transfer VM
// (Legacy VHD export)
type TransferRecord struct {
UrlFull string `xml:"url_full,attr"`
}
func Expose(c *Connection, vdiRef xenapi.VDIRef, format string) (url string, err error) {
hosts, err := c.client.Host.GetAll(c.session)
if err != nil {
err = errors.New(fmt.Sprintf("Could not retrieve hosts in the pool: %s", err.Error()))
return "", err
}
host := hosts[0]
if err != nil {
err = errors.New(fmt.Sprintf("Failed to get VDI uuid for %s: %s", vdiRef, err.Error()))
return "", err
}
args := make(map[string]string)
args["transfer_mode"] = "http"
args["vdi_uuid"] = string(vdiRef)
args["expose_vhd"] = "true"
args["network_uuid"] = "management"
args["timeout_minutes"] = "5"
handle, err := c.client.Host.CallPlugin(c.session, host, "transfer", "expose", args)
if err != nil {
err = errors.New(fmt.Sprintf("Error whilst exposing VDI %s: %s", vdiRef, err.Error()))
return "", err
}
args = make(map[string]string)
args["record_handle"] = handle
record_xml, err := c.client.Host.CallPlugin(c.session, host, "transfer", "get_record", args)
if err != nil {
err = errors.New(fmt.Sprintf("Unable to retrieve transfer record for VDI %s: %s", vdiRef, err.Error()))
return "", err
}
var record TransferRecord
xml.Unmarshal([]byte(record_xml), &record)
if record.UrlFull == "" {
return "", errors.New(fmt.Sprintf("Error: did not parse XML properly: '%s'", record_xml))
}
// Handles either raw or VHD formats
switch format {
case "vhd":
url = fmt.Sprintf("%s.vhd", record.UrlFull)
case "raw":
url = record.UrlFull
}
return
}
func Unexpose(c *Connection, vdiRef xenapi.VDIRef) (err error) {
disk_uuid, err := c.client.VDI.GetUUID(c.session, vdiRef)
if err != nil {
return err
}
hosts, err := c.client.Host.GetAll(c.session)
if err != nil {
err = errors.New(fmt.Sprintf("Could not retrieve hosts in the pool: %s", err.Error()))
return err
}
host := hosts[0]
args := make(map[string]string)
args["vdi_uuid"] = disk_uuid
result, err := c.client.Host.CallPlugin(c.session, host, "transfer", "unexpose", args)
if err != nil {
return err
}
log.Println(fmt.Sprintf("Unexpose result: %s", result))
return nil
}
// Task associated functions
// func (self *Task) GetResult() (object *XenAPIObject, err error) {
// result := APIResult{}
// err = self.Client.APICall(&result, "task.get_result", self.Ref)
// if err != nil {
// return
// }
// switch ref := result.Value.(type) {
// case string:
// // @fixme: xapi currently sends us an xmlrpc-encoded string via xmlrpc.
// // This seems to be a bug in xapi. Remove this workaround when it's fixed
// re := regexp.MustCompile("^<value><array><data><value>([^<]*)</value>.*</data></array></value>$")
// match := re.FindStringSubmatch(ref)
// if match == nil {
// object = nil
// } else {
// object = &XenAPIObject{
// Ref: match[1],
// Client: self.Client,
// }
// }
// case nil:
// object = nil
// default:
// err = fmt.Errorf("task.get_result: unknown value type %T (expected string or nil)", ref)
// }
// return
// }
// Client Initiator
type Connection struct {
client *xenapi.Client
session xenapi.SessionRef
Host string
Username string
Password string
}
func (c Connection) GetSession() string {
return string(c.session)
}
func NewXenAPIClient(host, username, password string) (*Connection, error) {
client, err := xenapi.NewClient("https://"+host, nil)
if err != nil {
return nil, err
}
session, err := client.Session.LoginWithPassword(username, password, "1.0", "packer")
if err != nil {
return nil, err
}
return &Connection{client, session, host, username, password}, nil
}
func (c *Connection) GetClient() *xenapi.Client {
return c.client
}
func (c *Connection) GetSessionRef() xenapi.SessionRef {
return c.session
}

View File

@@ -0,0 +1,289 @@
package common
import (
"errors"
"fmt"
"os"
"time"
"github.com/hashicorp/packer-plugin-sdk/common"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
xenapi "github.com/terra-farm/go-xen-api-client"
)
type CommonConfig struct {
Username string `mapstructure:"remote_username"`
Password string `mapstructure:"remote_password"`
HostIp string `mapstructure:"remote_host"`
VMName string `mapstructure:"vm_name"`
VMDescription string `mapstructure:"vm_description"`
SrName string `mapstructure:"sr_name"`
SrISOName string `mapstructure:"sr_iso_name"`
FloppyFiles []string `mapstructure:"floppy_files"`
NetworkNames []string `mapstructure:"network_names"`
ExportNetworkNames []string `mapstructure:"export_network_names"`
HostPortMin uint `mapstructure:"host_port_min"`
HostPortMax uint `mapstructure:"host_port_max"`
BootCommand []string `mapstructure:"boot_command"`
ShutdownCommand string `mapstructure:"shutdown_command"`
RawBootWait string `mapstructure:"boot_wait"`
BootWait time.Duration
ToolsIsoName string `mapstructure:"tools_iso_name"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
// SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
// SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
SSHConfig `mapstructure:",squash"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
SSHWaitTimeout time.Duration
OutputDir string `mapstructure:"output_directory"`
Format string `mapstructure:"format"`
KeepVM string `mapstructure:"keep_vm"`
IPGetter string `mapstructure:"ip_getter"`
}
func (c *CommonConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig) []error {
var err error
var errs []error
// Set default values
if c.HostPortMin == 0 {
c.HostPortMin = 5900
}
if c.HostPortMax == 0 {
c.HostPortMax = 6000
}
if c.RawBootWait == "" {
c.RawBootWait = "5s"
}
if c.ToolsIsoName == "" {
c.ToolsIsoName = "xs-tools.iso"
}
if c.HTTPPortMin == 0 {
c.HTTPPortMin = 8000
}
if c.HTTPPortMax == 0 {
c.HTTPPortMax = 9000
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "200m"
}
if c.FloppyFiles == nil {
c.FloppyFiles = make([]string, 0)
}
/*
if c.SSHHostPortMin == 0 {
c.SSHHostPortMin = 2222
}
if c.SSHHostPortMax == 0 {
c.SSHHostPortMax = 4444
}
*/
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
if c.OutputDir == "" {
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
}
if c.VMName == "" {
c.VMName = fmt.Sprintf("packer-%s-{{timestamp}}", pc.PackerBuildName)
}
if c.Format == "" {
c.Format = "xva"
}
if c.KeepVM == "" {
c.KeepVM = "never"
}
if c.IPGetter == "" {
c.IPGetter = "auto"
}
// Validation
if c.Username == "" {
errs = append(errs, errors.New("remote_username must be specified."))
}
if c.Password == "" {
errs = append(errs, errors.New("remote_password must be specified."))
}
if c.HostIp == "" {
errs = append(errs, errors.New("remote_host must be specified."))
}
if c.HostPortMin > c.HostPortMax {
errs = append(errs, errors.New("the host min port must be less than the max"))
}
if c.HTTPPortMin > c.HTTPPortMax {
errs = append(errs, errors.New("the HTTP min port must be less than the max"))
}
c.BootWait, err = time.ParseDuration(c.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed to parse boot_wait: %s", err))
}
if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
}
/*
if c.SSHHostPortMin > c.SSHHostPortMax {
errs = append(errs,
errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
*/
if c.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
}
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed to parse ssh_wait_timeout: %s", err))
}
switch c.Format {
case "xva", "xva_compressed", "vdi_raw", "vdi_vhd", "none":
default:
errs = append(errs, errors.New("format must be one of 'xva', 'vdi_raw', 'vdi_vhd', 'none'"))
}
switch c.KeepVM {
case "always", "never", "on_success":
default:
errs = append(errs, errors.New("keep_vm must be one of 'always', 'never', 'on_success'"))
}
switch c.IPGetter {
case "auto", "tools", "http":
default:
errs = append(errs, errors.New("ip_getter must be one of 'auto', 'tools', 'http'"))
}
return errs
}
// steps should check config.ShouldKeepVM first before cleaning up the VM
func (c CommonConfig) ShouldKeepVM(state multistep.StateBag) bool {
switch c.KeepVM {
case "always":
return true
case "never":
return false
case "on_success":
// only keep instance if build was successful
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
return !(cancelled || halted)
default:
panic(fmt.Sprintf("Unknown keep_vm value '%s'", c.KeepVM))
}
}
func (config CommonConfig) GetSR(c *Connection) (xenapi.SRRef, error) {
var srRef xenapi.SRRef
if config.SrName == "" {
hostRef, err := c.GetClient().Session.GetThisHost(c.session, c.session)
if err != nil {
return srRef, err
}
pools, err := c.GetClient().Pool.GetAllRecords(c.session)
if err != nil {
return srRef, err
}
for _, pool := range pools {
if pool.Master == hostRef {
return pool.DefaultSR, nil
}
}
return srRef, errors.New(fmt.Sprintf("failed to find default SR on host '%s'", hostRef))
} else {
// Use the provided name label to find the SR to use
srs, err := c.GetClient().SR.GetByNameLabel(c.session, config.SrName)
if err != nil {
return srRef, err
}
switch {
case len(srs) == 0:
return srRef, fmt.Errorf("Couldn't find a SR with the specified name-label '%s'", config.SrName)
case len(srs) > 1:
return srRef, fmt.Errorf("Found more than one SR with the name '%s'. The name must be unique", config.SrName)
}
return srs[0], nil
}
}
func (config CommonConfig) GetISOSR(c *Connection) (xenapi.SRRef, error) {
var srRef xenapi.SRRef
if config.SrISOName == "" {
return srRef, errors.New("sr_iso_name must be specified in the packer configuration")
} else {
// Use the provided name label to find the SR to use
srs, err := c.GetClient().SR.GetByNameLabel(c.session, config.SrName)
if err != nil {
return srRef, err
}
switch {
case len(srs) == 0:
return srRef, fmt.Errorf("Couldn't find a SR with the specified name-label '%s'", config.SrName)
case len(srs) > 1:
return srRef, fmt.Errorf("Found more than one SR with the name '%s'. The name must be unique", config.SrName)
}
return srs[0], nil
}
}

View File

@@ -0,0 +1,43 @@
//go:generate mapstructure-to-hcl2 -type Config
package common
import (
"time"
"github.com/hashicorp/packer-plugin-sdk/common"
"github.com/hashicorp/packer-plugin-sdk/communicator"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
CommonConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
VCPUsMax uint `mapstructure:"vcpus_max"`
VCPUsAtStartup uint `mapstructure:"vcpus_atstartup"`
VMMemory uint `mapstructure:"vm_memory"`
DiskSize uint `mapstructure:"disk_size"`
CloneTemplate string `mapstructure:"clone_template"`
VMOtherConfig map[string]string `mapstructure:"vm_other_config"`
ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrls []string `mapstructure:"iso_urls"`
ISOUrl string `mapstructure:"iso_url"`
ISOName string `mapstructure:"iso_name"`
PlatformArgs map[string]string `mapstructure:"platform_args"`
RawInstallTimeout string `mapstructure:"install_timeout"`
InstallTimeout time.Duration ``
SourcePath string `mapstructure:"source_path"`
Firmware string `mapstructure:"firmware"`
ctx interpolate.Context
}
func (c Config) GetInterpContext() *interpolate.Context {
return &c.ctx
}

View File

@@ -0,0 +1,225 @@
// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT.
package common
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
Username *string `mapstructure:"remote_username" cty:"remote_username" hcl:"remote_username"`
Password *string `mapstructure:"remote_password" cty:"remote_password" hcl:"remote_password"`
HostIp *string `mapstructure:"remote_host" cty:"remote_host" hcl:"remote_host"`
VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"`
VMDescription *string `mapstructure:"vm_description" cty:"vm_description" hcl:"vm_description"`
SrName *string `mapstructure:"sr_name" cty:"sr_name" hcl:"sr_name"`
SrISOName *string `mapstructure:"sr_iso_name" cty:"sr_iso_name" hcl:"sr_iso_name"`
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
NetworkNames []string `mapstructure:"network_names" cty:"network_names" hcl:"network_names"`
ExportNetworkNames []string `mapstructure:"export_network_names" cty:"export_network_names" hcl:"export_network_names"`
HostPortMin *uint `mapstructure:"host_port_min" cty:"host_port_min" hcl:"host_port_min"`
HostPortMax *uint `mapstructure:"host_port_max" cty:"host_port_max" hcl:"host_port_max"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
ShutdownCommand *string `mapstructure:"shutdown_command" cty:"shutdown_command" hcl:"shutdown_command"`
RawBootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
ToolsIsoName *string `mapstructure:"tools_iso_name" cty:"tools_iso_name" hcl:"tools_iso_name"`
HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"`
HTTPPortMin *uint `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
HTTPPortMax *uint `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
SSHKeyPath *string `mapstructure:"ssh_key_path" cty:"ssh_key_path" hcl:"ssh_key_path"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHPort *uint `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUser *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHTemporaryKeyPairType *string `mapstructure:"temporary_key_pair_type" cty:"temporary_key_pair_type" hcl:"temporary_key_pair_type"`
SSHTemporaryKeyPairBits *int `mapstructure:"temporary_key_pair_bits" cty:"temporary_key_pair_bits" hcl:"temporary_key_pair_bits"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
SSHHostPortMin *uint `mapstructure:"ssh_host_port_min" cty:"ssh_host_port_min" hcl:"ssh_host_port_min"`
SSHHostPortMax *uint `mapstructure:"ssh_host_port_max" cty:"ssh_host_port_max" hcl:"ssh_host_port_max"`
SSHSkipNatMapping *bool `mapstructure:"ssh_skip_nat_mapping" cty:"ssh_skip_nat_mapping" hcl:"ssh_skip_nat_mapping"`
OutputDir *string `mapstructure:"output_directory" cty:"output_directory" hcl:"output_directory"`
Format *string `mapstructure:"format" cty:"format" hcl:"format"`
KeepVM *string `mapstructure:"keep_vm" cty:"keep_vm" hcl:"keep_vm"`
IPGetter *string `mapstructure:"ip_getter" cty:"ip_getter" hcl:"ip_getter"`
VCPUsMax *uint `mapstructure:"vcpus_max" cty:"vcpus_max" hcl:"vcpus_max"`
VCPUsAtStartup *uint `mapstructure:"vcpus_atstartup" cty:"vcpus_atstartup" hcl:"vcpus_atstartup"`
VMMemory *uint `mapstructure:"vm_memory" cty:"vm_memory" hcl:"vm_memory"`
DiskSize *uint `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
CloneTemplate *string `mapstructure:"clone_template" cty:"clone_template" hcl:"clone_template"`
VMOtherConfig map[string]string `mapstructure:"vm_other_config" cty:"vm_other_config" hcl:"vm_other_config"`
ISOChecksum *string `mapstructure:"iso_checksum" cty:"iso_checksum" hcl:"iso_checksum"`
ISOChecksumType *string `mapstructure:"iso_checksum_type" cty:"iso_checksum_type" hcl:"iso_checksum_type"`
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
ISOUrl *string `mapstructure:"iso_url" cty:"iso_url" hcl:"iso_url"`
ISOName *string `mapstructure:"iso_name" cty:"iso_name" hcl:"iso_name"`
PlatformArgs map[string]string `mapstructure:"platform_args" cty:"platform_args" hcl:"platform_args"`
RawInstallTimeout *string `mapstructure:"install_timeout" cty:"install_timeout" hcl:"install_timeout"`
SourcePath *string `mapstructure:"source_path" cty:"source_path" hcl:"source_path"`
Firmware *string `mapstructure:"firmware" cty:"firmware" hcl:"firmware"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// The decoded values from this spec will then be applied to a FlatConfig.
func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false},
"packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false},
"packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false},
"packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false},
"packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false},
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"remote_username": &hcldec.AttrSpec{Name: "remote_username", Type: cty.String, Required: false},
"remote_password": &hcldec.AttrSpec{Name: "remote_password", Type: cty.String, Required: false},
"remote_host": &hcldec.AttrSpec{Name: "remote_host", Type: cty.String, Required: false},
"vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false},
"vm_description": &hcldec.AttrSpec{Name: "vm_description", Type: cty.String, Required: false},
"sr_name": &hcldec.AttrSpec{Name: "sr_name", Type: cty.String, Required: false},
"sr_iso_name": &hcldec.AttrSpec{Name: "sr_iso_name", Type: cty.String, Required: false},
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"network_names": &hcldec.AttrSpec{Name: "network_names", Type: cty.List(cty.String), Required: false},
"export_network_names": &hcldec.AttrSpec{Name: "export_network_names", Type: cty.List(cty.String), Required: false},
"host_port_min": &hcldec.AttrSpec{Name: "host_port_min", Type: cty.Number, Required: false},
"host_port_max": &hcldec.AttrSpec{Name: "host_port_max", Type: cty.Number, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
"shutdown_command": &hcldec.AttrSpec{Name: "shutdown_command", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"tools_iso_name": &hcldec.AttrSpec{Name: "tools_iso_name", Type: cty.String, Required: false},
"http_directory": &hcldec.AttrSpec{Name: "http_directory", Type: cty.String, Required: false},
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
"ssh_key_path": &hcldec.AttrSpec{Name: "ssh_key_path", Type: cty.String, Required: false},
"ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false},
"ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false},
"ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false},
"communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false},
"pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false},
"ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false},
"ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false},
"temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false},
"temporary_key_pair_type": &hcldec.AttrSpec{Name: "temporary_key_pair_type", Type: cty.String, Required: false},
"temporary_key_pair_bits": &hcldec.AttrSpec{Name: "temporary_key_pair_bits", Type: cty.Number, Required: false},
"ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false},
"ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false},
"ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false},
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
"ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_file", Type: cty.String, Required: false},
"ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false},
"ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false},
"ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false},
"ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false},
"ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false},
"ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false},
"ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false},
"ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false},
"ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false},
"ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false},
"ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false},
"ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false},
"ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false},
"ssh_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_file", Type: cty.String, Required: false},
"ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false},
"ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false},
"ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false},
"ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false},
"ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false},
"ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false},
"ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false},
"ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false},
"ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false},
"winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false},
"winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false},
"winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false},
"winrm_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, Required: false},
"winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false},
"winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false},
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"ssh_host_port_min": &hcldec.AttrSpec{Name: "ssh_host_port_min", Type: cty.Number, Required: false},
"ssh_host_port_max": &hcldec.AttrSpec{Name: "ssh_host_port_max", Type: cty.Number, Required: false},
"ssh_skip_nat_mapping": &hcldec.AttrSpec{Name: "ssh_skip_nat_mapping", Type: cty.Bool, Required: false},
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
"format": &hcldec.AttrSpec{Name: "format", Type: cty.String, Required: false},
"keep_vm": &hcldec.AttrSpec{Name: "keep_vm", Type: cty.String, Required: false},
"ip_getter": &hcldec.AttrSpec{Name: "ip_getter", Type: cty.String, Required: false},
"vcpus_max": &hcldec.AttrSpec{Name: "vcpus_max", Type: cty.Number, Required: false},
"vcpus_atstartup": &hcldec.AttrSpec{Name: "vcpus_atstartup", Type: cty.Number, Required: false},
"vm_memory": &hcldec.AttrSpec{Name: "vm_memory", Type: cty.Number, Required: false},
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
"clone_template": &hcldec.AttrSpec{Name: "clone_template", Type: cty.String, Required: false},
"vm_other_config": &hcldec.AttrSpec{Name: "vm_other_config", Type: cty.Map(cty.String), Required: false},
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
"iso_checksum_type": &hcldec.AttrSpec{Name: "iso_checksum_type", Type: cty.String, Required: false},
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
"iso_name": &hcldec.AttrSpec{Name: "iso_name", Type: cty.String, Required: false},
"platform_args": &hcldec.AttrSpec{Name: "platform_args", Type: cty.Map(cty.String), Required: false},
"install_timeout": &hcldec.AttrSpec{Name: "install_timeout", Type: cty.String, Required: false},
"source_path": &hcldec.AttrSpec{Name: "source_path", Type: cty.String, Required: false},
"firmware": &hcldec.AttrSpec{Name: "firmware", Type: cty.String, Required: false},
}
return s
}

View File

@@ -0,0 +1,25 @@
package common
import (
"fmt"
"log"
"net"
)
// FindPort finds and starts listening on a port in the range [portMin, portMax]
// returns the listener and the port number on success, or nil, 0 on failure
func FindPort(portMin uint, portMax uint) (net.Listener, uint) {
log.Printf("Looking for an available port between %d and %d", portMin, portMax)
for port := portMin; port <= portMax; port++ {
log.Printf("Trying port: %d", port)
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err == nil {
return l, port
} else {
log.Printf("Port %d unavailable: %s", port, err.Error())
}
}
return nil, 0
}

View File

@@ -0,0 +1,127 @@
package common
import (
"crypto/tls"
"fmt"
"log"
"net/http"
"net/url"
"os"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
xsclient "github.com/terra-farm/go-xen-api-client"
)
func appendQuery(urlstring, k, v string) (string, error) {
u, err := url.Parse(urlstring)
if err != nil {
return "", fmt.Errorf("Unable to parse URL '%s': %s", urlstring, err.Error())
}
m := u.Query()
m.Add(k, v)
u.RawQuery = m.Encode()
return u.String(), err
}
func HTTPUpload(import_url string, fh *os.File, state multistep.StateBag) (result string, err error) {
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
task, err := c.client.Task.Create(c.session, "packer-task", "Packer task")
if err != nil {
err = fmt.Errorf("Unable to create task: %s", err.Error())
return
}
defer c.client.Task.Destroy(c.session, task)
import_task_url, err := appendQuery(import_url, "task_id", string(task))
if err != nil {
return
}
// Get file length
fstat, err := fh.Stat()
if err != nil {
err = fmt.Errorf("Unable to stat '%s': %s", fh.Name(), err.Error())
return
}
fileLength := fstat.Size()
// Define a new transport which allows self-signed certs
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
// Create a client
httpClient := &http.Client{Transport: tr}
// Create request and download file
request, err := http.NewRequest("PUT", import_task_url, fh)
request.ContentLength = fileLength
ui.Say(fmt.Sprintf("PUT '%s'", import_task_url))
resp, err := httpClient.Do(request) // Do closes fh for us, according to docs
if err != nil {
return
}
if resp.StatusCode != 200 {
err = fmt.Errorf("PUT request got non-200 status code: %s", resp.Status)
return
}
logIteration := 0
err = InterruptibleWait{
Predicate: func() (bool, error) {
status, err := c.client.Task.GetStatus(c.session, task)
if err != nil {
return false, fmt.Errorf("Failed to get task status: %s", err.Error())
}
switch status {
case xsclient.TaskStatusTypePending:
progress, err := c.client.Task.GetProgress(c.session, task)
if err != nil {
return false, fmt.Errorf("Failed to get progress: %s", err.Error())
}
logIteration = logIteration + 1
if logIteration%5 == 0 {
log.Printf("Upload %.0f%% complete", progress*100)
}
return false, nil
case xsclient.TaskStatusTypeSuccess:
return true, nil
case xsclient.TaskStatusTypeFailure:
errorInfo, err := c.client.Task.GetErrorInfo(c.session, task)
if err != nil {
errorInfo = []string{fmt.Sprintf("furthermore, failed to get error info: %s", err.Error())}
}
return false, fmt.Errorf("Task failed: %s", errorInfo)
case xsclient.TaskStatusTypeCancelling, xsclient.TaskStatusTypeCancelled:
return false, fmt.Errorf("Task cancelled")
default:
return false, fmt.Errorf("Unknown task status %v", status)
}
},
PredicateInterval: 1 * time.Second,
Timeout: 24 * time.Hour,
}.Wait(state)
resp.Body.Close()
if err != nil {
err = fmt.Errorf("Error uploading: %s", err.Error())
return
}
result, err = c.client.Task.GetResult(c.session, task)
if err != nil {
err = fmt.Errorf("Error getting result: %s", err.Error())
return
}
log.Printf("Upload complete")
return
}

View File

@@ -0,0 +1,86 @@
package common
import (
"github.com/hashicorp/packer-plugin-sdk/multistep"
"time"
)
type InterruptibleWait struct {
Timeout time.Duration
// optional:
Predicate func() (result bool, err error)
PredicateInterval time.Duration
}
type TimeoutError struct{}
func (err TimeoutError) Error() string {
return "Timed out"
}
type InterruptedError struct{}
func (err InterruptedError) Error() string {
return "Interrupted"
}
type PredicateResult struct {
complete bool
err error
}
/* Wait waits for up to Timeout duration, checking an optional Predicate every PredicateInterval duration.
The first run of Predicate is immediately after Wait is called.
If the command is interrupted by the user, then an InterruptedError is returned.
If Predicate is not nil, a timeout leads to TimeoutError being returned, and a successful Predicate run leads to nil being returned.
If Predicate is nil, a timeout is not an error, and nil is returned.
*/
func (wait InterruptibleWait) Wait(state multistep.StateBag) error {
predicateResult := make(chan PredicateResult, 1)
stopWaiting := make(chan struct{})
defer close(stopWaiting)
if wait.Predicate != nil {
go func() {
for {
if complete, err := wait.Predicate(); err != nil || complete {
predicateResult <- PredicateResult{complete, err}
return
}
select {
case <-time.After(wait.PredicateInterval):
continue
case <-stopWaiting:
return
}
}
}()
}
timeout := time.After(wait.Timeout)
for {
// wait for either install to complete/error,
// an interrupt to come through, or a timeout to occur
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return InterruptedError{}
}
select {
case result := <-predicateResult:
return result.err
case <-time.After(1 * time.Second):
continue
case <-timeout:
if wait.Predicate != nil {
return TimeoutError{}
} else {
return nil
}
}
}
}

View File

@@ -0,0 +1,217 @@
package common
import (
"bytes"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"os"
"strings"
"github.com/hashicorp/packer-plugin-sdk/multistep"
gossh "golang.org/x/crypto/ssh"
)
func SSHAddress(state multistep.StateBag) (string, error) {
sshIP := state.Get("ssh_address").(string)
sshHostPort := 22
return fmt.Sprintf("%s:%d", sshIP, sshHostPort), nil
}
func SSHLocalAddress(state multistep.StateBag) (string, error) {
sshLocalPort, ok := state.Get("local_ssh_port").(uint)
if !ok {
return "", fmt.Errorf("SSH port forwarding hasn't been set up yet")
}
conn_str := fmt.Sprintf("%s:%d", "127.0.0.1", sshLocalPort)
return conn_str, nil
}
func SSHPort(state multistep.StateBag) (int, error) {
sshHostPort := state.Get("local_ssh_port").(uint)
return int(sshHostPort), nil
}
func CommHost(state multistep.StateBag) (string, error) {
return "127.0.0.1", nil
}
func SSHConfigFunc(config SSHConfig) func(multistep.StateBag) (*gossh.ClientConfig, error) {
return func(state multistep.StateBag) (*gossh.ClientConfig, error) {
config := state.Get("commonconfig").(CommonConfig)
auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword),
}
if config.SSHKeyPath != "" {
signer, err := FileSigner(config.SSHKeyPath)
if err != nil {
return nil, err
}
auth = append(auth, gossh.PublicKeys(signer))
}
return &gossh.ClientConfig{
User: config.SSHUser,
Auth: auth,
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
}, nil
}
}
func doExecuteSSHCmd(cmd, target string, config *gossh.ClientConfig) (stdout string, err error) {
client, err := gossh.Dial("tcp", target, config)
if err != nil {
return "", err
}
//Create session
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
var b bytes.Buffer
session.Stdout = &b
if err := session.Run(cmd); err != nil {
return "", err
}
return strings.Trim(b.String(), "\n"), nil
}
func ExecuteHostSSHCmd(state multistep.StateBag, cmd string) (stdout string, err error) {
config := state.Get("commonconfig").(CommonConfig)
sshAddress, _ := SSHAddress(state)
// Setup connection config
sshConfig := &gossh.ClientConfig{
User: config.Username,
Auth: []gossh.AuthMethod{
gossh.Password(config.Password),
},
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
}
return doExecuteSSHCmd(cmd, sshAddress, sshConfig)
}
func ExecuteGuestSSHCmd(state multistep.StateBag, cmd string) (stdout string, err error) {
config := state.Get("commonconfig").(CommonConfig)
localAddress, err := SSHLocalAddress(state)
if err != nil {
return
}
sshConfig, err := SSHConfigFunc(config.SSHConfig)(state)
if err != nil {
return
}
return doExecuteSSHCmd(cmd, localAddress, sshConfig)
}
func forward(local_conn net.Conn, config *gossh.ClientConfig, server, remote_dest string, remote_port uint) error {
defer local_conn.Close()
ssh_client_conn, err := gossh.Dial("tcp", server+":22", config)
if err != nil {
log.Printf("local ssh.Dial error: %s", err)
return err
}
defer ssh_client_conn.Close()
remote_loc := fmt.Sprintf("%s:%d", remote_dest, remote_port)
ssh_conn, err := ssh_client_conn.Dial("tcp", remote_loc)
if err != nil {
log.Printf("ssh.Dial error: %s", err)
return err
}
defer ssh_conn.Close()
txDone := make(chan struct{})
rxDone := make(chan struct{})
go func() {
_, err = io.Copy(ssh_conn, local_conn)
if err != nil {
log.Printf("io.copy failed: %v", err)
}
close(txDone)
}()
go func() {
_, err = io.Copy(local_conn, ssh_conn)
if err != nil {
log.Printf("io.copy failed: %v", err)
}
close(rxDone)
}()
<-txDone
<-rxDone
return nil
}
func ssh_port_forward(local_listener net.Listener, remote_port int, remote_dest, host, username, password string) error {
config := &gossh.ClientConfig{
User: username,
Auth: []gossh.AuthMethod{
gossh.Password(password),
},
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
}
for {
local_connection, err := local_listener.Accept()
if err != nil {
log.Printf("Local accept failed: %s", err)
return err
}
// Forward to a remote port
go forward(local_connection, config, host, remote_dest, uint(remote_port))
}
return nil
}
// FileSigner returns an gossh.Signer for a key file.
func FileSigner(path string) (gossh.Signer, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
keyBytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
// We parse the private key on our own first so that we can
// show a nicer error if the private key has a password.
block, _ := pem.Decode(keyBytes)
if block == nil {
return nil, fmt.Errorf(
"Failed to read key '%s': no key found", path)
}
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
return nil, fmt.Errorf(
"Failed to read key '%s': password protected keys are\n"+
"not supported. Please decrypt the key prior to use.", path)
}
signer, err := gossh.ParsePrivateKey(keyBytes)
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
return signer, nil
}

View File

@@ -0,0 +1,48 @@
package common
import (
"errors"
"time"
"github.com/hashicorp/packer-plugin-sdk/communicator"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)
type SSHConfig struct {
Comm communicator.Config `mapstructure:",squash"`
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHSkipNatMapping bool `mapstructure:"ssh_skip_nat_mapping"`
// These are deprecated, but we keep them around for BC
// TODO(@mitchellh): remove
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"`
}
func (c *SSHConfig) Prepare(ctx *interpolate.Context) []error {
if c.SSHHostPortMin == 0 {
c.SSHHostPortMin = 2222
}
if c.SSHHostPortMax == 0 {
c.SSHHostPortMax = 4444
}
// TODO: backwards compatibility, write fixer instead
if c.SSHKeyPath != "" {
c.Comm.SSHPrivateKeyFile = c.SSHKeyPath
}
if c.SSHWaitTimeout != 0 {
c.Comm.SSHTimeout = c.SSHWaitTimeout
}
errs := c.Comm.Prepare(ctx)
if c.SSHHostPortMin > c.SSHHostPortMax {
errs = append(errs,
errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
return errs
}

View File

@@ -0,0 +1,84 @@
package common
import (
"context"
"fmt"
"log"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
xsclient "github.com/terra-farm/go-xen-api-client"
)
type StepAttachVdi struct {
VdiUuidKey string
VdiType xsclient.VbdType
vdi xsclient.VDIRef
}
func (self *StepAttachVdi) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
log.Printf("Running attach vdi for key %s\n", self.VdiUuidKey)
var vdiUuid string
if vdiUuidRaw, ok := state.GetOk(self.VdiUuidKey); ok {
vdiUuid = vdiUuidRaw.(string)
} else {
log.Printf("Skipping attach of '%s'", self.VdiUuidKey)
return multistep.ActionContinue
}
var err error
self.vdi, err = c.client.VDI.GetByUUID(c.session, vdiUuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VDI from UUID '%s': %s", vdiUuid, err.Error()))
return multistep.ActionHalt
}
uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
err = ConnectVdi(c, instance, self.vdi, self.VdiType)
if err != nil {
ui.Error(fmt.Sprintf("Error attaching VDI '%s': '%s'", vdiUuid, err.Error()))
return multistep.ActionHalt
}
log.Printf("Attached VDI '%s'", vdiUuid)
return multistep.ActionContinue
}
func (self *StepAttachVdi) Cleanup(state multistep.StateBag) {
config := state.Get("commonconfig").(CommonConfig)
c := state.Get("client").(*Connection)
if config.ShouldKeepVM(state) {
return
}
if self.vdi == "" {
return
}
uuid := state.Get("instance_uuid").(string)
vmRef, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
log.Printf("Unable to get VM from UUID '%s': %s", uuid, err.Error())
return
}
vdiUuid := state.Get(self.VdiUuidKey).(string)
err = DisconnectVdi(c, vmRef, self.vdi)
if err != nil {
log.Printf("Unable to disconnect VDI '%s': %s", vdiUuid, err.Error())
return
}
log.Printf("Detached VDI '%s'", vdiUuid)
}

View File

@@ -0,0 +1,33 @@
package common
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepBootWait struct{}
func (self *StepBootWait) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
c := state.Get("client").(*Connection)
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
instance, _ := c.client.VM.GetByUUID(c.session, state.Get("instance_uuid").(string))
ui.Say("Unpausing VM " + state.Get("instance_uuid").(string))
Unpause(c, instance)
if int64(config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.BootWait))
err := InterruptibleWait{Timeout: config.BootWait}.Wait(state)
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (self *StepBootWait) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,53 @@
package common
import (
"context"
"fmt"
"log"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepDetachVdi struct {
VdiUuidKey string
}
func (self *StepDetachVdi) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
var vdiUuid string
if vdiUuidRaw, ok := state.GetOk(self.VdiUuidKey); ok {
vdiUuid = vdiUuidRaw.(string)
} else {
log.Printf("Skipping detach of '%s'", self.VdiUuidKey)
return multistep.ActionContinue
}
vdi, err := c.client.VDI.GetByUUID(c.session, vdiUuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VDI from UUID '%s': %s", vdiUuid, err.Error()))
return multistep.ActionHalt
}
uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
err = DisconnectVdi(c, instance, vdi)
if err != nil {
ui.Error(fmt.Sprintf("Unable to detach VDI '%s': %s", vdiUuid, err.Error()))
//return multistep.ActionHalt
return multistep.ActionContinue
}
log.Printf("Detached VDI '%s'", vdiUuid)
return multistep.ActionContinue
}
func (self *StepDetachVdi) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,278 @@
package common
import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepExport struct{}
func downloadFile(url, filename string, ui packer.Ui) (err error) {
// Create the file
fh, err := os.Create(filename)
if err != nil {
return err
}
defer fh.Close()
// Define a new transport which allows self-signed certs
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
// Create a client
client := &http.Client{Transport: tr}
// Create request and download file
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
var progress uint
var total uint
var percentage uint
var marker_len uint
progress = uint(0)
total = uint(resp.ContentLength)
percentage = uint(0)
marker_len = uint(5)
var buffer [4096]byte
for {
n, err := resp.Body.Read(buffer[:])
if err != nil && err != io.EOF {
return err
}
progress += uint(n)
if _, write_err := fh.Write(buffer[:n]); write_err != nil {
return write_err
}
if err == io.EOF {
break
}
// Increment percentage in multiples of marker_len
cur_percentage := ((progress * 100 / total) / marker_len) * marker_len
if cur_percentage > percentage {
percentage = cur_percentage
ui.Message(fmt.Sprintf("Downloading... %d%%", percentage))
}
}
return nil
}
func (StepExport) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
instance_uuid := state.Get("instance_uuid").(string)
suffix := ".vhd"
extrauri := "&format=vhd"
instance, err := c.client.VM.GetByUUID(c.session, instance_uuid)
if err != nil {
ui.Error(fmt.Sprintf("Could not get VM with UUID '%s': %s", instance_uuid, err.Error()))
return multistep.ActionHalt
}
if len(config.ExportNetworkNames) > 0 {
vifs, err := c.client.VM.GetVIFs(c.session, instance)
if err != nil {
ui.Error(fmt.Sprintf("Error occured getting VIFs: %s", err.Error()))
return multistep.ActionHalt
}
for _, vif := range vifs {
err := c.client.VIF.Destroy(c.session, vif)
if err != nil {
ui.Error(fmt.Sprintf("Destroy vif fail: '%s': %s", vif, err.Error()))
return multistep.ActionHalt
}
}
for i, networkNameLabel := range config.ExportNetworkNames {
networks, err := c.client.Network.GetByNameLabel(c.session, networkNameLabel)
if err != nil {
ui.Error(fmt.Sprintf("Error occured getting Network by name-label: %s", err.Error()))
return multistep.ActionHalt
}
switch {
case len(networks) == 0:
ui.Error(fmt.Sprintf("Couldn't find a network with the specified name-label '%s'. Aborting.", networkNameLabel))
return multistep.ActionHalt
case len(networks) > 1:
ui.Error(fmt.Sprintf("Found more than one network with the name '%s'. The name must be unique. Aborting.", networkNameLabel))
return multistep.ActionHalt
}
//we need the VIF index string
vifIndexString := fmt.Sprintf("%d", i)
_, err = ConnectNetwork(c, networks[0], instance, vifIndexString)
if err != nil {
ui.Say(err.Error())
}
}
}
ui.Say("Step: export artifact")
compress_option_xe := "compress=false"
compress_option_url := ""
switch config.Format {
case "none":
ui.Say("Skipping export")
return multistep.ActionContinue
case "xva_compressed":
compress_option_xe = "compress=true"
compress_option_url = "use_compression=true&"
fallthrough
case "xva":
// export the VM
export_filename := fmt.Sprintf("%s/%s.xva", config.OutputDir, config.VMName)
use_xe := os.Getenv("USE_XE") == "1"
if xe, e := exec.LookPath("xe"); e == nil && use_xe {
cmd := exec.Command(
xe,
"-s", c.Host,
"-p", "443",
"-u", c.Username,
"-pw", c.Password,
"vm-export",
"vm="+instance_uuid,
compress_option_xe,
"filename="+export_filename,
)
ui.Say(fmt.Sprintf("Getting XVA %+v %+v", cmd.Path, cmd.Args))
err = cmd.Run()
} else {
export_url := fmt.Sprintf("https://%s/export?%suuid=%s&session_id=%s",
c.Host,
compress_option_url,
instance_uuid,
c.GetSession(),
)
ui.Say("Getting XVA " + export_url)
err = downloadFile(export_url, export_filename, ui)
}
if err != nil {
ui.Error(fmt.Sprintf("Could not download XVA: %s", err.Error()))
return multistep.ActionHalt
}
case "vdi_raw":
suffix = ".raw"
extrauri = ""
fallthrough
case "vdi_vhd":
// export the disks
disks, err := GetDisks(c, instance)
if err != nil {
ui.Error(fmt.Sprintf("Could not get VM disks: %s", err.Error()))
return multistep.ActionHalt
}
for _, disk := range disks {
disk_uuid, err := c.client.VDI.GetUUID(c.session, disk)
if err != nil {
ui.Error(fmt.Sprintf("Could not get disk with UUID '%s': %s", disk_uuid, err.Error()))
return multistep.ActionHalt
}
// Work out XenServer version
hosts, err := c.client.Host.GetAll(c.session)
if err != nil {
ui.Error(fmt.Sprintf("Could not retrieve hosts in the pool: %s", err.Error()))
return multistep.ActionHalt
}
host := hosts[0]
host_software_versions, err := c.client.Host.GetSoftwareVersion(c.session, host)
xs_version := host_software_versions["product_version"]
if err != nil {
ui.Error(fmt.Sprintf("Could not get the software version: %s", err.Error()))
return multistep.ActionHalt
}
var disk_export_url string
// @todo: check for 6.5 SP1
if xs_version <= "6.5.0" && config.Format == "vdi_vhd" {
// Export the VHD using a Transfer VM
disk_export_url, err = Expose(c, disk, "vhd")
if err != nil {
ui.Error(fmt.Sprintf("Failed to expose disk %s: %s", disk_uuid, err.Error()))
return multistep.ActionHalt
}
} else {
// Use the preferred direct export from XAPI
// Basic auth in URL request is required as session token is not
// accepted for some reason.
// @todo: raise with XAPI team.
disk_export_url = fmt.Sprintf("https://%s:%s@%s/export_raw_vdi?vdi=%s%s",
c.Username,
c.Password,
c.Host,
disk_uuid,
extrauri)
}
disk_export_filename := fmt.Sprintf("%s/%s%s", config.OutputDir, disk_uuid, suffix)
ui.Say("Getting VDI " + disk_export_url)
err = downloadFile(disk_export_url, disk_export_filename, ui)
if err != nil {
ui.Error(fmt.Sprintf("Could not download VDI: %s", err.Error()))
return multistep.ActionHalt
}
// Call unexpose in case a TVM was used. The call is harmless
// if that is not the case.
Unexpose(c, disk)
}
default:
panic(fmt.Sprintf("Unknown export format '%s'", config.Format))
}
ui.Say("Download completed: " + config.OutputDir)
return multistep.ActionContinue
}
func (StepExport) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,49 @@
package common
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepFindVdi struct {
VdiName string
ImagePathFunc func() string
VdiUuidKey string
}
func (self *StepFindVdi) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
// Ignore if VdiName is not specified
if self.VdiName == "" {
return multistep.ActionContinue
}
vdis, err := c.client.VDI.GetByNameLabel(c.session, self.VdiName)
switch {
case len(vdis) == 0:
ui.Error(fmt.Sprintf("Couldn't find a VDI named '%s'", self.VdiName))
return multistep.ActionHalt
case len(vdis) > 1:
ui.Error(fmt.Sprintf("Found more than one VDI with name '%s'. Name must be unique", self.VdiName))
return multistep.ActionHalt
}
vdi := vdis[0]
vdiUuid, err := c.client.VDI.GetUUID(c.session, vdi)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get UUID of VDI '%s': %s", self.VdiName, err.Error()))
return multistep.ActionHalt
}
state.Put(self.VdiUuidKey, vdiUuid)
return multistep.ActionContinue
}
func (self *StepFindVdi) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,50 @@
package common
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepForwardPortOverSSH struct {
RemotePort func(state multistep.StateBag) (int, error)
RemoteDest func(state multistep.StateBag) (string, error)
HostPortMin uint
HostPortMax uint
ResultKey string
}
func (self *StepForwardPortOverSSH) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
// Find a free local port:
l, sshHostPort := FindPort(self.HostPortMin, self.HostPortMax)
if l == nil || sshHostPort == 0 {
ui.Error("Error: unable to find free host port. Try providing a larger range [host_port_min, host_port_max]")
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Creating a local port forward over SSH on local port %d", sshHostPort))
hostAddress, _ := state.Get("ssh_address").(string)
remotePort, _ := self.RemotePort(state)
remoteDest, _ := self.RemoteDest(state)
go ssh_port_forward(l, remotePort, remoteDest, hostAddress, config.Username, config.Password)
ui.Say(fmt.Sprintf("Port forward setup. %d ---> %s:%d on %s", sshHostPort, remoteDest, remotePort, hostAddress))
// Provide the local port to future steps.
state.Put(self.ResultKey, sshHostPort)
return multistep.ActionContinue
}
func (self *StepForwardPortOverSSH) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,51 @@
package common
import (
"fmt"
"strconv"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepGetVNCPort struct{}
func (self *StepGetVNCPort) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
ui.Say("Step: forward the instances VNC port over SSH")
domid := state.Get("domid").(int)
cmd := fmt.Sprintf("xenstore-read /local/domain/%d/console/vnc-port", domid)
remote_vncport, err := ExecuteHostSSHCmd(state, cmd)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VNC port (is the VM running?): %s", err.Error()))
return multistep.ActionHalt
}
remote_port, err := strconv.ParseUint(remote_vncport, 10, 16)
if err != nil {
ui.Error(fmt.Sprintf("Unable to convert '%s' to an int", remote_vncport))
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("instance_vnc_port", uint(remote_port))
return multistep.ActionContinue
}
func (self *StepGetVNCPort) Cleanup(state multistep.StateBag) {
}
func InstanceVNCPort(state multistep.StateBag) (uint, error) {
vncPort := state.Get("instance_vnc_port").(uint)
return vncPort, nil
}
func InstanceVNCIP(state multistep.StateBag) (string, error) {
// The port is in Dom0, so we want to forward from localhost
return "127.0.0.1", nil
}

View File

@@ -0,0 +1,96 @@
package common
// Taken from hashicorp/packer/builder/qemu/step_http_server.go
import (
"context"
"fmt"
"log"
"net"
"net/http"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
// This step creates and runs the HTTP server that is serving files from the
// directory specified by the 'http_directory` configuration parameter in the
// template.
//
// Uses:
// config *config
// ui packer.Ui
//
// Produces:
// http_port int - The port the HTTP server started on.
type StepHTTPServer struct {
Chan chan<- string
l net.Listener
}
type IPSnooper struct {
ch chan<- string
handler http.Handler
}
func (snooper IPSnooper) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
log.Printf("HTTP: %s %s %s", req.RemoteAddr, req.Method, req.URL)
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err == nil && ip != "" {
select {
case snooper.ch <- ip:
log.Printf("Remembering remote address '%s'", ip)
default:
// if ch is already full, don't block waiting to send the address, just drop it
}
}
snooper.handler.ServeHTTP(resp, req)
}
func (s *StepHTTPServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
var httpPort uint = 0
if config.HTTPDir == "" {
// the packer provision steps assert this type is an int
// so this cannot be a uint like the rest of the code
state.Put("http_port", int(httpPort))
return multistep.ActionContinue
}
s.l, httpPort = FindPort(config.HTTPPortMin, config.HTTPPortMax)
if s.l == nil || httpPort == 0 {
ui.Error("Error: unable to find free HTTP server port. Try providing a larger range [http_port_min, http_port_max]")
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Starting HTTP server on port %d", httpPort))
// Start the HTTP server and run it in the background
fileServer := http.FileServer(http.Dir(config.HTTPDir))
server := &http.Server{
Addr: fmt.Sprintf(":%d", httpPort),
Handler: IPSnooper{
ch: s.Chan,
handler: fileServer,
},
}
go server.Serve(s.l)
// Save the address into the state so it can be accessed in the future
// the packer provision steps assert this type is an int
// so this cannot be a uint like the rest of the code
state.Put("http_port", int(httpPort))
return multistep.ActionContinue
}
func (s *StepHTTPServer) Cleanup(multistep.StateBag) {
if s.l != nil {
// Close the listener so that the HTTP server stops
s.l.Close()
}
}

View File

@@ -0,0 +1,54 @@
package common
/* Taken from https://raw.githubusercontent.com/hashicorp/packer/master/builder/qemu/step_prepare_output_dir.go */
import (
"context"
"log"
"os"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepPrepareOutputDir struct {
Force bool
Path string
}
func (self *StepPrepareOutputDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if _, err := os.Stat(self.Path); err == nil && self.Force {
ui.Say("Deleting previous output directory...")
os.RemoveAll(self.Path)
}
if err := os.MkdirAll(self.Path, 0755); err != nil {
state.Put("error", err)
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (self *StepPrepareOutputDir) Cleanup(state multistep.StateBag) {
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting output directory...")
for i := 0; i < 5; i++ {
err := os.RemoveAll(self.Path)
if err == nil {
break
}
log.Printf("Error removing output dir: %s", err)
time.Sleep(2 * time.Second)
}
}
}

View File

@@ -0,0 +1,43 @@
package common
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepSetVmHostSshAddress struct{}
func (self *StepSetVmHostSshAddress) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
c := state.Get("client").(*Connection)
ui := state.Get("ui").(packer.Ui)
ui.Say("Step: Set SSH address to VM host IP")
uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
host, err := c.client.VM.GetResidentOn(c.session, instance)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM Host for VM '%s': %s", uuid, err.Error()))
}
address, err := c.client.Host.GetAddress(c.session, host)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get address from VM Host: %s", err.Error()))
}
state.Put("ssh_address", address)
ui.Say(fmt.Sprintf("Set host SSH address to '%s'.", address))
return multistep.ActionContinue
}
func (self *StepSetVmHostSshAddress) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,35 @@
package common
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepSetVmToTemplate struct{}
func (StepSetVmToTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
instance_uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, instance_uuid)
if err != nil {
ui.Error(fmt.Sprintf("Could not get VM with UUID '%s': %s", instance_uuid, err.Error()))
return multistep.ActionHalt
}
err = c.client.VM.SetIsATemplate(c.session, instance, true)
if err != nil {
ui.Error(fmt.Sprintf("failed to set VM '%s' as a template with error: %v", instance_uuid, err))
return multistep.ActionHalt
}
ui.Message("Successfully set VM as a template")
return multistep.ActionContinue
}
func (StepSetVmToTemplate) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,82 @@
package common
import (
"context"
"fmt"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
xenapi "github.com/terra-farm/go-xen-api-client"
)
type StepShutdown struct{}
func (StepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
instance_uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, instance_uuid)
if err != nil {
ui.Error(fmt.Sprintf("Could not get VM with UUID '%s': %s", instance_uuid, err.Error()))
return multistep.ActionHalt
}
ui.Say("Step: Shutting down VM")
// Shutdown the VM
success := func() bool {
if config.ShutdownCommand != "" {
ui.Message("Executing shutdown command...")
_, err := ExecuteGuestSSHCmd(state, config.ShutdownCommand)
if err != nil {
ui.Error(fmt.Sprintf("Shutdown command failed: %s", err.Error()))
return false
}
ui.Message("Waiting for VM to enter Halted state...")
err = InterruptibleWait{
Predicate: func() (bool, error) {
power_state, err := c.client.VM.GetPowerState(c.session, instance)
return power_state == xenapi.VMPowerStateHalted, err
},
PredicateInterval: 5 * time.Second,
Timeout: 300 * time.Second,
}.Wait(state)
if err != nil {
ui.Error(fmt.Sprintf("Error waiting for VM to halt: %s", err.Error()))
return false
}
} else {
ui.Message("Attempting to cleanly shutdown the VM...")
err = c.client.VM.CleanShutdown(c.session, instance)
if err != nil {
ui.Error(fmt.Sprintf("Could not shut down VM: %s", err.Error()))
return false
}
}
return true
}()
if !success {
ui.Say("WARNING: Forcing hard shutdown of the VM...")
err = c.client.VM.HardShutdown(c.session, instance)
if err != nil {
ui.Error(fmt.Sprintf("Could not hard shut down VM -- giving up: %s", err.Error()))
return multistep.ActionHalt
}
}
ui.Message("Successfully shut down VM")
return multistep.ActionContinue
}
func (StepShutdown) Cleanup(state multistep.StateBag) {}

View File

@@ -0,0 +1,137 @@
package common
import (
"fmt"
"log"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
gossh "golang.org/x/crypto/ssh"
)
type StepStartOnHIMN struct{}
/*
* This step starts the installed guest on the Host Internal Management Network
* as there exists an API to obtain the IP allocated to the VM by XAPI.
* This in turn will allow Packer to SSH into the VM, provided NATing has been
* enabled on the host.
*
*/
func (self *StepStartOnHIMN) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
ui.Say("Step: Start VM on the Host Internal Mangement Network")
uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
// Find the HIMN Ref
networks, err := c.client.Network.GetByNameLabel(c.session, "Host internal management network")
if err != nil || len(networks) == 0 {
ui.Error("Unable to find a host internal management network")
ui.Error(err.Error())
return multistep.ActionHalt
}
himn := networks[0]
// Create a VIF for the HIMN
himn_vif, err := ConnectNetwork(c, himn, instance, "0")
if err != nil {
ui.Error("Error creating VIF")
ui.Error(err.Error())
return multistep.ActionHalt
}
// Start the VM
c.client.VM.Start(c.session, instance, false, false)
var himn_iface_ip string = ""
// Obtain the allocated IP
err = InterruptibleWait{
Predicate: func() (found bool, err error) {
ips, err := c.client.Network.GetAssignedIps(c.session, himn)
if err != nil {
return false, fmt.Errorf("Can't get assigned IPs: %s", err.Error())
}
log.Printf("IPs: %s", ips)
log.Printf("Ref: %s", instance)
//Check for instance.Ref in map
if vm_ip, ok := ips[*himn_vif]; ok && vm_ip != "" {
ui.Say("Found the VM's IP: " + vm_ip)
himn_iface_ip = vm_ip
return true, nil
}
ui.Say("Wait for IP address...")
return false, nil
},
PredicateInterval: 10 * time.Second,
Timeout: 100 * time.Second,
}.Wait(state)
if err != nil {
ui.Error(fmt.Sprintf("Unable to find an IP on the Host-internal management interface: %s", err.Error()))
return multistep.ActionHalt
}
state.Put("himn_ssh_address", himn_iface_ip)
ui.Say("Stored VM's IP " + himn_iface_ip)
// Wait for the VM to boot, and check we can ping this interface
ping_cmd := fmt.Sprintf("ping -c 1 %s", himn_iface_ip)
err = InterruptibleWait{
Predicate: func() (success bool, err error) {
ui.Message(fmt.Sprintf("Attempting to ping interface: %s", ping_cmd))
_, err = ExecuteHostSSHCmd(state, ping_cmd)
switch err.(type) {
case nil:
// ping succeeded
return true, nil
case *gossh.ExitError:
// ping failed, try again
return false, nil
default:
// unknown error
return false, err
}
},
PredicateInterval: 10 * time.Second,
Timeout: 300 * time.Second,
}.Wait(state)
if err != nil {
ui.Error(fmt.Sprintf("Unable to ping interface. (Has the VM not booted?): %s", err.Error()))
return multistep.ActionHalt
}
ui.Message("Ping success! Continuing...")
return multistep.ActionContinue
}
func (self *StepStartOnHIMN) Cleanup(state multistep.StateBag) {}
func HimnSSHIP(state multistep.StateBag) (string, error) {
ip := state.Get("himn_ssh_address").(string)
return ip, nil
}
func HimnSSHPort(state multistep.StateBag) (uint, error) {
config := state.Get("commonconfig").(CommonConfig)
return config.SSHPort, nil
}

View File

@@ -0,0 +1,58 @@
package common
import (
"context"
"fmt"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepStartVmPaused struct {
VmCleanup
}
func (self *StepStartVmPaused) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
c := state.Get("client").(*Connection)
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(Config)
ui.Say("Step: Start VM Paused")
uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
// note that here "cd" means boot from hard drive ('c') first, then CDROM ('d')
err = c.client.VM.SetHVMBootPolicy(c.session, instance, "BIOS order")
if err != nil {
ui.Error(fmt.Sprintf("Unable to set HVM boot params: %s", err.Error()))
return multistep.ActionHalt
}
err = c.client.VM.SetHVMBootParams(c.session, instance, map[string]string{"order": "cd", "firmware": config.Firmware})
if err != nil {
ui.Error(fmt.Sprintf("Unable to set HVM boot params: %s", err.Error()))
return multistep.ActionHalt
}
err = c.client.VM.Start(c.session, instance, true, false)
if err != nil {
ui.Error(fmt.Sprintf("Unable to start VM with UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
domid, err := c.client.VM.GetDomid(c.session, instance)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get domid of VM with UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
state.Put("domid", domid)
return multistep.ActionContinue
}

View File

@@ -0,0 +1,272 @@
package common
/* Heavily borrowed from builder/quemu/step_type_boot_command.go */
import (
"context"
"crypto/tls"
"fmt"
"io"
"log"
"net"
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/mitchellh/go-vnc"
)
const KeyLeftShift uint = 0xFFE1
type bootCommandTemplateData struct {
Name string
HTTPIP string
HTTPPort uint
}
type StepTypeBootCommand struct {
Ctx interpolate.Context
}
func (self *StepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
httpPort := state.Get("http_port").(int)
// skip this step if we have nothing to type
if len(config.BootCommand) == 0 {
return multistep.ActionContinue
}
vmRef, err := c.client.VM.GetByNameLabel(c.session, config.VMName)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(vmRef) != 1 {
ui.Error(fmt.Sprintf("expected to find a single VM, instead found '%d'. Ensure the VM name is unique", len(vmRef)))
}
consoles, err := c.client.VM.GetConsoles(c.session, vmRef[0])
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(consoles) != 1 {
ui.Error(fmt.Sprintf("expected to find a VM console, instead found '%d'. Ensure there is only one console", len(consoles)))
return multistep.ActionHalt
}
location, err := c.client.Console.GetLocation(c.session, consoles[0])
if err != nil {
ui.Error(err.Error())
return multistep.ActionHalt
}
locationPieces := strings.SplitAfter(location, "/")
consoleHost := strings.TrimSuffix(locationPieces[2], "/")
ui.Say(fmt.Sprintf("Connecting to the VM console VNC over xapi via %s", consoleHost))
conn, err := net.Dial("tcp", fmt.Sprintf("%s:443", consoleHost))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer conn.Close()
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
}
tlsConn := tls.Client(conn, tlsConfig)
consoleLocation := strings.TrimSpace(fmt.Sprintf("/%s", locationPieces[len(locationPieces)-1]))
httpReq := fmt.Sprintf("CONNECT %s HTTP/1.0\r\nHost: %s\r\nCookie: session_id=%s\r\n\r\n", consoleLocation, consoleHost, c.session)
fmt.Printf("Sending the follow http req: %v", httpReq)
ui.Say(fmt.Sprintf("Making HTTP request to initiate VNC connection: %s", httpReq))
_, err = io.WriteString(tlsConn, httpReq)
if err != nil {
err := fmt.Errorf("failed to start vnc session: %v", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
buffer := make([]byte, 10000)
_, err = tlsConn.Read(buffer)
if err != nil && err != io.EOF {
err := fmt.Errorf("failed to read vnc session response: %v", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Received response: %s", string(buffer)))
vncClient, err := vnc.Client(tlsConn, &vnc.ClientConfig{Exclusive: true})
if err != nil {
err := fmt.Errorf("Error establishing VNC session: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer vncClient.Close()
log.Printf("Connected to the VNC console: %s", vncClient.DesktopName)
// find local ip
envVar, err := ExecuteHostSSHCmd(state, "echo $SSH_CLIENT")
if err != nil {
ui.Error(fmt.Sprintf("Error detecting local IP: %s", err))
return multistep.ActionHalt
}
if envVar == "" {
ui.Error("Error detecting local IP: $SSH_CLIENT was empty")
return multistep.ActionHalt
}
localIp := strings.Split(envVar, " ")[0]
ui.Message(fmt.Sprintf("Found local IP: %s", localIp))
self.Ctx.Data = &bootCommandTemplateData{
config.VMName,
localIp,
uint(httpPort),
}
ui.Say("Typing boot commands over VNC...")
for _, command := range config.BootCommand {
command, err := interpolate.Render(command, &self.Ctx)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Check for interrupts
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return multistep.ActionHalt
}
vncSendString(vncClient, command)
}
ui.Say("Finished typing.")
return multistep.ActionContinue
}
func (self *StepTypeBootCommand) Cleanup(multistep.StateBag) {}
// Taken from qemu's builder plugin - not an exported function.
func vncSendString(c *vnc.ClientConn, original string) {
// Scancodes reference: https://github.com/qemu/qemu/blob/master/ui/vnc_keysym.h
special := make(map[string]uint32)
special["<bs>"] = 0xFF08
special["<del>"] = 0xFFFF
special["<enter>"] = 0xFF0D
special["<esc>"] = 0xFF1B
special["<f1>"] = 0xFFBE
special["<f2>"] = 0xFFBF
special["<f3>"] = 0xFFC0
special["<f4>"] = 0xFFC1
special["<f5>"] = 0xFFC2
special["<f6>"] = 0xFFC3
special["<f7>"] = 0xFFC4
special["<f8>"] = 0xFFC5
special["<f9>"] = 0xFFC6
special["<f10>"] = 0xFFC7
special["<f11>"] = 0xFFC8
special["<f12>"] = 0xFFC9
special["<return>"] = 0xFF0D
special["<tab>"] = 0xFF09
special["<up>"] = 0xFF52
special["<down>"] = 0xFF54
special["<left>"] = 0xFF51
special["<right>"] = 0xFF53
special["<spacebar>"] = 0x020
special["<insert>"] = 0xFF63
special["<home>"] = 0xFF50
special["<end>"] = 0xFF57
special["<pageUp>"] = 0xFF55
special["<pageDown>"] = 0xFF56
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
// TODO(mitchellh): Ripe for optimizations of some point, perhaps.
for len(original) > 0 {
var keyCode uint32
keyShift := false
if strings.HasPrefix(original, "<wait>") {
log.Printf("Special code '<wait>' found, sleeping one second")
time.Sleep(1 * time.Second)
original = original[len("<wait>"):]
continue
}
if strings.HasPrefix(original, "<wait5>") {
log.Printf("Special code '<wait5>' found, sleeping 5 seconds")
time.Sleep(5 * time.Second)
original = original[len("<wait5>"):]
continue
}
if strings.HasPrefix(original, "<wait10>") {
log.Printf("Special code '<wait10>' found, sleeping 10 seconds")
time.Sleep(10 * time.Second)
original = original[len("<wait10>"):]
continue
}
for specialCode, specialValue := range special {
if strings.HasPrefix(original, specialCode) {
log.Printf("Special code '%s' found, replacing with: %d", specialCode, specialValue)
keyCode = specialValue
original = original[len(specialCode):]
break
}
}
if keyCode == 0 {
r, size := utf8.DecodeRuneInString(original)
original = original[size:]
keyCode = uint32(r)
keyShift = unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r)
log.Printf("Sending char '%c', code %d, shift %v", r, keyCode, keyShift)
}
if keyShift {
c.KeyEvent(uint32(KeyLeftShift), true)
}
c.KeyEvent(keyCode, true)
time.Sleep(time.Second / 10)
c.KeyEvent(keyCode, false)
time.Sleep(time.Second / 10)
if keyShift {
c.KeyEvent(uint32(KeyLeftShift), false)
}
// no matter what, wait a small period
time.Sleep(50 * time.Millisecond)
}
}

View File

@@ -0,0 +1,145 @@
package common
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
xenapi "github.com/terra-farm/go-xen-api-client"
)
type StepUploadVdi struct {
VdiNameFunc func() string
ImagePathFunc func() string
VdiUuidKey string
}
func (self *StepUploadVdi) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
imagePath := self.ImagePathFunc()
vdiName := self.VdiNameFunc()
if imagePath == "" {
// skip if no disk image to attach
return multistep.ActionContinue
}
ui.Say(fmt.Sprintf("Step: Upload VDI '%s'", vdiName))
// Create VDI for the image
srs, err := c.client.SR.GetAll(c.session)
ui.Say(fmt.Sprintf("Step: Found SRs '%v'", srs))
sr, err := config.GetISOSR(c)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get SR: %v", err))
return multistep.ActionHalt
}
// Open the file for reading (NB: HTTPUpload closes the file for us)
fh, err := os.Open(imagePath)
if err != nil {
ui.Error(fmt.Sprintf("Unable to open disk image '%s': %s", imagePath, err.Error()))
return multistep.ActionHalt
}
// Get file length
fstat, err := fh.Stat()
if err != nil {
ui.Error(fmt.Sprintf("Unable to stat disk image '%s': %s", imagePath, err.Error()))
return multistep.ActionHalt
}
fileLength := fstat.Size()
// Create the VDI
// vdi, err := sr.CreateVdi(vdiName, fileLength)
vdi, err := c.client.VDI.Create(c.session, xenapi.VDIRecord{
NameLabel: vdiName,
VirtualSize: int(fileLength),
Type: "user",
Sharable: false,
ReadOnly: false,
SR: sr,
OtherConfig: map[string]string{
"temp": "temp",
},
})
if err != nil {
ui.Error(fmt.Sprintf("Unable to create VDI '%s': %s", vdiName, err.Error()))
return multistep.ActionHalt
}
vdiUuid, err := c.client.VDI.GetUUID(c.session, vdi)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get UUID of VDI '%s': %s", vdiName, err.Error()))
return multistep.ActionHalt
}
state.Put(self.VdiUuidKey, vdiUuid)
_, err = HTTPUpload(fmt.Sprintf("https://%s/import_raw_vdi?vdi=%s&session_id=%s",
c.Host,
vdi,
c.GetSession(),
), fh, state)
if err != nil {
ui.Error(fmt.Sprintf("Unable to upload VDI: %s", err.Error()))
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (self *StepUploadVdi) Cleanup(state multistep.StateBag) {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
vdiName := self.VdiNameFunc()
if config.ShouldKeepVM(state) {
return
}
vdiUuidRaw, ok := state.GetOk(self.VdiUuidKey)
if !ok {
// VDI doesn't exist
return
}
vdiUuid := vdiUuidRaw.(string)
if vdiUuid == "" {
// VDI already cleaned up
return
}
vdi, err := c.client.VDI.GetByUUID(c.session, vdiUuid)
if err != nil {
ui.Error(fmt.Sprintf("Can't get VDI '%s': %s", vdiUuid, err.Error()))
return
}
// an interrupted import_raw_vdi takes a while to release the VDI
// so try several times
for i := 0; i < 3; i++ {
log.Printf("Trying to destroy VDI...")
err = c.client.VDI.Destroy(c.session, vdi)
if err == nil {
break
}
time.Sleep(1 * time.Second)
}
if err != nil {
ui.Error(fmt.Sprintf("Can't destroy VDI '%s': %s", vdiUuid, err.Error()))
return
}
ui.Say(fmt.Sprintf("Destroyed VDI '%s'", vdiName))
state.Put(self.VdiUuidKey, "")
}

View File

@@ -0,0 +1,96 @@
package common
import (
"context"
"fmt"
"time"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/packer"
)
type StepWaitForIP struct {
VmCleanup
Chan <-chan string
Timeout time.Duration
}
func (self *StepWaitForIP) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("client").(*Connection)
config := state.Get("commonconfig").(CommonConfig)
ui.Say("Step: Wait for VM's IP to become known to us.")
uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error()))
return multistep.ActionHalt
}
var ip string
err = InterruptibleWait{
Timeout: self.Timeout,
PredicateInterval: 5 * time.Second,
Predicate: func() (result bool, err error) {
if config.IPGetter == "auto" || config.IPGetter == "http" {
// Snoop IP from HTTP fetch
select {
case ip = <-self.Chan:
ui.Message(fmt.Sprintf("Got IP '%s' from HTTP request", ip))
return true, nil
default:
}
}
if config.IPGetter == "auto" || config.IPGetter == "tools" {
// Look for PV IP
m, err := c.client.VM.GetGuestMetrics(c.session, instance)
if err != nil {
return false, err
}
if m != "" {
metrics, err := c.client.VMGuestMetrics.GetRecord(c.session, m)
if err != nil {
return false, err
}
networks := metrics.Networks
var ok bool
if ip, ok = networks["0/ip"]; ok {
if ip != "" {
ui.Message(fmt.Sprintf("Got IP '%s' from XenServer tools", ip))
return true, nil
}
}
}
}
return false, nil
},
}.Wait(state)
if err != nil {
ui.Error(fmt.Sprintf("Could not get IP address of VM: %s", err.Error()))
// @todo: give advice on what went wrong (no HTTP server? no PV drivers?)
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Got IP address '%s'", ip))
state.Put("instance_ssh_address", ip)
return multistep.ActionContinue
}
func InstanceSSHIP(state multistep.StateBag) (string, error) {
ip := state.Get("instance_ssh_address").(string)
return ip, nil
}
func InstanceSSHPort(state multistep.StateBag) (int, error) {
return 22, nil
}

View File

@@ -0,0 +1,31 @@
package common
import (
"fmt"
"log"
"github.com/hashicorp/packer-plugin-sdk/multistep"
)
type VmCleanup struct{}
func (self *VmCleanup) Cleanup(state multistep.StateBag) {
config := state.Get("commonconfig").(CommonConfig)
c := state.Get("client").(*Connection)
if config.ShouldKeepVM(state) {
return
}
uuid := state.Get("instance_uuid").(string)
instance, err := c.client.VM.GetByUUID(c.session, uuid)
if err != nil {
log.Printf(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error()))
return
}
err = c.client.VM.HardShutdown(c.session, instance)
if err != nil {
log.Printf(fmt.Sprintf("Unable to force shutdown VM '%s': %s", uuid, err.Error()))
}
}