From 1f55dfc055324a92ac322a7d7e7048d58e82a50d Mon Sep 17 00:00:00 2001 From: peginelab Date: Tue, 23 Dec 2025 01:55:46 +0300 Subject: [PATCH] FreeVops 1.0 production: pitools.py + real project --- my_web/backup.sh | 0 my_web/db.env | 0 my_web/nginx.conf | 0 my_web/run.sh | 0 pitools.py | 271 ++++++++++++++++++++++++++++++++++++++++++++++ test_v3.prj | 5 + 6 files changed, 276 insertions(+) create mode 100755 my_web/backup.sh create mode 100644 my_web/db.env create mode 100644 my_web/nginx.conf create mode 100755 my_web/run.sh create mode 100755 pitools.py create mode 100644 test_v3.prj diff --git a/my_web/backup.sh b/my_web/backup.sh new file mode 100755 index 0000000..e69de29 diff --git a/my_web/db.env b/my_web/db.env new file mode 100644 index 0000000..e69de29 diff --git a/my_web/nginx.conf b/my_web/nginx.conf new file mode 100644 index 0000000..e69de29 diff --git a/my_web/run.sh b/my_web/run.sh new file mode 100755 index 0000000..e69de29 diff --git a/pitools.py b/pitools.py new file mode 100755 index 0000000..6eb0bde --- /dev/null +++ b/pitools.py @@ -0,0 +1,271 @@ +#!/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) \ No newline at end of file diff --git a/test_v3.prj b/test_v3.prj new file mode 100644 index 0000000..4784b41 --- /dev/null +++ b/test_v3.prj @@ -0,0 +1,5 @@ +PRJ: my_web +DR: config/subconfig/nested +FL: nginx.conf db.env +DR: scripts/deploy utils/ +FL: run.sh backup.sh