diff --git a/builder/xenserver/builder.go b/builder/xenserver/builder.go index 0b51192..9574caa 100644 --- a/builder/xenserver/builder.go +++ b/builder/xenserver/builder.go @@ -9,6 +9,8 @@ import ( "github.com/mitchellh/packer/packer" "log" "os" + "path" + "strings" "time" ) @@ -21,15 +23,14 @@ type config struct { Username string `mapstructure:"username"` Password string `mapstructure:"password"` HostIp string `mapstructure:"host_ip"` - IsoUrl string `mapstructure:"iso_url"` - InstanceName string `mapstructure:"instance_name"` - InstanceMemory string `mapstructure:"instance_memory"` - RootDiskSize string `mapstructure:"root_disk_size"` - CloneTemplate string `mapstructure:"clone_template"` - IsoName string `mapstructure:"iso_name"` - SrName string `mapstructure:"sr_name"` - NetworkName string `mapstructure:"network_name"` + 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"` @@ -45,6 +46,8 @@ type config struct { 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"` @@ -111,6 +114,10 @@ func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error self.config.RawInstallTimeout = "200m" } + if self.config.ToolsIsoName == "" { + self.config.ToolsIsoName = "xs-tools.iso" + } + if self.config.HTTPPortMin == 0 { self.config.HTTPPortMin = 8000 } @@ -123,14 +130,22 @@ func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error self.config.RawSSHWaitTimeout = "200m" } - if self.config.InstanceMemory == "" { - self.config.InstanceMemory = "1024000000" + 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) } @@ -160,18 +175,16 @@ func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error "username": &self.config.Username, "password": &self.config.Password, "host_ip": &self.config.HostIp, - "iso_url": &self.config.IsoUrl, - "instance_name": &self.config.InstanceName, - "instance_memory": &self.config.InstanceMemory, - "root_disk_size": &self.config.RootDiskSize, + "vm_name": &self.config.VMName, "clone_template": &self.config.CloneTemplate, - "iso_name": &self.config.IsoName, "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, @@ -184,6 +197,13 @@ func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error "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) @@ -194,18 +214,6 @@ func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error // Validation - /* - if self.config.IsoUrl == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("a iso url must be specified")) - } - */ - - if self.config.IsoName == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("an iso_name must be specified")) - } - self.config.BootWait, err = time.ParseDuration(self.config.RawBootWait) if err != nil { errs = packer.MultiErrorAppend( @@ -261,14 +269,9 @@ func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error errs, errors.New("An ip for the xenserver host must be specified.")) } - if self.config.InstanceName == "" { + if self.config.VMName == "" { errs = packer.MultiErrorAppend( - errs, errors.New("An instance name must be specified.")) - } - - if self.config.RootDiskSize == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("A root disk size must be specified.")) + errs, errors.New("vm_name must be specified.")) } switch self.config.ExportFormat { @@ -301,43 +304,48 @@ func (self *Builder) Prepare(raws ...interface{}) (params []string, retErr error 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 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 self.config.ISOUrl == "" { - errs = packer.MultiErrorAppend( - errs, errors.New("A ISO URL must be specfied.")) - } else { - self.config.ISOUrls = []string{self.config.ISOUrl} - } + } + } + + 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)) + } + } - 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 the iso_url (%d): %s", i, err)) - } - } - */ if len(errs.Errors) > 0 { retErr = errors.New(errs.Error()) } @@ -368,19 +376,52 @@ func (self *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (pa //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, - }, - */ + &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), - //new(stepUploadIso), + &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{ @@ -393,6 +434,12 @@ func (self *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (pa new(stepBootWait), new(stepTypeBootCommand), new(stepWait), + &stepDetachVdi{ + VdiUuidKey: "floppy_vdi_uuid", + }, + &stepDetachVdi{ + VdiUuidKey: "iso_vdi_uuid", + }, new(stepRemoveDevices), new(stepStartOnHIMN), &stepForwardPortOverSSH{ diff --git a/builder/xenserver/client.go b/builder/xenserver/client.go index 96dea6f..9fdffcd 100644 --- a/builder/xenserver/client.go +++ b/builder/xenserver/client.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/nilshell/xmlrpc" + "log" ) type XenAPIClient struct { @@ -36,6 +37,15 @@ type VDI struct { Client *XenAPIClient } +type VDIType int + +const ( + _ = iota + Disk + CD + Floppy +) + type Network struct { Ref string Client *XenAPIClient @@ -61,6 +71,22 @@ type Pool struct { Client *XenAPIClient } +type Task struct { + Ref string + Client *XenAPIClient +} + +type TaskStatusType int + +const ( + _ = iota + Pending + Success + Failure + Cancelling + Cancelled +) + func (c *XenAPIClient) RPCCall(result interface{}, method string, params []interface{}) (err error) { fmt.Println(params) p := new(xmlrpc.Params) @@ -305,6 +331,20 @@ func (client *XenAPIClient) GetPIFs() (pifs []*PIF, err error) { return pifs, nil } +func (client *XenAPIClient) CreateTask() (task *Task, err error) { + result := APIResult{} + err = client.APICall(&result, "task.create", "packer-task", "Packer task") + + if err != nil { + return + } + + task = new(Task) + task.Ref = result.Value.(string) + task.Client = client + return +} + // VM associated functions func (self *VM) Clone(label string) (new_instance *VM, err error) { @@ -497,16 +537,18 @@ func (self *VM) GetGuestMetrics() (metrics map[string]interface{}, err error) { return metrics, nil } -func (self *VM) SetStaticMemoryRange(min, max string) (err error) { +func (self *VM) SetStaticMemoryRange(min, max uint) (err error) { result := APIResult{} - err = self.Client.APICall(&result, "VM.set_memory_limits", self.Ref, min, max, min, max) + strMin := fmt.Sprintf("%d", min) + strMax := fmt.Sprintf("%d", max) + err = self.Client.APICall(&result, "VM.set_memory_limits", self.Ref, strMin, strMax, strMin, strMax) if err != nil { return err } return } -func (self *VM) ConnectVdi(vdi *VDI, iso bool) (err error) { +func (self *VM) ConnectVdi(vdi *VDI, vdiType VDIType) (err error) { // 1. Create a VBD @@ -514,20 +556,27 @@ func (self *VM) ConnectVdi(vdi *VDI, iso bool) (err error) { vbd_rec["VM"] = self.Ref vbd_rec["VDI"] = vdi.Ref vbd_rec["userdevice"] = "autodetect" - vbd_rec["unpluggable"] = false vbd_rec["empty"] = false vbd_rec["other_config"] = make(xmlrpc.Struct) vbd_rec["qos_algorithm_type"] = "" vbd_rec["qos_algorithm_params"] = make(xmlrpc.Struct) - if iso { + switch vdiType { + case CD: vbd_rec["mode"] = "RO" vbd_rec["bootable"] = true + vbd_rec["unpluggable"] = false vbd_rec["type"] = "CD" - } else { + case Disk: vbd_rec["mode"] = "RW" vbd_rec["bootable"] = false + vbd_rec["unpluggable"] = false vbd_rec["type"] = "Disk" + case Floppy: + vbd_rec["mode"] = "RW" + vbd_rec["bootable"] = false + vbd_rec["unpluggable"] = true + vbd_rec["type"] = "Floppy" } result := APIResult{} @@ -557,6 +606,36 @@ func (self *VM) ConnectVdi(vdi *VDI, iso bool) (err error) { return } +func (self *VM) DisconnectVdi(vdi *VDI) error { + vbds, err := self.GetVBDs() + if err != nil { + return fmt.Errorf("Unable to get VM VBDs: %s", err.Error()) + } + + for _, vbd := range vbds { + rec, err := vbd.GetRecord() + if err != nil { + return fmt.Errorf("Could not get record for VBD '%s': %s", vbd.Ref, err.Error()) + } + + if recVdi, ok := rec["VDI"].(string); ok { + if recVdi == vdi.Ref { + _ = vbd.Unplug() + err = vbd.Destroy() + if err != nil { + return fmt.Errorf("Could not destroy VBD '%s': %s", vbd.Ref, err.Error()) + } + + return nil + } + } else { + log.Printf("Could not find VDI record in VBD '%s'", vbd.Ref) + } + } + + return fmt.Errorf("Could not find VBD for VDI '%s'", vdi.Ref) +} + func (self *VM) SetPlatform(params map[string]string) (err error) { result := APIResult{} platform_rec := make(xmlrpc.Struct) @@ -612,13 +691,13 @@ func (self *VM) SetIsATemplate(is_a_template bool) (err error) { // SR associated functions -func (self *SR) CreateVdi(name_label, size string) (vdi *VDI, err error) { +func (self *SR) CreateVdi(name_label string, size int64) (vdi *VDI, err error) { vdi = new(VDI) vdi_rec := make(xmlrpc.Struct) vdi_rec["name_label"] = name_label vdi_rec["SR"] = self.Ref - vdi_rec["virtual_size"] = size + vdi_rec["virtual_size"] = fmt.Sprintf("%d", size) vdi_rec["type"] = "user" vdi_rec["sharable"] = false vdi_rec["read_only"] = false @@ -720,6 +799,24 @@ func (self *VBD) Eject() (err error) { return nil } +func (self *VBD) Unplug() (err error) { + result := APIResult{} + err = self.Client.APICall(&result, "VBD.unplug", self.Ref) + if err != nil { + return err + } + return nil +} + +func (self *VBD) Destroy() (err error) { + result := APIResult{} + err = self.Client.APICall(&result, "VBD.destroy", self.Ref) + if err != nil { + return err + } + return nil +} + // VIF associated functions func (self *VIF) Destroy() (err error) { @@ -743,6 +840,23 @@ func (self *VDI) GetUuid() (vdi_uuid string, err error) { return vdi_uuid, nil } +func (self *VDI) GetVBDs() (vbds []VBD, err error) { + vbds = make([]VBD, 0) + result := APIResult{} + err = self.Client.APICall(&result, "VDI.get_VBDs", self.Ref) + if err != nil { + return vbds, err + } + for _, elem := range result.Value.([]interface{}) { + vbd := VBD{} + vbd.Ref = elem.(string) + vbd.Client = self.Client + vbds = append(vbds, vbd) + } + + return vbds, nil +} + func (self *VDI) Destroy() (err error) { result := APIResult{} err = self.Client.APICall(&result, "VDI.destroy", self.Ref) @@ -752,6 +866,61 @@ func (self *VDI) Destroy() (err error) { return } +// Task associated functions + +func (self *Task) GetStatus() (status TaskStatusType, err error) { + result := APIResult{} + err = self.Client.APICall(&result, "task.get_status", self.Ref) + if err != nil { + return + } + rawStatus := result.Value.(string) + switch rawStatus { + case "pending": + status = Pending + case "success": + status = Success + case "failure": + status = Failure + case "cancelling": + status = Cancelling + case "cancelled": + status = Cancelled + default: + panic(fmt.Sprintf("Task.get_status: Unknown status '%s'", rawStatus)) + } + return +} + +func (self *Task) GetProgress() (progress float64, err error) { + result := APIResult{} + err = self.Client.APICall(&result, "task.get_progress", self.Ref) + if err != nil { + return + } + progress = result.Value.(float64) + return +} + +func (self *Task) GetErrorInfo() (errorInfo []string, err error) { + result := APIResult{} + err = self.Client.APICall(&result, "task.get_error_info", self.Ref) + if err != nil { + return + } + errorInfo = make([]string, 0) + for _, infoRaw := range result.Value.([]interface{}) { + errorInfo = append(errorInfo, infoRaw.(string)) + } + return +} + +func (self *Task) Destroy() (err error) { + result := APIResult{} + err = self.Client.APICall(&result, "task.destroy", self.Ref) + return +} + // Client Initiator func NewXenAPIClient(host, username, password string) (client XenAPIClient) { diff --git a/builder/xenserver/step_attach_vdi.go b/builder/xenserver/step_attach_vdi.go new file mode 100644 index 0000000..a43e8a7 --- /dev/null +++ b/builder/xenserver/step_attach_vdi.go @@ -0,0 +1,80 @@ +package xenserver + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type stepAttachVdi struct { + VdiUuidKey string + VdiType VDIType + + vdi *VDI +} + +func (self *stepAttachVdi) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + client := state.Get("client").(XenAPIClient) + + var vdiUuid string + if vdiUuidRaw, ok := state.GetOk(self.VdiUuidKey); ok { + vdiUuid = vdiUuidRaw.(string) + } else { + log.Printf("Skipping attach of '%s'", self.VdiUuidKey) + return multistep.ActionContinue + } + + var err error + self.vdi, err = client.GetVdiByUuid(vdiUuid) + if err != nil { + ui.Error(fmt.Sprintf("Unable to get VDI from UUID '%s': %s", vdiUuid, err.Error())) + return multistep.ActionHalt + } + + uuid := state.Get("instance_uuid").(string) + instance, err := client.GetVMByUuid(uuid) + if err != nil { + ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error())) + return multistep.ActionHalt + } + + err = instance.ConnectVdi(self.vdi, self.VdiType) + if err != nil { + ui.Error(fmt.Sprintf("Error attaching VDI '%s': '%s'", vdiUuid, err.Error())) + return multistep.ActionHalt + } + + log.Printf("Attached VDI '%s'", vdiUuid) + + return multistep.ActionContinue +} + +func (self *stepAttachVdi) Cleanup(state multistep.StateBag) { + config := state.Get("config").(config) + client := state.Get("client").(XenAPIClient) + if config.ShouldKeepInstance(state) { + return + } + + if self.vdi == nil { + return + } + + uuid := state.Get("instance_uuid").(string) + instance, err := client.GetVMByUuid(uuid) + if err != nil { + log.Printf("Unable to get VM from UUID '%s': %s", uuid, err.Error()) + return + } + + vdiUuid := state.Get(self.VdiUuidKey).(string) + + err = instance.DisconnectVdi(self.vdi) + if err != nil { + log.Printf("Unable to disconnect VDI '%s': %s", vdiUuid, err.Error()) + return + } + log.Printf("Detached VDI '%s'", vdiUuid) +} diff --git a/builder/xenserver/step_create_instance.go b/builder/xenserver/step_create_instance.go index fcb800c..927bca5 100644 --- a/builder/xenserver/step_create_instance.go +++ b/builder/xenserver/step_create_instance.go @@ -35,7 +35,7 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi template := vms[0] // Clone that VM template - instance, err := template.Clone(config.InstanceName) + instance, err := template.Clone(config.VMName) if err != nil { ui.Error(fmt.Sprintf("Error cloning VM: %s", err.Error())) return multistep.ActionHalt @@ -48,9 +48,9 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi return multistep.ActionHalt } - err = instance.SetStaticMemoryRange(config.InstanceMemory, config.InstanceMemory) + err = instance.SetStaticMemoryRange(config.VMMemory*1024*1024, config.VMMemory*1024*1024) if err != nil { - ui.Error(fmt.Sprintf("Error setting VM memory=%s: %s", config.InstanceMemory, err.Error())) + ui.Error(fmt.Sprintf("Error setting VM memory=%d: %s", config.VMMemory*1024*1024, err.Error())) return multistep.ActionHalt } @@ -68,14 +68,14 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi return multistep.ActionHalt } - vdi, err := sr.CreateVdi("Packer-disk", config.RootDiskSize) + vdi, err := sr.CreateVdi("Packer-disk", int64(config.DiskSize*1024*1024)) if err != nil { ui.Error(fmt.Sprintf("Unable to create packer disk VDI: %s", err.Error())) return multistep.ActionHalt } self.vdi = vdi - err = instance.ConnectVdi(vdi, false) + err = instance.ConnectVdi(vdi, Disk) if err != nil { ui.Error(fmt.Sprintf("Unable to connect packer disk VDI: %s", err.Error())) return multistep.ActionHalt @@ -132,7 +132,7 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi 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 SR with the name '%s'. The name must be unique. Aborting.", config.NetworkName)) + ui.Error(fmt.Sprintf("Found more than one network with the name '%s'. The name must be unique. Aborting.", config.NetworkName)) return multistep.ActionHalt } @@ -148,32 +148,6 @@ func (self *stepCreateInstance) Run(state multistep.StateBag) multistep.StepActi ui.Say(err.Error()) } - // Connect the ISO - //iso_vdi_uuid := state.Get("iso_vdi_uuid").(string) - - isos, err := client.GetVdiByNameLabel(config.IsoName) - - switch { - case len(isos) == 0: - ui.Error(fmt.Sprintf("Couldn't find an ISO named '%s'. Aborting", config.IsoName)) - return multistep.ActionHalt - case len(isos) > 1: - ui.Error(fmt.Sprintf("Found more than one VDI with name '%s'. Name must be unique. Aborting.", config.IsoName)) - return multistep.ActionHalt - } - - iso := isos[0] - - //iso, _ := client.GetVdiByUuid(config.IsoUuid) - //ui.Say("Using VDI: " + iso_vdi_uuid) - //iso, _ := client.GetVdiByUuid(iso_vdi_uuid) - - err = instance.ConnectVdi(iso, true) - if err != nil { - ui.Error(fmt.Sprintf("Unable to connect ISO VDI: %s", err.Error())) - return multistep.ActionHalt - } - instanceId, err := instance.GetUuid() if err != nil { ui.Error(fmt.Sprintf("Unable to get VM UUID: %s", err.Error())) @@ -196,7 +170,7 @@ func (self *stepCreateInstance) Cleanup(state multistep.StateBag) { if self.instance != nil { ui.Say("Destroying VM") - _ = self.instance.HardShutdown() + _ = self.instance.HardShutdown() // redundant, just in case err := self.instance.Destroy() if err != nil { ui.Error(err.Error()) diff --git a/builder/xenserver/step_detach_vdi.go b/builder/xenserver/step_detach_vdi.go new file mode 100644 index 0000000..63f9477 --- /dev/null +++ b/builder/xenserver/step_detach_vdi.go @@ -0,0 +1,50 @@ +package xenserver + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" + "log" +) + +type stepDetachVdi struct { + VdiUuidKey string +} + +func (self *stepDetachVdi) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + client := state.Get("client").(XenAPIClient) + + var vdiUuid string + if vdiUuidRaw, ok := state.GetOk(self.VdiUuidKey); ok { + vdiUuid = vdiUuidRaw.(string) + } else { + log.Printf("Skipping detach of '%s'", self.VdiUuidKey) + return multistep.ActionContinue + } + + vdi, err := client.GetVdiByUuid(vdiUuid) + if err != nil { + ui.Error(fmt.Sprintf("Unable to get VDI from UUID '%s': %s", vdiUuid, err.Error())) + return multistep.ActionHalt + } + + uuid := state.Get("instance_uuid").(string) + instance, err := client.GetVMByUuid(uuid) + if err != nil { + ui.Error(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error())) + return multistep.ActionHalt + } + + err = instance.DisconnectVdi(vdi) + if err != nil { + ui.Error(fmt.Sprintf("Unable to detach VDI '%s': %s", vdiUuid, err.Error())) + return multistep.ActionHalt + } + + log.Printf("Detached VDI '%s'", vdiUuid) + + return multistep.ActionContinue +} + +func (self *stepDetachVdi) Cleanup(state multistep.StateBag) {} diff --git a/builder/xenserver/step_find_vdi.go b/builder/xenserver/step_find_vdi.go new file mode 100644 index 0000000..7799887 --- /dev/null +++ b/builder/xenserver/step_find_vdi.go @@ -0,0 +1,42 @@ +package xenserver + +import ( + "fmt" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/packer" +) + +type stepFindVdi struct { + VdiName string + ImagePathFunc func() string + VdiUuidKey string +} + +func (self *stepFindVdi) Run(state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + client := state.Get("client").(XenAPIClient) + + vdis, err := client.GetVdiByNameLabel(self.VdiName) + + switch { + case len(vdis) == 0: + ui.Error(fmt.Sprintf("Couldn't find a VDI named '%s'", self.VdiName)) + return multistep.ActionHalt + case len(vdis) > 1: + ui.Error(fmt.Sprintf("Found more than one VDI with name '%s'. Name must be unique", self.VdiName)) + return multistep.ActionHalt + } + + vdi := vdis[0] + + 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) + + return multistep.ActionContinue +} + +func (self *stepFindVdi) Cleanup(state multistep.StateBag) {} diff --git a/builder/xenserver/step_remove_devices.go b/builder/xenserver/step_remove_devices.go index dd97e3c..cec045e 100644 --- a/builder/xenserver/step_remove_devices.go +++ b/builder/xenserver/step_remove_devices.go @@ -22,34 +22,6 @@ func (self *stepRemoveDevices) Run(state multistep.StateBag) multistep.StepActio return multistep.ActionHalt } - // Eject ISO from drive - vbds, err := instance.GetVBDs() - if err != nil { - ui.Error(fmt.Sprintf("Could not get VBDs")) - ui.Error(err.Error()) - return multistep.ActionHalt - } - for _, vbd := range vbds { - rec, err := vbd.GetRecord() - if err != nil { - ui.Error(fmt.Sprintf("Could not get record for VBD")) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Hack - should encapsulate this in the client really - // This is needed because we can't guarentee the type - // returned by the xmlrpc lib will be string - if recType, ok := rec["type"].(string); ok { - if recType == "CD" { - ui.Say("Ejecting CD...") - vbd.Eject() - } - } else { - break - } - } - // Destroy all connected VIFs vifs, err := instance.GetVIFs() if err != nil { diff --git a/builder/xenserver/step_shutdown_and_export.go b/builder/xenserver/step_shutdown_and_export.go index 79f1448..7dcd89f 100644 --- a/builder/xenserver/step_shutdown_and_export.go +++ b/builder/xenserver/step_shutdown_and_export.go @@ -119,7 +119,7 @@ func (stepShutdownAndExport) Run(state multistep.StateBag) multistep.StepAction client.Session.(string), ) - export_filename := fmt.Sprintf("%s/%s.xva", config.OutputDir, config.InstanceName) + export_filename := fmt.Sprintf("%s/%s.xva", config.OutputDir, config.VMName) ui.Say("Getting XVA " + export_url) err = downloadFile(export_url, export_filename) diff --git a/builder/xenserver/step_start_vm_paused.go b/builder/xenserver/step_start_vm_paused.go index 8a0e246..31b15d5 100644 --- a/builder/xenserver/step_start_vm_paused.go +++ b/builder/xenserver/step_start_vm_paused.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/mitchellh/multistep" "github.com/mitchellh/packer/packer" + "log" ) type stepStartVmPaused struct{} @@ -39,4 +40,22 @@ func (self *stepStartVmPaused) Run(state multistep.StateBag) multistep.StepActio } func (self *stepStartVmPaused) Cleanup(state multistep.StateBag) { + config := state.Get("config").(config) + client := state.Get("client").(XenAPIClient) + + if config.ShouldKeepInstance(state) { + return + } + + uuid := state.Get("instance_uuid").(string) + instance, err := client.GetVMByUuid(uuid) + if err != nil { + log.Printf(fmt.Sprintf("Unable to get VM from UUID '%s': %s", uuid, err.Error())) + return + } + + err = instance.HardShutdown() + if err != nil { + log.Printf(fmt.Sprintf("Unable to force shutdown VM '%s': %s", uuid, err.Error())) + } } diff --git a/builder/xenserver/step_type_boot_command.go b/builder/xenserver/step_type_boot_command.go index 1211e50..28b5f63 100644 --- a/builder/xenserver/step_type_boot_command.go +++ b/builder/xenserver/step_type_boot_command.go @@ -60,7 +60,7 @@ func (self *stepTypeBootCommand) Run(state multistep.StateBag) multistep.StepAct // @todo - include http port/ip so kickstarter files can be grabbed tplData := &bootCommandTemplateData{ - config.InstanceName, + config.VMName, config.LocalIp, http_port, } diff --git a/builder/xenserver/step_upload_iso.go b/builder/xenserver/step_upload_iso.go deleted file mode 100644 index b4dcaa5..0000000 --- a/builder/xenserver/step_upload_iso.go +++ /dev/null @@ -1,78 +0,0 @@ -package xenserver - -import ( - "github.com/mitchellh/multistep" - "github.com/mitchellh/packer/packer" - "log" - "os" - "os/exec" - "strconv" -) - -type stepUploadIso struct{} - -func (self *stepUploadIso) Run(state multistep.StateBag) multistep.StepAction { - - client := state.Get("client").(XenAPIClient) - config := state.Get("config").(config) - ui := state.Get("ui").(packer.Ui) - - ui.Say("Step: Upload ISO to server") - iso_path := state.Get("iso_path").(string) - - // Determine the ISO's filesize - file, err := os.Open(iso_path) - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } - stat, err := file.Stat() - - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } - - iso_filesize := stat.Size() - - // Create a VDI with the write size - srs, err := client.GetSRByNameLabel(config.SrName) - - sr := srs[0] - - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } - - filesize_str := strconv.FormatInt(iso_filesize, 10) - log.Printf("Filesize of the ISO is %d", filesize_str) - vdi, err := sr.CreateVdi("Packer Gen "+stat.Name(), filesize_str) - - if err != nil { - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Upload the ISO to this VDI - vdi_uuid, _ := vdi.GetUuid() - host_url := "https://" + client.Host - log.Printf("Host URL: %s", host_url) - ui.Say("Uploading ISO to " + vdi_uuid) - out, err := exec.Command("/usr/bin/importdisk.py", host_url, client.Username, client.Password, vdi_uuid, iso_path).CombinedOutput() - log.Printf("Output: %s", out) - - if err != nil { - log.Printf("%s", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - // Stash the vdi uuid to be used in preference - state.Put("iso_vdi_uuid", vdi_uuid) - - return multistep.ActionContinue -} - -func (self *stepUploadIso) Cleanup(state multistep.StateBag) { -} diff --git a/builder/xenserver/step_upload_vdi.go b/builder/xenserver/step_upload_vdi.go new file mode 100644 index 0000000..fe3e57f --- /dev/null +++ b/builder/xenserver/step_upload_vdi.go @@ -0,0 +1,201 @@ +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, "") +} diff --git a/examples/centos-6.4.conf b/examples/centos-6.4.conf index 18af4cb..2adfc77 100644 --- a/examples/centos-6.4.conf +++ b/examples/centos-6.4.conf @@ -4,9 +4,9 @@ "username": "root", "password": "hostpassword", "host_ip": "10.81.2.105", - "instance_name": "packer-centos-6-4", - "instance_memory": "2048000000", - "root_disk_size": "40000000000", + "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",