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

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

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

MAX: https://max.ru/bashdays

Курс: @tormozilla_bot
Блог: https://bashdays.ru
Download Telegram
Забавная хуита. Виндовз что с тобой?

Короче если создать текстовый файл с содержимым «This content is no longer available.» и сохранить его, винда начинает жестко угорать.

Мол, вы пиздец хакер и только что создали вирус под названием Trojan:Win32/Casdet!rfn. Касдет-пиздет.

Trojan:Win32/Casdet!rfn – это вредоносная программа, относящаяся к категории удаленного доступа (RAT). Она позволяет злоумышленникам получить контроль над вашим компьютером, красть ваши данные и устанавливать другие вредоносные программы.


Прикольно. Что-то из оперы EICAR

X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*


По-видимому, это коллизия хэшей. Потому что только Windows Defender его детектит, вот репорт с вирус-тотал.

tags: #мозготрах

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
74
Ну и пекло, по ощущениям +60 и комары. Сегодня на изи, KPI нулевой.

Узнаем имя и путь последнего файла/каталога. Иногда нужно отработать подобный кейс, чтобы произвести какие-то манипуляции над последним файлом в каталоге.

ls -t ~/backups/* | head -1


Выдаст полный путь до файла/каталога. В примере папка с бекапами. Чтобы не ебстись и городить кишку с find, у ls есть для этого ключик для сортировки.

Ну и с помощью head, забираем имя последнего файла/каталога.

Всё, пойду дальше плавиться…

tags: #bash #linux

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
1091
Со шрамами приходит сила

И снова здрасти. Хуй знает очевидная эта штука для тебя или нет, но я все равно ее подсвечу.

Предположим у тебя есть какой-то проект описанный в docker-compose.yml. Вся эта чача раскатывается через пайплайны gitlab на продакшен.

Из того что я постоянно вижу — После сборки проекта, все собранные файлы копируются на продакшен с помощью rsync, scp или подобных утилит, ну и ранится все это дело через ssh.

Например:

script 
- билд хуилд
- rsync -avz --delete ./ user@$prod:/var/www`
- ssh user@$prod "docker compose up -d"


1. Что-то там собралось
2. Файлы синканулись с продом
3. Запустилось если смогло

Хуита! Всё делается изящнее. Ща покажу.

stage: deploy
image:
name: docker/compose
script:
    - docker-compose -H "ssh://root@$prod" down --remove-orphans
    - docker-compose -H "ssh://root@$prod" pull
    - docker-compose -H "ssh://root@$prod" up -d


Используем мощности docker-compose, не привлекаем никакие rsync, scp и т.п. А сразу нативно запускаем на проде наш docker-compose.yml файл.

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

В штыки не воспринимай, способ с rsync и т.п. вполне годный. Но делюсь, тем, чем пользуюсь сам. У меня телеграм боты так деплоятся.

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

tags: #devops

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
208
а у кого какие бест практики подобных деплоев? 👆 давайте в комментах найдем эти практики, будет всем полезно, чтоб гавно не делать впредь

скидывайте, не бойтесь, любые даже самые топорные решения, главное не ругайтесь
35
Сегодня подъехал экземпляр «Релиз», Лемитед блядь эдищен.

Если моя ленивая жопа завтра соизволит сходить на почту, то завтра и разыграем эту колоду среди подписчиков нашего Пантеона.
123
Разыгрывать будем 👆 только среди подписчиков могзотрах, чатика и полезняшек, королевство кривых ебал — мимо. Так что не облажайся, подпишись на все три.
42
Сегодня будем искать самое длинное слово в текстовом файле. На pure bash. Зачем? Джаст фор фан, чтоб мозги подразмять. А вообще эта задачка из методички для собесов.

Создаем текстовый файл bashdays.txt с таким содержимым:

hello world хуйпиздаджигурда bashdays комутохеровато бидон


Пишем скрипт:

#!/bin/bash

longest=0
for word in $(<bashdays.txt)
do
len=${#word}
if (( len > longest ))
then
longest=$len
longword=$word
fi
done
printf 'Самое длинное слово %s и его длина %d.\n' "$longword" "$longest"


В результате получаем: Самое длинное слово «хуйпиздаджигурда» и его длина 16.

Алгоритм таков: цикл проходит по каждому слову в файле, вычисляет длину текущего слова len=${#word} и сохраняет результат в переменную len. Ну и логика больше-меньше.

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

Вечером еще чтиво подъедет. На связи!

tags: #bash

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
90
Отличный способ запихать все свои crontab джобы в git репозиторий и раскатывать по желанию на сервера.

Подойдет для хейтеров всяких ансамблей, паппетов и других бродячих цирков абсурда.

По мотивам этого поста про факап с кронами и его восстановление через grep.


Короче создаешь репу в гите, создаешь текстовый файл supercron. И в этот файл переносишь все свои действующие джобы. Коммит && Пуш --форцед инда мастер оф паппетс.

Ну а дальше элементарно. Накидываем чото подобное:

#!bin/bash

REPO_DIR=/tmp
CRON_FILE=/tmp/supercron
cd $REPO_DIR
git pull origin master
crontab $CRON_FILE


Суть — забрать из репы файл supercron и импортировать жопы джобы. Импорт осуществляется последней строкой crontab /tmp/supercron.

Сразу предупрежу, твой /var/spool/cron/crontab/root перетрется без предупреждения содержимым файла supercron.

Можешь этот скрипт повесить на крон и сервер «даст ёбу». Крон, который импортирует крон. 💃 Идеальная рекурсия.

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

Короче ништяк костыль. Инфраструктура как кот! Не благодари )

tags: #devops #bash

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
932
🔥 Кишочки. Короче познавательное.

В документации для команды eval довольно пространное описание. Давай сегодня с ней поковыряемся.

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

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

Короче дохера всего. Полный список можешь найти в разделах SIMPLE COMMAND EXPANSION и EXPANSION выполнив:

man 1 bash


Ну дак вот, команда eval запускает эту обработку кода. Но прежде чем команда eval будет запущена, код должен быть обработан.

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

Очевидно? Совсем нет. Но вот оно так работает, чо с него взять.

Эта возможность используется по-разному, например для генерации и последующего исполнения кода.

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

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

Расширение переменной в строке с командой echo происходит гораздо позже, чем расширение фигурных скобок.

Поэтому используем eval. Первая обработка развернет переменную. Вторая, которая запустит eval, развернет конструкцию с фигурными скобками.

str={1..10}   
echo $str
eval echo $str


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

{1..10}
1 2 3 4 5 6 7 8 9 10


Другой пример. Здесь из-за использования одинарных кавычек, переменной присваивается указанная строка, а не значение переменной.

И обработка строки делает расширение переменной один раз. Получаем значение переменной s0. Количество обработок строки равно количеству eval плюс один.

(s0=100 ; s1='$s0' ; s2='$s1' ; echo "$s2")
(s0=100 ; s1='$s0' ; s2='$s1' ; eval echo "$s2")
(s0=100 ; s1='$s0' ; s2='$s1' ; eval eval echo "$s2")


На экране ты увидишь:

$s1
$s0
100


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

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

И раз не указан дескриптор, файл будет ассоциирован со стандартным потоком ввода.

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

(fd=6 ; exec "$fd"</dev/null) 
(fd=6 ; eval exec "$fd</dev/null")
(fd=6 ; eval exec "$fd"\</dev/null)
(fd=6 ; eval exec "$fd"'</dev/null')


В первой строке: /dev/null на stdin и пытается запустить 6.

Круглые скобки использовались, чтобы не засорять текущую оболочку. И чтобы она не завершилась на первом примере. Из-за того что на её стандартный ввод повесили /dev/null. Проверяем какой дескриптор ассоциирован с устройством /dev/null.

bash -c 'shopt -s execfail ; fd=6 ; exec $fd</dev/null ; ls -l /proc/$$/fd'  
bash -c 'shopt -s execfail ; fd=6 ; eval exec $fd\</dev/null ; ls -l /proc/$$/fd'
bash -c 'shopt -s execfail ; fd=6 ; eval exec "$fd</dev/null" ; ls -l /proc/$$/fd'


Этот «хак» с перенаправлением можно использовать для исследований. Когда требуется определённый диапазон дескрипторов.

Почитать: man 1 bash, разделы:

# REDIRECTION  
# SHELL BUILTIN COMMANDS -> eval
# SHELL BUILTIN COMMANDS -> exec
# SHELL BUILTIN COMMANDS -> shopt


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

Хороших выходных!

tags: #bash

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
481
🚜 Синий трактор едет к нам

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


Про Gitlab!

Заебали блядь, мне чо каждый день это гавно обновлять? Ладно бы там нажал кнопку как в wordpress и забыл. А нет, надо хуеву гору телодвижений сделать.

И в 100% эта залупа с первого раза никогда не заводится, то миграции ручками надо запускать, то еще 10 версий предыдущих накатить и молиться.

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

Либо хуй пополам, либо пизда в дребезги, другого не дано.


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

И на закуску фаталити — обновку накатывать нужно поздно ночью, либо в выходные, когда разработчики в режиме AFK. Хуита какая-то!

Можно и в будни, но заебут вопросами — а когда заработает?

Короче я такие апдейты на хую Лунтика вертел.


Хоть отдельного человека нанимай, чтобы каждый день обновки катил в три часа ночи.

Что я буду с этим делать? НИЧЕГО! Пусть нахуй всё взламываеют и пиздят, заебали!

Пардон. Накипело!

ps: так, мне еще вам конкурс придумывать сегодня, так что чуть позже увидимся, пойду успокоительное пить да Зельду на свиче задрачивать.


tags: #рабочиебудни

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
138
Так, пора закончить с этими картами.

Короче, представляем ситуацию, что тебя нахуй уволили или не заплатили, а ты еще тот злой гений.

Задача — нужно применить все свои навыки заподлостроения в инфраструктуре и знатно усложнить жизнь этим гадам. У тебя есть root доступ ко всему и ты отрабатываешь две недели.

Условия — на тебя не должны подумать и взять за жопу.

Пиши в комментарии свой гениальный план. Чей коммент наберет больше реакций, тому и карты в руки.

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

Итоги подведем в следующую пятницу. Доставка по РФ и Беларусь.

Пару колод отдадим вторым и третьим местам, но нужно будет подождать, на руках у меня их щас нет. Там ченить придумаем.

Но если прям чешется, можешь заказать у Кати через Авито.

Погнали сочинять! Может в будущем кто-то и воспользуется твоим гениальным планом 🥳
Please open Telegram to view this post
VIEW IN TELEGRAM
39
Выходные заебись!

Сумма значений со стандартного ввода, или из файла.

➡️ Datamash

seq 1000 | datamash sum 1  
datamash sum 1 < <(seq 1000)


➡️ Awk

seq 1000 | awk '{sum += $1}END{print sum}'  
awk '{sum += $1}END{print sum}' <(seq 1000)


➡️ Dc

seq 1000 | dc -e '0d[?+2z>a]salaxp'  
dc -e '0d[?+2z>a]salaxp' < <(seq 1000)


Последнее решение самое занимательное. И самое небезопасное, потому что данные управляют программой.

Вообще во всех трёх случаях очень желательно очищать входные данные.

Почитать:

man datamash  
man seq
man awk
man dc


Да будет так!

tags: #bash

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
49
Здрасти. Существуют самописные скрипты, которые при запуске устанавливают необходимый набор программ. Этакая автоматизация на коленке, быстро, удобно, никаких ансиблов и паппетов.

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

Добавить в скрипт reboot на каждую команду? Да не, хуйня какая-то. Тут на помощь приходит reboot-required.

Ща покажу. Есть однострочник:

apt update && apt -fy upgrade && [ -f /var/run/reboot-required ] && shutdown -r now


Первая часть понятна, обновили, апгрейднули, а дальше идет магия.

В квадратных скобках конструкция, которая проверяет наличие файла /var/run/reboot-required и если он есть, то выполняется перезагрузка.

Файл reboot-required создается, если система считает, что после обновления требуется перезагрузка. Если файл существует, возвращается true. А если true, то запускается -r = reboot.

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

tags: #bash #linux

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
205
Такс. Вопрос из зала — как через bash скрипт запросить у пользователя пароль, но чтобы при вводе пароля отображались звездочки?

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

Кишка первая.

a=''; while read -n 1 -s c; do [ "$c" = '' ] && break; echo -n '*'; a="$a$c"; done


Согласен, выглядит хуёва, но вторая на визуал еще хуёвие.

В этой кишке есть нюанс, она НЕ работает с кнопкой Backspace, то есть ты не сможешь отредактировать, то что уже ввел.

Кишка вторая.

a=''; while read -n 1 -s c; do [ "$c" = '' ] && break; if [ "$c" = $'\x7f' ]; then [ "$a" != "" ] && { a=${a:0:-1}; printf "\b \b"; }; else echo -n '*'; a="$a$c"; fi; done;


А тут уже более совершенный вариант, с рабочей кнопкой Backspace.

Чо делать дальше? Ну логично же, в переменную «a» записывается введенный пароль, а дальше делай с ним все что захочешь.

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

tags: #bash

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
94
Про интересный факап и траблшутинг.

Обратился клиент — у нас mysql реплика не работает. Вернее работает, но отстала всего лишь на 3 месяца.

На вопрос — а хули вы 3 месяца сидели, у вас же мониторинг есть?

Очевидный ответ — да, есть, но оно молчит.

Ладно, полез разбираться. SHOW SLAVE STATUS\G;

А там ошибки брынчат, как хуи в бидоне.

Error 'Cannot delete or update a parent row: a foreign key constraint fails'


Ну это ладно, ошибка и ошибка, тут понятно что делать — НИЧЕГО.

Так как реплика используется чисто аналитиками на потыкать, игнорим эту ошибку через my.cfg. Быстрофикс.

Для продуктовой реплики такое делать - НЕЛЬЗЯ!

Самый важный вопрос — какого хера мониторинг 3 месяца молчал?

Тут уже интереснее. Реплика заведена в prometheus, экспортеры есть, все дела.

Но из графаны сервер пропал, хотя год назад я ее точно там видел.

Думаем… Думаем… Смотрю графану, мониторится поле: replica_seconds_behind

Хм, лезу обратно на реплику, а там сроду нет mysql_exporter. Что же это тогда такое?

Копаем вглубь и видим, что node_exporter мониторит папку /tmp на наличие файликов .prom.

Ага… То есть метрики с mysql реплики собирает какой-то bash скрипт по крону, генерит текстовичок и отдает в prometheus.

Типа такого:

replica_slave_io_running{host="replica"} 1
replica_seconds_behind{host="replica"} 1234567


Да, оно прекрасно работало, до момента пока не вылезла ошибка: Cannot delete or update a parent row

Соответственно текстовый файл replica.prom получился в таком формате:

replica_slave_io_running{host="replica"} 1
replica_seconds_behind{host="replica"} Null


Ну а дальше prometheus такое распарсить не смог (он хочет циферки, а не буковки) и тихонечко вывел эту ноду из графаны и вообще отовсюду. Ну и аллерты в придачу. На что им тригериться если ноды нет нигде?

Прикол в том, что во время возникновения ошибки, поле Seconds_Behind_Master в mysql принимает значение Null, а не продолжает дальше считать на сколько отстала реплика.

А вот и bash скрипт, который собирал метрики:

#!/bin/bash

MAINDIR=/tmp
METRICS=rstatus.prom
HOST="replica"

SLAVE_IO_RUNNING=$(mysql -e 'SHOW SLAVE STATUS \G' | grep 'Slave_IO_Running'| awk '{print $2}')
SLAVE_SECONDS_BEHIND=$(mysql -e 'SHOW SLAVE STATUS \G' | grep 'Seconds_Behind_Master'| awk '{print $2}')

if [[ "$SLAVE_IO_RUNNING" == "Yes" ]]; then
J=1
echo 'replica_slave_io_running{host="'$HOST'"}' $J > $MAINDIR/$METRICS
else
J=0
echo 'replica_slave_io_running{host="'$HOST'"}' $J > $MAINDIR/$METRICS
fi

echo 'replica_seconds_behind{host="'$HOST'"}' $SLAVE_SECONDS_BEHIND >> $MAINDIR/$METRICS


Работа над ошибками проведена, инцидент разобран. Ни одна жопа на ретроспективе пока не пострадала, но возможно дело времени.

Как писать подобные экспортеры я накидывал в этом посте.


И всегда помни — если изобретаешь велосипед, всегда обрабатывай эксепшены!

tags: #devops #debug #bash #monitoring

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
78
Тут персонаж один пристал, говорит что у него в Java есть какой-то «мусожор», а вот в Bash такого нет и поэтому от наших скриптов одни проблемы.

Термин «Мусожор» видимо профессиональный и является пресловутым «Garbage Collector».

К Java я отношусь ровно, оно меня не трогает и я его тоже.

Как говорится — Одно гавно другому не равно. Поэтому хуисосить не буду.

Давай проведем эксперимент:

1. ps -o rss -p $$
2. var=$(printf "%s\n" {1..100000})
3. ps -o rss -p $$
4. var="bashdays"
5. ps -o rss -p $$


В первой строчке выводится количество физической памяти в килобайтах, используемой текущим процессом оболочки. У меня выдало 4348.

Во второй строчке записываем в переменную var содержимое от 1-100000 с переносом на новую строку.

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

Ага, оно стало больше. Было 4348 стало 5276.

В четвертой строчке, перезаписываем var на строку «bashdays» и снова проверяем память.

На этот раз пятая строчка вновь показывает: 4348.

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

Все манипуляции с памятью выполняются на уровне Linux ядра. И нет никакой необходимости рвать жопу и греть голову.

Bash не работает с памятью и объектами, так же как обычные языки программирования.

tags: #bash

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
119
Ёхохо. Не пить алкашку по выходным конечно круто, по итогу все это время компенсируешь работой. Продуктивность зашкаливает. НО, скучно пиздец!

Поэтому приходится отвлекаться игрульками. Гоняя Зельду, невольно задумался, а квесты то очень похожи на алгоритмы в программировании. Порой что аж жопа горит. И их можно давать на собеседованиях. Без знания паттернов хер чо пройдешь.

Ладно, теперь по теме: Реврайтим CTRL+C.

Как прерывать запущенные программы и скрипты тут каждый знает. А как работает изнутри CTRL+C я напишу в следующих постах.

Сегодня что-то вроде на изи потыкать. Понедельник как-никак.

Где применяется реврайт CTRL+C? Да практически нигде, но то, что его можно зареврайтить это факт.

А вообще причины для реврайта разные:

- Избежание конфликтов с программным обеспечением
- Удобство и предпочтения пользователя
- Специальные рабочие процессы
- Случайные нажатия
- Ограниченные возможности клавиатуры
- и т.п.

Давай теперь на практике.

Смотрим дефолтное сочетание

stty -a


stty - утилита, которая настраивает параметры терминала.


Получаем портянку вида:

speed 38400 baud; rows 57; columns 237; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0; -parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany -imaxbel -iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke -flusho -extproc


Смотрим параметр intr, он и указывает на сочетание, которое будет слать SIGINT. По умолчанию как видим оно = CTRL+C.

Давай теперь поменяем эту штуку:

stty intr ^K


Теперь запускаем для теста sleep 1000 и пробуем нажать CTRL+C. Опа и нихуя не происходит.

Поздравляю, реврайт прошел успешно. А теперь нажми CTRL+K и sleep завершится. Клёва? Клёва!

Можешь теперь издеваться над коллегами и ломать им рабочие процессы.

Чтобы сохранить эти настройки, выполняем:

echo 'stty intr ^K' >> ~/.bashrc


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

А можно сделать CTRL+ALT+K ?

Через stty увы такое провернуть не получится.

Но используя xbindkeys такое возможно.

vim ~/.xbindkeysrc

и добавить туда:

"pkill -SIGINT -f ."
Control+Alt + k


Но xbindkeys со своими подводными камнями, оно для Иксов. А если у тебя Wayland то смотри в сторону sway или gnome-keybinding.

Такие дела. Ладно, пошли дальше мир спасать. Хорошего понедельника!

tags: #linux

🔔 @bashdays
Please open Telegram to view this post
VIEW IN TELEGRAM
83