Skip to content

Commit

Permalink
Single node support (#319)
Browse files Browse the repository at this point in the history
Adds basic single-node support.

This adds an initial question to `microcloud init` asking if the user
wants to set up just one node.
```
Do you want to set up more than one cluster member? (yes/no) [default=yes]: 
```

If the user enters `no`, the system will follow the ordinary setup with
ZFS and OVN (or optionally a FAN network instead), but the user will not
be prompted for a lookup subnet and will not be presented a list of
systems.

MicroCeph will be skipped as we require 3 systems. 

The cluster can be grown after this point by using `microcloud add`.

TODO:

- [x] Add tests
- [x] Support preseed
- [x] Support adding missing services (maybe, if it makes sense)
- [x] Test MicroOVN functions properly with 1 node.
  • Loading branch information
masnax authored Aug 29, 2024
2 parents 8ef99d3 + 347160f commit 8fe8668
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 41 deletions.
1 change: 1 addition & 0 deletions cmd/microcloud/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func (c *cmdAdd) Run(cmd *cobra.Command, args []string) error {

cfg := initConfig{
bootstrap: false,
setupMany: true,
autoSetup: c.flagAutoSetup,
wipeAllDisks: c.flagWipe,
common: c.common,
Expand Down
6 changes: 5 additions & 1 deletion cmd/microcloud/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (c *initConfig) askAddress() error {
return fmt.Errorf("Cloud not find valid subnet for address %q", listenAddr)
}

if !c.autoSetup {
if !c.autoSetup && c.setupMany {
filter, err := c.asker.AskBool(fmt.Sprintf("Limit search for other MicroCloud servers to %s? (yes/no) [default=yes]: ", subnet.String()), "yes")
if err != nil {
return err
Expand Down Expand Up @@ -1296,6 +1296,10 @@ func (c *initConfig) askCephNetwork(sh *service.Handler) error {
// In auto setup, we will expect no initialized services so that we can be opinionated about how we configure the cluster without user input.
// This works by deleting the record for the service from the `service.Handler`, thus ignoring it for the remainder of the setup.
func (c *initConfig) askClustered(s *service.Handler, expectedServices []types.ServiceType) error {
if !c.setupMany {
return nil
}

for _, serviceType := range expectedServices {
for name, info := range c.state {
_, newSystem := c.systems[name]
Expand Down
15 changes: 15 additions & 0 deletions cmd/microcloud/main_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ type initConfig struct {
// autoSetup indicates whether questions should automatically choose defaults.
autoSetup bool

// setupMany indicates whether we are setting up remote nodes concurrently, or just a single cluster member.
setupMany bool

// lookupTimeout is the duration to wait for mDNS records to appear during system lookup.
lookupTimeout time.Duration

Expand Down Expand Up @@ -139,6 +142,7 @@ func (c *cmdInit) Run(cmd *cobra.Command, args []string) error {

cfg := initConfig{
bootstrap: true,
setupMany: true,
address: c.flagAddress,
autoSetup: c.flagAutoSetup,
wipeAllDisks: c.flagWipeAllDisks,
Expand Down Expand Up @@ -176,6 +180,13 @@ func (c *initConfig) RunInteractive(cmd *cobra.Command, args []string) error {
return err
}

if !c.autoSetup {
c.setupMany, err = c.common.asker.AskBool("Do you want to set up more than one cluster member? (yes/no) [default=yes]: ", "yes")
if err != nil {
return err
}
}

err = c.askAddress()
if err != nil {
return err
Expand Down Expand Up @@ -283,6 +294,10 @@ func (c *initConfig) RunInteractive(cmd *cobra.Command, args []string) error {
// - `expectedSystems` is a list of expected hostnames. If given, the behaviour is similar to `autoSetup`,
// except it will wait up to a minute for exclusively these systems to be recorded.
func (c *initConfig) lookupPeers(s *service.Handler, expectedSystems []string) error {
if !c.setupMany {
return nil
}

header := []string{"NAME", "IFACE", "ADDR"}
var table *SelectableTable
var answers []string
Expand Down
56 changes: 20 additions & 36 deletions cmd/microcloud/main_init_preseed.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,6 @@ func (p *Preseed) validate(name string, bootstrap bool) error {
return fmt.Errorf("No systems given")
}

if bootstrap && len(p.Systems) < 2 {
return fmt.Errorf("At least 2 systems are required to set up MicroCloud")
}

for _, system := range p.Systems {
if system.Name == "" {
return fmt.Errorf("Missing system name")
Expand Down Expand Up @@ -446,9 +442,11 @@ func (p *Preseed) Parse(s *service.Handler, c *initConfig) (map[string]InitSyste
return nil, fmt.Errorf("Failed to find lookup interface %q", p.LookupInterface)
}

err = c.lookupPeers(s, expectedSystems)
if err != nil {
return nil, err
if len(expectedSystems) > 0 {
err = c.lookupPeers(s, expectedSystems)
if err != nil {
return nil, err
}
}

expectedServices := make(map[types.ServiceType]service.Service, len(s.Services))
Expand Down Expand Up @@ -492,6 +490,11 @@ func (p *Preseed) Parse(s *service.Handler, c *initConfig) (map[string]InitSyste
}
}

localInfo, err := s.CollectSystemInformation(context.Background(), mdns.ServerInfo{Name: c.name, Address: c.address})
if err != nil {
return nil, err
}

// If an uplink interface was explicitly chosen, we will try to set up an OVN network.
explicitOVN := len(ifaceByPeer) > 0

Expand All @@ -503,10 +506,12 @@ func (p *Preseed) Parse(s *service.Handler, c *initConfig) (map[string]InitSyste
}

// Take the first alphabetical interface for each system's uplink network.
for k := range uplinkIfaces {
currentIface := ifaceByPeer[system.ServerInfo.Name]
if k < currentIface || currentIface == "" {
ifaceByPeer[system.ServerInfo.Name] = k
if !explicitOVN {
for k := range uplinkIfaces {
currentIface := ifaceByPeer[system.ServerInfo.Name]
if k < currentIface || currentIface == "" {
ifaceByPeer[system.ServerInfo.Name] = k
}
}
}

Expand All @@ -520,30 +525,8 @@ func (p *Preseed) Parse(s *service.Handler, c *initConfig) (map[string]InitSyste
}

// If we have specified any part of OVN config, implicitly assume we want to set it up.
usingOVN := p.OVN.IPv4Gateway != "" || p.OVN.IPv6Gateway != "" || explicitOVN
if usingOVN {
// Only select systems not explicitly picked above.
infos := make([]mdns.ServerInfo, 0, len(c.systems))
for peer, system := range c.systems {
if ifaceByPeer[peer] == "" {
infos = append(infos, system.ServerInfo)
}
}

for _, info := range infos {
ifaces, _, _, err := lxd.GetNetworkInterfaces(context.Background(), info.Name, info.Address, info.AuthSecret)
if err != nil {
return nil, err
}

for k := range ifaces {
ifaceByPeer[info.Name] = k
break
}
}
}

// Setup FAN network if OVN not available.
hasOVN, _ := localInfo.SupportsOVNNetwork()
usingOVN := p.OVN.IPv4Gateway != "" || p.OVN.IPv6Gateway != "" || explicitOVN || hasOVN
if usingOVN {
for peer, iface := range ifaceByPeer {
system := c.systems[peer]
Expand Down Expand Up @@ -863,7 +846,8 @@ func (p *Preseed) Parse(s *service.Handler, c *initConfig) (map[string]InitSyste
return nil, fmt.Errorf("Failed to find at least 1 disk on each machine for local storage pool configuration")
}

if len(cephMatches)+len(directCephMatches) > 0 && p.Storage.CephFS {
hasCephFS, _ := localInfo.SupportsRemoteFSPool()
if (len(cephMatches)+len(directCephMatches) > 0 && p.Storage.CephFS) || hasCephFS {
for name, system := range c.systems {
if c.bootstrap {
system.TargetStoragePools = append(system.TargetStoragePools, lxd.DefaultPendingCephFSStoragePool())
Expand Down
4 changes: 2 additions & 2 deletions cmd/microcloud/preseed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (s *preseedSuite) Test_preseedValidateInvalid() {
err: errors.New("No systems given"),
},
{
desc: "Not enough systems",
desc: "Single node preseed",
subnet: "10.0.0.1/24",
iface: "enp5s0",
systems: []System{{Name: "n1", UplinkInterface: "eth0", Storage: InitStorage{}}},
Expand All @@ -55,7 +55,7 @@ func (s *preseedSuite) Test_preseedValidateInvalid() {
},

addErr: false,
err: errors.New("At least 2 systems are required to set up MicroCloud"),
err: nil,
},
{
desc: "Missing lookup subnet",
Expand Down
1 change: 1 addition & 0 deletions cmd/microcloud/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func (c *cmdServiceAdd) Run(cmd *cobra.Command, args []string) error {
cfg := initConfig{
// Set bootstrap to true because we are setting up a new cluster for new services.
bootstrap: true,
setupMany: true,
common: c.common,
asker: &c.common.asker,
systems: map[string]InitSystem{},
Expand Down
12 changes: 10 additions & 2 deletions test/includes/microcloud.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ unset_interactive_vars() {
SETUP_ZFS ZFS_FILTER ZFS_WIPE \
SETUP_CEPH CEPH_MISSING_DISKS CEPH_FILTER CEPH_WIPE CEPH_ENCRYPT SETUP_CEPHFS CEPH_CLUSTER_NETWORK \
PROCEED_WITH_NO_OVERLAY_NETWORKING SETUP_OVN OVN_WARNING OVN_FILTER IPV4_SUBNET IPV4_START IPV4_END DNS_ADDRESSES IPV6_SUBNET \
REPLACE_PROFILE CEPH_RETRY_HA
REPLACE_PROFILE CEPH_RETRY_HA MULTI_NODE
}

# microcloud_interactive: outputs text that can be passed to `TEST_CONSOLE=1 microcloud init`
# to simulate terminal input to the interactive CLI.
# The lines that are output are based on the values passed to the listed environment variables.
# Any unset variables will be omitted.
microcloud_interactive() {
MULTI_NODE=${MULTI_NODE:-} # (yes/no) whether to set up multiple nodes
SKIP_LOOKUP=${SKIP_LOOKUP:-} # whether or not to skip the whole lookup block in the interactive command list.
LOOKUP_IFACE=${LOOKUP_IFACE:-} # filter string for the lookup interface table.
LIMIT_SUBNET=${LIMIT_SUBNET:-} # (yes/no) input for limiting lookup of systems to the above subnet.
Expand Down Expand Up @@ -44,11 +45,18 @@ microcloud_interactive() {
REPLACE_PROFILE="${REPLACE_PROFILE:-}" # Replace default profile config and devices.

setup=""
if ! [ "${SKIP_LOOKUP}" = 1 ]; then
if [ -n "${MULTI_NODE}" ]; then
setup="
${MULTI_NODE} # lookup multiple nodes
${LOOKUP_IFACE} # filter the lookup interface
$([ -n "${LOOKUP_IFACE}" ] && printf "select") # select the interface
$([ -n "${LOOKUP_IFACE}" ] && printf -- "---")
$(true)
"
fi

if ! [ "${SKIP_LOOKUP}" = 1 ]; then
setup="${setup}
${LIMIT_SUBNET} # limit lookup subnet (yes/no)
$([ "${SKIP_SERVICE}" = "yes" ] && printf "%s" "${SKIP_SERVICE}") # skip MicroOVN/MicroCeph (yes/no)
expect ${EXPECT_PEERS} # wait until the systems show up
Expand Down
1 change: 1 addition & 0 deletions test/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ run_instances_tests() {

run_basic_tests() {
run_test test_reuse_cluster "reuse_cluster"
run_test test_add_services "add_services"
run_test test_auto "auto"
run_test test_remove_cluster_member "remove_cluster_member"
run_test test_non_ha "non_ha"
Expand Down
2 changes: 2 additions & 0 deletions test/suites/add.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ test_add_interactive() {

echo "Test growing a MicroCloud with all services and devices set up"
unset_interactive_vars
export MULTI_NODE="yes"
export LOOKUP_IFACE="enp5s0"
export LIMIT_SUBNET="yes"
export EXPECT_PEERS=2
Expand Down Expand Up @@ -200,6 +201,7 @@ test_add_interactive() {
reset_systems 4 2 1
echo "Test growing a MicroCloud when storage & networks were not already set up"
unset_interactive_vars
export MULTI_NODE="yes"
export LOOKUP_IFACE="enp5s0"
export LIMIT_SUBNET="yes"
export EXPECT_PEERS=2
Expand Down
Loading

0 comments on commit 8fe8668

Please sign in to comment.