github.com/supr/packer@v0.3.10-0.20131015195147-7b09e24ac3c1/builder/virtualbox/builder.go (about) 1 package virtualbox 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 = "mitchellh.virtualbox" 18 19 type Builder struct { 20 config config 21 runner multistep.Runner 22 } 23 24 type config struct { 25 common.PackerConfig `mapstructure:",squash"` 26 27 BootCommand []string `mapstructure:"boot_command"` 28 DiskSize uint `mapstructure:"disk_size"` 29 FloppyFiles []string `mapstructure:"floppy_files"` 30 Format string `mapstructure:"format"` 31 GuestAdditionsAttach bool `mapstructure:"guest_additions_attach"` 32 GuestAdditionsPath string `mapstructure:"guest_additions_path"` 33 GuestAdditionsURL string `mapstructure:"guest_additions_url"` 34 GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"` 35 GuestOSType string `mapstructure:"guest_os_type"` 36 HardDriveInterface string `mapstructure:"hard_drive_interface"` 37 Headless bool `mapstructure:"headless"` 38 HTTPDir string `mapstructure:"http_directory"` 39 HTTPPortMin uint `mapstructure:"http_port_min"` 40 HTTPPortMax uint `mapstructure:"http_port_max"` 41 ISOChecksum string `mapstructure:"iso_checksum"` 42 ISOChecksumType string `mapstructure:"iso_checksum_type"` 43 ISOUrls []string `mapstructure:"iso_urls"` 44 OutputDir string `mapstructure:"output_directory"` 45 ShutdownCommand string `mapstructure:"shutdown_command"` 46 SSHHostPortMin uint `mapstructure:"ssh_host_port_min"` 47 SSHHostPortMax uint `mapstructure:"ssh_host_port_max"` 48 SSHKeyPath string `mapstructure:"ssh_key_path"` 49 SSHPassword string `mapstructure:"ssh_password"` 50 SSHPort uint `mapstructure:"ssh_port"` 51 SSHUser string `mapstructure:"ssh_username"` 52 VBoxVersionFile string `mapstructure:"virtualbox_version_file"` 53 VBoxManage [][]string `mapstructure:"vboxmanage"` 54 VMName string `mapstructure:"vm_name"` 55 56 RawBootWait string `mapstructure:"boot_wait"` 57 RawSingleISOUrl string `mapstructure:"iso_url"` 58 RawShutdownTimeout string `mapstructure:"shutdown_timeout"` 59 RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"` 60 61 bootWait time.Duration `` 62 shutdownTimeout time.Duration `` 63 sshWaitTimeout time.Duration `` 64 tpl *packer.ConfigTemplate 65 } 66 67 func (b *Builder) Prepare(raws ...interface{}) error { 68 md, err := common.DecodeConfig(&b.config, raws...) 69 if err != nil { 70 return err 71 } 72 73 b.config.tpl, err = packer.NewConfigTemplate() 74 if err != nil { 75 return err 76 } 77 b.config.tpl.UserVars = b.config.PackerUserVars 78 79 // Accumulate any errors 80 errs := common.CheckUnusedConfig(md) 81 82 if b.config.DiskSize == 0 { 83 b.config.DiskSize = 40000 84 } 85 86 if b.config.FloppyFiles == nil { 87 b.config.FloppyFiles = make([]string, 0) 88 } 89 90 if b.config.GuestAdditionsPath == "" { 91 b.config.GuestAdditionsPath = "VBoxGuestAdditions.iso" 92 } 93 94 if b.config.HardDriveInterface == "" { 95 b.config.HardDriveInterface = "ide" 96 } 97 98 if b.config.GuestOSType == "" { 99 b.config.GuestOSType = "Other" 100 } 101 102 if b.config.HTTPPortMin == 0 { 103 b.config.HTTPPortMin = 8000 104 } 105 106 if b.config.HTTPPortMax == 0 { 107 b.config.HTTPPortMax = 9000 108 } 109 110 if b.config.OutputDir == "" { 111 b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName) 112 } 113 114 if b.config.RawBootWait == "" { 115 b.config.RawBootWait = "10s" 116 } 117 118 if b.config.SSHHostPortMin == 0 { 119 b.config.SSHHostPortMin = 2222 120 } 121 122 if b.config.SSHHostPortMax == 0 { 123 b.config.SSHHostPortMax = 4444 124 } 125 126 if b.config.SSHPort == 0 { 127 b.config.SSHPort = 22 128 } 129 130 if b.config.VBoxManage == nil { 131 b.config.VBoxManage = make([][]string, 0) 132 } 133 134 if b.config.VBoxVersionFile == "" { 135 b.config.VBoxVersionFile = ".vbox_version" 136 } 137 138 if b.config.VMName == "" { 139 b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName) 140 } 141 142 if b.config.Format == "" { 143 b.config.Format = "ovf" 144 } 145 146 // Errors 147 templates := map[string]*string{ 148 "guest_additions_sha256": &b.config.GuestAdditionsSHA256, 149 "guest_os_type": &b.config.GuestOSType, 150 "hard_drive_interface": &b.config.HardDriveInterface, 151 "http_directory": &b.config.HTTPDir, 152 "iso_checksum": &b.config.ISOChecksum, 153 "iso_checksum_type": &b.config.ISOChecksumType, 154 "iso_url": &b.config.RawSingleISOUrl, 155 "output_directory": &b.config.OutputDir, 156 "shutdown_command": &b.config.ShutdownCommand, 157 "ssh_password": &b.config.SSHPassword, 158 "ssh_username": &b.config.SSHUser, 159 "virtualbox_version_file": &b.config.VBoxVersionFile, 160 "vm_name": &b.config.VMName, 161 "format": &b.config.Format, 162 "boot_wait": &b.config.RawBootWait, 163 "shutdown_timeout": &b.config.RawShutdownTimeout, 164 "ssh_wait_timeout": &b.config.RawSSHWaitTimeout, 165 } 166 167 for n, ptr := range templates { 168 var err error 169 *ptr, err = b.config.tpl.Process(*ptr, nil) 170 if err != nil { 171 errs = packer.MultiErrorAppend( 172 errs, fmt.Errorf("Error processing %s: %s", n, err)) 173 } 174 } 175 176 for i, url := range b.config.ISOUrls { 177 var err error 178 b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil) 179 if err != nil { 180 errs = packer.MultiErrorAppend( 181 errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err)) 182 } 183 } 184 185 validates := map[string]*string{ 186 "guest_additions_path": &b.config.GuestAdditionsPath, 187 "guest_additions_url": &b.config.GuestAdditionsURL, 188 } 189 190 for n, ptr := range validates { 191 if err := b.config.tpl.Validate(*ptr); err != nil { 192 errs = packer.MultiErrorAppend( 193 errs, fmt.Errorf("Error parsing %s: %s", n, err)) 194 } 195 } 196 197 for i, command := range b.config.BootCommand { 198 if err := b.config.tpl.Validate(command); err != nil { 199 errs = packer.MultiErrorAppend(errs, 200 fmt.Errorf("Error processing boot_command[%d]: %s", i, err)) 201 } 202 } 203 204 for i, file := range b.config.FloppyFiles { 205 var err error 206 b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil) 207 if err != nil { 208 errs = packer.MultiErrorAppend(errs, 209 fmt.Errorf("Error processing floppy_files[%d]: %s", 210 i, err)) 211 } 212 } 213 214 if !(b.config.Format == "ovf" || b.config.Format == "ova") { 215 errs = packer.MultiErrorAppend( 216 errs, errors.New("invalid format, only 'ovf' or 'ova' are allowed")) 217 } 218 219 if b.config.HardDriveInterface != "ide" && b.config.HardDriveInterface != "sata" { 220 errs = packer.MultiErrorAppend( 221 errs, errors.New("hard_drive_interface can only be ide or sata")) 222 } 223 224 if b.config.HTTPPortMin > b.config.HTTPPortMax { 225 errs = packer.MultiErrorAppend( 226 errs, errors.New("http_port_min must be less than http_port_max")) 227 } 228 229 if b.config.ISOChecksum == "" { 230 errs = packer.MultiErrorAppend( 231 errs, errors.New("Due to large file sizes, an iso_checksum is required")) 232 } else { 233 b.config.ISOChecksum = strings.ToLower(b.config.ISOChecksum) 234 } 235 236 if b.config.ISOChecksumType == "" { 237 errs = packer.MultiErrorAppend( 238 errs, errors.New("The iso_checksum_type must be specified.")) 239 } else { 240 b.config.ISOChecksumType = strings.ToLower(b.config.ISOChecksumType) 241 if h := common.HashForType(b.config.ISOChecksumType); h == nil { 242 errs = packer.MultiErrorAppend( 243 errs, 244 fmt.Errorf("Unsupported checksum type: %s", b.config.ISOChecksumType)) 245 } 246 } 247 248 if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 { 249 errs = packer.MultiErrorAppend( 250 errs, errors.New("One of iso_url or iso_urls must be specified.")) 251 } else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 { 252 errs = packer.MultiErrorAppend( 253 errs, errors.New("Only one of iso_url or iso_urls may be specified.")) 254 } else if b.config.RawSingleISOUrl != "" { 255 b.config.ISOUrls = []string{b.config.RawSingleISOUrl} 256 } 257 258 for i, url := range b.config.ISOUrls { 259 b.config.ISOUrls[i], err = common.DownloadableURL(url) 260 if err != nil { 261 errs = packer.MultiErrorAppend( 262 errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err)) 263 } 264 } 265 266 if b.config.GuestAdditionsSHA256 != "" { 267 b.config.GuestAdditionsSHA256 = strings.ToLower(b.config.GuestAdditionsSHA256) 268 } 269 270 if !b.config.PackerForce { 271 if _, err := os.Stat(b.config.OutputDir); err == nil { 272 errs = packer.MultiErrorAppend( 273 errs, 274 fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir)) 275 } 276 } 277 278 b.config.bootWait, err = time.ParseDuration(b.config.RawBootWait) 279 if err != nil { 280 errs = packer.MultiErrorAppend( 281 errs, fmt.Errorf("Failed parsing boot_wait: %s", err)) 282 } 283 284 if b.config.RawShutdownTimeout == "" { 285 b.config.RawShutdownTimeout = "5m" 286 } 287 288 if b.config.RawSSHWaitTimeout == "" { 289 b.config.RawSSHWaitTimeout = "20m" 290 } 291 292 b.config.shutdownTimeout, err = time.ParseDuration(b.config.RawShutdownTimeout) 293 if err != nil { 294 errs = packer.MultiErrorAppend( 295 errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err)) 296 } 297 298 if b.config.SSHKeyPath != "" { 299 if _, err := os.Stat(b.config.SSHKeyPath); err != nil { 300 errs = packer.MultiErrorAppend( 301 errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) 302 } else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil { 303 errs = packer.MultiErrorAppend( 304 errs, fmt.Errorf("ssh_key_path is invalid: %s", err)) 305 } 306 } 307 308 if b.config.SSHHostPortMin > b.config.SSHHostPortMax { 309 errs = packer.MultiErrorAppend( 310 errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max")) 311 } 312 313 if b.config.SSHUser == "" { 314 errs = packer.MultiErrorAppend( 315 errs, errors.New("An ssh_username must be specified.")) 316 } 317 318 b.config.sshWaitTimeout, err = time.ParseDuration(b.config.RawSSHWaitTimeout) 319 if err != nil { 320 errs = packer.MultiErrorAppend( 321 errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err)) 322 } 323 324 for i, args := range b.config.VBoxManage { 325 for j, arg := range args { 326 if err := b.config.tpl.Validate(arg); err != nil { 327 errs = packer.MultiErrorAppend(errs, 328 fmt.Errorf("Error processing vboxmanage[%d][%d]: %s", i, j, err)) 329 } 330 } 331 } 332 333 if errs != nil && len(errs.Errors) > 0 { 334 return errs 335 } 336 337 return nil 338 } 339 340 func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { 341 // Create the driver that we'll use to communicate with VirtualBox 342 driver, err := b.newDriver() 343 if err != nil { 344 return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err) 345 } 346 347 steps := []multistep.Step{ 348 new(stepDownloadGuestAdditions), 349 &common.StepDownload{ 350 Checksum: b.config.ISOChecksum, 351 ChecksumType: b.config.ISOChecksumType, 352 Description: "ISO", 353 ResultKey: "iso_path", 354 Url: b.config.ISOUrls, 355 }, 356 new(stepPrepareOutputDir), 357 &common.StepCreateFloppy{ 358 Files: b.config.FloppyFiles, 359 }, 360 new(stepHTTPServer), 361 new(stepSuppressMessages), 362 new(stepCreateVM), 363 new(stepCreateDisk), 364 new(stepAttachISO), 365 new(stepAttachGuestAdditions), 366 new(stepAttachFloppy), 367 new(stepForwardSSH), 368 new(stepVBoxManage), 369 new(stepRun), 370 new(stepTypeBootCommand), 371 &common.StepConnectSSH{ 372 SSHAddress: sshAddress, 373 SSHConfig: sshConfig, 374 SSHWaitTimeout: b.config.sshWaitTimeout, 375 }, 376 new(stepUploadVersion), 377 new(stepUploadGuestAdditions), 378 new(common.StepProvision), 379 new(stepShutdown), 380 new(stepExport), 381 } 382 383 // Setup the state bag 384 state := new(multistep.BasicStateBag) 385 state.Put("cache", cache) 386 state.Put("config", &b.config) 387 state.Put("driver", driver) 388 state.Put("hook", hook) 389 state.Put("ui", ui) 390 391 // Run 392 if b.config.PackerDebug { 393 b.runner = &multistep.DebugRunner{ 394 Steps: steps, 395 PauseFn: common.MultistepDebugFn(ui), 396 } 397 } else { 398 b.runner = &multistep.BasicRunner{Steps: steps} 399 } 400 401 b.runner.Run(state) 402 403 // If there was an error, return that 404 if rawErr, ok := state.GetOk("error"); ok { 405 return nil, rawErr.(error) 406 } 407 408 // If we were interrupted or cancelled, then just exit. 409 if _, ok := state.GetOk(multistep.StateCancelled); ok { 410 return nil, errors.New("Build was cancelled.") 411 } 412 413 if _, ok := state.GetOk(multistep.StateHalted); ok { 414 return nil, errors.New("Build was halted.") 415 } 416 417 // Compile the artifact list 418 files := make([]string, 0, 5) 419 visit := func(path string, info os.FileInfo, err error) error { 420 if !info.IsDir() { 421 files = append(files, path) 422 } 423 424 return err 425 } 426 427 if err := filepath.Walk(b.config.OutputDir, visit); err != nil { 428 return nil, err 429 } 430 431 artifact := &Artifact{ 432 dir: b.config.OutputDir, 433 f: files, 434 } 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() (Driver, error) { 447 vboxmanagePath, err := exec.LookPath("VBoxManage") 448 if err != nil { 449 return nil, err 450 } 451 452 log.Printf("VBoxManage path: %s", vboxmanagePath) 453 driver := &VBox42Driver{vboxmanagePath} 454 if err := driver.Verify(); err != nil { 455 return nil, err 456 } 457 458 return driver, nil 459 }