1This NQE script audits NTP configuration compliance for IOS-XE Branch devices in the AMRS and EMEA regions. It compares each device's running configuration against locally-defined gold standards (approved NTP server IPs per region, with variants for LAN switches, SDWAN routers, and WLCs — both with and without VRF syntax). For each device it identifies missing NTP servers (present in the standard but absent from the device) using blockDiff, and extra NTP servers (configured on the device but not in the approved list) using set subtraction. The results are output as a compliance table with a violation flag, the missing/extra server details, and device metadata derived from tags (region, environment, function, manager).
Edit the IP’s in the xxxx Standard to your own (and remove what you don’t need).
Additionally, there is a Utility in here (export_get_list_match_from_tags) at the beginning. The code requires the devices to be tagged with a region (AMRS APAC EMEA LATM)
Thanks
/*** @intent NTP Configs for tw different regions
@description Validate NTP configuration from two separate regions (AMRS and EMEA). Note, devices must be tagged with AMRS or EMEA as part of the get_list_match_from_tags
Places and names have been removed to protect the innocent.
Thanks Rob T @forwardNetworks getting me started with the logic
***/
export get_list_match_from_tags(tags: Bag<String>, expectedValues: Bag<String>) =
max(foreach v in expectedValues
where v in tags
select v);
export get_region_from_tags(tags: Bag<String>) =
get_list_match_from_tags(tags, ["EMEA", "AMRS", "APAC", "LATM"]);
// =====================
// Utility Functions / Helpers
// =====================
minByFunc(blockDiffResult) = blockDiffResult.diffCount;
ntpPattern = ```
ntp server {server:string}
```;
ntpPattern_vrf = ```
ntp server vrf {string} {server:string}
```;
getConfigAsString(device) =
max(foreach command in device.outputs.commands
where command.commandType == CommandType.CONFIG
select command.response);
emptyStringList =
foreach x in [0]
where false
select " ";
emptyPatternList =
foreach x in [0]
where false
select blockPattern(" ");
// =====================
// Shared Parsing Patterns
// =====================
reXeNtpServerPattern =
re`ntp server(?:\s+vrf\s+\S+)?\s+(?<ip:String>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})`;
reXeNtpSourcePattern = re`\nntp source[^\n]*(?:\n[ \t].*)*`;
// =====================
// Local "Golden" Standards
// =====================
// Put the region-specific approved NTP config text here.
// Keep it formatted like device config lines (one "ntp server ..." per line).
amrsLanNtpStandard =
join("\n", ["ntp server 1.1.1.123",
"ntp server 1.1.1.124",
"ntp server 1.1.1.125",
"ntp server 1.1.1.126"
]) +
"\n";
amrsSdwanNtpStandard =
join("\n", ["ntp server vrf 1 1.1.123",
"ntp server vrf 1 1.1.1.124",
"ntp server vrf 1 1.1.1.125",
"ntp server vrf 1 1.1.1.126"
]) +
"\n";
amrsWlcNtpStandard =
join("\n", ["ntp server 1.1.1.234",
"ntp server 1.1.1.234"
]) +
"\n";
// EMEA Configs -
emeaLanNtpStandard =
join("\n", ["ntp server 2.2.2.123",
"ntp server 2.2.2.124",
"ntp source"
]) +
"\n";
emeaSdwanNtpStandard =
join("\n", ["ntp server vrf 1 2.2.2.123",
"ntp server vrf 1 2.2.2.124",
"ntp source"
]) +
"\n";
emeaWlcNtpStandard =
join("\n", ["ntp server 2.2.2.234",
" ntp server 2.2.2.234"
]) +
"\n";
// =====================
// AMRS Constants (derived from local standards)
// =====================
xePatternAmrsSwitchesAsConfig = parseConfigBlocks(OS.OTHER, amrsLanNtpStandard);
validAmrsNtpServers =
foreach blockMatch in blockMatches(xePatternAmrsSwitchesAsConfig, ntpPattern)
select blockMatch.data.server;
XePatternAmrsSwitchesNoVRF = blockPattern(amrsLanNtpStandard);
XePatternAmrsWlcNoVRF = blockPattern(amrsWlcNtpStandard);
XePatternAmrsSwitchesWithVRF =
blockPattern(join("\n", order(foreach server in validAmrsNtpServers
select "ntp server vrf {string} " + server)) +
"\n");
XePatternAmrsRoutersNoVRF = blockPattern(amrsSdwanNtpStandard);
XePatternAmrsRoutersWithVRF =
blockPattern(join("\n", order(foreach server in validAmrsNtpServers
select "ntp server vrf {string} " + server)) +
"\n");
xeAMRSDiffPatterns =
[XePatternAmrsSwitchesNoVRF,
XePatternAmrsSwitchesWithVRF,
XePatternAmrsRoutersNoVRF,
XePatternAmrsRoutersWithVRF,
XePatternAmrsWlcNoVRF
];
// =====================
// EMEA Constants (derived from local standards)
// =====================
xePatternEmeaSwitchesAsConfig = parseConfigBlocks(OS.OTHER, emeaLanNtpStandard);
validEmeaNtpServers =
foreach blockMatch in blockMatches(xePatternEmeaSwitchesAsConfig, ntpPattern)
select blockMatch.data.server;
XePatternEmeaSwitchesNoVRF = blockPattern(emeaLanNtpStandard);
XePatternEmeaWlcNoVRF = blockPattern(emeaWlcNtpStandard);
XePatternEmeaSwitchesWithVRF =
blockPattern(join("\n", order(foreach server in validEmeaNtpServers
select "ntp server vrf {string} " + server)) +
"\n");
XePatternEmeaRoutersNoVRF = blockPattern(emeaSdwanNtpStandard);
XePatternEmeaRoutersWithVRF =
blockPattern(join("\n", order(foreach server in validEmeaNtpServers
select "ntp server vrf {string} " + server)) +
"\n");
xeEMEADiffPatterns =
[XePatternEmeaSwitchesNoVRF,
XePatternEmeaSwitchesWithVRF,
XePatternEmeaRoutersNoVRF,
XePatternEmeaRoutersWithVRF,
XePatternEmeaWlcNoVRF
];
// =====================
// Per-device evaluation function (IOS-XE)
// =====================
getXeResult(device) =
max(foreach device in [device]
let configAsString = getConfigAsString(device)
let configAsString = if isPresent(configAsString)
then configAsString
else ""
let region = get_region_from_tags(device.tagNames)
let validNtpServers = if region == "AMRS"
then validAmrsNtpServers
else if region == "EMEA"
then validEmeaNtpServers
else emptyStringList
let diffPatternList = if region == "AMRS"
then xeAMRSDiffPatterns
else if region == "EMEA"
then xeEMEADiffPatterns
else emptyPatternList
let blockDiffResult = minBy((foreach diffPattern in diffPatternList
select blockDiff(device.files.config, diffPattern)),
minByFunc)
let NtpServerMatchResults = regexMatches(configAsString, reXeNtpServerPattern)
let configuredNtpServers = (foreach ntpServer in NtpServerMatchResults
select ntpServer.data.ip)
let extraServers = configuredNtpServers - validNtpServers
let NtpSourceMatchResults = regexMatches(configAsString, reXeNtpSourcePattern)
let ServerConfig = if length(NtpServerMatchResults) == 0
then null : String
else join("\n", foreach result in NtpServerMatchResults
select result.string)
let SourceConfig = if length(NtpSourceMatchResults) == 0
then null : String
else join("", foreach result in NtpSourceMatchResults
select result.string)
select {
diffCount: blockDiffResult.diffCount,
missingBlocks: blockDiffResult.blocks,
extraBlocks: extraServers,
ServerConfig,
SourceConfig,
debug: configAsString == ""
});
// =====================
// Main query
// =====================
foreach device in network.devices
where device.platform.os == OS.IOS_XE &&
("AMRS" in device.tagNames || "EMEA" in device.tagNames) &&
"Branch" in device.tagNames
let result = when device.platform.os is
IOS_XE -> getXeResult(device);
otherwise ->
{ diffCount: null : Integer,
missingBlocks: null : MatchBlocks,
extraBlocks: null : List<String>,
ServerConfig: null : String,
SourceConfig: null : String,
aaaConfig: null : String,
debug: null : Bool
}
select {
violation: result.diffCount > 0 || length(result.extraBlocks) > 0,
region: get_region_from_tags(device.tagNames),
device: device.name,
OS: device.platform.os,
model: device.platform.model,
diffCount: result.diffCount,
missing: result.missingBlocks,
extra: result.extraBlocks,
NTP_Server: result.ServerConfig,
NTP_Source: result.SourceConfig,
}




