diff --git a/microcloud/cmd/microcloud/main_init.go b/microcloud/cmd/microcloud/main_init.go index fe1c23395..2d3cc958f 100644 --- a/microcloud/cmd/microcloud/main_init.go +++ b/microcloud/cmd/microcloud/main_init.go @@ -141,6 +141,11 @@ func (c *cmdInit) RunInteractive(cmd *cobra.Command, args []string) error { return err } + err = validateSystems(s, systems) + if err != nil { + return err + } + err = setupCluster(s, systems) if err != nil { return err @@ -414,6 +419,82 @@ func AddPeers(sh *service.Handler, systems map[string]InitSystem) error { return nil } +func ensureSystemsOutsideGateway(gateway string, localAddr string, systems map[string]InitSystem) error { + _, ip4Net, err := net.ParseCIDR(gateway) + if err != nil { + return fmt.Errorf("Invalid ipv4.gateway for UPLINK: %w", err) + } + + localAddrIP := net.ParseIP(localAddr) + if localAddrIP == nil { + return fmt.Errorf("Invalid local address %q", localAddr) + } + + if ip4Net.Contains(localAddrIP) { + return fmt.Errorf("UPLINK ipv4.gateway must not include local address %q", localAddr) + } + + for _, system := range systems { + systemAddr := net.ParseIP(system.ServerInfo.Address) + if systemAddr == nil { + return fmt.Errorf("Invalid address %q for system %q", system.ServerInfo.Address, system.ServerInfo.Name) + } + + if ip4Net.Contains(systemAddr) { + return fmt.Errorf("UPLINK ipv4.gateway must not include system address %q", systemAddr) + } + } + + return nil +} + +func validateSystems(s *service.Handler, systems map[string]InitSystem) error { + _, bootstrap := systems[s.Name] + if !bootstrap { + return nil + } + + for _, curSystem := range systems { + for _, network := range curSystem.Networks { + if network.Type != "physical" || network.Name != "UPLINK" { + continue + } + + ip4Gateway, hasIP4Gateway := network.Config["ipv4.gateway"] + ip6Gateway, hasIP6Gateway := network.Config["ipv6.gateway"] + ovnRanges, hasOVNRanges := network.Config["ipv4.ovn.ranges"] + + if hasIP4Gateway { + err := ensureSystemsOutsideGateway(ip4Gateway, s.Address, systems) + if err != nil { + return err + } + } + + if hasIP6Gateway { + err := ensureSystemsOutsideGateway(ip6Gateway, s.Address, systems) + if err != nil { + return err + } + } + + if hasIP4Gateway && hasOVNRanges { + _, ip4GatewayNet, err := net.ParseCIDR(ip4Gateway) + if err != nil { + return fmt.Errorf("Invalid ipv4.gateway %q: %w", ip4Gateway, err) + } + + _, err = shared.ParseIPRanges(ovnRanges, ip4GatewayNet) + if err != nil { + return fmt.Errorf("Invalid ipv4.ovn.ranges %q: %w", ovnRanges, err) + } + } + } + } + + return nil +} + // setupCluster Bootstraps the cluster if necessary, adds all peers to the cluster, and completes any post cluster // configuration. func setupCluster(s *service.Handler, systems map[string]InitSystem) error { diff --git a/microcloud/cmd/microcloud/main_init_preseed.go b/microcloud/cmd/microcloud/main_init_preseed.go index 8397f2b6c..ab90a40e7 100644 --- a/microcloud/cmd/microcloud/main_init_preseed.go +++ b/microcloud/cmd/microcloud/main_init_preseed.go @@ -159,6 +159,11 @@ func (c *CmdControl) RunPreseed(cmd *cobra.Command, init bool) error { } } + err = validateSystems(s, systems) + if err != nil { + return err + } + return setupCluster(s, systems) } diff --git a/microcloud/cmd/microcloud/main_init_test.go b/microcloud/cmd/microcloud/main_init_test.go new file mode 100644 index 000000000..9129302f7 --- /dev/null +++ b/microcloud/cmd/microcloud/main_init_test.go @@ -0,0 +1,189 @@ +package main + +import ( + "testing" + + lxdAPI "github.com/canonical/lxd/shared/api" + + "github.com/canonical/microcloud/microcloud/mdns" + "github.com/canonical/microcloud/microcloud/service" +) + +func newSystemWithNetworks(systemName string, networks []lxdAPI.NetworksPost) InitSystem { + return InitSystem{ + ServerInfo: mdns.ServerInfo{ + Name: systemName, + Address: "192.168.1.28", + }, + Networks: networks, + } +} + +func newSystemWithUplinkNetConfig(systemName string, config map[string]string) InitSystem { + return newSystemWithNetworks(systemName, []lxdAPI.NetworksPost{{ + Name: "UPLINK", + Type: "physical", + NetworkPut: lxdAPI.NetworkPut{ + Config: config, + }, + }}) +} + +func newTestHandler(addr string, t *testing.T) *service.Handler { + handler, err := service.NewHandler("testSystem", addr, "/tmp/microcloud_test_hander", true, true) + if err != nil { + t.Fatalf("Failed to create test service handler: %s", err) + } + + return handler +} + +func newTestSystemsMap(systems ...InitSystem) map[string]InitSystem { + systemsMap := map[string]InitSystem{ + // The handler must have the same name as one of the systems in order + // for validateSystems to perform validation + // (we must be "bootstrapping a cluster") + "testSystem": newSystemWithNetworks("testSystem", []lxdAPI.NetworksPost{}), + } + + for _, system := range systems { + systemsMap[system.ServerInfo.Name] = system + } + + return systemsMap +} + +func ensureValidateSystemsPasses(handler *service.Handler, testSystems []InitSystem, t *testing.T) { + for _, system := range testSystems { + systems := newTestSystemsMap(system) + + err := validateSystems(handler, systems) + if err != nil { + t.Fatalf("Valid system %q failed validate: %s", system.ServerInfo.Name, err) + } + } +} + +func ensureValidateSystemsFails(handler *service.Handler, testSystems []InitSystem, t *testing.T) { + for _, system := range testSystems { + systems := newTestSystemsMap(system) + + err := validateSystems(handler, systems) + if err == nil { + t.Fatalf("Invalid system %q passed validation", system.ServerInfo.Name) + } + } +} + +func TestValidateSystemsIP6(t *testing.T) { + handler := newTestHandler("fc00:feed:beef::bed1", t) + + validSystems := []InitSystem{ + newSystemWithUplinkNetConfig("64Net", map[string]string{ + "ipv6.gateway": "fc00:bad:feed::1/64", + }), + } + + ensureValidateSystemsPasses(handler, validSystems, t) + + invalidSystems := []InitSystem{ + newSystemWithUplinkNetConfig("uplinkInsideManagement6Net", map[string]string{ + "ipv6.gateway": "fc00:feed:beef::1/64", + }), + } + + ensureValidateSystemsFails(handler, invalidSystems, t) +} + +func TestValidateSystemsIP4(t *testing.T) { + handler := newTestHandler("192.168.1.27", t) + + validSystems := []InitSystem{ + newSystemWithUplinkNetConfig("plainGateway", map[string]string{ + "ipv4.gateway": "10.234.0.1/16", + }), + newSystemWithUplinkNetConfig("16Net", map[string]string{ + "ipv4.gateway": "10.42.0.1/16", + "ipv4.ovn.ranges": "10.42.1.1-10.42.5.255", + }), + newSystemWithUplinkNetConfig("24Net", map[string]string{ + "ipv4.gateway": "190.168.4.1/24", + "ipv4.ovn.ranges": "190.168.4.50-190.168.4.60", + }), + } + + ensureValidateSystemsPasses(handler, validSystems, t) + + invalidSystems := []InitSystem{ + //"gatewayNotCIDR": newSystemWithUplinkNetwork(map[string]string{ + // "ipv4.gateway": "192.168.1.1", + //}), + newSystemWithUplinkNetConfig("backwardsRange", map[string]string{ + "ipv4.gateway": "10.42.0.1/16", + "ipv4.ovn.ranges": "10.42.5.255-10.42.1.1", + }), + newSystemWithUplinkNetConfig("rangesOutsideGateway", map[string]string{ + "ipv4.gateway": "10.1.1.0/24", + "ipv4.ovn.ranges": "10.2.2.50-10.2.2.100", + }), + newSystemWithUplinkNetConfig("uplinkInsideManagementNet", map[string]string{ + "ipv4.gateway": "192.168.1.1/24", + "ipv4.ovn.ranges": "192.168.1.50-192.168.1.200", + }), + newSystemWithUplinkNetConfig("uplinkInsideManagementNetNoRange", map[string]string{ + "ipv4.gateway": "192.168.1.1/16", + }), + } + + ensureValidateSystemsFails(handler, invalidSystems, t) +} + +func TestValidateSystemsMultiSystem(t *testing.T) { + handler := newTestHandler("10.23.1.72", t) + + sys1 := newSystemWithUplinkNetConfig("sys1", map[string]string{ + "ipv4.gateway": "10.100.1.1/16", + }) + + sys2 := newSystemWithNetworks("sys2", []lxdAPI.NetworksPost{ + { + Name: "default", + Type: "ovn", + NetworkPut: lxdAPI.NetworkPut{ + Config: map[string]string{ + "ipv4.address": "10.100.20.1/24", + }, + }, + }, + }) + + systems := newTestSystemsMap(sys1, sys2) + + err := validateSystems(handler, systems) + if err != nil { + t.Fatalf("InitSystems with conflicting uplink and default networks passed validation") + } + + sys3 := newSystemWithUplinkNetConfig("sys3", map[string]string{ + "ipv6.gateway": "fc00:bad:feed::1/64", + }) + + sys4 := newSystemWithNetworks("sys4", []lxdAPI.NetworksPost{ + { + Name: "default", + Type: "ovn", + NetworkPut: lxdAPI.NetworkPut{ + Config: map[string]string{ + "ipv6.address": "fc00:bad:feed::1/32", + }, + }, + }, + }) + + systems = newTestSystemsMap(sys3, sys4) + + err = validateSystems(handler, systems) + if err != nil { + t.Fatalf("InitSystems with conflicting uplink and default networks passed validation") + } +}