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