From 888b8d468b5a66a709748cc197eb8a36ee2bbc94 Mon Sep 17 00:00:00 2001 From: Patrick Fairbank Date: Sun, 29 Oct 2023 09:42:19 -0700 Subject: [PATCH] Change match load to always redo AP and switch configs even if teams are the same (closes #94). --- network/access_point.go | 6 -- network/switch.go | 131 ++++++++++++++-------------------------- network/switch_test.go | 118 ++++++++++++++++++++++++++---------- 3 files changed, 133 insertions(+), 122 deletions(-) diff --git a/network/access_point.go b/network/access_point.go index 92dc91c2..f448259f 100644 --- a/network/access_point.go +++ b/network/access_point.go @@ -130,12 +130,6 @@ func (ap *AccessPoint) handleTeamWifiConfiguration(teams [6]*model.Team) { return } - err := ap.updateTeamWifiStatuses() - if err == nil && ap.configIsCorrectForTeams(teams) { - log.Printf("WiFi configuration is already correct; skipping configuration cycle.") - return - } - if !ap.isVividType { // Clear the state of the radio before loading teams; the Linksys AP is crash-prone otherwise. ap.configureTeams([6]*model.Team{nil, nil, nil, nil, nil, nil}) diff --git a/network/switch.go b/network/switch.go index 59385483..37411a21 100644 --- a/network/switch.go +++ b/network/switch.go @@ -11,14 +11,13 @@ import ( "fmt" "github.com/Team254/cheesy-arena/model" "net" - "regexp" - "strconv" "sync" "time" ) const ( switchConfigBackoffDurationSec = 5 + switchConfigPauseDurationSec = 2 switchTeamGatewayAddress = 4 switchTelnetPort = 23 ) @@ -38,6 +37,7 @@ type Switch struct { password string mutex sync.Mutex configBackoffDuration time.Duration + configPauseDuration time.Duration } var ServerIpAddress = "10.0.100.5" // The DS will try to connect to this address only. @@ -48,6 +48,7 @@ func NewSwitch(address, password string) *Switch { port: switchTelnetPort, password: password, configBackoffDuration: switchConfigBackoffDurationSec * time.Second, + configPauseDuration: switchConfigPauseDurationSec * time.Second, } } @@ -57,66 +58,58 @@ func (sw *Switch) ConfigureTeamEthernet(teams [6]*model.Team) error { sw.mutex.Lock() defer sw.mutex.Unlock() - // Determine what new team VLANs are needed and build the commands to set them up. - oldTeamVlans, err := sw.getTeamVlans() + // Remove old team VLANs to reset the switch state. + removeTeamVlansCommand := "" + for vlan := 10; vlan <= 60; vlan += 10 { + removeTeamVlansCommand += fmt.Sprintf( + "interface Vlan%d\nno ip address\nno access-list 1%d\nno ip dhcp pool dhcp%d\n", vlan, vlan, vlan, + ) + } + _, err := sw.runConfigCommand(removeTeamVlansCommand) if err != nil { return err } + time.Sleep(sw.configPauseDuration) + + // Create the new team VLANs. addTeamVlansCommand := "" - replaceTeamVlan := func(team *model.Team, vlan int) { + addTeamVlan := func(team *model.Team, vlan int) { if team == nil { return } - if oldTeamVlans[team.Id] == vlan { - delete(oldTeamVlans, team.Id) - } else { - teamPartialIp := fmt.Sprintf("%d.%d", team.Id/100, team.Id%100) - addTeamVlansCommand += fmt.Sprintf( - "ip dhcp excluded-address 10.%s.1 10.%s.100\n"+ - "no ip dhcp pool dhcp%d\n"+ - "ip dhcp pool dhcp%d\n"+ - "network 10.%s.0 255.255.255.0\n"+ - "default-router 10.%s.%d\n"+ - "lease 7\n"+ - "no access-list 1%d\n"+ - "access-list 1%d permit ip 10.%s.0 0.0.0.255 host %s\n"+ - "access-list 1%d permit udp any eq bootpc any eq bootps\n"+ - "interface Vlan%d\nip address 10.%s.%d 255.255.255.0\n", - teamPartialIp, - teamPartialIp, - vlan, - vlan, - teamPartialIp, - teamPartialIp, - switchTeamGatewayAddress, - vlan, - vlan, - teamPartialIp, - ServerIpAddress, - vlan, - vlan, - teamPartialIp, - switchTeamGatewayAddress, - ) - } + teamPartialIp := fmt.Sprintf("%d.%d", team.Id/100, team.Id%100) + addTeamVlansCommand += fmt.Sprintf( + "ip dhcp excluded-address 10.%s.1 10.%s.100\n"+ + "ip dhcp pool dhcp%d\n"+ + "network 10.%s.0 255.255.255.0\n"+ + "default-router 10.%s.%d\n"+ + "lease 7\n"+ + "access-list 1%d permit ip 10.%s.0 0.0.0.255 host %s\n"+ + "access-list 1%d permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan%d\nip address 10.%s.%d 255.255.255.0\n", + teamPartialIp, + teamPartialIp, + vlan, + teamPartialIp, + teamPartialIp, + switchTeamGatewayAddress, + vlan, + teamPartialIp, + ServerIpAddress, + vlan, + vlan, + teamPartialIp, + switchTeamGatewayAddress, + ) } - replaceTeamVlan(teams[0], red1Vlan) - replaceTeamVlan(teams[1], red2Vlan) - replaceTeamVlan(teams[2], red3Vlan) - replaceTeamVlan(teams[3], blue1Vlan) - replaceTeamVlan(teams[4], blue2Vlan) - replaceTeamVlan(teams[5], blue3Vlan) - - // Build the command to remove the team VLANs that are no longer needed. - removeTeamVlansCommand := "" - for _, vlan := range oldTeamVlans { - removeTeamVlansCommand += fmt.Sprintf("interface Vlan%d\nno ip address\nno access-list 1%d\n", vlan, vlan) - } - - // Build and run the overall command to do everything in a single telnet session. - command := removeTeamVlansCommand + addTeamVlansCommand - if len(command) > 0 { - _, err = sw.runConfigCommand(removeTeamVlansCommand + addTeamVlansCommand) + addTeamVlan(teams[0], red1Vlan) + addTeamVlan(teams[1], red2Vlan) + addTeamVlan(teams[2], red3Vlan) + addTeamVlan(teams[3], blue1Vlan) + addTeamVlan(teams[4], blue2Vlan) + addTeamVlan(teams[5], blue3Vlan) + if len(addTeamVlansCommand) > 0 { + _, err = sw.runConfigCommand(addTeamVlansCommand) if err != nil { return err } @@ -128,36 +121,6 @@ func (sw *Switch) ConfigureTeamEthernet(teams [6]*model.Team) error { return nil } -// Returns a map of currently-configured teams to VLANs. -func (sw *Switch) getTeamVlans() (map[int]int, error) { - // Get the entire config dump. - config, err := sw.runCommand("show running-config\n") - if err != nil { - return nil, err - } - - // Parse out the team IDs and VLANs from the config dump. - re := regexp.MustCompile( - fmt.Sprintf("(?s)interface Vlan(\\d\\d)\\s+ip address 10\\.(\\d+)\\.(\\d+)\\.%d", switchTeamGatewayAddress), - ) - teamVlanMatches := re.FindAllStringSubmatch(config, -1) - if teamVlanMatches == nil { - // There are probably no teams currently configured. - return nil, nil - } - - // Build the map of team to VLAN. - teamVlans := make(map[int]int) - for _, match := range teamVlanMatches { - team100s, _ := strconv.Atoi(match[2]) - team1s, _ := strconv.Atoi(match[3]) - team := int(team100s)*100 + team1s - vlan, _ := strconv.Atoi(match[1]) - teamVlans[team] = vlan - } - return teamVlans, nil -} - // Logs into the switch via Telnet and runs the given command in user exec mode. Reads the output and // returns it as a string. func (sw *Switch) runCommand(command string) (string, error) { diff --git a/network/switch_test.go b/network/switch_test.go index bcadab29..4e8a97c1 100644 --- a/network/switch_test.go +++ b/network/switch_test.go @@ -17,56 +17,110 @@ func TestConfigureSwitch(t *testing.T) { sw := NewSwitch("127.0.0.1", "password") sw.port = 9050 sw.configBackoffDuration = time.Millisecond - var command string + sw.configPauseDuration = time.Millisecond + var command1, command2 string + expectedResetCommand := "password\nenable\npassword\nterminal length 0\nconfig terminal\n" + + "interface Vlan10\nno ip address\nno access-list 110\nno ip dhcp pool dhcp10\n" + + "interface Vlan20\nno ip address\nno access-list 120\nno ip dhcp pool dhcp20\n" + + "interface Vlan30\nno ip address\nno access-list 130\nno ip dhcp pool dhcp30\n" + + "interface Vlan40\nno ip address\nno access-list 140\nno ip dhcp pool dhcp40\n" + + "interface Vlan50\nno ip address\nno access-list 150\nno ip dhcp pool dhcp50\n" + + "interface Vlan60\nno ip address\nno access-list 160\nno ip dhcp pool dhcp60\n" + + "end\ncopy running-config startup-config\n\nexit\n" - // Should do nothing if current configuration is blank. - mockTelnet(t, sw.port, "", &command) + // Should remove all previous VLANs and do nothing else if current configuration is blank. + mockTelnet(t, sw.port, &command1, &command2) assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, nil, nil, nil, nil, nil})) - assert.Equal(t, "", command) + assert.Equal(t, expectedResetCommand, command1) + assert.Equal(t, "", command2) - // Should remove any existing teams but not other SSIDs. + // Should configure one team if only one is present. sw.port += 1 - mockTelnet(t, sw.port, - "interface Vlan100\nip address 10.0.100.2\ninterface Vlan50\nip address 10.2.54.4\n", &command) - assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, nil, nil, nil, nil, nil})) - assert.Equal(t, "password\nenable\npassword\nterminal length 0\nconfig terminal\ninterface Vlan50\nno ip"+ - " address\nno access-list 150\nend\ncopy running-config startup-config\n\nexit\n", command) + mockTelnet(t, sw.port, &command1, &command2) + assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, nil, nil, nil, {Id: 254}, nil})) + assert.Equal(t, expectedResetCommand, command1) + assert.Equal( + t, + "password\nenable\npassword\nterminal length 0\nconfig terminal\n"+ + "ip dhcp excluded-address 10.2.54.1 10.2.54.100\nip dhcp pool dhcp50\n"+ + "network 10.2.54.0 255.255.255.0\ndefault-router 10.2.54.4\nlease 7\n"+ + "access-list 150 permit ip 10.2.54.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 150 permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan50\nip address 10.2.54.4 255.255.255.0\n"+ + "end\ncopy running-config startup-config\n\nexit\n", + command2, + ) - // Should configure new teams and leave existing ones alone if still needed. + // Should configure all teams if all are present. sw.port += 1 - mockTelnet(t, sw.port, "interface Vlan50\nip address 10.2.54.4\n", &command) - assert.Nil(t, sw.ConfigureTeamEthernet([6]*model.Team{nil, &model.Team{Id: 1114}, nil, nil, &model.Team{Id: 254}, - nil})) - assert.Equal(t, "password\nenable\npassword\nterminal length 0\nconfig terminal\n"+ - "ip dhcp excluded-address 10.11.14.1 10.11.14.100\nno ip dhcp pool dhcp20\nip dhcp pool dhcp20\n"+ - "network 10.11.14.0 255.255.255.0\ndefault-router 10.11.14.4\nlease 7\nno access-list 120\n"+ - "access-list 120 permit ip 10.11.14.0 0.0.0.255 host 10.0.100.5\n"+ - "access-list 120 permit udp any eq bootpc any eq bootps\ninterface Vlan20\n"+ - "ip address 10.11.14.4 255.255.255.0\nend\ncopy running-config startup-config\n\nexit\n", command) + mockTelnet(t, sw.port, &command1, &command2) + assert.Nil( + t, + sw.ConfigureTeamEthernet([6]*model.Team{{Id: 1114}, {Id: 254}, {Id: 296}, {Id: 1503}, {Id: 1678}, {Id: 1538}}), + ) + assert.Equal(t, expectedResetCommand, command1) + assert.Equal( + t, + "password\nenable\npassword\nterminal length 0\nconfig terminal\n"+ + "ip dhcp excluded-address 10.11.14.1 10.11.14.100\nip dhcp pool dhcp10\n"+ + "network 10.11.14.0 255.255.255.0\ndefault-router 10.11.14.4\nlease 7\n"+ + "access-list 110 permit ip 10.11.14.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 110 permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan10\nip address 10.11.14.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.2.54.1 10.2.54.100\nip dhcp pool dhcp20\n"+ + "network 10.2.54.0 255.255.255.0\ndefault-router 10.2.54.4\nlease 7\n"+ + "access-list 120 permit ip 10.2.54.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 120 permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan20\nip address 10.2.54.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.2.96.1 10.2.96.100\nip dhcp pool dhcp30\n"+ + "network 10.2.96.0 255.255.255.0\ndefault-router 10.2.96.4\nlease 7\n"+ + "access-list 130 permit ip 10.2.96.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 130 permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan30\nip address 10.2.96.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.15.3.1 10.15.3.100\nip dhcp pool dhcp40\n"+ + "network 10.15.3.0 255.255.255.0\ndefault-router 10.15.3.4\nlease 7\n"+ + "access-list 140 permit ip 10.15.3.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 140 permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan40\nip address 10.15.3.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.16.78.1 10.16.78.100\nip dhcp pool dhcp50\n"+ + "network 10.16.78.0 255.255.255.0\ndefault-router 10.16.78.4\nlease 7\n"+ + "access-list 150 permit ip 10.16.78.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 150 permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan50\nip address 10.16.78.4 255.255.255.0\n"+ + "ip dhcp excluded-address 10.15.38.1 10.15.38.100\nip dhcp pool dhcp60\n"+ + "network 10.15.38.0 255.255.255.0\ndefault-router 10.15.38.4\nlease 7\n"+ + "access-list 160 permit ip 10.15.38.0 0.0.0.255 host 10.0.100.5\n"+ + "access-list 160 permit udp any eq bootpc any eq bootps\n"+ + "interface Vlan60\nip address 10.15.38.4 255.255.255.0\n"+ + "end\ncopy running-config startup-config\n\nexit\n", + command2, + ) } -func mockTelnet(t *testing.T, port int, response string, command *string) { +func mockTelnet(t *testing.T, port int, command1 *string, command2 *string) { go func() { - // Fake the first connection which should just get the configuration. ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) assert.Nil(t, err) defer ln.Close() - conn, err := ln.Accept() + *command1 = "" + *command2 = "" + + // Fake the first connection. + conn1, err := ln.Accept() assert.Nil(t, err) - conn.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) + conn1.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) var reader bytes.Buffer - reader.ReadFrom(conn) - assert.Contains(t, reader.String(), "terminal length 0\nshow running-config\nexit\n") - conn.Write([]byte(response)) - conn.Close() + reader.ReadFrom(conn1) + *command1 = reader.String() + conn1.Close() - // Fake the second connection which should configure stuff. + // Fake the second connection. conn2, err := ln.Accept() assert.Nil(t, err) conn2.SetReadDeadline(time.Now().Add(10 * time.Millisecond)) - var reader2 bytes.Buffer - reader2.ReadFrom(conn2) - *command = reader2.String() + reader.Reset() + reader.ReadFrom(conn2) + *command2 = reader.String() conn2.Close() }() time.Sleep(100 * time.Millisecond) // Give it some time to open the socket.