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