Проверка доступности сервисов с автоперезапуском
Иногда нужен не Zabbix, а простейший контроль жив или нет. Для HTTP-сервисов это легко решается связкой
▪️ Базовая проверка доступности
-s - тихо
-f - ошибка, если HTTP ≠ 2xx
exit-код ≠ 0 - сервис недоступен
▪️ Автоперезапуск сервиса
▪️ Защита от флаппинга (несколько неудач подряд)
Рестарт только если сервис падает несколько раз подряд.
▪️ Запуск по cron
BashTex📱 #bash #utils
Иногда нужен не Zabbix, а простейший контроль жив или нет. Для HTTP-сервисов это легко решается связкой
curl + systemctl.
curl -sf http://127.0.0.1:8080/health || echo "DOWN"
-s - тихо
-f - ошибка, если HTTP ≠ 2xx
exit-код ≠ 0 - сервис недоступен
SERVICE=myapp
URL=http://127.0.0.1:8080/health
if ! curl -sf --max-time 3 "$URL"; then
logger "watchdog: $SERVICE is down, restarting"
systemctl restart "$SERVICE"
fi
--max-time защищает от зависших запросовlogger пишет в syslog (удобно для аудита)
FAILS=/run/myapp.watchdog.fail
MAX=3
if ! curl -sf "$URL"; then
n=$(($(cat "$FAILS" 2>/dev/null || echo 0)+1))
echo "$n" > "$FAILS"
[[ $n -ge $MAX ]] && systemctl restart "$SERVICE"
else
rm -f "$FAILS"
fi
Рестарт только если сервис падает несколько раз подряд.
*/1 * * * * /usr/local/bin/watchdog.sh
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Быстрый сбор системной информации одной командой
Когда нужно быстро понять, что за система перед тобой, без Ansible и inventory - достаточно одного короткого скрипта.
🛠 Мини-инвентаризация за один запуск
▪️ Компактный вариант в одну строку
Удобно кидать в чат или запускать на десятках хостов по SSH.
▪️ Формат для логов/отчетов
Можно сохранять как снапшот состояния перед деплоем или обновлением.
BashTex📱 #bash
Когда нужно быстро понять, что за система перед тобой, без Ansible и inventory - достаточно одного короткого скрипта.
#!/usr/bin/env bash
echo "=== SYSTEM ==="
uname -a
echo -e "\n=== CPU / MEM ==="
free -h
echo -e "\n=== DISK ==="
df -hT | grep -v tmpfs
echo -e "\n=== NETWORK ==="
ip -br addr
Подходит для:
первичного осмотра сервера;
SSH-подключений к незнакомой машине;
логирования состояния перед изменениями.
uname -srmo; free -h; df -hT | awk 'NR==1||!/tmpfs/'; ip -br a
Удобно кидать в чат или запускать на десятках хостов по SSH.
{
uname -srmo
free -h
df -hT
ip -br a
} > sysinfo.txt
Можно сохранять как снапшот состояния перед деплоем или обновлением.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Проверка подвисших NFS-монтов и их исправление
1️⃣ Находим NFS-монты
И проверяем, жив ли маунт:
2️⃣ Проверяем доступность сервера
Если команда висит или падает - это означает, что сервер недоступен, маунт мертв.
3️⃣ Аккуратное отключение (без убийства процессов)
4️⃣ Автовосстановление
Или перезапуск через fstab:
Или мини-watchdog
BashTex📱 #bash #utils
mount | grep nfs
И проверяем, жив ли маунт:
mountpoint -q /mnt/nfs && echo OK || echo BROKEN
showmount -e nfs-server
Если команда висит или падает - это означает, что сервер недоступен, маунт мертв.
umount -l /mnt/nfs
-l (lazy umount) сразу убирает маунт из системы, а реальные дескрипторы закроются, когда процессы отпустят их.
mount /mnt/nfs && echo "NFS restored"
Или перезапуск через fstab:
mount -a -t nfs
Или мини-watchdog
check_nfs() {
mountpoint -q "$1" || return
timeout 3 ls "$1" >/dev/null || umount -l "$1"
}
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Анализ больших логов за секунды
▪️ Быстрый фильтр по событию
grep читает файл построчно, подходит даже для очень больших логов.
▪️ Извлечение нужных полей
Допустим, формат:
Берём только IP:
▪️ Агрегация и подсчет
Топ источников ошибок:
Результат:
▪️ Группировка по пользователям
BashTex📱 #bash
grep "ERROR" app.log
grep читает файл построчно, подходит даже для очень больших логов.
Допустим, формат:
2026-01-20 12:01:33 ERROR user=alice ip=10.0.0.5
Берём только IP:
grep "ERROR" app.log | awk '{print $NF}'
Топ источников ошибок:
grep "ERROR" app.log \
| awk '{print $NF}' \
| sort \
| uniq -c \
| sort -nr
Результат:
120 10.0.0.5
87 10.0.0.3
grep "ERROR" app.log \
| awk -F'user=' '{print $2}' \
| awk '{print $1}' \
| sort | uniq -c
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10😁5
Массовое обновление конфигов через шаблоны
▪️ Шаблон конфига
▪️ Задаём переменные
▪️ Массовая генерация конфигов
На выходе:
▪️ Dry-run перед применением
▪️ Применение с бэкапом
▪️ Использование:
массовая смена портов/доменов
автогенерация конфигов при деплое
одинаковые шаблоны на разных хостах
BashTex📱 #bash #utils
configs/app.conf.tpl
server_name=${HOST};
listen=${PORT};
env=${ENV};
max_clients=${MAX_CLIENTS};
export HOST=bashtex.com
export PORT=8080
export ENV=prod
export MAX_CLIENTS=500
mkdir -p out
for tpl in configs/*.tpl; do
envsubst < "$tpl" > "out/$(basename "${tpl%.tpl}")"
done
На выходе:
out/app.conf
out/other.conf
...
envsubst < configs/app.conf.tpl | diff -u /etc/app/app.conf -
for f in out/*.conf; do
sudo cp -a "/etc/app/$(basename "$f")"{,.bak}
sudo cp "$f" /etc/app/
done
массовая смена портов/доменов
автогенерация конфигов при деплое
одинаковые шаблоны на разных хостах
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Замена части файла через временный буфер
Иногда
▪️ Задача
Заменить блок между маркерами
▪️ Идея
читаем файл построчно;
отслеживаем, где мы находимся;
пишем результат во временный буфер;
атомарно подменяем файл;
🛠 Реализация
▪️ Что здесь важно
BashTex📱 #bash #utils
Иногда
sed слишком грубый инструмент: нужны условия, состояние, счетчики. В таких случаях проще пройтись по файлу построчно и собрать новый контент вручную.Заменить блок между маркерами
# BEGIN и # END, не трогая остальной файл.читаем файл построчно;
отслеживаем, где мы находимся;
пишем результат во временный буфер;
атомарно подменяем файл;
in_block=0
tmp=$(mktemp)
while IFS= read -r line; do
case "$line" in
"# BEGIN"*)
in_block=1
echo "$line" >>"$tmp"
echo "new content line 1" >>"$tmp"
echo "new content line 2" >>"$tmp"
;;
"# END"*)
in_block=0
echo "$line" >>"$tmp"
;;
*)
(( in_block )) || echo "$line" >>"$tmp"
;;
esac
done < config.conf
mv "$tmp" config.conf
IFS= read -r - сохраняет пробелы и \mktemp - безопасный временный файлmv - атомарная замена (без битых конфигов)BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Мониторинг логов с уведомлениями
Обычный
Мы будем читать лог в реальном времени, искать по нужному шаблону и отправлять уведомление. Это отлично подойдет на случай, когда ошибку нужно отловить разово и не нужно городить новый пробник в забиксе.
▪️ Простой пример
▪️ Десктоп-уведомление
▪️ Полезные детали
BashTex📱 #bash #utils
Обычный
tail -f - это пассивный просмотр. Но его легко превратить в активный мониторинг, который сам уведомит вас о проблеме.Мы будем читать лог в реальном времени, искать по нужному шаблону и отправлять уведомление. Это отлично подойдет на случай, когда ошибку нужно отловить разово и не нужно городить новый пробник в забиксе.
LOG="/var/log/myapp/error.log"
PATTERN="ERROR|CRITICAL|FATAL"
tail -F "$LOG" | grep --line-buffered -E "$PATTERN" | while read -r line; do
echo "[$(date)] $line" | mail -s "Ошибка в логе" admin@bashtex.com
done
tail -F "$LOG" | grep --line-buffered "ERROR" | while read -r line; do
notify-send "Ошибка в логе" "$line"
done
-F - переживает logrotate и пересоздание файла--line-buffered - мгновенная реакция, без задержекgrep -E - несколько шаблонов сразуwhile read - обработка построчно без потери данныхBashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Автоочистка логов по размеру и времени
Логи имеют неприятную привычку расти бесконечно. Ниже простой скрипт, который следит за возрастом и размером логов: старые архивирует, слишком большие режет или удаляет.
▪️ Логика
логи старше N дней перемещаем в .gz;
логи больше X МБ убираем в архив или удаление;
все работает без logrotate;
🛠 Скрипт
▪️ Что здесь важно
BashTex📱 #bash #utils
Логи имеют неприятную привычку расти бесконечно. Ниже простой скрипт, который следит за возрастом и размером логов: старые архивирует, слишком большие режет или удаляет.
логи старше N дней перемещаем в .gz;
логи больше X МБ убираем в архив или удаление;
все работает без logrotate;
LOG_DIR="/var/log/myapp"
MAX_DAYS=7
MAX_SIZE_MB=100
find "$LOG_DIR" -type f -name "*.log" | while read -r file; do
size_mb=$(du -m "$file" | awk '{print $1}')
# Архивируем старые логи
if [[ $(find "$file" -mtime +$MAX_DAYS) ]]; then
gzip -f "$file"
continue
fi
# Архивируем слишком большие
if (( size_mb > MAX_SIZE_MB )); then
gzip -c "$file" > "${file}.gz"
: > "$file" # обнуляем без удаления
fi
done
find -mtime - контроль по времениdu -m - размер в мегабайтах: > file - безопасное обнуление без потери inodegzip -c - архив без удаления исходникаBashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Please open Telegram to view this post
VIEW IN TELEGRAM
😁16👍1
Автоматическая очистка старых бэкапов по количеству копий
▪️ Исходные условия
Все бэкапы лежат в одном каталоге;
Имена содержат дату или время (важно для сортировки);
Нужно оставить, например, 7 последних копий.
🛠 Простой, но надежный скрипт
▪️ Полезные улучшения
Проверка без удаления:
Фильтрация только нужных бэкапов:
Логирование:
BashTex📱 #bash #utils
Все бэкапы лежат в одном каталоге;
Имена содержат дату или время (важно для сортировки);
Нужно оставить, например, 7 последних копий.
#!/bin/bash
BACKUP_DIR="/backups"
KEEP=7
cd "$BACKUP_DIR" || exit 1
ls -1t | tail -n +$((KEEP + 1)) | while read -r old_backup; do
echo "Удаляю старый бэкап: $old_backup"
rm -rf -- "$old_backup"
done
ls -1t- сортирует файлы по времени изменения (новые сверху)tail -n +$((KEEP + 1))- выбирает всё после N первыхrm -rf --- безопасно удаляет, даже если имя начинается с -
Проверка без удаления:
ls -1t | tail -n +$((KEEP + 1))
Фильтрация только нужных бэкапов:
ls -1t backup-* | tail -n +$((KEEP + 1))
Логирование:
rm -rf -- "$old_backup" >> /var/log/backup-cleanup.log 2>&1
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6
Проверка доступности DNS и fallback на другой сервер
Когда DNS внезапно падает, проблемы выглядят как сбой сети или сервиса. Чтобы не гадать, можно сделать простой механизм: проверяем DNS и если не отвечает, то временно переключаемся на резервный сервер.
🛠 Базовый скрипт
▪️ Автовосстановление (по cron)
⚠️ Обратить внимание
В системах с systemd-resolved или NetworkManager resolv.conf может перезаписываться;
Для серверов лучше использовать resolvectl или override конфигурации;
Скрипт требует root.
BashTex📱 #bash
Когда DNS внезапно падает, проблемы выглядят как сбой сети или сервиса. Чтобы не гадать, можно сделать простой механизм: проверяем DNS и если не отвечает, то временно переключаемся на резервный сервер.
#!/bin/bash
TEST_DOMAIN="google.com"
PRIMARY_DNS="8.8.8.8"
FALLBACK_DNS="1.1.1.1"
RESOLV="/etc/resolv.conf"
BACKUP="/tmp/resolv.conf.bak"
if dig +time=2 +tries=1 "$TEST_DOMAIN" @"$PRIMARY_DNS" > /dev/null; then
echo "DNS доступен"
exit 0
fi
echo "DNS недоступен, переключаемся на fallback"
cp "$RESOLV" "$BACKUP"
cat > "$RESOLV" <<EOF
nameserver $FALLBACK_DNS
EOF
dig - быстрая проверка реального DNS-запроса
+time и +tries - не зависаем надолго
resolv.conf сохраняется перед изменением
if dig "$TEST_DOMAIN" @"$PRIMARY_DNS" > /dev/null && [ -f "$BACKUP" ]; then
mv "$BACKUP" "$RESOLV"
fi
В системах с systemd-resolved или NetworkManager resolv.conf может перезаписываться;
Для серверов лучше использовать resolvectl или override конфигурации;
Скрипт требует root.
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍5
Проверка существования команды и версии утилиты
Скрипты часто падают не из-за логики, а из-за отсутствующей команды или неподходящей версии утилиты. Хорошая практика для часто переезжающих скриптов - проверять наличие команды до выполнения основной логики и иметь fallback.
▪️ Базовая проверка: есть ли команда
быстрее и безопаснее
корректно работает с alias и builtins;
не пишет лишнего в stdout.
▪️ Проверка с fallback
Один интерфейс - разные реализации.
▪️ Проверка версии утилиты
▪️ Универсальная функция
Использование:
BashTex📱 #bash #utils
Скрипты часто падают не из-за логики, а из-за отсутствующей команды или неподходящей версии утилиты. Хорошая практика для часто переезжающих скриптов - проверять наличие команды до выполнения основной логики и иметь fallback.
if ! command -v curl >/dev/null 2>&1; then
echo "curl не найден"
exit 1
fi
command -v в данном случае:быстрее и безопаснее
which;корректно работает с alias и builtins;
не пишет лишнего в stdout.
if command -v curl >/dev/null; then
DL="curl -fsSL"
elif command -v wget >/dev/null; then
DL="wget -qO-"
else
echo "Нет curl или wget"
exit 1
fi
$DL https://bashtex.com
Один интерфейс - разные реализации.
REQ="7.60"
CUR=$(curl -V | awk '{print $2}')
if [ "$(printf '%s\n' "$REQ" "$CUR" | sort -V | head -n1)" != "$REQ" ]; then
echo "curl слишком старый: $CUR"
exit 1
fi
sort -V корректно сравнивает версии (7.9 < 7.10).
require_cmd() {
command -v "$1" >/dev/null || {
echo "Требуется команда: $1"
exit 1
}
}
Использование:
require_cmd awk
require_cmd tar
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Поиск файлов, измененных за последние 24 часа
Простая задача, которая регулярно спасает время: быстро найти, что именно менялось на системе за сутки - конфиги, логи, скрипты, данные.
▪️ Базовый вариант
▪️ Только конфиги или нужные расширения
Удобно после апдейтов или ручных правок.
▪️ С деталями (размер, дата, путь)
Или аккуратнее через stat:
▪️ Исключаем лишнее
Актуально для системного аудита.
▪️ За последние N часов (точнее суток)
BashTex📱 #bash #utils
Простая задача, которая регулярно спасает время: быстро найти, что именно менялось на системе за сутки - конфиги, логи, скрипты, данные.
find /path -type f -mtime -1
-mtime -1 - файлы, измененные менее суток назад. Работает по времени изменения содержимого (mtime)find /etc -type f -mtime -1 \( -name "*.conf" -o -name "*.cfg" \)
Удобно после апдейтов или ручных правок.
find /var/log -type f -mtime -1 -exec ls -lh {} +
Или аккуратнее через stat:
find . -type f -mtime -1 -exec stat --format '%y %s %n' {} +
find / -type f -mtime -1 \
! -path "/proc/*" \
! -path "/sys/*" \
! -path "/dev/*"
Актуально для системного аудита.
find /home -type f -mmin -1440
-mmin работает с минутамиBashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7
Please open Telegram to view this post
VIEW IN TELEGRAM
😁8🗿3👨💻2
Работа с датами и временем
Дата и время в bash нужны постоянно: бэкапы, логи, ротация, отчеты, cron и т.д. Хорошая новость -
▪️ Форматированный вывод даты:
▪️ Типовые форматы:
▪️ Вчера / завтра
С форматированием:
▪️ Смещение на N дней
Пример для бэкапов:
▪️ Часы, минуты, секунды
Комбинирование:
▪️ Начало и конец дня
Вчерашний день целиком:
▪️ Unix timestamp в человекочитаемо
Обратно:
▪️ Имена файлов и каталогов
▪️ Проверка возраста файлов (совместно с date)
BashTex📱 #bash #utils
Дата и время в bash нужны постоянно: бэкапы, логи, ротация, отчеты, cron и т.д. Хорошая новость -
date умеет почти все, если знать приемы.
date +"%Y-%m-%d %H:%M:%S"
%Y - год%m - месяц%d - день%H:%M:%S - время%s - Unix timestamp
now=$(date +"%F_%H-%M-%S")
date -d "yesterday"
date -d "tomorrow"
С форматированием:
date -d "yesterday" +"%Y-%m-%d"
date -d "+3 days"
date -d "-7 days"
Пример для бэкапов:
backup_date=$(date -d "+1 day" +"%F")
date -d "+2 hours"
date -d "-15 minutes"
date -d "+30 seconds"
Комбинирование:
date -d "+1 day +2 hours"
date -d "today 00:00"
date -d "today 23:59:59"
Вчерашний день целиком:
date -d "yesterday 00:00"
date -d "yesterday 23:59:59"
date -d @1700000000
Обратно:
date +%s
logfile="app_$(date +%F).log"
mkdir "backup_$(date +%Y%m%d)"
file_ts=$(stat -c %Y file.txt)
now_ts=$(date +%s)
(( now_ts - file_ts > 86400 )) && echo "Файл старше суток"
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12
Безопасная работа с rm
▪️ Простой способ - ls
Если вывод выглядит корректно только потом:
▪️ Проверка через echo. Отлично работает со сложными шаблонами:
Пока есть echo - ничего не удалится.
▪️ Проверка через find
Проверили список -> заменили
▪️ Подтверждение перед удалением. Встроенный интерактивный режим
▪️ Свой безопасный wrapper
Использование:
▪️ Проверка на пустые переменные (обязательно!)
Опасно:
Безопасно:
BashTex📱 #bash #utils
rm - одна из самых опасных команд в linux ввиду отсутствия корзины. Ошибся путем, переменной или шаблоном и данные ушли навсегда. Но есть простые приемы, которые делают работу с удалением безопасной.
ls -ld /path/to/delete/*
Если вывод выглядит корректно только потом:
rm -rf /path/to/delete/*
echo rm -rf /data/tmp/*
Пока есть echo - ничего не удалится.
find /logs -type f -mtime +7 -print
Проверили список -> заменили
-print на -delete:
find /logs -type f -mtime +7 -delete
rm -ri /important/data
-i - спрашивает подтверждение-r - рекурсивно
safe_rm() {
echo "Будут удалены:"
ls -ld "$@"
read -p "Продолжить? [y/N] " ans
[[ "$ans" == "y" ]] && rm -rf "$@"
}
Использование:
safe_rm /path/to/delete
Опасно:
rm -rf "$DIR"
Безопасно:
[[ -n "$DIR" ]] && rm -rf "$DIR"
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11
Please open Telegram to view this post
VIEW IN TELEGRAM
😁7
Чтение файлов построчно без потери спецсимволов
Чтение файла строка за строкой в bash кажется простым… пока не начинают теряться символы.
▪️ Классическая ошибка
Проблемы:
начальные/конечные пробелы обрезаются
строки с
Для конфигов, путей и логов это критично.
▪️ Правильный способ
Это канонический вариант чтения строк.
▪️ Что здесь происходит
отключает разделение по пробелам и табам;
сохраняет все пробелы в начале и конце строки.
запрещает интерпретацию
строка читается как есть.
▪️ Пример проблемы
Файл:
Без -r:
С IFS= read -r:
▪️ Чтение с обработкой строк
▪️ Альтернатива: mapfile
Но есть один минус -
BashTex📱 #bash #utils
Чтение файла строка за строкой в bash кажется простым… пока не начинают теряться символы.
while read line; do
echo "$line"
done < file.txt
Проблемы:
\ - пропадаетначальные/конечные пробелы обрезаются
строки с
\n, \t, \\ читаются некорректноДля конфигов, путей и логов это критично.
while IFS= read -r line; do
echo "$line"
done < file.txt
Это канонический вариант чтения строк.
IFS=отключает разделение по пробелам и табам;
сохраняет все пробелы в начале и конце строки.
read -rзапрещает интерпретацию
\ как escape;строка читается как есть.
Файл:
path=C:\Program Files\App\
Без -r:
path=C:Program FilesApp
С IFS= read -r:
path=C:\Program Files\App\
while IFS= read -r line; do
[[ "$line" =~ ^#|^$ ]] && continue
echo "Обработка: $line"
done < config.conf
mapfile -t lines < file.txt
Но есть один минус -
mapfile загружает весь файл в памятьBashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Поиск пользователей без пароля и с заблокированным shell
Со временем в системе накапливаются сервисные, забытые или некорректные аккаунты. Часть из них может быть: без пароля, с истекшим паролем
или с заблокированным shell. Быстрый аудит можно сделать обычным bash.
▪️ Пользователи без пароля. Пароль хранится в /
Если вывод не пустой - это повод проверить, зачем аккаунт существует.
▪️ Пользователи с заблокированным паролем
! или * в поле пароля значит, что вход запрещен
Обычно это нормально для сервисных аккаунтов.
▪️ Пользователи с отключенным shell
Типично для:
www-data
nginx
backup
Если обычный пользователь с таким shell - стоит проверить.
▪️ Проверка статуса пароля через chage
Полезно для поиска:
истекших паролей;
аккаунтов без политики смены.
▪️ Массово:
▪️ Быстрый отчет:
BashTex📱 #bash #utils
Со временем в системе накапливаются сервисные, забытые или некорректные аккаунты. Часть из них может быть: без пароля, с истекшим паролем
или с заблокированным shell. Быстрый аудит можно сделать обычным bash.
etc/shadow, но факт его отсутствия легко проверить:
sudo awk -F: '($2==""){print $1}' /etc/shadow
Если вывод не пустой - это повод проверить, зачем аккаунт существует.
sudo awk -F: '($2 ~ /^!|^\*/){print $1}' /etc/shadow
! или * в поле пароля значит, что вход запрещен
Обычно это нормально для сервисных аккаунтов.
awk -F: '($7 ~ /(nologin|false)$/){print $1, $7}' /etc/passwd
Типично для:
www-data
nginx
backup
Если обычный пользователь с таким shell - стоит проверить.
sudo chage -l username
Полезно для поиска:
истекших паролей;
аккаунтов без политики смены.
for u in $(cut -d: -f1 /etc/passwd); do
chage -l "$u" 2>/dev/null | grep -q "never" && echo "$u"
done
awk -F: '{print $1 ":" $7}' /etc/passwd
BashTex
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Please open Telegram to view this post
VIEW IN TELEGRAM
😁9