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