2152b93f5b24996a40312c46353f621941873c17 — Louis Solofrizzo 1 year, 6 months ago e48f236
Slave: Add slave

Signed-off-by: Louis Solofrizzo <louis@ne02ptzero.me>
5 files changed, 535 insertions(+), 0 deletions(-)

A slave/CMakeLists.txt
A slave/config.go
A slave/instance.go
A slave/main.go
A slave/test.json
A slave/CMakeLists.txt => slave/CMakeLists.txt +13 -0
@@ 0,0 1,13 @@
+ add_go_component(lf-slave
+     main.go
+     instance.go
+     config.go
+ )
+ 
+ add_arm_go_component(lf-slave-arm
+     main.go
+     instance.go
+     config.go
+ )
+ 
+ add_dependencies(lf-slave-arm lxc sqlite3)

A slave/config.go => slave/config.go +68 -0
@@ 0,0 1,68 @@
+ package main
+ 
+ import (
+ 	"flag"
+ 	"github.com/go-xorm/xorm"
+ 	_ "github.com/mattn/go-sqlite3"
+ 	log "github.com/sirupsen/logrus"
+ 	"gopkg.in/yaml.v2"
+ 	"io/ioutil"
+ )
+ 
+ type SlaveConfig struct {
+ 	Db   string `yaml:"db" binding:"required"`
+ 	Port string `yaml:"port" binding:"required"`
+ }
+ 
+ var Database *xorm.Engine
+ var Binds *xorm.Engine
+ var Config SlaveConfig
+ 
+ func setConfig() error {
+ 	var err error
+ 	filename := flag.String("config", "/etc/lf-slave.conf", "The yaml configuration file")
+ 	flag.Parse()
+ 
+ 	source, err := ioutil.ReadFile(*filename)
+ 	if err != nil {
+ 		return err
+ 	}
+ 	err = yaml.Unmarshal(source, &Config)
+ 	if err != nil {
+ 		return err
+ 	}
+ 
+ 	Database, err = xorm.NewEngine("sqlite3", Config.Db)
+ 	if err != nil {
+ 		log.Fatal(err)
+ 		return err
+ 	}
+ 
+ 	if Database == nil {
+ 		log.Fatal("db is nil")
+ 	}
+ 
+ 	err = Database.Sync2(new(Instance))
+ 	if err != nil {
+ 		log.Fatal(err)
+ 	}
+ 
+ 	Binds, err = xorm.NewEngine("sqlite3", Config.Db)
+ 	if err != nil {
+ 		log.Fatal(err)
+ 		return err
+ 	}
+ 
+ 	if Binds == nil {
+ 		log.Fatal("db is nil")
+ 	}
+ 
+ 	err = Binds.Sync2(new(InstanceBindEntry))
+ 	if err != nil {
+ 		log.Fatal(err)
+ 	}
+ 
+ 	log.Info("Db created")
+ 
+ 	return nil
+ }

A slave/instance.go => slave/instance.go +375 -0
@@ 0,0 1,375 @@
+ package main
+ 
+ import (
+ 	"net"
+ 	"os/exec"
+ 	"strconv"
+ 
+ 	"github.com/kataras/iris"
+ 	"github.com/phayes/freeport"
+ 	log "github.com/sirupsen/logrus"
+ 
+ 	"cisco/contrib/go-lxc"
+ )
+ 
+ type InstanceAdd struct {
+ 	Distro  string `json:"distro"`
+ 	Release string `json:"release"`
+ 	Name    string `json:"name"`
+ 	Arch    string `json:"arch"`
+ }
+ 
+ type Instance struct {
+ 	Name    string    `json:"name xorm:"varchar(200)"`
+ 	Status  lxc.State `json:"status" xorm:"int"`
+ 	IPv4    []string  `json:"IPv4"`
+ 	IPv6    []string  `json:"IPv6"`
+ 	Gateway string    `json:"gateway"`
+ }
+ 
+ func instance_create(ctx iris.Context) {
+ 	var instance InstanceAdd
+ 
+ 	ctx.ReadJSON(&instance)
+ 	c, err := lxc.NewContainer(instance.Name, lxc.DefaultConfigPath())
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 		return
+ 	}
+ 
+ 	defer c.Release()
+ 	c.SetVerbosity(lxc.Verbose)
+ 
+ 	options := lxc.TemplateOptions{
+ 		Template: "download",
+ 		Distro:   instance.Distro,
+ 		Release:  instance.Release,
+ 		Arch:     instance.Arch,
+ 		Backend:  lxc.RBD,
+ 	}
+ 
+ 	log.Infof("Creating container %v", options)
+ 
+ 	if err := c.Create(options); err != nil {
+ 		log.Errorf("Cannot create container: %s", err)
+ 		ctx.StatusCode(iris.StatusAlreadyReported)
+ 	} else {
+ 		Database.Insert(&Instance{
+ 			Name:   instance.Name,
+ 			Status: c.State(),
+ 		})
+ 		ctx.StatusCode(iris.StatusCreated)
+ 	}
+ }
+ 
+ func instance_get(ctx iris.Context) {
+ 	name := ctx.Params().Get("name")
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	ip, _ := c.IPAddress("eth0")
+ 	ipv6, _ := c.IPv6Addresses()
+ 	_, gateway := get_local_ip(1)
+ 
+ 	ret := Instance{
+ 		Name:    c.Name(),
+ 		Status:  c.State(),
+ 		IPv4:    ip,
+ 		IPv6:    ipv6,
+ 		Gateway: gateway,
+ 	}
+ 
+ 	ctx.JSON(&ret)
+ }
+ 
+ func instance_delete(ctx iris.Context) {
+ 	var instance Instance
+ 	var instancebinds []InstanceBindEntry
+ 
+ 	name := ctx.Params().Get("name")
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	err = c.Stop()
+ 	if err != nil {
+ 		log.Errorf("Could not stop instance")
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	err = c.Destroy()
+ 	if err != nil {
+ 		log.Errorf("Destroy failed: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	Binds.Where("Name = ?", name).Find(&instancebinds)
+ 	for _, bind := range instancebinds {
+ 		run := exec.Command("/bin/bash", "-c", bind.IpTableDel)
+ 		run.Run()
+ 		Database.Delete(&bind)
+ 	}
+ 
+ 	Database.Where("Name = ?", name).Delete(&instance)
+ }
+ 
+ func instance_start(ctx iris.Context) {
+ 	name := ctx.Params().Get("name")
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	err = c.Start()
+ 	if err != nil {
+ 		log.Errorf("Error starting the container: %s", err)
+ 	}
+ 	Database.Cols("Status").Update(&Instance{Name: name, Status: c.State()})
+ }
+ 
+ func instance_reboot(ctx iris.Context) {
+ 	name := ctx.Params().Get("name")
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	c.Stop()
+ 	c.Start()
+ 	Database.Cols("Status").Update(&Instance{Name: name, Status: c.State()})
+ }
+ 
+ func instance_stop(ctx iris.Context) {
+ 	name := ctx.Params().Get("name")
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	c.Stop()
+ 	Database.Cols("Status").Update(&Instance{Name: name, Status: c.State()})
+ }
+ 
+ type InstanceExec struct {
+ 	Cmd []string `json:"cmd"`
+ }
+ 
+ func instance_exec(ctx iris.Context) {
+ 	var instance InstanceExec
+ 
+ 	name := ctx.Params().Get("name")
+ 	ctx.ReadJSON(&instance)
+ 
+ 	log.Printf("%v", instance)
+ 
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	c.SetVerbosity(lxc.Verbose)
+ 
+ 	for _, cmd := range instance.Cmd {
+ 		log.Printf("Executing '%s'", cmd)
+ 		if _, err := c.RunCommand([]string{
+ 			"bash", "-c",
+ 			cmd,
+ 		}, lxc.DefaultAttachOptions); err != nil {
+ 			log.Errorf("ERROR: %s\n", err.Error())
+ 			ctx.StatusCode(iris.StatusInternalServerError)
+ 		}
+ 	}
+ }
+ 
+ type InstanceBind struct {
+ 	IP   string `json:"ip"`
+ 	Port int    `json:"port"`
+ }
+ 
+ func get_local_ip(id int) (string, string) {
+ 	ifaces, err := net.Interfaces()
+ 
+ 	if err != nil {
+ 		log.Errorf("localAddresses: %+v", err)
+ 	}
+ 
+ 	for _, i := range ifaces {
+ 		addrs, _ := i.Addrs()
+ 		if i.Name == "eth0" {
+ 			return "eth0", addrs[id].(*net.IPNet).IP.String()
+ 		} else if i.Name == "enp2s0" {
+ 			return "enp2s0", addrs[id].(*net.IPNet).IP.String()
+ 		}
+ 	}
+ 
+ 	return "", ""
+ }
+ 
+ type InstanceBindEntry struct {
+ 	Name       string `xorm:"varchar(200)" json:"-"`
+ 	SourcePort int    `xorm:"int" json:"source"`
+ 	NatPort    int    `xorm:"int" json:"nat"`
+ 	Iface      string `json:"ip"`
+ 	IpTableDel string `json:"-"`
+ 	System     bool   `json:"system"`
+ }
+ 
+ func instance_bind_local_port(ctx iris.Context) {
+ 	var ret InstanceBind
+ 
+ 	name := ctx.Params().Get("name")
+ 	port, _ := ctx.Params().GetUint16("port")
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	iface, ip := get_local_ip(0)
+ 	container_ip, _ := c.IPAddress("eth0")
+ 	to := container_ip[0] + ":" + strconv.Itoa(int(port))
+ 	random_port, _ := freeport.GetFreePort()
+ 
+ 	ret.IP = ip
+ 	ret.Port = random_port
+ 
+ 	iptable_add_cmd := "iptables -t nat -I PREROUTING -p tcp -i " + iface + " --dport " + strconv.Itoa(random_port) + " -j DNAT --to " + to
+ 
+ 	run := exec.Command("/bin/bash", "-c", iptable_add_cmd)
+ 	run.Run()
+ 	Binds.Insert(&InstanceBindEntry{
+ 		Name:       name,
+ 		SourcePort: int(port),
+ 		NatPort:    random_port,
+ 		Iface:      iface,
+ 		IpTableDel: "iptables -t nat -D PREROUTING -p tcp -i " + iface + " --dport " + strconv.Itoa(random_port) + " -j DNAT --to " + to,
+ 	})
+ 	ctx.JSON(ret)
+ }
+ 
+ func instance_bind_port(ctx iris.Context) {
+ 	var ret InstanceBind
+ 
+ 	name := ctx.Params().Get("name")
+ 	port, _ := ctx.Params().GetUint16("port")
+ 
+ 	c, err := lxc.NewContainer(name, lxc.DefaultConfigPath())
+ 
+ 	if err != nil {
+ 		log.Errorf("Error on new container: %s", err)
+ 		ctx.StatusCode(iris.StatusInternalServerError)
+ 	}
+ 
+ 	defer c.Release()
+ 	if !c.Defined() {
+ 		ctx.StatusCode(iris.StatusNotFound)
+ 		return
+ 	}
+ 
+ 	container_ip, _ := c.IPAddress("eth0")
+ 	to := container_ip[0] + ":" + strconv.Itoa(int(port))
+ 	random_port, _ := freeport.GetFreePort()
+ 
+ 	iface, _ := net.InterfaceByName("tun0")
+ 	ip, _ := iface.Addrs()
+ 
+ 	ret.IP = ip[0].(*net.IPNet).IP.String()
+ 	ret.Port = random_port
+ 
+ 	iptable_add_cmd := "iptables -t nat -I PREROUTING -p tcp -i tun0 --dport " + strconv.Itoa(random_port) + " -j DNAT --to " + to
+ 
+ 	run := exec.Command("/bin/bash", "-c", iptable_add_cmd)
+ 	run.Run()
+ 
+ 	Binds.Insert(&InstanceBindEntry{
+ 		Name:       name,
+ 		SourcePort: int(port),
+ 		NatPort:    random_port,
+ 		Iface:      "tun0",
+ 		IpTableDel: "iptables -t nat -D PREROUTING -p tcp -i tun0 --dport " + strconv.Itoa(random_port) + " -j DNAT --to " + to,
+ 	})
+ 	ctx.JSON(ret)
+ }
+ 
+ func instance_get_network(ctx iris.Context) {
+ 	var ports []InstanceBindEntry
+ 
+ 	name := ctx.Params().Get("name")
+ 	Binds.Where("name = ?", name).Find(&ports)
+ 	for i, port := range ports {
+ 		if port.SourcePort == 80 || port.SourcePort == 22 {
+ 			ports[i].System = true
+ 		} else {
+ 			ports[i].System = false
+ 		}
+ 
+ 		if port.Iface == "tun0" {
+ 			iface, _ := net.InterfaceByName("tun0")
+ 			ip, _ := iface.Addrs()
+ 			ports[i].Iface = ip[0].(*net.IPNet).IP.String()
+ 		} else {
+ 			_, ports[i].Iface = get_local_ip(0)
+ 		}
+ 	}
+ 
+ 	ctx.JSON(ports)
+ }

A slave/main.go => slave/main.go +73 -0
@@ 0,0 1,73 @@
+ package main
+ 
+ import (
+ 	"github.com/kataras/iris"
+ 	log "github.com/sirupsen/logrus"
+ 
+ 	"cisco/contrib/go-lxc"
+ 	"os"
+ 	"runtime"
+ )
+ 
+ func startInstances() {
+ 	var instances []Instance
+ 
+ 	Database.Where("status = 3").Find(&instances)
+ 	for _, inst := range instances {
+ 		log.Infof("Starting instance %s", inst.Name)
+ 		c, err := lxc.NewContainer(inst.Name, lxc.DefaultConfigPath())
+ 		if err == nil {
+ 			c.Start()
+ 			c.Release()
+ 		} else {
+ 			log.Error(err)
+ 		}
+ 	}
+ }
+ 
+ type Infos struct {
+ 	Name         string `json:"name"`
+ 	Architecture string `json:"arch"`
+ }
+ 
+ func get_infos(ctx iris.Context) {
+ 	hostname, _ := os.Hostname()
+ 	info := Infos{
+ 		Name:         hostname,
+ 		Architecture: runtime.GOARCH,
+ 	}
+ 
+ 	ctx.JSON(&info)
+ }
+ 
+ func main() {
+ 	app := iris.New()
+ 	app.Use(func(ctx iris.Context) {
+ 		log.WithFields(log.Fields{
+ 			"ip":     ctx.RemoteAddr(),
+ 			"path":   ctx.Path(),
+ 			"method": ctx.Method(),
+ 		}).Info("api request")
+ 		ctx.Next()
+ 	})
+ 
+ 	app.Get("/api/info", get_infos)
+ 	app.Post("/api/instance", instance_create)
+ 	app.Get("/api/instance/{name:string}", instance_get)
+ 	app.Delete("/api/instance/{name:string}", instance_delete)
+ 	app.Put("/api/instance/{name:string}/start", instance_start)
+ 	app.Put("/api/instance/{name:string}/stop", instance_stop)
+ 	app.Put("/api/instance/{name:string}/reboot", instance_reboot)
+ 	app.Post("/api/instance/{name:string}/exec", instance_exec)
+ 	app.Put("/api/instance/{name:string}/bind_local/{port:uint16}", instance_bind_local_port)
+ 	app.Put("/api/instance/{name:string}/bind/{port:uint16}", instance_bind_port)
+ 	app.Get("/api/instance/{name:string}/network", instance_get_network)
+ 
+ 	setConfig()
+ 
+ 	startInstances()
+ 
+ 	app.Run(iris.Addr(":"+Config.Port), iris.WithConfiguration(iris.Configuration{
+ 		FireMethodNotAllowed: true,
+ 	}))
+ }

A slave/test.json => slave/test.json +6 -0
@@ 0,0 1,6 @@
+ {
+     "distro": "ubuntu",
+     "release": "bionic",
+     "name": "louis",
+     "arch": "amd64"
+ }