github.com/rothwerx/packer@v0.9.0/builder/qemu/builder.go (about) 1 package qemu 2 3 import ( 4 "errors" 5 "fmt" 6 "log" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "runtime" 11 "time" 12 13 "github.com/mitchellh/multistep" 14 "github.com/mitchellh/packer/common" 15 "github.com/mitchellh/packer/helper/communicator" 16 "github.com/mitchellh/packer/helper/config" 17 "github.com/mitchellh/packer/packer" 18 "github.com/mitchellh/packer/template/interpolate" 19 ) 20 21 const BuilderId = "transcend.qemu" 22 23 var accels = map[string]struct{}{ 24 "none": struct{}{}, 25 "kvm": struct{}{}, 26 "tcg": struct{}{}, 27 "xen": struct{}{}, 28 } 29 30 var netDevice = map[string]bool{ 31 "ne2k_pci": true, 32 "i82551": true, 33 "i82557b": true, 34 "i82559er": true, 35 "rtl8139": true, 36 "e1000": true, 37 "pcnet": true, 38 "virtio": true, 39 "virtio-net": true, 40 "virtio-net-pci": true, 41 "usb-net": true, 42 "i82559a": true, 43 "i82559b": true, 44 "i82559c": true, 45 "i82550": true, 46 "i82562": true, 47 "i82557a": true, 48 "i82557c": true, 49 "i82801": true, 50 "vmxnet3": true, 51 "i82558a": true, 52 "i82558b": true, 53 } 54 55 var diskInterface = map[string]bool{ 56 "ide": true, 57 "scsi": true, 58 "virtio": true, 59 "virtio-scsi": true, 60 } 61 62 var diskCache = map[string]bool{ 63 "writethrough": true, 64 "writeback": true, 65 "none": true, 66 "unsafe": true, 67 "directsync": true, 68 } 69 70 var diskDiscard = map[string]bool{ 71 "unmap": true, 72 "ignore": true, 73 } 74 75 type Builder struct { 76 config Config 77 runner multistep.Runner 78 } 79 80 type Config struct { 81 common.PackerConfig `mapstructure:",squash"` 82 common.HTTPConfig `mapstructure:",squash"` 83 common.ISOConfig `mapstructure:",squash"` 84 Comm communicator.Config `mapstructure:",squash"` 85 86 ISOSkipCache bool `mapstructure:"iso_skip_cache"` 87 Accelerator string `mapstructure:"accelerator"` 88 BootCommand []string `mapstructure:"boot_command"` 89 DiskInterface string `mapstructure:"disk_interface"` 90 DiskSize uint `mapstructure:"disk_size"` 91 DiskCache string `mapstructure:"disk_cache"` 92 DiskDiscard string `mapstructure:"disk_discard"` 93 SkipCompaction bool `mapstructure:"skip_compaction"` 94 DiskCompression bool `mapstructure:"disk_compression"` 95 FloppyFiles []string `mapstructure:"floppy_files"` 96 Format string `mapstructure:"format"` 97 Headless bool `mapstructure:"headless"` 98 DiskImage bool `mapstructure:"disk_image"` 99 MachineType string `mapstructure:"machine_type"` 100 NetDevice string `mapstructure:"net_device"` 101 OutputDir string `mapstructure:"output_directory"` 102 QemuArgs [][]string `mapstructure:"qemuargs"` 103 QemuBinary string `mapstructure:"qemu_binary"` 104 ShutdownCommand string `mapstructure:"shutdown_command"` 105 SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` 106 SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` 107 VNCPortMin uint `mapstructure:"vnc_port_min"` 108 VNCPortMax uint `mapstructure:"vnc_port_max"` 109 VMName string `mapstructure:"vm_name"` 110 111 // These are deprecated, but we keep them around for BC 112 // TODO(@mitchellh): remove 113 SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"` 114 115 // TODO(mitchellh): deprecate 116 RunOnce bool `mapstructure:"run_once"` 117 118 RawBootWait string `mapstructure:"boot_wait"` 119 RawShutdownTimeout string `mapstructure:"shutdown_timeout"` 120 121 bootWait time.Duration `` 122 shutdownTimeout time.Duration `` 123 ctx interpolate.Context 124 } 125 126 func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 127 err := config.Decode(&b.config, &config.DecodeOpts{ 128 Interpolate: true, 129 InterpolateContext: &b.config.ctx, 130 InterpolateFilter: &interpolate.RenderFilter{ 131 Exclude: []string{ 132 "boot_command", 133 "qemuargs", 134 }, 135 }, 136 }, raws...) 137 if err != nil { 138 return nil, err 139 } 140 141 if b.config.DiskSize == 0 { 142 b.config.DiskSize = 40000 143 } 144 145 if b.config.DiskCache == "" { 146 b.config.DiskCache = "writeback" 147 } 148 149 if b.config.DiskDiscard == "" { 150 b.config.DiskDiscard = "ignore" 151 } 152 153 if b.config.Accelerator == "" { 154 if runtime.GOOS == "windows" { 155 b.config.Accelerator = "tcg" 156 } else { 157 b.config.Accelerator = "kvm" 158 } 159 } 160 161 if b.config.MachineType == "" { 162 b.config.MachineType = "pc" 163 } 164 165 if b.config.OutputDir == "" { 166 b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) 167 } 168 169 if b.config.QemuBinary == "" { 170 b.config.QemuBinary = "qemu-system-x86_64" 171 } 172 173 if b.config.RawBootWait == "" { 174 b.config.RawBootWait = "10s" 175 } 176 177 if b.config.SSHHostPortMin == 0 { 178 b.config.SSHHostPortMin = 2222 179 } 180 181 if b.config.SSHHostPortMax == 0 { 182 b.config.SSHHostPortMax = 4444 183 } 184 185 if b.config.VNCPortMin == 0 { 186 b.config.VNCPortMin = 5900 187 } 188 189 if b.config.VNCPortMax == 0 { 190 b.config.VNCPortMax = 6000 191 } 192 193 if b.config.VMName == "" { 194 b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) 195 } 196 197 if b.config.Format == "" { 198 b.config.Format = "qcow2" 199 } 200 201 if b.config.FloppyFiles == nil { 202 b.config.FloppyFiles = make([]string, 0) 203 } 204 205 if b.config.NetDevice == "" { 206 b.config.NetDevice = "virtio-net" 207 } 208 209 if b.config.DiskInterface == "" { 210 b.config.DiskInterface = "virtio" 211 } 212 213 // TODO: backwards compatibility, write fixer instead 214 if b.config.SSHWaitTimeout != 0 { 215 b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout 216 } 217 218 var errs *packer.MultiError 219 warnings := make([]string, 0) 220 221 if b.config.ISOSkipCache { 222 b.config.ISOChecksumType = "none" 223 } 224 225 isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx) 226 warnings = append(warnings, isoWarnings...) 227 errs = packer.MultiErrorAppend(errs, isoErrs...) 228 229 errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...) 230 if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 { 231 errs = packer.MultiErrorAppend(errs, es...) 232 } 233 234 if !(b.config.Format == "qcow2" || b.config.Format == "raw") { 235 errs = packer.MultiErrorAppend( 236 errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed")) 237 } 238 239 if b.config.Format != "qcow2" { 240 b.config.SkipCompaction = true 241 b.config.DiskCompression = false 242 } 243 244 if _, ok := accels[b.config.Accelerator]; !ok { 245 errs = packer.MultiErrorAppend( 246 errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', or 'none' are allowed")) 247 } 248 249 if _, ok := netDevice[b.config.NetDevice]; !ok { 250 errs = packer.MultiErrorAppend( 251 errs, errors.New("unrecognized network device type")) 252 } 253 254 if _, ok := diskInterface[b.config.DiskInterface]; !ok { 255 errs = packer.MultiErrorAppend( 256 errs, errors.New("unrecognized disk interface type")) 257 } 258 259 if _, ok := diskCache[b.config.DiskCache]; !ok { 260 errs = packer.MultiErrorAppend( 261 errs, errors.New("unrecognized disk cache type")) 262 } 263 264 if _, ok := diskDiscard[b.config.DiskDiscard]; !ok { 265 errs = packer.MultiErrorAppend( 266 errs, errors.New("unrecognized disk cache type")) 267 } 268 269 if !b.config.PackerForce { 270 if _, err := os.Stat(b.config.OutputDir); err == nil { 271 errs = packer.MultiErrorAppend( 272 errs, 273 fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir)) 274 } 275 } 276 277 b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait) 278 if err != nil { 279 errs = packer.MultiErrorAppend( 280 errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) 281 } 282 283 if b.config.RawShutdownTimeout == "" { 284 b.config.RawShutdownTimeout = "5m" 285 } 286 287 b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout) 288 if err != nil { 289 errs = packer.MultiErrorAppend( 290 errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) 291 } 292 293 if b.config.SSHHostPortMin > b.config.SSHHostPortMax { 294 errs = packer.MultiErrorAppend( 295 errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) 296 } 297 298 if b.config.VNCPortMin > b.config.VNCPortMax { 299 errs = packer.MultiErrorAppend( 300 errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) 301 } 302 303 if b.config.QemuArgs == nil { 304 b.config.QemuArgs = make([][]string, 0) 305 } 306 307 if errs != nil && len(errs.Errors) > 0 { 308 return warnings, errs 309 } 310 311 return warnings, nil 312 } 313 314 func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { 315 // Create the driver that we'll use to communicate with Qemu 316 driver, err := b.newDriver(b.config.QemuBinary) 317 if err != nil { 318 return nil, fmt.Errorf("Failed creating Qemu driver: %s", err) 319 } 320 321 steprun := &stepRun{} 322 if !b.config.DiskImage { 323 steprun.BootDrive = "once=d" 324 steprun.Message = "Starting VM, booting from CD-ROM" 325 } else { 326 steprun.BootDrive = "c" 327 steprun.Message = "Starting VM, booting disk image" 328 } 329 330 steps := []multistep.Step{} 331 if !b.config.ISOSkipCache { 332 steps = append(steps, &common.StepDownload{ 333 Checksum: b.config.ISOChecksum, 334 ChecksumType: b.config.ISOChecksumType, 335 Description: "ISO", 336 Extension: "iso", 337 ResultKey: "iso_path", 338 TargetPath: b.config.TargetPath, 339 Url: b.config.ISOUrls, 340 }, 341 ) 342 } else { 343 steps = append(steps, &stepSetISO{ 344 ResultKey: "iso_path", 345 Url: b.config.ISOUrls, 346 }, 347 ) 348 } 349 350 steps = append(steps, new(stepPrepareOutputDir), 351 &common.StepCreateFloppy{ 352 Files: b.config.FloppyFiles, 353 }, 354 new(stepCreateDisk), 355 new(stepCopyDisk), 356 new(stepResizeDisk), 357 &common.StepHTTPServer{ 358 HTTPDir: b.config.HTTPDir, 359 HTTPPortMin: b.config.HTTPPortMin, 360 HTTPPortMax: b.config.HTTPPortMax, 361 }, 362 new(stepForwardSSH), 363 new(stepConfigureVNC), 364 steprun, 365 &stepBootWait{}, 366 &stepTypeBootCommand{}, 367 &communicator.StepConnect{ 368 Config: &b.config.Comm, 369 Host: commHost, 370 SSHConfig: sshConfig, 371 SSHPort: commPort, 372 }, 373 new(common.StepProvision), 374 new(stepShutdown), 375 new(stepConvertDisk), 376 ) 377 378 // Setup the state bag 379 state := new(multistep.BasicStateBag) 380 state.Put("cache", cache) 381 state.Put("config", &b.config) 382 state.Put("driver", driver) 383 state.Put("hook", hook) 384 state.Put("ui", ui) 385 386 // Run 387 if b.config.PackerDebug { 388 b.runner = &multistep.DebugRunner{ 389 Steps: steps, 390 PauseFn: common.MultistepDebugFn(ui), 391 } 392 } else { 393 b.runner = &multistep.BasicRunner{Steps: steps} 394 } 395 396 b.runner.Run(state) 397 398 // If there was an error, return that 399 if rawErr, ok := state.GetOk("error"); ok { 400 return nil, rawErr.(error) 401 } 402 403 // If we were interrupted or cancelled, then just exit. 404 if _, ok := state.GetOk(multistep.StateCancelled); ok { 405 return nil, errors.New("Build was cancelled.") 406 } 407 408 if _, ok := state.GetOk(multistep.StateHalted); ok { 409 return nil, errors.New("Build was halted.") 410 } 411 412 // Compile the artifact list 413 files := make([]string, 0, 5) 414 visit := func(path string, info os.FileInfo, err error) error { 415 if !info.IsDir() { 416 files = append(files, path) 417 } 418 419 return err 420 } 421 422 if err := filepath.Walk(b.config.OutputDir, visit); err != nil { 423 return nil, err 424 } 425 426 artifact := &Artifact{ 427 dir: b.config.OutputDir, 428 f: files, 429 state: make(map[string]interface{}), 430 } 431 432 artifact.state["diskName"] = state.Get("disk_filename").(string) 433 artifact.state["diskType"] = b.config.Format 434 artifact.state["diskSize"] = uint64(b.config.DiskSize) 435 artifact.state["domainType"] = b.config.Accelerator 436 437 return artifact, nil 438 } 439 440 func (b *Builder) Cancel() { 441 if b.runner != nil { 442 log.Println("Cancelling the step runner...") 443 b.runner.Cancel() 444 } 445 } 446 447 func (b *Builder) newDriver(qemuBinary string) (Driver, error) { 448 qemuPath, err := exec.LookPath(qemuBinary) 449 if err != nil { 450 return nil, err 451 } 452 453 qemuImgPath, err := exec.LookPath("qemu-img") 454 if err != nil { 455 return nil, err 456 } 457 458 log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath) 459 driver := &QemuDriver{ 460 QemuPath: qemuPath, 461 QemuImgPath: qemuImgPath, 462 } 463 464 if err := driver.Verify(); err != nil { 465 return nil, err 466 } 467 468 return driver, nil 469 }