Содержание
Был у меня Bash-скрипт, который обходил серверы и собирал занятое место на дисках. За пару лет он оброс awk, sed и проверками кодов возврата. Любая правка превращалась в квест с кавычками. Переписал на Python, и скрипт снова стало можно читать.
Дальше без теории: как поставить Python, где держать скрипты, как их настраивать, логировать и запускать. Сквозной пример — тот самый проверяльщик дисков. Он ходит по SSH, пишет отчёт в JSON и шлёт алерт в Telegram через API.
Что понадобится для Python для системного администратора
Требования и подготовка
Debian 12 или 13, Ubuntu 22.04 или 24.04. python3 там почти всегда уже стоит, только на минимальном Debian его иногда нет. Версии из коробки такие:
- Debian 12 — Python 3.11;
- Debian 13 — Python 3.13;
- Ubuntu 22.04 — Python 3.10;
- Ubuntu 24.04 — Python 3.12.
Для примеров ниже нужен 3.11 или новее, там появился tomllib для чтения TOML. На Ubuntu 22.04 конфиг придётся читать через пакет tomli или держать в JSON.
Кроме Python нужен SSH-доступ по ключу до серверов, которые будем опрашивать. И понимание, где Python действительно нужен.
Bash я оставляю для обвязки строк на двадцать. PowerShell — для Windows и AD. Всё, где есть логика, JSON, API или разбор ошибок, пишу на Python.
Установка и подготовка среды
Установка пакетов / компонентов
Ставим интерпретатор, модуль venv и pipx:
apt install python3 python3-venv python3-pip pipx
Системный Python трогаем только через apt. С Debian 12 и Ubuntu 23.04 pip install вне виртуального окружения падает с ошибкой externally-managed-environment. Это не баг, это защита системы от вас :).
Под скрипт заводим пользователя, каталоги и venv:
useradd --system --create-home --shell /usr/sbin/nologin diskwatch
mkdir -p /opt/diskwatch /etc/diskwatch
install -d -o diskwatch -g diskwatch /var/lib/diskwatch
python3 -m venv /opt/diskwatch/venv
/opt/diskwatch/venv/bin/pip install requests
/opt/diskwatch/venv/bin/pip freeze > /opt/diskwatch/requirements.txt
Утилиты, которые нужны как команды, а не как библиотеки, ставлю через pipx. Каждая получает свой venv и не мешает остальным. Так у меня живут ruff, pip-audit и прочие инструменты.
Ключ для SSH генерируем от имени пользователя скрипта:
sudo -u diskwatch ssh-keygen -t ed25519 -N "" -f /home/diskwatch/.ssh/id_ed25519
Публичную часть раскладываем по серверам. Как её ограничить, расскажу в разделе про безопасность.
Базовая настройка Python для системного администратора
Основные параметры
Любой мой админский скрипт собран из одних и тех же кусков. Их немного, и все из стандартной библиотеки:
argparse— параметры командной строки. Минимум--configи--dry-run.logging— вместо print. Уровни, формат и вывод в stderr, откуда его заберёт journald.subprocess.run— внешние команды списком аргументов, сtimeoutиcheck=True.pathlib— пути и файлы.jsonиtomllib— отчёты и конфиги.- Код возврата: 0, если всё хорошо, и не 0, если нет. Мониторинг и systemd смотрят именно на него.
Формат лога делаю без даты. Под systemd время к каждой строке добавит journald, и дата будет дублироваться.
Секреты в код не пишу никогда. Токены и пароли приходят через переменные окружения. Их задаёт EnvironmentFile в unit-файле.
Основная конфигурация
Пример конфигурации
Конфиг — обычный TOML. Список серверов, порог и куда класть отчёт:
# /etc/diskwatch/config.toml
hosts = ["web01", "web02", "db01"]
threshold = 85
ssh_timeout = 5
report = "/var/lib/diskwatch/report.json"
Секреты лежат отдельно, в файле с правами 600:
# /etc/diskwatch/env
TG_TOKEN=123456:replace-me
TG_CHAT_ID=-1001234567890
Сам скрипт короткий, но в нём есть всё, о чём шла речь выше:
# /opt/diskwatch/diskwatch.py
import argparse
import json
import logging
import os
import subprocess
import sys
import tomllib
from pathlib import Path
import requests
log = logging.getLogger("diskwatch")
DF = ["df", "-P", "-x", "tmpfs", "-x", "devtmpfs", "-x", "squashfs", "-x", "overlay"]
def parse_df(host: str, text: str) -> list[dict]:
rows = []
for line in text.splitlines()[1:]:
_, _, _, _, pcent, mount = line.split(maxsplit=5)
rows.append({"host": host, "mount": mount, "used": int(pcent.rstrip("%"))})
return rows
def get_usage(host: str, timeout: int) -> list[dict]:
cmd = ["ssh", "-o", "BatchMode=yes", "-o", f"ConnectTimeout={timeout}", host, *DF]
out = subprocess.run(cmd, capture_output=True, text=True,
timeout=timeout + 10, check=True)
return parse_df(host, out.stdout)
def notify(text: str) -> None:
url = f"https://api.telegram.org/bot{os.environ['TG_TOKEN']}/sendMessage"
r = requests.post(url, json={"chat_id": os.environ["TG_CHAT_ID"], "text": text},
timeout=10)
r.raise_for_status()
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="/etc/diskwatch/config.toml")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
with open(args.config, "rb") as f:
cfg = tomllib.load(f)
problems, failed = [], []
for host in cfg["hosts"]:
try:
rows = get_usage(host, cfg["ssh_timeout"])
except subprocess.CalledProcessError as e:
log.error("%s: ssh exit %d: %s", host, e.returncode, e.stderr.strip())
failed.append(host)
continue
except (subprocess.TimeoutExpired, ValueError) as e:
log.error("%s: %s", host, e)
failed.append(host)
continue
problems += [r for r in rows if r["used"] >= cfg["threshold"]]
Path(cfg["report"]).write_text(json.dumps(problems, indent=2), encoding="utf-8")
if problems and not args.dry_run:
notify("\n".join(f"{p['host']} {p['mount']}: {p['used']}%" for p in problems))
log.info("hosts=%d problems=%d failed=%d",
len(cfg["hosts"]), len(problems), len(failed))
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
Пара моментов. df уходит в SSH списком аргументов, shell на нашей стороне не участвует. -x squashfs отсекает snap-пакеты на Ubuntu, они всегда заняты на 100%.
У requests.post стоит таймаут, без него скрипт может висеть на API вечно. А stderr от ssh попадает в лог, и причина обрыва видна сразу.
Запускаю через systemd timer раз в 15 минут:
# /etc/systemd/system/diskwatch.service
[Unit]
Description=Disk usage check over SSH
[Service]
Type=oneshot
User=diskwatch
EnvironmentFile=/etc/diskwatch/env
StateDirectory=diskwatch
ExecStart=/opt/diskwatch/venv/bin/python /opt/diskwatch/diskwatch.py
# /etc/systemd/system/diskwatch.timer
[Unit]
Description=Run diskwatch every 15 minutes
[Timer]
OnCalendar=*:0/15
Persistent=true
[Install]
WantedBy=timers.target
В ExecStart указан python из venv. Активировать окружение не нужно, библиотеки подхватятся сами.
Проверка работоспособности
Проверка и тестирование
Первый запуск делаю руками, от пользователя скрипта и с --dry-run:
sudo -u diskwatch /opt/diskwatch/venv/bin/python /opt/diskwatch/diskwatch.py --dry-run
echo $?
Запуск именно от этого пользователя важен. У root другие ключи, другой known_hosts и другие права. Скрипт, который работает от root, под сервисным пользователем падает на первом же SSH.
Отчёт смотрю через json.tool. Заодно это проверка, что JSON валидный:
python3 -m json.tool /var/lib/diskwatch/report.json
Разбор вывода df вынесен в отдельную функцию не случайно. На неё легко написать тест без серверов и сети:
# /opt/diskwatch/test_diskwatch.py
from diskwatch import parse_df
def test_parse_df():
out = ("Filesystem 1024-blocks Used Available Capacity Mounted on\n"
"/dev/sda1 100 90 10 90% /\n")
assert parse_df("web01", out) == [{"host": "web01", "mount": "/", "used": 90}]
Запуск через pytest из того же venv:
/opt/diskwatch/venv/bin/pip install pytest
cd /opt/diskwatch && venv/bin/python -m pytest -q
Когда всё зелёное, включаю таймер и дёргаю сервис вручную, не дожидаясь расписания:
systemctl daemon-reload
systemctl enable --now diskwatch.timer
systemctl start diskwatch.service
systemctl list-timers diskwatch.timer
Логи и диагностика
Всё, что скрипт пишет через logging, лежит в журнале:
journalctl -u diskwatch -n 50 --no-pager
journalctl -u diskwatch --since today | grep ERROR
Фильтр -p err тут не поможет. Уровни Python сами в приоритеты journald не превращаются, все строки придут с одним уровнем. Поэтому ищу по тексту.
Если скрипт упал с исключением, traceback тоже будет в журнале. Для ручной отладки удобен python3 -X dev. Он включает предупреждения, которые обычно молчат, например про незакрытые файлы.
Для проблем с SSH добавляю -v в команду и смотрю, на каком шаге рвётся. Чаще всего виноваты known_hosts или права на ключ.
Безопасность и резервное копирование
Главное правило для subprocess — никакого shell=True с данными из конфига или из сети. Имя хоста вида web01; rm -rf / в shell превращается в команду. Список аргументов такой фокус не пропускает.
Имена хостов из конфига я ещё и проверяю регуляркой. Имя, которое начинается с дефиса, ssh примет за опцию.
Ключ, которым скрипт ходит на серверы, ограничиваю на стороне сервера. В authorized_keys ему разрешена одна команда и ничего больше:
# ~/.ssh/authorized_keys на целевом сервере
restrict,command="df -P -x tmpfs -x devtmpfs -x squashfs -x overlay" ssh-ed25519 AAAA... diskwatch
Украдут ключ — получат вывод df. Переживу.
Версии зависимостей фиксирую в requirements.txt и ставлю только из него. Перед обновлением прогоняю pip-audit, он сверяет пакеты с базой уязвимостей.
Env-файл с секретами принадлежит root, права 600, в git не попадает. В репозиторий кладу env.example с пустыми значениями.
Теперь резервное копирование. Venv не бэкаплю, он пересоздаётся за минуту из requirements.txt. Бэкапить нужно другое:
- код,
requirements.txtи тесты — в git; /etc/diskwatch— в обычный бэкап/etc, секреты отдельно в хранилище паролей;- отчёты из
/var/lib/diskwatch— если по ним строится история.
Восстановление тогда укладывается в три команды: git clone, python3 -m venv и pip install -r requirements.txt.
Типичные ошибки
externally-managed-environment. sudo pip install на Debian 12+ и Ubuntu 23.04+ заканчивается этой ошибкой. Флаг --break-system-packages её обходит, но название у флага честное. Ставьте в venv, утилиты — через pipx, системные библиотеки — через apt install python3-....
ModuleNotFoundError под cron или systemd. Руками скрипт работает, из планировщика нет. Обычно в shebang или в ExecStart стоит /usr/bin/python3, а библиотеки лежат в venv. Указывайте полный путь к python из venv.
Host key verification failed. Сервисный пользователь ни разу не ходил на сервер, known_hosts пустой. С BatchMode=yes ssh сразу упадёт с этой ошибкой. Без него при ручном запуске скрипт повиснет на вопросе про fingerprint. Ключи хостов добавляем заранее через ssh-keyscan или раздаём known_hosts централизованно.
Логи приходят с опозданием. print пишет в stdout, а без терминала он буферизуется. В журнале строки появляются пачкой в конце или не появляются, если процесс убит. Используйте logging в stderr или PYTHONUNBUFFERED=1 в unit-файле.
Запросы без timeout. requests по умолчанию ждёт ответа бесконечно. Один зависший API, и таймер больше не стартует, потому что прошлый запуск ещё идёт. Timeout указываю в каждом вызове.
Snap-пакеты в алертах. На Ubuntu df показывает squashfs-разделы snap, и они всегда заполнены на 100%. Без -x squashfs алерт висит вечно. С overlay от Docker похожая история: дубли одного и того же диска.
Относительные пути. Скрипт ищет config.toml рядом с собой, а под systemd рабочий каталог — /. Все пути либо абсолютные, либо от Path(__file__).parent.
Практические рекомендации
Обновление и обслуживание
Venv привязан к конкретной версии интерпретатора. Обновили Debian 12 до 13 — Python сменился с 3.11 на 3.13, и старые venv сломались. Лечится пересозданием:
rm -rf /opt/diskwatch/venv
python3 -m venv /opt/diskwatch/venv
/opt/diskwatch/venv/bin/pip install -r /opt/diskwatch/requirements.txt
Поэтому requirements.txt обязателен, даже если зависимость одна.
Библиотеки обновляю осознанно, а не по расписанию. pip list --outdated показывает, что устарело. Обновляю на тестовом сервере, прогоняю тесты и --dry-run, потом раскатываю.
Ещё несколько привычек:
- Один скрипт — одна задача. Проверка дисков не отправляет заодно бэкапы.
ruff checkперед коммитом. Ловит неиспользуемые импорты и кучу опечаток.- Type hints в сигнатурах. Через полгода они объясняют код лучше комментариев.
- Для сложных сценариев по SSH берите paramiko или fabric. Для простого
dfхватает системного ssh с его конфигом и ключами. - Если скрипт укладывается в 20 строк без логики, оставьте его на Bash. Python ради Python не нужен.
Первый алерт этот скрипт прислал через час после запуска. Это был snap на 100%, -x squashfs я тогда ещё не добавил. Ну для мониторинга конечно лучше использовать Zabbix, тут больше про то как python может помочь в системном администрировании на небольшом примере — дальше всё зависит только от вашей фантазии и конкретных задач.