summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authornico <nico@magicbroccoli.de>2020-08-27 18:10:23 +0200
committernico <nico@magicbroccoli.de>2020-08-27 18:10:23 +0200
commit893b93379fffd91c18a21315dc0b5c5d7285494c (patch)
treef17f35d763fa069ea6cec46ed199eb8e4a2fe6aa
Initial working releaseHEADmaster
TeamSpeak InfluxDB exporter base of ejabberd-tools framework + implement TeamSpeak REST api call to gather statistics + implement metrics logic to omit unnecessary data points + add systemd service file + implement pre-commit framework
-rw-r--r--.gitignore116
-rw-r--r--.pre-commit-config.yaml25
-rw-r--r--api.py37
-rw-r--r--config.py76
-rw-r--r--contrib/init/linux-systemd/teamspeak-influx.service23
-rw-r--r--influx.py128
-rw-r--r--metrics.py24
-rw-r--r--pyproject.toml20
-rw-r--r--requirements.txt3
-rw-r--r--setup.cfg5
-rw-r--r--teamspeak-influx.yaml.template13
11 files changed, 470 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a87969a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,116 @@
+# Created by https://www.gitignore.io/api/python
+# Edit at https://www.gitignore.io/?templates=python
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# pyenv
+.python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# Mr Developer
+.mr.developer.cfg
+.project
+.pydevproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# Pycharm
+.idea/
+.venv/
+venv
+
+# config
+teamspeak-influx.yml
+config.yml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..44cb32f
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,25 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v3.2.0
+ hooks:
+ - id: check-docstring-first
+ - id: check-executables-have-shebangs
+ - id: fix-encoding-pragma
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ args:
+ - "--fix=lf"
+ - id: check-yaml
+
+- repo: https://github.com/psf/black
+ rev: 19.10b0
+ hooks:
+ - id: black
+
+- repo: https://gitlab.com/pycqa/flake8
+ rev: 3.8.3
+ hooks:
+ - id: flake8
diff --git a/api.py b/api.py
new file mode 100644
index 0000000..9cfb820
--- /dev/null
+++ b/api.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import requests
+
+
+class TeamSpeakApi:
+ """
+ class to interact with the teamspeak rest api
+ """
+
+ def __init__(self, apikey: str, url: str):
+ # variables
+ self._apikey = apikey
+ self._url = url
+
+ # define api handler
+ self.cmd = self._rest
+
+ self.session = requests.Session()
+
+ @property
+ def _auth(self):
+ if self._apikey is not None:
+ return {"x-api-key": self._apikey}
+
+ def _rest(self, command: str) -> dict:
+ # add authentication header to the session obj
+ auth_header = self._auth
+
+ # build get request
+ r = self.session.get("/".join([self._url, command]), headers=auth_header, verify=False)
+
+ # proceed if response is ok
+ if r.ok:
+ return r.json()
+
+ return {}
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..c3db1b4
--- /dev/null
+++ b/config.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import sys
+from pathlib import Path
+
+from ruamel.yaml import YAML
+from ruamel.yaml.parser import ParserError
+from ruamel.yaml.scanner import ScannerError
+
+
+class Config:
+ def __init__(self):
+ # class variables
+ self.content = None
+
+ # select config file
+ if Path.exists(Path("teamspeak-influx.yml")):
+ self.conf_file = Path("teamspeak-influx.yml")
+ else:
+ self.conf_file = Path("/etc/teamspeak-influx.yml")
+
+ # read config file
+ self._read()
+
+ def _read(self):
+ """init the config object with this method"""
+ self._check()
+
+ # open file as an iostream
+ with open(self.conf_file, "r", encoding="utf-8") as f:
+ try:
+ self.content = YAML(typ="safe").load(f)
+
+ # catch json decoding errors
+ except (ParserError, ScannerError) as err:
+ print(err, file=sys.stderr)
+ exit(1)
+
+ def _check(self):
+ """internal method to check if the config file exists"""
+ try:
+ # if file is present continue
+ if self.conf_file.exists():
+ return
+
+ # if not create a blank file
+ else:
+ self.conf_file.touch(mode=0o640)
+
+ # catch permission exceptions as this tries to write to /etc/
+ except PermissionError as err:
+ print(err, file=sys.stderr)
+ sys.exit(err.errno)
+
+ def get(self, key: str = None, default: (str, int) = None) -> (dict, str, int, None):
+ """
+ method to retrieve all config values, a single value
+ or the optional default value
+ """
+ # if a special key is request, return only that value
+ if key is not None:
+
+ # safety measure
+ if key in self.content:
+ return self.content[key]
+
+ # if a default value is given return that
+ if default is not None:
+ return default
+
+ # if the key isn't part if self.content return None
+ else:
+ return None
+
+ # else return everything
+ return self.content
diff --git a/contrib/init/linux-systemd/teamspeak-influx.service b/contrib/init/linux-systemd/teamspeak-influx.service
new file mode 100644
index 0000000..6ff9978
--- /dev/null
+++ b/contrib/init/linux-systemd/teamspeak-influx.service
@@ -0,0 +1,23 @@
+[Unit]
+Description=teamspeak influxdb exporter
+Requires=teamspeak.service
+
+[Service]
+Type=simple
+User=nobody
+Group=nogroup
+Environment="PATH=/opt/teamspeak-influx/venv/bin:/usr/local/bin:/usr/bin:/bin"
+ExecStart=/opt/teamspeak-influx/influx.py
+WorkingDirectory=/opt/teamspeak-influx/
+PrivateDevices=true
+ProtectControlGroups=true
+ProtectHome=true
+ProtectKernelTunables=true
+ProtectKernelModules=yes
+ProtectSystem=full
+NoNewPrivileges=yes
+Restart=always
+RestartSec=5s
+
+[Install]
+WantedBy=multi-user.target
diff --git a/influx.py b/influx.py
new file mode 100644
index 0000000..e2488ed
--- /dev/null
+++ b/influx.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import time
+import string
+
+from influxdb import InfluxDBClient
+
+from config import Config
+from metrics import TeamSpeakMetrics
+
+keys = [
+ "connection_bytes_received_control",
+ "connection_bytes_received_keepalive",
+ "connection_bytes_received_speech",
+ "connection_bytes_received_total",
+ "connection_bytes_sent_control",
+ "connection_bytes_sent_keepalive",
+ "connection_bytes_sent_speech",
+ "connection_bytes_sent_total",
+ "connection_filetransfer_bytes_received_total",
+ "connection_filetransfer_bytes_sent_total",
+ "connection_packets_received_control",
+ "connection_packets_received_keepalive",
+ "connection_packets_received_speech",
+ "connection_packets_received_total",
+ "connection_packets_sent_control",
+ "connection_packets_sent_keepalive",
+ "connection_packets_sent_speech",
+ "connection_packets_sent_total",
+ "virtualserver_autostart",
+ "virtualserver_channelsonline",
+ "virtualserver_clientsonline",
+ "virtualserver_maxclients",
+ "virtualserver_queryclientsonline",
+ "virtualserver_reserved_slots",
+ "virtualserver_status",
+ "virtualserver_total_packetloss_control",
+ "virtualserver_total_packetloss_keepalive",
+ "virtualserver_total_packetloss_speech",
+ "virtualserver_total_packetloss_total",
+ "virtualserver_total_ping",
+ "virtualserver_uptime",
+]
+
+
+class Influx:
+ def __init__(self, data, cld):
+ self._metrics = data
+ self.client = cld
+
+ @staticmethod
+ def _timestamp():
+ return int(time.time() * 1000)
+
+ @staticmethod
+ def _rmspace(key: str = None, value: (str, int) = None):
+ try:
+ key = key.replace(" ", "\ ") # noqa: W605
+ value = value.replace(" ", "\ ") # noqa: W605
+ except (TypeError, AttributeError):
+ pass
+
+ return key, value
+
+ def _parse(self, name, key, value, ts, tags=None):
+ output = name
+
+ # check if tags is a dict
+ if isinstance(tags, dict):
+
+ # create tag_key=tag_value pairs for all elements and append them to name
+ for k, v in tags.items():
+ output += ",{}={}".format(*self._rmspace(k, v))
+
+ # append key=value to name
+ if value[0] in string.ascii_letters:
+ output += ' {}="{}" {}'.format(*self._rmspace(key, value), ts)
+ elif value.isdigit():
+ output += " {}={}i {}".format(*self._rmspace(key, value), ts)
+ else:
+ output += " {}={} {}".format(*self._rmspace(key, value), ts)
+
+ return output
+
+ def write_metrics(self):
+ data = list()
+ cur_ts = self._timestamp()
+
+ # serverinfo
+ for id in ids:
+ query = self._metrics.serverinfo(id)
+
+ name, sid = query["virtualserver_name"], query["virtualserver_id"]
+ for key, value in query.items():
+ if key in keys:
+ data.append(self._parse("teamspeak", key, value, cur_ts, {"name": name, "virtual_server": sid}))
+
+ self.client.write_points(data, time_precision="ms", batch_size=10000, protocol="line")
+
+
+if __name__ == "__main__":
+ # load config
+ config = Config()
+
+ # credentials and parameters
+ url = config.get("url", default="https://localhost:10443")
+ apikey = config.get("apikey", default="")
+ ids = config.get("ids", default=1)
+
+ # config influxdb
+ influx_host = config.get("influxdb_host", default="localhost")
+ influx_port = config.get("influxdb_port", default=8086)
+ influx_dbname = config.get("influxdb_db", default="teamspeak")
+
+ # init handler
+ metrics = TeamSpeakMetrics(url=url, apikey=apikey)
+ client = InfluxDBClient(host=influx_host, port=influx_port, database=influx_dbname, retries=5)
+
+ # create database only once
+ client.create_database(influx_dbname)
+
+ # init influx class
+ influx = Influx(metrics, client)
+
+ while True:
+ influx.write_metrics()
+
+ time.sleep(10)
diff --git a/metrics.py b/metrics.py
new file mode 100644
index 0000000..af07f1d
--- /dev/null
+++ b/metrics.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+from api import TeamSpeakApi
+
+
+class TeamSpeakMetrics(TeamSpeakApi):
+ """
+ TeamSpeak metrics harvester
+ """
+
+ def __init__(self, url: str, apikey: str = None):
+ # init teamspeak api
+ super().__init__(apikey=apikey, url=url)
+
+ # variables
+ self.url = url
+
+ def serverinfo(self, id: int = 1) -> dict:
+ tmp = self.cmd(f"{id}/serverinfo")
+
+ if tmp["status"]["code"] != 0:
+ return {}
+ else:
+ return tmp["body"][0]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..e834e5c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,20 @@
+[tool.black]
+line-length = 120
+target-version = ['py37', 'py38']
+include = '\.pyi?$'
+exclude = '''
+(
+ /(
+ \.eggs # exclude a few common directories in the
+ | \.git # root of the project
+ | \.hg
+ | \.mypy_cache
+ | \.tox
+ | \.venv
+ | _build
+ | buck-out
+ | build
+ | dist
+ )/ # the root of the project
+)
+'''
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..6402d0a
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+influxdb>=5.2.0
+requests>=2.21.0
+ruamel.yaml
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..76eda04
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[flake8]
+ignore = E501,E203
+exclude = .git,__pycache__,.gitlab
+max-complexity = 15
+max-line-length = 120
diff --git a/teamspeak-influx.yaml.template b/teamspeak-influx.yaml.template
new file mode 100644
index 0000000..a446bd2
--- /dev/null
+++ b/teamspeak-influx.yaml.template
@@ -0,0 +1,13 @@
+# endpoint url
+url: "https://127.0.0.1:10443"
+
+# virtual server ids
+ids: [1,2,3]
+
+# authentication
+apikey: "loreipsumloreipsumloreipsum"
+
+# influx db configuration
+influxdb_host: "localhost"
+influxdb_port: 8086
+influxdb_db: "example"