From 49feb334571f9486e934806aea23a3efdd136fea Mon Sep 17 00:00:00 2001 From: Nathan LeClaire and Simon Thulborn Date: Mon, 16 Feb 2015 23:16:36 +0000 Subject: [PATCH] Implement majority of provisioning changes Signed-off-by: Simon Thulborn Signed-off-by: Nathan LeClaire --- commands.go | 98 ++-- drivers/amazonec2/amz/ec2.go | 1 + libmachine/auth/auth_options.go | 14 + libmachine/host.go | 553 +++-------------------- libmachine/machine.go | 48 +- libmachine/provision/boot2docker.go | 141 ++++++ libmachine/provision/errors.go | 11 + libmachine/provision/os_release.go | 83 ++++ libmachine/provision/os_release_test.go | 138 ++++++ libmachine/provision/pkgaction/action.go | 43 ++ libmachine/provision/provisioner.go | 90 ++++ libmachine/provision/ubuntu.go | 162 +++++++ libmachine/provision/utils.go | 274 +++++++++++ libmachine/swarm/swarm.go | 6 + libmachine/swarm/swarm_options.go | 1 + utils/utils.go | 13 + 16 files changed, 1096 insertions(+), 580 deletions(-) create mode 100644 libmachine/auth/auth_options.go create mode 100644 libmachine/provision/boot2docker.go create mode 100644 libmachine/provision/errors.go create mode 100644 libmachine/provision/os_release.go create mode 100644 libmachine/provision/os_release_test.go create mode 100644 libmachine/provision/pkgaction/action.go create mode 100644 libmachine/provision/provisioner.go create mode 100644 libmachine/provision/ubuntu.go create mode 100644 libmachine/provision/utils.go create mode 100644 libmachine/swarm/swarm_options.go diff --git a/commands.go b/commands.go index ad36b72943..7fabc9d0f8 100644 --- a/commands.go +++ b/commands.go @@ -30,6 +30,7 @@ import ( _ "github.com/docker/machine/drivers/vmwarevcloudair" _ "github.com/docker/machine/drivers/vmwarevsphere" "github.com/docker/machine/libmachine" + "github.com/docker/machine/libmachine/auth" "github.com/docker/machine/libmachine/engine" "github.com/docker/machine/libmachine/swarm" "github.com/docker/machine/state" @@ -39,26 +40,24 @@ import ( type machineConfig struct { machineName string machineDir string - caCertPath string - caKeyPath string - clientCertPath string + machineUrl string clientKeyPath string serverCertPath string + clientCertPath string + caCertPath string + caKeyPath string serverKeyPath string - machineUrl string - swarmMaster bool - swarmHost string - swarmDiscovery string + AuthConfig auth.AuthOptions + SwarmConfig swarm.SwarmOptions } type hostListItem struct { - Name string - Active bool - DriverName string - State state.State - URL string - SwarmMaster bool - SwarmDiscovery string + Name string + Active bool + DriverName string + State state.State + URL string + SwarmConfig swarm.SwarmOptions } type certPathInfo struct { @@ -406,10 +405,18 @@ func cmdCreate(c *cli.Context) { log.Fatal(err) } - hostOptions := &libmachine.HostOptions{ - DriverOptions: c, - EngineOptions: &engine.EngineOptions{}, - SwarmOptions: &swarm.SwarmOptions{ + hostConfig := &libmachine.HostOptions{ + AuthConfig: &auth.AuthOptions{ + CaCertPath: c.GlobalString("tls-ca-cert"), + PrivateKeyPath: c.GlobalString("tls-ca-key"), + ClientCertPath: c.GlobalString("tls-client-cert"), + ClientKeyPath: filepath.Join(utils.GetMachineCertDir(), "key.pem"), + ServerCertPath: filepath.Join(utils.GetMachineDir(), name, "server.pem"), + ServerKeyPath: filepath.Join(utils.GetMachineDir(), name, "server-key.pem"), + }, + EngineConfig: &engine.EngineOptions{}, + SwarmConfig: &swarm.SwarmOptions{ + IsSwarm: c.Bool("swarm"), Master: c.GlobalBool("swarm-master"), Discovery: c.GlobalString("swarm-discovery"), Address: c.GlobalString("swarm-addr"), @@ -417,7 +424,7 @@ func cmdCreate(c *cli.Context) { }, } - host, err := mcn.Create(name, driver, hostOptions) + host, err := mcn.Create(name, driver, hostConfig, c) if err != nil { log.Errorf("Error creating machine: %s", err) log.Warn("You will want to check the provider to make sure the machine and associated resources were properly removed.") @@ -456,10 +463,10 @@ func cmdConfig(c *cli.Context) { } if c.Bool("swarm") { - if !cfg.swarmMaster { + if !cfg.SwarmConfig.Master { log.Fatalf("%s is not a swarm master", cfg.machineName) } - u, err := url.Parse(cfg.swarmHost) + u, err := url.Parse(cfg.SwarmConfig.Host) if err != nil { log.Fatal(err) } @@ -563,13 +570,14 @@ func cmdLs(c *cli.Context) { swarmInfo := make(map[string]string) for _, host := range hostList { + swarmConfig := host.HostConfig.SwarmConfig if !quiet { - if host.SwarmOptions.Master { - swarmMasters[host.SwarmOptions.Discovery] = host.Name + if swarmConfig.Master { + swarmMasters[swarmConfig.Discovery] = host.Name } - if host.SwarmOptions.Discovery != "" { - swarmInfo[host.Name] = host.SwarmOptions.Discovery + if swarmConfig.Discovery != "" { + swarmInfo[host.Name] = swarmConfig.Discovery } go getHostState(*host, defaultStore, hostListItems) @@ -596,9 +604,9 @@ func cmdLs(c *cli.Context) { swarmInfo := "" - if item.SwarmDiscovery != "" { - swarmInfo = swarmMasters[item.SwarmDiscovery] - if item.SwarmMaster { + if item.SwarmConfig.Discovery != "" { + swarmInfo = swarmMasters[item.SwarmConfig.Discovery] + if item.SwarmConfig.Master { swarmInfo = fmt.Sprintf("%s (master)", swarmInfo) } } @@ -674,10 +682,10 @@ func cmdEnv(c *cli.Context) { dockerHost := cfg.machineUrl if c.Bool("swarm") { - if !cfg.swarmMaster { + if !cfg.SwarmConfig.Master { log.Fatalf("%s is not a swarm master", cfg.machineName) } - u, err := url.Parse(cfg.swarmHost) + u, err := url.Parse(cfg.SwarmConfig.Host) if err != nil { log.Fatal(err) } @@ -1032,13 +1040,12 @@ func getHostState(host libmachine.Host, store libmachine.Store, hostListItems ch } hostListItems <- hostListItem{ - Name: host.Name, - Active: isActive, - DriverName: host.Driver.DriverName(), - State: currentState, - URL: url, - SwarmMaster: host.SwarmOptions.Master, - SwarmDiscovery: host.SwarmOptions.Discovery, + Name: host.Name, + Active: isActive, + DriverName: host.Driver.DriverName(), + State: currentState, + URL: url, + SwarmConfig: *host.HostConfig.SwarmConfig, } } @@ -1096,16 +1103,15 @@ func getMachineConfig(c *cli.Context) (*machineConfig, error) { return &machineConfig{ machineName: name, machineDir: machineDir, - caCertPath: caCert, - caKeyPath: caKey, - clientCertPath: clientCert, - clientKeyPath: clientKey, - serverCertPath: serverCert, - serverKeyPath: serverKey, machineUrl: machineUrl, - swarmMaster: machine.SwarmOptions.Master, - swarmHost: machine.SwarmOptions.Host, - swarmDiscovery: machine.SwarmOptions.Discovery, + clientKeyPath: clientKey, + clientCertPath: clientCert, + serverCertPath: serverCert, + caKeyPath: caKey, + caCertPath: caCert, + serverKeyPath: serverKey, + AuthConfig: *machine.HostConfig.AuthConfig, + SwarmConfig: *machine.HostConfig.SwarmConfig, }, nil } diff --git a/drivers/amazonec2/amz/ec2.go b/drivers/amazonec2/amz/ec2.go index 6a8b40470c..3bcde2a78f 100644 --- a/drivers/amazonec2/amz/ec2.go +++ b/drivers/amazonec2/amz/ec2.go @@ -158,6 +158,7 @@ func (e *EC2) awsApiCall(v url.Values) (*http.Response, error) { fmt.Printf("client encountered error while doing the request: %s", err.Error()) return resp, fmt.Errorf("client encountered error while doing the request: %s", err) } + if resp.StatusCode != http.StatusOK { return resp, newAwsApiResponseError(*resp) } diff --git a/libmachine/auth/auth_options.go b/libmachine/auth/auth_options.go new file mode 100644 index 0000000000..ef0bf583ed --- /dev/null +++ b/libmachine/auth/auth_options.go @@ -0,0 +1,14 @@ +package auth + +type AuthOptions struct { + StorePath string + CaCertPath string + CaCertRemotePath string + ServerCertPath string + ServerKeyPath string + ClientKeyPath string + ServerCertRemotePath string + ServerKeyRemotePath string + PrivateKeyPath string + ClientCertPath string +} diff --git a/libmachine/host.go b/libmachine/host.go index 3010413ebe..20c1e1d21d 100644 --- a/libmachine/host.go +++ b/libmachine/host.go @@ -1,26 +1,20 @@ package libmachine import ( - "bytes" "encoding/json" "fmt" "io/ioutil" - "net" - "net/url" "os" "os/exec" - "path" "path/filepath" "regexp" - "strconv" - "strings" - "time" log "github.com/Sirupsen/logrus" "github.com/docker/machine/drivers" + "github.com/docker/machine/libmachine/auth" "github.com/docker/machine/libmachine/engine" + "github.com/docker/machine/libmachine/provision" "github.com/docker/machine/libmachine/swarm" - "github.com/docker/machine/provider" "github.com/docker/machine/ssh" "github.com/docker/machine/state" "github.com/docker/machine/utils" @@ -31,74 +25,53 @@ var ( validHostNamePattern = regexp.MustCompile(`^` + validHostNameChars + `+$`) ) -const ( - swarmDockerImage = "swarm:latest" - swarmDiscoveryServiceEndpoint = "https://discovery-stage.hub.docker.com/v1" -) - type Host struct { - Name string `json:"-"` - DriverName string - Driver drivers.Driver + Name string `json:"-"` + DriverName string + Driver drivers.Driver + StorePath string + EngineOptions *engine.EngineOptions + SwarmOptions *swarm.SwarmOptions + HostConfig HostOptions + + // deprecated options; these are left to assist in config migrations + SwarmHost string + SwarmMaster bool + SwarmDiscovery string CaCertPath string PrivateKeyPath string ServerCertPath string ServerKeyPath string ClientCertPath string - StorePath string - EngineOptions *engine.EngineOptions - SwarmOptions *swarm.SwarmOptions - // deprecated options; these are left to assist in config migrations - SwarmHost string - SwarmMaster bool - SwarmDiscovery string } type HostOptions struct { - Driver string - Memory int - Disk int - DriverOptions drivers.DriverOptions - EngineOptions *engine.EngineOptions - SwarmOptions *swarm.SwarmOptions + Driver string + Memory int + Disk int + EngineConfig *engine.EngineOptions + SwarmConfig *swarm.SwarmOptions + AuthConfig *auth.AuthOptions } -type DockerConfig struct { - EngineConfig string - EngineConfigPath string -} - -type hostConfig struct { +type HostMetadata struct { DriverName string + HostConfig HostOptions } -func waitForDocker(addr string) error { - for { - conn, err := net.DialTimeout("tcp", addr, time.Second*5) - if err != nil { - time.Sleep(time.Second * 5) - continue - } - conn.Close() - break - } - return nil -} - -func NewHost(name, driverName, StorePath, caCert, privateKey string, engineOptions *engine.EngineOptions, swarmOptions *swarm.SwarmOptions) (*Host, error) { - driver, err := drivers.NewDriver(driverName, name, StorePath, caCert, privateKey) +func NewHost(name, driverName string, hostConfig HostOptions) (*Host, error) { + authConfig := hostConfig.AuthConfig + storePath := filepath.Join(utils.GetMachineDir(), name) + driver, err := drivers.NewDriver(driverName, name, storePath, authConfig.CaCertPath, authConfig.PrivateKeyPath) if err != nil { return nil, err } return &Host{ - Name: name, - DriverName: driverName, - Driver: driver, - CaCertPath: caCert, - PrivateKeyPath: privateKey, - EngineOptions: engineOptions, - SwarmOptions: swarmOptions, - StorePath: StorePath, + Name: name, + DriverName: driverName, + Driver: driver, + StorePath: storePath, + HostConfig: hostConfig, }, nil } @@ -121,373 +94,7 @@ func ValidateHostName(name string) (string, error) { return name, nil } -func (h *Host) GetDockerConfigDir() (string, error) { - // TODO: this will be refactored in https://github.com/docker/machine/issues/699 - switch h.Driver.GetProviderType() { - case provider.Local: - return "/var/lib/boot2docker", nil - case provider.Remote: - return "/etc/docker", nil - case provider.None: - return "", nil - default: - return "", ErrUnknownProviderType - } -} - -func (h *Host) ConfigureSwarm(discovery string, master bool, host string, addr string) error { - d := h.Driver - - if d.DriverName() == "none" { - return nil - } - - if addr == "" { - ip, err := d.GetIP() - if err != nil { - return err - } - // TODO: remove hardcoded port - addr = fmt.Sprintf("%s:2376", ip) - } - - basePath, err := h.GetDockerConfigDir() - if err != nil { - return err - } - - tlsCaCert := path.Join(basePath, "ca.pem") - tlsCert := path.Join(basePath, "server.pem") - tlsKey := path.Join(basePath, "server-key.pem") - masterArgs := fmt.Sprintf("--tlsverify --tlscacert=%s --tlscert=%s --tlskey=%s -H %s %s", - tlsCaCert, tlsCert, tlsKey, host, discovery) - nodeArgs := fmt.Sprintf("--addr %s %s", addr, discovery) - - u, err := url.Parse(host) - if err != nil { - return err - } - - parts := strings.Split(u.Host, ":") - port := parts[1] - - if err := waitForDocker(addr); err != nil { - return err - } - - cmd, err := h.GetSSHCommand(fmt.Sprintf("sudo docker pull %s", swarmDockerImage)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - dockerDir, err := h.GetDockerConfigDir() - if err != nil { - return err - } - - // if master start master agent - if master { - log.Debug("launching swarm master") - log.Debugf("master args: %s", masterArgs) - cmd, err = h.GetSSHCommand(fmt.Sprintf("sudo docker run -d -p %s:%s --restart=always --name swarm-agent-master -v %s:%s %s manage %s", - port, port, dockerDir, dockerDir, swarmDockerImage, masterArgs)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - } - - // start node agent - log.Debug("launching swarm node") - log.Debugf("node args: %s", nodeArgs) - cmd, err = h.GetSSHCommand(fmt.Sprintf("sudo docker run -d --restart=always --name swarm-agent -v %s:%s %s join %s", - dockerDir, dockerDir, swarmDockerImage, nodeArgs)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - return nil -} - -func (h *Host) StartDocker() error { - log.Debug("Starting Docker...") - - var ( - cmd *exec.Cmd - err error - ) - - switch h.Driver.GetProviderType() { - case provider.Local: - cmd, err = h.GetSSHCommand("sudo /etc/init.d/docker start") - case provider.Remote: - cmd, err = h.GetSSHCommand("sudo service docker start") - default: - return ErrUnknownProviderType - } - - if err != nil { - return err - } - - if err := cmd.Run(); err != nil { - return err - } - - return nil -} - -func (h *Host) StopDocker() error { - log.Debug("Stopping Docker...") - - var ( - cmd *exec.Cmd - err error - ) - - switch h.Driver.GetProviderType() { - case provider.Local: - cmd, err = h.GetSSHCommand("if [ -e /var/run/docker.pid ] && [ -d /proc/$(cat /var/run/docker.pid) ]; then sudo /etc/init.d/docker stop ; exit 0; fi") - case provider.Remote: - cmd, err = h.GetSSHCommand("sudo service docker stop") - default: - return ErrUnknownProviderType - } - - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - return nil -} - -func (h *Host) ConfigureAuth() error { - d := h.Driver - - if d.DriverName() == "none" { - return nil - } - - // copy certs to client dir for docker client - machineDir := filepath.Join(utils.GetMachineDir(), h.Name) - if err := utils.CopyFile(h.CaCertPath, filepath.Join(machineDir, "ca.pem")); err != nil { - log.Fatalf("Error copying ca.pem to machine dir: %s", err) - } - - clientCertPath := filepath.Join(utils.GetMachineCertDir(), "cert.pem") - if err := utils.CopyFile(clientCertPath, filepath.Join(machineDir, "cert.pem")); err != nil { - log.Fatalf("Error copying cert.pem to machine dir: %s", err) - } - - clientKeyPath := filepath.Join(utils.GetMachineCertDir(), "key.pem") - if err := utils.CopyFile(clientKeyPath, filepath.Join(machineDir, "key.pem")); err != nil { - log.Fatalf("Error copying key.pem to machine dir: %s", err) - } - - var ( - ip = "" - ipErr error - maxRetries = 4 - ) - - for i := 0; i < maxRetries; i++ { - ip, ipErr = h.Driver.GetIP() - if ip != "" { - break - } - log.Debugf("waiting for ip: %s", ipErr) - time.Sleep(5 * time.Second) - } - - if ipErr != nil { - return ipErr - } - - if ip == "" { - return fmt.Errorf("unable to get machine IP") - } - - serverCertPath := filepath.Join(h.StorePath, "server.pem") - serverKeyPath := filepath.Join(h.StorePath, "server-key.pem") - - org := h.Name - bits := 2048 - - log.Debugf("generating server cert: %s ca-key=%s private-key=%s org=%s", - serverCertPath, - h.CaCertPath, - h.PrivateKeyPath, - org, - ) - - if err := utils.GenerateCert([]string{ip}, serverCertPath, serverKeyPath, h.CaCertPath, h.PrivateKeyPath, org, bits); err != nil { - return fmt.Errorf("error generating server cert: %s", err) - } - - if err := h.StopDocker(); err != nil { - return err - } - - dockerDir, err := h.GetDockerConfigDir() - if err != nil { - return err - } - - cmd, err := h.GetSSHCommand(fmt.Sprintf("sudo mkdir -p %s", dockerDir)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - // upload certs and configure TLS auth - caCert, err := ioutil.ReadFile(h.CaCertPath) - if err != nil { - return err - } - - // due to windows clients, we cannot use filepath.Join as the paths - // will be mucked on the linux hosts - machineCaCertPath := path.Join(dockerDir, "ca.pem") - - serverCert, err := ioutil.ReadFile(serverCertPath) - if err != nil { - return err - } - machineServerCertPath := path.Join(dockerDir, "server.pem") - - serverKey, err := ioutil.ReadFile(serverKeyPath) - if err != nil { - return err - } - machineServerKeyPath := path.Join(dockerDir, "server-key.pem") - - cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(caCert), machineCaCertPath)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(serverKey), machineServerKeyPath)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(serverCert), machineServerCertPath)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - dockerUrl, err := h.Driver.GetURL() - if err != nil { - return err - } - u, err := url.Parse(dockerUrl) - if err != nil { - return err - } - dockerPort := 2376 - parts := strings.Split(u.Host, ":") - if len(parts) == 2 { - dPort, err := strconv.Atoi(parts[1]) - if err != nil { - return err - } - dockerPort = dPort - } - - cfg, err := h.generateDockerConfig(dockerPort, machineCaCertPath, machineServerKeyPath, machineServerCertPath) - if err != nil { - return err - } - - cmd, err = h.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %s", cfg.EngineConfig, cfg.EngineConfigPath)) - if err != nil { - return err - } - if err := cmd.Run(); err != nil { - return err - } - - if err := h.StartDocker(); err != nil { - return err - } - - return nil -} - -func (h *Host) generateDockerConfig(dockerPort int, caCertPath string, serverKeyPath string, serverCertPath string) (*DockerConfig, error) { - d := h.Driver - var ( - daemonOpts string - daemonOptsCfg string - daemonCfg string - swarmLabels = []string{} - ) - - swarmLabels = append(swarmLabels, fmt.Sprintf("--label=provider=%s", h.Driver.DriverName())) - - defaultDaemonOpts := fmt.Sprintf(`--tlsverify --tlscacert=%s --tlskey=%s --tlscert=%s %s`, - caCertPath, - serverKeyPath, - serverCertPath, - strings.Join(swarmLabels, " "), - ) - - dockerDir, err := h.GetDockerConfigDir() - if err != nil { - return nil, err - } - - switch d.DriverName() { - case "virtualbox", "vmwarefusion", "vmwarevsphere", "hyper-v": - daemonOpts = fmt.Sprintf("-H tcp://0.0.0.0:%d", dockerPort) - daemonOptsCfg = path.Join(dockerDir, "profile") - opts := fmt.Sprintf("%s %s", defaultDaemonOpts, daemonOpts) - daemonCfg = fmt.Sprintf(`EXTRA_ARGS='%s' -CACERT=%s -SERVERCERT=%s -SERVERKEY=%s -DOCKER_TLS=no`, opts, caCertPath, serverKeyPath, serverCertPath) - default: - daemonOpts = fmt.Sprintf("--host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:%d", dockerPort) - daemonOptsCfg = "/etc/default/docker" - opts := fmt.Sprintf("%s %s", defaultDaemonOpts, daemonOpts) - daemonCfg = fmt.Sprintf("export DOCKER_OPTS=\\\"%s\\\"", opts) - } - - return &DockerConfig{ - EngineConfig: daemonCfg, - EngineConfigPath: daemonOptsCfg, - }, nil -} - func (h *Host) Create(name string) error { - name, err := ValidateHostName(name) - if err != nil { - return err - } - // create the instance if err := h.Driver.Create(); err != nil { return err @@ -498,45 +105,17 @@ func (h *Host) Create(name string) error { return err } - // set hostname - if err := h.SetHostname(); err != nil { - return err - } - - // install docker - if err := h.Provision(); err != nil { - return err - } - - return nil -} - -func (h *Host) Provision() error { - // "local" providers use b2d; no provisioning necessary - switch h.Driver.DriverName() { - case "none", "virtualbox", "vmwarefusion", "vmwarevsphere": - return nil - } - if err := WaitForSSH(h); err != nil { return err } - // install docker - until cloudinit we use ubuntu everywhere so we - // just install it using the docker repos - cmd, err := h.GetSSHCommand("if [ ! -e /usr/bin/docker ]; then curl -sSL https://get.docker.com | sh -; fi") + provisioner, err := provision.DetectProvisioner(h.Driver) if err != nil { return err } - // HACK: the script above will output debug to stderr; we save it and - // then check if the command returned an error; if so, we show the debug - - var buf bytes.Buffer - cmd.Stderr = &buf - - if err := cmd.Run(); err != nil { - return fmt.Errorf("error installing docker: %s\n%s\n", err, string(buf.Bytes())) + if err := provisioner.Provision(*h.HostConfig.SwarmConfig, *h.HostConfig.AuthConfig); err != nil { + return err } return nil @@ -561,48 +140,6 @@ func (h *Host) GetSSHCommand(args ...string) (*exec.Cmd, error) { return cmd, nil } -func (h *Host) SetHostname() error { - var ( - cmd *exec.Cmd - err error - ) - - log.Debugf("setting hostname for provider type %s: %s", - h.Driver.GetProviderType(), - h.Name, - ) - - switch h.Driver.GetProviderType() { - case provider.None: - return nil - case provider.Local: - cmd, err = h.GetSSHCommand(fmt.Sprintf( - "sudo hostname %s && echo \"%s\" | sudo tee /var/lib/boot2docker/etc/hostname", - h.Name, - h.Name, - )) - case provider.Remote: - cmd, err = h.GetSSHCommand(fmt.Sprintf( - "echo \"127.0.0.1 %s\" | sudo tee -a /etc/hosts && sudo hostname %s && echo \"%s\" | sudo tee /etc/hostname", - h.Name, - h.Name, - h.Name, - )) - default: - return ErrUnknownProviderType - } - - if err != nil { - return err - } - - if err := cmd.Run(); err != nil { - return err - } - - return nil -} - func (h *Host) MachineInState(desiredState state.State) func() bool { return func() bool { currentState, err := h.Driver.GetState() @@ -719,15 +256,18 @@ func (h *Host) LoadConfig() error { } // First pass: find the driver name and load the driver - var config hostConfig - if err := json.Unmarshal(data, &config); err != nil { + var hostMetadata HostMetadata + if err := json.Unmarshal(data, &hostMetadata); err != nil { return err } - driver, err := drivers.NewDriver(config.DriverName, h.Name, h.StorePath, h.CaCertPath, h.PrivateKeyPath) + authConfig := hostMetadata.HostConfig.AuthConfig + + driver, err := drivers.NewDriver(hostMetadata.DriverName, h.Name, h.StorePath, authConfig.CaCertPath, authConfig.PrivateKeyPath) if err != nil { return err } + h.Driver = driver // Second pass: unmarshal driver config into correct driver @@ -738,6 +278,19 @@ func (h *Host) LoadConfig() error { return nil } +func (h *Host) ConfigureAuth() error { + provisioner, err := provision.DetectProvisioner(h.Driver) + if err != nil { + return err + } + + if err := provision.ConfigureAuth(provisioner, *h.HostConfig.AuthConfig); err != nil { + return err + } + + return nil +} + func (h *Host) SaveConfig() error { data, err := json.Marshal(h) if err != nil { diff --git a/libmachine/machine.go b/libmachine/machine.go index 28b6a39019..4a27367e55 100644 --- a/libmachine/machine.go +++ b/libmachine/machine.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - log "github.com/Sirupsen/logrus" + "github.com/docker/machine/drivers" "github.com/docker/machine/utils" ) @@ -19,10 +19,16 @@ func New(store Store) (*Machine, error) { }, nil } -func (m *Machine) Create(name string, driverName string, options *HostOptions) (*Host, error) { - driverOptions := options.DriverOptions - engineOptions := options.EngineOptions - swarmOptions := options.SwarmOptions +func (m *Machine) Create(name string, driverName string, options *HostOptions, driverConfig drivers.DriverOptions) (*Host, error) { + engineConfig := options.EngineConfig + swarmConfig := options.SwarmConfig + authConfig := options.AuthConfig + + hostConfig := HostOptions{ + AuthConfig: authConfig, + EngineConfig: engineConfig, + SwarmConfig: swarmConfig, + } exists, err := m.store.Exists(name) if err != nil { @@ -34,22 +40,12 @@ func (m *Machine) Create(name string, driverName string, options *HostOptions) ( hostPath := filepath.Join(utils.GetMachineDir(), name) - caCert, err := m.store.GetCACertificatePath() - if err != nil { - return nil, err - } - - privateKey, err := m.store.GetPrivateKeyPath() - if err != nil { - return nil, err - } - - host, err := NewHost(name, driverName, hostPath, caCert, privateKey, engineOptions, swarmOptions) + host, err := NewHost(name, driverName, hostConfig) if err != nil { return host, err } - if driverOptions != nil { - if err := host.Driver.SetConfigFromFlags(driverOptions); err != nil { + if driverConfig != nil { + if err := host.Driver.SetConfigFromFlags(driverConfig); err != nil { return host, err } } @@ -70,22 +66,6 @@ func (m *Machine) Create(name string, driverName string, options *HostOptions) ( return host, err } - if err := host.ConfigureAuth(); err != nil { - return host, err - } - - if swarmOptions.Host != "" { - log.Info("Configuring Swarm...") - - discovery := swarmOptions.Discovery - master := swarmOptions.Master - swarmHost := swarmOptions.Host - addr := swarmOptions.Address - if err := host.ConfigureSwarm(discovery, master, swarmHost, addr); err != nil { - log.Errorf("Error configuring Swarm: %s", err) - } - } - if err := m.store.SetActive(host); err != nil { return nil, err } diff --git a/libmachine/provision/boot2docker.go b/libmachine/provision/boot2docker.go new file mode 100644 index 0000000000..c56cd276bd --- /dev/null +++ b/libmachine/provision/boot2docker.go @@ -0,0 +1,141 @@ +package provision + +import ( + "bytes" + "fmt" + "os/exec" + "path" + + "github.com/docker/machine/drivers" + "github.com/docker/machine/libmachine/auth" + "github.com/docker/machine/libmachine/provision/pkgaction" + "github.com/docker/machine/libmachine/swarm" +) + +func init() { + Register("boot2docker", &RegisteredProvisioner{ + New: NewBoot2DockerProvisioner, + }) +} + +func NewBoot2DockerProvisioner(d drivers.Driver) Provisioner { + return &Boot2DockerProvisioner{ + Driver: d, + } +} + +type Boot2DockerProvisioner struct { + OsReleaseInfo *OsRelease + Driver drivers.Driver + SwarmConfig swarm.SwarmOptions +} + +func (provisioner *Boot2DockerProvisioner) Service(name string, action pkgaction.ServiceAction) error { + var ( + cmd *exec.Cmd + err error + ) + if name == "docker" && action == pkgaction.Stop { + cmd, err = provisioner.SSHCommand("if [ -e /var/run/docker.pid ] && [ -d /proc/$(cat /var/run/docker.pid) ]; then sudo /etc/init.d/docker stop ; exit 0; fi") + } else { + cmd, err = provisioner.SSHCommand(fmt.Sprintf("sudo /etc/init.d/%s %s", name, action.String())) + if err != nil { + return err + } + } + if err := cmd.Run(); err != nil { + return err + } + return nil +} + +func (provisioner *Boot2DockerProvisioner) Package(name string, action pkgaction.PackageAction) error { + return nil +} + +func (provisioner *Boot2DockerProvisioner) Hostname() (string, error) { + cmd, err := provisioner.SSHCommand(fmt.Sprintf("hostname")) + if err != nil { + return "", err + } + + var so bytes.Buffer + cmd.Stdout = &so + + if err := cmd.Run(); err != nil { + return "", err + } + + return so.String(), nil +} + +func (provisioner *Boot2DockerProvisioner) SetHostname(hostname string) error { + cmd, err := provisioner.SSHCommand(fmt.Sprintf( + "sudo hostname %s && echo %q | sudo tee /var/lib/boot2docker/etc/hostname", + hostname, + hostname, + )) + if err != nil { + return err + } + + return cmd.Run() +} + +func (provisioner *Boot2DockerProvisioner) GetDockerConfigDir() string { + return "/var/lib/boot2docker" +} + +func (provisioner *Boot2DockerProvisioner) GenerateDockerConfig(dockerPort int, authConfig auth.AuthOptions) (*DockerConfig, error) { + defaultDaemonOpts := getDefaultDaemonOpts(provisioner.Driver.DriverName(), authConfig) + daemonOpts := fmt.Sprintf("-H tcp://0.0.0.0:%d", dockerPort) + daemonOptsCfg := path.Join(provisioner.GetDockerConfigDir(), "profile") + opts := fmt.Sprintf("%s %s", defaultDaemonOpts, daemonOpts) + daemonCfg := fmt.Sprintf(`EXTRA_ARGS='%s' +CACERT=%s +SERVERCERT=%s +SERVERKEY=%s +DOCKER_TLS=no`, opts, authConfig.CaCertRemotePath, authConfig.ServerKeyRemotePath, authConfig.ServerCertRemotePath) + return &DockerConfig{ + EngineConfig: daemonCfg, + EngineConfigPath: daemonOptsCfg, + }, nil +} + +func (provisioner *Boot2DockerProvisioner) CompatibleWithHost() bool { + return provisioner.OsReleaseInfo.Id == "boot2docker" +} + +func (provisioner *Boot2DockerProvisioner) SetOsReleaseInfo(info *OsRelease) { + provisioner.OsReleaseInfo = info +} + +func (provisioner *Boot2DockerProvisioner) Provision(swarmConfig swarm.SwarmOptions, authConfig auth.AuthOptions) error { + fmt.Println("before set hostname") + if err := provisioner.SetHostname(provisioner.Driver.GetMachineName()); err != nil { + return err + } + fmt.Println("after set hostname") + + if err := installDockerGeneric(provisioner); err != nil { + return err + } + + if err := ConfigureAuth(provisioner, authConfig); err != nil { + return err + } + + if err := configureSwarm(provisioner, swarmConfig); err != nil { + return err + } + + return nil +} + +func (provisioner *Boot2DockerProvisioner) SSHCommand(args ...string) (*exec.Cmd, error) { + return drivers.GetSSHCommandFromDriver(provisioner.Driver, args...) +} + +func (provisioner *Boot2DockerProvisioner) GetDriver() drivers.Driver { + return provisioner.Driver +} diff --git a/libmachine/provision/errors.go b/libmachine/provision/errors.go new file mode 100644 index 0000000000..b3b2caa301 --- /dev/null +++ b/libmachine/provision/errors.go @@ -0,0 +1,11 @@ +package provision + +import ( + "errors" +) + +var ( + ErrDetectionFailed = errors.New("OS type not recognized") + ErrSSHCommandFailed = errors.New("SSH command failure") + ErrNotImplemented = errors.New("Runtime not implemented") +) diff --git a/libmachine/provision/os_release.go b/libmachine/provision/os_release.go new file mode 100644 index 0000000000..61b91b6260 --- /dev/null +++ b/libmachine/provision/os_release.go @@ -0,0 +1,83 @@ +package provision + +import ( + "bufio" + "bytes" + "fmt" + "reflect" + "strings" + + log "github.com/Sirupsen/logrus" +) + +// The /etc/os-release file contains operating system identification data +// See http://www.freedesktop.org/software/systemd/man/os-release.html for more details + +// Values in this struct must always be string +// or the reflection will not work properly. +type OsRelease struct { + AnsiColor string `osr:"ANSI_COLOR"` + Name string `osr:"NAME"` + Version string `osr:"VERSION"` + Id string `osr:"ID"` + IdLike string `osr:"ID_LIKE"` + PrettyName string `osr:"PRETTY_NAME"` + VersionId string `osr:"VERSION_ID"` + HomeUrl string `osr:"HOME_URL"` + SupportUrl string `osr:"SUPPORT_URL"` + BugReportUrl string `osr:"BUG_REPORT_URL"` +} + +func stripQuotes(val string) string { + if val[0] == '"' { + return val[1 : len(val)-1] + } + return val +} + +func (osr *OsRelease) setIfPossible(key, val string) error { + v := reflect.ValueOf(osr).Elem() + for i := 0; i < v.NumField(); i++ { + fieldValue := v.Field(i) + fieldType := v.Type().Field(i) + originalName := fieldType.Tag.Get("osr") + if key == originalName && fieldValue.Kind() == reflect.String { + fieldValue.SetString(val) + return nil + } + } + return fmt.Errorf("Couldn't set key %s, no corresponding struct field found", key) +} + +func parseLine(osrLine string) (string, string, error) { + vals := strings.Split(osrLine, "=") + if len(vals) != 2 { + return "", "", fmt.Errorf("Expected %s to split by '=' char into two strings, instead got %d strings", osrLine, len(vals)) + } + key := vals[0] + val := stripQuotes(vals[1]) + return key, val, nil +} + +func (osr *OsRelease) ParseOsRelease(osReleaseContents []byte) error { + r := bytes.NewReader(osReleaseContents) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + key, val, err := parseLine(scanner.Text()) + if err != nil { + return err + } + if err := osr.setIfPossible(key, val); err != nil { + log.Debug(err) + } + } + return nil +} + +func NewOsRelease(contents []byte) (*OsRelease, error) { + osr := &OsRelease{} + if err := osr.ParseOsRelease(contents); err != nil { + return nil, err + } + return osr, nil +} diff --git a/libmachine/provision/os_release_test.go b/libmachine/provision/os_release_test.go new file mode 100644 index 0000000000..b273cc6295 --- /dev/null +++ b/libmachine/provision/os_release_test.go @@ -0,0 +1,138 @@ +package provision + +import ( + "reflect" + "testing" +) + +func TestParseOsRelease(t *testing.T) { + // These example osr files stolen shamelessly from + // https://github.com/docker/docker/blob/master/pkg/parsers/operatingsystem/operatingsystem_test.go + // cheers @tiborvass + var ( + ubuntuTrusty = []byte(`NAME="Ubuntu" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +PRETTY_NAME="Ubuntu 14.04 LTS" +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`) + gentoo = []byte(`NAME=Gentoo +ID=gentoo +PRETTY_NAME="Gentoo/Linux" +ANSI_COLOR="1;32" +HOME_URL="http://www.gentoo.org/" +SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" +BUG_REPORT_URL="https://bugs.gentoo.org/" +`) + noPrettyName = []byte(`NAME="Ubuntu" +VERSION="14.04, Trusty Tahr" +ID=ubuntu +ID_LIKE=debian +VERSION_ID="14.04" +HOME_URL="http://www.ubuntu.com/" +SUPPORT_URL="http://help.ubuntu.com/" +BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"`) + ) + + osr, err := NewOsRelease(ubuntuTrusty) + if err != nil { + t.Fatal("Unexpected error parsing os release: %s", err) + } + + expectedOsr := OsRelease{ + AnsiColor: "", + Name: "Ubuntu", + Version: "14.04, Trusty Tahr", + Id: "ubuntu", + IdLike: "debian", + PrettyName: "Ubuntu 14.04 LTS", + VersionId: "14.04", + HomeUrl: "http://www.ubuntu.com/", + SupportUrl: "http://help.ubuntu.com/", + BugReportUrl: "http://bugs.launchpad.net/ubuntu/", + } + + if !reflect.DeepEqual(*osr, expectedOsr) { + t.Fatal("Error with ubuntu osr parsing: structs do not match") + } + + osr, err = NewOsRelease(gentoo) + if err != nil { + t.Fatal("Unexpected error parsing os release: %s", err) + } + + expectedOsr = OsRelease{ + AnsiColor: "1;32", + Name: "Gentoo", + Version: "", + Id: "gentoo", + IdLike: "", + PrettyName: "Gentoo/Linux", + VersionId: "", + HomeUrl: "http://www.gentoo.org/", + SupportUrl: "http://www.gentoo.org/main/en/support.xml", + BugReportUrl: "https://bugs.gentoo.org/", + } + + if !reflect.DeepEqual(*osr, expectedOsr) { + t.Fatal("Error with gentoo osr parsing: structs do not match") + } + + osr, err = NewOsRelease(noPrettyName) + if err != nil { + t.Fatal("Unexpected error parsing os release: %s", err) + } + + expectedOsr = OsRelease{ + AnsiColor: "", + Name: "Ubuntu", + Version: "14.04, Trusty Tahr", + Id: "ubuntu", + IdLike: "debian", + PrettyName: "", + VersionId: "14.04", + HomeUrl: "http://www.ubuntu.com/", + SupportUrl: "http://help.ubuntu.com/", + BugReportUrl: "http://bugs.launchpad.net/ubuntu/", + } + + if !reflect.DeepEqual(*osr, expectedOsr) { + t.Fatal("Error with noPrettyName osr parsing: structs do not match") + } +} + +func TestParseLine(t *testing.T) { + var ( + withQuotes = "ID=\"ubuntu\"" + withoutQuotes = "ID=gentoo" + wtf = "LOTS=OF=EQUALS" + ) + + key, val, err := parseLine(withQuotes) + if key != "ID" { + t.Fatalf("Expected ID, got %s", key) + } + if val != "ubuntu" { + t.Fatalf("Expected ubuntu, got %s", val) + } + if err != nil { + t.Fatalf("Got error on parseLine with quotes: %s", err) + } + key, val, err = parseLine(withoutQuotes) + if key != "ID" { + t.Fatalf("Expected ID, got %s", key) + } + if val != "gentoo" { + t.Fatalf("Expected gentoo, got %s", val) + } + if err != nil { + t.Fatalf("Got error on parseLine without quotes: %s", err) + } + key, val, err = parseLine(wtf) + if err == nil { + t.Fatal("Expected to get an error on parseLine, got nil") + } +} diff --git a/libmachine/provision/pkgaction/action.go b/libmachine/provision/pkgaction/action.go new file mode 100644 index 0000000000..af5b29770a --- /dev/null +++ b/libmachine/provision/pkgaction/action.go @@ -0,0 +1,43 @@ +package pkgaction + +type ServiceAction int + +const ( + Restart ServiceAction = iota + Start + Stop +) + +var serviceActions = []string{ + "restart", + "start", + "stop", +} + +func (s ServiceAction) String() string { + if int(s) >= 0 && int(s) < len(serviceActions) { + return serviceActions[s] + } + + return "" +} + +type PackageAction int + +const ( + Install PackageAction = iota + Remove +) + +var packageActions = []string{ + "install", + "remove", +} + +func (s PackageAction) String() string { + if int(s) >= 0 && int(s) < len(packageActions) { + return packageActions[s] + } + + return "" +} diff --git a/libmachine/provision/provisioner.go b/libmachine/provision/provisioner.go new file mode 100644 index 0000000000..a50f3ead9d --- /dev/null +++ b/libmachine/provision/provisioner.go @@ -0,0 +1,90 @@ +package provision + +import ( + "bytes" + "fmt" + "os/exec" + + "github.com/docker/machine/drivers" + "github.com/docker/machine/libmachine/auth" + "github.com/docker/machine/libmachine/provision/pkgaction" + "github.com/docker/machine/libmachine/swarm" +) + +var provisioners = make(map[string]*RegisteredProvisioner) + +// Distribution specific actions +type Provisioner interface { + GenerateDockerConfig(dockerPort int, authConfig auth.AuthOptions) (*DockerConfig, error) + + GetDockerConfigDir() string + + // Run a package action + Package(name string, action pkgaction.PackageAction) error + + // Hostname + Hostname() (string, error) + + // Set hostname + SetHostname(hostname string) error + + // Detection function + CompatibleWithHost() bool + + Provision(swarmConfig swarm.SwarmOptions, authConfig auth.AuthOptions) error + + // Perform action on a named service + Service(name string, action pkgaction.ServiceAction) error + + GetDriver() drivers.Driver + + SSHCommand(args ...string) (*exec.Cmd, error) + + // Set the OS Release info depending on how it's represented + // internally + SetOsReleaseInfo(info *OsRelease) +} + +// Detection +type RegisteredProvisioner struct { + New func(d drivers.Driver) Provisioner +} + +func Register(name string, p *RegisteredProvisioner) { + provisioners[name] = p +} + +func DetectProvisioner(d drivers.Driver) (Provisioner, error) { + var ( + osReleaseOut bytes.Buffer + ) + catOsReleaseCmd, err := drivers.GetSSHCommandFromDriver(d, "cat /etc/os-release") + if err != nil { + return nil, fmt.Errorf("Error getting SSH command: %s", err) + } + + // Normally I would just use Output() for this, but d.GetSSHCommand + // defaults to sending the output of the command to stdout in debug + // mode, so that will be broken if we don't set it ourselves. + catOsReleaseCmd.Stdout = &osReleaseOut + + if err := catOsReleaseCmd.Run(); err != nil { + return nil, fmt.Errorf("Error running SSH command to get /etc/os-release: %s", err) + } + + osReleaseInfo, err := NewOsRelease(osReleaseOut.Bytes()) + if err != nil { + return nil, fmt.Errorf("Error parsing /etc/os-release file: %s", err) + } + + for _, p := range provisioners { + provisioner := p.New(d) + provisioner.SetOsReleaseInfo(osReleaseInfo) + + if provisioner.CompatibleWithHost() { + return provisioner, nil + } + } + + return nil, ErrDetectionFailed +} diff --git a/libmachine/provision/ubuntu.go b/libmachine/provision/ubuntu.go new file mode 100644 index 0000000000..71101326fc --- /dev/null +++ b/libmachine/provision/ubuntu.go @@ -0,0 +1,162 @@ +package provision + +import ( + "bytes" + "fmt" + "os/exec" + + "github.com/docker/machine/drivers" + "github.com/docker/machine/libmachine/auth" + "github.com/docker/machine/libmachine/provision/pkgaction" + "github.com/docker/machine/libmachine/swarm" +) + +func init() { + Register("Ubuntu", &RegisteredProvisioner{ + New: NewUbuntuProvisioner, + }) +} + +func NewUbuntuProvisioner(d drivers.Driver) Provisioner { + return &UbuntuProvisioner{ + packages: []string{ + "curl", + }, + Driver: d, + } +} + +type UbuntuProvisioner struct { + packages []string + OsReleaseInfo *OsRelease + Driver drivers.Driver + SwarmConfig swarm.SwarmOptions +} + +func (provisioner *UbuntuProvisioner) Service(name string, action pkgaction.ServiceAction) error { + command := fmt.Sprintf("sudo service %s %s", name, action.String()) + + cmd, err := provisioner.SSHCommand(command) + if err != nil { + return err + } + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +func (provisioner *UbuntuProvisioner) Package(name string, action pkgaction.PackageAction) error { + var packageAction string + + switch action { + case pkgaction.Install: + packageAction = "install" + case pkgaction.Remove: + packageAction = "remove" + } + + command := fmt.Sprintf("DEBIAN_FRONTEND=noninteractive sudo -E apt-get %s -y %s", packageAction, name) + + cmd, err := provisioner.SSHCommand(command) + if err != nil { + return err + } + + if err := cmd.Run(); err != nil { + return err + } + + return nil +} + +func (provisioner *UbuntuProvisioner) Provision(swarmConfig swarm.SwarmOptions, authConfig auth.AuthOptions) error { + if err := provisioner.SetHostname(provisioner.Driver.GetMachineName()); err != nil { + return err + } + + for _, pkg := range provisioner.packages { + if err := provisioner.Package(pkg, pkgaction.Install); err != nil { + return err + } + } + + if err := installDockerGeneric(provisioner); err != nil { + return err + } + + if err := ConfigureAuth(provisioner, authConfig); err != nil { + return err + } + + if err := configureSwarm(provisioner, swarmConfig); err != nil { + return err + } + + return nil +} + +func (provisioner *UbuntuProvisioner) Hostname() (string, error) { + cmd, err := provisioner.SSHCommand("hostname") + if err != nil { + return "", err + } + + var so bytes.Buffer + cmd.Stdout = &so + + if err := cmd.Run(); err != nil { + return "", err + } + + return so.String(), nil +} + +func (provisioner *UbuntuProvisioner) SetHostname(hostname string) error { + cmd, err := provisioner.SSHCommand(fmt.Sprintf( + "sudo hostname %s && echo %q | sudo tee /etc/hostname && echo \"127.0.0.1 %s\" | sudo tee -a /etc/hosts", + hostname, + hostname, + hostname, + )) + + if err != nil { + return err + } + + return cmd.Run() +} + +func (provisioner *UbuntuProvisioner) GetDockerConfigDir() string { + return "/etc/docker" +} + +func (provisioner *UbuntuProvisioner) SSHCommand(args ...string) (*exec.Cmd, error) { + return drivers.GetSSHCommandFromDriver(provisioner.Driver, args...) +} + +func (provisioner *UbuntuProvisioner) CompatibleWithHost() bool { + return provisioner.OsReleaseInfo.Id == "ubuntu" +} + +func (provisioner *UbuntuProvisioner) SetOsReleaseInfo(info *OsRelease) { + provisioner.OsReleaseInfo = info +} + +func (provisioner *UbuntuProvisioner) GenerateDockerConfig(dockerPort int, authConfig auth.AuthOptions) (*DockerConfig, error) { + defaultDaemonOpts := getDefaultDaemonOpts(provisioner.Driver.DriverName(), authConfig) + daemonOpts := fmt.Sprintf("--host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:%d", dockerPort) + daemonOptsCfg := "/etc/default/docker" + opts := fmt.Sprintf("%s %s", defaultDaemonOpts, daemonOpts) + daemonCfg := fmt.Sprintf("export DOCKER_OPTS=\\\"%s\\\"", opts) + return &DockerConfig{ + EngineConfig: daemonCfg, + EngineConfigPath: daemonOptsCfg, + }, nil +} + +func (provisioner *UbuntuProvisioner) GetDriver() drivers.Driver { + return provisioner.Driver +} diff --git a/libmachine/provision/utils.go b/libmachine/provision/utils.go new file mode 100644 index 0000000000..2eed1e4633 --- /dev/null +++ b/libmachine/provision/utils.go @@ -0,0 +1,274 @@ +package provision + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/url" + "path" + "path/filepath" + "strconv" + "strings" + + log "github.com/Sirupsen/logrus" + "github.com/docker/machine/libmachine/auth" + "github.com/docker/machine/libmachine/provision/pkgaction" + "github.com/docker/machine/libmachine/swarm" + "github.com/docker/machine/utils" +) + +type DockerConfig struct { + EngineConfig string + EngineConfigPath string +} + +func installDockerGeneric(p Provisioner) error { + // install docker - until cloudinit we use ubuntu everywhere so we + // just install it using the docker repos + cmd, err := p.SSHCommand("if ! type docker; then curl -sSL https://get.docker.com | sh -; fi") + if err != nil { + return err + } + + // HACK: the script above will output debug to stderr; we save it and + // then check if the command returned an error; if so, we show the debug + var buf bytes.Buffer + cmd.Stderr = &buf + + if err := cmd.Run(); err != nil { + return fmt.Errorf("error installing docker: %s\n%s\n", err, string(buf.Bytes())) + } + + return nil +} + +func ConfigureAuth(p Provisioner, authConfig auth.AuthOptions) error { + var ( + err error + ) + + machineName := p.GetDriver().GetMachineName() + org := machineName + bits := 2048 + + ip, err := p.GetDriver().GetIP() + if err != nil { + return err + } + + // copy certs to client dir for docker client + machineDir := filepath.Join(utils.GetMachineDir(), machineName) + + if err := utils.CopyFile(authConfig.CaCertPath, filepath.Join(machineDir, "ca.pem")); err != nil { + log.Fatalf("Error copying ca.pem to machine dir: %s", err) + } + + if err := utils.CopyFile(authConfig.ClientCertPath, filepath.Join(machineDir, "cert.pem")); err != nil { + log.Fatalf("Error copying cert.pem to machine dir: %s", err) + } + + if err := utils.CopyFile(authConfig.ClientKeyPath, filepath.Join(machineDir, "key.pem")); err != nil { + log.Fatalf("Error copying key.pem to machine dir: %s", err) + } + + log.Debugf("generating server cert: %s ca-key=%s private-key=%s org=%s", + authConfig.ServerCertPath, + authConfig.CaCertPath, + authConfig.PrivateKeyPath, + org, + ) + + // TODO: Switch to passing just authConfig to this func + // instead of all these individual fields + err = utils.GenerateCert( + []string{ip}, + authConfig.ServerCertPath, + authConfig.ServerKeyPath, + authConfig.CaCertPath, + authConfig.PrivateKeyPath, + org, + bits, + ) + if err != nil { + return fmt.Errorf("error generating server cert: %s", err) + } + + if err := p.Service("docker", pkgaction.Stop); err != nil { + return err + } + + dockerDir := p.GetDockerConfigDir() + + cmd, err := p.SSHCommand(fmt.Sprintf("sudo mkdir -p %s", dockerDir)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + // upload certs and configure TLS auth + caCert, err := ioutil.ReadFile(authConfig.CaCertPath) + if err != nil { + return err + } + + // due to windows clients, we cannot use filepath.Join as the paths + // will be mucked on the linux hosts + machineCaCertPath := path.Join(dockerDir, "ca.pem") + authConfig.CaCertRemotePath = machineCaCertPath + + serverCert, err := ioutil.ReadFile(authConfig.ServerCertPath) + if err != nil { + return err + } + machineServerCertPath := path.Join(dockerDir, "server.pem") + authConfig.ServerCertRemotePath = machineServerCertPath + + serverKey, err := ioutil.ReadFile(authConfig.ServerKeyPath) + if err != nil { + return err + } + machineServerKeyPath := path.Join(dockerDir, "server-key.pem") + authConfig.ServerKeyRemotePath = machineServerKeyPath + + cmd, err = p.SSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(caCert), machineCaCertPath)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + cmd, err = p.SSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(serverKey), machineServerKeyPath)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + cmd, err = p.SSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee %s", string(serverCert), machineServerCertPath)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + dockerUrl, err := p.GetDriver().GetURL() + if err != nil { + return err + } + u, err := url.Parse(dockerUrl) + if err != nil { + return err + } + dockerPort := 2376 + parts := strings.Split(u.Host, ":") + if len(parts) == 2 { + dPort, err := strconv.Atoi(parts[1]) + if err != nil { + return err + } + dockerPort = dPort + } + + dkrcfg, err := p.GenerateDockerConfig(dockerPort, authConfig) + if err != nil { + return err + } + + cmd, err = p.SSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %s", dkrcfg.EngineConfig, dkrcfg.EngineConfigPath)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + if err := p.Service("docker", pkgaction.Start); err != nil { + return err + } + + return nil +} + +func getDefaultDaemonOpts(driverName string, authConfig auth.AuthOptions) string { + return fmt.Sprintf(`--tlsverify --tlscacert=%s --tlskey=%s --tlscert=%s %s`, + authConfig.CaCertRemotePath, + authConfig.ServerKeyRemotePath, + authConfig.ServerCertRemotePath, + fmt.Sprintf("--label=provider=%s", driverName), + ) +} + +func configureSwarm(p Provisioner, swarmConfig swarm.SwarmOptions) error { + if !swarmConfig.IsSwarm { + return nil + } + + basePath := p.GetDockerConfigDir() + ip, err := p.GetDriver().GetIP() + if err != nil { + return err + } + + tlsCaCert := path.Join(basePath, "ca.pem") + tlsCert := path.Join(basePath, "server.pem") + tlsKey := path.Join(basePath, "server-key.pem") + masterArgs := fmt.Sprintf("--tlsverify --tlscacert=%s --tlscert=%s --tlskey=%s -H %s %s", + tlsCaCert, tlsCert, tlsKey, swarmConfig.Host, swarmConfig.Discovery) + nodeArgs := fmt.Sprintf("--addr %s:2376 %s", ip, swarmConfig.Discovery) + + u, err := url.Parse(swarmConfig.Host) + if err != nil { + return err + } + + parts := strings.Split(u.Host, ":") + port := parts[1] + + // TODO: Do not hardcode daemon port, ask the driver + if err := utils.WaitForDocker(ip, 2376); err != nil { + return err + } + + cmd, err := p.SSHCommand(fmt.Sprintf("sudo docker pull %s", swarm.DockerImage)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + dockerDir := p.GetDockerConfigDir() + + // if master start master agent + if swarmConfig.Master { + log.Debug("launching swarm master") + log.Debugf("master args: %s", masterArgs) + cmd, err = p.SSHCommand(fmt.Sprintf("sudo docker run -d -p %s:%s --restart=always --name swarm-agent-master -v %s:%s %s manage %s", + port, port, dockerDir, dockerDir, swarm.DockerImage, masterArgs)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + } + + // start node agent + log.Debug("launching swarm node") + log.Debugf("node args: %s", nodeArgs) + cmd, err = p.SSHCommand(fmt.Sprintf("sudo docker run -d --restart=always --name swarm-agent -v %s:%s %s join %s", + dockerDir, dockerDir, swarm.DockerImage, nodeArgs)) + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + return nil +} diff --git a/libmachine/swarm/swarm.go b/libmachine/swarm/swarm.go index 5e8e8948b3..2c752c5dd7 100644 --- a/libmachine/swarm/swarm.go +++ b/libmachine/swarm/swarm.go @@ -1,6 +1,12 @@ package swarm +const ( + DockerImage = "swarm:latest" + DiscoveryServiceEndpoint = "https://discovery-stage.hub.docker.com/v1" +) + type SwarmOptions struct { + IsSwarm bool Address string Discovery string Master bool diff --git a/libmachine/swarm/swarm_options.go b/libmachine/swarm/swarm_options.go new file mode 100644 index 0000000000..6f225b055b --- /dev/null +++ b/libmachine/swarm/swarm_options.go @@ -0,0 +1 @@ +package swarm diff --git a/utils/utils.go b/utils/utils.go index fe20674f46..be065951bd 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "net" "os" "path/filepath" "runtime" @@ -95,6 +96,18 @@ func WaitFor(f func() bool) error { return WaitForSpecific(f, 60, 3*time.Second) } +func WaitForDocker(ip string, daemonPort int) error { + return WaitFor(func() bool { + conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", ip, daemonPort)) + if err != nil { + fmt.Println("Got an error it was", err) + return false + } + conn.Close() + return true + }) +} + func DumpVal(vals ...interface{}) { for _, val := range vals { prettyJSON, err := json.MarshalIndent(val, "", " ")