FreeVops 1.0 production: pitools.py + real project
This commit is contained in:
commit
1f55dfc055
6 changed files with 276 additions and 0 deletions
271
pitools.py
Executable file
271
pitools.py
Executable 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue