Bash Days | Linux | DevOps
23.3K subscribers
147 photos
25 videos
667 links
Авторский канал от действующего девопса

Самобытно про разработку, devops, linux, скрипты, сисадминство, техдирство и за айтишную жизу.

Автор: Роман Шубин
Реклама: @maxgrue

MAX: https://max.ru/bashdays

Курс: @tormozilla_bot
Блог: https://bashdays.ru
Download Telegram
reexec VS reload

Порой народ путает команды systemctl daemon-reload и systemctl daemon-reexec.

С виду вроде похожие, но нет. Спросил тут на досуге товарища — а ты знаешь чем они отличаются?

Да, ответил товариш, reexec это старая версия перечитывания сервисов и юнитов. Я обычно делаю так:

systemctl daemon-reexec
systemctl daemon-reload
systemctl enable node_exporter
systemctl start node_exporter


Неее… так не нужно! Это хуйня! По крайней мере первая команда тебе тут не нужна для перезапуска и обновления сервисов.

Команда systemctl daemon-reexec перезапускает сам systemd, это нужно например при обновлении бинарников systemd, но никак не для перезапуска юнитов и сервисов.

После редактирования *.service / *.timer / *.mount файлов, достаточно сделать daemon-reload, эта команда перечитает unit-файлы.

Обычно проходится по каталогам:

/etc/systemd/system/
/lib/systemd/system/
/usr/lib/systemd/system/
/run/systemd/system/


То есть она перезагружает только конфигурацию юнитов, без перезапуска сервисов.

Так что не путай, в большинстве случаев тебе достаточно daemon-reload.

🛠 #linux #tricks #debug #systemd

@bashdays / @linuxfactory / @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
870
Имена файлов удаляются с помощью системных вызовов unlink, unlinkat.

Ну дак вот. Когда счетчик ссылок (имен) становится равен нулю — файл удаляется.

Физическое удаление (блоки маркируются свободными) происходит когда больше никто не удерживает файл. То есть процесс закрыл последний дескриптор ассоциированный с файлом.

Давай попробуем поймать (открыть файл) до того, как запустится вызов unlink.

Создаем искусственный временный файл:

printf '%s\n' {a..z} > /tmp/sh-thd-bashdays


Создаем и запускаем скрипт:

#!/bin/bash

file=/tmp/sh-thd-bashdays
exec 3< "$file"
rm -vf "$file" # Имени уже нет
read -N 4096 -ru 3 # Но мы читаем
printf '%s' $REPLY # Выводим
exec 3>&-
exit


Что тут происходит?

exec 3< - файл открывается на чтение, привязывается к дескриптору 3 и с этого момента файл начинает жить в «памяти» в open file table ядра, даже если мы его ёбнем удалим. Дескриптор 3 теперь указывает на открытый файл, независимо от имени.

rm -vf - файл удаляется из файловой системы, имени больше нет, НО, его содержимое еще доступно, потому что он открыт.

read -N 4096 - читаем до 4096 байт из открытого файла через дескриптор 3.

exec 3>& - закрываем дескриптор 3 и после этого файл окончательно исчезнет, если его больше никто не держит, а ядро высвобождает ресурсы.

Итоги:

Эта фишка демонстрирует, что можно удалить файл, но продолжить его использовать если он был открыт на момент удаления.

Это классическая Unix-модель: имя файла — не сам файл, а просто ссылка.

Если держишь дескриптор — файл всё ещё существует.


А тут как-то рассказывал, как восстанавливать удаленные файлы, в том числе и запущенные.


Чтиво почитать:

man 2 unlink
man 2 unlinkat

🛠 #linux #bash

@bashdays / @linuxfactory / @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
38
CRON в первый рабочий день месяца

Да, ключевой момент тут именно в «рабочий день».

Задачка вроде не тривиальная, но как оказалось cron нихуя не умеет определять рабочий сегодня день или нет. И systemd timer тоже не умеет. Никто ничо блядь не умеет.

Мы с тобой не пальцем деланы, изобретем свой велосипед!

Чтобы реализовать желаемое, я изобрел такую кишку:

0 8 1-3 * * [ "$(date +\%u)" -lt 6 ] && [ "$(curl -s https://isdayoff.ru/$(date +\%Y\%m\%d))" = "0" ] && /usr/local/sbin/bashdays.sh


Выглядит монструозно, но блядь... Можно в сам скрипт проверку забить, но это еще один костыль.

Что тут происходит?

Короче…

0 8 1-3 * * — Запуск 1–3 числа месяца в 08:00. Потому что первый рабочий день месяца может выпасть на 1-е число, если это будний день. 2-е число, если 1-е — выходной. 3-е число, если 1-е и 2-е — выходные (например, сб + вс).

[ "$(date +%u)" -lt 6 ] — Проверяем, что сегодня будний день (1–5 = Пн–Пт). Пусть тебя здесь 6ка не смущает. Она означает: день меньше 6, т.е. 1, 2, 3, 4, 5 — это будни (Пн – Пт).

curl ... = "0" — Проверка, что сегодня рабочий день по календарю РФ, выполняется через АПИху производственного календаря.

&& /opt/scripts/first-workday.sh — Выполняем только если сошлись оба условия.

То есть АПИха возвращает 0 если сегодня будни. Если выходной, то оно вернет что-то другое.

Тут еще бы для curl и date указать что-то вроде CURL=$(which curl), а то крон частенько залупается и не знает откуда запускать эти команды.

Всегда старайся указывать полные пути до программ и утилит. Это распространенный кейс, крон вроде отрабатывает, но ничего не работает. А оно просто не знает откуда запускать, то что ты ему прописал.

Да, есть зависимость от внешнего API и порой это пиздец хуёва. Отвалилась АПИха, скрипт не получил данные. Поэтому рекомендую сделать себе собственный микросервис с АПИхой и ни от кого не зависеть. Тем более если сервер в оффлайне, запрос на внешнюю АПИху он сделать не сможет.


Если знаешь еще какие-то способы, велком в комментарии.

UPD: более корректные алгоритмы решения этой задачи смотрим в комментах, всем спасибо!

