Initial checkin to GitHub -- has extensive changes to conform to the latest API model to match the 0.3.6 (Sept. 2, 2013) release.
This commit is contained in:
parent
8e9428633b
commit
30d004022e
33
builder/qemu/artifact.go
Normal file
33
builder/qemu/artifact.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Artifact is the result of running the VirtualBox builder, namely a set
|
||||||
|
// of files associated with the resulting machine.
|
||||||
|
type Artifact struct {
|
||||||
|
dir string
|
||||||
|
f []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Artifact) BuilderId() string {
|
||||||
|
return BuilderId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Files() []string {
|
||||||
|
return a.f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Artifact) Id() string {
|
||||||
|
return "VM"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) String() string {
|
||||||
|
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Destroy() error {
|
||||||
|
return os.RemoveAll(a.dir)
|
||||||
|
}
|
435
builder/qemu/builder.go
Normal file
435
builder/qemu/builder.go
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/common"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const BuilderId = "tdhite.qemu"
|
||||||
|
|
||||||
|
type Builder struct {
|
||||||
|
config config
|
||||||
|
runner multistep.Runner
|
||||||
|
}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
|
||||||
|
BootCommand []string `mapstructure:"boot_command"`
|
||||||
|
DiskSize uint `mapstructure:"disk_size"`
|
||||||
|
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||||
|
Format string `mapstructure:"format"`
|
||||||
|
Accelerator string `mapstructure:"accelerator"`
|
||||||
|
Headless bool `mapstructure:"headless"`
|
||||||
|
HTTPDir string `mapstructure:"http_directory"`
|
||||||
|
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||||
|
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||||
|
ISOChecksum string `mapstructure:"iso_checksum"`
|
||||||
|
ISOChecksumType string `mapstructure:"iso_checksum_type"`
|
||||||
|
ISOUrls []string `mapstructure:"iso_urls"`
|
||||||
|
OutputDir string `mapstructure:"output_directory"`
|
||||||
|
QemuArgs [][]string `mapstructure:"qemuargs"`
|
||||||
|
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||||
|
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||||
|
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||||
|
SSHPassword string `mapstructure:"ssh_password"`
|
||||||
|
SSHPort uint `mapstructure:"ssh_port"`
|
||||||
|
SSHUser string `mapstructure:"ssh_username"`
|
||||||
|
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||||
|
VNCPortMin uint `mapstructure:"vnc_port_min"`
|
||||||
|
VNCPortMax uint `mapstructure:"vnc_port_max"`
|
||||||
|
VMName string `mapstructure:"vm_name"`
|
||||||
|
|
||||||
|
RawBootWait string `mapstructure:"boot_wait"`
|
||||||
|
RawSingleISOUrl string `mapstructure:"iso_url"`
|
||||||
|
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
|
||||||
|
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
||||||
|
|
||||||
|
bootWait time.Duration ``
|
||||||
|
shutdownTimeout time.Duration ``
|
||||||
|
sshWaitTimeout time.Duration ``
|
||||||
|
tpl *packer.ConfigTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||||
|
md, err := common.DecodeConfig(&b.config, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.config.tpl, err = packer.NewConfigTemplate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||||
|
|
||||||
|
// Accumulate any errors
|
||||||
|
errs := common.CheckUnusedConfig(md)
|
||||||
|
|
||||||
|
if b.config.DiskSize == 0 {
|
||||||
|
b.config.DiskSize = 40000
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.FloppyFiles == nil {
|
||||||
|
b.config.FloppyFiles = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.Accelerator == "" {
|
||||||
|
b.config.Accelerator = "kvm"
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.HTTPPortMin == 0 {
|
||||||
|
b.config.HTTPPortMin = 8000
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.HTTPPortMax == 0 {
|
||||||
|
b.config.HTTPPortMax = 9000
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.OutputDir == "" {
|
||||||
|
b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.RawBootWait == "" {
|
||||||
|
b.config.RawBootWait = "10s"
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHHostPortMin == 0 {
|
||||||
|
b.config.SSHHostPortMin = 2222
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHHostPortMax == 0 {
|
||||||
|
b.config.SSHHostPortMax = 4444
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHPort == 0 {
|
||||||
|
b.config.SSHPort = 22
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.VNCPortMin == 0 {
|
||||||
|
b.config.VNCPortMin = 5900
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.VNCPortMax == 0 {
|
||||||
|
b.config.VNCPortMax = 6000
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.QemuArgs == nil {
|
||||||
|
b.config.QemuArgs = make([][]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.VMName == "" {
|
||||||
|
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.Format == "" {
|
||||||
|
b.config.Format = "qcow2"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
templates := map[string]*string{
|
||||||
|
"http_directory": &b.config.HTTPDir,
|
||||||
|
"iso_checksum": &b.config.ISOChecksum,
|
||||||
|
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||||
|
"iso_url": &b.config.RawSingleISOUrl,
|
||||||
|
"output_directory": &b.config.OutputDir,
|
||||||
|
"shutdown_command": &b.config.ShutdownCommand,
|
||||||
|
"ssh_password": &b.config.SSHPassword,
|
||||||
|
"ssh_username": &b.config.SSHUser,
|
||||||
|
"vm_name": &b.config.VMName,
|
||||||
|
"format": &b.config.Format,
|
||||||
|
"boot_wait": &b.config.RawBootWait,
|
||||||
|
"shutdown_timeout": &b.config.RawShutdownTimeout,
|
||||||
|
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
|
||||||
|
"accelerator": &b.config.Accelerator,
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, ptr := range templates {
|
||||||
|
var err error
|
||||||
|
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, url := range b.config.ISOUrls {
|
||||||
|
var err error
|
||||||
|
b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, command := range b.config.BootCommand {
|
||||||
|
if err := b.config.tpl.Validate(command); err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, file := range b.config.FloppyFiles {
|
||||||
|
var err error
|
||||||
|
b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Error processing floppy_files[%d]: %s",
|
||||||
|
i, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("invalid format, only 'ovf' or 'ova' are allowed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(b.config.Accelerator == "kvm" || b.config.Accelerator == "xen") {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("invalid format, only 'kvm' or 'xen' are allowed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.ISOChecksum == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("Due to large file sizes, an iso_checksum is required"))
|
||||||
|
} else {
|
||||||
|
b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.ISOChecksumType == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("The iso_checksum_type must be specified."))
|
||||||
|
} else {
|
||||||
|
b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType)
|
||||||
|
if h := common.HashForType(b.config.ISOChecksumType); h == nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs,
|
||||||
|
fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("One of iso_url or iso_urls must be specified."))
|
||||||
|
} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("Only one of iso_url or iso_urls may be specified."))
|
||||||
|
} else if b.config.RawSingleISOUrl != "" {
|
||||||
|
b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, url := range b.config.ISOUrls {
|
||||||
|
b.config.ISOUrls[i], err = common.DownloadableURL(url)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.config.PackerForce {
|
||||||
|
if _, err := os.Stat(b.config.OutputDir); err == nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs,
|
||||||
|
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Failed parsing boot_wait: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.RawShutdownTimeout == "" {
|
||||||
|
b.config.RawShutdownTimeout = "5m"
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.RawSSHWaitTimeout == "" {
|
||||||
|
b.config.RawSSHWaitTimeout = "20m"
|
||||||
|
}
|
||||||
|
|
||||||
|
b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHKeyPath != "" {
|
||||||
|
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||||
|
} else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHUser == "" {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, errors.New("An ssh_username must be specified."))
|
||||||
|
}
|
||||||
|
|
||||||
|
b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout)
|
||||||
|
if err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.VNCPortMin > b.config.VNCPortMax {
|
||||||
|
errs = packer.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, args := range b.config.QemuArgs {
|
||||||
|
for j, arg := range args {
|
||||||
|
if err := b.config.tpl.Validate(arg); err != nil {
|
||||||
|
errs = packer.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("Error processing qemu-system_x86-64[%d][%d]: %s", i, j, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||||
|
// Create the driver that we'll use to communicate with Qemu
|
||||||
|
driver, err := b.newDriver()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := []multistep.Step{
|
||||||
|
&common.StepDownload{
|
||||||
|
Checksum: b.config.ISOChecksum,
|
||||||
|
ChecksumType: b.config.ISOChecksumType,
|
||||||
|
Description: "ISO",
|
||||||
|
ResultKey: "iso_path",
|
||||||
|
Url: b.config.ISOUrls,
|
||||||
|
},
|
||||||
|
new(stepPrepareOutputDir),
|
||||||
|
&common.StepCreateFloppy{
|
||||||
|
Files: b.config.FloppyFiles,
|
||||||
|
},
|
||||||
|
new(stepCreateDisk),
|
||||||
|
new(stepSuppressMessages),
|
||||||
|
new(stepHTTPServer),
|
||||||
|
new(stepForwardSSH),
|
||||||
|
new(stepConfigureVNC),
|
||||||
|
new(stepRun),
|
||||||
|
&common.StepConnectSSH{
|
||||||
|
SSHAddress: sshAddress,
|
||||||
|
SSHConfig: sshConfig,
|
||||||
|
SSHWaitTimeout: b.config.sshWaitTimeout,
|
||||||
|
},
|
||||||
|
new(common.StepProvision),
|
||||||
|
new(stepShutdown),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the state bag
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("cache", cache)
|
||||||
|
state.Put("config", &b.config)
|
||||||
|
state.Put("driver", driver)
|
||||||
|
state.Put("hook", hook)
|
||||||
|
state.Put("ui", ui)
|
||||||
|
|
||||||
|
// Run
|
||||||
|
if b.config.PackerDebug {
|
||||||
|
b.runner = &multistep.DebugRunner{
|
||||||
|
Steps: steps,
|
||||||
|
PauseFn: common.MultistepDebugFn(ui),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.runner.Run(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.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk(multistep.StateHalted); ok {
|
||||||
|
return nil, errors.New("Build was halted.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile the artifact list
|
||||||
|
files := make([]string, 0, 5)
|
||||||
|
visit := func(path string, info os.FileInfo, err error) error {
|
||||||
|
if !info.IsDir() {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
artifact := &Artifact{
|
||||||
|
dir: b.config.OutputDir,
|
||||||
|
f: files,
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifact, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Cancel() {
|
||||||
|
if b.runner != nil {
|
||||||
|
log.Println("Cancelling the step runner...")
|
||||||
|
b.runner.Cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) newDriver() (Driver, error) {
|
||||||
|
qemuPath, err := exec.LookPath("qemu-system-x86_64")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qemuImgPath, err := exec.LookPath("qemu-img")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath)
|
||||||
|
driver := &QemuDriver{}
|
||||||
|
driver.Initialize(qemuPath, qemuImgPath)
|
||||||
|
|
||||||
|
if err := driver.Verify(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return driver, nil
|
||||||
|
}
|
571
builder/qemu/builder_test.go
Normal file
571
builder/qemu/builder_test.go
Normal file
@ -0,0 +1,571 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testPem = `
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
|
||||||
|
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
|
||||||
|
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
|
||||||
|
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
|
||||||
|
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
|
||||||
|
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
|
||||||
|
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
|
||||||
|
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
|
||||||
|
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
|
||||||
|
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
|
||||||
|
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
|
||||||
|
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
|
||||||
|
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
|
||||||
|
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
|
||||||
|
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
|
||||||
|
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
|
||||||
|
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
|
||||||
|
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
|
||||||
|
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
|
||||||
|
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
|
||||||
|
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
|
||||||
|
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
|
||||||
|
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
|
||||||
|
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
|
||||||
|
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
func testConfig() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"iso_checksum": "foo",
|
||||||
|
"iso_checksum_type": "md5",
|
||||||
|
"iso_url": "http://www.google.com/",
|
||||||
|
"ssh_username": "foo",
|
||||||
|
packer.BuildNameConfigKey: "foo",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Builder{}
|
||||||
|
if _, ok := raw.(packer.Builder); !ok {
|
||||||
|
t.Error("Builder must implement builder.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_Defaults(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.OutputDir != "output-foo" {
|
||||||
|
t.Errorf("bad output dir: %s", b.config.OutputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHHostPortMin != 2222 {
|
||||||
|
t.Errorf("bad min ssh host port: %d", b.config.SSHHostPortMin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHHostPortMax != 4444 {
|
||||||
|
t.Errorf("bad max ssh host port: %d", b.config.SSHHostPortMax)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.SSHPort != 22 {
|
||||||
|
t.Errorf("bad ssh port: %d", b.config.SSHPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.VMName != "packer-foo" {
|
||||||
|
t.Errorf("bad vm name: %s", b.config.VMName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.Format != "qcow2" {
|
||||||
|
t.Errorf("bad format: %s", b.config.Format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_BootWait(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Test a default boot_wait
|
||||||
|
delete(config, "boot_wait")
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.RawBootWait != "10s" {
|
||||||
|
t.Fatalf("bad value: %s", b.config.RawBootWait)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a bad boot_wait
|
||||||
|
config["boot_wait"] = "this is not good"
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a good one
|
||||||
|
config["boot_wait"] = "5s"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_DiskSize(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
delete(config, "disk_size")
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.DiskSize != 40000 {
|
||||||
|
t.Fatalf("bad size: %d", b.config.DiskSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
config["disk_size"] = 60000
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.DiskSize != 60000 {
|
||||||
|
t.Fatalf("bad size: %s", b.config.DiskSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
delete(config, "floppy_files")
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.config.FloppyFiles) != 0 {
|
||||||
|
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
config["floppy_files"] = []string{"foo", "bar"}
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"foo", "bar"}
|
||||||
|
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
|
||||||
|
t.Fatalf("bad: %#v", b.config.FloppyFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
config["http_port_min"] = 1000
|
||||||
|
config["http_port_max"] = 500
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
config["http_port_min"] = -500
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good
|
||||||
|
config["http_port_min"] = 500
|
||||||
|
config["http_port_max"] = 1000
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_Format(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
config["format"] = "illegal value"
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good
|
||||||
|
config["format"] = "qcow2"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good
|
||||||
|
config["format"] = "raw"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Add a random key
|
||||||
|
config["i_should_not_be_valid"] = true
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_ISOChecksum(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Test bad
|
||||||
|
config["iso_checksum"] = ""
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test good
|
||||||
|
config["iso_checksum"] = "FOo"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.ISOChecksum != "foo" {
|
||||||
|
t.Fatalf("should've lowercased: %s", b.config.ISOChecksum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Test bad
|
||||||
|
config["iso_checksum_type"] = ""
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test good
|
||||||
|
config["iso_checksum_type"] = "mD5"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.ISOChecksumType != "md5" {
|
||||||
|
t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test unknown
|
||||||
|
config["iso_checksum_type"] = "fake"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_ISOUrl(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
delete(config, "iso_url")
|
||||||
|
delete(config, "iso_urls")
|
||||||
|
|
||||||
|
// Test both epty
|
||||||
|
config["iso_url"] = ""
|
||||||
|
b = Builder{}
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test iso_url set
|
||||||
|
config["iso_url"] = "http://www.packer.io"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"http://www.packer.io"}
|
||||||
|
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
|
||||||
|
t.Fatalf("bad: %#v", b.config.ISOUrls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test both set
|
||||||
|
config["iso_url"] = "http://www.packer.io"
|
||||||
|
config["iso_urls"] = []string{"http://www.packer.io"}
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test just iso_urls set
|
||||||
|
delete(config, "iso_url")
|
||||||
|
config["iso_urls"] = []string{
|
||||||
|
"http://www.packer.io",
|
||||||
|
"http://www.hashicorp.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = []string{
|
||||||
|
"http://www.packer.io",
|
||||||
|
"http://www.hashicorp.com",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
|
||||||
|
t.Fatalf("bad: %#v", b.config.ISOUrls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Test with existing dir
|
||||||
|
dir, err := ioutil.TempDir("", "packer")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
|
||||||
|
config["output_directory"] = dir
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a good one
|
||||||
|
config["output_directory"] = "i-hope-i-dont-exist"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Test with a bad value
|
||||||
|
config["shutdown_timeout"] = "this is not good"
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a good one
|
||||||
|
config["shutdown_timeout"] = "5s"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_SSHHostPort(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
config["ssh_host_port_min"] = 1000
|
||||||
|
config["ssh_host_port_max"] = 500
|
||||||
|
b = Builder{}
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad
|
||||||
|
config["ssh_host_port_min"] = -500
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good
|
||||||
|
config["ssh_host_port_min"] = 500
|
||||||
|
config["ssh_host_port_max"] = 1000
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_sshKeyPath(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
config["ssh_key_path"] = ""
|
||||||
|
b = Builder{}
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config["ssh_key_path"] = "/i/dont/exist"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test bad contents
|
||||||
|
tf, err := ioutil.TempFile("", "packer")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tf.Name())
|
||||||
|
defer tf.Close()
|
||||||
|
|
||||||
|
if _, err := tf.Write([]byte("HELLO!")); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config["ssh_key_path"] = tf.Name()
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test good contents
|
||||||
|
tf.Seek(0, 0)
|
||||||
|
tf.Truncate(0)
|
||||||
|
tf.Write([]byte(testPem))
|
||||||
|
config["ssh_key_path"] = tf.Name()
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_SSHUser(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
config["ssh_username"] = ""
|
||||||
|
b = Builder{}
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
config["ssh_username"] = "exists"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Test a default boot_wait
|
||||||
|
delete(config, "ssh_wait_timeout")
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.config.RawSSHWaitTimeout != "20m" {
|
||||||
|
t.Fatalf("bad value: %s", b.config.RawSSHWaitTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a bad value
|
||||||
|
config["ssh_wait_timeout"] = "this is not good"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a good one
|
||||||
|
config["ssh_wait_timeout"] = "5s"
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_QemuArgs(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
config := testConfig()
|
||||||
|
|
||||||
|
// Test with empty
|
||||||
|
delete(config, "qemuargs")
|
||||||
|
err := b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(b.config.QemuArgs, [][]string{}) {
|
||||||
|
t.Fatalf("bad: %#v", b.config.QemuArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with a good one
|
||||||
|
config["qemuargs"] = [][]interface{}{
|
||||||
|
[]interface{}{"foo", "bar", "baz"},
|
||||||
|
}
|
||||||
|
|
||||||
|
b = Builder{}
|
||||||
|
err = b.Prepare(config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("should not have error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := [][]string{
|
||||||
|
[]string{"foo", "bar", "baz"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(b.config.QemuArgs, expected) {
|
||||||
|
t.Fatalf("bad: %#v", b.config.QemuArgs)
|
||||||
|
}
|
||||||
|
}
|
252
builder/qemu/driver.go
Normal file
252
builder/qemu/driver.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DriverCancelCallback func(state multistep.StateBag) bool
|
||||||
|
|
||||||
|
// A driver is able to talk to VirtualBox and perform certain
|
||||||
|
// operations with it.
|
||||||
|
type Driver interface {
|
||||||
|
// Initializes the driver with the given values:
|
||||||
|
// Arguments: qemuPath - string value for the qemu-system-x86_64 executable
|
||||||
|
// qemuImgPath - string value for the qemu-img executable
|
||||||
|
Initialize(string, string)
|
||||||
|
|
||||||
|
// Checks if the VM with the given name is running.
|
||||||
|
IsRunning(string) (bool, error)
|
||||||
|
|
||||||
|
// Stop stops a running machine, forcefully.
|
||||||
|
Stop(string) error
|
||||||
|
|
||||||
|
// SuppressMessages should do what needs to be done in order to
|
||||||
|
// suppress any annoying popups from VirtualBox.
|
||||||
|
SuppressMessages() error
|
||||||
|
|
||||||
|
// Qemu executes the given command via qemu-system-x86_64
|
||||||
|
Qemu(vmName string, qemuArgs ...string) error
|
||||||
|
|
||||||
|
// wait on shutdown of the VM with option to cancel
|
||||||
|
WaitForShutdown(
|
||||||
|
vmName string,
|
||||||
|
block bool,
|
||||||
|
state multistep.StateBag,
|
||||||
|
cancellCallback DriverCancelCallback) error
|
||||||
|
|
||||||
|
// Qemu executes the given command via qemu-img
|
||||||
|
QemuImg(...string) error
|
||||||
|
|
||||||
|
// Verify checks to make sure that this driver should function
|
||||||
|
// properly. If there is any indication the driver can't function,
|
||||||
|
// this will return an error.
|
||||||
|
Verify() error
|
||||||
|
|
||||||
|
// Version reads the version of VirtualBox that is installed.
|
||||||
|
Version() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type driverState struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
cancelChan chan struct{}
|
||||||
|
waitDone chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
type QemuDriver struct {
|
||||||
|
qemuPath string
|
||||||
|
qemuImgPath string
|
||||||
|
state map[string]*driverState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) getDriverState(name string) *driverState {
|
||||||
|
if _, ok := d.state[name]; !ok {
|
||||||
|
d.state[name] = &driverState{}
|
||||||
|
}
|
||||||
|
return d.state[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) Initialize(qemuPath string, qemuImgPath string) {
|
||||||
|
d.qemuPath = qemuPath
|
||||||
|
d.qemuImgPath = qemuImgPath
|
||||||
|
d.state = make(map[string]*driverState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) IsRunning(name string) (bool, error) {
|
||||||
|
ds := d.getDriverState(name)
|
||||||
|
return ds.cancelChan != nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) Stop(name string) error {
|
||||||
|
ds := d.getDriverState(name)
|
||||||
|
|
||||||
|
// signal to the command 'wait' to kill the process
|
||||||
|
if ds.cancelChan != nil {
|
||||||
|
close(ds.cancelChan)
|
||||||
|
ds.cancelChan = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) SuppressMessages() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) Qemu(vmName string, qemuArgs ...string) error {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
log.Printf("Executing %s: %#v", d.qemuPath, qemuArgs)
|
||||||
|
ds := d.getDriverState(vmName)
|
||||||
|
ds.cmd = exec.Command(d.qemuPath, qemuArgs...)
|
||||||
|
ds.cmd.Stdout = &stdout
|
||||||
|
ds.cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
err := ds.cmd.Start()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error starting VM: %s", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("---- Started Qemu ------- PID = ", ds.cmd.Process.Pid)
|
||||||
|
|
||||||
|
ds.cancelChan = make(chan struct{})
|
||||||
|
|
||||||
|
// make the channel to watch the process
|
||||||
|
ds.waitDone = make(chan error)
|
||||||
|
|
||||||
|
// start the virtual machine in the background
|
||||||
|
go func() {
|
||||||
|
ds.waitDone <- ds.cmd.Wait()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) WaitForShutdown(vmName string,
|
||||||
|
block bool,
|
||||||
|
state multistep.StateBag,
|
||||||
|
cancelCallback DriverCancelCallback) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ds := d.getDriverState(vmName)
|
||||||
|
|
||||||
|
if block {
|
||||||
|
// wait in the background for completion or caller cancel
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ds.cancelChan:
|
||||||
|
log.Println("Qemu process request to cancel -- killing Qemu process.")
|
||||||
|
if err = ds.cmd.Process.Kill(); err != nil {
|
||||||
|
log.Printf("Failed to kill qemu: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear out the error channel since it's just a cancel
|
||||||
|
// and therefore the reason for failure is clear
|
||||||
|
log.Println("Empytying waitDone channel.")
|
||||||
|
<-ds.waitDone
|
||||||
|
|
||||||
|
// this gig is over -- assure calls to IsRunning see the nil
|
||||||
|
log.Println("'Nil'ing out cancelChan.")
|
||||||
|
ds.cancelChan = nil
|
||||||
|
return errors.New("WaitForShutdown cancelled")
|
||||||
|
case err = <-ds.waitDone:
|
||||||
|
log.Printf("Qemu Process done with output = %v", err)
|
||||||
|
// assure calls to IsRunning see the nil
|
||||||
|
log.Println("'Nil'ing out cancelChan.")
|
||||||
|
ds.cancelChan = nil
|
||||||
|
return nil
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
cancel := cancelCallback(state)
|
||||||
|
if cancel {
|
||||||
|
log.Println("Qemu process request to cancel -- killing Qemu process.")
|
||||||
|
|
||||||
|
// The step sequence was cancelled, so cancel waiting for SSH
|
||||||
|
// and just start the halting process.
|
||||||
|
close(ds.cancelChan)
|
||||||
|
|
||||||
|
log.Println("Cancel request made, quitting waiting for Qemu.")
|
||||||
|
return errors.New("WaitForShutdown cancelled by interrupt.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ds.cancelChan:
|
||||||
|
log.Println("Qemu process request to cancel -- killing Qemu process.")
|
||||||
|
if err = ds.cmd.Process.Kill(); err != nil {
|
||||||
|
log.Printf("Failed to kill qemu: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear out the error channel since it's just a cancel
|
||||||
|
// and therefore the reason for failure is clear
|
||||||
|
log.Println("Empytying waitDone channel.")
|
||||||
|
<-ds.waitDone
|
||||||
|
log.Println("'Nil'ing out cancelChan.")
|
||||||
|
ds.cancelChan = nil
|
||||||
|
|
||||||
|
case err = <-ds.waitDone:
|
||||||
|
log.Printf("Qemu Process done with output = %v", err)
|
||||||
|
log.Println("'Nil'ing out cancelChan.")
|
||||||
|
ds.cancelChan = nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.cancelChan = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) QemuImg(args ...string) error {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
log.Printf("Executing qemu-img: %#v", args)
|
||||||
|
cmd := exec.Command(d.qemuImgPath, args...)
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
stdoutString := strings.TrimSpace(stdout.String())
|
||||||
|
stderrString := strings.TrimSpace(stderr.String())
|
||||||
|
|
||||||
|
if _, ok := err.(*exec.ExitError); ok {
|
||||||
|
err = fmt.Errorf("QemuImg error: %s", stderrString)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("stdout: %s", stdoutString)
|
||||||
|
log.Printf("stderr: %s", stderrString)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) Verify() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *QemuDriver) Version() (string, error) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
|
||||||
|
cmd := exec.Command(d.qemuPath, "-version")
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
versionOutput := strings.TrimSpace(stdout.String())
|
||||||
|
log.Printf("Qemu --version output: %s", versionOutput)
|
||||||
|
versionRe := regexp.MustCompile("qemu-kvm-[0-9]\\.[0-9]")
|
||||||
|
matches := versionRe.Split(versionOutput, 2)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return "", fmt.Errorf("No version found: %s", versionOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Qemu version: %s", matches[0])
|
||||||
|
return matches[0], nil
|
||||||
|
}
|
59
builder/qemu/ssh.go
Normal file
59
builder/qemu/ssh.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
gossh "code.google.com/p/go.crypto/ssh"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/communicator/ssh"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sshAddress(state multistep.StateBag) (string, error) {
|
||||||
|
sshHostPort := state.Get("sshHostPort").(uint)
|
||||||
|
return fmt.Sprintf("127.0.0.1:%d", sshHostPort), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
|
||||||
|
auth := []gossh.ClientAuth{
|
||||||
|
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
|
||||||
|
gossh.ClientAuthKeyboardInteractive(
|
||||||
|
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SSHKeyPath != "" {
|
||||||
|
keyring, err := sshKeyToKeyring(config.SSHKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
auth = append(auth, gossh.ClientAuthKeyring(keyring))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &gossh.ClientConfig{
|
||||||
|
User: config.SSHUser,
|
||||||
|
Auth: auth,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshKeyToKeyring(path string) (gossh.ClientKeyring, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
keyring := new(ssh.SimpleKeychain)
|
||||||
|
if err := keyring.AddPEMKey(string(keyBytes)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyring, nil
|
||||||
|
}
|
53
builder/qemu/step_configure_vnc.go
Normal file
53
builder/qemu/step_configure_vnc.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This step configures the VM to enable the VNC server.
|
||||||
|
//
|
||||||
|
// Uses:
|
||||||
|
// config *config
|
||||||
|
// ui packer.Ui
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// vnc_port uint - The port that VNC is configured to listen on.
|
||||||
|
type stepConfigureVNC struct{}
|
||||||
|
|
||||||
|
func (stepConfigureVNC) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// Find an open VNC port. Note that this can still fail later on
|
||||||
|
// because we have to release the port at some point. But this does its
|
||||||
|
// best.
|
||||||
|
msg := fmt.Sprintf("Looking for available port between %d and %d", config.VNCPortMin, config.VNCPortMax)
|
||||||
|
ui.Say(msg)
|
||||||
|
log.Printf(msg)
|
||||||
|
var vncPort uint
|
||||||
|
portRange := int(config.VNCPortMax - config.VNCPortMin)
|
||||||
|
for {
|
||||||
|
vncPort = uint(rand.Intn(portRange)) + config.VNCPortMin
|
||||||
|
log.Printf("Trying port: %d", vncPort)
|
||||||
|
l, err := net.Listen("tcp", fmt.Sprintf(":%d", vncPort))
|
||||||
|
if err == nil {
|
||||||
|
defer l.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msg = fmt.Sprintf("Found available VNC port: %d", vncPort)
|
||||||
|
ui.Say(msg)
|
||||||
|
log.Printf(msg)
|
||||||
|
|
||||||
|
state.Put("vnc_port", vncPort)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stepConfigureVNC) Cleanup(multistep.StateBag) {}
|
84
builder/qemu/step_copy_floppy.go
Normal file
84
builder/qemu/step_copy_floppy.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This step attaches the ISO to the virtual machine.
|
||||||
|
//
|
||||||
|
// Uses:
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
type stepCopyFloppy struct {
|
||||||
|
floppyPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCopyFloppy) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
// Determine if we even have a floppy disk to attach
|
||||||
|
var floppyPath string
|
||||||
|
if floppyPathRaw, ok := state.GetOk("floppy_path"); ok {
|
||||||
|
floppyPath = floppyPathRaw.(string)
|
||||||
|
} else {
|
||||||
|
log.Println("No floppy disk, not attaching.")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the floppy for exclusive use during the vm creation
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
ui.Say("Copying floppy disk for exclusive use...")
|
||||||
|
floppyPath, err := s.copyFloppy(floppyPath)
|
||||||
|
if err != nil {
|
||||||
|
state.Put("error", fmt.Errorf("Error preparing floppy: %s", err))
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track the path so that we can remove it later
|
||||||
|
s.floppyPath = floppyPath
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCopyFloppy) Cleanup(state multistep.StateBag) {
|
||||||
|
if s.floppyPath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the floppy disk
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
ui.Say("Removing floppy disk previously copied...")
|
||||||
|
defer os.Remove(s.floppyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCopyFloppy) copyFloppy(path string) (string, error) {
|
||||||
|
tempdir, err := ioutil.TempDir("", "packer")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
floppyPath := filepath.Join(tempdir, "floppy.img")
|
||||||
|
f, err := os.Create(floppyPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
sourceF, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer sourceF.Close()
|
||||||
|
|
||||||
|
log.Printf("Copying floppy to temp location: %s", floppyPath)
|
||||||
|
if _, err := io.Copy(f, sourceF); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return floppyPath, nil
|
||||||
|
}
|
40
builder/qemu/step_create_disk.go
Normal file
40
builder/qemu/step_create_disk.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This step creates the virtual disk that will be used as the
|
||||||
|
// hard drive for the virtual machine.
|
||||||
|
type stepCreateDisk struct{}
|
||||||
|
|
||||||
|
func (s *stepCreateDisk) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
driver := state.Get("driver").(Driver)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s.%s", config.VMName,
|
||||||
|
strings.ToLower(config.Format)))
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"create",
|
||||||
|
"-f", config.Format,
|
||||||
|
path,
|
||||||
|
fmt.Sprintf("%vM", config.DiskSize),
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Creating hard drive...")
|
||||||
|
if err := driver.QemuImg(command...); err != nil {
|
||||||
|
err := fmt.Errorf("Error creating hard drive: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {}
|
44
builder/qemu/step_forward_ssh.go
Normal file
44
builder/qemu/step_forward_ssh.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This step adds a NAT port forwarding definition so that SSH is available
|
||||||
|
// on the guest machine.
|
||||||
|
//
|
||||||
|
// Uses:
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
type stepForwardSSH struct{}
|
||||||
|
|
||||||
|
func (s *stepForwardSSH) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
log.Printf("Looking for available SSH port between %d and %d", config.SSHHostPortMin, config.SSHHostPortMax)
|
||||||
|
var sshHostPort uint
|
||||||
|
portRange := int(config.SSHHostPortMax - config.SSHHostPortMin)
|
||||||
|
for {
|
||||||
|
sshHostPort = uint(rand.Intn(portRange)) + config.SSHHostPortMin
|
||||||
|
log.Printf("Trying port: %d", sshHostPort)
|
||||||
|
l, err := net.Listen("tcp", fmt.Sprintf(":%d", sshHostPort))
|
||||||
|
if err == nil {
|
||||||
|
defer l.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.Say(fmt.Sprintf("Found port for SSH: %d.", sshHostPort))
|
||||||
|
|
||||||
|
// Save the port we're using so that future steps can use it
|
||||||
|
state.Put("sshHostPort", sshHostPort)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepForwardSSH) Cleanup(state multistep.StateBag) {}
|
75
builder/qemu/step_http_server.go
Normal file
75
builder/qemu/step_http_server.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This step creates and runs the HTTP server that is serving the files
|
||||||
|
// specified by the 'http_files` 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 {
|
||||||
|
l net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
var httpPort uint = 0
|
||||||
|
if config.HTTPDir == "" {
|
||||||
|
state.Put("http_port", httpPort)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find an available TCP port for our HTTP server
|
||||||
|
var httpAddr string
|
||||||
|
portRange := int(config.HTTPPortMax - config.HTTPPortMin)
|
||||||
|
for {
|
||||||
|
var err error
|
||||||
|
var offset uint = 0
|
||||||
|
|
||||||
|
if portRange > 0 {
|
||||||
|
// Intn will panic if portRange == 0, so we do a check.
|
||||||
|
offset = uint(rand.Intn(portRange))
|
||||||
|
}
|
||||||
|
|
||||||
|
httpPort = offset + config.HTTPPortMin
|
||||||
|
httpAddr = fmt.Sprintf(":%d", httpPort)
|
||||||
|
log.Printf("Trying port: %d", httpPort)
|
||||||
|
s.l, err = net.Listen("tcp", httpAddr)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: httpAddr, Handler: fileServer}
|
||||||
|
go server.Serve(s.l)
|
||||||
|
|
||||||
|
// Save the address into the state so it can be accessed in the future
|
||||||
|
state.Put("http_port", 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()
|
||||||
|
}
|
||||||
|
}
|
49
builder/qemu/step_prepare_output_dir.go
Normal file
49
builder/qemu/step_prepare_output_dir.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepPrepareOutputDir struct{}
|
||||||
|
|
||||||
|
func (stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce {
|
||||||
|
ui.Say("Deleting previous output directory...")
|
||||||
|
os.RemoveAll(config.OutputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) {
|
||||||
|
_, cancelled := state.GetOk(multistep.StateCancelled)
|
||||||
|
_, halted := state.GetOk(multistep.StateHalted)
|
||||||
|
|
||||||
|
if cancelled || halted {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
ui.Say("Deleting output directory...")
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
err := os.RemoveAll(config.OutputDir)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Error removing output dir: %s", err)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
134
builder/qemu/step_run.go
Normal file
134
builder/qemu/step_run.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepRun struct {
|
||||||
|
vmName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBootCommand(state multistep.StateBag,
|
||||||
|
actionChannel chan multistep.StepAction) {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
bootCmd := stepTypeBootCommand{}
|
||||||
|
|
||||||
|
if int64(config.bootWait) > 0 {
|
||||||
|
ui.Say(fmt.Sprintf("Waiting %s for boot...", config.bootWait))
|
||||||
|
time.Sleep(config.bootWait)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionChannel <- bootCmd.Run(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelCallback(state multistep.StateBag) bool {
|
||||||
|
cancel := false
|
||||||
|
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||||
|
cancel = true
|
||||||
|
}
|
||||||
|
return cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepRun) runVM(
|
||||||
|
sendBootCommands bool,
|
||||||
|
bootDrive string,
|
||||||
|
state multistep.StateBag) multistep.StepAction {
|
||||||
|
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
driver := state.Get("driver").(Driver)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
vmName := config.VMName
|
||||||
|
|
||||||
|
imgPath := filepath.Join(config.OutputDir,
|
||||||
|
fmt.Sprintf("%s.%s", vmName, strings.ToLower(config.Format)))
|
||||||
|
isoPath := state.Get("iso_path").(string)
|
||||||
|
vncPort := state.Get("vnc_port").(uint)
|
||||||
|
guiArgument := "sdl"
|
||||||
|
sshHostPort := state.Get("sshHostPort").(uint)
|
||||||
|
vnc := fmt.Sprintf("0.0.0.0:%d", vncPort-5900)
|
||||||
|
|
||||||
|
ui.Say("Starting the virtual machine for OS Install...")
|
||||||
|
if config.Headless == true {
|
||||||
|
ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" +
|
||||||
|
"In headless mode, errors during the boot sequence or OS setup\n" +
|
||||||
|
"won't be easily visible. Use at your own discretion.")
|
||||||
|
guiArgument = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"-name", vmName,
|
||||||
|
"-machine", fmt.Sprintf("type=pc-1.0,accel=%s", config.Accelerator),
|
||||||
|
"-display", guiArgument,
|
||||||
|
"-net", "nic,model=virtio",
|
||||||
|
"-net", "user",
|
||||||
|
"-drive", fmt.Sprintf("file=%s,if=virtio", imgPath),
|
||||||
|
"-cdrom", isoPath,
|
||||||
|
"-boot", bootDrive,
|
||||||
|
"-m", "512m",
|
||||||
|
"-redir", fmt.Sprintf("tcp:%v::22", sshHostPort),
|
||||||
|
"-vnc", vnc,
|
||||||
|
}
|
||||||
|
if err := driver.Qemu(vmName, command...); err != nil {
|
||||||
|
err := fmt.Errorf("Error launching VM: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
s.vmName = vmName
|
||||||
|
|
||||||
|
// run the boot command after its own timeout
|
||||||
|
if sendBootCommands {
|
||||||
|
waitDone := make(chan multistep.StepAction, 1)
|
||||||
|
go runBootCommand(state, waitDone)
|
||||||
|
select {
|
||||||
|
case action := <-waitDone:
|
||||||
|
if action != multistep.ActionContinue {
|
||||||
|
// stop the VM in its tracks
|
||||||
|
driver.Stop(vmName)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Waiting for VM to shutdown...")
|
||||||
|
if err := driver.WaitForShutdown(vmName, sendBootCommands, state, cancelCallback); err != nil {
|
||||||
|
err := fmt.Errorf("Error waiting for initial VM install to shutdown: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepRun) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
// First, the OS install boot
|
||||||
|
action := s.runVM(true, "d", state)
|
||||||
|
|
||||||
|
if action == multistep.ActionContinue {
|
||||||
|
// Then the provisioning install
|
||||||
|
action = s.runVM(false, "c", state)
|
||||||
|
}
|
||||||
|
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepRun) Cleanup(state multistep.StateBag) {
|
||||||
|
if s.vmName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
driver := state.Get("driver").(Driver)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
if running, _ := driver.IsRunning(s.vmName); running {
|
||||||
|
if err := driver.Stop(s.vmName); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error shutting down VM: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
builder/qemu/step_shutdown.go
Normal file
77
builder/qemu/step_shutdown.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This step shuts down the machine. It first attempts to do so gracefully,
|
||||||
|
// but ultimately forcefully shuts it down if that fails.
|
||||||
|
//
|
||||||
|
// Uses:
|
||||||
|
// communicator packer.Communicator
|
||||||
|
// config *config
|
||||||
|
// driver Driver
|
||||||
|
// ui packer.Ui
|
||||||
|
// vmName string
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// <nothing>
|
||||||
|
type stepShutdown struct{}
|
||||||
|
|
||||||
|
func (s *stepShutdown) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
comm := state.Get("communicator").(packer.Communicator)
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
driver := state.Get("driver").(Driver)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
vmName := config.VMName
|
||||||
|
|
||||||
|
if config.ShutdownCommand != "" {
|
||||||
|
ui.Say("Gracefully halting virtual machine...")
|
||||||
|
log.Printf("Executing shutdown command: %s", config.ShutdownCommand)
|
||||||
|
cmd := &packer.RemoteCmd{Command: config.ShutdownCommand}
|
||||||
|
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||||
|
err := fmt.Errorf("Failed to send shutdown command: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the machine to actually shut down
|
||||||
|
log.Printf("Waiting max %s for shutdown to complete", config.shutdownTimeout)
|
||||||
|
shutdownTimer := time.After(config.shutdownTimeout)
|
||||||
|
for {
|
||||||
|
running, _ := driver.IsRunning(vmName)
|
||||||
|
if !running {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-shutdownTimer:
|
||||||
|
err := errors.New("Timeout while waiting for machine to shut down.")
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
default:
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.Say("Halting the virtual machine...")
|
||||||
|
if err := driver.Stop(vmName); err != nil {
|
||||||
|
err := fmt.Errorf("Error stopping VM: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("VM shut down.")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepShutdown) Cleanup(state multistep.StateBag) {}
|
29
builder/qemu/step_suppress_messages.go
Normal file
29
builder/qemu/step_suppress_messages.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This step sets some variables in Qemu so that annoying
|
||||||
|
// pop-up messages don't exist.
|
||||||
|
type stepSuppressMessages struct{}
|
||||||
|
|
||||||
|
func (stepSuppressMessages) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
driver := state.Get("driver").(Driver)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
log.Println("Suppressing messages in Qemu")
|
||||||
|
if err := driver.SuppressMessages(); err != nil {
|
||||||
|
err := fmt.Errorf("Error configuring Qemu to suppress messages: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stepSuppressMessages) Cleanup(state multistep.StateBag) {}
|
175
builder/qemu/step_type_boot_command.go
Normal file
175
builder/qemu/step_type_boot_command.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
package qemu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mitchellh/go-vnc"
|
||||||
|
"github.com/mitchellh/multistep"
|
||||||
|
"github.com/mitchellh/packer/packer"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const KeyLeftShift uint32 = 0xFFE1
|
||||||
|
|
||||||
|
type bootCommandTemplateData struct {
|
||||||
|
HTTPIP string
|
||||||
|
HTTPPort uint
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// This step "types" the boot command into the VM over VNC.
|
||||||
|
//
|
||||||
|
// Uses:
|
||||||
|
// config *config
|
||||||
|
// http_port int
|
||||||
|
// ui packer.Ui
|
||||||
|
// vnc_port uint
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// <nothing>
|
||||||
|
type stepTypeBootCommand struct{}
|
||||||
|
|
||||||
|
func (s *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*config)
|
||||||
|
httpPort := state.Get("http_port").(uint)
|
||||||
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
vncPort := state.Get("vnc_port").(uint)
|
||||||
|
|
||||||
|
// Connect to VNC
|
||||||
|
ui.Say("Connecting to VM via VNC")
|
||||||
|
nc, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", vncPort))
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error connecting to VNC: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
defer nc.Close()
|
||||||
|
|
||||||
|
c, err := vnc.Client(nc, &vnc.ClientConfig{Exclusive: true})
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error handshaking with VNC: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
log.Printf("Connected to VNC desktop: %s", c.DesktopName)
|
||||||
|
|
||||||
|
tplData := &bootCommandTemplateData{
|
||||||
|
"127.0.0.1",
|
||||||
|
httpPort,
|
||||||
|
config.VMName,
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Typing the boot command over VNC...")
|
||||||
|
for _, command := range config.BootCommand {
|
||||||
|
command, err := config.tpl.Process(command, tplData)
|
||||||
|
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 between typing things so we can cancel
|
||||||
|
// since this isn't the fastest thing.
|
||||||
|
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
vncSendString(c, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
|
||||||
|
|
||||||
|
func vncSendString(c *vnc.ClientConn, original string) {
|
||||||
|
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
|
||||||
|
|
||||||
|
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(KeyLeftShift, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.KeyEvent(keyCode, true)
|
||||||
|
c.KeyEvent(keyCode, false)
|
||||||
|
|
||||||
|
if keyShift {
|
||||||
|
c.KeyEvent(KeyLeftShift, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// qemu is picky, so no matter what, wait a small period
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@ const defaultConfig = `
|
|||||||
"amazon-instance": "packer-builder-amazon-instance",
|
"amazon-instance": "packer-builder-amazon-instance",
|
||||||
"digitalocean": "packer-builder-digitalocean",
|
"digitalocean": "packer-builder-digitalocean",
|
||||||
"openstack": "packer-builder-openstack",
|
"openstack": "packer-builder-openstack",
|
||||||
|
"qemu": "packer-builder-qemu",
|
||||||
"virtualbox": "packer-builder-virtualbox",
|
"virtualbox": "packer-builder-virtualbox",
|
||||||
"vmware": "packer-builder-vmware"
|
"vmware": "packer-builder-vmware"
|
||||||
},
|
},
|
||||||
|
10
plugin/builder-qemu/main.go
Normal file
10
plugin/builder-qemu/main.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mitchellh/packer/builder/qemu"
|
||||||
|
"github.com/mitchellh/packer/packer/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.ServeBuilder(new(qemu.Builder))
|
||||||
|
}
|
1
plugin/builder-qemu/main_test.go
Normal file
1
plugin/builder-qemu/main_test.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package main
|
303
website/source/docs/builders/qemu.html.markdown
Normal file
303
website/source/docs/builders/qemu.html.markdown
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
---
|
||||||
|
layout: "docs"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Qemu (qemu-system-x86_64) Builder
|
||||||
|
|
||||||
|
Type: `qemu`
|
||||||
|
|
||||||
|
The Qemu builder is able to create [KVM](http://www.linux-kvm.org)
|
||||||
|
and [Xen](http://www.xenproject.org) virtual machine images. Support
|
||||||
|
for Xen is experimanetal at this time.
|
||||||
|
|
||||||
|
The builder builds a virtual machine by creating a new virtual machine
|
||||||
|
from scratch, booting it, installing an OS, rebooting the machine with the
|
||||||
|
boot media as the virtual hard drive, provisioning software within
|
||||||
|
the OS, then shutting it down. The result of the Qemu builder is a directory
|
||||||
|
containing the image file necessary to run the virtual machine on KVM or Xen.
|
||||||
|
|
||||||
|
## Basic Example
|
||||||
|
|
||||||
|
Here is a basic example. This example is functional so long as you fixup
|
||||||
|
paths to files, URLS for ISOs and checksums.
|
||||||
|
|
||||||
|
<pre class="prettyprint">
|
||||||
|
{
|
||||||
|
"builders":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "qemu",
|
||||||
|
"iso_url": "http://mirror.raystedman.net/centos/6/isos/x86_64/CentOS-6.4-x86_64-minimal.iso",
|
||||||
|
"iso_checksum": "4a5fa01c81cc300f4729136e28ebe600",
|
||||||
|
"iso_checksum_type": "md5",
|
||||||
|
"output_directory": "output_centos_tdhtest",
|
||||||
|
"ssh_wait_timeout": "30s",
|
||||||
|
"shutdown_command": "shutdown -P now",
|
||||||
|
"disk_size": 5000,
|
||||||
|
"format": "qcow2",
|
||||||
|
"headless": false,
|
||||||
|
"accelerator": "kvm",
|
||||||
|
"http_directory": "/home/tdhite/packer/httpfiles",
|
||||||
|
"http_port_min": 10082,
|
||||||
|
"http_port_max": 10089,
|
||||||
|
"ssh_host_port_min": 2222,
|
||||||
|
"ssh_host_port_max": 2229,
|
||||||
|
"ssh_username": "root",
|
||||||
|
"ssh_password": "s0m3password",
|
||||||
|
"ssh_port": 22,
|
||||||
|
"ssh_wait_timeout": "90m",
|
||||||
|
"vm_name": "tdhtest",
|
||||||
|
"boot_command":
|
||||||
|
[
|
||||||
|
"<tab><wait>",
|
||||||
|
" ks=http://10.0.2.2:{{ .HTTPPort }}/centos6-ks.cfg<enter>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
The following is a sample CentOS kickstart file you should place in the
|
||||||
|
ttp_files directory with the name centos6-ks.cfg:
|
||||||
|
|
||||||
|
<pre class="prettyprint">
|
||||||
|
text
|
||||||
|
skipx
|
||||||
|
install
|
||||||
|
url --url http://mirror.raystedman.net/centos/6/os/x86_64/
|
||||||
|
repo --name=updates --baseurl=http://mirror.raystedman.net/centos/6/updates/x86_64/
|
||||||
|
lang en_US.UTF-8
|
||||||
|
keyboard us
|
||||||
|
rootpw s0m3password
|
||||||
|
firewall --disable
|
||||||
|
authconfig --enableshadow --passalgo=sha512
|
||||||
|
selinux --disabled
|
||||||
|
timezone Etc/UTC
|
||||||
|
%include /tmp/kspre.cfg
|
||||||
|
|
||||||
|
services --enabled=network,sshd/sendmail
|
||||||
|
|
||||||
|
poweroff
|
||||||
|
|
||||||
|
%packages --nobase
|
||||||
|
at
|
||||||
|
acpid
|
||||||
|
cronie-noanacron
|
||||||
|
crontabs
|
||||||
|
logrotate
|
||||||
|
mailx
|
||||||
|
mlocate
|
||||||
|
openssh-clients
|
||||||
|
openssh-server
|
||||||
|
rsync
|
||||||
|
sendmail
|
||||||
|
tmpwatch
|
||||||
|
vixie-cron
|
||||||
|
which
|
||||||
|
wget
|
||||||
|
yum
|
||||||
|
-biosdevname
|
||||||
|
-postfix
|
||||||
|
-prelink
|
||||||
|
%end
|
||||||
|
|
||||||
|
%pre
|
||||||
|
bootdrive=vda
|
||||||
|
|
||||||
|
if [ -f "/dev/$bootdrive" ] ; then
|
||||||
|
exec < /dev/tty3 > /dev/tty3
|
||||||
|
chvt 3
|
||||||
|
echo "ERROR: Drive device does not exist at /dev/$bootdrive!"
|
||||||
|
sleep 5
|
||||||
|
halt -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat >/tmp/kspre.cfg <<CFG
|
||||||
|
zerombr
|
||||||
|
bootloader --location=mbr --driveorder=$bootdrive --append="nomodeset"
|
||||||
|
clearpart --all --initlabel
|
||||||
|
part /boot --ondrive=$bootdrive --fstype ext4 --fsoptions="relatime,nodev" --size=512
|
||||||
|
part pv.1 --ondrive=$bootdrive --size 1 --grow
|
||||||
|
volgroup vg0 pv.1
|
||||||
|
logvol / --fstype ext4 --fsoptions="noatime,nodiratime,relatime,nodev" --name=root --vgname=vg0 --size=4096
|
||||||
|
logvol swap --fstype swap --name=swap --vgname=vg0 --size 1 --grow
|
||||||
|
CFG
|
||||||
|
|
||||||
|
%end
|
||||||
|
|
||||||
|
%post
|
||||||
|
|
||||||
|
%end
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
## Configuration Reference
|
||||||
|
|
||||||
|
There are many configuration options available for the Qemu builder.
|
||||||
|
They are organized below into two categories: required and optional. Within
|
||||||
|
each category, the available options are alphabetized and described.
|
||||||
|
|
||||||
|
Required:
|
||||||
|
|
||||||
|
* `iso_checksum` (string) - The checksum for the OS ISO file. Because ISO
|
||||||
|
files are so large, this is required and Packer will verify it prior
|
||||||
|
to booting a virtual machine with the ISO attached. The type of the
|
||||||
|
checksum is specified with `iso_checksum_type`, documented below.
|
||||||
|
|
||||||
|
* `iso_checksum_type` (string) - The type of the checksum specified in
|
||||||
|
`iso_checksum`. Valid values are "md5", "sha1", "sha256", or "sha512" currently.
|
||||||
|
|
||||||
|
* `iso_url` (string) - A URL to the ISO containing the installation image.
|
||||||
|
This URL can be either an HTTP URL or a file URL (or path to a file).
|
||||||
|
If this is an HTTP URL, Packer will download it and cache it between
|
||||||
|
runs.
|
||||||
|
|
||||||
|
* `ssh_username` (string) - The username to use to SSH into the machine
|
||||||
|
once the OS is installed.
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
|
||||||
|
* `boot_command` (array of strings) - This is an array of commands to type
|
||||||
|
when the virtual machine is first booted. The goal of these commands should
|
||||||
|
be to type just enough to initialize the operating system installer. Special
|
||||||
|
keys can be typed as well, and are covered in the section below on the boot
|
||||||
|
command. If this is not specified, it is assumed the installer will start
|
||||||
|
itself.
|
||||||
|
|
||||||
|
* `boot_wait` (string) - The time to wait after booting the initial virtual
|
||||||
|
machine before typing the `boot_command`. The value of this should be
|
||||||
|
a duration. Examples are "5s" and "1m30s" which will cause Packer to wait
|
||||||
|
five seconds and one minute 30 seconds, respectively. If this isn't specified,
|
||||||
|
the default is 10 seconds.
|
||||||
|
|
||||||
|
* `disk_size` (int) - The size, in megabytes, of the hard disk to create
|
||||||
|
for the VM. By default, this is 40000 (40 GB).
|
||||||
|
|
||||||
|
* `floppy_files` (array of strings) - A list of files to put onto a floppy
|
||||||
|
disk that is attached when the VM is booted for the first time. This is
|
||||||
|
most useful for unattended Windows installs, which look for an
|
||||||
|
`Autounattend.xml` file on removable media. By default no floppy will
|
||||||
|
be attached. The files listed in this configuration will all be put
|
||||||
|
into the root directory of the floppy disk; sub-directories are not supported.
|
||||||
|
|
||||||
|
* `format` (string) - Either "qcow2" or "img", this specifies the output
|
||||||
|
format of the virtual machine image. This defaults to "qcow2".
|
||||||
|
|
||||||
|
* `accelerator` (string) - The accelerator type to use when running the VM.
|
||||||
|
This may have a value of either "kvm" or "xen" and you must have that
|
||||||
|
support in on the machine on which you run the builder.
|
||||||
|
|
||||||
|
* `headless` (bool) - Packer defaults to building VirtualBox
|
||||||
|
virtual machines by launching a GUI that shows the console of the
|
||||||
|
machine being built. When this value is set to true, the machine will
|
||||||
|
start without a console.
|
||||||
|
|
||||||
|
* `http_directory` (string) - Path to a directory to serve using an HTTP
|
||||||
|
server. The files in this directory will be available over HTTP that will
|
||||||
|
be requestable from the virtual machine. This is useful for hosting
|
||||||
|
kickstart files and so on. By default this is "", which means no HTTP
|
||||||
|
server will be started. The address and port of the HTTP server will be
|
||||||
|
available as variables in `boot_command`. This is covered in more detail
|
||||||
|
below.
|
||||||
|
|
||||||
|
* `http_port_min` and `http_port_max` (int) - These are the minimum and
|
||||||
|
maximum port to use for the HTTP server started to serve the `http_directory`.
|
||||||
|
Because Packer often runs in parallel, Packer will choose a randomly available
|
||||||
|
port in this range to run the HTTP server. If you want to force the HTTP
|
||||||
|
server to be on one port, make this minimum and maximum port the same.
|
||||||
|
By default the values are 8000 and 9000, respectively.
|
||||||
|
|
||||||
|
* `iso_urls` (array of strings) - Multiple URLs for the ISO to download.
|
||||||
|
Packer will try these in order. If anything goes wrong attempting to download
|
||||||
|
or while downloading a single URL, it will move on to the next. All URLs
|
||||||
|
must point to the same file (same checksum). By default this is empty
|
||||||
|
and `iso_url` is used. Only one of `iso_url` or `iso_urls` can be specified.
|
||||||
|
|
||||||
|
* `output_directory` (string) - This is the path to the directory where the
|
||||||
|
resulting virtual machine will be created. This may be relative or absolute.
|
||||||
|
If relative, the path is relative to the working directory when `packer`
|
||||||
|
is executed. This directory must not exist or be empty prior to running the builder.
|
||||||
|
By default this is "output-BUILDNAME" where "BUILDNAME" is the name
|
||||||
|
of the build.
|
||||||
|
|
||||||
|
* `shutdown_command` (string) - The command to use to gracefully shut down
|
||||||
|
the machine once all the provisioning is done. By default this is an empty
|
||||||
|
string, which tells Packer to just forcefully shut down the machine.
|
||||||
|
|
||||||
|
* `shutdown_timeout` (string) - The amount of time to wait after executing
|
||||||
|
the `shutdown_command` for the virtual machine to actually shut down.
|
||||||
|
If it doesn't shut down in this time, it is an error. By default, the timeout
|
||||||
|
is "5m", or five minutes.
|
||||||
|
|
||||||
|
* `ssh_host_port_min` and `ssh_host_port_max` (uint) - The minimum and
|
||||||
|
maximum port to use for the SSH port on the host machine which is forwarded
|
||||||
|
to the SSH port on the guest machine. Because Packer often runs in parallel,
|
||||||
|
Packer will choose a randomly available port in this range to use as the
|
||||||
|
host port.
|
||||||
|
|
||||||
|
* `ssh_key_path` (string) - Path to a private key to use for authenticating
|
||||||
|
with SSH. By default this is not set (key-based auth won't be used).
|
||||||
|
The associated public key is expected to already be configured on the
|
||||||
|
VM being prepared by some other process (kickstart, etc.).
|
||||||
|
|
||||||
|
* `ssh_password` (string) - The password for `ssh_username` to use to
|
||||||
|
authenticate with SSH. By default this is the empty string.
|
||||||
|
|
||||||
|
* `ssh_port` (int) - The port that SSH will be listening on in the guest
|
||||||
|
virtual machine. By default this is 22. The Qemu builder will map, via
|
||||||
|
port forward, a port on the host machine to the port listed here so
|
||||||
|
machines outside the installing VM can access the VM.
|
||||||
|
|
||||||
|
* `ssh_wait_timeout` (string) - The duration to wait for SSH to become
|
||||||
|
available. By default this is "20m", or 20 minutes. Note that this should
|
||||||
|
be quite long since the timer begins as soon as the virtual machine is booted.
|
||||||
|
|
||||||
|
* `qemuargs` (array of strings reserved for future use).
|
||||||
|
|
||||||
|
* `vm_name` (string) - This is the name of the image (QCOW2 or IMG) file for
|
||||||
|
the new virtual machine, without the file extension. By default this is
|
||||||
|
"packer-BUILDNAME", where "BUILDNAME" is the name of the build.
|
||||||
|
|
||||||
|
## Boot Command
|
||||||
|
|
||||||
|
The `boot_command` configuration is very important: it specifies the keys
|
||||||
|
to type when the virtual machine is first booted in order to start the
|
||||||
|
OS installer. This command is typed after `boot_wait`, which gives the
|
||||||
|
virtual machine some time to actually load the ISO.
|
||||||
|
|
||||||
|
As documented above, the `boot_command` is an array of strings. The
|
||||||
|
strings are all typed in sequence. It is an array only to improve readability
|
||||||
|
within the template.
|
||||||
|
|
||||||
|
The boot command is "typed" character for character over a VNC connection
|
||||||
|
to the machine, simulating a human actually typing the keyboard. There are
|
||||||
|
a set of special keys available. If these are in your boot command, they
|
||||||
|
will be replaced by the proper key:
|
||||||
|
|
||||||
|
* `<enter>` and `<return>` - Simulates an actual "enter" or "return" keypress.
|
||||||
|
|
||||||
|
* `<esc>` - Simulates pressing the escape key.
|
||||||
|
|
||||||
|
* `<tab>` - Simulates pressing the tab key.
|
||||||
|
|
||||||
|
* `<wait>` `<wait5>` `<wait10>` - Adds a 1, 5 or 10 second pause before sending any additional keys. This
|
||||||
|
is useful if you have to generally wait for the UI to update before typing more.
|
||||||
|
|
||||||
|
In addition to the special keys, each command to type is treated as a
|
||||||
|
[configuration template](/docs/templates/configuration-templates.html).
|
||||||
|
The available variables are:
|
||||||
|
|
||||||
|
* `HTTPIP` and `HTTPPort` - The IP and port, respectively of an HTTP server
|
||||||
|
that is started serving the directory specified by the `http_directory`
|
||||||
|
configuration parameter. If `http_directory` isn't specified, these will
|
||||||
|
be blank!
|
||||||
|
|
||||||
|
Example boot command. This is actually a working boot command used to start
|
||||||
|
an CentOS 6.4 installer:
|
||||||
|
|
||||||
|
<pre class="prettyprint">
|
||||||
|
"boot_command":
|
||||||
|
[
|
||||||
|
"<tab><wait>",
|
||||||
|
" ks=http://10.0.2.2:{{ .HTTPPort }}/centos6-ks.cfg<enter>"
|
||||||
|
]
|
||||||
|
</pre>
|
Loading…
x
Reference in New Issue
Block a user