diff --git a/.github/config/gitversion.yml b/.github/config/gitversion.yml new file mode 100644 index 0000000..94fdb98 --- /dev/null +++ b/.github/config/gitversion.yml @@ -0,0 +1,11 @@ +next-version: 1.0.0 +assembly-versioning-scheme: MajorMinorPatch +assembly-file-versioning-scheme: MajorMinorPatch + +branches: + master: + regex: ^master$ + mode: ContinuousDelivery + increment: Patch + tag: '' + is-release-branch: true diff --git a/.github/workflows/pull_request_dev.yml b/.github/workflows/pull_request_dev.yml new file mode 100644 index 0000000..641e6c2 --- /dev/null +++ b/.github/workflows/pull_request_dev.yml @@ -0,0 +1,26 @@ +name: Chack code on dev pull request + +on: + pull_request: + branches: + - "dev" + paths: + - "ipsec_exporter.py" + - "src/*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: pylint $(git ls-files '*.py') diff --git a/.github/workflows/pull_request_master.yml b/.github/workflows/pull_request_master.yml new file mode 100644 index 0000000..4af4006 --- /dev/null +++ b/.github/workflows/pull_request_master.yml @@ -0,0 +1,26 @@ +name: Chack code on master pull request + +on: + pull_request: + branches: + - "master" + paths: + - "ipsec_exporter.py" + - "src/*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: pylint $(git ls-files '*.py') diff --git a/.github/workflows/push_dev.yml b/.github/workflows/push_dev.yml new file mode 100644 index 0000000..43c061a --- /dev/null +++ b/.github/workflows/push_dev.yml @@ -0,0 +1,26 @@ +name: Chack code on dev push + +on: + push: + branches: + - "dev" + paths: + - "ipsec_exporter.py" + - "src/*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: pylint $(git ls-files '*.py') diff --git a/.github/workflows/push_master.yml b/.github/workflows/push_master.yml new file mode 100644 index 0000000..9693a27 --- /dev/null +++ b/.github/workflows/push_master.yml @@ -0,0 +1,84 @@ +name: Chack code and publish on master push + +on: + push: + branches: + - "master" + paths: + - "*.py" + +jobs: + check: + name: Code check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: pylint --exit-zero $(git ls-files '*.py') + publish: + needs: check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup GitVersion + uses: gittools/actions/gitversion/setup@v0.9.7 + with: + versionSpec: 5.x + - name: Determine Version + uses: gittools/actions/gitversion/execute@v0.9.7 + id: gitversion + with: + useConfigFile: true + configFilePath: ./.github/config/gitversion.yml + - name: Create zip + uses: ihiroky/archive-action@v1 + with: + root_dir: ./ + file_path: ipsec_exporter_${{steps.gitversion.outputs.version}}.zip + - name: Create tar.gz + uses: ihiroky/archive-action@v1 + with: + root_dir: ./ + file_path: ipsec_exporter_${{steps.gitversion.outputs.version}}.tar.gz + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{steps.gitversion.outputs.version}} + release_name: ${{steps.gitversion.outputs.version}} + body_path: ./RELEASE.md + draft: false + prerelease: false + - name: Upload zip archive + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./ipsec_exporter_${{steps.gitversion.outputs.version}}.zip + asset_name: ipsec_exporter_${{steps.gitversion.outputs.version}}.zip + asset_content_type: application/zip + - name: Upload tar.gz archive + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./ipsec_exporter_${{steps.gitversion.outputs.version}}.tar.gz + asset_name: ipsec_exporter_${{steps.gitversion.outputs.version}}.tar.gz + asset_content_type: application/gzip + diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..599e23a --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +# Changelog + +- Initial version diff --git a/ipsec_exporter.py b/ipsec_exporter.py new file mode 100644 index 0000000..8f94055 --- /dev/null +++ b/ipsec_exporter.py @@ -0,0 +1,5 @@ +from src.app import App + +if __name__ == "__main__": + app = App() + app.main() \ No newline at end of file diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..9b9e262 --- /dev/null +++ b/src/app.py @@ -0,0 +1,42 @@ +from argparse import * +from src.prometheus_metrics_server import * +from src.metrics_source import * + +class App: + args: Namespace + + def __init__(self): + parser = ArgumentParser(description="IPsec Prometheus exporter for Libreswan") + parser.add_argument("-a", "--address", dest="address", required=False, type=str, default="0.0.0.0", help="Server IP address") + parser.add_argument("-p", "--port", dest="port", required=False, type=int, default=9446, help="Server port") + parser.add_argument("-i", "--interval", dest="interval", required=False, type=int, default=1, help="Metrics read interval (in seconds)") + self.args = parser.parse_args() + + def main(self): + server = PrometheusMetricsServer(self.args.port, "IPsec exporter") + server.address = self.args.address + server.interval = self.args.interval + + globalstatus_source = CommandMetricsSource("sudo ipsec globalstatus") + globalstatus_source.add_metric("ipsec_current_states", r"current\.states\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_current_states_iketype", r"current\.states\.iketype\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_current_states_enumerate", r"current\.states\.enumerate\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ipsec_type", r"total\.ipsec\.type\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_traffic", r"total\.(?P\w+)\.traffic\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ike", r"total\.ike\.(?P\w+)\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ikev2_redirect", r"total\.ike\.ikev2\.redirect\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_pamauth", r"total\.pamauth\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_iketcp", r"total\.iketcp\.(?P\w+)\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ike_encr", r"total\.(?P\w+)\.encr\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ike_integ", r"total\.(?P\w+)\.integ\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ike_group", r"total\.(?P\w+)\.group\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ike_notifies_error", r"total\.(?P\w+)\.(?P\w+)\.notifies\.error\.(?P\w+)=(?P\d+)") + globalstatus_source.add_metric("ipsec_total_ikev2_notifies_status", r"total\.ikev2\.(?P\w+)\.notifies\.status\.(?P\w+)=(?P\d+)") + server.add_metrics_source(globalstatus_source) + + custom_metrics_source = CustomMetricsSource() + custom_metrics_source.add_metric(IPsecTrafficCustomMetric("ipsec_traffic")) + server.add_metrics_source(custom_metrics_source) + + server.run() + \ No newline at end of file diff --git a/src/metric.py b/src/metric.py new file mode 100644 index 0000000..cdaf78c --- /dev/null +++ b/src/metric.py @@ -0,0 +1,74 @@ +from abc import abstractmethod +from prometheus_client import * +from re import Pattern +import re +import os + + + +class Metric: + gauge: Gauge + + def __init__(self, name: str, labels: list[str], description: str = ""): + self.gauge = Gauge( + name, + description, + labels + ) + + +class CommandMetric(Metric): + regex: Pattern + + def __init__(self, name: str, regex: str, description: str = ""): + self.regex = re.compile(regex) + + labels = list(self.regex.groupindex.keys()) + labels.remove("VALUE") + + super().__init__(name, labels, description) + + def update(self, command_output: str): + self.gauge.clear() + + results = re.finditer(self.regex, command_output) + + for result in results: + groups = result.groupdict() + value = groups.pop("VALUE") + self.gauge.labels(*list(groups.values())).set(int(value)) + + +class CustomMetric(Metric): + def __init__(self, name: str, labels: list[str], description: str = ""): + super().__init__(name, labels, description) + + @abstractmethod + def update(self): + pass + + +class IPsecTrafficCustomMetric(CustomMetric): + def __init__(self, name: str, description: str = ""): + labels = [ + "lease", + "connection", + "direction" + ] + super().__init__(name, labels, description) + + def update(self): + self.gauge.clear() + + trafficstatus = os.popen("sudo ipsec trafficstatus").read() + + trafficstatus_results = re.finditer(r""""(?P.+)"\[\d+\] \d+\.\d+\.\d+\.\d+, type=\w+, add_time=\d+, inBytes=(?P\d+), outBytes=(?P\d+), maxBytes=.+, id='.+', lease=(?P\d+\.\d+\.\d+\.\d+\/\d+)""", trafficstatus) + + for result in trafficstatus_results: + lease = result.groupdict()["lease"] + connection = result.groupdict()["connection"] + in_value = result.groupdict()["IN_VALUE"] + out_value = result.groupdict()["OUT_VALUE"] + + self.gauge.labels(lease, connection, "in").set(int(in_value)) + self.gauge.labels(lease, connection, "out").set(int(out_value)) \ No newline at end of file diff --git a/src/metrics_source.py b/src/metrics_source.py new file mode 100644 index 0000000..0ba3a6f --- /dev/null +++ b/src/metrics_source.py @@ -0,0 +1,43 @@ +from abc import ABC, abstractmethod +from src.metric import * +import os + + + +class MetricsSource: + _metrics : list[Metric] + + def __init__(self): + self._metrics = [] + + @abstractmethod + def update(self): + pass + + +class CommandMetricsSource(MetricsSource): + command : str + + def __init__(self, command: str): + super().__init__() + self.command = command + + def add_metric(self, name: str, regex: str, description: str = ""): + self._metrics.append(CommandMetric(name, regex, description)) + + def update(self): + output = os.popen(self.command).read() + for metric in self._metrics: + metric.update(output) + + +class CustomMetricsSource(MetricsSource): + def __init__(self): + super().__init__() + + def add_metric(self, custom_metric : CustomMetric): + self._metrics.append(custom_metric) + + def update(self): + for metric in self._metrics: + metric.update() \ No newline at end of file diff --git a/src/prometheus_metrics_server.py b/src/prometheus_metrics_server.py new file mode 100644 index 0000000..3e6d517 --- /dev/null +++ b/src/prometheus_metrics_server.py @@ -0,0 +1,32 @@ +import prometheus_client +import time +from src.metrics_source import * + + + +class PrometheusMetricsServer: + _metrics_sources: list[MetricsSource] + + address: str + port: int + interval: int + server_name: str + + def __init__(self, port: int, server_name: str): + self._metrics_sources = [] + + self.address = "0.0.0.0" + self.port = port + self.interval = 1 + self.server_name = server_name + + def add_metrics_source(self, source: MetricsSource): + self._metrics_sources.append(source) + + def run(self): + prometheus_client.start_http_server(self.port, addr=self.address) + print(f"{self.server_name} is running on {self.address}:{self.port}") + while True: + for source in self._metrics_sources: + source.update() + time.sleep(self.interval) \ No newline at end of file