ntpd.chart.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. # -*- coding: utf-8 -*-
  2. # Description: ntpd netdata python.d module
  3. # Author: Sven Mäder (rda0)
  4. # Author: Ilya Mashchenko (l2isbad)
  5. # SPDX-License-Identifier: GPL-3.0-or-later
  6. import struct
  7. import re
  8. from bases.FrameworkServices.SocketService import SocketService
  9. # default module values
  10. update_every = 1
  11. priority = 60000
  12. retries = 60
  13. # NTP Control Message Protocol constants
  14. MODE = 6
  15. HEADER_FORMAT = '!BBHHHHH'
  16. HEADER_LEN = 12
  17. OPCODES = {
  18. 'readstat': 1,
  19. 'readvar': 2
  20. }
  21. # Maximal dimension precision
  22. PRECISION = 1000000
  23. # Static charts
  24. ORDER = [
  25. 'sys_offset',
  26. 'sys_jitter',
  27. 'sys_frequency',
  28. 'sys_wander',
  29. 'sys_rootdelay',
  30. 'sys_rootdisp',
  31. 'sys_stratum',
  32. 'sys_tc',
  33. 'sys_precision',
  34. 'peer_offset',
  35. 'peer_delay',
  36. 'peer_dispersion',
  37. 'peer_jitter',
  38. 'peer_xleave',
  39. 'peer_rootdelay',
  40. 'peer_rootdisp',
  41. 'peer_stratum',
  42. 'peer_hmode',
  43. 'peer_pmode',
  44. 'peer_hpoll',
  45. 'peer_ppoll',
  46. 'peer_precision'
  47. ]
  48. CHARTS = {
  49. 'sys_offset': {
  50. 'options': [None, 'Combined offset of server relative to this host', 'ms', 'system', 'ntpd.sys_offset', 'area'],
  51. 'lines': [
  52. ['offset', 'offset', 'absolute', 1, PRECISION]
  53. ]
  54. },
  55. 'sys_jitter': {
  56. 'options': [None, 'Combined system jitter and clock jitter', 'ms', 'system', 'ntpd.sys_jitter', 'line'],
  57. 'lines': [
  58. ['sys_jitter', 'system', 'absolute', 1, PRECISION],
  59. ['clk_jitter', 'clock', 'absolute', 1, PRECISION]
  60. ]
  61. },
  62. 'sys_frequency': {
  63. 'options': [None, 'Frequency offset relative to hardware clock', 'ppm', 'system', 'ntpd.sys_frequency', 'area'],
  64. 'lines': [
  65. ['frequency', 'frequency', 'absolute', 1, PRECISION]
  66. ]
  67. },
  68. 'sys_wander': {
  69. 'options': [None, 'Clock frequency wander', 'ppm', 'system', 'ntpd.sys_wander', 'area'],
  70. 'lines': [
  71. ['clk_wander', 'clock', 'absolute', 1, PRECISION]
  72. ]
  73. },
  74. 'sys_rootdelay': {
  75. 'options': [None, 'Total roundtrip delay to the primary reference clock', 'ms', 'system',
  76. 'ntpd.sys_rootdelay', 'area'],
  77. 'lines': [
  78. ['rootdelay', 'delay', 'absolute', 1, PRECISION]
  79. ]
  80. },
  81. 'sys_rootdisp': {
  82. 'options': [None, 'Total root dispersion to the primary reference clock', 'ms', 'system',
  83. 'ntpd.sys_rootdisp', 'area'],
  84. 'lines': [
  85. ['rootdisp', 'dispersion', 'absolute', 1, PRECISION]
  86. ]
  87. },
  88. 'sys_stratum': {
  89. 'options': [None, 'Stratum (1-15)', 'stratum', 'system', 'ntpd.sys_stratum', 'line'],
  90. 'lines': [
  91. ['stratum', 'stratum', 'absolute', 1, PRECISION]
  92. ]
  93. },
  94. 'sys_tc': {
  95. 'options': [None, 'Time constant and poll exponent (3-17)', 'log2 s', 'system', 'ntpd.sys_tc', 'line'],
  96. 'lines': [
  97. ['tc', 'current', 'absolute', 1, PRECISION],
  98. ['mintc', 'minimum', 'absolute', 1, PRECISION]
  99. ]
  100. },
  101. 'sys_precision': {
  102. 'options': [None, 'Precision', 'log2 s', 'system', 'ntpd.sys_precision', 'line'],
  103. 'lines': [
  104. ['precision', 'precision', 'absolute', 1, PRECISION]
  105. ]
  106. }
  107. }
  108. PEER_CHARTS = {
  109. 'peer_offset': {
  110. 'options': [None, 'Filter offset', 'ms', 'peers', 'ntpd.peer_offset', 'line'],
  111. 'lines': []
  112. },
  113. 'peer_delay': {
  114. 'options': [None, 'Filter delay', 'ms', 'peers', 'ntpd.peer_delay', 'line'],
  115. 'lines': []
  116. },
  117. 'peer_dispersion': {
  118. 'options': [None, 'Filter dispersion', 'ms', 'peers', 'ntpd.peer_dispersion', 'line'],
  119. 'lines': []
  120. },
  121. 'peer_jitter': {
  122. 'options': [None, 'Filter jitter', 'ms', 'peers', 'ntpd.peer_jitter', 'line'],
  123. 'lines': []
  124. },
  125. 'peer_xleave': {
  126. 'options': [None, 'Interleave delay', 'ms', 'peers', 'ntpd.peer_xleave', 'line'],
  127. 'lines': []
  128. },
  129. 'peer_rootdelay': {
  130. 'options': [None, 'Total roundtrip delay to the primary reference clock', 'ms', 'peers',
  131. 'ntpd.peer_rootdelay', 'line'],
  132. 'lines': []
  133. },
  134. 'peer_rootdisp': {
  135. 'options': [None, 'Total root dispersion to the primary reference clock', 'ms', 'peers',
  136. 'ntpd.peer_rootdisp', 'line'],
  137. 'lines': []
  138. },
  139. 'peer_stratum': {
  140. 'options': [None, 'Stratum (1-15)', 'stratum', 'peers', 'ntpd.peer_stratum', 'line'],
  141. 'lines': []
  142. },
  143. 'peer_hmode': {
  144. 'options': [None, 'Host mode (1-6)', 'hmode', 'peers', 'ntpd.peer_hmode', 'line'],
  145. 'lines': []
  146. },
  147. 'peer_pmode': {
  148. 'options': [None, 'Peer mode (1-5)', 'pmode', 'peers', 'ntpd.peer_pmode', 'line'],
  149. 'lines': []
  150. },
  151. 'peer_hpoll': {
  152. 'options': [None, 'Host poll exponent', 'log2 s', 'peers', 'ntpd.peer_hpoll', 'line'],
  153. 'lines': []
  154. },
  155. 'peer_ppoll': {
  156. 'options': [None, 'Peer poll exponent', 'log2 s', 'peers', 'ntpd.peer_ppoll', 'line'],
  157. 'lines': []
  158. },
  159. 'peer_precision': {
  160. 'options': [None, 'Precision', 'log2 s', 'peers', 'ntpd.peer_precision', 'line'],
  161. 'lines': []
  162. }
  163. }
  164. class Base:
  165. regex = re.compile(r'([a-z_]+)=((?:-)?[0-9]+(?:\.[0-9]+)?)')
  166. @staticmethod
  167. def get_header(associd=0, operation='readvar'):
  168. """
  169. Constructs the NTP Control Message header:
  170. 0 1 2 3
  171. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  172. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  173. |LI | VN |Mode |R|E|M| OpCode | Sequence Number |
  174. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  175. | Status | Association ID |
  176. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  177. | Offset | Count |
  178. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  179. """
  180. version = 2
  181. sequence = 1
  182. status = 0
  183. offset = 0
  184. count = 0
  185. header = struct.pack(HEADER_FORMAT, (version << 3 | MODE), OPCODES[operation],
  186. sequence, status, associd, offset, count)
  187. return header
  188. class System(Base):
  189. def __init__(self):
  190. self.request = self.get_header()
  191. def get_data(self, raw):
  192. """
  193. Extracts key=value pairs with float/integer from ntp response packet data.
  194. """
  195. data = dict()
  196. for key, value in self.regex.findall(raw):
  197. data[key] = float(value) * PRECISION
  198. return data
  199. class Peer(Base):
  200. def __init__(self, idx, name):
  201. self.id = idx
  202. self.real_name = name
  203. self.name = name.replace('.', '_')
  204. self.request = self.get_header(self.id)
  205. def get_data(self, raw):
  206. """
  207. Extracts key=value pairs with float/integer from ntp response packet data.
  208. """
  209. data = dict()
  210. for key, value in self.regex.findall(raw):
  211. dimension = '_'.join([self.name, key])
  212. data[dimension] = float(value) * PRECISION
  213. return data
  214. class Service(SocketService):
  215. def __init__(self, configuration=None, name=None):
  216. SocketService.__init__(self, configuration=configuration, name=name)
  217. self.order = list(ORDER)
  218. self.definitions = dict(CHARTS)
  219. self.port = 'ntp'
  220. self.dgram_socket = True
  221. self.system = System()
  222. self.peers = dict()
  223. self.request = str()
  224. self.retries = 0
  225. self.show_peers = self.configuration.get('show_peers', False)
  226. self.peer_rescan = self.configuration.get('peer_rescan', 60)
  227. if self.show_peers:
  228. self.definitions.update(PEER_CHARTS)
  229. def check(self):
  230. """
  231. Checks if we can get valid systemvars.
  232. If not, returns None to disable module.
  233. """
  234. self._parse_config()
  235. peer_filter = self.configuration.get('peer_filter', r'127\..*')
  236. try:
  237. self.peer_filter = re.compile(r'^((0\.0\.0\.0)|({0}))$'.format(peer_filter))
  238. except re.error as error:
  239. self.error('Compile pattern error (peer_filter) : {0}'.format(error))
  240. return None
  241. self.request = self.system.request
  242. raw_systemvars = self._get_raw_data()
  243. if not self.system.get_data(raw_systemvars):
  244. return None
  245. return True
  246. def get_data(self):
  247. """
  248. Gets systemvars data on each update.
  249. Gets peervars data for all peers on each update.
  250. """
  251. data = dict()
  252. self.request = self.system.request
  253. raw = self._get_raw_data()
  254. if not raw:
  255. return None
  256. data.update(self.system.get_data(raw))
  257. if not self.show_peers:
  258. return data
  259. if not self.peers or self.runs_counter % self.peer_rescan == 0 or self.retries > 8:
  260. self.find_new_peers()
  261. for peer in self.peers.values():
  262. self.request = peer.request
  263. peer_data = peer.get_data(self._get_raw_data())
  264. if peer_data:
  265. data.update(peer_data)
  266. else:
  267. self.retries += 1
  268. return data
  269. def find_new_peers(self):
  270. new_peers = dict((p.real_name, p) for p in self.get_peers())
  271. if new_peers:
  272. peers_to_remove = set(self.peers) - set(new_peers)
  273. peers_to_add = set(new_peers) - set(self.peers)
  274. for peer_name in peers_to_remove:
  275. self.hide_old_peer_from_charts(self.peers[peer_name])
  276. del self.peers[peer_name]
  277. for peer_name in peers_to_add:
  278. self.add_new_peer_to_charts(new_peers[peer_name])
  279. self.peers.update(new_peers)
  280. self.retries = 0
  281. def add_new_peer_to_charts(self, peer):
  282. for chart_id in set(self.charts.charts) & set(PEER_CHARTS):
  283. dim_id = peer.name + chart_id[4:]
  284. if dim_id not in self.charts[chart_id]:
  285. self.charts[chart_id].add_dimension([dim_id, peer.real_name, 'absolute', 1, PRECISION])
  286. else:
  287. self.charts[chart_id].hide_dimension(dim_id, reverse=True)
  288. def hide_old_peer_from_charts(self, peer):
  289. for chart_id in set(self.charts.charts) & set(PEER_CHARTS):
  290. dim_id = peer.name + chart_id[4:]
  291. self.charts[chart_id].hide_dimension(dim_id)
  292. def get_peers(self):
  293. self.request = Base.get_header(operation='readstat')
  294. raw_data = self._get_raw_data(raw=True)
  295. if not raw_data:
  296. return list()
  297. peer_ids = self.get_peer_ids(raw_data)
  298. if not peer_ids:
  299. return list()
  300. new_peers = list()
  301. for peer_id in peer_ids:
  302. self.request = Base.get_header(peer_id)
  303. raw_peer_data = self._get_raw_data()
  304. if not raw_peer_data:
  305. continue
  306. srcadr = re.search(r'(srcadr)=([^,]+)', raw_peer_data)
  307. if not srcadr:
  308. continue
  309. srcadr = srcadr.group(2)
  310. if self.peer_filter.search(srcadr):
  311. continue
  312. stratum = re.search(r'(stratum)=([^,]+)', raw_peer_data)
  313. if not stratum:
  314. continue
  315. if int(stratum.group(2)) > 15:
  316. continue
  317. new_peer = Peer(idx=peer_id, name=srcadr)
  318. new_peers.append(new_peer)
  319. return new_peers
  320. def get_peer_ids(self, res):
  321. """
  322. Unpack the NTP Control Message header
  323. Get data length from header
  324. Get list of association ids returned in the readstat response
  325. """
  326. try:
  327. count = struct.unpack(HEADER_FORMAT, res[:HEADER_LEN])[6]
  328. except struct.error as error:
  329. self.error('error unpacking header: {0}'.format(error))
  330. return None
  331. if not count:
  332. self.error('empty data field in NTP control packet')
  333. return None
  334. data_end = HEADER_LEN + count
  335. data = res[HEADER_LEN:data_end]
  336. data_format = ''.join(['!', 'H' * int(count / 2)])
  337. try:
  338. peer_ids = list(struct.unpack(data_format, data))[::2]
  339. except struct.error as error:
  340. self.error('error unpacking data: {0}'.format(error))
  341. return None
  342. return peer_ids