github.com/sneal/packer@v0.5.2/builder/qemu/builder.go (about) 1 package qemu 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/mitchellh/multistep" 7 "github.com/mitchellh/packer/common" 8 "github.com/mitchellh/packer/packer" 9 "log" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strings" 14 "time" 15 ) 16 17 const BuilderId = "transcend.qemu" 18 19 var netDevice = map[string]bool{ 20 "ne2k_pci": true, 21 "i82551": true, 22 "i82557b": true, 23 "i82559er": true, 24 "rtl8139": true, 25 "e1000": true, 26 "pcnet": true, 27 "virtio": true, 28 "virtio-net": true, 29 "usb-net": true, 30 "i82559a": true, 31 "i82559b": true, 32 "i82559c": true, 33 "i82550": true, 34 "i82562": true, 35 "i82557a": true, 36 "i82557c": true, 37 "i82801": true, 38 "vmxnet3": true, 39 "i82558a": true, 40 "i82558b": true, 41 } 42 43 var diskInterface = map[string]bool{ 44 "ide": true, 45 "scsi": true, 46 "virtio": true, 47 } 48 49 type Builder struct { 50 config config 51 runner multistep.Runner 52 } 53 54 type config struct { 55 common.PackerConfig `mapstructure:",squash"` 56 57 Accelerator string `mapstructure:"accelerator"` 58 BootCommand []string `mapstructure:"boot_command"` 59 DiskInterface string `mapstructure:"disk_interface"` 60 DiskSize uint `mapstructure:"disk_size"` 61 FloppyFiles []string `mapstructure:"floppy_files"` 62 Format string `mapstructure:"format"` 63 Headless bool `mapstructure:"headless"` 64 HTTPDir string `mapstructure:"http_directory"` 65 HTTPPortMin uint `mapstructure:"http_port_min"` 66 HTTPPortMax uint `mapstructure:"http_port_max"` 67 ISOChecksum string `mapstructure:"iso_checksum"` 68 ISOChecksumType string `mapstructure:"iso_checksum_type"` 69 ISOUrls []string `mapstructure:"iso_urls"` 70 NetDevice string `mapstructure:"net_device"` 71 OutputDir string `mapstructure:"output_directory"` 72 QemuArgs [][]string `mapstructure:"qemuargs"` 73 QemuBinary string `mapstructure:"qemu_binary"` 74 ShutdownCommand string `mapstructure:"shutdown_command"` 75 SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` 76 SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` 77 SSHPassword string `mapstructure:"ssh_password"` 78 SSHPort uint `mapstructure:"ssh_port"` 79 SSHUser string `mapstructure:"ssh_username"` 80 SSHKeyPath string `mapstructure:"ssh_key_path"` 81 VNCPortMin uint `mapstructure:"vnc_port_min"` 82 VNCPortMax uint `mapstructure:"vnc_port_max"` 83 VMName string `mapstructure:"vm_name"` 84 85 // TODO(mitchellh): deprecate 86 RunOnce bool `mapstructure:"run_once"` 87 88 RawBootWait string `mapstructure:"boot_wait"` 89 RawSingleISOUrl string `mapstructure:"iso_url"` 90 RawShutdownTimeout string `mapstructure:"shutdown_timeout"` 91 RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` 92 93 bootWait time.Duration `` 94 shutdownTimeout time.Duration `` 95 sshWaitTimeout time.Duration `` 96 tpl *packer.ConfigTemplate 97 } 98 99 func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { 100 md, err := common.DecodeConfig(&b.config, raws...) 101 if err != nil { 102 return nil, err 103 } 104 105 b.config.tpl, err = packer.NewConfigTemplate() 106 if err != nil { 107 return nil, err 108 } 109 b.config.tpl.UserVars = b.config.PackerUserVars 110 111 // Accumulate any errors 112 errs := common.CheckUnusedConfig(md) 113 114 if b.config.DiskSize == 0 { 115 b.config.DiskSize = 40000 116 } 117 118 if b.config.Accelerator == "" { 119 b.config.Accelerator = "kvm" 120 } 121 122 if b.config.HTTPPortMin == 0 { 123 b.config.HTTPPortMin = 8000 124 } 125 126 if b.config.HTTPPortMax == 0 { 127 b.config.HTTPPortMax = 9000 128 } 129 130 if b.config.OutputDir == "" { 131 b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) 132 } 133 134 if b.config.QemuBinary == "" { 135 b.config.QemuBinary = "qemu-system-x86_64" 136 } 137 138 if b.config.RawBootWait == "" { 139 b.config.RawBootWait = "10s" 140 } 141 142 if b.config.SSHHostPortMin == 0 { 143 b.config.SSHHostPortMin = 2222 144 } 145 146 if b.config.SSHHostPortMax == 0 { 147 b.config.SSHHostPortMax = 4444 148 } 149 150 if b.config.SSHPort == 0 { 151 b.config.SSHPort = 22 152 } 153 154 if b.config.VNCPortMin == 0 { 155 b.config.VNCPortMin = 5900 156 } 157 158 if b.config.VNCPortMax == 0 { 159 b.config.VNCPortMax = 6000 160 } 161 162 for i, args := range b.config.QemuArgs { 163 for j, arg := range args { 164 if err := b.config.tpl.Validate(arg); err != nil { 165 errs = packer.MultiErrorAppend(errs, 166 fmt.Errorf("Error processing qemu-system_x86-64[%d][%d]: %s", i, j, err)) 167 } 168 } 169 } 170 171 if b.config.VMName == "" { 172 b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) 173 } 174 175 if b.config.Format == "" { 176 b.config.Format = "qcow2" 177 } 178 179 if b.config.FloppyFiles == nil { 180 b.config.FloppyFiles = make([]string, 0) 181 } 182 183 if b.config.NetDevice == "" { 184 b.config.NetDevice = "virtio-net" 185 } 186 187 if b.config.DiskInterface == "" { 188 b.config.DiskInterface = "virtio" 189 } 190 191 // Errors 192 templates := map[string]*string{ 193 "http_directory": &b.config.HTTPDir, 194 "iso_checksum": &b.config.ISOChecksum, 195 "iso_checksum_type": &b.config.ISOChecksumType, 196 "iso_url": &b.config.RawSingleISOUrl, 197 "output_directory": &b.config.OutputDir, 198 "shutdown_command": &b.config.ShutdownCommand, 199 "ssh_password": &b.config.SSHPassword, 200 "ssh_username": &b.config.SSHUser, 201 "vm_name": &b.config.VMName, 202 "format": &b.config.Format, 203 "boot_wait": &b.config.RawBootWait, 204 "shutdown_timeout": &b.config.RawShutdownTimeout, 205 "ssh_wait_timeout": &b.config.RawSSHWaitTimeout, 206 "accelerator": &b.config.Accelerator, 207 "net_device": &b.config.NetDevice, 208 "disk_interface": &b.config.DiskInterface, 209 } 210 211 for n, ptr := range templates { 212 var err error 213 *ptr, err = b.config.tpl.Process(*ptr, nil) 214 if err != nil { 215 errs = packer.MultiErrorAppend( 216 errs, fmt.Errorf("Error processing %s: %s", n, err)) 217 } 218 } 219 220 for i, url := range b.config.ISOUrls { 221 var err error 222 b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil) 223 if err != nil { 224 errs = packer.MultiErrorAppend( 225 errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err)) 226 } 227 } 228 229 for i, command := range b.config.BootCommand { 230 if err := b.config.tpl.Validate(command); err != nil { 231 errs = packer.MultiErrorAppend(errs, 232 fmt.Errorf("Error processing boot_command[%d]: %s", i, err)) 233 } 234 } 235 236 for i, file := range b.config.FloppyFiles { 237 var err error 238 b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil) 239 if err != nil { 240 errs = packer.MultiErrorAppend(errs, 241 fmt.Errorf("Error processing floppy_files[%d]: %s", 242 i, err)) 243 } 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.Accelerator == "kvm" || b.config.Accelerator == "xen") { 252 errs = packer.MultiErrorAppend( 253 errs, errors.New("invalid format, only 'kvm' or 'xen' are allowed")) 254 } 255 256 if _, ok := netDevice[b.config.NetDevice]; !ok { 257 errs = packer.MultiErrorAppend( 258 errs, errors.New("unrecognized network device type")) 259 } 260 261 if _, ok := diskInterface[b.config.DiskInterface]; !ok { 262 errs = packer.MultiErrorAppend( 263 errs, errors.New("unrecognized disk interface type")) 264 } 265 266 if b.config.HTTPPortMin > b.config.HTTPPortMax { 267 errs = packer.MultiErrorAppend( 268 errs, errors.New("http_port_min must be less than http_port_max")) 269 } 270 271 if b.config.ISOChecksum == "" { 272 errs = packer.MultiErrorAppend( 273 errs, errors.New("Due to large file sizes, an iso_checksum is required")) 274 } else { 275 b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum) 276 } 277 278 if b.config.ISOChecksumType == "" { 279 errs = packer.MultiErrorAppend( 280 errs, errors.New("The iso_checksum_type must be specified.")) 281 } else { 282 b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType) 283 if h := common.HashForType(b.config.ISOChecksumType); h == nil { 284 errs = packer.MultiErrorAppend( 285 errs, 286 fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType)) 287 } 288 } 289 290 if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 { 291 errs = packer.MultiErrorAppend( 292 errs, errors.New("One of iso_url or iso_urls must be specified.")) 293 } else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 { 294 errs = packer.MultiErrorAppend( 295 errs, errors.New("Only one of iso_url or iso_urls may be specified.")) 296 } else if b.config.RawSingleISOUrl != "" { 297 b.config.ISOUrls = []string{b.config.RawSingleISOUrl} 298 } 299 300 for i, url := range b.config.ISOUrls { 301 b.config.ISOUrls[i], err = common.DownloadableURL(url) 302 if err != nil { 303 errs = packer.MultiErrorAppend( 304 errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err)) 305 } 306 } 307 308 if !b.config.PackerForce { 309 if _, err := os.Stat(b.config.OutputDir); err == nil { 310 errs = packer.MultiErrorAppend( 311 errs, 312 fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir)) 313 } 314 } 315 316 b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait) 317 if err != nil { 318 errs = packer.MultiErrorAppend( 319 errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) 320 } 321 322 if b.config.RawShutdownTimeout == "" { 323 b.config.RawShutdownTimeout = "5m" 324 } 325 326 if b.config.RawSSHWaitTimeout == "" { 327 b.config.RawSSHWaitTimeout = "20m" 328 } 329 330 b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout) 331 if err != nil { 332 errs = packer.MultiErrorAppend( 333 errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) 334 } 335 336 if b.config.SSHKeyPath != "" { 337 if _, err := os.Stat(b.config.SSHKeyPath); err != nil { 338 errs = packer.MultiErrorAppend( 339 errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) 340 } else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil { 341 errs = packer.MultiErrorAppend( 342 errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) 343 } 344 } 345 346 if b.config.SSHHostPortMin > b.config.SSHHostPortMax { 347 errs = packer.MultiErrorAppend( 348 errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) 349 } 350 351 if b.config.SSHUser == "" { 352 errs = packer.MultiErrorAppend( 353 errs, errors.New("An ssh_username must be specified.")) 354 } 355 356 b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout) 357 if err != nil { 358 errs = packer.MultiErrorAppend( 359 errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err)) 360 } 361 362 if b.config.VNCPortMin > b.config.VNCPortMax { 363 errs = packer.MultiErrorAppend( 364 errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max")) 365 } 366 367 if b.config.QemuArgs == nil { 368 b.config.QemuArgs = make([][]string, 0) 369 } 370 371 if errs != nil && len(errs.Errors) > 0 { 372 return nil, errs 373 } 374 375 return nil, nil 376 } 377 378 func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { 379 // Create the driver that we'll use to communicate with Qemu 380 driver, err := b.newDriver(b.config.QemuBinary) 381 if err != nil { 382 return nil, fmt.Errorf("Failed creating Qemu driver: %s", err) 383 } 384 385 steps := []multistep.Step{ 386 &common.StepDownload{ 387 Checksum: b.config.ISOChecksum, 388 ChecksumType: b.config.ISOChecksumType, 389 Description: "ISO", 390 ResultKey: "iso_path", 391 Url: b.config.ISOUrls, 392 }, 393 new(stepPrepareOutputDir), 394 &common.StepCreateFloppy{ 395 Files: b.config.FloppyFiles, 396 }, 397 new(stepCreateDisk), 398 new(stepHTTPServer), 399 new(stepForwardSSH), 400 new(stepConfigureVNC), 401 &stepRun{ 402 BootDrive: "once=d", 403 Message: "Starting VM, booting from CD-ROM", 404 }, 405 &stepBootWait{}, 406 &stepTypeBootCommand{}, 407 &common.StepConnectSSH{ 408 SSHAddress: sshAddress, 409 SSHConfig: sshConfig, 410 SSHWaitTimeout: b.config.sshWaitTimeout, 411 }, 412 new(common.StepProvision), 413 new(stepShutdown), 414 } 415 416 // Setup the state bag 417 state := new(multistep.BasicStateBag) 418 state.Put("cache", cache) 419 state.Put("config", &b.config) 420 state.Put("driver", driver) 421 state.Put("hook", hook) 422 state.Put("ui", ui) 423 424 // Run 425 if b.config.PackerDebug { 426 b.runner = &multistep.DebugRunner{ 427 Steps: steps, 428 PauseFn: common.MultistepDebugFn(ui), 429 } 430 } else { 431 b.runner = &multistep.BasicRunner{Steps: steps} 432 } 433 434 b.runner.Run(state) 435 436 // If there was an error, return that 437 if rawErr, ok := state.GetOk("error"); ok { 438 return nil, rawErr.(error) 439 } 440 441 // If we were interrupted or cancelled, then just exit. 442 if _, ok := state.GetOk(multistep.StateCancelled); ok { 443 return nil, errors.New("Build was cancelled.") 444 } 445 446 if _, ok := state.GetOk(multistep.StateHalted); ok { 447 return nil, errors.New("Build was halted.") 448 } 449 450 // Compile the artifact list 451 files := make([]string, 0, 5) 452 visit := func(path string, info os.FileInfo, err error) error { 453 if !info.IsDir() { 454 files = append(files, path) 455 } 456 457 return err 458 } 459 460 if err := filepath.Walk(b.config.OutputDir, visit); err != nil { 461 return nil, err 462 } 463 464 artifact := &Artifact{ 465 dir: b.config.OutputDir, 466 f: files, 467 } 468 469 return artifact, nil 470 } 471 472 func (b *Builder) Cancel() { 473 if b.runner != nil { 474 log.Println("Cancelling the step runner...") 475 b.runner.Cancel() 476 } 477 } 478 479 func (b *Builder) newDriver(qemuBinary string) (Driver, error) { 480 qemuPath, err := exec.LookPath(qemuBinary) 481 if err != nil { 482 return nil, err 483 } 484 485 qemuImgPath, err := exec.LookPath("qemu-img") 486 if err != nil { 487 return nil, err 488 } 489 490 log.Printf("Qemu path: %s, Qemu Image page: %s", qemuPath, qemuImgPath) 491 driver := &QemuDriver{ 492 QemuPath: qemuPath, 493 QemuImgPath: qemuImgPath, 494 } 495 496 if err := driver.Verify(); err != nil { 497 return nil, err 498 } 499 500 return driver, nil 501 }