pitools/pitools.py

271 lines
No EOL
10 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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)