Механизм поддержания описаний в актуальном состоянии
Сверка fr1 показала, что дока расходится с фактом по десятку пунктов —
значит нужен не разовый проход, а контракт и инструмент.
CLAUDE.md — правило «тронул сервер, обнови servers/<хост>.md в том же
прогоне» плюс стиль: факт + причина + что сломается молча. Лежит именно
в CLAUDE.md, потому что AGENTS.md автоматически агенту не подгружается.
.claude/skills/server-audit/ — метод сверки и накопленные грабли: где
дока врёт чаще всего, почему ufw не отвечает на вопрос об открытых
портах, почему нужен up -d --dry-run, почему «executed successfully»
не доказывает работу задания.
scripts/snapshot.sh — инвентаризация живого состояния за один проход:
только чтение, без sudo, секретов не печатает, недоступное под root
помечает и подсказывает команду.
Первый же прогон скрипта нашёл две вещи, обе описаны:
- IPv6 не «починился», а мигает: за один день FAILED -> REACHABLE ->
FAILED без единой правки конфига. Описан как мигающий, с выводом
ничего на него не завязывать.
- В основном sshd_config стоит PermitRootLogin yes (правка шаблона
Contabo). Root-вход закрыт только потому, что Include дроп-инов
стоит выше, а sshd берёт первое значение. Уберёшь дроп-ин —
включится.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4308c5c7e1
commit
b56c137b5c
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: server-audit
|
||||
description: Сверить фактическое состояние сервера с его описанием в servers/*.md и обновить описание. Использовать после любых работ на сервере (поднял/погасил сервис, поменял конфиг, удалил каталог, завёл ключ), а также когда просят проверить, соответствует ли дока реальности, актуализировать описание хоста или разобраться, что на сервере вообще происходит.
|
||||
---
|
||||
|
||||
# Сверка описания сервера с фактом
|
||||
|
||||
Цель — чтобы `servers/<хост>.md` описывал сервер таким, какой он есть, и
|
||||
чтобы по нему можно было принимать решения. Правила оформления — в
|
||||
`CLAUDE.md` репозитория, здесь метод и грабли.
|
||||
|
||||
## Порядок
|
||||
|
||||
1. **Собери факт до чтения доки.** Сначала `./scripts/snapshot.sh <хост>`,
|
||||
потом читай описание. Наоборот — начнёшь подгонять наблюдения под текст.
|
||||
2. **Сверяй по пунктам, не по впечатлению.** Прогоняй утверждения доки
|
||||
через факт по одному: версии, размеры, пути, порты, «включено/выключено».
|
||||
При сверке fr1 2026-09-08 неверными оказались десять утверждений, включая
|
||||
«IPv6 не работает» про работавший IPv6 и версию Gitea на два минора назад.
|
||||
3. **Добери то, что требует root** — snapshot это помечает. Неинтерактивный
|
||||
sudo не проходит: нужен `ssh -t <хост> 'sudo ...'`, то есть попроси
|
||||
пользователя выполнить, а не пытайся обойти.
|
||||
4. **Правь описание в том же прогоне.** Не откладывай на «потом опишу».
|
||||
5. **Незакрытое — в «Открытые хвосты»** файла хоста. Токен, который не
|
||||
удалось отозвать; права, которые не поправил; решение, которое за
|
||||
пользователя не принимаешь.
|
||||
|
||||
## Где дока врёт чаще всего
|
||||
|
||||
- **Версии и размеры** — меняются молча при обновлении контейнера. Смотри
|
||||
фактический тег у запущенного контейнера, а не запись в compose: они
|
||||
расходятся, и тогда `docker compose up -d` откатывает сервис.
|
||||
- **«Не работает»** — могло починиться на стороне провайдера. И наоборот:
|
||||
IPv6 на fr1 за один день был `FAILED`, потом `REACHABLE`, потом снова
|
||||
`FAILED` — такое надо описывать как мигающее, а не как состояние.
|
||||
- **Открытые порты.** `ufw status` не отвечает на вопрос «что торчит
|
||||
наружу»: docker публикует порты в iptables напрямую, минуя ufw. Только
|
||||
`ss -tlnH` + `docker ps`. Непубликуемый порт контейнера всё равно
|
||||
доступен с хоста по адресу контейнера — «внутренняя сеть» не периметр.
|
||||
- **Процессы вне контейнеров.** `docker ps` их не покажет, а они не
|
||||
переживут перезагрузку. Ищи `ps` с `PPID=1` и чужими пользователями.
|
||||
- **Кроны других пользователей** — в `docker ps` и в `crontab -l` их нет.
|
||||
Осиротевшее задание может годами падать молча: MTA не установлен, вывод
|
||||
крона теряется.
|
||||
- **Планы, записанные как факт.** «Планируется перенести DNS» при уже
|
||||
перенесённом DNS. Проверяй раздел «планируется» отдельно.
|
||||
|
||||
## Грабли, которые стоили времени (не повторяй)
|
||||
|
||||
- **Пути в БД абсолютные.** Прежде чем контейнеризовать сервис, проверь
|
||||
**все** префиксы путей в его базе, а не только основной каталог данных:
|
||||
`select substr(file_path,1,30), count(*) from images group by 1;`
|
||||
Монтируй в тот же путь, что на хосте, иначе часть данных пропадёт молча.
|
||||
- **`up -d --dry-run` перед любым compose-действием.** Он показывает, что
|
||||
compose собирается сделать, — например поднять забытый контейнер того же
|
||||
проекта и словить конфликт портов.
|
||||
- **Задание может глушить свои ошибки.** У imgbot `analyze` логирует
|
||||
исключение и возвращается нормально, поэтому планировщик пишет
|
||||
`executed successfully` после падения. Отсутствие ошибок не доказывает
|
||||
работу — проверяй результат, а не статус.
|
||||
- **Ретенция.** journald был на 500M (11 дней), sysstat на `HISTORY=7`.
|
||||
Сервер, где разбирают аварии, обязан хранить историю дольше, чем живёт
|
||||
проблема. У sysstat при `HISTORY>28` имена переключаются на
|
||||
`saYYYYMMDD` — иначе поднятие числа бессмысленно.
|
||||
- **Группа `docker` = root.** Пользователь без sudo, но в группе docker,
|
||||
монтирует `/` в контейнер и делает что угодно. Учитывай при описании
|
||||
доступа.
|
||||
|
||||
## Проверка результата
|
||||
|
||||
Перед отчётом прогони по получившемуся файлу: каждое ⚠️ подтверждено
|
||||
наблюдением? у каждого изменения указана дата и прежнее значение? не
|
||||
попали ли в текст секреты? если роль хоста изменилась — поправлена ли
|
||||
строка в `AGENTS.md`?
|
||||
@@ -7,4 +7,15 @@
|
||||
- [`servers/fr1.md`](servers/fr1.md) — `fr1.ruzzy.dev` (161.97.93.252). Основная нода (Ubuntu 24.04): мониторинг, gitea, AmneziaWG exit для ru1, боты (claude-channels, openclaw), статика `www`/`share`. Держит апекс `ruzzy.dev`.
|
||||
- [`servers/ru1.md`](servers/ru1.md) — `85.198.109.86` (VDSina, РФ). VPN entry-нода (OpenVPN ×2, AmneziaWG **до fr1** после переключения, WireGuard клиентов, telemt), умный роутинг RU/зарубеж. ⚠️ Файл описания сверялся до переключения.
|
||||
|
||||
## Поддержание описаний в актуальном состоянии
|
||||
Тронул сервер — обнови `servers/<хост>.md` в том же прогоне. Контракт и
|
||||
правила оформления — в [`CLAUDE.md`](CLAUDE.md), метод сверки и накопленные
|
||||
грабли — в скилле [`server-audit`](.claude/skills/server-audit/SKILL.md),
|
||||
инвентаризация живого состояния одной командой:
|
||||
|
||||
./scripts/snapshot.sh rus-161
|
||||
|
||||
Скрипт только читает, sudo не требует и секретов не печатает; то, что
|
||||
доступно лишь под root, он помечает и подсказывает команду.
|
||||
|
||||
Секреты (пароли, токены) в репозитории не хранятся — только на серверах.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Работа с этим репозиторием
|
||||
|
||||
Здесь описано состояние серверов: один файл на хост в `servers/`, стеки
|
||||
docker-compose в `stacks/`, индекс в `AGENTS.md`.
|
||||
|
||||
## Главное правило
|
||||
|
||||
**Тронул сервер — обнови `servers/<хост>.md` в том же прогоне, до отчёта
|
||||
пользователю.** Не «потом», не «если попросят». Расхождение описания с
|
||||
фактом опаснее отсутствия описания: по доке принимают решения, и неверная
|
||||
строка стоит дороже пустой.
|
||||
|
||||
Это относится к любому изменению состояния: поднял или погасил сервис,
|
||||
поменял конфиг, удалил каталог, завёл ключ, отключил юнит. Если менял
|
||||
что-то в `stacks/`, проверь, что описание сервиса в `servers/` ему не
|
||||
противоречит.
|
||||
|
||||
## Перед тем как что-то описывать — проверь
|
||||
|
||||
Доке не верь, сверяй с живым сервером. При последней сверке (2026-09-08)
|
||||
десяток утверждений в `servers/fr1.md` оказались неверными: версии,
|
||||
размеры, «не работает» про то, что уже работало.
|
||||
|
||||
Подробный чеклист и метод — в скилле `server-audit`
|
||||
(`.claude/skills/server-audit/`), инвентаризация одной командой —
|
||||
`scripts/snapshot.sh <ssh-хост>`.
|
||||
|
||||
## Как писать
|
||||
|
||||
Стиль этого репо — не список фактов, а факт плюс причина и грабли:
|
||||
|
||||
- **Почему так, а не иначе.** «`node-exporter` в host-network» бесполезно
|
||||
без «иначе видел бы только сеть docker-бриджа».
|
||||
- **Что сломается молча.** Помечай `⚠️`. Такие места — главная ценность
|
||||
файла: `ufw` не управляет docker-портами; путь монтирования обязан
|
||||
совпадать с абсолютными путями в БД; `HISTORY=7` у sysstat хранит неделю.
|
||||
- **Дату и прежнее значение** при изменении: «поднято до 365 (было 7)».
|
||||
- **Незакрытое — в «Открытые хвосты»**, а не в никуда. Если оставил
|
||||
токен, задачу или сомнительные права — запиши, иначе это потеряется.
|
||||
- **Секретов здесь нет.** Токены, пароли, ключи — только на сервере.
|
||||
Ссылаться на путь к секрету можно, содержимое — нельзя.
|
||||
- Роль хоста поменялась — поправь и строку в `AGENTS.md`.
|
||||
|
||||
## Чего не делать
|
||||
|
||||
- Не описывай планы как факт. «Планируется» и «сделано» — разные разделы.
|
||||
- Не удаляй ⚠️-заметку, потому что «выглядит устаревшей». Сначала проверь
|
||||
на сервере, потом переписывай, указав дату проверки.
|
||||
- Не коммить в `main` напрямую, если пользователь не просил.
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Инвентаризация живого состояния сервера — только чтение, sudo не требуется.
|
||||
#
|
||||
# Смысл: сверка описания с фактом стоит дорого не из-за писанины, а из-за
|
||||
# выяснения, что на сервере вообще происходит. Этот скрипт собирает всё, что
|
||||
# понадобилось при сверке fr1 2026-09-08, за один проход.
|
||||
#
|
||||
# ./scripts/snapshot.sh rus-161 # по ssh
|
||||
# ./scripts/snapshot.sh rus-161 > /tmp/fr1.txt
|
||||
#
|
||||
# Секретов не печатает: содержимое .env, токенов и ключей не читается —
|
||||
# только факт наличия файла и права.
|
||||
#
|
||||
# Часть данных требует root и будет помечена как недоступная: правила ufw,
|
||||
# сертификаты, /var/log/syslog, кроны других пользователей. Их надо
|
||||
# добирать вручную через `ssh -t <хост> sudo ...`.
|
||||
set -uo pipefail
|
||||
|
||||
HOST="${1:-}"
|
||||
[ -z "$HOST" ] && { echo "usage: $0 <ssh-host>" >&2; exit 2; }
|
||||
|
||||
ssh -o BatchMode=yes "$HOST" 'bash -s' <<'REMOTE'
|
||||
set +e
|
||||
h() { printf "\n\n===== %s =====\n" "$1"; }
|
||||
|
||||
h "СИСТЕМА"
|
||||
uname -srm; . /etc/os-release && echo "$PRETTY_NAME"
|
||||
echo "hostname: $(hostname) / $(hostname -f 2>/dev/null)"
|
||||
uptime; who -b
|
||||
echo "vCPU: $(nproc)"; free -h | head -2; df -h / | tail -1
|
||||
ls /var/run/reboot-required 2>/dev/null && echo "ТРЕБУЕТСЯ ПЕРЕЗАГРУЗКА"
|
||||
|
||||
h "ПОЛЬЗОВАТЕЛИ И ДОСТУП"
|
||||
getent passwd | awk -F: '$3>=1000 && $3<65000 {print $1" uid="$3" shell="$7}'
|
||||
echo "группа docker (= root-эквивалент): $(getent group docker)"
|
||||
echo "группа sudo: $(getent group sudo)"
|
||||
grep -hE "^(PermitRootLogin|PasswordAuthentication|AllowUsers|MaxAuthTries)" \
|
||||
/etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf 2>/dev/null | sort -u
|
||||
|
||||
h "СЕТЬ"
|
||||
ip -br addr | grep -vE "^(veth|br-|docker)"
|
||||
ip route | head -5
|
||||
ip -6 route | grep default
|
||||
echo "-- NDP шлюза (FAILED = IPv6 не работает) --"; ip -6 neigh | grep -E "fe80::1 |router"
|
||||
resolvectl status 2>/dev/null | grep -A1 "Current DNS"
|
||||
|
||||
h "ЧТО СЛУШАЕТ НАРУЖУ (а не ufw status!)"
|
||||
# ufw НЕ управляет docker-портами: docker пишет их в iptables напрямую,
|
||||
# поэтому единственный честный ответ даёт ss + docker ps.
|
||||
ss -tlnH | awk '{print $4}' | sort -u
|
||||
echo "-- udp --"; ss -ulnH | awk '{print $4}' | sort -u
|
||||
|
||||
h "DOCKER"
|
||||
docker --version 2>/dev/null; docker compose version 2>/dev/null
|
||||
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null
|
||||
echo; docker system df 2>/dev/null
|
||||
echo "-- откуда запущен каждый контейнер (compose-проект и файл) --"
|
||||
for c in $(docker ps -aq 2>/dev/null); do
|
||||
docker inspect "$c" --format '{{.Name}} <- {{index .Config.Labels "com.docker.compose.project.config_files"}}'
|
||||
done
|
||||
echo "-- монтирования (host -> container) --"
|
||||
for c in $(docker ps -aq 2>/dev/null); do
|
||||
docker inspect "$c" --format '{{.Name}}:{{range .Mounts}} {{.Source}}->{{.Destination}}{{end}}'
|
||||
done
|
||||
echo "-- тома и их размер --"
|
||||
for v in $(docker volume ls -q 2>/dev/null); do
|
||||
printf "%s %s\n" "$(docker run --rm -v "$v":/v alpine du -sh /v 2>/dev/null | cut -f1)" "$v"
|
||||
done
|
||||
echo "-- сироты (dangling) --"; docker volume ls -qf dangling=true 2>/dev/null
|
||||
|
||||
h "ПРОЦЕССЫ ВНЕ КОНТЕЙНЕРОВ И ВНЕ SYSTEMD"
|
||||
# Такие процессы не переживут перезагрузку и не видны cAdvisor.
|
||||
ps -eo user:12,pid,ppid,etime:12,%cpu,rss,args --sort=-rss \
|
||||
| grep -vE "\[|containerd-shim|/usr/lib/systemd|^USER" | head -25
|
||||
|
||||
h "РАСПИСАНИЯ"
|
||||
echo "-- /etc/cron.d --"; for f in /etc/cron.d/*; do [ -f "$f" ] && { echo "# $f"; grep -vE '^#|^$|^SHELL|^PATH' "$f"; }; done
|
||||
for d in hourly daily weekly monthly; do echo "-- cron.$d: $(ls /etc/cron.$d 2>/dev/null | tr '\n' ' ')"; done
|
||||
echo "-- crontab текущего пользователя --"; crontab -l 2>/dev/null | grep -vE '^#|^$'
|
||||
echo "-- кроны остальных пользователей: нужен root --"
|
||||
sudo -n ls /var/spool/cron/crontabs 2>/dev/null || echo " (недоступно: ssh -t <хост> 'sudo ls /var/spool/cron/crontabs')"
|
||||
echo "-- таймеры --"; systemctl list-timers --all --no-pager 2>/dev/null | head -20
|
||||
|
||||
h "NGINX / TLS"
|
||||
nginx -v 2>&1; ls -l /etc/nginx/sites-enabled/ 2>/dev/null
|
||||
grep -hE "server_name|proxy_pass|root |listen " /etc/nginx/sites-available/*.conf 2>/dev/null | sed 's/^ *//' | sort -u
|
||||
echo "-- сертификаты: нужен root --"
|
||||
sudo -n ls /etc/letsencrypt/live 2>/dev/null || echo " (недоступно)"
|
||||
|
||||
h "ЛОГИ И ИСТОРИЯ (для будущих разборов)"
|
||||
cat /etc/systemd/journald.conf.d/*.conf 2>/dev/null | grep -vE '^#|^$'
|
||||
journalctl --disk-usage 2>/dev/null | tail -1
|
||||
echo "-- sysstat: глубина истории --"
|
||||
grep -E '^HISTORY|^COMPRESSAFTER' /etc/sysstat/sysstat 2>/dev/null
|
||||
ls /var/log/sysstat 2>/dev/null | head -3
|
||||
echo " (файлы saDD = максимум месяц; saYYYYMMDD = HISTORY>28)"
|
||||
|
||||
h "ОБНОВЛЕНИЯ"
|
||||
pro status --format json 2>/dev/null | python3 -c 'import json,sys; d=json.load(sys.stdin); print("Ubuntu Pro:", d["account"]["name"]); [print(" ",s["name"],s["status"]) for s in d["services"] if s["status"]=="enabled"]' 2>/dev/null
|
||||
grep -hE "Update-Package-Lists|Unattended-Upgrade " /etc/apt/apt.conf.d/20auto-upgrades 2>/dev/null
|
||||
snap list 2>/dev/null | tail -n +2 | awk '{print " snap:",$1,$2}'
|
||||
snap refresh --time 2>/dev/null | head -2
|
||||
|
||||
h "ЧЕГО ЗДЕСЬ НЕТ — ДОБРАТЬ ВРУЧНУЮ ПОД ROOT"
|
||||
cat <<'HINT'
|
||||
ssh -t <хост> "sudo bash -c '
|
||||
ufw status verbose
|
||||
certbot certificates | grep -E \"Certificate Name|Expiry\"
|
||||
ls /var/spool/cron/crontabs && for f in /var/spool/cron/crontabs/*; do echo \$f; grep -v ^# \$f; done
|
||||
awg show 2>/dev/null | head -20
|
||||
grep -c suspended /var/log/syslog
|
||||
'"
|
||||
HINT
|
||||
REMOTE
|
||||
+4
-1
@@ -17,6 +17,8 @@
|
||||
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
|
||||
|
||||
## SSH (`/etc/ssh/sshd_config.d/00-hardening.conf`)
|
||||
⚠️ В основном `/etc/ssh/sshd_config` строкой 42 стоит `PermitRootLogin yes` (правка шаблона Contabo). Побеждает всё равно дроп-ин: `Include /etc/ssh/sshd_config.d/*.conf` стоит строкой 12, а sshd берёт **первое** встреченное значение. То есть root-вход закрыт, но держится на порядке строк — уберёшь дроп-ин, и root-вход по ключу включится. ⏳ Рядом лежит `50-cloud-init.conf`, который из-под `rus` не читается: под root стоит проверить `sshd -T | grep -i permitrootlogin`.
|
||||
|
||||
```
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
@@ -32,7 +34,7 @@ AllowUsers rus tgclaude
|
||||
- Управляется **netplan → systemd-networkd**, конфиг `/etc/netplan/50-cloud-init.yaml` (наследие cloud-init, сейчас просто статический файл).
|
||||
- Матч интерфейса **по MAC `00:50:56:64:ff:15`** + `set-name: eth0`. Ядро называет карту `ens18`, netplan переименовывает её в **`eth0`** через сгенерированный `/run/systemd/network/10-netplan-eth0.link`.
|
||||
- IPv4 `161.97.93.252/18`, GW `161.97.64.1`. DNS `195.179.224.53` / `209.126.15.53`.
|
||||
- IPv6 `2a02:c207:2338:7709::1/64`, дефолт `via fe80::1 onlink`. Долго не работал — шлюз не отвечал на NDP (`fe80::1 FAILED`). **Починился на стороне Contabo** (обнаружено 2026-09-08): `fe80::1` в `ip -6 neigh` REACHABLE, наружу пингуется. Конфиг на сервере при этом не менялся — значит, может так же молча отвалиться обратно.
|
||||
- ⚠️ IPv6 `2a02:c207:2338:7709::1/64`, дефолт `via fe80::1 onlink` — **мигает, а не «работает» или «не работает»**. Хронология одного дня, 2026-09-08: утром `fe80::1` в `ip -6 neigh` был `REACHABLE` и `ping6` наружу шёл без потерь (до этого месяцами `FAILED`); к 15:50 снова `FAILED`, `ping6` — 100% потерь. Конфиг на сервере не менялся ни разу, то есть причина целиком на стороне Contabo. Практический вывод: **не завязывать ничего на IPv6 этого хоста** и не считать очередное «заработало» починкой. IPv4 при этом не страдает.
|
||||
- ⚠️ **На имя `eth0` завязано** NAT-правило AmneziaWG (`-o eth0 MASQUERADE` в `wg0.conf`) и правила ufw. Если netplan не отработает, интерфейс останется `ens18` и VPN-выход молча сломается.
|
||||
- **Страховка `net-rescue`** (заведена 2026-08-01): `/usr/local/sbin/net-rescue.sh` + `net-rescue.timer` (`OnBootSec=150`, enabled). Идемпотентно находит карту по MAC — независимо от её имени — и доставляет адрес, дефолтный маршрут и DNS, если их нет. Пишет в journal тегом `net-rescue`. Смысл: пережить повторение аварии 2026-07-31 без похода в VNC.
|
||||
- **Третий механизм правки маршрута — `/etc/cron.d/staticroute`** (из образа Contabo, не наш): `@reboot root ip route replace $(link-scope сеть eth0) via $(дефолтный GW) dev eth0`. Аргументы вычисляет на ходу из текущей таблицы, поэтому если адреса на eth0 нет вообще — тихо ничего не делает. Дублирует задачу `net-rescue`, но вслепую и без привязки к MAC. Знать о нём надо: при разборе «кто поправил маршрут» это третий кандидат после netplan и net-rescue.
|
||||
@@ -236,6 +238,7 @@ backend=systemd banaction=ufw
|
||||
- `~tgclaude/.openclaw-auth-profile-secrets` — права **777** на каталоге для секретов (сейчас пуст), стоит `chmod 700`.
|
||||
- Не решено, нужны ли на fr1: `~rus/backups` (5.4 ГБ архивов с выведенного de1) и спящий openclaw (947 МБ дерева + 2.0 ГБ состояния + образ-снимок 1.55 ГБ).
|
||||
- Страховочные файлы, оставленные после работ: `~rus/imgbot-repo-pre-merge-20260908-133444.tar.gz` (369 КБ), `~rus/imgbot-legacy-archive-rows-20260908.json` (808 КБ), `~rus/gitea/docker-compose.yml.bak-20260908`.
|
||||
- ⏳ Дроп-ин `/etc/ssh/sshd_config.d/50-cloud-init.conf` не читается из-под `rus` — под root проверить `sshd -T | grep -iE "permitrootlogin|passwordauth"`, чтобы знать эффективные значения, а не собранные из файлов вручную.
|
||||
- ⚠️ Описания **других** серверов не сверялись: `servers/de1.md` и `servers/ru1.md` датируются временем до вывода de1, так что могут врать.
|
||||
|
||||
## Установленные пакеты (сверх базы)
|
||||
|
||||
Reference in New Issue
Block a user