2021-09-22 14:42:08 +00:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2021-09-23 07:56:03 +00:00
|
|
|
|
"bytes"
|
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")
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
if len(os.Args) < 2 {
|
|
|
|
|
usage()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
switch os.Args[1] {
|
|
|
|
|
case "spawn":
|
|
|
|
|
err = spawn()
|
|
|
|
|
case "run":
|
|
|
|
|
err = run()
|
|
|
|
|
case "destroy":
|
|
|
|
|
err = destroy()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "cmd failed: %v\n", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func usage() {
|
|
|
|
|
var programName = "swtool"
|
|
|
|
|
if len(os.Args) > 0 {
|
|
|
|
|
programName = os.Args[0]
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %v (spawn|run|destroy)\n", programName)
|
|
|
|
|
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)
|
|
|
|
|
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 {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "❌ Operation failed for %v (%v, %v, %v): %v\n", name, zone, machine, image, err)
|
|
|
|
|
}
|
|
|
|
|
com <- err
|
|
|
|
|
}(zone, machine, image, name)
|
|
|
|
|
count += 1
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-23 06:23:54 +00:00
|
|
|
|
fmt.Fprintf(os.Stdout, "ℹ️ 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 {
|
|
|
|
|
fmt.Fprintf(os.Stdout, "🟣 Found %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip)
|
|
|
|
|
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)
|
|
|
|
|
fmt.Fprintf(os.Stdout, "✅ Started %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip)
|
|
|
|
|
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 {
|
|
|
|
|
fmt.Fprintf(os.Stdout, "🟣 %v is already destroyed\n", name)
|
|
|
|
|
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)
|
|
|
|
|
fmt.Fprintf(os.Stdout, "✅ Destroyed %v on zone %v with ip %v\n", targetServer.Name, targetServer.Zone, ip)
|
|
|
|
|
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 {
|
|
|
|
|
return errors.New("Missing script to run on the command line")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 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()
|
|
|
|
|
|
|
|
|
|
var sshStdout, sshStderr bytes.Buffer
|
|
|
|
|
session.Stdout = &sshStdout
|
|
|
|
|
session.Stderr = &sshStderr
|
|
|
|
|
|
|
|
|
|
err = session.Run("/tmp/nuage")
|
|
|
|
|
if os.Getenv("VERBOSE") != "" {
|
2021-09-23 08:22:19 +00:00
|
|
|
|
fmt.Fprintf(os.Stdout, "logs\n")
|
2021-09-23 07:56:03 +00:00
|
|
|
|
io.Copy(os.Stdout, &sshStdout)
|
|
|
|
|
io.Copy(os.Stderr, &sshStderr)
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Fprintf(os.Stdout, "✅ Successfully ran the script on %v (zone %v)\n", name, zone)
|
|
|
|
|
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
|
|
|
|
|
}
|