f-50: lab: lancement, état, arrêt et accès SSH des VM #50
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
d1cd943d86
commit
1146d13d45
10 changed files with 1309 additions and 4 deletions
269
internal/lab/machine/machine.go
Normal file
269
internal/lab/machine/machine.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
package machine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/lab/provision"
|
||||
"git.g3e.fr/syonad/two/internal/lab/render"
|
||||
"git.g3e.fr/syonad/two/internal/lab/topology"
|
||||
)
|
||||
|
||||
const (
|
||||
User = "debian"
|
||||
|
||||
sshUnreachable = 255
|
||||
cloudInitRecoverable = 2
|
||||
)
|
||||
|
||||
type Lab struct {
|
||||
Plan *topology.Plan
|
||||
RunDir string
|
||||
ProcDir string
|
||||
Runner provision.Runner
|
||||
Poll time.Duration
|
||||
Stop time.Duration
|
||||
Out io.Writer
|
||||
|
||||
signal func(pid int, sig syscall.Signal) error
|
||||
}
|
||||
|
||||
func (l Lab) node(name string) (topology.NodePlan, error) {
|
||||
for _, n := range l.Plan.Nodes {
|
||||
if n.Name == name {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return topology.NodePlan{}, fmt.Errorf("node %q is not in lab %s", name, l.Plan.Name)
|
||||
}
|
||||
|
||||
func (l Lab) pidFile(name string) string {
|
||||
return filepath.Join(l.RunDir, name, render.PIDFile)
|
||||
}
|
||||
|
||||
func (l Lab) PID(name string) (int, error) {
|
||||
data, err := os.ReadFile(l.pidFile(name))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||
if err != nil || pid <= 0 {
|
||||
return 0, fmt.Errorf("%s: not a pid: %q", l.pidFile(name), data)
|
||||
}
|
||||
if !l.runs(pid, name) {
|
||||
return 0, nil
|
||||
}
|
||||
return pid, nil
|
||||
}
|
||||
|
||||
func (l Lab) runs(pid int, name string) bool {
|
||||
if err := syscall.Kill(pid, 0); err != nil && !errors.Is(err, syscall.EPERM) {
|
||||
return false
|
||||
}
|
||||
cmdline, err := os.ReadFile(filepath.Join(l.ProcDir, strconv.Itoa(pid), "cmdline"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return bytes.Contains(cmdline, []byte("\x00-name\x00"+name+"\x00"))
|
||||
}
|
||||
|
||||
func (l Lab) Running() ([]string, error) {
|
||||
var running []string
|
||||
for _, n := range l.Plan.Nodes {
|
||||
pid, err := l.PID(n.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pid != 0 {
|
||||
running = append(running, n.Name)
|
||||
}
|
||||
}
|
||||
return running, nil
|
||||
}
|
||||
|
||||
func (l Lab) Up(ctx context.Context, fetcher provision.Fetcher, timeout time.Duration) error {
|
||||
running, err := l.Running()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(running) > 0 {
|
||||
return fmt.Errorf("lab %s is already running (%s): 'lab down' first", l.Plan.Name, strings.Join(running, ", "))
|
||||
}
|
||||
|
||||
nodes, err := provision.Prepare(ctx, l.Plan, provision.Options{RunDir: l.RunDir, Fetcher: fetcher, Runner: l.Runner})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, n := range switchesFirst(l.Plan, nodes) {
|
||||
if err := os.Remove(l.pidFile(n.Name)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if err := l.Runner.Run(ctx, render.QEMUBinary, append(n.QEMU, "-daemonize")...); err != nil {
|
||||
return fmt.Errorf("node %s: %w", n.Name, err)
|
||||
}
|
||||
fmt.Fprintf(l.Out, "%s: started\n", n.Name)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
var errs []error
|
||||
for _, n := range l.Plan.Nodes {
|
||||
if err := l.waitReady(ctx, n); err != nil {
|
||||
errs = append(errs, fmt.Errorf("node %s: %w", n.Name, err))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(l.Out, "%s: ready\n", n.Name)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func switchesFirst(p *topology.Plan, nodes []render.Node) []render.Node {
|
||||
var first, rest []render.Node
|
||||
for i, n := range nodes {
|
||||
if p.Nodes[i].Role == topology.RoleSwitch {
|
||||
first = append(first, n)
|
||||
} else {
|
||||
rest = append(rest, n)
|
||||
}
|
||||
}
|
||||
return append(first, rest...)
|
||||
}
|
||||
|
||||
func (l Lab) waitReady(ctx context.Context, n topology.NodePlan) error {
|
||||
args := l.sshArgs(n, true, []string{"cloud-init", "status", "--wait"})
|
||||
for {
|
||||
err := l.Runner.Run(ctx, "ssh", args...)
|
||||
switch code := exitCode(err); {
|
||||
case err == nil:
|
||||
return nil
|
||||
case code == cloudInitRecoverable:
|
||||
fmt.Fprintf(l.Out, "%s: cloud-init finished with recoverable errors: %v\n", n.Name, err)
|
||||
return nil
|
||||
case code != sshUnreachable:
|
||||
return fmt.Errorf("cloud-init failed: %w", err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("not reachable over ssh: %w", ctx.Err())
|
||||
case <-time.After(l.Poll):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func exitCode(err error) int {
|
||||
var coded interface{ ExitCode() int }
|
||||
if errors.As(err, &coded) {
|
||||
return coded.ExitCode()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (l Lab) sshArgs(n topology.NodePlan, batch bool, command []string) []string {
|
||||
args := []string{
|
||||
"-i", filepath.Join(l.RunDir, provision.KeyFile),
|
||||
"-o", "IdentitiesOnly=yes",
|
||||
"-o", "IdentityAgent=none",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
"-o", "LogLevel=ERROR",
|
||||
"-p", strconv.Itoa(n.SSHPort),
|
||||
}
|
||||
if batch {
|
||||
args = append(args, "-o", "BatchMode=yes", "-o", "ConnectTimeout=5")
|
||||
}
|
||||
args = append(args, User+"@127.0.0.1")
|
||||
return append(args, command...)
|
||||
}
|
||||
|
||||
func (l Lab) SSH(name string, terminal bool, command []string) ([]string, error) {
|
||||
n, err := l.node(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args := []string{"ssh"}
|
||||
if terminal && len(command) > 0 {
|
||||
args = append(args, "-t")
|
||||
}
|
||||
return append(args, l.sshArgs(n, false, command)...), nil
|
||||
}
|
||||
|
||||
func (l Lab) Status(w io.Writer) error {
|
||||
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(tw, "node\trole\tstate\tpid\tssh\n")
|
||||
for _, n := range l.Plan.Nodes {
|
||||
pid, err := l.PID(n.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, shown := "stopped", "-"
|
||||
if pid != 0 {
|
||||
state, shown = "running", strconv.Itoa(pid)
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t127.0.0.1:%d\n", n.Name, n.Role, state, shown, n.SSHPort)
|
||||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
func (l Lab) Down(ctx context.Context) error {
|
||||
var errs []error
|
||||
for _, n := range l.Plan.Nodes {
|
||||
pid, err := l.PID(n.Name)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
if pid == 0 {
|
||||
continue
|
||||
}
|
||||
if err := l.stop(ctx, n.Name, pid); err != nil {
|
||||
errs = append(errs, fmt.Errorf("node %s: %w", n.Name, err))
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(l.pidFile(n.Name)); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(l.Out, "%s: stopped\n", n.Name)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
func (l Lab) stop(ctx context.Context, name string, pid int) error {
|
||||
if pid <= 0 {
|
||||
return fmt.Errorf("refusing to signal pid %d", pid)
|
||||
}
|
||||
send := l.signal
|
||||
if send == nil {
|
||||
send = syscall.Kill
|
||||
}
|
||||
for _, sig := range []syscall.Signal{syscall.SIGTERM, syscall.SIGKILL} {
|
||||
if err := send(pid, sig); err != nil && !errors.Is(err, syscall.ESRCH) {
|
||||
return err
|
||||
}
|
||||
deadline := time.Now().Add(l.Stop)
|
||||
for l.runs(pid, name) && time.Now().Before(deadline) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(l.Poll):
|
||||
}
|
||||
}
|
||||
if !l.runs(pid, name) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("pid %d still running after SIGKILL", pid)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue