Compare commits

17 Commits

Author SHA1 Message Date
ff1f3432cd Update for new godo library 2017-07-03 20:00:09 -05:00
432b47d224 Make activity timeout configuraable 2017-01-28 21:30:18 -06:00
6c1321e485 Remove scripts 2017-01-28 15:19:45 -06:00
32408e4471 Rename test_config.json to test_cofig.yaml in gitignore 2017-01-28 15:02:05 -06:00
8879463cb9 Switch config file to yaml 2017-01-28 02:37:07 -06:00
2b06810bd7 Fix issue with keep alive caused by nil channel 2017-01-28 00:04:04 -06:00
5dd596d755 Move scripts to scripts directory 2017-01-27 19:58:41 -06:00
0a2bc38196 Add defer logFile.Close() 2017-01-27 19:52:58 -06:00
42e65ebf52 Add ability to set log file in command line options 2017-01-27 19:48:03 -06:00
10a959e9f3 Switch key list over to using key names 2017-01-27 19:28:18 -06:00
d57884e2d7 Add checking to prevent failed deletion of nonexistent droplets and
unnecessary archiving of droplets in images
2017-01-27 18:06:27 -06:00
2f105fa6b5 Add keepalive message to port forwarder 2017-01-27 15:28:57 -06:00
913f70a2af Add standalone, pure Go port forwarding 2017-01-27 14:50:22 -06:00
12ec5e4f93 Remove varstatedir option 2017-01-26 20:06:43 -06:00
31044fe0c3 Remove GatewayInterface setting 2017-01-26 20:04:33 -06:00
6f3f8d5474 Merge branch 'master' of github.com:gtalent/dospin 2017-01-25 18:00:52 -06:00
ccc14c2606 Run liccor for 2017 2017-01-25 17:59:25 -06:00
17 changed files with 239 additions and 240 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
dospin.json dospin.json
test_config.json test_config.yaml
dospin dospin
main main

View File

