package main import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net" "net/url" "os" "path" "path/filepath" "regexp" "strconv" "strings" "time" log "github.com/Sirupsen/logrus" "github.com/docker/machine/drivers" "github.com/docker/machine/utils" ) var ( validHostNameChars = `[a-zA-Z0-9\-\.]` validHostNamePattern = regexp.MustCompile(`^` + validHostNameChars + `+$`) ErrInvalidHostname = errors.New("Invalid hostname specified") ) const ( swarmDockerImage = "swarm:latest" swarmDiscoveryServiceEndpoint = "https://discovery-stage.hub.docker.com/v1" ) type Host struct { Name string `json:"-"` DriverName string Driver drivers.Driver CaCertPath string ServerCertPath string ServerKeyPath string PrivateKeyPath string ClientCertPath string SwarmMaster bool SwarmHost string SwarmDiscovery string storePath string } type DockerConfig struct { EngineConfig string EngineConfigPath string } type hostConfig struct { DriverName string } 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, swarmMaster bool, swarmHost string, swarmDiscovery string) (*Host, error) { driver, err := drivers.NewDriver(driverName, name, storePath, caCert, privateKey) if err != nil { return nil, err } return &Host{ Name: name, DriverName: driverName, Driver: driver, CaCertPath: caCert, PrivateKeyPath: privateKey, SwarmMaster: swarmMaster, SwarmHost: swarmHost, SwarmDiscovery: swarmDiscovery, storePath: storePath, }, nil } func LoadHost(name string, storePath string) (*Host, error) { if _, err := os.Stat(storePath); os.IsNotExist(err) { return nil, fmt.Errorf("Host %q does not exist", name) } host := &Host{Name: name, storePath: storePath} if err := host.LoadConfig(); err != nil { return nil, err } return host, nil } func ValidateHostName(name string) (string, error) { if !validHostNamePattern.MatchString(name) { return name, ErrInvalidHostname } return name, nil } func GenerateClientCertificate(caCertPath, privateKeyPath string) error { var ( org = "docker-machine" bits = 2048 ) clientCertPath := filepath.Join(utils.GetMachineDir(), "cert.pem") clientKeyPath := filepath.Join(utils.GetMachineDir(), "key.pem") if err := os.MkdirAll(utils.GetMachineDir(), 0700); err != nil { return err } log.Debugf("generating client cert: %s", clientCertPath) if err := utils.GenerateCert([]string{""}, clientCertPath, clientKeyPath, caCertPath, privateKeyPath, org, bits); err != nil { return fmt.Errorf("error generating client cert: %s", err) } return nil } 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 := d.GetDockerConfigDir() 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 := d.GetSSHCommand(fmt.Sprintf("sudo docker pull %s", swarmDockerImage)) if err != nil { return err } if err := cmd.Run(); err != nil { return err } // if master start master agent if master { log.Debug("launching swarm master") log.Debugf("master args: %s", masterArgs) cmd, err = d.GetSSHCommand(fmt.Sprintf("sudo docker run -d -p %s:%s --restart=always --name swarm-agent-master -v %s:%s %s manage %s", port, port, d.GetDockerConfigDir(), d.GetDockerConfigDir(), 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 = d.GetSSHCommand(fmt.Sprintf("sudo docker run -d --restart=always --name swarm-agent -v %s:%s %s join %s", d.GetDockerConfigDir(), d.GetDockerConfigDir(), swarmDockerImage, nodeArgs)) 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 } ip, err := h.Driver.GetIP() if err != nil { return err } 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", serverCertPath) 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 := d.StopDocker(); err != nil { return err } cmd, err := d.GetSSHCommand(fmt.Sprintf("sudo mkdir -p %s", d.GetDockerConfigDir())) 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(d.GetDockerConfigDir(), "ca.pem") serverCert, err := ioutil.ReadFile(serverCertPath) if err != nil { return err } machineServerCertPath := path.Join(d.GetDockerConfigDir(), "server.pem") serverKey, err := ioutil.ReadFile(serverKeyPath) if err != nil { return err } machineServerKeyPath := path.Join(d.GetDockerConfigDir(), "server-key.pem") cmd, err = d.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %s", string(caCert), machineCaCertPath)) if err != nil { return err } if err := cmd.Run(); err != nil { return err } cmd, err = d.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %s", string(serverKey), machineServerKeyPath)) if err != nil { return err } if err := cmd.Run(); err != nil { return err } cmd, err = d.GetSSHCommand(fmt.Sprintf("echo \"%s\" | sudo tee -a %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 := h.generateDockerConfig(dockerPort, machineCaCertPath, machineServerKeyPath, machineServerCertPath) cmd, err = d.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 := d.StartDocker(); err != nil { return err } return nil } func (h *Host) generateDockerConfig(dockerPort int, caCertPath string, serverKeyPath string, serverCertPath string) *DockerConfig { 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, " "), ) switch d.DriverName() { case "virtualbox", "vmwarefusion", "vmwarevsphere", "hyper-v": daemonOpts = fmt.Sprintf("-H tcp://0.0.0.0:%d", dockerPort) daemonOptsCfg = path.Join(d.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, 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, } } 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 } // install docker if err := h.Provision(); err != nil { return err } // save to store if err := h.SaveConfig(); 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", "openstack": return nil } // install docker - until cloudinit we use ubuntu everywhere so we // just install it using the docker repos cmd, err := h.Driver.GetSSHCommand("if [ ! -e /usr/bin/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 (h *Host) Start() error { return h.Driver.Start() } func (h *Host) Stop() error { return h.Driver.Stop() } func (h *Host) Upgrade() error { return h.Driver.Upgrade() } func (h *Host) Remove(force bool) error { if err := h.Driver.Remove(); err != nil { if !force { return err } } return h.removeStorePath() } func (h *Host) removeStorePath() error { file, err := os.Stat(h.storePath) if err != nil { return err } if !file.IsDir() { return fmt.Errorf("%q is not a directory", h.storePath) } return os.RemoveAll(h.storePath) } func (h *Host) GetURL() (string, error) { return h.Driver.GetURL() } func (h *Host) LoadConfig() error { data, err := ioutil.ReadFile(filepath.Join(h.storePath, "config.json")) if err != nil { return err } // First pass: find the driver name and load the driver var config hostConfig if err := json.Unmarshal(data, &config); err != nil { return err } driver, err := drivers.NewDriver(config.DriverName, h.Name, h.storePath, h.CaCertPath, h.PrivateKeyPath) if err != nil { return err } h.Driver = driver // Second pass: unmarshal driver config into correct driver if err := json.Unmarshal(data, &h); err != nil { return err } return nil } func (h *Host) SaveConfig() error { data, err := json.Marshal(h) if err != nil { return err } if err := ioutil.WriteFile(filepath.Join(h.storePath, "config.json"), data, 0600); err != nil { return err } return nil }