Implement Proxmox builder

This commit is contained in:
Calle Pettersson 2019-03-10 14:39:47 +01:00 committed by Megan Marsh
parent 5b07926b69
commit 9f8fc37fde
10 changed files with 880 additions and 0 deletions

View File

@ -0,0 +1,44 @@
package proxmox
import (
"fmt"
"log"
"strconv"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/packer"
)
type Artifact struct {
templateID int
proxmoxClient *proxmox.Client
}
// Artifact implements packer.Artifact
var _ packer.Artifact = &Artifact{}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (*Artifact) Files() []string {
return nil
}
func (a *Artifact) Id() string {
return strconv.Itoa(a.templateID)
}
func (a *Artifact) String() string {
return fmt.Sprintf("A template was created: %d", a.templateID)
}
func (a *Artifact) State(name string) interface{} {
return nil
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying template: %d", a.templateID)
_, err := a.proxmoxClient.DeleteVm(proxmox.NewVmRef(a.templateID))
return err
}

View File

@ -0,0 +1,123 @@
package proxmox
import (
"fmt"
"strings"
"time"
"unicode"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/common/bootcommand"
)
type proxmoxDriver struct {
client *proxmox.Client
vmRef *proxmox.VmRef
specialMap map[string]string
runeMap map[rune]string
interval time.Duration
}
func NewProxmoxDriver(c *proxmox.Client, vmRef *proxmox.VmRef, interval time.Duration) *proxmoxDriver {
// Mappings for packer shorthand to qemu qkeycodes
sMap := map[string]string{
"spacebar": "spc",
"bs": "backspace",
"del": "delete",
"return": "ret",
"enter": "ret",
"pageUp": "pgup",
"pageDown": "pgdn",
}
// Mappings for runes that need to be translated to special qkeycodes
// Taken from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us
rMap := map[rune]string{
// Clean mappings
' ': "spc",
'.': "dot",
',': "comma",
';': "semicolon",
'*': "asterisk",
'-': "minus",
'[': "bracket_left",
']': "bracket_right",
'=': "equal",
'\'': "apostrophe",
'`': "grave_accent",
'/': "slash",
'\\': "backslash",
'!': "shift-1", // "exclam"
'@': "shift-2", // "at"
'#': "shift-3", // "numbersign"
'$': "shift-4", // "dollar"
'%': "shift-5", // "percent"
'^': "shift-6", // "asciicircum"
'&': "shift-7", // "ampersand"
'(': "shift-9", // "parenleft"
')': "shift-0", // "parenright"
'{': "shift-bracket_left", // "braceleft"
'}': "shift-bracket_right", // "braceright"
'"': "shift-apostrophe", // "quotedbl"
'+': "shift-equal", // "plus"
'_': "shift-minus", // "underscore"
':': "shift-semicolon", // "colon"
'<': "shift-comma", // "less" is recognized, but seem to map to '/'?
'>': "shift-dot", // "greater"
'~': "shift-grave_accent", // "asciitilde"
'?': "shift-slash", // "question"
'|': "shift-backslash", // "bar"
}
return &proxmoxDriver{
client: c,
vmRef: vmRef,
specialMap: sMap,
runeMap: rMap,
interval: interval,
}
}
func (p *proxmoxDriver) SendKey(key rune, action bootcommand.KeyAction) error {
if special, ok := p.runeMap[key]; ok {
return p.send(special)
}
const shiftFormat = "shift-%c"
const shiftedChars = "~!@#$%^&*()_+{}|:\"<>?" // Copied from bootcommand/driver.go
keyShift := unicode.IsUpper(key) || strings.ContainsRune(shiftedChars, key)
var keys string
if keyShift {
keys = fmt.Sprintf(shiftFormat, key)
} else {
keys = fmt.Sprintf("%c", key)
}
return p.send(keys)
}
func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction) error {
keys := special
if replacement, ok := p.specialMap[special]; ok {
keys = replacement
}
return p.send(keys)
}
func (p *proxmoxDriver) send(keys string) error {
res, err := p.client.MonitorCmd(p.vmRef, "sendkey "+keys)
if err != nil {
return err
}
if data, ok := res["data"].(string); ok && len(data) > 0 {
return fmt.Errorf("failed to send keys: %s", data)
}
time.Sleep(p.interval)
return nil
}
func (p *proxmoxDriver) Flush() error { return nil }

129
builder/proxmox/builder.go Normal file
View File

@ -0,0 +1,129 @@
package proxmox
import (
"crypto/tls"
"fmt"
"log"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// The unique id for the builder
const BuilderId = "proxmox.builder"
type Builder struct {
config Config
runner multistep.Runner
proxmoxClient *proxmox.Client
}
// Builder implements packer.Builder
var _ packer.Builder = &Builder{}
var pluginVersion = "1.0.0"
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
config, warnings, errs := NewConfig(raws...)
if errs != nil {
return warnings, errs
}
b.config = *config
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
var err error
tlsConfig := &tls.Config{
InsecureSkipVerify: b.config.SkipCertValidation,
}
b.proxmoxClient, err = proxmox.NewClient(b.config.ProxmoxURL.String(), nil, tlsConfig)
if err != nil {
return nil, err
}
err = b.proxmoxClient.Login(b.config.Username, b.config.Password)
if err != nil {
return nil, err
}
// Set up the state
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("proxmoxClient", b.proxmoxClient)
state.Put("hook", hook)
state.Put("ui", ui)
// Build the steps
steps := []multistep.Step{
&stepStartVM{},
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
HTTPPortMin: b.config.HTTPPortMin,
HTTPPortMax: b.config.HTTPPortMax,
},
&stepTypeBootCommand{
BootConfig: b.config.BootConfig,
Ctx: b.config.ctx,
},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: getVMIP,
SSHConfig: b.config.Comm.SSHConfigFunc(),
},
&common.StepProvision{},
&common.StepCleanupTempKeys{
Comm: &b.config.Comm,
},
&stepConvertToTemplate{},
&stepFinalizeTemplateConfig{},
&stepSuccess{},
}
// Run the steps
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
artifact := &Artifact{
templateID: state.Get("template_id").(int),
proxmoxClient: b.proxmoxClient,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}
func getVMIP(state multistep.StateBag) (string, error) {
c := state.Get("proxmoxClient").(*proxmox.Client)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
ifs, err := c.GetVmAgentNetworkInterfaces(vmRef)
if err != nil {
return "", err
}
// TODO: Do something smarter here? Allow specifying interface? Or address family?
// For now, just go for first non-loopback
for _, iface := range ifs {
for _, addr := range iface.IPAddresses {
if addr.IsLoopback() {
continue
}
return addr.String(), nil
}
}
return "", fmt.Errorf("Found no IP addresses on VM")
}

196
builder/proxmox/config.go Normal file
View File

@ -0,0 +1,196 @@
package proxmox
import (
"errors"
"fmt"
"log"
"net/url"
"os"
"time"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
"github.com/mitchellh/mapstructure"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"`
RawBootKeyInterval string `mapstructure:"boot_key_interval"`
BootKeyInterval time.Duration ``
Comm communicator.Config `mapstructure:",squash"`
ProxmoxURLRaw string `mapstructure:"proxmox_url"`
ProxmoxURL *url.URL
SkipCertValidation bool `mapstructure:"insecure_skip_tls_verify"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Node string `mapstructure:"node"`
VMName string `mapstructure:"vm_name"`
VMID int `mapstructure:"vm_id"`
Memory int `mapstructure:"memory"`
Cores int `mapstructure:"cores"`
Sockets int `mapstructure:"sockets"`
OS string `mapstructure:"os"`
NICs []nicConfig `mapstructure:"network_adapters"`
Disks []diskConfig `mapstructure:"disks"`
ISOFile string `mapstructure:"iso_file"`
TemplateName string `mapstructure:"template_name"`
TemplateDescription string `mapstructure:"template_description"`
UnmountISO bool `mapstructure:"unmount_iso"`
ctx interpolate.Context
}
type nicConfig struct {
Model string `mapstructure:"model"`
MACAddress string `mapstructure:"mac_address"`
Bridge string `mapstructure:"bridge"`
VLANTag string `mapstructure:"vlan_tag"`
}
type diskConfig struct {
Type string `mapstructure:"type"`
StoragePool string `mapstructure:"storage_pool"`
StoragePoolType string `mapstructure:"storage_pool_type"`
Size string `mapstructure:"size"`
CacheMode string `mapstructure:"cache_mode"`
DiskFormat string `mapstructure:"format"`
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{
Metadata: &md,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"boot_command",
},
},
}, raws...)
if err != nil {
return nil, nil, err
}
var errs *packer.MultiError
// Defaults
if c.ProxmoxURLRaw == "" {
c.ProxmoxURLRaw = os.Getenv("PROXMOX_URL")
}
if c.Username == "" {
c.Username = os.Getenv("PROXMOX_USERNAME")
}
if c.Password == "" {
c.Password = os.Getenv("PROXMOX_PASSWORD")
}
if c.RawBootKeyInterval == "" {
c.RawBootKeyInterval = os.Getenv(common.PackerKeyEnv)
}
if c.RawBootKeyInterval == "" {
c.BootKeyInterval = common.PackerKeyDefault
} else {
if interval, err := time.ParseDuration(c.RawBootKeyInterval); err == nil {
c.BootKeyInterval = interval
} else {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Could not parse boot_key_interval: %v", err))
}
}
if c.VMName == "" {
// Default to packer-[time-ordered-uuid]
c.VMName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
if c.Memory < 16 {
log.Printf("Memory %d is too small, using default: 512", c.Memory)
c.Memory = 512
}
if c.Cores < 1 {
log.Printf("Number of cores %d is too small, using default: 1", c.Cores)
c.Cores = 1
}
if c.Sockets < 1 {
log.Printf("Number of sockets %d is too small, using default: 1", c.Sockets)
c.Sockets = 1
}
if c.OS == "" {
log.Printf("OS not set, using default 'other'")
c.OS = "other"
}
for idx := range c.NICs {
if c.NICs[idx].Model == "" {
log.Printf("NIC %d model not set, using default 'e1000'", idx)
c.NICs[idx].Model = "e1000"
}
}
for idx := range c.Disks {
if c.Disks[idx].Type == "" {
log.Printf("Disk %d type not set, using default 'scsi'", idx)
c.Disks[idx].Type = "scsi"
}
if c.Disks[idx].Size == "" {
log.Printf("Disk %d size not set, using default '20G'", idx)
c.Disks[idx].Size = "20G"
}
if c.Disks[idx].CacheMode == "" {
log.Printf("Disk %d cache mode not set, using default 'none'", idx)
c.Disks[idx].CacheMode = "none"
}
}
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
// Required configurations that will display errors if not set
if c.Username == "" {
errs = packer.MultiErrorAppend(errs, errors.New("username must be specified"))
}
if c.Password == "" {
errs = packer.MultiErrorAppend(errs, errors.New("password must be specified"))
}
if c.ProxmoxURLRaw == "" {
errs = packer.MultiErrorAppend(errs, errors.New("proxmox_url must be specified"))
}
if c.ProxmoxURL, err = url.Parse(c.ProxmoxURLRaw); err != nil {
errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("Could not parse proxmox_url: %s", err)))
}
if c.ISOFile == "" {
errs = packer.MultiErrorAppend(errs, errors.New("iso_file must be specified"))
}
if c.Node == "" {
errs = packer.MultiErrorAppend(errs, errors.New("node must be specified"))
}
for idx := range c.NICs {
if c.NICs[idx].Bridge == "" {
errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("network_adapters[%d].bridge must be specified", idx)))
}
}
for idx := range c.Disks {
if c.Disks[idx].StoragePool == "" {
errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("disks[%d].storage_pool must be specified", idx)))
}
if c.Disks[idx].StoragePoolType == "" {
errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("disks[%d].storage_pool_type must be specified", idx)))
}
}
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}
packer.LogSecretFilter.Set(c.Password)
return c, nil, nil
}

View File

@ -0,0 +1,46 @@
package proxmox
import (
"context"
"fmt"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// stepConvertToTemplate takes the running VM configured in earlier steps, stops it, and
// converts it into a Proxmox template.
//
// It sets the template_id state which is used for Artifact lookup.
type stepConvertToTemplate struct{}
func (s *stepConvertToTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
client := state.Get("proxmoxClient").(*proxmox.Client)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
ui.Say("Stopping VM")
_, err := client.ShutdownVm(vmRef)
if err != nil {
err := fmt.Errorf("Error converting VM to template, could not stop: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Converting VM to template")
err = client.CreateTemplate(vmRef)
if err != nil {
err := fmt.Errorf("Error converting VM to template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("template_id", vmRef.VmId())
return multistep.ActionContinue
}
func (s *stepConvertToTemplate) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,65 @@
package proxmox
import (
"context"
"fmt"
"strings"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// stepFinalizeTemplateConfig does any required modifications to the configuration _after_
// the VM has been converted into a template, such as updating name and description, or
// unmounting the installation ISO.
type stepFinalizeTemplateConfig struct{}
func (s *stepFinalizeTemplateConfig) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
client := state.Get("proxmoxClient").(*proxmox.Client)
c := state.Get("config").(*Config)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
changes := make(map[string]interface{})
if c.TemplateName != "" {
changes["name"] = c.TemplateName
}
// During build, the description is "Packer ephemeral build VM", so if no description is
// set, we need to clear it
changes["description"] = c.TemplateDescription
if c.UnmountISO {
vmParams, err := client.GetVmConfig(vmRef)
if err != nil {
err := fmt.Errorf("Error fetching template config: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if !strings.HasSuffix(vmParams["ide2"].(string), "media=cdrom") {
err := fmt.Errorf("Cannot eject ISO from cdrom drive, ide2 is not present, or not a cdrom media")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
changes["ide2"] = "none,media=cdrom"
}
if len(changes) > 0 {
_, err := client.SetVmConfig(vmRef, changes)
if err != nil {
err := fmt.Errorf("Error updating template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *stepFinalizeTemplateConfig) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,143 @@
package proxmox
import (
"context"
"fmt"
"log"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// stepStartVM takes the given configuration and starts a VM on the given Proxmox node.
//
// It sets the vmRef state which is used throughout the later steps to reference the VM
// in API calls.
type stepStartVM struct{}
func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
client := state.Get("proxmoxClient").(*proxmox.Client)
c := state.Get("config").(*Config)
ui.Say("Creating VM")
config := proxmox.ConfigQemu{
Name: c.VMName,
Agent: "1",
Description: "Packer ephemeral build VM",
Memory: c.Memory,
QemuCores: c.Cores,
QemuSockets: c.Sockets,
QemuOs: c.OS,
QemuIso: c.ISOFile,
QemuNetworks: generateProxmoxNetworkAdapters(c.NICs),
QemuDisks: generateProxmoxDisks(c.Disks),
}
if c.VMID == 0 {
ui.Say("No VM ID given, getting next free from Proxmox")
for n := 0; n < 5; n++ {
id, err := proxmox.MaxVmId(client)
if err != nil {
log.Printf("Error getting max used VM ID: %v (attempt %d/5)", err, n+1)
continue
}
c.VMID = id + 1
break
}
if c.VMID == 0 {
err := fmt.Errorf("Failed to get free VM ID")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
vmRef := proxmox.NewVmRef(c.VMID)
vmRef.SetNode(c.Node)
err := config.CreateVm(vmRef, client)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Store the vm id for later
state.Put("vmRef", vmRef)
ui.Say("Starting VM")
_, err = client.StartVm(vmRef)
if err != nil {
err := fmt.Errorf("Error starting VM: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func generateProxmoxNetworkAdapters(nics []nicConfig) proxmox.QemuDevices {
devs := make(proxmox.QemuDevices)
for idx := range nics {
devs[idx] = make(proxmox.QemuDevice)
setDeviceParamIfDefined(devs[idx], "model", nics[idx].Model)
setDeviceParamIfDefined(devs[idx], "macaddr", nics[idx].MACAddress)
setDeviceParamIfDefined(devs[idx], "bridge", nics[idx].Bridge)
setDeviceParamIfDefined(devs[idx], "tag", nics[idx].VLANTag)
}
return devs
}
func generateProxmoxDisks(disks []diskConfig) proxmox.QemuDevices {
devs := make(proxmox.QemuDevices)
for idx := range disks {
devs[idx] = make(proxmox.QemuDevice)
setDeviceParamIfDefined(devs[idx], "type", disks[idx].Type)
setDeviceParamIfDefined(devs[idx], "size", disks[idx].Size)
setDeviceParamIfDefined(devs[idx], "storage", disks[idx].StoragePool)
setDeviceParamIfDefined(devs[idx], "storage_type", disks[idx].StoragePoolType)
setDeviceParamIfDefined(devs[idx], "cache", disks[idx].CacheMode)
setDeviceParamIfDefined(devs[idx], "format", disks[idx].DiskFormat)
}
return devs
}
func setDeviceParamIfDefined(dev proxmox.QemuDevice, key, value string) {
if value != "" {
dev[key] = value
}
}
func (s *stepStartVM) Cleanup(state multistep.StateBag) {
vmRefUntyped, ok := state.GetOk("vmRef")
// If not ok, we probably errored out before creating the VM
if !ok {
return
}
vmRef := vmRefUntyped.(*proxmox.VmRef)
// The vmRef will actually refer to the created template if everything
// finished successfully, so in that case we shouldn't cleanup
if _, ok := state.GetOk("success"); ok {
return
}
client := state.Get("proxmoxClient").(*proxmox.Client)
ui := state.Get("ui").(packer.Ui)
// Destroy the server we just created
ui.Say("Stopping VM")
_, err := client.StopVm(vmRef)
if err != nil {
ui.Error(fmt.Sprintf("Error stop VM. Please stop and delete it manually: %s", err))
return
}
ui.Say("Deleting VM")
_, err = client.DeleteVm(vmRef)
if err != nil {
ui.Error(fmt.Sprintf("Error deleting VM. Please delete it manually: %s", err))
return
}
}

View File

@ -0,0 +1,22 @@
package proxmox
import (
"context"
"github.com/hashicorp/packer/helper/multistep"
)
// stepSuccess runs after the full build has succeeded.
//
// It sets the success state, which ensures cleanup does not remove the finished template
type stepSuccess struct{}
func (s *stepSuccess) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
// We need to ensure stepStartVM.Cleanup doesn't delete the template (no
// difference between VMs and templates when deleting)
state.Put("success", true)
return multistep.ActionContinue
}
func (s *stepSuccess) Cleanup(state multistep.StateBag) {}

View File

@ -0,0 +1,110 @@
package proxmox
import (
"context"
"errors"
"fmt"
"log"
"net"
"time"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
// stepTypeBootCommand takes the started VM, and sends the keystrokes required to start
// the installation process such that Packer can later reach the VM over SSH/WinRM
type stepTypeBootCommand struct {
bootcommand.BootConfig
Ctx interpolate.Context
}
type bootCommandTemplateData struct {
HTTPIP string
HTTPPort uint
}
func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(*Config)
client := state.Get("proxmoxClient").(*proxmox.Client)
vmRef := state.Get("vmRef").(*proxmox.VmRef)
if len(s.BootCommand) == 0 {
log.Println("No boot command given, skipping")
return multistep.ActionContinue
}
if int64(s.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot", s.BootWait.String()))
select {
case <-time.After(s.BootWait):
break
case <-ctx.Done():
return multistep.ActionHalt
}
}
httpIP, err := hostIP()
if err != nil {
err := fmt.Errorf("Failed to determine host IP: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
common.SetHTTPIP(httpIP)
s.Ctx.Data = &bootCommandTemplateData{
HTTPIP: httpIP,
HTTPPort: state.Get("http_port").(uint),
}
ui.Say("Typing the boot command")
d := NewProxmoxDriver(client, vmRef, c.BootKeyInterval)
command, err := interpolate.Render(s.FlatBootCommand(), &s.Ctx)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
seq, err := bootcommand.GenerateExpressionSequence(command)
if err != nil {
err := fmt.Errorf("Error generating boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := seq.Do(ctx, d); err != nil {
err := fmt.Errorf("Error running boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
func hostIP() (string, error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String(), nil
}
}
}
return "", errors.New("No host IP found")
}

View File

@ -40,6 +40,7 @@ import (
parallelsisobuilder "github.com/hashicorp/packer/builder/parallels/iso"
parallelspvmbuilder "github.com/hashicorp/packer/builder/parallels/pvm"
profitbricksbuilder "github.com/hashicorp/packer/builder/profitbricks"
proxmoxbuilder "github.com/hashicorp/packer/builder/proxmox"
qemubuilder "github.com/hashicorp/packer/builder/qemu"
scalewaybuilder "github.com/hashicorp/packer/builder/scaleway"
tencentcloudcvmbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm"
@ -117,6 +118,7 @@ var Builders = map[string]packer.Builder{
"parallels-iso": new(parallelsisobuilder.Builder),
"parallels-pvm": new(parallelspvmbuilder.Builder),
"profitbricks": new(profitbricksbuilder.Builder),
"proxmox": new(proxmoxbuilder.Builder),
"qemu": new(qemubuilder.Builder),
"scaleway": new(scalewaybuilder.Builder),
"tencentcloud-cvm": new(tencentcloudcvmbuilder.Builder),