271 lines
No EOL
10 KiB
Python
Executable file
271 lines
No EOL
10 KiB
Python
Executable file
#!/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) |