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