diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7d364ff5..ec290a9d 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -126,6 +126,11 @@ "ImportPath": "github.com/docker/libtrust", "Rev": "c54fbb67c1f1e68d7d6f8d2ad7c9360404616a41" }, + { + "ImportPath": "github.com/docker/machine/utils", + "Comment": "v0.1.0-rc3-18-gd674e87", + "Rev": "d674e87813ffc10048f55d884396be1af327705e" + }, { "ImportPath": "github.com/flynn/go-shlex", "Rev": "70644ac2a65dbf1691ce00c209d185163a14edc6" diff --git a/Godeps/_workspace/src/github.com/docker/machine/utils/b2d.go b/Godeps/_workspace/src/github.com/docker/machine/utils/b2d.go new file mode 100644 index 00000000..b18235b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/machine/utils/b2d.go @@ -0,0 +1,110 @@ +package utils + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "os" + "path/filepath" + "time" +) + +const ( + timeout = time.Second * 5 +) + +func defaultTimeout(network, addr string) (net.Conn, error) { + return net.DialTimeout(network, addr, timeout) +} + +func getClient() *http.Client { + transport := http.Transport{ + Dial: defaultTimeout, + } + + client := http.Client{ + Transport: &transport, + } + + return &client +} + +type B2dUtils struct { + githubApiBaseUrl string + githubBaseUrl string +} + +func NewB2dUtils(githubApiBaseUrl, githubBaseUrl string) *B2dUtils { + defaultBaseApiUrl := "https://api.github.com" + defaultBaseUrl := "https://github.com" + + if githubApiBaseUrl == "" { + githubApiBaseUrl = defaultBaseApiUrl + } + + if githubBaseUrl == "" { + githubBaseUrl = defaultBaseUrl + } + + return &B2dUtils{ + githubApiBaseUrl: githubApiBaseUrl, + githubBaseUrl: githubBaseUrl, + } +} + +// Get the latest boot2docker release tag name (e.g. "v0.6.0"). +// FIXME: find or create some other way to get the "latest release" of boot2docker since the GitHub API has a pretty low rate limit on API requests +func (b *B2dUtils) GetLatestBoot2DockerReleaseURL() (string, error) { + client := getClient() + apiUrl := fmt.Sprintf("%s/repos/boot2docker/boot2docker/releases", b.githubApiBaseUrl) + rsp, err := client.Get(apiUrl) + if err != nil { + return "", err + } + defer rsp.Body.Close() + + var t []struct { + TagName string `json:"tag_name"` + } + if err := json.NewDecoder(rsp.Body).Decode(&t); err != nil { + return "", err + } + if len(t) == 0 { + return "", fmt.Errorf("no releases found") + } + + tag := t[0].TagName + isoUrl := fmt.Sprintf("%s/boot2docker/boot2docker/releases/download/%s/boot2docker.iso", b.githubBaseUrl, tag) + return isoUrl, nil +} + +// Download boot2docker ISO image for the given tag and save it at dest. +func (b *B2dUtils) DownloadISO(dir, file, url string) error { + client := getClient() + rsp, err := client.Get(url) + if err != nil { + return err + } + defer rsp.Body.Close() + + // Download to a temp file first then rename it to avoid partial download. + f, err := ioutil.TempFile(dir, file+".tmp") + if err != nil { + return err + } + defer os.Remove(f.Name()) + if _, err := io.Copy(f, rsp.Body); err != nil { + // TODO: display download progress? + return err + } + if err := f.Close(); err != nil { + return err + } + if err := os.Rename(f.Name(), filepath.Join(dir, file)); err != nil { + return err + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/machine/utils/b2d_test.go b/Godeps/_workspace/src/github.com/docker/machine/utils/b2d_test.go new file mode 100644 index 00000000..2fe5a381 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/machine/utils/b2d_test.go @@ -0,0 +1,58 @@ +package utils + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" +) + +func TestGetLatestBoot2DockerReleaseUrl(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + respText := `[{"tag_name": "0.1"}]` + w.Write([]byte(respText)) + })) + defer ts.Close() + + b := NewB2dUtils(ts.URL, ts.URL) + isoUrl, err := b.GetLatestBoot2DockerReleaseURL() + if err != nil { + t.Fatal(err) + } + + expectedUrl := fmt.Sprintf("%s/boot2docker/boot2docker/releases/download/0.1/boot2docker.iso", ts.URL) + if isoUrl != expectedUrl { + t.Fatalf("expected url %s; received %s", isoUrl) + } +} + +func TestDownloadIso(t *testing.T) { + testData := "test-download" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(testData)) + })) + defer ts.Close() + + filename := "test" + + tmpDir, err := ioutil.TempDir("", "machine-test-") + if err != nil { + t.Fatal(err) + } + + b := NewB2dUtils(ts.URL, ts.URL) + if err := b.DownloadISO(tmpDir, filename, ts.URL); err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadFile(filepath.Join(tmpDir, filename)) + if err != nil { + t.Fatal(err) + } + + if string(data) != testData { + t.Fatalf("expected data \"%s\"; received \"%s\"", testData, string(data)) + } +} diff --git a/Godeps/_workspace/src/github.com/docker/machine/utils/certs.go b/Godeps/_workspace/src/github.com/docker/machine/utils/certs.go new file mode 100644 index 00000000..70b8a6cd --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/machine/utils/certs.go @@ -0,0 +1,150 @@ +package utils + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "os" + "time" +) + +func newCertificate(org string) (*x509.Certificate, error) { + now := time.Now() + // need to set notBefore slightly in the past to account for time + // skew in the VMs otherwise the certs sometimes are not yet valid + notBefore := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute()-5, 0, 0, time.Local) + notAfter := notBefore.Add(time.Hour * 24 * 1080) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + return &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{org}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + }, nil + +} + +// GenerateCACertificate generates a new certificate authority from the specified org +// and bit size and stores the resulting certificate and key file +// in the arguments. +func GenerateCACertificate(certFile, keyFile, org string, bits int) error { + template, err := newCertificate(org) + if err != nil { + return err + } + + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return err + } + + derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) + if err != nil { + return err + } + + certOut, err := os.Create(certFile) + if err != nil { + return err + } + + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + + keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + + } + + pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + keyOut.Close() + + return nil +} + +// GenerateCert generates a new certificate signed using the provided +// certificate authority files and stores the result in the certificate +// file and key provided. The provided host names are set to the +// appropriate certificate fields. +func GenerateCert(hosts []string, certFile, keyFile, caFile, caKeyFile, org string, bits int) error { + template, err := newCertificate(org) + if err != nil { + return err + } + // client + if len(hosts) == 1 && hosts[0] == "" { + template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth} + template.KeyUsage = x509.KeyUsageDigitalSignature + } else { // server + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + } + + tlsCert, err := tls.LoadX509KeyPair(caFile, caKeyFile) + if err != nil { + return err + + } + + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return err + + } + + x509Cert, err := x509.ParseCertificate(tlsCert.Certificate[0]) + if err != nil { + return err + } + + derBytes, err := x509.CreateCertificate(rand.Reader, template, x509Cert, &priv.PublicKey, tlsCert.PrivateKey) + if err != nil { + return err + } + + certOut, err := os.Create(certFile) + if err != nil { + return err + + } + + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + + keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + + } + + pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + keyOut.Close() + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/machine/utils/certs_test.go b/Godeps/_workspace/src/github.com/docker/machine/utils/certs_test.go new file mode 100644 index 00000000..042f47e0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/machine/utils/certs_test.go @@ -0,0 +1,76 @@ +package utils + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestGenerateCACertificate(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "machine-test-") + if err != nil { + t.Fatal(err) + } + + os.Setenv("MACHINE_DIR", tmpDir) + caCertPath := filepath.Join(tmpDir, "ca.pem") + caKeyPath := filepath.Join(tmpDir, "key.pem") + testOrg := "test-org" + bits := 2048 + if err := GenerateCACertificate(caCertPath, caKeyPath, testOrg, bits); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(caCertPath); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(caKeyPath); err != nil { + t.Fatal(err) + } + os.Setenv("MACHINE_DIR", "") + + // cleanup + _ = os.RemoveAll(tmpDir) +} + +func TestGenerateCert(t *testing.T) { + tmpDir, err := ioutil.TempDir("", "machine-test-") + if err != nil { + t.Fatal(err) + } + + os.Setenv("MACHINE_DIR", tmpDir) + caCertPath := filepath.Join(tmpDir, "ca.pem") + caKeyPath := filepath.Join(tmpDir, "key.pem") + certPath := filepath.Join(tmpDir, "cert.pem") + keyPath := filepath.Join(tmpDir, "cert-key.pem") + testOrg := "test-org" + bits := 2048 + if err := GenerateCACertificate(caCertPath, caKeyPath, testOrg, bits); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(caCertPath); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(caKeyPath); err != nil { + t.Fatal(err) + } + os.Setenv("MACHINE_DIR", "") + + if err := GenerateCert([]string{}, certPath, keyPath, caCertPath, caKeyPath, testOrg, bits); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(certPath); err != nil { + t.Fatalf("certificate not created at %s", certPath) + } + + if _, err := os.Stat(keyPath); err != nil { + t.Fatalf("key not created at %s", keyPath) + } + + // cleanup + _ = os.RemoveAll(tmpDir) +} diff --git a/Godeps/_workspace/src/github.com/docker/machine/utils/utils.go b/Godeps/_workspace/src/github.com/docker/machine/utils/utils.go new file mode 100644 index 00000000..fb4bd171 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/machine/utils/utils.go @@ -0,0 +1,73 @@ +package utils + +import ( + "io" + "os" + "path/filepath" + "runtime" +) + +func GetHomeDir() string { + if runtime.GOOS == "windows" { + return os.Getenv("USERPROFILE") + } + return os.Getenv("HOME") +} + +func GetBaseDir() string { + baseDir := os.Getenv("MACHINE_DIR") + if baseDir == "" { + baseDir = GetHomeDir() + } + return baseDir +} + +func GetDockerDir() string { + return filepath.Join(GetBaseDir(), ".docker") +} + +func GetMachineDir() string { + return filepath.Join(GetDockerDir(), "machines") +} + +func GetMachineClientCertDir() string { + return filepath.Join(GetMachineDir(), ".client") +} + +func GetUsername() string { + u := "unknown" + osUser := "" + + switch runtime.GOOS { + case "darwin", "linux": + osUser = os.Getenv("USER") + case "windows": + osUser = os.Getenv("USERNAME") + } + + if osUser != "" { + u = osUser + } + + return u +} + +func CopyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + + if _, err = io.Copy(out, in); err != nil { + return err + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/machine/utils/utils_test.go b/Godeps/_workspace/src/github.com/docker/machine/utils/utils_test.go new file mode 100644 index 00000000..450c8c07 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/machine/utils/utils_test.go @@ -0,0 +1,149 @@ +package utils + +import ( + "io/ioutil" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestGetBaseDir(t *testing.T) { + // reset any override env var + homeDir := GetHomeDir() + baseDir := GetBaseDir() + + if strings.Index(homeDir, baseDir) != 0 { + t.Fatalf("expected base dir with prefix %s; received %s", homeDir, baseDir) + } +} + +func TestGetCustomBaseDir(t *testing.T) { + root := "/tmp" + os.Setenv("MACHINE_DIR", root) + baseDir := GetBaseDir() + + if strings.Index(root, baseDir) != 0 { + t.Fatalf("expected base dir with prefix %s; received %s", root, baseDir) + } + os.Setenv("MACHINE_DIR", "") +} + +func TestGetDockerDir(t *testing.T) { + root := "/tmp" + os.Setenv("MACHINE_DIR", root) + dockerDir := GetDockerDir() + + if strings.Index(dockerDir, root) != 0 { + t.Fatalf("expected docker dir with prefix %s; received %s", root, dockerDir) + } + + path, filename := path.Split(dockerDir) + if strings.Index(path, root) != 0 { + t.Fatalf("expected base path of %s; received %s", root, path) + } + if filename != ".docker" { + t.Fatalf("expected docker dir \".docker\"; received %s", filename) + } + os.Setenv("MACHINE_DIR", "") +} + +func TestGetMachineDir(t *testing.T) { + root := "/tmp" + os.Setenv("MACHINE_DIR", root) + machineDir := GetMachineDir() + + if strings.Index(machineDir, root) != 0 { + t.Fatalf("expected machine dir with prefix %s; received %s", root, machineDir) + } + + path, filename := path.Split(machineDir) + if strings.Index(path, root) != 0 { + t.Fatalf("expected base path of %s; received %s", root, path) + } + if filename != "machines" { + t.Fatalf("expected machine dir \"machines\"; received %s", filename) + } + os.Setenv("MACHINE_DIR", "") +} + +func TestGetMachineClientCertDir(t *testing.T) { + root := "/tmp" + os.Setenv("MACHINE_DIR", root) + clientDir := GetMachineClientCertDir() + + if strings.Index(clientDir, root) != 0 { + t.Fatalf("expected machine client cert dir with prefix %s; received %s", root, clientDir) + } + + path, filename := path.Split(clientDir) + if strings.Index(path, root) != 0 { + t.Fatalf("expected base path of %s; received %s", root, path) + } + if filename != ".client" { + t.Fatalf("expected machine client dir \".client\"; received %s", filename) + } + os.Setenv("MACHINE_DIR", "") +} + +func TestCopyFile(t *testing.T) { + testStr := "test-machine" + + srcFile, err := ioutil.TempFile("", "machine-test-") + if err != nil { + t.Fatal(err) + } + srcFi, err := srcFile.Stat() + if err != nil { + t.Fatal(err) + } + + srcFile.Write([]byte(testStr)) + srcFile.Close() + + srcFilePath := filepath.Join(os.TempDir(), srcFi.Name()) + + destFile, err := ioutil.TempFile("", "machine-copy-test-") + if err != nil { + t.Fatal(err) + } + + destFi, err := destFile.Stat() + if err != nil { + t.Fatal(err) + } + + destFile.Close() + + destFilePath := filepath.Join(os.TempDir(), destFi.Name()) + + if err := CopyFile(srcFilePath, destFilePath); err != nil { + t.Fatal(err) + } + + data, err := ioutil.ReadFile(destFilePath) + if err != nil { + t.Fatal(err) + } + + if string(data) != testStr { + t.Fatalf("expected data \"%s\"; received \"%\"", testStr, string(data)) + } +} + +func TestGetUsername(t *testing.T) { + currentUser := "unknown" + switch runtime.GOOS { + case "darwin", "linux": + currentUser = os.Getenv("USER") + case "windows": + currentUser = os.Getenv("USERNAME") + } + + username := GetUsername() + if username != currentUser { + t.Fatalf("expected username %s; received %s", currentUser, username) + } +}