l2db.py 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. # -*- coding: utf-8 -*-
  2. """Small DB bridge for the out-of-process L2 collectors.
  3. The collectors run on the Docker host, outside the Zabbix PHP container. We
  4. deliberately do not keep a second set of MySQL credentials: the credentials
  5. are read from the already-running Zabbix MySQL container's environment (the
  6. same MYSQL_* values that the official Zabbix compose file passed to MySQL and
  7. Zabbix). Queries are executed inside that container, so the DB host never
  8. needs to be resolvable from the host shell.
  9. """
  10. import json
  11. import os
  12. import subprocess
  13. def _container_name():
  14. return os.environ.get('L2_DB_CONTAINER') or os.environ.get('L2_MYSQL_CONTAINER') or 'zabbix-mysql'
  15. def _container_env():
  16. """Return the real environment of the Zabbix MySQL container as a dict."""
  17. name = _container_name()
  18. out = subprocess.check_output(
  19. ['docker', 'inspect', name, '--format', '{{json .Config.Env}}'],
  20. text=True, timeout=30
  21. )
  22. env = {}
  23. for item in json.loads(out or '[]'):
  24. key, sep, value = str(item).partition('=')
  25. if sep:
  26. env[key] = value
  27. return env
  28. def credentials():
  29. """Return (container, user, password, database) without hard-coded secrets."""
  30. env = _container_env()
  31. user = env.get('MYSQL_USER') or env.get('DB_SERVER_USER') or ''
  32. password = env.get('MYSQL_PASSWORD') or env.get('DB_SERVER_PASS') or ''
  33. database = env.get('MYSQL_DATABASE') or env.get('DB_SERVER_DBNAME') or 'zabbix'
  34. if not user or not password:
  35. raise RuntimeError('Zabbix MySQL container has no MYSQL_USER/MYSQL_PASSWORD')
  36. return _container_name(), user, password, database
  37. def run_sql(sql, timeout=120):
  38. """Execute SQL inside the Zabbix MySQL container.
  39. Returns the completed process. stdin is used instead of argv so long
  40. compressed history blobs never hit the command-line length limit.
  41. """
  42. container, user, password, database = credentials()
  43. cmd = [
  44. 'docker', 'exec', '-i', container, 'mysql',
  45. '-N', '-B', '--default-character-set=utf8mb4',
  46. '-u' + user, '-p' + password, database
  47. ]
  48. return subprocess.run(cmd, input=sql.encode('utf-8'), capture_output=True, timeout=timeout)
  49. def query(sql, timeout=120):
  50. """Run a SELECT and return rows as lists of strings (TSV, no header)."""
  51. r = run_sql(sql, timeout=timeout)
  52. if r.returncode != 0:
  53. raise RuntimeError((r.stderr or b'').decode('utf-8', 'ignore')[:500])
  54. text = (r.stdout or b'').decode('utf-8', 'ignore')
  55. return [line.split('\t') for line in text.splitlines() if line != '']
  56. def scalar(sql, timeout=120):
  57. rows = query(sql, timeout=timeout)
  58. return rows[0][0] if rows and rows[0] else None