Merge pull request #10 from chengsun/master

Split ISO and XVA builders, add documentation
This commit is contained in:
Rob Dobson 2015-01-05 12:26:58 +00:00
commit 36f10c7dc1
38 changed files with 2353 additions and 958 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
*.swp *.swp
*~ *~
bin
pkg

View File

@ -38,14 +38,14 @@ Follow these instructions and install golang on your system:
## Install Packer ## Install Packer
Visit https://packer.io and install the latest version of packer. Once the Clone the Packer repository:
install has completed, setup an environment variable 'PACKERPATH' pointing
to the installation location. E.g.
```shell ```shell
export PACKERPATH=/usr/local/packer git clone https://github.com/mitchellh/packer.git
``` ```
Then follow the [instructions to build and install a development version of Packer](https://github.com/mitchellh/packer#developing-packer).
## Compile the plugin ## Compile the plugin
Once you have installed Packer, you must compile this plugin and install the Once you have installed Packer, you must compile this plugin and install the
@ -58,55 +58,50 @@ cd src/github.com/rdobson
git clone https://github.com/rdobson/packer-builder-xenserver.git git clone https://github.com/rdobson/packer-builder-xenserver.git
cd packer-builder-xenserver cd packer-builder-xenserver
./build.sh ./build.sh
``` ```
If the build is successful, you should now have a 'packer-builder-xenserver' binary If the build is successful, you should now have a `packer-builder-xenserver` binary
in your $PACKERPATH directory and you are ready to get going with packer. in your `$GOPATH/bin` directory and you are ready to get going with packer.
## Centos 6.4 Example ## Centos 6.6 Example
Once you've setup the above, you are good to go with an example. Once you've setup the above, you are good to go with an example.
To get you started, there is an example config file which you can use: [`examples/centos-6.4.conf`](https://github.com/rdobson/packer-builder-xenserver/blob/master/examples/centos-6.4.conf) To get you started, there is an example config file which you can use:
[`examples/centos-6.6.json`](https://github.com/rdobson/packer-builder-xenserver/blob/master/examples/centos-6.6.json)
Currently it is not (easily) possible to take care of the ISO download and upload, The example is functional, once suitable `remote_host`, `remote_username` and
so you will need to attach an ISO SR to the XenServer host (NFS/CIFS) with the `remote_password` configurations have been substituted.
ISO you want to use for installation. You will then need to specify the name
in the config file (this must be unique).
A brief explanation of what the config parameters mean:
An explanation of what these parameters are doing: * `type` - specifies the builder type. This is 'xenserver-iso', for installing
* `type` - this specifies the builder. This must be 'xenserver'. a VM from scratch, or 'xenserver-xva' to import existing XVA as a starting
* `username` - this is the username for the XenServer host being used. point.
* `password` - this is the password for the XenServer host being used. * `remote_username` - the username for the XenServer host being used.
* `host_ip` - this is the IP for the XenServer host being used. * `remote_password` - the password for the XenServer host being used.
* `instance_name` - this is the name that should be given to the created VM. * `remote_host` - the IP for the XenServer host being used.
* `instance_memory` - this is the static memory configuration for the VM. * `vm_name` - the name that should be given to the created VM.
* `root_disk_size` - this is the size of the disk the VM should be created with. * `vm_memory` - the static memory configuration for the VM, in MB.
* `iso_name` - this is the name of the ISO visible on a ISO SR connected to the XenServer host. * `disk_size` - the size of the disk the VM should be created with, in MB.
* `iso_name` - the name of the ISO visible on a ISO SR connected to the XenServer host.
* `http_directory` - the path to a local directory to serve up over http. * `http_directory` - the path to a local directory to serve up over http.
* `local_ip` - the IP on the machine you are running packer that your XenServer can connect too.
* `ssh_username` - the username set by the installer for the instance. * `ssh_username` - the username set by the installer for the instance.
* `ssh_password` - the password set by the installer for the instance. * `ssh_password` - the password set by the installer for the instance.
* `boot_command` - a set of commands to be sent to the instance over VNC. * `boot_command` - a list of commands to be sent to the instance over VNC.
Note, the `http_directory` parameter is only required if you
Note, the `http_directory` and `local_ip` parameters are only required if you want Packer to serve up files over HTTP. In this example, the templated variables
want packer to serve up files over HTTP. In this example, the templated variables `{{ .HTTPIP }}` and `{{ .HTTPPort }}` will be substituted for the local IP and
`{{ .HTTPIP }}` and `{{ .HTTPPort }}` will be substituted for the `local_ip` and the port that Packer starts its HTTP service on.
the port that packer starts it's HTTP service on.
Once you've updated the config file with your own parameters, you can use packer Once you've updated the config file with your own parameters, you can use packer
to build this VM with the following: to build this VM with the following command:
``` ```
packer build centos-6.4.conf packer build centos-6.6.json
``` ```
# Documentation
For complete documentation on configuration commands, see either [the
xenserver-iso docs](https://github.com/rdobson/packer-builder-xenserver/blob/master/docs/builders/xenserver-iso.html.markdown) or [the xenserver-xva docs](https://github.com/rdobson/packer-builder-xenserver/blob/master/docs/builders/xenserver-xva.html.markdown).

View File

@ -1 +1,49 @@
go build -o $PACKERPATH/packer-builder-xenserver main.go #!/bin/bash
#
# This script builds the application from source for multiple platforms.
# Adapted from from packer/scripts/build.sh
# Determine the arch/os combos we're building for
XC_OS=${XC_OS:-$(go env GOOS)}
XC_ARCH=${XC_ARCH:-$(go env GOARCH)}
# Install dependencies
echo "==> Getting dependencies..."
go get ./...
# Delete the old dir
echo "==> Removing old directory..."
rm -f bin/*
rm -rf pkg/*
mkdir -p bin/
gox \
-os="${XC_OS}" \
-arch="${XC_ARCH}" \
-output "pkg/{{.OS}}_{{.Arch}}/packer-{{.Dir}}" \
./... \
|| exit 1
# Move all the compiled things to the $GOPATH/bin
GOPATH=${GOPATH:-$(go env GOPATH)}
case $(uname) in
CYGWIN*)
GOPATH="$(cygpath $GOPATH)"
;;
esac
OLDIFS=$IFS
IFS=: MAIN_GOPATH=($GOPATH)
IFS=$OLDIFS
# Copy our OS/Arch to the bin/ directory
echo "==> Copying binaries for this platform..."
DEV_PLATFORM="./pkg/$(go env GOOS)_$(go env GOARCH)"
for F in $(find ${DEV_PLATFORM} -mindepth 1 -maxdepth 1 -type f); do
cp ${F} bin/
cp ${F} ${MAIN_GOPATH}/bin/
done
# Done!
echo
echo "==> Results:"
ls -hl bin/

View File

@ -1,528 +0,0 @@
package xenserver
import (
"errors"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/packer"
"log"
"os"
"path"
"strings"
"time"
)
// Set the unique ID for this builder
const BuilderId = "packer.xenserver"
type config struct {
common.PackerConfig `mapstructure:",squash"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
HostIp string `mapstructure:"host_ip"`
VMName string `mapstructure:"vm_name"`
VMMemory uint `mapstructure:"vm_memory"`
DiskSize uint `mapstructure:"disk_size"`
CloneTemplate string `mapstructure:"clone_template"`
SrName string `mapstructure:"sr_name"`
FloppyFiles []string `mapstructure:"floppy_files"`
NetworkName string `mapstructure:"network_name"`
HostPortMin uint `mapstructure:"host_port_min"`
HostPortMax uint `mapstructure:"host_port_max"`
BootCommand []string `mapstructure:"boot_command"`
ShutdownCommand string `mapstructure:"shutdown_command"`
RawBootWait string `mapstructure:"boot_wait"`
BootWait time.Duration ``
ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrls []string `mapstructure:"iso_urls"`
ISOUrl string `mapstructure:"iso_url"`
ToolsIsoName string `mapstructure:"tools_iso_name"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
LocalIp string `mapstructure:"local_ip"`
PlatformArgs map[string]string `mapstructure:"platform_args"`
RawInstallTimeout string `mapstructure:"install_timeout"`
InstallTimeout time.Duration ``
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
SSHWaitTimeout time.Duration ``
SSHPassword string `mapstructure:"ssh_password"`
SSHUser string `mapstructure:"ssh_username"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
OutputDir string `mapstructure:"output_directory"`
ExportFormat string `mapstructure:"export_format"`
KeepInstance string `mapstructure:"keep_instance"`
tpl *packer.ConfigTemplate
}
type Builder struct {
config config
runner multistep.Runner
}
func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error) {
md, err := common.DecodeConfig(&self.config, raws...)
if err != nil {
return nil, err
}
errs := common.CheckUnusedConfig(md)
if errs == nil {
errs = &packer.MultiError{}
}
self.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, err
}
self.config.tpl.UserVars = self.config.PackerUserVars
// Set default values
if self.config.HostPortMin == 0 {
self.config.HostPortMin = 5900
}
if self.config.HostPortMax == 0 {
self.config.HostPortMax = 6000
}
if self.config.RawBootWait == "" {
self.config.RawBootWait = "5s"
}
if self.config.RawInstallTimeout == "" {
self.config.RawInstallTimeout = "200m"
}
if self.config.ToolsIsoName == "" {
self.config.ToolsIsoName = "xs-tools.iso"
}
if self.config.HTTPPortMin == 0 {
self.config.HTTPPortMin = 8000
}
if self.config.HTTPPortMax == 0 {
self.config.HTTPPortMax = 9000
}
if self.config.RawSSHWaitTimeout == "" {
self.config.RawSSHWaitTimeout = "200m"
}
if self.config.DiskSize == 0 {
self.config.DiskSize = 40000
}
if self.config.VMMemory == 0 {
self.config.VMMemory = 1024
}
if self.config.CloneTemplate == "" {
self.config.CloneTemplate = "Other install media"
}
if self.config.FloppyFiles == nil {
self.config.FloppyFiles = make([]string, 0)
}
if self.config.OutputDir == "" {
self.config.OutputDir = fmt.Sprintf("output-%s", self.config.PackerBuildName)
}
if self.config.ExportFormat == "" {
self.config.ExportFormat = "xva"
}
if self.config.KeepInstance == "" {
self.config.KeepInstance = "never"
}
if len(self.config.PlatformArgs) == 0 {
pargs := make(map[string]string)
pargs["viridian"] = "false"
pargs["nx"] = "true"
pargs["pae"] = "true"
pargs["apic"] = "true"
pargs["timeoffset"] = "0"
pargs["acpi"] = "1"
self.config.PlatformArgs = pargs
}
// Template substitution
templates := map[string]*string{
"username": &self.config.Username,
"password": &self.config.Password,
"host_ip": &self.config.HostIp,
"vm_name": &self.config.VMName,
"clone_template": &self.config.CloneTemplate,
"sr_name": &self.config.SrName,
"network_name": &self.config.NetworkName,
"shutdown_command": &self.config.ShutdownCommand,
"boot_wait": &self.config.RawBootWait,
"iso_checksum": &self.config.ISOChecksum,
"iso_checksum_type": &self.config.ISOChecksumType,
"iso_url": &self.config.ISOUrl,
"tools_iso_name": &self.config.ToolsIsoName,
"http_directory": &self.config.HTTPDir,
"local_ip": &self.config.LocalIp,
"install_timeout": &self.config.RawInstallTimeout,
"ssh_wait_timeout": &self.config.RawSSHWaitTimeout,
"ssh_username": &self.config.SSHUser,
"ssh_password": &self.config.SSHPassword,
"ssh_key_path": &self.config.SSHKeyPath,
"output_directory": &self.config.OutputDir,
"export_format": &self.config.ExportFormat,
"keep_instance": &self.config.KeepInstance,
}
for i := range self.config.FloppyFiles {
templates[fmt.Sprintf("floppy_files[%d]", i)] = &self.config.FloppyFiles[i]
}
for i := range self.config.ISOUrls {
templates[fmt.Sprintf("iso_urls[%d]", i)] = &self.config.ISOUrls[i]
}
for n, ptr := range templates {
var err error
*ptr, err = self.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
// Validation
self.config.BootWait, err = time.ParseDuration(self.config.RawBootWait)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed to parse boot_wait: %s", err))
}
self.config.SSHWaitTimeout, err = time.ParseDuration(self.config.RawSSHWaitTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed to parse ssh_wait_timeout: %s", err))
}
self.config.InstallTimeout, err = time.ParseDuration(self.config.RawInstallTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed to parse install_timeout: %s", err))
}
for i, command := range self.config.BootCommand {
if err := self.config.tpl.Validate(command); err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
}
}
if self.config.SSHUser == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("An ssh_username must be specified."))
}
if self.config.SSHKeyPath != "" {
if _, err := os.Stat(self.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(self.config.SSHKeyPath); err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
}
if self.config.Username == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("A username for the xenserver host must be specified."))
}
if self.config.Password == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("A password for the xenserver host must be specified."))
}
if self.config.HostIp == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("An ip for the xenserver host must be specified."))
}
if self.config.VMName == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("vm_name must be specified."))
}
switch self.config.ExportFormat {
case "xva", "vdi_raw":
default:
errs = packer.MultiErrorAppend(
errs, errors.New("export_format must be one of 'xva', 'vdi_raw'"))
}
switch self.config.KeepInstance {
case "always", "never", "on_success":
default:
errs = packer.MultiErrorAppend(
errs, errors.New("keep_instance must be one of 'always', 'never', 'on_success'"))
}
/*
if self.config.LocalIp == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("A local IP visible to XenServer's mangement interface is required to serve files."))
}
*/
if self.config.HTTPPortMin > self.config.HTTPPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("the HTTP min port must be less than the max"))
}
if self.config.HostPortMin > self.config.HostPortMax {
errs = packer.MultiErrorAppend(
errs, errors.New("the host min port must be less than the max"))
}
if self.config.ISOChecksumType == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
self.config.ISOChecksumType = strings.ToLower(self.config.ISOChecksumType)
if self.config.ISOChecksumType != "none" {
if self.config.ISOChecksum == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Due to the file size being large, an iso_checksum is required."))
} else {
self.config.ISOChecksum = strings.ToLower(self.config.ISOChecksum)
}
if hash := common.HashForType(self.config.ISOChecksumType); hash == nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported checksum type: %s", self.config.ISOChecksumType))
}
}
}
if len(self.config.ISOUrls) == 0 {
if self.config.ISOUrl == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("One of iso_url or iso_urls must be specified."))
} else {
self.config.ISOUrls = []string{self.config.ISOUrl}
}
} else if self.config.ISOUrl != "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Only one of iso_url or iso_urls may be specified."))
}
for i, url := range self.config.ISOUrls {
self.config.ISOUrls[i], err = common.DownloadableURL(url)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed to parse iso_urls[%d]: %s", i, err))
}
}
if len(errs.Errors) > 0 {
retErr = errors.New(errs.Error())
}
return nil, retErr
}
func (self *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
//Setup XAPI client
client := NewXenAPIClient(self.config.HostIp, self.config.Username, self.config.Password)
err := client.Login()
if err != nil {
return nil, err.(error)
}
ui.Say("XAPI client session established")
client.GetHosts()
//Share state between the other steps using a statebag
state := new(multistep.BasicStateBag)
state.Put("cache", cache)
state.Put("client", client)
state.Put("config", self.config)
state.Put("hook", hook)
state.Put("ui", ui)
//Build the steps
steps := []multistep.Step{
&common.StepDownload{
Checksum: self.config.ISOChecksum,
ChecksumType: self.config.ISOChecksumType,
Description: "ISO",
ResultKey: "iso_path",
Url: self.config.ISOUrls,
},
new(stepPrepareOutputDir),
&common.StepCreateFloppy{
Files: self.config.FloppyFiles,
},
new(stepHTTPServer),
&stepUploadVdi{
VdiName: "Packer-floppy-disk",
ImagePathFunc: func() string {
if floppyPath, ok := state.GetOk("floppy_path"); ok {
return floppyPath.(string)
}
return ""
},
VdiUuidKey: "floppy_vdi_uuid",
},
&stepUploadVdi{
VdiName: path.Base(self.config.ISOUrls[0]),
ImagePathFunc: func() string {
return state.Get("iso_path").(string)
},
VdiUuidKey: "iso_vdi_uuid",
},
&stepFindVdi{
VdiName: self.config.ToolsIsoName,
VdiUuidKey: "tools_vdi_uuid",
},
new(stepCreateInstance),
&stepAttachVdi{
VdiUuidKey: "floppy_vdi_uuid",
VdiType: Floppy,
},
&stepAttachVdi{
VdiUuidKey: "iso_vdi_uuid",
VdiType: CD,
},
&stepAttachVdi{
VdiUuidKey: "tools_vdi_uuid",
VdiType: CD,
},
new(stepStartVmPaused),
new(stepGetVNCPort),
&stepForwardPortOverSSH{
RemotePort: instanceVNCPort,
RemoteDest: instanceVNCIP,
HostPortMin: self.config.HostPortMin,
HostPortMax: self.config.HostPortMax,
ResultKey: "local_vnc_port",
},
new(stepBootWait),
new(stepTypeBootCommand),
new(stepWait),
&stepDetachVdi{
VdiUuidKey: "floppy_vdi_uuid",
},
&stepDetachVdi{
VdiUuidKey: "iso_vdi_uuid",
},
new(stepRemoveDevices),
new(stepStartOnHIMN),
&stepForwardPortOverSSH{
RemotePort: himnSSHPort,
RemoteDest: himnSSHIP,
HostPortMin: self.config.HostPortMin,
HostPortMax: self.config.HostPortMax,
ResultKey: "local_ssh_port",
},
&common.StepConnectSSH{
SSHAddress: sshLocalAddress,
SSHConfig: sshConfig,
SSHWaitTimeout: self.config.SSHWaitTimeout,
},
new(common.StepProvision),
new(stepShutdownAndExport),
}
self.runner = &multistep.BasicRunner{Steps: steps}
self.runner.Run(state)
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.")
}
artifact, _ := NewArtifact(self.config.OutputDir)
return artifact, nil
}
func (self *Builder) Cancel() {
if self.runner != nil {
log.Println("Cancelling the step runner...")
self.runner.Cancel()
}
fmt.Println("Cancelling the builder")
}
// all steps should check config.ShouldKeepInstance first before cleaning up
func (cfg config) ShouldKeepInstance(state multistep.StateBag) bool {
switch cfg.KeepInstance {
case "always":
return true
case "never":
return false
case "on_success":
// only keep instance if build was successful
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
return !(cancelled || halted)
default:
panic(fmt.Sprintf("Unknown keep_instance value '%s'", cfg.KeepInstance))
}
}
func (config config) GetSR(client XenAPIClient) (*SR, error) {
if config.SrName == "" {
// Find the default SR
return client.GetDefaultSR()
} else {
// Use the provided name label to find the SR to use
srs, err := client.GetSRByNameLabel(config.SrName)
if err != nil {
return nil, err
}
switch {
case len(srs) == 0:
return nil, fmt.Errorf("Couldn't find a SR with the specified name-label '%s'", config.SrName)
case len(srs) > 1:
return nil, fmt.Errorf("Found more than one SR with the name '%s'. The name must be unique", config.SrName)
}
return srs[0], nil
}
}

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -7,6 +7,9 @@ import (
"path/filepath" "path/filepath"
) )
// This is the common builder ID to all of these artifacts.
const BuilderId = "packer.xenserver"
type LocalArtifact struct { type LocalArtifact struct {
dir string dir string
f []string f []string

View File

@ -1,10 +1,11 @@
package xenserver package common
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/nilshell/xmlrpc" "github.com/nilshell/xmlrpc"
"log" "log"
"regexp"
) )
type XenAPIClient struct { type XenAPIClient struct {
@ -22,64 +23,34 @@ type APIResult struct {
ErrorDescription string ErrorDescription string
} }
type VM struct { type XenAPIObject struct {
Ref string Ref string
Client *XenAPIClient Client *XenAPIClient
} }
type SR struct { type VM XenAPIObject
Ref string type SR XenAPIObject
Client *XenAPIClient type VDI XenAPIObject
} type Network XenAPIObject
type VBD XenAPIObject
type VDI struct { type VIF XenAPIObject
Ref string type PIF XenAPIObject
Client *XenAPIClient type Pool XenAPIObject
} type Task XenAPIObject
type VDIType int type VDIType int
const ( const (
_ = iota _ VDIType = iota
Disk Disk
CD CD
Floppy Floppy
) )
type Network struct {
Ref string
Client *XenAPIClient
}
type VBD struct {
Ref string
Client *XenAPIClient
}
type VIF struct {
Ref string
Client *XenAPIClient
}
type PIF struct {
Ref string
Client *XenAPIClient
}
type Pool struct {
Ref string
Client *XenAPIClient
}
type Task struct {
Ref string
Client *XenAPIClient
}
type TaskStatusType int type TaskStatusType int
const ( const (
_ = iota _ TaskStatusType = iota
Pending Pending
Success Success
Failure Failure
@ -902,6 +873,34 @@ func (self *Task) GetProgress() (progress float64, err error) {
return return
} }
func (self *Task) GetResult() (object *XenAPIObject, err error) {
result := APIResult{}
err = self.Client.APICall(&result, "task.get_result", self.Ref)
if err != nil {
return
}
switch ref := result.Value.(type) {
case string:
// @fixme: xapi currently sends us an xmlrpc-encoded string via xmlrpc.
// This seems to be a bug in xapi. Remove this workaround when it's fixed
re := regexp.MustCompile("^<value><array><data><value>([^<]*)</value>.*</data></array></value>$")
match := re.FindStringSubmatch(ref)
if match == nil {
object = nil
} else {
object = &XenAPIObject{
Ref: match[1],
Client: self.Client,
}
}
case nil:
object = nil
default:
err = fmt.Errorf("task.get_result: unknown value type %T (expected string or nil)", ref)
}
return
}
func (self *Task) GetErrorInfo() (errorInfo []string, err error) { func (self *Task) GetErrorInfo() (errorInfo []string, err error) {
result := APIResult{} result := APIResult{}
err = self.Client.APICall(&result, "task.get_error_info", self.Ref) err = self.Client.APICall(&result, "task.get_error_info", self.Ref)
@ -910,7 +909,7 @@ func (self *Task) GetErrorInfo() (errorInfo []string, err error) {
} }
errorInfo = make([]string, 0) errorInfo = make([]string, 0)
for _, infoRaw := range result.Value.([]interface{}) { for _, infoRaw := range result.Value.([]interface{}) {
errorInfo = append(errorInfo, infoRaw.(string)) errorInfo = append(errorInfo, fmt.Sprintf("%v", infoRaw))
} }
return return
} }

View File

@ -0,0 +1,269 @@
package common
import (
"errors"
"fmt"
"os"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
commonssh "github.com/mitchellh/packer/common/ssh"
"github.com/mitchellh/packer/packer"
)
type CommonConfig struct {
Username string `mapstructure:"remote_username"`
Password string `mapstructure:"remote_password"`
HostIp string `mapstructure:"remote_host"`
VMName string `mapstructure:"vm_name"`
SrName string `mapstructure:"sr_name"`
FloppyFiles []string `mapstructure:"floppy_files"`
NetworkName string `mapstructure:"network_name"`
HostPortMin uint `mapstructure:"host_port_min"`
HostPortMax uint `mapstructure:"host_port_max"`
BootCommand []string `mapstructure:"boot_command"`
ShutdownCommand string `mapstructure:"shutdown_command"`
RawBootWait string `mapstructure:"boot_wait"`
BootWait time.Duration
ToolsIsoName string `mapstructure:"tools_iso_name"`
HTTPDir string `mapstructure:"http_directory"`
HTTPPortMin uint `mapstructure:"http_port_min"`
HTTPPortMax uint `mapstructure:"http_port_max"`
// SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
// SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
SSHKeyPath string `mapstructure:"ssh_key_path"`
SSHPassword string `mapstructure:"ssh_password"`
SSHPort uint `mapstructure:"ssh_port"`
SSHUser string `mapstructure:"ssh_username"`
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
SSHWaitTimeout time.Duration
OutputDir string `mapstructure:"output_directory"`
Format string `mapstructure:"format"`
KeepVM string `mapstructure:"keep_vm"`
}
func (c *CommonConfig) Prepare(t *packer.ConfigTemplate, pc *common.PackerConfig) []error {
var err error
// Set default values
if c.HostPortMin == 0 {
c.HostPortMin = 5900
}
if c.HostPortMax == 0 {
c.HostPortMax = 6000
}
if c.RawBootWait == "" {
c.RawBootWait = "5s"
}
if c.ToolsIsoName == "" {
c.ToolsIsoName = "xs-tools.iso"
}
if c.HTTPPortMin == 0 {
c.HTTPPortMin = 8000
}
if c.HTTPPortMax == 0 {
c.HTTPPortMax = 9000
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "200m"
}
if c.FloppyFiles == nil {
c.FloppyFiles = make([]string, 0)
}
/*
if c.SSHHostPortMin == 0 {
c.SSHHostPortMin = 2222
}
if c.SSHHostPortMax == 0 {
c.SSHHostPortMax = 4444
}
*/
if c.SSHPort == 0 {
c.SSHPort = 22
}
if c.RawSSHWaitTimeout == "" {
c.RawSSHWaitTimeout = "20m"
}
if c.OutputDir == "" {
c.OutputDir = fmt.Sprintf("output-%s", pc.PackerBuildName)
}
if c.VMName == "" {
c.VMName = fmt.Sprintf("packer-%s-{{timestamp}}", pc.PackerBuildName)
}
if c.Format == "" {
c.Format = "xva"
}
if c.KeepVM == "" {
c.KeepVM = "never"
}
// Template substitution
templates := map[string]*string{
"remote_username": &c.Username,
"remote_password": &c.Password,
"remote_host": &c.HostIp,
"vm_name": &c.VMName,
"sr_name": &c.SrName,
"shutdown_command": &c.ShutdownCommand,
"boot_wait": &c.RawBootWait,
"tools_iso_name": &c.ToolsIsoName,
"http_directory": &c.HTTPDir,
"ssh_key_path": &c.SSHKeyPath,
"ssh_password": &c.SSHPassword,
"ssh_username": &c.SSHUser,
"ssh_wait_timeout": &c.RawSSHWaitTimeout,
"output_directory": &c.OutputDir,
"format": &c.Format,
"keep_vm": &c.KeepVM,
}
for i := range c.FloppyFiles {
templates[fmt.Sprintf("floppy_files[%d]", i)] = &c.FloppyFiles[i]
}
errs := make([]error, 0)
for n, ptr := range templates {
*ptr, err = t.Process(*ptr, nil)
if err != nil {
errs = append(errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
// Validation
if c.Username == "" {
errs = append(errs, errors.New("remote_username must be specified."))
}
if c.Password == "" {
errs = append(errs, errors.New("remote_password must be specified."))
}
if c.HostIp == "" {
errs = append(errs, errors.New("remote_host must be specified."))
}
if c.HostPortMin > c.HostPortMax {
errs = append(errs, errors.New("the host min port must be less than the max"))
}
if c.HTTPPortMin > c.HTTPPortMax {
errs = append(errs, errors.New("the HTTP min port must be less than the max"))
}
c.BootWait, err = time.ParseDuration(c.RawBootWait)
if err != nil {
errs = append(errs, fmt.Errorf("Failed to parse boot_wait: %s", err))
}
for i, command := range c.BootCommand {
if err := t.Validate(command); err != nil {
errs = append(errs,
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
}
}
if c.SSHKeyPath != "" {
if _, err := os.Stat(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
} else if _, err := commonssh.FileSigner(c.SSHKeyPath); err != nil {
errs = append(errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
}
}
/*
if c.SSHHostPortMin > c.SSHHostPortMax {
errs = append(errs,
errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
}
*/
if c.SSHUser == "" {
errs = append(errs, errors.New("An ssh_username must be specified."))
}
c.SSHWaitTimeout, err = time.ParseDuration(c.RawSSHWaitTimeout)
if err != nil {
errs = append(errs, fmt.Errorf("Failed to parse ssh_wait_timeout: %s", err))
}
switch c.Format {
case "xva", "vdi_raw":
default:
errs = append(errs, errors.New("format must be one of 'xva', 'vdi_raw'"))
}
switch c.KeepVM {
case "always", "never", "on_success":
default:
errs = append(errs, errors.New("keep_vm must be one of 'always', 'never', 'on_success'"))
}
return errs
}
// steps should check config.ShouldKeepVM first before cleaning up the VM
func (c CommonConfig) ShouldKeepVM(state multistep.StateBag) bool {
switch c.KeepVM {
case "always":
return true
case "never":
return false
case "on_success":
// only keep instance if build was successful
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
return !(cancelled || halted)
default:
panic(fmt.Sprintf("Unknown keep_vm value '%s'", c.KeepVM))
}
}
func (config CommonConfig) GetSR(client XenAPIClient) (*SR, error) {
if config.SrName == "" {
// Find the default SR
return client.GetDefaultSR()
} else {
// Use the provided name label to find the SR to use
srs, err := client.GetSRByNameLabel(config.SrName)
if err != nil {
return nil, err
}
switch {
case len(srs) == 0:
return nil, fmt.Errorf("Couldn't find a SR with the specified name-label '%s'", config.SrName)
case len(srs) > 1:
return nil, fmt.Errorf("Found more than one SR with the name '%s'. The name must be unique", config.SrName)
}
return srs[0], nil
}
}

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"

View File

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

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"bytes" "bytes"
@ -13,22 +13,23 @@ import (
"strings" "strings"
) )
func sshAddress(state multistep.StateBag) (string, error) { func SSHAddress(state multistep.StateBag) (string, error) {
sshIP := state.Get("ssh_address").(string) sshIP := state.Get("ssh_address").(string)
sshHostPort := 22 sshHostPort := 22
return fmt.Sprintf("%s:%d", sshIP, sshHostPort), nil return fmt.Sprintf("%s:%d", sshIP, sshHostPort), nil
} }
func sshLocalAddress(state multistep.StateBag) (string, error) { func SSHLocalAddress(state multistep.StateBag) (string, error) {
sshLocalPort := state.Get("local_ssh_port").(uint) sshLocalPort, ok := state.Get("local_ssh_port").(uint)
if !ok {
return "", fmt.Errorf("SSH port forwarding hasn't been set up yet")
}
conn_str := fmt.Sprintf("%s:%d", "127.0.0.1", sshLocalPort) conn_str := fmt.Sprintf("%s:%d", "127.0.0.1", sshLocalPort)
log.Printf("sshLocalAddress: %s", conn_str)
return conn_str, nil return conn_str, nil
} }
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) { func SSHConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
auth := []gossh.AuthMethod{ auth := []gossh.AuthMethod{
gossh.Password(config.SSHPassword), gossh.Password(config.SSHPassword),
gossh.KeyboardInteractive( gossh.KeyboardInteractive(
@ -50,24 +51,14 @@ func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
}, nil }, nil
} }
func execute_ssh_cmd(cmd, host, port, username, password string) (stdout string, err error) { func doExecuteSSHCmd(cmd, target string, config *gossh.ClientConfig) (stdout string, err error) {
// Setup connection config client, err := gossh.Dial("tcp", target, config)
config := &gossh.ClientConfig{
User: username,
Auth: []gossh.AuthMethod{
gossh.Password(password),
},
}
client, err := gossh.Dial("tcp", host+":"+port, config)
if err != nil { if err != nil {
return "", err return "", err
} }
//Create session //Create session
session, err := client.NewSession() session, err := client.NewSession()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -83,6 +74,31 @@ func execute_ssh_cmd(cmd, host, port, username, password string) (stdout string,
return strings.Trim(b.String(), "\n"), nil return strings.Trim(b.String(), "\n"), nil
} }
func ExecuteHostSSHCmd(state multistep.StateBag, cmd string) (stdout string, err error) {
config := state.Get("commonconfig").(CommonConfig)
// Setup connection config
sshConfig := &gossh.ClientConfig{
User: config.Username,
Auth: []gossh.AuthMethod{
gossh.Password(config.Password),
},
}
return doExecuteSSHCmd(cmd, config.HostIp+":22", sshConfig)
}
func ExecuteGuestSSHCmd(state multistep.StateBag, cmd string) (stdout string, err error) {
localAddress, err := SSHLocalAddress(state)
if err != nil {
return
}
sshConfig, err := SSHConfig(state)
if err != nil {
return
}
return doExecuteSSHCmd(cmd, localAddress, sshConfig)
}
func forward(local_conn net.Conn, config *gossh.ClientConfig, server, remote_dest string, remote_port uint) error { func forward(local_conn net.Conn, config *gossh.ClientConfig, server, remote_dest string, remote_port uint) error {
defer local_conn.Close() defer local_conn.Close()

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -7,14 +7,14 @@ import (
"log" "log"
) )
type stepAttachVdi struct { type StepAttachVdi struct {
VdiUuidKey string VdiUuidKey string
VdiType VDIType VdiType VDIType
vdi *VDI vdi *VDI
} }
func (self *stepAttachVdi) Run(state multistep.StateBag) multistep.StepAction { func (self *StepAttachVdi) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
@ -51,10 +51,10 @@ func (self *stepAttachVdi) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepAttachVdi) Cleanup(state multistep.StateBag) { func (self *StepAttachVdi) Cleanup(state multistep.StateBag) {
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
if config.ShouldKeepInstance(state) { if config.ShouldKeepVM(state) {
return return
} }

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -6,11 +6,11 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepBootWait struct{} type StepBootWait struct{}
func (self *stepBootWait) Run(state multistep.StateBag) multistep.StepAction { func (self *StepBootWait) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
instance, _ := client.GetVMByUuid(state.Get("instance_uuid").(string)) instance, _ := client.GetVMByUuid(state.Get("instance_uuid").(string))
@ -28,4 +28,4 @@ func (self *stepBootWait) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepBootWait) Cleanup(state multistep.StateBag) {} func (self *StepBootWait) Cleanup(state multistep.StateBag) {}

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -7,11 +7,11 @@ import (
"log" "log"
) )
type stepDetachVdi struct { type StepDetachVdi struct {
VdiUuidKey string VdiUuidKey string
} }
func (self *stepDetachVdi) Run(state multistep.StateBag) multistep.StepAction { func (self *StepDetachVdi) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
@ -39,7 +39,8 @@ func (self *stepDetachVdi) Run(state multistep.StateBag) multistep.StepAction {
err = instance.DisconnectVdi(vdi) err = instance.DisconnectVdi(vdi)
if err != nil { if err != nil {
ui.Error(fmt.Sprintf("Unable to detach VDI '%s': %s", vdiUuid, err.Error())) ui.Error(fmt.Sprintf("Unable to detach VDI '%s': %s", vdiUuid, err.Error()))
return multistep.ActionHalt //return multistep.ActionHalt
return multistep.ActionContinue
} }
log.Printf("Detached VDI '%s'", vdiUuid) log.Printf("Detached VDI '%s'", vdiUuid)
@ -47,4 +48,4 @@ func (self *stepDetachVdi) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepDetachVdi) Cleanup(state multistep.StateBag) {} func (self *StepDetachVdi) Cleanup(state multistep.StateBag) {}

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -6,13 +6,13 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepFindVdi struct { type StepFindVdi struct {
VdiName string VdiName string
ImagePathFunc func() string ImagePathFunc func() string
VdiUuidKey string VdiUuidKey string
} }
func (self *stepFindVdi) Run(state multistep.StateBag) multistep.StepAction { func (self *StepFindVdi) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
@ -39,4 +39,4 @@ func (self *stepFindVdi) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepFindVdi) Cleanup(state multistep.StateBag) {} func (self *StepFindVdi) Cleanup(state multistep.StateBag) {}

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -6,7 +6,7 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepForwardPortOverSSH struct { type StepForwardPortOverSSH struct {
RemotePort func(state multistep.StateBag) (uint, error) RemotePort func(state multistep.StateBag) (uint, error)
RemoteDest func(state multistep.StateBag) (string, error) RemoteDest func(state multistep.StateBag) (string, error)
@ -16,9 +16,9 @@ type stepForwardPortOverSSH struct {
ResultKey string ResultKey string
} }
func (self *stepForwardPortOverSSH) Run(state multistep.StateBag) multistep.StepAction { func (self *StepForwardPortOverSSH) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
// Find a free local port: // Find a free local port:
@ -46,4 +46,4 @@ func (self *stepForwardPortOverSSH) Run(state multistep.StateBag) multistep.Step
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepForwardPortOverSSH) Cleanup(state multistep.StateBag) {} func (self *StepForwardPortOverSSH) Cleanup(state multistep.StateBag) {}

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -7,11 +7,9 @@ import (
"strconv" "strconv"
) )
type stepGetVNCPort struct{} type StepGetVNCPort struct{}
func (self *stepGetVNCPort) Run(state multistep.StateBag) multistep.StepAction { func (self *StepGetVNCPort) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Step: forward the instances VNC port over SSH") ui.Say("Step: forward the instances VNC port over SSH")
@ -19,7 +17,7 @@ func (self *stepGetVNCPort) Run(state multistep.StateBag) multistep.StepAction {
domid := state.Get("domid").(string) domid := state.Get("domid").(string)
cmd := fmt.Sprintf("xenstore-read /local/domain/%s/console/vnc-port", domid) cmd := fmt.Sprintf("xenstore-read /local/domain/%s/console/vnc-port", domid)
remote_vncport, err := execute_ssh_cmd(cmd, config.HostIp, "22", config.Username, config.Password) remote_vncport, err := ExecuteHostSSHCmd(state, cmd)
if err != nil { if err != nil {
ui.Error(fmt.Sprintf("Unable to get VNC port (is the VM running?): %s", err.Error())) ui.Error(fmt.Sprintf("Unable to get VNC port (is the VM running?): %s", err.Error()))
return multistep.ActionHalt return multistep.ActionHalt
@ -38,15 +36,15 @@ func (self *stepGetVNCPort) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepGetVNCPort) Cleanup(state multistep.StateBag) { func (self *StepGetVNCPort) Cleanup(state multistep.StateBag) {
} }
func instanceVNCPort(state multistep.StateBag) (uint, error) { func InstanceVNCPort(state multistep.StateBag) (uint, error) {
vncPort := state.Get("instance_vnc_port").(uint) vncPort := state.Get("instance_vnc_port").(uint)
return vncPort, nil return vncPort, nil
} }
func instanceVNCIP(state multistep.StateBag) (string, error) { func InstanceVNCIP(state multistep.StateBag) (string, error) {
// The port is in Dom0, so we want to forward from localhost // The port is in Dom0, so we want to forward from localhost
return "127.0.0.1", nil return "127.0.0.1", nil
} }

View File

@ -1,4 +1,4 @@
package xenserver package common
// Taken from mitchellh/packer/builder/qemu/step_http_server.go // Taken from mitchellh/packer/builder/qemu/step_http_server.go
@ -20,12 +20,12 @@ import (
// //
// Produces: // Produces:
// http_port int - The port the HTTP server started on. // http_port int - The port the HTTP server started on.
type stepHTTPServer struct { type StepHTTPServer struct {
l net.Listener l net.Listener
} }
func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction { func (s *StepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
var httpPort uint = 0 var httpPort uint = 0
@ -54,7 +54,7 @@ func (s *stepHTTPServer) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (s *stepHTTPServer) Cleanup(multistep.StateBag) { func (s *StepHTTPServer) Cleanup(multistep.StateBag) {
if s.l != nil { if s.l != nil {
// Close the listener so that the HTTP server stops // Close the listener so that the HTTP server stops
s.l.Close() s.l.Close()

View File

@ -1,4 +1,4 @@
package xenserver package common
/* Taken from https://raw.githubusercontent.com/mitchellh/packer/master/builder/qemu/step_prepare_output_dir.go */ /* Taken from https://raw.githubusercontent.com/mitchellh/packer/master/builder/qemu/step_prepare_output_dir.go */
@ -10,18 +10,20 @@ import (
"time" "time"
) )
type stepPrepareOutputDir struct{} type StepPrepareOutputDir struct {
Force bool
Path string
}
func (stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction { func (self *StepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce { if _, err := os.Stat(self.Path); err == nil && self.Force {
ui.Say("Deleting previous output directory...") ui.Say("Deleting previous output directory...")
os.RemoveAll(config.OutputDir) os.RemoveAll(self.Path)
} }
if err := os.MkdirAll(config.OutputDir, 0755); err != nil { if err := os.MkdirAll(self.Path, 0755); err != nil {
state.Put("error", err) state.Put("error", err)
return multistep.ActionHalt return multistep.ActionHalt
} }
@ -29,17 +31,16 @@ func (stepPrepareOutputDir) Run(state multistep.StateBag) multistep.StepAction {
return multistep.ActionContinue return multistep.ActionContinue
} }
func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) { func (self *StepPrepareOutputDir) Cleanup(state multistep.StateBag) {
_, cancelled := state.GetOk(multistep.StateCancelled) _, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted) _, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted { if cancelled || halted {
config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting output directory...") ui.Say("Deleting output directory...")
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
err := os.RemoveAll(config.OutputDir) err := os.RemoveAll(self.Path)
if err == nil { if err == nil {
break break
} }

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -6,9 +6,9 @@ import (
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
) )
type stepRemoveDevices struct{} type StepRemoveDevices struct{}
func (self *stepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction { func (self *StepRemoveDevices) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
@ -37,4 +37,4 @@ func (self *stepRemoveDevices) Run(state multistep.StateBag) multistep.StepActio
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepRemoveDevices) Cleanup(state multistep.StateBag) {} func (self *StepRemoveDevices) Cleanup(state multistep.StateBag) {}

View File

@ -1,4 +1,4 @@
package xenserver package common
/* Taken from https://raw.githubusercontent.com/mitchellh/packer/master/builder/qemu/step_prepare_output_dir.go */ /* Taken from https://raw.githubusercontent.com/mitchellh/packer/master/builder/qemu/step_prepare_output_dir.go */
@ -13,7 +13,7 @@ import (
"time" "time"
) )
type stepShutdownAndExport struct{} type StepShutdownAndExport struct{}
func downloadFile(url, filename string) (err error) { func downloadFile(url, filename string) (err error) {
@ -44,8 +44,8 @@ func downloadFile(url, filename string) (err error) {
return nil return nil
} }
func (stepShutdownAndExport) Run(state multistep.StateBag) multistep.StepAction { func (StepShutdownAndExport) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
instance_uuid := state.Get("instance_uuid").(string) instance_uuid := state.Get("instance_uuid").(string)
@ -56,14 +56,14 @@ func (stepShutdownAndExport) Run(state multistep.StateBag) multistep.StepAction
return multistep.ActionHalt return multistep.ActionHalt
} }
ui.Say("Step: Shutdown and export VPX") ui.Say("Step: Shutdown and export")
// Shutdown the VM // Shutdown the VM
success := func() bool { success := func() bool {
if config.ShutdownCommand != "" { if config.ShutdownCommand != "" {
ui.Say("Executing shutdown command...") ui.Say("Executing shutdown command...")
_, err := execute_ssh_cmd(config.ShutdownCommand, config.HostIp, "22", config.Username, config.Password) _, err := ExecuteGuestSSHCmd(state, config.ShutdownCommand)
if err != nil { if err != nil {
ui.Error(fmt.Sprintf("Shutdown command failed: %s", err.Error())) ui.Error(fmt.Sprintf("Shutdown command failed: %s", err.Error()))
return false return false
@ -109,7 +109,7 @@ func (stepShutdownAndExport) Run(state multistep.StateBag) multistep.StepAction
ui.Say("Successfully shut down VM") ui.Say("Successfully shut down VM")
switch config.ExportFormat { switch config.Format {
case "xva": case "xva":
// export the VM // export the VM
@ -164,7 +164,7 @@ func (stepShutdownAndExport) Run(state multistep.StateBag) multistep.StepAction
} }
default: default:
panic(fmt.Sprintf("Unknown export_format '%s'", config.ExportFormat)) panic(fmt.Sprintf("Unknown export format '%s'", config.Format))
} }
ui.Say("Download completed: " + config.OutputDir) ui.Say("Download completed: " + config.OutputDir)
@ -172,5 +172,4 @@ func (stepShutdownAndExport) Run(state multistep.StateBag) multistep.StepAction
return multistep.ActionContinue return multistep.ActionContinue
} }
func (stepShutdownAndExport) Cleanup(state multistep.StateBag) { func (StepShutdownAndExport) Cleanup(state multistep.StateBag) {}
}

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
gossh "code.google.com/p/go.crypto/ssh" gossh "code.google.com/p/go.crypto/ssh"
@ -9,7 +9,7 @@ import (
"time" "time"
) )
type stepStartOnHIMN struct{} type StepStartOnHIMN struct{}
/* /*
* This step starts the installed guest on the Host Internal Management Network * This step starts the installed guest on the Host Internal Management Network
@ -19,11 +19,10 @@ type stepStartOnHIMN struct{}
* *
*/ */
func (self *stepStartOnHIMN) Run(state multistep.StateBag) multistep.StepAction { func (self *StepStartOnHIMN) Run(state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
config := state.Get("config").(config)
ui.Say("Step: Start VM on the Host Internal Mangement Network") ui.Say("Step: Start VM on the Host Internal Mangement Network")
@ -96,7 +95,7 @@ func (self *stepStartOnHIMN) Run(state multistep.StateBag) multistep.StepAction
err = InterruptibleWait{ err = InterruptibleWait{
Predicate: func() (success bool, err error) { Predicate: func() (success bool, err error) {
ui.Message(fmt.Sprintf("Attempting to ping interface: %s", ping_cmd)) ui.Message(fmt.Sprintf("Attempting to ping interface: %s", ping_cmd))
_, err = execute_ssh_cmd(ping_cmd, config.HostIp, "22", config.Username, config.Password) _, err = ExecuteHostSSHCmd(state, ping_cmd)
switch err.(type) { switch err.(type) {
case nil: case nil:
@ -124,13 +123,14 @@ func (self *stepStartOnHIMN) Run(state multistep.StateBag) multistep.StepAction
} }
func (self *stepStartOnHIMN) Cleanup(state multistep.StateBag) {} func (self *StepStartOnHIMN) Cleanup(state multistep.StateBag) {}
func himnSSHIP(state multistep.StateBag) (string, error) { func HimnSSHIP(state multistep.StateBag) (string, error) {
ip := state.Get("himn_ssh_address").(string) ip := state.Get("himn_ssh_address").(string)
return ip, nil return ip, nil
} }
func himnSSHPort(state multistep.StateBag) (uint, error) { func HimnSSHPort(state multistep.StateBag) (uint, error) {
return 22, nil config := state.Get("commonconfig").(CommonConfig)
return config.SSHPort, nil
} }

View File

@ -1,4 +1,4 @@
package xenserver package common
import ( import (
"fmt" "fmt"
@ -7,9 +7,9 @@ import (
"log" "log"
) )
type stepStartVmPaused struct{} type StepStartVmPaused struct{}
func (self *stepStartVmPaused) Run(state multistep.StateBag) multistep.StepAction { func (self *StepStartVmPaused) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
@ -39,11 +39,11 @@ func (self *stepStartVmPaused) Run(state multistep.StateBag) multistep.StepActio
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepStartVmPaused) Cleanup(state multistep.StateBag) { func (self *StepStartVmPaused) Cleanup(state multistep.StateBag) {
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(XenAPIClient)
if config.ShouldKeepInstance(state) { if config.ShouldKeepVM(state) {
return return
} }

View File

@ -1,4 +1,4 @@
package xenserver package common
/* Heavily borrowed from builder/quemu/step_type_boot_command.go */ /* Heavily borrowed from builder/quemu/step_type_boot_command.go */
@ -23,10 +23,12 @@ type bootCommandTemplateData struct {
HTTPPort uint HTTPPort uint
} }
type stepTypeBootCommand struct{} type StepTypeBootCommand struct {
Tpl *packer.ConfigTemplate
}
func (self *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction { func (self *StepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config) config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
vnc_port := state.Get("local_vnc_port").(uint) vnc_port := state.Get("local_vnc_port").(uint)
http_port := state.Get("http_port").(uint) http_port := state.Get("http_port").(uint)
@ -58,17 +60,29 @@ func (self *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAct
log.Printf("Connected to the VNC console: %s", c.DesktopName) log.Printf("Connected to the VNC console: %s", c.DesktopName)
// @todo - include http port/ip so kickstarter files can be grabbed // find local ip
envVar, err := ExecuteHostSSHCmd(state, "echo $SSH_CLIENT")
if err != nil {
ui.Error(fmt.Sprintf("Error detecting local IP: %s", err))
return multistep.ActionHalt
}
if envVar == "" {
ui.Error("Error detecting local IP: $SSH_CLIENT was empty")
return multistep.ActionHalt
}
localIp := strings.Split(envVar, " ")[0]
ui.Message(fmt.Sprintf("Found local IP: %s", localIp))
tplData := &bootCommandTemplateData{ tplData := &bootCommandTemplateData{
config.VMName, config.VMName,
config.LocalIp, localIp,
http_port, http_port,
} }
ui.Say("About to type boot commands over VNC...") ui.Say("About to type boot commands over VNC...")
for _, command := range config.BootCommand { for _, command := range config.BootCommand {
command, err := config.tpl.Process(command, tplData) command, err := self.Tpl.Process(command, tplData)
if err != nil { if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err) err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err) state.Put("error", err)
@ -89,7 +103,7 @@ func (self *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAct
return multistep.ActionContinue return multistep.ActionContinue
} }
func (self *stepTypeBootCommand) Cleanup(multistep.StateBag) {} func (self *StepTypeBootCommand) Cleanup(multistep.StateBag) {}
// Taken from qemu's builder plugin - not an exported function. // Taken from qemu's builder plugin - not an exported function.
func vncSendString(c *vnc.ClientConn, original string) { func vncSendString(c *vnc.ClientConn, original string) {

View File

@ -0,0 +1,124 @@
package common
import (
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"os"
"time"
)
type StepUploadVdi struct {
VdiName string
ImagePathFunc func() string
VdiUuidKey string
}
func (self *StepUploadVdi) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient)
imagePath := self.ImagePathFunc()
if imagePath == "" {
// skip if no disk image to attach
return multistep.ActionContinue
}
ui.Say(fmt.Sprintf("Step: Upload VDI '%s'", self.VdiName))
// Create VDI for the image
sr, err := config.GetSR(client)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get SR: %s", err.Error()))
return multistep.ActionHalt
}
// Open the file for reading (NB: HTTPUpload closes the file for us)
fh, err := os.Open(imagePath)
if err != nil {
ui.Error(fmt.Sprintf("Unable to open disk image '%s': %s", imagePath, err.Error()))
return multistep.ActionHalt
}
// Get file length
fstat, err := fh.Stat()
if err != nil {
ui.Error(fmt.Sprintf("Unable to stat disk image '%s': %s", imagePath, err.Error()))
return multistep.ActionHalt
}
fileLength := fstat.Size()
// Create the VDI
vdi, err := sr.CreateVdi(self.VdiName, fileLength)
if err != nil {
ui.Error(fmt.Sprintf("Unable to create VDI '%s': %s", self.VdiName, err.Error()))
return multistep.ActionHalt
}
vdiUuid, err := vdi.GetUuid()
if err != nil {
ui.Error(fmt.Sprintf("Unable to get UUID of VDI '%s': %s", self.VdiName, err.Error()))
return multistep.ActionHalt
}
state.Put(self.VdiUuidKey, vdiUuid)
_, err = HTTPUpload(fmt.Sprintf("https://%s/import_raw_vdi?vdi=%s&session_id=%s",
client.Host,
vdi.Ref,
client.Session.(string),
), fh, state)
if err != nil {
ui.Error(fmt.Sprintf("Unable to upload VDI: %s", err.Error()))
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (self *StepUploadVdi) Cleanup(state multistep.StateBag) {
config := state.Get("commonconfig").(CommonConfig)
ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient)
if config.ShouldKeepVM(state) {
return
}
vdiUuidRaw, ok := state.GetOk(self.VdiUuidKey)
if !ok {
// VDI doesn't exist
return
}
vdiUuid := vdiUuidRaw.(string)
if vdiUuid == "" {
// VDI already cleaned up
return
}
vdi, err := client.GetVdiByUuid(vdiUuid)
if err != nil {
ui.Error(fmt.Sprintf("Can't get VDI '%s': %s", vdiUuid, err.Error()))
return
}
// an interrupted import_raw_vdi takes a while to release the VDI
// so try several times
for i := 0; i < 3; i++ {
log.Printf("Trying to destroy VDI...")
err = vdi.Destroy()
if err == nil {
break
}
time.Sleep(1 * time.Second)
}
if err != nil {
ui.Error(fmt.Sprintf("Can't destroy VDI '%s': %s", vdiUuid, err.Error()))
return
}
ui.Say(fmt.Sprintf("Destroyed VDI '%s'", self.VdiName))
state.Put(self.VdiUuidKey, "")
}

View File

@ -0,0 +1,304 @@
package iso
import (
"errors"
"fmt"
"log"
"path"
"strings"
"time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
xscommon "github.com/rdobson/packer-builder-xenserver/builder/xenserver/common"
)
type config struct {
common.PackerConfig `mapstructure:",squash"`
xscommon.CommonConfig `mapstructure:",squash"`
VMMemory uint `mapstructure:"vm_memory"`
DiskSize uint `mapstructure:"disk_size"`
CloneTemplate string `mapstructure:"clone_template"`
ISOChecksum string `mapstructure:"iso_checksum"`
ISOChecksumType string `mapstructure:"iso_checksum_type"`
ISOUrls []string `mapstructure:"iso_urls"`
ISOUrl string `mapstructure:"iso_url"`
PlatformArgs map[string]string `mapstructure:"platform_args"`
RawInstallTimeout string `mapstructure:"install_timeout"`
InstallTimeout time.Duration ``
tpl *packer.ConfigTemplate
}
type Builder struct {
config config
runner multistep.Runner
}
func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error) {
md, err := common.DecodeConfig(&self.config, raws...)
if err != nil {
return nil, err
}
self.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, err
}
self.config.tpl.UserVars = self.config.PackerUserVars
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(
errs, self.config.CommonConfig.Prepare(self.config.tpl, &self.config.PackerConfig)...)
// Set default values
if self.config.RawInstallTimeout == "" {
self.config.RawInstallTimeout = "200m"
}
if self.config.DiskSize == 0 {
self.config.DiskSize = 40000
}
if self.config.VMMemory == 0 {
self.config.VMMemory = 1024
}
if self.config.CloneTemplate == "" {
self.config.CloneTemplate = "Other install media"
}
if len(self.config.PlatformArgs) == 0 {
pargs := make(map[string]string)
pargs["viridian"] = "false"
pargs["nx"] = "true"
pargs["pae"] = "true"
pargs["apic"] = "true"
pargs["timeoffset"] = "0"
pargs["acpi"] = "1"
self.config.PlatformArgs = pargs
}
// Template substitution
templates := map[string]*string{
"clone_template": &self.config.CloneTemplate,
"network_name": &self.config.NetworkName,
"iso_checksum": &self.config.ISOChecksum,
"iso_checksum_type": &self.config.ISOChecksumType,
"iso_url": &self.config.ISOUrl,
"install_timeout": &self.config.RawInstallTimeout,
}
for i := range self.config.ISOUrls {
templates[fmt.Sprintf("iso_urls[%d]", i)] = &self.config.ISOUrls[i]
}
for n, ptr := range templates {
var err error
*ptr, err = self.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
// Validation
self.config.InstallTimeout, err = time.ParseDuration(self.config.RawInstallTimeout)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed to parse install_timeout: %s", err))
}
if self.config.ISOChecksumType == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("The iso_checksum_type must be specified."))
} else {
self.config.ISOChecksumType = strings.ToLower(self.config.ISOChecksumType)
if self.config.ISOChecksumType != "none" {
if self.config.ISOChecksum == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Due to the file size being large, an iso_checksum is required."))
} else {
self.config.ISOChecksum = strings.ToLower(self.config.ISOChecksum)
}
if hash := common.HashForType(self.config.ISOChecksumType); hash == nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Unsupported checksum type: %s", self.config.ISOChecksumType))
}
}
}
if len(self.config.ISOUrls) == 0 {
if self.config.ISOUrl == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("One of iso_url or iso_urls must be specified."))
} else {
self.config.ISOUrls = []string{self.config.ISOUrl}
}
} else if self.config.ISOUrl != "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Only one of iso_url or iso_urls may be specified."))
}
for i, url := range self.config.ISOUrls {
self.config.ISOUrls[i], err = common.DownloadableURL(url)
if err != nil {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("Failed to parse iso_urls[%d]: %s", i, err))
}
}
if len(errs.Errors) > 0 {
retErr = errors.New(errs.Error())
}
return nil, retErr
}
func (self *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
//Setup XAPI client
client := xscommon.NewXenAPIClient(self.config.HostIp, self.config.Username, self.config.Password)
err := client.Login()
if err != nil {
return nil, err.(error)
}
ui.Say("XAPI client session established")
client.GetHosts()
//Share state between the other steps using a statebag
state := new(multistep.BasicStateBag)
state.Put("cache", cache)
state.Put("client", client)
state.Put("config", self.config)
state.Put("commonconfig", self.config.CommonConfig)
state.Put("hook", hook)
state.Put("ui", ui)
//Build the steps
steps := []multistep.Step{
&common.StepDownload{
Checksum: self.config.ISOChecksum,
ChecksumType: self.config.ISOChecksumType,
Description: "ISO",
ResultKey: "iso_path",
Url: self.config.ISOUrls,
},
&xscommon.StepPrepareOutputDir{
Force: self.config.PackerForce,
Path: self.config.OutputDir,
},
&common.StepCreateFloppy{
Files: self.config.FloppyFiles,
},
new(xscommon.StepHTTPServer),
&xscommon.StepUploadVdi{
VdiName: "Packer-floppy-disk",
ImagePathFunc: func() string {
if floppyPath, ok := state.GetOk("floppy_path"); ok {
return floppyPath.(string)
}
return ""
},
VdiUuidKey: "floppy_vdi_uuid",
},
&xscommon.StepUploadVdi{
VdiName: path.Base(self.config.ISOUrls[0]),
ImagePathFunc: func() string {
return state.Get("iso_path").(string)
},
VdiUuidKey: "iso_vdi_uuid",
},
&xscommon.StepFindVdi{
VdiName: self.config.ToolsIsoName,
VdiUuidKey: "tools_vdi_uuid",
},
new(stepCreateInstance),
&xscommon.StepAttachVdi{
VdiUuidKey: "floppy_vdi_uuid",
VdiType: xscommon.Floppy,
},
&xscommon.StepAttachVdi{
VdiUuidKey: "iso_vdi_uuid",
VdiType: xscommon.CD,
},
&xscommon.StepAttachVdi{
VdiUuidKey: "tools_vdi_uuid",
VdiType: xscommon.CD,
},
new(xscommon.StepStartVmPaused),
new(xscommon.StepGetVNCPort),
&xscommon.StepForwardPortOverSSH{
RemotePort: xscommon.InstanceVNCPort,
RemoteDest: xscommon.InstanceVNCIP,
HostPortMin: self.config.HostPortMin,
HostPortMax: self.config.HostPortMax,
ResultKey: "local_vnc_port",
},
new(xscommon.StepBootWait),
&xscommon.StepTypeBootCommand{
Tpl: self.config.tpl,
},
new(stepWait),
&xscommon.StepDetachVdi{
VdiUuidKey: "floppy_vdi_uuid",
},
&xscommon.StepDetachVdi{
VdiUuidKey: "iso_vdi_uuid",
},
new(xscommon.StepRemoveDevices),
new(xscommon.StepStartOnHIMN),
&xscommon.StepForwardPortOverSSH{
RemotePort: xscommon.HimnSSHPort,
RemoteDest: xscommon.HimnSSHIP,
HostPortMin: self.config.HostPortMin,
HostPortMax: self.config.HostPortMax,
ResultKey: "local_ssh_port",
},
&common.StepConnectSSH{
SSHAddress: xscommon.SSHLocalAddress,
SSHConfig: xscommon.SSHConfig,
SSHWaitTimeout: self.config.SSHWaitTimeout,
},
new(common.StepProvision),
new(xscommon.StepShutdownAndExport),
}
self.runner = &multistep.BasicRunner{Steps: steps}
self.runner.Run(state)
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.")
}
artifact, _ := xscommon.NewArtifact(self.config.OutputDir)
return artifact, nil
}
func (self *Builder) Cancel() {
if self.runner != nil {
log.Println("Cancelling the step runner...")
self.runner.Cancel()
}
fmt.Println("Cancelling the builder")
}

View File

@ -0,0 +1,360 @@
package iso
import (
"github.com/mitchellh/packer/packer"
"reflect"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"remote_host": "localhost",
"remote_username": "admin",
"remote_password": "admin",
"vm_name": "foo",
"iso_checksum": "foo",
"iso_checksum_type": "md5",
"iso_url": "http://www.google.com/",
"shutdown_command": "yes",
"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()
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ToolsIsoName != "xs-tools.iso" {
t.Errorf("bad tools ISO name: %s", b.config.ToolsIsoName)
}
if b.config.CloneTemplate != "Other install media" {
t.Errorf("bad clone template: %s", b.config.CloneTemplate)
}
if b.config.VMName == "" {
t.Errorf("bad vm name: %s", b.config.VMName)
}
if b.config.Format != "xva" {
t.Errorf("bad format: %s", b.config.Format)
}
if b.config.KeepVM != "never" {
t.Errorf("bad keep instance: %s", b.config.KeepVM)
}
}
func TestBuilderPrepare_DiskSize(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "disk_size")
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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_Format(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["format"] = "foo"
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["format"] = "vdi_raw"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_HTTPPort(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["http_port_min"] = 1000
config["http_port_max"] = 500
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Bad
config["http_port_min"] = -500
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["http_port_min"] = 500
config["http_port_max"] = 1000
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_ISOChecksum(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["iso_checksum"] = ""
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_checksum"] = "FOo"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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"] = ""
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_checksum_type"] = "mD5"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test none
config["iso_checksum_type"] = "none"
b = Builder{}
warns, err = b.Prepare(config)
// @todo: give warning in this case?
/*
if len(warns) == 0 {
t.Fatalf("bad: %#v", warns)
}
*/
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOChecksumType != "none" {
t.Fatalf("should've lowercased: %s", b.config.ISOChecksumType)
}
}
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{}
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test iso_url set
config["iso_url"] = "http://www.packer.io"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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_KeepVM(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["keep_vm"] = "foo"
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["keep_vm"] = "always"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}

View File

@ -1,19 +1,21 @@
package xenserver package iso
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
xscommon "github.com/rdobson/packer-builder-xenserver/builder/xenserver/common"
) )
type stepCreateInstance struct { type stepCreateInstance struct {
instance *VM instance *xscommon.VM
vdi *VDI vdi *xscommon.VDI
} }
func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction { func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(XenAPIClient) client := state.Get("client").(xscommon.XenAPIClient)
config := state.Get("config").(config) config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
@ -75,7 +77,7 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi
} }
self.vdi = vdi self.vdi = vdi
err = instance.ConnectVdi(vdi, Disk) err = instance.ConnectVdi(vdi, xscommon.Disk)
if err != nil { if err != nil {
ui.Error(fmt.Sprintf("Unable to connect packer disk VDI: %s", err.Error())) ui.Error(fmt.Sprintf("Unable to connect packer disk VDI: %s", err.Error()))
return multistep.ActionHalt return multistep.ActionHalt
@ -83,11 +85,11 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi
// Connect Network // Connect Network
var network *Network var network *xscommon.Network
if config.NetworkName == "" { if config.NetworkName == "" {
// No network has be specified. Use the management interface // No network has be specified. Use the management interface
network = new(Network) network = new(xscommon.Network)
network.Ref = "" network.Ref = ""
network.Client = &client network.Client = &client
@ -162,7 +164,7 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi
func (self *stepCreateInstance) Cleanup(state multistep.StateBag) { func (self *stepCreateInstance) Cleanup(state multistep.StateBag) {
config := state.Get("config").(config) config := state.Get("config").(config)
if config.ShouldKeepInstance(state) { if config.ShouldKeepVM(state) {
return return
} }

View File

@ -1,11 +1,13 @@
package xenserver package iso
import ( import (
"fmt" "fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log" "log"
"time" "time"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
xscommon "github.com/rdobson/packer-builder-xenserver/builder/xenserver/common"
) )
type stepWait struct{} type stepWait struct{}
@ -13,7 +15,7 @@ type stepWait struct{}
func (self *stepWait) Run(state multistep.StateBag) multistep.StepAction { func (self *stepWait) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config) config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui) ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient) client := state.Get("client").(xscommon.XenAPIClient)
ui.Say("Step: Wait for install to complete.") ui.Say("Step: Wait for install to complete.")
@ -26,7 +28,7 @@ func (self *stepWait) Run(state multistep.StateBag) multistep.StepAction {
} }
//Expect install to be configured to shutdown on completion //Expect install to be configured to shutdown on completion
err = InterruptibleWait{ err = xscommon.InterruptibleWait{
Predicate: func() (bool, error) { Predicate: func() (bool, error) {
log.Printf("Waiting for install to complete.") log.Printf("Waiting for install to complete.")
power_state, err := instance.GetPowerState() power_state, err := instance.GetPowerState()

View File

@ -1,201 +0,0 @@
package xenserver
import (
"crypto/tls"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"net/http"
"os"
"time"
)
type stepUploadVdi struct {
VdiName string
ImagePathFunc func() string
VdiUuidKey string
}
func (self *stepUploadVdi) Run(state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient)
imagePath := self.ImagePathFunc()
if imagePath == "" {
// skip if no disk image to attach
return multistep.ActionContinue
}
ui.Say(fmt.Sprintf("Step: Upload VDI '%s'", self.VdiName))
// Create VDI for the image
sr, err := config.GetSR(client)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get SR: %s", err.Error()))
return multistep.ActionHalt
}
// Open the file for reading (NB: putFile closes the file for us)
fh, err := os.Open(imagePath)
if err != nil {
ui.Error(fmt.Sprintf("Unable to open disk image '%s': %s", imagePath, err.Error()))
return multistep.ActionHalt
}
// Get file length
fstat, err := fh.Stat()
if err != nil {
ui.Error(fmt.Sprintf("Unable to stat disk image '%s': %s", imagePath, err.Error()))
return multistep.ActionHalt
}
fileLength := fstat.Size()
// Create the VDI
vdi, err := sr.CreateVdi(self.VdiName, fileLength)
if err != nil {
ui.Error(fmt.Sprintf("Unable to create VDI '%s': %s", self.VdiName, err.Error()))
return multistep.ActionHalt
}
vdiUuid, err := vdi.GetUuid()
if err != nil {
ui.Error(fmt.Sprintf("Unable to get UUID of VDI '%s': %s", self.VdiName, err.Error()))
return multistep.ActionHalt
}
state.Put(self.VdiUuidKey, vdiUuid)
task, err := client.CreateTask()
if err != nil {
ui.Error(fmt.Sprintf("Unable to create task: %s", err.Error()))
return multistep.ActionHalt
}
defer task.Destroy()
import_url := fmt.Sprintf("https://%s/import_raw_vdi?vdi=%s&session_id=%s&task_id=%s",
client.Host,
vdi.Ref,
client.Session.(string),
task.Ref,
)
// Define a new transport which allows self-signed certs
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
// Create a client
httpClient := &http.Client{Transport: tr}
// Create request and download file
request, err := http.NewRequest("PUT", import_url, fh)
request.ContentLength = fileLength
ui.Say(fmt.Sprintf("PUT disk image '%s'", import_url))
resp, err := httpClient.Do(request) // Do closes fh for us, according to docs
if err != nil {
ui.Error(fmt.Sprintf("Unable to upload disk image: %s", err.Error()))
return multistep.ActionHalt
}
if resp.StatusCode != 200 {
ui.Error(fmt.Sprintf("Unable to upload disk image: PUT request got non-200 status code: %s", resp.Status))
return multistep.ActionHalt
}
logInterval := 0
err = InterruptibleWait{
Predicate: func() (bool, error) {
status, err := task.GetStatus()
if err != nil {
return false, fmt.Errorf("Failed to get task status: %s", err.Error())
}
switch status {
case Pending:
progress, err := task.GetProgress()
if err != nil {
return false, fmt.Errorf("Failed to get progress: %s", err.Error())
}
logInterval = logInterval + 1
if logInterval%5 == 0 {
log.Printf("Upload %.0f%% complete", progress*100)
}
return false, nil
case Success:
return true, nil
case Failure:
errorInfo, err := task.GetErrorInfo()
if err != nil {
errorInfo = []string{fmt.Sprintf("furthermore, failed to get error info: %s", err.Error())}
}
return false, fmt.Errorf("Task failed: %s", errorInfo)
case Cancelling, Cancelled:
return false, fmt.Errorf("Task cancelled")
default:
return false, fmt.Errorf("Unknown task status %v", status)
}
},
PredicateInterval: 1 * time.Second,
Timeout: 24 * time.Hour,
}.Wait(state)
resp.Body.Close()
if err != nil {
ui.Error(fmt.Sprintf("Error uploading: %s", err.Error()))
return multistep.ActionHalt
}
log.Printf("Upload complete")
return multistep.ActionContinue
}
func (self *stepUploadVdi) Cleanup(state multistep.StateBag) {
config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(XenAPIClient)
if config.ShouldKeepInstance(state) {
return
}
vdiUuidRaw, ok := state.GetOk(self.VdiUuidKey)
if !ok {
// VDI doesn't exist
return
}
vdiUuid := vdiUuidRaw.(string)
if vdiUuid == "" {
// VDI already cleaned up
return
}
vdi, err := client.GetVdiByUuid(vdiUuid)
if err != nil {
ui.Error(fmt.Sprintf("Can't get VDI '%s': %s", vdiUuid, err.Error()))
return
}
// an interrupted import_raw_vdi takes a while to release the VDI
// so try several times
for i := 0; i < 3; i++ {
log.Printf("Trying to destroy VDI...")
err = vdi.Destroy()
if err == nil {
break
}
time.Sleep(1 * time.Second)
}
if err != nil {
ui.Error(fmt.Sprintf("Can't destroy VDI '%s': %s", vdiUuid, err.Error()))
return
}
ui.Say(fmt.Sprintf("Destroyed VDI '%s'", self.VdiName))
state.Put(self.VdiUuidKey, "")
}

View File

@ -0,0 +1,198 @@
package xva
import (
"errors"
"fmt"
"log"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/common"
"github.com/mitchellh/packer/packer"
xscommon "github.com/rdobson/packer-builder-xenserver/builder/xenserver/common"
)
type config struct {
common.PackerConfig `mapstructure:",squash"`
xscommon.CommonConfig `mapstructure:",squash"`
SourcePath string `mapstructure:"source_path"`
VMMemory uint `mapstructure:"vm_memory"`
PlatformArgs map[string]string `mapstructure:"platform_args"`
tpl *packer.ConfigTemplate
}
type Builder struct {
config config
runner multistep.Runner
}
func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error) {
md, err := common.DecodeConfig(&self.config, raws...)
if err != nil {
return nil, err
}
self.config.tpl, err = packer.NewConfigTemplate()
if err != nil {
return nil, err
}
self.config.tpl.UserVars = self.config.PackerUserVars
errs := common.CheckUnusedConfig(md)
errs = packer.MultiErrorAppend(
errs, self.config.CommonConfig.Prepare(self.config.tpl, &self.config.PackerConfig)...)
// Set default values
if self.config.VMMemory == 0 {
self.config.VMMemory = 1024
}
if len(self.config.PlatformArgs) == 0 {
pargs := make(map[string]string)
pargs["viridian"] = "false"
pargs["nx"] = "true"
pargs["pae"] = "true"
pargs["apic"] = "true"
pargs["timeoffset"] = "0"
pargs["acpi"] = "1"
self.config.PlatformArgs = pargs
}
// Template substitution
templates := map[string]*string{
"source_path": &self.config.SourcePath,
"network_name": &self.config.NetworkName,
}
for n, ptr := range templates {
var err error
*ptr, err = self.config.tpl.Process(*ptr, nil)
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Error processing %s: %s", n, err))
}
}
// Validation
if self.config.SourcePath == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("A source_path must be specified"))
}
if len(errs.Errors) > 0 {
retErr = errors.New(errs.Error())
}
return nil, retErr
}
func (self *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
//Setup XAPI client
client := xscommon.NewXenAPIClient(self.config.HostIp, self.config.Username, self.config.Password)
err := client.Login()
if err != nil {
return nil, err.(error)
}
ui.Say("XAPI client session established")
client.GetHosts()
//Share state between the other steps using a statebag
state := new(multistep.BasicStateBag)
state.Put("cache", cache)
state.Put("client", client)
state.Put("config", self.config)
state.Put("commonconfig", self.config.CommonConfig)
state.Put("hook", hook)
state.Put("ui", ui)
//Build the steps
steps := []multistep.Step{
&xscommon.StepPrepareOutputDir{
Force: self.config.PackerForce,
Path: self.config.OutputDir,
},
&common.StepCreateFloppy{
Files: self.config.FloppyFiles,
},
new(xscommon.StepHTTPServer),
&xscommon.StepUploadVdi{
VdiName: "Packer-floppy-disk",
ImagePathFunc: func() string {
if floppyPath, ok := state.GetOk("floppy_path"); ok {
return floppyPath.(string)
}
return ""
},
VdiUuidKey: "floppy_vdi_uuid",
},
&xscommon.StepFindVdi{
VdiName: self.config.ToolsIsoName,
VdiUuidKey: "tools_vdi_uuid",
},
new(stepImportInstance),
&xscommon.StepAttachVdi{
VdiUuidKey: "floppy_vdi_uuid",
VdiType: xscommon.Floppy,
},
&xscommon.StepAttachVdi{
VdiUuidKey: "tools_vdi_uuid",
VdiType: xscommon.CD,
},
new(xscommon.StepRemoveDevices),
new(xscommon.StepStartOnHIMN),
&xscommon.StepForwardPortOverSSH{
RemotePort: xscommon.HimnSSHPort,
RemoteDest: xscommon.HimnSSHIP,
HostPortMin: self.config.HostPortMin,
HostPortMax: self.config.HostPortMax,
ResultKey: "local_ssh_port",
},
&common.StepConnectSSH{
SSHAddress: xscommon.SSHLocalAddress,
SSHConfig: xscommon.SSHConfig,
SSHWaitTimeout: self.config.SSHWaitTimeout,
},
new(common.StepProvision),
&xscommon.StepDetachVdi{
VdiUuidKey: "floppy_vdi_uuid",
},
&xscommon.StepDetachVdi{
VdiUuidKey: "iso_vdi_uuid",
},
new(xscommon.StepShutdownAndExport),
}
self.runner = &multistep.BasicRunner{Steps: steps}
self.runner.Run(state)
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.")
}
artifact, _ := xscommon.NewArtifact(self.config.OutputDir)
return artifact, nil
}
func (self *Builder) Cancel() {
if self.runner != nil {
log.Println("Cancelling the step runner...")
self.runner.Cancel()
}
fmt.Println("Cancelling the builder")
}

View File

@ -0,0 +1,188 @@
package xva
import (
"github.com/mitchellh/packer/packer"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"remote_host": "localhost",
"remote_username": "admin",
"remote_password": "admin",
"vm_name": "foo",
"shutdown_command": "yes",
"ssh_username": "foo",
"source_path": ".",
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()
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ToolsIsoName != "xs-tools.iso" {
t.Errorf("bad tools ISO name: %s", b.config.ToolsIsoName)
}
if b.config.VMName == "" {
t.Errorf("bad vm name: %s", b.config.VMName)
}
if b.config.Format != "xva" {
t.Errorf("bad format: %s", b.config.Format)
}
if b.config.KeepVM != "never" {
t.Errorf("bad keep instance: %s", b.config.KeepVM)
}
}
func TestBuilderPrepare_Format(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["format"] = "foo"
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["format"] = "vdi_raw"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_HTTPPort(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["http_port_min"] = 1000
config["http_port_max"] = 500
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Bad
config["http_port_min"] = -500
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["http_port_min"] = 500
config["http_port_max"] = 1000
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
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
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_KeepVM(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["keep_vm"] = "foo"
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["keep_vm"] = "always"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SourcePath(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["source_path"] = ""
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["source_path"] = "."
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}

View File

@ -0,0 +1,175 @@
package xva
import (
"fmt"
"os"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
xscommon "github.com/rdobson/packer-builder-xenserver/builder/xenserver/common"
)
type stepImportInstance struct {
instance *xscommon.VM
vdi *xscommon.VDI
}
func (self *stepImportInstance) Run(state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(xscommon.XenAPIClient)
config := state.Get("config").(config)
ui := state.Get("ui").(packer.Ui)
ui.Say("Step: Import Instance")
// find the SR
sr, err := config.GetSR(client)
if err != nil {
ui.Error(fmt.Sprintf("Unable to get SR: %s", err.Error()))
return multistep.ActionHalt
}
// Open the file for reading (NB: httpUpload closes the file for us)
fh, err := os.Open(config.SourcePath)
if err != nil {
ui.Error(fmt.Sprintf("Unable to open XVA '%s': %s", config.SourcePath, err.Error()))
return multistep.ActionHalt
}
result, err := xscommon.HTTPUpload(fmt.Sprintf("https://%s/import?session_id=%s&sr_id=%s",
client.Host,
client.Session.(string),
sr.Ref,
), fh, state)
if err != nil {
ui.Error(fmt.Sprintf("Unable to upload VDI: %s", err.Error()))
return multistep.ActionHalt
}
if result == nil {
ui.Error("XAPI did not reply with an instance reference")
return multistep.ActionHalt
}
instance := xscommon.VM(*result)
/*
err = instance.SetStaticMemoryRange(config.VMMemory*1024*1024, config.VMMemory*1024*1024)
if err != nil {
ui.Error(fmt.Sprintf("Error setting VM memory=%d: %s", config.VMMemory*1024*1024, err.Error()))
return multistep.ActionHalt
}
instance.SetPlatform(config.PlatformArgs)
if err != nil {
ui.Error(fmt.Sprintf("Error setting VM platform: %s", err.Error()))
return multistep.ActionHalt
}
// Connect Network
var network *xscommon.Network
if config.NetworkName == "" {
// No network has be specified. Use the management interface
network = new(xscommon.Network)
network.Ref = ""
network.Client = &client
pifs, err := client.GetPIFs()
if err != nil {
ui.Error(fmt.Sprintf("Error getting PIFs: %s", err.Error()))
return multistep.ActionHalt
}
for _, pif := range pifs {
pif_rec, err := pif.GetRecord()
if err != nil {
ui.Error(fmt.Sprintf("Error getting PIF record: %s", err.Error()))
return multistep.ActionHalt
}
if pif_rec["management"].(bool) {
network.Ref = pif_rec["network"].(string)
}
}
if network.Ref == "" {
ui.Error("Error: couldn't find management network. Aborting.")
return multistep.ActionHalt
}
} else {
// Look up the network by it's name label
networks, err := client.GetNetworkByNameLabel(config.NetworkName)
if err != nil {
ui.Error(fmt.Sprintf("Error occured getting Network by name-label: %s", err.Error()))
return multistep.ActionHalt
}
switch {
case len(networks) == 0:
ui.Error(fmt.Sprintf("Couldn't find a network with the specified name-label '%s'. Aborting.", config.NetworkName))
return multistep.ActionHalt
case len(networks) > 1:
ui.Error(fmt.Sprintf("Found more than one network with the name '%s'. The name must be unique. Aborting.", config.NetworkName))
return multistep.ActionHalt
}
network = networks[0]
}
if err != nil {
ui.Say(err.Error())
}
_, err = instance.ConnectNetwork(network, "0")
if err != nil {
ui.Say(err.Error())
}
*/
instanceId, err := instance.GetUuid()
if err != nil {
ui.Error(fmt.Sprintf("Unable to get VM UUID: %s", err.Error()))
return multistep.ActionHalt
}
state.Put("instance_uuid", instanceId)
ui.Say(fmt.Sprintf("Imported instance '%s'", instanceId))
return multistep.ActionContinue
}
func (self *stepImportInstance) Cleanup(state multistep.StateBag) {
/*
config := state.Get("config").(config)
if config.ShouldKeepVM(state) {
return
}
ui := state.Get("ui").(packer.Ui)
if self.instance != nil {
ui.Say("Destroying VM")
_ = self.instance.HardShutdown() // redundant, just in case
err := self.instance.Destroy()
if err != nil {
ui.Error(err.Error())
}
}
if self.vdi != nil {
ui.Say("Destroying VDI")
err := self.vdi.Destroy()
if err != nil {
ui.Error(err.Error())
}
}
*/
}

View File

@ -0,0 +1,279 @@
---
layout: "docs"
page_title: "XenServer Builder (from an ISO)"
description: |-
The XenServer Packer builder is able to create XenServer virtual machines and export them either as an XVA or a VDI, starting from an ISO image.
---
# XenServer Builder (from an ISO)
Type: `xenserver-iso`
The XenServer Packer builder is able to create [XenServer](https://www.xenserver.org/)
virtual machines and export them either as an XVA or a VDI, starting from an
ISO image.
The builder builds a virtual machine by creating a new virtual machine
from scratch, booting it, installing an OS, provisioning software within
the OS, then shutting it down. The result of the XenServer builder is a
directory containing all the files necessary to run the virtual machine
portably.
## Basic Example
Here is a basic example. This example is not functional. Even when the
`remote_*` fields have been completed, it will start the OS installer but then
fail because we don't provide the preseed file for Ubuntu to self-install.
Still, the example serves to show the basic configuration:
```javascript
{
"type": "xenserver-iso",
"remote_host": "your-server.example.com",
"remote_username": "root",
"remote_password": "password",
"iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.5-server-amd64.iso",
"iso_checksum": "769474248a3897f4865817446f9a4a53",
"iso_checksum_type": "md5",
"ssh_username": "packer",
"ssh_password": "packer",
"ssh_wait_timeout": "30s",
"shutdown_command": "echo 'packer' | sudo -S shutdown -P now"
}
```
It is important to add a `shutdown_command`. By default Packer forcibly halts the
virtual machine and the file system may not be sync'd. Thus, changes made in a
provisioner might not be saved.
## Configuration Reference
There are many configuration options available for the XenServer 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 "none", "md5", "sha1", "sha256", or
"sha512" currently. While "none" will skip checksumming, this is not
recommended since ISO files are generally large and corruption does happen
from time to time.
* `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.
* `remote_host` (string) - The host of the remote machine.
* `remote_username` (string) - The XenServer username used to access the remote machine.
* `remote_password` (string) - The XenServer password for access to the remote machine.
* `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.
* `clone_template` (string) - The template to clone. Defaults to "Other install
media", this is "other", but you can get _dramatic_ performance improvements
by setting this to the proper value. To view all available values for this
run `xe template-list`. Setting the correct value hints to XenServer how to
optimize the virtual hardware to work best with that operating system.
* `disk_size` (integer) - The size, in megabytes, of the hard disk to create
for the VM. By default, this is 40000 (about 40 GB).
* `floppy_files` (array of strings) - A list of files to place onto a floppy
disk that is attached when the VM is booted. 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. All files
listed in this setting get placed into the root directory of the floppy
and the floppy is attached as the first floppy device. Currently, no
support exists for creating sub-directories on the floppy. Wildcard
characters (\*, ?, and []) are allowed. Directory names are also allowed,
which will add all the files found in the directory to the floppy.
* `format` (string) - Either "xva" or "vdi_raw", this specifies the output
format of the exported virtual machine. This defaults to "xva". Set to
"vdi_raw" to export just the raw disk image.
* `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` (integer) - 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.
* `install_timeout` (string) - The amount of time to wait after booting the VM
for the installer to shut itself down.
If it doesn't shut down in this time, it is an error. By default, the timeout
is "200m", or over three hours.
* `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.
* `platform_args` (object of key/value strings) - The platform args.
Defaults to
```javascript
{
"viridian": "false",
"nx": "true",
"pae": "true",
"apic": "true",
"timeoffset": "0",
"acpi": "1"
}
```
* `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` (integer) - 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` (integer) - The port that SSH will be listening on in the guest
virtual machine. By default this is 22.
* `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.
* `tools_iso_name` (string) - The name of the XenServer Tools ISO. Defaults to
"xs-tools.iso".
* `vm_name` (string) - This is the name of the new virtual
machine, without the file extension. By default this is
"packer-BUILDNAME-TIMESTAMP", where "BUILDNAME" is the name of the build.
* `vm_memory` (integer) - The size, in megabytes, of the amount of memory to
allocate for the VM. By default, this is 1024 (1 GB).
## Differences with other Packer builders
Currently the XenServer builder has some quirks when compared with other Packer builders.
The builder currently only works remotely.
The installer is expected to shut down the VM to indicate that it has completed. This is in contrast to other builders, which instead detect completion by a successful SSH connection. The reason for this difference is that currently the builder has no way of knowing what the IP address of the VM is without starting it on the HIMN.
## 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:
* `<bs>` - Backspace
* `<del>` - Delete
* `<enter>` and `<return>` - Simulates an actual "enter" or "return" keypress.
* `<esc>` - Simulates pressing the escape key.
* `<tab>` - Simulates pressing the tab key.
* `<f1>` - `<f12>` - Simulates pressing a function key.
* `<up>` `<down>` `<left>` `<right>` - Simulates pressing an arrow key.
* `<spacebar>` - Simulates pressing the spacebar.
* `<insert>` - Simulates pressing the insert key.
* `<home>` `<end>` - Simulates pressing the home and end keys.
* `<pageUp>` `<pageDown>` - Simulates pressing the page up and page down keys.
* `<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 Ubuntu 12.04 installer:
```javascript
[
"&lt;esc&gt;&lt;esc&gt;&lt;enter&gt;&lt;wait&gt;",
"/install/vmlinuz noapic ",
"preseed/url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg ",
"debian-installer=en_US auto locale=en_US kbd-chooser/method=us ",
"hostname={{ .Name }} ",
"fb=false debconf/frontend=noninteractive ",
"keyboard-configuration/modelcode=SKIP keyboard-configuration/layout=USA ",
"keyboard-configuration/variant=USA console-setup/ask_detect=false ",
"initrd=/install/initrd.gz -- &lt;enter&gt;"
]
```

View File

@ -1,23 +0,0 @@
"builders": [{
"type": "xenserver",
"username": "root",
"password": "hostpassword",
"host_ip": "10.81.2.105",
"vm_name": "packer-centos-6-4",
"vm_memory": 2048,
"disk_size": 40000,
"iso_name": "CentOS-6.4-x86_64-minimal.iso",
"http_directory": "http",
"local_ip": "10.80.3.223",
"install_timeout": "600s",
"ssh_username": "root",
"ssh_password": "vmpassword",
"boot_command":
[
"<tab><wait>",
" ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/centos6-ks.cfg<enter>"
],
"keep_instance": "always"
}]
}

30
examples/centos-6.6.json Normal file
View File

@ -0,0 +1,30 @@
{
"builders": [
{
"type": "xenserver-iso",
"remote_host": "your-server.example.com",
"remote_username": "root",
"remote_password": "password",
"boot_command": [
"<tab> text ks=http://{{ .HTTPIP }}:{{ .HTTPPort }}/centos6-ks.cfg<enter><wait>"
],
"boot_wait": "10s",
"disk_size": 40960,
"http_directory": "http",
"iso_checksum": "4ed6c56d365bd3ab12cd88b8a480f4a62e7c66d2",
"iso_checksum_type": "sha1",
"iso_url": "{{user `mirror`}}/6.6/isos/x86_64/CentOS-6.6-x86_64-minimal.iso",
"output_directory": "packer-centos-6.6-x86_64-xenserver",
"shutdown_command": "/sbin/halt",
"ssh_username": "root",
"ssh_password": "vmpassword",
"ssh_wait_timeout": "10000s",
"vm_name": "packer-centos-6.6-x86_64"
}
],
"variables": {
"mirror": "http://www.mirrorservice.org/sites/mirror.centos.org"
}
}

View File

@ -2,7 +2,7 @@ package main
import ( import (
"github.com/mitchellh/packer/packer/plugin" "github.com/mitchellh/packer/packer/plugin"
"github.com/rdobson/packer-builder-xenserver/builder/xenserver" "github.com/rdobson/packer-builder-xenserver/builder/xenserver/iso"
) )
func main() { func main() {
@ -10,6 +10,6 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
server.RegisterBuilder(new(xenserver.Builder)) server.RegisterBuilder(new(iso.Builder))
server.Serve() server.Serve()
} }

View File

@ -0,0 +1,15 @@
package main
import (
"github.com/mitchellh/packer/packer/plugin"
"github.com/rdobson/packer-builder-xenserver/builder/xenserver/xva"
)
func main() {
server, err := plugin.Server()
if err != nil {
panic(err)
}
server.RegisterBuilder(new(xva.Builder))
server.Serve()
}