gen_integrations.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. #!/usr/bin/env python3
  2. import json
  3. import os
  4. import sys
  5. from pathlib import Path
  6. from jsonschema import Draft7Validator, ValidationError
  7. from referencing import Registry, Resource
  8. from referencing.jsonschema import DRAFT7
  9. from ruamel.yaml import YAML, YAMLError
  10. AGENT_REPO = 'netdata/netdata'
  11. GO_REPO = 'netdata/go.d.plugin'
  12. INTEGRATIONS_PATH = Path(__file__).parent
  13. TEMPLATE_PATH = INTEGRATIONS_PATH / 'templates'
  14. OUTPUT_PATH = INTEGRATIONS_PATH / 'integrations.js'
  15. CATEGORIES_FILE = INTEGRATIONS_PATH / 'categories.yaml'
  16. REPO_PATH = INTEGRATIONS_PATH.parent
  17. SCHEMA_PATH = INTEGRATIONS_PATH / 'schemas'
  18. GO_REPO_PATH = REPO_PATH / 'go.d.plugin'
  19. DISTROS_FILE = REPO_PATH / '.github' / 'data' / 'distros.yml'
  20. METADATA_PATTERN = '*/metadata.yaml'
  21. COLLECTOR_SOURCES = [
  22. (AGENT_REPO, REPO_PATH / 'collectors', True),
  23. (AGENT_REPO, REPO_PATH / 'collectors' / 'charts.d.plugin', True),
  24. (AGENT_REPO, REPO_PATH / 'collectors' / 'python.d.plugin', True),
  25. (GO_REPO, GO_REPO_PATH / 'modules', True),
  26. ]
  27. DEPLOY_SOURCES = [
  28. (AGENT_REPO, INTEGRATIONS_PATH / 'deploy.yaml', False),
  29. ]
  30. EXPORTER_SOURCES = [
  31. (AGENT_REPO, REPO_PATH / 'exporting', True),
  32. ]
  33. NOTIFICATION_SOURCES = [
  34. (AGENT_REPO, REPO_PATH / 'health' / 'notifications', True),
  35. (AGENT_REPO, INTEGRATIONS_PATH / 'cloud-notifications' / 'metadata.yaml', False),
  36. ]
  37. COLLECTOR_RENDER_KEYS = [
  38. 'alerts',
  39. 'metrics',
  40. 'overview',
  41. 'related_resources',
  42. 'setup',
  43. 'troubleshooting',
  44. ]
  45. EXPORTER_RENDER_KEYS = [
  46. 'overview',
  47. 'setup',
  48. 'troubleshooting',
  49. ]
  50. NOTIFICATION_RENDER_KEYS = [
  51. 'overview',
  52. 'setup',
  53. 'troubleshooting',
  54. ]
  55. GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS', False)
  56. DEBUG = os.environ.get('DEBUG', False)
  57. def debug(msg):
  58. if GITHUB_ACTIONS:
  59. print(f':debug:{ msg }')
  60. elif DEBUG:
  61. print(f'>>> { msg }')
  62. else:
  63. pass
  64. def warn(msg, path):
  65. if GITHUB_ACTIONS:
  66. print(f':warning file={ path }:{ msg }')
  67. else:
  68. print(f'!!! WARNING:{ path }:{ msg }')
  69. def retrieve_from_filesystem(uri):
  70. path = SCHEMA_PATH / Path(uri)
  71. contents = json.loads(path.read_text())
  72. return Resource.from_contents(contents, DRAFT7)
  73. registry = Registry(retrieve=retrieve_from_filesystem)
  74. CATEGORY_VALIDATOR = Draft7Validator(
  75. {'$ref': './categories.json#'},
  76. registry=registry,
  77. )
  78. DEPLOY_VALIDATOR = Draft7Validator(
  79. {'$ref': './deploy.json#'},
  80. registry=registry,
  81. )
  82. EXPORTER_VALIDATOR = Draft7Validator(
  83. {'$ref': './exporter.json#'},
  84. registry=registry,
  85. )
  86. NOTIFICATION_VALIDATOR = Draft7Validator(
  87. {'$ref': './notification.json#'},
  88. registry=registry,
  89. )
  90. COLLECTOR_VALIDATOR = Draft7Validator(
  91. {'$ref': './collector.json#'},
  92. registry=registry,
  93. )
  94. _jinja_env = False
  95. def get_jinja_env():
  96. global _jinja_env
  97. if not _jinja_env:
  98. from jinja2 import Environment, FileSystemLoader, select_autoescape
  99. _jinja_env = Environment(
  100. loader=FileSystemLoader(TEMPLATE_PATH),
  101. autoescape=select_autoescape(),
  102. block_start_string='[%',
  103. block_end_string='%]',
  104. variable_start_string='[[',
  105. variable_end_string=']]',
  106. comment_start_string='[#',
  107. comment_end_string='#]',
  108. trim_blocks=True,
  109. lstrip_blocks=True,
  110. )
  111. return _jinja_env
  112. def get_category_sets(categories):
  113. default = set()
  114. valid = set()
  115. for c in categories:
  116. if 'id' in c:
  117. valid.add(c['id'])
  118. if c.get('collector_default', False):
  119. default.add(c['id'])
  120. if 'children' in c and c['children']:
  121. d, v = get_category_sets(c['children'])
  122. default |= d
  123. valid |= v
  124. return (default, valid)
  125. def get_collector_metadata_entries():
  126. ret = []
  127. for r, d, m in COLLECTOR_SOURCES:
  128. if d.exists() and d.is_dir() and m:
  129. for item in d.glob(METADATA_PATTERN):
  130. ret.append((r, item))
  131. elif d.exists() and d.is_file() and not m:
  132. if d.match(METADATA_PATTERN):
  133. ret.append(d)
  134. return ret
  135. def load_yaml(src):
  136. yaml = YAML(typ='safe')
  137. if not src.is_file():
  138. warn(f'{ src } is not a file.', src)
  139. return False
  140. try:
  141. contents = src.read_text()
  142. except (IOError, OSError):
  143. warn(f'Failed to read { src }.', src)
  144. return False
  145. try:
  146. data = yaml.load(contents)
  147. except YAMLError:
  148. warn(f'Failed to parse { src } as YAML.', src)
  149. return False
  150. return data
  151. def load_categories():
  152. categories = load_yaml(CATEGORIES_FILE)
  153. if not categories:
  154. sys.exit(1)
  155. try:
  156. CATEGORY_VALIDATOR.validate(categories)
  157. except ValidationError:
  158. warn(f'Failed to validate { CATEGORIES_FILE } against the schema.', CATEGORIES_FILE)
  159. sys.exit(1)
  160. return categories
  161. def load_collectors():
  162. ret = []
  163. entries = get_collector_metadata_entries()
  164. for repo, path in entries:
  165. debug(f'Loading { path }.')
  166. data = load_yaml(path)
  167. if not data:
  168. continue
  169. try:
  170. COLLECTOR_VALIDATOR.validate(data)
  171. except ValidationError:
  172. warn(f'Failed to validate { path } against the schema.', path)
  173. continue
  174. for idx, item in enumerate(data['modules']):
  175. item['meta']['plugin_name'] = data['plugin_name']
  176. item['integration_type'] = 'collector'
  177. item['_src_path'] = path
  178. item['_repo'] = repo
  179. item['_index'] = idx
  180. ret.append(item)
  181. return ret
  182. def _load_deploy_file(file, repo):
  183. ret = []
  184. debug(f'Loading { file }.')
  185. data = load_yaml(file)
  186. if not data:
  187. return []
  188. try:
  189. DEPLOY_VALIDATOR.validate(data)
  190. except ValidationError:
  191. warn(f'Failed to validate { file } against the schema.', file)
  192. return []
  193. for idx, item in enumerate(data):
  194. item['integration_type'] = 'deploy'
  195. item['_src_path'] = file
  196. item['_repo'] = repo
  197. item['_index'] = idx
  198. ret.append(item)
  199. return ret
  200. def load_deploy():
  201. ret = []
  202. for repo, path, match in DEPLOY_SOURCES:
  203. if match and path.exists() and path.is_dir():
  204. for file in path.glob(METADATA_PATTERN):
  205. ret.extend(_load_deploy_file(file, repo))
  206. elif not match and path.exists() and path.is_file():
  207. ret.extend(_load_deploy_file(path, repo))
  208. return ret
  209. def _load_exporter_file(file, repo):
  210. debug(f'Loading { file }.')
  211. data = load_yaml(file)
  212. if not data:
  213. return []
  214. try:
  215. EXPORTER_VALIDATOR.validate(data)
  216. except ValidationError:
  217. warn(f'Failed to validate { file } against the schema.', file)
  218. return []
  219. if 'id' in data:
  220. data['integration_type'] = 'exporter'
  221. data['_src_path'] = file
  222. data['_repo'] = repo
  223. data['_index'] = 0
  224. return [data]
  225. else:
  226. ret = []
  227. for idx, item in enumerate(data):
  228. item['integration_type'] = 'exporter'
  229. item['_src_path'] = file
  230. item['_repo'] = repo
  231. item['_index'] = idx
  232. ret.append(item)
  233. return ret
  234. def load_exporters():
  235. ret = []
  236. for repo, path, match in EXPORTER_SOURCES:
  237. if match and path.exists() and path.is_dir():
  238. for file in path.glob(METADATA_PATTERN):
  239. ret.extend(_load_exporter_file(file, repo))
  240. elif not match and path.exists() and path.is_file():
  241. ret.extend(_load_exporter_file(path, repo))
  242. return ret
  243. def _load_notification_file(file, repo):
  244. debug(f'Loading { file }.')
  245. data = load_yaml(file)
  246. if not data:
  247. return []
  248. try:
  249. NOTIFICATION_VALIDATOR.validate(data)
  250. except ValidationError:
  251. warn(f'Failed to validate { file } against the schema.', file)
  252. return []
  253. if 'id' in data:
  254. data['integration_type'] = 'notification'
  255. data['_src_path'] = file
  256. data['_repo'] = repo
  257. data['_index'] = 0
  258. return [data]
  259. else:
  260. ret = []
  261. for idx, item in enumerate(data):
  262. item['integration_type'] = 'notification'
  263. item['_src_path'] = file
  264. item['_repo'] = repo
  265. item['_index'] = idx
  266. ret.append(item)
  267. return ret
  268. def load_notifications():
  269. ret = []
  270. for repo, path, match in NOTIFICATION_SOURCES:
  271. if match and path.exists() and path.is_dir():
  272. for file in path.glob(METADATA_PATTERN):
  273. ret.extend(_load_notification_file(file, repo))
  274. elif not match and path.exists() and path.is_file():
  275. ret.extend(_load_notification_file(path, repo))
  276. return ret
  277. def make_id(meta):
  278. if 'monitored_instance' in meta:
  279. instance_name = meta['monitored_instance']['name'].replace(' ', '_')
  280. elif 'instance_name' in meta:
  281. instance_name = meta['instance_name']
  282. else:
  283. instance_name = '000_unknown'
  284. return f'{ meta["plugin_name"] }-{ meta["module_name"] }-{ instance_name }'
  285. def make_edit_link(item):
  286. if item['_repo'] == 'netdata/go.d.plugin':
  287. item_path = item['_src_path'].relative_to(GO_REPO_PATH)
  288. else:
  289. item_path = item['_src_path'].relative_to(REPO_PATH)
  290. return f'https://github.com/{ item["_repo"] }/blob/master/{ item_path }'
  291. def sort_integrations(integrations):
  292. integrations.sort(key=lambda i: i['_index'])
  293. integrations.sort(key=lambda i: i['_src_path'])
  294. integrations.sort(key=lambda i: i['id'])
  295. def dedupe_integrations(integrations, ids):
  296. tmp_integrations = []
  297. for i in integrations:
  298. if ids.get(i['id'], False):
  299. first_path, first_index = ids[i['id']]
  300. warn(f'Duplicate integration ID found at { i["_src_path"] } index { i["_index"] } (original definition at { first_path } index { first_index }), ignoring that integration.', i['_src_path'])
  301. else:
  302. tmp_integrations.append(i)
  303. ids[i['id']] = (i['_src_path'], i['_index'])
  304. return tmp_integrations, ids
  305. def render_collectors(categories, collectors, ids):
  306. debug('Computing default categories.')
  307. default_cats, valid_cats = get_category_sets(categories)
  308. debug('Generating collector IDs.')
  309. for item in collectors:
  310. item['id'] = make_id(item['meta'])
  311. debug('Sorting collectors.')
  312. sort_integrations(collectors)
  313. debug('Removing duplicate collectors.')
  314. collectors, ids = dedupe_integrations(collectors, ids)
  315. idmap = {i['id']: i for i in collectors}
  316. for item in collectors:
  317. debug(f'Processing { item["id"] }.')
  318. related = []
  319. for res in item['meta']['related_resources']['integrations']['list']:
  320. res_id = make_id(res)
  321. if res_id not in idmap.keys():
  322. warn(f'Could not find related integration { res_id }, ignoring it.', item['_src_path'])
  323. continue
  324. related.append({
  325. 'plugin_name': res['plugin_name'],
  326. 'module_name': res['module_name'],
  327. 'id': res_id,
  328. 'name': idmap[res_id]['meta']['monitored_instance']['name'],
  329. 'info': idmap[res_id]['meta']['info_provided_to_referring_integrations'],
  330. })
  331. item_cats = set(item['meta']['monitored_instance']['categories'])
  332. bogus_cats = item_cats - valid_cats
  333. actual_cats = item_cats & valid_cats
  334. if bogus_cats:
  335. warn(f'Ignoring invalid categories: { ", ".join(bogus_cats) }', item["_src_path"])
  336. if not item_cats:
  337. item['meta']['monitored_instance']['categories'] = list(default_cats)
  338. warn(f'{ item["id"] } does not list any caregories, adding it to: { default_cats }', item["_src_path"])
  339. else:
  340. item['meta']['monitored_instance']['categories'] = list(actual_cats)
  341. for scope in item['metrics']['scopes']:
  342. if scope['name'] == 'global':
  343. scope['name'] = f'{ item["meta"]["monitored_instance"]["name"] } instance'
  344. for cfg_example in item['setup']['configuration']['examples']['list']:
  345. if 'folding' not in cfg_example:
  346. cfg_example['folding'] = {
  347. 'enabled': item['setup']['configuration']['examples']['folding']['enabled']
  348. }
  349. for key in COLLECTOR_RENDER_KEYS:
  350. if key in item.keys():
  351. template = get_jinja_env().get_template(f'{ key }.md')
  352. data = template.render(entry=item, related=related)
  353. if 'variables' in item['meta']['monitored_instance']:
  354. template = get_jinja_env().from_string(data)
  355. data = template.render(variables=item['meta']['monitored_instance']['variables'])
  356. else:
  357. data = ''
  358. item[key] = data
  359. item['edit_link'] = make_edit_link(item)
  360. del item['_src_path']
  361. del item['_repo']
  362. del item['_index']
  363. return collectors, ids
  364. def render_deploy(distros, categories, deploy, ids):
  365. debug('Sorting deployments.')
  366. sort_integrations(deploy)
  367. debug('Checking deployment ids.')
  368. deploy, ids = dedupe_integrations(deploy, ids)
  369. template = get_jinja_env().get_template('platform_info.md')
  370. for item in deploy:
  371. debug(f'Processing { item["id"] }.')
  372. if item['platform_info']['group']:
  373. entries = [
  374. {
  375. 'version': i['version'],
  376. 'support': i['support_type'],
  377. 'arches': i.get('packages', {'arches': []})['arches'],
  378. 'notes': i['notes'],
  379. } for i in distros[item['platform_info']['group']] if i['distro'] == item['platform_info']['distro']
  380. ]
  381. else:
  382. entries = []
  383. data = template.render(entries=entries)
  384. item['platform_info'] = data
  385. item['edit_link'] = make_edit_link(item)
  386. del item['_src_path']
  387. del item['_repo']
  388. del item['_index']
  389. return deploy, ids
  390. def render_exporters(categories, exporters, ids):
  391. debug('Sorting exporters.')
  392. sort_integrations(exporters)
  393. debug('Checking exporter ids.')
  394. exporters, ids = dedupe_integrations(exporters, ids)
  395. for item in exporters:
  396. for key in EXPORTER_RENDER_KEYS:
  397. if key in item.keys():
  398. template = get_jinja_env().get_template(f'{ key }.md')
  399. data = template.render(entry=item)
  400. if 'variables' in item['meta']:
  401. template = get_jinja_env().from_string(data)
  402. data = template.render(variables=item['meta']['variables'])
  403. else:
  404. data = ''
  405. item[key] = data
  406. item['edit_link'] = make_edit_link(item)
  407. del item['_src_path']
  408. del item['_repo']
  409. del item['_index']
  410. return exporters, ids
  411. def render_notifications(categories, notifications, ids):
  412. debug('Sorting notifications.')
  413. sort_integrations(notifications)
  414. debug('Checking notification ids.')
  415. notifications, ids = dedupe_integrations(notifications, ids)
  416. for item in notifications:
  417. for key in NOTIFICATION_RENDER_KEYS:
  418. if key in item.keys():
  419. template = get_jinja_env().get_template(f'{ key }.md')
  420. data = template.render(entry=item)
  421. if 'variables' in item['meta']:
  422. template = get_jinja_env().from_string(data)
  423. data = template.render(variables=item['meta']['variables'])
  424. else:
  425. data = ''
  426. item[key] = data
  427. item['edit_link'] = make_edit_link(item)
  428. del item['_src_path']
  429. del item['_repo']
  430. del item['_index']
  431. return notifications, ids
  432. def render_integrations(categories, integrations):
  433. template = get_jinja_env().get_template('integrations.js')
  434. data = template.render(
  435. categories=json.dumps(categories),
  436. integrations=json.dumps(integrations),
  437. )
  438. OUTPUT_PATH.write_text(data)
  439. def main():
  440. categories = load_categories()
  441. distros = load_yaml(DISTROS_FILE)
  442. collectors = load_collectors()
  443. deploy = load_deploy()
  444. exporters = load_exporters()
  445. notifications = load_notifications()
  446. collectors, ids = render_collectors(categories, collectors, dict())
  447. deploy, ids = render_deploy(distros, categories, deploy, ids)
  448. exporters, ids = render_exporters(categories, exporters, ids)
  449. notifications, ids = render_notifications(categories, notifications, ids)
  450. integrations = collectors + deploy + exporters + notifications
  451. render_integrations(categories, integrations)
  452. if __name__ == '__main__':
  453. sys.exit(main())