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
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {}
|
|
@ -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
|
||||
}
|
|
@ -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) {}
|
|
@ -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) {}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {}
|
|
@ -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) {}
|
|
@ -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",
|
||||
"digitalocean": "packer-builder-digitalocean",
|
||||
"openstack": "packer-builder-openstack",
|
||||
"qemu": "packer-builder-qemu",
|
||||
"virtualbox": "packer-builder-virtualbox",
|
||||
"vmware": "packer-builder-vmware"
|
||||
},
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package main
|
|
@ -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…
Reference in New Issue