github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/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 "time" 12 13 "github.com/mitchellh/multistep" 14 "github.com/mitchellh/packer/common" 15 "github.com/mitchellh/packer/helper/communicator" 16 "github.com/mitchellh/packer/helper/config" 17 "github.com/mitchellh/packer/packer" 18 "github.com/mitchellh/packer/template/interpolate" 19 ) 20 21 const BuilderId = "transcend.qemu" 22 23 var accels = map[string]struct{}{ 24 "none": {}, 25 "kvm": {}, 26 "tcg": {}, 27 "xen": {}, 28 } 29 30 var netDevice = map[string]bool{ 31 "ne2k_pci": true, 32 "i82551": true, 33 "i82557b": true, 34 "i82559er": true, 35 "rtl8139": true, 36 "e1000": true, 37 "pcnet": true, 38 "virtio": true, 39 "virtio-net": true, 40 "virtio-net-pci": true, 41 "usb-net": true, 42 "i82559a": true, 43 "i82559b": true, 44 "i82559c": true, 45 "i82550": true, 46 "i82562": true, 47 "i82557a": true, 48 "i82557c": true, 49 "i82801": true, 50 "vmxnet3": true, 51 "i82558a": true, 52 "i82558b": true, 53 } 54 55 var diskInterface = map[string]bool{ 56 "ide": true, 57 "scsi": true, 58 "virtio": true, 59 "virtio-scsi": 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 common.HTTPConfig `mapstructure:",squash"` 83 common.ISOConfig `mapstructure:",squash"` 84 Comm communicator.Config `mapstructure:",squash"` 85 common.FloppyConfig `mapstructure:",squash"` 86 87 ISOSkipCache bool `mapstructure:"iso_skip_cache"` 88 Accelerator string `mapstructure:"accelerator"` 89 BootCommand []string `mapstructure:"boot_command"` 90 DiskInterface string `mapstructure:"disk_interface"` 91 DiskSize uint `mapstructure:"disk_size"` 92 DiskCache string `mapstructure:"disk_cache"` 93 DiskDiscard string `mapstructure:"disk_discard"` 94 SkipCompaction bool `mapstructure:"skip_compaction"` 95 DiskCompression bool `mapstructure:"disk_compression"` 96 Format string `mapstructure:"format"` 97 Headless bool `mapstructure:"headless"` 98 DiskImage bool `mapstructure:"disk_image"` 99 MachineType string `mapstructure:"machine_type"` 100 NetDevice string `mapstructure:"net_device"` 101 OutputDir string `mapstructure:"output_directory"` 102 QemuArgs [][]string `mapstructure:"qemuargs"` 103 QemuBinary string `mapstructure:"qemu_binary"` 104 ShutdownCommand string `mapstructure:"shutdown_command"` 105 SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` 106 SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` 107 VNCBindAddress string `mapstructure:"vnc_bind_address"` 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 SSHWaitTimeout time.Duration `mapstructure:"ssh_wait_timeout"` 115 116 // TODO(mitchellh): deprecate 117 RunOnce bool `mapstructure:"run_once"` 118 119 RawBootWait string `mapstructure:"boot_wait"` 120 RawShutdownTimeout string `mapstructure:"shutdown_timeout"` 121 122 bootWait time.Duration `` 123 shutdownTimeout time.Duration `` 124 ctx interpolate.Context 125 } 126 127 func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 128 err := config.Decode(&b.config, &config.DecodeOpts{ 129 Interpolate: true, 130 InterpolateContext: &b.config.ctx, 131 InterpolateFilter: &interpolate.RenderFilter{ 132 Exclude: []string{ 133 "boot_command", 134 "qemuargs", 135 }, 136 }, 137 }, raws...) 138 if err != nil { 139 return nil, err 140 } 141 142 var errs *packer.MultiError 143 warnings := make([]string, 0) 144 145 if b.config.DiskSize == 0 { 146 b.config.DiskSize = 40000 147 } 148 149 if b.config.DiskCache == "" { 150 b.config.DiskCache = "writeback" 151 } 152 153 if b.config.DiskDiscard == "" { 154 b.config.DiskDiscard = "ignore" 155 } 156 157 if b.config.Accelerator == "" { 158 if runtime.GOOS == "windows" { 159 b.config.Accelerator = "tcg" 160 } else { 161 // /dev/kvm is a kernel module that may be loaded if kvm is 162 // installed and the host supports VT-x extensions. To make sure 163 // this will actually work we need to os.Open() it. If os.Open fails 164 // the kernel module was not installed or loaded correctly. 165 if fp, err := os.Open("/dev/kvm"); err != nil { 166 b.config.Accelerator = "tcg" 167 } else { 168 fp.Close() 169 b.config.Accelerator = "kvm" 170 } 171 } 172 log.Printf("use detected accelerator: %s", b.config.Accelerator) 173 } else { 174 log.Printf("use specified accelerator: %s", b.config.Accelerator) 175 } 176 177 if b.config.MachineType == "" { 178 b.config.MachineType = "pc" 179 } 180 181 if b.config.OutputDir == "" { 182 b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) 183 } 184 185 if b.config.QemuBinary == "" { 186 b.config.QemuBinary = "qemu-system-x86_64" 187 } 188 189 if b.config.RawBootWait == "" { 190 b.config.RawBootWait = "10s" 191 } 192 193 if b.config.SSHHostPortMin == 0 { 194 b.config.SSHHostPortMin = 2222 195 } 196 197 if b.config.SSHHostPortMax == 0 { 198 b.config.SSHHostPortMax = 4444 199 } 200 201 if b.config.VNCBindAddress == "" { 202 b.config.VNCBindAddress = "127.0.0.1" 203 } 204 205 if b.config.VNCPortMin == 0 { 206 b.config.VNCPortMin = 5900 207 } 208 209 if b.config.VNCPortMax == 0 { 210 b.config.VNCPortMax = 6000 211 } 212 213 if b.config.VMName == "" { 214 b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) 215 } 216 217 if b.config.Format == "" { 218 b.config.Format = "qcow2" 219 } 220 221 errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...) 222 223 if b.config.NetDevice == "" { 224 b.config.NetDevice = "virtio-net" 225 } 226 227 if b.config.DiskInterface == "" { 228 b.config.DiskInterface = "virtio" 229 } 230 231 // TODO: backwards compatibility, write fixer instead 232 if b.config.SSHWaitTimeout != 0 { 233 b.config.Comm.SSHTimeout = b.config.SSHWaitTimeout 234 } 235 236 if b.config.ISOSkipCache { 237 b.config.ISOChecksumType = "none" 238 } 239 240 isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx) 241 warnings = append(warnings, isoWarnings...) 242 errs = packer.MultiErrorAppend(errs, isoErrs...) 243 244 errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...) 245 if es := b.config.Comm.Prepare(&b.config.ctx); len(es) > 0 { 246 errs = packer.MultiErrorAppend(errs, es...) 247 } 248 249 if !(b.config.Format == "qcow2" || b.config.Format == "raw") { 250 errs = packer.MultiErrorAppend( 251 errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed")) 252 } 253 254 if b.config.Format != "qcow2" { 255 b.config.SkipCompaction = true 256 b.config.DiskCompression = false 257 } 258 259 if _, ok := accels[b.config.Accelerator]; !ok { 260 errs = packer.MultiErrorAppend( 261 errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', or 'none' are allowed")) 262 } 263 264 if _, ok := netDevice[b.config.NetDevice]; !ok { 265 errs = packer.MultiErrorAppend( 266 errs, errors.New("unrecognized network device type")) 267 } 268 269 if _, ok := diskInterface[b.config.DiskInterface]; !ok { 270 errs = packer.MultiErrorAppend( 271 errs, errors.New("unrecognized disk interface type")) 272 } 273 274 if _, ok := diskCache[b.config.DiskCache]; !ok { 275 errs = packer.MultiErrorAppend( 276 errs, errors.New("unrecognized disk cache type")) 277 } 278 279 if _, ok := diskDiscard[b.config.DiskDiscard]; !ok { 280 errs = packer.MultiErrorAppend( 281 errs, errors.New("unrecognized disk cache type")) 282 } 283 284 if !b.config.PackerForce { 285 if _, err := os.Stat(b.config.OutputDir); err == nil { 286 errs = packer.MultiErrorAppend( 287 errs, 288 fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir)) 289 } 290 } 291 292 b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait) 293 if err != nil { 294 errs = packer.MultiErrorAppend( 295 errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) 296 } 297 298 if b.config.RawShutdownTimeout == "" { 299 b.config.RawShutdownTimeout = "5m" 300 } 301 302 b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout) 303 if err != nil { 304 errs = packer.MultiErrorAppend( 305 errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) 306 } 307 308 if b.config.SSHHostPortMin > b.config.SSHHostPortMax { 309 errs = packer.MultiErrorAppend( 310 errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) 311 } 312 313 if b.config.VNCPortMin > b.config.VNCPortMax { 314 errs = packer.MultiErrorAppend( 315 errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) 316 } 317 318 if b.config.QemuArgs == nil { 319 b.config.QemuArgs = make([][]string, 0) 320 } 321 322 if errs != nil && len(errs.Errors) > 0 { 323 return warnings, errs 324 } 325 326 return warnings, nil 327 } 328 329 func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { 330 // Create the driver that we'll use to communicate with Qemu 331 driver, err := b.newDriver(b.config.QemuBinary) 332 if err != nil { 333 return nil, fmt.Errorf("Failed creating Qemu driver: %s", err) 334 } 335 336 steprun := &stepRun{} 337 if !b.config.DiskImage { 338 steprun.BootDrive = "once=d" 339 steprun.Message = "Starting VM, booting from CD-ROM" 340 } else { 341 steprun.BootDrive = "c" 342 steprun.Message = "Starting VM, booting disk image" 343 } 344 345 steps := []multistep.Step{} 346 if !b.config.ISOSkipCache { 347 steps = append(steps, &common.StepDownload{ 348 Checksum: b.config.ISOChecksum, 349 ChecksumType: b.config.ISOChecksumType, 350 Description: "ISO", 351 Extension: "iso", 352 ResultKey: "iso_path", 353 TargetPath: b.config.TargetPath, 354 Url: b.config.ISOUrls, 355 }, 356 ) 357 } else { 358 steps = append(steps, &stepSetISO{ 359 ResultKey: "iso_path", 360 Url: b.config.ISOUrls, 361 }, 362 ) 363 } 364 365 steps = append(steps, new(stepPrepareOutputDir), 366 &common.StepCreateFloppy{ 367 Files: b.config.FloppyConfig.FloppyFiles, 368 Directories: b.config.FloppyConfig.FloppyDirectories, 369 }, 370 new(stepCreateDisk), 371 new(stepCopyDisk), 372 new(stepResizeDisk), 373 &common.StepHTTPServer{ 374 HTTPDir: b.config.HTTPDir, 375 HTTPPortMin: b.config.HTTPPortMin, 376 HTTPPortMax: b.config.HTTPPortMax, 377 }, 378 ) 379 380 if b.config.Comm.Type != "none" { 381 steps = append(steps, 382 new(stepForwardSSH), 383 ) 384 } 385 386 steps = append(steps, 387 new(stepConfigureVNC), 388 steprun, 389 &stepBootWait{}, 390 &stepTypeBootCommand{}, 391 ) 392 393 if b.config.Comm.Type != "none" { 394 steps = append(steps, 395 &communicator.StepConnect{ 396 Config: &b.config.Comm, 397 Host: commHost, 398 SSHConfig: sshConfig, 399 SSHPort: commPort, 400 }, 401 ) 402 } 403 404 steps = append(steps, 405 new(common.StepProvision), 406 ) 407 steps = append(steps, 408 new(stepShutdown), 409 ) 410 411 steps = append(steps, 412 new(stepConvertDisk), 413 ) 414 415 // Setup the state bag 416 state := new(multistep.BasicStateBag) 417 state.Put("cache", cache) 418 state.Put("config", &b.config) 419 state.Put("debug", b.config.PackerDebug) 420 state.Put("driver", driver) 421 state.Put("hook", hook) 422 state.Put("ui", ui) 423 424 // Run 425 b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state) 426 b.runner.Run(state) 427 428 // If there was an error, return that 429 if rawErr, ok := state.GetOk("error"); ok { 430 return nil, rawErr.(error) 431 } 432 433 // If we were interrupted or cancelled, then just exit. 434 if _, ok := state.GetOk(multistep.StateCancelled); ok { 435 return nil, errors.New("Build was cancelled.") 436 } 437 438 if _, ok := state.GetOk(multistep.StateHalted); ok { 439 return nil, errors.New("Build was halted.") 440 } 441 442 // Compile the artifact list 443 files := make([]string, 0, 5) 444 visit := func(path string, info os.FileInfo, err error) error { 445 if !info.IsDir() { 446 files = append(files, path) 447 } 448 449 return err 450 } 451 452 if err := filepath.Walk(b.config.OutputDir, visit); err != nil { 453 return nil, err 454 } 455 456 artifact := &Artifact{ 457 dir: b.config.OutputDir, 458 f: files, 459 state: make(map[string]interface{}), 460 } 461 462 artifact.state["diskName"] = state.Get("disk_filename").(string) 463 artifact.state["diskType"] = b.config.Format 464 artifact.state["diskSize"] = uint64(b.config.DiskSize) 465 artifact.state["domainType"] = b.config.Accelerator 466 467 return artifact, nil 468 } 469 470 func (b *Builder) Cancel() { 471 if b.runner != nil { 472 log.Println("Cancelling the step runner...") 473 b.runner.Cancel() 474 } 475 } 476 477 func (b *Builder) newDriver(qemuBinary string) (Driver, error) { 478 qemuPath, err := exec.LookPath(qemuBinary) 479 if err != nil { 480 return nil, err 481 } 482 483 qemuImgPath, err := exec.LookPath("qemu-img") 484 if err != nil { 485 return nil, err 486 } 487 488 log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath) 489 driver := &QemuDriver{ 490 QemuPath: qemuPath, 491 QemuImgPath: qemuImgPath, 492 } 493 494 if err := driver.Verify(); err != nil { 495 return nil, err 496 } 497 498 return driver, nil 499 }