From c03f6926c0cd002760a546bc812d1a437abc2ee2 Mon Sep 17 00:00:00 2001 From: Albert Choi Date: Fri, 4 Dec 2015 11:27:01 -0800 Subject: [PATCH] initial version --- .gitignore | 1 + LICENSE | 190 +++++++++++++++++ README.md | 90 ++++++++ bin/.gitkeep | 0 build.sh | 25 +++ driver.go | 572 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 878 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bin/.gitkeep create mode 100755 build.sh create mode 100644 driver.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaac832 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/docker-machine-driver-clc* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..589848b --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2015 CenturyLinkCloud + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..51f24f1 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ + +# Docker Machine driver for CenturyLinkCloud + +```bash +docker-machine create -d clc machine0 +``` + + + +## Installation + +The easiest way to install ovh docker-machine driver is to: + +```bash +go install github.com/CenturyLinkCloud/docker-machine-driver-clc +``` + +## Example Usage + +inspect available options via --help + + +```bash +export CLC_USERNAME='' +export CLC_PASSWORD='' +export CLC_ALIAS='' + +token=$(docker run swarm create) +docker-machine create -d clc --clc-server-group "my docker swarm" --swarm --swarm-discovery --swarm-token token://$token master +docker $(docker-machine config --swarm master) info +``` + + +## Options + +```bash +docker-machine create -d clc --help +``` + + +| Option Name | Description | Default Value | required | +|------------------------------------------------------|-------------------------------------------------|-----------------------+----------| +| ``--clc-account-username`` or ``$CLC_USERNAME`` | CLC account user | none | yes | +| ``--clc-account-password`` or ``$CLC_PASSWORD`` | CLC account password | none | yes | +| ``--clc-account-alias`` or ``$CLC_ALIAS`` | CLC account alias | none | yes | +| ``--clc-server-private`` or ``CLC_SERVER_PRIVATE`` | allocates public ip (if disabled, VPN required) | false | no | +| ``--clc-server-group`` or ``$CLC_SERVER_GROUP`` | server group (name or id) to spawn into | Default Group | no | +| ``--clc-server-location`` or ``CLC_SERVER_LOCATION`` | datacenter | WA1 | no | +| ``--clc-server-cpu`` or ``CLC_SERVER_CPU`` | cpu cores | 2 | no | +| ``--clc-server-mem`` or ``CLC_SERVER_MEM`` | memory GB | 2 | no | +| ``--clc-server-template`` or ``CLC_SERVER_TEMPLATE`` | OS image | ubuntu-14-64-template | no | +| ``--clc-ssh-user`` or ``CLC_SSH_USER`` | ssh user (specific to OS image) | root | no | +| ``--clc-ssh-password`` or ``CLC_SSH_PASSWORD`` | ssh password | | no | + + +Each environment variable may be overloaded by its option equivalent at runtime. + +## Kernels + +The default ubuntu image runs kernel 3.13.xx but advanced swarm/networking features require a newer kernel + +Optionally `Docker-machine ssh` in and `apt-get install -qy linux-image-generic-lts-wily && reboot` + +## Hacking + +### Get the sources + +```bash +go get github.com/CenturyLinkCloud/docker-machine-driver-clc +cd $GOPATH/src/github.com/CenturyLinkCloud/docker-machine-driver-clc +``` + +### Test the driver + +To test the driver make sure your current build directory has the highest +priority in your ``$PATH`` so that docker-machine can find it. + +``` +export PATH=$GOPATH/src/github.com/CenturyLinkCloud/docker-machine-driver-clc:$PATH +``` + +## Related links + +- **Docker Machine**: https://docs.docker.com/machine/ +- **Contribute**: https://github.com/CenturyLinkCloud/docker-machine-driver-clc +- **Report bugs**: https://github.com/CenturyLinkCloud/docker-machine-driver-clc/issues + +## License + +Apache 2.0 diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..73c4041 --- /dev/null +++ b/build.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +OS="darwin linux windows" +ARCH="amd64" + +echo "Getting build dependencies" +go get -u github.com/golang/lint/golint + +echo "Ensuring code quality" +go vet ./... +golint ./... + +dmver=$(cd $GOPATH/src/github.com/docker/machine && git describe --abbrev=0 --tags) +echo "VERSION docker-machine '$dmver'" + +for GOOS in $OS; do + for GOARCH in $ARCH; do + arch="$GOOS-$GOARCH" + binary="bin/docker-machine-driver-clc-$arch" + echo "Building $binary" + GOOS=$GOOS GOARCH=$GOARCH go build -o $binary + done +done diff --git a/driver.go b/driver.go new file mode 100644 index 0000000..ef3483b --- /dev/null +++ b/driver.go @@ -0,0 +1,572 @@ +package main + +import ( + "fmt" + "io/ioutil" + "math/rand" + "time" + + sdk "github.com/CenturyLinkCloud/clc-sdk" + api "github.com/CenturyLinkCloud/clc-sdk/api" + group "github.com/CenturyLinkCloud/clc-sdk/group" + server "github.com/CenturyLinkCloud/clc-sdk/server" + "github.com/CenturyLinkCloud/clc-sdk/status" + "github.com/docker/machine/libmachine/drivers" + "github.com/docker/machine/libmachine/drivers/plugin" + "github.com/docker/machine/libmachine/log" + "github.com/docker/machine/libmachine/mcnflag" + "github.com/docker/machine/libmachine/ssh" + "github.com/docker/machine/libmachine/state" +) + +func main() { + plugin.RegisterDriver(&Driver{}) +} + +// Driver for CLC +type Driver struct { + *drivers.BaseDriver + Username string // CLC account + credentials + Password string + Alias string + SSHUsername string + SSHPassword string + DockerPort int + DockerSwarmMasterPort int + ServerID string // https://www.ctl.io/api-docs/v2/#servers-create-server + Location string + Template string + CPU int + MemoryGB int + ServerType string + GroupName string + NameTemplate string + Description string + Public bool // allocate public IP for docker ports + PublicIP string // calculated public IP +} + +const ( + defaultSSHPort = 22 + defaultSSHUser = "root" + defaultDockerPort = 2376 + defaultSwarmMasterPort = 3376 + defaultLocation = "WA1" + defaultTemplate = "ubuntu-14-64-template" + defaultCPU = 2 + defaultMemoryGB = 2 + defaultServerType = "standard" + defaultGroupName = "Default Group" + defaultNameTemplate = "DOCK" + defaultDescription = "docker-machine" + defaultSSHKeyPackage = "77abb844-579d-478d-3955-c69ab4a7ba1a" // uuid of ssh pubkey pkg +) + +// GetCreateFlags registers the flags this d adds to +// "docker hosts create" +func (d *Driver) GetCreateFlags() []mcnflag.Flag { + return []mcnflag.Flag{ + mcnflag.StringFlag{ + EnvVar: "CLC_USERNAME", + Name: "clc-account-username", + Usage: "REQUIRED: CLC account username", + }, + mcnflag.StringFlag{ + EnvVar: "CLC_PASSWORD", + Name: "clc-account-password", + Usage: "REQUIRED: CLC account password", + }, + mcnflag.StringFlag{ + EnvVar: "CLC_ALIAS", + Name: "clc-account-alias", + Usage: "REQUIRED: CLC account alias", + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SSH_USER", + Name: "clc-ssh-user", + Usage: "ssh username (default:root)", + Value: defaultSSHUser, + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SSH_PASSWORD", + Name: "clc-ssh-password", + Usage: "ssh password (default:)", + }, + mcnflag.IntFlag{ + EnvVar: "CLC_SSH_PORT", + Name: "clc-ssh-port", + Usage: "ssh port (default:22)", + Value: defaultSSHPort, + }, + mcnflag.IntFlag{ + EnvVar: "CLC_DOCKER_PORT", + Name: "clc-docker-port", + Usage: "docker port (default:2376)", + Value: defaultDockerPort, + }, + mcnflag.IntFlag{ + EnvVar: "CLC_DOCKER_SWARM_MASTER_PORT", + Name: "clc-docker-swarm-master-port", + Usage: "swarm master port (default:3376)", + Value: defaultSwarmMasterPort, + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SERVER_LOCATION", + Name: "clc-server-location", + Usage: "datacenter location (default:WA1)", + Value: defaultLocation, + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SERVER_TEMPLATE", + Name: "clc-server-template", + Usage: "server template (default:ubuntu-14-64-template)", + Value: defaultTemplate, + }, + mcnflag.IntFlag{ + EnvVar: "CLC_SERVER_CPU", + Name: "clc-server-cpu", + Usage: "server cpu cores (default:2)", + Value: defaultCPU, + }, + mcnflag.IntFlag{ + EnvVar: "CLC_SERVER_MEM", + Name: "clc-server-mem", + Usage: "server memory in GB (default:2)", + Value: defaultMemoryGB, + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SERVER_TYPE", + Name: "clc-server-type", + Usage: "server type (default:standard)", + Value: defaultServerType, + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SERVER_GROUP", + Name: "clc-server-group", + Usage: "server group name (default:Default Group)", + Value: defaultGroupName, + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SERVER_NAME", + Name: "clc-server-name", + Usage: "server name template (default:DOCK)", + Value: defaultNameTemplate, + }, + mcnflag.StringFlag{ + EnvVar: "CLC_SERVER_DESC", + Name: "clc-server-desc", + Usage: "server description (default:docker-machine)", + Value: defaultDescription, + }, + mcnflag.BoolFlag{ + EnvVar: "CLC_SERVER_PRIVATE", + Name: "clc-server-private", + Usage: "disable public IP (default:publicly accessible)", + }, + } +} + +// NewDriver instantiates a new driver with hostName into storePath +func NewDriver(hostName, storePath string) drivers.Driver { + d := &Driver{ + DockerPort: defaultDockerPort, + DockerSwarmMasterPort: defaultSwarmMasterPort, + /* + Location: defaultLocation, + Template: defaultTemplate, + CPU: defaultCPU, + MemoryGB: defaultMemoryGB, + ServerType: defaultServerType, + GroupName: defaultGroupName, + NameTemplate: defaultNameTemplate, + Description: defaultDescription, + */ + BaseDriver: &drivers.BaseDriver{ + SSHPort: defaultSSHPort, + SSHUser: defaultSSHUser, + MachineName: hostName, + StorePath: storePath, + }, + } + return d +} + +// SetConfigFromFlags implements interface method for parsing cmdline args +func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { + d.Username = flags.String("clc-account-username") + d.Password = flags.String("clc-account-password") + d.Alias = flags.String("clc-account-alias") + if d.Username == "" || d.Password == "" || d.Alias == "" { + return fmt.Errorf("Missing CLC Account credentials (see help)") + } + + d.SSHPort = flags.Int("clc-ssh-port") + if d.SSHPort == 0 { + d.SSHPort = defaultSSHPort + } + d.SSHUser = flags.String("clc-ssh-user") + if d.SSHUser == "" { + d.SSHUser = defaultSSHUser + } + d.SSHPassword = flags.String("clc-ssh-password") + if d.SSHPassword == "" { + d.SSHPassword = generatePassword(12) + log.Infof("SSH Password not provided. Generated: %v", d.SSHPassword) + } + d.DockerPort = flags.Int("clc-docker-port") + d.DockerSwarmMasterPort = flags.Int("clc-docker-swarm-master-port") + + //d.SwarmMaster = flags.Bool("swarm-master") + //d.SwarmHost = flags.String("swarm-host") + //d.SwarmDiscovery = flags.String("swarm-discovery") + + d.Location = flags.String("clc-server-location") + d.Template = flags.String("clc-server-template") + d.CPU = flags.Int("clc-server-cpu") + d.MemoryGB = flags.Int("clc-server-mem") + d.ServerType = flags.String("clc-server-type") + d.GroupName = flags.String("clc-server-group") + d.NameTemplate = flags.String("clc-server-name") + d.Description = flags.String("clc-server-desc") + d.Public = flags.Bool("clc-server-private") == false + log.Warnf("public: %v", d.Public) + return nil +} + +var apiClient *sdk.Client + +func (d *Driver) client() *sdk.Client { + if apiClient == nil { + config := api.NewConfig(d.Username, d.Password, d.Alias) + apiClient = sdk.New(config) + err := apiClient.Authenticate() + if err != nil { + log.Errorf("Error authenticating %v", err) + } + } + return apiClient +} + +// GetIP returns an IP or hostname that this host is available at +// e.g. 1.2.3.4 or docker-host-d60b70a14d3a.cloudapp.net +func (d *Driver) GetIP() (string, error) { + ip, err := d.detectIP() + if err != nil { + return "", err + } + return ip, nil +} + +// GetSSHHostname aliases GetIP +func (d *Driver) GetSSHHostname() (string, error) { + return d.GetIP() +} + +// GetSSHUsername returns a configurable ssh user +func (d *Driver) GetSSHUsername() string { + if d.SSHUser == "" { + d.SSHUser = "root" + } + return d.SSHUser +} + +// GetURL returns a Docker compatible host URL for connecting to this host +// e.g. tcp://1.2.3.4:2376 +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + return fmt.Sprintf("tcp://%v:%v", ip, d.DockerPort), nil +} + +// DriverName returns the name of the driver +func (d *Driver) DriverName() string { + return "clc" +} + +// Create a new machine (installs a generated pubkey and optionally allocates a public IP) +func (d *Driver) Create() error { + m, err := dcGroups(d.client(), d.Location) + if err != nil { + return fmt.Errorf("Failed pulling groups in location %v - %v", d.Location, err) + } + + gid, ok := m[d.GroupName] + if !ok { + return fmt.Errorf("Failed resolving group %v", d.GroupName) + } + log.Debugf("Spawning server into group: %v", gid) + + spec := server.Server{ + Name: d.NameTemplate, + Password: d.SSHPassword, + Description: d.Description, + GroupID: gid, + CPU: d.CPU, + MemoryGB: d.MemoryGB, + SourceServerID: d.Template, + Type: d.ServerType, + } + + if false { + //spec.Additionaldisks = disks + } + if false { + //spec.Customfields = fields + } + + log.Debugf("Spawning server with: %v", spec) + + resp, err := d.client().Server.Create(spec) + if err != nil { + return fmt.Errorf("Error creating server: %v", err) + } + + ok, st := resp.GetStatusID() + if !ok { + return fmt.Errorf("Failed extracting status to poll on %v: %v", resp, err) + } + err = waitStatus(d.client(), st) + if err != nil { + return fmt.Errorf("Failed polling status: %v", err) + } + + _, uuid := resp.Links.GetID("self") + s, err := d.client().Server.Get(uuid) + + d.ServerID = s.ID + log.Infof("Created server: %v", d.ServerID) + + // add ssh pubkey + if err := ssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { + return err + } + path := fmt.Sprintf("%v.pub", d.GetSSHKeyPath()) + pubkey, err := ioutil.ReadFile(path) + if err != nil { + return err + } + pkg := server.Package{ + ID: defaultSSHKeyPackage, + Params: map[string]string{ + "User": d.SSHUser, + "SshKey": string(pubkey), + }, + } + presp, err := d.client().Server.ExecutePackage(pkg, d.ServerID) + if err != nil { + return fmt.Errorf("Failed exec'ing pubkey package on %v - %v", d.ServerID, err) + } + ok, st = presp[0].GetStatusID() + err = waitStatus(d.client(), st) + if err != nil { + return fmt.Errorf("Failed installing pubkey on %v - %v", d.ServerID, err) + } + log.Infof("SSH Pubkey installed from %v for user %v", path, d.SSHUser) + + // allocate IP and open ports + if d.Public { + internal, err := d.detectIP() + pip := server.PublicIP{ + InternalIP: internal, + Ports: []server.Port{ + server.Port{ + Protocol: "tcp", + Port: d.SSHPort, + }, + server.Port{ + Protocol: "tcp", + Port: d.DockerPort, + }, + server.Port{ + Protocol: "tcp", + Port: d.DockerSwarmMasterPort, + }, + }, + } + resp, err := d.client().Server.AddPublicIP(s.ID, pip) + if err != nil { + return fmt.Errorf("Failed adding public ip to %v - %v", s.ID, err) + } + err = waitStatus(d.client(), resp.ID) + if err != nil { + return fmt.Errorf("Failed while polling public ip %v - %v", s.ID, err) + } + + // scan NICs for any public ip + s, err = d.client().Server.Get(s.ID) + ip, err := d.detectIP() + if err != nil { + return fmt.Errorf("Failed detecting public ip on %v - %v", d.ServerID, err) + } + d.PublicIP = ip + log.Infof("Added public IP %v to %v", d.PublicIP, d.ServerID) + } + + return nil +} + +// GetState returns the state that the host is in (running, stopped, etc) +func (d *Driver) GetState() (state.State, error) { + s, err := d.client().Server.Get(d.ServerID) + if err != nil { + return state.Error, fmt.Errorf("Failed fetching server %v - %v", d.ServerID, err) + } + log.Debugf("server.status: %v powerstate: %v", s.Status, s.Details.Powerstate) + if s.Status == "underConstruction" { + return state.Starting, nil + } + + switch s.Details.Powerstate { + case "started": + return state.Running, nil + case "stopped": + return state.Stopped, nil + case "paused": + return state.Paused, nil + } + log.Warnf("server powerstate: %v not matched, returning state.None", s.Details.Powerstate) + return state.None, nil +} + +// PreCreateCheck allows for pre-create operations to make sure a driver is ready for creation +func (d *Driver) PreCreateCheck() error { + return nil +} + +// Remove a host +func (d *Driver) Remove() error { + return nil +} + +// Start a host +func (d *Driver) Start() error { + resp, err := d.client().Server.PowerState(server.On, d.ServerID) + st := "" + if err == nil { + _, st = resp[0].GetStatusID() + } + if err == nil && st != "" { + err = waitStatus(d.client(), st) + } + if err != nil { + return fmt.Errorf("Failed starting server: %v - %v", d.ServerID, err) + } + return nil +} + +// Stop a host gracefully +func (d *Driver) Stop() error { + resp, err := d.client().Server.PowerState(server.Off, d.ServerID) + st := "" + if err == nil { + _, st = resp[0].GetStatusID() + } + if err == nil && st != "" { + err = waitStatus(d.client(), st) + } + if err != nil { + return fmt.Errorf("Failed stopping server: %v - %v", d.ServerID, err) + } + return nil +} + +// Restart a host. This may just call Stop(); Start() if the provider does not +// have any special restart behaviour. +func (d *Driver) Restart() error { + var err error + err = d.Stop() + if err != nil { + return err + } + err = d.Start() + if err != nil { + return err + } + return nil +} + +// Kill stops a host forcefully +func (d *Driver) Kill() error { + resp, err := d.client().Server.Delete(d.ServerID) + st := "" + if err == nil { + _, st = resp.GetStatusID() + } + if err == nil && st != "" { + err = waitStatus(d.client(), st) + } + if err != nil { + return fmt.Errorf("Failed killing server: %v - %v", d.ServerID, err) + } + return nil +} + +func (d *Driver) detectIP() (string, error) { + // scan NICs for any public ip + s, err := d.client().Server.Get(d.ServerID) + if err != nil { + return "", fmt.Errorf("Failed fetching server while fetching IP: %v", err) + } + ip := "" + for _, i := range s.Details.IPaddresses { + if i.Public != "" { + log.Debugf("Found public ip: %v", i.Public) + ip = i.Public + } + } + if ip == "" { + log.Infof("Failed finding public IP on %v. scanning private NICs", d.ServerID) + for _, i := range s.Details.IPaddresses { + if i.Internal != "" { + log.Debugf("Found private ip: %v", i.Internal) + ip = i.Internal + } + } + } + return ip, nil +} + +func waitStatus(client *sdk.Client, id string) error { + // block until queue is processed and server is up + poll := make(chan *status.Response, 1) + err := client.Status.Poll(id, poll) + if err != nil { + return nil + } + status := <-poll + log.Debugf("status %v", status) + return nil +} + +func dcGroups(apiClient *sdk.Client, dcname string) (map[string]string, error) { + // FIXME: does not handle name collisions. passing group id oughta work though + dc, _ := apiClient.DC.Get(dcname) + _, id := dc.Links.GetID("group") + m := map[string]string{} + resp, _ := apiClient.Group.Get(id) + m[resp.Name] = resp.ID // top + for _, x := range resp.Groups { + deepGroups(x, &m) + } + return m, nil +} + +func deepGroups(g group.Groups, m *map[string]string) { + (*m)[g.Name] = g.ID + for _, sg := range g.Groups { + deepGroups(sg, m) + } +} + +func generatePassword(strlen int) string { + // adapted from http://siongui.github.io/2015/04/13/go-generate-random-string/ + rand.Seed(time.Now().UTC().UnixNano()) + const chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&()_+" + result := make([]byte, strlen) + for i := 0; i < strlen; i++ { + result[i] = chars[rand.Intn(len(chars))] + } + return string(result) +}