В работе любой инфраструктуры есть вещи, которые редко вспоминают каждый день, но их отказ может привести к очень неприятным последствиям. Один из таких примеров — окончание срока действия доменного имени.
Если домен вовремя не продлить, могут перестать открываться сайты, API, личные кабинеты, почта, VPN-сервисы, внешние интеграции и другие важные сервисы. При этом проблема может выглядеть как авария сети, сбой DNS или недоступность сервера, хотя настоящая причина будет банальной — домен не был оплачен.
Чтобы не контролировать такие вещи вручную, можно добавить проверку доменов в Zabbix.
Что делает решение
Данное решение добавляет в Zabbix мониторинг сроков оплаты доменов.
Скрипт:
- читает список доменов из файла;
- автоматически добавляет их в Zabbix через Low-Level Discovery;
- выполняет WHOIS-запрос по каждому домену;
- ищет поле окончания срока действия домена;
- возвращает дату окончания оплаты;
- считает количество дней до окончания;
- показывает статус проверки;
- возвращает текст ошибки, если данные получить не удалось.
На основе этих данных в Zabbix можно настроить триггеры, например:
- домен истекает менее чем через 30 дней;
- домен истекает менее чем через 14 дней;
- домен уже просрочен;
- WHOIS-запрос не выполнился;
- не удалось найти дату окончания регистрации.
Где это может быть полезно
Такой мониторинг особенно полезен для инфраструктуры, где используется много доменов и поддоменов:
- сайты компании;
- API-шлюзы;
- личные кабинеты;
- почтовые домены;
- VPN-доступ;
- внутренние и внешние порталы;
- интеграции с клиентами и подрядчиками;
- сервисы мониторинга;
- домены для тестовых и продуктовых окружений.
Даже если домены продлеваются автоматически, мониторинг всё равно нужен. Автопродление может не сработать из-за проблем с оплатой, банковской картой, регистратором или изменениями в личном кабинете.
Структура решения
Для работы используется каталог:
/etc/zabbix/scripts/domain-payments/
В нём размещаются:
domain_payment.py
domains.txt
Где:
-
domain_payment.py— основной Python-скрипт проверки доменов; -
domains.txt— список доменов для мониторинга.
Также в конфигурацию Zabbix Agent добавляются UserParameter.
Установка
Создаём каталог для скрипта:
sudo mkdir -p /etc/zabbix/scripts/domain-payments
Создаём файл со списком доменов:
sudo nano /etc/zabbix/scripts/domain-payments/domains.txt
Пример содержимого:
example.ru
example.com
company.site
Можно добавлять комментарии:
example.ru # основной сайт
api.example.ru # API
Пустые строки и комментарии будут проигнорированы.
Установка скрипта
Создаём файл:
sudo nano /etc/zabbix/scripts/domain-payments/domain_payment.py
Содержимое скрипта:
#!/usr/bin/env python3
import argparse
import datetime as dt
import json
import re
import socket
import sys
from pathlib import Path
DOMAINS_FILE = Path('/etc/zabbix/scripts/domain-payments/domains.txt')
WHOIS_SERVERS = {
'ru': 'whois.tcinet.ru',
'рф': 'whois.tcinet.ru',
'su': 'whois.tcinet.ru',
}
DEFAULT_WHOIS_SERVER = 'whois.iana.org'
TIMEOUT = 10
DOMAIN_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9.-]*[A-Za-z0-9]$')
EXPIRY_RE_LIST = [
re.compile(r'^Registry Expiry Date:\s*(\S+)', re.IGNORECASE | re.MULTILINE),
re.compile(r'^Expiry Date:\s*(\S+)', re.IGNORECASE | re.MULTILINE),
re.compile(r'^Expiration Date:\s*(\S+)', re.IGNORECASE | re.MULTILINE),
re.compile(r'^paid-till:\s*(\S+)', re.IGNORECASE | re.MULTILINE),
]
def load_domains():
domains = []
seen = set()
if not DOMAINS_FILE.exists():
return domains
for raw in DOMAINS_FILE.read_text(encoding='utf-8').splitlines():
line = raw.split('#', 1)[0].strip().lower().rstrip('.')
if not line or line in seen:
continue
if not DOMAIN_RE.match(line):
continue
domains.append(line)
seen.add(line)
return domains
def whois_server(domain):
tld = domain.rsplit('.', 1)[-1].lower()
return WHOIS_SERVERS.get(tld, DEFAULT_WHOIS_SERVER)
def query_whois(domain):
server = whois_server(domain)
with socket.create_connection((server, 43), TIMEOUT) as sock:
sock.settimeout(TIMEOUT)
sock.sendall((domain + '\r\n').encode('utf-8'))
chunks = []
while True:
try:
chunk = sock.recv(4096)
except socket.timeout:
break
if not chunk:
break
chunks.append(chunk)
return b''.join(chunks).decode('utf-8', 'replace')
def parse_datetime(value):
value = value.strip()
candidates = [value]
if value.endswith('Z'):
candidates.append(value[:-1] + '+00:00')
for candidate in candidates:
try:
parsed = dt.datetime.fromisoformat(candidate)
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=dt.timezone.utc)
return parsed.astimezone(dt.timezone.utc)
except ValueError:
pass
for fmt in ('%Y-%m-%d', '%Y.%m.%d', '%d.%m.%Y'):
try:
parsed_date = dt.datetime.strptime(value, fmt).date()
return dt.datetime.combine(parsed_date, dt.time.min, tzinfo=dt.timezone.utc)
except ValueError:
pass
raise ValueError('unsupported expiry date format: ' + value)
def paid_till(domain):
text = query_whois(domain)
for regex in EXPIRY_RE_LIST:
match = regex.search(text)
if match:
expires_at = parse_datetime(match.group(1))
now = dt.datetime.now(dt.timezone.utc)
days = int((expires_at - now).total_seconds() // 86400)
return {
'domain': domain,
'ok': 1,
'paid_till': expires_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'days': days,
'error': '',
}
raise RuntimeError('expiry/paid-till field not found')
def result(domain):
try:
return paid_till(domain)
except Exception as exc:
return {
'domain': domain,
'ok': 0,
'paid_till': '',
'days': -1,
'error': str(exc),
}
def discovery():
return {'data': [{'{#DOMAIN}': domain} for domain in load_domains()]}
def main():
parser = argparse.ArgumentParser()
parser.add_argument('mode', choices=['discovery', 'days', 'paid_till', 'ok', 'error', 'json'])
parser.add_argument('domain', nargs='?')
args = parser.parse_args()
if args.mode == 'discovery':
print(json.dumps(discovery(), ensure_ascii=False, separators=(',', ':')))
return 0
if not args.domain:
print('domain argument is required', file=sys.stderr)
return 2
domain = args.domain.strip().lower().rstrip('.')
data = result(domain)
if args.mode == 'json':
print(json.dumps(data, ensure_ascii=False, separators=(',', ':')))
elif args.mode == 'days':
print(data['days'])
elif args.mode == 'paid_till':
print(data['paid_till'])
elif args.mode == 'ok':
print(data['ok'])
elif args.mode == 'error':
print(data['error'])
return 0
if __name__ == '__main__':
raise SystemExit(main())
Делаем скрипт исполняемым:
sudo chmod +x /etc/zabbix/scripts/domain-payments/domain_payment.py
Выставляем права:
sudo chown -R zabbix:zabbix /etc/zabbix/scripts/domain-payments
Проверка вручную
Проверяем discovery:
sudo -u zabbix /etc/zabbix/scripts/domain-payments/domain_payment.py discovery
Пример ответа:
{"data":[{"{#DOMAIN}":"example.ru"},{"{#DOMAIN}":"example.com"}]}
Проверяем количество дней до окончания домена:
sudo -u zabbix /etc/zabbix/scripts/domain-payments/domain_payment.py days example.ru
Проверяем дату окончания:
sudo -u zabbix /etc/zabbix/scripts/domain-payments/domain_payment.py paid_till example.ru
Проверяем полный JSON:
sudo -u zabbix /etc/zabbix/scripts/domain-payments/domain_payment.py json example.ru
Пример ответа:
{"domain":"example.ru","ok":1,"paid_till":"2026-08-15T00:00:00Z","days":101,"error":""}
Настройка Zabbix Agent
Создаём отдельный конфигурационный файл:
sudo nano /etc/zabbix/zabbix_agentd.d/domain_payment.conf
Добавляем UserParameter:
UserParameter=domain.payment.discovery,/etc/zabbix/scripts/domain-payments/domain_payment.py discovery
UserParameter=domain.payment.days[*],/etc/zabbix/scripts/domain-payments/domain_payment.py days "$1"
UserParameter=domain.payment.paid_till[*],/etc/zabbix/scripts/domain-payments/domain_payment.py paid_till "$1"
UserParameter=domain.payment.ok[*],/etc/zabbix/scripts/domain-payments/domain_payment.py ok "$1"
UserParameter=domain.payment.error[*],/etc/zabbix/scripts/domain-payments/domain_payment.py error "$1"
UserParameter=domain.payment.json[*],/etc/zabbix/scripts/domain-payments/domain_payment.py json "$1"
Перезапускаем агент:
sudo systemctl restart zabbix-agent
Или для Zabbix Agent 2:
sudo systemctl restart zabbix-agent2
Проверяем, что агент отдаёт данные:
zabbix_get -s 127.0.0.1 -k domain.payment.discovery
Проверка конкретного домена:
zabbix_get -s 127.0.0.1 -k 'domain.payment.days[example.ru]'
Какие ключи используются
| Ключ | Назначение |
|---|---|
domain.payment.discovery | Автоматическое обнаружение доменов из файла domains.txt |
domain.payment.days[domain] | Количество дней до окончания оплаты домена |
domain.payment.paid_till[domain] | Дата окончания оплаты домена |
domain.payment.ok[domain] | Статус проверки: 1 — успешно, 0 — ошибка |
domain.payment.error[domain] | Текст ошибки при неудачной проверке |
domain.payment.json[domain] | Полный JSON с результатом проверки |
Настройка шаблона в Zabbix
В Zabbix необходимо создать шаблон, например:
Template Domain Payment Monitoring
В шаблоне создаётся LLD-правило:
domain.payment.discovery
Макрос обнаружения:
{#DOMAIN}
Далее создаются item prototypes.
Количество дней до окончания
Name: Domain {#DOMAIN}: days before expiration
Key: domain.payment.days[{#DOMAIN}]
Type: Zabbix agent
Type of information: Numeric (signed)
Дата окончания оплаты
Name: Domain {#DOMAIN}: paid till
Key: domain.payment.paid_till[{#DOMAIN}]
Type: Zabbix agent
Type of information: Text
Статус проверки
Name: Domain {#DOMAIN}: check status
Key: domain.payment.ok[{#DOMAIN}]
Type: Zabbix agent
Type of information: Numeric (unsigned)
Ошибка проверки
Name: Domain {#DOMAIN}: error
Key: domain.payment.error[{#DOMAIN}]
Type: Zabbix agent
Type of information: Text
Примеры триггеров
Домен истекает менее чем через 30 дней
last(/Template Domain Payment Monitoring/domain.payment.days[{#DOMAIN}])<30
Домен истекает менее чем через 14 дней
last(/Template Domain Payment Monitoring/domain.payment.days[{#DOMAIN}])<14
Домен уже просрочен
last(/Template Domain Payment Monitoring/domain.payment.days[{#DOMAIN}])<0
Ошибка проверки WHOIS
last(/Template Domain Payment Monitoring/domain.payment.ok[{#DOMAIN}])=0
Рекомендуемая логика оповещений
Для удобства можно разделить уровни важности:
| Условие | Уровень |
|---|---|
| Меньше 30 дней | Warning |
| Меньше 14 дней | Average |
| Меньше 7 дней | High |
| Домен просрочен | Disaster |
| WHOIS не отвечает или дата не найдена | Average |
Это позволит заранее получать уведомления и не доводить ситуацию до фактической недоступности сервиса.
Особенности работы
Для доменов .ru, .рф и .su используется WHOIS-сервер:
whois.tcinet.ru
Для остальных доменов используется:
whois.iana.org
Скрипт ищет дату окончания по нескольким возможным полям:
Registry Expiry Date
Expiry Date
Expiration Date
paid-till
Это позволяет использовать его не только для российских доменных зон, но и для части международных доменов.
Возможные проблемы
Не найдено поле окончания срока действия
Ошибка:
expiry/paid-till field not found
Это значит, что WHOIS-ответ по домену не содержит ожидаемого поля с датой окончания.
Причины могут быть разные:
- регистратор скрывает часть WHOIS-данных;
- доменная зона использует нестандартный формат ответа;
- WHOIS-запрос ушёл не на тот сервер;
- домен не существует;
- WHOIS-сервис временно недоступен.
Ошибка соединения
Если WHOIS-сервер недоступен, в Zabbix будет возвращён статус:
ok = 0
А в поле error будет записана причина ошибки.
Домен не появляется в Zabbix
Нужно проверить:
cat /etc/zabbix/scripts/domain-payments/domains.txt
Затем выполнить:
sudo -u zabbix /etc/zabbix/scripts/domain-payments/domain_payment.py discovery
Если домена нет в discovery, значит он не проходит проверку регулярным выражением или был некорректно указан в файле.
Зачем это нужно
Контроль оплаты доменов — это простая, но важная часть инфраструктурного мониторинга.
Такой мониторинг позволяет:
- заранее видеть домены, которые скоро истекают;
- получать уведомления до наступления проблемы;
- исключить ручную проверку доменов;
- снизить риск простоя сайтов и сервисов;
- быстро понимать, что проблема связана именно с доменом;
- централизованно контролировать все доменные имена в Zabbix.
В результате Zabbix становится не только системой мониторинга серверов, сетевого оборудования и сервисов, но и инструментом контроля важных инфраструктурных зависимостей.
Комментариев пока нет.