builder/virtualbox: download ISO

This commit is contained in:
Mitchell Hashimoto 2013-06-11 20:00:30 -07:00
parent c1c9204ee8
commit 54057b7b49
3 changed files with 283 additions and 1 deletions

View File

@ -1,12 +1,16 @@
package virtualbox
import (
"errors"
"fmt"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"log"
"net/url"
"os"
"os/exec"
"strings"
)
const BuilderId = "mitchellh.virtualbox"
@ -19,6 +23,8 @@ type Builder struct {
type config struct {
GuestOSType string `mapstructure:"guest_os_type"`
ISOMD5 string `mapstructure:"iso_md5"`
ISOUrl string `mapstructure:"iso_url"`
OutputDir string `mapstructure:"output_directory"`
VMName string `mapstructure:"vm_name"`
}
@ -43,6 +49,51 @@ func (b *Builder) Prepare(raw interface{}) error {
errs := make([]error, 0)
if b.config.ISOMD5 == "" {
errs = append(errs, errors.New("Due to large file sizes, an iso_md5 is required"))
} else {
b.config.ISOMD5 = strings.ToLower(b.config.ISOMD5)
}
if b.config.ISOUrl == "" {
errs = append(errs, errors.New("An iso_url must be specified."))
} else {
url, err := url.Parse(b.config.ISOUrl)
if err != nil {
errs = append(errs, fmt.Errorf("iso_url is not a valid URL: %s", err))
} else {
if url.Scheme == "" {
url.Scheme = "file"
}
if url.Scheme == "file" {
if _, err := os.Stat(b.config.ISOUrl); err != nil {
errs = append(errs, fmt.Errorf("iso_url points to bad file: %s", err))
}
} else {
supportedSchemes := []string{"file", "http", "https"}
scheme := strings.ToLower(url.Scheme)
found := false
for _, supported := range supportedSchemes {
if scheme == supported {
found = true
break
}
}
if !found {
errs = append(errs, fmt.Errorf("Unsupported URL scheme in iso_url: %s", scheme))
}
}
}
if len(errs) == 0 {
// Put the URL back together since we may have modified it
b.config.ISOUrl = url.String()
}
}
b.driver, err = b.newDriver()
if err != nil {
errs = append(errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
@ -57,6 +108,7 @@ func (b *Builder) Prepare(raw interface{}) error {
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) packer.Artifact {
steps := []multistep.Step{
new(stepDownloadISO),
new(stepPrepareOutputDir),
new(stepSuppressMessages),
new(stepCreateVM),

View File

@ -2,11 +2,16 @@ package virtualbox
import (
"github.com/mitchellh/packer/packer"
"io/ioutil"
"os"
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{}
return map[string]interface{}{
"iso_md5": "foo",
"iso_url": "http://www.google.com/",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
@ -37,3 +42,71 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
t.Errorf("bad vm name: %s", b.config.VMName)
}
}
func TestBuilderPrepare_ISOMD5(t *testing.T) {
var b Builder
config := testConfig()
// Test bad
config["iso_md5"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
// Test good
config["iso_md5"] = "FOo"
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOMD5 != "foo" {
t.Fatalf("should've lowercased: %s", b.config.ISOMD5)
}
}
func TestBuilderPrepare_ISOUrl(t *testing.T) {
var b Builder
config := testConfig()
config["iso_url"] = ""
err := b.Prepare(config)
if err == nil {
t.Fatal("should have error")
}
config["iso_url"] = "i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["iso_url"] = "file:i/am/a/file/that/doesnt/exist"
err = b.Prepare(config)
if err == nil {
t.Error("should have error")
}
config["iso_url"] = "http://www.packer.io"
err = b.Prepare(config)
if err != nil {
t.Errorf("should not have error: %s", err)
}
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
config["iso_url"] = tf.Name()
err = b.Prepare(config)
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ISOUrl != "file://"+tf.Name() {
t.Fatalf("iso_url should be modified: %s", b.config.ISOUrl)
}
}

View File

@ -0,0 +1,157 @@
package virtualbox
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// This step downloads the ISO specified.
//
// Uses:
// cache packer.Cache
// config *config
// ui packer.Ui
//
// Produces:
// iso_path string
type stepDownloadISO struct{}
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
cache := state["cache"].(packer.Cache)
config := state["config"].(*config)
ui := state["ui"].(packer.Ui)
log.Printf("Acquiring lock to download the ISO.")
cachePath := cache.Lock(config.ISOUrl)
defer cache.Unlock(config.ISOUrl)
err := s.checkMD5(cachePath, config.ISOMD5)
haveFile := err == nil
if err != nil {
if !os.IsNotExist(err) {
ui.Say(fmt.Sprintf("Error validating MD5 of ISO: %s", err))
return multistep.ActionHalt
}
}
if !haveFile {
url, err := url.Parse(config.ISOUrl)
if err != nil {
ui.Error(fmt.Sprintf("Error parsing iso_url: %s", err))
return multistep.ActionHalt
}
// Start the download in a goroutine so that we cancel it and such.
var progress uint
downloadComplete := make(chan bool, 1)
go func() {
ui.Say("Copying or downloading ISO. Progress will be shown periodically.")
cachePath, err = s.downloadUrl(cachePath, url, &progress)
downloadComplete <- true
}()
progressTimer := time.NewTicker(15 * time.Second)
defer progressTimer.Stop()
DownloadWaitLoop:
for {
select {
case <-downloadComplete:
log.Println("Download of ISO completed.")
break DownloadWaitLoop
case <-progressTimer.C:
ui.Say(fmt.Sprintf("Download progress: %d%%", progress))
case <-time.After(1 * time.Second):
if _, ok := state[multistep.StateCancelled]; ok {
ui.Say("Interrupt received. Cancelling download...")
return multistep.ActionHalt
}
}
}
if err != nil {
ui.Error(fmt.Sprintf("Error downloading ISO: %s", err))
return multistep.ActionHalt
}
if err = s.checkMD5(cachePath, config.ISOMD5); err != nil {
ui.Say(fmt.Sprintf("Error validating MD5 of ISO: %s", err))
return multistep.ActionHalt
}
}
log.Printf("Path to ISO on disk: %s", cachePath)
state["iso_path"] = cachePath
return multistep.ActionContinue
}
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
func (stepDownloadISO) checkMD5(path string, expected string) error {
f, err := os.Open(path)
if err != nil {
return err
}
hash := md5.New()
io.Copy(hash, f)
result := strings.ToLower(hex.EncodeToString(hash.Sum(nil)))
if result != expected {
return fmt.Errorf("result != expected: %s != %s", result, expected)
}
return nil
}
func (stepDownloadISO) downloadUrl(path string, url *url.URL, progress *uint) (string, error) {
if url.Scheme == "file" {
// If it is just a file URL, then we already have the ISO
return url.Path, nil
}
// Otherwise, it is an HTTP URL, and we must download it.
f, err := os.Create(path)
if err != nil {
return "", err
}
defer f.Close()
log.Printf("Beginning download of ISO: %s", url.String())
resp, err := http.Get(url.String())
if err != nil {
return "", err
}
var buffer [4096]byte
var totalRead int64
for {
n, err := resp.Body.Read(buffer[:])
if err != nil && err != io.EOF {
return "", err
}
totalRead += int64(n)
*progress = uint((float64(totalRead) / float64(resp.ContentLength)) * 100)
if _, werr := f.Write(buffer[:n]); werr != nil {
return "", werr
}
if err == io.EOF {
break
}
}
return path, nil
}