package softlayer import ( "fmt" "io/ioutil" "net" "os" "regexp" "time" "github.com/docker/machine/libmachine/drivers" "github.com/docker/machine/libmachine/log" "github.com/docker/machine/libmachine/mcnflag" "github.com/docker/machine/libmachine/ssh" "github.com/docker/machine/libmachine/state" ) const ( APIEndpoint = "https://api.softlayer.com/rest/v3" ) type Driver struct { *drivers.BaseDriver deviceConfig *deviceConfig Id int Client *Client SSHKeyID int } type deviceConfig struct { DiskSize int Cpu int Hostname string Domain string Region string Memory int Image string HourlyBilling bool LocalDisk bool PrivateNet bool PublicVLAN int PrivateVLAN int NetworkMaxSpeed int } const ( defaultMemory = 1024 defaultDiskSize = 0 defaultRegion = "dal01" defaultCpus = 1 defaultImage = "UBUNTU_LATEST" defaultPublicVLANIP = 0 defaultPrivateVLANIP = 0 defaultNetworkMaxSpeed = 100 ) func NewDriver(hostName, storePath string) drivers.Driver { return &Driver{ Client: &Client{ Endpoint: APIEndpoint, }, deviceConfig: &deviceConfig{ HourlyBilling: true, DiskSize: defaultDiskSize, Image: defaultImage, Memory: defaultMemory, Cpu: defaultCpus, Region: defaultRegion, PrivateVLAN: defaultPrivateVLANIP, PublicVLAN: defaultPublicVLANIP, NetworkMaxSpeed: defaultNetworkMaxSpeed, }, BaseDriver: &drivers.BaseDriver{ MachineName: hostName, StorePath: storePath, }, } } func (d *Driver) GetSSHHostname() (string, error) { return d.GetIP() } func (d *Driver) GetCreateFlags() []mcnflag.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 []mcnflag.Flag{ mcnflag.IntFlag{ EnvVar: "SOFTLAYER_MEMORY", Name: "softlayer-memory", Usage: "Memory in MB for machine", Value: defaultMemory, }, mcnflag.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: defaultDiskSize, }, mcnflag.StringFlag{ EnvVar: "SOFTLAYER_USER", Name: "softlayer-user", Usage: "softlayer user account name", }, mcnflag.StringFlag{ EnvVar: "SOFTLAYER_API_KEY", Name: "softlayer-api-key", Usage: "softlayer user API key", }, mcnflag.StringFlag{ EnvVar: "SOFTLAYER_REGION", Name: "softlayer-region", Usage: "softlayer region for machine", Value: defaultRegion, }, mcnflag.IntFlag{ EnvVar: "SOFTLAYER_CPU", Name: "softlayer-cpu", Usage: "number of CPU's for the machine", Value: defaultCpus, }, mcnflag.StringFlag{ EnvVar: "SOFTLAYER_HOSTNAME", Name: "softlayer-hostname", Usage: "hostname for the machine - defaults to machine name", }, mcnflag.StringFlag{ EnvVar: "SOFTLAYER_DOMAIN", Name: "softlayer-domain", Usage: "domain name for machine", }, mcnflag.StringFlag{ EnvVar: "SOFTLAYER_API_ENDPOINT", Name: "softlayer-api-endpoint", Usage: "softlayer api endpoint to use", Value: APIEndpoint, }, mcnflag.BoolFlag{ EnvVar: "SOFTLAYER_HOURLY_BILLING", Name: "softlayer-hourly-billing", Usage: "set hourly billing for machine - on by default", }, mcnflag.BoolFlag{ EnvVar: "SOFTLAYER_LOCAL_DISK", Name: "softlayer-local-disk", Usage: "use machine local disk instead of softlayer SAN", }, mcnflag.BoolFlag{ EnvVar: "SOFTLAYER_PRIVATE_NET", Name: "softlayer-private-net-only", Usage: "Use only private networking", }, mcnflag.StringFlag{ EnvVar: "SOFTLAYER_IMAGE", Name: "softlayer-image", Usage: "OS image for machine", Value: defaultImage, }, mcnflag.IntFlag{ EnvVar: "SOFTLAYER_PUBLIC_VLAN_ID", Name: "softlayer-public-vlan-id", Usage: "", }, mcnflag.IntFlag{ EnvVar: "SOFTLAYER_PRIVATE_VLAN_ID", Name: "softlayer-private-vlan-id", Usage: "", }, mcnflag.IntFlag{ EnvVar: "SOFTLAYER_NETWORK_MAX_SPEED", Name: "softlayer-network-max-speed", Usage: "Max speed of public and private network", Value: defaultNetworkMaxSpeed, }, } } func validateDeviceConfig(c *deviceConfig) error { 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") } if c.PrivateNet && c.PublicVLAN > 0 { return fmt.Errorf("Can not specify both --softlayer-private-net-only and --softlayer-public-vlan-id") } if c.PublicVLAN > 0 && c.PrivateVLAN == 0 { return fmt.Errorf("Missing required setting - --softlayer-private-vlan-id (because --softlayer-public-vlan-id is specified)") } if c.PrivateVLAN > 0 && !c.PrivateNet && c.PublicVLAN == 0 { return fmt.Errorf("Missing required setting - --softlayer-public-vlan-id (because --softlayer-private-vlan-id is specified)") } 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"), } d.SetSwarmConfigFromFlags(flags) d.SSHUser = "root" d.SSHPort = 22 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"), Image: flags.String("softlayer-image"), Region: flags.String("softlayer-region"), PublicVLAN: flags.Int("softlayer-public-vlan-id"), PrivateVLAN: flags.Int("softlayer-private-vlan-id"), NetworkMaxSpeed: flags.Int("softlayer-network-max-speed"), } if d.deviceConfig.Hostname == "" { d.deviceConfig.Hostname = d.GetMachineName() } return validateDeviceConfig(d.deviceConfig) } func (d *Driver) getClient() *Client { return d.Client } // DriverName returns the name of the driver func (d *Driver) DriverName() string { return "softlayer" } func (d *Driver) GetURL() (string, error) { if err := drivers.MustBeRunning(d); err != nil { return "", err } ip, err := d.GetIP() if err != nil { return "", err } if ip == "" { return "", nil } return "tcp://" + net.JoinHostPort(ip, "2376"), nil } func (d *Driver) GetIP() (string, error) { if d.IPAddress != "" { return d.IPAddress, nil } if d.deviceConfig != nil && d.deviceConfig.PrivateNet == true { return d.getClient().VirtualGuest().GetPrivateIP(d.Id) } if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "" { os.Setenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP", "false") } if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "true" { return d.getClient().VirtualGuest().GetPrivateIP(d.Id) } else { 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) GetActiveTransaction() (string, error) { t, err := d.getClient().VirtualGuest().ActiveTransaction(d.Id) if err != nil { return "", err } return t, nil } func (d *Driver) waitForStart() { log.Infof("Waiting for host to become available") for { s, err := d.GetState() if err != nil { log.Debugf("Failed to GetState - %+v", err) continue } if s == state.Running { break } else { log.Debugf("Still waiting - state is %s...", s) } time.Sleep(2 * time.Second) } } func (d *Driver) getIP() (string, error) { 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 } // if docker daemon is expected to run on private IP // it will overwrite settings from PrivateNet if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "" { os.Setenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP", "false") } if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "true" { ip, err = d.getClient().VirtualGuest().GetPrivateIP(d.Id) } else { ip, err = d.getClient().VirtualGuest().GetPublicIP(d.Id) } // 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 return ip, nil } time.Sleep(2 * time.Second) } } func (d *Driver) waitForSetupTransactions() { log.Infof("Waiting for host setup transactions to complete") // sometimes we'll hit a case where there's no active transaction, but if // we check again in a few seconds, it moves to the next transaction. We // don't want to get false-positives, so we check a few times in a row to make sure! noActiveCount, maxNoActiveCount := 0, 3 for { t, err := d.GetActiveTransaction() if err != nil { noActiveCount = 0 log.Debugf("Failed to GetActiveTransaction - %+v", err) continue } if t == "" { if noActiveCount == maxNoActiveCount { break } noActiveCount++ } else { noActiveCount = 0 log.Debugf("Still waiting - active transaction is %s...", t) } time.Sleep(2 * time.Second) } } func (d *Driver) Create() error { spec := d.buildHostSpec() log.Infof("Creating SSH key...") key, err := d.createSSHKey() if err != nil { return err } log.Infof("SSH key %s (%d) created in SoftLayer", key.Label, key.Id) d.SSHKeyID = key.Id 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 d.getIP() d.waitForStart() d.waitForSetupTransactions() 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}, Os: d.deviceConfig.Image, HourlyBilling: d.deviceConfig.HourlyBilling, PrivateNetOnly: d.deviceConfig.PrivateNet, LocalDisk: d.deviceConfig.LocalDisk, } if d.deviceConfig.NetworkMaxSpeed > 0 { spec.NetworkMaxSpeeds = []NetworkMaxSpeed{{MaxSpeed: d.deviceConfig.NetworkMaxSpeed}} } if d.deviceConfig.DiskSize > 0 { spec.BlockDevices = []BlockDevice{{Device: "0", DiskImage: DiskImage{Capacity: d.deviceConfig.DiskSize}}} } if d.deviceConfig.PublicVLAN > 0 { spec.PrimaryNetworkComponent = &NetworkComponent{ NetworkVLAN: &NetworkVLAN{ Id: d.deviceConfig.PublicVLAN, }, } } if d.deviceConfig.PrivateVLAN > 0 { spec.PrimaryBackendNetworkComponent = &NetworkComponent{ NetworkVLAN: &NetworkVLAN{ Id: d.deviceConfig.PrivateVLAN, }, } } log.Debugf("Built host spec %#v", spec) return spec } func (d *Driver) createSSHKey() (*SSHKey, error) { if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); 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.GetSSHKeyPath() + ".pub" } func (d *Driver) Remove() error { log.Infof("Canceling SoftLayer instance %d...", d.Id) 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 } if err != nil { return err } log.Infof("Removing SSH Key %d...", d.SSHKeyID) if err = d.getClient().SSHKey().Delete(d.SSHKeyID); err != nil { return err } return nil } 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) Restart() error { return d.getClient().VirtualGuest().Reboot(d.Id) } func (d *Driver) Kill() error { return d.Stop() }