f-50: lab: arguments QEMU et seeds cloud-init des VM #50
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
parent
a706e24b1c
commit
99ecb59aad
9 changed files with 1132 additions and 10 deletions
117
cmd/lab/main.go
117
cmd/lab/main.go
|
|
@ -1,45 +1,73 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.g3e.fr/syonad/two/internal/lab/render"
|
||||
"git.g3e.fr/syonad/two/internal/lab/topology"
|
||||
)
|
||||
|
||||
const usage = `usage: lab <command> <topology.yml>
|
||||
const usage = `usage: lab <command> [options] <topology.yml> [dir]
|
||||
|
||||
plan validate the topology and print the deterministic plan: addresses, cables, ports
|
||||
plan <topology.yml>
|
||||
validate the topology and print the deterministic plan: addresses, cables, ports
|
||||
|
||||
render -key <public key file> <topology.yml> <dir>
|
||||
write, for each node, <dir>/<node>/qemu.args (one argument per line) and the
|
||||
cloud-init seed files meta-data, user-data and network-config
|
||||
`
|
||||
|
||||
type keyFiles []string
|
||||
|
||||
func (k *keyFiles) String() string { return strings.Join(*k, ",") }
|
||||
func (k *keyFiles) Set(v string) error { *k = append(*k, v); return nil }
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
func run(args []string, stdout, stderr io.Writer) int {
|
||||
if len(args) != 2 {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprint(stderr, usage)
|
||||
return 2
|
||||
}
|
||||
switch args[0] {
|
||||
case "plan":
|
||||
if len(args) != 2 {
|
||||
fmt.Fprint(stderr, usage)
|
||||
return 2
|
||||
}
|
||||
return plan(args[1], stdout, stderr)
|
||||
case "render":
|
||||
return renderCmd(args[1:], stdout, stderr)
|
||||
default:
|
||||
fmt.Fprint(stderr, usage)
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
func plan(path string, stdout, stderr io.Writer) int {
|
||||
func load(path string, stderr io.Writer) (*topology.Plan, bool) {
|
||||
t, err := topology.Load(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
return nil, false
|
||||
}
|
||||
p, err := topology.Compute(t)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %s:\n%v\n", path, err)
|
||||
return nil, false
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
func plan(path string, stdout, stderr io.Writer) int {
|
||||
p, ok := load(path, stderr)
|
||||
if !ok {
|
||||
return 1
|
||||
}
|
||||
if err := p.Write(stdout); err != nil {
|
||||
|
|
@ -48,3 +76,82 @@ func plan(path string, stdout, stderr io.Writer) int {
|
|||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func renderCmd(args []string, stdout, stderr io.Writer) int {
|
||||
fs := flag.NewFlagSet("render", flag.ContinueOnError)
|
||||
fs.SetOutput(stderr)
|
||||
fs.Usage = func() { fmt.Fprint(stderr, usage) }
|
||||
var keys keyFiles
|
||||
fs.Var(&keys, "key", "public key file allowed to log in, repeatable")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if fs.NArg() != 2 || len(keys) == 0 {
|
||||
fmt.Fprint(stderr, usage)
|
||||
return 2
|
||||
}
|
||||
|
||||
p, ok := load(fs.Arg(0), stderr)
|
||||
if !ok {
|
||||
return 1
|
||||
}
|
||||
dir, err := filepath.Abs(fs.Arg(1))
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
authorized, err := readKeys(keys)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
nodes, err := render.Render(p, render.Options{RunDir: dir, AuthorizedKeys: authorized})
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
for _, n := range nodes {
|
||||
if err := writeNode(n); err != nil {
|
||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
fmt.Fprintf(stdout, "%s\n", n.Dir)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func readKeys(files []string) ([]string, error) {
|
||||
var keys []string
|
||||
for _, f := range files {
|
||||
data, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, line)
|
||||
}
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func writeNode(n render.Node) error {
|
||||
if err := os.MkdirAll(n.Dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
files := map[string][]byte{
|
||||
"qemu.args": []byte(strings.Join(n.QEMU, "\n") + "\n"),
|
||||
"meta-data": n.MetaData,
|
||||
"user-data": n.UserData,
|
||||
"network-config": n.NetworkConfig,
|
||||
}
|
||||
for name, content := range files {
|
||||
if err := os.WriteFile(filepath.Join(n.Dir, name), content, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ func runLab(args ...string) (int, string, string) {
|
|||
}
|
||||
|
||||
func TestRun_UsageOnMissingArguments(t *testing.T) {
|
||||
for _, args := range [][]string{nil, {"plan"}, {"deploy", "x.yml"}, {"plan", "a.yml", "b.yml"}} {
|
||||
for _, args := range [][]string{nil, {"plan"}, {"deploy", "x.yml"}, {"plan", "a.yml", "b.yml"}, {"render", "a.yml", "dir"}, {"render", "-key", "k.pub", "a.yml"}, {"render", "-bogus"}} {
|
||||
code, _, stderr := runLab(args...)
|
||||
if code != 2 || !strings.Contains(stderr, "usage: lab") {
|
||||
t.Errorf("args %v: code %d, stderr %q", args, code, stderr)
|
||||
|
|
@ -70,3 +70,52 @@ func TestRun_MissingFile(t *testing.T) {
|
|||
t.Errorf("code %d, stderr %q", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_RenderWritesEveryNodeFiles(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
key := filepath.Join(dir, "lab.pub")
|
||||
if err := os.WriteFile(key, []byte("# lab key\n\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFG/JMmjfko96WkJV8DiL6rip/H/q/R++y8s27Z+Cj6O two-lab-automation\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := filepath.Join(dir, "run")
|
||||
code, stdout, stderr := runLab("render", "-key", key, filepath.Join("..", "..", "conf", "lab", "evpn-2hv.yml"), out)
|
||||
if code != 0 {
|
||||
t.Fatalf("code %d, stderr %s", code, stderr)
|
||||
}
|
||||
for _, node := range []string{"sw1", "rr1", "hv1", "hv2"} {
|
||||
if !strings.Contains(stdout, filepath.Join(out, node)) {
|
||||
t.Errorf("stdout does not list %s:\n%s", node, stdout)
|
||||
}
|
||||
for _, f := range []string{"qemu.args", "meta-data", "user-data", "network-config"} {
|
||||
info, err := os.Stat(filepath.Join(out, node, f))
|
||||
if err != nil {
|
||||
t.Errorf("%s/%s: %v", node, f, err)
|
||||
continue
|
||||
}
|
||||
if info.Mode().Perm() != 0o600 {
|
||||
t.Errorf("%s/%s mode %o, want 600", node, f, info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
}
|
||||
args, err := os.ReadFile(filepath.Join(out, "hv1", "qemu.args"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(args), "\n-netdev\nuser,id=mgmt0,restrict=on,ipv6=off,hostfwd=tcp:127.0.0.1:2202-:22\n") {
|
||||
t.Errorf("qemu.args is not one argument per line:\n%s", args)
|
||||
}
|
||||
userData, err := os.ReadFile(filepath.Join(out, "hv1", "user-data"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(userData), "two-lab-automation") || strings.Contains(string(userData), "# lab key") {
|
||||
t.Errorf("keys not read as an authorized_keys file:\n%s", userData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_RenderRefusesMissingKeyFile(t *testing.T) {
|
||||
code, _, stderr := runLab("render", "-key", filepath.Join(t.TempDir(), "absent.pub"), filepath.Join("..", "..", "conf", "lab", "evpn-2hv.yml"), t.TempDir())
|
||||
if code != 1 || !strings.Contains(stderr, "absent.pub") {
|
||||
t.Errorf("code %d, stderr %q", code, stderr)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue