Added automatic configuration of Cisco Aironet AP.

This commit is contained in:
Patrick Fairbank
2014-08-15 22:47:12 -07:00
parent 35d6cc7e47
commit 792881919a
13 changed files with 566 additions and 2 deletions

149
aironet.go Normal file
View File

@@ -0,0 +1,149 @@
// Copyright 2014 Team 254. All Rights Reserved.
// Author: pat@patfairbank.com (Patrick Fairbank)
//
// Methods for configuring a Cisco Aironet AP1252AG access point for team SSIDs and VLANs.
package main
import (
"bufio"
"bytes"
"fmt"
"net"
"regexp"
"strconv"
"sync"
)
const aironetTelnetPort = 23
const (
red1Vlan = 11
red2Vlan = 12
red3Vlan = 13
blue1Vlan = 14
blue2Vlan = 15
blue3Vlan = 16
)
var aironetMutex sync.Mutex
// Sets up wireless networks for the given set of teams.
func ConfigureTeamWifi(red1, red2, red3, blue1, blue2, blue3 *Team) error {
for _, team := range []*Team{red1, red2, red3, blue1, blue2, blue3} {
if team != nil && (len(team.WpaKey) < 8 || len(team.WpaKey) > 63) {
return fmt.Errorf("Invalid WPA key '%s' configured for team %d.", team.WpaKey, team.Id)
}
}
// Determine what new SSIDs are needed and build the commands to set them up.
oldSsids, err := getSsids()
if err != nil {
return err
}
addSsidsCommand := ""
associateSsidsCommand := ""
replaceSsid := func(team *Team, vlan int) {
if team == nil {
return
}
if oldSsids[strconv.Itoa(team.Id)] == vlan {
delete(oldSsids, strconv.Itoa(team.Id))
} else {
addSsidsCommand += fmt.Sprintf("dot11 ssid %d\nvlan %d\nauthentication open\nauthentication "+
"key-management wpa version 2\nmbssid guest-mode\nwpa-psk ascii %s\n", team.Id, vlan, team.WpaKey)
associateSsidsCommand += fmt.Sprintf("ssid %d\n", team.Id)
}
}
replaceSsid(red1, red1Vlan)
replaceSsid(red2, red2Vlan)
replaceSsid(red3, red3Vlan)
replaceSsid(blue1, blue1Vlan)
replaceSsid(blue2, blue2Vlan)
replaceSsid(blue3, blue3Vlan)
if len(addSsidsCommand) != 0 {
associateSsidsCommand = "interface Dot11Radio1\n" + associateSsidsCommand
}
// Build the command to remove the SSIDs that are no longer needed.
removeSsidsCommand := ""
for ssid, _ := range oldSsids {
removeSsidsCommand += fmt.Sprintf("no dot11 ssid %s\n", ssid)
}
command := removeSsidsCommand + addSsidsCommand + associateSsidsCommand
if len(command) > 0 {
_, err = runAironetConfigCommand(removeSsidsCommand + addSsidsCommand + associateSsidsCommand)
if err != nil {
return err
}
}
return nil
}
// Returns a map of currently-configured SSIDs to VLANs.
func getSsids() (map[string]int, error) {
// Get the entire config dump.
config, err := runAironetCommand("show running-config\n")
if err != nil {
return nil, err
}
// Parse out the SSIDs and VLANs from the config dump.
re := regexp.MustCompile("(?s)dot11 ssid (\\w+)\\s+vlan (\\d+)")
ssidMatches := re.FindAllStringSubmatch(config, -1)
if ssidMatches == nil {
// There are probably no SSIDs currently configured.
return nil, nil
}
// Build the map of SSID to VLAN.
ssids := make(map[string]int)
for _, match := range ssidMatches {
vlan, _ := strconv.Atoi(match[2])
ssids[match[1]] = vlan
}
return ssids, nil
}
// Logs into the Aironet via Telnet and runs the given command in user exec mode. Reads the output and returns
// it as a string.
func runAironetCommand(command string) (string, error) {
// Make sure multiple commands aren't being run at the same time.
aironetMutex.Lock()
defer aironetMutex.Unlock()
// Open a Telnet connection to the AP.
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", eventSettings.ApAddress, aironetTelnetPort))
if err != nil {
return "", err
}
defer conn.Close()
// Login to the AP, send the command, and log out all at once.
writer := bufio.NewWriter(conn)
_, err = writer.WriteString(fmt.Sprintf("%s\n%s\nterminal length 0\n%sexit\n", eventSettings.ApUsername,
eventSettings.ApPassword, command))
if err != nil {
return "", err
}
err = writer.Flush()
if err != nil {
return "", err
}
// Read the response.
var reader bytes.Buffer
_, err = reader.ReadFrom(conn)
if err != nil {
return "", err
}
return reader.String(), nil
}
// Logs into the Aironet via Telnet and runs the given command in global configuration mode. Reads the output
// and returns it as a string.
func runAironetConfigCommand(command string) (string, error) {
return runAironetCommand(fmt.Sprintf("config terminal\n%send\ncopy running-config startup-config\n\n",
command))
}

