FreeVops 1.0 production: pitools.py + real project
This commit is contained in:
commit
1f55dfc055
6 changed files with 276 additions and 0 deletions
0
my_web/backup.sh
Executable file
0
my_web/backup.sh
Executable file
0
my_web/db.env
Normal file
0
my_web/db.env
Normal file
0
my_web/nginx.conf
Normal file
0
my_web/nginx.conf
Normal file
0
my_web/run.sh
Executable file
0
my_web/run.sh
Executable file
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)
|
||||||
5
test_v3.prj
Normal file
5
test_v3.prj
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue