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