306
ap_config.txt Normal file
View File

@@ -0,0 +1,306 @@
!
! Last configuration change at 05:36:53 UTC Sat Aug 16 2014 by patfair
! NVRAM config last updated at 05:36:53 UTC Sat Aug 16 2014 by patfair
! NVRAM config last updated at 05:36:53 UTC Sat Aug 16 2014 by patfair
version 15.2
no service pad
service timestamps debug datetime msec
service timestamps log datetime msec
service password-encryption
!
hostname ChezyAP
!
logging rate-limit console 9
!
aaa new-model
!
!
aaa authentication login default local
aaa authorization exec default local
!
!
!
!
!
aaa session-id common
!
!
dot11 syslog
dot11 vlan-name Blue1 vlan 14
dot11 vlan-name Blue2 vlan 15
dot11 vlan-name Blue3 vlan 16
dot11 vlan-name CheesyArena vlan 2
dot11 vlan-name Red1 vlan 11
dot11 vlan-name Red2 vlan 12
dot11 vlan-name Red3 vlan 13
!
dot11 ssid 1
vlan 11
authentication open
authentication key-management wpa version 2
mbssid guest-mode
wpa-psk ascii 7 0257550A5A575E701D
!
dot11 ssid 2
vlan 12
authentication open
authentication key-management wpa version 2
mbssid guest-mode
wpa-psk ascii 7 06545D731E1C5B4B57
!
dot11 ssid 3
vlan 13
authentication open
authentication key-management wpa version 2
mbssid guest-mode
wpa-psk ascii 7 115A4A564441585F57
!
dot11 ssid 4
vlan 14
authentication open
authentication key-management wpa version 2
mbssid guest-mode
wpa-psk ascii 7 101A5D4D5143465F58
!
dot11 ssid 5
vlan 15
authentication open
authentication key-management wpa version 2
mbssid guest-mode
wpa-psk ascii 7 00514653510E5E535A
!
dot11 ssid 6
vlan 16
authentication open
authentication key-management wpa version 2
mbssid guest-mode
wpa-psk ascii 7 1441445D5A527C7D72
!
dot11 ssid Cheesy Arena
vlan 2
authentication open
authentication key-management wpa version 2
guest-mode
wpa-psk ascii 7 030D5704100A22435C071D0A1001
!
crypto pki token default removal timeout 0
!
!
username patfair privilege 15 password 7 0548545F70181C2F101343
!
!
bridge irb
!
!
interface Dot11Radio0
no ip address
no ip route-cache
!
encryption mode ciphers aes-ccm tkip
!
encryption vlan 2 mode ciphers aes-ccm tkip
!
ssid Cheesy Arena
!
antenna gain 0
station-role root
no dot11 extension aironet
bridge-group 1
bridge-group 1 subscriber-loop-control
bridge-group 1 spanning-disabled
bridge-group 1 block-unknown-source
no bridge-group 1 source-learning
no bridge-group 1 unicast-flooding
!
interface Dot11Radio0.2
encapsulation dot1Q 2
no ip route-cache
bridge-group 2
bridge-group 2 subscriber-loop-control
bridge-group 2 spanning-disabled
bridge-group 2 block-unknown-source
no bridge-group 2 source-learning
no bridge-group 2 unicast-flooding
!
interface Dot11Radio1
no ip address
no ip route-cache
!
encryption mode ciphers aes-ccm tkip
!
encryption vlan 11 mode ciphers aes-ccm tkip
!
encryption vlan 12 mode ciphers aes-ccm tkip
!
encryption vlan 13 mode ciphers aes-ccm tkip
!
encryption vlan 14 mode ciphers aes-ccm tkip
!
encryption vlan 15 mode ciphers aes-ccm tkip
!
encryption vlan 16 mode ciphers aes-ccm tkip
!
ssid 1
!
ssid 2
!
ssid 3
!
ssid 4
!
ssid 5
!
ssid 6
!
antenna gain 0
dfs band 3 block
mbssid
channel dfs
station-role root
no dot11 extension aironet
bridge-group 1
bridge-group 1 subscriber-loop-control
bridge-group 1 spanning-disabled
bridge-group 1 block-unknown-source
no bridge-group 1 source-learning
no bridge-group 1 unicast-flooding
!
interface Dot11Radio1.11
encapsulation dot1Q 11
no ip route-cache
bridge-group 11
bridge-group 11 subscriber-loop-control
bridge-group 11 spanning-disabled
bridge-group 11 block-unknown-source
no bridge-group 11 source-learning
no bridge-group 11 unicast-flooding
!
interface Dot11Radio1.12
encapsulation dot1Q 12
no ip route-cache
bridge-group 12
bridge-group 12 subscriber-loop-control
bridge-group 12 spanning-disabled
bridge-group 12 block-unknown-source
no bridge-group 12 source-learning
no bridge-group 12 unicast-flooding
!
interface Dot11Radio1.13
encapsulation dot1Q 13
no ip route-cache
bridge-group 13
bridge-group 13 subscriber-loop-control
bridge-group 13 spanning-disabled
bridge-group 13 block-unknown-source
no bridge-group 13 source-learning
no bridge-group 13 unicast-flooding
!
interface Dot11Radio1.14
encapsulation dot1Q 14
no ip route-cache
bridge-group 14
bridge-group 14 subscriber-loop-control
bridge-group 14 spanning-disabled
bridge-group 14 block-unknown-source
no bridge-group 14 source-learning
no bridge-group 14 unicast-flooding
!
interface Dot11Radio1.15
encapsulation dot1Q 15
no ip route-cache
bridge-group 15
bridge-group 15 subscriber-loop-control
bridge-group 15 spanning-disabled
bridge-group 15 block-unknown-source
no bridge-group 15 source-learning
no bridge-group 15 unicast-flooding
!
interface Dot11Radio1.16
encapsulation dot1Q 16
no ip route-cache
bridge-group 16
bridge-group 16 subscriber-loop-control
bridge-group 16 spanning-disabled
bridge-group 16 block-unknown-source
no bridge-group 16 source-learning
no bridge-group 16 unicast-flooding
!
interface GigabitEthernet0
no ip address
no ip route-cache
duplex auto
speed auto
bridge-group 1
bridge-group 1 spanning-disabled
no bridge-group 1 source-learning
!
interface GigabitEthernet0.2
encapsulation dot1Q 2
no ip route-cache
bridge-group 2
bridge-group 2 spanning-disabled
no bridge-group 2 source-learning
!
interface GigabitEthernet0.11
encapsulation dot1Q 11
no ip route-cache
bridge-group 11
bridge-group 11 spanning-disabled
no bridge-group 11 source-learning
!
interface GigabitEthernet0.12
encapsulation dot1Q 12
no ip route-cache
bridge-group 12
bridge-group 12 spanning-disabled
no bridge-group 12 source-learning
!
interface GigabitEthernet0.13
encapsulation dot1Q 13
no ip route-cache
bridge-group 13
bridge-group 13 spanning-disabled
no bridge-group 13 source-learning
!
interface GigabitEthernet0.14
encapsulation dot1Q 14
no ip route-cache
bridge-group 14
bridge-group 14 spanning-disabled
no bridge-group 14 source-learning
!
interface GigabitEthernet0.15
encapsulation dot1Q 15
no ip route-cache
bridge-group 15
bridge-group 15 spanning-disabled
no bridge-group 15 source-learning
!
interface GigabitEthernet0.16
encapsulation dot1Q 16
no ip route-cache
bridge-group 16
bridge-group 16 spanning-disabled
no bridge-group 16 source-learning
!
interface BVI1
ip address 10.0.0.60 255.0.0.0
no ip route-cache
!
ip default-gateway 10.0.0.1
ip http server
ip http authentication aaa
no ip http secure-server
ip http help-path http://www.cisco.com/warp/public/779/smbiz/prodconfig/help/eag
!
bridge 1 route ip
!
!
!
line con 0
line vty 0 4
transport input all
!
sntp server 216.66.0.142
end

