Skip to content

Commit

Permalink
Change match load to always redo AP and switch configs even if teams …
Browse files Browse the repository at this point in the history
…are the same (closes #94).
  • Loading branch information
patfair committed Oct 29, 2023
1 parent 6171f0d commit 888b8d4
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 122 deletions.
6 changes: 0 additions & 6 deletions network/access_point.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
131 changes: 47 additions & 84 deletions network/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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.
Expand All @@ -48,6 +48,7 @@ func NewSwitch(address, password string) *Switch {
port: switchTelnetPort,
password: password,
configBackoffDuration: switchConfigBackoffDurationSec * time.Second,
configPauseDuration: switchConfigPauseDurationSec * time.Second,
}
}

Expand All @@ -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
}
Expand All @@ -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) {
Expand Down
118 changes: 86 additions & 32 deletions network/switch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 888b8d4

Please sign in to comment.