Initial version
This commit is contained in:
commit
2ef39b9eaa
5 changed files with 340 additions and 0 deletions
37
.pre-commit-config.yaml
Normal file
37
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Pre-commit hooks to run tests and ensure code is cleaned.
|
||||||
|
# See https://pre-commit.com for more information
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
|
rev: v3.15.0
|
||||||
|
hooks:
|
||||||
|
- id: pyupgrade
|
||||||
|
args: ['--keep-percent-format', '--py37-plus']
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 23.11.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
args: ['--target-version', 'py37', '--line-length', '100']
|
||||||
|
- repo: https://github.com/PyCQA/isort
|
||||||
|
rev: 5.12.0
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
args: ['--profile', 'black', '--line-length', '100']
|
||||||
|
- repo: https://github.com/PyCQA/flake8
|
||||||
|
rev: 6.1.0
|
||||||
|
hooks:
|
||||||
|
- id: flake8
|
||||||
|
args: ['--max-line-length=100']
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: pylint
|
||||||
|
name: pylint
|
||||||
|
entry: pylint
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
|
require_serial: true
|
||||||
|
- repo: https://github.com/PyCQA/bandit
|
||||||
|
rev: 1.7.5
|
||||||
|
hooks:
|
||||||
|
- id: bandit
|
||||||
|
args: [--skip, "B101", --recursive, "mylib"]
|
||||||
|
minimum_pre_commit_version: 3.2.0
|
17
.pylintrc
Normal file
17
.pylintrc
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=invalid-name,
|
||||||
|
locally-disabled,
|
||||||
|
too-many-arguments,
|
||||||
|
too-many-branches,
|
||||||
|
too-many-locals,
|
||||||
|
too-many-return-statements,
|
||||||
|
too-many-nested-blocks,
|
||||||
|
too-many-instance-attributes,
|
||||||
|
too-many-lines,
|
||||||
|
too-many-statements,
|
||||||
|
logging-too-many-args,
|
||||||
|
duplicate-code,
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
# Maximum number of characters on a single line.
|
||||||
|
max-line-length=100
|
72
README.md
Normal file
72
README.md
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# Monitoring plugin to check Syncthing status
|
||||||
|
|
||||||
|
This Icinga/Nagios check plugin permit to check Syncthing status :
|
||||||
|
|
||||||
|
- verify Syncthing REST API is responding
|
||||||
|
- check remote devices last seen datetime
|
||||||
|
- check system and shared folders errors
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
First, generate and retreive the REST API key from Syncthing web interface:
|
||||||
|
- in Actions menu, click on Configuration
|
||||||
|
- in General tab, under API key, click on Generate button and copy the key
|
||||||
|
|
||||||
|
```
|
||||||
|
API_KEY=n6RseK3HWY5LJ29PzNSUJbmK4XKHa5uV
|
||||||
|
apt install git
|
||||||
|
git clone https://gitea.zionetrix.net/bn8/check_syncthing.git /usr/local/src/check_syncthing
|
||||||
|
mkdir -p /usr/local/lib/nagios/plugins
|
||||||
|
ln -s /usr/local/src/check_syncthing/check_syncthing /usr/local/lib/nagios/plugins/
|
||||||
|
echo "command[check_syncthing]=/usr/local/lib/nagios/plugins/check_syncthing -k $API_KEY" > /etc/nagios/nrpe.d/syncthing.cfg
|
||||||
|
service nagios-nrpe-server reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: check_syncthing [-h] [-d] [-v] [-H HOST] [-p PORT] -k API_KEY [-S]
|
||||||
|
[-t TIMEOUT] [-D DEVICES] [-x EXCLUDED_DEVICES] [-F FOLDERS]
|
||||||
|
[-X EXCLUDED_FOLDERS] [-w WARNING_DEVICE_LAST_SEEN]
|
||||||
|
[-c CRITICAL_DEVICE_LAST_SEEN]
|
||||||
|
|
||||||
|
Monitoring plugin to check Syncthing status
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-d, --debug Enable debug mode (default: False)
|
||||||
|
-v, --verbose Enable verbose mode (default: False)
|
||||||
|
-H HOST, --host HOST Syncthing host (default: 127.0.0.1)
|
||||||
|
-p PORT, --port PORT Syncthing port (default: 8384)
|
||||||
|
-k API_KEY, --api-key API_KEY
|
||||||
|
Syncthing REST API key (default: None)
|
||||||
|
-S, --ssl Enable SSL (default: False)
|
||||||
|
-t TIMEOUT, --timeout TIMEOUT
|
||||||
|
Requests timeout (in seconds) (default: 5)
|
||||||
|
-D DEVICES, --devices DEVICES
|
||||||
|
Monitor only specified devices (default: [])
|
||||||
|
-x EXCLUDED_DEVICES, --excluded-devices EXCLUDED_DEVICES
|
||||||
|
Do not monitor specified devices (default: [])
|
||||||
|
-F FOLDERS, --folders FOLDERS
|
||||||
|
Monitor only specified folders (default: [])
|
||||||
|
-X EXCLUDED_FOLDERS, --excluded-folders EXCLUDED_FOLDERS
|
||||||
|
Do not monitor specified folders (default: [])
|
||||||
|
-w WARNING_DEVICE_LAST_SEEN, --warning-device-last-seen WARNING_DEVICE_LAST_SEEN
|
||||||
|
Warning threshold for device last seen time in hours
|
||||||
|
(default: 12)
|
||||||
|
-c CRITICAL_DEVICE_LAST_SEEN, --critical-device-last-seen CRITICAL_DEVICE_LAST_SEEN
|
||||||
|
Critical threshold for device last seen time in hours
|
||||||
|
(default: 24)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copyright
|
||||||
|
|
||||||
|
Copyright (c) 2023 Benjamin Renard <brenard@zionetrix.net>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 3 as published by the Free Software Foundation.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
211
check_syncthing
Executable file
211
check_syncthing
Executable file
|
@ -0,0 +1,211 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
"""Monitoring plugin to check Syncthing status"""
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
import requests
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument("-d", "--debug", action="store_true", help="Enable debug mode")
|
||||||
|
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose mode")
|
||||||
|
|
||||||
|
parser.add_argument("-H", "--host", help="Syncthing host", default="127.0.0.1")
|
||||||
|
parser.add_argument("-p", "--port", type=int, help="Syncthing port", default=8384)
|
||||||
|
parser.add_argument("-k", "--api-key", help="Syncthing REST API key", required=True)
|
||||||
|
parser.add_argument("-S", "--ssl", action="store_true", help="Enable SSL")
|
||||||
|
parser.add_argument("-t", "--timeout", type=int, help="Requests timeout (in seconds)", default=5)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-D", "--devices", action="append", help="Monitor only specified devices", default=[]
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-x", "--excluded-devices", action="append", help="Do not monitor specified devices", default=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-F", "--folders", action="append", help="Monitor only specified folders", default=[]
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-X", "--excluded-folders", action="append", help="Do not monitor specified folders", default=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-w",
|
||||||
|
"--warning-device-last-seen",
|
||||||
|
type=int,
|
||||||
|
help="Warning threshold for device last seen time in hours",
|
||||||
|
default=12,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-c",
|
||||||
|
"--critical-device-last-seen",
|
||||||
|
type=int,
|
||||||
|
help="Critical threshold for device last seen time in hours",
|
||||||
|
default=24,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logformat = f"%(asctime)s - {sys.argv[0]} - %(levelname)s - %(message)s"
|
||||||
|
if args.debug:
|
||||||
|
loglevel = logging.DEBUG
|
||||||
|
elif args.verbose:
|
||||||
|
loglevel = logging.INFO
|
||||||
|
else:
|
||||||
|
loglevel = logging.WARNING
|
||||||
|
|
||||||
|
logging.basicConfig(level=loglevel, format=logformat)
|
||||||
|
|
||||||
|
|
||||||
|
def call_api(uri, method=None, data=None, headers=None, what=None):
|
||||||
|
"""Call Syncthing REST API"""
|
||||||
|
method = method.lower() if method else "get"
|
||||||
|
headers = headers if headers else {}
|
||||||
|
headers["X-API-Key"] = args.api_key
|
||||||
|
try:
|
||||||
|
r = getattr(requests, method)(
|
||||||
|
f"http{'s' if args.ssl else ''}://{args.host}:{args.port}/rest/{uri}",
|
||||||
|
data=json.dumps(data) if data else None,
|
||||||
|
headers=headers,
|
||||||
|
timeout=args.timeout,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
except Exception: # pylint: disable=broad-exception-caught
|
||||||
|
print(f"UNKNOWN - Fail to retreive {what if what else 'data'} from Syncthing REST API")
|
||||||
|
if data:
|
||||||
|
print(f"Request data: {json.dumps(data, indent=2)}")
|
||||||
|
logging.exception("Error on requesting %s %s", method, uri)
|
||||||
|
sys.exit(3)
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except requests.exceptions.JSONDecodeError:
|
||||||
|
print(
|
||||||
|
"UNKNOWN - Fail to decode Syncthing REST API data on reqesting "
|
||||||
|
f"{what if what else 'data'}"
|
||||||
|
)
|
||||||
|
print("Raw API return:")
|
||||||
|
print(r.text)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
|
||||||
|
config = call_api("config", what="configuration")
|
||||||
|
my_dev_id = config["defaults"]["folder"]["devices"][0]["deviceID"]
|
||||||
|
logging.debug("Config: %s", json.dumps(config, indent=2))
|
||||||
|
devices = {
|
||||||
|
dev["deviceID"]: dev["name"]
|
||||||
|
for dev in config["devices"]
|
||||||
|
if (
|
||||||
|
not dev["paused"]
|
||||||
|
and dev["deviceID"] != my_dev_id
|
||||||
|
and (not args.devices or dev["name"] in args.devices)
|
||||||
|
and dev["name"] not in args.excluded_devices
|
||||||
|
)
|
||||||
|
}
|
||||||
|
logging.debug("Devices from config: %s", json.dumps(devices, indent=2))
|
||||||
|
|
||||||
|
if args.devices:
|
||||||
|
unknown_devices = set(args.devices) - set(devices.values())
|
||||||
|
if unknown_devices:
|
||||||
|
print(f"UNKNOWN - Devices(s) are unknowns: {', '.join(unknown_devices)}")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
devices_stats = call_api("stats/device", what="devices stats")
|
||||||
|
logging.debug("Devices stats: %s", json.dumps(devices_stats, indent=2))
|
||||||
|
devices_last_seen = {
|
||||||
|
dev_id: dateutil.parser.isoparse(info["lastSeen"])
|
||||||
|
for dev_id, info in devices_stats.items()
|
||||||
|
if info["lastConnectionDurationS"] > 0
|
||||||
|
}
|
||||||
|
logging.debug("Device last seen: %s", json.dumps(devices_last_seen, indent=2, default=str))
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
error_level = "OK"
|
||||||
|
exit_status = {
|
||||||
|
"OK": 0,
|
||||||
|
"WARNING": 1,
|
||||||
|
"CRITICAL": 2,
|
||||||
|
"UNKNOWN": 3,
|
||||||
|
}
|
||||||
|
extra_lines = []
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
|
||||||
|
def add_error(level, msg):
|
||||||
|
"""Add error"""
|
||||||
|
global error_level # pylint: disable=global-statement
|
||||||
|
error_level = level if exit_status[level] > exit_status[error_level] else error_level
|
||||||
|
errors.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
warning_device_last_seen = datetime.timedelta(hours=args.warning_device_last_seen)
|
||||||
|
critical_device_last_seen = datetime.timedelta(hours=args.critical_device_last_seen)
|
||||||
|
now = datetime.datetime.now().replace(tzinfo=dateutil.tz.tzlocal())
|
||||||
|
|
||||||
|
extra_lines.append("Remote devices:")
|
||||||
|
for dev_id, dev_name in devices.items():
|
||||||
|
if args.devices and dev_name not in args.devices:
|
||||||
|
continue
|
||||||
|
last_seen = devices_last_seen.get(dev_id)
|
||||||
|
if not last_seen:
|
||||||
|
add_error("WARNING", f"Never seen device {dev_name}")
|
||||||
|
extra_lines.append(f"- {dev_name}: Never seen")
|
||||||
|
continue
|
||||||
|
delta = now - devices_last_seen.get(dev_id)
|
||||||
|
delta_str = str(delta).split(".", maxsplit=1)[0]
|
||||||
|
extra_lines.append(f"- {dev_name}: last seen on {last_seen} ({delta_str})")
|
||||||
|
if delta < warning_device_last_seen:
|
||||||
|
continue
|
||||||
|
error = f"Device {dev_name} not seen since {delta_str}"
|
||||||
|
if delta >= critical_device_last_seen:
|
||||||
|
add_error("CRITICAL", error)
|
||||||
|
elif delta >= warning_device_last_seen:
|
||||||
|
add_error("WARNING", error)
|
||||||
|
|
||||||
|
extra_lines.append("Folders:")
|
||||||
|
for folder in config["folders"]:
|
||||||
|
share_with = [
|
||||||
|
devices[dev["deviceID"]] for dev in folder["devices"] if dev["deviceID"] in devices
|
||||||
|
]
|
||||||
|
extra_lines.append(
|
||||||
|
f"- {folder['label']} ({folder['path']}, share with {', '.join(share_with)})"
|
||||||
|
)
|
||||||
|
folder_errors = call_api(
|
||||||
|
f"folder/errors?folder={folder['id']}", what=f"folder '{folder['label']}' errors"
|
||||||
|
)
|
||||||
|
if folder_errors["errors"]:
|
||||||
|
add_error(
|
||||||
|
"WARNING",
|
||||||
|
f"{len(folder_errors['errors'])} errors detected on folder '{folder['label']}'",
|
||||||
|
)
|
||||||
|
extra_lines.append(f"Folder '{folder['label']}' errors:")
|
||||||
|
extra_lines.extend(
|
||||||
|
[f"{error['path']}: {error['error']}" for error in folder_errors["errors"]]
|
||||||
|
)
|
||||||
|
|
||||||
|
system_errors = call_api("system/error", what="system errors")
|
||||||
|
logging.debug("System errors: %s", json.dumps(system_errors, indent=2))
|
||||||
|
if system_errors["errors"]:
|
||||||
|
add_error("WARNING", f"{len(system_errors)} system errors detected")
|
||||||
|
extra_lines.extend(
|
||||||
|
[f"{error['when']}: {error['message']}" for error in system_errors["errors"]]
|
||||||
|
)
|
||||||
|
|
||||||
|
status = [error_level]
|
||||||
|
if errors:
|
||||||
|
status.append(", ".join(errors))
|
||||||
|
else:
|
||||||
|
status.append(
|
||||||
|
f"All {len(devices)} seen since less than {args.warning_device_last_seen}, "
|
||||||
|
"no error detected"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" - ".join(status))
|
||||||
|
print("\n".join(extra_lines))
|
||||||
|
sys.exit(exit_status[error_level])
|
3
setup.cfg
Normal file
3
setup.cfg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[flake8]
|
||||||
|
ignore = E501,W503
|
||||||
|
max-line-length = 100
|
Loading…
Reference in a new issue