🛠 #bash #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
660
Слыхал про with-lock-ex, flock, lckdo?

Если коротко, это штуки, которые не дают запустить второй экземпляр скрипта/программы, если первый ещё работает.

Всё это и многое другое входит в пакет moreutils, в убунтах из коробки я их не нашел, пришлось ставить. Но это детали. В этот пакет входит еще дюжина полезных хуёвин.

Например (нажми на спойлер):

sudo apt install moreutils

chronic — показывает вывод команды, только если она завершилась с ошибкой.

combine — логические операции над двумя файлами (как `sort` + `comm`, но проще).

errno — выводит описание кода ошибки (EACCES 13 Permission denied).

ifdata — dыводит информацию о сетевых интерфейсах в скриптабельном виде.

ifne — выполняет команду только если предыдущая команда что-то вывела (if-not-empty).

isutf8 — проверяет, является ли файл валидным UTF-8.

и так далее…


Вернемся к lckdo (Lock and Do). Это обёртка вокруг flock. Как и написал выше, оно не позволяет запуститься второму экземпляру программы/скрипты.

Допустим у тебя что-то запустилось по крону, не успело завершиться и через промежуток снова запускается. Наплодилось 100500 процессов, сервак встал раком, а потом и тебя поставили раком.

lckdo — cтавит блокировку на файл, если блокировка прошла — выполняет указанную команду, если не удалось — завершает выполнение (по умолчанию).

Давай на практике!

В первом терминале запускаем:

lckdo /tmp/bashdays.lock bash -c 'echo start; sleep 60; echo done'


Во втором терминале запускаем:

lckdo /tmp/bashdays.lock echo "Hello bashdays"


И получаем: lckdo: lockfile /tmp/bashdays.lock' is already locked

По умолчанию lckdo ничего не ждет, если нужно, чтобы он ждал до освобождения блокировки, добавляем -w.

lckdo -w /tmp/bashdays.lock echo "I'm free"


Теперь когда файл /tmp/bashdays.lock будет отпущен, отработает команда и выведет на экран I'm free.

Как lckdo работает с lock-файлом.

1. Файл /tmp/bashdays.lock сам по себе ничего не блокирует.

2. Блокировку держит процесс, который открыл файл и вызвал flock/lckdo на него.

3. Когда процесс завершается — блокировка автоматически снимается.

Даже если файл /tmp/bashdays.lock остаётся на диске — он больше не заблокирован, и lckdo снова сможет его использовать.

Всё логично, вот еще одни пример с кроном:

* * * * * lckdo /tmp/bashdays.lock /usr/local/bin/bashdays.sh


Скрипт не будет запущен, если предыдущий запуск не завершился.

Такие дела. Бери на вооружение, возможно где-то в хозяйстве сгодится.

🛠 #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
55
Сопроцессы. Практика. Часть Вторая.

🔤🔤🔥🔤🔤🔤🔤

coproc хорошо подходит для общения в клиент-серверном режиме. Для примера попробуем подключиться к POP3 серверу с шифрованием ssl прямо из bash-скрипта.

Сам ssl несколько сложноват для bash, поэтому в качестве посредника будем использовать openssl s_client.

Протокол и команды POP3 лучше посмотреть на википедии.

1. Cоздим сопроцесс. Для этого запустим openssl в режиме s_client. При этом из дескриптора POP3_CONN[0] можно читать данные от сопроцесса.

В дескриптор POP3_CONN[1] можно писать для сопроцесса.

При записи используем перенаправление >&${POP3_CONN[1] . При чтении тоже можно использовать перенаправление, но поскольку у команды read есть ключ -u красивее воспользоваться им.

2. Аутентифицируемся

3. Закроем сессию и дескрипторы.

# Функция для отправки команд серверу
function SEND_CMD() {
sleep 0.3
echo "$@" >&${POP3_CONN[1]}
sleep 0.3
}

# аутентификация. Обычный логин
function POP3_LOGIN() {
declare REC
declare -a AREC
# проверка соединения
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ "${AREC[0]}" == "+OK" ]];then
# Отправляем логин
SEND_CMD "USER $USER"
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ "${AREC[0]}" == "+OK" ]];then
# Отправляем пароль
SEND_CMD "PASS $PASS"
read -ert 2 -u "${POP3_CONN[0]}" REC
read -ra AREC <<<${REC//$'\r'/}
if [[ "${AREC[0]}" == "+OK" ]];then
return 0 # аутентификация успешна
else
return 3 # не правильный пароль
fi
else
return 2 #не правильный login
fi
else
return 1 # ошибка соединения с сервером
fi
}

#Выход и закрытие дескрипторов.
function POP3_QUIT(){
SEND_CMD "QUIT"
# Закрываем coproc
exec ${POP3_CONN[0]}<&-
exec ${POP3_CONN[1]}>&-
}


Задержки 0.3 секунды при отправке нужны для того, чтобы сервер успел сформировать ответ.

Ошибки -ERR не обрабатывал. В случае чего команда read завершится по таймауту в 2 сек. (-t 2)

${REC//$'\r'/} конструкция удаляет cr, потому что POP3 сервер отвечает c lfcr.


#!/bin/bash

SERVER="server"
PORT=995
USER="user@server"
PASS="StrongPass"

# создаем сопроцесс и соединяемся с сервером pop3
coproc POP3_CONN { openssl s_client -connect "${SERVER}:${PORT}" -quiet 2>/dev/null;}
POP3_LOGIN
POP3_QUIT


help coproc
help read
man openssl
вики POP3

🛠 #bash #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
221
Сопроцессы. Практика. Часть Третья.

🔤🔤🔥🔤🔤🔤🔤

Это уже больше не сопроцессы, а про то, как принять почту в скрипте bash.

Соединение с POP3 сервером есть. Аутентификация тоже. Осталось написать что-нибудь полезное.

# возвращает число писем в ящике
function POP3_STAT(){
declare -a AREC
declare REC
SEND_CMD "STAT"
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
echo ${AREC[1]} # число сообщений
return 0
else
echo 0
return 1
fi
}
#Помечает к удалению указанное письмо
function POP3_DELE(){
declare -i MSG_NUM=${1:-1} # по умолчанию первое
declare -a AREC
declare REC
SEND_CMD "DELE $MSG_NUM" #удаляем указанное сообщение
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
return 0
else
return 1
fi
}
# читает письмо с заголовками
function POP3_RETR(){
declare -i MSG_NUM=${1:-1} # по умолчанию первое
declare -a AREC
declare REC
SEND_CMD "RETR $MSG_NUM" #читаем указанное сообщение
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
while read -r -t 2 -u ${POP3_CONN[0]} REC ; do
REC=${REC//$'\r'/}
echo "$REC"
if [[ "$REC" == "." ]];then
return 0 # msg end
fi
done
else
return 1
fi
}
# читает указанное число строк письма
function POP3_TOP(){
declare -i MSG_NUM=${1:-1} # по умолчанию первое
declare -i STR_NUM=${2:-1} # по умолчанию одна строка
declare -a AREC
declare REC
#читаем указанное сообщение
SEND_CMD "TOP $MSG_NUM $STR_NUM"
read -ert 2 -u ${POP3_CONN[0]} REC
read -ra AREC <<<${REC//$'\r'/}
if [[ ${AREC[0]} == "+OK" ]];then
while read -ert 2 -u ${POP3_CONN[0]} REC ; do
REC=${REC//$'\r'/}
echo "$REC"
if [[ "$REC" == "." ]];then
return 0
fi
done
else
return 1
fi
}

Финальный код
#!/bin/bash

SERVER="server"
PORT=995
USER="user@server"
PASS="StrongPass"

coproc POP3_CONN { openssl s_client -connect "${SERVER}:${PORT}" -quiet 2>/dev/null;}

POP3_LOGIN && echo POP3_LOGIN OK
MSG_NUM=$(POP3_STAT)
#цикл перебора сообщений
while ((MSG_NUM));do
POP3_TOP $MSG_NUM 1 # Заголовки + 1 строку сообщения
# POP3_RETR $MSG_NUM # сообщения целиком
# POP3_DELE $MSG_NUM # помечаем к удалению.
((--MSG_NUM))
done

POP3_QUIT


help coproc
help read
man openssl

🛠 #bash #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
20
Хочешь устроить себе челлендж на знание командной строки? Да пожалуйста, лови тренажер по Linux-терминалу. Правда он на английском, но мы с тобой тоже не пальцем деланные.

Тренажер содержит 77 вопросов. Вполне достаточно чтобы заебаться.


Я даж успешно прошел первую задачку, правда по-олдскульному и затронул все бед-практики, которые только существуют.

Установка простая:

cd /tmp
python3 -m venv textual_apps
cd textual_apps
source bin/activate
pip install cliexercises
cliexercises


Здесь я использовал tmp папку и venv, чтобы систему всяким гавно не засорять, а чисто поиграться.

Что прикольно, если совсем тупой, то можно получить подсказку, будет показано несколько решений. Подсказка открывается по CTRL+S.

Сможешь выбить все 77 вопросов без подсказок?

Исходники этого тренажера лежат тут, а видосик с работой можешь посмотреть тут.

🛠 #linux #bash

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
364
Ремонтировал внешний диск, в моменте отвалился от малины и я получил RAW вместо ext3. Печалька. Бекапы естественно я никакие не делал, хотя 5 лет собирался этим заняться.

Как говорится - пока гром не грянет мужик не перекрестится.


Велосипед я изобретать не стал, а так же не стал брать сразу какие-то продвинутые инструменты для восстановления. Решил воспользоваться нативным fsck, чем чёрт не шутит.

Пошел погуглил синтаксис, не каждый же день этой хернёй пользуешься. А там блядь:

fsck -y /dev/sdb
fsck.ext3 -y /dev/sdb


Хмм... помимо fsck.ext3 есть еще и fsck.ext4 и еще несколько штук. Ёбтвою мать. Отправляемся ресерчить, чем эта поебота отличается от обычного fsck.

TL:DR: НИЧЕМ!

Ща, расскажу. Короче в чистом виде fsck это обёртка, универсальная оболочка, которая автоматом определяет тип файловой системы и затем уже запускает условно fsck.ext3, ну или какая там у тебя на дисках.

Автоопределение это заебись, но порой fsck не хочет ничего проверять. Поэтому самостоятельно определяем тип своей файловой системы и в зависимости от результата запускаем fsck.ext3 и т.п.

Чтобы узнать, что запустит fsck делаем так:

fsck -N /dev/sda1


В результате получишь:

[/usr/sbin/fsck.ext4 (1) -- fsck.ext4 /dev/sda1


А еще у fsck нет ключа - [-E extended-options]. В нее можно передать:

-E discard - Включает TRIM (удаление неиспользуемых блоков на SSD) во время проверки. Аналог fstrim, но в процессе fsck.

-E journal_only - Проверяет только журнал ext3/ext4, не сканируя всю ФС. Быстро, но полезно только в определённых сценариях.

-E frag - Проводит анализ фрагментации. Полезно, если интересует дефрагментация ext4.

-E bmap2extent - Преобразует старые "indirect" блоки в extent-формат (для старых ext4).

-E test_fs - Включает особое поведение для тестирования (не используется в продакшене).

Пример:

fsck.ext4 -f -E discard /dev/sda1


Принудительная проверка + удаление "мусорных" блоков на SSD.

# Как fsck определяет тип файловой системы

Порядок определения:

1. Смотрит в /etc/fstab и выгребает третий столбец.
2. Если в fstab хуй, то оно запускает blkid /dev/sda1
3. Если определить не получилось, пиздует в /etc/filesystems, но в большинстве случаев такого файла в современных дистрибутивах нет. Этот файл опционален.

Вот и вся наука. В кишки лезть уже не будет, этой информации вполне достаточно.

Ну и чтобы на каждую ошибку не вводить y, пропиши автофикс:

fsck -y /dev/sda1


Оно там само пошуршит, все исправит и будет тебе счастье. Изучай.

🛠 #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
463
Забавная ситуёвина с ubuntu 24. После того, как человек поменял в файле /etc/ssh/sshd_config порт с 22 на 2222 и сделал systemctl restart ssh - ничего не произошло.

По-прежнему слушался порт 22. Хм... Хуйня какая-то.

Всё оказалось прозаичнее. В новых версиях дистрибутива, ssh демон стал использовать сокетовую модель для подключения клиентов. Как раз недавно мы разбирали systemd и его функционал, поищи по тегу #systemd

Ну так вот. Если подключиться к консоли дистрибутива (не по ssh), то обнаружим, что никакой ssh сервис не запущен. Однако...

Но если выполнить: ss -tln то увидим:

tcp LISTEN 0 4096 *:22 *:*


Да ёбтвою мать! Разбираемся.

Суть такая. Для ssh используется не ssh.service юнит, а ssh.socket. А как мы знаем юнит socket не держит процесс постоянно запущенным. Экономия ресурсов и т.п. А вот когда кто-то обращается к порту 22, запрос улетает на сокет systemd, который уже и запускает сервис ssh.service..

Пиздец конечно конструкция. Если посмотреть файл ssh.socket, в нём будет:

[Unit]
Description=OpenBSD Secure Shell server socket
Before=sockets.target ssh.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Socket]
ListenStream=0.0.0.0:22
ListenStream=[::]:22
BindIPv6Only=ipv6-only
Accept=no
FreeBind=yes

[Install]
WantedBy=sockets.target
RequiredBy=ssh.service


Смотрим на ListenStream, вот тебе и 22 порт.

А чё блядь делать?

Снимать штаны и бегать. А если серьезно, то решается это просто. После правки конфига /etc/ssh/sshd_config делаем:

systemctl daemon-reload
systemctl restart ssh.socket


Перезапускаем именно юнит сокета. После этого порт изменится. А в файле ssh.socket будет такое:

[Socket]
ListenStream=0.0.0.0:2222
ListenStream=[::]:2222


Красота. Если тебя удручают эти нововведения, то можно вернуться к привычной схеме.

Делается это так:

systemctl disable --now ssh.socket
rm -f /etc/systemd/system/ssh.service.d/00-socket.conf
rm -f /etc/systemd/system/ssh.socket.d/addresses.conf
systemctl daemon-reload
systemctl enable --now ssh.service


Способ описан на официальном сайте бубунты.

Вот такие пироги.

Ну а чем отличается ssh_config от sshd_config я описывал в этом посте.


🛠 #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
9121
Как-то давненько пришлось мне отлаживать большущую APIху, но под рукой была только консолька. Ни о каком GUI и речь не шла. А пулять запросы через curl я заебался.

Тем более хотелось что-то организованное и визуально приятное.

Postman сразу мимо, это гавно сделали настолько перегруженным, что им пользоваться уже нереально, да и под чистым терминалом его не запустишь.

Выбор пал на Posting. Это TUI клиент со всем необходимым функционалом.

Так он и прижился, сейчас если что-то нужно отладить или потестить у меня есть эта замечательная тулза. Не перегружена хуйнёй и дружелюбная. Есть все самое необходимое, даже мышкой можно тыкать. Ну и сессии сохраняет со всеми запросами.

Ставится элементарно:

curl -LsSf https://astral.sh/uv/install.sh | sh
uv tool install --python 3.13 posting


uv — это современный инструмент для Python, который объединяет в себе функционал менеджера пакетов и виртуальных окружений, задумывается как замена pip + venv/virtualenv + pip-tools.


Для хакерменов прям мастхев. Забирай в копилку, годнота!

🛠 #linux #utilites

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
65
Вечный вопрос — как сохранить правила iptables?

Вот навтыкал ты всяких безумных правил и цепочек через командную строку, радуешься. А после ребута все твои поделки к хуям улетучиваются.

И каждый раз ты идешь гуглить, как это решить. Жиза. Каждый раз, как в первый раз.

А делается всё просто, двумя командами:

sudo iptables-save > /etc/iptables/rules.v4
sudo ip6tables-save > /etc/iptables/rules.v6


Ладно, спиздел, нужно еще доставить:

sudo apt install iptables-persistent


Эта хуёвина автоматом создает файлы /etc/iptables/rules.v4, /etc/iptables/rules.v6 и будет применять их при загрузке.

А если у тебя длинный писюн, можно и от первых двух команд избавиться.

😀😃😄😁😅😂🤣😊
😇🙂🙃😉😌😍🥰😘
😗😙😚😋😛😝😜🤪
🤨🧐🤓😎🤩🥳😏😒

Просто открываешь напрямую файл rules.v4/rules.v6 и прописываешь туда все свои хотелки.

После этого не забываем дёрнуть слона за хобот:

systemctl restart netfilter-persistent


Либо по старинке, добавляешь правила через терминал и затем делаешь:

sudo netfilter-persistent save


Но в этом случае файлы rules.v4/rules.v6 будут перезатерты, имей это ввиду если лез в них своими руками.

Саммари

1. Ставим iptables-persistent
2. Пиздярим правила в терминале
3. Применяем netfilter-persistent save

Как делаю я? У меня писюн длинный, поэтому сразу вношу все необходимое руками в rules.v4/rules.v6, хотя это не есть бест-практика, не делай так.

Такие дела, изучай.

🛠 #linux #security #iptables

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
166
Вчера в посте я упомянул конфиги iptables лишь для части дистрибутивов, Alex эту ситуацию в комментариях подметил верно.

Давай посмотрим что в других дистрибутивах происходит.

Debian / Ubuntu

Основной пакет → iptables-persistent / netfilter-persistent

/etc/iptables/rules.v4
/etc/iptables/rules.v6


RHEL / CentOS / Rocky / Alma (iptables-services)

/etc/sysconfig/iptables
/etc/sysconfig/ip6tables


Fedora (новые версии)

По умолчанию использует firewalld (поверх nftables). Если ставишь iptables-services:

/etc/sysconfig/iptables
/etc/sysconfig/ip6tables


Arch Linux / Manjaro

Из коробки iptables не сохраняет правила, обычно юзеры делают сами:

/etc/iptables/iptables.rules
/etc/iptables/ip6tables.rules


OpenSUSE / SLES

По умолчанию тоже firewalld, если ставить пакет iptables, правила обычно хранят в:

/etc/sysconfig/iptables


Astra Linux (Смоленск, Орёл и др. редакции)

Астра базируется на Debian, поэтому у неё схема как у Debian/Ubuntu:

/etc/iptables/rules.v4
/etc/iptables/rules.v6


РЕД ОС (RedOS)

RedOS базируется на RHEL/CentOS, поэтому там всё по «редхэту»:

/etc/sysconfig/iptables
/etc/sysconfig/ip6tables


Вроде основное осветил, если что-то проебал, забыл, затроил — пиши в комменты, поправим.

А вообще при возможности пользуйся облачным фаерволом, если страшно конфигурять iptables или руки растут из жопы.

🛠 #linux #security #iptables

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
35
Мне прилетела интересная задача.

🔤🔤🔥🔤🔤🔤🔤

ТЗ: Сервер без GUI, к нему подключают usb-диск с NTFS и начинается автоматическое архивирование. Логин не требуется, сообщения выдаются на tty1.

sudo apt install ntfs-3g # для монтирования ntfs


Использовать будем штатные средства udev + systemd .

1. Подключаем диск, и запоминаем UUID.

lsblk -f


sdc                                                                         
└─sdc1
ntfs FLASH
1234567890ABCDEF

здесь FLASH - метка диска 1234567890ABCDEF - UUID

создадим каталог для монтирования:

mkdir -p /media/usb_ntfs; chmod 777 /media/usb_ntfs


Дальше создаем 4 файла. Чтобы было проще, в каждом файле в начале коммент с названием файла.

После создания:

sudo systemctl daemon-reload


Как это работает:

При подключении диска usb-диска срабатывает UDEV правило 99-usb-mount.rules и пытается запустить службу autousbbackup.service

autousbbackup.service пытается запустить media-usb_ntfs.mount, поскольку жестко он нее зависит.

Сама media-usb_ntfs.mount запустится только в том случае, если UUID диска будет 1234567890ABCDEF.

Если все условия совпадают, autousbbackup.service запустит скрипт autousbbackup.sh, внутри которого Вы напишите копирование или синхронизацию данных в каталог /media/usb_ntfs.

Если используется архивирование с чередованием дисков - просто сделайте у дисков одинаковые метки:

sudo umount /dev/sdXN # где /dev/sdXN  имя вашего NTFS-раздела.
sudo ntfslabel --new-serial=1234567890ABCDEF /dev/sdXN #задайте UUID


👆 Если в mount не указать опцию nofail система будет тормозить при загрузке.
👆 Запустить скрипт через UDEV даже в фоне не получится, поскольку система вырубит его через 5 сек.

#/etc/udev/rules.d/99-usb-mount.rules

SUBSYSTEM=="block", KERNEL=="sd*", ACTION=="add", TAG+="systemd", ENV{SYSTEMD_WANTS}="autousbbackup.service"


#/etc/systemd/system/media-usb_ntfs.mount

[Unit]
Description=Mount USB NTFS by UUID

[Mount]
What=/dev/disk/by-uuid/1234567890ABCDEF
Where=/media/usb_ntfs
Type=ntfs-3g
Options=defaults,big_writes,nofail,uid=1000,gid=1000

[Install]
WantedBy=multi-user.target


#/etc/systemd/system/autousbbackup.service

[Unit]
Description=Simple service autousbbackup
Requires=media-usb_ntfs.mount
After=media-usb_ntfs.mount

[Service]
Type=simple
ExecStart=/root/work/autousbbackup/autousbbackup.sh

[Install]
WantedBy=multi-user.target


#!/bin/bash
#/root/work/autousbbackup/autousbbackup.sh

declare -x PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
declare MOUNT_POINT=/media/usb_ntfs
declare TTY=/dev/tty1
declare DF='%(%Y%m%d-%H%M%S)T' #date format
declare LOG="$0.log"
exec &>"$LOG" # only one backup log
#exec &>>"$LOG" # all backups log
printf "$DF %s\n" -1 "START BACKUP"|tee "$TTY"
##############################################

sleep 3 # backup emulation

##############################################linux
printf "$DF %s\n" -1 "END BACKUP"|tee "$TTY"

#suicide, because service require mount point
systemd-mount --umount "$MOUNT_POINT"


🛠 #linux #bash

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
677
Как оказалось многие не знают, как нативным cron (без systemd timers) запускать скрипты с интервалом в 30 секунд, без модификации самого скрипта.

Все довольно просто и очевидно. Нужно сделать бутерброд.

Cron исторически работает только с минутной точностью. В crontab нельзя написать «каждые 10 секунд» или «раз в 30 секунд». Для этого обычно использую systemd timers или отдельный демонический скрипт с while true; sleep ...


В crontab строка запускается раз в минуту. Но внутри можно поставить sleep — задержку перед запуском. Таким образом мы получим несколько запусков в рамках одной минуты.

* * * * * /usr/local/sbin/bashdays.sh
* * * * * sleep 30; /usr/local/sbin/bashdays.sh


1. Первая строка запускает скрипт в начале минуты (00:00, 00:01, 00:02…)

2. Вторая строка — ждёт 30 секунд и запускает скрипт (00:00:30, 00:01:30, 00:02:30…).

Тут и получаем шаг в 30 секунд, именно через 2 вызова.

Костыльно? Ага! Но порой не хочется ебаться с таймерами и сделать все по-быстрому. Как вариант, вполне годный. Аналогично можно городить и другие интервалы.

Минусы подхода

Нет гарантии точности. Если первый запуск скрипта будет работать дольше, чем пауза (sleep), запуски могут наложиться.
Мусор в crontab. Для мелкого интервала надо плодить много строк.
Нет гибкой логики.

Где это полезно

Лёгкие скрипты мониторинга (ping, проверка статуса).
Хаотизация нагрузки (например, sleep $((RANDOM % 60)) для рассинхрона).
Если systemd timers или другие планировщики недоступны (например, в ограниченных окружениях или старых системах).

А как работать с таймерами ищи по тегу #systemd, много про это писал.

🛠 #linux #cron #systemd

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
51
Как не стремись к автоматизации, всегда найдется какой-нибудь легаси сервис, который требует ручного обслуживания.

Был у меня такой сервис и работал он только тогда, когда все его файлы и каталоги принадлежали определенному пользователю.

Доступ к сервису имели многие, поэтому люди порой троили и запускали команды в каталоге сервиса от root. Сервису было на это поебать, но до момента его перезапуска.

Обычно это чинилось очень легко, через chown -R. Все это знали и никого это не смущало. Короче костыль ебаный.

Казалось бы, есть масса способов предотвратить такие ошибки: правильные права на файлы, ACL’ы, SELinux.

Но веселья в этом мало! Поэтому я решил заебенить свой собственный мониторинг файловой системы. Скиллов то предостаточно, хули нет.

Спойлер: Я залез в кроличью нору и знатно так хлебнул гавна.

Попытка намбер 1

В Linux есть API под названием fanotify, с его помощью можно получать события о действиях с файлами в юзерспейс.

Всё просто: инициализируем fanotify_init, указываем каталоги через fanotify_mark и читаем события из дескриптора.

Но тут же вылез огромный хуй:

- нельзя отслеживать каталоги рекурсивно (только целый маунт)
- anotify даёт только PID процесса, который что-то сделал. А чтобы узнать UID/GID — нужно лезть в /proc/<pid>/status. То есть на каждое событие приходится открывать и парсить файлы в /proc.

Решение вполне рабочее, но громоздкое. Я такие не люблю, этож думать надо, вайбкодингом не решается.

Попытка намбер 2

Вспоминаем что есть eBPF. Это штука позволяет запускать программы прямо в ядре Linux. Они компилируются в байткод, проходят проверку, а потом гоняются через JIT почти с нативной скоростью.

Что такое eBPF можешь почитать тут и тут.


В eBPF заебись то, что можно цепляться за разные функции ядра. Например, можно подцепиться к vfs_mkdir или vfs_create — это общий слой для работы с файлами.

То есть единая точка входа. Там можно отлавливать события и фильтровать их, не гоняя лишние переключения контекста.

Но и тут вылез хуй:

- kprobes на функции VFS нестабильны, в новых ядрах сигнатуры могут меняться или функции вообще исчезнуть.

- фильтрацию приходится писать прямо в eBPF, а там свои ограничения. Нет бесконечных циклов, стек всего ~512 байт.

Да блядь…

Как я победил рекурсивный обход

Чтобы понять, что именно меняется в каталоге сервиса, пришлось использовать структуру dentry и подниматься по дереву до родителя.

Но так как в eBPF нельзя сделать «бесконечный» цикл, я ограничил глубину с помощью MAX_DEPTH.

На практике этого вполне достаточно. Глубина каталогов мне известна. Ну и конечно, пришлось аккуратно работать с RCU-локами, чтобы дерево не поменялось в момент обхода.

➡️ Кусок кода в первом комментарии, сюда не влез.


Как можно улучшить

В идеале использовать не VFS-хуки, а LSM hooks (Linux Security Module).

Они стабильнее, понятнее и позволяют сразу работать с путями. Там можно красиво получить path и сразу преобразовать его в строку, а потом делать поиск подстроки.

Но в моём ядре этих хуков не было, хуй знает почему, видимо дистрибутив слишком древний. Надо попробовать на новых, чем черт не шутит.

Итоги

Эта поделка как и предполагалась, погрузила меня в печаль, душевные страдания, НО стала отличным тренажером для прокачки:

- Внутренностей Linux ядра
- Работы с eBPF
- И кучу другого с kernel-space

eBPF — мощнейший инструмент, но очень тонкий. Ошибёшься — будешь выебан в жопу.


Информации по нему много, но вся она разбросана. Собрать всё это в кучу было отдельным квестом.

Мораль?

Иногда самое простое решение — это chown -R. Но куда интереснее — написать свой велосипед и заглянуть в кроличью нору Linux ядра.

🛠 #linux #debug #dev

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
947
Настройка core dump в Docker

Цель этого поста — дать тебе общее руководство по включению и сбору core dumps для приложений, работающих в docker контейнере.

Настраиваем хост для сохранения дампов

Для начала надо сконфигурировать хостовую машину, чтобы сохранять такие дампы в нужное место. Нет, не в жопу.

Универсальный способ — задать шаблон core pattern. Шаблон определяет путь и информацию о процессе который наебнулся.

echo '/tmp/core.%e.%p' | sudo tee /proc/sys/kernel/core_pattern


Кратенько:

%e — имя процесса
%p — pid процесса

Более подробно о конфигурации core pattern можешь почитать в man-странице ядра Linux.


Как вариант, можно настроить host изнутри контейнера через CMD или ENTRYPOINT. Но контейнер в этом случае должен запускаться в privileged mode, что хуева с точки зрения безопасности.

Пример хуёвого приложения

#include <cstdlib>
void foo() {
std::abort();
}

int main() {
foo();
return 0;
}


После компиляции и запуска, приложение наебнется с ошибкой.

Пишем Dockerfile для этого приложения

FROM ubuntu:22.04

# Install tools
RUN apt update \
&& apt -y install \
build-essential \
gdb \
&& rm -rf /var/lib/apt/lists/*

# Build the application
COPY ./ /src/
WORKDIR /src/
RUN g++ main.cpp -o app

CMD ["/src/app"]


Тот кто в теме, сможет его прочитать и понять. Если не понятно, гугли и разбирай, качнешь свой девопсовый скиллз.


Запускаем контейнер с нужными опциями:

docker run \
--init \
--ulimit core=-1 \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest


Разбираем опции:

--init — гарантирует корректную обработку сигналов внутри контейнера

--ulimit — устанавливает неограниченный размер core dump для процессов внутри контейнера

--mount — монтирует /tmp из хоста в контейнер, чтобы дампы, создаваемые внутри контейнера, были доступны после его остановки или удаления

Здесь важно: путь source на хосте должен совпадать с тем, который задан в шаблоне core pattern.

После того как приложение наебнется, core dump будет сохранён на хостовой машине в директории /tmp.

ls /tmp/core*
# /tmp/core.app.5


Анализируем дамп с помощью gdb

Такс, мы получили core dump и он у нас лежит на хостовой машине, но рекомендуется открыть его внутри контейнера. Контейнер должен быть из того же образа, в котором компилилось приложение.

Это поможет убедиться, что все зависимости (библиотеки и прочая хуитень) доступны.

docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
application:latest \
bash


Если в образе нет исходного кода, можно допом смаунтить исходники:

docker run \
-it \
--mount type=bind,source=/tmp/,target=/tmp/ \
--mount type=bind,source=<путь_к_исходникам_на_хосте>,target=/src/ \
application:latest \
bash


Теперь внутри контейнера запускаем:

gdb app /tmp/core.app.5


После загрузки дампа можно выполнить команду bt (backtrace), чтобы увидеть трассировку стека:

(gdb) bt
#0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f263f378921 in __GI_abort () at abort.c:79
#2 0x000055f9a9d16653 in foo() ()
#3 0x000055f9a9d1665c in main ()


Команда поможет определить, где именно произошел факап.

Давай быстренько разберем ошибку.

#0 и #1 показывают, что процесс получил сигнал 6 (SIGABRT) и завершился через abort()

#2 — вызов произошёл внутри функции foo()

#3main() вызвал foo()

В исходнике было:

void foo() {
std::abort();
}


То есть ошибка здесь не баг компиляции или рантайма, а намеренно вставленный вызов std::abort(), который и приводит к аварийному завершению и генерации core dump.

Если у тебя docker-compose, то все флаги (--init, --ulimit, --mount и т.д.) применимы и для него. То есть отладку можно легко адаптировать.

Хуй знает чё еще написать, завтра тему дебага продолжим, чет в конце года много траблшутинга навалило разнообразного.

🛠 #linux #debug #dev

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
545
Здесь: я как-то поднимал проблему с торможением 1c на postgres.

🔤🔤🔥🔤🔤🔤🔤

Благодаря нашему коллеге @ovchinnikovmy, дело сдвинулось с мертвой точки. Спасибо ему большое за консультации и рекомендации по PG.

Мы начали попытки оптимизировать работу postgres для нашей задачи. И сразу столкнулись с проблемой. Ну, оптимизировали. А насколько?

Улучшение есть, а кто был виноват в тормозах PG или 1С?

Все может прекрасно работать в тестах, и становится колом, когда идет интенсивная работа в нескольких базах одновременно. Где горлышко бутылки - число ядер, частота или скорость диска, или может пора памяти добавить?

Там маленькая конторка. Фактически один сервак. Не будешь же zabbix ради этого ставить.

Онлайн можно посмотреть через nmon, top/htop. nmon даже позволяет записывать данные в лог, и есть программа, которая позволяет генерить html с отчетами, но там все интегрально. По системе. А хочется по процессам.

Остановился на пакете sysstat. Это такой консольный zabbix. Он позволяет собирать статистику по процессам. Анализировать можно память, проц, диск, стэк. Причем по каждому PID в отдельности и прямо в консоли. В общем, все, что нужно. Для большего удобства я набросал скрипт.

#!/bin/bash

# 20251005
# apt install sysstat gawk
# работа с 9 до 18, запись с 8:30 до 18:30
# запуск через cron
# 30 8 * * * /root/work/stat/stat.sh &

declare -x PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
declare -i INTERVAL_SEC=10
declare -i COUNT=3600 # итого 10 часов
declare -i WEEK_DAY;printf -v WEEK_DAY "%(%-u)T"
declare LOG="$0_${WEEK_DAY}.csv"

pidstat -r -l -d -H -h -U -u $INTERVAL_SEC $COUNT |
gawk 'NR<4;$2=="usr1cv83"||$2=="postgres"{$1=strftime("%Y%m%d_%H%M%S",$1);print}'>"$LOG"


Он собирает статистику каждые 10 сек по двум пользователям postgres (PG) и usr1cv83 (1С) в csv-лог (разделитель пробел, но это можно исправить).

Поскольку лог текстовый, дальше его можно вертеть с помощью awk/sort или просто в LibreOffice Calc.

pidstat ключи:

-r - память
-l - командная строка процесса
-d - диск
-h - табличный режим
-H - время unix
-U - username
-u - проц

gawk ключи:

NR<4 - заголовок (легенда) из трех строк
$2=="usr1cv83"||$2=="postgres" - фильтрация по username
$1=strftime("%Y%m%d_%H%M%S",$1) - удобный формат времени.

LOG="$0_${WEEK_DAY}.csv" - Недельная ротация. По одному на день.

🛠 #debug #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
39
Сколько линуксоида не корми, он все равно на винду поглядывает.

Накопал я тут тебе WinBoat.

Эта штука запускает Windows-приложения прямо в Linux с максимально «нативной» интеграцией: окна приложений — как обычные X/Wayland-окна, общий доступ к файлам и даже полноценный Windows-десктоп.

Что в коробке

- Запустит любое приложение, которое работает в Windows — в том числе коммерческие программы.

Ради прикола запустил Excel, ну что сказать, минимум приседаний и танцев с бубном. Работает прям отлично.

- Окна приложений интегрируются в рабочий стол Linux, не просто RDP в едином окне, а RemoteApp-композитинг.

- Автоматические инсталляции через GUI — выбираешь параметры, остальное делает WinBoat.

- Файловая интеграция, домашний каталог монтируется в винду.

- Полный Windows-десктоп по запросу (если нужно работать внутри полноценной виртуальной машины).

Что под капотом

Это Electron-приложение + сопутствующий «guest server». Windows запускается как VM внутри контейнера Docker. А между хостом и гостем общаются через WinBoat Guest Server.

Затем для прорисовки отдельных окон используется FreeRDP + Windows RemoteApp — это и даёт ложное ощущение нативности.

Штука интересная возможно бы я ее использовал, если бы плотно сидел на линуксе.

Так что приглядись, глядишь установишь в хозяйстве приживется.

🛠 #утилиты #utilites #linux

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
74
Фиксим кривой Exit Code в docker

Во время работы с docker контейнером наткнулся на неочевидный код выхода (exit code).

Про exit codes (коды выхода) писал тут


Суть проблемы: Когда программа внутри контейнера падает с abort(), Docker возвращает неправильный код выхода. Вместо ожидаемого 134 (128 + SIGABRT), контейнер отдаёт 139 (128 + SIGSEGV).

То есть контейнер маскирует реальную причину падения приложения. Соответственно дальнейший дебаг не имеет смысла, потому что код выхода не соответствует действительности.

Давай проверим:

#include <cstdlib>

int main() {
std::abort();
return 0;
}


Пишем Dockerfile:

FROM ubuntu:22.04

RUN apt-get update \
&& apt-get -y install \
build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY ./ /src/
WORKDIR /src/
RUN g++ main.cpp -o app

WORKDIR /
CMD ["/src/app"]


Собираем и запускаем:

docker build -f ./Dockerfile -t sigabort_test:latest .
docker run --name test sigabort_test:latest ; echo $?


А на выходе у нас код: 139.

В примере выше код выхода — 139 = 128 + 11, где 11 соответствует SIGSEGV (ошибка сегментации), а не 134 = 128 + 6, что был бы SIGABRT (аварийное завершение).

Чтобы это пофиксить, нужно захерачить хак:

CMD ["bash", "-c", "/src/app ; exit $(echo $?)"]


docker run --name test sigabort_test:latest ; echo $?
bash: line 1: 6 Aborted /src/app
134


После этого контейнер будет возвращать корректный код 134.

Вариант рабочий, но костыльный. Правильнее использовать ключ --init.

Если запустить контейнер с флагом --init, используя исходную команду CMD ["/src/app"], мы получим ожидаемый 134 код. Что нам и нужно.

docker run --init --name test sigabort_test:latest ; echo $?

134


Почему init все починил?

Давай копнём глубже. В Linux процесс с PID 1 (init) имеет нестандартную семантику сигналов:

- Если у PID 1 для сигнала стоит действие «по умолчанию» (никакого обработчика), то сигналы с действием terminate игнорируются ядром. Это сделано, чтобы случайным SIGTERM/SIGINT нельзя было «уронить» init.

- PID 1 должен забирать зомби-процессы (делать wait() за умершими детьми). Если этого не делать, накопятся зомби.

- PID 1 обычно пробрасывает сигналы дальше — тому «настоящему» приложению, которое оно запускает.

Когда мы запускаем контейнер без --init, приложение становится PID 1.

Большинство обычных приложений (на C/C++/Go/Node/Java и т.д.) не написаны как «инит-системы», они не настраивают обработку всех сигналов, не занимаются «реапингом» детей и не пробрасывают сигналы. В результате вылазиют баги.


Наш сценарий с abort() (который поднимает SIGABRT) упирается именно в правила для PID 1. abort() внутри процесса поднимает SIGABRT.

Для обычного процесса с PID ≠ 1 это приводит к завершению с кодом 128 + 6 = 134. Но если процесс — PID 1, ядро игнорирует «терминирующие» сигналы при действии по умолчанию. В результате стандартные ожидания вокруг SIGABRT ломаются.

Ну а дальше вступают в силу детали реализации рантайма/сишной библиотеки, как именно контейнерный рантайм считывает статус.

На практике это может приводить к тому, что ты видишь 139 (SIGSEGV) вместо ожидаемого 134 (SIGABRT).

И тут проблема не в docker, а в том, что приложение неожиданно оказалось в роли init-процесса и попало под его особые правила.

Вот и вся наука. Изучай.

🛠 #docker #devops #linux #debug

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
568
Fedora Post Install

Набрёл тут на Bash поделку CRIMS0NH4T.

В двух словах — делает за тебя необходимую рутину после чистой установки Fedora.

— Настройка DNF
— Установка RMP Fusion
— Установка кодеков
— GPU драйвера (Intel, Nvidia, AMD)
— Тюнинг производительности
— Оптимизация Gnome

Как я понял, версия прям ранняя альфа-альфа и активно допиливается автором.

Я бы минимально запатчил так:

- set -u
+ set -euo pipefail
+ IFS=$'\n\t'

- readonly LOG_FILE="/tmp/crimsonhat_$(date +%Y%m%d_%H%M%S).log"
+ LOG_FILE="$(mktemp /tmp/crimsonhat.XXXXXX.log)"
+ readonly LOG_FILE

- run_with_progress() { local message="$1" shift log INFO "$message" if "$@" >/dev/null 2>&1; then return 0 else return 1 fi }
+ run_with_progress() {
+ local message="$1"; shift
+ log INFO "$message"
+ if "$@" >>"$LOG_FILE" 2>&1; then
+ return 0
+ else
+ return 1
+ fi
+}

- disk_type=$(lsblk -d -o name,rota | awk 'NR==2 {print $2}')
- primary_disk=$(lsblk -d -o name,rota | awk 'NR==2 {print $1}')
+ read -r primary_disk disk_type < <(lsblk -dn -o NAME,ROTA | head -n1)
...
- current_scheduler=$(cmd < "$scheduler_path" 2>/dev/null | grep -o '\[.*\]' | tr -d '[]')
+ current_scheduler=$(tr -d '\n' < "$scheduler_path" | sed -n 's/.*\[\(.*\)\].*/\1/p')


Хотя set -euo pipefail тут конечно спорный вариант, можно наступить на грабли.

pipefail заставляет конвейер возвращать ошибку, если любая команда в скрипте упала. Так что применив set -u автор обезопасил себя от неочевидных багов.


Как концепт, вполне можно форкнуть и подсмотреть какие-то штуки, возможно в своих поделках тебе что-то сгодится.

🛠 #linux #tweaks

@bashdays @linuxfactory @blog
Please open Telegram to view this post
VIEW IN TELEGRAM
523