From 8ec21b41fa4d99f5d611cd9e93ab24852011baae Mon Sep 17 00:00:00 2001 From: peginelab Date: Tue, 23 Dec 2025 02:45:20 +0300 Subject: [PATCH] Add CI/CD --- pitoolsv2.py | 637 +++++++++++++++++++++++++++++++++++++++++++++++++ pitoolsv2.sh | 663 +++++++++++++++++++++++++++++++++++++++++++++++++++ pitoolsv3.sh | 629 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1929 insertions(+) create mode 100755 pitoolsv2.py create mode 100755 pitoolsv2.sh create mode 100644 pitoolsv3.sh diff --git a/pitoolsv2.py b/pitoolsv2.py new file mode 100755 index 0000000..a3ed52a --- /dev/null +++ b/pitoolsv2.py @@ -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()) \ No newline at end of file diff --git a/pitoolsv2.sh b/pitoolsv2.sh new file mode 100755 index 0000000..789f86e --- /dev/null +++ b/pitoolsv2.sh @@ -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 <&2 + echo "Используйте: pinstall, piproject, deploypi, multideploypi, pitools_help" >&2 + exit 1 + ;; + esac +fi diff --git a/pitoolsv3.sh b/pitoolsv3.sh new file mode 100644 index 0000000..1a44c62 --- /dev/null +++ b/pitoolsv3.sh @@ -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 \ No newline at end of file