#!/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 <&2 echo "Используйте: pinstall, piproject, deploypi, multideploypi, pitools_help" >&2 exit 1 ;; esac fi