Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: don't panic when IPv6 is not supported #248

Merged
merged 7 commits into from
Jul 2, 2024
56 changes: 45 additions & 11 deletions cmd/microcloud/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,32 +625,62 @@ func (c *CmdControl) askRemotePool(systems map[string]InitSystem, autoSetup bool
return nil
}

func (c *CmdControl) askProceedIfNoOverlayNetwork() error {
proceedWithNoOverlayNetworking, err := c.asker.AskBool("FAN networking is not usable. Do you want to proceed with setting up an inoperable cluster? (yes/no) [default=no]: ", "no")
if err != nil {
return err
}

if proceedWithNoOverlayNetworking {
return nil
}

return fmt.Errorf("cluster bootstrapping aborted due to lack of usable networking")
}

func (c *CmdControl) askNetwork(sh *service.Handler, systems map[string]InitSystem, microCloudInternalSubnet *net.IPNet, autoSetup bool) error {
_, bootstrap := systems[sh.Name]
lxd := sh.Services[types.LXD].(*service.LXDService)
for peer, system := range systems {
if bootstrap {
system.TargetNetworks = []api.NetworksPost{lxd.DefaultPendingFanNetwork()}
if peer == sh.Name {
network, err := lxd.DefaultFanNetwork()
if err != nil {
return err
}

system.Networks = []api.NetworksPost{network}
// Check if FAN networking is usable.
fanUsable, _, err := lxd.FanNetworkUsable()
if err != nil {
return err
}

if fanUsable {
for peer, system := range systems {
if bootstrap {
system.TargetNetworks = []api.NetworksPost{lxd.DefaultPendingFanNetwork()}
if peer == sh.Name {
network, err := lxd.DefaultFanNetwork()
if err != nil {
return err
}

system.Networks = []api.NetworksPost{network}
}
}
}

systems[peer] = system
systems[peer] = system
}
}

// Automatic setup gets a basic fan setup.
if autoSetup {
if !fanUsable {
return c.askProceedIfNoOverlayNetwork()
}

return nil
}

// Environments without OVN get a basic fan setup.
if sh.Services[types.MicroOVN] == nil {
if !fanUsable {
return c.askProceedIfNoOverlayNetwork()
}

return nil
}

Expand Down Expand Up @@ -769,6 +799,10 @@ func (c *CmdControl) askNetwork(sh *service.Handler, systems map[string]InitSyst

if !canOVN {
fmt.Println("No dedicated uplink interfaces detected, skipping distributed networking")
if !fanUsable {
return c.askProceedIfNoOverlayNetwork()
}

return nil
}

Expand Down
13 changes: 6 additions & 7 deletions cmd/microcloud/main_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,13 @@ func lookupPeers(s *service.Handler, autoSetup bool, iface *net.Interface, subne
}()
}

var timeAfter <-chan time.Time
timeoutDuration := time.Minute
if autoSetup {
timeAfter = time.After(5 * time.Second)
timeoutDuration = 5 * time.Second
}

if len(expectedSystems) > 0 {
timeAfter = time.After(1 * time.Minute)
}
ctx, cancel := context.WithTimeout(context.Background(), timeoutDuration)
defer cancel()

expectedSystemsMap := make(map[string]bool, len(expectedSystems))
for _, system := range expectedSystems {
Expand All @@ -219,7 +218,7 @@ func lookupPeers(s *service.Handler, autoSetup bool, iface *net.Interface, subne
done := false
for !done {
select {
case <-timeAfter:
case <-ctx.Done():
done = true
case err := <-selectionCh:
if err != nil {
Expand All @@ -235,7 +234,7 @@ func lookupPeers(s *service.Handler, autoSetup bool, iface *net.Interface, subne
break
}

peers, err := mdns.LookupPeers(context.Background(), iface, mdns.Version, s.Name)
peers, err := mdns.LookupPeers(ctx, iface, mdns.Version, s.Name)
if err != nil {
return err
}
Expand Down
8 changes: 7 additions & 1 deletion cmd/microcloud/main_init_preseed.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,14 @@ func (p *Preseed) Parse(s *service.Handler, bootstrap bool) (map[string]InitSyst
systems[peer] = system
}

// Check if FAN networking is usable.
fanUsable, _, err := lxd.FanNetworkUsable()
if err != nil {
return nil, err
}

// Setup FAN network if OVN not available.
if len(ifaceByPeer) == 0 {
if len(ifaceByPeer) == 0 && fanUsable {
for peer, system := range systems {
if bootstrap {
system.TargetNetworks = append(system.TargetNetworks, lxd.DefaultPendingFanNetwork())
Expand Down
21 changes: 20 additions & 1 deletion mdns/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,26 @@ func Lookup(ctx context.Context, iface *net.Interface, service string, size int)
params.Interface = iface
params.Entries = entriesCh
params.Timeout = 100 * time.Millisecond
err := mdns.Query(params)
ipv4Supported, ipv6Supported, err := checkIPStatus(iface.Name)
if err != nil {
return nil, fmt.Errorf("Failed to check IP status: %w", err)
}

if !ipv4Supported {
logger.Info("IPv4 is not supported on this system network interface: disabling IPv4 mDNS", logger.Ctx{"iface": iface.Name})
params.DisableIPv4 = true
}

if !ipv6Supported {
logger.Info("IPv6 is not supported on this system network interface: disabling IPv6 mDNS", logger.Ctx{"iface": iface.Name})
params.DisableIPv6 = true
}

if params.DisableIPv4 && params.DisableIPv6 {
return nil, fmt.Errorf("No supported IP versions on the network interface %q", iface.Name)
}

err = mdns.Query(params)
if err != nil {
return nil, fmt.Errorf("Failed lookup: %w", err)
}
Expand Down
36 changes: 36 additions & 0 deletions mdns/mdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,39 @@ func dnsTXTSlice(list []byte) []string {

return parts
}

// checkIPStatus checks if the interface is up, has multicast, and supports IPv4/IPv6.
func checkIPStatus(iface string) (ipv4OK bool, ipv6OK bool, err error) {
netInterface, err := net.InterfaceByName(iface)
if err != nil {
return false, false, err
}

if netInterface.Flags&net.FlagUp != net.FlagUp {
return false, false, nil
}

if netInterface.Flags&net.FlagMulticast != net.FlagMulticast {
return false, false, nil
}

addrs, err := netInterface.Addrs()
if err != nil {
return false, false, err
}

for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}

if ipNet.IP.To4() != nil {
ipv4OK = true
} else if ipNet.IP.To16() != nil {
ipv6OK = true
}
}

return ipv4OK, ipv6OK, nil
}
29 changes: 4 additions & 25 deletions service/lxd.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package service

import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"

Expand Down Expand Up @@ -638,33 +636,14 @@ func (s *LXDService) waitReady(ctx context.Context, c lxd.InstanceServer, timeou
}

// defaultGatewaySubnetV4 returns subnet of default gateway interface.
func defaultGatewaySubnetV4() (*net.IPNet, string, error) {
file, err := os.Open("/proc/net/route")
func (s LXDService) defaultGatewaySubnetV4() (*net.IPNet, string, error) {
available, ifaceName, err := s.FanNetworkUsable()
if err != nil {
return nil, "", err
}

defer func() { _ = file.Close() }()

ifaceName := ""

scanner := bufio.NewReader(file)
for {
line, _, err := scanner.ReadLine()
if err != nil {
break
}

fields := strings.Fields(string(line))

if fields[1] == "00000000" && fields[7] == "00000000" {
ifaceName = fields[0]
break
}
}

if ifaceName == "" {
return nil, "", fmt.Errorf("No default gateway for IPv4")
if !available {
return nil, "", fmt.Errorf("No default IPv4 gateway available")
}

iface, err := net.InterfaceByName(ifaceName)
Expand Down
37 changes: 36 additions & 1 deletion service/lxd_config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package service

import (
"bufio"
"fmt"
"net"
"os"
"strconv"
"strings"

"github.com/canonical/lxd/shared/api"
)
Expand All @@ -23,10 +26,42 @@ func (s LXDService) DefaultPendingFanNetwork() api.NetworksPost {
return api.NetworksPost{Name: "lxdfan0", Type: "bridge"}
}

// FanNetworkUsable checks if the current host is capable of using a Fan network.
// It actually checks if there is a default IPv4 gateway available.
func (s LXDService) FanNetworkUsable() (available bool, ifaceName string, err error) {
file, err := os.Open("/proc/net/route")
if err != nil {
return false, "", err
}

defer func() { _ = file.Close() }()

scanner := bufio.NewReader(file)
for {
line, _, err := scanner.ReadLine()
if err != nil {
break
}

fields := strings.Fields(string(line))

if fields[1] == "00000000" && fields[7] == "00000000" {
ifaceName = fields[0]
break
}
}

if ifaceName == "" {
return false, "", nil // There is no default gateway for IPv4
}

return true, ifaceName, nil
}

// DefaultFanNetwork returns the default Ubuntu Fan network configuration when
// creating the finalized network.
func (s LXDService) DefaultFanNetwork() (api.NetworksPost, error) {
underlay, _, err := defaultGatewaySubnetV4()
underlay, _, err := s.defaultGatewaySubnetV4()
if err != nil {
return api.NetworksPost{}, fmt.Errorf("Could not determine Fan overlay subnet: %w", err)
}
Expand Down
42 changes: 25 additions & 17 deletions test/includes/microcloud.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ unset_interactive_vars() {
unset LOOKUP_IFACE LIMIT_SUBNET SKIP_SERVICE EXPECT_PEERS REUSE_EXISTING REUSE_EXISTING_COUNT \
SETUP_ZFS ZFS_FILTER ZFS_WIPE \
SETUP_CEPH CEPH_WARNING CEPH_FILTER CEPH_WIPE SETUP_CEPHFS CEPH_CLUSTER_NETWORK IGNORE_CEPH_NETWORKING \
SETUP_OVN OVN_WARNING OVN_FILTER IPV4_SUBNET IPV4_START IPV4_END DNS_ADDRESSES IPV6_SUBNET
PROCEED_WITH_NO_OVERLAY_NETWORKING SETUP_OVN OVN_WARNING OVN_FILTER IPV4_SUBNET IPV4_START IPV4_END DNS_ADDRESSES IPV6_SUBNET
}

# microcloud_interactive: outputs text that can be passed to `TEST_CONSOLE=1 microcloud init`
Expand All @@ -19,24 +19,25 @@ microcloud_interactive() {
EXPECT_PEERS=${EXPECT_PEERS:-} # wait for this number of systems to be available to join the cluster.
REUSE_EXISTING=${REUSE_EXISTING:-} # (yes/no) incorporate an existing clustered service.
REUSE_EXISTING_COUNT=${REUSE_EXISTING_COUNT:-0} # (number) number of existing clusters to incorporate.
SETUP_ZFS=${SETUP_ZFS:-} # (yes/no) input for initiating ZFS storage pool setup.
ZFS_FILTER=${ZFS_FILTER:-} # filter string for ZFS disks.
ZFS_WIPE=${ZFS_WIPE:-} # (yes/no) to wipe all disks.
SETUP_CEPH=${SETUP_CEPH:-} # (yes/no) input for initiating CEPH storage pool setup.
SETUP_CEPHFS=${SETUP_CEPHFS:-} # (yes/no) input for initialising CephFS storage pool setup.
CEPH_WARNING=${CEPH_WARNING:-} # (yes/no) input for warning about eligible disk detection.
CEPH_FILTER=${CEPH_FILTER:-} # filter string for CEPH disks.
CEPH_WIPE=${CEPH_WIPE:-} # (yes/no) to wipe all disks.
SETUP_ZFS=${SETUP_ZFS:-} # (yes/no) input for initiating ZFS storage pool setup.
ZFS_FILTER=${ZFS_FILTER:-} # filter string for ZFS disks.
ZFS_WIPE=${ZFS_WIPE:-} # (yes/no) to wipe all disks.
SETUP_CEPH=${SETUP_CEPH:-} # (yes/no) input for initiating CEPH storage pool setup.
SETUP_CEPHFS=${SETUP_CEPHFS:-} # (yes/no) input for initialising CephFS storage pool setup.
CEPH_WARNING=${CEPH_WARNING:-} # (yes/no) input for warning about eligible disk detection.
CEPH_FILTER=${CEPH_FILTER:-} # filter string for CEPH disks.
CEPH_WIPE=${CEPH_WIPE:-} # (yes/no) to wipe all disks.
CEPH_CLUSTER_NETWORK=${CEPH_CLUSTER_NETWORK:-} # (default: MicroCloud internal subnet or Ceph public network if specified previously) input for setting up a cluster network.
IGNORE_CEPH_NETWORKING=${IGNORE_CEPH_NETWORKING:-} # (yes/no) input for ignoring Ceph network setup. Set it to `yes` during `microcloud add` .
SETUP_OVN=${SETUP_OVN:-} # (yes/no) input for initiating OVN network setup.
OVN_WARNING=${OVN_WARNING:-} # (yes/no) input for warning about eligible interface detection.
OVN_FILTER=${OVN_FILTER:-} # filter string for OVN interfaces.
IPV4_SUBNET=${IPV4_SUBNET:-} # OVN ipv4 gateway subnet.
IPV4_START=${IPV4_START:-} # OVN ipv4 range start.
IPV4_END=${IPV4_END:-} # OVN ipv4 range end.
DNS_ADDRESSES=${DNS_ADDRESSES:-} # OVN custom DNS addresses.
IPV6_SUBNET=${IPV6_SUBNET:-} # OVN ipv6 range.
PROCEED_WITH_NO_OVERLAY_NETWORKING=${PROCEED_WITH_NO_OVERLAY_NETWORKING:-} # (yes/no) input for proceeding without overlay networking.
SETUP_OVN=${SETUP_OVN:-} # (yes/no) input for initiating OVN network setup.
OVN_WARNING=${OVN_WARNING:-} # (yes/no) input for warning about eligible interface detection.
OVN_FILTER=${OVN_FILTER:-} # filter string for OVN interfaces.
IPV4_SUBNET=${IPV4_SUBNET:-} # OVN ipv4 gateway subnet.
IPV4_START=${IPV4_START:-} # OVN ipv4 range start.
IPV4_END=${IPV4_END:-} # OVN ipv4 range end.
DNS_ADDRESSES=${DNS_ADDRESSES:-} # OVN custom DNS addresses.
IPV6_SUBNET=${IPV6_SUBNET:-} # OVN ipv6 range.

setup="
${LOOKUP_IFACE} # filter the lookup interface
Expand Down Expand Up @@ -88,6 +89,13 @@ $(true) # workaround for set -e
"
fi

if [ -n "${PROCEED_WITH_NO_OVERLAY_NETWORKING}" ]; then
setup="${setup}
${PROCEED_WITH_NO_OVERLAY_NETWORKING} # agree to proceed without overlay networking (neither FAN nor OVN networking) (yes/no)
$(true) # workaround for set -e
"
fi

if [ -z "${IGNORE_CEPH_NETWORKING}" ]; then
if [ -n "${CEPH_CLUSTER_NETWORK}" ]; then
setup="${setup}
Expand Down
Loading
Loading