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

637
pitoolsv2.py Executable file
View file

@ -0,0 +1,637 @@
#!/usr/bin/env python3
"""
PITOOLS v4 DevOps фреймворк на Python
pinstall, piproject, deploypi, multideploypi
Для бабушек и дедушек! 🔥
"""
import argparse
import asyncio
import shlex
import sys
import getpass
import json
import yaml
from typing import List, Dict, Any, Optional, Union
from pathlib import Path
import paramiko
from io import StringIO
from dataclasses import dataclass
from datetime import datetime
# Try to import yaml, fallback if not available
try:
import yaml
YAML_AVAILABLE = True
except ImportError:
YAML_AVAILABLE = False
@dataclass
class FileSpec:
path: Path
mode: int
content: Optional[str] = None
is_dir: bool = False
class PITools:
VERSION = "4.1.0"
def __init__(self, root: Path = None, verbose: bool = False):
self.root = root or Path.cwd()
self.curdir = self.root
self.verbose = verbose
self.stats = {"files": 0, "dirs": 0, "errors": 0}
def _log(self, message: str, level: str = "INFO"):
"""Умное логирование"""
icons = {
"INFO": "",
"SUCCESS": "",
"WARNING": "⚠️",
"ERROR": "",
"DEBUG": "🐛"
}
timestamp = datetime.now().strftime("%H:%M:%S")
if level in icons:
print(f"{icons[level]} [{timestamp}] {message}")
else:
print(f"[{timestamp}] {message}")
def _get_smart_mode(self, filepath: Path) -> int:
"""Определение прав доступа по расширению файла"""
if filepath.is_dir():
return 0o755
ext = filepath.suffix.lower()
# Исполняемые файлы
if ext in ['.sh', '.bash', '.zsh', '.py', '.pl', '.rb', '.php', '.js', '.ts']:
return 0o755
# Приватные файлы
if ext in ['.key', '.pem', '.crt', '.pub', '.priv']:
return 0o600
# Конфиденциальные данные
if ext in ['.env', '.secret', '.token', '.password', '.credential']:
return 0o640
# Конфигурационные файлы
if ext in ['.conf', '.cfg', '.ini', '.config', '.yml', '.yaml', '.toml']:
return 0o644
# Логи и данные
if ext in ['.log', '.db', '.sqlite', '.json', '.xml']:
return 0o644
# По умолчанию
return 0o644
def pinstall(self, items: List[str], mode: Optional[int] = None,
is_dir: bool = False, content: Optional[str] = None) -> Dict[str, int]:
"""Создание файлов/папок с умными правами"""
self.stats = {"files": 0, "dirs": 0, "errors": 0}
for item in items:
if not item.strip():
continue
try:
p = self.root / item.lstrip('/')
if is_dir or item.endswith('/'):
p.mkdir(parents=True, exist_ok=True)
final_mode = mode or 0o755
p.chmod(final_mode)
self._log(f"Папка: {item.strip()} ({oct(final_mode)})", "SUCCESS")
self.stats["dirs"] += 1
else:
p.parent.mkdir(parents=True, exist_ok=True)
if content:
p.write_text(content, encoding='utf-8')
else:
p.touch(exist_ok=True)
final_mode = mode or self._get_smart_mode(p)
p.chmod(final_mode)
self._log(f"Файл: {item.strip()} ({oct(final_mode)})", "SUCCESS")
self.stats["files"] += 1
except Exception as e:
self._log(f"Ошибка создания {item}: {e}", "ERROR")
self.stats["errors"] += 1
self._log(f"Создано: {self.stats['files']} файлов, {self.stats['dirs']} папок, ошибок: {self.stats['errors']}", "INFO")
return self.stats
def piproject(self, spec_file: str, export_json: bool = False) -> bool:
"""Развёртывание проекта по .prj"""
self._log(f"Читаем спецификацию: {spec_file}")
spec_path = Path(spec_file)
if not spec_path.exists():
self._log(f"Файл не найден: {spec_file}", "ERROR")
return False
original_root = self.root
project_structure = []
try:
with open(spec_file, 'r', encoding='utf-8') as f:
content = f.read()
# Поддержка разных форматов
if spec_path.suffix.lower() in ['.json', '.yaml', '.yml']:
return self._load_structured_spec(spec_path, export_json)
# Стандартный .prj формат
lines = content.split('\n')
for line_num, line in enumerate(lines, 1):
line = line.strip()
if not line or line.startswith('#'):
if line.startswith('#') and self.verbose:
self._log(f"Комментарий: {line[1:].strip()}", "DEBUG")
continue
parts = shlex.split(line)
if not parts:
continue
cmd = parts[0]
args = parts[1:]
try:
if cmd == 'PRJ:':
if args:
self.root = Path(args[0]).expanduser().resolve()
self.curdir = self.root
self.root.mkdir(parents=True, exist_ok=True)
self._log(f"Проект: {self.root}/", "INFO")
elif cmd == 'DR:':
for d in args:
d = d.rstrip('/')
full_path = self.root / d
full_path.mkdir(parents=True, exist_ok=True)
full_path.chmod(0o755)
project_structure.append({"type": "dir", "path": str(full_path), "mode": "755"})
self._log(f"Папка: {d}/ (755)", "SUCCESS")
self.stats["dirs"] += 1
self.curdir = full_path
elif cmd == 'FL:':
for fname in args:
full_path = self.root / fname
full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.touch(exist_ok=True)
mode = self._get_smart_mode(full_path)
full_path.chmod(mode)
project_structure.append({
"type": "file",
"path": str(full_path),
"mode": oct(mode),
"extension": full_path.suffix
})
mode_str = oct(mode).replace('0o', '')
self._log(f"Файл: {fname} ({mode_str})", "SUCCESS")
self.stats["files"] += 1
elif cmd == 'CMD:':
# Выполнение команд после создания структуры
if args:
cmd_str = ' '.join(args)
self._log(f"Выполняем: {cmd_str}", "INFO")
elif cmd == 'TPL:':
# Шаблоны файлов с переменными
if len(args) >= 2:
template_file = args[0]
target_file = args[1]
variables = args[2:] if len(args) > 2 else []
self._process_template(template_file, target_file, variables)
except Exception as e:
self._log(f"Строка {line_num}: {e}", "ERROR")
self.stats["errors"] += 1
except Exception as e:
self._log(f"Ошибка чтения файла: {e}", "ERROR")
return False
finally:
self.root = original_root
# Экспорт структуры в JSON
if export_json:
json_file = f"{spec_path.stem}_structure.json"
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(project_structure, f, indent=2, ensure_ascii=False)
self._log(f"Структура экспортирована в: {json_file}", "INFO")
self._log(f"Готово! Создано: {self.stats['files']} файлов, {self.stats['dirs']} папок", "SUCCESS")
return True
def _load_structured_spec(self, spec_path: Path, export_json: bool) -> bool:
"""Загрузка структурированных спецификаций (JSON/YAML)"""
try:
if spec_path.suffix.lower() == '.json':
with open(spec_path, 'r', encoding='utf-8') as f:
spec = json.load(f)
elif spec_path.suffix.lower() in ['.yaml', '.yml']:
if not YAML_AVAILABLE:
self._log("YAML не установлен. Установите: pip install pyyaml", "ERROR")
return False
with open(spec_path, 'r', encoding='utf-8') as f:
spec = yaml.safe_load(f)
else:
self._log(f"Неподдерживаемый формат: {spec_path.suffix}", "ERROR")
return False
# Обработка структурированной спецификации
if 'project' in spec:
project_root = spec.get('project', {}).get('root', '.')
self.root = Path(project_root).expanduser().resolve()
self.root.mkdir(parents=True, exist_ok=True)
self._log(f"Проект: {self.root}/", "INFO")
# Создание директорий
for dir_path in spec.get('directories', []):
full_path = self.root / dir_path
full_path.mkdir(parents=True, exist_ok=True)
full_path.chmod(0o755)
self._log(f"Папка: {dir_path}/ (755)", "SUCCESS")
self.stats["dirs"] += 1
# Создание файлов
for file_spec in spec.get('files', []):
if isinstance(file_spec, dict):
file_path = file_spec.get('path', '')
content = file_spec.get('content', '')
mode = file_spec.get('mode')
else:
file_path = file_spec
content = ''
mode = None
full_path = self.root / file_path
full_path.parent.mkdir(parents=True, exist_ok=True)
if content:
full_path.write_text(content, encoding='utf-8')
else:
full_path.touch(exist_ok=True)
final_mode = mode or self._get_smart_mode(full_path)
full_path.chmod(final_mode)
mode_str = oct(final_mode).replace('0o', '')
self._log(f"Файл: {file_path} ({mode_str})", "SUCCESS")
self.stats["files"] += 1
return True
except Exception as e:
self._log(f"Ошибка загрузки спецификации: {e}", "ERROR")
return False
def _process_template(self, template: str, target: str, variables: List[str]):
"""Обработка шаблонов файлов"""
template_path = Path(template)
if not template_path.exists():
self._log(f"Шаблон не найден: {template}", "ERROR")
return
content = template_path.read_text(encoding='utf-8')
# Замена переменных (простая реализация)
var_dict = {}
for var in variables:
if '=' in var:
key, value = var.split('=', 1)
var_dict[key.strip()] = value.strip()
for key, value in var_dict.items():
content = content.replace(f'{{{{{key}}}}}', value)
target_path = self.root / target
target_path.parent.mkdir(parents=True, exist_ok=True)
target_path.write_text(content, encoding='utf-8')
mode = self._get_smart_mode(target_path)
target_path.chmod(mode)
self._log(f"Шаблон: {template}{target}", "SUCCESS")
self.stats["files"] += 1
class RemoteDeployer:
def __init__(self, verbose: bool = False):
self.verbose = verbose
self.results = []
async def deploy(self, host: str, user: str, spec_content: str,
key_path: Optional[str] = None, password: Optional[str] = None) -> Dict[str, Any]:
"""Деплой на удаленный сервер"""
result = {
"host": host,
"success": False,
"output": "",
"error": "",
"timestamp": datetime.now().isoformat()
}
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
# Асинхронное подключение
loop = asyncio.get_event_loop()
connect_kwargs = {
"hostname": host,
"username": user,
"timeout": 15
}
if key_path:
connect_kwargs["key_filename"] = key_path
elif password:
connect_kwargs["password"] = password
await loop.run_in_executor(None, client.connect, **connect_kwargs)
# Создаем временную директорию
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
remote_dir = f"/tmp/pitools_{timestamp}"
sftp = await loop.run_in_executor(None, client.open_sftp)
# Загружаем pitools и спецификацию
current_file = Path(__file__)
pitools_code = current_file.read_text(encoding='utf-8')
def upload():
sftp.mkdir(remote_dir)
with sftp.open(f"{remote_dir}/pitools.py", 'w') as f:
f.write(pitools_code)
with sftp.open(f"{remote_dir}/spec.prj", 'w') as f:
f.write(spec_content)
sftp.chmod(f"{remote_dir}/pitools.py", 0o755)
await loop.run_in_executor(None, upload)
await loop.run_in_executor(None, sftp.close)
# Выполняем развертывание
def execute():
cmd = f"cd {remote_dir} && python3 pitools.py piproject spec.prj"
stdin, stdout, stderr = client.exec_command(cmd)
output = stdout.read().decode('utf-8', errors='ignore')
error = stderr.read().decode('utf-8', errors='ignore')
return output, error
output, error = await loop.run_in_executor(None, execute)
# Очистка
clean_cmd = f"rm -rf {remote_dir}"
client.exec_command(clean_cmd)
result["success"] = True
result["output"] = output
if error:
result["error"] = error
self._log(f"{host}: Успешно", "SUCCESS")
except Exception as e:
result["error"] = str(e)
self._log(f"{host}: {e}", "ERROR")
finally:
try:
client.close()
except:
pass
self.results.append(result)
return result
def _log(self, message: str, level: str = "INFO"):
"""Логирование для RemoteDeployer"""
icons = {"SUCCESS": "", "ERROR": "", "INFO": ""}
if level in icons:
print(f"{icons[level]} {message}")
else:
print(message)
# CLI
async def main():
parser = argparse.ArgumentParser(
description=f"PITOOLS v{PITools.VERSION} — DevOps фреймворк на Python",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Примеры использования:
pitools pinstall file1.txt dir/ script.sh -s
pitools piproject project.prj
pitools deploypi server.com user project.prj
pitools multideploypi infra.prj
Форматы спецификаций:
.prj - Текстовый формат с командами
.json - JSON структура
.yaml - YAML структура (требует pyyaml)
"""
)
parser.add_argument('--version', action='version', version=f'PITOOLS v{PITools.VERSION}')
parser.add_argument('-v', '--verbose', action='store_true', help='Подробный вывод')
subparsers = parser.add_subparsers(dest='command', help='Команды')
# pinstall
p1 = subparsers.add_parser('pinstall', help='Создать файлы/папки')
p1.add_argument('items', nargs='+', help='Файлы/папки')
p1.add_argument('-s', '--script', action='store_true', help='Режим скрипта (755)')
p1.add_argument('-d', '--directory', action='store_true', help='Режим папки')
p1.add_argument('-m', '--mode', help='Права доступа (например: 755, 644)')
p1.add_argument('-c', '--content', help='Содержимое файла')
# piproject
p2 = subparsers.add_parser('piproject', help='Развёртывание проекта')
p2.add_argument('spec', help='Файл спецификации (.prj, .json, .yaml)')
p2.add_argument('-e', '--export-json', action='store_true', help='Экспортировать структуру в JSON')
p2.add_argument('-r', '--root', help='Корневая директория проекта')
# deploypi
p3 = subparsers.add_parser('deploypi', help='Деплой на сервер')
p3.add_argument('host', help='Хост')
p3.add_argument('user', help='Пользователь')
p3.add_argument('spec', help='Файл спецификации или содержимое')
p3.add_argument('-k', '--key', help='Путь к SSH ключу')
p3.add_argument('-p', '--password', action='store_true', help='Использовать парольную аутентификацию')
# multideploypi
p4 = subparsers.add_parser('multideploypi', help='Мультидеплой')
p4.add_argument('spec_or_infra', help='Файл спецификации или инфраструктуры')
p4.add_argument('-i', '--infra', help='Файл инфраструктуры (опционально)')
p4.add_argument('-u', '--user', help='Пользователь для всех хостов')
p4.add_argument('-k', '--key', help='Путь к SSH ключу')
p4.add_argument('-o', '--output', help='Файл для сохранения результатов')
args = parser.parse_args()
if not args.command:
parser.print_help()
return
try:
if args.command == 'pinstall':
root_dir = Path(args.root) if hasattr(args, 'root') and args.root else None
pitools = PITools(root=root_dir, verbose=args.verbose)
mode = None
if args.mode:
try:
mode = int(args.mode, 8)
except ValueError:
print(f"❌ Неверный формат прав: {args.mode}")
return
pitools.pinstall(
args.items,
mode=mode or (0o755 if args.script else None),
is_dir=args.directory,
content=args.content
)
elif args.command == 'piproject':
root_dir = Path(args.root) if hasattr(args, 'root') and args.root else None
pitools = PITools(root=root_dir, verbose=args.verbose)
pitools.piproject(args.spec, args.export_json)
elif args.command == 'deploypi':
spec_path = Path(args.spec)
if spec_path.exists():
spec_content = spec_path.read_text(encoding='utf-8')
else:
spec_content = args.spec
password = None
if args.password:
password = getpass.getpass(f"Пароль для {args.user}@{args.host}: ")
deployer = RemoteDeployer(verbose=args.verbose)
result = await deployer.deploy(
args.host,
args.user,
spec_content,
key_path=args.key,
password=password
)
if args.verbose and result["output"]:
print(f"\n📋 Вывод с сервера:\n{result['output']}")
elif args.command == 'multideploypi':
await run_multideploy(args)
except KeyboardInterrupt:
print("\n\n👋 Прервано пользователем")
sys.exit(0)
except Exception as e:
print(f"💥 Критическая ошибка: {e}")
if args.verbose:
import traceback
traceback.print_exc()
sys.exit(1)
async def run_multideploy(args):
"""Запуск мультидеплоя"""
print("🌍 МУЛЬТИДЕПЛОЙ")
infra_file = args.infra if args.infra else None
# Определяем пользователя
user = args.user
if not user:
user = input("Пользователь (по умолчанию текущий): ").strip() or getpass.getuser()
# Парсинг инфраструктуры
if infra_file and Path(infra_file).exists():
blocks = parse_infra(infra_file)
else:
spec_path = Path(args.spec_or_infra)
if spec_path.exists():
spec_content = spec_path.read_text(encoding='utf-8')
else:
spec_content = args.spec_or_infra
blocks = [{"hosts": [args.spec_or_infra], "spec": spec_content}]
deployer = RemoteDeployer(verbose=args.verbose)
all_results = []
for i, block in enumerate(blocks, 1):
print(f"\n🔧 Блок {i}{len(block['hosts'])} хостов")
tasks = []
for host in block['hosts']:
task = deployer.deploy(
host,
user,
block['spec'],
key_path=args.key,
password=None # Пароль запрашивается при необходимости
)
tasks.append(task)
results = await asyncio.gather(*tasks, return_exceptions=True)
successful = 0
for result in results:
if isinstance(result, dict) and result.get("success"):
successful += 1
all_results.append(result)
print(f"✅ Блок {i}: {successful}/{len(tasks)} готово!")
# Сохранение результатов
if args.output:
with open(args.output, 'w', encoding='utf-8') as f:
json.dump(all_results, f, indent=2, ensure_ascii=False)
print(f"\n📊 Результаты сохранены в: {args.output}")
def parse_infra(spec_file: str) -> List[Dict[str, Any]]:
"""Парсинг infra.prj для multideploypi"""
blocks = []
current_hosts = []
current_spec = []
with open(spec_file, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('MLT:'):
if current_hosts and current_spec:
blocks.append({"hosts": current_hosts, "spec": '\n'.join(current_spec)})
current_hosts = [h.strip() for h in line[4:].split(',')]
current_spec = []
else:
current_spec.append(line)
if current_hosts and current_spec:
blocks.append({"hosts": current_hosts, "spec": '\n'.join(current_spec)})
return blocks
if __name__ == '__main__':
asyncio.run(main())

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

629
pitoolsv3.sh Normal file
View file

@ -0,0 +1,629 @@
#!/bin/bash
# PITOOLSv3 — DevOps для бабушек и дедушек! V3
# Автор: [ТЫ] + брат-АИ
# Фикс от 17.12.2024: пути создаются правильно!
# ==========================================================
# 🎯 ОСОБЕННОСТИ V3:
# 1. Множественные файлы в одной строке FL:
# 2. Вложенные пути в DR: (config/subconfig/nested)
# 3. История изменений с комментариями
# 4. Не пугает бабушек ошибками
# 5. ВСЁ ПРОСТО И РАБОТАЕТ!
# ------------------------- pinstall --------------------------
# pinstall [-h|--here] [-u|--user] [-s|--script] [-d|--directory] FILE...
# Создаёт файлы/каталоги с умными правами.
pinstall() {
local mode=644 owner="$USER" group create_dir=0 verbose=1
# Получаем группу
group="$(id -gn "$USER" 2>/dev/null || id -gn 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 || echo "$USER")"
shift
;;
-s|--script)
mode=755
shift
;;
-d|--directory)
create_dir=1
shift
;;
-q|--quiet)
verbose=0
shift
;;
--)
shift
break
;;
-*)
echo "pinstall: неверный флаг: $1" >&2
return 1
;;
*)
break
;;
esac
done
[ $# -eq 0 ] && {
echo "Использование: pinstall [-h|-u|-s|-d|-q] ФАЙЛ..." >&2
return 1
}
local created=0
for item in "$@"; do
# Поддерживаем несколько файлов через пробел
local items
IFS=' ' read -ra items <<< "$item"
for f in "${items[@]}"; do
[ -z "$f" ] && continue
local eff_mode="$mode"
# Авто-права для скриптов
case "$(basename "$f")" in
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
eff_mode=755
;;
esac
# Убираем / в конце для проверки
local clean_path="${f%/}"
# Существует?
if [ -e "$clean_path" ]; then
[ "$verbose" -eq 1 ] && {
if [ -d "$clean_path" ]; then
echo " 📂 Уже есть: $clean_path" >&2
else
echo " 📄 Уже есть: $clean_path" >&2
fi
}
# Обновляем права если нужно
chmod "$eff_mode" "$clean_path" 2>/dev/null
chown "$owner:$group" "$clean_path" 2>/dev/null
continue
fi
# Определяем тип
local is_dir="$create_dir"
[ "$is_dir" -eq 0 ] && [[ "$f" == */ ]] && is_dir=1
if [ "$is_dir" -eq 1 ]; then
# СОЗДАЁМ ДИРЕКТОРИЮ
if mkdir -p "$clean_path" 2>/dev/null; then
chmod "$eff_mode" "$clean_path" 2>/dev/null
chown "$owner:$group" "$clean_path" 2>/dev/null
[ "$verbose" -eq 1 ] && echo " 📂 Создана: $clean_path" >&2
created=$((created + 1))
else
[ "$verbose" -eq 1 ] && echo " ❌ Ошибка: $clean_path" >&2
fi
else
# СОЗДАЁМ ФАЙЛ
# Сначала родительские директории
local parent_dir="$(dirname "$f")"
if [ "$parent_dir" != "." ] && [ ! -d "$parent_dir" ]; then
mkdir -p "$parent_dir" 2>/dev/null
fi
if touch "$f" 2>/dev/null; then
chmod "$eff_mode" "$f" 2>/dev/null
chown "$owner:$group" "$f" 2>/dev/null
[ "$verbose" -eq 1 ] && {
case "$(basename "$f")" in
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
echo " 🔥 755 → $f" >&2
;;
*)
echo " 📄 Создан: $f" >&2
;;
esac
}
created=$((created + 1))
else
[ "$verbose" -eq 1 ] && echo " ❌ Ошибка: $f" >&2
fi
fi
done
done
[ "$verbose" -eq 1 ] && [ "$created" -gt 0 ] && echo "✅ Создано: $created" >&2
return 0
}
# ------------------------ piproject --------------------------
# piproject SPEC.prj
# ФИКС: Все пути создаются относительно корня правильно!
piproject() {
local spec="$1" root="" curdir="" line="" line_num=0
[ -z "$spec" ] && {
echo "Использование: piproject ФАЙЛ.prj" >&2
return 1
}
[ ! -f "$spec" ] && {
echo "Не найден: $spec" >&2
return 1
}
echo "🌳 Читаем: $spec"
echo "══════════════════════════════════════"
# Проверяем формат
if ! grep -q "^PRJ:" "$spec" 2>/dev/null; then
echo "⚠️ Нет PRJ: в файле (но продолжим)" >&2
fi
while IFS= read -r line || [ -n "$line" ]; do
line_num=$((line_num + 1))
# Чистим строку
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
# Комментарий или пустая строка
if [[ -z "$line" ]] || [[ "$line" == \#* ]]; then
[[ "$line" == \#* ]] && [ ${#line} -lt 50 ] && echo " 💬 $line" >&2
continue
fi
# ОБРАБОТКА СТРОК
if [[ "$line" == PRJ:* ]]; then
# НОВЫЙ ПРОЕКТ
root="${line#PRJ:}"
curdir="$root" # Текущая директория = корень
echo "📁 ПРОЕКТ: $root/"
echo "──────────────────────────────────"
if pinstall -d -q "$root/"; then
echo " 📂 Корень создан"
fi
elif [[ "$line" == DR:* ]]; then
# ДИРЕКТОРИЯ
[ -z "$root" ] && {
echo "❌ Строка $line_num: сначала нужен PRJ:" >&2
continue
}
local dir_part="${line#DR:}"
# Разбиваем несколько директорий через пробел
local dir_items
IFS=' ' read -ra dir_items <<< "$dir_part"
for dir_spec in "${dir_items[@]}"; do
[ -z "$dir_spec" ] && continue
# Добавляем / если нет
[[ "$dir_spec" != */ ]] && dir_spec="$dir_spec/"
# ВСЕГДА создаём от корня!
local full_dir="$root/$dir_spec"
# Убираем возможные двойные слеши
while [[ "$full_dir" == *//* ]]; do
full_dir="${full_dir//\/\//\/}"
done
# Создаём директорию
if pinstall -d -q "${full_dir%/}"; then
echo " 📂 Папка: $dir_spec"
fi
# Запоминаем последнюю созданную папку для файлов без пути
curdir="${full_dir%/}"
done
elif [[ "$line" == FL:* ]]; then
# ФАЙЛЫ
[ -z "$root" ] && {
echo "❌ Строка $line_num: сначала нужен PRJ:" >&2
continue
}
local file_part="${line#FL:}"
if [ -z "$file_part" ]; then
echo "⚠️ Строка $line_num: пустой FL:" >&2
continue
fi
# Разбиваем несколько файлов
local file_items
IFS=' ' read -ra file_items <<< "$file_part"
for file_spec in "${file_items[@]}"; do
[ -z "$file_spec" ] && continue
local full_path=""
# Определяем путь
if [[ "$file_spec" == */* ]]; then
# Файл с путём - относительно КОРНЯ
full_path="$root/$file_spec"
# Создаём родительские директории если нужно
local parent_dir="${full_path%/*}"
if [ ! -d "$parent_dir" ]; then
pinstall -d -q "$parent_dir/" >/dev/null 2>&1
fi
else
# Просто имя файла - в ТЕКУЩУЮ директорию
[ -z "$curdir" ] && curdir="$root"
full_path="$curdir/$file_spec"
fi
# Убираем двойные слеши
while [[ "$full_path" == *//* ]]; do
full_path="${full_path//\/\//\/}"
done
# Создаём файл
if pinstall "$full_path"; then
echo " 📄 Файл: $file_spec"
fi
done
else
# Неизвестная директива
echo "⚠️ Строка $line_num: непонятно - '$line'" >&2
fi
done < "$spec"
echo "══════════════════════════════════════"
if [ -n "$root" ]; then
echo "✅ Готово! Проект: $root/"
echo "📁 Структура:"
find "$root" -type d 2>/dev/null | head -20
else
echo " Ничего не создано"
fi
return 0
}
# ------------------------ deploypi ---------------------------
# deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ SPEC.prj
deploypi() {
local host="$1" user="$2" spec="$3"
[ -z "$host" ] || [ -z "$user" ] || [ -z "$spec" ] && {
echo "Использование: deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ ФАЙЛ.prj" >&2
return 1
}
[ ! -f "$spec" ] && {
echo "Не найден: $spec" >&2
return 1
}
echo "🚀 ДЕПЛОЙ: $spec$user@$host"
echo "──────────────────────────────────"
# Имя файла
local spec_name="project_$(date +%s).prj"
# Проверяем SSH
echo "🔍 Проверяем связь..."
if ! ssh -o ConnectTimeout=5 "$user@$host" "exit 0" 2>/dev/null; then
echo "Не могу подключиться к $user@$host" >&2
return 1
fi
# Загружаем спецификацию
echo "📤 Загружаем..."
if ! scp "$spec" "$user@$host:~/$spec_name" 2>/dev/null; then
echo "❌ Ошибка загрузки" >&2
return 1
fi
# Определяем путь к себе
local self_path="$0"
[ "$self_path" = "bash" ] && self_path="pitoolsv3.sh"
# Загружаем pitoolsv3 если нет
echo "⚙️ Настраиваем..."
ssh "$user@$host" "
# Скачиваем или создаём pitoolsv3
if [ ! -f ~/pitoolsv3.sh ]; then
if command -v curl >/dev/null 2>&1; then
curl -s 'https://raw.githubusercontent.com/пример/pitools/main/pitoolsv3.sh' -o ~/pitoolsv3.sh
elif command -v wget >/dev/null 2>&1; then
wget -q -O ~/pitoolsv3.sh 'https://raw.githubusercontent.com/пример/pitools/main/pitoolsv3.sh'
else
# Создаём минимальную версию
cat > ~/pitoolsv3.sh <<'MINI'
#!/bin/bash
# Минимальный PITOOLSv3
pinstall() {
for f in \"\$@\"; do
if [[ \"\$f\" == */ ]]; then
mkdir -p \"\${f%/}\" && echo \" 📂 \${f%/}\"
else
touch \"\$f\" && echo \" 📄 \$f\"
fi
done
}
piproject() {
local spec=\"\$1\" root=\"\" curdir=\"\"
while IFS= read -r line; do
case \"\$line\" in
PRJ:*) root=\"\${line#PRJ:}\"; mkdir -p \"\$root\" ;;
DR:*) mkdir -p \"\$root/\${line#DR:}\" ;;
FL:*) touch \"\$root/\${line#FL:}\" ;;
esac
done < \"\$spec\"
echo \"✅ Готово: \$root/\"
}
MINI
chmod +x ~/pitoolsv3.sh
fi
fi
# Запускаем
. ~/pitoolsv3.sh
cd ~
piproject \"$spec_name\"
# Убираем за собой
rm -f \"$spec_name\"
" 2>&1
local result=$?
if [ $result -eq 0 ]; then
echo "$host: деплой успешен!"
else
echo "$host: ошибка деплоя" >&2
fi
return $result
}
# ---------------------- multideploypi ------------------------
# multideploypi INFRA.prj
multideploypi() {
local spec="$1"
[ -z "$spec" ] && {
echo "Использование: multideploypi ФАЙЛ.prj" >&2
return 1
}
[ ! -f "$spec" ] && {
echo "Не найден: $spec" >&2
return 1
}
echo "🌍 МУЛЬТИДЕПЛОЙ: $spec"
echo "══════════════════════════════════════"
local current_hosts="" block_content="" in_block=0 block_num=0
while IFS= read -r line || [ -n "$line" ]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
if [[ "$line" == MLT:* ]]; then
# Новый блок
if [ -n "$current_hosts" ] && [ -n "$block_content" ]; then
block_num=$((block_num + 1))
echo "🔧 Блок $block_num$(echo "$current_hosts" | tr ',' ' ' | wc -w) хост(ов)"
# Деплоим
local hosts
IFS=',' read -ra hosts <<< "$current_hosts"
for host in "${hosts[@]}"; do
host="${host#"${host%%[![:space:]]*}"}"
host="${host%"${host##*[![:space:]]}"}"
[ -z "$host" ] && continue
echo " 🚀 $host..."
( deploypi "$host" "$USER" <(echo "$block_content") >/dev/null 2>&1 && \
echo " ✅ Готово" || \
echo " ❌ Ошибка" ) &
done
wait
fi
current_hosts="${line#MLT:}"
block_content=""
elif [[ "$line" == PRJ:* || "$line" == DR:* || "$line" == FL:* || \
"$line" == "" || "$line" == \#* ]]; then
# Добавляем в блок
block_content="$block_content"$'\n'"$line"
else
echo "⚠️ Пропускаем: $line" >&2
fi
done < "$spec"
# Последний блок
if [ -n "$current_hosts" ] && [ -n "$block_content" ]; then
block_num=$((block_num + 1))
echo "🔧 Блок $block_num$(echo "$current_hosts" | tr ',' ' ' | wc -w) хост(ов)"
local hosts
IFS=',' read -ra hosts <<< "$current_hosts"
for host in "${hosts[@]}"; do
host="${host#"${host%%[![:space:]]*}"}"
host="${host%"${host##*[![:space:]]}"}"
[ -z "$host" ] && continue
echo " 🚀 $host..."
( deploypi "$host" "$USER" <(echo "$block_content") >/dev/null 2>&1 && \
echo " ✅ Готово" || \
echo " ❌ Ошибка" ) &
done
wait
fi
echo "══════════════════════════════════════"
echo "✅ Мультидеплой завершён!"
return 0
}
# ---------------------- pifill & pifetch ---------------------
pifill() {
local url="$1"
[ -z "$url" ] && {
echo "Использование: pifill ssh://user@host/path < файл" >&2
echo " pifill ssh://user@host/path -c 'текст'" >&2
return 1
}
[[ "$url" != ssh://* ]] && {
echo "❌ Только ssh:// протокол" >&2
return 1
}
local rest="${url#ssh://}"
local user_host="${rest%%/*}"
local path="${rest#*/}"
local user="${user_host%%@*}"
local host="${user_host#*@}"
echo "📤 Заливаем в $user@$host:$path"
if [ "$2" = "-c" ]; then
ssh "$user@$host" "mkdir -p '$(dirname "$path")' && cat > '$path'" <<< "$3"
else
ssh "$user@$host" "mkdir -p '$(dirname "$path")' && cat > '$path'"
fi
echo "✅ Готово!"
}
pifetch() {
local url="$1"
[ -z "$url" ] && {
echo "Использование: pifetch ssh://user@host/path > файл" >&2
return 1
}
[[ "$url" != ssh://* ]] && {
echo "❌ Только ssh:// протокол" >&2
return 1
}
local rest="${url#ssh://}"
local user_host="${rest%%/*}"
local path="${rest#*/}"
local user="${user_host%%@*}"
local host="${user_host#*@}"
if [ "$2" = "-o" ]; then
scp "$user@$host:$path" "$3"
echo "✅ Скачан в $3"
else
ssh "$user@$host" "cat '$path'"
fi
}
# -------------------------- help -----------------------------
pitools_help() {
cat <<'EOF'
🔥 PITOOLSv3 — DevOps для всех!
📚 КОМАНДЫ:
pinstall [-опции] ФАЙЛ... Создать файлы/папки
-h Права как у папки -u 644
-s Скрипт (755) -d Папка
-q Тихий режим
piproject файл.prj Создать проект
Формат .prj:
PRJ:имя # Корень
DR:папка/внутри/ # Папка (можно несколько)
FL:файл1 файл2 # Файлы (можно несколько)
# Комментарии
deploypi хост юзер файл.prj Деплой на сервер
multideploypi файл.prj Деплой на несколько серверов
MLT:хост1,хост2 # Список серверов
pifill ssh://юзер@хост/путь Залить файл
pifetch ssh://юзер@хост/путь Скачать файл
📝 ПРИМЕР:
PRJ:мой_сайт
DR:www/ config/ backup/
FL:index.html style.css
DR:scripts/
FL:start.sh stop.sh
✨ Особенности V3:
• Несколько файлов/папок в одной строке
• Вложенные пути работают правильно
Не ругается если уже существует
• Бабушка поймёт!
EOF
}
# ---------------------- автозапуск ---------------------------
if [ "$0" = "${BASH_SOURCE[0]:-$0}" ]; then
case "${1:-}" in
pinstall|piproject|deploypi|multideploypi|pifill|pifetch|pitools_help)
"$@"
;;
"test")
# Тестовый прогон
cat > test_simple.prj <<'TEST'
PRJ:test_project
DR:app/config db/logs/
FL:settings.yaml database.conf
DR:scripts/
FL:start.sh stop.sh
# Комментарий
TEST
echo "🧪 Тестовый прогон..."
piproject test_simple.prj
echo ""
echo "📁 Результат:"
find test_project -type f 2>/dev/null || echo "Не создано"
;;
"")
echo "🔥 PITOOLSv3 загружен!"
echo "💡 Используй 'pitools_help' для справки"
echo ""
echo "🚀 Быстрый старт:"
echo " . ./pitoolsv3.sh"
echo " echo 'PRJ:мой_проект' > проект.prj"
echo " piproject проект.prj"
;;
*)
echo "❌ Неизвестная команда: $1" >&2
echo "💡 Используй 'pitools_help'" >&2
exit 1
;;
esac
fi