github.com/emate/packer@v0.8.1-0.20150625195101-fe0fde195dc6/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 "strings" 12 "time" 13 14 "github.com/mitchellh/multistep" 15 "github.com/mitchellh/packer/common" 16 "github.com/mitchellh/packer/helper/communicator" 17 "github.com/mitchellh/packer/helper/config" 18 "github.com/mitchellh/packer/packer" 19 "github.com/mitchellh/packer/template/interpolate" 20 ) 21 22 const BuilderId = "transcend.qemu" 23 24 var accels = map[string]struct{}{ 25 "none": struct{}{}, 26 "kvm": struct{}{}, 27 "tcg": struct{}{}, 28 "xen": struct{}{}, 29 } 30 31 var netDevice = map[string]bool{ 32 "ne2k_pci": true, 33 "i82551": true, 34 "i82557b": true, 35 "i82559er": true, 36 "rtl8139": true, 37 "e1000": true, 38 "pcnet": true, 39 "virtio": true, 40 "virtio-net": true, 41 "virtio-net-pci": true, 42 "usb-net": true, 43 "i82559a": true, 44 "i82559b": true, 45 "i82559c": true, 46 "i82550": true, 47 "i82562": true, 48 "i82557a": true, 49 "i82557c": true, 50 "i82801": true, 51 "vmxnet3": true, 52 "i82558a": true, 53 "i82558b": true, 54 } 55 56 var diskInterface = map[string]bool{ 57 "ide": true, 58 "scsi": true, 59 "virtio": 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 Comm communicator.Config `mapstructure:",squash"` 83 84 Accelerator string `mapstructure:"accelerator"` 85 BootCommand []string `mapstructure:"boot_command"` 86 DiskInterface string `mapstructure:"disk_interface"` 87 DiskSize uint `mapstructure:"disk_size"` 88 DiskCache string `mapstructure:"disk_cache"` 89 DiskDiscard string `mapstructure:"disk_discard"` 90 FloppyFiles []string `mapstructure:"floppy_files"` 91 Format string `mapstructure:"format"` 92 Headless bool `mapstructure:"headless"` 93 DiskImage bool `mapstructure:"disk_image"` 94 HTTPDir string `mapstructure:"http_directory"` 95 HTTPPortMin uint `mapstructure:"http_port_min"` 96 HTTPPortMax uint `mapstructure:"http_port_max"` 97 ISOChecksum string `mapstructure:"iso_checksum"` 98 ISOChecksumType string `mapstructure:"iso_checksum_type"` 99 ISOUrls []string `mapstructure:"iso_urls"` 100 MachineType string `mapstructure:"machine_type"` 101 NetDevice string `mapstructure:"net_device"` 102 OutputDir string `mapstructure:"output_directory"` 103 QemuArgs [][]string `mapstructure:"qemuargs"` 104 QemuBinary string `mapstructure:"qemu_binary"` 105 ShutdownCommand string `mapstructure:"shutdown_command"` 106 SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` 107 SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` 108 VNCPortMin uint `mapstructure:"vnc_port_min"` 109 VNCPortMax uint `mapstructure:"vnc_port_max"` 110 VMName string `mapstructure:"vm_name"` 111 112 // These are deprecated, but we keep them around for BC 113 // TODO(@mitchellh): remove 114 SSHKeyPath string `mapstructure:"ssh_key_path"` 115 SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"` 116 117 // TODO(mitchellh): deprecate 118 RunOnce bool `mapstructure:"run_once"` 119 120 RawBootWait string `mapstructure:"boot_wait"` 121 RawSingleISOUrl string `mapstructure:"iso_url"` 122 RawShutdownTimeout string `mapstructure:"shutdown_timeout"` 123 124 bootWait time.Duration `` 125 shutdownTimeout time.Duration `` 126 ctx interpolate.Context 127 } 128 129 func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 130 err := config.Decode(&b.config, &config.DecodeOpts{ 131 Interpolate: true, 132 InterpolateContext: &b.config.ctx, 133 InterpolateFilter: &interpolate.RenderFilter{ 134 Exclude: []string{ 135 "boot_command", 136 "qemuargs", 137 }, 138 }, 139 }, raws...) 140 if err != nil { 141 return nil, err 142 } 143 144 if b.config.DiskSize == 0 { 145 b.config.DiskSize = 40000 146 } 147 148 if b.config.DiskCache == "" { 149 b.config.DiskCache = "writeback" 150 } 151 152 if b.config.DiskDiscard == "" { 153 b.config.DiskDiscard = "ignore" 154 } 155 156 if b.config.Accelerator == "" { 157 if runtime.GOOS == "windows" { 158 b.config.Accelerator = "tcg" 159 } else { 160 b.config.Accelerator = "kvm" 161 } 162 } 163 164 if b.config.HTTPPortMin == 0 { 165 b.config.HTTPPortMin = 8000 166 } 167 168 if b.config.HTTPPortMax == 0 { 169 b.config.HTTPPortMax = 9000 170 } 171 172 if b.config.MachineType == "" { 173 b.config.MachineType = "pc" 174 } 175 176 if b.config.OutputDir == "" { 177 b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) 178 } 179 180 if b.config.QemuBinary == "" { 181 b.config.QemuBinary = "qemu-system-x86_64" 182 } 183 184 if b.config.RawBootWait == "" { 185 b.config.RawBootWait = "10s" 186 } 187 188 if b.config.SSHHostPortMin == 0 { 189 b.config.SSHHostPortMin = 2222 190 } 191 192 if b.config.SSHHostPortMax == 0 { 193 b.config.SSHHostPortMax = 4444 194 } 195 196 if b.config.VNCPortMin == 0 { 197 b.config.VNCPortMin = 5900 198 } 199 200 if b.config.VNCPortMax == 0 { 201 b.config.VNCPortMax = 6000 202 } 203 204 if b.config.VMName == "" { 205 b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) 206 } 207 208 if b.config.Format == "" { 209 b.config.Format = "qcow2" 210 } 211 212 if b.config.FloppyFiles == nil { 213 b.config.FloppyFiles = make([]string, 0) 214 } 215 216 if b.config.NetDevice == "" { 217 b.config.NetDevice = "virtio-net" 218 } 219 220 if b.config.DiskInterface == "" { 221 b.config.DiskInterface = "virtio" 222 } 223 224 // TODO: backwards compatibility, write fixer instead 225 if b.config.SSHKeyPath != "" { 226 b.config.Comm.SSHPrivateKey = b.config.SSHKeyPath 227 } 228 if b.config.SSHWaitTimeout != 0 { 229 b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout 230 } 231 232 var errs *packer.MultiError 233 warnings := make([]string, 0) 234 235 if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 { 236 errs = packer.MultiErrorAppend(errs, es...) 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 _, 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.HTTPPortMin > b.config.HTTPPortMax { 270 errs = packer.MultiErrorAppend( 271 errs, errors.New("http_port_min must be less than http_port_max")) 272 } 273 274 if b.config.ISOChecksumType == "" { 275 errs = packer.MultiErrorAppend( 276 errs, errors.New("The iso_checksum_type must be specified.")) 277 } else { 278 b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType) 279 if b.config.ISOChecksumType != "none" { 280 if b.config.ISOChecksum == "" { 281 errs = packer.MultiErrorAppend( 282 errs, errors.New("Due to large file sizes, an iso_checksum is required")) 283 } else { 284 b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum) 285 } 286 287 if h := common.HashForType(b.config.ISOChecksumType); h == nil { 288 errs = packer.MultiErrorAppend( 289 errs, 290 fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType)) 291 } 292 } 293 } 294 295 if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 { 296 errs = packer.MultiErrorAppend( 297 errs, errors.New("One of iso_url or iso_urls must be specified.")) 298 } else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 { 299 errs = packer.MultiErrorAppend( 300 errs, errors.New("Only one of iso_url or iso_urls may be specified.")) 301 } else if b.config.RawSingleISOUrl != "" { 302 b.config.ISOUrls = []string{b.config.RawSingleISOUrl} 303 } 304 305 for i, url := range b.config.ISOUrls { 306 b.config.ISOUrls[i], err = common.DownloadableURL(url) 307 if err != nil { 308 errs = packer.MultiErrorAppend( 309 errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err)) 310 } 311 } 312 313 if !b.config.PackerForce { 314 if _, err := os.Stat(b.config.OutputDir); err == nil { 315 errs = packer.MultiErrorAppend( 316 errs, 317 fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir)) 318 } 319 } 320 321 b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait) 322 if err != nil { 323 errs = packer.MultiErrorAppend( 324 errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) 325 } 326 327 if b.config.RawShutdownTimeout == "" { 328 b.config.RawShutdownTimeout = "5m" 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.SSHHostPortMin > b.config.SSHHostPortMax { 338 errs = packer.MultiErrorAppend( 339 errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) 340 } 341 342 if b.config.VNCPortMin > b.config.VNCPortMax { 343 errs = packer.MultiErrorAppend( 344 errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) 345 } 346 347 if b.config.QemuArgs == nil { 348 b.config.QemuArgs = make([][]string, 0) 349 } 350 351 if b.config.ISOChecksumType == "none" { 352 warnings = append(warnings, 353 "A checksum type of 'none' was specified. Since ISO files are so big,\n"+ 354 "a checksum is highly recommended.") 355 } 356 357 if errs != nil && len(errs.Errors) > 0 { 358 return warnings, errs 359 } 360 361 return warnings, nil 362 } 363 364 func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { 365 // Create the driver that we'll use to communicate with Qemu 366 driver, err := b.newDriver(b.config.QemuBinary) 367 if err != nil { 368 return nil, fmt.Errorf("Failed creating Qemu driver: %s", err) 369 } 370 371 steprun := &stepRun{} 372 if !b.config.DiskImage { 373 steprun.BootDrive = "once=d" 374 steprun.Message = "Starting VM, booting from CD-ROM" 375 } else { 376 steprun.BootDrive = "c" 377 steprun.Message = "Starting VM, booting disk image" 378 } 379 380 steps := []multistep.Step{ 381 &common.StepDownload{ 382 Checksum: b.config.ISOChecksum, 383 ChecksumType: b.config.ISOChecksumType, 384 Description: "ISO", 385 ResultKey: "iso_path", 386 Url: b.config.ISOUrls, 387 }, 388 new(stepPrepareOutputDir), 389 &common.StepCreateFloppy{ 390 Files: b.config.FloppyFiles, 391 }, 392 new(stepCreateDisk), 393 new(stepCopyDisk), 394 new(stepResizeDisk), 395 new(stepHTTPServer), 396 new(stepForwardSSH), 397 new(stepConfigureVNC), 398 steprun, 399 &stepBootWait{}, 400 &stepTypeBootCommand{}, 401 &communicator.StepConnect{ 402 Config: &b.config.Comm, 403 Host: commHost, 404 SSHConfig: sshConfig, 405 SSHPort: commPort, 406 }, 407 new(common.StepProvision), 408 new(stepShutdown), 409 } 410 411 // Setup the state bag 412 state := new(multistep.BasicStateBag) 413 state.Put("cache", cache) 414 state.Put("config", &b.config) 415 state.Put("driver", driver) 416 state.Put("hook", hook) 417 state.Put("ui", ui) 418 419 // Run 420 if b.config.PackerDebug { 421 b.runner = &multistep.DebugRunner{ 422 Steps: steps, 423 PauseFn: common.MultistepDebugFn(ui), 424 } 425 } else { 426 b.runner = &multistep.BasicRunner{Steps: steps} 427 } 428 429 b.runner.Run(state) 430 431 // If there was an error, return that 432 if rawErr, ok := state.GetOk("error"); ok { 433 return nil, rawErr.(error) 434 } 435 436 // If we were interrupted or cancelled, then just exit. 437 if _, ok := state.GetOk(multistep.StateCancelled); ok { 438 return nil, errors.New("Build was cancelled.") 439 } 440 441 if _, ok := state.GetOk(multistep.StateHalted); ok { 442 return nil, errors.New("Build was halted.") 443 } 444 445 // Compile the artifact list 446 files := make([]string, 0, 5) 447 visit := func(path string, info os.FileInfo, err error) error { 448 if !info.IsDir() { 449 files = append(files, path) 450 } 451 452 return err 453 } 454 455 if err := filepath.Walk(b.config.OutputDir, visit); err != nil { 456 return nil, err 457 } 458 459 artifact := &Artifact{ 460 dir: b.config.OutputDir, 461 f: files, 462 state: make(map[string]interface{}), 463 } 464 465 artifact.state["diskName"] = state.Get("disk_filename").(string) 466 artifact.state["diskType"] = b.config.Format 467 artifact.state["diskSize"] = uint64(b.config.DiskSize) 468 artifact.state["domainType"] = b.config.Accelerator 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 }