FreeVops 1.0 production: pitools.py + real project

This commit is contained in:
peginelab 2025-12-23 01:55:46 +03:00
commit 1f55dfc055
6 changed files with 276 additions and 0 deletions

0
my_web/backup.sh Executable file
View file

0
my_web/db.env Normal file
View file

0
my_web/nginx.conf Normal file
View file

0
my_web/run.sh Executable file
View file

271
pitools.py Executable file
View file

@ -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)

5
test_v3.prj Normal file
View file

@ -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