Add nanoswitch and cluster testing
Adds nanoswitch and the `switched-multi2` launch target to launch two Smalltown instances on a switched
network and enroll them into a single cluster. Nanoswitch contains a Linux bridge and a minimal DHCP server
and connects to the two Smalltown instances over virtual Ethernet cables. Also moves out the DHCP client into
a package since nanoswitch needs it.
Test Plan:
Manually tested using `bazel run //:launch -- switched-multi2` and observing that the second VM
(whose serial port is mapped to stdout) prints that it is enrolled. Also validated by `bazel run //core/cmd/dbg -- kubectl get node -o wide` returning two ready nodes.
X-Origin-Diff: phab/D572
GitOrigin-RevId: 9f6e2b3d8268749dd81588205646ae3976ad14b3
diff --git a/core/internal/network/BUILD.bazel b/core/internal/network/BUILD.bazel
index 7e45086..9eefc1b 100644
--- a/core/internal/network/BUILD.bazel
+++ b/core/internal/network/BUILD.bazel
@@ -2,16 +2,12 @@
go_library(
name = "go_default_library",
- srcs = [
- "dhcp.go",
- "main.go",
- ],
+ srcs = ["main.go"],
importpath = "git.monogon.dev/source/nexantic.git/core/internal/network",
visibility = ["//:__subpackages__"],
deps = [
"//core/internal/common/supervisor:go_default_library",
- "@com_github_insomniacslk_dhcp//dhcpv4:go_default_library",
- "@com_github_insomniacslk_dhcp//dhcpv4/nclient4:go_default_library",
+ "//core/internal/network/dhcp:go_default_library",
"@com_github_vishvananda_netlink//:go_default_library",
"@org_golang_x_sys//unix:go_default_library",
"@org_uber_go_zap//:go_default_library",
diff --git a/core/internal/network/dhcp/BUILD.bazel b/core/internal/network/dhcp/BUILD.bazel
new file mode 100644
index 0000000..40ac372
--- /dev/null
+++ b/core/internal/network/dhcp/BUILD.bazel
@@ -0,0 +1,15 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["dhcp.go"],
+ importpath = "git.monogon.dev/source/nexantic.git/core/internal/network/dhcp",
+ visibility = ["//core:__subpackages__"],
+ deps = [
+ "//core/internal/common/supervisor:go_default_library",
+ "@com_github_insomniacslk_dhcp//dhcpv4:go_default_library",
+ "@com_github_insomniacslk_dhcp//dhcpv4/nclient4:go_default_library",
+ "@com_github_vishvananda_netlink//:go_default_library",
+ "@org_uber_go_zap//:go_default_library",
+ ],
+)
diff --git a/core/internal/network/dhcp.go b/core/internal/network/dhcp/dhcp.go
similarity index 70%
rename from core/internal/network/dhcp.go
rename to core/internal/network/dhcp/dhcp.go
index 983c25c..0eef2cc 100644
--- a/core/internal/network/dhcp.go
+++ b/core/internal/network/dhcp/dhcp.go
@@ -14,7 +14,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package network
+package dhcp
import (
"context"
@@ -29,48 +29,48 @@
"go.uber.org/zap"
)
-type dhcpClient struct {
+type Client struct {
reqC chan *dhcpStatusReq
}
-func newDHCPClient() *dhcpClient {
- return &dhcpClient{
+func New() *Client {
+ return &Client{
reqC: make(chan *dhcpStatusReq),
}
}
type dhcpStatusReq struct {
- resC chan *dhcpStatus
+ resC chan *Status
wait bool
}
-func (r *dhcpStatusReq) fulfill(s *dhcpStatus) {
+func (r *dhcpStatusReq) fulfill(s *Status) {
go func() {
r.resC <- s
}()
}
-// dhcpStatus is the IPv4 configuration provisioned via DHCP for a given interface. It does not necessarily represent
+// Status is the IPv4 configuration provisioned via DHCP for a given interface. It does not necessarily represent
// a configuration that is active or even valid.
-type dhcpStatus struct {
- // address is 'our' (the node's) IPv4 address on the network.
- address net.IPNet
- // gateway is the default gateway/router of this network, or 0.0.0.0 if none was given.
- gateway net.IP
- // dns is a list of IPv4 DNS servers to use.
- dns []net.IP
+type Status struct {
+ // Address is 'our' (the node's) IPv4 Address on the network.
+ Address net.IPNet
+ // Gateway is the default Gateway/router of this network, or 0.0.0.0 if none was given.
+ Gateway net.IP
+ // DNS is a list of IPv4 DNS servers to use.
+ DNS []net.IP
}
-func (s *dhcpStatus) String() string {
- return fmt.Sprintf("Address: %s, Gateway: %s, DNS: %v", s.address.String(), s.gateway.String(), s.dns)
+func (s *Status) String() string {
+ return fmt.Sprintf("Address: %s, Gateway: %s, DNS: %v", s.Address.String(), s.Gateway.String(), s.DNS)
}
-func (c *dhcpClient) run(iface netlink.Link) supervisor.Runnable {
+func (c *Client) Run(iface netlink.Link) supervisor.Runnable {
return func(ctx context.Context) error {
logger := supervisor.Logger(ctx)
- // Channel updated with address once one gets assigned/updated
- newC := make(chan *dhcpStatus)
+ // Channel updated with Address once one gets assigned/updated
+ newC := make(chan *Status)
// Status requests waiting for configuration
waiters := []*dhcpStatusReq{}
@@ -106,7 +106,7 @@
// We start at WAITING, once we get a current config we move to ASSIGNED
// Once this becomes more complex (ie. has to handle link state changes)
// this should grow into a real state machine.
- var current *dhcpStatus
+ var current *Status
logger.Info("DHCP client WAITING")
for {
select {
@@ -133,28 +133,28 @@
}
}
-// parseAck turns an internal status (from the dhcpv4 library) into a dhcpStatus
-func parseAck(ack *dhcpv4.DHCPv4) *dhcpStatus {
+// parseAck turns an internal Status (from the dhcpv4 library) into a Status
+func parseAck(ack *dhcpv4.DHCPv4) *Status {
address := net.IPNet{IP: ack.YourIPAddr, Mask: ack.SubnetMask()}
- // DHCP routers are optional - if none are provided, assume no router and set gateway to 0.0.0.0
- // (this makes gateway.IsUnspecified() == true)
+ // DHCP routers are optional - if none are provided, assume no router and set Gateway to 0.0.0.0
+ // (this makes Gateway.IsUnspecified() == true)
gateway, _, _ := net.ParseCIDR("0.0.0.0/0")
if routers := ack.Router(); len(routers) > 0 {
gateway = routers[0]
}
- return &dhcpStatus{
- address: address,
- gateway: gateway,
- dns: ack.DNS(),
+ return &Status{
+ Address: address,
+ Gateway: gateway,
+ DNS: ack.DNS(),
}
}
-// status returns the DHCP configuration requested from us by the local DHCP server.
-// If wait is true, this function will block until a DHCP configuration is available. Otherwise, a nil status may be
+// Status returns the DHCP configuration requested from us by the local DHCP server.
+// If wait is true, this function will block until a DHCP configuration is available. Otherwise, a nil Status may be
// returned to indicate that no configuration has been received yet.
-func (c *dhcpClient) status(ctx context.Context, wait bool) (*dhcpStatus, error) {
- resC := make(chan *dhcpStatus)
+func (c *Client) Status(ctx context.Context, wait bool) (*Status, error) {
+ resC := make(chan *Status)
c.reqC <- &dhcpStatusReq{
resC: resC,
wait: wait,
diff --git a/core/internal/network/main.go b/core/internal/network/main.go
index 00d7fb2..2466e05 100644
--- a/core/internal/network/main.go
+++ b/core/internal/network/main.go
@@ -22,11 +22,12 @@
"net"
"os"
- "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
-
"github.com/vishvananda/netlink"
"go.uber.org/zap"
"golang.org/x/sys/unix"
+
+ "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
+ "git.monogon.dev/source/nexantic.git/core/internal/network/dhcp"
)
const (
@@ -36,7 +37,7 @@
type Service struct {
config Config
- dhcp *dhcpClient
+ dhcp *dhcp.Client
logger *zap.Logger
}
@@ -47,7 +48,7 @@
func New(config Config) *Service {
return &Service{
config: config,
- dhcp: newDHCPClient(),
+ dhcp: dhcp.New(),
}
}
@@ -76,7 +77,7 @@
func (s *Service) addNetworkRoutes(link netlink.Link, addr net.IPNet, gw net.IP) error {
if err := netlink.AddrReplace(link, &netlink.Addr{IPNet: &addr}); err != nil {
- return err
+ return fmt.Errorf("failed to add DHCP address to network interface \"%v\": %w", link.Attrs().Name, err)
}
if gw.IsUnspecified() {
@@ -96,20 +97,20 @@
}
func (s *Service) useInterface(ctx context.Context, iface netlink.Link) error {
- err := supervisor.Run(ctx, "dhcp", s.dhcp.run(iface))
+ err := supervisor.Run(ctx, "dhcp", s.dhcp.Run(iface))
if err != nil {
return err
}
- status, err := s.dhcp.status(ctx, true)
+ status, err := s.dhcp.Status(ctx, true)
if err != nil {
- return fmt.Errorf("could not get DHCP status: %w", err)
+ return fmt.Errorf("could not get DHCP Status: %w", err)
}
- if err := setResolvconf(status.dns, []string{}); err != nil {
+ if err := setResolvconf(status.DNS, []string{}); err != nil {
s.logger.Warn("failed to set resolvconf", zap.Error(err))
}
- if err := s.addNetworkRoutes(iface, net.IPNet{IP: status.address.IP, Mask: status.address.Mask}, status.gateway); err != nil {
+ if err := s.addNetworkRoutes(iface, status.Address, status.Gateway); err != nil {
s.logger.Warn("failed to add routes", zap.Error(err))
}
@@ -118,11 +119,11 @@
// GetIP returns the current IP (and optionally waits for one to be assigned)
func (s *Service) GetIP(ctx context.Context, wait bool) (*net.IP, error) {
- status, err := s.dhcp.status(ctx, wait)
+ status, err := s.dhcp.Status(ctx, wait)
if err != nil {
return nil, err
}
- return &status.address.IP, nil
+ return &status.Address.IP, nil
}
func (s *Service) Run(ctx context.Context) error {