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