1package softlayer
2
3import (
4	"fmt"
5	"io/ioutil"
6	"net"
7	"os"
8	"regexp"
9	"time"
10
11	"github.com/docker/machine/libmachine/drivers"
12	"github.com/docker/machine/libmachine/log"
13	"github.com/docker/machine/libmachine/mcnflag"
14	"github.com/docker/machine/libmachine/ssh"
15	"github.com/docker/machine/libmachine/state"
16)
17
18const (
19	APIEndpoint = "https://api.softlayer.com/rest/v3"
20)
21
22type Driver struct {
23	*drivers.BaseDriver
24	deviceConfig *deviceConfig
25	Id           int
26	Client       *Client
27	SSHKeyID     int
28}
29
30type deviceConfig struct {
31	DiskSize        int
32	Cpu             int
33	Hostname        string
34	Domain          string
35	Region          string
36	Memory          int
37	Image           string
38	HourlyBilling   bool
39	LocalDisk       bool
40	PrivateNet      bool
41	PublicVLAN      int
42	PrivateVLAN     int
43	NetworkMaxSpeed int
44}
45
46const (
47	defaultMemory          = 1024
48	defaultDiskSize        = 0
49	defaultRegion          = "dal01"
50	defaultCpus            = 1
51	defaultImage           = "UBUNTU_LATEST"
52	defaultPublicVLANIP    = 0
53	defaultPrivateVLANIP   = 0
54	defaultNetworkMaxSpeed = 100
55)
56
57func NewDriver(hostName, storePath string) drivers.Driver {
58	return &Driver{
59		Client: &Client{
60			Endpoint: APIEndpoint,
61		},
62		deviceConfig: &deviceConfig{
63			HourlyBilling:   true,
64			DiskSize:        defaultDiskSize,
65			Image:           defaultImage,
66			Memory:          defaultMemory,
67			Cpu:             defaultCpus,
68			Region:          defaultRegion,
69			PrivateVLAN:     defaultPrivateVLANIP,
70			PublicVLAN:      defaultPublicVLANIP,
71			NetworkMaxSpeed: defaultNetworkMaxSpeed,
72		},
73		BaseDriver: &drivers.BaseDriver{
74			MachineName: hostName,
75			StorePath:   storePath,
76		},
77	}
78}
79
80func (d *Driver) GetSSHHostname() (string, error) {
81	return d.GetIP()
82}
83
84func (d *Driver) GetCreateFlags() []mcnflag.Flag {
85	// Set hourly billing to true by default since codegangsta cli doesn't take default bool values
86	if os.Getenv("SOFTLAYER_HOURLY_BILLING") == "" {
87		os.Setenv("SOFTLAYER_HOURLY_BILLING", "true")
88	}
89	return []mcnflag.Flag{
90		mcnflag.IntFlag{
91			EnvVar: "SOFTLAYER_MEMORY",
92			Name:   "softlayer-memory",
93			Usage:  "Memory in MB for machine",
94			Value:  defaultMemory,
95		},
96		mcnflag.IntFlag{
97			EnvVar: "SOFTLAYER_DISK_SIZE",
98			Name:   "softlayer-disk-size",
99			Usage:  "Disk size for machine, a value of 0 uses the default size on softlayer",
100			Value:  defaultDiskSize,
101		},
102		mcnflag.StringFlag{
103			EnvVar: "SOFTLAYER_USER",
104			Name:   "softlayer-user",
105			Usage:  "softlayer user account name",
106		},
107		mcnflag.StringFlag{
108			EnvVar: "SOFTLAYER_API_KEY",
109			Name:   "softlayer-api-key",
110			Usage:  "softlayer user API key",
111		},
112		mcnflag.StringFlag{
113			EnvVar: "SOFTLAYER_REGION",
114			Name:   "softlayer-region",
115			Usage:  "softlayer region for machine",
116			Value:  defaultRegion,
117		},
118		mcnflag.IntFlag{
119			EnvVar: "SOFTLAYER_CPU",
120			Name:   "softlayer-cpu",
121			Usage:  "number of CPU's for the machine",
122			Value:  defaultCpus,
123		},
124		mcnflag.StringFlag{
125			EnvVar: "SOFTLAYER_HOSTNAME",
126			Name:   "softlayer-hostname",
127			Usage:  "hostname for the machine - defaults to machine name",
128		},
129		mcnflag.StringFlag{
130			EnvVar: "SOFTLAYER_DOMAIN",
131			Name:   "softlayer-domain",
132			Usage:  "domain name for machine",
133		},
134		mcnflag.StringFlag{
135			EnvVar: "SOFTLAYER_API_ENDPOINT",
136			Name:   "softlayer-api-endpoint",
137			Usage:  "softlayer api endpoint to use",
138			Value:  APIEndpoint,
139		},
140		mcnflag.BoolFlag{
141			EnvVar: "SOFTLAYER_HOURLY_BILLING",
142			Name:   "softlayer-hourly-billing",
143			Usage:  "set hourly billing for machine - on by default",
144		},
145		mcnflag.BoolFlag{
146			EnvVar: "SOFTLAYER_LOCAL_DISK",
147			Name:   "softlayer-local-disk",
148			Usage:  "use machine local disk instead of softlayer SAN",
149		},
150		mcnflag.BoolFlag{
151			EnvVar: "SOFTLAYER_PRIVATE_NET",
152			Name:   "softlayer-private-net-only",
153			Usage:  "Use only private networking",
154		},
155		mcnflag.StringFlag{
156			EnvVar: "SOFTLAYER_IMAGE",
157			Name:   "softlayer-image",
158			Usage:  "OS image for machine",
159			Value:  defaultImage,
160		},
161		mcnflag.IntFlag{
162			EnvVar: "SOFTLAYER_PUBLIC_VLAN_ID",
163			Name:   "softlayer-public-vlan-id",
164			Usage:  "",
165		},
166		mcnflag.IntFlag{
167			EnvVar: "SOFTLAYER_PRIVATE_VLAN_ID",
168			Name:   "softlayer-private-vlan-id",
169			Usage:  "",
170		},
171		mcnflag.IntFlag{
172			EnvVar: "SOFTLAYER_NETWORK_MAX_SPEED",
173			Name:   "softlayer-network-max-speed",
174			Usage:  "Max speed of public and private network",
175			Value:  defaultNetworkMaxSpeed,
176		},
177	}
178}
179
180func validateDeviceConfig(c *deviceConfig) error {
181	if c.Domain == "" {
182		return fmt.Errorf("Missing required setting - --softlayer-domain")
183	}
184
185	if c.Region == "" {
186		return fmt.Errorf("Missing required setting - --softlayer-region")
187	}
188	if c.Cpu < 1 {
189		return fmt.Errorf("Missing required setting - --softlayer-cpu")
190	}
191
192	if c.PrivateNet && c.PublicVLAN > 0 {
193		return fmt.Errorf("Can not specify both --softlayer-private-net-only and --softlayer-public-vlan-id")
194	}
195	if c.PublicVLAN > 0 && c.PrivateVLAN == 0 {
196		return fmt.Errorf("Missing required setting - --softlayer-private-vlan-id (because --softlayer-public-vlan-id is specified)")
197	}
198	if c.PrivateVLAN > 0 && !c.PrivateNet && c.PublicVLAN == 0 {
199		return fmt.Errorf("Missing required setting - --softlayer-public-vlan-id (because --softlayer-private-vlan-id is specified)")
200	}
201
202	return nil
203}
204
205func validateClientConfig(c *Client) error {
206	if c.ApiKey == "" {
207		return fmt.Errorf("Missing required setting - --softlayer-api-key")
208	}
209
210	if c.User == "" {
211		return fmt.Errorf("Missing required setting - --softlayer-user")
212	}
213
214	if c.Endpoint == "" {
215		return fmt.Errorf("Missing required setting - --softlayer-api-endpoint")
216	}
217
218	return nil
219}
220
221func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
222
223	d.Client = &Client{
224		Endpoint: flags.String("softlayer-api-endpoint"),
225		User:     flags.String("softlayer-user"),
226		ApiKey:   flags.String("softlayer-api-key"),
227	}
228
229	d.SetSwarmConfigFromFlags(flags)
230	d.SSHUser = "root"
231	d.SSHPort = 22
232
233	if err := validateClientConfig(d.Client); err != nil {
234		return err
235	}
236
237	d.deviceConfig = &deviceConfig{
238		Hostname:        flags.String("softlayer-hostname"),
239		DiskSize:        flags.Int("softlayer-disk-size"),
240		Cpu:             flags.Int("softlayer-cpu"),
241		Domain:          flags.String("softlayer-domain"),
242		Memory:          flags.Int("softlayer-memory"),
243		PrivateNet:      flags.Bool("softlayer-private-net-only"),
244		LocalDisk:       flags.Bool("softlayer-local-disk"),
245		HourlyBilling:   flags.Bool("softlayer-hourly-billing"),
246		Image:           flags.String("softlayer-image"),
247		Region:          flags.String("softlayer-region"),
248		PublicVLAN:      flags.Int("softlayer-public-vlan-id"),
249		PrivateVLAN:     flags.Int("softlayer-private-vlan-id"),
250		NetworkMaxSpeed: flags.Int("softlayer-network-max-speed"),
251	}
252
253	if d.deviceConfig.Hostname == "" {
254		d.deviceConfig.Hostname = d.GetMachineName()
255	}
256
257	return validateDeviceConfig(d.deviceConfig)
258}
259
260func (d *Driver) getClient() *Client {
261	return d.Client
262}
263
264// DriverName returns the name of the driver
265func (d *Driver) DriverName() string {
266	return "softlayer"
267}
268
269func (d *Driver) GetURL() (string, error) {
270	if err := drivers.MustBeRunning(d); err != nil {
271		return "", err
272	}
273
274	ip, err := d.GetIP()
275	if err != nil {
276		return "", err
277	}
278	if ip == "" {
279		return "", nil
280	}
281
282	return "tcp://" + net.JoinHostPort(ip, "2376"), nil
283}
284
285func (d *Driver) GetIP() (string, error) {
286	if d.IPAddress != "" {
287		return d.IPAddress, nil
288	}
289	if d.deviceConfig != nil && d.deviceConfig.PrivateNet == true {
290		return d.getClient().VirtualGuest().GetPrivateIP(d.Id)
291	}
292
293	if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "" {
294		os.Setenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP", "false")
295	}
296
297	if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "true" {
298		return d.getClient().VirtualGuest().GetPrivateIP(d.Id)
299	} else {
300		return d.getClient().VirtualGuest().GetPublicIP(d.Id)
301	}
302}
303
304func (d *Driver) GetState() (state.State, error) {
305	s, err := d.getClient().VirtualGuest().PowerState(d.Id)
306	if err != nil {
307		return state.None, err
308	}
309	var vmState state.State
310	switch s {
311	case "Running":
312		vmState = state.Running
313	case "Halted":
314		vmState = state.Stopped
315	default:
316		vmState = state.None
317	}
318	return vmState, nil
319}
320
321func (d *Driver) GetActiveTransaction() (string, error) {
322	t, err := d.getClient().VirtualGuest().ActiveTransaction(d.Id)
323	if err != nil {
324		return "", err
325	}
326	return t, nil
327}
328
329func (d *Driver) waitForStart() {
330	log.Infof("Waiting for host to become available")
331	for {
332		s, err := d.GetState()
333		if err != nil {
334			log.Debugf("Failed to GetState - %+v", err)
335			continue
336		}
337
338		if s == state.Running {
339			break
340		} else {
341			log.Debugf("Still waiting - state is %s...", s)
342		}
343		time.Sleep(2 * time.Second)
344	}
345}
346
347func (d *Driver) getIP() (string, error) {
348	log.Infof("Getting Host IP")
349	for {
350		var (
351			ip  string
352			err error
353		)
354		if d.deviceConfig.PrivateNet {
355			ip, err = d.getClient().VirtualGuest().GetPrivateIP(d.Id)
356		} else {
357			ip, err = d.getClient().VirtualGuest().GetPublicIP(d.Id)
358		}
359		if err != nil {
360			time.Sleep(2 * time.Second)
361			continue
362		}
363
364		// if docker daemon is expected to run on private IP
365		// it will overwrite settings from PrivateNet
366		if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "" {
367			os.Setenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP", "false")
368		}
369
370		if os.Getenv("SOFTLAYER_DOCKER_ON_PRIVATE_IP") == "true" {
371			ip, err = d.getClient().VirtualGuest().GetPrivateIP(d.Id)
372		} else {
373			ip, err = d.getClient().VirtualGuest().GetPublicIP(d.Id)
374		}
375
376		// not a perfect regex, but should be just fine for our needs
377		exp := regexp.MustCompile(`\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}`)
378		if exp.MatchString(ip) {
379			d.IPAddress = ip
380			return ip, nil
381		}
382		time.Sleep(2 * time.Second)
383	}
384}
385
386func (d *Driver) waitForSetupTransactions() {
387	log.Infof("Waiting for host setup transactions to complete")
388	// sometimes we'll hit a case where there's no active transaction, but if
389	// we check again in a few seconds, it moves to the next transaction. We
390	// don't want to get false-positives, so we check a few times in a row to make sure!
391	noActiveCount, maxNoActiveCount := 0, 3
392	for {
393		t, err := d.GetActiveTransaction()
394		if err != nil {
395			noActiveCount = 0
396			log.Debugf("Failed to GetActiveTransaction - %+v", err)
397			continue
398		}
399
400		if t == "" {
401			if noActiveCount == maxNoActiveCount {
402				break
403			}
404			noActiveCount++
405		} else {
406			noActiveCount = 0
407			log.Debugf("Still waiting - active transaction is %s...", t)
408		}
409		time.Sleep(2 * time.Second)
410	}
411}
412
413func (d *Driver) Create() error {
414	spec := d.buildHostSpec()
415
416	log.Infof("Creating SSH key...")
417	key, err := d.createSSHKey()
418	if err != nil {
419		return err
420	}
421
422	log.Infof("SSH key %s (%d) created in SoftLayer", key.Label, key.Id)
423	d.SSHKeyID = key.Id
424
425	spec.SshKeys = []*SSHKey{key}
426
427	id, err := d.getClient().VirtualGuest().Create(spec)
428	if err != nil {
429		return fmt.Errorf("Error creating host: %q", err)
430	}
431	d.Id = id
432	d.getIP()
433	d.waitForStart()
434	d.waitForSetupTransactions()
435
436	return nil
437}
438
439func (d *Driver) buildHostSpec() *HostSpec {
440	spec := &HostSpec{
441		Hostname:       d.deviceConfig.Hostname,
442		Domain:         d.deviceConfig.Domain,
443		Cpu:            d.deviceConfig.Cpu,
444		Memory:         d.deviceConfig.Memory,
445		Datacenter:     Datacenter{Name: d.deviceConfig.Region},
446		Os:             d.deviceConfig.Image,
447		HourlyBilling:  d.deviceConfig.HourlyBilling,
448		PrivateNetOnly: d.deviceConfig.PrivateNet,
449		LocalDisk:      d.deviceConfig.LocalDisk,
450	}
451
452	if d.deviceConfig.NetworkMaxSpeed > 0 {
453		spec.NetworkMaxSpeeds = []NetworkMaxSpeed{{MaxSpeed: d.deviceConfig.NetworkMaxSpeed}}
454	}
455	if d.deviceConfig.DiskSize > 0 {
456		spec.BlockDevices = []BlockDevice{{Device: "0", DiskImage: DiskImage{Capacity: d.deviceConfig.DiskSize}}}
457	}
458	if d.deviceConfig.PublicVLAN > 0 {
459		spec.PrimaryNetworkComponent = &NetworkComponent{
460			NetworkVLAN: &NetworkVLAN{
461				Id: d.deviceConfig.PublicVLAN,
462			},
463		}
464	}
465	if d.deviceConfig.PrivateVLAN > 0 {
466		spec.PrimaryBackendNetworkComponent = &NetworkComponent{
467			NetworkVLAN: &NetworkVLAN{
468				Id: d.deviceConfig.PrivateVLAN,
469			},
470		}
471	}
472	log.Debugf("Built host spec %#v", spec)
473	return spec
474}
475
476func (d *Driver) createSSHKey() (*SSHKey, error) {
477	if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil {
478		return nil, err
479	}
480
481	publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath())
482	if err != nil {
483		return nil, err
484	}
485
486	key, err := d.getClient().SSHKey().Create(d.deviceConfig.Hostname, string(publicKey))
487	if err != nil {
488		return nil, err
489	}
490
491	return key, nil
492}
493
494func (d *Driver) publicSSHKeyPath() string {
495	return d.GetSSHKeyPath() + ".pub"
496}
497
498func (d *Driver) Remove() error {
499	log.Infof("Canceling SoftLayer instance %d...", d.Id)
500	var err error
501	for i := 0; i < 5; i++ {
502		if err = d.getClient().VirtualGuest().Cancel(d.Id); err != nil {
503			time.Sleep(2 * time.Second)
504			continue
505		}
506		break
507	}
508	if err != nil {
509		return err
510	}
511
512	log.Infof("Removing SSH Key %d...", d.SSHKeyID)
513	if err = d.getClient().SSHKey().Delete(d.SSHKeyID); err != nil {
514		return err
515	}
516
517	return nil
518}
519
520func (d *Driver) Start() error {
521	return d.getClient().VirtualGuest().PowerOn(d.Id)
522}
523
524func (d *Driver) Stop() error {
525	return d.getClient().VirtualGuest().PowerOff(d.Id)
526}
527
528func (d *Driver) Restart() error {
529	return d.getClient().VirtualGuest().Reboot(d.Id)
530}
531
532func (d *Driver) Kill() error {
533	return d.Stop()
534}
535