Guest User

Untitled

a guest
Nov 23rd, 2017
104
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.99 KB | None | 0 0
  1. """ ntpq out parser and reporter
  2. """
  3. import re
  4. import socket
  5. # pylint: disable=redefined-builtin
  6. from ansible.module_utils.basic import AnsibleModule
  7.  
  8. # - name: Run the ntp health module against the out from the ntp status command
  9. # ntp_health:
  10. # output: "{{ output['stdout'][0] }}" # the output of 'show ntp associations or show ntp peer-status'
  11. # ntp_servers: "{{ ntp_servers }}" # a list of ntp servers
  12. # os: "{{ os }}" # netmiko style os (cisco_os, cisco_nxos, cisco_xe etc)
  13. # domain_name: "company.net" # the domain name to add to ntp servers where their domain gets cut off
  14. # register: health
  15. # ignore_errors: true
  16. #
  17. # - debug: var=health
  18.  
  19.  
  20.  
  21. REGEXES = {
  22. "arista_eos": re.compile(r'''
  23. ^ # Beginning of line
  24. (?P<status>[\sx\.\-+#\*o]) # Capture the status
  25. (?P<remote>\S+)\s+ # The remote name
  26. (?P<refid>(\d{1,3}.){3}\d{1,3})\s+ # The remote's refid followed by spaces
  27. (?P<stratum>\d{1,2})\s+ # The stratum of the remote followed by spaces
  28. (?P<type>[lumb-])\s+ # The type of remote, followed by spaces
  29. (?P<when>\d+)\s+ # The last time the server was queried
  30. (?P<poll>\d+)\s+ # Frequency of poll, followed by spaces
  31. (?P<reach>\d+)\s+ # Reach, followed by spaces
  32. (?P<delay>[\d\.]*)\s+ # Delay, followed by spaces
  33. (?P<offset>[\d\.-]*)\s+ # Offset followed by spaces
  34. (?P<jitter>[\d\.]*) # Jitter
  35. $ ''', # End of line
  36. re.VERBOSE),
  37. "cisco_ios": re.compile(r'''
  38. ^ # Beginning of line
  39. (?P<status>[\sx\.\-+#\*o]) # Capture the status
  40. (?P<configured>\~) # Configured
  41. (?P<remote>(\d{1,3}.){3}\d{1,3})\s+ # The remote IP followed by spaces
  42. (?P<refid>(\d{1,3}.){3}\d{1,3})\s+ # The remote's refid followed by spaces
  43. (?P<stratum>\d{1,2})\s+ # The stratum of the remote followed by spaces
  44. (?P<when>\d+)\s+ # The last time the server was queried
  45. (?P<poll>\d+)\s+ # Frequency of poll, followed by spaces
  46. (?P<reach>\d+)\s+ # Reach, followed by spaces
  47. (?P<delay>[\d\.]*)\s+ # Delay, followed by spaces
  48. (?P<offset>[\d\.-]*)\s+ # Offset followed by spaces
  49. (?P<jitter>[\d\.]*) # Jitter
  50. $''', # End of line
  51. re.VERBOSE),
  52. "cisco_nxos": re.compile(r'''
  53. ^
  54. (?P<status>[\*\+=-]) # Beginning of line
  55. (?P<remote>(\d{1,3}.){3}\d{1,3})\s+ # The remote IP followed by spaces
  56. (?P<local>(\d{1,3}.){3}\d{1,3})\s+ # The local IP followed by spaces
  57. (?P<stratum>\d{1,2})\s+ # The stratum of the remote followed by spaces
  58. (?P<poll>\d+)\s+ # Frequency of poll, followed by spaces
  59. (?P<reach>\d+)\s+ # Delay, followed by spaces
  60. (?P<delay>[\d\.]*) # Reach
  61. (\s+(?P<vrf>\S+))? # spaces, VRF (optional)
  62. (\s+)? # stoopid eol spaces
  63. $''', # End of line
  64. re.VERBOSE),
  65. "cisco_xr": re.compile(r'''
  66. ^ # Beginning of line
  67. (?P<status>[\sx\.\-+#\*o]) # Capture the status
  68. (?P<configured>\~) # Configured
  69. (?P<remote>(\d{1,3}.){3}\d{1,3})\s+ # The remote IP followed by spaces
  70. (vrf\s(?P<vrf>\S+)\s+)? # Optional VRF
  71. (?P<refid>(\d{1,3}.){3}\d{1,3})\s+ # The remote's refid followed by spaces
  72. (?P<stratum>\d{1,2})\s+ # The stratum of the remote followed by spaces
  73. (?P<when>\d+)\s+ # The last time the server was queried
  74. (?P<poll>\d+)\s+ # Frequency of poll, followed by spaces
  75. (?P<reach>\d+)\s+ # Reach, followed by spaces
  76. (?P<delay>[\d\.]*)\s+ # Delay, followed by spaces
  77. (?P<offset>[\d\.-]*)\s+ # Offset followed by spaces
  78. (?P<jitter>[\d\.]*) # Jitter
  79. $''', # End of line
  80. re.VERBOSE),
  81. }
  82.  
  83. def nxos_vdc(os, output):
  84. """ Determine if this is a VDC and set desired accordingly
  85.  
  86. Args:
  87. output (str): The output from the ntp status command
  88.  
  89. Returns:
  90. dict: Mock desired since we cna resolve
  91.  
  92. """
  93. if os == "cisco_nxos":
  94. if "System clock is not controlled by NTP in this VDC" in output:
  95. return True
  96. else:
  97. return False
  98.  
  99. def parse_output(params):
  100. """ Parses the output and returns structured data
  101.  
  102. Args:
  103. output (str): The output from the ntp status command
  104.  
  105. Returns:
  106. dict: A dictionary of entries
  107.  
  108. """
  109. entries = []
  110. if params['os'] in REGEXES:
  111. for line in params['output'].splitlines():
  112. result = re.match(REGEXES[params['os']], line)
  113. if result:
  114. entry = result.groupdict()
  115. entries.append(entry)
  116. return None, entries
  117. else:
  118. return "No regex support for %s" % params['os'], None
  119.  
  120. def arista_resolve(entries, domain_name):
  121. """ Resolves entries to an IP address
  122.  
  123. Args:
  124. entries (dict): The dict of entries
  125.  
  126. Returns:
  127. dict: A dictionary of entries
  128.  
  129. """
  130. for entry in entries:
  131. try:
  132. entry['remote'] = socket.gethostbyname("%s.%s" % (entry['remote'].split('.')[0], domain_name))
  133. except socket.gaierror:
  134. pass
  135. return entries
  136.  
  137. def xr_unwrap(output):
  138. """ Unwrap long XR entries
  139.  
  140. Args:
  141. output (str): The str output
  142.  
  143. Returns:
  144. str: Unwrapped output
  145.  
  146. """
  147. lines = output.splitlines()
  148. i = len(lines) - 1
  149. newlist = []
  150. while i >= 0:
  151. hl_regex = re.compile(r'^\s+(\d{1,3}.){3}\d{1,3}')
  152. if hl_regex.match(lines[i]):
  153. newlist.insert(0, "%s %s" % (lines[i-1], lines[i]))
  154. i -= 2
  155. else:
  156. newlist.insert(0, lines[i])
  157. i -= 1
  158. lines = "\n".join(newlist)
  159. return lines
  160.  
  161. def find_failed(entries):
  162. """ Walk the entries and look for stratum 16
  163.  
  164. Args:
  165. entries (dict): A dictionary of NTP server entries
  166.  
  167. Return:
  168. dict: The dict of entries with an additonal k,v for each
  169.  
  170. """
  171. for entry in entries:
  172. entry['stratum_ok'] = bool(entry['stratum'] != "16")
  173. return entries
  174.  
  175. def resolve_expected(entries):
  176. """ Resolve each in the list of entries to an IP address
  177.  
  178. Args:
  179. entries (list): A list of NTP servers
  180.  
  181. Returns:
  182. dict: A dict of name, ip
  183. """
  184.  
  185. desired = {}
  186. for entry in entries:
  187. try:
  188. desired[entry] = socket.gethostbyname(entry)
  189. except socket.gaierror:
  190. desired[entry] = None
  191. except Exception as error: # pylint: disable=broad-except
  192. desired[entry] = str(error)
  193. return desired
  194.  
  195. def roll_up(summary):
  196. """ Assess each of the individual health check and produce a final health check
  197.  
  198. Args:
  199. summary (dict): A dictionary of summary information
  200.  
  201. Returns:
  202. dict: The same dict, with a health note and health
  203.  
  204. """
  205. summary['healthy_failed_reasons'] = []
  206. if summary['missing']:
  207. note = "One or more NTP servers is missing from the output."
  208. summary['healthy_failed_reasons'].append(note)
  209. if summary['extra']:
  210. note = "One or more extra NTP servers found in the output."
  211. summary['healthy_failed_reasons'].append(note)
  212. if not summary['stratums_ok']:
  213. note = "One or more NTP servers found with a stratum of 16."
  214. summary['healthy_failed_reasons'].append(note)
  215. if not summary['desired_ok']:
  216. note = "One or more of the desired NTP servers did not resolve to an IP address."
  217. summary['healthy_failed_reasons'].append(note)
  218. if not summary['desired_no_dupes']:
  219. note = "Duplicate entries found in desired NTP server list."
  220. summary['healthy_failed_reasons'].append(note)
  221. if not bool(summary['chosen']):
  222. note = "No 'chosen' NTP server found in output."
  223. summary['healthy_failed_reasons'].append(note)
  224. if not summary['reachability_ok']:
  225. note = "One or more NTP server is experiencing reachability issues."
  226. summary['healthy_failed_reasons'].append(note)
  227. summary['healthy'] = not bool(summary['healthy_failed_reasons'])
  228. return summary
  229.  
  230. def summarize(entries, desired, output):
  231. """ Use both the output and parsed entries to produce summary information
  232.  
  233. Args:
  234. output (str): The output from the ntpq -pn command
  235. entries (dict): A dictionary of NTP server entries
  236. desired (dict): The list of NTP servers, resolved to IPs
  237.  
  238. Returns:
  239. summary (dict): The summary dictionary
  240.  
  241. """
  242. summary = {}
  243. current_ntp_ip_list = set(x['remote'] for x in entries)
  244. desired_ntp_ip_list = set(desired.values())
  245.  
  246. summary['chosen'] = next((x for x in entries if x['status'] == "*"), None)
  247. summary['current_ntp_ip_list'] = list(current_ntp_ip_list)
  248. summary['desired_no_dupes'] = len(desired_ntp_ip_list) == len(desired.values())
  249. summary['desired_ntp_ip_list'] = list(desired_ntp_ip_list)
  250. summary['desired_ok'] = all([bool(v) for v in desired.values()])
  251. summary['desired'] = desired
  252. summary['entries'] = entries
  253. summary['extra'] = list(current_ntp_ip_list - desired_ntp_ip_list)
  254. summary['missing'] = list(desired_ntp_ip_list - current_ntp_ip_list)
  255. summary['output'] = output.splitlines()
  256. summary['reachability_issues'] = [x for x in entries if x['reach'] != "377"]
  257. summary['reachability_ok'] = not bool(summary['reachability_issues'])
  258. summary['stratums_ok'] = all([x['stratum_ok'] for x in entries])
  259. summary = roll_up(summary)
  260. return summary
  261.  
  262. def main():
  263. """ Genisis
  264. """
  265. module = AnsibleModule(
  266. argument_spec=dict(
  267. ntp_servers=dict(required=True, type='list'),
  268. output=dict(required=True, type='str'),
  269. os=dict(required=True, type='str'),
  270. domain_name=dict(required=False, type='str'),
  271. ),
  272. supports_check_mode=True)
  273.  
  274. try:
  275. # Look for VDC warning in NX-OS
  276. is_vdc = nxos_vdc(module.params['os'], module.params['output'])
  277. if is_vdc:
  278. module.exit_json(changed=False, results="Is a NXOS VDC")
  279. else:
  280. desired = resolve_expected(module.params['ntp_servers'])
  281. # Reuse the IOS regex
  282. if module.params['os'] in ["cisco_xe", "cisco_asa", "cisco_fwsm", "cisco_pix"]:
  283. module.params['os'] = "cisco_ios"
  284. # Unwrap XR output
  285. if module.params['os'] == 'cisco_xr':
  286. module.params['output'] = xr_unwrap(module.params['output'])
  287. # Move along
  288. error, entries = parse_output(module.params)
  289. if error:
  290. module.fail_json(msg=error)
  291. # Resolve arista names back to IPs
  292. if module.params['os'] == "arista_eos":
  293. entries = arista_resolve(entries, module.params['domain_name'])
  294. entries = find_failed(entries)
  295. summary = summarize(entries, desired, module.params['output'])
  296. if summary['healthy']:
  297. module.exit_json(changed=False, results=summary)
  298. else:
  299. module.fail_json(msg=(", ".join(summary['healthy_failed_reasons'])), results=summary)
  300.  
  301.  
  302. except Exception as error: # pylint: disable=broad-except
  303. error_type = error.__class__.__name__
  304. module.fail_json(msg=error_type + ": " + str(error))
  305.  
  306. if __name__ == "__main__":
  307. main()
Add Comment
Please, Sign In to add comment