mirror of
https://github.com/malarinv/iperf3-monitor.git
synced 2026-11-06 14:50:28 +00:00
Compare commits
19 Commits
a3cf2ca10b
...
v0.2.6
| Author | SHA1 | Date | |
|---|---|---|---|
| ec0a16353d | |||
| 0d93c9ea67 | |||
| 587290f1fb | |||
| 24904ef084 | |||
| 966985dc3e | |||
| d3cb92eb0f | |||
| 4cce553441 | |||
| a0ecc5c11a | |||
| 5fa41a6aad | |||
| 49fb881f24 | |||
| e54a02ad8c | |||
| 0a3249f30b | |||
| 1eeb4b20df | |||
| 0c490e95d2 | |||
| 81b771d1ee | |||
| 3e21f978ee | |||
| 458b786ff4 | |||
| 96be13a23c | |||
| 8d51afc24e |
59
.github/workflows/ci.yaml
vendored
59
.github/workflows/ci.yaml
vendored
@@ -9,6 +9,26 @@ env:
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
test-exporter:
|
||||
name: Run Exporter Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r exporter/requirements.txt
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
python -m unittest exporter/test_exporter.py
|
||||
|
||||
validate-chart:
|
||||
name: Validate Helm Chart
|
||||
runs-on: ubuntu-latest
|
||||
@@ -19,7 +39,16 @@ jobs:
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
version: v3.10.0 # Using a specific version, can be updated
|
||||
|
||||
- name: Add Helm repositories
|
||||
run: |
|
||||
helm repo add bjw-s https://bjw-s-labs.github.io/helm-charts/ --force-update
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts --force-update
|
||||
helm repo update
|
||||
|
||||
- name: Build Helm chart dependencies
|
||||
run: helm dependency build ./charts/iperf3-monitor
|
||||
|
||||
- name: Helm Lint
|
||||
run: helm lint ./charts/iperf3-monitor
|
||||
@@ -27,23 +56,49 @@ jobs:
|
||||
build:
|
||||
name: Build Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # Needed to checkout the repository
|
||||
packages: write # Needed to push Docker images to GHCR
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
# Tag with the PR number if it's a pull request event
|
||||
type=match,pattern=pull_request,value=pr-{{number}}
|
||||
# Tag with the git SHA
|
||||
type=sha,prefix=
|
||||
# Tag with 'latest' if on the main branch (though this workflow only runs on PRs to main)
|
||||
type=ref,event=branch,pattern=main,value=latest
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: ./exporter
|
||||
push: false # Do not push on PRs
|
||||
# Push the image if the event is a pull request.
|
||||
# The workflow currently only triggers on pull_request events.
|
||||
push: ${{ github.event_name == 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
test:
|
||||
name: Run Tests
|
||||
|
||||
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
@@ -17,11 +17,33 @@ jobs:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r exporter/requirements.txt
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
python -m unittest exporter/test_exporter.py
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v3
|
||||
with:
|
||||
version: v3.10.0
|
||||
|
||||
- name: Add Helm repositories
|
||||
run: |
|
||||
helm repo add bjw-s https://bjw-s-labs.github.io/helm-charts/ --force-update
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts --force-update
|
||||
helm repo update
|
||||
|
||||
- name: Build Helm chart dependencies
|
||||
run: helm dependency build ./charts/iperf3-monitor
|
||||
|
||||
- name: Helm Lint
|
||||
run: helm lint ./charts/iperf3-monitor
|
||||
|
||||
@@ -36,6 +58,12 @@ jobs:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -48,6 +76,11 @@ jobs:
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
# This ensures that for a git tag like "v0.1.0",
|
||||
# an image tag "0.1.0" is generated.
|
||||
# It will also generate "latest" for the most recent semver tag.
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
@@ -56,6 +89,7 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
package-and-publish-chart:
|
||||
name: Package and Publish Helm Chart
|
||||
@@ -79,6 +113,15 @@ jobs:
|
||||
sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq &&\
|
||||
sudo chmod +x /usr/bin/yq
|
||||
|
||||
- name: Add Helm repositories
|
||||
run: |
|
||||
helm repo add bjw-s https://bjw-s-labs.github.io/helm-charts/ --force-update
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts --force-update
|
||||
helm repo update
|
||||
|
||||
- name: Build Helm chart dependencies
|
||||
run: helm dependency build ./charts/iperf3-monitor
|
||||
|
||||
- name: Set Chart Version from Tag
|
||||
run: |
|
||||
VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//')
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -36,4 +36,8 @@ Thumbs.db
|
||||
|
||||
# Helm
|
||||
!charts/iperf3-monitor/.helmignore
|
||||
charts/*.tgz # Ignore packaged chart files
|
||||
charts/iperf3-monitor/charts/
|
||||
|
||||
# Rendered Kubernetes manifests (for local testing)
|
||||
rendered-manifests.yaml
|
||||
rendered-manifests-updated.yaml
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
120
README.md
120
README.md
@@ -74,37 +74,123 @@ nameOverride: ""
|
||||
# -- Override the fully qualified app name.
|
||||
fullnameOverride: ""
|
||||
|
||||
exporter:
|
||||
# -- Configuration for the exporter container image.
|
||||
image:
|
||||
# -- The container image repository for the exporter.
|
||||
repository: ghcr.io/malarinv/iperf3-monitor
|
||||
# -- The container image tag for the exporter. If not set, the chart's appVersion is used.
|
||||
tag: ""
|
||||
# -- The image pull policy for the exporter container.
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# -- Number of exporter pod replicas. Typically 1 is sufficient.
|
||||
replicaCount: 1
|
||||
# Exporter Configuration (`controllers.exporter`)
|
||||
# The iperf3 exporter is managed under the `controllers.exporter` section,
|
||||
# leveraging the `bjw-s/common-library` for robust workload management.
|
||||
controllers:
|
||||
exporter:
|
||||
# -- Enable the exporter controller.
|
||||
enabled: true
|
||||
# -- Set the controller type for the exporter.
|
||||
# Valid options are "deployment" or "daemonset".
|
||||
# Use "daemonset" for N-to-N node monitoring where an exporter runs on each node (or selected nodes).
|
||||
# Use "deployment" for a centralized exporter (typically with replicaCount: 1).
|
||||
# @default -- "deployment"
|
||||
type: deployment
|
||||
# -- Number of desired exporter pods. Only used if type is "deployment".
|
||||
# @default -- 1
|
||||
replicas: 1
|
||||
|
||||
# -- Application-specific configuration for the iperf3 exporter.
|
||||
# These values are used to populate environment variables for the exporter container.
|
||||
appConfig:
|
||||
# -- Interval in seconds between complete test cycles (i.e., testing all server nodes).
|
||||
testInterval: 300
|
||||
|
||||
# -- Log level for the iperf3 exporter (e.g., DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
||||
logLevel: INFO
|
||||
# -- Timeout in seconds for a single iperf3 test run.
|
||||
testTimeout: 10
|
||||
|
||||
# -- Protocol to use for testing (tcp or udp).
|
||||
testProtocol: tcp
|
||||
# -- iperf3 server port to connect to. Should match the server's listening port.
|
||||
serverPort: "5201"
|
||||
# -- Label selector to find iperf3 server pods.
|
||||
# This is templated. Default: 'app.kubernetes.io/name=<chart-name>,app.kubernetes.io/instance=<release-name>,app.kubernetes.io/component=server'
|
||||
serverLabelSelector: 'app.kubernetes.io/name={{ include "iperf3-monitor.name" . }},app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=server'
|
||||
|
||||
# -- CPU and memory resource requests and limits for the exporter pod.
|
||||
# @default -- A small default is provided if commented out.
|
||||
resources: {}
|
||||
# -- Pod-level configurations for the exporter.
|
||||
pod:
|
||||
# -- Annotations for the exporter pod.
|
||||
annotations: {}
|
||||
# -- Labels for the exporter pod (the common library adds its own defaults too).
|
||||
labels: {}
|
||||
# -- Node selector for scheduling exporter pods. Useful for DaemonSet or specific scheduling with Deployments.
|
||||
# Example:
|
||||
# nodeSelector:
|
||||
# kubernetes.io/os: linux
|
||||
nodeSelector: {}
|
||||
# -- Tolerations for scheduling exporter pods.
|
||||
# Example:
|
||||
# tolerations:
|
||||
# - key: "node-role.kubernetes.io/control-plane"
|
||||
# operator: "Exists"
|
||||
# effect: "NoSchedule"
|
||||
tolerations: []
|
||||
# -- Affinity rules for scheduling exporter pods.
|
||||
# Example:
|
||||
# affinity:
|
||||
# nodeAffinity:
|
||||
# requiredDuringSchedulingIgnoredDuringExecution:
|
||||
# nodeSelectorTerms:
|
||||
# - matchExpressions:
|
||||
# - key: "kubernetes.io/arch"
|
||||
# operator: In
|
||||
# values:
|
||||
# - amd64
|
||||
affinity: {}
|
||||
# -- Security context for the exporter pod.
|
||||
# securityContext:
|
||||
# fsGroup: 65534
|
||||
# runAsUser: 65534
|
||||
# runAsGroup: 65534
|
||||
# runAsNonRoot: true
|
||||
securityContext: {}
|
||||
# -- Automount service account token for the pod.
|
||||
automountServiceAccountToken: true
|
||||
|
||||
# -- Container-level configurations for the main exporter container.
|
||||
containers:
|
||||
exporter: # Name of the primary container
|
||||
image:
|
||||
repository: ghcr.io/malarinv/iperf3-monitor
|
||||
tag: "" # Defaults to .Chart.AppVersion
|
||||
pullPolicy: IfNotPresent
|
||||
# -- Custom environment variables for the exporter container.
|
||||
# These are merged with the ones generated from appConfig.
|
||||
# env:
|
||||
# MY_CUSTOM_VAR: "my_value"
|
||||
env: {}
|
||||
# -- Ports for the exporter container.
|
||||
ports:
|
||||
metrics: # Name of the port
|
||||
port: 9876 # Container port for metrics
|
||||
protocol: TCP
|
||||
enabled: true
|
||||
# -- CPU and memory resource requests and limits.
|
||||
# resources:
|
||||
# requests:
|
||||
# cpu: "100m"
|
||||
# memory: "128Mi"
|
||||
# limits:
|
||||
# cpu: "500m"
|
||||
# memory: "256Mi"
|
||||
resources: {}
|
||||
# -- Probes configuration for the exporter container.
|
||||
# probes:
|
||||
# liveness:
|
||||
# enabled: true # Example: enable liveness probe
|
||||
# spec: # Customize probe spec if needed
|
||||
# initialDelaySeconds: 30
|
||||
# periodSeconds: 15
|
||||
# timeoutSeconds: 5
|
||||
# failureThreshold: 3
|
||||
probes:
|
||||
liveness:
|
||||
enabled: false
|
||||
readiness:
|
||||
enabled: false
|
||||
startup:
|
||||
enabled: false
|
||||
|
||||
server:
|
||||
# -- Configuration for the iperf3 server container image (DaemonSet).
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
dependencies:
|
||||
- name: kube-prometheus-stack
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
version: 75.3.6
|
||||
digest: sha256:d15acd48bfc0b842654ae025e1bd1969e636a66508020312d555db84f381c379
|
||||
generated: "2025-06-19T20:40:53.415529365Z"
|
||||
version: 91.5.3
|
||||
- name: common
|
||||
repository: https://bjw-s-labs.github.io/helm-charts/
|
||||
version: 4.1.2
|
||||
digest: sha256:a3a063101484521564d3e8922c1c1fe7ae8040fe28c2ae246a40d0c00b860e65
|
||||
generated: "2026-09-26T09:52:53.951178644+05:30"
|
||||
|
||||
@@ -21,10 +21,20 @@ maintainers:
|
||||
icon: https://raw.githubusercontent.com/malarinv/iperf3-monitor/main/icon.png # Optional icon URL
|
||||
annotations:
|
||||
artifacthub.io/changes: |
|
||||
- Add initial Helm chart structure.
|
||||
- kind: changed
|
||||
description: Refactor exporter to subprocess with active stall watchdog and connect-timeout
|
||||
- kind: added
|
||||
description: Expose TCP retransmits, mean RTT (ms), and cycle timestamp metrics
|
||||
- kind: changed
|
||||
description: Run exporter container as non-root on Python 3.12
|
||||
- kind: fixed
|
||||
description: Support testDuration, connectTimeoutMs, and stallThresholdSeconds in Helm values
|
||||
artifacthub.io/category: networking
|
||||
dependencies:
|
||||
- name: kube-prometheus-stack # Example dependency if you package the whole stack
|
||||
version: ">=30.0.0" # Specify a compatible version range
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
condition: serviceMonitor.enabled # Only include if ServiceMonitor is enabled (assuming Prometheus Operator)
|
||||
condition: "dependencies.install, serviceMonitor.enabled"
|
||||
- name: common
|
||||
version: "4.1.2"
|
||||
repository: "https://bjw-s-labs.github.io/helm-charts/"
|
||||
|
||||
Binary file not shown.
194
charts/iperf3-monitor/grafana/iperf3-dashboard.json
Normal file
194
charts/iperf3-monitor/grafana/iperf3-dashboard.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"__inputs": [],
|
||||
"__requires": [
|
||||
{
|
||||
"type": "grafana",
|
||||
"id": "grafana",
|
||||
"name": "Grafana",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "prometheus",
|
||||
"name": "Prometheus",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "avg(iperf_network_bandwidth_mbps) by (source_node, destination_node)",
|
||||
"format": "heatmap",
|
||||
"legendFormat": "{{source_node}} -> {{destination_node}}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"cards": { "cardPadding": null, "cardRound": null },
|
||||
"color": {
|
||||
"mode": "spectrum",
|
||||
"scheme": "red-yellow-green",
|
||||
"exponent": 0.5,
|
||||
"reverse": false
|
||||
},
|
||||
"dataFormat": "tsbuckets",
|
||||
"yAxis": { "show": true, "format": "short" },
|
||||
"xAxis": { "show": true }
|
||||
},
|
||||
{
|
||||
"title": "Bandwidth Over Time (Source: $source_node, Dest: $destination_node)",
|
||||
"type": "timeseries",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "iperf_network_bandwidth_mbps{source_node=~\"^$source_node$\", destination_node=~\"^$destination_node$\", protocol=~\"^$protocol$\"}",
|
||||
"legendFormat": "Bandwidth",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "mbps"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Jitter Over Time (Source: $source_node, Dest: $destination_node)",
|
||||
"type": "timeseries",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 9
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "iperf_network_jitter_ms{source_node=~\"^$source_node$\", destination_node=~\"^$destination_node$\", protocol=\"udp\"}",
|
||||
"legendFormat": "Jitter",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 36,
|
||||
"style": "dark",
|
||||
"tags": ["iperf3", "network", "kubernetes"],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"definition": "label_values(iperf_network_bandwidth_mbps, source_node)",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "source_node",
|
||||
"options": [],
|
||||
"query": "label_values(iperf_network_bandwidth_mbps, source_node)",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"definition": "label_values(iperf_network_bandwidth_mbps{source_node=~\"^$source_node$\"}, destination_node)",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "destination_node",
|
||||
"options": [],
|
||||
"query": "label_values(iperf_network_bandwidth_mbps{source_node=~\"^$source_node$\"}, destination_node)",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": { "selected": true, "text": "tcp", "value": "tcp" },
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "protocol",
|
||||
"options": [
|
||||
{ "selected": true, "text": "tcp", "value": "tcp" },
|
||||
{ "selected": false, "text": "udp", "value": "udp" }
|
||||
],
|
||||
"query": "tcp,udp",
|
||||
"skipUrlSync": false,
|
||||
"type": "custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "Kubernetes iperf3 Network Performance",
|
||||
"uid": "k8s-iperf3-dashboard",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -47,7 +47,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "iperf3-monitor.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{- if .Values.rbac.create -}}
|
||||
{{- default (include "iperf3-monitor.fullname" .) .Values.serviceAccount.name -}}
|
||||
{{- else -}}
|
||||
{{- default "default" .Values.serviceAccount.name -}}
|
||||
|
||||
144
charts/iperf3-monitor/templates/exporter-controller.yaml
Normal file
144
charts/iperf3-monitor/templates/exporter-controller.yaml
Normal file
@@ -0,0 +1,144 @@
|
||||
{{- /*
|
||||
This template is responsible for rendering the 'exporter' controller (Deployment or DaemonSet)
|
||||
by calling the bjw-s common library.
|
||||
|
||||
The primary values for the exporter are expected under .Values.controllers.exporter.
|
||||
Modifications to environment variables and service account are handled here before
|
||||
passing the configuration to the common library.
|
||||
*/}}
|
||||
|
||||
{{- /*
|
||||
Prepare a local, modifiable copy of the .Values. This allows us to adjust the
|
||||
exporter controller's configuration (like env vars and SA) specifically for this chart's needs
|
||||
before the common library processes it.
|
||||
Convert to map[string]interface{} via toYaml/fromYaml to ensure compatibility with 'dig'.
|
||||
*/}}
|
||||
{{- $localValues := .Values | toYaml | fromYaml | deepCopy -}}
|
||||
{{- $chart := .Chart -}}
|
||||
{{- $release := .Release -}}
|
||||
{{- $appName := include "iperf3-monitor.name" . -}}
|
||||
{{- $fullName := include "iperf3-monitor.fullname" . -}}
|
||||
|
||||
{{- /*
|
||||
Define the key for the exporter controller, typically "exporter" as per our values.yaml.
|
||||
*/}}
|
||||
{{- $exporterControllerKey := "exporter" -}}
|
||||
|
||||
{{- /*
|
||||
Attempt to get the exporter controller's configuration block.
|
||||
Proceed with modifications only if the exporter controller is defined.
|
||||
*/}}
|
||||
{{- $exporterControllerConfig := get $localValues.controllers $exporterControllerKey -}}
|
||||
{{- if $exporterControllerConfig -}}
|
||||
|
||||
{{- /*
|
||||
Construct the base set of environment variables required by the iperf3-exporter application.
|
||||
These are derived from the 'appConfig' section of the exporter's controller configuration.
|
||||
*/}}
|
||||
{{- $baseExporterEnv := dict -}}
|
||||
{{- if $exporterControllerConfig.appConfig -}}
|
||||
{{- $_ := set $baseExporterEnv "SOURCE_NODE_NAME" (dict "valueFrom" (dict "fieldRef" (dict "fieldPath" "spec.nodeName"))) -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_TEST_INTERVAL" ($exporterControllerConfig.appConfig.testInterval | default "300" | toString) -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_TEST_PROTOCOL" ($exporterControllerConfig.appConfig.testProtocol | default "tcp") -}}
|
||||
{{- $_ := set $baseExporterEnv "LOG_LEVEL" ($exporterControllerConfig.appConfig.logLevel | default "INFO") -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_SERVER_PORT" ($exporterControllerConfig.appConfig.serverPort | default "5201" | toString) -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_SERVER_NAMESPACE" (dict "valueFrom" (dict "fieldRef" (dict "fieldPath" "metadata.namespace"))) -}}
|
||||
{{- $testDuration := $exporterControllerConfig.appConfig.testDuration | default $exporterControllerConfig.appConfig.testTimeout | default "5" | toString -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_TEST_DURATION" $testDuration -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_TEST_TIMEOUT" $testDuration -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_CONNECT_TIMEOUT_MS" ($exporterControllerConfig.appConfig.connectTimeoutMs | default "3000" | toString) -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_STALL_THRESHOLD_SECONDS" ($exporterControllerConfig.appConfig.stallThresholdSeconds | default "5" | toString) -}}
|
||||
{{- $serverLabelSelectorDefault := printf "app.kubernetes.io/name=%s,app.kubernetes.io/instance=%s,app.kubernetes.io/component=server" $appName $release.Name -}}
|
||||
{{- $serverLabelSelector := tpl ($exporterControllerConfig.appConfig.serverLabelSelector | default $serverLabelSelectorDefault) . -}}
|
||||
{{- $_ := set $baseExporterEnv "IPERF_SERVER_LABEL_SELECTOR" $serverLabelSelector -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Merge the base environment variables with any user-defined environment variables.
|
||||
User-defined variables (from .Values.controllers.exporter.containers.exporter.env)
|
||||
will take precedence in case of conflicting keys.
|
||||
*/}}
|
||||
{{- $userExporterEnv := $exporterControllerConfig.containers.exporter.env | default dict -}}
|
||||
{{- $finalExporterEnv := mergeOverwrite $baseExporterEnv $userExporterEnv -}}
|
||||
|
||||
{{- /*
|
||||
Ensure the container structure exists and update its 'env' field with the final set.
|
||||
The common library expects this under controllers.<key>.containers.<containerName>.env
|
||||
*/}}
|
||||
{{- if not $exporterControllerConfig.containers -}}
|
||||
{{- $_ := set $exporterControllerConfig "containers" dict -}}
|
||||
{{- end -}}
|
||||
{{- if not $exporterControllerConfig.containers.exporter -}}
|
||||
{{- $_ := set $exporterControllerConfig.containers "exporter" dict -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $exporterControllerConfig.containers.exporter "env" $finalExporterEnv -}}
|
||||
|
||||
{{- /*
|
||||
Ensure the container image tag is set, defaulting to Chart.AppVersion if empty,
|
||||
as the common library validation requires it during 'helm template'.
|
||||
|
||||
NOTE: BJW-S common library typically handles defaulting image.tag to Chart.appVersion
|
||||
if image.tag is empty or null in values. The custom logic below prepending "v"
|
||||
is specific to this chart and might be redundant if the common library's default
|
||||
is preferred. For now, we keep it as it was the reason for previous errors if tag was not set.
|
||||
However, if common library handles it, this block could be removed and image.tag in values.yaml set to "" or null.
|
||||
Forcing the tag to be set (even if to chart.appVersion) ensures the common library doesn't complain.
|
||||
The issue encountered during `helm template` earlier (empty output) was resolved by
|
||||
explicitly setting the tag (e.g. via --set or by ensuring values.yaml has it).
|
||||
The common library's internal validation likely needs *a* tag to be present in the values passed to it,
|
||||
even if that tag is derived from AppVersion. This block ensures that.
|
||||
*/}}
|
||||
{{- $exporterContainerCfg := get $exporterControllerConfig.containers "exporter" -}}
|
||||
{{- if $exporterContainerCfg -}}
|
||||
{{- if not $exporterContainerCfg.image.tag -}}
|
||||
{{- if $chart.AppVersion -}}
|
||||
{{- $_ := set $exporterContainerCfg.image "tag" (printf "%s" $chart.AppVersion) -}} # Removed "v" prefix
|
||||
{{- else -}}
|
||||
{{- fail (printf "Error: Container image tag is not specified for controller '%s', container '%s', and Chart.AppVersion is also empty." $exporterControllerKey "exporter") -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Configure the Service Account for the exporter controller.
|
||||
This ensures the controller pod uses the ServiceAccount that is intended by this chart's
|
||||
RBAC configuration (.Values.rbac.create and .Values.serviceAccount.name).
|
||||
*/}}
|
||||
{{- $serviceAccountNameFromValues := $localValues.serviceAccount.name | default (printf "%s-exporter" $fullName) -}}
|
||||
{{- if not $exporterControllerConfig.serviceAccount -}}
|
||||
{{- $_ := set $exporterControllerConfig "serviceAccount" dict -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $exporterControllerConfig.serviceAccount "name" $serviceAccountNameFromValues -}}
|
||||
{{- $_ := set $exporterControllerConfig.serviceAccount "create" $localValues.rbac.create -}}
|
||||
{{- $_ := set $exporterControllerConfig.serviceAccount "automountServiceAccountToken" ($exporterControllerConfig.pod.automountServiceAccountToken | default true) -}}
|
||||
|
||||
{{- /*
|
||||
Replace the original exporter controller config in our $localValues copy
|
||||
with the modified version (that now includes the correct env and SA settings).
|
||||
*/}}
|
||||
{{- $_ := set $localValues.controllers $exporterControllerKey $exporterControllerConfig -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Ensure .Values.global exists and is a map, as the common library expects it.
|
||||
*/}}
|
||||
{{- if not (get $localValues "global") -}}
|
||||
{{- $_ := set $localValues "global" dict -}}
|
||||
{{- else if not (kindIs "map" (get $localValues "global")) -}}
|
||||
{{- $_ := set $localValues "global" dict -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Ensure defaultPodOptionsStrategy exists, as common lib expects it at the root of Values.
|
||||
*/}}
|
||||
{{- if not (get $localValues "defaultPodOptionsStrategy") -}}
|
||||
{{- $_ := set $localValues "defaultPodOptionsStrategy" "overwrite" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- /*
|
||||
Call the common library's main render function for controllers.
|
||||
This function iterates through all controllers defined under $localValues.controllers
|
||||
(in our case, just "exporter") and renders them using their specified type and configuration.
|
||||
The context passed must mirror the global Helm context, including 'Values', 'Chart', 'Release', 'Capabilities', and 'Template'.
|
||||
*/}}
|
||||
{{- include "bjw-s.common.render.controllers" (dict "Values" $localValues "Chart" $chart "Release" $release "Capabilities" .Capabilities "Template" .Template) | nindent 0 -}}
|
||||
@@ -1,48 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "iperf3-monitor.fullname" . }}-exporter
|
||||
labels:
|
||||
{{- include "iperf3-monitor.labels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: exporter
|
||||
spec:
|
||||
replicas: {{ .Values.exporter.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "iperf3-monitor.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: exporter
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "iperf3-monitor.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: exporter
|
||||
spec:
|
||||
serviceAccountName: {{ include "iperf3-monitor.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: iperf3-exporter
|
||||
image: "{{ .Values.exporter.image.repository }}:{{ .Values.exporter.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.exporter.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.service.targetPort }}
|
||||
name: metrics
|
||||
env:
|
||||
- name: SOURCE_NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: IPERF_TEST_INTERVAL
|
||||
value: "{{ .Values.exporter.testInterval }}"
|
||||
- name: IPERF_TEST_PROTOCOL
|
||||
value: "{{ .Values.exporter.testProtocol }}"
|
||||
- name: IPERF_SERVER_PORT
|
||||
value: "5201" # Hardcoded as per server DaemonSet
|
||||
- name: IPERF_SERVER_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: IPERF_SERVER_LABEL_SELECTOR
|
||||
value: 'app.kubernetes.io/name={{ include "iperf3-monitor.name" . }},app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=server'
|
||||
{{- with .Values.exporter.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-grafana-dashboard
|
||||
labels:
|
||||
grafana_dashboard: "1"
|
||||
app.kubernetes.io/name: {{ include "iperf3-monitor.name" . }}
|
||||
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
data:
|
||||
iperf3-dashboard.json: |
|
||||
{{ .Files.Get "grafana/iperf3-dashboard.json" | nindent 4 }}
|
||||
@@ -7,9 +7,10 @@ metadata:
|
||||
{{- include "iperf3-monitor.labels" . | nindent 4 }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
kind: Role
|
||||
metadata:
|
||||
name: {{ include "iperf3-monitor.fullname" . }}-role
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "iperf3-monitor.labels" . | nindent 4 }}
|
||||
rules:
|
||||
@@ -18,9 +19,10 @@ rules:
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: {{ include "iperf3-monitor.fullname" . }}-rb
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "iperf3-monitor.labels" . | nindent 4 }}
|
||||
subjects:
|
||||
@@ -28,7 +30,7 @@ subjects:
|
||||
name: {{ include "iperf3-monitor.serviceAccountName" . }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
kind: Role # Changed from ClusterRole
|
||||
name: {{ include "iperf3-monitor.fullname" . }}-role
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
{{- end -}}
|
||||
@@ -11,7 +11,7 @@ spec:
|
||||
{{- include "iperf3-monitor.selectorLabels" . | nindent 4 }}
|
||||
app.kubernetes.io/component: exporter
|
||||
ports:
|
||||
- name: metrics
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.targetPort }}
|
||||
protocol: TCP
|
||||
- name: metrics # Assuming 'metrics' is the intended name, aligns with values structure
|
||||
port: {{ .Values.service.main.ports.metrics.port }}
|
||||
targetPort: {{ .Values.service.main.ports.metrics.targetPort }}
|
||||
protocol: {{ .Values.service.main.ports.metrics.protocol | default "TCP" }}
|
||||
@@ -8,30 +8,102 @@ nameOverride: ""
|
||||
# -- Override the fully qualified app name.
|
||||
fullnameOverride: ""
|
||||
|
||||
exporter:
|
||||
# -- Configuration for the exporter container image.
|
||||
controllers:
|
||||
exporter:
|
||||
# -- Enable the exporter controller.
|
||||
enabled: true
|
||||
# -- Set the controller type for the exporter.
|
||||
# Valid options are "deployment" or "daemonset".
|
||||
# @default -- "deployment"
|
||||
type: deployment
|
||||
# -- Number of desired exporter pods. Only used if type is "deployment".
|
||||
# @default -- 1
|
||||
replicas: 1
|
||||
|
||||
# -- Application-specific configuration for the iperf3 exporter.
|
||||
# These values are used to populate environment variables for the exporter container.
|
||||
appConfig:
|
||||
# -- Interval in seconds between complete test cycles (i.e., testing all server nodes).
|
||||
testInterval: 300
|
||||
# -- Log level for the iperf3 exporter (e.g., DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
||||
logLevel: INFO
|
||||
# -- Timeout in seconds for a single iperf3 test run (kept for backward compatibility).
|
||||
testTimeout: 5
|
||||
# -- Duration in seconds for a single iperf3 test run.
|
||||
testDuration: 5
|
||||
# -- Connect timeout in milliseconds for control connection setup.
|
||||
connectTimeoutMs: 3000
|
||||
# -- Stall threshold in seconds (abort test if data transfer halts during test).
|
||||
stallThresholdSeconds: 5
|
||||
# -- Protocol to use for testing (tcp or udp).
|
||||
testProtocol: tcp
|
||||
# -- iperf3 server port to connect to. Should match the server's listening port.
|
||||
# @default -- "5201" (hardcoded in the original chart for server daemonset)
|
||||
serverPort: "5201"
|
||||
# -- Label selector to find iperf3 server pods.
|
||||
# This will be templated in the actual deployment.
|
||||
# Example default (if not overridden by template logic): 'app.kubernetes.io/component=server'
|
||||
serverLabelSelector: 'app.kubernetes.io/name={{ include "iperf3-monitor.name" . }},app.kubernetes.io/instance={{ .Release.Name }},app.kubernetes.io/component=server'
|
||||
|
||||
|
||||
# -- Pod-level configurations for the exporter, leveraging bjw-s common library structure.
|
||||
pod:
|
||||
# -- Annotations for the exporter pod.
|
||||
annotations: {}
|
||||
# -- Labels for the exporter pod.
|
||||
labels:
|
||||
app.kubernetes.io/component: exporter # Ensure pods get the component label for service selection
|
||||
# -- Node selector for scheduling exporter pods.
|
||||
nodeSelector: {}
|
||||
# -- Tolerations for scheduling exporter pods.
|
||||
tolerations: []
|
||||
# -- Affinity rules for scheduling exporter pods.
|
||||
affinity: {}
|
||||
# -- Security context for the exporter pod.
|
||||
securityContext: {}
|
||||
# fsGroup: 65534
|
||||
# runAsUser: 65534
|
||||
# runAsGroup: 65534
|
||||
# runAsNonRoot: true
|
||||
# -- Automount service account token for the pod.
|
||||
automountServiceAccountToken: true # Default from common lib
|
||||
|
||||
# -- Container-level configurations for the main exporter container.
|
||||
containers:
|
||||
exporter: # This is the primary container, name it 'exporter'
|
||||
image:
|
||||
# -- The container image repository for the exporter.
|
||||
repository: ghcr.io/malarinv/iperf3-monitor
|
||||
# -- The container image tag for the exporter. If not set, the chart's appVersion is used.
|
||||
tag: ""
|
||||
tag: "" # Defaults to .Chart.AppVersion via common library
|
||||
# -- The image pull policy for the exporter container.
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# -- Number of exporter pod replicas. Typically 1 is sufficient.
|
||||
replicaCount: 1
|
||||
# -- Environment variables for the exporter container.
|
||||
# The actual env map will be constructed in the main chart template
|
||||
# and passed to the common library. This section is for user overrides
|
||||
# if they want to directly set other env vars using common lib's env schema.
|
||||
env: {}
|
||||
# Example:
|
||||
# MY_CUSTOM_VAR: "my_value"
|
||||
# ANOTHER_VAR:
|
||||
# valueFrom:
|
||||
# secretKeyRef:
|
||||
# name: mysecret
|
||||
# key: mykey
|
||||
|
||||
# -- Interval in seconds between complete test cycles (i.e., testing all server nodes).
|
||||
testInterval: 300
|
||||
# -- Ports for the exporter container.
|
||||
# Expected by Kubernetes and bjw-s common library as a list of objects.
|
||||
ports:
|
||||
- name: metrics # Name of the port, referenced by Service's targetPort
|
||||
# -- Port number for the metrics endpoint on the container.
|
||||
containerPort: 9876
|
||||
# -- Protocol for the metrics port.
|
||||
protocol: TCP
|
||||
# -- Whether this port definition is enabled. Specific to bjw-s common library.
|
||||
enabled: true
|
||||
|
||||
# -- Timeout in seconds for a single iperf3 test run.
|
||||
testTimeout: 10
|
||||
|
||||
# -- Protocol to use for testing (tcp or udp).
|
||||
testProtocol: tcp
|
||||
|
||||
# -- CPU and memory resource requests and limits for the exporter pod.
|
||||
# @default -- A small default is provided if commented out.
|
||||
# -- CPU and memory resource requests and limits for the exporter container.
|
||||
resources:
|
||||
{}
|
||||
# requests:
|
||||
@@ -41,6 +113,16 @@ exporter:
|
||||
# cpu: "500m"
|
||||
# memory: "256Mi"
|
||||
|
||||
# -- Probes configuration for the exporter container.
|
||||
probes:
|
||||
liveness:
|
||||
enabled: false
|
||||
readiness:
|
||||
enabled: false
|
||||
startup:
|
||||
enabled: false
|
||||
|
||||
# Server configuration (iperf3 server daemonset)
|
||||
server:
|
||||
# -- Configuration for the iperf3 server container image (DaemonSet).
|
||||
image:
|
||||
@@ -50,8 +132,6 @@ server:
|
||||
tag: latest
|
||||
|
||||
# -- CPU and memory resource requests and limits for the iperf3 server pods (DaemonSet).
|
||||
# These should be very low as the server is mostly idle.
|
||||
# @default -- A small default is provided if commented out.
|
||||
resources:
|
||||
{}
|
||||
# requests:
|
||||
@@ -62,13 +142,9 @@ server:
|
||||
# memory: "128Mi"
|
||||
|
||||
# -- Node selector for scheduling iperf3 server pods.
|
||||
# Use this to restrict the DaemonSet to a subset of nodes.
|
||||
# @default -- {} (schedule on all nodes)
|
||||
nodeSelector: {}
|
||||
|
||||
# -- Tolerations for scheduling iperf3 server pods on tainted nodes (e.g., control-plane nodes).
|
||||
# This is often necessary to include master nodes in the test mesh.
|
||||
# @default -- Tolerates control-plane and master taints.
|
||||
# -- Tolerations for scheduling iperf3 server pods on tainted nodes.
|
||||
tolerations:
|
||||
- key: "node-role.kubernetes.io/control-plane"
|
||||
operator: "Exists"
|
||||
@@ -77,44 +153,60 @@ server:
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
|
||||
# RBAC and ServiceAccount settings
|
||||
# These are for the exporter. The exporter deployment (managed by common library)
|
||||
# will need to use the ServiceAccount specified here or one created by the library.
|
||||
rbac:
|
||||
# -- If true, create ServiceAccount, ClusterRole, and ClusterRoleBinding for the exporter.
|
||||
# Set to false if you manage RBAC externally.
|
||||
create: true
|
||||
|
||||
serviceAccount:
|
||||
# -- The name of the ServiceAccount to use for the exporter pod.
|
||||
# Only used if rbac.create is false. If not set, it defaults to the chart's fullname.
|
||||
name: ""
|
||||
# -- The name of the ServiceAccount to use/create for the exporter pod.
|
||||
# If rbac.create is true, this SA is created. The exporter pod must use this SA.
|
||||
name: "iperf3-monitor"
|
||||
|
||||
# Service Monitor configuration for Prometheus
|
||||
serviceMonitor:
|
||||
# -- If true, create a ServiceMonitor resource for integration with Prometheus Operator.
|
||||
# Requires a running Prometheus Operator in the cluster.
|
||||
# -- If true, create a ServiceMonitor resource.
|
||||
enabled: true
|
||||
|
||||
# -- Scrape interval for the ServiceMonitor. How often Prometheus scrapes the exporter metrics.
|
||||
# -- Scrape interval for the ServiceMonitor.
|
||||
interval: 60s
|
||||
|
||||
# -- Scrape timeout for the ServiceMonitor. How long Prometheus waits for metrics response.
|
||||
# -- Scrape timeout for the ServiceMonitor.
|
||||
scrapeTimeout: 30s
|
||||
|
||||
# -- Configuration for the exporter Service.
|
||||
# Service configuration for the exporter
|
||||
# This defines how the exporter is exposed.
|
||||
# The common library can also manage services, or we can use our own template.
|
||||
# This structure is compatible with bjw-s common library's service management if we choose to use it.
|
||||
service:
|
||||
# -- Service type. ClusterIP is typically sufficient.
|
||||
type: ClusterIP
|
||||
# -- Port on which the exporter service is exposed.
|
||||
main: # A key for the service, 'main' is a common convention.
|
||||
# -- Enable the exporter service.
|
||||
enabled: true
|
||||
# -- Service type.
|
||||
type: ClusterIP # ClusterIP is typical for internal services scraped by Prometheus.
|
||||
# -- Ports configuration for the service.
|
||||
ports:
|
||||
metrics: # Name of the service port, should align with a container port name.
|
||||
# -- Port number on which the service is exposed.
|
||||
port: 9876
|
||||
# -- Target port on the exporter pod.
|
||||
targetPort: 9876
|
||||
# -- Target port on the exporter pod. Can be a number or name.
|
||||
# Refers to the 'metrics' port defined in controllers.exporter.containers.exporter.ports.
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
|
||||
# -- Optional configuration for a network policy to allow traffic to the iperf3 server DaemonSet.
|
||||
# This is often necessary if you are using a network policy controller.
|
||||
# Network Policy (optional)
|
||||
networkPolicy:
|
||||
# -- If true, create a NetworkPolicy resource.
|
||||
enabled: false
|
||||
# -- Specify source selectors if needed (e.g., pods in a specific namespace).
|
||||
# -- Source selectors for ingress rules.
|
||||
from: []
|
||||
# -- Specify namespace selectors if needed.
|
||||
# -- Namespace selectors for ingress rules.
|
||||
namespaceSelector: {}
|
||||
# -- Specify pod selectors if needed.
|
||||
# -- Pod selectors for ingress rules.
|
||||
podSelector: {}
|
||||
|
||||
# Dependency Configuration (for Prometheus Operator)
|
||||
dependencies:
|
||||
# -- Set to false by default. Set to true to install a Prometheus operator dependency (used if serviceMonitor.enabled=true).
|
||||
# -- If false (default), and serviceMonitor.enabled is true, you must have a compatible Prometheus Operator already running in your cluster.
|
||||
install: false
|
||||
|
||||
14
devbox.json
14
devbox.json
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json",
|
||||
"packages": [],
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
"echo 'Welcome to devbox!' > /dev/null"
|
||||
],
|
||||
"scripts": {
|
||||
"test": [
|
||||
"echo \"Error: no test specified\" && exit 1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"lockfile_version": "1",
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,28 +1,24 @@
|
||||
# Stage 1: Build stage with dependencies
|
||||
FROM python:3.9-slim as builder
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install iperf3 and build dependencies
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc iperf3 libiperf-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||
|
||||
# Stage 2: Final runtime stage
|
||||
FROM python:3.9-slim
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy iperf3 binary and library from the builder stage
|
||||
COPY --from=builder /usr/bin/iperf3 /usr/bin/iperf3
|
||||
COPY --from=builder /usr/lib/x86_64-linux-gnu/libiperf.so.0 /usr/lib/x86_64-linux-gnu/libiperf.so.0
|
||||
# Install iperf3
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends iperf3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy installed Python packages from the builder stage
|
||||
COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
# Copy the exporter application code
|
||||
COPY exporter.py .
|
||||
@@ -30,5 +26,9 @@ COPY exporter.py .
|
||||
# Expose the metrics port
|
||||
EXPOSE 9876
|
||||
|
||||
# Run as non-root user (nobody)
|
||||
USER 65534:65534
|
||||
|
||||
# Set the entrypoint
|
||||
CMD ["python", "exporter.py"]
|
||||
|
||||
|
||||
@@ -1,28 +1,53 @@
|
||||
"""
|
||||
Prometheus exporter for iperf3 network performance monitoring.
|
||||
|
||||
This script runs iperf3 tests between the node it's running on (source) and
|
||||
other iperf3 server pods discovered in a Kubernetes cluster. It then exposes
|
||||
these metrics for Prometheus consumption.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import signal
|
||||
import logging
|
||||
import argparse
|
||||
import subprocess
|
||||
from kubernetes import client, config
|
||||
from prometheus_client import start_http_server, Gauge
|
||||
import iperf3
|
||||
|
||||
# --- Configuration ---
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
# --- Global Configuration & Setup ---
|
||||
|
||||
parser = argparse.ArgumentParser(description="iperf3 Prometheus exporter.")
|
||||
parser.add_argument(
|
||||
'--log-level',
|
||||
default=os.environ.get('LOG_LEVEL', 'INFO').upper(),
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
help='Set the logging level. Overrides LOG_LEVEL environment variable. (Default: INFO)'
|
||||
)
|
||||
args, _ = parser.parse_known_args()
|
||||
log_level_str = args.log_level
|
||||
|
||||
numeric_level = getattr(logging, log_level_str.upper(), None)
|
||||
if not isinstance(numeric_level, int):
|
||||
logging.error(f"Invalid log level: {log_level_str}. Defaulting to INFO.")
|
||||
numeric_level = logging.INFO
|
||||
logging.basicConfig(level=numeric_level, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# --- Prometheus Metrics Definition ---
|
||||
IPERF_BANDWIDTH_MBPS = Gauge(
|
||||
'iperf_network_bandwidth_mbps',
|
||||
'Network bandwidth measured by iperf3 in Megabits per second',
|
||||
'Network bandwidth measured by iperf3 in Megabits per second (Mbps)',
|
||||
['source_node', 'destination_node', 'protocol']
|
||||
)
|
||||
IPERF_JITTER_MS = Gauge(
|
||||
'iperf_network_jitter_ms',
|
||||
'Network jitter measured by iperf3 in milliseconds',
|
||||
'Network jitter measured by iperf3 in milliseconds (ms) for UDP tests',
|
||||
['source_node', 'destination_node', 'protocol']
|
||||
)
|
||||
IPERF_PACKETS_TOTAL = Gauge(
|
||||
'iperf_network_packets_total',
|
||||
'Total packets transmitted or received during the iperf3 test',
|
||||
'Total packets transmitted/received during the iperf3 UDP test',
|
||||
['source_node', 'destination_node', 'protocol']
|
||||
)
|
||||
IPERF_LOST_PACKETS = Gauge(
|
||||
@@ -35,14 +60,60 @@ IPERF_TEST_SUCCESS = Gauge(
|
||||
'Indicates if the iperf3 test was successful (1) or failed (0)',
|
||||
['source_node', 'destination_node', 'protocol']
|
||||
)
|
||||
IPERF_TCP_RETRANSMITS = Gauge(
|
||||
'iperf_network_tcp_retransmits_total',
|
||||
'Total TCP retransmits measured by iperf3 during the test',
|
||||
['source_node', 'destination_node', 'protocol']
|
||||
)
|
||||
IPERF_TCP_RTT_MS = Gauge(
|
||||
'iperf_network_tcp_rtt_ms',
|
||||
'Mean TCP round-trip time in milliseconds (ms) measured by iperf3',
|
||||
['source_node', 'destination_node', 'protocol']
|
||||
)
|
||||
IPERF_TEST_DURATION_SECONDS = Gauge(
|
||||
'iperf_network_test_duration_seconds',
|
||||
'Duration of the iperf3 test in seconds',
|
||||
['source_node', 'destination_node', 'protocol']
|
||||
)
|
||||
IPERF_LAST_TEST_TIMESTAMP_SECONDS = Gauge(
|
||||
'iperf_exporter_last_success_timestamp_seconds',
|
||||
'Unix timestamp of the last completed test cycle',
|
||||
['source_node']
|
||||
)
|
||||
IPERF_SERVERS_DISCOVERED = Gauge(
|
||||
'iperf_exporter_servers_discovered',
|
||||
'Number of target iperf3 servers discovered in the cluster',
|
||||
['source_node']
|
||||
)
|
||||
|
||||
def discover_iperf_servers():
|
||||
# --- Graceful Shutdown & Exceptions ---
|
||||
shutdown_requested = False
|
||||
active_subprocess = None
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
global shutdown_requested, active_subprocess
|
||||
logging.info(f"Received signal {signum}. Initiating graceful shutdown...")
|
||||
shutdown_requested = True
|
||||
if active_subprocess and active_subprocess.poll() is None:
|
||||
try:
|
||||
active_subprocess.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
class IperfStallException(Exception):
|
||||
"""Raised when an iperf3 test experiences a silent or busy-wait stall."""
|
||||
pass
|
||||
|
||||
class IperfTimeoutException(Exception):
|
||||
"""Raised when an iperf3 test exceeds its maximum expected deadline."""
|
||||
pass
|
||||
|
||||
# --- Kubernetes Discovery ---
|
||||
def discover_iperf_servers(source_node_name):
|
||||
"""
|
||||
Discover iperf3 server pods in the cluster using the Kubernetes API.
|
||||
Discovers iperf3 server pods within a Kubernetes cluster.
|
||||
"""
|
||||
try:
|
||||
# Load in-cluster configuration
|
||||
# Assumes the exporter runs in a pod with a service account having permissions
|
||||
config.load_incluster_config()
|
||||
v1 = client.CoreV1Api()
|
||||
|
||||
@@ -51,110 +122,317 @@ def discover_iperf_servers():
|
||||
|
||||
logging.info(f"Discovering iperf3 servers with label '{label_selector}' in namespace '{namespace}'")
|
||||
|
||||
# List pods across all namespaces with the specified label selector
|
||||
# Note: list_pod_for_all_namespaces requires cluster-wide permissions
|
||||
ret = v1.list_pod_for_all_namespaces(label_selector=label_selector, watch=False)
|
||||
ret = v1.list_namespaced_pod(namespace=namespace, label_selector=label_selector, watch=False)
|
||||
|
||||
servers = []
|
||||
for i in ret.items:
|
||||
# Ensure pod has an IP and is running
|
||||
if i.status.pod_ip and i.status.phase == 'Running':
|
||||
for item in ret.items:
|
||||
if item.status.pod_ip and item.status.phase == 'Running':
|
||||
servers.append({
|
||||
'ip': i.status.pod_ip,
|
||||
'node_name': i.spec.node_name
|
||||
'ip': item.status.pod_ip,
|
||||
'node_name': item.spec.node_name
|
||||
})
|
||||
logging.info(f"Discovered {len(servers)} iperf3 server pods.")
|
||||
logging.info(f"Discovered {len(servers)} iperf3 server pods in namespace '{namespace}'.")
|
||||
IPERF_SERVERS_DISCOVERED.labels(source_node=source_node_name).set(len(servers))
|
||||
return servers
|
||||
except config.ConfigException as e:
|
||||
logging.error(f"Kubernetes config error: {e}. Is the exporter running in a cluster with RBAC permissions?")
|
||||
return []
|
||||
except Exception as e:
|
||||
logging.error(f"Error discovering iperf servers: {e}")
|
||||
return [] # Return empty list on error to avoid crashing the loop
|
||||
return []
|
||||
|
||||
def run_iperf_test(server_ip, server_port, protocol, source_node, dest_node):
|
||||
# --- iperf3 Subprocess Execution with Stall Detection ---
|
||||
def execute_iperf_test(server_ip, server_port, protocol, duration, connect_timeout_ms, stall_threshold_sec):
|
||||
"""
|
||||
Runs a single iperf3 test and updates Prometheus metrics.
|
||||
Executes the iperf3 CLI in a subprocess with JSON output and active stall detection.
|
||||
"""
|
||||
logging.info(f"Running iperf3 test from {source_node} to {dest_node} ({server_ip}:{server_port}) using {protocol.upper()}")
|
||||
global active_subprocess
|
||||
|
||||
client = iperf3.Client()
|
||||
client.server_hostname = server_ip
|
||||
client.port = server_port
|
||||
client.protocol = protocol
|
||||
# Duration of the test (seconds)
|
||||
client.duration = int(os.getenv('IPERF_TEST_DURATION', 5))
|
||||
# Output results as JSON for easy parsing
|
||||
client.json_output = True
|
||||
cmd = [
|
||||
"iperf3",
|
||||
"-c", server_ip,
|
||||
"-p", str(server_port),
|
||||
"-t", str(duration),
|
||||
"--connect-timeout", str(connect_timeout_ms),
|
||||
"-J",
|
||||
]
|
||||
if protocol == 'udp':
|
||||
cmd.append("-u")
|
||||
|
||||
result = client.run()
|
||||
logging.debug(f"Executing: {' '.join(cmd)}")
|
||||
|
||||
# Parse results and update metrics
|
||||
parse_and_publish_metrics(result, source_node, dest_node, protocol)
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
active_subprocess = proc
|
||||
|
||||
def parse_and_publish_metrics(result, source_node, dest_node, protocol):
|
||||
start_time = time.monotonic()
|
||||
# Maximum deadline: connect timeout + duration + stall threshold buffer
|
||||
connect_timeout_sec = connect_timeout_ms / 1000.0
|
||||
max_deadline = start_time + connect_timeout_sec + duration + stall_threshold_sec
|
||||
|
||||
last_io_change = start_time
|
||||
last_wchar = 0
|
||||
test_transfer_detected = False
|
||||
|
||||
try:
|
||||
while proc.poll() is None:
|
||||
if shutdown_requested:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
raise KeyboardInterrupt("Shutdown requested")
|
||||
|
||||
now = time.monotonic()
|
||||
if now > max_deadline:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
raise IperfTimeoutException(f"iperf3 test exceeded absolute deadline of {max_deadline - start_time:.1f}s")
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Check I/O progress via /proc/<pid>/io to detect busy-spin without I/O
|
||||
try:
|
||||
with open(f"/proc/{proc.pid}/io", "r") as f:
|
||||
for line in f:
|
||||
if line.startswith("wchar:"):
|
||||
wchar = int(line.split()[1])
|
||||
if wchar > last_wchar:
|
||||
last_wchar = wchar
|
||||
last_io_change = now
|
||||
test_transfer_detected = True
|
||||
break
|
||||
except (FileNotFoundError, ProcessLookupError, PermissionError):
|
||||
pass
|
||||
|
||||
# If traffic was active and has now completely ceased progressing for >= stall_threshold_sec
|
||||
if test_transfer_detected and (now - last_io_change) >= stall_threshold_sec:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
raise IperfStallException(f"iperf3 stalled: data transfer ceased for {now - last_io_change:.1f}s")
|
||||
|
||||
stdout, stderr = proc.communicate()
|
||||
return proc.returncode, stdout, stderr
|
||||
finally:
|
||||
active_subprocess = None
|
||||
|
||||
# --- Metric Reset Helper ---
|
||||
def reset_failure_metrics(labels):
|
||||
IPERF_TEST_SUCCESS.labels(**labels).set(0)
|
||||
for g in (
|
||||
IPERF_BANDWIDTH_MBPS,
|
||||
IPERF_JITTER_MS,
|
||||
IPERF_PACKETS_TOTAL,
|
||||
IPERF_LOST_PACKETS,
|
||||
IPERF_TCP_RETRANSMITS,
|
||||
IPERF_TCP_RTT_MS,
|
||||
IPERF_TEST_DURATION_SECONDS,
|
||||
):
|
||||
try:
|
||||
g.labels(**labels).set(0)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# --- Metric Parsing ---
|
||||
def parse_and_publish_metrics(data, source_node, dest_node, protocol):
|
||||
"""
|
||||
Parses the iperf3 result and updates Prometheus gauges.
|
||||
Handles both successful and failed tests.
|
||||
Parses iperf3 JSON result and updates Prometheus gauges.
|
||||
"""
|
||||
labels = {'source_node': source_node, 'destination_node': dest_node, 'protocol': protocol}
|
||||
|
||||
if result and result.error:
|
||||
logging.error(f"Test from {source_node} to {dest_node} failed: {result.error}")
|
||||
IPERF_TEST_SUCCESS.labels(**labels).set(0)
|
||||
# Set metrics to 0 on failure
|
||||
try:
|
||||
IPERF_BANDWIDTH_MBPS.labels(**labels).set(0)
|
||||
IPERF_JITTER_MS.labels(**labels).set(0)
|
||||
IPERF_PACKETS_TOTAL.labels(**labels).set(0)
|
||||
IPERF_LOST_PACKETS.labels(**labels).set(0)
|
||||
except KeyError:
|
||||
# Labels might not be registered yet if this is the first failure
|
||||
pass
|
||||
if not data or not isinstance(data, dict):
|
||||
logging.warning(f"Test from {source_node} to {dest_node} ({protocol.upper()}) failed: empty or invalid result")
|
||||
reset_failure_metrics(labels)
|
||||
return
|
||||
|
||||
if not result:
|
||||
logging.error(f"Test from {source_node} to {dest_node} failed to return a result object.")
|
||||
IPERF_TEST_SUCCESS.labels(**labels).set(0)
|
||||
try:
|
||||
IPERF_BANDWIDTH_MBPS.labels(**labels).set(0)
|
||||
IPERF_JITTER_MS.labels(**labels).set(0)
|
||||
IPERF_PACKETS_TOTAL.labels(**labels).set(0)
|
||||
IPERF_LOST_PACKETS.labels(**labels).set(0)
|
||||
except KeyError:
|
||||
pass
|
||||
if data.get("error"):
|
||||
error_msg = data.get("error")
|
||||
logging.warning(f"Test from {source_node} to {dest_node} ({protocol.upper()}) failed: {error_msg}")
|
||||
reset_failure_metrics(labels)
|
||||
return
|
||||
|
||||
end = data.get("end")
|
||||
if not end or not isinstance(end, dict):
|
||||
logging.warning(f"Test from {source_node} to {dest_node} ({protocol.upper()}) failed: missing 'end' summary in JSON")
|
||||
reset_failure_metrics(labels)
|
||||
return
|
||||
|
||||
# Successful test execution
|
||||
IPERF_TEST_SUCCESS.labels(**labels).set(1)
|
||||
|
||||
# The summary data is typically in result.json['end']['sum_sent'] or result.json['end']['sum_received']
|
||||
# The iperf3-python client often exposes this directly as attributes like sent_Mbps or received_Mbps
|
||||
# For TCP, we usually care about the received bandwidth on the client side (which is the exporter)
|
||||
# For UDP, the client report contains jitter, lost packets, etc.
|
||||
bandwidth_mbps = 0
|
||||
if hasattr(result, 'received_Mbps') and result.received_Mbps is not None:
|
||||
bandwidth_mbps = result.received_Mbps
|
||||
elif hasattr(result, 'sent_Mbps') and result.sent_Mbps is not None:
|
||||
# Fallback, though received_Mbps is usually more relevant for TCP client
|
||||
bandwidth_mbps = result.sent_Mbps
|
||||
# Add a check for the raw JSON output structure as a fallback
|
||||
elif result.json and 'end' in result.json and 'sum_received' in result.json['end'] and result.json['end']['sum_received']['bits_per_second'] is not None:
|
||||
bandwidth_mbps = result.json['end']['sum_received']['bits_per_second'] / 1000000
|
||||
elif result.json and 'end' in result.json and 'sum_sent' in result.json['end'] and result.json['end']['sum_sent']['bits_per_second'] is not None:
|
||||
bandwidth_mbps = result.json['end']['sum_sent']['bits_per_second'] / 1000000
|
||||
sum_received = end.get("sum_received") or {}
|
||||
sum_sent = end.get("sum_sent") or {}
|
||||
sum_udp = end.get("sum") or {}
|
||||
|
||||
bandwidth_mbps = 0.0
|
||||
|
||||
if protocol == 'tcp':
|
||||
if "bits_per_second" in sum_received and sum_received["bits_per_second"] is not None:
|
||||
bandwidth_mbps = sum_received["bits_per_second"] / 1_000_000.0
|
||||
elif "bits_per_second" in sum_sent and sum_sent["bits_per_second"] is not None:
|
||||
bandwidth_mbps = sum_sent["bits_per_second"] / 1_000_000.0
|
||||
|
||||
# TCP Retransmits
|
||||
retransmits = sum_sent.get("retransmits", 0)
|
||||
IPERF_TCP_RETRANSMITS.labels(**labels).set(retransmits)
|
||||
|
||||
# TCP RTT
|
||||
mean_rtt_us = 0.0
|
||||
streams = end.get("streams", [])
|
||||
if streams and isinstance(streams, list):
|
||||
sender_info = streams[0].get("sender", {})
|
||||
mean_rtt_us = sender_info.get("mean_rtt", sender_info.get("rtt", 0.0))
|
||||
IPERF_TCP_RTT_MS.labels(**labels).set(mean_rtt_us / 1000.0)
|
||||
|
||||
# Zero out UDP-specific gauges
|
||||
IPERF_JITTER_MS.labels(**labels).set(0)
|
||||
IPERF_PACKETS_TOTAL.labels(**labels).set(0)
|
||||
IPERF_LOST_PACKETS.labels(**labels).set(0)
|
||||
|
||||
# Test duration
|
||||
test_seconds = sum_received.get("seconds", sum_sent.get("seconds", 0.0))
|
||||
IPERF_TEST_DURATION_SECONDS.labels(**labels).set(test_seconds)
|
||||
else:
|
||||
if "bits_per_second" in sum_udp and sum_udp["bits_per_second"] is not None:
|
||||
bandwidth_mbps = sum_udp["bits_per_second"] / 1_000_000.0
|
||||
elif "bits_per_second" in sum_sent and sum_sent["bits_per_second"] is not None:
|
||||
bandwidth_mbps = sum_sent["bits_per_second"] / 1_000_000.0
|
||||
|
||||
IPERF_JITTER_MS.labels(**labels).set(sum_udp.get("jitter_ms", 0.0) or 0.0)
|
||||
IPERF_PACKETS_TOTAL.labels(**labels).set(sum_udp.get("packets", 0) or 0)
|
||||
IPERF_LOST_PACKETS.labels(**labels).set(sum_udp.get("lost_packets", 0) or 0)
|
||||
IPERF_TCP_RETRANSMITS.labels(**labels).set(0)
|
||||
IPERF_TCP_RTT_MS.labels(**labels).set(0)
|
||||
|
||||
test_seconds = sum_udp.get("seconds", 0.0)
|
||||
IPERF_TEST_DURATION_SECONDS.labels(**labels).set(test_seconds)
|
||||
|
||||
IPERF_BANDWIDTH_MBPS.labels(**labels).set(bandwidth_mbps)
|
||||
|
||||
# UDP specific metrics
|
||||
if protocol == 'udp':
|
||||
# iperf3-python exposes UDP results directly
|
||||
IPERF_JITTER_MS.labels(**labels).set(result.jitter_ms if hasattr(result, 'jitter_ms') and result.jitter_ms is not None else 0)
|
||||
IPERF_PACKETS_TOTAL.labels(**labels).set(result.packets if hasattr(result, 'packets') and result.packets is not None else 0)
|
||||
IPERF_LOST_PACKETS.labels(**labels).set(result.lost_packets if hasattr(result, 'lost_packets') and result.lost_packets is not None else 0)
|
||||
else:
|
||||
# Ensure UDP metrics are zeroed or absent for TCP tests
|
||||
# --- Test Execution Wrapper ---
|
||||
def run_iperf_test(server_ip, server_port, protocol, source_node_name, dest_node_name, duration, connect_timeout_ms, stall_threshold_sec):
|
||||
"""
|
||||
Runs a single iperf3 test with stall monitoring and publishes metrics.
|
||||
"""
|
||||
logging.info(f"Running iperf3 {protocol.upper()} test from {source_node_name} to {dest_node_name} ({server_ip}:{server_port})")
|
||||
labels = {'source_node': source_node_name, 'destination_node': dest_node_name, 'protocol': protocol}
|
||||
|
||||
try:
|
||||
IPERF_JITTER_MS.labels(**labels).set(0)
|
||||
IPERF_PACKETS_TOTAL.labels(**labels).set(0)
|
||||
IPERF_LOST_PACKETS.labels(**labels).set(0)
|
||||
except KeyError:
|
||||
pass
|
||||
returncode, stdout, stderr = execute_iperf_test(
|
||||
server_ip=server_ip,
|
||||
server_port=server_port,
|
||||
protocol=protocol,
|
||||
duration=duration,
|
||||
connect_timeout_ms=connect_timeout_ms,
|
||||
stall_threshold_sec=stall_threshold_sec,
|
||||
)
|
||||
|
||||
if not stdout.strip():
|
||||
logging.warning(f"iperf3 produced no stdout for {dest_node_name}. Stderr: {stderr.strip()}")
|
||||
reset_failure_metrics(labels)
|
||||
return
|
||||
|
||||
try:
|
||||
result_json = json.loads(stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
logging.warning(f"Failed to parse iperf3 JSON output for {dest_node_name}: {e}. Output: {stdout[:200]}")
|
||||
reset_failure_metrics(labels)
|
||||
return
|
||||
|
||||
parse_and_publish_metrics(result_json, source_node_name, dest_node_name, protocol)
|
||||
except IperfStallException as e:
|
||||
logging.warning(f"Test from {source_node_name} to {dest_node_name} ({protocol.upper()}) aborted: {e}")
|
||||
reset_failure_metrics(labels)
|
||||
except IperfTimeoutException as e:
|
||||
logging.warning(f"Test from {source_node_name} to {dest_node_name} ({protocol.upper()}) timed out: {e}")
|
||||
reset_failure_metrics(labels)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected exception during iperf3 test for {dest_node_name}: {e}")
|
||||
reset_failure_metrics(labels)
|
||||
|
||||
# --- Main Exporter Loop ---
|
||||
def main_loop():
|
||||
"""
|
||||
Main operational loop of the iperf3 exporter.
|
||||
"""
|
||||
test_interval = int(os.getenv('IPERF_TEST_INTERVAL', 300))
|
||||
server_port = int(os.getenv('IPERF_SERVER_PORT', 5201))
|
||||
protocol = os.getenv('IPERF_TEST_PROTOCOL', 'tcp').lower()
|
||||
source_node_name = os.getenv('SOURCE_NODE_NAME')
|
||||
|
||||
# Support either IPERF_TEST_DURATION or IPERF_TEST_TIMEOUT from Helm values
|
||||
duration = int(os.getenv('IPERF_TEST_DURATION', os.getenv('IPERF_TEST_TIMEOUT', 5)))
|
||||
connect_timeout_ms = int(os.getenv('IPERF_CONNECT_TIMEOUT_MS', 3000))
|
||||
stall_threshold_sec = int(os.getenv('IPERF_STALL_THRESHOLD_SECONDS', 5))
|
||||
|
||||
if not source_node_name:
|
||||
logging.error("CRITICAL: SOURCE_NODE_NAME environment variable not set. This is required. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
logging.info(
|
||||
f"Exporter configured. Source Node: {source_node_name}, "
|
||||
f"Test Interval: {test_interval}s, Duration: {duration}s, Server Port: {server_port}, Protocol: {protocol.upper()}, "
|
||||
f"Connect Timeout: {connect_timeout_ms}ms, Stall Threshold: {stall_threshold_sec}s"
|
||||
)
|
||||
|
||||
while not shutdown_requested:
|
||||
logging.info("Starting new iperf test cycle...")
|
||||
servers = discover_iperf_servers(source_node_name)
|
||||
|
||||
if not servers:
|
||||
logging.warning("No iperf servers discovered in this cycle. Check K8s setup and RBAC permissions.")
|
||||
else:
|
||||
for server in servers:
|
||||
if shutdown_requested:
|
||||
break
|
||||
|
||||
dest_node_name = server.get('node_name', 'unknown_destination_node')
|
||||
server_ip = server.get('ip')
|
||||
|
||||
if not server_ip:
|
||||
logging.warning(f"Discovered server entry missing an IP: {server}. Skipping.")
|
||||
continue
|
||||
|
||||
if dest_node_name == source_node_name:
|
||||
logging.info(f"Skipping test to self: {source_node_name} to {server_ip} (on same node: {dest_node_name}).")
|
||||
continue
|
||||
|
||||
run_iperf_test(
|
||||
server_ip=server_ip,
|
||||
server_port=server_port,
|
||||
protocol=protocol,
|
||||
source_node_name=source_node_name,
|
||||
dest_node_name=dest_node_name,
|
||||
duration=duration,
|
||||
connect_timeout_ms=connect_timeout_ms,
|
||||
stall_threshold_sec=stall_threshold_sec,
|
||||
)
|
||||
|
||||
if not shutdown_requested:
|
||||
IPERF_LAST_TEST_TIMESTAMP_SECONDS.labels(source_node=source_node_name).set(time.time())
|
||||
logging.info(f"Test cycle completed. Sleeping for {test_interval} seconds.")
|
||||
slept = 0
|
||||
while slept < test_interval and not shutdown_requested:
|
||||
time.sleep(1)
|
||||
slept += 1
|
||||
|
||||
logging.info("Exporter main loop stopped gracefully.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
listen_port = int(os.getenv('LISTEN_PORT', 9876))
|
||||
|
||||
try:
|
||||
start_http_server(listen_port)
|
||||
logging.info(f"Prometheus exporter listening on port {listen_port}")
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to start Prometheus HTTP server on port {listen_port}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
main_loop()
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
prometheus-client
|
||||
iperf3
|
||||
kubernetes
|
||||
prometheus-client>=0.20.0
|
||||
kubernetes>=29.0.0
|
||||
111
exporter/test_exporter.py
Normal file
111
exporter/test_exporter.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import importlib.util
|
||||
|
||||
spec = importlib.util.spec_from_file_location("exporter_mod", os.path.join(os.path.dirname(__file__), "exporter.py"))
|
||||
exporter = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(exporter)
|
||||
|
||||
class TestExporter(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.source = "node-a"
|
||||
self.dest = "node-b"
|
||||
self.labels = {'source_node': self.source, 'destination_node': self.dest, 'protocol': 'tcp'}
|
||||
self.udp_labels = {'source_node': self.source, 'destination_node': self.dest, 'protocol': 'udp'}
|
||||
|
||||
def test_parse_tcp_success(self):
|
||||
tcp_json = {
|
||||
"end": {
|
||||
"streams": [
|
||||
{
|
||||
"sender": {
|
||||
"mean_rtt": 250, # 250 microseconds = 0.25 ms
|
||||
"retransmits": 3
|
||||
}
|
||||
}
|
||||
],
|
||||
"sum_sent": {
|
||||
"seconds": 5.0,
|
||||
"bytes": 50000000,
|
||||
"bits_per_second": 80000000.0,
|
||||
"retransmits": 3
|
||||
},
|
||||
"sum_received": {
|
||||
"seconds": 5.0,
|
||||
"bytes": 49000000,
|
||||
"bits_per_second": 78400000.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exporter.parse_and_publish_metrics(tcp_json, self.source, self.dest, 'tcp')
|
||||
|
||||
self.assertEqual(exporter.IPERF_TEST_SUCCESS.labels(**self.labels)._value.get(), 1)
|
||||
self.assertAlmostEqual(exporter.IPERF_BANDWIDTH_MBPS.labels(**self.labels)._value.get(), 78.4)
|
||||
self.assertEqual(exporter.IPERF_TCP_RETRANSMITS.labels(**self.labels)._value.get(), 3)
|
||||
self.assertAlmostEqual(exporter.IPERF_TCP_RTT_MS.labels(**self.labels)._value.get(), 0.25)
|
||||
self.assertEqual(exporter.IPERF_TEST_DURATION_SECONDS.labels(**self.labels)._value.get(), 5.0)
|
||||
self.assertEqual(exporter.IPERF_JITTER_MS.labels(**self.labels)._value.get(), 0)
|
||||
|
||||
def test_parse_udp_success(self):
|
||||
udp_json = {
|
||||
"end": {
|
||||
"sum": {
|
||||
"seconds": 5.0,
|
||||
"bytes": 1000000,
|
||||
"bits_per_second": 1600000.0,
|
||||
"jitter_ms": 1.25,
|
||||
"packets": 1000,
|
||||
"lost_packets": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exporter.parse_and_publish_metrics(udp_json, self.source, self.dest, 'udp')
|
||||
|
||||
self.assertEqual(exporter.IPERF_TEST_SUCCESS.labels(**self.udp_labels)._value.get(), 1)
|
||||
self.assertAlmostEqual(exporter.IPERF_BANDWIDTH_MBPS.labels(**self.udp_labels)._value.get(), 1.6)
|
||||
self.assertEqual(exporter.IPERF_JITTER_MS.labels(**self.udp_labels)._value.get(), 1.25)
|
||||
self.assertEqual(exporter.IPERF_PACKETS_TOTAL.labels(**self.udp_labels)._value.get(), 1000)
|
||||
self.assertEqual(exporter.IPERF_LOST_PACKETS.labels(**self.udp_labels)._value.get(), 2)
|
||||
self.assertEqual(exporter.IPERF_TCP_RETRANSMITS.labels(**self.udp_labels)._value.get(), 0)
|
||||
|
||||
def test_parse_error_result(self):
|
||||
err_json = {
|
||||
"error": "error - unable to connect to server: Connection timed out"
|
||||
}
|
||||
|
||||
exporter.parse_and_publish_metrics(err_json, self.source, self.dest, 'tcp')
|
||||
|
||||
self.assertEqual(exporter.IPERF_TEST_SUCCESS.labels(**self.labels)._value.get(), 0)
|
||||
self.assertEqual(exporter.IPERF_BANDWIDTH_MBPS.labels(**self.labels)._value.get(), 0)
|
||||
self.assertEqual(exporter.IPERF_TCP_RETRANSMITS.labels(**self.labels)._value.get(), 0)
|
||||
self.assertEqual(exporter.IPERF_TCP_RTT_MS.labels(**self.labels)._value.get(), 0)
|
||||
|
||||
@patch("subprocess.Popen")
|
||||
def test_stall_exception_raised_on_stall(self, mock_popen):
|
||||
# Simulate a process that never terminates and produces no I/O progress
|
||||
fake_proc = MagicMock()
|
||||
fake_proc.poll.return_value = None # Process is running
|
||||
fake_proc.pid = 999999 # Non-existent PID to simulate no /proc IO advancement
|
||||
mock_popen.return_value = fake_proc
|
||||
|
||||
with self.assertRaises(exporter.IperfTimeoutException):
|
||||
exporter.execute_iperf_test(
|
||||
server_ip="127.0.0.1",
|
||||
server_port=5201,
|
||||
protocol="tcp",
|
||||
duration=1,
|
||||
connect_timeout_ms=500,
|
||||
stall_threshold_sec=1,
|
||||
)
|
||||
|
||||
fake_proc.kill.assert_called()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
234
index.yaml
234
index.yaml
@@ -1,234 +0,0 @@
|
||||
apiVersion: v1
|
||||
entries:
|
||||
iperf3-monitor:
|
||||
- annotations:
|
||||
artifacthub.io/category: networking
|
||||
artifacthub.io/changes: |
|
||||
- Add initial Helm chart structure.
|
||||
apiVersion: v2
|
||||
appVersion: 0.1.9
|
||||
created: "2025-07-01T20:10:45.681665311Z"
|
||||
dependencies:
|
||||
- condition: dependencies.install, serviceMonitor.enabled, !dependencies.useTrueChartsPrometheusOperator
|
||||
name: kube-prometheus-stack
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
version: '>=30.0.0'
|
||||
- condition: dependencies.install, serviceMonitor.enabled, dependencies.useTrueChartsPrometheusOperator
|
||||
name: prometheus-operator
|
||||
repository: oci://tccr.io/truecharts
|
||||
version: '>=8.11.1'
|
||||
description: A Helm chart for deploying a Kubernetes-native iperf3 network performance
|
||||
monitoring service with Prometheus and Grafana.
|
||||
digest: ba71668f2e79f143971758eddf2c1ccb81e8fa8dd681748f4a34718af83c523c
|
||||
home: https://github.com/malarinv/iperf3-monitor
|
||||
icon: https://raw.githubusercontent.com/malarinv/iperf3-monitor/main/icon.png
|
||||
keywords:
|
||||
- iperf3
|
||||
- network
|
||||
- performance
|
||||
- monitoring
|
||||
- kubernetes
|
||||
- prometheus
|
||||
- grafana
|
||||
maintainers:
|
||||
- email: malarkannan.invention@gmail.com
|
||||
name: Malar Invention
|
||||
name: iperf3-monitor
|
||||
sources:
|
||||
- https://github.com/malarinv/iperf3-monitor
|
||||
type: application
|
||||
urls:
|
||||
- https://malarinv.github.io/iperf3-monitor/iperf3-monitor-0.1.9.tgz
|
||||
version: 0.1.9
|
||||
- annotations:
|
||||
artifacthub.io/category: networking
|
||||
artifacthub.io/changes: |
|
||||
- Add initial Helm chart structure.
|
||||
apiVersion: v2
|
||||
appVersion: 0.1.8
|
||||
created: "2025-06-20T20:43:41.906216787Z"
|
||||
dependencies:
|
||||
- condition: dependencies.install, serviceMonitor.enabled, !dependencies.useTrueChartsPrometheusOperator
|
||||
name: kube-prometheus-stack
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
version: '>=30.0.0'
|
||||
- condition: dependencies.install, serviceMonitor.enabled, dependencies.useTrueChartsPrometheusOperator
|
||||
name: prometheus-operator
|
||||
repository: oci://tccr.io/truecharts
|
||||
version: '>=8.11.1'
|
||||
description: A Helm chart for deploying a Kubernetes-native iperf3 network performance
|
||||
monitoring service with Prometheus and Grafana.
|
||||
digest: dd1707ef16c06a1a3c4979983a0141c0f19bbd51b7b95a3bda22033780a517ba
|
||||
home: https://github.com/malarinv/iperf3-monitor
|
||||
icon: https://raw.githubusercontent.com/malarinv/iperf3-monitor/main/icon.png
|
||||
keywords:
|
||||
- iperf3
|
||||
- network
|
||||
- performance
|
||||
- monitoring
|
||||
- kubernetes
|
||||
- prometheus
|
||||
- grafana
|
||||
maintainers:
|
||||
- email: malarkannan.invention@gmail.com
|
||||
name: Malar Invention
|
||||
name: iperf3-monitor
|
||||
sources:
|
||||
- https://github.com/malarinv/iperf3-monitor
|
||||
type: application
|
||||
urls:
|
||||
- https://malarinv.github.io/iperf3-monitor/iperf3-monitor-0.1.8.tgz
|
||||
version: 0.1.8
|
||||
- annotations:
|
||||
artifacthub.io/category: networking
|
||||
artifacthub.io/changes: |
|
||||
- Add initial Helm chart structure.
|
||||
apiVersion: v2
|
||||
appVersion: 0.1.6
|
||||
created: "2025-06-20T19:36:24.8256252Z"
|
||||
dependencies:
|
||||
- condition: dependencies.install, serviceMonitor.enabled, !dependencies.useTrueChartsPrometheusOperator
|
||||
name: kube-prometheus-stack
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
version: '>=30.0.0'
|
||||
- condition: dependencies.install, serviceMonitor.enabled, dependencies.useTrueChartsPrometheusOperator
|
||||
name: prometheus-operator
|
||||
repository: oci://tccr.io/truecharts
|
||||
version: '>=8.11.1'
|
||||
description: A Helm chart for deploying a Kubernetes-native iperf3 network performance
|
||||
monitoring service with Prometheus and Grafana.
|
||||
digest: e23e2801a298edf8c345fcce210f7186a2304cf8321f315a244e44f0ef83e4bd
|
||||
home: https://github.com/malarinv/iperf3-monitor
|
||||
icon: https://raw.githubusercontent.com/malarinv/iperf3-monitor/main/icon.png
|
||||
keywords:
|
||||
- iperf3
|
||||
- network
|
||||
- performance
|
||||
- monitoring
|
||||
- kubernetes
|
||||
- prometheus
|
||||
- grafana
|
||||
maintainers:
|
||||
- email: malarkannan.invention@gmail.com
|
||||
name: Malar Invention
|
||||
name: iperf3-monitor
|
||||
sources:
|
||||
- https://github.com/malarinv/iperf3-monitor
|
||||
type: application
|
||||
urls:
|
||||
- https://malarinv.github.io/iperf3-monitor/iperf3-monitor-0.1.6.tgz
|
||||
version: 0.1.6
|
||||
- annotations:
|
||||
artifacthub.io/category: networking
|
||||
artifacthub.io/changes: |
|
||||
- Add initial Helm chart structure.
|
||||
apiVersion: v2
|
||||
appVersion: 0.1.5
|
||||
created: "2025-06-20T13:41:15.223267266Z"
|
||||
dependencies:
|
||||
- condition: dependencies.install, serviceMonitor.enabled, !dependencies.useTrueChartsPrometheusOperator
|
||||
name: kube-prometheus-stack
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
version: '>=30.0.0'
|
||||
- condition: dependencies.install, serviceMonitor.enabled, dependencies.useTrueChartsPrometheusOperator
|
||||
name: prometheus-operator
|
||||
repository: oci://tccr.io/truecharts
|
||||
version: '>=8.11.1'
|
||||
description: A Helm chart for deploying a Kubernetes-native iperf3 network performance
|
||||
monitoring service with Prometheus and Grafana.
|
||||
digest: 565b4615cbfa166cb5b158d43bc0f79fb2482c6213e8d9c67b32cd2e2dac6103
|
||||
home: https://github.com/malarinv/iperf3-monitor
|
||||
icon: https://raw.githubusercontent.com/malarinv/iperf3-monitor/main/icon.png
|
||||
keywords:
|
||||
- iperf3
|
||||
- network
|
||||
- performance
|
||||
- monitoring
|
||||
- kubernetes
|
||||
- prometheus
|
||||
- grafana
|
||||
maintainers:
|
||||
- email: malarkannan.invention@gmail.com
|
||||
name: Malar Invention
|
||||
name: iperf3-monitor
|
||||
sources:
|
||||
- https://github.com/malarinv/iperf3-monitor
|
||||
type: application
|
||||
urls:
|
||||
- https://malarinv.github.io/iperf3-monitor/iperf3-monitor-0.1.5.tgz
|
||||
version: 0.1.5
|
||||
- annotations:
|
||||
artifacthub.io/category: networking
|
||||
artifacthub.io/changes: |
|
||||
- Add initial Helm chart structure.
|
||||
apiVersion: v2
|
||||
appVersion: 0.1.4
|
||||
created: "2025-06-20T12:13:54.388432454Z"
|
||||
dependencies:
|
||||
- condition: serviceMonitor.enabled, !dependencies.useTrueChartsPrometheusOperator
|
||||
name: kube-prometheus-stack
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
version: '>=30.0.0'
|
||||
- condition: serviceMonitor.enabled, dependencies.useTrueChartsPrometheusOperator
|
||||
name: prometheus-operator
|
||||
repository: oci://tccr.io/truecharts
|
||||
version: '>=8.11.1'
|
||||
description: A Helm chart for deploying a Kubernetes-native iperf3 network performance
|
||||
monitoring service with Prometheus and Grafana.
|
||||
digest: 6c0dbe54f696cdc2d98ecb54cff5c5b3c9eaea913e3caa798ad81c587ef5d199
|
||||
home: https://github.com/malarinv/iperf3-monitor
|
||||
icon: https://raw.githubusercontent.com/malarinv/iperf3-monitor/main/icon.png
|
||||
keywords:
|
||||
- iperf3
|
||||
- network
|
||||
- performance
|
||||
- monitoring
|
||||
- kubernetes
|
||||
- prometheus
|
||||
- grafana
|
||||
maintainers:
|
||||
- email: malarkannan.invention@gmail.com
|
||||
name: Malar Invention
|
||||
name: iperf3-monitor
|
||||
sources:
|
||||
- https://github.com/malarinv/iperf3-monitor
|
||||
type: application
|
||||
urls:
|
||||
- https://malarinv.github.io/iperf3-monitor/iperf3-monitor-0.1.4.tgz
|
||||
version: 0.1.4
|
||||
- annotations:
|
||||
artifacthub.io/category: networking
|
||||
artifacthub.io/changes: |
|
||||
- Add initial Helm chart structure.
|
||||
apiVersion: v2
|
||||
appVersion: 0.1.1
|
||||
created: "2025-06-19T21:43:47.560731092Z"
|
||||
dependencies:
|
||||
- condition: serviceMonitor.enabled
|
||||
name: kube-prometheus-stack
|
||||
repository: https://prometheus-community.github.io/helm-charts
|
||||
version: '>=30.0.0'
|
||||
description: A Helm chart for deploying a Kubernetes-native iperf3 network performance
|
||||
monitoring service with Prometheus and Grafana.
|
||||
digest: e334d11e50f6303486830cc1384bfe21eecfe803f665868eaeb294d5ef168559
|
||||
home: https://github.com/malarinv/iperf3-monitor
|
||||
icon: https://raw.githubusercontent.com/malarinv/iperf3-monitor/main/icon.png
|
||||
keywords:
|
||||
- iperf3
|
||||
- network
|
||||
- performance
|
||||
- monitoring
|
||||
- kubernetes
|
||||
- prometheus
|
||||
- grafana
|
||||
maintainers:
|
||||
- email: malarkannan.invention@gmail.com
|
||||
name: Malar Invention
|
||||
name: iperf3-monitor
|
||||
sources:
|
||||
- https://github.com/malarinv/iperf3-monitor
|
||||
type: application
|
||||
urls:
|
||||
- https://malarinv.github.io/iperf3-monitor/iperf3-monitor-0.1.1.tgz
|
||||
version: 0.1.1
|
||||
generated: "2025-07-01T20:10:45.613763939Z"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
17
mise.toml
Normal file
17
mise.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[tools]
|
||||
python = "3.12"
|
||||
uv = "latest"
|
||||
helm = "3.17.3"
|
||||
yq = "latest"
|
||||
|
||||
[tasks.test]
|
||||
description = "Run exporter unit tests"
|
||||
run = "uv run --with-requirements exporter/requirements.txt python -m unittest discover -s exporter -p 'test_*.py'"
|
||||
|
||||
[tasks."chart:build"]
|
||||
description = "Build Helm chart dependencies"
|
||||
run = "helm dependency build ./charts/iperf3-monitor"
|
||||
|
||||
[tasks."chart:lint"]
|
||||
description = "Lint Helm chart"
|
||||
run = "helm lint ./charts/iperf3-monitor"
|
||||
Reference in New Issue
Block a user