Add CI/CD

This commit is contained in:
peginelab 2025-12-23 02:45:20 +03:00
commit 8ec21b41fa
3 changed files with 1929 additions and 0 deletions

663
pitoolsv2.sh Executable file
View file

@ -0,0 +1,663 @@
#!/bin/bash
# PITOOLSV2 — простой SSH-оркестратор:
# pinstall — создать файл/директорию с правами
# piproject — собрать проект по .prj
# deploypi — деплой одного .prj на один хост
# multideploypi — деплой нескольких блоков MLT+PRJ/DR/FL из одного .prj
# ------------------------- pinstall --------------------------
# pinstall [-h|--here] [-u|--user] [-s|--script] FILE...
# Создаёт файлы/каталоги с умными правами и владельцем.
pinstall() {
local mode=644 owner="$USER" group create_dir=0
# Получаем группу текущего пользователя
group="$(id -gn "$USER" 2>/dev/null || id -gn 2>/dev/null || whoami 2>/dev/null || echo "$USER")"
# Парсим флаги
while [ $# -gt 0 ] && [ "${1#-}" != "$1" ]; do
case "$1" in
-h|--here)
# Наследуем права и владельца от текущей директории
if [ -d . ]; then
mode="$(stat -c '%a' . 2>/dev/null || echo 644)"
owner="$(stat -c '%U' . 2>/dev/null || echo "$USER")"
group="$(stat -c '%G' . 2>/dev/null || echo "$group")"
fi
shift
;;
-u|--user)
# Стандартные права для пользовательских файлов
mode=644
owner="$USER"
group="$(id -gn "$USER" 2>/dev/null || id -gn 2>/dev/null || whoami 2>/dev/null || echo "$USER")"
shift
;;
-s|--script)
# Принудительно исполняемые права
mode=755
shift
;;
-d|--directory)
# Создавать директорию
create_dir=1
shift
;;
--)
# Конец флагов
shift
break
;;
-*)
echo "Использование: pinstall [-h|--here] [-u|--user] [-s|--script] [-d|--directory] ФАЙЛ..." >&2
return 1
;;
*)
# Не флаг - выходим из цикла
break
;;
esac
done
# Проверка: хотя бы один файл должен быть указан
[ $# -eq 0 ] && {
echo "Использование: pinstall [-h|--here] [-u|--user] [-s|--script] [-d|--directory] ФАЙЛ..." >&2
return 1
}
# Счётчик созданных объектов
local created=0
# Обрабатываем все переданные файлы
for f in "$@"; do
local eff_mode="$mode"
# Автоматически делаем скрипты исполняемыми
case "$(basename "$f")" in
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
eff_mode=755
;;
esac
# Убираем возможный / в конце для проверки существования
local check_path="$f"
[[ "$check_path" == */ ]] && check_path="${check_path%/}"
# Проверяем, существует ли уже
if [ -e "$check_path" ]; then
# Существует — обновляем права если нужно
if [ -d "$check_path" ]; then
# Это директория
if [ "$eff_mode" != "644" ] && [ "$eff_mode" != "755" ]; then
chmod "$eff_mode" "$check_path" 2>/dev/null && \
echo " 🔧 Обновлены права директории: $f" >&2
fi
else
# Это файл
if [ "$eff_mode" != "644" ]; then
chmod "$eff_mode" "$check_path" 2>/dev/null && \
echo " 🔧 Обновлены права файла: $f" >&2
fi
fi
continue # Уже существует, пропускаем создание
fi
# Определяем, создавать директорию или файл
local is_dir="$create_dir"
[ "$is_dir" -eq 0 ] && [[ "$f" == */ ]] && is_dir=1
if [ "$is_dir" -eq 1 ]; then
# Создаём директорию
if install -d -m "$eff_mode" -o "$owner" -g "$group" "$f" 2>/dev/null; then
echo " 📂 Создана директория: ${f%/}" >&2
created=$((created + 1))
else
echo " ⚠️ Не удалось создать директорию: $f" >&2
fi
else
# Создаём файл
if install -D -m "$eff_mode" -o "$owner" -g "$group" \
--backup=numbered /dev/null "$f" 2>/dev/null; then
# Проверяем, был ли авто-режим 755
case "$(basename "$f")" in
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
echo " 🔥 AUTO 755 → $f" >&2
;;
*)
echo " 📄 Создан файл: $f" >&2
;;
esac
created=$((created + 1))
else
echo " ⚠️ Не удалось создать файл: $f" >&2
fi
fi
done
[ "$created" -gt 0 ] && echo "✅ Создано объектов: $created" >&2
return 0
}
# ------------------------ piproject --------------------------
# piproject SPEC.prj
# Читает PRJ/DR/FL и создаёт структуру проекта локально.
piproject() {
local spec="$1" root="" curdir="" line="" created_projects=0
# Проверка аргументов
[ -z "$spec" ] && {
echo "Использование: piproject ФАЙЛ.prj" >&2
echo "Пример: piproject мойпроект.prj" >&2
return 1
}
[ ! -f "$spec" ] && {
echo "❌ Файл спецификации не найден: $spec" >&2
return 1
}
echo "🌳 Читаем спецификацию: $spec"
# Проверяем, есть ли хоть одна PRJ: строка
if ! grep -q "^PRJ:" "$spec" 2>/dev/null; then
echo "❌ В файле нет PRJ: определения" >&2
return 1
fi
# Читаем файл построчно
while IFS= read -r line || [ -n "$line" ]; do
# Убираем пробелы в начале и конце
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
# Пропускаем пустые строки и комментарии
case "$line" in
""|"#"*)
continue
;;
PRJ:*)
# Начало нового проекта - определяем корневую директорию
root="${line#PRJ:}"
root="${root#"${root%%[![:space:]]*}"}"
root="${root%"${root##*[![:space:]]}"}"
# Проверяем безопасность пути
if [[ "$root" == *".."* ]] || [[ "$root" == /* ]]; then
echo "⚠️ Предупреждение: путь '$root' может быть небезопасен" >&2
fi
# Создаём корневую директорию
echo "📁 Создаём проект: $root/"
if pinstall -d "$root/"; then
curdir="$root"
created_projects=$((created_projects + 1))
else
echo "Не удалось создать корень проекта: $root" >&2
return 1
fi
;;
DR:*)
# Создание поддиректории
[ -z "$root" ] && {
echo "❌ Сначала определите PRJ: в файле $spec" >&2
return 1
}
local dir_path="${line#DR:}"
dir_path="${dir_path#"${dir_path%%[![:space:]]*}"}"
dir_path="${dir_path%"${dir_path##*[![:space:]]}"}"
# Защита от небезопасных путей
if [[ "$dir_path" == *".."* ]] || [[ "$dir_path" == /* ]]; then
echo "⚠️ Предупреждение: путь '$dir_path' может быть небезопасен" >&2
fi
# Создаём поддиректорию
if [ -n "$dir_path" ]; then
curdir="$root/$dir_path"
if pinstall -d "$curdir/"; then
echo " 📂 Директория: $dir_path"
else
echo " ⚠️ Не удалось создать директорию: $dir_path" >&2
fi
fi
;;
FL:*)
# Создание файла
[ -z "$root" ] && {
echo "❌ Сначала определите PRJ: в файле $spec" >&2
return 1
}
local file_path="${line#FL:}"
file_path="${file_path#"${file_path%%[![:space:]]*}"}"
file_path="${file_path%"${file_path##*[![:space:]]}"}"
if [ -z "$file_path" ]; then
echo "⚠️ Пустое имя файла в FL:, пропускаем" >&2
continue
fi
# Определяем полный путь к файлу
local full_path=""
if [[ "$file_path" == */* ]]; then
# В пути есть слеш - создаём относительно корня
full_path="$root/$file_path"
# Создаём родительские директории если нужно
local parent_dir="${full_path%/*}"
if [ -n "$parent_dir" ] && [ ! -d "$parent_dir" ]; then
pinstall -d "$parent_dir/" >/dev/null 2>&1
fi
else
# Простое имя файла - создаём в текущей директории
[ -z "$curdir" ] && curdir="$root"
full_path="$curdir/$file_path"
fi
# Создаём файл
if pinstall "$full_path"; then
echo " 📄 Файл: $file_path"
else
echo " ⚠️ Не удалось создать файл: $file_path" >&2
fi
;;
*)
# Неизвестная строка
echo "⚠️ Неизвестная строка в $spec (пропускаем): $line" >&2
;;
esac
done < "$spec"
if [ "$created_projects" -gt 0 ]; then
echo "✅ Готово! Создано проектов: $created_projects"
else
echo " Ничего не создано (всё уже существует?)"
fi
return 0
}
# ------------------------ deploypi ---------------------------
# deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ SPEC.prj
# Копирует pitoolsv2 + spec на хост и запускает piproject.
deploypi() {
local host="$1" user="$2" spec="$3"
# Проверка аргументов
[ -z "$host" ] || [ -z "$user" ] || [ -z "$spec" ] && {
echo "Использование: deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ ФАЙЛ.prj" >&2
echo "Пример: deploypi server.ru vasya мойпроект.prj" >&2
return 1
}
[ ! -f "$spec" ] && {
echo "❌ Файл спецификации не найден: $spec" >&2
return 1
}
echo "🚀 DEPLOYpi: $spec$user@$host"
# Определяем путь к текущему скрипту
local self=""
if [ -f "$0" ]; then
self="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
elif [ -n "${BASH_SOURCE[0]}" ]; then
self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
else
self="./pitoolsv2.sh"
fi
# Безопасное получение имени спецификации
local spec_basename="$(basename "$spec")"
spec_basename="${spec_basename//[!a-zA-Zа-яА-ЯёЁ0-9._-]/_}"
echo "🔍 Проверяем соединение с $host..."
# Проверяем доступность SSH
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
"$user@$host" "exit 0" 2>/dev/null; then
echo "Не могу подключиться к $user@$host через SSH" >&2
echo " Проверьте: ssh $user@$host" >&2
return 1
fi
echo "📤 Загружаем инструменты и спецификацию..."
# 1. Копируем сам скрипт на удалённый хост
if ! scp -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"$self" "$user@$host:~/.pitoolsv2.sh" 2>/dev/null; then
echo "Не удалось загрузить pitoolsv2 на $host" >&2
return 1
fi
# 2. Копируем спецификацию проекта
if ! scp -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
"$spec" "$user@$host:~/$spec_basename" 2>/dev/null; then
echo "Не удалось загрузить спецификацию на $host" >&2
return 1
fi
echo "⚡ Выполняем удалённо..."
# 3. Запускаем создание проекта на удалённом хосте
ssh -o ConnectTimeout=30 -o StrictHostKeyChecking=accept-new "$user@$host" "
echo '🔍 Проверяем загруженные файлы...'
if [ ! -f ~/.pitoolsv2.sh ]; then
echo '❌ Ошибка: pitoolsv2 не найден' >&2
exit 1
fi
if [ ! -f ~/'$spec_basename' ]; then
echo '❌ Ошибка: спецификация не найдена' >&2
exit 1
fi
echo '⚙️ Настраиваем окружение...'
# Добавляем загрузку pitools в .bashrc если ещё нет
if ! grep -q 'pitoolsv2' ~/.bashrc 2>/dev/null; then
echo '[ -f ~/.pitoolsv2.sh ] && . ~/.pitoolsv2.sh # PITOOLSv2' >> ~/.bashrc
echo ' ✅ Добавили в .bashrc'
fi
# Загружаем скрипт и выполняем
. ~/.pitoolsv2.sh 2>/dev/null || {
echo '❌ Не удалось загрузить pitoolsv2' >&2
exit 1
}
cd ~ || { echo '❌ Не могу перейти в домашнюю директорию' >&2; exit 1; }
echo '🛠️ Запускаем создание проекта...'
if piproject '$spec_basename'; then
echo '✅ Удалённый проект успешно создан!'
echo ''
echo 'Созданные файлы:'
find . -type f -name '*$spec_basename*' -o -path './*' | head -10
exit 0
else
echo '❌ Ошибка при создании проекта' >&2
exit 1
fi
" 2>&1
local result=$?
if [ $result -eq 0 ]; then
echo "$host: проект '$spec_basename' успешно развёрнут!"
return 0
else
echo "❌ Ошибка выполнения на $host" >&2
return $result
fi
}
# ---------------------- multideploypi ------------------------
# multideploypi INFRA.prj
# Читает файл, разбивает на блоки:
# MLT:host1,host2
# PRJ/DR/FL...
# Каждый блок деплоит на свои хосты через deploypi.
multideploypi() {
local spec="$1"
# Проверка аргументов
[ -z "$spec" ] && {
echo "Использование: multideploypi ФАЙЛ_МУЛЬТИ.prj" >&2
echo "Пример: multideploypi инфраструктура.prj" >&2
return 1
}
[ ! -f "$spec" ] && {
echo "❌ Файл спецификации не найден: $spec" >&2
return 1
}
echo "🌍 MULTIDEPLOYpi: обрабатываем $spec"
local line current_hosts="" tmpdir block_spec prj_seen=0
local block_counter=0
local pids=""
local temp_files=""
# Создаём временную директорию
tmpdir="$(mktemp -d -t pitools-XXXXXX 2>/dev/null)" || {
echo "❌ Не удалось создать временную директорию" >&2
return 1
}
# Функция для очистки временных файлов
cleanup() {
for pid in $pids; do
kill "$pid" 2>/dev/null || true
done
rm -rf "$tmpdir" 2>/dev/null || true
}
# Устанавливаем обработчик прерывания
trap cleanup EXIT INT TERM
# Завершение текущего блока
finish_block() {
local hosts="$1" specfile="$2"
[ -z "$hosts" ] && return 0
[ ! -s "$specfile" ] && return 0
[ "$prj_seen" -eq 0 ] && {
echo "⚠️ Блок без PRJ: директивы, пропускаем" >&2
return 0
}
# Разбиваем хосты по запятым
local hosts_arr
IFS=',' read -r -a hosts_arr <<< "$hosts"
local valid_hosts=0
for h in "${hosts_arr[@]}"; do
h="${h#"${h%%[![:space:]]*}"}"
h="${h%"${h##*[![:space:]]}"}"
[ -z "$h" ] && continue
valid_hosts=$((valid_hosts + 1))
done
if [ "$valid_hosts" -eq 0 ]; then
echo "⚠️ В блоке нет валидных хостов, пропускаем" >&2
return 0
fi
echo "🔧 Блок $block_counter$valid_hosts хост(ов): ${hosts_arr[*]}"
# Запускаем деплой на каждый хост
for h in "${hosts_arr[@]}"; do
h="${h#"${h%%[![:space:]]*}"}"
h="${h%"${h##*[![:space:]]}"}"
[ -z "$h" ] && continue
# Создаём копию спецификации для каждого хоста
local host_spec=""
host_spec="$(mktemp "$tmpdir/spec_${h}_XXXXXX.prj" 2>/dev/null)" || continue
temp_files="$temp_files $host_spec"
cp "$specfile" "$host_spec"
# Запускаем деплой в фоне
(
echo " 🚀 Начинаем деплой на $h..."
if deploypi "$h" "$USER" "$host_spec" > "$tmpdir/deploy_${h}.log" 2>&1; then
echo "$h: деплой успешен"
else
echo "$h: деплой не удался (лог: $tmpdir/deploy_${h}.log)" >&2
fi
) &
pids="$pids $!"
done
}
# Инициализируем первый блок
block_counter=1
block_spec="$(mktemp "$tmpdir/block_${block_counter}_XXXXXX.prj" 2>/dev/null)" || {
echo "❌ Не удалось создать временный файл" >&2
return 1
}
temp_files="$temp_files $block_spec"
echo "📖 Парсим спецификацию..."
# Читаем файл построчно
while IFS= read -r line || [ -n "$line" ]; do
# Убираем лишние пробелы
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
case "$line" in
""|"#"*)
# Комментарии и пустые строки сохраняем для читаемости
[ -n "$block_spec" ] && echo "$line" >> "$block_spec"
;;
MLT:*)
# Новый блок хостов
echo "📦 Нашли MLT блок: ${line#MLT:}"
# Завершаем предыдущий блок
finish_block "$current_hosts" "$block_spec"
# Начинаем новый блок
current_hosts="${line#MLT:}"
block_counter=$((block_counter + 1))
block_spec="$(mktemp "$tmpdir/block_${block_counter}_XXXXXX.prj" 2>/dev/null)" || {
echo "Не удалось создать временный файл для блока $block_counter" >&2
return 1
}
temp_files="$temp_files $block_spec"
prj_seen=0
# Сохраняем MLT строку в новый блок
echo "$line" >> "$block_spec"
;;
PRJ:*)
# Начало описания проекта
echo "$line" >> "$block_spec"
prj_seen=1
;;
DR:*|FL:*)
# Описание директорий и файлов
[ -z "$block_spec" ] && {
echo "❌ DR: или FL: без MLT: блока" >&2
return 1
}
echo "$line" >> "$block_spec"
;;
*)
# Неизвестные строки пропускаем с предупреждением
echo "⚠️ Неизвестная директива, пропускаем: $line" >&2
;;
esac
done < "$spec"
# Завершаем последний блок
finish_block "$current_hosts" "$block_spec"
# Ждём завершения всех фоновых процессов
if [ -n "$pids" ]; then
echo "⏳ Ждём завершения всех деплоев..."
local failed=0
for pid in $pids; do
if wait "$pid"; then
echo "✓ Процесс $pid завершился успешно"
else
echo "✗ Процесс $pid завершился с ошибкой" >&2
failed=1
fi
done
if [ "$failed" -eq 1 ]; then
echo "⚠️ Некоторые деплои не удались. Логи в: $tmpdir"
echo " Посмотреть: ls -la $tmpdir/*.log"
# Не удаляем временную директорию при ошибках
trap - EXIT INT TERM
return 1
else
echo "✅ Все деплои успешно завершены!"
return 0
fi
else
echo " Нет хостов для деплоя"
return 0
fi
}
# -------------------------- help -----------------------------
# Короткая подсказка по доступным командам.
pitools_help() {
cat <<EOF
PITOOLSv2 — Простой SSH-оркестратор для всех
===========================================
Доступные команды:
pinstall [-h|-u|-s|-d] ФАЙЛ... Создать файлы/директории
-h, --here Унаследовать права от текущей папки
-u, --user Стандартные права пользователя (644)
-s, --script Исполняемые права (755)
-d, --directory Создать директорию (не файл)
.sh, .py, .js файлы автоматически получают 755!
piproject ФАЙЛ.prj Создать структуру проекта
Формат:
PRJ:имя_проекта # Корень проекта
DR:подпапка # Создать папку
FL:файл # Создать файл
FL:путь/к/файлу # Файл с путём
# Комментарии разрешены
deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ ФАЙЛ.prj Деплой на один хост
Пример: deploypi server.ru vasya проект.prj
multideploypi ФАЙЛ_МУЛЬТИ.prj Деплой на несколько хостов
Формат:
MLT:хост1,хост2,хост3 # Хосты для этого блока
PRJ:проект # Описание проекта
DR:... # Папки
FL:... # Файлы
pitools_help Показать эту справку
Примеры:
pinstall script.sh # Создаёт script.sh с правами 755
piproject мойпроект.prj # Создать локальный проект
deploypi server1 vasya app.prj # Деплой на один сервер
multideploypi infra.prj # Деплой на несколько серверов
Советы:
- Если файл/папка уже существует, права будут обновлены
- Ошибки не останавливают выполнение (только предупреждения)
- Все пути безопасно обрабатываются
EOF
}
# Автоматическое выполнение команды если скрипт вызван напрямую
if [ "$0" = "${BASH_SOURCE[0]:-$0}" ]; then
case "${1:-}" in
pinstall|piproject|deploypi|multideploypi|pitools_help)
"$@"
;;
"")
echo "PITOOLSv2 загружен успешно!"
echo "Используйте 'pitools_help' для справки."
echo ""
echo "Быстрый старт:"
echo " . ./pitoolsv2.sh # Загрузить в текущую сессию"
echo " piproject пример.prj # Создать проект"
;;
*)
echo "❌ Неизвестная команда: $1" >&2
echo "Используйте: pinstall, piproject, deploypi, multideploypi, pitools_help" >&2
exit 1
;;
esac
fi