2016-02-26 17:35:57 -06:00
|
|
|
/*
|
2017-01-25 17:59:25 -06:00
|
|
|
Copyright 2016-2017 gtalent2@gmail.com
|
2016-02-26 17:35:57 -06:00
|
|
|
|
|
|
|
This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
*/
|
2016-02-15 00:18:56 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"github.com/digitalocean/godo"
|
2016-02-26 18:40:47 -06:00
|
|
|
"golang.org/x/oauth2"
|
2016-02-26 17:32:39 -06:00
|
|
|
"log"
|
2016-02-26 00:33:53 -06:00
|
|
|
"time"
|
2016-02-15 00:18:56 -06:00
|
|
|
)
|
|
|
|
|
2016-02-26 00:33:53 -06:00
|
|
|
const DROPLET_NS = "dospin-"
|
|
|
|
|
2016-02-26 18:40:47 -06:00
|
|
|
type tokenSource struct {
|
|
|
|
AccessToken string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *tokenSource) Token() (*oauth2.Token, error) {
|
|
|
|
token := &oauth2.Token{
|
|
|
|
AccessToken: t.AccessToken,
|
|
|
|
}
|
|
|
|
return token, nil
|
|
|
|
}
|
|
|
|
|
2017-01-21 16:02:41 -06:00
|
|
|
func sshKeys(ids []int) []godo.DropletCreateSSHKey {
|
|
|
|
var out []godo.DropletCreateSSHKey
|
|
|
|
for _, id := range ids {
|
|
|
|
out = append(out, godo.DropletCreateSSHKey{ID: id})
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func volumes(names []string) []godo.DropletCreateVolume {
|
|
|
|
var out []godo.DropletCreateVolume
|
|
|
|
for _, name := range names {
|
|
|
|
out = append(out, godo.DropletCreateVolume{Name: name})
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
2016-02-27 14:39:35 -06:00
|
|
|
type DropletHandler struct {
|
2016-02-15 00:18:56 -06:00
|
|
|
client *godo.Client
|
|
|
|
settings Settings
|
|
|
|
}
|
|
|
|
|
2016-02-27 14:39:35 -06:00
|
|
|
func NewDropletHandler(settings Settings) *DropletHandler {
|
|
|
|
retval := new(DropletHandler)
|
2016-02-15 00:18:56 -06:00
|
|
|
retval.settings = settings
|
2016-02-26 18:40:47 -06:00
|
|
|
|
|
|
|
// setup DO client
|
|
|
|
tokenSource := &tokenSource{settings.ApiToken}
|
|
|
|
oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource)
|
|
|
|
retval.client = godo.NewClient(oauthClient)
|
|
|
|
|
2016-02-15 00:18:56 -06:00
|
|
|
return retval
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Gets the Droplet if it already exists, instantiates it if it does not.
|
|
|
|
*/
|
2016-02-27 16:10:44 -06:00
|
|
|
func (me *DropletHandler) Spinup(name string) (string, error) {
|
2016-02-29 00:39:33 -06:00
|
|
|
vd := me.settings.Servers[name]
|
2016-02-15 00:18:56 -06:00
|
|
|
if droplet, err := me.getDroplet(name); err == nil {
|
2016-02-29 00:39:33 -06:00
|
|
|
if vd.UsePublicIP {
|
|
|
|
return droplet.PublicIPv4()
|
|
|
|
} else {
|
|
|
|
return droplet.PrivateIPv4()
|
|
|
|
}
|
2016-02-15 00:18:56 -06:00
|
|
|
} else {
|
2016-02-26 17:32:39 -06:00
|
|
|
// create the droplet
|
2017-01-21 16:02:41 -06:00
|
|
|
var image godo.DropletCreateImage
|
|
|
|
|
|
|
|
if vd.ImageSlug == "" {
|
|
|
|
snapshot, err := me.getSnapshot(name)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
image = godo.DropletCreateImage{
|
|
|
|
ID: snapshot.ID,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
image = godo.DropletCreateImage{
|
|
|
|
Slug: vd.ImageSlug,
|
|
|
|
}
|
2016-02-15 00:18:56 -06:00
|
|
|
}
|
2016-02-28 02:45:41 -06:00
|
|
|
|
|
|
|
// determine droplet size
|
|
|
|
var size string
|
|
|
|
if vd.InitialSize != "" {
|
|
|
|
size = vd.InitialSize
|
|
|
|
} else {
|
|
|
|
size = vd.Size
|
|
|
|
}
|
|
|
|
|
2016-02-15 00:18:56 -06:00
|
|
|
createRequest := &godo.DropletCreateRequest{
|
2016-02-26 00:33:53 -06:00
|
|
|
Name: DROPLET_NS + name,
|
|
|
|
Region: vd.Region,
|
2016-02-28 02:45:41 -06:00
|
|
|
Size: size,
|
2016-02-26 00:33:53 -06:00
|
|
|
PrivateNetworking: true,
|
2017-01-21 16:02:41 -06:00
|
|
|
SSHKeys: sshKeys(vd.SshKeys),
|
|
|
|
Volumes: volumes(vd.Volumes),
|
|
|
|
UserData: vd.UserData,
|
|
|
|
Image: image,
|
2016-02-15 00:18:56 -06:00
|
|
|
}
|
|
|
|
|
2016-02-26 17:32:39 -06:00
|
|
|
log.Println("Spinup: Creating " + name)
|
|
|
|
droplet, _, err := me.client.Droplets.Create(createRequest)
|
2016-02-26 18:40:47 -06:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
if droplet == nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
}
|
2016-02-26 17:32:39 -06:00
|
|
|
// wait until machine is ready
|
|
|
|
for {
|
|
|
|
d, _, err := me.client.Droplets.Get(droplet.ID)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
2016-02-26 18:40:47 -06:00
|
|
|
return "", err
|
2016-02-26 17:32:39 -06:00
|
|
|
} else if d.Status == "active" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
}
|
2016-02-26 18:40:47 -06:00
|
|
|
log.Println("Spinup: Created " + name)
|
|
|
|
|
2016-02-28 02:45:41 -06:00
|
|
|
// resize if necessary
|
|
|
|
if vd.InitialSize != "" && vd.InitialSize != vd.Size {
|
|
|
|
// power off
|
|
|
|
me.poweroff(name)
|
|
|
|
|
|
|
|
// resize
|
|
|
|
log.Println("Spinup: Resizing " + name)
|
|
|
|
action, _, err := me.client.DropletActions.Resize(droplet.ID, vd.Size, false)
|
|
|
|
if err != nil || !me.actionWait(action.ID) {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
log.Println("Spinup: Resized " + name)
|
|
|
|
|
|
|
|
// power back on
|
|
|
|
log.Println("Spinup: Powering on " + name)
|
|
|
|
action, _, err = me.client.DropletActions.PowerOn(droplet.ID)
|
|
|
|
if err != nil || !me.actionWait(action.ID) {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
log.Println("Spinup: Powered on " + name)
|
|
|
|
}
|
|
|
|
|
2016-02-26 18:40:47 -06:00
|
|
|
// delete the image
|
|
|
|
log.Println("Spinup: Deleting image " + name)
|
2017-01-21 16:07:46 -06:00
|
|
|
if image.ID > -1 {
|
|
|
|
_, err = me.client.Images.Delete(image.ID)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Spinup: Could not delete image: ", err)
|
|
|
|
} else {
|
|
|
|
log.Println("Spinup: Deleted image " + name)
|
|
|
|
}
|
2016-02-15 00:18:56 -06:00
|
|
|
}
|
|
|
|
|
2016-02-26 00:33:53 -06:00
|
|
|
// get the private IP and return it
|
2017-01-21 16:02:41 -06:00
|
|
|
|
|
|
|
// get new copy of droplet that has IP
|
|
|
|
droplet, _, err = me.client.Droplets.Get(droplet.ID)
|
|
|
|
if err == nil {
|
|
|
|
if vd.UsePublicIP {
|
|
|
|
return droplet.PublicIPv4()
|
|
|
|
} else {
|
|
|
|
return droplet.PrivateIPv4()
|
|
|
|
}
|
2016-02-29 00:39:33 -06:00
|
|
|
} else {
|
2017-01-21 16:02:41 -06:00
|
|
|
return "", err
|
2016-02-26 17:32:39 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-27 16:10:44 -06:00
|
|
|
func (me *DropletHandler) Spindown(name string) error {
|
2016-02-26 17:32:39 -06:00
|
|
|
droplet, err := me.getDroplet(name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// power off
|
2016-02-28 02:45:41 -06:00
|
|
|
err = me.poweroff(name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2016-02-26 17:32:39 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// snapshot existing droplet
|
2016-02-26 18:40:47 -06:00
|
|
|
log.Println("Spindown: Creating image " + name)
|
2016-02-26 17:32:39 -06:00
|
|
|
action, _, err := me.client.DropletActions.Snapshot(droplet.ID, DROPLET_NS+name)
|
|
|
|
if err != nil || !me.actionWait(action.ID) {
|
|
|
|
return err
|
2016-02-15 00:18:56 -06:00
|
|
|
}
|
2016-03-05 14:19:22 -06:00
|
|
|
log.Println("Spindown: Created image " + name)
|
2016-02-26 17:32:39 -06:00
|
|
|
|
|
|
|
// delete droplet
|
2016-02-26 18:40:47 -06:00
|
|
|
log.Println("Spindown: Deleting droplet " + name)
|
2016-02-26 17:32:39 -06:00
|
|
|
_, err = me.client.Droplets.Delete(droplet.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-02-26 18:40:47 -06:00
|
|
|
log.Println("Spindown: Deleted droplet " + name)
|
2016-02-26 17:32:39 -06:00
|
|
|
|
|
|
|
return err
|
2016-02-15 00:18:56 -06:00
|
|
|
}
|
|
|
|
|
2016-02-28 02:45:41 -06:00
|
|
|
func (me *DropletHandler) poweroff(name string) error {
|
|
|
|
droplet, err := me.getDroplet(name)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if droplet.Status != "off" {
|
|
|
|
log.Println("Powering down " + name)
|
|
|
|
// wait until machine is off
|
|
|
|
for {
|
|
|
|
droplet, err = me.getDroplet(name)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Power down of", name, "failed:", err)
|
|
|
|
if droplet.ID < 1 {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
} else if droplet.Status == "off" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
_, _, err = me.client.DropletActions.Shutdown(droplet.ID)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Power down of", name, "failed:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log.Println("Powered down", name)
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-02-27 14:39:35 -06:00
|
|
|
func (me *DropletHandler) getDroplet(name string) (godo.Droplet, error) {
|
2016-02-26 00:33:53 -06:00
|
|
|
name = DROPLET_NS + name
|
2016-02-15 00:18:56 -06:00
|
|
|
page := 0
|
|
|
|
perPage := 200
|
|
|
|
var droplet godo.Droplet
|
|
|
|
for {
|
|
|
|
page++
|
|
|
|
// get list of droplets
|
|
|
|
opt := &godo.ListOptions{
|
|
|
|
Page: page,
|
|
|
|
PerPage: perPage,
|
|
|
|
}
|
|
|
|
images, _, err := me.client.Droplets.List(opt)
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// find droplet
|
|
|
|
for _, a := range images {
|
|
|
|
if a.Name == name {
|
|
|
|
return a, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// check next page?
|
|
|
|
if len(images) < perPage {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2016-02-26 00:33:53 -06:00
|
|
|
return droplet, errors.New("Could not find droplet: " + name)
|
2016-02-15 00:18:56 -06:00
|
|
|
}
|
|
|
|
|
2016-02-27 14:39:35 -06:00
|
|
|
func (me *DropletHandler) getSnapshot(name string) (godo.Image, error) {
|
2016-02-26 00:33:53 -06:00
|
|
|
name = DROPLET_NS + name
|
2016-02-15 00:18:56 -06:00
|
|
|
page := 0
|
|
|
|
perPage := 200
|
|
|
|
var image godo.Image
|
2016-02-26 00:33:53 -06:00
|
|
|
var err error
|
2016-02-15 00:18:56 -06:00
|
|
|
for {
|
|
|
|
page++
|
2016-02-26 00:33:53 -06:00
|
|
|
|
2016-02-15 00:18:56 -06:00
|
|
|
// get list of images
|
|
|
|
opt := &godo.ListOptions{
|
|
|
|
Page: page,
|
|
|
|
PerPage: perPage,
|
|
|
|
}
|
|
|
|
images, _, err := me.client.Images.ListUser(opt)
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
2016-02-26 00:33:53 -06:00
|
|
|
|
2016-02-15 00:18:56 -06:00
|
|
|
// find image
|
|
|
|
for _, a := range images {
|
|
|
|
if a.Name == name {
|
|
|
|
return a, nil
|
|
|
|
}
|
|
|
|
}
|
2016-02-26 00:33:53 -06:00
|
|
|
|
2016-02-15 00:18:56 -06:00
|
|
|
// check next page?
|
|
|
|
if len(images) < perPage {
|
2016-02-26 00:33:53 -06:00
|
|
|
err = errors.New("Could not find image: " + name)
|
2016-02-15 00:18:56 -06:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2016-02-26 00:33:53 -06:00
|
|
|
return image, err
|
2016-02-15 00:18:56 -06:00
|
|
|
}
|
2016-02-26 17:32:39 -06:00
|
|
|
|
2016-02-27 14:39:35 -06:00
|
|
|
func (me *DropletHandler) actionWait(actionId int) bool {
|
2016-02-26 17:32:39 -06:00
|
|
|
for {
|
|
|
|
a, _, err := me.client.Actions.Get(actionId)
|
|
|
|
if err != nil {
|
2016-02-28 02:45:41 -06:00
|
|
|
log.Println("Action retrieval failed: ", err)
|
2016-02-26 17:32:39 -06:00
|
|
|
} else if a.Status == "completed" {
|
|
|
|
return true
|
|
|
|
} else if a.Status == "errored" {
|
|
|
|
log.Println("Action failed: ", a.Type, " on ", a.ResourceID)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
time.Sleep(1000 * time.Millisecond)
|
|
|
|
}
|
|
|
|
}
|