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