@@ -1,4 +1,4 @@
Copyright 2016 gtalent2@gmail.com Copyright 2016-2017 gtalent2@gmail.com
This Source Code Form is subject to the terms of the Mozilla Public 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 License, v. 2.0. If a copy of the MPL was not distributed with this

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2016 gtalent2@gmail.com Copyright 2016-2017 gtalent2@gmail.com
This Source Code Form is subject to the terms of the Mozilla Public 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 License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -10,6 +10,7 @@ package main
import ( import (
"flag" "flag"
"log" "log"
"os"
) )
const ( const (
@@ -18,16 +19,16 @@ const (
) )
type cmdOptions struct { type cmdOptions struct {
config string config string
cmd string logFile string
varStateDir string cmd string
} }
func parseCmdOptions() cmdOptions { func parseCmdOptions() cmdOptions {
var o cmdOptions var o cmdOptions
flag.StringVar(&o.cmd, "cmd", CMD_SERVE, "Mode to run command in ("+CMD_SERVE+","+CMD_SPINDOWNALL+")") flag.StringVar(&o.cmd, "cmd", CMD_SERVE, "Mode to run command in ("+CMD_SERVE+","+CMD_SPINDOWNALL+")")
flag.StringVar(&o.config, "config", "/etc/dospin.json", "Path to the dospin config file") flag.StringVar(&o.config, "config", "dospin.yaml", "Path to the dospin config file")
flag.StringVar(&o.varStateDir, "varstate", "/var/lib/dospin", "Path to the var state directory") flag.StringVar(&o.logFile, "logFile", "stdout", "Path to the dospin log file")
flag.Parse() flag.Parse()
return o return o
} }
@@ -46,7 +47,17 @@ func spindownAll(opts cmdOptions) {
} }
} }
func runServer(opts cmdOptions) { func runServer(opts cmdOptions) int {
if opts.logFile != "stdout" {
logFile, err := os.OpenFile(opts.logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
if err == nil {
defer logFile.Close()
log.SetOutput(logFile)
} else {
log.Print("Could not open log file: ", err)
}
}
log.Println("Loading config:", opts.config) log.Println("Loading config:", opts.config)
settings, err := loadSettings(opts.config) settings, err := loadSettings(opts.config)
if err != nil { if err != nil {
@@ -67,8 +78,8 @@ func runServer(opts cmdOptions) {
} }
} }
done := make(chan interface{}) done := make(chan int)
<-done return <-done
} }
func main() { func main() {

View File

@@ -1,17 +0,0 @@
{
"ApiToken": "<your token here>",
"GatewayInterface": "eth0:2",
"Servers": {
"minecraft": {
"Ports": [25565],
"UsePublicIP": false,
"InitialSize": "4gb",
"Size": "4gb",
"Region": "nyc1",
"SshKeys": [513314, 1667247],
"ImageSlug": "ubuntu-16-04-x64",
"Volumes": ["volume-nyc1-01"],
"UserData": "#!/bin/bash\napt-get update\napt-get install -y docker.io\nmkdir -p /mnt/volume-nyc1-01\nmount -o discard,defaults /dev/disk/by-id/scsi-0DO_Volume_volume-nyc1-01 /mnt/volume-nyc1-01\necho /dev/disk/by-id/scsi-0DO_Volume_volume-nyc1-01 /mnt/volume-nyc1-01 ext4 defaults,nofail,discard 0 0 | tee -a /etc/fstab\ndocker run -d --restart=always -p 25565:25565 -v /mnt/volume-nyc1-01/minecraft-server:/minecraft-server -w /minecraft-server -t java:8-alpine sh start.sh"
}
}
}

26
dospin.yaml Normal file
View File

@@ -0,0 +1,26 @@
---
api_token: <your token here>
servers:
minecraft:
ports:
- 25565
activity_timeout_min: 20m
use_public_ip: false
initial_size: 4gb
size: 4gb
region: nyc1
ssh_keys:
- Key1
- gtalent2@gmail.com
use_persistent_image: false
image_slug: ubuntu-16-04-x64
volumes:
- volume-nyc1-01
user_data: |-
#!/bin/bash
apt-get update
apt-get install -y docker.io
mkdir -p /mnt/volume-nyc1-01
mount -o discard,defaults /dev/disk/by-id/scsi-0DO_Volume_volume-nyc1-01 /mnt/volume-nyc1-01
echo /dev/disk/by-id/scsi-0DO_Volume_volume-nyc1-01 /mnt/volume-nyc1-01 ext4 defaults,nofail,discard 0 0 | tee -a /etc/fstab
docker run -d --restart=always -p 25565:25565 -v /mnt/volume-nyc1-01/minecraft-server:/minecraft-server -w /minecraft-server -t java:8-alpine sh start.sh

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2016 gtalent2@gmail.com Copyright 2016-2017 gtalent2@gmail.com
This Source Code Form is subject to the terms of the Mozilla Public 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 License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -8,6 +8,7 @@
package main package main
import ( import (
"context"
"errors" "errors"
"github.com/digitalocean/godo" "github.com/digitalocean/godo"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@@ -28,30 +29,16 @@ func (t *tokenSource) Token() (*oauth2.Token, error) {
return token, nil return token, nil
} }
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
}
type DropletHandler struct { type DropletHandler struct {
client *godo.Client client *godo.Client
ctx context.Context
settings Settings settings Settings
} }
func NewDropletHandler(settings Settings) *DropletHandler { func NewDropletHandler(settings Settings) *DropletHandler {
retval := new(DropletHandler) retval := new(DropletHandler)
retval.settings = settings retval.settings = settings
retval.ctx = context.TODO()
// setup DO client // setup DO client
tokenSource := &tokenSource{settings.ApiToken} tokenSource := &tokenSource{settings.ApiToken}
@@ -103,14 +90,14 @@ func (me *DropletHandler) Spinup(name string) (string, error) {
Region: vd.Region, Region: vd.Region,
Size: size, Size: size,
PrivateNetworking: true, PrivateNetworking: true,
SSHKeys: sshKeys(vd.SshKeys), SSHKeys: me.sshKeys(vd.SshKeys),
Volumes: volumes(vd.Volumes), Volumes: me.volumes(vd.Volumes),
UserData: vd.UserData, UserData: vd.UserData,
Image: image, Image: image,
} }
log.Println("Spinup: Creating " + name) log.Println("Spinup: Creating " + name)
droplet, _, err := me.client.Droplets.Create(createRequest) droplet, _, err := me.client.Droplets.Create(me.ctx, createRequest)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
if droplet == nil { if droplet == nil {
@@ -119,7 +106,7 @@ func (me *DropletHandler) Spinup(name string) (string, error) {
} }
// wait until machine is ready // wait until machine is ready
for { for {
d, _, err := me.client.Droplets.Get(droplet.ID) d, _, err := me.client.Droplets.Get(me.ctx, droplet.ID)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return "", err return "", err
@@ -137,7 +124,7 @@ func (me *DropletHandler) Spinup(name string) (string, error) {
// resize // resize
log.Println("Spinup: Resizing " + name) log.Println("Spinup: Resizing " + name)
action, _, err := me.client.DropletActions.Resize(droplet.ID, vd.Size, false) action, _, err := me.client.DropletActions.Resize(me.ctx, droplet.ID, vd.Size, false)
if err != nil || !me.actionWait(action.ID) { if err != nil || !me.actionWait(action.ID) {
return "", err return "", err
} }
@@ -145,7 +132,7 @@ func (me *DropletHandler) Spinup(name string) (string, error) {
// power back on // power back on
log.Println("Spinup: Powering on " + name) log.Println("Spinup: Powering on " + name)
action, _, err = me.client.DropletActions.PowerOn(droplet.ID) action, _, err = me.client.DropletActions.PowerOn(me.ctx, droplet.ID)
if err != nil || !me.actionWait(action.ID) { if err != nil || !me.actionWait(action.ID) {
return "", err return "", err
} }
@@ -153,9 +140,9 @@ func (me *DropletHandler) Spinup(name string) (string, error) {
} }
// delete the image // delete the image
log.Println("Spinup: Deleting image " + name) if image.ID > 0 {
if image.ID > -1 { log.Println("Spinup: Deleting image " + name)
_, err = me.client.Images.Delete(image.ID) _, err = me.client.Images.Delete(me.ctx, image.ID)
if err != nil { if err != nil {
log.Println("Spinup: Could not delete image: ", err) log.Println("Spinup: Could not delete image: ", err)
} else { } else {
@@ -166,7 +153,7 @@ func (me *DropletHandler) Spinup(name string) (string, error) {
// get the private IP and return it // get the private IP and return it
// get new copy of droplet that has IP // get new copy of droplet that has IP
droplet, _, err = me.client.Droplets.Get(droplet.ID) droplet, _, err = me.client.Droplets.Get(me.ctx, droplet.ID)
if err == nil { if err == nil {
if vd.UsePublicIP { if vd.UsePublicIP {
return droplet.PublicIPv4() return droplet.PublicIPv4()
@@ -182,7 +169,8 @@ func (me *DropletHandler) Spinup(name string) (string, error) {
func (me *DropletHandler) Spindown(name string) error { func (me *DropletHandler) Spindown(name string) error {
droplet, err := me.getDroplet(name) droplet, err := me.getDroplet(name)
if err != nil { if err != nil {
return err // droplet not existing is not an error
return nil
} }
// power off // power off
@@ -192,16 +180,18 @@ func (me *DropletHandler) Spindown(name string) error {
} }
// snapshot existing droplet // snapshot existing droplet
log.Println("Spindown: Creating image " + name) if me.settings.Servers[name].UsePersistentImage {
action, _, err := me.client.DropletActions.Snapshot(droplet.ID, DROPLET_NS+name) log.Println("Spindown: Creating image " + name)
if err != nil || !me.actionWait(action.ID) { action, _, err := me.client.DropletActions.Snapshot(me.ctx, droplet.ID, DROPLET_NS+name)
return err if err != nil || !me.actionWait(action.ID) {
return err
}
log.Println("Spindown: Created image " + name)
} }
log.Println("Spindown: Created image " + name)
// delete droplet // delete droplet
log.Println("Spindown: Deleting droplet " + name) log.Println("Spindown: Deleting droplet " + name)
_, err = me.client.Droplets.Delete(droplet.ID) _, err = me.client.Droplets.Delete(me.ctx, droplet.ID)
if err != nil { if err != nil {
return err return err
} }
@@ -229,7 +219,7 @@ func (me *DropletHandler) poweroff(name string) error {
break break
} }
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
_, _, err = me.client.DropletActions.Shutdown(droplet.ID) _, _, err = me.client.DropletActions.Shutdown(me.ctx, droplet.ID)
if err != nil { if err != nil {
log.Println("Power down of", name, "failed:", err) log.Println("Power down of", name, "failed:", err)
} }
@@ -251,7 +241,7 @@ func (me *DropletHandler) getDroplet(name string) (godo.Droplet, error) {
Page: page, Page: page,
PerPage: perPage, PerPage: perPage,
} }
images, _, err := me.client.Droplets.List(opt) images, _, err := me.client.Droplets.List(me.ctx, opt)
if err != nil { if err != nil {
break break
} }
@@ -283,7 +273,7 @@ func (me *DropletHandler) getSnapshot(name string) (godo.Image, error) {
Page: page, Page: page,
PerPage: perPage, PerPage: perPage,
} }
images, _, err := me.client.Images.ListUser(opt) images, _, err := me.client.Images.ListUser(me.ctx, opt)
if err != nil { if err != nil {
break break
} }
@@ -306,7 +296,7 @@ func (me *DropletHandler) getSnapshot(name string) (godo.Image, error) {
func (me *DropletHandler) actionWait(actionId int) bool { func (me *DropletHandler) actionWait(actionId int) bool {
for { for {
a, _, err := me.client.Actions.Get(actionId) a, _, err := me.client.Actions.Get(me.ctx, actionId)
if err != nil { if err != nil {
log.Println("Action retrieval failed: ", err) log.Println("Action retrieval failed: ", err)
} else if a.Status == "completed" { } else if a.Status == "completed" {
@@ -318,3 +308,46 @@ func (me *DropletHandler) actionWait(actionId int) bool {
time.Sleep(1000 * time.Millisecond) time.Sleep(1000 * time.Millisecond)
} }
} }
func (me *DropletHandler) sshKeys(keyNames []string) []godo.DropletCreateSSHKey {
// build key map
page := 0
perPage := 200
keyMap := make(map[string]string)
for {
page++
opt := &godo.ListOptions{
Page: page,
PerPage: perPage,
}
keys, _, err := me.client.Keys.List(me.ctx, opt)
if err != nil {
break
}
for _, v := range keys {
keyMap[v.Name] = v.Fingerprint
}
// check next page?
if len(keys) < perPage {
break
}
}
// build output key list
var out []godo.DropletCreateSSHKey
for _, kn := range keyNames {
fp := keyMap[kn]
out = append(out, godo.DropletCreateSSHKey{Fingerprint: fp})
}
return out
}
func (me *DropletHandler) volumes(names []string) []godo.DropletCreateVolume {
var out []godo.DropletCreateVolume
for _, name := range names {
out = append(out, godo.DropletCreateVolume{Name: name})
}
return out
}

View File

@@ -1,2 +0,0 @@
#! /usr/bin/env sh
curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer `cat dospin.json | jq -r .ApiToken`" "https://api.digitalocean.com/v2/droplets?page=1&per_page=100&private=true" | jq .

View File

@@ -1,2 +0,0 @@
#! /usr/bin/env sh
curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer `cat dospin.json | jq -r .ApiToken`" "https://api.digitalocean.com/v2/images?page=1&per_page=100&private=true" | jq .

View File

@@ -1,4 +0,0 @@
#! /usr/bin/env sh
echo Keys
curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer `cat dospin.json | jq -r .ApiToken`" "https://api.digitalocean.com/v2/account/keys" | jq .

View File

@@ -1,4 +0,0 @@
#! /usr/bin/env sh
echo Volumes
curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer `cat dospin.json | jq -r .ApiToken`" "https://api.digitalocean.com/v2/volumes" | jq .

63
net.go Normal file
View File

@@ -0,0 +1,63 @@
/*
Copyright 2016-2017 gtalent2@gmail.com
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/.
*/
package main
import (
"io"
"log"
"net"
"time"
)
const (
CONN_ACTIVE = iota
CONN_DISCONNECTED
)
type ConnStatus struct {
Status int
Err error
}
func portForward(wanConn *net.TCPConn, lanIp, port string, connStatus chan ConnStatus) {
done := make(chan error)
log.Print("Proxy: Connecting to ", lanIp+":"+port)
lanConn, err := net.Dial("tcp", lanIp+":"+port)
if err != nil {
log.Print("Proxy: LAN dial error: ", err)
return
}
go forwardConn(wanConn, lanConn, done)
go forwardConn(lanConn, wanConn, done)
ticker := time.NewTicker(1 * time.Minute)
for i := 0; i < 2; {
select {
case err = <-done:
if err != nil {
log.Print("Proxy: ", err)
}
i++
case <-ticker.C:
connStatus <- ConnStatus{Status: CONN_ACTIVE}
}
}
log.Print("Proxy: ending connection: ", wanConn.LocalAddr().String())
ticker.Stop()
wanConn.Close()
lanConn.Close()
connStatus <- ConnStatus{Status: CONN_DISCONNECTED, Err: err}
}
func forwardConn(writer, reader net.Conn, done chan error) {
_, err := io.Copy(writer, reader)
done <- err
}

View File

@@ -1,51 +0,0 @@
/*
Copyright 2016 gtalent2@gmail.com
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/.
*/
package main
import (
"bytes"
"log"
"os/exec"
"strconv"
)
func addPortForward(ruleName, ip, port string) {
pfrule := "\"rdr pass on $dospin_ext_if proto { tcp, udp } from any to any port {" + port + "} -> " + ip + "\""
in, err := exec.Command("pfctl", "-a", "\"dospin_"+ruleName+"\"", "-f", "-").StdinPipe()
defer in.Close()
if err != nil {
log.Println("Port Forwarding:", err)
}
_, err = in.Write([]byte(pfrule))
if err != nil {
log.Println("Port Forwarding:", err)
}
}
func rmPortForward(ruleName string) {
_, err := exec.Command("pfctl", "-a", "\"dospin_"+ruleName+"\"", "-F", "rules").Output()
if err != nil {
log.Println("Port Forwarding:", err)
}
}
func portUsageCount(ports ...int) int {
cmd := "sockstat"
args := []string{"-4c"}
for _, v := range ports {
args = append(args, "-p")
args = append(args, strconv.Itoa(v))
}
out, err := exec.Command(cmd, args...).Output()
if err != nil {
log.Println("Port Usage Check: Could not run \""+cmd+"\":", err)
}
return bytes.Count(out, []byte{'\n'}) - 1
}

View File

@@ -1,32 +0,0 @@
/*
Copyright 2016 gtalent2@gmail.com
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/.
*/
package main
import (
"log"
"os/exec"
)
func addPortForward(ruleName, gatewayInt, localIp, targetIp, port string) {
log.Println("Setting up port", port, "to", targetIp)
cmdOut, err := exec.Command("iptables", "-A", "PREROUTING", "-t", "nat", "-i", "\""+gatewayInt+"\"", "-p", "tcp", "--dport", port, "-j", "DNAT", "--to", targetIp+":"+port).Output()
log.Println("iptables", "-A", "PREROUTING,", "-t", "nat", "-i", "\""+gatewayInt+"\"", "-p", "tcp", "--dport", port, "-j", "DNAT", "--to", targetIp+":"+port)
if err != nil {
log.Println("iptables error:", err)
}
cmdOut, err = exec.Command("iptables", "-A", "FORWARD", "-p", "tcp", "-d", targetIp, "--dport", port, "-j", "ACCEPT").Output()
log.Println("iptables", "-A", "FORWARD", "-p", "tcp", "-d", targetIp, "--dport", port, "-j", "ACCEPT")
if err != nil {
log.Println("iptables error:", err)
}
}
func rmPortForward(ruleName string) {
log.Print("Port forwarding not currently implemented for Linux/iptables")
}

View File

@@ -1,27 +0,0 @@
/*
Copyright 2016 gtalent2@gmail.com
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/.
*/
package main
import (
"net"
"strconv"
"testing"
)
func TestPortCount(t *testing.T) {
port := 49214
// listen on some port that nothing should be using for the sake of the test
go func() {
addr, _ := net.ResolveTCPAddr("tcp", "0.0.0.0:"+strconv.Itoa(port))
net.ListenTCP("tcp", addr)
}()
if portUsageCount(49214) != 1 {
t.Errorf("Port count usage reporting wrong number")
}
}

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2016 gtalent2@gmail.com Copyright 2016-2017 gtalent2@gmail.com
This Source Code Form is subject to the terms of the Mozilla Public 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 License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -11,6 +11,7 @@ import (
"log" "log"
"net" "net"
"strconv" "strconv"
"time"
) )
const ( const (
@@ -31,13 +32,14 @@ type ServerHandler interface {
} }
type ServerManager struct { type ServerManager struct {
name string name string
ports []int ports []int
gatewayInt string in chan serverManagerEvent
in chan serverManagerEvent done chan int
done chan interface{} connStatus chan ConnStatus
usageScore int // spin down server when this reaches 0 lastKeepAliveTime time.Time
server ServerHandler server ServerHandler
activityTimeout time.Duration
} }
func NewServerManager(name string, server ServerHandler, settings Settings) *ServerManager { func NewServerManager(name string, server ServerHandler, settings Settings) *ServerManager {
@@ -45,11 +47,18 @@ func NewServerManager(name string, server ServerHandler, settings Settings) *Ser
sm.name = name sm.name = name
sm.ports = settings.Servers[name].Ports sm.ports = settings.Servers[name].Ports
sm.gatewayInt = settings.GatewayInterface
sm.in = make(chan serverManagerEvent) sm.in = make(chan serverManagerEvent)
sm.done = make(chan interface{}) sm.done = make(chan int)
sm.usageScore = 5 sm.connStatus = make(chan ConnStatus)
sm.server = server sm.server = server
sm.lastKeepAliveTime = time.Now()
activityTimeout, err := time.ParseDuration(settings.Servers[sm.name].ActivityTimeout)
if err != nil { // invalid timeout, default to 5 minutes
activityTimeout = time.Duration(5 * time.Minute)
}
sm.activityTimeout = activityTimeout
log.Println("ServerManager: ", name, " has activity timeout of ", sm.activityTimeout.String())
return sm return sm
} }
@@ -60,16 +69,28 @@ func NewServerManager(name string, server ServerHandler, settings Settings) *Ser
func (me *ServerManager) Serve() { func (me *ServerManager) Serve() {
// TODO: see if server is currently up, and setup port forwarding if so // TODO: see if server is currently up, and setup port forwarding if so
ticker := time.NewTicker(1 * time.Minute)
// event loop // event loop
for running := true; running; { for running := true; running; {
select { select {
case status := <-me.connStatus:
if status.Status == CONN_ACTIVE {
me.lastKeepAliveTime = time.Now()
}
case action := <-me.in: case action := <-me.in:
running = me.serveAction(action) running = me.serveAction(action)
case <-ticker.C:
if time.Since(me.lastKeepAliveTime) > me.activityTimeout {
running = me.serveAction(serverManagerEvent{eventType: SERVERMANAGER_SPINDOWN})
}
} }
} }
ticker.Stop()
// notify done // notify done
me.done <- 42 me.done <- 0
} }
/* /*
@@ -82,7 +103,7 @@ func (me *ServerManager) Spinup(c *net.TCPConn) {
/* /*
Sends the serve loop a spindown message. Sends the serve loop a spindown message.
*/ */
func (me *ServerManager) Spindown(c *net.TCPConn) { func (me *ServerManager) Spindown() {
me.in <- serverManagerEvent{eventType: SERVERMANAGER_SPINDOWN} me.in <- serverManagerEvent{eventType: SERVERMANAGER_SPINDOWN}
} }
@@ -97,18 +118,6 @@ func (me *ServerManager) Done() {
<-me.done <-me.done
} }
func (me *ServerManager) addPortForwards(localIp, remoteIp string) {
log.Println("Ports:", me.ports)
for _, p := range me.ports {
port := strconv.Itoa(p)
addPortForward(me.name, me.gatewayInt, localIp, remoteIp, port)
}
}
func (me *ServerManager) rmPortForwards() {
rmPortForward(me.name)
}
func (me *ServerManager) setupListener(port int) { func (me *ServerManager) setupListener(port int) {
portStr := strconv.Itoa(port) portStr := strconv.Itoa(port)
addr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:"+portStr) addr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:"+portStr)
@@ -127,14 +136,9 @@ func (me *ServerManager) setupListener(port int) {
conn, err := l.AcceptTCP() conn, err := l.AcceptTCP()
if err != nil { if err != nil {
log.Print("Could not accept TCP connection:", err) log.Print("Could not accept TCP connection:", err)
} else { } else { // connection accepted
// connection accepted
// spinup machine // spinup machine
me.Spinup(conn) me.Spinup(conn)
// close existing connection, not doing anything with it
conn.Close()
} }
} }
} }
@@ -146,18 +150,18 @@ func (me *ServerManager) serveAction(event serverManagerEvent) bool {
switch event.eventType { switch event.eventType {
case SERVERMANAGER_SPINUP: case SERVERMANAGER_SPINUP:
targetIp, err := me.server.Spinup(me.name) targetIp, err := me.server.Spinup(me.name)
me.lastKeepAliveTime = time.Now()
if err == nil { if err == nil {
log.Println("ServerManager: Got IP for", me.name+":", targetIp) log.Println("ServerManager: Got IP for", me.name+":", targetIp)
localIp := event.tcpConn.LocalAddr().String() wanAddr := event.tcpConn.LocalAddr().String()
me.addPortForwards(localIp, targetIp) _, port, _ := net.SplitHostPort(wanAddr)
go portForward(event.tcpConn, targetIp, port, me.connStatus)
} else { } else {
log.Println("ServerManager: Could not spin up "+me.name+":", err) log.Println("ServerManager: Could not spin up "+me.name+":", err)
} }
case SERVERMANAGER_SPINDOWN: case SERVERMANAGER_SPINDOWN:
err := me.server.Spindown(me.name) err := me.server.Spindown(me.name)
if err == nil { if err != nil {
me.rmPortForwards()
} else {
log.Println("ServerManager: Could not spin down "+me.name+":", err) log.Println("ServerManager: Could not spin down "+me.name+":", err)
} }
case SERVERMANAGER_STOP: case SERVERMANAGER_STOP:

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2016 gtalent2@gmail.com Copyright 2016-2017 gtalent2@gmail.com
This Source Code Form is subject to the terms of the Mozilla Public 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 License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -8,26 +8,27 @@
package main package main
import ( import (
"encoding/json" "gopkg.in/yaml.v2"
"io/ioutil" "io/ioutil"
) )
type Settings struct { type Settings struct {
ApiToken string ApiToken string `yaml:"api_token"`
GatewayInterface string Servers map[string]Server `yaml:"servers"`
Servers map[string]Server
} }
type Server struct { type Server struct {
Ports []int Ports []int `yaml:"ports"`
UsePublicIP bool ActivityTimeout string `yaml:"activity_timeout"`
InitialSize string UsePublicIP bool `yaml:"use_public_ip"`
Size string InitialSize string `yaml:"initial_size"`
Region string Size string `yaml:"size"`
ImageSlug string Region string `yaml:"region"`
UserData string UsePersistentImage bool `yaml:"use_persistent_image"`
SshKeys []int ImageSlug string `yaml:"image_slug"`
Volumes []string UserData string `yaml:"user_data"`
SshKeys []string `yaml:"ssh_keys"`
Volumes []string `yaml:"volumes"`
} }
func loadSettings(path string) (Settings, error) { func loadSettings(path string) (Settings, error) {
@@ -37,7 +38,7 @@ func loadSettings(path string) (Settings, error) {
return s, err return s, err
} }
err = json.Unmarshal(data, &s) err = yaml.Unmarshal(data, &s)
if err != nil { if err != nil {
return s, err return s, err
} }

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2016 gtalent2@gmail.com Copyright 2016-2017 gtalent2@gmail.com
This Source Code Form is subject to the terms of the Mozilla Public 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 License, v. 2.0. If a copy of the MPL was not distributed with this