Implement majority of provisioning changes

Signed-off-by: Simon Thulborn <simon+github@thulborn.com>
Signed-off-by: Nathan LeClaire <nathan.leclaire@gmail.com>
This commit is contained in:
Nathan LeClaire and Simon Thulborn
2015-02-16 23:16:36 +00:00
committed by Nathan LeClaire
parent 8d5a59b43e
commit 49feb33457
16 changed files with 1096 additions and 580 deletions

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")
)

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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 ""
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -0,0 +1 @@
package swarm