#!/usr/bin/env python3 #PITOOLS v4 — DevOps фреймворк на Python #pinstall, piproject, deploypi, multideploypi # import argparse import asyncio import shlex import sys from typing import List, Dict, Any from pathlib import Path import paramiko from io import StringIO class PITools: def __init__(self, root: Path = None): self.root = root or Path.cwd() self.curdir = self.root def pinstall(self, items: List[str], mode: int = 0o644, is_dir: bool = False) -> int: #Создание файлов/папок с умными правами""" created = 0 for item in items: if not item.strip(): continue p = self.root / item.lstrip('/') if is_dir or item.endswith('/'): p.mkdir(parents=True, exist_ok=True) print(f"📂 {item.strip()}") else: p.parent.mkdir(parents=True, exist_ok=True) p.touch(exist_ok=True) p.chmod(mode) print(f"📄 {item.strip()}") created += 1 return created def piproject(self, spec_file: str) -> bool: #Развёртывание проекта по .prj""" print(f"🌳 Читаем: {spec_file}") spec_path = Path(spec_file) if not spec_path.exists(): print(f"❌ Не найден: {spec_file}") return False self.root.mkdir(exist_ok=True) created = 0 with open(spec_file, 'r', encoding='utf-8') as f: for line_num, line in enumerate(f, 1): line = line.strip() if not line or line.startswith('#'): if line.startswith('#'): print(f"💬 {line}") 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]) self.curdir = self.root self.root.mkdir(exist_ok=True) print(f"📁 ПРОЕКТ: {self.root}/") elif cmd == 'DR:': for d in args: d = d.rstrip('/') self.pinstall([f"{d}/"], is_dir=True) self.curdir = self.root / d elif cmd == 'FL:': files = [] for fname in args: if '/' in fname: files.append(fname) else: files.append(str(self.curdir / fname)) self.pinstall(files) except Exception as e: print(f"⚠️ Строка {line_num}: {e}") print("✅ Готово!") print(f"📁 Структура: {self.root}/") return True async def deploypi(host: str, user: str, spec_content: str, pitools_content: str = None) -> bool: #Деплой на один сервер""" print(f"🚀 Деплой: {user}@{host}") client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: # Используем run_in_executor для асинхронного выполнения блокирующих операций loop = asyncio.get_event_loop() await loop.run_in_executor(None, client.connect, host, username=user, timeout=10) sftp = await loop.run_in_executor(None, client.open_sftp) # Загружаем pitools.py if pitools_content is None: current_file = Path(__file__) pitools_code = current_file.read_text(encoding='utf-8') else: pitools_code = pitools_content # Используем StringIO для содержимого pitools_io = StringIO(pitools_code) spec_io = StringIO(spec_content) # Загружаем файлы def upload_files(): # Создаем временные файлы для загрузки with sftp.open('/tmp/pitools.py', 'w') as f: f.write(pitools_code) with sftp.open('/tmp/spec.prj', 'w') as f: f.write(spec_content) await loop.run_in_executor(None, upload_files) # Закрываем SFTP await loop.run_in_executor(None, sftp.close) # Выполняем команду def execute_command(): stdin, stdout, stderr = client.exec_command( "chmod +x /tmp/pitools.py && python3 /tmp/pitools.py piproject /tmp/spec.prj" ) result = stdout.read().decode('utf-8', errors='ignore') error = stderr.read().decode('utf-8', errors='ignore') return result, error result, error = await loop.run_in_executor(None, execute_command) print(f"✅ {host}: готово!") if result: print(f"📋 {host} вывод:\n{result}") if error: print(f"⚠️ {host} ошибки:\n{error}") client.close() return True except Exception as e: print(f"❌ {host}: {e}") return False 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 async def multideploypi(spec_file: str, infra_file: str = None): #Мультидеплой на множество серверов""" print("🌍 МУЛЬТИДЕПЛОЙ") if infra_file and Path(infra_file).exists(): blocks = parse_infra(infra_file) else: # Fallback на один блок spec_path = Path(spec_file) if spec_path.exists(): spec_content = spec_path.read_text(encoding='utf-8') else: spec_content = spec_file blocks = [{"hosts": [spec_file], "spec": spec_content}] user = input("Пользователь (по умолчанию текущий): ").strip() or Path.home().name for i, block in enumerate(blocks, 1): print(f"\n🔧 Блок {i} → {len(block['hosts'])} хостов") tasks = [] for host in block['hosts']: task = deploypi(host, user, block['spec']) tasks.append(task) results = await asyncio.gather(*tasks, return_exceptions=True) success = sum(1 for r in results if isinstance(r, bool) and r) print(f"✅ Блок {i}: {success}/{len(tasks)} готово!") # CLI async def main(): parser = argparse.ArgumentParser(description="PITOOLS v4 — DevOps фреймворк") 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='Режим папки') # piproject p2 = subparsers.add_parser('piproject', help='Развёртывание проекта') p2.add_argument('spec', help='.prj файл') # deploypi p3 = subparsers.add_parser('deploypi', help='Деплой на сервер') p3.add_argument('host', help='Хост') p3.add_argument('user', help='Пользователь') p3.add_argument('spec', help='.prj файл или содержимое') # multideploypi p4 = subparsers.add_parser('multideploypi', help='Мультидеплой') p4.add_argument('spec_or_infra', help='.prj или infra.prj') p4.add_argument('-i', '--infra', help='Файл инфраструктуры (опционально)') args = parser.parse_args() if args.command == 'pinstall': pitools = PITools() mode = 0o755 if args.script else 0o644 pitools.pinstall(args.items, mode, args.directory) elif args.command == 'piproject': pitools = PITools() pitools.piproject(args.spec) 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 await deploypi(args.host, args.user, spec_content) elif args.command == 'multideploypi': infra_file = args.infra if hasattr(args, 'infra') else None await multideploypi(args.spec_or_infra, infra_file) else: parser.print_help() if __name__ == '__main__': try: asyncio.run(main()) except KeyboardInterrupt: print("\n\n👋 Прервано пользователем") sys.exit(0) except Exception as e: print(f"💥 Критическая ошибка: {e}") sys.exit(1)