From 8d8c6fe71dc14b96ebc0cc702ba20a3e7e2fc0b2 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 8 Dec 2014 16:48:15 -0500 Subject: [PATCH] Add softlayer driver Signed-off-by: Brian Goff --- README.md | 23 ++ commands.go | 1 + drivers/softlayer/driver.go | 452 +++++++++++++++++++++++++++++++++ drivers/softlayer/softlayer.go | 308 ++++++++++++++++++++++ 4 files changed, 784 insertions(+) create mode 100644 drivers/softlayer/driver.go create mode 100644 drivers/softlayer/softlayer.go diff --git a/README.md b/README.md index 6739c1eec2..ecd6e2a2e1 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,29 @@ variable and CLI option are provided the CLI option takes the precedence. | `OS_REGION_NAME` | `--rackspace-region` | | `OS_ENDPOINT_TYPE` | `--rackspace-endpoint-type` | +### Softlayer + +Create machines on [Softlayer](http://softlayer.com). + +You need to generate an API key in the softlayer control panel. +[Retrieve your API key](http://knowledgelayer.softlayer.com/procedure/retrieve-your-api-key) + +Options: + - `--softlayer-api-endpoint=`: Change softlayer API endpoint + - `--softlayer-user`: **required** username for your softlayer account, api key needs to match this user. + - `--softlayer-api-key`: **required** API key for your user account + - `--softlayer-cpu`: Number of CPU's for the machine. + - `--softlayer-disk-size: Size of the disk in MB. `0` sets the softlayer default. + - `--softlayer-domain`: **required** domain name for the machine + - `--softlayer-hostname`: hostname for the machine + - `--softlayer-hourly-billing`: Sets the hourly billing flag (default), otherwise uses monthly billing + - `--softlayer-image`: OS Image to use + - `--softlayer-install-script`: custom install script to use for installing Docker, other setup actions + - `--softlayer-local-disk`: Use local machine disk instead of softlayer SAN. + - `--softlayer-memory`: Memory for host in MB + - `--softlayer-private-net-only`: Disable public networking + - `--softlayer-region`: softlayer region + ## Contributing [![GoDoc](https://godoc.org/github.com/docker/machine?status.png)](https://godoc.org/github.com/docker/machine) diff --git a/commands.go b/commands.go index 5374de9531..19c782baab 100644 --- a/commands.go +++ b/commands.go @@ -21,6 +21,7 @@ import ( _ "github.com/docker/machine/drivers/none" _ "github.com/docker/machine/drivers/openstack" _ "github.com/docker/machine/drivers/rackspace" + _ "github.com/docker/machine/drivers/softlayer" _ "github.com/docker/machine/drivers/virtualbox" _ "github.com/docker/machine/drivers/vmwarefusion" _ "github.com/docker/machine/drivers/vmwarevcloudair" diff --git a/drivers/softlayer/driver.go b/drivers/softlayer/driver.go new file mode 100644 index 0000000000..d1725de758 --- /dev/null +++ b/drivers/softlayer/driver.go @@ -0,0 +1,452 @@ +package softlayer + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "regexp" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + "github.com/docker/machine/drivers" + "github.com/docker/machine/ssh" + "github.com/docker/machine/state" +) + +const ApiEndpoint = "https://api.softlayer.com/rest/v3" +const DockerInstallUrl = "https://get.docker.com" + +type Driver struct { + storePath string + IPAddress string + deviceConfig *deviceConfig + Id int + Client *Client +} + +type deviceConfig struct { + DiskSize int + Cpu int + Hostname string + Domain string + Region string + Memory int + Image string + HourlyBilling bool + InstallScript string + LocalDisk bool + PrivateNet bool +} + +func init() { + drivers.Register("softlayer", &drivers.RegisteredDriver{ + New: NewDriver, + GetCreateFlags: GetCreateFlags, + }) +} + +func NewDriver(storePath string) (drivers.Driver, error) { + return &Driver{storePath: storePath}, nil +} + +func GetCreateFlags() []cli.Flag { + // Set hourly billing to true by default since codegangsta cli doesn't take default bool values + if os.Getenv("SOFTLAYER_HOURLY_BILLING") == "" { + os.Setenv("SOFTLAYER_HOURLY_BILLING", "true") + } + return []cli.Flag{ + cli.IntFlag{ + EnvVar: "SOFTLAYER_MEMORY", + Name: "softlayer-memory", + Usage: "Memory in MB for machine", + Value: 1024, + }, + cli.IntFlag{ + EnvVar: "SOFTLAYER_DISK_SIZE", + Name: "softlayer-disk-size", + Usage: "Disk size for machine, a value of 0 uses the default size on softlayer", + Value: 0, + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_USER", + Name: "softlayer-user", + Usage: "softlayer user account name", + Value: "", + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_API_KEY", + Name: "softlayer-api-key", + Usage: "softlayer user API key", + Value: "", + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_REGION", + Name: "softlayer-region", + Usage: "softlayer region for machine", + Value: "dal01", + }, + cli.IntFlag{ + EnvVar: "SOFTLAYER_CPU", + Name: "softlayer-cpu", + Usage: "number of CPU's for the machine", + Value: 1, + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_HOSTNAME", + Name: "softlayer-hostname", + Usage: "hostname for the machine", + Value: "docker", + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_DOMAIN", + Name: "softlayer-domain", + Usage: "domain name for machine", + Value: "", + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_API_ENDPOINT", + Name: "softlayer-api-endpoint", + Usage: "softlayer api endpoint to use", + Value: ApiEndpoint, + }, + cli.BoolFlag{ + EnvVar: "SOFTLAYER_HOURLY_BILLING", + Name: "softlayer-hourly-billing", + Usage: "set hourly billing for machine - on by default", + }, + cli.BoolFlag{ + EnvVar: "SOFTLAYER_LOCAL_DISK", + Name: "softlayer-local-disk", + Usage: "use machine local disk instead of softlayer SAN", + }, + cli.BoolFlag{ + EnvVar: "SOFTLAYER_PRIVATE_NET", + Name: "softlayer-private-net-only", + Usage: "Use only private networking", + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_IMAGE", + Name: "softlayer-image", + Usage: "OS image for machine", + Value: "UBUNTU_LATEST", + }, + cli.StringFlag{ + EnvVar: "SOFTLAYER_INSTALL_SCRIPT", + Name: "softlayer-install-script", + Usage: "Install script to call after the machine is initialized (should install Docker)", + Value: DockerInstallUrl, + }, + } +} + +func validateDeviceConfig(c *deviceConfig) error { + if c.Hostname == "" { + return fmt.Errorf("Missing required setting - --softlayer-hostname") + } + if c.Domain == "" { + return fmt.Errorf("Missing required setting - --softlayer-domain") + } + + if c.Region == "" { + return fmt.Errorf("Missing required setting - --softlayer-region") + } + if c.Cpu < 1 { + return fmt.Errorf("Missing required setting - --softlayer-cpu") + } + + return nil +} + +func validateClientConfig(c *Client) error { + if c.ApiKey == "" { + return fmt.Errorf("Missing required setting - --softlayer-api-key") + } + + if c.User == "" { + return fmt.Errorf("Missing required setting - --softlayer-user") + } + + if c.Endpoint == "" { + return fmt.Errorf("Missing required setting - --softlayer-api-endpoint") + } + + return nil +} + +func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { + + d.Client = &Client{ + Endpoint: flags.String("softlayer-api-endpoint"), + User: flags.String("softlayer-user"), + ApiKey: flags.String("softlayer-api-key"), + } + + if err := validateClientConfig(d.Client); err != nil { + return err + } + + d.deviceConfig = &deviceConfig{ + Hostname: flags.String("softlayer-hostname"), + DiskSize: flags.Int("softlayer-disk-size"), + Cpu: flags.Int("softlayer-cpu"), + Domain: flags.String("softlayer-domain"), + Memory: flags.Int("softlayer-memory"), + PrivateNet: flags.Bool("softlayer-private-net-only"), + LocalDisk: flags.Bool("softlayer-local-disk"), + HourlyBilling: flags.Bool("softlayer-hourly-billing"), + InstallScript: flags.String("softlayer-install-script"), + Image: "UBUNTU_LATEST", + Region: flags.String("softlayer-region"), + } + return validateDeviceConfig(d.deviceConfig) +} + +func (d *Driver) getClient() *Client { + return d.Client +} + +func (d *Driver) DriverName() string { + return "softlayer" +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + if ip == "" { + return "", nil + } + return "tcp://" + ip + ":2376", nil +} + +func (d *Driver) GetIP() (string, error) { + if d.IPAddress != "" { + return d.IPAddress, nil + } + return d.getClient().VirtualGuest().GetPublicIp(d.Id) +} + +func (d *Driver) GetState() (state.State, error) { + s, err := d.getClient().VirtualGuest().PowerState(d.Id) + if err != nil { + return state.None, err + } + var vmState state.State + switch s { + case "Running": + vmState = state.Running + case "Halted": + vmState = state.Stopped + default: + vmState = state.None + } + return vmState, nil +} + +func (d *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) { + return ssh.GetSSHCommand(d.IPAddress, 22, "root", d.sshKeyPath(), args...), nil +} + +func (d *Driver) Create() error { + waitForStart := func() { + log.Infof("Waiting for host to become available") + for { + s, err := d.GetState() + if err != nil { + continue + } + + if s == state.Running { + break + } + time.Sleep(2 * time.Second) + } + } + + getIp := func() { + log.Infof("Getting Host IP") + for { + var ( + ip string + err error + ) + if d.deviceConfig.PrivateNet { + ip, err = d.getClient().VirtualGuest().GetPrivateIp(d.Id) + } else { + ip, err = d.getClient().VirtualGuest().GetPublicIp(d.Id) + } + if err != nil { + time.Sleep(2 * time.Second) + continue + } + // not a perfect regex, but should be just fine for our needs + exp := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`) + if exp.MatchString(ip) { + d.IPAddress = ip + break + } + time.Sleep(2 * time.Second) + } + } + + log.Infof("Creating SSH key...") + key, err := d.createSSHKey() + if err != nil { + return err + } + + spec := d.buildHostSpec() + spec.SshKeys = []*SshKey{key} + + id, err := d.getClient().VirtualGuest().Create(spec) + if err != nil { + return fmt.Errorf("Error creating host: %q", err) + } + d.Id = id + getIp() + waitForStart() + ssh.WaitForTCP(d.IPAddress + ":22") + if err := d.setupHost(); err != nil { + fmt.Fprintf(os.Stderr, "Error setting up host config: %q", err) + } + return nil +} + +func (d *Driver) buildHostSpec() *HostSpec { + spec := &HostSpec{ + Hostname: d.deviceConfig.Hostname, + Domain: d.deviceConfig.Domain, + Cpu: d.deviceConfig.Cpu, + Memory: d.deviceConfig.Memory, + Datacenter: Datacenter{Name: d.deviceConfig.Region}, + InstallScript: d.deviceConfig.InstallScript, + Os: d.deviceConfig.Image, + HourlyBilling: d.deviceConfig.HourlyBilling, + PrivateNetOnly: d.deviceConfig.PrivateNet, + } + if d.deviceConfig.DiskSize > 0 { + spec.BlockDevices = []BlockDevice{{Device: "0", DiskImage: DiskImage{Capacity: d.deviceConfig.DiskSize}}} + } + return spec +} + +func (d *Driver) createSSHKey() (*SshKey, error) { + if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil { + return nil, err + } + + publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath()) + if err != nil { + return nil, err + } + + key, err := d.getClient().SshKey().Create(d.deviceConfig.Hostname, string(publicKey)) + if err != nil { + return nil, err + } + + return key, nil +} + +func (d *Driver) publicSSHKeyPath() string { + return d.sshKeyPath() + ".pub" +} + +func (d *Driver) sshKeyPath() string { + return path.Join(d.storePath, "id_rsa") +} + +func (d *Driver) Kill() error { + return d.getClient().VirtualGuest().PowerOff(d.Id) +} +func (d *Driver) Remove() error { + var err error + for i := 0; i < 5; i++ { + if err = d.getClient().VirtualGuest().Cancel(d.Id); err != nil { + time.Sleep(2 * time.Second) + continue + } + break + } + return err +} +func (d *Driver) Restart() error { + return d.getClient().VirtualGuest().Reboot(d.Id) +} +func (d *Driver) Start() error { + return d.getClient().VirtualGuest().PowerOn(d.Id) +} +func (d *Driver) Stop() error { + return d.getClient().VirtualGuest().PowerOff(d.Id) +} + +func (d *Driver) Upgrade() error { + sshCmd, err := d.GetSSHCommand("curl -sSL https://get.docker.com/builds/Linux/x86_64/docker-latest > /tmp/docker && chmod +x /tmp/docker && mv /tmp/docker $(which docker)") + if err != nil { + return err + } + sshCmd.Stdin = os.Stdin + sshCmd.Stdout = os.Stdout + sshCmd.Stderr = os.Stderr + if err := sshCmd.Run(); err != nil { + return fmt.Errorf("%s", err) + } + return nil +} + +func (d *Driver) setupHost() error { + log.Infof("Configuring host OS") + ssh.WaitForTCP(d.IPAddress + ":22") + // Wait to make sure docker is installed + for { + cmd, err := d.GetSSHCommand(`[ -f "$(which docker)" ] && [ -f "/etc/default/docker" ] || exit 1`) + if err != nil { + return err + } + if err := cmd.Run(); err == nil { + break + } + time.Sleep(2 * time.Second) + } + // Remove this once ID auth is released officialy + cmd, err := d.GetSSHCommand("service docker stop") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + cmd, err = d.GetSSHCommand("dbin=$(which docker); wget -O $dbin https://bfirsh.s3.amazonaws.com/docker/docker-1.3.1-dev-identity-auth > /dev/null 2>&1 && chmod +x $dbin") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + log.Debugf("Updating /etc/default/docker to use identity auth...") + + cmd, err = d.GetSSHCommand("echo 'export DOCKER_OPTS=\"--auth=identity --host=tcp://0.0.0.0:2376\"' >> /etc/default/docker") + if err != nil { + return err + } + if err := cmd.Run(); err != nil { + return err + } + + log.Debugf("Adding key to authorized-keys.d...") + + if err := drivers.AddPublicKeyToAuthorizedHosts(d, "/.docker/authorized-keys.d"); err != nil { + return err + } + + cmd, err = d.GetSSHCommand("service docker start") + if err != nil { + return err + } + return cmd.Run() +} diff --git a/drivers/softlayer/softlayer.go b/drivers/softlayer/softlayer.go new file mode 100644 index 0000000000..e094b50459 --- /dev/null +++ b/drivers/softlayer/softlayer.go @@ -0,0 +1,308 @@ +package softlayer + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +type Client struct { + User string + ApiKey string + Endpoint string +} + +type HostSpec struct { + Hostname string `json:"hostname"` + Domain string `json:"domain"` + Cpu int `json:"startCpus"` + Memory int `json:"maxMemory"` + Datacenter Datacenter `json:"datacenter"` + SshKeys []*SshKey `json:"sshKeys"` + BlockDevices []BlockDevice `json:"blockDevices"` + InstallScript string `json:"postInstallScriptUri"` + PrivateNetOnly bool `json:"privateNetworkOnlyFlag"` + Os string `json:"operatingSystemReferenceCode"` + HourlyBilling bool `json:"hourlyBillingFlag"` + LocalDisk bool `json:"localDiskFlag"` +} + +type SshKey struct { + Key string `json:"key,omitempty"` + Id int `json:"id,omitempty"` + Label string `json:"label,omitempty"` +} + +type BlockDevice struct { + Device string `json:"device"` + DiskImage DiskImage `json:"diskImage"` +} + +type DiskImage struct { + Capacity int `json:"capacity"` +} + +type Datacenter struct { + Name string `json:"name"` +} + +type sshKey struct { + *Client +} + +type virtualGuest struct { + *Client +} + +func NewClient(user, key, endpoint string) *Client { + return &Client{User: user, ApiKey: key, Endpoint: endpoint} +} + +func (c *Client) isOkStatus(code int) bool { + codes := map[int]bool{ + 200: true, + 201: true, + 204: true, + 400: false, + 404: false, + 500: false, + 409: false, + 406: false, + } + + return codes[code] +} + +func (c *Client) newRequest(method, uri string, body interface{}) ([]byte, error) { + var ( + client = &http.Client{} + url = fmt.Sprintf("%s/%s", c.Endpoint, uri) + err error + req *http.Request + ) + + if body != nil { + bodyJson, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err = http.NewRequest(method, url, bytes.NewBuffer(bodyJson)) + } else { + req, err = http.NewRequest(method, url, nil) + } + + if err != nil { + return nil, fmt.Errorf("Error with request: %v - %q", url, err) + } + + req.SetBasicAuth(c.User, c.ApiKey) + req.Method = method + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if !c.isOkStatus(resp.StatusCode) { + type apiErr struct { + Err string `json:"error"` + } + var outErr apiErr + json.Unmarshal(data, &outErr) + return nil, fmt.Errorf("Error in response: %s", outErr.Err) + } + if err != nil { + return nil, err + } + + return data, nil +} + +func (c *Client) SshKey() *sshKey { + return &sshKey{c} +} + +func (c *sshKey) namespace() string { + return "SoftLayer_Security_Ssh_Key" +} + +func (c *sshKey) Create(label, key string) (*SshKey, error) { + var ( + method = "POST" + uri = c.namespace() + body = SshKey{Key: key, Label: label} + ) + + data, err := c.newRequest(method, uri, map[string]interface{}{"parameters": []interface{}{body}}) + if err != nil { + return nil, err + } + + var k SshKey + if err := json.Unmarshal(data, &k); err != nil { + return nil, err + } + + return &k, nil +} + +func (c *Client) VirtualGuest() *virtualGuest { + return &virtualGuest{c} +} + +func (c *virtualGuest) namespace() string { + return "SoftLayer_Virtual_Guest" +} + +func (c *virtualGuest) PowerState(id int) (string, error) { + type state struct { + KeyName string `json:"keyName"` + Name string `json:"name"` + } + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/getPowerState.json", c.namespace(), id) + ) + + data, err := c.newRequest(method, uri, nil) + if err != nil { + return "", err + } + var s state + if err := json.Unmarshal(data, &s); err != nil { + return "", err + } + + return s.Name, nil +} + +func (c *virtualGuest) Create(spec *HostSpec) (int, error) { + var ( + method = "POST" + uri = c.namespace() + ".json" + ) + + data, err := c.newRequest(method, uri, map[string]interface{}{"parameters": []interface{}{spec}}) + if err != nil { + return -1, err + } + + type createResp struct { + Id int `json:"id"` + } + + var r createResp + if err := json.Unmarshal(data, &r); err != nil { + return -1, err + } + + return r.Id, nil +} + +func (c *virtualGuest) Cancel(id int) error { + var ( + method = "DELETE" + uri = fmt.Sprintf("%s/%v", c.namespace(), id) + ) + + _, err := c.newRequest(method, uri, nil) + if err != nil { + return err + } + return nil +} + +func (c *virtualGuest) PowerOn(id int) error { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/powerOn.json", c.namespace(), id) + ) + + _, err := c.newRequest(method, uri, nil) + if err != nil { + return err + } + return nil +} + +func (c *virtualGuest) PowerOff(id int) error { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/powerOff.json", c.namespace(), id) + ) + + _, err := c.newRequest(method, uri, nil) + if err != nil { + return err + } + return nil +} + +func (c *virtualGuest) Pause(id int) error { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/pause.json", c.namespace(), id) + ) + + _, err := c.newRequest(method, uri, nil) + if err != nil { + return err + } + return nil +} + +func (c *virtualGuest) Resume(id int) error { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/resume.json", c.namespace(), id) + ) + + _, err := c.newRequest(method, uri, nil) + if err != nil { + return err + } + return nil +} + +func (c *virtualGuest) Reboot(id int) error { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/rebootSoft.json", c.namespace(), id) + ) + + _, err := c.newRequest(method, uri, nil) + if err != nil { + return err + } + return nil +} + +func (c *virtualGuest) GetPublicIp(id int) (string, error) { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/getPrimaryIpAddress.json", c.namespace(), id) + ) + + data, err := c.newRequest(method, uri, nil) + if err != nil { + return "", err + } + return strings.Replace(string(data), "\"", "", -1), nil +} + +func (c *virtualGuest) GetPrivateIp(id int) (string, error) { + var ( + method = "GET" + uri = fmt.Sprintf("%s/%v/getPrimaryBackendIpAddress.json", c.namespace(), id) + ) + + data, err := c.newRequest(method, uri, nil) + if err != nil { + return "", err + } + return strings.Replace(string(data), "\"", "", -1), nil +}