mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2026-09-30 11:37:16 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e35d70595 | ||
|
|
e4625cfcae | ||
|
|
eb803dce0e | ||
|
|
a06f08217a | ||
|
|
29d27cf255 | ||
|
|
235a60e587 | ||
|
|
a8f7c46b0d | ||
|
|
26d895ccb7 | ||
|
|
b43efc458f | ||
|
|
21222ff119 | ||
|
|
72f60fcaa9 |
@@ -6,6 +6,9 @@ on:
|
|||||||
- "salt/sensoroni/files/analyzers/**"
|
- "salt/sensoroni/files/analyzers/**"
|
||||||
- "salt/manager/tools/sbin/**"
|
- "salt/manager/tools/sbin/**"
|
||||||
- "salt/_beacons/**"
|
- "salt/_beacons/**"
|
||||||
|
- "salt/telegraf/tools/sbin_jinja/**"
|
||||||
|
- "salt/telegraf/defaults.yaml"
|
||||||
|
- "salt/telegraf/soc_telegraf.yaml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -34,3 +37,25 @@ jobs:
|
|||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
PYTHONPATH=${{ matrix.python-code-path }} pytest ${{ matrix.python-code-path }} --cov=${{ matrix.python-code-path }} --doctest-modules --cov-report=term --cov-fail-under=100 --cov-config=pytest.ini
|
PYTHONPATH=${{ matrix.python-code-path }} pytest ${{ matrix.python-code-path }} --cov=${{ matrix.python-code-path }} --doctest-modules --cov-report=term --cov-fail-under=100 --cov-config=pytest.ini
|
||||||
|
|
||||||
|
telegraf-collector:
|
||||||
|
# so-container-stats is a jinja template rather than an importable module, so it gets its
|
||||||
|
# own job: the test renders it the way salt does, then drives it with a faked docker engine
|
||||||
|
# and cgroup tree. No container runtime is needed.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: "3.14"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install flake8 pytest jinja2 pyyaml
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
flake8 salt/telegraf/tools/sbin_jinja/so-container-stats_test.py --config=pytest.ini
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
pytest salt/telegraf/tools/sbin_jinja/so-container-stats_test.py -v
|
||||||
|
|||||||
@@ -217,9 +217,11 @@ sostatus_log:
|
|||||||
- replace: False
|
- replace: False
|
||||||
|
|
||||||
# Install sostatus check cron. This is used to populate Grid.
|
# Install sostatus check cron. This is used to populate Grid.
|
||||||
|
# telegraf reads status.log on the same minute boundary this runs, so write aside and rename
|
||||||
|
# rather than truncating the file it is reading
|
||||||
so-status_check_cron:
|
so-status_check_cron:
|
||||||
cron.present:
|
cron.present:
|
||||||
- name: '/usr/sbin/so-status -j > /opt/so/log/sostatus/status.log 2>&1'
|
- name: '/usr/sbin/so-status -j > /opt/so/log/sostatus/status.log.tmp 2>&1; mv -f /opt/so/log/sostatus/status.log.tmp /opt/so/log/sostatus/status.log'
|
||||||
- identifier: so-status_check_cron
|
- identifier: so-status_check_cron
|
||||||
- user: root
|
- user: root
|
||||||
- minute: '*/1'
|
- minute: '*/1'
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import sys
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import tempfile
|
||||||
|
|
||||||
sys.path.append('/opt/saltstack/salt/lib/python3.10/site-packages/')
|
sys.path.append('/opt/saltstack/salt/lib/python3.10/site-packages/')
|
||||||
import salt.config
|
import salt.config
|
||||||
@@ -17,6 +18,21 @@ import salt.loader
|
|||||||
__opts__ = salt.config.minion_config('/etc/salt/minion')
|
__opts__ = salt.config.minion_config('/etc/salt/minion')
|
||||||
__grains__ = salt.loader.grains(__opts__)
|
__grains__ = salt.loader.grains(__opts__)
|
||||||
|
|
||||||
|
def write_atomic(path, value):
|
||||||
|
# telegraf reads these files on its own schedule; replacing them by rename means it never
|
||||||
|
# reads a truncated file and reports an empty value as if it were real
|
||||||
|
directory = os.path.dirname(path)
|
||||||
|
handle, temp = tempfile.mkstemp(dir=directory)
|
||||||
|
try:
|
||||||
|
with os.fdopen(handle, 'w') as f:
|
||||||
|
f.write(str(value))
|
||||||
|
os.chmod(temp, 0o644)
|
||||||
|
os.replace(temp, path)
|
||||||
|
except Exception:
|
||||||
|
os.path.exists(temp) and os.unlink(temp)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def check_needs_restarted():
|
def check_needs_restarted():
|
||||||
osfam = __grains__['os_family']
|
osfam = __grains__['os_family']
|
||||||
val = '0'
|
val = '0'
|
||||||
@@ -34,8 +50,7 @@ def check_needs_restarted():
|
|||||||
else:
|
else:
|
||||||
fail("Unsupported OS")
|
fail("Unsupported OS")
|
||||||
|
|
||||||
with open(outfile, 'w') as f:
|
write_atomic(outfile, val)
|
||||||
f.write(val)
|
|
||||||
|
|
||||||
def check_for_fps():
|
def check_for_fps():
|
||||||
feat = 'fps'
|
feat = 'fps'
|
||||||
@@ -56,8 +71,7 @@ def check_for_fps():
|
|||||||
# Unknown, so assume 0
|
# Unknown, so assume 0
|
||||||
fps = 0
|
fps = 0
|
||||||
|
|
||||||
with open('/opt/so/log/sostatus/fps_enabled', 'w') as f:
|
write_atomic('/opt/so/log/sostatus/fps_enabled', fps)
|
||||||
f.write(str(fps))
|
|
||||||
|
|
||||||
def check_for_lks():
|
def check_for_lks():
|
||||||
feat = 'Lks'
|
feat = 'Lks'
|
||||||
@@ -80,8 +94,7 @@ def check_for_lks():
|
|||||||
lks = 1
|
lks = 1
|
||||||
if lks:
|
if lks:
|
||||||
break
|
break
|
||||||
with open('/opt/so/log/sostatus/lks_enabled', 'w') as f:
|
write_atomic('/opt/so/log/sostatus/lks_enabled', lks)
|
||||||
f.write(str(lks))
|
|
||||||
|
|
||||||
def fail(msg):
|
def fail(msg):
|
||||||
print(msg, file=sys.stderr)
|
print(msg, file=sys.stderr)
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
|
|||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Unexpected authorization header" # expected WARN log lines indicating invalid auth header
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Unexpected authorization header" # expected WARN log lines indicating invalid auth header
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing ory_kratos_session cookie" # expected WARN log lines indicating invalid auth header
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing ory_kratos_session cookie" # expected WARN log lines indicating invalid auth header
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Static assets preprocessor only supports GET and HEAD requests" # expected WARN log lines indicating invalid auth header
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Static assets preprocessor only supports GET and HEAD requests" # expected WARN log lines indicating invalid auth header
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|respondError" # respondError is a function name, output via http middleware as standard request logging
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
||||||
|
|||||||
@@ -125,4 +125,6 @@ else
|
|||||||
RAIDSTATUS=1
|
RAIDSTATUS=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "nsmraid=$RAIDSTATUS" > /opt/so/log/raid/status.log
|
# telegraf reads this file; write aside and rename so it never sees a half-written file
|
||||||
|
echo "nsmraid=$RAIDSTATUS" > /opt/so/log/raid/status.log.tmp
|
||||||
|
mv -f /opt/so/log/raid/status.log.tmp /opt/so/log/raid/status.log
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ elastalert:
|
|||||||
- gid: 933
|
- gid: 933
|
||||||
- home: /opt/so/conf/elastalert
|
- home: /opt/so/conf/elastalert
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elastalogdir:
|
elastalogdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ elastic-agent-pr:
|
|||||||
- gid: 948
|
- gid: 948
|
||||||
- home: /opt/so/conf/elastic-fleet-pr
|
- home: /opt/so/conf/elastic-fleet-pr
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ elastic-agent:
|
|||||||
- gid: 949
|
- gid: 949
|
||||||
- home: /opt/so/conf/elastic-agent
|
- home: /opt/so/conf/elastic-agent
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticagentconfdir:
|
elasticagentconfdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ elastic-fleet:
|
|||||||
- gid: 947
|
- gid: 947
|
||||||
- home: /opt/so/conf/elastic-fleet
|
- home: /opt/so/conf/elastic-fleet
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticfleet_sbin:
|
elasticfleet_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ elasticsearch:
|
|||||||
- gid: 930
|
- gid: 930
|
||||||
- home: /opt/so/conf/elasticsearch
|
- home: /opt/so/conf/elasticsearch
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticsearch_sbin:
|
elasticsearch_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
|
|||||||
@@ -94,9 +94,11 @@ metrics_link_file:
|
|||||||
- docker_container: so-influxdb
|
- docker_container: so-influxdb
|
||||||
|
|
||||||
# Install cron job to determine size of influxdb for telegraf
|
# Install cron job to determine size of influxdb for telegraf
|
||||||
|
# telegraf reads this while the cron rewrites it, so write aside and rename rather than
|
||||||
|
# truncating in place. tgraflogdir recurses ownership, so the temp file is chowned to match
|
||||||
get_influxdb_size:
|
get_influxdb_size:
|
||||||
cron.present:
|
cron.present:
|
||||||
- name: 'du -s -k /nsm/influxdb | cut -f1 > /opt/so/log/telegraf/influxdb_size.log 2>&1'
|
- name: 'du -s -k /nsm/influxdb | cut -f1 > /opt/so/log/telegraf/influxdb_size.log.tmp 2>&1; chown 939:939 /opt/so/log/telegraf/influxdb_size.log.tmp; mv -f /opt/so/log/telegraf/influxdb_size.log.tmp /opt/so/log/telegraf/influxdb_size.log'
|
||||||
- identifier: get_influxdb_size
|
- identifier: get_influxdb_size
|
||||||
- user: root
|
- user: root
|
||||||
- minute: '*/1'
|
- minute: '*/1'
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ kafka_user:
|
|||||||
- gid: 960
|
- gid: 960
|
||||||
- home: /opt/so/conf/kafka
|
- home: /opt/so/conf/kafka
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
kafka_home_dir:
|
kafka_home_dir:
|
||||||
file.absent:
|
file.absent:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ kibana:
|
|||||||
- gid: 932
|
- gid: 932
|
||||||
- home: /opt/so/conf/kibana
|
- home: /opt/so/conf/kibana
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
# Drop the correct nginx config based on role
|
# Drop the correct nginx config based on role
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ kratos:
|
|||||||
- uid: 928
|
- uid: 928
|
||||||
- gid: 928
|
- gid: 928
|
||||||
- home: /opt/so/conf/kratos
|
- home: /opt/so/conf/kratos
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
kratosdir:
|
kratosdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ logstash:
|
|||||||
- uid: 931
|
- uid: 931
|
||||||
- gid: 931
|
- gid: 931
|
||||||
- home: /opt/so/conf/logstash
|
- home: /opt/so/conf/logstash
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
logstash_sbin:
|
logstash_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ so-repo-sync:
|
|||||||
|
|
||||||
so_fleetagent_status:
|
so_fleetagent_status:
|
||||||
cron.present:
|
cron.present:
|
||||||
- name: /usr/sbin/so-elasticagent-status > /opt/so/log/agents/agentstatus.log 2>&1
|
- name: '/usr/sbin/so-elasticagent-status > /opt/so/log/agents/agentstatus.log.tmp 2>&1; mv -f /opt/so/log/agents/agentstatus.log.tmp /opt/so/log/agents/agentstatus.log'
|
||||||
- identifier: so_fleetagent_status
|
- identifier: so_fleetagent_status
|
||||||
- user: root
|
- user: root
|
||||||
- minute: '*/5'
|
- minute: '*/5'
|
||||||
|
|||||||
@@ -1496,7 +1496,7 @@ soc:
|
|||||||
verifyCert: false
|
verifyCert: false
|
||||||
notification:
|
notification:
|
||||||
dismissedPruneDays: 30
|
dismissedPruneDays: 30
|
||||||
enabled: false
|
enabled: true
|
||||||
playbook:
|
playbook:
|
||||||
autoUpdateEnabled: true
|
autoUpdateEnabled: true
|
||||||
playbookImportFrequencySeconds: 86400
|
playbookImportFrequencySeconds: 86400
|
||||||
@@ -2799,7 +2799,7 @@ soc:
|
|||||||
- id: sonnet
|
- id: sonnet
|
||||||
displayName: Claude Sonnet
|
displayName: Claude Sonnet
|
||||||
origin: USA
|
origin: USA
|
||||||
contextLimitSmall: 200000
|
contextLimitSmall: 1000000
|
||||||
contextLimitLarge: 1000000
|
contextLimitLarge: 1000000
|
||||||
lowBalanceColorAlert: 500000
|
lowBalanceColorAlert: 500000
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ soc:
|
|||||||
description: Schedules that are shared across the Security Onion product. Modify via one of the SOC Schedules view.
|
description: Schedules that are shared across the Security Onion product. Modify via one of the SOC Schedules view.
|
||||||
readonlyUi: True
|
readonlyUi: True
|
||||||
global: True
|
global: True
|
||||||
|
advanced: True
|
||||||
forcedType: string
|
forcedType: string
|
||||||
syntax: json
|
syntax: json
|
||||||
storage: db
|
storage: db
|
||||||
@@ -495,6 +496,7 @@ soc:
|
|||||||
description: JSON list of notifications. Modify via the SOC Notifications view.
|
description: JSON list of notifications. Modify via the SOC Notifications view.
|
||||||
readonlyUi: True
|
readonlyUi: True
|
||||||
global: True
|
global: True
|
||||||
|
advanced: True
|
||||||
forcedType: string
|
forcedType: string
|
||||||
syntax: json
|
syntax: json
|
||||||
storage: db
|
storage: db
|
||||||
@@ -503,6 +505,10 @@ soc:
|
|||||||
description: The number of days to retain dismissed notifications. When a notification is dismissed, it will be pruned after this many days. Only one user need dismiss a notification for it to be pruned.
|
description: The number of days to retain dismissed notifications. When a notification is dismissed, it will be pruned after this many days. Only one user need dismiss a notification for it to be pruned.
|
||||||
forcedType: int
|
forcedType: int
|
||||||
global: True
|
global: True
|
||||||
|
maxListLimit:
|
||||||
|
description: Maximum number of notifications to display.
|
||||||
|
forcedType: int
|
||||||
|
global: True
|
||||||
enabled:
|
enabled:
|
||||||
description: Enables or disables the SOC notification module.
|
description: Enables or disables the SOC notification module.
|
||||||
forcedType: bool
|
forcedType: bool
|
||||||
@@ -533,6 +539,48 @@ soc:
|
|||||||
global: True
|
global: True
|
||||||
sensitive: True
|
sensitive: True
|
||||||
advanced: True
|
advanced: True
|
||||||
|
postgresmetrics:
|
||||||
|
host:
|
||||||
|
description: Hostname or IP address of the PostgreSQL server used by Telegraf. Defaults to the manager hostname.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
port:
|
||||||
|
description: Port of the PostgreSQL server used by Telegraf.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
sslMode:
|
||||||
|
description: "Use encrypted connections to the PostgreSQL server used by Telegraf. Must be one of the following values: disable, allow, prefer, require, verify-ca, verify-full."
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
database:
|
||||||
|
description: Database to authenticate to on the PostgreSQL server.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
user:
|
||||||
|
description: Username to authenticate to the PostgreSQL server used by Telegraf.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
password:
|
||||||
|
description: Password used to authenticate to the PostgreSQL server used by Telegraf.
|
||||||
|
global: True
|
||||||
|
sensitive: True
|
||||||
|
advanced: True
|
||||||
|
cacheExpirationMs:
|
||||||
|
description: The interval (in milliseconds) to wait before querying the DB for updated metrics.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
maxMetricAgeSeconds:
|
||||||
|
description: The maximum age (in seconds) of metrics to display in the SOC Grid Metrics view. Metrics older than this value will not be displayed.
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
alarms:
|
||||||
|
description: JSON list of metric alarms. Modify via the SOC Grid Alarms view.
|
||||||
|
readonlyUi: True
|
||||||
|
advanced: True
|
||||||
|
global: True
|
||||||
|
forcedType: string
|
||||||
|
syntax: json
|
||||||
|
storage: db
|
||||||
salt:
|
salt:
|
||||||
longRelayTimeoutMs:
|
longRelayTimeoutMs:
|
||||||
description: Duration (in milliseconds) to wait for a response from the Salt API when executing tasks known for being long running before giving up and showing an error on the SOC UI.
|
description: Duration (in milliseconds) to wait for a response from the Salt API when executing tasks known for being long running before giving up and showing an error on the SOC UI.
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ suricata:
|
|||||||
- gid: 940
|
- gid: 940
|
||||||
- home: /nsm/suricata
|
- home: /nsm/suricata
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
socoregroupwithsuricata:
|
socoregroupwithsuricata:
|
||||||
group.present:
|
group.present:
|
||||||
|
|||||||
+90
-10
@@ -36,7 +36,7 @@ tgraf_sync_script_{{script}}:
|
|||||||
- name: /opt/so/conf/telegraf/scripts/{{script}}
|
- name: /opt/so/conf/telegraf/scripts/{{script}}
|
||||||
- user: root
|
- user: root
|
||||||
- group: 939
|
- group: 939
|
||||||
- mode: 770
|
- mode: 750
|
||||||
- template: jinja
|
- template: jinja
|
||||||
- source: salt://telegraf/scripts/{{script}}
|
- source: salt://telegraf/scripts/{{script}}
|
||||||
- defaults:
|
- defaults:
|
||||||
@@ -49,7 +49,7 @@ tgraf_sync_script_esindexsize.sh:
|
|||||||
- name: /opt/so/conf/telegraf/scripts/esindexsize.sh
|
- name: /opt/so/conf/telegraf/scripts/esindexsize.sh
|
||||||
- user: root
|
- user: root
|
||||||
- group: 939
|
- group: 939
|
||||||
- mode: 770
|
- mode: 750
|
||||||
- source: salt://telegraf/scripts/esindexsize.sh
|
- source: salt://telegraf/scripts/esindexsize.sh
|
||||||
{# Copy conf/elasticsearch/curl.config for telegraf to use with esindexsize.sh #}
|
{# Copy conf/elasticsearch/curl.config for telegraf to use with esindexsize.sh #}
|
||||||
tgraf_sync_escurl_conf:
|
tgraf_sync_escurl_conf:
|
||||||
@@ -61,6 +61,80 @@ tgraf_sync_escurl_conf:
|
|||||||
- source: salt://elasticsearch/curl.config
|
- source: salt://elasticsearch/curl.config
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
# so-container-stats runs on the host as somon, a docker group member, so the container does
|
||||||
|
# not need the docker socket
|
||||||
|
somongroup:
|
||||||
|
group.present:
|
||||||
|
- name: somon
|
||||||
|
- gid: 961
|
||||||
|
|
||||||
|
# cron chdirs to $HOME before running a job, so home must exist
|
||||||
|
somon:
|
||||||
|
user.present:
|
||||||
|
- uid: 961
|
||||||
|
- gid: 961
|
||||||
|
- home: /opt/so/log/somon
|
||||||
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
- groups:
|
||||||
|
- docker
|
||||||
|
# renumbering an existing somon is a no-op on a fresh host and lets a host created
|
||||||
|
# before the id changed converge instead of failing the whole telegraf state
|
||||||
|
- allow_uid_change: True
|
||||||
|
- allow_gid_change: True
|
||||||
|
- require:
|
||||||
|
- group: somongroup
|
||||||
|
|
||||||
|
somonlogdir:
|
||||||
|
file.directory:
|
||||||
|
- name: /opt/so/log/somon
|
||||||
|
- user: 961
|
||||||
|
- group: 961
|
||||||
|
- mode: 755
|
||||||
|
# the lock file is not otherwise managed; recurse so a renumber rechowns it too
|
||||||
|
- recurse:
|
||||||
|
- user
|
||||||
|
- group
|
||||||
|
- require:
|
||||||
|
- user: somon
|
||||||
|
|
||||||
|
containers_log:
|
||||||
|
file.managed:
|
||||||
|
- name: /opt/so/log/somon/containers.log
|
||||||
|
- user: 961
|
||||||
|
- group: 961
|
||||||
|
- mode: 644
|
||||||
|
- replace: False
|
||||||
|
- require:
|
||||||
|
- file: somonlogdir
|
||||||
|
|
||||||
|
# telegraf reads on the same minute boundary the collector runs, and docker stats takes
|
||||||
|
# seconds, so write aside and rename rather than truncating the file telegraf is reading.
|
||||||
|
# ; not && so a failed run replaces the file instead of leaving stale metrics behind.
|
||||||
|
# flock -n keeps a run that outlives its minute from racing the next one over the same tmp
|
||||||
|
# file; the skipped run leaves a stale containers.log, which containers.sh discards by age
|
||||||
|
so-container-stats_cron:
|
||||||
|
cron.present:
|
||||||
|
- name: "flock -n /opt/so/log/somon/containers.lock -c '/usr/sbin/so-container-stats > /opt/so/log/somon/containers.log.tmp 2>&1; mv -f /opt/so/log/somon/containers.log.tmp /opt/so/log/somon/containers.log'"
|
||||||
|
- identifier: so-container-stats_cron
|
||||||
|
- user: somon
|
||||||
|
- minute: '*/1'
|
||||||
|
- hour: '*'
|
||||||
|
- daymonth: '*'
|
||||||
|
- month: '*'
|
||||||
|
- dayweek: '*'
|
||||||
|
- require:
|
||||||
|
- user: somon
|
||||||
|
|
||||||
|
# salt.lasthighstate touches this at order 9001, after the container starts; pre-create it so
|
||||||
|
# docker does not create a directory at the bind mount source
|
||||||
|
lasthighstate_placeholder:
|
||||||
|
file.managed:
|
||||||
|
- name: /opt/so/log/salt/lasthighstate
|
||||||
|
- mode: 644
|
||||||
|
- replace: False
|
||||||
|
- makedirs: True
|
||||||
|
|
||||||
telegraf_sbin:
|
telegraf_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
@@ -69,14 +143,20 @@ telegraf_sbin:
|
|||||||
- group: root
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
|
|
||||||
#telegraf_sbin_jinja:
|
# so-container-stats needs the per-stat toggles, so it renders instead of copying
|
||||||
# file.recurse:
|
tgraf_sbin_jinja:
|
||||||
# - name: /usr/sbin
|
file.recurse:
|
||||||
# - source: salt://telegraf/tools/sbin_jinja
|
- name: /usr/sbin
|
||||||
# - user: 939
|
- source: salt://telegraf/tools/sbin_jinja
|
||||||
# - group: 939
|
- user: root
|
||||||
# - file_mode: 755
|
- group: root
|
||||||
# - template: jinja
|
- file_mode: 755
|
||||||
|
# the unit test lives beside the script; it must not ship or be rendered as jinja
|
||||||
|
- exclude_pat:
|
||||||
|
- "*_test.py"
|
||||||
|
- template: jinja
|
||||||
|
- defaults:
|
||||||
|
CONTAINER_STATS: {{ TELEGRAFMERGED.container_stats }}
|
||||||
|
|
||||||
tgrafconf:
|
tgrafconf:
|
||||||
file.managed:
|
file.managed:
|
||||||
|
|||||||
@@ -10,6 +10,69 @@ telegraf:
|
|||||||
flush_jitter: '0s'
|
flush_jitter: '0s'
|
||||||
debug: false
|
debug: false
|
||||||
quiet: false
|
quiet: false
|
||||||
|
container_stats:
|
||||||
|
tags:
|
||||||
|
identity: False
|
||||||
|
engine:
|
||||||
|
n_containers: False
|
||||||
|
n_containers_running: False
|
||||||
|
n_containers_stopped: False
|
||||||
|
n_containers_paused: False
|
||||||
|
n_images: False
|
||||||
|
n_cpus: False
|
||||||
|
n_goroutines: False
|
||||||
|
n_used_file_descriptors: False
|
||||||
|
n_listener_events: False
|
||||||
|
memory_total: False
|
||||||
|
cpu:
|
||||||
|
usage_percent: True
|
||||||
|
usage_total: False
|
||||||
|
usage_in_usermode: False
|
||||||
|
usage_in_kernelmode: False
|
||||||
|
usage_system: False
|
||||||
|
throttling_periods: False
|
||||||
|
throttling_throttled_periods: False
|
||||||
|
throttling_throttled_time: False
|
||||||
|
container_id: False
|
||||||
|
mem:
|
||||||
|
usage_percent: True
|
||||||
|
usage: False
|
||||||
|
limit: False
|
||||||
|
max_usage: False
|
||||||
|
active_anon: False
|
||||||
|
active_file: False
|
||||||
|
inactive_anon: False
|
||||||
|
inactive_file: False
|
||||||
|
unevictable: False
|
||||||
|
pgfault: False
|
||||||
|
pgmajfault: False
|
||||||
|
container_id: False
|
||||||
|
net:
|
||||||
|
rx_bytes: True
|
||||||
|
rx_packets: False
|
||||||
|
rx_errors: False
|
||||||
|
rx_dropped: False
|
||||||
|
tx_bytes: False
|
||||||
|
tx_packets: False
|
||||||
|
tx_errors: False
|
||||||
|
tx_dropped: False
|
||||||
|
container_id: False
|
||||||
|
blkio:
|
||||||
|
io_service_bytes_recursive_read: False
|
||||||
|
io_service_bytes_recursive_write: False
|
||||||
|
container_id: False
|
||||||
|
status:
|
||||||
|
uptime_ns: True
|
||||||
|
oomkilled: True
|
||||||
|
pid: False
|
||||||
|
exitcode: False
|
||||||
|
restart_count: False
|
||||||
|
started_at: False
|
||||||
|
finished_at: False
|
||||||
|
container_id: False
|
||||||
|
health:
|
||||||
|
health_status: False
|
||||||
|
failing_streak: False
|
||||||
scripts:
|
scripts:
|
||||||
eval:
|
eval:
|
||||||
- agentstatus.sh
|
- agentstatus.sh
|
||||||
@@ -19,6 +82,7 @@ telegraf:
|
|||||||
- oldpcap.sh
|
- oldpcap.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- suriloss.sh
|
- suriloss.sh
|
||||||
- surirules.sh
|
- surirules.sh
|
||||||
@@ -34,6 +98,7 @@ telegraf:
|
|||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
- redis.sh
|
- redis.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- suriloss.sh
|
- suriloss.sh
|
||||||
- surirules.sh
|
- surirules.sh
|
||||||
@@ -47,6 +112,7 @@ telegraf:
|
|||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
- redis.sh
|
- redis.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- features.sh
|
- features.sh
|
||||||
managerhype:
|
managerhype:
|
||||||
@@ -56,6 +122,7 @@ telegraf:
|
|||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
- redis.sh
|
- redis.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- features.sh
|
- features.sh
|
||||||
managersearch:
|
managersearch:
|
||||||
@@ -66,12 +133,14 @@ telegraf:
|
|||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
- redis.sh
|
- redis.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- features.sh
|
- features.sh
|
||||||
import:
|
import:
|
||||||
- influxdbsize.sh
|
- influxdbsize.sh
|
||||||
- lasthighstate.sh
|
- lasthighstate.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
sensor:
|
sensor:
|
||||||
- checkfiles.sh
|
- checkfiles.sh
|
||||||
@@ -79,6 +148,7 @@ telegraf:
|
|||||||
- oldpcap.sh
|
- oldpcap.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- suriloss.sh
|
- suriloss.sh
|
||||||
- surirules.sh
|
- surirules.sh
|
||||||
@@ -93,6 +163,7 @@ telegraf:
|
|||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
- redis.sh
|
- redis.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- suriloss.sh
|
- suriloss.sh
|
||||||
- surirules.sh
|
- surirules.sh
|
||||||
@@ -101,12 +172,14 @@ telegraf:
|
|||||||
idh:
|
idh:
|
||||||
- lasthighstate.sh
|
- lasthighstate.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
searchnode:
|
searchnode:
|
||||||
- eps.sh
|
- eps.sh
|
||||||
- lasthighstate.sh
|
- lasthighstate.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
- features.sh
|
- features.sh
|
||||||
receiver:
|
receiver:
|
||||||
@@ -115,16 +188,20 @@ telegraf:
|
|||||||
- os.sh
|
- os.sh
|
||||||
- raid.sh
|
- raid.sh
|
||||||
- redis.sh
|
- redis.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
fleet:
|
fleet:
|
||||||
- lasthighstate.sh
|
- lasthighstate.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
hypervisor:
|
hypervisor:
|
||||||
- lasthighstate.sh
|
- lasthighstate.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
desktop:
|
desktop:
|
||||||
- lasthighstate.sh
|
- lasthighstate.sh
|
||||||
- os.sh
|
- os.sh
|
||||||
|
- containers.sh
|
||||||
- sostatus.sh
|
- sostatus.sh
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ so-telegraf:
|
|||||||
docker_container.absent:
|
docker_container.absent:
|
||||||
- force: True
|
- force: True
|
||||||
|
|
||||||
|
so-container-stats_cron:
|
||||||
|
cron.absent:
|
||||||
|
- identifier: so-container-stats_cron
|
||||||
|
- user: somon
|
||||||
|
|
||||||
so-telegraf_so-status.disabled:
|
so-telegraf_so-status.disabled:
|
||||||
file.comment:
|
file.comment:
|
||||||
- name: /opt/so/conf/so-status/so-status.conf
|
- name: /opt/so/conf/so-status/so-status.conf
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ so-telegraf:
|
|||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-telegraf:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-telegraf:{{ GLOBALS.so_version }}
|
||||||
- restart_policy: unless-stopped
|
- restart_policy: unless-stopped
|
||||||
- user: 939
|
- user: 939:939
|
||||||
- group_add: 939,920
|
|
||||||
- environment:
|
- environment:
|
||||||
- HOST_ETC=/host/etc
|
- HOST_ETC=/host/etc
|
||||||
- HOST_SYS=/host/sys
|
- HOST_SYS=/host/sys
|
||||||
@@ -38,7 +37,6 @@ so-telegraf:
|
|||||||
- /opt/so/conf/telegraf/etc/telegraf.conf:/etc/telegraf/telegraf.conf:ro
|
- /opt/so/conf/telegraf/etc/telegraf.conf:/etc/telegraf/telegraf.conf:ro
|
||||||
- /opt/so/conf/telegraf/node_config.json:/etc/telegraf/node_config.json:ro
|
- /opt/so/conf/telegraf/node_config.json:/etc/telegraf/node_config.json:ro
|
||||||
- /var/run/utmp:/var/run/utmp:ro
|
- /var/run/utmp:/var/run/utmp:ro
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
- /:/host:ro
|
- /:/host:ro
|
||||||
- /sys:/host/sys:ro
|
- /sys:/host/sys:ro
|
||||||
- /proc:/host/proc:ro
|
- /proc:/host/proc:ro
|
||||||
@@ -51,7 +49,8 @@ so-telegraf:
|
|||||||
- /opt/so/log/suricata:/var/log/suricata:ro
|
- /opt/so/log/suricata:/var/log/suricata:ro
|
||||||
- /opt/so/log/raid:/var/log/raid:ro
|
- /opt/so/log/raid:/var/log/raid:ro
|
||||||
- /opt/so/log/sostatus:/var/log/sostatus:ro
|
- /opt/so/log/sostatus:/var/log/sostatus:ro
|
||||||
- /opt/so/log/salt:/var/log/salt:ro
|
- /opt/so/log/somon:/var/log/somon:ro
|
||||||
|
- /opt/so/log/salt/lasthighstate:/var/log/salt/lasthighstate:ro
|
||||||
- /opt/so/log/agents:/var/log/agents:ro
|
- /opt/so/log/agents:/var/log/agents:ro
|
||||||
{% if GLOBALS.is_manager or GLOBALS.role == 'so-heavynode' %}
|
{% if GLOBALS.is_manager or GLOBALS.role == 'so-heavynode' %}
|
||||||
- /opt/so/conf/telegraf/etc/escurl.config:/etc/telegraf/elasticsearch.config:ro
|
- /opt/so/conf/telegraf/etc/escurl.config:/etc/telegraf/elasticsearch.config:ro
|
||||||
@@ -74,6 +73,8 @@ so-telegraf:
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
- watch:
|
- watch:
|
||||||
|
- file: tgraf_sbin_jinja
|
||||||
|
- file: lasthighstate_placeholder
|
||||||
- file: trusttheca
|
- file: trusttheca
|
||||||
- x509: telegraf_crt
|
- x509: telegraf_crt
|
||||||
- x509: telegraf_key
|
- x509: telegraf_key
|
||||||
@@ -83,6 +84,8 @@ so-telegraf:
|
|||||||
- file: tgraf_sync_script_{{script}}
|
- file: tgraf_sync_script_{{script}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
- require:
|
- require:
|
||||||
|
- file: lasthighstate_placeholder
|
||||||
|
- file: somonlogdir
|
||||||
- file: trusttheca
|
- file: trusttheca
|
||||||
- x509: telegraf_crt
|
- x509: telegraf_crt
|
||||||
- x509: telegraf_key
|
- x509: telegraf_key
|
||||||
|
|||||||
@@ -228,13 +228,6 @@
|
|||||||
# ## bond interfaces.
|
# ## bond interfaces.
|
||||||
# # bond_interfaces = ["bond0"]
|
# # bond_interfaces = ["bond0"]
|
||||||
|
|
||||||
# # Read metrics about docker containers
|
|
||||||
[[inputs.docker]]
|
|
||||||
# ## Docker Endpoint
|
|
||||||
# ## To use TCP, set endpoint = "tcp://[ip]:[port]"
|
|
||||||
# ## To use environment variables (ie, docker-machine), set endpoint = "ENV"
|
|
||||||
endpoint = "unix:///var/run/docker.sock"
|
|
||||||
#
|
|
||||||
|
|
||||||
# # Read stats from one or more Elasticsearch servers or clusters
|
# # Read stats from one or more Elasticsearch servers or clusters
|
||||||
{%- if GLOBALS.is_manager or GLOBALS.role == 'so-heavynode' %}
|
{%- if GLOBALS.is_manager or GLOBALS.role == 'so-heavynode' %}
|
||||||
@@ -342,6 +335,17 @@
|
|||||||
interval = "60s"
|
interval = "60s"
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if 'containers.sh' in TELEGRAFMERGED.scripts[GLOBALS.role.split('-')[1]] %}
|
||||||
|
{%- do TELEGRAFMERGED.scripts[GLOBALS.role.split('-')[1]].remove('containers.sh') %}
|
||||||
|
[[inputs.exec]]
|
||||||
|
commands = [
|
||||||
|
["/scripts/containers.sh"]
|
||||||
|
]
|
||||||
|
data_format = "influx"
|
||||||
|
timeout = "15s"
|
||||||
|
interval = "60s"
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
{%- if TELEGRAFMERGED.scripts[GLOBALS.role.split('-')[1]] | length > 0 %}
|
{%- if TELEGRAFMERGED.scripts[GLOBALS.role.split('-')[1]] | length > 0 %}
|
||||||
[[inputs.exec]]
|
[[inputs.exec]]
|
||||||
commands = [
|
commands = [
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# if this script isn't already running
|
||||||
|
if [[ ! "`pidof -x $(basename $0) -o %PPID`" ]]; then
|
||||||
|
|
||||||
|
CONTAINERSLOG=/var/log/somon/containers.log
|
||||||
|
# the collector rewrites this every minute; report nothing rather than repeating a stale
|
||||||
|
# file as if it were current, in case a run was skipped or the collector is wedged
|
||||||
|
MAXAGE=150
|
||||||
|
|
||||||
|
if [ -r "$CONTAINERSLOG" ]; then
|
||||||
|
AGE=$(( $(date +%s) - $(stat -c %Y "$CONTAINERSLOG") ))
|
||||||
|
if [ "$AGE" -le "$MAXAGE" ]; then
|
||||||
|
cat $CONTAINERSLOG
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -8,10 +8,12 @@
|
|||||||
# if this script isn't already running
|
# if this script isn't already running
|
||||||
if [[ ! "`pidof -x $(basename $0) -o %PPID`" ]]; then
|
if [[ ! "`pidof -x $(basename $0) -o %PPID`" ]]; then
|
||||||
|
|
||||||
LAST_HIGHSTATE_END=$([ -e "/var/log/salt/lasthighstate" ] && date -r /var/log/salt/lasthighstate +%s || echo 0)
|
if [ -r "/var/log/salt/lasthighstate" ]; then
|
||||||
NOW=$(date +%s)
|
LAST_HIGHSTATE_END=$(date -r /var/log/salt/lasthighstate +%s)
|
||||||
HIGHSTATE_AGE_SECONDS=$((NOW-LAST_HIGHSTATE_END))
|
NOW=$(date +%s)
|
||||||
echo "salt highstate_age_seconds=$HIGHSTATE_AGE_SECONDS"
|
HIGHSTATE_AGE_SECONDS=$((NOW-LAST_HIGHSTATE_END))
|
||||||
|
echo "salt highstate_age_seconds=$HIGHSTATE_AGE_SECONDS"
|
||||||
|
fi
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,319 @@ telegraf:
|
|||||||
forcedType: bool
|
forcedType: bool
|
||||||
advanced: True
|
advanced: True
|
||||||
helpLink: influxdb
|
helpLink: influxdb
|
||||||
|
container_stats:
|
||||||
|
tags:
|
||||||
|
identity:
|
||||||
|
description: Adds the container_image, container_version, engine_host and server_version tags to every container measurement, as the retired inputs.docker plugin did. Increases series cardinality. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
engine:
|
||||||
|
n_containers:
|
||||||
|
description: Total number of containers known to the Docker engine, running or not. Part of the engine-level docker measurement. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_containers_running:
|
||||||
|
description: Number of containers currently running. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_containers_stopped:
|
||||||
|
description: Number of containers currently stopped. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_containers_paused:
|
||||||
|
description: Number of containers currently paused. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_images:
|
||||||
|
description: Number of container images held by the Docker engine. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_cpus:
|
||||||
|
description: Number of CPUs the Docker engine reports for this host. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_goroutines:
|
||||||
|
description: Number of goroutines inside the Docker daemon. Diagnostic detail for the daemon itself. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_used_file_descriptors:
|
||||||
|
description: Number of file descriptors held open by the Docker daemon. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
n_listener_events:
|
||||||
|
description: Number of event listeners subscribed to the Docker daemon. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
memory_total:
|
||||||
|
description: Total physical memory the Docker engine reports for this host, in bytes. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
cpu:
|
||||||
|
usage_percent:
|
||||||
|
description: Percentage of host CPU consumed by the container. Required by the Container CPU Usage chart on the Security Onion Performance dashboard.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
usage_total:
|
||||||
|
description: Cumulative CPU time consumed by the container, in nanoseconds. Read from the cgroup. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
usage_in_usermode:
|
||||||
|
description: Cumulative CPU time consumed in user mode, in nanoseconds. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
usage_in_kernelmode:
|
||||||
|
description: Cumulative CPU time consumed in kernel mode, in nanoseconds. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
usage_system:
|
||||||
|
description: Cumulative host-wide CPU time, in nanoseconds. Used as the denominator when calculating container CPU percentage. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
throttling_periods:
|
||||||
|
description: Number of CPU enforcement periods the container has seen. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
throttling_throttled_periods:
|
||||||
|
description: Number of periods in which the container was throttled against its CPU limit. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
throttling_throttled_time:
|
||||||
|
description: Total time the container spent throttled, in nanoseconds. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
container_id: &containerid
|
||||||
|
description: Full 64 character container ID, emitted as a field on this measurement. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
mem:
|
||||||
|
usage_percent:
|
||||||
|
description: Percentage of its memory limit the container is using. Required by the Container Memory Usage chart on the Security Onion Performance dashboard.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
usage:
|
||||||
|
description: Container memory usage in bytes, excluding reclaimable page cache. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
limit:
|
||||||
|
description: Memory limit for the container in bytes. Reports total host memory when the container is unlimited. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
max_usage:
|
||||||
|
description: Peak memory usage for the container in bytes, read from the cgroup peak counter. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
active_anon:
|
||||||
|
description: Anonymous memory on the active LRU list, in bytes. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
active_file:
|
||||||
|
description: Page cache on the active LRU list, in bytes. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
inactive_anon:
|
||||||
|
description: Anonymous memory on the inactive LRU list, in bytes. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
inactive_file:
|
||||||
|
description: Page cache on the inactive LRU list, in bytes. This is the reclaimable cache subtracted from usage. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
unevictable:
|
||||||
|
description: Memory that cannot be reclaimed, in bytes. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
pgfault:
|
||||||
|
description: Cumulative number of page faults taken by the container. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
pgmajfault:
|
||||||
|
description: Cumulative number of major page faults, those requiring disk access. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
container_id: *containerid
|
||||||
|
net:
|
||||||
|
rx_bytes:
|
||||||
|
description: Bytes received by the container across all interfaces except loopback. Required by the Container Traffic - Inbound chart on the Security Onion Performance dashboard.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
rx_packets:
|
||||||
|
description: Packets received by the container. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
rx_errors:
|
||||||
|
description: Receive errors counted on the container interfaces. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
rx_dropped:
|
||||||
|
description: Received packets dropped by the container interfaces. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
tx_bytes:
|
||||||
|
description: Bytes transmitted by the container. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
tx_packets:
|
||||||
|
description: Packets transmitted by the container. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
tx_errors:
|
||||||
|
description: Transmit errors counted on the container interfaces. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
tx_dropped:
|
||||||
|
description: Transmitted packets dropped by the container interfaces. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
container_id: *containerid
|
||||||
|
blkio:
|
||||||
|
io_service_bytes_recursive_read:
|
||||||
|
description: Cumulative bytes read from block devices by the container. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
io_service_bytes_recursive_write:
|
||||||
|
description: Cumulative bytes written to block devices by the container. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
container_id: *containerid
|
||||||
|
status:
|
||||||
|
uptime_ns:
|
||||||
|
description: How long the container has been running, in nanoseconds. Required by the Container Uptime chart on the Security Onion Performance dashboard.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
oomkilled:
|
||||||
|
description: Whether the container was killed by the kernel out-of-memory handler. Required by the Most Recent Container Events table on the Security Onion Performance dashboard.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
pid:
|
||||||
|
description: Host process ID of the container main process. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
exitcode:
|
||||||
|
description: Exit code of the container main process. Meaningful once the container has stopped. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
restart_count:
|
||||||
|
description: Number of times the Docker engine has restarted this container. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
started_at:
|
||||||
|
description: Time the container last started, as a Unix timestamp in nanoseconds. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
finished_at:
|
||||||
|
description: Time the container last exited, as a Unix timestamp in nanoseconds. Absent for a container that has never exited. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
container_id: *containerid
|
||||||
|
health:
|
||||||
|
health_status:
|
||||||
|
description: Result of the container healthcheck, such as healthy, unhealthy or starting. Only emitted for containers that define a healthcheck. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
|
failing_streak:
|
||||||
|
description: Number of consecutive failed healthchecks. Only emitted for containers that define a healthcheck. Defaults to off.
|
||||||
|
forcedType: bool
|
||||||
|
global: True
|
||||||
|
advanced: True
|
||||||
|
helpLink: influxdb
|
||||||
scripts:
|
scripts:
|
||||||
eval: &telegrafscripts
|
eval: &telegrafscripts
|
||||||
description: List of input.exec scripts to run for this node type. The script must be present in salt/telegraf/scripts.
|
description: List of input.exec scripts to run for this node type. The script must be present in salt/telegraf/scripts.
|
||||||
|
|||||||
@@ -0,0 +1,398 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# Runs from cron as somon, a docker group member, and emits influx line protocol for telegraf to
|
||||||
|
# read. This exists so so-telegraf does not need the docker socket; the container reads the output
|
||||||
|
# file instead.
|
||||||
|
# Measurement/tag/field names match telegraf's inputs.docker plugin because the InfluxDB
|
||||||
|
# dashboards query them directly. Which fields are emitted is set per stat in SOC; see
|
||||||
|
# telegraf.container_stats in defaults.yaml.
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
SETTINGS = json.loads('''{{ CONTAINER_STATS | tojson }}''')
|
||||||
|
{% raw %}
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
CGROUP_ROOT = '/sys/fs/cgroup'
|
||||||
|
NANOSEC = 10**9
|
||||||
|
# cgroup and docker report cpu time in microseconds; inputs.docker published nanoseconds
|
||||||
|
USEC_TO_NSEC = 1000
|
||||||
|
|
||||||
|
|
||||||
|
def want(group, field):
|
||||||
|
return bool(SETTINGS.get(group, {}).get(field))
|
||||||
|
|
||||||
|
|
||||||
|
def wants_any(group, fields):
|
||||||
|
return any(want(group, field) for field in fields)
|
||||||
|
|
||||||
|
|
||||||
|
def docker(args):
|
||||||
|
proc = subprocess.run(['docker'] + args, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, encoding='utf-8')
|
||||||
|
if proc.returncode != 0:
|
||||||
|
print('Container system error; unable to query docker', file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return proc.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def escape_tag(value):
|
||||||
|
return value.replace(',', '\\,').replace(' ', '\\ ').replace('=', '\\=')
|
||||||
|
|
||||||
|
|
||||||
|
def quote(value):
|
||||||
|
return '"{}"'.format(str(value).replace('\\', '\\\\').replace('"', '\\"'))
|
||||||
|
|
||||||
|
|
||||||
|
def integer(value):
|
||||||
|
return '{}i'.format(int(value))
|
||||||
|
|
||||||
|
|
||||||
|
def unsigned(value):
|
||||||
|
# inputs.docker published the cgroup and network counters as uint64; influx stores u and i
|
||||||
|
# as different field types, so matching it keeps historical series readable
|
||||||
|
return '{}u'.format(max(int(value), 0))
|
||||||
|
|
||||||
|
|
||||||
|
def read_text(path):
|
||||||
|
try:
|
||||||
|
with open(path) as handle:
|
||||||
|
return handle.read()
|
||||||
|
except OSError:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def read_pairs(path):
|
||||||
|
# cgroup files such as memory.stat and cpu.stat are "key value" per line
|
||||||
|
values = {}
|
||||||
|
for line in read_text(path).splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) == 2:
|
||||||
|
try:
|
||||||
|
values[parts[0]] = int(parts[1])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def read_value(path):
|
||||||
|
raw = read_text(path).strip()
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def cgroup_path(pid):
|
||||||
|
# "0::/system.slice/docker-<id>.scope" on cgroup v2
|
||||||
|
for line in read_text('/proc/{}/cgroup'.format(pid)).splitlines():
|
||||||
|
parts = line.split(':', 2)
|
||||||
|
if len(parts) == 3 and parts[1] == '':
|
||||||
|
return CGROUP_ROOT + parts[2]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def host_memory_total():
|
||||||
|
match = re.search(r'MemTotal:\s+(\d+) kB', read_text('/proc/meminfo'))
|
||||||
|
return int(match.group(1)) * 1024 if match else 0
|
||||||
|
|
||||||
|
|
||||||
|
def host_cpu_nanoseconds():
|
||||||
|
# inputs.docker's usage_system is the host-wide cpu time the daemon reads from /proc/stat
|
||||||
|
for line in read_text('/proc/stat').splitlines():
|
||||||
|
if line.startswith('cpu '):
|
||||||
|
ticks = sum(int(value) for value in line.split()[1:])
|
||||||
|
return int(ticks * NANOSEC / os.sysconf('SC_CLK_TCK'))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def parse_image(image):
|
||||||
|
# mirrors telegraf's internal/docker ParseImage so the tags match what inputs.docker emitted
|
||||||
|
domain = ''
|
||||||
|
remainder = image
|
||||||
|
if '/' in image:
|
||||||
|
head, _, tail = image.partition('/')
|
||||||
|
if '.' in head or ':' in head or head == 'localhost':
|
||||||
|
domain, remainder = head + '/', tail
|
||||||
|
if ':' in remainder:
|
||||||
|
name, _, version = remainder.rpartition(':')
|
||||||
|
return domain + name, version
|
||||||
|
return domain + remainder, 'unknown'
|
||||||
|
|
||||||
|
|
||||||
|
def to_nanoseconds(stamp):
|
||||||
|
# docker emits 9 fractional digits; fromisoformat takes at most 6 before python 3.11
|
||||||
|
stamp = stamp.rstrip('Z')
|
||||||
|
if '.' in stamp:
|
||||||
|
whole, _, frac = stamp.partition('.')
|
||||||
|
stamp = whole + '.' + frac[:6]
|
||||||
|
try:
|
||||||
|
parsed = datetime.fromisoformat(stamp).replace(tzinfo=timezone.utc)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if parsed.year <= 1:
|
||||||
|
return None
|
||||||
|
return int(parsed.timestamp() * NANOSEC)
|
||||||
|
|
||||||
|
|
||||||
|
def percent(value):
|
||||||
|
try:
|
||||||
|
return float(value.strip().rstrip('%'))
|
||||||
|
except ValueError:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def net_counters(pid):
|
||||||
|
# summed across interfaces except loopback, matching the daemon's per-container totals
|
||||||
|
rows = read_text('/proc/{}/net/dev'.format(pid)).splitlines()[2:]
|
||||||
|
if not rows:
|
||||||
|
return None
|
||||||
|
names = ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped']
|
||||||
|
totals = dict.fromkeys(names + ['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped'], 0)
|
||||||
|
found = False
|
||||||
|
for row in rows:
|
||||||
|
iface, _, rest = row.partition(':')
|
||||||
|
if iface.strip() == 'lo':
|
||||||
|
continue
|
||||||
|
columns = rest.split()
|
||||||
|
if len(columns) < 12:
|
||||||
|
continue
|
||||||
|
found = True
|
||||||
|
for index, name in enumerate(names):
|
||||||
|
totals[name] += int(columns[index])
|
||||||
|
for index, name in enumerate(['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped']):
|
||||||
|
totals[name] += int(columns[8 + index])
|
||||||
|
return totals if found else None
|
||||||
|
|
||||||
|
|
||||||
|
def blkio_counters(path):
|
||||||
|
# inputs.docker emitted the device=total row unconditionally, so a container that has done
|
||||||
|
# no block io reports zeros rather than dropping out of the measurement entirely
|
||||||
|
totals = {'io_service_bytes_recursive_read': 0, 'io_service_bytes_recursive_write': 0}
|
||||||
|
rows = read_text(path + '/io.stat').splitlines()
|
||||||
|
for row in rows:
|
||||||
|
for token in row.split()[1:]:
|
||||||
|
key, _, value = token.partition('=')
|
||||||
|
try:
|
||||||
|
if key == 'rbytes':
|
||||||
|
totals['io_service_bytes_recursive_read'] += int(value)
|
||||||
|
elif key == 'wbytes':
|
||||||
|
totals['io_service_bytes_recursive_write'] += int(value)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return totals
|
||||||
|
|
||||||
|
|
||||||
|
def collect_stats():
|
||||||
|
stats = {}
|
||||||
|
for line in docker(['stats', '--no-stream', '--format', '{{json .}}']).splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entry = json.loads(line)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue
|
||||||
|
stats[entry.get('Name', '')] = entry
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def collect_inspect():
|
||||||
|
ids = docker(['ps', '-aq']).split()
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
return json.loads(docker(['inspect'] + ids))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def collect_info():
|
||||||
|
try:
|
||||||
|
return json.loads(docker(['info', '--format', '{{json .}}']))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class Emitter:
|
||||||
|
def __init__(self):
|
||||||
|
self.lines = []
|
||||||
|
|
||||||
|
def add(self, measurement, tags, fields):
|
||||||
|
if not fields:
|
||||||
|
return
|
||||||
|
tagset = ','.join('{}={}'.format(key, escape_tag(value)) for key, value in tags)
|
||||||
|
body = ','.join('{}={}'.format(key, fields[key]) for key in sorted(fields))
|
||||||
|
self.lines.append('{},{} {}'.format(measurement, tagset, body))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cpu_extra = ['usage_total', 'usage_in_usermode', 'usage_in_kernelmode',
|
||||||
|
'throttling_periods', 'throttling_throttled_periods', 'throttling_throttled_time']
|
||||||
|
mem_extra = ['usage', 'limit', 'max_usage', 'active_anon', 'active_file', 'inactive_anon',
|
||||||
|
'inactive_file', 'unevictable', 'pgfault', 'pgmajfault']
|
||||||
|
net_fields = ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped',
|
||||||
|
'tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped']
|
||||||
|
engine_fields = ['n_containers', 'n_containers_running', 'n_containers_stopped',
|
||||||
|
'n_containers_paused', 'n_images', 'n_cpus', 'n_goroutines',
|
||||||
|
'n_used_file_descriptors', 'n_listener_events']
|
||||||
|
|
||||||
|
identity = want('tags', 'identity')
|
||||||
|
need_stats = want('cpu', 'usage_percent')
|
||||||
|
need_cgroup_cpu = wants_any('cpu', cpu_extra)
|
||||||
|
need_cgroup_mem = wants_any('mem', mem_extra + ['usage_percent'])
|
||||||
|
need_blkio = wants_any('blkio', ['io_service_bytes_recursive_read',
|
||||||
|
'io_service_bytes_recursive_write', 'container_id'])
|
||||||
|
need_net = wants_any('net', net_fields + ['container_id'])
|
||||||
|
need_info = identity or wants_any('engine', engine_fields + ['memory_total'])
|
||||||
|
|
||||||
|
emitter = Emitter()
|
||||||
|
stats = collect_stats() if need_stats else {}
|
||||||
|
info = collect_info() if need_info else {}
|
||||||
|
engine_tags = [('engine_host', info.get('Name', '')),
|
||||||
|
('server_version', info.get('ServerVersion', ''))]
|
||||||
|
system_ns = host_cpu_nanoseconds() if want('cpu', 'usage_system') else 0
|
||||||
|
mem_total = host_memory_total() if wants_any('mem', ['limit', 'usage_percent']) else 0
|
||||||
|
|
||||||
|
if info:
|
||||||
|
counts = {'n_containers': 'Containers', 'n_containers_running': 'ContainersRunning',
|
||||||
|
'n_containers_stopped': 'ContainersStopped', 'n_containers_paused': 'ContainersPaused',
|
||||||
|
'n_images': 'Images', 'n_cpus': 'NCPU', 'n_goroutines': 'NGoroutines',
|
||||||
|
'n_used_file_descriptors': 'NFd', 'n_listener_events': 'NEventsListener'}
|
||||||
|
fields = {name: integer(info[key]) for name, key in counts.items()
|
||||||
|
if want('engine', name) and key in info}
|
||||||
|
emitter.add('docker', engine_tags, fields)
|
||||||
|
# inputs.docker published memory_total as its own point
|
||||||
|
if want('engine', 'memory_total') and 'MemTotal' in info:
|
||||||
|
emitter.add('docker', engine_tags, {'memory_total': integer(info['MemTotal'])})
|
||||||
|
|
||||||
|
for container in collect_inspect():
|
||||||
|
name = container.get('Name', '').lstrip('/')
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
state = container.get('State', {})
|
||||||
|
status = state.get('Status', 'unknown')
|
||||||
|
pid = state.get('Pid')
|
||||||
|
container_id = container.get('Id', '')
|
||||||
|
tags = [('container_name', name), ('container_status', status)]
|
||||||
|
if identity:
|
||||||
|
image, version = parse_image(container.get('Config', {}).get('Image', ''))
|
||||||
|
tags += [('container_image', image), ('container_version', version)] + engine_tags
|
||||||
|
|
||||||
|
status_fields = {}
|
||||||
|
if want('status', 'uptime_ns') or want('status', 'started_at') or want('status', 'finished_at'):
|
||||||
|
started = to_nanoseconds(state.get('StartedAt', ''))
|
||||||
|
finished = to_nanoseconds(state.get('FinishedAt', ''))
|
||||||
|
if started is not None:
|
||||||
|
if want('status', 'started_at'):
|
||||||
|
status_fields['started_at'] = integer(started)
|
||||||
|
if want('status', 'uptime_ns'):
|
||||||
|
end = finished if finished is not None and finished >= started else int(
|
||||||
|
datetime.now(timezone.utc).timestamp() * NANOSEC)
|
||||||
|
status_fields['uptime_ns'] = integer(end - started)
|
||||||
|
if finished is not None and want('status', 'finished_at'):
|
||||||
|
status_fields['finished_at'] = integer(finished)
|
||||||
|
if want('status', 'oomkilled'):
|
||||||
|
status_fields['oomkilled'] = 'true' if state.get('OOMKilled') else 'false'
|
||||||
|
if want('status', 'pid'):
|
||||||
|
status_fields['pid'] = integer(pid or 0)
|
||||||
|
if want('status', 'exitcode'):
|
||||||
|
status_fields['exitcode'] = integer(state.get('ExitCode', 0))
|
||||||
|
if want('status', 'restart_count'):
|
||||||
|
status_fields['restart_count'] = integer(container.get('RestartCount', 0))
|
||||||
|
if want('status', 'container_id'):
|
||||||
|
status_fields['container_id'] = quote(container_id)
|
||||||
|
emitter.add('docker_container_status', tags, status_fields)
|
||||||
|
|
||||||
|
health = state.get('Health')
|
||||||
|
if health:
|
||||||
|
health_fields = {}
|
||||||
|
if want('health', 'health_status'):
|
||||||
|
health_fields['health_status'] = quote(health.get('Status', ''))
|
||||||
|
if want('health', 'failing_streak'):
|
||||||
|
health_fields['failing_streak'] = integer(health.get('FailingStreak', 0))
|
||||||
|
emitter.add('docker_container_health', tags, health_fields)
|
||||||
|
|
||||||
|
if status != 'running' or not pid:
|
||||||
|
continue
|
||||||
|
path = cgroup_path(pid)
|
||||||
|
entry = stats.get(name, {})
|
||||||
|
|
||||||
|
cpu_fields = {}
|
||||||
|
if want('cpu', 'usage_percent'):
|
||||||
|
cpu_fields['usage_percent'] = percent(entry.get('CPUPerc', '0%'))
|
||||||
|
if want('cpu', 'usage_system'):
|
||||||
|
cpu_fields['usage_system'] = unsigned(system_ns)
|
||||||
|
if want('cpu', 'container_id'):
|
||||||
|
cpu_fields['container_id'] = quote(container_id)
|
||||||
|
if need_cgroup_cpu and path:
|
||||||
|
cpu = read_pairs(path + '/cpu.stat')
|
||||||
|
mapping = {'usage_total': 'usage_usec', 'usage_in_usermode': 'user_usec',
|
||||||
|
'usage_in_kernelmode': 'system_usec',
|
||||||
|
'throttling_periods': 'nr_periods',
|
||||||
|
'throttling_throttled_periods': 'nr_throttled',
|
||||||
|
'throttling_throttled_time': 'throttled_usec'}
|
||||||
|
for field, key in mapping.items():
|
||||||
|
if want('cpu', field) and key in cpu:
|
||||||
|
scale = 1 if field.startswith('throttling_') and field != 'throttling_throttled_time' else USEC_TO_NSEC
|
||||||
|
cpu_fields[field] = unsigned(cpu[key] * scale)
|
||||||
|
emitter.add('docker_container_cpu', tags + [('cpu', 'cpu-total')], cpu_fields)
|
||||||
|
|
||||||
|
mem_fields = {}
|
||||||
|
if want('mem', 'container_id'):
|
||||||
|
mem_fields['container_id'] = quote(container_id)
|
||||||
|
if need_cgroup_mem and path:
|
||||||
|
memory = read_pairs(path + '/memory.stat')
|
||||||
|
for field in ['active_anon', 'active_file', 'inactive_anon', 'inactive_file',
|
||||||
|
'unevictable', 'pgfault', 'pgmajfault']:
|
||||||
|
if want('mem', field) and field in memory:
|
||||||
|
mem_fields[field] = unsigned(memory[field])
|
||||||
|
current = read_value(path + '/memory.current')
|
||||||
|
# inputs.docker reports usage net of reclaimable page cache
|
||||||
|
usage = max(current - memory.get('inactive_file', 0), 0) if current is not None else None
|
||||||
|
raw_limit = read_value(path + '/memory.max')
|
||||||
|
limit = raw_limit if raw_limit is not None else mem_total
|
||||||
|
if want('mem', 'usage') and usage is not None:
|
||||||
|
mem_fields['usage'] = unsigned(usage)
|
||||||
|
if want('mem', 'limit'):
|
||||||
|
mem_fields['limit'] = unsigned(limit)
|
||||||
|
if want('mem', 'usage_percent') and usage is not None:
|
||||||
|
# same ratio inputs.docker computes, from the same two values
|
||||||
|
mem_fields['usage_percent'] = usage / limit * 100.0 if limit else 0.0
|
||||||
|
if want('mem', 'max_usage'):
|
||||||
|
peak = read_value(path + '/memory.peak')
|
||||||
|
if peak is not None:
|
||||||
|
mem_fields['max_usage'] = unsigned(peak)
|
||||||
|
emitter.add('docker_container_mem', tags, mem_fields)
|
||||||
|
|
||||||
|
# inputs.docker emitted nothing for host-network containers; its Networks map was empty
|
||||||
|
if need_net and container.get('HostConfig', {}).get('NetworkMode', '') != 'host':
|
||||||
|
counters = net_counters(pid)
|
||||||
|
if counters is not None:
|
||||||
|
net_out = {field: unsigned(counters[field]) for field in net_fields if want('net', field)}
|
||||||
|
if want('net', 'container_id'):
|
||||||
|
net_out['container_id'] = quote(container_id)
|
||||||
|
emitter.add('docker_container_net', tags + [('network', 'total')], net_out)
|
||||||
|
|
||||||
|
if need_blkio and path:
|
||||||
|
counters = blkio_counters(path)
|
||||||
|
if counters is not None:
|
||||||
|
blkio_out = {field: unsigned(value) for field, value in counters.items() if want('blkio', field)}
|
||||||
|
if want('blkio', 'container_id'):
|
||||||
|
blkio_out['container_id'] = quote(container_id)
|
||||||
|
emitter.add('docker_container_blkio', tags + [('device', 'total')], blkio_out)
|
||||||
|
|
||||||
|
print('\n'.join(emitter.lines))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
{% endraw %}
|
||||||
@@ -0,0 +1,635 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# so-container-stats replaces telegraf's inputs.docker plugin, which was dropped along with the
|
||||||
|
# docker socket. The InfluxDB dashboards query its output by measurement, tag and field name, so
|
||||||
|
# these tests pin that contract: the shipped defaults, the per-stat toggles, the field types, and
|
||||||
|
# the value semantics copied from the plugin.
|
||||||
|
#
|
||||||
|
# The collector is a jinja template, so every test renders it the way salt does and imports the
|
||||||
|
# result. Docker and the cgroup filesystem are faked, so nothing here needs a container runtime.
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
TEMPLATE = os.path.join(HERE, 'so-container-stats')
|
||||||
|
DEFAULTS = os.path.join(HERE, '..', '..', 'defaults.yaml')
|
||||||
|
|
||||||
|
# a container id is a 64 character hex string; the plugin published it in full
|
||||||
|
SOC_ID = 'a' * 64
|
||||||
|
TELEGRAF_ID = 'b' * 64
|
||||||
|
NGINX_ID = 'c' * 64
|
||||||
|
IDSTOOLS_ID = 'd' * 64
|
||||||
|
|
||||||
|
|
||||||
|
def shipped_defaults():
|
||||||
|
with open(DEFAULTS) as handle:
|
||||||
|
return yaml.safe_load(handle)['telegraf']['container_stats']
|
||||||
|
|
||||||
|
|
||||||
|
def all_enabled():
|
||||||
|
return {group: {field: True for field in fields} for group, fields in shipped_defaults().items()}
|
||||||
|
|
||||||
|
|
||||||
|
def render(settings):
|
||||||
|
"""Render the template as salt does, import it, and hand back the module."""
|
||||||
|
with open(TEMPLATE) as handle:
|
||||||
|
source = handle.read()
|
||||||
|
rendered = jinja2.Template(source, keep_trailing_newline=True).render(CONTAINER_STATS=settings)
|
||||||
|
path = os.path.join(tempfile.mkdtemp(), 'so_container_stats.py')
|
||||||
|
with open(path, 'w') as handle:
|
||||||
|
handle.write(rendered)
|
||||||
|
spec = importlib.util.spec_from_file_location('so_container_stats', path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def split_escaped(text, sep):
|
||||||
|
"""Split on an unescaped, unquoted separator, the way influx line protocol is written."""
|
||||||
|
parts, current, escaped, quoted = [], '', False, False
|
||||||
|
for char in text:
|
||||||
|
if escaped:
|
||||||
|
current += char
|
||||||
|
escaped = False
|
||||||
|
elif char == '\\':
|
||||||
|
current += char
|
||||||
|
escaped = True
|
||||||
|
elif char == '"':
|
||||||
|
quoted = not quoted
|
||||||
|
current += char
|
||||||
|
elif char == sep and not quoted:
|
||||||
|
parts.append(current)
|
||||||
|
current = ''
|
||||||
|
else:
|
||||||
|
current += char
|
||||||
|
parts.append(current)
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def split_on_space(line):
|
||||||
|
escaped = quoted = False
|
||||||
|
for index, char in enumerate(line):
|
||||||
|
if escaped:
|
||||||
|
escaped = False
|
||||||
|
elif char == '\\':
|
||||||
|
escaped = True
|
||||||
|
elif char == '"':
|
||||||
|
quoted = not quoted
|
||||||
|
elif char == ' ' and not quoted:
|
||||||
|
return line[:index], line[index + 1:]
|
||||||
|
return line, ''
|
||||||
|
|
||||||
|
|
||||||
|
def parse(output):
|
||||||
|
"""Parse line protocol into {(measurement, container): (tags, fields)}."""
|
||||||
|
points = {}
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
head, fieldpart = split_on_space(line)
|
||||||
|
pieces = split_escaped(head, ',')
|
||||||
|
measurement, tags = pieces[0], {}
|
||||||
|
for piece in pieces[1:]:
|
||||||
|
key, _, value = piece.partition('=')
|
||||||
|
tags[key] = value
|
||||||
|
fields = {}
|
||||||
|
for piece in split_escaped(fieldpart, ','):
|
||||||
|
key, _, value = piece.partition('=')
|
||||||
|
fields[key] = value
|
||||||
|
key = (measurement, tags.get('container_name', ''))
|
||||||
|
if key in points:
|
||||||
|
# the engine measurement is published as two points, the way inputs.docker did
|
||||||
|
points[key][1].update(fields)
|
||||||
|
else:
|
||||||
|
points[key] = (tags, fields)
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def field_type(value):
|
||||||
|
if value.endswith('u'):
|
||||||
|
return 'unsigned'
|
||||||
|
if value.endswith('i'):
|
||||||
|
return 'integer'
|
||||||
|
if value.startswith('"'):
|
||||||
|
return 'string'
|
||||||
|
if value in ('true', 'false'):
|
||||||
|
return 'boolean'
|
||||||
|
return 'float'
|
||||||
|
|
||||||
|
|
||||||
|
def container(name, cid, pid, status='running', network='bridge', image='registry:5000/repo/img:3.4.0',
|
||||||
|
started='2026-09-24T10:00:00.123456789Z', finished='0001-01-01T00:00:00Z', health=None,
|
||||||
|
oomkilled=False, exitcode=0, restarts=0):
|
||||||
|
state = {'Status': status, 'Pid': pid, 'StartedAt': started, 'FinishedAt': finished,
|
||||||
|
'OOMKilled': oomkilled, 'ExitCode': exitcode}
|
||||||
|
if health is not None:
|
||||||
|
state['Health'] = health
|
||||||
|
return {'Id': cid, 'Name': '/' + name, 'State': state, 'RestartCount': restarts,
|
||||||
|
'Config': {'Image': image}, 'HostConfig': {'NetworkMode': network}}
|
||||||
|
|
||||||
|
|
||||||
|
class CollectorTestCase(unittest.TestCase):
|
||||||
|
"""Builds a fake docker engine and cgroup tree, then runs the collector against it."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.inspect = [
|
||||||
|
container('so-soc', SOC_ID, 1001),
|
||||||
|
container('so-telegraf', TELEGRAF_ID, 1002, network='host'),
|
||||||
|
container('so-nginx', NGINX_ID, 1003, health={'Status': 'healthy', 'FailingStreak': 0}),
|
||||||
|
]
|
||||||
|
self.stats = {
|
||||||
|
'so-soc': {'Name': 'so-soc', 'CPUPerc': '1.25%', 'MemPerc': '6.39%', 'NetIO': '1kB / 2kB'},
|
||||||
|
'so-telegraf': {'Name': 'so-telegraf', 'CPUPerc': '0.04%', 'MemPerc': '0.58%', 'NetIO': '0B / 0B'},
|
||||||
|
'so-nginx': {'Name': 'so-nginx', 'CPUPerc': '0.00%', 'MemPerc': '0.09%', 'NetIO': '3kB / 4kB'},
|
||||||
|
}
|
||||||
|
self.info = {'Name': 'sohost', 'ServerVersion': '29.2.1', 'Containers': 3, 'ContainersRunning': 3,
|
||||||
|
'ContainersStopped': 0, 'ContainersPaused': 0, 'Images': 9, 'NCPU': 8,
|
||||||
|
'NGoroutines': 42, 'NFd': 77, 'NEventsListener': 1, 'MemTotal': 16000000000}
|
||||||
|
# one cgroup per container, addressed through /proc/<pid>/cgroup exactly as the collector does
|
||||||
|
self.files = {
|
||||||
|
'/proc/meminfo': 'MemTotal: 15625000 kB\n',
|
||||||
|
'/proc/stat': 'cpu 100 200 300 400\ncpu0 1 2 3 4\n',
|
||||||
|
}
|
||||||
|
for pid in (1001, 1002, 1003):
|
||||||
|
self.files['/proc/%d/cgroup' % pid] = '0::/scope%d\n' % pid
|
||||||
|
self.cgroup(pid, 'cpu.stat',
|
||||||
|
'usage_usec 1000\nuser_usec 600\nsystem_usec 400\nnr_periods 5\nnr_throttled 2\nthrottled_usec 700\n')
|
||||||
|
self.cgroup(pid, 'memory.stat',
|
||||||
|
'active_anon 300\nactive_file 40\ninactive_anon 200\ninactive_file 50\nunevictable 0\npgfault 1234\npgmajfault 56\n')
|
||||||
|
self.cgroup(pid, 'memory.current', '1000\n')
|
||||||
|
self.cgroup(pid, 'memory.max', '4000\n')
|
||||||
|
self.cgroup(pid, 'memory.peak', '2500\n')
|
||||||
|
self.cgroup(pid, 'io.stat', '8:0 rbytes=100 wbytes=200 rios=1 wios=2\n252:0 rbytes=10 wbytes=20 rios=1 wios=1\n')
|
||||||
|
self.files['/proc/%d/net/dev' % pid] = (
|
||||||
|
'Inter-| Receive | Transmit\n'
|
||||||
|
' face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed\n'
|
||||||
|
' lo: 9999 99 9 9 0 0 0 0 9999 99 9 9 0 0 0 0\n'
|
||||||
|
' eth0: 1000 10 1 2 0 0 0 0 2000 20 3 4 0 0 0 0\n'
|
||||||
|
' eth1: 500 5 0 0 0 0 0 0 1000 10 0 0 0 0 0 0\n')
|
||||||
|
|
||||||
|
def cgroup(self, pid, name, contents):
|
||||||
|
self.files['/sys/fs/cgroup/scope%d/%s' % (pid, name)] = contents
|
||||||
|
|
||||||
|
def run_collector(self, settings=None, module=None):
|
||||||
|
module = module or render(settings if settings is not None else all_enabled())
|
||||||
|
|
||||||
|
def fake_docker(args):
|
||||||
|
if args[0] == 'stats':
|
||||||
|
return ''.join(json.dumps(entry) + '\n' for entry in self.stats.values())
|
||||||
|
if args[0] == 'ps':
|
||||||
|
return ' '.join(entry['Id'] for entry in self.inspect)
|
||||||
|
if args[0] == 'inspect':
|
||||||
|
return json.dumps(self.inspect)
|
||||||
|
if args[0] == 'info':
|
||||||
|
return json.dumps(self.info)
|
||||||
|
raise AssertionError('unexpected docker call: %s' % args)
|
||||||
|
|
||||||
|
module.docker = fake_docker
|
||||||
|
module.read_text = lambda path: self.files.get(path, '')
|
||||||
|
module.CGROUP_ROOT = '/sys/fs/cgroup'
|
||||||
|
buffer = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(buffer):
|
||||||
|
module.main()
|
||||||
|
self.output = buffer.getvalue()
|
||||||
|
return parse(self.output)
|
||||||
|
|
||||||
|
|
||||||
|
class TestShippedDefaults(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_defaults_emit_only_the_dashboard_fields(self):
|
||||||
|
# the Security Onion Performance dashboard queries exactly these five
|
||||||
|
points = self.run_collector(shipped_defaults())
|
||||||
|
emitted = {(measurement, field) for (measurement, _), (_, fields) in points.items() for field in fields}
|
||||||
|
self.assertEqual(emitted, {
|
||||||
|
('docker_container_cpu', 'usage_percent'),
|
||||||
|
('docker_container_mem', 'usage_percent'),
|
||||||
|
('docker_container_net', 'rx_bytes'),
|
||||||
|
('docker_container_status', 'uptime_ns'),
|
||||||
|
('docker_container_status', 'oomkilled'),
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_defaults_do_not_emit_the_opt_in_measurements(self):
|
||||||
|
points = self.run_collector(shipped_defaults())
|
||||||
|
measurements = {measurement for measurement, _ in points}
|
||||||
|
self.assertNotIn('docker', measurements)
|
||||||
|
self.assertNotIn('docker_container_blkio', measurements)
|
||||||
|
self.assertNotIn('docker_container_health', measurements)
|
||||||
|
|
||||||
|
def test_defaults_carry_the_tags_the_dashboard_filters_on(self):
|
||||||
|
points = self.run_collector(shipped_defaults())
|
||||||
|
tags, _ = points[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(tags['container_status'], 'running')
|
||||||
|
self.assertEqual(tags['cpu'], 'cpu-total')
|
||||||
|
# identity tags are opt in, so they must be absent by default
|
||||||
|
self.assertNotIn('container_image', tags)
|
||||||
|
self.assertNotIn('engine_host', tags)
|
||||||
|
|
||||||
|
|
||||||
|
class TestToggles(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_enabling_one_stat_adds_only_that_field(self):
|
||||||
|
settings = shipped_defaults()
|
||||||
|
settings['cpu']['usage_total'] = True
|
||||||
|
_, fields = self.run_collector(settings)[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertIn('usage_total', fields)
|
||||||
|
self.assertNotIn('usage_in_usermode', fields)
|
||||||
|
|
||||||
|
def test_disabling_one_stat_leaves_its_neighbours(self):
|
||||||
|
settings = all_enabled()
|
||||||
|
settings['blkio']['io_service_bytes_recursive_read'] = False
|
||||||
|
_, fields = self.run_collector(settings)[('docker_container_blkio', 'so-soc')]
|
||||||
|
self.assertNotIn('io_service_bytes_recursive_read', fields)
|
||||||
|
self.assertIn('io_service_bytes_recursive_write', fields)
|
||||||
|
|
||||||
|
def test_identity_tags_are_added_when_enabled(self):
|
||||||
|
tags, _ = self.run_collector(all_enabled())[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(tags['container_image'], 'registry:5000/repo/img')
|
||||||
|
self.assertEqual(tags['container_version'], '3.4.0')
|
||||||
|
self.assertEqual(tags['engine_host'], 'sohost')
|
||||||
|
self.assertEqual(tags['server_version'], '29.2.1')
|
||||||
|
|
||||||
|
def test_everything_off_emits_nothing(self):
|
||||||
|
settings = {group: {field: False for field in fields} for group, fields in shipped_defaults().items()}
|
||||||
|
self.assertEqual(self.run_collector(settings), {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestFieldTypes(CollectorTestCase):
|
||||||
|
"""inputs.docker wrote the cgroup and network counters as unsigned; influx treats u and i as
|
||||||
|
different field types, so a mismatch breaks queries spanning the change."""
|
||||||
|
|
||||||
|
def test_counter_fields_are_unsigned(self):
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
for measurement, field in (('docker_container_cpu', 'usage_total'),
|
||||||
|
('docker_container_cpu', 'throttling_periods'),
|
||||||
|
('docker_container_mem', 'usage'),
|
||||||
|
('docker_container_mem', 'limit'),
|
||||||
|
('docker_container_mem', 'pgfault'),
|
||||||
|
('docker_container_net', 'rx_bytes'),
|
||||||
|
('docker_container_blkio', 'io_service_bytes_recursive_read')):
|
||||||
|
_, fields = points[(measurement, 'so-soc')]
|
||||||
|
self.assertEqual(field_type(fields[field]), 'unsigned', '%s.%s' % (measurement, field))
|
||||||
|
|
||||||
|
def test_status_and_engine_fields_are_signed(self):
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
_, status = points[('docker_container_status', 'so-soc')]
|
||||||
|
for field in ('uptime_ns', 'pid', 'exitcode', 'restart_count', 'started_at'):
|
||||||
|
self.assertEqual(field_type(status[field]), 'integer', field)
|
||||||
|
_, engine = points[('docker', '')]
|
||||||
|
self.assertEqual(field_type(engine['n_containers']), 'integer')
|
||||||
|
|
||||||
|
def test_percentages_are_floats_and_ids_are_quoted_strings(self):
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
_, cpu = points[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(field_type(cpu['usage_percent']), 'float')
|
||||||
|
self.assertEqual(field_type(cpu['container_id']), 'string')
|
||||||
|
self.assertEqual(cpu['container_id'], '"%s"' % SOC_ID)
|
||||||
|
_, status = points[('docker_container_status', 'so-soc')]
|
||||||
|
self.assertEqual(field_type(status['oomkilled']), 'boolean')
|
||||||
|
_, health = points[('docker_container_health', 'so-nginx')]
|
||||||
|
self.assertEqual(health['health_status'], '"healthy"')
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemorySemantics(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_usage_subtracts_reclaimable_page_cache(self):
|
||||||
|
# inputs.docker reports usage net of inactive_file: 1000 - 50
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_mem', 'so-soc')]
|
||||||
|
self.assertEqual(fields['usage'], '950u')
|
||||||
|
|
||||||
|
def test_usage_percent_is_usage_over_limit(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_mem', 'so-soc')]
|
||||||
|
self.assertAlmostEqual(float(fields['usage_percent']), 950 / 4000 * 100.0)
|
||||||
|
|
||||||
|
def test_limit_falls_back_to_host_memory_when_unlimited(self):
|
||||||
|
for pid in (1001, 1002, 1003):
|
||||||
|
self.cgroup(pid, 'memory.max', 'max\n')
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_mem', 'so-soc')]
|
||||||
|
self.assertEqual(fields['limit'], '%du' % (15625000 * 1024))
|
||||||
|
|
||||||
|
def test_max_usage_reports_the_cgroup_peak(self):
|
||||||
|
# the daemon reports 0 on cgroup v2, so this deliberately carries the real peak
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_mem', 'so-soc')]
|
||||||
|
self.assertEqual(fields['max_usage'], '2500u')
|
||||||
|
|
||||||
|
|
||||||
|
class TestCpuSemantics(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_microsecond_counters_are_published_as_nanoseconds(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(fields['usage_total'], '1000000u')
|
||||||
|
self.assertEqual(fields['usage_in_usermode'], '600000u')
|
||||||
|
self.assertEqual(fields['usage_in_kernelmode'], '400000u')
|
||||||
|
self.assertEqual(fields['throttling_throttled_time'], '700000u')
|
||||||
|
|
||||||
|
def test_throttling_counts_are_not_scaled(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(fields['throttling_periods'], '5u')
|
||||||
|
self.assertEqual(fields['throttling_throttled_periods'], '2u')
|
||||||
|
|
||||||
|
def test_usage_system_is_host_wide_cpu_time(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(field_type(fields['usage_system']), 'unsigned')
|
||||||
|
self.assertNotEqual(fields['usage_system'], '0u')
|
||||||
|
|
||||||
|
|
||||||
|
class TestNetwork(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_counters_sum_interfaces_and_ignore_loopback(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_net', 'so-soc')]
|
||||||
|
self.assertEqual(fields['rx_bytes'], '1500u')
|
||||||
|
self.assertEqual(fields['rx_packets'], '15u')
|
||||||
|
self.assertEqual(fields['tx_bytes'], '3000u')
|
||||||
|
self.assertEqual(fields['rx_dropped'], '2u')
|
||||||
|
|
||||||
|
def test_host_network_containers_emit_no_row(self):
|
||||||
|
# inputs.docker skipped these: its Networks map is empty for --net=host
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
self.assertNotIn(('docker_container_net', 'so-telegraf'), points)
|
||||||
|
self.assertIn(('docker_container_net', 'so-soc'), points)
|
||||||
|
|
||||||
|
def test_total_tag_is_present(self):
|
||||||
|
tags, _ = self.run_collector(all_enabled())[('docker_container_net', 'so-soc')]
|
||||||
|
self.assertEqual(tags['network'], 'total')
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlkio(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_counters_sum_devices(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_blkio', 'so-soc')]
|
||||||
|
self.assertEqual(fields['io_service_bytes_recursive_read'], '110u')
|
||||||
|
self.assertEqual(fields['io_service_bytes_recursive_write'], '220u')
|
||||||
|
|
||||||
|
def test_container_with_no_io_reports_zero_rather_than_disappearing(self):
|
||||||
|
for pid in (1001, 1002, 1003):
|
||||||
|
self.cgroup(pid, 'io.stat', '')
|
||||||
|
tags, fields = self.run_collector(all_enabled())[('docker_container_blkio', 'so-soc')]
|
||||||
|
self.assertEqual(fields['io_service_bytes_recursive_read'], '0u')
|
||||||
|
self.assertEqual(tags['device'], 'total')
|
||||||
|
|
||||||
|
|
||||||
|
class TestStatus(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_running_container_uptime_counts_from_start(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_status', 'so-soc')]
|
||||||
|
self.assertGreater(int(fields['uptime_ns'].rstrip('i')), 0)
|
||||||
|
self.assertNotIn('finished_at', fields)
|
||||||
|
|
||||||
|
def test_exited_container_reports_its_lifetime_and_finished_at(self):
|
||||||
|
self.inspect.append(container('so-idstools', IDSTOOLS_ID, 0, status='exited',
|
||||||
|
started='2026-09-24T10:00:00.000000000Z',
|
||||||
|
finished='2026-09-24T10:00:02.000000000Z', exitcode=3, restarts=1))
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
tags, fields = points[('docker_container_status', 'so-idstools')]
|
||||||
|
self.assertEqual(tags['container_status'], 'exited')
|
||||||
|
self.assertEqual(fields['uptime_ns'], '2000000000i')
|
||||||
|
self.assertEqual(fields['finished_at'], '1790244002000000000i')
|
||||||
|
self.assertEqual(fields['exitcode'], '3i')
|
||||||
|
self.assertEqual(fields['restart_count'], '1i')
|
||||||
|
# a stopped container has no live stats, so only the status row is emitted
|
||||||
|
self.assertNotIn(('docker_container_cpu', 'so-idstools'), points)
|
||||||
|
|
||||||
|
def test_health_is_emitted_only_for_containers_with_a_healthcheck(self):
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
self.assertIn(('docker_container_health', 'so-nginx'), points)
|
||||||
|
self.assertNotIn(('docker_container_health', 'so-soc'), points)
|
||||||
|
|
||||||
|
def test_oomkilled_is_reported(self):
|
||||||
|
self.inspect[0]['State']['OOMKilled'] = True
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker_container_status', 'so-soc')]
|
||||||
|
self.assertEqual(fields['oomkilled'], 'true')
|
||||||
|
|
||||||
|
|
||||||
|
class TestEngineMeasurement(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_engine_counts_come_from_docker_info(self):
|
||||||
|
_, fields = self.run_collector(all_enabled())[('docker', '')]
|
||||||
|
self.assertEqual(fields['n_containers'], '3i')
|
||||||
|
self.assertEqual(fields['n_cpus'], '8i')
|
||||||
|
self.assertEqual(fields['n_used_file_descriptors'], '77i')
|
||||||
|
|
||||||
|
def test_engine_is_published_as_two_points(self):
|
||||||
|
# inputs.docker emitted memory_total on its own point, so keep that shape
|
||||||
|
self.run_collector(all_enabled())
|
||||||
|
engine = [line for line in self.output.splitlines() if line.startswith('docker,')]
|
||||||
|
self.assertEqual(len(engine), 2)
|
||||||
|
self.assertTrue(any('memory_total=' in line for line in engine))
|
||||||
|
self.assertTrue(any('n_containers=' in line for line in engine))
|
||||||
|
|
||||||
|
def test_engine_rows_are_tagged_with_host_and_version(self):
|
||||||
|
tags, _ = self.run_collector(all_enabled())[('docker', '')]
|
||||||
|
self.assertEqual(tags['engine_host'], 'sohost')
|
||||||
|
self.assertEqual(tags['server_version'], '29.2.1')
|
||||||
|
|
||||||
|
|
||||||
|
class TestLineProtocol(CollectorTestCase):
|
||||||
|
|
||||||
|
def test_tag_values_are_escaped(self):
|
||||||
|
self.inspect[0]['Name'] = '/odd name,with=chars'
|
||||||
|
self.stats['odd name,with=chars'] = self.stats.pop('so-soc')
|
||||||
|
self.stats['odd name,with=chars']['Name'] = 'odd name,with=chars'
|
||||||
|
output = self.run_collector(all_enabled())
|
||||||
|
self.assertIn(('docker_container_cpu', 'odd\\ name\\,with\\=chars'), output)
|
||||||
|
|
||||||
|
def test_image_without_a_tag_reports_version_unknown(self):
|
||||||
|
self.inspect[0]['Config']['Image'] = 'busybox'
|
||||||
|
tags, _ = self.run_collector(all_enabled())[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(tags['container_image'], 'busybox')
|
||||||
|
self.assertEqual(tags['container_version'], 'unknown')
|
||||||
|
|
||||||
|
def test_every_line_has_a_measurement_tagset_and_fieldset(self):
|
||||||
|
module = render(all_enabled())
|
||||||
|
|
||||||
|
def fake_docker(args):
|
||||||
|
if args[0] == 'stats':
|
||||||
|
return ''.join(json.dumps(entry) + '\n' for entry in self.stats.values())
|
||||||
|
if args[0] == 'ps':
|
||||||
|
return ' '.join(entry['Id'] for entry in self.inspect)
|
||||||
|
if args[0] == 'inspect':
|
||||||
|
return json.dumps(self.inspect)
|
||||||
|
return json.dumps(self.info)
|
||||||
|
|
||||||
|
module.docker = fake_docker
|
||||||
|
module.read_text = lambda path: self.files.get(path, '')
|
||||||
|
buffer = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(buffer):
|
||||||
|
module.main()
|
||||||
|
lines = [line for line in buffer.getvalue().splitlines() if line.strip()]
|
||||||
|
self.assertTrue(lines)
|
||||||
|
for line in lines:
|
||||||
|
head, fieldpart = split_on_space(line)
|
||||||
|
self.assertIn(',', head, line)
|
||||||
|
self.assertIn('=', fieldpart, line)
|
||||||
|
self.assertFalse(fieldpart.endswith(','), line)
|
||||||
|
|
||||||
|
|
||||||
|
# group -> (measurement, the container whose row carries it)
|
||||||
|
GROUP_TARGET = {
|
||||||
|
'engine': ('docker', ''),
|
||||||
|
'cpu': ('docker_container_cpu', 'so-soc'),
|
||||||
|
'mem': ('docker_container_mem', 'so-soc'),
|
||||||
|
'net': ('docker_container_net', 'so-soc'),
|
||||||
|
'blkio': ('docker_container_blkio', 'so-soc'),
|
||||||
|
'status': ('docker_container_status', 'so-soc'),
|
||||||
|
'health': ('docker_container_health', 'so-nginx'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestEverySetting(CollectorTestCase):
|
||||||
|
"""Whatever is offered in defaults.yaml has to actually be collectable. These tests are driven
|
||||||
|
off that file, so a new setting that is never wired up fails here rather than shipping."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
# an exited container so status.finished_at has a value to report
|
||||||
|
self.inspect.append(container('so-idstools', IDSTOOLS_ID, 0, status='exited',
|
||||||
|
started='2026-09-24T10:00:00.000000000Z',
|
||||||
|
finished='2026-09-24T10:00:02.000000000Z'))
|
||||||
|
|
||||||
|
def test_every_setting_emits_its_field_when_enabled(self):
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
for group, fields in shipped_defaults().items():
|
||||||
|
if group == 'tags':
|
||||||
|
continue
|
||||||
|
measurement, name = GROUP_TARGET[group]
|
||||||
|
if group == 'status':
|
||||||
|
# finished_at only exists for a container that has actually exited
|
||||||
|
_, exited = points[(measurement, 'so-idstools')]
|
||||||
|
self.assertIn('finished_at', exited)
|
||||||
|
_, emitted = points[(measurement, name)]
|
||||||
|
for field in fields:
|
||||||
|
if group == 'status' and field == 'finished_at':
|
||||||
|
continue
|
||||||
|
self.assertIn(field, emitted, '%s.%s is offered but never emitted' % (group, field))
|
||||||
|
|
||||||
|
def test_every_setting_is_individually_wired(self):
|
||||||
|
# enabling one stat on its own must produce exactly that field, proving each toggle is
|
||||||
|
# read rather than riding along with a neighbour
|
||||||
|
for group, fields in shipped_defaults().items():
|
||||||
|
if group == 'tags':
|
||||||
|
continue
|
||||||
|
measurement, name = GROUP_TARGET[group]
|
||||||
|
for field in fields:
|
||||||
|
settings = {other: {key: False for key in values} for other, values in shipped_defaults().items()}
|
||||||
|
settings[group][field] = True
|
||||||
|
target = 'so-idstools' if (group == 'status' and field == 'finished_at') else name
|
||||||
|
points = self.run_collector(settings)
|
||||||
|
self.assertIn((measurement, target), points, '%s.%s emitted no row' % (group, field))
|
||||||
|
_, emitted = points[(measurement, target)]
|
||||||
|
self.assertEqual(sorted(emitted), [field], '%s.%s did not emit itself alone' % (group, field))
|
||||||
|
|
||||||
|
def test_all_enabled_values_are_exact(self):
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
clock = os.sysconf('SC_CLK_TCK')
|
||||||
|
expected = {
|
||||||
|
('docker_container_cpu', 'so-soc'): {
|
||||||
|
'usage_percent': '1.25', 'usage_total': '1000000u', 'usage_in_usermode': '600000u',
|
||||||
|
'usage_in_kernelmode': '400000u', 'usage_system': '%du' % int(1000 * 10**9 / clock),
|
||||||
|
'throttling_periods': '5u', 'throttling_throttled_periods': '2u',
|
||||||
|
'throttling_throttled_time': '700000u', 'container_id': '"%s"' % SOC_ID,
|
||||||
|
},
|
||||||
|
('docker_container_mem', 'so-soc'): {
|
||||||
|
'usage': '950u', 'limit': '4000u', 'max_usage': '2500u', 'active_anon': '300u',
|
||||||
|
'active_file': '40u', 'inactive_anon': '200u', 'inactive_file': '50u',
|
||||||
|
'unevictable': '0u', 'pgfault': '1234u', 'pgmajfault': '56u',
|
||||||
|
'usage_percent': '23.75', 'container_id': '"%s"' % SOC_ID,
|
||||||
|
},
|
||||||
|
('docker_container_net', 'so-soc'): {
|
||||||
|
'rx_bytes': '1500u', 'rx_packets': '15u', 'rx_errors': '1u', 'rx_dropped': '2u',
|
||||||
|
'tx_bytes': '3000u', 'tx_packets': '30u', 'tx_errors': '3u', 'tx_dropped': '4u',
|
||||||
|
'container_id': '"%s"' % SOC_ID,
|
||||||
|
},
|
||||||
|
('docker_container_blkio', 'so-soc'): {
|
||||||
|
'io_service_bytes_recursive_read': '110u',
|
||||||
|
'io_service_bytes_recursive_write': '220u', 'container_id': '"%s"' % SOC_ID,
|
||||||
|
},
|
||||||
|
('docker', ''): {
|
||||||
|
'n_containers': '3i', 'n_containers_running': '3i', 'n_containers_stopped': '0i',
|
||||||
|
'n_containers_paused': '0i', 'n_images': '9i', 'n_cpus': '8i', 'n_goroutines': '42i',
|
||||||
|
'n_used_file_descriptors': '77i', 'n_listener_events': '1i',
|
||||||
|
'memory_total': '16000000000i',
|
||||||
|
},
|
||||||
|
('docker_container_health', 'so-nginx'): {
|
||||||
|
'health_status': '"healthy"', 'failing_streak': '0i',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for key, fields in expected.items():
|
||||||
|
_, emitted = points[key]
|
||||||
|
for field, value in fields.items():
|
||||||
|
self.assertEqual(emitted[field], value, '%s %s' % (key[0], field))
|
||||||
|
|
||||||
|
def test_status_values_are_exact(self):
|
||||||
|
# uptime is relative to now, so it is checked separately from the fixed fields
|
||||||
|
points = self.run_collector(all_enabled())
|
||||||
|
_, running = points[('docker_container_status', 'so-soc')]
|
||||||
|
self.assertEqual(running['pid'], '1001i')
|
||||||
|
self.assertEqual(running['exitcode'], '0i')
|
||||||
|
self.assertEqual(running['restart_count'], '0i')
|
||||||
|
self.assertEqual(running['oomkilled'], 'false')
|
||||||
|
self.assertEqual(running['container_id'], '"%s"' % SOC_ID)
|
||||||
|
self.assertEqual(int(running['started_at'].rstrip('i')) // 10**9, 1790244000)
|
||||||
|
self.assertGreater(int(running['uptime_ns'].rstrip('i')), 0)
|
||||||
|
_, exited = points[('docker_container_status', 'so-idstools')]
|
||||||
|
self.assertEqual(exited['uptime_ns'], '2000000000i')
|
||||||
|
self.assertEqual(exited['finished_at'], '1790244002000000000i')
|
||||||
|
|
||||||
|
def test_tags_identity_toggle_controls_the_identity_tags(self):
|
||||||
|
settings = shipped_defaults()
|
||||||
|
settings['tags']['identity'] = False
|
||||||
|
tags, _ = self.run_collector(settings)[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(sorted(tags), ['container_name', 'container_status', 'cpu'])
|
||||||
|
settings['tags']['identity'] = True
|
||||||
|
tags, _ = self.run_collector(settings)[('docker_container_cpu', 'so-soc')]
|
||||||
|
self.assertEqual(sorted(tags), ['container_image', 'container_name', 'container_status',
|
||||||
|
'container_version', 'cpu', 'engine_host', 'server_version'])
|
||||||
|
|
||||||
|
def test_identity_tags_cover_every_documented_tag(self):
|
||||||
|
tags, _ = self.run_collector(all_enabled())[('docker_container_cpu', 'so-soc')]
|
||||||
|
for tag in ('container_image', 'container_version', 'engine_host', 'server_version'):
|
||||||
|
self.assertIn(tag, tags)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTemplate(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_template_renders_to_valid_python_for_the_shipped_defaults(self):
|
||||||
|
with open(TEMPLATE) as handle:
|
||||||
|
source = handle.read()
|
||||||
|
rendered = jinja2.Template(source, keep_trailing_newline=True).render(CONTAINER_STATS=shipped_defaults())
|
||||||
|
compile(rendered, 'so-container-stats', 'exec')
|
||||||
|
self.assertNotIn('{%', rendered)
|
||||||
|
# the docker format strings must survive rendering untouched
|
||||||
|
self.assertIn('{{json .}}', rendered)
|
||||||
|
|
||||||
|
def test_every_annotated_setting_exists_in_defaults(self):
|
||||||
|
# SOC reads both trees; an annotation without a default cannot be reverted in the UI
|
||||||
|
with open(os.path.join(HERE, '..', '..', 'soc_telegraf.yaml')) as handle:
|
||||||
|
annotated = yaml.safe_load(handle)['telegraf']['container_stats']
|
||||||
|
defaults = shipped_defaults()
|
||||||
|
for group, fields in annotated.items():
|
||||||
|
self.assertIn(group, defaults)
|
||||||
|
for field in fields:
|
||||||
|
self.assertIn(field, defaults[group], '%s.%s annotated but missing from defaults' % (group, field))
|
||||||
|
|
||||||
|
def test_every_default_setting_is_annotated_for_soc(self):
|
||||||
|
with open(os.path.join(HERE, '..', '..', 'soc_telegraf.yaml')) as handle:
|
||||||
|
annotated = yaml.safe_load(handle)['telegraf']['container_stats']
|
||||||
|
for group, fields in shipped_defaults().items():
|
||||||
|
self.assertIn(group, annotated)
|
||||||
|
for field in fields:
|
||||||
|
self.assertIn(field, annotated[group], '%s.%s missing a SOC annotation' % (group, field))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -23,6 +23,7 @@ zeek:
|
|||||||
- gid: 937
|
- gid: 937
|
||||||
- home: /opt/so/conf/zeek
|
- home: /opt/so/conf/zeek
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
# Create some directories
|
# Create some directories
|
||||||
zeekpolicydir:
|
zeekpolicydir:
|
||||||
|
|||||||
@@ -1611,6 +1611,7 @@ reserve_group_ids() {
|
|||||||
logCmd "groupadd -g 949 elastic-agent"
|
logCmd "groupadd -g 949 elastic-agent"
|
||||||
logCmd "groupadd -g 947 elastic-fleet"
|
logCmd "groupadd -g 947 elastic-fleet"
|
||||||
logCmd "groupadd -g 960 kafka"
|
logCmd "groupadd -g 960 kafka"
|
||||||
|
logCmd "groupadd -g 961 somon"
|
||||||
}
|
}
|
||||||
|
|
||||||
reserve_ports() {
|
reserve_ports() {
|
||||||
|
|||||||
Reference in New Issue
Block a user