View File

@@ -197,6 +197,8 @@ func (arena *Arena) LoadMatch(match *Match) error {
return err
}
arena.SetupNetwork()
// Reset the realtime scores.
arena.redRealtimeScore = new(RealtimeScore)
arena.blueRealtimeScore = new(RealtimeScore)
@@ -256,10 +258,25 @@ func (arena *Arena) SubstituteTeam(teamId int, station string) error {
case "B3":
arena.currentMatch.Blue3 = teamId
}
arena.SetupNetwork()
arena.matchLoadTeamsNotifier.Notify(nil)
return nil
}
// Asynchronously reconfigures the networking hardware for the new set of teams.
func (arena *Arena) SetupNetwork() {
if eventSettings.NetworkSecurityEnabled {
go func() {
err := ConfigureTeamWifi(arena.AllianceStations["R1"].team, arena.AllianceStations["R2"].team,
arena.AllianceStations["R3"].team, arena.AllianceStations["B1"].team,
arena.AllianceStations["B2"].team, arena.AllianceStations["B3"].team)
if err != nil {
log.Printf("Failed to configure team WiFi: %s", err.Error())
}
}()
}
}
// Returns nil if the match can be started, and an error otherwise.
func (arena *Arena) CheckCanStartMatch() error {
if arena.MatchState != PRE_MATCH {

View File

@@ -8,7 +8,8 @@ CREATE TABLE teams (
country VARCHAR(255),
rookieyear int,
robotname VARCHAR(255),
accomplishments VARCHAR(1000)
accomplishments VARCHAR(1000),
wpakey VARCHAR(16)
);
-- +goose Down

View File

@@ -10,7 +10,11 @@ CREATE TABLE event_settings (
tbapublishingenabled bool,
tbaeventcode VARCHAR(16),
tbasecretid VARCHAR(255),
tbasecret VARCHAR(255)
tbasecret VARCHAR(255),
networksecurityenabled bool,
apaddress VARCHAR(255),
apusername VARCHAR(255),
appassword VARCHAR(255)
);
-- +goose Down

View File

@@ -17,6 +17,10 @@ type EventSettings struct {
TbaEventCode string
TbaSecretId string
TbaSecret string
NetworkSecurityEnabled bool
ApAddress string
ApUsername string
ApPassword string
}
const eventSettingsId = 0

View File

@@ -45,6 +45,10 @@ func SettingsPostHandler(w http.ResponseWriter, r *http.Request) {
eventSettings.TbaEventCode = r.PostFormValue("tbaEventCode")
eventSettings.TbaSecretId = r.PostFormValue("tbaSecretId")
eventSettings.TbaSecret = r.PostFormValue("tbaSecret")
eventSettings.NetworkSecurityEnabled = r.PostFormValue("networkSecurityEnabled") == "on"
eventSettings.ApAddress = r.PostFormValue("apAddress")
eventSettings.ApUsername = r.PostFormValue("apUsername")
eventSettings.ApPassword = r.PostFormValue("apPassword")
err := db.SaveEventSettings(eventSettings)
if err != nil {
handleWebErr(w, err)

View File

@@ -8,6 +8,7 @@ package main
import (
"encoding/csv"
"fmt"
"github.com/dchest/uniuri"
"github.com/gorilla/mux"
"html"
"html/template"
@@ -19,6 +20,8 @@ import (
"strings"
)
const wpaKeyLength = 8
var officialTeamInfoUrl = "https://my.usfirst.org/frc/scoring/index.lasso?page=teamlist"
var officialTeamInfo map[int][]string
@@ -124,6 +127,13 @@ func TeamEditPostHandler(w http.ResponseWriter, r *http.Request) {
team.RookieYear, _ = strconv.Atoi(r.PostFormValue("rookieYear"))
team.RobotName = r.PostFormValue("robotName")
team.Accomplishments = r.PostFormValue("accomplishments")
if eventSettings.NetworkSecurityEnabled {
team.WpaKey = r.PostFormValue("wpaKey")
if len(team.WpaKey) < 8 || len(team.WpaKey) > 63 {
handleWebErr(w, fmt.Errorf("WPA key must be between 8 and 63 characters."))
return
}
}
err = db.SaveTeam(team)
if err != nil {
handleWebErr(w, err)
@@ -168,6 +178,28 @@ func TeamsPublishHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/setup/teams", 302)
}
// Generates random WPA keys and saves them to the team models.
func TeamsGenerateWpaKeysHandler(w http.ResponseWriter, r *http.Request) {
generateAllKeys := false
if all, ok := r.URL.Query()["all"]; ok {
generateAllKeys = all[0] == "true"
}
teams, err := db.GetAllTeams()
if err != nil {
handleWebErr(w, err)
return
}
for _, team := range teams {
if len(team.WpaKey) == 0 || generateAllKeys {
team.WpaKey = uniuri.NewLen(wpaKeyLength)
db.SaveTeam(&team)
}
}
http.Redirect(w, r, "/setup/teams", 302)
}
func renderTeams(w http.ResponseWriter, r *http.Request, showErrorMessage bool) {
teams, err := db.GetAllTeams()
if err != nil {

View File

@@ -15,6 +15,7 @@ type Team struct {
RookieYear int
RobotName string
Accomplishments string
WpaKey string
}
func (database *Database) CreateTeam(team *Team) error {

View File

@@ -54,6 +54,14 @@
<textarea class="form-control" rows="5" name="accomplishments">{{.Team.Accomplishments}}</textarea>
</div>
</div>
{{if .EventSettings.NetworkSecurityEnabled}}
<div class="form-group">
<label class="col-lg-3 control-label">WPA Key</label>
<div class="col-lg-9">
<input type="text" class="form-control" name="wpaKey" value="{{.Team.WpaKey}}">
</div>
</div>
{{end}}
<div class="form-group">
<div class="col-lg-9 col-lg-offset-3">
<a href="/setup/teams"><button type="button" class="btn btn-default">Cancel</button></a>

View File

@@ -113,6 +113,35 @@
</div>
</div>
</fieldset>
<fieldset>
<legend>Hardware</legend>
<p>Enable this setting if you have a Cisco Aironet AP1252AG access point and Catalyst 3500-series
switch available, for isolating each team to its own SSID and VLAN.</p>
<div class="form-group">
<label class="col-lg-7 control-label">Enable advanced network security</label>
<div class="col-lg-1 checkbox">
<input type="checkbox" name="networkSecurityEnabled"{{if .NetworkSecurityEnabled}} checked{{end}}>
</div>
</div>
<div class="form-group">
<label class="col-lg-5 control-label">AP Address</label>
<div class="col-lg-7">
<input type="text" class="form-control" name="apAddress" value="{{.ApAddress}}">
</div>
</div>
<div class="form-group">
<label class="col-lg-5 control-label">AP Username</label>
<div class="col-lg-7">
<input type="text" class="form-control" name="apUsername" value="{{.ApUsername}}">
</div>
</div>
<div class="form-group">
<label class="col-lg-5 control-label">AP Password</label>
<div class="col-lg-7">
<input type="password" class="form-control" name="apPassword" value="{{.ApPassword}}">
</div>
</div>
</fieldset>
<div class="form-group">
<div class="col-lg-7 col-lg-offset-5">
<button type="submit" class="btn btn-info">Save</button>

View File

@@ -31,6 +31,14 @@
</button>
</div>
{{end}}
{{if .EventSettings.NetworkSecurityEnabled}}
<div class="form-group">
<a href="/setup/teams/generate_wpa_keys?all=true" class="btn btn-primary">Generate All WPA Keys</a>
</div>
<div class="form-group">
<a href="/setup/teams/generate_wpa_keys?all=false" class="btn btn-primary">Generate Missing WPA Keys</a>
</div>
{{end}}
</fieldset>
</form>
</div>

1
web.go
View File

@@ -112,6 +112,7 @@ func newHandler() http.Handler {
router.HandleFunc("/setup/teams/{id}/edit", TeamEditPostHandler).Methods("POST")
router.HandleFunc("/setup/teams/{id}/delete", TeamDeletePostHandler).Methods("POST")
router.HandleFunc("/setup/teams/publish", TeamsPublishHandler).Methods("POST")
router.HandleFunc("/setup/teams/generate_wpa_keys", TeamsGenerateWpaKeysHandler).Methods("GET")
router.HandleFunc("/setup/schedule", ScheduleGetHandler).Methods("GET")
router.HandleFunc("/setup/schedule/generate", ScheduleGeneratePostHandler).Methods("POST")
router.HandleFunc("/setup/schedule/save", ScheduleSavePostHandler).Methods("POST")