Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix nasty edge case where we can't find guest additions on windows if they are on a different drive #5761

Merged
merged 11 commits into from
Jan 11, 2018
14 changes: 5 additions & 9 deletions builder/virtualbox/ovf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package ovf

import (
"fmt"
"net/url"
"os"
"strings"

vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common"
Expand Down Expand Up @@ -103,14 +101,12 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is invalid: %s", err))
}
// file must exist now.
fileURL, _ := url.Parse(c.SourcePath)
if fileURL.Scheme == "file" {
if _, err := os.Stat(fileURL.Path); err != nil {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("source file needs to exist at time of config validation: %s", err))
}
fileOK := common.FileExistsLocally(c.SourcePath)
if !fileOK {
packer.MultiErrorAppend(errs,
fmt.Errorf("Source file needs to exist at time of config validation!"))
}

}

validMode := false
Expand Down
91 changes: 67 additions & 24 deletions common/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,78 +50,121 @@ func DownloadableURL(original string) (string, error) {
if runtime.GOOS == "windows" {
// If the distance to the first ":" is just one character, assume
// we're dealing with a drive letter and thus a file path.
// prepend with "file:///"" now so that url.Parse won't accidentally
// parse the drive letter into the url scheme.
// See https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/
// for more info about valid windows URIs
idx := strings.Index(original, ":")
if idx == 1 {
original = "file:///" + original
}
}

url, err := url.Parse(original)
u, err := url.Parse(original)
if err != nil {
return "", err
}

if url.Scheme == "" {
url.Scheme = "file"
if u.Scheme == "" {
u.Scheme = "file"
}

if url.Scheme == "file" {
if u.Scheme == "file" {
// Windows file handling is all sorts of tricky...
if runtime.GOOS == "windows" {
// If the path is using Windows-style slashes, URL parses
// it into the host field.
if url.Path == "" && strings.Contains(url.Host, `\`) {
url.Path = url.Host
url.Host = ""
}

// For Windows absolute file paths, remove leading / prior to processing
// since net/url turns "C:/" into "/C:/"
if len(url.Path) > 0 && url.Path[0] == '/' {
url.Path = url.Path[1:len(url.Path)]
if u.Path == "" && strings.Contains(u.Host, `\`) {
u.Path = u.Host
u.Host = ""
}
}

// Only do the filepath transformations if the file appears
// to actually exist.
if _, err := os.Stat(url.Path); err == nil {
url.Path, err = filepath.Abs(url.Path)
if _, err := os.Stat(u.Path); err == nil {
u.Path, err = filepath.Abs(u.Path)
if err != nil {
return "", err
}

url.Path, err = filepath.EvalSymlinks(url.Path)
u.Path, err = filepath.EvalSymlinks(u.Path)
if err != nil {
return "", err
}

url.Path = filepath.Clean(url.Path)
u.Path = filepath.Clean(u.Path)
}

if runtime.GOOS == "windows" {
// Also replace all backslashes with forwardslashes since Windows
// users are likely to do this but the URL should actually only
// contain forward slashes.
url.Path = strings.Replace(url.Path, `\`, `/`, -1)
u.Path = strings.Replace(u.Path, `\`, `/`, -1)
// prepend absolute windows paths with "/" so that when we
// compose u.String() below the outcome will be correct
// file:///c/blah syntax; otherwise u.String() will only add
// file:// which is not technically a correct windows URI
if filepath.IsAbs(u.Path) && !strings.HasPrefix(u.Path, "/") {
u.Path = "/" + u.Path
}

}
}

// Make sure it is lowercased
url.Scheme = strings.ToLower(url.Scheme)
u.Scheme = strings.ToLower(u.Scheme)

// Verify that the scheme is something we support in our common downloader.
supported := []string{"file", "http", "https"}
found := false
for _, s := range supported {
if url.Scheme == s {
if u.Scheme == s {
found = true
break
}
}

if !found {
return "", fmt.Errorf("Unsupported URL scheme: %s", url.Scheme)
return "", fmt.Errorf("Unsupported URL scheme: %s", u.Scheme)
}
return u.String(), nil
}

return url.String(), nil
// FileExistsLocally takes the URL output from DownloadableURL, and determines
// whether it is present on the file system.
// example usage:
//
// myFile, err = common.DownloadableURL(c.SourcePath)
// ...
// fileExists := common.StatURL(myFile)
// possible output:
// true -- should occur if the file is present, or if the file is not present,
// but is not supposed to be (e.g. the schema is http://, not file://)
// false -- should occur if there was an error stating the file, so the
// file is not present when it should be.

func FileExistsLocally(original string) bool {
// original should be something like file://C:/my/path.iso

fileURL, _ := url.Parse(original)
fileExists := false

if fileURL.Scheme == "file" {
// on windows, correct URI is file:///c:/blah/blah.iso.
// url.Parse will pull out the scheme "file://" and leave the path as
// "/c:/blah/blah/iso". Here we remove this forward slash on absolute
// Windows file URLs before processing
// see https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/
// for more info about valid windows URIs
filePath := fileURL.Path
if runtime.GOOS == "windows" && len(filePath) > 0 && filePath[0] == '/' {
filePath = filePath[1:]
}
_, err := os.Stat(filePath)
if err != nil {
return fileExists
} else {
fileExists = true
}
}
return fileExists
}
103 changes: 103 additions & 0 deletions common/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,72 @@ func TestDownloadableURL(t *testing.T) {
}
}

func TestDownloadableURL_WindowsFiles(t *testing.T) {
if runtime.GOOS == "windows" {
dirCases := []struct {
InputString string
OutputURL string
ErrExpected bool
}{ // TODO: add different directories
{
"C:\\Temp\\SomeDir\\myfile.txt",
"file:///C:/Temp/SomeDir/myfile.txt",
false,
},
{ // need windows drive
"\\Temp\\SomeDir\\myfile.txt",
"",
true,
},
{ // need windows drive
"/Temp/SomeDir/myfile.txt",
"",
true,
},
{ // UNC paths; why not?
"\\\\?\\c:\\Temp\\SomeDir\\myfile.txt",
"",
true,
},
{
"file:///C:\\Temp\\SomeDir\\myfile.txt",
"file:///c:/Temp/SomeDir/myfile.txt",
false,
},
{
"file:///c:/Temp/Somedir/myfile.txt",
"file:///c:/Temp/SomeDir/myfile.txt",
false,
},
}
// create absolute-pathed tempfile to play with
err := os.Mkdir("C:\\Temp\\SomeDir", 0755)
if err != nil {
t.Fatalf("err creating test dir: %s", err)
}
fi, err := os.Create("C:\\Temp\\SomeDir\\myfile.txt")
if err != nil {
t.Fatalf("err creating test file: %s", err)
}
fi.Close()
defer os.Remove("C:\\Temp\\SomeDir\\myfile.txt")
defer os.Remove("C:\\Temp\\SomeDir")

// Run through test cases to make sure they all parse correctly
for _, tc := range dirCases {
u, err := DownloadableURL(tc.InputString)
if (err != nil) != tc.ErrExpected {
t.Fatalf("Test Case failed: Expected err = %#v, err = %#v, input = %s",
tc.ErrExpected, err, tc.InputString)
}
if u != tc.OutputURL {
t.Fatalf("Test Case failed: Expected %s but received %s from input %s",
tc.OutputURL, u, tc.InputString)
}
}
}
}

func TestDownloadableURL_FilePaths(t *testing.T) {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
Expand Down Expand Up @@ -142,6 +208,43 @@ func TestDownloadableURL_FilePaths(t *testing.T) {
}
}

func test_FileExistsLocally(t *testing.T) {
if runtime.GOOS == "windows" {
dirCases := []struct {
Input string
Output bool
}{
// file exists locally
{"file:///C:/Temp/SomeDir/myfile.txt", true},
// file is not supposed to exist locally
{"https://myfile.iso", true},
// file does not exist locally
{"file:///C/i/dont/exist", false},
}
// create absolute-pathed tempfile to play with
err := os.Mkdir("C:\\Temp\\SomeDir", 0755)
if err != nil {
t.Fatalf("err creating test dir: %s", err)
}
fi, err := os.Create("C:\\Temp\\SomeDir\\myfile.txt")
if err != nil {
t.Fatalf("err creating test file: %s", err)
}
fi.Close()
defer os.Remove("C:\\Temp\\SomeDir\\myfile.txt")
defer os.Remove("C:\\Temp\\SomeDir")

// Run through test cases to make sure they all parse correctly
for _, tc := range dirCases {
fileOK := FileExistsLocally(tc.Input)
if !fileOK {
t.Fatalf("Test Case failed: Expected %#v, received = %#v, input = %s",
tc.Output, fileOK, tc.Input)
}
}
}
}

func TestScrubConfig(t *testing.T) {
type Inner struct {
Baz string
Expand Down
6 changes: 6 additions & 0 deletions common/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"net/http"
"net/url"
"os"
"runtime"
)

// DownloadConfig is the configuration given to instantiate a new
Expand Down Expand Up @@ -129,6 +130,11 @@ func (d *DownloadClient) Get() (string, error) {
// locally and we don't make a copy. Normally we would copy or download.
log.Printf("[DEBUG] Using local file: %s", finalPath)

// Remove forward slash on absolute Windows file URLs before processing
if runtime.GOOS == "windows" && len(finalPath) > 0 && finalPath[0] == '/' {
finalPath = finalPath[1:]
}

// Keep track of the source so we can make sure not to delete this later
sourcePath = finalPath
if _, err = os.Stat(finalPath); err != nil {
Expand Down