Problem Overview
How much could I save on my Cisco support renewal, if I could just tell what devices are ‘over-licensed’?
That is a question that one of my customers was struggling with. Trying to answer this kind of question is very difficult if you have a large estate of switches that has been deployed over time. To complicate things, there are also various generations of Nexus switches and a number of licensing models and names. It is all very confusing.
This script hopefully gives you the ability to answer this question, and in addition it calculates a 🚨 potential cost saving 🚨 based on licence prices you enter at the start.
The result of this script is a report that looks like this:

If that has got you interested, read on!
Cisco NXOS Licenses
In terms of licensing, Nexus mainly breaks down into two license types - a switching-only license and a dynamic routing license. Depending on the model of Nexus, this will be ESSENTIALS (switching) / ADVANTAGE (routing/switching), or it might be LAN_BASE (switching) or LAN_ENTERPRISE (routing/switching). There are others but lets keep it simple right now.
I have written the query below based on this cheat-sheet I made - I think the data is correct but let me know in the comments if I have missed something:

Collecting The Right Data
To do this, we need first to collect the license usage using a custom collection group. This makes the collector harvest additional output beyond what it collects by default. In the good old days, the licence was tied to the package name and visible in the ‘show version’ output but that has changed. The command ‘show license usage’ is now needed to see what is applied to the device.
Writing the Script
The script needs to do a few things:
- Parse the custom command output and get the licences deployed
- If a LAN_BASE and a LAN_ENTERPRISE licence are present, consider just the LAN_ENTERPRISE since it costs more
- Parse the configuration file and look for any dynamic routing protocols in use.
- Compare whether the configuration in use matches the license level
- Determine any potential downgrade that might save money
- Calculate the monetary cost of the downgrade
For the monetary cost, I have a data-structure called licenseDowngradeMap which has the downgrade paths, and the costs in. These are dummy values of course, so edit them to reflect your pricing.
/**
* @intent Checks if license supports dynamic routing but configuration does not use dynamic routing protocols.
* @description When support renewal comes around, you can save money by only having the licenses you need.
*
* The script compares a list of licence names with whether it finds any dynamic protocols in the device's
* configuration file. It flags a mismatch if there is an advanced licence but no dynamic protocols are
* in use.
*
* NOTE: This requires the custom collection command 'show license usage' to work
*/
pattern = `{licenseName:(string)} Yes {count:string}`;
bgpPattern = `router bgp`;
ospfPattern = `router ospf`;
isisPattern = `router isis`;
eigrpPattern = `router eigrp`;
ipRoutePattern = `ip route`;
// License validation arrays with wildcards
validDynamicLicenses = ["NXOS_ADVANTAGE", "LAN_ENTERPRISE_SERVICES", "ENTERPRISE_PKG"];
validNonDynamicLicenses = ["NXOS_ESSENTIALS", "LAN_BASE_SERVICES", "LAN_ADVANCED_SERVICES", "ENHANCED_LAYER2"];
// License downgrade mapping with constraints and costs
licenseDowngradeMap = [
{ from: "NXOS_ADVANTAGE", to: ["NXOS_ESSENTIALS"], fromCost: 1000, toCosts: [500] },
{ from: "LAN_ENTERPRISE_SERVICES", to: ["LAN_BASE_SERVICES", "LAN_ADVANCED_SERVICES"], fromCost: 2000, toCosts: [800, 1200] },
{ from: "ENTERPRISE_PKG", to: ["ENHANCED_LAYER2"], fromCost: 1500, toCosts: [600] }
];
// Function to get potential downgrade options for a license
getPotentialDowngrades(licenseName: String) =
foreach mapping in licenseDowngradeMap
where matches(licenseName, mapping.from + "*")
foreach i in fromTo(0, length(mapping.to) - 1)
select {
currentLicense: licenseName,
suggestedLicense: mapping.to[i],
currentCost: mapping.fromCost,
suggestedCost: mapping.toCosts[i],
potentialSavings: mapping.fromCost - mapping.toCosts[i]
};
// Function to check if a license name matches any pattern in a list (with wildcards)
matchesLicensePattern(licenseName: String, validLicenses: List<String>) =
any(foreach validLicense in validLicenses
select matches(licenseName, validLicense + "*"));
// Function to check if BGP is configured on a device
isBgpConfigured(device: Device) =
length(patternMatches(device.files.config, bgpPattern)) > 0;
// Function to check if OSPF is configured on a device
isOspfConfigured(device: Device) =
length(patternMatches(device.files.config, ospfPattern)) > 0;
// Function to check if ISIS is configured on a device
isIsisConfigured(device: Device) =
length(patternMatches(device.files.config, isisPattern)) > 0;
// Function to check if EIGRP is configured on a device
isEigrpConfigured(device: Device) =
length(patternMatches(device.files.config, eigrpPattern)) > 0;
// Function to check if static routes (ip route) are configured on a device
isStaticRouteConfigured(device: Device) =
length(patternMatches(device.files.config, ipRoutePattern)) > 0;
// Filter for just Cisco NXOS switches
foreach device in network.devices
where device.platform.vendor == Vendor.CISCO && device.platform.os == OS.NXOS && device.platform.deviceType == DeviceType.SWITCH
// Parse the show license usage output where it is present
foreach command in device.outputs.commands
where command.commandText == "show license usage"
let licenseOutput = parseConfigBlocks(OS.NXOS, command.response)
// Collect all licenses for this device
let allLicenses = (foreach match in patternMatches(licenseOutput, pattern)
select {
licenseName: match.data.licenseName,
count: match.data.count,
isDynamic: matchesLicensePattern(match.data.licenseName, validDynamicLicenses),
isNonDynamic: matchesLicensePattern(match.data.licenseName, validNonDynamicLicenses)
})
// Check if device has both dynamic and non-dynamic licenses
let hasDynamicLicenses = any(foreach license in allLicenses select license.isDynamic)
let hasNonDynamicLicenses = any(foreach license in allLicenses select license.isNonDynamic)
// Determine which licenses to show: if both exist, show only dynamic; otherwise show all valid
let licensesToShow = if hasDynamicLicenses && hasNonDynamicLicenses
then (foreach license in allLicenses where license.isDynamic select license)
else (foreach license in allLicenses where license.isDynamic || license.isNonDynamic select license)
// Process each license to show
foreach licenseToShow in licensesToShow
// Check if BGP, OSPF, ISIS, and static routes are configured using the functions
let bgpConfigured = isBgpConfigured(device)
let ospfConfigured = isOspfConfigured(device)
let isisConfigured = isIsisConfigured(device)
let eigrpConfigured = isEigrpConfigured(device)
let staticRouteConfigured = isStaticRouteConfigured(device)
// Determine if any dynamic routing protocols are configured (excluding static routes)
let dynamicRoutingConfigured = bgpConfigured || ospfConfigured || isisConfigured || eigrpConfigured
// Check license validity using wildcard matching
let isDynamicLicense = licenseToShow.isDynamic
let isNonDynamicLicense = licenseToShow.isNonDynamic
let isValidLicense = isDynamicLicense || isNonDynamicLicense
// Determine mismatch status: Red if dynamic license present but no dynamic routing, Green otherwise
let isMismatch = isDynamicLicense && !dynamicRoutingConfigured
// Get potential downgrades if there's a mismatch
let potentialDowngrades = if isMismatch
then getPotentialDowngrades(licenseToShow.licenseName)
else null : List<{currentLicense: String, suggestedLicense: String, currentCost: Number, suggestedCost: Number, potentialSavings: Number}>
// Calculate maximum potential savings
let maxPotentialSavings = if isPresent(potentialDowngrades) && length(potentialDowngrades) > 0
then max(foreach downgrade in potentialDowngrades select downgrade.potentialSavings)
else 0
select {
Device: device.name,
"Mismatch": if isMismatch
then withInfoStatus("Mismatch between license and features", InfoStatus.ERROR)
else withInfoStatus("License matches features", InfoStatus.OK),
"License Name": licenseToShow.licenseName,
"License Type": if isDynamicLicense
then "Dynamic Routing"
else if isNonDynamicLicense
then "Switching Only"
else "Unknown",
"Routing Configured?": if dynamicRoutingConfigured
then withInfoStatus("Dynamic routing enabled", InfoStatus.WARNING)
else if staticRouteConfigured
then withInfoStatus("Only switching and static routes configured", InfoStatus.OK)
else withInfoStatus("Only switching enabled", InfoStatus.OK),
"Potential Downgrades": if isMismatch && isPresent(potentialDowngrades)
then (foreach downgrade in potentialDowngrades
select downgrade.suggestedLicense + " (Save $" + toString(downgrade.potentialSavings) + ")")
else ["No downgrades available"],
"Maximum Potential Savings": if isMismatch && maxPotentialSavings > 0
then withInfoStatus("$" + toString(maxPotentialSavings), InfoStatus.OK)
else withInfoStatus("$0", InfoStatus.WARNING),
"Cost Analysis": if isMismatch && isPresent(potentialDowngrades) && length(potentialDowngrades) > 0
then withInfoStatus("License can be downgraded to save costs", InfoStatus.WARNING)
else if isMismatch
then withInfoStatus("No valid downgrade path available", InfoStatus.ERROR)
else withInfoStatus("License appropriately sized", InfoStatus.OK),
"BGP Configured?": bgpConfigured,
"OSPF Configured?": ospfConfigured,
"ISIS Configured?": isisConfigured,
"EIGRP Configured?": eigrpConfigured,
"Static Routes Configured?": staticRouteConfigured,
}
Now the script may not be perfect, and I can’t profess to understand Cisco’s licensing model completely, but hopefully this will be of some use to you. Maybe it will even save you some 💰💰💰💰!
Please comment below if you can see an improvements that could be made! I look forward to any suggestions.



