From b26fee385f149bb7cdc87d570228fb38cb27f25c Mon Sep 17 00:00:00 2001
From: Simon Ruderich <simon@ruderich.org>
Date: Mon, 4 Nov 2024 09:03:55 +0100
Subject: [PATCH] Add "down" command to remove an existing lab

---
 main.go  |  7 +++++++
 setup.go | 62 +++++++++++++++++++++++++++++++++++++++++++++-----------
 2 files changed, 57 insertions(+), 12 deletions(-)

diff --git a/main.go b/main.go
index 9ca00ad..700afd9 100644
--- a/main.go
+++ b/main.go
@@ -39,6 +39,13 @@ func main() {
 		cfg := loadConfig()
 		mustUp(cfg)
 
+	case "down":
+		if len(os.Args) != 3 {
+			log.Fatalf("invalid arguments; expected: \"down\" <config>")
+		}
+		cfg := loadConfig()
+		mustDown(cfg)
+
 	case "dot":
 		// Write dot file showing the network architecture
 		if len(os.Args) != 4 {
diff --git a/setup.go b/setup.go
index 7d17520..80b3276 100644
--- a/setup.go
+++ b/setup.go
@@ -26,15 +26,7 @@ func mustUp(cfg *Config) {
 		ns := node.Name
 		if netnsExists(ns) {
 			// Terminate processes in old namespaces
-			for _, x := range netnsPids(ns) {
-				log.Printf("  Killing old PID %d", x)
-				err := syscall.Kill(x, syscall.SIGTERM)
-				if err != nil {
-					log.Fatalf("failed to kill %d: %v", x, err)
-				}
-				// Also try SIGHUP to terminate shells which ignore SIGTERM
-				_ = syscall.Kill(x, syscall.SIGHUP)
-			}
+			netnsKillPids(ns)
 			// Prevent any conflicts with existing data
 			ip("netns", "del", ns)
 			// Don't remove anything in /etc/netns/ as the user might store
@@ -43,10 +35,9 @@ func mustUp(cfg *Config) {
 		ip("netns", "add", ns)
 
 		// Write /etc/netns/$netns/hosts with all known hosts
-		nsCfgPath := filepath.Join("/etc/netns", ns)
-		nsHostsPath := filepath.Join(nsCfgPath, "hosts")
+		nsHostsPath := netnsHostsPath(ns)
 		log.Printf("  Writing %q", nsHostsPath)
-		err := os.MkdirAll(nsCfgPath, 0755)
+		err := os.MkdirAll(netnsCfgPath(ns), 0755)
 		if err != nil {
 			log.Fatal(err)
 		}
@@ -117,6 +108,33 @@ func mustUp(cfg *Config) {
 	}
 }
 
+func mustDown(cfg *Config) {
+	for _, node := range cfg.Nodes {
+		log.Printf("Removing node %q ...", node.Name)
+
+		ns := node.Name
+		if !netnsExists(ns) {
+			log.Printf("  netns doesn't exist, skipping cleanup")
+			continue
+		}
+
+		netnsKillPids(ns)
+		ip("netns", "del", ns)
+
+		// Delete files we created in /etc/netns/. Keep the rest as the user
+		// might store configuration there!
+		nsHostsPath := netnsHostsPath(ns)
+		log.Printf("  Removing %q (including parent directory if empty)",
+			nsHostsPath)
+		err := os.Remove(nsHostsPath)
+		if err != nil && !os.IsNotExist(err) {
+			log.Fatal(err)
+		}
+		// Ignore error as /etc/netns/<ns>/ might not be empty
+		_ = os.Remove(netnsCfgPath(ns))
+	}
+}
+
 func ip(args ...string) {
 	xargs := append([]string{"ip"}, args...)
 	log.Printf("  Running %q", xargs)
@@ -167,6 +185,26 @@ func netnsPids(netns string) []int {
 	return res
 }
 
+func netnsKillPids(netns string) {
+	for _, x := range netnsPids(netns) {
+		log.Printf("  Killing old PID %d", x)
+		err := syscall.Kill(x, syscall.SIGTERM)
+		if err != nil {
+			log.Fatalf("failed to kill %d: %v", x, err)
+		}
+		// Also try SIGHUP to terminate shells which ignore SIGTERM
+		_ = syscall.Kill(x, syscall.SIGHUP)
+	}
+}
+
+func netnsCfgPath(netns string) string {
+	return filepath.Join("/etc/netns", netns)
+}
+
+func netnsHostsPath(netns string) string {
+	return filepath.Join(netnsCfgPath(netns), "hosts")
+}
+
 func ifaceExists(netns, name string) bool {
 	args := []string{"-n", netns, "-json", "link"}
 	xargs := append([]string{"ip"}, args...)
-- 
2.49.0