nuage/main.go

468 lines
9 KiB
Go
Raw Normal View History

2021-09-22 14:42:08 +00:00
package main
import (
2021-09-23 14:40:41 +00:00
"bufio"
2021-09-22 14:42:08 +00:00
"io"
2021-09-23 08:22:19 +00:00
"net"
2021-09-22 14:42:08 +00:00
"os"
"fmt"
"errors"
"time"
2021-09-23 07:56:03 +00:00
"golang.org/x/crypto/ssh"
2021-09-23 08:22:19 +00:00
"golang.org/x/crypto/ssh/agent"
2021-09-23 07:56:03 +00:00
"github.com/pkg/sftp"
2021-09-23 08:22:19 +00:00
2021-09-22 14:42:08 +00:00
"github.com/scaleway/scaleway-sdk-go/scw"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
)
const PARALLELIZE = 16
const instanceNotFound = Error("instance not found")
2021-09-23 14:40:41 +00:00
var msgOut chan string
var msgErr chan string
2021-09-22 14:42:08 +00:00
func main() {
if len(os.Args) < 2 {
usage()
}
2021-09-23 14:40:41 +00:00
msgOut = make(chan string, PARALLELIZE)
msgErr = make(chan string, PARALLELIZE)
go logger(os.Stdout, msgOut)
go logger(os.Stderr, msgErr)
2021-09-22 14:42:08 +00:00
var err error
switch os.Args[1] {
case "spawn":
err = spawn()
case "run":
err = run()
case "destroy":
err = destroy()
}
if err != nil {
2021-09-23 14:40:41 +00:00
msgErr <- fmt.Sprintf("cmd failed: %v\n", err)
2021-09-22 14:42:08 +00:00
os.Exit(1)
}
}
2021-09-23 14:40:41 +00:00
func logger(o *os.File, c chan string) {
for m := range c {
fmt.Fprint(o, m)
}
}
2021-09-22 14:42:08 +00:00
func usage() {
var programName = "swtool"
if len(os.Args) > 0 {
programName = os.Args[0]
}
2021-09-23 14:40:41 +00:00
msgErr <- fmt.Sprintf("Usage: %v (spawn|run|destroy)\n", programName)
2021-09-22 14:42:08 +00:00
os.Exit(1)
}
/**
* Errors
*/
type Error string
func (e Error) Error() string {
return string(e)
}
/**
* Parser logic
*/
type instanceReceiver interface {
onInstance(zone, machine, image, name string) error
}
func passInstanceTo(r io.Reader, d instanceReceiver) error {
com := make(chan error, PARALLELIZE)
2021-09-23 14:40:41 +00:00
2021-09-22 14:42:08 +00:00
count := 0
failed := 0
for {
var zone, machine, image, name string
n, err := fmt.Fscanf(r, "%s %s %s %s", &zone, &machine, &image, &name)
if err == io.EOF || n == 0 {
break
} else if err != nil {
return err
} else if n != 4 {
return errors.New(fmt.Sprintf("Wrong number of values (got %d, expected 4)\n", n))
}
go func(zone, machine, image, name string) {
err := d.onInstance(zone, machine, image, name)
if err != nil {
2021-09-23 14:40:41 +00:00
msgErr <- fmt.Sprintf("❌ Operation failed for %v (%v, %v, %v): %v\n", name, zone, machine, image, err)
2021-09-22 14:42:08 +00:00
}
com <- err
}(zone, machine, image, name)
2021-09-23 14:40:41 +00:00
2021-09-22 14:42:08 +00:00
count += 1
}
2021-09-23 14:40:41 +00:00
msgOut <- fmt.Sprintf(" Waiting for %v servers\n", count)
2021-09-22 14:42:08 +00:00
for count > 0 {
err := <- com
count -= 1
if err != nil {
failed += 1
}
}
if failed > 0 {
return errors.New(fmt.Sprintf("%d operations failed", failed))
}
return nil
}
/**
2021-09-22 21:21:05 +00:00
* instance wrapper
2021-09-22 14:42:08 +00:00
*/
2021-09-22 21:21:05 +00:00
2021-09-23 06:23:54 +00:00
type action struct {
2021-09-22 14:42:08 +00:00
api *instance.API
}
2021-09-23 06:23:54 +00:00
func (i *action) init() error {
2021-09-22 14:42:08 +00:00
client, err := getClient()
if err != nil {
2021-09-22 21:21:05 +00:00
return err
2021-09-22 14:42:08 +00:00
}
2021-09-23 06:23:54 +00:00
i.api = instance.NewAPI(client)
return nil
2021-09-22 14:42:08 +00:00
}
2021-09-23 06:23:54 +00:00
func (i *action) getInstanceByName(zone scw.Zone, name string) (*instance.Server, error) {
2021-09-22 21:21:05 +00:00
lr, err := i.api.ListServers(&instance.ListServersRequest{
2021-09-22 14:42:08 +00:00
Zone: zone,
Name: &name,
})
if err != nil {
return nil, err
}
for _, s := range lr.Servers {
if s.Name == name {
return s, nil
}
}
return nil, instanceNotFound
}
2021-09-22 21:21:05 +00:00
func parseIP(s *instance.Server) string {
ip := "(no address)"
if s.PublicIP != nil {
ip = s.PublicIP.Address.String()
if s.PublicIP.Dynamic {
ip += " (dynamic)"
}
} else if s.PrivateIP != nil {
2021-09-23 06:23:54 +00:00
ip = fmt.Sprintf("%s %s", *s.PrivateIP, "(private)")
2021-09-22 21:21:05 +00:00
}
return ip
}
/**
* Spawner
*/
2021-09-23 06:23:54 +00:00
type spawner struct { action }
2021-09-22 21:21:05 +00:00
func (sp *spawner) onInstance(zone, machine, image, name string) error {
2021-09-22 14:42:08 +00:00
z, err := scw.ParseZone(zone)
if err != nil {
return err
}
targetServer, err := sp.getInstanceByName(z, name)
if err == nil {
ip := parseIP(targetServer)
if targetServer.State == instance.ServerStateRunning {
2021-09-23 14:40:41 +00:00
msgOut <- fmt.Sprintf("🟣 Found %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip)
2021-09-22 14:42:08 +00:00
return nil
}
} else if err == instanceNotFound {
// Create a new server
createRes, err := sp.api.CreateServer(&instance.CreateServerRequest{
Zone: z,
Name: name,
CommercialType: machine,
Image: image,
2021-09-23 07:56:03 +00:00
DynamicIPRequired: scw.BoolPtr(true),
2021-09-22 14:42:08 +00:00
})
if err != nil {
return err
}
targetServer = createRes.Server
} else {
return err
}
timeout := 5 * time.Minute
err = sp.api.ServerActionAndWait(&instance.ServerActionAndWaitRequest{
ServerID: targetServer.ID,
Action: instance.ServerActionPoweron,
Zone: z,
Timeout: &timeout,
})
if err != nil {
return err
}
targetServer, err = sp.getInstanceByName(z, name)
if err != nil {
return err
}
ip := parseIP(targetServer)
2021-09-23 14:40:41 +00:00
msgOut <- fmt.Sprintf("✅ Started %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip)
2021-09-22 14:42:08 +00:00
return nil
}
2021-09-22 21:21:05 +00:00
/**
* Destroyer
*/
2021-09-23 06:23:54 +00:00
type destroyer struct { action }
2021-09-22 14:42:08 +00:00
2021-09-22 21:21:05 +00:00
func (dt *destroyer) onInstance(zone, machine, image, name string) error {
z, err := scw.ParseZone(zone)
if err != nil {
return err
2021-09-22 14:42:08 +00:00
}
2021-09-22 21:21:05 +00:00
2021-09-23 06:23:54 +00:00
targetServer, err := dt.getInstanceByName(z, name)
if err == instanceNotFound {
2021-09-23 14:40:41 +00:00
msgOut <- fmt.Sprintf("🟣 %v is already destroyed\n", name)
2021-09-23 06:23:54 +00:00
return nil
} else if err != nil {
return err
}
err = dt.api.ServerActionAndWait(&instance.ServerActionAndWaitRequest{
Zone: z,
ServerID: targetServer.ID,
Action: instance.ServerActionPoweroff,
})
2021-09-22 21:21:05 +00:00
if err != nil {
return err
}
2021-09-23 06:23:54 +00:00
err = dt.api.DeleteServer(&instance.DeleteServerRequest{
2021-09-22 21:21:05 +00:00
Zone: z,
2021-09-23 06:23:54 +00:00
ServerID: targetServer.ID,
2021-09-22 21:21:05 +00:00
})
2021-09-23 06:23:54 +00:00
if err != nil {
return err
}
2021-09-22 21:21:05 +00:00
2021-09-23 06:23:54 +00:00
ip := parseIP(targetServer)
2021-09-23 14:40:41 +00:00
msgOut <- fmt.Sprintf("✅ Destroyed %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip)
2021-09-23 06:23:54 +00:00
return nil
2021-09-22 14:42:08 +00:00
}
2021-09-23 07:56:03 +00:00
/**
* Runner
*/
type runner struct { action }
2021-09-23 08:22:19 +00:00
func (r *runner) connect(zone, name string) (*ssh.Client, error) {
2021-09-23 07:56:03 +00:00
// Connect to the remote
z, err := scw.ParseZone(zone)
if err != nil {
2021-09-23 08:22:19 +00:00
return nil, err
2021-09-23 07:56:03 +00:00
}
targetServer, err := r.getInstanceByName(z, name)
if err != nil {
2021-09-23 08:22:19 +00:00
return nil, err
2021-09-23 07:56:03 +00:00
}
if targetServer.PublicIP == nil {
2021-09-23 08:22:19 +00:00
return nil, errors.New("run failed: this instance has no public ip.")
2021-09-23 07:56:03 +00:00
}
ip := targetServer.PublicIP.Address
2021-09-23 08:22:19 +00:00
socket := os.Getenv("SSH_AUTH_SOCK")
conn, err := net.Dial("unix", socket)
2021-09-23 07:56:03 +00:00
if err != nil {
2021-09-23 08:22:19 +00:00
return nil, err
2021-09-23 07:56:03 +00:00
}
2021-09-23 08:22:19 +00:00
agentClient := agent.NewClient(conn)
2021-09-23 07:56:03 +00:00
config := &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
2021-09-23 08:22:19 +00:00
ssh.PublicKeysCallback(agentClient.Signers),
2021-09-23 07:56:03 +00:00
},
2021-09-23 08:22:19 +00:00
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
2021-09-23 07:56:03 +00:00
}
2021-09-23 08:22:19 +00:00
return ssh.Dial("tcp", ip.String()+":22", config)
}
func (r *runner) send(sshClient *ssh.Client) error {
// Source script
if len(os.Args) < 3 {
2021-09-23 14:40:41 +00:00
return errors.New("missing script to run on the command line")
2021-09-23 08:22:19 +00:00
}
src, err := os.Open(os.Args[2])
if err != nil {
2021-09-23 07:56:03 +00:00
return err
2021-09-23 08:22:19 +00:00
}
defer src.Close()
2021-09-23 07:56:03 +00:00
// Send our script to the destination
sftpClient, err := sftp.NewClient(sshClient)
if err != nil {
return err
}
defer sftpClient.Close()
dst, err := sftpClient.Create("/tmp/nuage")
if err != nil {
return err
}
defer dst.Close()
err = dst.Chmod(0755)
if err != nil {
return err
}
_, err = io.Copy(dst, src)
2021-09-23 08:22:19 +00:00
return err
}
2021-09-23 07:56:03 +00:00
2021-09-23 14:40:41 +00:00
func readerToChan(r io.Reader, c chan string) {
br := bufio.NewReader(r)
for {
s, err := br.ReadString('\n')
if err != nil {
break
}
c <- s
}
}
2021-09-23 08:22:19 +00:00
func (r *runner) exec(sshClient *ssh.Client) error {
2021-09-23 07:56:03 +00:00
// Run the script
session, err := sshClient.NewSession()
if err != nil {
return err
}
defer session.Close()
if os.Getenv("VERBOSE") != "" {
2021-09-23 14:40:41 +00:00
readErr, err := session.StderrPipe()
if err != nil {
return err
}
readerToChan(readErr, msgErr)
readOut, err := session.StdoutPipe()
if err != nil {
return err
}
readerToChan(readOut, msgOut)
2021-09-23 07:56:03 +00:00
}
2021-09-23 14:40:41 +00:00
err = session.Run("/tmp/nuage")
2021-09-23 07:56:03 +00:00
if err != nil {
return err
}
return nil
2021-09-23 08:22:19 +00:00
}
func (r *runner) onInstance(zone, machine, image, name string) error {
sshClient, err := r.connect(zone, name)
if err != nil {
return err
}
defer sshClient.Close()
err = r.send(sshClient)
if err != nil {
return err
}
err = r.exec(sshClient)
if err != nil {
return err
}
2021-09-23 14:40:41 +00:00
msgOut <- fmt.Sprintf("✅ Successfully ran the script on %v (zone %v)\n", name, zone)
2021-09-23 08:22:19 +00:00
return nil
2021-09-23 07:56:03 +00:00
}
2021-09-22 14:42:08 +00:00
/**
* Commands
*/
func spawn() error {
2021-09-22 21:21:05 +00:00
sp := spawner{}
err := sp.init()
2021-09-22 14:42:08 +00:00
if err != nil {
return err
}
err = passInstanceTo(os.Stdin, &sp)
2021-09-22 21:21:05 +00:00
return err
2021-09-22 14:42:08 +00:00
}
func run() error {
2021-09-23 07:56:03 +00:00
r := runner{}
err := r.init()
if err != nil {
return err
}
err = passInstanceTo(os.Stdin, &r)
return err
2021-09-22 14:42:08 +00:00
}
func destroy() error {
2021-09-22 21:21:05 +00:00
dt := destroyer{}
err := dt.init()
if err != nil {
return err
}
2021-09-22 14:42:08 +00:00
2021-09-22 21:21:05 +00:00
err = passInstanceTo(os.Stdin, &dt)
return err
}
2021-09-22 14:42:08 +00:00
func getClient() (*scw.Client, error) {
// Get config
// Install scw: https://github.com/scaleway/scaleway-cli
// And run `scw init` to create the config file
config, err := scw.LoadConfig()
if err != nil {
return nil, err
}
// Use active profile
profile, err := config.GetActiveProfile()
if err != nil {
return nil, err
}
// We use the default profile
client, err := scw.NewClient(
scw.WithProfile(profile),
scw.WithEnv(), // env variable may overwrite profile values
)
return client, err
}