diff --git a/.forgejo/workflows/prerelease.yml b/.forgejo/workflows/prerelease.yml index fcb74c6..f9b8a43 100644 --- a/.forgejo/workflows/prerelease.yml +++ b/.forgejo/workflows/prerelease.yml @@ -32,7 +32,13 @@ jobs: matrix: goos: [linux] goarch: [amd64] - binaries: [db, metadata] + binaries: + - db + - metadata + - metacli + - agent + - vpc + - dhcp uses: ./.forgejo/workflows/build.yml with: tag: ${{ needs.set-release-target.outputs.release_cible }} @@ -40,10 +46,28 @@ jobs: goarch: ${{ matrix.goarch }} binari: ${{ matrix.binaries }} secrets: inherit + upload-scripts: + runs-on: docker + needs: [set-release-target] + strategy: + matrix: + script: + - run-dnsmasq-in-netns.sh + steps: + - uses: actions/checkout@v3 + - name: Move asset + run: | + mkdir -p "dist" + cp scripts/${{ matrix.script }} dist/ + - name: Upload script + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.script }}-${{ needs.set-release-target.outputs.release_cible }} + path: dist/${{ matrix.script }} prerelease: runs-on: docker needs: [set-release-target, build] uses: ./.forgejo/workflows/release.yml with: tag: ${{ needs.set-release-target.outputs.release_cible }} - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/cmd/agent/.keep b/cmd/agent/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/agent/main.go b/cmd/agent/main.go new file mode 100644 index 0000000..0f82e93 --- /dev/null +++ b/cmd/agent/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "os" +) + +var ( + bin_name = os.Args[0] +) + +func main() { + + fmt.Printf("%s: Start process\n", bin_name) + + os.Exit(0) +} diff --git a/cmd/db/main.go b/cmd/db/main.go index 0c6c1ba..1166f44 100644 --- a/cmd/db/main.go +++ b/cmd/db/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "os" "strings" @@ -51,17 +52,13 @@ func AddInDB(dbName string, line string) error { id := strings.Split(line, ";")[0] + "/bash" key := []byte(dbName + "/" + id) - return DB.Update(func(txn *badger.Txn) error { - return txn.Set(key, []byte(line)) - }) + return kv.AddInDB(DB, string(key), line) } func DeleteInDB(dbName, id string) error { key := []byte(dbName + "/" + id + "/bash") - return DB.Update(func(txn *badger.Txn) error { - return txn.Delete(key) - }) + return kv.DeleteInDB(DB, string(key)) } func CountInDB(dbName, id string) int { @@ -125,7 +122,13 @@ func printDB() { } func main() { - conf, err := configuration.LoadConfig("/etc/two/agent.yml") + conf_file := flag.String("conf", "/etc/two/agent.yml", "configuration file") + + flag.Parse() + + args := flag.Args() + + conf, err := configuration.LoadConfig(*conf_file) if err != nil { fmt.Println(err) return @@ -133,56 +136,56 @@ func main() { DB = kv.InitDB(kv.Config{ Path: conf.Database.Path, - }) + }, false) defer DB.Close() - if len(os.Args) < 2 { + if len(args) < 1 { fmt.Println("Usage: db [args...]") return } - cmd := os.Args[1] + cmd := args[0] switch cmd { case "check_in_db": - if len(os.Args) != 4 { + if len(args) != 3 { fmt.Println("Usage: check_in_db ") os.Exit(1) } - ret := CheckInDB(os.Args[2], os.Args[3]) + ret := CheckInDB(args[1], args[2]) os.Exit(ret) case "add_in_db": - if len(os.Args) < 4 { + if len(args) < 3 { fmt.Println("Usage: add_in_db ") os.Exit(1) } - line := strings.Join(os.Args[3:], ";") - if err := AddInDB(os.Args[2], line); err != nil { + line := strings.Join(args[2:], ";") + if err := AddInDB(args[1], line); err != nil { fmt.Println("Error:", err) os.Exit(1) } case "delete_in_db": - if len(os.Args) != 4 { + if len(args) != 3 { fmt.Println("Usage: delete_in_db ") os.Exit(1) } - if err := DeleteInDB(os.Args[2], os.Args[3]); err != nil { + if err := DeleteInDB(args[1], args[2]); err != nil { fmt.Println("Error:", err) os.Exit(1) } case "count_in_db": - if len(os.Args) != 4 { + if len(args) != 3 { fmt.Println("Usage: count_in_db ") os.Exit(1) } - count := CountInDB(os.Args[2], os.Args[3]) + count := CountInDB(args[1], args[2]) fmt.Println(count) case "get_from_db": - if len(os.Args) != 4 { + if len(args) != 3 { fmt.Println("Usage: get_from_db ") os.Exit(1) } - line, _ := GetFromDB(os.Args[2], os.Args[3]) + line, _ := GetFromDB(args[1], args[2]) fmt.Println(line) case "print": printDB() diff --git a/cmd/dhcp/main.go b/cmd/dhcp/main.go new file mode 100644 index 0000000..b2ce08d --- /dev/null +++ b/cmd/dhcp/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "fmt" + "net" + "os" + + "git.g3e.fr/syonad/two/internal/dhcp" + "git.g3e.fr/syonad/two/pkg/systemd" +) + +func main() { + subnet := flag.String("subnet", "", "Subnet CIDR (e.g. 10.10.10.0/24)") + name := flag.String("name", "", "Config name (e.g. vpc1_br-00002)") + gateway := flag.String("gateway", "", "Gateway IP (e.g. 10.10.10.1)") + confDir := flag.String("confdir", "/etc/dnsmasq.d", "dnsmasq config directory") + flag.Parse() + + if *subnet == "" || *name == "" || *gateway == "" { + flag.Usage() + os.Exit(1) + } + + _, network, err := net.ParseCIDR(*subnet) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid subnet: %v\n", err) + os.Exit(1) + } + + gw := net.ParseIP(*gateway) + if gw == nil { + fmt.Fprintf(os.Stderr, "invalid gateway IP: %q\n", *gateway) + os.Exit(1) + } + + conf := dhcp.Config{ + Network: network, + Gateway: gw, + Name: *name, + ConfDir: *confDir, + } + + confPath, err := dhcp.GenerateConfig(conf) + if err != nil { + fmt.Fprintf(os.Stderr, "error generating config: %v\n", err) + os.Exit(1) + } + fmt.Printf("dnsmasq config written to %s\n", confPath) + + svc, err := systemd.New() + if err != nil { + fmt.Fprintf(os.Stderr, "error connecting to systemd: %v\n", err) + os.Exit(1) + } + defer svc.Close() + + unit := "dnsmasq@" + *name + ".service" + if err := svc.Start(unit); err != nil { + fmt.Fprintf(os.Stderr, "error starting %s: %v\n", unit, err) + os.Exit(1) + } + fmt.Printf("started %s\n", unit) +} diff --git a/cmd/metacli/main.go b/cmd/metacli/main.go new file mode 100644 index 0000000..20706d2 --- /dev/null +++ b/cmd/metacli/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "flag" + "fmt" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/metadata" + "git.g3e.fr/syonad/two/pkg/db/kv" +) + +func main() { + conf_file := flag.String("conf", "/etc/two/agent.yml", "configuration file") + vm_name := flag.String("vm_name", "", "Nom de la vm") + vpc := flag.String("vpc_name", "", "vpc name") + bind_ip := flag.String("ip", "", "bind ip") + bind_port := flag.String("port", "", "bind port") + ssh_key := flag.String("key", "", "Clef ssh") + password := flag.String("pass", "", "password user") + start := flag.Bool("start", false, "start metadata server") + stop := flag.Bool("stop", false, "stop metadata server") + dryrun := flag.Bool("dryrun", false, "launch in dry node") + + flag.Parse() + + conf, err := configuration.LoadConfig(*conf_file) + if err != nil { + fmt.Println(err) + return + } + + db := kv.InitDB(kv.Config{ + Path: conf.Database.Path, + }, false) + defer db.Close() + + if *start { + metadata.StartMetadata(metadata.NoCloudConfig{ + VpcName: *vpc, + Name: *vm_name, + BindIP: *bind_ip, + BindPort: *bind_port, + Password: *password, + SSHKEY: *ssh_key, + }, db, *dryrun) + } else if *stop { + metadata.StopMetadata(*vm_name, db, *dryrun) + } +} diff --git a/cmd/metadata/main.go b/cmd/metadata/main.go index 54bae88..96e3f75 100644 --- a/cmd/metadata/main.go +++ b/cmd/metadata/main.go @@ -1,9 +1,27 @@ package main import ( + "flag" + "git.g3e.fr/syonad/two/internal/metadata" ) +var ( + iface = flag.String("interface", "0.0.0.0", "Interface IP à écouter") + port = flag.Int("port", 0, "Port à utiliser") + netns_name = flag.String("netns", "", "Network namespace à utiliser") + conf_file = flag.String("conf", "/etc/two/agent.yml", "configuration file") + vm_name = flag.String("vm", "", "Name of the vm") +) + func main() { - metadata.StartServer() + flag.Parse() + + metadata.StartServer(metadata.ServerConfig{ + Netns: *netns_name, + Iface: *iface, + Port: *port, + ConfFile: *conf_file, + VmName: *vm_name, + }) } diff --git a/cmd/vpc/main.go b/cmd/vpc/main.go new file mode 100644 index 0000000..e73f7e9 --- /dev/null +++ b/cmd/vpc/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "flag" + "fmt" + "os" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/vpc" + "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/dgraph-io/badger/v4" +) + +var ( + netns = flag.String("netns", "", "Network namespace à faire") + name = flag.String("name", "", "interface name") + action = flag.String("action", "", "Action a faire") + conf_file = flag.String("conf", "/etc/two/agent.yml", "configuration file") +) + +var DB *badger.DB + +func main() { + flag.Parse() + + conf, err := configuration.LoadConfig(*conf_file) + if err != nil { + fmt.Println(err) + return + } + + DB = kv.InitDB(kv.Config{ + Path: conf.Database.Path, + }, false) + defer DB.Close() + + switch *action { + case "create": + kv.AddInDB(DB, "vpc/"+*name+"/state", "creating") + if err := vpc.CreateVPC(DB, *name); err != nil { + fmt.Println(err) + } + case "delete": + kv.AddInDB(DB, "vpc/"+*name+"/state", "deleting") + if err := vpc.DeleteVPC(DB, *name); err != nil { + fmt.Println(err) + } + if state, err := kv.GetFromDB(DB, "vpc/"+*name+"/state"); err != nil { + fmt.Println(err) + os.Exit(1) + } else if state == "deleted" { + kv.DeleteInDB(DB, "vpc/"+*name) + } + case "check": + if state, err := kv.GetFromDB(DB, "vpc/"+*name+"/state"); err != nil { + os.Exit(1) + } else if state != "created" { + os.Exit(1) + } + default: + fmt.Printf("Available commande:\n - create\n - delete\n - check\n") + os.Exit(1) + } + os.Exit(0) +} diff --git a/conf/agent/config.dev.yml b/conf/agent/config.dev.yml new file mode 100644 index 0000000..62cb624 --- /dev/null +++ b/conf/agent/config.dev.yml @@ -0,0 +1,2 @@ +database: + path: "./data/" \ No newline at end of file diff --git a/exemple/agent/config.exemple.yml b/conf/agent/config.exemple.yml similarity index 100% rename from exemple/agent/config.exemple.yml rename to conf/agent/config.exemple.yml diff --git a/go.mod b/go.mod index cbc34b5..7430e72 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,12 @@ module git.g3e.fr/syonad/two -go 1.23.8 +go 1.24.0 + +toolchain go1.24.11 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/dgraph-io/badger/v4 v4.8.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -11,6 +14,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -21,13 +25,15 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/vishvananda/netlink v1.3.1 // indirect + github.com/vishvananda/netns v0.0.5 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.28.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 2482f6c..73d16f3 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= @@ -15,6 +17,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -35,6 +39,10 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= @@ -47,8 +55,12 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/internal/.keep b/internal/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/internal/config/agent/config_test.go b/internal/config/agent/config_test.go new file mode 100644 index 0000000..d0f3d31 --- /dev/null +++ b/internal/config/agent/config_test.go @@ -0,0 +1,83 @@ +package configuration + +import ( + "os" + "path/filepath" + "testing" +) + +func writeYAML(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "config.yml") + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + t.Fatalf("impossible d'écrire le fichier de config : %v", err) + } + return path +} + +// --- LoadConfig --- + +func TestLoadConfig_ValidFile(t *testing.T) { + path := writeYAML(t, ` +database: + path: /tmp/mydb +`) + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/tmp/mydb" { + t.Errorf("database.path attendu %q, obtenu %q", "/tmp/mydb", cfg.Database.Path) + } +} + +func TestLoadConfig_DefaultPath(t *testing.T) { + // Fichier vide → viper applique la valeur par défaut + path := writeYAML(t, "") + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/var/lib/two/data/" { + t.Errorf("valeur par défaut attendue %q, obtenu %q", "/var/lib/two/data/", cfg.Database.Path) + } +} + +func TestLoadConfig_MissingFile_UsesDefaults(t *testing.T) { + // Fichier inexistant : viper ignore l'erreur ReadInConfig et retourne les défauts + cfg, err := LoadConfig("/chemin/inexistant/config.yml") + if err != nil { + t.Fatalf("LoadConfig devrait retourner les défauts si le fichier est absent : %v", err) + } + if cfg.Database.Path != "/var/lib/two/data/" { + t.Errorf("valeur par défaut attendue, obtenu %q", cfg.Database.Path) + } +} + +func TestLoadConfig_PartialConfig_MissingDatabaseKey(t *testing.T) { + // Fichier sans la clé database → valeur par défaut + path := writeYAML(t, ` +autrekey: valeur +`) + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/var/lib/two/data/" { + t.Errorf("valeur par défaut attendue, obtenu %q", cfg.Database.Path) + } +} + +func TestLoadConfig_CustomPath(t *testing.T) { + path := writeYAML(t, ` +database: + path: /opt/two/data +`) + cfg, err := LoadConfig(path) + if err != nil { + t.Fatalf("LoadConfig a échoué : %v", err) + } + if cfg.Database.Path != "/opt/two/data" { + t.Errorf("attendu %q, obtenu %q", "/opt/two/data", cfg.Database.Path) + } +} diff --git a/internal/config/agent/struct.go b/internal/config/agent/struct.go index 12a46d3..c9537bf 100644 --- a/internal/config/agent/struct.go +++ b/internal/config/agent/struct.go @@ -17,9 +17,7 @@ func LoadConfig(path string) (*Config, error) { v.SetDefault("database.path", "/var/lib/two/data/") - if err := v.ReadInConfig(); err != nil { - return nil, err - } + v.ReadInConfig() var cfg Config if err := v.Unmarshal(&cfg); err != nil { diff --git a/internal/dhcp/dhcp_test.go b/internal/dhcp/dhcp_test.go new file mode 100644 index 0000000..c4a659f --- /dev/null +++ b/internal/dhcp/dhcp_test.go @@ -0,0 +1,157 @@ +package dhcp + +import ( + "net" + "os" + "path/filepath" + "strings" + "testing" +) + +func parseNet(t *testing.T, cidr string) *net.IPNet { + t.Helper() + _, network, err := net.ParseCIDR(cidr) + if err != nil { + t.Fatalf("ParseCIDR(%q) : %v", cidr, err) + } + return network +} + +// --- cloneIP --- + +func TestCloneIP_IsIndependent(t *testing.T) { + ip := net.ParseIP("10.0.0.1").To4() + clone := cloneIP(ip) + clone[3] = 99 + if ip[3] == 99 { + t.Error("cloneIP devrait retourner une copie indépendante") + } +} + +// --- incrementIP --- + +func TestIncrementIP_Simple(t *testing.T) { + ip := net.ParseIP("10.0.0.1").To4() + incrementIP(ip) + if ip.String() != "10.0.0.2" { + t.Errorf("attendu 10.0.0.2, obtenu %s", ip) + } +} + +func TestIncrementIP_Carry(t *testing.T) { + ip := net.ParseIP("10.0.0.255").To4() + incrementIP(ip) + if ip.String() != "10.0.1.0" { + t.Errorf("attendu 10.0.1.0, obtenu %s", ip) + } +} + +// --- GenerateConfig --- + +func newConf(t *testing.T, cidr string) Config { + t.Helper() + _, network, _ := net.ParseCIDR(cidr) + return Config{ + Network: network, + Gateway: net.ParseIP("192.168.1.1").To4(), + Name: "test", + ConfDir: t.TempDir(), + } +} + +func TestGenerateConfig_CreatesFile(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") // 6 hôtes + path, err := GenerateConfig(conf) + if err != nil { + t.Fatalf("GenerateConfig a échoué : %v", err) + } + + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Errorf("le fichier %q n'a pas été créé", path) + } +} + +func TestGenerateConfig_FilenameMatchesName(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + path, err := GenerateConfig(conf) + if err != nil { + t.Fatalf("GenerateConfig a échoué : %v", err) + } + + expected := filepath.Join(conf.ConfDir, "test.conf") + if path != expected { + t.Errorf("chemin attendu %q, obtenu %q", expected, path) + } +} + +func TestGenerateConfig_ContainsGateway(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + if !strings.Contains(string(content), "dhcp-option=3,192.168.1.1") { + t.Errorf("gateway absente du fichier généré :\n%s", content) + } +} + +func TestGenerateConfig_ContainsDhcpRange(t *testing.T) { + _, network, _ := net.ParseCIDR("10.10.0.0/24") + conf := Config{ + Network: network, + Gateway: net.ParseIP("10.10.0.1").To4(), + Name: "vpc1", + ConfDir: t.TempDir(), + } + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + if !strings.Contains(string(content), "dhcp-range=10.10.0.0,static,255.255.255.0,12h") { + t.Errorf("dhcp-range absent ou incorrect :\n%s", content) + } +} + +func TestGenerateConfig_OneHostEntryPerIP(t *testing.T) { + // /29 = réseau + broadcast + 6 hôtes → 8 adresses + conf := newConf(t, "10.0.0.0/29") + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + lines := strings.Split(string(content), "\n") + count := 0 + for _, l := range lines { + if strings.HasPrefix(l, "dhcp-host=") { + count++ + } + } + // /29 contient 8 adresses (0 à 7) + if count != 8 { + t.Errorf("attendu 8 entrées dhcp-host, obtenu %d", count) + } +} + +func TestGenerateConfig_MACPrefix(t *testing.T) { + conf := newConf(t, "10.0.0.0/30") // 4 adresses + path, _ := GenerateConfig(conf) + content, _ := os.ReadFile(path) + + if !strings.Contains(string(content), "00:22:33:") { + t.Errorf("préfixe MAC 00:22:33: absent :\n%s", content) + } +} + +func TestGenerateConfig_CreatesConfDir(t *testing.T) { + dir := filepath.Join(t.TempDir(), "sous", "dossier") + _, network, _ := net.ParseCIDR("10.0.0.0/30") + conf := Config{ + Network: network, + Gateway: net.ParseIP("10.0.0.1").To4(), + Name: "net", + ConfDir: dir, + } + if _, err := GenerateConfig(conf); err != nil { + t.Fatalf("GenerateConfig devrait créer les répertoires manquants : %v", err) + } + if _, err := os.Stat(dir); os.IsNotExist(err) { + t.Errorf("répertoire %q non créé", dir) + } +} diff --git a/internal/dhcp/generate.go b/internal/dhcp/generate.go new file mode 100644 index 0000000..01bfe20 --- /dev/null +++ b/internal/dhcp/generate.go @@ -0,0 +1,47 @@ +package dhcp + +import ( + "fmt" + "net" + "os" + "path/filepath" + "strings" +) + +func GenerateConfig(c Config) (string, error) { + mask := fmt.Sprintf("%d.%d.%d.%d", c.Network.Mask[0], c.Network.Mask[1], c.Network.Mask[2], c.Network.Mask[3]) + + var sb strings.Builder + fmt.Fprintf(&sb, "no-resolv\n") + fmt.Fprintf(&sb, "dhcp-range=%s,static,%s,12h\n", c.Network.IP.String(), mask) + fmt.Fprintf(&sb, "dhcp-option=3,%s\n", c.Gateway.String()) + fmt.Fprintf(&sb, "dhcp-option=6,1.1.1.1,8.8.8.8\n\n") + + i := 0 + for ip := cloneIP(c.Network.IP); c.Network.Contains(ip); incrementIP(ip) { + fmt.Fprintf(&sb, "dhcp-host=00:22:33:%02X:%02X:%02X,%s\n", + (i>>16)&0xFF, (i>>8)&0xFF, i&0xFF, ip) + i++ + } + + outPath := filepath.Join(c.ConfDir, c.Name+".conf") + if err := os.MkdirAll(c.ConfDir, 0755); err != nil { + return "", err + } + return outPath, os.WriteFile(outPath, []byte(sb.String()), 0644) +} + +func incrementIP(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] != 0 { + break + } + } +} + +func cloneIP(ip net.IP) net.IP { + clone := make(net.IP, len(ip)) + copy(clone, ip) + return clone +} diff --git a/internal/dhcp/struct.go b/internal/dhcp/struct.go new file mode 100644 index 0000000..4c69b9c --- /dev/null +++ b/internal/dhcp/struct.go @@ -0,0 +1,12 @@ +package dhcp + +import ( + "net" +) + +type Config struct { + Network *net.IPNet + Gateway net.IP + Name string + ConfDir string +} diff --git a/internal/metadata/handle.go b/internal/metadata/handle.go new file mode 100644 index 0000000..33ac088 --- /dev/null +++ b/internal/metadata/handle.go @@ -0,0 +1,26 @@ +package metadata + +import ( + "git.g3e.fr/syonad/two/pkg/systemd" + "github.com/dgraph-io/badger/v4" +) + +func StartMetadata(config NoCloudConfig, db *badger.DB, dryrun bool) { + service, _ := systemd.New() + defer service.Close() + + LoadNcCloudInDB(config, db) + if !dryrun { + service.Start("metadata@" + config.Name) + } +} + +func StopMetadata(vm_name string, db *badger.DB, dryrun bool) { + service, _ := systemd.New() + defer service.Close() + + UnLoadNoCloudInDB(vm_name, db) + if !dryrun { + service.Stop("metadata@" + vm_name) + } +} diff --git a/internal/metadata/metadata_test.go b/internal/metadata/metadata_test.go new file mode 100644 index 0000000..62a2830 --- /dev/null +++ b/internal/metadata/metadata_test.go @@ -0,0 +1,200 @@ +package metadata + +import ( + "strings" + "testing" + + "git.g3e.fr/syonad/two/pkg/db/kv" +) + +func newCfg() NoCloudConfig { + return NoCloudConfig{ + VpcName: "vpc-test", + BindIP: "169.254.169.254", + BindPort: "80", + Name: "vm1", + Password: "s3cr3t", + SSHKEY: "ssh-ed25519 AAAA... user@host", + } +} + +func newTestDB(t *testing.T) interface{ Close() error } { + t.Helper() + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + return db +} + +// --- RenderConfig --- + +func TestRenderConfig_MetaData(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/meta-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig meta-data : %v", err) + } + if !strings.Contains(out, "instance-id: vm1") { + t.Errorf("instance-id absent :\n%s", out) + } + if !strings.Contains(out, "local-hostname: vm1") { + t.Errorf("local-hostname absent :\n%s", out) + } +} + +func TestRenderConfig_VendorData_ContainsPassword(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/vendor-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig vendor-data : %v", err) + } + if !strings.Contains(out, "s3cr3t") { + t.Errorf("password absent du vendor-data :\n%s", out) + } +} + +func TestRenderConfig_VendorData_ContainsSSHKey(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/vendor-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig vendor-data : %v", err) + } + if !strings.Contains(out, "ssh-ed25519 AAAA... user@host") { + t.Errorf("clé SSH absente du vendor-data :\n%s", out) + } +} + +func TestRenderConfig_NetworkConfig(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/network-config.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig network-config : %v", err) + } + if !strings.Contains(out, "dhcp4: true") { + t.Errorf("dhcp4 absent du network-config :\n%s", out) + } +} + +func TestRenderConfig_UserData(t *testing.T) { + cfg := newCfg() + out, err := RenderConfig("templates/user-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig user-data : %v", err) + } + if !strings.Contains(out, "passwd -d root") { + t.Errorf("user-data inattendu :\n%s", out) + } +} + +func TestRenderConfig_InvalidTemplate(t *testing.T) { + _, err := RenderConfig("templates/inexistant.tmpl", newCfg()) + if err == nil { + t.Error("RenderConfig devrait retourner une erreur pour un template inexistant") + } +} + +func TestRenderConfig_SpecialCharsInName(t *testing.T) { + cfg := newCfg() + cfg.Name = "vm-prod-01" + out, err := RenderConfig("templates/meta-data.tmpl", cfg) + if err != nil { + t.Fatalf("RenderConfig : %v", err) + } + if !strings.Contains(out, "vm-prod-01") { + t.Errorf("nom vm-prod-01 absent :\n%s", out) + } +} + +// --- LoadNcCloudInDB / UnLoadNoCloudInDB --- + +func TestLoadNcCloudInDB_StoresAllKeys(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg := newCfg() + LoadNcCloudInDB(cfg, db) + + keys := []string{ + "metadata/vm1/meta-data", + "metadata/vm1/user-data", + "metadata/vm1/network-config", + "metadata/vm1/vendor-data", + "metadata/vm1/vpc", + "metadata/vm1/bind_ip", + "metadata/vm1/bind_port", + } + for _, key := range keys { + val, err := kv.GetFromDB(db, key) + if err != nil { + t.Errorf("clé %q absente après LoadNcCloudInDB : %v", key, err) + } + if val == "" && key != "metadata/vm1/user-data" { + t.Errorf("clé %q vide après LoadNcCloudInDB", key) + } + } +} + +func TestLoadNcCloudInDB_VpcAndBindValues(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg := newCfg() + LoadNcCloudInDB(cfg, db) + + vpc, _ := kv.GetFromDB(db, "metadata/vm1/vpc") + if vpc != "vpc-test" { + t.Errorf("vpc attendu %q, obtenu %q", "vpc-test", vpc) + } + + ip, _ := kv.GetFromDB(db, "metadata/vm1/bind_ip") + if ip != "169.254.169.254" { + t.Errorf("bind_ip attendu %q, obtenu %q", "169.254.169.254", ip) + } + + port, _ := kv.GetFromDB(db, "metadata/vm1/bind_port") + if port != "80" { + t.Errorf("bind_port attendu %q, obtenu %q", "80", port) + } +} + +func TestUnLoadNoCloudInDB_RemovesAllKeys(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg := newCfg() + LoadNcCloudInDB(cfg, db) + UnLoadNoCloudInDB("vm1", db) + + keys := []string{ + "metadata/vm1/meta-data", + "metadata/vm1/user-data", + "metadata/vm1/network-config", + "metadata/vm1/vendor-data", + "metadata/vm1/vpc", + "metadata/vm1/bind_ip", + "metadata/vm1/bind_port", + } + for _, key := range keys { + _, err := kv.GetFromDB(db, key) + if err == nil { + t.Errorf("clé %q devrait être supprimée après UnLoadNoCloudInDB", key) + } + } +} + +func TestUnLoadNoCloudInDB_DoesNotAffectOtherVMs(t *testing.T) { + db := kv.InitDB(kv.Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + + cfg1 := newCfg() + cfg2 := newCfg() + cfg2.Name = "vm2" + LoadNcCloudInDB(cfg1, db) + LoadNcCloudInDB(cfg2, db) + + UnLoadNoCloudInDB("vm1", db) + + _, err := kv.GetFromDB(db, "metadata/vm2/vpc") + if err != nil { + t.Errorf("vm2 ne devrait pas être supprimée : %v", err) + } +} diff --git a/internal/metadata/render.go b/internal/metadata/render.go new file mode 100644 index 0000000..f6c3cae --- /dev/null +++ b/internal/metadata/render.go @@ -0,0 +1,46 @@ +package metadata + +import ( + "bytes" + "embed" + "text/template" + + "git.g3e.fr/syonad/two/pkg/db/kv" + "github.com/dgraph-io/badger/v4" +) + +//go:embed templates/*.tmpl +var templateFS embed.FS + +func RenderConfig(path string, cfg NoCloudConfig) (string, error) { + tpl, err := template.ParseFS(templateFS, path) + if err != nil { + return "", err + } + + var buf bytes.Buffer + if err := tpl.Execute(&buf, cfg); err != nil { + return "", err + } + + return buf.String(), nil +} + +func LoadNcCloudInDB(config NoCloudConfig, db *badger.DB) { + meta_data, _ := RenderConfig("templates/meta-data.tmpl", config) + user_data, _ := RenderConfig("templates/user-data.tmpl", config) + network_config, _ := RenderConfig("templates/network-config.tmpl", config) + vendor_data, _ := RenderConfig("templates/vendor-data.tmpl", config) + + kv.AddInDB(db, "metadata/"+config.Name+"/meta-data", meta_data) + kv.AddInDB(db, "metadata/"+config.Name+"/user-data", user_data) + kv.AddInDB(db, "metadata/"+config.Name+"/network-config", network_config) + kv.AddInDB(db, "metadata/"+config.Name+"/vendor-data", vendor_data) + kv.AddInDB(db, "metadata/"+config.Name+"/vpc", config.VpcName) + kv.AddInDB(db, "metadata/"+config.Name+"/bind_ip", config.BindIP) + kv.AddInDB(db, "metadata/"+config.Name+"/bind_port", config.BindPort) +} + +func UnLoadNoCloudInDB(vm_name string, db *badger.DB) { + kv.DeleteInDB(db, "metadata/"+vm_name) +} diff --git a/internal/metadata/server.go b/internal/metadata/server.go index 336f754..3b4e12f 100644 --- a/internal/metadata/server.go +++ b/internal/metadata/server.go @@ -1,24 +1,20 @@ package metadata import ( - "encoding/json" - "flag" "fmt" - "io/ioutil" "log" "net" "net/http" + "strconv" "time" + + configuration "git.g3e.fr/syonad/two/internal/config/agent" + "git.g3e.fr/syonad/two/internal/netns" + "git.g3e.fr/syonad/two/pkg/db/kv" ) var data NoCloudData -var ( - iface = flag.String("interface", "0.0.0.0", "Interface IP à écouter") - port = flag.Int("port", 8080, "Port à utiliser") - file = flag.String("file", "", "Fichier JSON contenant les données NoCloud") -) - func getIP(r *http.Request) string { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { @@ -27,6 +23,51 @@ func getIP(r *http.Request) string { return ip } +func getFromDB(config ServerConfig) NoCloudData { + var netns_name string + var port int + var iface string + + conf_db, _ := configuration.LoadConfig(config.ConfFile) + + db := kv.InitDB(kv.Config{Path: conf_db.Database.Path}, true) + defer db.Close() + + metadata, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/meta-data") + userdata, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/user-data") + networkconfig, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/network-config") + vendordata, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/vendor-data") + + if config.Netns == "" { + netns_name, _ = kv.GetFromDB(db, "metadata/"+config.VmName+"/vpc") + } else { + netns_name = config.Netns + } + + if config.Iface == "" { + iface, _ = kv.GetFromDB(db, "metadata/"+config.VmName+"/bind_ip") + } else { + iface = config.Iface + } + + if config.Port == 0 { + sport, _ := kv.GetFromDB(db, "metadata/"+config.VmName+"/bind_port") + port, _ = strconv.Atoi(sport) + } else { + port = config.Port + } + + return NoCloudData{ + MetaData: metadata, + UserData: userdata, + NetworkConfig: networkconfig, + VendorData: vendordata, + NetNs: netns_name, + Iface: iface, + Port: port, + } +} + func rootHandler(w http.ResponseWriter, r *http.Request) { ip := getIP(r) path := r.URL.Path @@ -51,25 +92,18 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { } } -func StartServer() { - flag.Parse() +func StartServer(config ServerConfig) { + data = getFromDB(config) - if *file == "" { - log.Fatal("Vous devez spécifier un fichier via --file") - } - - raw, err := ioutil.ReadFile(*file) - if err != nil { - log.Fatalf("Erreur de lecture du fichier: %v", err) - } - - if err := json.Unmarshal(raw, &data); err != nil { - log.Fatalf("Erreur de parsing JSON: %v", err) + if data.NetNs != "" { + if err := netns.Enter(data.NetNs); err != nil { + log.Fatalf("Impossible d'entrer dans le netns: %v", err) + } } http.HandleFunc("/", rootHandler) - address := fmt.Sprintf("%s:%d", *iface, *port) + address := fmt.Sprintf("%s:%d", data.Iface, data.Port) log.Printf("Serveur NoCloud démarré sur http://%s/", address) log.Fatal(http.ListenAndServe(address, nil)) } diff --git a/internal/metadata/struct.go b/internal/metadata/struct.go index f1edf18..5810ff4 100644 --- a/internal/metadata/struct.go +++ b/internal/metadata/struct.go @@ -1,8 +1,29 @@ package metadata type NoCloudData struct { - MetaData string `json:"meta-data"` - UserData string `json:"user-data"` - NetworkConfig string `json:"network-config"` - VendorData string `json:"vendor-data"` + MetaData string + UserData string + NetworkConfig string + VendorData string + NetNs string + Iface string + Port int +} + +type ServerConfig struct { + Netns string + File string + Iface string + Port int + ConfFile string + VmName string +} + +type NoCloudConfig struct { + VpcName string + BindIP string + BindPort string + Name string + Password string + SSHKEY string } diff --git a/internal/metadata/templates/meta-data.tmpl b/internal/metadata/templates/meta-data.tmpl new file mode 100644 index 0000000..ff876bb --- /dev/null +++ b/internal/metadata/templates/meta-data.tmpl @@ -0,0 +1,2 @@ +instance-id: {{ .Name }} +local-hostname: {{ .Name }} diff --git a/internal/metadata/templates/network-config.tmpl b/internal/metadata/templates/network-config.tmpl new file mode 100644 index 0000000..0f8d052 --- /dev/null +++ b/internal/metadata/templates/network-config.tmpl @@ -0,0 +1,4 @@ +version: 2 +ethernets: + eth0: + dhcp4: true diff --git a/internal/metadata/templates/user-data.tmpl b/internal/metadata/templates/user-data.tmpl new file mode 100644 index 0000000..84195ab --- /dev/null +++ b/internal/metadata/templates/user-data.tmpl @@ -0,0 +1,3 @@ +#!/bin/sh + +passwd -d root diff --git a/internal/metadata/templates/vendor-data.tmpl b/internal/metadata/templates/vendor-data.tmpl new file mode 100644 index 0000000..148d6db --- /dev/null +++ b/internal/metadata/templates/vendor-data.tmpl @@ -0,0 +1,13 @@ +#cloud-config +users: + - name: syonad + lock_passwd: false + gecos: alpine Cloud User + groups: [adm, wheel] + doas: + - permit nopass syonad + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + shell: /bin/ash + passwd: "{{ .Password }}" + ssh_authorized_keys: + - "{{ .SSHKEY }}" \ No newline at end of file diff --git a/internal/netif/bridge.go b/internal/netif/bridge.go new file mode 100644 index 0000000..a4754a3 --- /dev/null +++ b/internal/netif/bridge.go @@ -0,0 +1,38 @@ +package netif + +import ( + "github.com/vishvananda/netlink" +) + +func CreateBridge(name string, mtu int) error { + br := &netlink.Bridge{ + LinkAttrs: netlink.LinkAttrs{ + Name: name, + MTU: mtu, + }, + } + + if err := netlink.LinkAdd(br); err != nil { + return err + } + + if err := netlink.LinkSetUp(br); err != nil { + return err + } + + return nil +} + +func BridgeSetMaster(iface, bridge string) error { + link, err := netlink.LinkByName(iface) + if err != nil { + return err + } + + br, err := netlink.LinkByName(bridge) + if err != nil { + return err + } + + return netlink.LinkSetMaster(link, br) +} diff --git a/internal/netif/delete.go b/internal/netif/delete.go new file mode 100644 index 0000000..9b06c55 --- /dev/null +++ b/internal/netif/delete.go @@ -0,0 +1,13 @@ +package netif + +import ( + "github.com/vishvananda/netlink" +) + +func DeleteLink(name string) error { + link, err := netlink.LinkByName(name) + if err != nil { + return err + } + return netlink.LinkDel(link) +} diff --git a/internal/netif/upDown.go b/internal/netif/upDown.go new file mode 100644 index 0000000..6d27cc2 --- /dev/null +++ b/internal/netif/upDown.go @@ -0,0 +1,21 @@ +package netif + +import ( + "github.com/vishvananda/netlink" +) + +func LinkSetUp(name string) error { + link, err := netlink.LinkByName(name) + if err != nil { + return err + } + return netlink.LinkSetUp(link) +} + +func LinkSetDown(name string) error { + link, err := netlink.LinkByName(name) + if err != nil { + return err + } + return netlink.LinkSetDown(link) +} diff --git a/internal/netif/veth.go b/internal/netif/veth.go new file mode 100644 index 0000000..0056c60 --- /dev/null +++ b/internal/netif/veth.go @@ -0,0 +1,48 @@ +package netif + +import ( + "fmt" + "runtime" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" +) + +func CreateVethToNetns(rootIf, nsIf, netnsPath string, mtu int) error { + // Obligatoire : netns lié au thread + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Ouvrir le netns cible + ns, err := netns.GetFromPath(netnsPath) + if err != nil { + return fmt.Errorf("open netns: %w, %s", err, netnsPath) + } + defer ns.Close() + + // Créer le veth dans le netns courant + veth := &netlink.Veth{ + LinkAttrs: netlink.LinkAttrs{ + Name: rootIf, + MTU: mtu, + }, + PeerName: nsIf, + } + + if err := netlink.LinkAdd(veth); err != nil { + return fmt.Errorf("link add: %w", err) + } + + // Récupérer l'interface peer + peer, err := netlink.LinkByName(nsIf) + if err != nil { + return fmt.Errorf("peer not found: %w", err) + } + + // Déplacer le peer dans le netns cible + if err := netlink.LinkSetNsFd(peer, int(ns)); err != nil { + return fmt.Errorf("set ns: %w", err) + } + + return nil +} diff --git a/internal/netns/call.go b/internal/netns/call.go new file mode 100644 index 0000000..da326fd --- /dev/null +++ b/internal/netns/call.go @@ -0,0 +1,5 @@ +package netns + +func Call(name string, fn func() error) error { + return call(name, fn) +} diff --git a/internal/netns/call_linux.go b/internal/netns/call_linux.go new file mode 100644 index 0000000..b23ac19 --- /dev/null +++ b/internal/netns/call_linux.go @@ -0,0 +1,44 @@ +//go:build linux + +package netns + +import ( + "fmt" + "os" + "runtime" + + "golang.org/x/sys/unix" +) + +func call(name string, fn func() error) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // sauvegarde du netns courant + orig, err := os.Open("/proc/self/ns/net") + if err != nil { + return err + } + defer orig.Close() + + // entrer dans le netns cible + f, err := os.Open(fmt.Sprintf("/var/run/netns/%s", name)) + if err != nil { + return err + } + defer f.Close() + + if err := unix.Setns(int(f.Fd()), unix.CLONE_NEWNET); err != nil { + return err + } + + // exécuter la fonction dans le netns + err = fn() + + // toujours revenir au netns d'origine + if restoreErr := unix.Setns(int(orig.Fd()), unix.CLONE_NEWNET); restoreErr != nil { + return restoreErr + } + + return err +} diff --git a/internal/netns/call_other.go b/internal/netns/call_other.go new file mode 100644 index 0000000..14a8924 --- /dev/null +++ b/internal/netns/call_other.go @@ -0,0 +1,7 @@ +//go:build !linux + +package netns + +func call(name string, fn func() error) error { + return fn() +} diff --git a/internal/netns/create.go b/internal/netns/create.go new file mode 100644 index 0000000..d6ca305 --- /dev/null +++ b/internal/netns/create.go @@ -0,0 +1,5 @@ +package netns + +func Create(name string) error { + return create(name) +} diff --git a/internal/netns/create_linux.go b/internal/netns/create_linux.go new file mode 100644 index 0000000..8ee0afa --- /dev/null +++ b/internal/netns/create_linux.go @@ -0,0 +1,55 @@ +//go:build linux + +package netns + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func create(name string) error { + base := "/var/run/netns" + path := base + "/" + name + + if err := os.MkdirAll(base, 0755); err != nil { + return err + } + + // fichier cible + f, err := os.Create(path) + if err != nil { + return err + } + f.Close() + + // sauvegarde du netns courant + orig, err := os.Open("/proc/self/ns/net") + if err != nil { + return err + } + defer orig.Close() + + // nouveau netns + if err := unix.Unshare(unix.CLONE_NEWNET); err != nil { + return err + } + + // bind mount du netns courant vers /var/run/netns/ + if err := unix.Mount( + "/proc/self/ns/net", + path, + "", + unix.MS_BIND, + "", + ); err != nil { + return err + } + + // revenir au netns original + if err := unix.Setns(int(orig.Fd()), unix.CLONE_NEWNET); err != nil { + return err + } + + return nil +} diff --git a/internal/netns/create_other.go b/internal/netns/create_other.go new file mode 100644 index 0000000..3c0fd66 --- /dev/null +++ b/internal/netns/create_other.go @@ -0,0 +1,5 @@ +//go:build !linux + +package netns + +func create(string) error { return nil } diff --git a/internal/netns/delete.go b/internal/netns/delete.go new file mode 100644 index 0000000..af0a585 --- /dev/null +++ b/internal/netns/delete.go @@ -0,0 +1,5 @@ +package netns + +func Delete(name string) error { + return delete(name) +} diff --git a/internal/netns/delete_linux.go b/internal/netns/delete_linux.go new file mode 100644 index 0000000..ac0af1f --- /dev/null +++ b/internal/netns/delete_linux.go @@ -0,0 +1,18 @@ +//go:build linux + +package netns + +import ( + "os" + + "golang.org/x/sys/unix" +) + +func delete(name string) error { + path := "/var/run/netns/" + name + + if err := unix.Unmount(path, unix.MNT_DETACH); err != nil { + return err + } + return os.Remove(path) +} diff --git a/internal/netns/delete_other.go b/internal/netns/delete_other.go new file mode 100644 index 0000000..186d355 --- /dev/null +++ b/internal/netns/delete_other.go @@ -0,0 +1,5 @@ +//go:build !linux + +package netns + +func delete(string) error { return nil } diff --git a/internal/netns/enter.go b/internal/netns/enter.go new file mode 100644 index 0000000..68ea372 --- /dev/null +++ b/internal/netns/enter.go @@ -0,0 +1,5 @@ +package netns + +func Enter(name string) error { + return enter(name) +} diff --git a/internal/netns/enter_linux.go b/internal/netns/enter_linux.go new file mode 100644 index 0000000..04e116a --- /dev/null +++ b/internal/netns/enter_linux.go @@ -0,0 +1,26 @@ +//go:build linux + +package netns + +import ( + "fmt" + "os" + "runtime" + + "golang.org/x/sys/unix" +) + +func enter(name string) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + path := fmt.Sprintf("/var/run/netns/%s", name) + + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + return unix.Setns(int(f.Fd()), unix.CLONE_NEWNET) +} diff --git a/internal/netns/enter_other.go b/internal/netns/enter_other.go new file mode 100644 index 0000000..2691ff2 --- /dev/null +++ b/internal/netns/enter_other.go @@ -0,0 +1,8 @@ +//go:build !linux + +package netns + +func enter(name string) error { + // Ignoré hors Linux + return nil +} diff --git a/internal/netns/exist.go b/internal/netns/exist.go new file mode 100644 index 0000000..7783dd9 --- /dev/null +++ b/internal/netns/exist.go @@ -0,0 +1,14 @@ +package netns + +import ( + "os" +) + +func exist(name string) bool { + _, err := os.Stat("/var/run/netns/" + name) + return err == nil +} + +func Exist(name string) bool { + return exist(name) +} diff --git a/internal/vpc/create.go b/internal/vpc/create.go new file mode 100644 index 0000000..3c3ad87 --- /dev/null +++ b/internal/vpc/create.go @@ -0,0 +1,61 @@ +package vpc + +import ( + "git.g3e.fr/syonad/two/internal/netif" + "git.g3e.fr/syonad/two/internal/netns" + "git.g3e.fr/syonad/two/pkg/db/kv" + + "github.com/dgraph-io/badger/v4" +) + +func CreateVPC(db *badger.DB, name string) error { + // missing + // search data in db + // change state in db + + // create netns + if state, err := kv.GetFromDB(db, "vpc/"+name+"/state"); err != nil { + return err + } else if state == "creating" { + if err := netns.Create(name); err != nil { + return err + } + + // create veth public for this netns + if err := netif.CreateVethToNetns("veth"+name+"ext", "vethpublicint", "/var/run/netns/"+name, 9000); err != nil { + return err + } + + // create public bridge in netns + if err := netns.Call(name, func() error { + return netif.CreateBridge("br-public", 1500) + }); err != nil { + return err + } + + // set veth to ext public bridge + if err := netif.BridgeSetMaster("veth"+name+"ext", "br-public"); err != nil { + return err + } + + // set veth to int public bridge + if err := netns.Call(name, func() error { + return netif.BridgeSetMaster("vethpublicint", "br-public") + }); err != nil { + return err + } + + // set set ext veth up + if err := netif.LinkSetUp("veth" + name + "ext"); err != nil { + return nil + } + // set set int veth up + if err := netns.Call(name, func() error { + return netif.LinkSetUp("vethpublicint") + }); err != nil { + return err + } + kv.AddInDB(db, "vpc/"+name+"/state", "created") + } + return nil +} diff --git a/internal/vpc/delete.go b/internal/vpc/delete.go new file mode 100644 index 0000000..a05e807 --- /dev/null +++ b/internal/vpc/delete.go @@ -0,0 +1,26 @@ +package vpc + +import ( + "git.g3e.fr/syonad/two/internal/netif" + "git.g3e.fr/syonad/two/internal/netns" + "git.g3e.fr/syonad/two/pkg/db/kv" + + "github.com/dgraph-io/badger/v4" +) + +func DeleteVPC(db *badger.DB, name string) error { + if state, err := kv.GetFromDB(db, "vpc/"+name+"/state"); err != nil { + return err + } else if state == "deleting" { + if err := netif.DeleteLink(name + "-ext"); err != nil { + return err + } + + if err := netns.Delete(name); err != nil { + return err + } + kv.AddInDB(db, "vpc/"+name+"/state", "deleted") + } + + return nil +} diff --git a/pkg/.keep b/pkg/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/pkg/db/kv/addInDB.go b/pkg/db/kv/addInDB.go new file mode 100644 index 0000000..44ea19c --- /dev/null +++ b/pkg/db/kv/addInDB.go @@ -0,0 +1,11 @@ +package kv + +import ( + "github.com/dgraph-io/badger/v4" +) + +func AddInDB(db *badger.DB, key string, value string) error { + return db.Update(func(txn *badger.Txn) error { + return txn.Set([]byte(key), []byte(value)) + }) +} diff --git a/pkg/db/kv/deleteInDB.go b/pkg/db/kv/deleteInDB.go new file mode 100644 index 0000000..1943c81 --- /dev/null +++ b/pkg/db/kv/deleteInDB.go @@ -0,0 +1,42 @@ +package kv + +import ( + "log" + + "github.com/dgraph-io/badger/v4" +) + +func deleteKey(db *badger.DB, key string) error { + return db.Update(func(txn *badger.Txn) error { + return txn.Delete([]byte(key)) + }) +} + +func DeleteInDB(db *badger.DB, key string) error { + + prefix := []byte(key + "/") + + err := db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false + + it := txn.NewIterator(opts) + defer it.Close() + + for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() { + item := it.Item() + key := item.Key() + + k := append([]byte{}, key...) + if err := deleteKey(db, string(k)); err != nil { + return err + } + } + return nil + }) + if err != nil { + log.Fatal(err) + } + + return deleteKey(db, key) +} diff --git a/pkg/db/kv/getInDB.go b/pkg/db/kv/getInDB.go new file mode 100644 index 0000000..bfe7590 --- /dev/null +++ b/pkg/db/kv/getInDB.go @@ -0,0 +1,22 @@ +package kv + +import ( + "github.com/dgraph-io/badger/v4" +) + +func GetFromDB(db *badger.DB, key string) (string, error) { + var result string + + err := db.View(func(txn *badger.Txn) error { + item, err := txn.Get([]byte(key)) + if err != nil { + return err + } + return item.Value(func(val []byte) error { + result = string(val) + return nil + }) + }) + + return result, err +} diff --git a/pkg/db/kv/init.go b/pkg/db/kv/init.go index 18148df..cc09cb0 100644 --- a/pkg/db/kv/init.go +++ b/pkg/db/kv/init.go @@ -4,8 +4,9 @@ import ( "github.com/dgraph-io/badger/v4" ) -func InitDB(conf Config) *badger.DB { - opts := badger.DefaultOptions(conf.Path) +func InitDB(conf Config, readonly bool) *badger.DB { + opts := badger.DefaultOptions(conf.Path). + WithReadOnly(readonly) opts.Logger = nil opts.ValueLogFileSize = 10 << 20 // 10 Mo par fichier vlog opts.NumMemtables = 1 diff --git a/pkg/db/kv/kv_test.go b/pkg/db/kv/kv_test.go new file mode 100644 index 0000000..de4fd68 --- /dev/null +++ b/pkg/db/kv/kv_test.go @@ -0,0 +1,153 @@ +package kv + +import ( + "errors" + "testing" + + "github.com/dgraph-io/badger/v4" +) + +// newTestDB ouvre une base BadgerDB dans un répertoire temporaire. +// La base est fermée automatiquement en fin de test. +func newTestDB(t *testing.T) *badger.DB { + t.Helper() + db := InitDB(Config{Path: t.TempDir()}, false) + t.Cleanup(func() { db.Close() }) + return db +} + +// --- InitDB --- + +func TestInitDB_ValidPath(t *testing.T) { + db := newTestDB(t) + if db == nil { + t.Fatal("InitDB devrait retourner une DB non-nil") + } +} + +func TestInitDB_InvalidPath_Panics(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("InitDB avec un chemin invalide devrait paniquer") + } + }() + InitDB(Config{Path: "/chemin/inexistant/absolu"}, false) +} + +// --- AddInDB --- + +func TestAddInDB_NewKey(t *testing.T) { + db := newTestDB(t) + if err := AddInDB(db, "vpc/test", "valeur"); err != nil { + t.Fatalf("AddInDB a échoué : %v", err) + } +} + +func TestAddInDB_OverwriteExistingKey(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/test", "premiere") + if err := AddInDB(db, "vpc/test", "deuxieme"); err != nil { + t.Fatalf("AddInDB (écrasement) a échoué : %v", err) + } + + val, _ := GetFromDB(db, "vpc/test") + if val != "deuxieme" { + t.Errorf("valeur attendue %q, obtenu %q", "deuxieme", val) + } +} + +// --- GetFromDB --- + +func TestGetFromDB_ExistingKey(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/foo", "bar") + + val, err := GetFromDB(db, "vpc/foo") + if err != nil { + t.Fatalf("GetFromDB a échoué : %v", err) + } + if val != "bar" { + t.Errorf("valeur attendue %q, obtenu %q", "bar", val) + } +} + +func TestGetFromDB_MissingKey(t *testing.T) { + db := newTestDB(t) + + _, err := GetFromDB(db, "inexistant") + if !errors.Is(err, badger.ErrKeyNotFound) { + t.Errorf("erreur attendue ErrKeyNotFound, obtenu : %v", err) + } +} + +func TestGetFromDB_EmptyValue(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/vide", "") + + val, err := GetFromDB(db, "vpc/vide") + if err != nil { + t.Fatalf("GetFromDB a échoué : %v", err) + } + if val != "" { + t.Errorf("valeur attendue vide, obtenu %q", val) + } +} + +// --- DeleteInDB --- + +func TestDeleteInDB_SimpleKey(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/a", "v") + + if err := DeleteInDB(db, "vpc/a"); err != nil { + t.Fatalf("DeleteInDB a échoué : %v", err) + } + + _, err := GetFromDB(db, "vpc/a") + if !errors.Is(err, badger.ErrKeyNotFound) { + t.Errorf("la clé devrait être supprimée, obtenu : %v", err) + } +} + +func TestDeleteInDB_WithSubkeys(t *testing.T) { + db := newTestDB(t) + // Clé parente + sous-clés (préfixe "vpc/net1/") + AddInDB(db, "vpc/net1", "parent") + AddInDB(db, "vpc/net1/ip", "10.0.0.1") + AddInDB(db, "vpc/net1/gw", "10.0.0.254") + + if err := DeleteInDB(db, "vpc/net1"); err != nil { + t.Fatalf("DeleteInDB a échoué : %v", err) + } + + for _, key := range []string{"vpc/net1", "vpc/net1/ip", "vpc/net1/gw"} { + _, err := GetFromDB(db, key) + if !errors.Is(err, badger.ErrKeyNotFound) { + t.Errorf("clé %q devrait être supprimée, obtenu : %v", key, err) + } + } +} + +func TestDeleteInDB_DoesNotDeleteSiblings(t *testing.T) { + db := newTestDB(t) + AddInDB(db, "vpc/net1", "a") + AddInDB(db, "vpc/net2", "b") // ne doit pas être supprimée + + DeleteInDB(db, "vpc/net1") + + val, err := GetFromDB(db, "vpc/net2") + if err != nil { + t.Fatalf("vpc/net2 ne devrait pas être supprimée : %v", err) + } + if val != "b" { + t.Errorf("valeur attendue %q, obtenu %q", "b", val) + } +} + +func TestDeleteInDB_MissingKey(t *testing.T) { + db := newTestDB(t) + // Supprimer une clé inexistante ne doit pas crasher + if err := DeleteInDB(db, "inexistant"); err != nil { + t.Logf("DeleteInDB clé inexistante retourne : %v (non bloquant)", err) + } +} diff --git a/pkg/systemd/main.go b/pkg/systemd/main.go new file mode 100644 index 0000000..ea5fef0 --- /dev/null +++ b/pkg/systemd/main.go @@ -0,0 +1,109 @@ +package systemd + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/coreos/go-systemd/v22/dbus" +) + +const ( + defaultTimeout = 5 * time.Second + jobMode = "replace" +) + +type Manager struct { + conn *dbus.Conn +} + +type ServiceStatus struct { + Name string + LoadState string + ActiveState string + SubState string + MainPID uint32 +} + +// New crée une connexion D-Bus systemd (scope système) +func New() (*Manager, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + conn, err := dbus.NewSystemConnectionContext(ctx) + if err != nil { + return nil, err + } + + return &Manager{conn: conn}, nil +} + +// Close ferme la connexion D-Bus +func (m *Manager) Close() { + if m.conn != nil { + m.conn.Close() + } +} + +// Start démarre un service systemd +func (m *Manager) Start(service string) error { + return m.job("StartUnit", service) +} + +// Stop arrête un service systemd +func (m *Manager) Stop(service string) error { + return m.job("StopUnit", service) +} + +func (m *Manager) job(method, service string) error { + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + ch := make(chan string, 1) + + var err error + switch method { + case "StartUnit": + _, err = m.conn.StartUnitContext(ctx, service, jobMode, ch) + case "StopUnit": + _, err = m.conn.StopUnitContext(ctx, service, jobMode, ch) + default: + return errors.New("unsupported job method") + } + + if err != nil { + return err + } + + result := <-ch + if result != "done" { + return fmt.Errorf("%s %s failed: %s", method, service, result) + } + + return nil +} + +// Status retourne l’état courant du service +func (m *Manager) Status(service string) (*ServiceStatus, error) { + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() + + props, err := m.conn.GetUnitPropertiesContext(ctx, service) + if err != nil { + return nil, err + } + + status := &ServiceStatus{ + Name: service, + LoadState: props["LoadState"].(string), + ActiveState: props["ActiveState"].(string), + SubState: props["SubState"].(string), + } + + if pid, ok := props["MainPID"].(uint32); ok { + status.MainPID = pid + } + + return status, nil +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..7532b14 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,92 @@ +#!/bin/bash +set -e + +SED_PARAM="" +unameOut="$(uname -s)" +case "${unameOut}" in + Linux*) SED_PARAM=" -i ";; + Darwin*) SED_PARAM=" -i '' ";; + *) exit 1 +esac + +SCRIPT_PATH="scripts/deploy.sh" + +exec_with_dry_run () { + if [[ ${1} -eq ${FLAGS_TRUE} ]]; then + echo "# ${2}" + else + eval "${2}" 2> /tmp/error || \ + { + echo -e "failed with following error"; + output=$(cat /tmp/error | sed -e "s/^/ error -> /g"); + echo -e "${output}"; + return 1; + } + fi + return 0 +} + +check_latest_script () { + REMOTE_URL="${1}" + LOCAL_PATH="${2}" + + REMOTE=$(curl --silent "${REMOTE_URL}" | sha256sum) + LOCAL=$(cat ${LOCAL_PATH} | sha256sum) + + [[ "${REMOTE}" == "${LOCAL}" ]] || return 1 + return 0 +} + +download_binaries () { + DRY_RUN="${1}" + TAG="${2}" + GIT_SERVER="${3}" + REPO_PATH="${4}" + + #'.[0].assets.[].browser_download_url' + [[ "${TAG}" == "" ]] && TAG=$(curl --silent "${GIT_SERVER}api/v1/repos/${REPO_PATH}releases/?limit=1" | jq -r '.[0].tag_name') + echo "Deploy ${TAG} binaries" + + BIN_PATH="/opt/two/${TAG}/bin/" + LN_PATH="/opt/two/bin/" + + exec_with_dry_run "${DRY_RUN}" "mkdir -p \"${BIN_PATH}\"" + exec_with_dry_run "${DRY_RUN}" "mkdir -p \"${LN_PATH}\"" + + curl --silent "${GIT_SERVER}api/v1/repos/${REPO_PATH}releases/tags/${TAG}" | jq -c '.assets[]' | while read tmp + do + BINARY_NAME=$(echo "${tmp}" | jq -r '.name') + BINARY_SHORT_NAME=$(echo "${BINARY_NAME}" | cut -d_ -f 1) + BINARY_URL=$(echo "${tmp}" | jq -r '.browser_download_url') + exec_with_dry_run "${DRY_RUN}" "curl --silent '${BINARY_URL}' -o '${BIN_PATH}${BINARY_NAME}'" + exec_with_dry_run "${DRY_RUN}" "chmod +x '${BIN_PATH}${BINARY_NAME}'" + exec_with_dry_run "${DRY_RUN}" "rm -f '${LN_PATH}${BINARY_SHORT_NAME}'" + exec_with_dry_run "${DRY_RUN}" "ln -s '${BIN_PATH}${BINARY_NAME}' '${LN_PATH}${BINARY_SHORT_NAME}'" + done +} + +main () { + [[ -f ./libs/shflags ]] && . ./libs/shflags || eval "$(curl --silent https://git.g3e.fr/H6N/tools/raw/branch/main/libs/shflags)" + + DEFINE_boolean 'dryrun' false 'Enable dry-run mode' 'd' + DEFINE_boolean 'up_script' true 'Upgrade script' 's' + DEFINE_string 'git_server' 'https://git.g3e.fr/' 'Git Server' 'g' + DEFINE_string 'repo_path' 'syonad/two/' 'Path of repository' 'r' + DEFINE_string 'branch' 'main/' 'Branch name' 'b' + DEFINE_string 'tag' '' 'Tag name' 't' + + FLAGS "$@" || exit $? + eval set -- "${FLAGS_ARGV}" + + SCRIPT_URL="${FLAGS_git_server}${FLAGS_repo_path}raw/branch/${FLAGS_branch}${SCRIPT_PATH}" + check_latest_script "${SCRIPT_URL}" "${0}" || ( + [[ ${FLAGS_up_script} -eq ${FLAGS_TRUE} ]] && \ + exec_with_dry_run "${FLAGS_dryrun}" "curl --silent '${SCRIPT_URL}' -o '${0}'" + exit 1 + ) + + download_binaries "${FLAGS_dryrun}" "${FLAGS_tag}" "${FLAGS_git_server}" "${FLAGS_repo_path}" +} + +[[ "${BASH_SOURCE[0]}" == "${0}" ]] && (main "$@" || exit 1) +[[ "${BASH_SOURCE[0]}" == "" ]] && (main "$@" || exit 1) \ No newline at end of file diff --git a/scripts/run-dnsmasq-in-netns.sh b/scripts/run-dnsmasq-in-netns.sh new file mode 100644 index 0000000..c0f9253 --- /dev/null +++ b/scripts/run-dnsmasq-in-netns.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +# Expects one argument: netns_bridge (e.g. vpc-00003_br-00002 or vpc1_br0) +arg="$1" +NETNS="${arg%%_*}" +BRIDGE="${arg#*_}" + +echo "start dnsmasq ${NETNS} ${BRIDGE}" + +exec ip netns exec "${NETNS}" \ + dnsmasq \ + --no-daemon \ + --interface="${BRIDGE}" \ + --bind-interfaces \ + --pid-file="/run/dnsmasq-$arg.pid" \ + --conf-file="/etc/dnsmasq.d/$arg.conf" \ + --no-hosts \ + --no-resolv \ + --log-facility="/var/log/dnsmasq-$arg.log" \ + --no-daemon -p0 diff --git a/systemd/dnsmasq@.service b/systemd/dnsmasq@.service new file mode 100644 index 0000000..0f9d72e --- /dev/null +++ b/systemd/dnsmasq@.service @@ -0,0 +1,11 @@ +[Unit] +Description=dnsmasq in netns %i +After=network.target + +[Service] +Type=simple +ExecStart=/opt/two/bin/run-dnsmasq-in-netns.sh %i +ExecStopPost=/bin/rm -f /run/dnsmasq-%i.pid + +[Install] +WantedBy=multi-user.target diff --git a/systemd/metadata@.service b/systemd/metadata@.service new file mode 100644 index 0000000..626b97c --- /dev/null +++ b/systemd/metadata@.service @@ -0,0 +1,10 @@ +[Unit] +Description=metadata in netns %i +After=network.target + +[Service] +Type=simple +ExecStart=/opt/two/bin/metadata --vm %i + +[Install] +WantedBy=multi-user.target \ No newline at end of file