176 lines
4.7 KiB
Go
176 lines
4.7 KiB
Go
package proxmox
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/Telmate/proxmox-api-go/proxmox"
|
|
"github.com/hashicorp/hcl/v2/hcldec"
|
|
"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) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
|
|
|
|
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|
warnings, errs := b.config.Prepare(raws...)
|
|
if errs != nil {
|
|
return nil, warnings, errs
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
const downloadPathKey = "downloaded_iso_path"
|
|
|
|
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (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{
|
|
&common.StepDownload{
|
|
Checksum: b.config.ISOChecksum,
|
|
Description: "ISO",
|
|
Extension: b.config.TargetExtension,
|
|
ResultKey: downloadPathKey,
|
|
TargetPath: b.config.TargetPath,
|
|
Url: b.config.ISOUrls,
|
|
}}
|
|
|
|
for idx := range b.config.AdditionalISOFiles {
|
|
steps = append(steps, &common.StepDownload{
|
|
Checksum: b.config.AdditionalISOFiles[idx].ISOChecksum,
|
|
Description: "additional ISO",
|
|
Extension: b.config.AdditionalISOFiles[idx].TargetExtension,
|
|
ResultKey: b.config.AdditionalISOFiles[idx].downloadPathKey,
|
|
TargetPath: b.config.AdditionalISOFiles[idx].downloadPathKey,
|
|
Url: b.config.AdditionalISOFiles[idx].ISOUrls,
|
|
})
|
|
}
|
|
steps = append(steps,
|
|
&stepUploadISO{},
|
|
&stepUploadAdditionalISOs{},
|
|
&stepStartVM{},
|
|
&common.StepHTTPServer{
|
|
HTTPDir: b.config.HTTPDir,
|
|
HTTPPortMin: b.config.HTTPPortMin,
|
|
HTTPPortMax: b.config.HTTPPortMax,
|
|
HTTPAddress: b.config.HTTPAddress,
|
|
},
|
|
&stepTypeBootCommand{
|
|
BootConfig: b.config.BootConfig,
|
|
Ctx: b.config.ctx,
|
|
},
|
|
&communicator.StepConnect{
|
|
Config: &b.config.Comm,
|
|
Host: commHost(b.config.Comm.Host()),
|
|
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(ctx, state)
|
|
// If there was an error, return that
|
|
if rawErr, ok := state.GetOk("error"); ok {
|
|
return nil, rawErr.(error)
|
|
}
|
|
// If we were interrupted or cancelled, then just exit.
|
|
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
|
return nil, errors.New("build was cancelled")
|
|
}
|
|
|
|
// Verify that the template_id was set properly, otherwise we didn't progress through the last step
|
|
tplID, ok := state.Get("template_id").(int)
|
|
if !ok {
|
|
return nil, fmt.Errorf("template ID could not be determined")
|
|
}
|
|
|
|
artifact := &Artifact{
|
|
templateID: tplID,
|
|
proxmoxClient: b.proxmoxClient,
|
|
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
|
}
|
|
|
|
return artifact, nil
|
|
}
|
|
|
|
// Returns ssh_host or winrm_host (see communicator.Config.Host) config
|
|
// parameter when set, otherwise gets the host IP from running VM
|
|
func commHost(host string) func(state multistep.StateBag) (string, error) {
|
|
if host != "" {
|
|
return func(state multistep.StateBag) (string, error) {
|
|
return host, nil
|
|
}
|
|
}
|
|
return getVMIP
|
|
}
|
|
|
|
// Reads the first non-loopback interface's IP address from the VM.
|
|
// qemu-guest-agent package must be installed on the VM
|
|
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")
|
|
}
|