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