github.com/daniellockard/packer@v0.7.6-0.20141210173435-5a9390934716/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 "strings" 11 "time" 12 13 "github.com/mitchellh/multistep" 14 "github.com/mitchellh/packer/common" 15 commonssh "github.com/mitchellh/packer/common/ssh" 16 "github.com/mitchellh/packer/packer" 17 ) 18 19 const BuilderId = "transcend.qemu" 20 21 var accels = map[string]struct{}{ 22 "none": struct{}{}, 23 "kvm": struct{}{}, 24 "tcg": struct{}{}, 25 "xen": struct{}{}, 26 } 27 28 var netDevice = map[string]bool{ 29 "ne2k_pci": true, 30 "i82551": true, 31 "i82557b": true, 32 "i82559er": true, 33 "rtl8139": true, 34 "e1000": true, 35 "pcnet": true, 36 "virtio": true, 37 "virtio-net": true, 38 "virtio-net-pci": true, 39 "usb-net": true, 40 "i82559a": true, 41 "i82559b": true, 42 "i82559c": true, 43 "i82550": true, 44 "i82562": true, 45 "i82557a": true, 46 "i82557c": true, 47 "i82801": true, 48 "vmxnet3": true, 49 "i82558a": true, 50 "i82558b": true, 51 } 52 53 var diskInterface = map[string]bool{ 54 "ide": true, 55 "scsi": true, 56 "virtio": true, 57 } 58 59 var diskCache = map[string]bool{ 60 "writethrough": true, 61 "writeback": true, 62 "none": true, 63 "unsafe": true, 64 "directsync": true, 65 } 66 67 type Builder struct { 68 config config 69 runner multistep.Runner 70 } 71 72 type config struct { 73 common.PackerConfig `mapstructure:",squash"` 74 75 Accelerator string `mapstructure:"accelerator"` 76 BootCommand []string `mapstructure:"boot_command"` 77 DiskInterface string `mapstructure:"disk_interface"` 78 DiskSize uint `mapstructure:"disk_size"` 79 DiskCache string `mapstructure:"disk_cache"` 80 FloppyFiles []string `mapstructure:"floppy_files"` 81 Format string `mapstructure:"format"` 82 Headless bool `mapstructure:"headless"` 83 DiskImage bool `mapstructure:"disk_image"` 84 HTTPDir string `mapstructure:"http_directory"` 85 HTTPPortMin uint `mapstructure:"http_port_min"` 86 HTTPPortMax uint `mapstructure:"http_port_max"` 87 ISOChecksum string `mapstructure:"iso_checksum"` 88 ISOChecksumType string `mapstructure:"iso_checksum_type"` 89 ISOUrls []string `mapstructure:"iso_urls"` 90 MachineType string `mapstructure:"machine_type"` 91 NetDevice string `mapstructure:"net_device"` 92 OutputDir string `mapstructure:"output_directory"` 93 QemuArgs [][]string `mapstructure:"qemuargs"` 94 QemuBinary string `mapstructure:"qemu_binary"` 95 ShutdownCommand string `mapstructure:"shutdown_command"` 96 SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` 97 SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` 98 SSHPassword string `mapstructure:"ssh_password"` 99 SSHPort uint `mapstructure:"ssh_port"` 100 SSHUser string `mapstructure:"ssh_username"` 101 SSHKeyPath string `mapstructure:"ssh_key_path"` 102 VNCPortMin uint `mapstructure:"vnc_port_min"` 103 VNCPortMax uint `mapstructure:"vnc_port_max"` 104 VMName string `mapstructure:"vm_name"` 105 106 // TODO(mitchellh): deprecate 107 RunOnce bool `mapstructure:"run_once"` 108 109 RawBootWait string `mapstructure:"boot_wait"` 110 RawSingleISOUrl string `mapstructure:"iso_url"` 111 RawShutdownTimeout string `mapstructure:"shutdown_timeout"` 112 RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` 113 114 bootWait time.Duration `` 115 shutdownTimeout time.Duration `` 116 sshWaitTimeout time.Duration `` 117 tpl *packer.ConfigTemplate 118 } 119 120 func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 121 md, err := common.DecodeConfig(&b.config, raws...) 122 if err != nil { 123 return nil, err 124 } 125 warnings := make([]string, 0) 126 127 b.config.tpl, err = packer.NewConfigTemplate() 128 if err != nil { 129 return nil, err 130 } 131 b.config.tpl.UserVars = b.config.PackerUserVars 132 133 // Accumulate any errors 134 errs := common.CheckUnusedConfig(md) 135 136 if b.config.DiskSize == 0 { 137 b.config.DiskSize = 40000 138 } 139 140 if b.config.DiskCache == "" { 141 b.config.DiskCache = "writeback" 142 } 143 144 if b.config.Accelerator == "" { 145 b.config.Accelerator = "kvm" 146 } 147 148 if b.config.HTTPPortMin == 0 { 149 b.config.HTTPPortMin = 8000 150 } 151 152 if b.config.HTTPPortMax == 0 { 153 b.config.HTTPPortMax = 9000 154 } 155 156 if b.config.MachineType == "" { 157 b.config.MachineType = "pc" 158 } 159 160 if b.config.OutputDir == "" { 161 b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) 162 } 163 164 if b.config.QemuBinary == "" { 165 b.config.QemuBinary = "qemu-system-x86_64" 166 } 167 168 if b.config.RawBootWait == "" { 169 b.config.RawBootWait = "10s" 170 } 171 172 if b.config.SSHHostPortMin == 0 { 173 b.config.SSHHostPortMin = 2222 174 } 175 176 if b.config.SSHHostPortMax == 0 { 177 b.config.SSHHostPortMax = 4444 178 } 179 180 if b.config.SSHPort == 0 { 181 b.config.SSHPort = 22 182 } 183 184 if b.config.VNCPortMin == 0 { 185 b.config.VNCPortMin = 5900 186 } 187 188 if b.config.VNCPortMax == 0 { 189 b.config.VNCPortMax = 6000 190 } 191 192 for i, args := range b.config.QemuArgs { 193 for j, arg := range args { 194 if err := b.config.tpl.Validate(arg); err != nil { 195 errs = packer.MultiErrorAppend(errs, 196 fmt.Errorf("Error processing qemu-system_x86-64[%d][%d]: %s", i, j, err)) 197 } 198 } 199 } 200 201 if b.config.VMName == "" { 202 b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) 203 } 204 205 if b.config.Format == "" { 206 b.config.Format = "qcow2" 207 } 208 209 if b.config.FloppyFiles == nil { 210 b.config.FloppyFiles = make([]string, 0) 211 } 212 213 if b.config.NetDevice == "" { 214 b.config.NetDevice = "virtio-net" 215 } 216 217 if b.config.DiskInterface == "" { 218 b.config.DiskInterface = "virtio" 219 } 220 221 // Errors 222 templates := map[string]*string{ 223 "http_directory": &b.config.HTTPDir, 224 "iso_checksum": &b.config.ISOChecksum, 225 "iso_checksum_type": &b.config.ISOChecksumType, 226 "iso_url": &b.config.RawSingleISOUrl, 227 "output_directory": &b.config.OutputDir, 228 "shutdown_command": &b.config.ShutdownCommand, 229 "ssh_key_path": &b.config.SSHKeyPath, 230 "ssh_password": &b.config.SSHPassword, 231 "ssh_username": &b.config.SSHUser, 232 "vm_name": &b.config.VMName, 233 "format": &b.config.Format, 234 "boot_wait": &b.config.RawBootWait, 235 "shutdown_timeout": &b.config.RawShutdownTimeout, 236 "ssh_wait_timeout": &b.config.RawSSHWaitTimeout, 237 "accelerator": &b.config.Accelerator, 238 "machine_type": &b.config.MachineType, 239 "net_device": &b.config.NetDevice, 240 "disk_interface": &b.config.DiskInterface, 241 } 242 243 for n, ptr := range templates { 244 var err error 245 *ptr, err = b.config.tpl.Process(*ptr, nil) 246 if err != nil { 247 errs = packer.MultiErrorAppend( 248 errs, fmt.Errorf("Error processing %s: %s", n, err)) 249 } 250 } 251 252 for i, url := range b.config.ISOUrls { 253 var err error 254 b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil) 255 if err != nil { 256 errs = packer.MultiErrorAppend( 257 errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err)) 258 } 259 } 260 261 for i, command := range b.config.BootCommand { 262 if err := b.config.tpl.Validate(command); err != nil { 263 errs = packer.MultiErrorAppend(errs, 264 fmt.Errorf("Error processing boot_command[%d]: %s", i, err)) 265 } 266 } 267 268 for i, file := range b.config.FloppyFiles { 269 var err error 270 b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil) 271 if err != nil { 272 errs = packer.MultiErrorAppend(errs, 273 fmt.Errorf("Error processing floppy_files[%d]: %s", 274 i, err)) 275 } 276 } 277 278 if !(b.config.Format == "qcow2" || b.config.Format == "raw") { 279 errs = packer.MultiErrorAppend( 280 errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed")) 281 } 282 283 if _, ok := accels[b.config.Accelerator]; !ok { 284 errs = packer.MultiErrorAppend( 285 errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', or 'none' are allowed")) 286 } 287 288 if _, ok := netDevice[b.config.NetDevice]; !ok { 289 errs = packer.MultiErrorAppend( 290 errs, errors.New("unrecognized network device type")) 291 } 292 293 if _, ok := diskInterface[b.config.DiskInterface]; !ok { 294 errs = packer.MultiErrorAppend( 295 errs, errors.New("unrecognized disk interface type")) 296 } 297 298 if _, ok := diskCache[b.config.DiskCache]; !ok { 299 errs = packer.MultiErrorAppend( 300 errs, errors.New("unrecognized disk cache type")) 301 } 302 303 if b.config.HTTPPortMin > b.config.HTTPPortMax { 304 errs = packer.MultiErrorAppend( 305 errs, errors.New("http_port_min must be less than http_port_max")) 306 } 307 308 if b.config.ISOChecksumType == "" { 309 errs = packer.MultiErrorAppend( 310 errs, errors.New("The iso_checksum_type must be specified.")) 311 } else { 312 b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType) 313 if b.config.ISOChecksumType != "none" { 314 if b.config.ISOChecksum == "" { 315 errs = packer.MultiErrorAppend( 316 errs, errors.New("Due to large file sizes, an iso_checksum is required")) 317 } else { 318 b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum) 319 } 320 321 if h := common.HashForType(b.config.ISOChecksumType); h == nil { 322 errs = packer.MultiErrorAppend( 323 errs, 324 fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType)) 325 } 326 } 327 } 328 329 if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 { 330 errs = packer.MultiErrorAppend( 331 errs, errors.New("One of iso_url or iso_urls must be specified.")) 332 } else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 { 333 errs = packer.MultiErrorAppend( 334 errs, errors.New("Only one of iso_url or iso_urls may be specified.")) 335 } else if b.config.RawSingleISOUrl != "" { 336 b.config.ISOUrls = []string{b.config.RawSingleISOUrl} 337 } 338 339 for i, url := range b.config.ISOUrls { 340 b.config.ISOUrls[i], err = common.DownloadableURL(url) 341 if err != nil { 342 errs = packer.MultiErrorAppend( 343 errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err)) 344 } 345 } 346 347 if !b.config.PackerForce { 348 if _, err := os.Stat(b.config.OutputDir); err == nil { 349 errs = packer.MultiErrorAppend( 350 errs, 351 fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir)) 352 } 353 } 354 355 b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait) 356 if err != nil { 357 errs = packer.MultiErrorAppend( 358 errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) 359 } 360 361 if b.config.RawShutdownTimeout == "" { 362 b.config.RawShutdownTimeout = "5m" 363 } 364 365 if b.config.RawSSHWaitTimeout == "" { 366 b.config.RawSSHWaitTimeout = "20m" 367 } 368 369 b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout) 370 if err != nil { 371 errs = packer.MultiErrorAppend( 372 errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) 373 } 374 375 if b.config.SSHKeyPath != "" { 376 if _, err := os.Stat(b.config.SSHKeyPath); err != nil { 377 errs = packer.MultiErrorAppend( 378 errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) 379 } else if _, err := commonssh.FileSigner(b.config.SSHKeyPath); err != nil { 380 errs = packer.MultiErrorAppend( 381 errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) 382 } 383 } 384 385 if b.config.SSHHostPortMin > b.config.SSHHostPortMax { 386 errs = packer.MultiErrorAppend( 387 errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) 388 } 389 390 if b.config.SSHUser == "" { 391 errs = packer.MultiErrorAppend( 392 errs, errors.New("An ssh_username must be specified.")) 393 } 394 395 b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout) 396 if err != nil { 397 errs = packer.MultiErrorAppend( 398 errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err)) 399 } 400 401 if b.config.VNCPortMin > b.config.VNCPortMax { 402 errs = packer.MultiErrorAppend( 403 errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) 404 } 405 406 if b.config.QemuArgs == nil { 407 b.config.QemuArgs = make([][]string, 0) 408 } 409 410 if b.config.ISOChecksumType == "none" { 411 warnings = append(warnings, 412 "A checksum type of 'none' was specified. Since ISO files are so big,\n"+ 413 "a checksum is highly recommended.") 414 } 415 416 if errs != nil && len(errs.Errors) > 0 { 417 return warnings, errs 418 } 419 420 return warnings, nil 421 } 422 423 func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { 424 // Create the driver that we'll use to communicate with Qemu 425 driver, err := b.newDriver(b.config.QemuBinary) 426 if err != nil { 427 return nil, fmt.Errorf("Failed creating Qemu driver: %s", err) 428 } 429 430 steprun := &stepRun{} 431 if !b.config.DiskImage { 432 steprun.BootDrive = "once=d" 433 steprun.Message = "Starting VM, booting from CD-ROM" 434 } else { 435 steprun.BootDrive = "c" 436 steprun.Message = "Starting VM, booting disk image" 437 } 438 439 steps := []multistep.Step{ 440 &common.StepDownload{ 441 Checksum: b.config.ISOChecksum, 442 ChecksumType: b.config.ISOChecksumType, 443 Description: "ISO", 444 ResultKey: "iso_path", 445 Url: b.config.ISOUrls, 446 }, 447 new(stepPrepareOutputDir), 448 &common.StepCreateFloppy{ 449 Files: b.config.FloppyFiles, 450 }, 451 new(stepCreateDisk), 452 new(stepCopyDisk), 453 new(stepResizeDisk), 454 new(stepHTTPServer), 455 new(stepForwardSSH), 456 new(stepConfigureVNC), 457 steprun, 458 &stepBootWait{}, 459 &stepTypeBootCommand{}, 460 &common.StepConnectSSH{ 461 SSHAddress: sshAddress, 462 SSHConfig: sshConfig, 463 SSHWaitTimeout: b.config.sshWaitTimeout, 464 }, 465 new(common.StepProvision), 466 new(stepShutdown), 467 } 468 469 // Setup the state bag 470 state := new(multistep.BasicStateBag) 471 state.Put("cache", cache) 472 state.Put("config", &b.config) 473 state.Put("driver", driver) 474 state.Put("hook", hook) 475 state.Put("ui", ui) 476 477 // Run 478 if b.config.PackerDebug { 479 b.runner = &multistep.DebugRunner{ 480 Steps: steps, 481 PauseFn: common.MultistepDebugFn(ui), 482 } 483 } else { 484 b.runner = &multistep.BasicRunner{Steps: steps} 485 } 486 487 b.runner.Run(state) 488 489 // If there was an error, return that 490 if rawErr, ok := state.GetOk("error"); ok { 491 return nil, rawErr.(error) 492 } 493 494 // If we were interrupted or cancelled, then just exit. 495 if _, ok := state.GetOk(multistep.StateCancelled); ok { 496 return nil, errors.New("Build was cancelled.") 497 } 498 499 if _, ok := state.GetOk(multistep.StateHalted); ok { 500 return nil, errors.New("Build was halted.") 501 } 502 503 // Compile the artifact list 504 files := make([]string, 0, 5) 505 visit := func(path string, info os.FileInfo, err error) error { 506 if !info.IsDir() { 507 files = append(files, path) 508 } 509 510 return err 511 } 512 513 if err := filepath.Walk(b.config.OutputDir, visit); err != nil { 514 return nil, err 515 } 516 517 artifact := &Artifact{ 518 dir: b.config.OutputDir, 519 f: files, 520 state: make(map[string]interface{}), 521 } 522 523 artifact.state["diskName"] = state.Get("disk_filename").(string) 524 artifact.state["diskType"] = b.config.Format 525 artifact.state["diskSize"] = uint64(b.config.DiskSize) 526 artifact.state["domainType"] = b.config.Accelerator 527 528 return artifact, nil 529 } 530 531 func (b *Builder) Cancel() { 532 if b.runner != nil { 533 log.Println("Cancelling the step runner...") 534 b.runner.Cancel() 535 } 536 } 537 538 func (b *Builder) newDriver(qemuBinary string) (Driver, error) { 539 qemuPath, err := exec.LookPath(qemuBinary) 540 if err != nil { 541 return nil, err 542 } 543 544 qemuImgPath, err := exec.LookPath("qemu-img") 545 if err != nil { 546 return nil, err 547 } 548 549 log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath) 550 driver := &QemuDriver{ 551 QemuPath: qemuPath, 552 QemuImgPath: qemuImgPath, 553 } 554 555 if err := driver.Verify(); err != nil { 556 return nil, err 557 } 558 559 return driver, nil 560 }