Add softlayer driver

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff
2014-12-08 16:48:15 -05:00
parent f5ba0dc1d7
commit 8d8c6fe71d
4 changed files with 784 additions and 0 deletions

View File

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

View File

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

452
drivers/softlayer/driver.go Normal file
View File

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

View File

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