docs/drivers/exoscale/exoscale.go

471 lines
11 KiB
Go

package exoscale
import (
"bytes"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/codegangsta/cli"
"github.com/docker/machine/drivers"
"github.com/docker/machine/log"
"github.com/docker/machine/provider"
"github.com/docker/machine/state"
"github.com/pyr/egoscale/src/egoscale"
)
type Driver struct {
URL string
ApiKey string
ApiSecretKey string
InstanceProfile string
DiskSize int
Image string
SecurityGroup string
AvailabilityZone string
MachineName string
KeyPair string
IPAddress string
PublicKey string
Id string
CaCertPath string
PrivateKeyPath string
DriverKeyPath string
SwarmMaster bool
SwarmHost string
SwarmDiscovery string
storePath string
}
func init() {
drivers.Register("exoscale", &drivers.RegisteredDriver{
New: NewDriver,
GetCreateFlags: GetCreateFlags,
})
}
// RegisterCreateFlags registers the flags this driver adds to
// "docker hosts create"
func GetCreateFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
EnvVar: "EXOSCALE_ENDPOINT",
Name: "exoscale-url",
Usage: "exoscale API endpoint",
},
cli.StringFlag{
EnvVar: "EXOSCALE_API_KEY",
Name: "exoscale-api-key",
Usage: "exoscale API key",
},
cli.StringFlag{
EnvVar: "EXOSCALE_API_SECRET",
Name: "exoscale-api-secret-key",
Usage: "exoscale API secret key",
},
cli.StringFlag{
EnvVar: "EXOSCALE_INSTANCE_PROFILE",
Name: "exoscale-instance-profile",
Value: "small",
Usage: "exoscale instance profile (small, medium, large, ...)",
},
cli.IntFlag{
EnvVar: "EXOSCALE_DISK_SIZE",
Name: "exoscale-disk-size",
Value: 50,
Usage: "exoscale disk size (10, 50, 100, 200, 400)",
},
cli.StringFlag{
EnvVar: "EXSOCALE_IMAGE",
Name: "exoscale-image",
Value: "ubuntu-14.04",
Usage: "exoscale image template",
},
cli.StringFlag{
EnvVar: "EXOSCALE_SECURITY_GROUP",
Name: "exoscale-security-group",
Value: "docker-machine",
Usage: "exoscale security group",
},
cli.StringFlag{
EnvVar: "EXOSCALE_AVAILABILITY_ZONE",
Name: "exoscale-availability-zone",
Value: "ch-gva-2",
Usage: "exoscale availibility zone",
},
cli.StringFlag{
EnvVar: "EXOSCALE_KEYPAIR",
Name: "exoscale-keypair",
Usage: "exoscale keypair name",
},
}
}
func NewDriver(machineName string, storePath string, caCert string, privateKey string) (drivers.Driver, error) {
return &Driver{MachineName: machineName, storePath: storePath, CaCertPath: caCert, PrivateKeyPath: privateKey}, nil
}
func (d *Driver) AuthorizePort(ports []*drivers.Port) error {
return nil
}
func (d *Driver) DeauthorizePort(ports []*drivers.Port) error {
return nil
}
func (d *Driver) GetMachineName() string {
return d.MachineName
}
func (d *Driver) GetSSHHostname() (string, error) {
return d.GetIP()
}
func (d *Driver) GetSSHKeyPath() string {
return filepath.Join(d.storePath, "id_rsa")
}
func (d *Driver) GetSSHPort() (int, error) {
return 22, nil
}
func (d *Driver) GetSSHUsername() string {
return "ubuntu"
}
func (d *Driver) GetProviderType() provider.ProviderType {
return provider.Remote
}
func (d *Driver) DriverName() string {
return "exoscale"
}
func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
d.URL = flags.String("exoscale-endpoint")
d.ApiKey = flags.String("exoscale-api-key")
d.ApiSecretKey = flags.String("exoscale-api-secret-key")
d.InstanceProfile = flags.String("exoscale-instance-profile")
d.DiskSize = flags.Int("exoscale-disk-size")
d.Image = flags.String("exoscale-image")
d.SecurityGroup = flags.String("exoscale-security-group")
d.AvailabilityZone = flags.String("exoscale-availability-zone")
d.KeyPair = flags.String("exoscale-keypair")
d.SwarmMaster = flags.Bool("swarm-master")
d.SwarmHost = flags.String("swarm-host")
d.SwarmDiscovery = flags.String("swarm-discovery")
if d.URL == "" {
d.URL = "https://api.exoscale.ch/compute"
}
if d.ApiKey == "" || d.ApiSecretKey == "" {
return fmt.Errorf("Please specify an API key (--exoscale-api-key) and an API secret key (--exoscale-api-secret-key).")
}
return nil
}
func (d *Driver) GetURL() (string, error) {
ip, err := d.GetIP()
if err != nil {
return "", err
}
return fmt.Sprintf("tcp://%s:2376", ip), nil
}
func (d *Driver) GetIP() (string, error) {
if d.IPAddress == "" {
return "", fmt.Errorf("IP address is not set")
}
return d.IPAddress, nil
}
func (d *Driver) GetState() (state.State, error) {
client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey)
vm, err := client.GetVirtualMachine(d.Id)
if err != nil {
return state.Error, err
}
switch vm.State {
case "Starting":
return state.Starting, nil
case "Running":
return state.Running, nil
case "Stopping":
return state.Running, nil
case "Stopped":
return state.Stopped, nil
case "Destroyed":
return state.Stopped, nil
case "Expunging":
return state.Stopped, nil
case "Migrating":
return state.Paused, nil
case "Error":
return state.Error, nil
case "Unknown":
return state.Error, nil
case "Shutdowned":
return state.Stopped, nil
}
return state.None, nil
}
func (d *Driver) PreCreateCheck() error {
return nil
}
func (d *Driver) Create() error {
log.Infof("Querying exoscale for the requested parameters...")
client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey)
topology, err := client.GetTopology()
if err != nil {
return err
}
// Availability zone UUID
zone, ok := topology.Zones[d.AvailabilityZone]
if !ok {
return fmt.Errorf("Availability zone %v doesn't exist",
d.AvailabilityZone)
}
log.Debugf("Availability zone %v = %s", d.AvailabilityZone, zone)
// Image UUID
var tpl string
images, ok := topology.Images[strings.ToLower(d.Image)]
if ok {
tpl, ok = images[d.DiskSize]
}
if !ok {
return fmt.Errorf("Unable to find image %v with size %d",
d.Image, d.DiskSize)
}
log.Debugf("Image %v(%d) = %s", d.Image, d.DiskSize, tpl)
// Profile UUID
profile, ok := topology.Profiles[strings.ToLower(d.InstanceProfile)]
if !ok {
return fmt.Errorf("Unable to find the %s profile",
d.InstanceProfile)
}
log.Debugf("Profile %v = %s", d.InstanceProfile, profile)
// Security group
sg, ok := topology.SecurityGroups[d.SecurityGroup]
if !ok {
log.Infof("Security group %v does not exist, create it",
d.SecurityGroup)
rules := []egoscale.SecurityGroupRule{
{
SecurityGroupId: "",
Cidr: "0.0.0.0/0",
Protocol: "TCP",
Port: 22,
},
{
SecurityGroupId: "",
Cidr: "0.0.0.0/0",
Protocol: "TCP",
Port: 2376,
},
{
SecurityGroupId: "",
Cidr: "0.0.0.0/0",
Protocol: "ICMP",
IcmpType: 8,
IcmpCode: 0,
},
}
sgresp, err := client.CreateSecurityGroupWithRules(d.SecurityGroup,
rules,
make([]egoscale.SecurityGroupRule, 0, 0))
if err != nil {
return err
}
sg = sgresp.Id
}
log.Debugf("Security group %v = %s", d.SecurityGroup, sg)
if d.KeyPair == "" {
log.Infof("Generate an SSH keypair...")
kpresp, err := client.CreateKeypair(d.MachineName)
if err != nil {
return err
}
err = ioutil.WriteFile(d.GetSSHKeyPath(), []byte(kpresp.Privatekey), 0600)
if err != nil {
return err
}
d.KeyPair = d.MachineName
}
log.Infof("Spawn exoscale host...")
userdata, err := d.getCloudInit()
if err != nil {
return err
}
log.Debugf("Using the following cloud-init file:")
log.Debugf("%s", userdata)
machineProfile := egoscale.MachineProfile{
Template: tpl,
ServiceOffering: profile,
SecurityGroups: []string{sg},
Userdata: userdata,
Zone: zone,
Keypair: d.KeyPair,
Name: d.MachineName,
}
cvmresp, err := client.CreateVirtualMachine(machineProfile)
if err != nil {
return err
}
vm, err := d.waitForVM(client, cvmresp)
if err != nil {
return err
}
d.IPAddress = vm.Nic[0].Ipaddress
d.Id = vm.Id
return nil
}
func (d *Driver) Start() error {
vmstate, err := d.GetState()
if err != nil {
return err
}
if vmstate == state.Running || vmstate == state.Starting {
log.Infof("Host is already running or starting")
return nil
}
client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey)
svmresp, err := client.StartVirtualMachine(d.Id)
if err != nil {
return err
}
_, err = d.waitForVM(client, svmresp)
if err != nil {
return err
}
return nil
}
func (d *Driver) Stop() error {
vmstate, err := d.GetState()
if err != nil {
return err
}
if vmstate == state.Stopped {
log.Infof("Host is already stopped")
return nil
}
client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey)
svmresp, err := client.StopVirtualMachine(d.Id)
if err != nil {
return err
}
_, err = d.waitForVM(client, svmresp)
if err != nil {
return err
}
return nil
}
func (d *Driver) Remove() error {
client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey)
dvmresp, err := client.DestroyVirtualMachine(d.Id)
if err != nil {
return err
}
_, err = d.waitForVM(client, dvmresp)
if err != nil {
return err
}
return nil
}
func (d *Driver) Restart() error {
vmstate, err := d.GetState()
if err != nil {
return err
}
if vmstate == state.Stopped {
return fmt.Errorf("Host is stopped, use start command to start it")
}
client := egoscale.NewClient(d.URL, d.ApiKey, d.ApiSecretKey)
svmresp, err := client.RebootVirtualMachine(d.Id)
if err != nil {
return err
}
_, err = d.waitForVM(client, svmresp)
if err != nil {
return err
}
return nil
}
func (d *Driver) Kill() error {
return d.Stop()
}
func (d *Driver) waitForVM(client *egoscale.Client, jobid string) (*egoscale.DeployVirtualMachineResponse, error) {
log.Infof("Waiting for VM...")
maxRepeats := 60
i := 0
var resp *egoscale.QueryAsyncJobResultResponse
var err error
for ; i < maxRepeats; i++ {
resp, err = client.PollAsyncJob(jobid)
if err != nil {
return nil, err
}
if resp.Jobstatus == 1 {
break
}
time.Sleep(2 * time.Second)
}
if i == maxRepeats {
return nil, fmt.Errorf("Timeout while waiting for VM")
}
vm, err := client.AsyncToVirtualMachine(*resp)
if err != nil {
return nil, err
}
return vm, nil
}
// Build a cloud-init user data string that will install and run
// docker.
func (d *Driver) getCloudInit() (string, error) {
const tpl = `#cloud-config
manage_etc_hosts: true
fqdn: {{ .MachineName }}
resize_rootfs: true
`
var buffer bytes.Buffer
tmpl, err := template.New("cloud-init").Parse(tpl)
if err != nil {
return "", err
}
err = tmpl.Execute(&buffer, d)
if err != nil {
return "", err
}
return buffer.String(), nil
}