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