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