Cisco CL9800 Wireless Controller support

Hi,

Ok so far i’ve found that LibreNMS support Cisco WLC’s, but the newly introduced Catalyst 9800 Wireless Controllers not. The older Wireless Controllers (such like the 5500 series) were running the AireOS (from the Airespace acquisition almost 15 (!!) years ago.
(source: Introducing the Cisco Catalyst 9800 Wireless Controllers - Wifi Reference)

So the Catalyst 9800 Wireless Controllers are based on IOS-XE. The new Catalysty 9800 code has no relation anymore with Airespace; although i’ve found out it not entirely supports it, some parts of the AIRESPACE-WIRELESS-MIB still work on the CL9800.

So in a former topic i suggested to start working on a new device model; I started with reading Intro - LibreNMS Docs

I pieced this YAML file after reading it LibreNMS Support New OS doc and some comparing with the ciscowlc.yaml file:

os: IOSXE 
text: 'Cisco WLC 9800'
type: wireless
icon: cisco
over:
    - { graph: device_bits, text: 'Device Traffic' }
    - { graph: device_processor, text: 'CPU Usage' }
    - { graph: device_mempool, text: 'Memory Usage' }
    - { graph: device_wireless_ap-count, text: 'Connected APs' }
    - { graph: device_wireless_clients, text: 'Number of Clients' }
mib_dir: cisco
discovery:
    - sysObjectID:
        - .1.3.6.1.4.1.9.1.2391.
	- sysDescr:
            - 'C9800-CL'

for OS: i picked the OS running on it; is this correct what i’ve done ?
for text: similar like above
for type and icon: looks obvious to me
for over: i’ve copied the parts here from the ciscowlc.yaml file; i’m not sure if this is ok and what i should do next with it
for mib_dir: clear to me
for discovery: i doublec checked with a mibbrowser tool if the sysObjectID is correct; and it is
also i noticed the ciscowlc.yaml file utilizes only the sysDescr; so I thought; lets put in 'C9800-CL' according to the LibreNMS docs (Use this in addition to sysObjectID if required. Check that the sysDescr contains one of the strings under this item)

So then i looked at the ciscowlc.php under /opt/librenms/LibreNMS/OS
For the convience; i copied the lines 42 to 64:

public function discoverWirelessClients()
{
$ssids = $this->getCacheByIndex(‘bsnDot11EssSsid’, ‘AIRESPACE-WIRELESS-MIB’);
$counts = $this->getCacheByIndex(‘bsnDot11EssNumberOfMobileStations’, ‘AIRESPACE-WIRELESS-MIB’);

$sensors = [];
$total_oids = [];
$total = 0;
foreach ($counts as $index => $count) {
    $oid = '.1.3.6.1.4.1.14179.2.1.1.1.38.' . $index;
    $total_oids[] = $oid;
    $total += $count;

    $sensors[] = new WirelessSensor(
        'clients',
        $this->getDeviceId(),
        $oid,
        'ciscowlc-ssid',
        $index,
        'SSID: ' . $ssids[$index],
        $count
    );
}

When i look at line 42 and check the bsnDot11EssSsid in the AIRESPACE-WIRELESS-MIB; it shows 0 when i do a SNMP Get on it; it seems that the new C9800 does not populate this.
Also the SSID’s names are not populated when polled with AIRESPACE-WIRELESS-MIB.


.
.
.

But when i look at the bsnMobileStationTable; i see that it is populated with a client that is really connected. (due to the Christmas Holiday; nobody is in the office; somebody left his device in the building connected to WiFI :slight_smile:)

Also other parts of the AIRESPACE-WIRELESS-MIB are still working, for example the AP table

So i then looked that the ZiP file i pulled from cisco support download and looked at various MIB files, and found out that the CISCO-LWAPP-WLAN-MIB has a SNMP OID that shows the SSID names with a index:


So piecing al this together; what should i do to get the client count and SSID discovery working again for the new C9800 wireless controllers?

Thank you again for reading and replying.

Kinds regards

Well you are on the right track, you just need to do a little more digging to figure out why you can’t get SSID and SSID client count from SNMP. I would think maybe Cisco has another MIB for this since they moved to a new platform? You can also just start walking the entire MIB tree. You might see a OID pop up without a name and the name of the SSID as the value, that would tell you you have a missing mib somewhere.

Other then that, over in yaml is just what graphs show up when you over over the device.

Thank you for reaching out!

I’ve found that the CISCO-LWAPP-WLAN-MIB can show me the SSID names; the CISCO-LWAPP-WLAN-MIB is not ‘out of the box’ delived with LibreNMS. So i should place it in the cisco mib dir.

For the client count; the bsnMobileStationTable of the AIRESPACE-WIRELESS-MIB shows the clients that are connected; it is possible to do some array lookup (for the SSID name) and count those rows ? Or would that be to much complicated ?

Kind regards

There seems to be a few bugs in the 9800 WLC where it doesn’t answer SNMP queries as it should.
Haven’t been able to test the 17.5.x releases though or if they work with the existing code.

I’m running 17.3.3 and has the same issues as you describe above.

Problems with SNMP:
https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvv44330
https://bst.cloudapps.cisco.com/bugsearch/bug/CSCvs48567

Only ID due too limit…
CSCvu26309
CSCvv15144

I own a 9800wlc also and work for a cisco Partner , interesting in helping, I have access and I am willing to help, if there is anything I can do, that beeing said I know programing, however I am more of a troubleshooting guy then a coder :stuck_out_tongue:

1 Like

Any update on this? I Added 2 9800 WLCs to LibreNMS and it seems that it was recognized wrongly as a network device, not as wireless device and only thing related to wireless it can get from the device (after manually changing the type to wireless) is the SSID client cound but even that doesn’t work correctly because the SSIDs do not have names.

Hey all,

I’ve just upgraded from the 5500 to the 9600-CL and as you guys have said limited information being reported in LibreNMS. Anyone had luck or any further attempts to get this further supported? For me AP count is the most important so i can get reports and alarms around this.

Hi,
according to Monitor Catalyst 9800 WLC via SNMP with OIDs - Cisco
there is no OID for connected APs.

Looks like support for everything missing came in IOS-XE 17.5.x. Anyone would like to take a stab on this? I’ve got WLCs ready.

We observed the same problem and could help with testing.
Just discovered https://github.com/librenms/librenms/commit/b9881bbf36389eae7fb0be976212e1dc32fd8b31 so it should be fixed in the next release.

Hello everyone,

I was also experiencing the same issue.
LibreNMS only identifies the controller but not the connected APs.
So I made the following modifications:

includes/definitions/iosxe.yaml:

os: iosxe
group: cisco
text: 'Cisco IOS-XE'
type: network
ifXmcbc: true
over:
    - { graph: device_bits, text: 'Device Traffic' }
    - { graph: device_processor, text: 'CPU Usage' }
    - { graph: device_mempool, text: 'Memory Usage' }
    - { graph: device_wireless_ap-count, text: 'Connected APs' }
    - { graph: device_wireless_clients, text: 'Number of Clients' }
icon: cisco
mib_dir: cisco
poller_modules:
    cisco-ace-serverfarms: true
    cisco-ace-loadbalancer: true
    cisco-cbqos: true
    cisco-cef: true
    cisco-mac-accounting: true
    cisco-voice: true
    cisco-remote-access-monitor: true
    slas: true
    cisco-ipsec-flow-monitor: true
    cipsec-tunnels: true
    cisco-otv: true
    ipmi: false
    cisco-vpdn: true
    cisco-qfp: true
    xdsl: true
discovery_modules:
    cisco-cef: true
    slas: true
    cisco-mac-accounting: true
    cisco-otv: true
    cisco-pw: true
    vrf: true
    cisco-vrf-lite: true
    cisco-qfp: true
    xdsl: true
discovery:
    -
        sysDescr:
            - IOS-XE
            - IOSXE
            - LINUX_IOSD
            - CAT3K_CAA
bad_iftype:
    - macSecControlledIF
    - macSecUncontrolledIF

LibreNMS/OS/Iosxe.php:

<?php
/**
 * Iosxe.php
 *
 * Cisco IOS-XE Wireless LAN Controller
 * Cisco IOS-XE ISIS Neighbors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * @link       https://www.librenms.org
 *
 * @copyright  2017 Tony Murray
 * @author     Tony Murray <[email protected]>
 */

namespace LibreNMS\OS;

use App\Models\AccessPoint;
use App\Models\IsisAdjacency;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use LibreNMS\DB\SyncsModels;
use LibreNMS\Device\WirelessSensor;
use LibreNMS\Interfaces\Data\DataStorageInterface;
use LibreNMS\Interfaces\Discovery\IsIsDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessCellDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessChannelDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessRsrpDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessRsrqDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessRssiDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessSnrDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessClientsDiscovery;
use LibreNMS\Interfaces\Discovery\Sensors\WirelessApCountDiscovery;
use LibreNMS\Interfaces\Polling\IsIsPolling;
use LibreNMS\Interfaces\Polling\OSPolling;
use LibreNMS\OS\Traits\CiscoCellular;
use LibreNMS\RRD\RrdDefinition;
use LibreNMS\Util\IP;
use SnmpQuery;

class Iosxe extends Ciscowlc implements
    IsIsDiscovery,
    IsIsPolling,
    OSPolling,
    WirelessCellDiscovery,
    WirelessChannelDiscovery,
    WirelessRssiDiscovery,
    WirelessRsrqDiscovery,
    WirelessRsrpDiscovery,
    WirelessSnrDiscovery,
    WirelessClientsDiscovery,
    WirelessApCountDiscovery
{
    use SyncsModels;
    use CiscoCellular;

    public function pollOS(DataStorageInterface $datastore): void
    {
        $device = $this->getDeviceArray();
        $apNames = \SnmpQuery::enumStrings()->walk('AIRESPACE-WIRELESS-MIB::bsnAPName')->table(1);
        $radios = \SnmpQuery::enumStrings()->walk('AIRESPACE-WIRELESS-MIB::bsnAPIfTable')->table(2);
        \SnmpQuery::walk('AIRESPACE-WIRELESS-MIB::bsnAPIfLoadChannelUtilization')->table(2, $radios);
        $interferences = \SnmpQuery::walk('AIRESPACE-WIRELESS-MIB::bsnAPIfInterferencePower')->table(3);

        $numAccessPoints = count($apNames);
        $numClients = 0;

        foreach ($radios as $radio) {
            foreach ($radio as $slot) {
                $numClients += $slot['AIRESPACE-WIRELESS-MIB::bsnApIfNoOfUsers'] ?? 0;
            }
        }

        $rrd_def = RrdDefinition::make()
            ->addDataset('NUMAPS', 'GAUGE', 0, 12500000000)
            ->addDataset('NUMCLIENTS', 'GAUGE', 0, 12500000000);

        $fields = [
            'NUMAPS' => $numAccessPoints,
            'NUMCLIENTS' => $numClients,
        ];

        $tags = compact('rrd_def');
        $datastore->put($device, 'ciscowlc', $tags, $fields);

        $db_aps = $this->getDevice()->accessPoints->keyBy->getCompositeKey();
        $valid_ap_ids = [];

        foreach ($radios as $mac => $radio) {
            foreach ($radio as $slot => $value) {
                $channel = str_replace('ch', '', $value['AIRESPACE-WIRELESS-MIB::bsnAPIfPhyChannelNumber'] ?? '');

                $ap = new AccessPoint([
                    'device_id' => $this->getDeviceId(),
                    'name' => $apNames[$mac]['AIRESPACE-WIRELESS-MIB::bsnAPName'] ?? '',
                    'radio_number' => $slot,
                    'type' => $value['AIRESPACE-WIRELESS-MIB::bsnAPIfType'] ?? '',
                    'mac_addr' => $mac,
                    'channel' => $channel,
                    'txpow' => $value['AIRESPACE-WIRELESS-MIB::bsnAPIfPhyTxPowerLevel'] ?? 0,
                    'radioutil' => $value['AIRESPACE-WIRELESS-MIB::bsnAPIfLoadChannelUtilization'] ?? 0,
                    'numasoclients' => $value['AIRESPACE-WIRELESS-MIB::bsnApIfNoOfUsers'] ?? 0,
                    'nummonclients' => 0,
                    'nummonbssid' => 0,
                    'interference' => 128 + ($interferences[$mac][$slot][$channel]['AIRESPACE-WIRELESS-MIB::bsnAPIfInterferencePower'] ?? -128), // why are we adding 128?
                ]);

                d_echo($ap->toArray());

                // if there is a numeric channel, assume the rest of the data is valid, I guess
                if (! is_numeric($channel)) {
                    continue;
                }

                $rrd_def = RrdDefinition::make()
                    ->addDataset('channel', 'GAUGE', 0, 200)
                    ->addDataset('txpow', 'GAUGE', 0, 200)
                    ->addDataset('radioutil', 'GAUGE', 0, 100)
                    ->addDataset('nummonclients', 'GAUGE', 0, 500)
                    ->addDataset('nummonbssid', 'GAUGE', 0, 200)
                    ->addDataset('numasoclients', 'GAUGE', 0, 500)
                    ->addDataset('interference', 'GAUGE', 0, 2000);

                $datastore->put($device, 'arubaap', [
                    'name' => $ap->name,
                    'radionum' => $ap->radio_number,
                    'rrd_name' => ['arubaap', $ap->name . $ap->radio_number],
                    'rrd_def' => $rrd_def,
                ], $ap->only([
                    'channel',
                    'txpow',
                    'radioutil',
                    'nummonclients',
                    'nummonbssid',
                    'numasoclients',
                    'interference',
                ]));

                /** @var AccessPoint $db_ap */
                if ($db_ap = $db_aps->get($ap->getCompositeKey())) {
                    $ap = $db_ap->fill($ap->getAttributes());
                }

                $ap->save(); // persist ap
                $valid_ap_ids[] = $ap->accesspoint_id;
            }
        }

        // delete invalid aps
        $this->getDevice()->accessPoints->whereNotIn('accesspoint_id', $valid_ap_ids)->each->delete();
    }

    /**
     * Array of shortened ISIS codes
     *
     * @var array
     */
    protected $isis_codes = [
        'l1IntermediateSystem' => 'L1',
        'l2IntermediateSystem' => 'L2',
        'l1L2IntermediateSystem' => 'L1L2',
    ];

    public function discoverIsIs(): Collection
    {
        // Check if the device has any ISIS enabled interfaces
        $circuits = SnmpQuery::enumStrings()->walk('CISCO-IETF-ISIS-MIB::ciiCirc');
        $adjacencies = new Collection;

        if ($circuits->isValid()) {
            $circuits = $circuits->table(1);
            $adjacencies_data = SnmpQuery::enumStrings()->walk('CISCO-IETF-ISIS-MIB::ciiISAdj')->table(2);

            foreach ($adjacencies_data as $circuit_index => $adjacency_list) {
                foreach ($adjacency_list as $adjacency_index => $adjacency_data) {
                    if (empty($circuits[$circuit_index]['CISCO-IETF-ISIS-MIB::ciiCircIfIndex'])) {
                        continue;
                    }

                    if (($circuits[$circuit_index]['CISCO-IETF-ISIS-MIB::ciiCircPassiveCircuit'] ?? 'true') == 'true') {
                        continue; // Do not poll passive interfaces and bad data
                    }

                    $adjacencies->push(new IsisAdjacency([
                        'device_id' => $this->getDeviceId(),
                        'index' => "[$circuit_index][$adjacency_index]",
                        'ifIndex' => $circuits[$circuit_index]['CISCO-IETF-ISIS-MIB::ciiCircIfIndex'],
                        'port_id' => $this->ifIndexToId($circuits[$circuit_index]['CISCO-IETF-ISIS-MIB::ciiCircIfIndex']),
                        'isisCircAdminState' => $circuits[$circuit_index]['CISCO-IETF-ISIS-MIB::ciiCircAdminState'] ?? 'down',
                        'isisISAdjState' => $adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjState'] ?? 'down',
                        'isisISAdjNeighSysType' => Arr::get($this->isis_codes, $adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjNeighSysType'] ?? '', 'unknown'),
                        'isisISAdjNeighSysID' => $this->formatIsIsId($adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjNeighSysID'] ?? ''),
                        'isisISAdjNeighPriority' => $adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjNeighPriority'] ?? '',
                        'isisISAdjLastUpTime' => $this->parseAdjacencyTime($adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjLastUpTime'] ?? 0),
                        'isisISAdjAreaAddress' => implode(',', array_map([$this, 'formatIsIsId'], $adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjAreaAddress'] ?? [])),
                        'isisISAdjIPAddrType' => implode(',', $adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjIPAddrType'] ?? []),
                        'isisISAdjIPAddrAddress' => implode(',', array_map(function ($ip) {
                            return (string) IP::fromHexString($ip, true);
                        }, $adjacency_data['CISCO-IETF-ISIS-MIB::ciiISAdjIPAddrAddress'] ?? [])),
                    ]));
                }
            }
        }

        return $adjacencies;
    }

    public function pollIsIs($adjacencies): Collection
    {
        $states = SnmpQuery::enumStrings()->walk('CISCO-IETF-ISIS-MIB::ciiISAdjState')->values();
        $up_count = array_count_values($states)['up'] ?? 0;

        if ($up_count !== $adjacencies->count()) {
            echo 'New Adjacencies, running discovery';

            return $this->fillNew($adjacencies, $this->discoverIsIs());
        }

        $uptime = SnmpQuery::walk('CISCO-IETF-ISIS-MIB::ciiISAdjLastUpTime')->values();

        return $adjacencies->each(function ($adjacency) use ($states, $uptime) {
            $adjacency->isisISAdjState = $states['CISCO-IETF-ISIS-MIB::ciiISAdjState' . $adjacency->index] ?? $adjacency->isisISAdjState;
            $adjacency->isisISAdjLastUpTime = $this->parseAdjacencyTime($uptime['CISCO-IETF-ISIS-MIB::ciiISAdjLastUpTime' . $adjacency->index] ?? 0);
        });
    }
    
    /**
     * Discover wireless client counts. Type is clients.
     * Returns an array of LibreNMS\Device\Sensor objects that have been discovered
     *
     * @return array Sensors
     */
    public function discoverWirelessClients()
    {
        $counts = $this->getCacheByIndex('bsnDot11EssNumberOfMobileStations', 'AIRESPACE-WIRELESS-MIB');
        if (empty($counts)) {
            return []; // no counts to be had
        }

        $ssids = $this->getCacheByIndex('bsnDot11EssSsid', 'AIRESPACE-WIRELESS-MIB');
        if (empty($ssids)) {
            //  Try to check the LWAPP mib
            $ssids = $this->getCacheByIndex('cLWlanSsid', 'CISCO-LWAPP-WLAN-MIB');
        }

        $sensors = [];
        $total_oids = [];
        $total = 0;
        foreach ($counts as $index => $count) {
            $oid = '.1.3.6.1.4.1.14179.2.1.1.1.38.' . $index;
            $total_oids[] = $oid;
            $total += $count;

            $sensors[] = new WirelessSensor(
                'clients',
                $this->getDeviceId(),
                $oid,
                'ciscowlc-ssid',
                $index,
                'SSID: ' . $ssids[$index],
                $count
            );
        }

        $sensors[] = new WirelessSensor(
            'clients',
            $this->getDeviceId(),
            $total_oids,
            'ciscowlc',
            0,
            'Clients: Total',
            $total
        );

        return $sensors;
    }

    /**
     * Discover wireless capacity.  This is a percent. Type is capacity.
     * Returns an array of LibreNMS\Device\Sensor objects that have been discovered
     *
     * @return array Sensors
     */
    
    public function discoverWirelessApCount()
    {
        $oids = [
	    'CISCO-LWAPP-AP-MIB::cLApGlobalAPConnectCount.0',
	    'CISCO-LWAPP-AP-MIB::cLApGlobalMaxApsSupported.0',
        ];
        $data = snmp_get_multi($this->getDeviceArray(), $oids);

        if (isset($data[0]['cLApGlobalAPConnectCount'])) {
            return [
                new WirelessSensor(
                    'ap-count',
                    $this->getDeviceId(),
                    '.1.3.6.1.4.1.9.9.513.1.3.35.0',
                    'ciscowlc',
                    0,
                    'Connected APs',
                    $data[0]['cLApGlobalAPConnectCount'],
                    1,
                    1,
                    'sum',
                    null,
                    $data[0]['cLApGlobalMaxApsSupported'],
                    0
                ),
            ];
        }

        return [];
    }

    /**
     * Converts SNMP time to int in seconds
     *
     * @param  string|int  $uptime
     * @return int
     */
    protected function parseAdjacencyTime($uptime): int
    {
        return (int) round(max($uptime, 1) / 100);
    }

    protected function formatIsIsId(string $raw): string
    {
        return str_replace(' ', '.', trim($raw));
    }
}

I also replaced the MIBs: mibs/cisco/CISCO-LWAPP-AP-MIB mibs/cisco/CISCO-LWAPP-TC-MIB by downloading the files directly from the Cisco website: Cisco Feature Navigator.

However, the CISCO-LWAPP-TC-MIB comes with some issues, requiring adjustments:

  1. Remove the “END” on line 868 (as it is duplicated with line 866)
  2. Change the names “dot11_6ghz” and “dot11_xor_5_6ghz” by removing the underscore (example: “dot11-6ghz” “dot11-xor-5-6ghz”)

After that, it was possible to see the APs.

5 Likes

Hi everyone, thanks for all your effort on figuring this out.

@AllanHahn the changes you suggested appear to be working for me. Did you or can you submit those changes as a PR so it gets pulled into the LibreNMS release?

2 Likes

Hey @AllanHahn, this looks real promising great work! If you can throw this on Github to get this fixed up and pulled that would be great

1 Like

Hi, great work solving this, do you know when this is going to be commited?