Add CI/CD
This commit is contained in:
parent
1f55dfc055
commit
8ec21b41fa
3 changed files with 1929 additions and 0 deletions
637
pitoolsv2.py
Executable file
637
pitoolsv2.py
Executable file
|
|
@ -0,0 +1,637 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
PITOOLS v4 — DevOps фреймворк на Python
|
||||
pinstall, piproject, deploypi, multideploypi
|
||||
Для бабушек и дедушек! 🔥
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import shlex
|
||||
import sys
|
||||
import getpass
|
||||
import json
|
||||
import yaml
|
||||
from typing import List, Dict, Any, Optional, Union
|
||||
from pathlib import Path
|
||||
import paramiko
|
||||
from io import StringIO
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
# Try to import yaml, fallback if not available
|
||||
try:
|
||||
import yaml
|
||||
YAML_AVAILABLE = True
|
||||
except ImportError:
|
||||
YAML_AVAILABLE = False
|
||||
|
||||
@dataclass
|
||||
class FileSpec:
|
||||
path: Path
|
||||
mode: int
|
||||
content: Optional[str] = None
|
||||
is_dir: bool = False
|
||||
|
||||
class PITools:
|
||||
VERSION = "4.1.0"
|
||||
|
||||
def __init__(self, root: Path = None, verbose: bool = False):
|
||||
self.root = root or Path.cwd()
|
||||
self.curdir = self.root
|
||||
self.verbose = verbose
|
||||
self.stats = {"files": 0, "dirs": 0, "errors": 0}
|
||||
|
||||
def _log(self, message: str, level: str = "INFO"):
|
||||
"""Умное логирование"""
|
||||
icons = {
|
||||
"INFO": "ℹ️",
|
||||
"SUCCESS": "✅",
|
||||
"WARNING": "⚠️",
|
||||
"ERROR": "❌",
|
||||
"DEBUG": "🐛"
|
||||
}
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
if level in icons:
|
||||
print(f"{icons[level]} [{timestamp}] {message}")
|
||||
else:
|
||||
print(f"[{timestamp}] {message}")
|
||||
|
||||
def _get_smart_mode(self, filepath: Path) -> int:
|
||||
"""Определение прав доступа по расширению файла"""
|
||||
if filepath.is_dir():
|
||||
return 0o755
|
||||
|
||||
ext = filepath.suffix.lower()
|
||||
|
||||
# Исполняемые файлы
|
||||
if ext in ['.sh', '.bash', '.zsh', '.py', '.pl', '.rb', '.php', '.js', '.ts']:
|
||||
return 0o755
|
||||
|
||||
# Приватные файлы
|
||||
if ext in ['.key', '.pem', '.crt', '.pub', '.priv']:
|
||||
return 0o600
|
||||
|
||||
# Конфиденциальные данные
|
||||
if ext in ['.env', '.secret', '.token', '.password', '.credential']:
|
||||
return 0o640
|
||||
|
||||
# Конфигурационные файлы
|
||||
if ext in ['.conf', '.cfg', '.ini', '.config', '.yml', '.yaml', '.toml']:
|
||||
return 0o644
|
||||
|
||||
# Логи и данные
|
||||
if ext in ['.log', '.db', '.sqlite', '.json', '.xml']:
|
||||
return 0o644
|
||||
|
||||
# По умолчанию
|
||||
return 0o644
|
||||
|
||||
def pinstall(self, items: List[str], mode: Optional[int] = None,
|
||||
is_dir: bool = False, content: Optional[str] = None) -> Dict[str, int]:
|
||||
"""Создание файлов/папок с умными правами"""
|
||||
self.stats = {"files": 0, "dirs": 0, "errors": 0}
|
||||
|
||||
for item in items:
|
||||
if not item.strip():
|
||||
continue
|
||||
|
||||
try:
|
||||
p = self.root / item.lstrip('/')
|
||||
|
||||
if is_dir or item.endswith('/'):
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
final_mode = mode or 0o755
|
||||
p.chmod(final_mode)
|
||||
self._log(f"Папка: {item.strip()} ({oct(final_mode)})", "SUCCESS")
|
||||
self.stats["dirs"] += 1
|
||||
|
||||
else:
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if content:
|
||||
p.write_text(content, encoding='utf-8')
|
||||
else:
|
||||
p.touch(exist_ok=True)
|
||||
|
||||
final_mode = mode or self._get_smart_mode(p)
|
||||
p.chmod(final_mode)
|
||||
|
||||
self._log(f"Файл: {item.strip()} ({oct(final_mode)})", "SUCCESS")
|
||||
self.stats["files"] += 1
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Ошибка создания {item}: {e}", "ERROR")
|
||||
self.stats["errors"] += 1
|
||||
|
||||
self._log(f"Создано: {self.stats['files']} файлов, {self.stats['dirs']} папок, ошибок: {self.stats['errors']}", "INFO")
|
||||
return self.stats
|
||||
|
||||
def piproject(self, spec_file: str, export_json: bool = False) -> bool:
|
||||
"""Развёртывание проекта по .prj"""
|
||||
self._log(f"Читаем спецификацию: {spec_file}")
|
||||
|
||||
spec_path = Path(spec_file)
|
||||
if not spec_path.exists():
|
||||
self._log(f"Файл не найден: {spec_file}", "ERROR")
|
||||
return False
|
||||
|
||||
original_root = self.root
|
||||
project_structure = []
|
||||
|
||||
try:
|
||||
with open(spec_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Поддержка разных форматов
|
||||
if spec_path.suffix.lower() in ['.json', '.yaml', '.yml']:
|
||||
return self._load_structured_spec(spec_path, export_json)
|
||||
|
||||
# Стандартный .prj формат
|
||||
lines = content.split('\n')
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
if line.startswith('#') and self.verbose:
|
||||
self._log(f"Комментарий: {line[1:].strip()}", "DEBUG")
|
||||
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]).expanduser().resolve()
|
||||
self.curdir = self.root
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
self._log(f"Проект: {self.root}/", "INFO")
|
||||
|
||||
elif cmd == 'DR:':
|
||||
for d in args:
|
||||
d = d.rstrip('/')
|
||||
full_path = self.root / d
|
||||
full_path.mkdir(parents=True, exist_ok=True)
|
||||
full_path.chmod(0o755)
|
||||
project_structure.append({"type": "dir", "path": str(full_path), "mode": "755"})
|
||||
self._log(f"Папка: {d}/ (755)", "SUCCESS")
|
||||
self.stats["dirs"] += 1
|
||||
self.curdir = full_path
|
||||
|
||||
elif cmd == 'FL:':
|
||||
for fname in args:
|
||||
full_path = self.root / fname
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
full_path.touch(exist_ok=True)
|
||||
|
||||
mode = self._get_smart_mode(full_path)
|
||||
full_path.chmod(mode)
|
||||
|
||||
project_structure.append({
|
||||
"type": "file",
|
||||
"path": str(full_path),
|
||||
"mode": oct(mode),
|
||||
"extension": full_path.suffix
|
||||
})
|
||||
|
||||
mode_str = oct(mode).replace('0o', '')
|
||||
self._log(f"Файл: {fname} ({mode_str})", "SUCCESS")
|
||||
self.stats["files"] += 1
|
||||
|
||||
elif cmd == 'CMD:':
|
||||
# Выполнение команд после создания структуры
|
||||
if args:
|
||||
cmd_str = ' '.join(args)
|
||||
self._log(f"Выполняем: {cmd_str}", "INFO")
|
||||
|
||||
elif cmd == 'TPL:':
|
||||
# Шаблоны файлов с переменными
|
||||
if len(args) >= 2:
|
||||
template_file = args[0]
|
||||
target_file = args[1]
|
||||
variables = args[2:] if len(args) > 2 else []
|
||||
|
||||
self._process_template(template_file, target_file, variables)
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Строка {line_num}: {e}", "ERROR")
|
||||
self.stats["errors"] += 1
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Ошибка чтения файла: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
finally:
|
||||
self.root = original_root
|
||||
|
||||
# Экспорт структуры в JSON
|
||||
if export_json:
|
||||
json_file = f"{spec_path.stem}_structure.json"
|
||||
with open(json_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(project_structure, f, indent=2, ensure_ascii=False)
|
||||
self._log(f"Структура экспортирована в: {json_file}", "INFO")
|
||||
|
||||
self._log(f"Готово! Создано: {self.stats['files']} файлов, {self.stats['dirs']} папок", "SUCCESS")
|
||||
return True
|
||||
|
||||
def _load_structured_spec(self, spec_path: Path, export_json: bool) -> bool:
|
||||
"""Загрузка структурированных спецификаций (JSON/YAML)"""
|
||||
try:
|
||||
if spec_path.suffix.lower() == '.json':
|
||||
with open(spec_path, 'r', encoding='utf-8') as f:
|
||||
spec = json.load(f)
|
||||
elif spec_path.suffix.lower() in ['.yaml', '.yml']:
|
||||
if not YAML_AVAILABLE:
|
||||
self._log("YAML не установлен. Установите: pip install pyyaml", "ERROR")
|
||||
return False
|
||||
with open(spec_path, 'r', encoding='utf-8') as f:
|
||||
spec = yaml.safe_load(f)
|
||||
else:
|
||||
self._log(f"Неподдерживаемый формат: {spec_path.suffix}", "ERROR")
|
||||
return False
|
||||
|
||||
# Обработка структурированной спецификации
|
||||
if 'project' in spec:
|
||||
project_root = spec.get('project', {}).get('root', '.')
|
||||
self.root = Path(project_root).expanduser().resolve()
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
self._log(f"Проект: {self.root}/", "INFO")
|
||||
|
||||
# Создание директорий
|
||||
for dir_path in spec.get('directories', []):
|
||||
full_path = self.root / dir_path
|
||||
full_path.mkdir(parents=True, exist_ok=True)
|
||||
full_path.chmod(0o755)
|
||||
self._log(f"Папка: {dir_path}/ (755)", "SUCCESS")
|
||||
self.stats["dirs"] += 1
|
||||
|
||||
# Создание файлов
|
||||
for file_spec in spec.get('files', []):
|
||||
if isinstance(file_spec, dict):
|
||||
file_path = file_spec.get('path', '')
|
||||
content = file_spec.get('content', '')
|
||||
mode = file_spec.get('mode')
|
||||
else:
|
||||
file_path = file_spec
|
||||
content = ''
|
||||
mode = None
|
||||
|
||||
full_path = self.root / file_path
|
||||
full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if content:
|
||||
full_path.write_text(content, encoding='utf-8')
|
||||
else:
|
||||
full_path.touch(exist_ok=True)
|
||||
|
||||
final_mode = mode or self._get_smart_mode(full_path)
|
||||
full_path.chmod(final_mode)
|
||||
|
||||
mode_str = oct(final_mode).replace('0o', '')
|
||||
self._log(f"Файл: {file_path} ({mode_str})", "SUCCESS")
|
||||
self.stats["files"] += 1
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self._log(f"Ошибка загрузки спецификации: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
def _process_template(self, template: str, target: str, variables: List[str]):
|
||||
"""Обработка шаблонов файлов"""
|
||||
template_path = Path(template)
|
||||
if not template_path.exists():
|
||||
self._log(f"Шаблон не найден: {template}", "ERROR")
|
||||
return
|
||||
|
||||
content = template_path.read_text(encoding='utf-8')
|
||||
|
||||
# Замена переменных (простая реализация)
|
||||
var_dict = {}
|
||||
for var in variables:
|
||||
if '=' in var:
|
||||
key, value = var.split('=', 1)
|
||||
var_dict[key.strip()] = value.strip()
|
||||
|
||||
for key, value in var_dict.items():
|
||||
content = content.replace(f'{{{{{key}}}}}', value)
|
||||
|
||||
target_path = self.root / target
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
target_path.write_text(content, encoding='utf-8')
|
||||
|
||||
mode = self._get_smart_mode(target_path)
|
||||
target_path.chmod(mode)
|
||||
|
||||
self._log(f"Шаблон: {template} → {target}", "SUCCESS")
|
||||
self.stats["files"] += 1
|
||||
|
||||
class RemoteDeployer:
|
||||
def __init__(self, verbose: bool = False):
|
||||
self.verbose = verbose
|
||||
self.results = []
|
||||
|
||||
async def deploy(self, host: str, user: str, spec_content: str,
|
||||
key_path: Optional[str] = None, password: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Деплой на удаленный сервер"""
|
||||
result = {
|
||||
"host": host,
|
||||
"success": False,
|
||||
"output": "",
|
||||
"error": "",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
try:
|
||||
# Асинхронное подключение
|
||||
loop = asyncio.get_event_loop()
|
||||
connect_kwargs = {
|
||||
"hostname": host,
|
||||
"username": user,
|
||||
"timeout": 15
|
||||
}
|
||||
|
||||
if key_path:
|
||||
connect_kwargs["key_filename"] = key_path
|
||||
elif password:
|
||||
connect_kwargs["password"] = password
|
||||
|
||||
await loop.run_in_executor(None, client.connect, **connect_kwargs)
|
||||
|
||||
# Создаем временную директорию
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
remote_dir = f"/tmp/pitools_{timestamp}"
|
||||
|
||||
sftp = await loop.run_in_executor(None, client.open_sftp)
|
||||
|
||||
# Загружаем pitools и спецификацию
|
||||
current_file = Path(__file__)
|
||||
pitools_code = current_file.read_text(encoding='utf-8')
|
||||
|
||||
def upload():
|
||||
sftp.mkdir(remote_dir)
|
||||
|
||||
with sftp.open(f"{remote_dir}/pitools.py", 'w') as f:
|
||||
f.write(pitools_code)
|
||||
|
||||
with sftp.open(f"{remote_dir}/spec.prj", 'w') as f:
|
||||
f.write(spec_content)
|
||||
|
||||
sftp.chmod(f"{remote_dir}/pitools.py", 0o755)
|
||||
|
||||
await loop.run_in_executor(None, upload)
|
||||
await loop.run_in_executor(None, sftp.close)
|
||||
|
||||
# Выполняем развертывание
|
||||
def execute():
|
||||
cmd = f"cd {remote_dir} && python3 pitools.py piproject spec.prj"
|
||||
stdin, stdout, stderr = client.exec_command(cmd)
|
||||
output = stdout.read().decode('utf-8', errors='ignore')
|
||||
error = stderr.read().decode('utf-8', errors='ignore')
|
||||
return output, error
|
||||
|
||||
output, error = await loop.run_in_executor(None, execute)
|
||||
|
||||
# Очистка
|
||||
clean_cmd = f"rm -rf {remote_dir}"
|
||||
client.exec_command(clean_cmd)
|
||||
|
||||
result["success"] = True
|
||||
result["output"] = output
|
||||
if error:
|
||||
result["error"] = error
|
||||
|
||||
self._log(f"✅ {host}: Успешно", "SUCCESS")
|
||||
|
||||
except Exception as e:
|
||||
result["error"] = str(e)
|
||||
self._log(f"❌ {host}: {e}", "ERROR")
|
||||
|
||||
finally:
|
||||
try:
|
||||
client.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
self.results.append(result)
|
||||
return result
|
||||
|
||||
def _log(self, message: str, level: str = "INFO"):
|
||||
"""Логирование для RemoteDeployer"""
|
||||
icons = {"SUCCESS": "✅", "ERROR": "❌", "INFO": "ℹ️"}
|
||||
if level in icons:
|
||||
print(f"{icons[level]} {message}")
|
||||
else:
|
||||
print(message)
|
||||
|
||||
# CLI
|
||||
async def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"PITOOLS v{PITools.VERSION} — DevOps фреймворк на Python",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Примеры использования:
|
||||
pitools pinstall file1.txt dir/ script.sh -s
|
||||
pitools piproject project.prj
|
||||
pitools deploypi server.com user project.prj
|
||||
pitools multideploypi infra.prj
|
||||
|
||||
Форматы спецификаций:
|
||||
.prj - Текстовый формат с командами
|
||||
.json - JSON структура
|
||||
.yaml - YAML структура (требует pyyaml)
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('--version', action='version', version=f'PITOOLS v{PITools.VERSION}')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='Подробный вывод')
|
||||
|
||||
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='Режим папки')
|
||||
p1.add_argument('-m', '--mode', help='Права доступа (например: 755, 644)')
|
||||
p1.add_argument('-c', '--content', help='Содержимое файла')
|
||||
|
||||
# piproject
|
||||
p2 = subparsers.add_parser('piproject', help='Развёртывание проекта')
|
||||
p2.add_argument('spec', help='Файл спецификации (.prj, .json, .yaml)')
|
||||
p2.add_argument('-e', '--export-json', action='store_true', help='Экспортировать структуру в JSON')
|
||||
p2.add_argument('-r', '--root', help='Корневая директория проекта')
|
||||
|
||||
# deploypi
|
||||
p3 = subparsers.add_parser('deploypi', help='Деплой на сервер')
|
||||
p3.add_argument('host', help='Хост')
|
||||
p3.add_argument('user', help='Пользователь')
|
||||
p3.add_argument('spec', help='Файл спецификации или содержимое')
|
||||
p3.add_argument('-k', '--key', help='Путь к SSH ключу')
|
||||
p3.add_argument('-p', '--password', action='store_true', help='Использовать парольную аутентификацию')
|
||||
|
||||
# multideploypi
|
||||
p4 = subparsers.add_parser('multideploypi', help='Мультидеплой')
|
||||
p4.add_argument('spec_or_infra', help='Файл спецификации или инфраструктуры')
|
||||
p4.add_argument('-i', '--infra', help='Файл инфраструктуры (опционально)')
|
||||
p4.add_argument('-u', '--user', help='Пользователь для всех хостов')
|
||||
p4.add_argument('-k', '--key', help='Путь к SSH ключу')
|
||||
p4.add_argument('-o', '--output', help='Файл для сохранения результатов')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
try:
|
||||
if args.command == 'pinstall':
|
||||
root_dir = Path(args.root) if hasattr(args, 'root') and args.root else None
|
||||
pitools = PITools(root=root_dir, verbose=args.verbose)
|
||||
|
||||
mode = None
|
||||
if args.mode:
|
||||
try:
|
||||
mode = int(args.mode, 8)
|
||||
except ValueError:
|
||||
print(f"❌ Неверный формат прав: {args.mode}")
|
||||
return
|
||||
|
||||
pitools.pinstall(
|
||||
args.items,
|
||||
mode=mode or (0o755 if args.script else None),
|
||||
is_dir=args.directory,
|
||||
content=args.content
|
||||
)
|
||||
|
||||
elif args.command == 'piproject':
|
||||
root_dir = Path(args.root) if hasattr(args, 'root') and args.root else None
|
||||
pitools = PITools(root=root_dir, verbose=args.verbose)
|
||||
pitools.piproject(args.spec, args.export_json)
|
||||
|
||||
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
|
||||
|
||||
password = None
|
||||
if args.password:
|
||||
password = getpass.getpass(f"Пароль для {args.user}@{args.host}: ")
|
||||
|
||||
deployer = RemoteDeployer(verbose=args.verbose)
|
||||
result = await deployer.deploy(
|
||||
args.host,
|
||||
args.user,
|
||||
spec_content,
|
||||
key_path=args.key,
|
||||
password=password
|
||||
)
|
||||
|
||||
if args.verbose and result["output"]:
|
||||
print(f"\n📋 Вывод с сервера:\n{result['output']}")
|
||||
|
||||
elif args.command == 'multideploypi':
|
||||
await run_multideploy(args)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 Прервано пользователем")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"💥 Критическая ошибка: {e}")
|
||||
if args.verbose:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
async def run_multideploy(args):
|
||||
"""Запуск мультидеплоя"""
|
||||
print("🌍 МУЛЬТИДЕПЛОЙ")
|
||||
|
||||
infra_file = args.infra if args.infra else None
|
||||
|
||||
# Определяем пользователя
|
||||
user = args.user
|
||||
if not user:
|
||||
user = input("Пользователь (по умолчанию текущий): ").strip() or getpass.getuser()
|
||||
|
||||
# Парсинг инфраструктуры
|
||||
if infra_file and Path(infra_file).exists():
|
||||
blocks = parse_infra(infra_file)
|
||||
else:
|
||||
spec_path = Path(args.spec_or_infra)
|
||||
if spec_path.exists():
|
||||
spec_content = spec_path.read_text(encoding='utf-8')
|
||||
else:
|
||||
spec_content = args.spec_or_infra
|
||||
blocks = [{"hosts": [args.spec_or_infra], "spec": spec_content}]
|
||||
|
||||
deployer = RemoteDeployer(verbose=args.verbose)
|
||||
all_results = []
|
||||
|
||||
for i, block in enumerate(blocks, 1):
|
||||
print(f"\n🔧 Блок {i} → {len(block['hosts'])} хостов")
|
||||
|
||||
tasks = []
|
||||
for host in block['hosts']:
|
||||
task = deployer.deploy(
|
||||
host,
|
||||
user,
|
||||
block['spec'],
|
||||
key_path=args.key,
|
||||
password=None # Пароль запрашивается при необходимости
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
successful = 0
|
||||
for result in results:
|
||||
if isinstance(result, dict) and result.get("success"):
|
||||
successful += 1
|
||||
all_results.append(result)
|
||||
|
||||
print(f"✅ Блок {i}: {successful}/{len(tasks)} готово!")
|
||||
|
||||
# Сохранение результатов
|
||||
if args.output:
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_results, f, indent=2, ensure_ascii=False)
|
||||
print(f"\n📊 Результаты сохранены в: {args.output}")
|
||||
|
||||
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
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
663
pitoolsv2.sh
Executable file
663
pitoolsv2.sh
Executable file
|
|
@ -0,0 +1,663 @@
|
|||
#!/bin/bash
|
||||
# PITOOLSV2 — простой SSH-оркестратор:
|
||||
# pinstall — создать файл/директорию с правами
|
||||
# piproject — собрать проект по .prj
|
||||
# deploypi — деплой одного .prj на один хост
|
||||
# multideploypi — деплой нескольких блоков MLT+PRJ/DR/FL из одного .prj
|
||||
|
||||
# ------------------------- pinstall --------------------------
|
||||
# pinstall [-h|--here] [-u|--user] [-s|--script] FILE...
|
||||
# Создаёт файлы/каталоги с умными правами и владельцем.
|
||||
pinstall() {
|
||||
local mode=644 owner="$USER" group create_dir=0
|
||||
|
||||
# Получаем группу текущего пользователя
|
||||
group="$(id -gn "$USER" 2>/dev/null || id -gn 2>/dev/null || whoami 2>/dev/null || echo "$USER")"
|
||||
|
||||
# Парсим флаги
|
||||
while [ $# -gt 0 ] && [ "${1#-}" != "$1" ]; do
|
||||
case "$1" in
|
||||
-h|--here)
|
||||
# Наследуем права и владельца от текущей директории
|
||||
if [ -d . ]; then
|
||||
mode="$(stat -c '%a' . 2>/dev/null || echo 644)"
|
||||
owner="$(stat -c '%U' . 2>/dev/null || echo "$USER")"
|
||||
group="$(stat -c '%G' . 2>/dev/null || echo "$group")"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-u|--user)
|
||||
# Стандартные права для пользовательских файлов
|
||||
mode=644
|
||||
owner="$USER"
|
||||
group="$(id -gn "$USER" 2>/dev/null || id -gn 2>/dev/null || whoami 2>/dev/null || echo "$USER")"
|
||||
shift
|
||||
;;
|
||||
-s|--script)
|
||||
# Принудительно исполняемые права
|
||||
mode=755
|
||||
shift
|
||||
;;
|
||||
-d|--directory)
|
||||
# Создавать директорию
|
||||
create_dir=1
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
# Конец флагов
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "Использование: pinstall [-h|--here] [-u|--user] [-s|--script] [-d|--directory] ФАЙЛ..." >&2
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
# Не флаг - выходим из цикла
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Проверка: хотя бы один файл должен быть указан
|
||||
[ $# -eq 0 ] && {
|
||||
echo "Использование: pinstall [-h|--here] [-u|--user] [-s|--script] [-d|--directory] ФАЙЛ..." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Счётчик созданных объектов
|
||||
local created=0
|
||||
|
||||
# Обрабатываем все переданные файлы
|
||||
for f in "$@"; do
|
||||
local eff_mode="$mode"
|
||||
|
||||
# Автоматически делаем скрипты исполняемыми
|
||||
case "$(basename "$f")" in
|
||||
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
|
||||
eff_mode=755
|
||||
;;
|
||||
esac
|
||||
|
||||
# Убираем возможный / в конце для проверки существования
|
||||
local check_path="$f"
|
||||
[[ "$check_path" == */ ]] && check_path="${check_path%/}"
|
||||
|
||||
# Проверяем, существует ли уже
|
||||
if [ -e "$check_path" ]; then
|
||||
# Существует — обновляем права если нужно
|
||||
if [ -d "$check_path" ]; then
|
||||
# Это директория
|
||||
if [ "$eff_mode" != "644" ] && [ "$eff_mode" != "755" ]; then
|
||||
chmod "$eff_mode" "$check_path" 2>/dev/null && \
|
||||
echo " 🔧 Обновлены права директории: $f" >&2
|
||||
fi
|
||||
else
|
||||
# Это файл
|
||||
if [ "$eff_mode" != "644" ]; then
|
||||
chmod "$eff_mode" "$check_path" 2>/dev/null && \
|
||||
echo " 🔧 Обновлены права файла: $f" >&2
|
||||
fi
|
||||
fi
|
||||
continue # Уже существует, пропускаем создание
|
||||
fi
|
||||
|
||||
# Определяем, создавать директорию или файл
|
||||
local is_dir="$create_dir"
|
||||
[ "$is_dir" -eq 0 ] && [[ "$f" == */ ]] && is_dir=1
|
||||
|
||||
if [ "$is_dir" -eq 1 ]; then
|
||||
# Создаём директорию
|
||||
if install -d -m "$eff_mode" -o "$owner" -g "$group" "$f" 2>/dev/null; then
|
||||
echo " 📂 Создана директория: ${f%/}" >&2
|
||||
created=$((created + 1))
|
||||
else
|
||||
echo " ⚠️ Не удалось создать директорию: $f" >&2
|
||||
fi
|
||||
else
|
||||
# Создаём файл
|
||||
if install -D -m "$eff_mode" -o "$owner" -g "$group" \
|
||||
--backup=numbered /dev/null "$f" 2>/dev/null; then
|
||||
# Проверяем, был ли авто-режим 755
|
||||
case "$(basename "$f")" in
|
||||
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
|
||||
echo " 🔥 AUTO 755 → $f" >&2
|
||||
;;
|
||||
*)
|
||||
echo " 📄 Создан файл: $f" >&2
|
||||
;;
|
||||
esac
|
||||
created=$((created + 1))
|
||||
else
|
||||
echo " ⚠️ Не удалось создать файл: $f" >&2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
[ "$created" -gt 0 ] && echo "✅ Создано объектов: $created" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------ piproject --------------------------
|
||||
# piproject SPEC.prj
|
||||
# Читает PRJ/DR/FL и создаёт структуру проекта локально.
|
||||
piproject() {
|
||||
local spec="$1" root="" curdir="" line="" created_projects=0
|
||||
|
||||
# Проверка аргументов
|
||||
[ -z "$spec" ] && {
|
||||
echo "Использование: piproject ФАЙЛ.prj" >&2
|
||||
echo "Пример: piproject мойпроект.prj" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[ ! -f "$spec" ] && {
|
||||
echo "❌ Файл спецификации не найден: $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "🌳 Читаем спецификацию: $spec"
|
||||
|
||||
# Проверяем, есть ли хоть одна PRJ: строка
|
||||
if ! grep -q "^PRJ:" "$spec" 2>/dev/null; then
|
||||
echo "❌ В файле нет PRJ: определения" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Читаем файл построчно
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Убираем пробелы в начале и конце
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
|
||||
# Пропускаем пустые строки и комментарии
|
||||
case "$line" in
|
||||
""|"#"*)
|
||||
continue
|
||||
;;
|
||||
|
||||
PRJ:*)
|
||||
# Начало нового проекта - определяем корневую директорию
|
||||
root="${line#PRJ:}"
|
||||
root="${root#"${root%%[![:space:]]*}"}"
|
||||
root="${root%"${root##*[![:space:]]}"}"
|
||||
|
||||
# Проверяем безопасность пути
|
||||
if [[ "$root" == *".."* ]] || [[ "$root" == /* ]]; then
|
||||
echo "⚠️ Предупреждение: путь '$root' может быть небезопасен" >&2
|
||||
fi
|
||||
|
||||
# Создаём корневую директорию
|
||||
echo "📁 Создаём проект: $root/"
|
||||
if pinstall -d "$root/"; then
|
||||
curdir="$root"
|
||||
created_projects=$((created_projects + 1))
|
||||
else
|
||||
echo "❌ Не удалось создать корень проекта: $root" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
|
||||
DR:*)
|
||||
# Создание поддиректории
|
||||
[ -z "$root" ] && {
|
||||
echo "❌ Сначала определите PRJ: в файле $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local dir_path="${line#DR:}"
|
||||
dir_path="${dir_path#"${dir_path%%[![:space:]]*}"}"
|
||||
dir_path="${dir_path%"${dir_path##*[![:space:]]}"}"
|
||||
|
||||
# Защита от небезопасных путей
|
||||
if [[ "$dir_path" == *".."* ]] || [[ "$dir_path" == /* ]]; then
|
||||
echo "⚠️ Предупреждение: путь '$dir_path' может быть небезопасен" >&2
|
||||
fi
|
||||
|
||||
# Создаём поддиректорию
|
||||
if [ -n "$dir_path" ]; then
|
||||
curdir="$root/$dir_path"
|
||||
if pinstall -d "$curdir/"; then
|
||||
echo " 📂 Директория: $dir_path"
|
||||
else
|
||||
echo " ⚠️ Не удалось создать директорию: $dir_path" >&2
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
FL:*)
|
||||
# Создание файла
|
||||
[ -z "$root" ] && {
|
||||
echo "❌ Сначала определите PRJ: в файле $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local file_path="${line#FL:}"
|
||||
file_path="${file_path#"${file_path%%[![:space:]]*}"}"
|
||||
file_path="${file_path%"${file_path##*[![:space:]]}"}"
|
||||
|
||||
if [ -z "$file_path" ]; then
|
||||
echo "⚠️ Пустое имя файла в FL:, пропускаем" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Определяем полный путь к файлу
|
||||
local full_path=""
|
||||
if [[ "$file_path" == */* ]]; then
|
||||
# В пути есть слеш - создаём относительно корня
|
||||
full_path="$root/$file_path"
|
||||
|
||||
# Создаём родительские директории если нужно
|
||||
local parent_dir="${full_path%/*}"
|
||||
if [ -n "$parent_dir" ] && [ ! -d "$parent_dir" ]; then
|
||||
pinstall -d "$parent_dir/" >/dev/null 2>&1
|
||||
fi
|
||||
else
|
||||
# Простое имя файла - создаём в текущей директории
|
||||
[ -z "$curdir" ] && curdir="$root"
|
||||
full_path="$curdir/$file_path"
|
||||
fi
|
||||
|
||||
# Создаём файл
|
||||
if pinstall "$full_path"; then
|
||||
echo " 📄 Файл: $file_path"
|
||||
else
|
||||
echo " ⚠️ Не удалось создать файл: $file_path" >&2
|
||||
fi
|
||||
;;
|
||||
|
||||
*)
|
||||
# Неизвестная строка
|
||||
echo "⚠️ Неизвестная строка в $spec (пропускаем): $line" >&2
|
||||
;;
|
||||
esac
|
||||
done < "$spec"
|
||||
|
||||
if [ "$created_projects" -gt 0 ]; then
|
||||
echo "✅ Готово! Создано проектов: $created_projects"
|
||||
else
|
||||
echo "ℹ️ Ничего не создано (всё уже существует?)"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------ deploypi ---------------------------
|
||||
# deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ SPEC.prj
|
||||
# Копирует pitoolsv2 + spec на хост и запускает piproject.
|
||||
deploypi() {
|
||||
local host="$1" user="$2" spec="$3"
|
||||
|
||||
# Проверка аргументов
|
||||
[ -z "$host" ] || [ -z "$user" ] || [ -z "$spec" ] && {
|
||||
echo "Использование: deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ ФАЙЛ.prj" >&2
|
||||
echo "Пример: deploypi server.ru vasya мойпроект.prj" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[ ! -f "$spec" ] && {
|
||||
echo "❌ Файл спецификации не найден: $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "🚀 DEPLOYpi: $spec → $user@$host"
|
||||
|
||||
# Определяем путь к текущему скрипту
|
||||
local self=""
|
||||
if [ -f "$0" ]; then
|
||||
self="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
|
||||
elif [ -n "${BASH_SOURCE[0]}" ]; then
|
||||
self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
else
|
||||
self="./pitoolsv2.sh"
|
||||
fi
|
||||
|
||||
# Безопасное получение имени спецификации
|
||||
local spec_basename="$(basename "$spec")"
|
||||
spec_basename="${spec_basename//[!a-zA-Zа-яА-ЯёЁ0-9._-]/_}"
|
||||
|
||||
echo "🔍 Проверяем соединение с $host..."
|
||||
|
||||
# Проверяем доступность SSH
|
||||
if ! ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=accept-new \
|
||||
"$user@$host" "exit 0" 2>/dev/null; then
|
||||
echo "❌ Не могу подключиться к $user@$host через SSH" >&2
|
||||
echo " Проверьте: ssh $user@$host" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "📤 Загружаем инструменты и спецификацию..."
|
||||
|
||||
# 1. Копируем сам скрипт на удалённый хост
|
||||
if ! scp -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
|
||||
"$self" "$user@$host:~/.pitoolsv2.sh" 2>/dev/null; then
|
||||
echo "❌ Не удалось загрузить pitoolsv2 на $host" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 2. Копируем спецификацию проекта
|
||||
if ! scp -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new \
|
||||
"$spec" "$user@$host:~/$spec_basename" 2>/dev/null; then
|
||||
echo "❌ Не удалось загрузить спецификацию на $host" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "⚡ Выполняем удалённо..."
|
||||
|
||||
# 3. Запускаем создание проекта на удалённом хосте
|
||||
ssh -o ConnectTimeout=30 -o StrictHostKeyChecking=accept-new "$user@$host" "
|
||||
echo '🔍 Проверяем загруженные файлы...'
|
||||
|
||||
if [ ! -f ~/.pitoolsv2.sh ]; then
|
||||
echo '❌ Ошибка: pitoolsv2 не найден' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ~/'$spec_basename' ]; then
|
||||
echo '❌ Ошибка: спецификация не найдена' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo '⚙️ Настраиваем окружение...'
|
||||
# Добавляем загрузку pitools в .bashrc если ещё нет
|
||||
if ! grep -q 'pitoolsv2' ~/.bashrc 2>/dev/null; then
|
||||
echo '[ -f ~/.pitoolsv2.sh ] && . ~/.pitoolsv2.sh # PITOOLSv2' >> ~/.bashrc
|
||||
echo ' ✅ Добавили в .bashrc'
|
||||
fi
|
||||
|
||||
# Загружаем скрипт и выполняем
|
||||
. ~/.pitoolsv2.sh 2>/dev/null || {
|
||||
echo '❌ Не удалось загрузить pitoolsv2' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd ~ || { echo '❌ Не могу перейти в домашнюю директорию' >&2; exit 1; }
|
||||
echo '🛠️ Запускаем создание проекта...'
|
||||
|
||||
if piproject '$spec_basename'; then
|
||||
echo '✅ Удалённый проект успешно создан!'
|
||||
echo ''
|
||||
echo 'Созданные файлы:'
|
||||
find . -type f -name '*$spec_basename*' -o -path './*' | head -10
|
||||
exit 0
|
||||
else
|
||||
echo '❌ Ошибка при создании проекта' >&2
|
||||
exit 1
|
||||
fi
|
||||
" 2>&1
|
||||
|
||||
local result=$?
|
||||
if [ $result -eq 0 ]; then
|
||||
echo "✅ $host: проект '$spec_basename' успешно развёрнут!"
|
||||
return 0
|
||||
else
|
||||
echo "❌ Ошибка выполнения на $host" >&2
|
||||
return $result
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------- multideploypi ------------------------
|
||||
# multideploypi INFRA.prj
|
||||
# Читает файл, разбивает на блоки:
|
||||
# MLT:host1,host2
|
||||
# PRJ/DR/FL...
|
||||
# Каждый блок деплоит на свои хосты через deploypi.
|
||||
multideploypi() {
|
||||
local spec="$1"
|
||||
|
||||
# Проверка аргументов
|
||||
[ -z "$spec" ] && {
|
||||
echo "Использование: multideploypi ФАЙЛ_МУЛЬТИ.prj" >&2
|
||||
echo "Пример: multideploypi инфраструктура.prj" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[ ! -f "$spec" ] && {
|
||||
echo "❌ Файл спецификации не найден: $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "🌍 MULTIDEPLOYpi: обрабатываем $spec"
|
||||
|
||||
local line current_hosts="" tmpdir block_spec prj_seen=0
|
||||
local block_counter=0
|
||||
local pids=""
|
||||
local temp_files=""
|
||||
|
||||
# Создаём временную директорию
|
||||
tmpdir="$(mktemp -d -t pitools-XXXXXX 2>/dev/null)" || {
|
||||
echo "❌ Не удалось создать временную директорию" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# Функция для очистки временных файлов
|
||||
cleanup() {
|
||||
for pid in $pids; do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
rm -rf "$tmpdir" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Устанавливаем обработчик прерывания
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# Завершение текущего блока
|
||||
finish_block() {
|
||||
local hosts="$1" specfile="$2"
|
||||
|
||||
[ -z "$hosts" ] && return 0
|
||||
[ ! -s "$specfile" ] && return 0
|
||||
[ "$prj_seen" -eq 0 ] && {
|
||||
echo "⚠️ Блок без PRJ: директивы, пропускаем" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
# Разбиваем хосты по запятым
|
||||
local hosts_arr
|
||||
IFS=',' read -r -a hosts_arr <<< "$hosts"
|
||||
|
||||
local valid_hosts=0
|
||||
for h in "${hosts_arr[@]}"; do
|
||||
h="${h#"${h%%[![:space:]]*}"}"
|
||||
h="${h%"${h##*[![:space:]]}"}"
|
||||
[ -z "$h" ] && continue
|
||||
valid_hosts=$((valid_hosts + 1))
|
||||
done
|
||||
|
||||
if [ "$valid_hosts" -eq 0 ]; then
|
||||
echo "⚠️ В блоке нет валидных хостов, пропускаем" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "🔧 Блок $block_counter → $valid_hosts хост(ов): ${hosts_arr[*]}"
|
||||
|
||||
# Запускаем деплой на каждый хост
|
||||
for h in "${hosts_arr[@]}"; do
|
||||
h="${h#"${h%%[![:space:]]*}"}"
|
||||
h="${h%"${h##*[![:space:]]}"}"
|
||||
[ -z "$h" ] && continue
|
||||
|
||||
# Создаём копию спецификации для каждого хоста
|
||||
local host_spec=""
|
||||
host_spec="$(mktemp "$tmpdir/spec_${h}_XXXXXX.prj" 2>/dev/null)" || continue
|
||||
temp_files="$temp_files $host_spec"
|
||||
cp "$specfile" "$host_spec"
|
||||
|
||||
# Запускаем деплой в фоне
|
||||
(
|
||||
echo " 🚀 Начинаем деплой на $h..."
|
||||
if deploypi "$h" "$USER" "$host_spec" > "$tmpdir/deploy_${h}.log" 2>&1; then
|
||||
echo " ✅ $h: деплой успешен"
|
||||
else
|
||||
echo " ❌ $h: деплой не удался (лог: $tmpdir/deploy_${h}.log)" >&2
|
||||
fi
|
||||
) &
|
||||
pids="$pids $!"
|
||||
done
|
||||
}
|
||||
|
||||
# Инициализируем первый блок
|
||||
block_counter=1
|
||||
block_spec="$(mktemp "$tmpdir/block_${block_counter}_XXXXXX.prj" 2>/dev/null)" || {
|
||||
echo "❌ Не удалось создать временный файл" >&2
|
||||
return 1
|
||||
}
|
||||
temp_files="$temp_files $block_spec"
|
||||
|
||||
echo "📖 Парсим спецификацию..."
|
||||
|
||||
# Читаем файл построчно
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
# Убираем лишние пробелы
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
|
||||
case "$line" in
|
||||
""|"#"*)
|
||||
# Комментарии и пустые строки сохраняем для читаемости
|
||||
[ -n "$block_spec" ] && echo "$line" >> "$block_spec"
|
||||
;;
|
||||
|
||||
MLT:*)
|
||||
# Новый блок хостов
|
||||
echo "📦 Нашли MLT блок: ${line#MLT:}"
|
||||
|
||||
# Завершаем предыдущий блок
|
||||
finish_block "$current_hosts" "$block_spec"
|
||||
|
||||
# Начинаем новый блок
|
||||
current_hosts="${line#MLT:}"
|
||||
block_counter=$((block_counter + 1))
|
||||
block_spec="$(mktemp "$tmpdir/block_${block_counter}_XXXXXX.prj" 2>/dev/null)" || {
|
||||
echo "❌ Не удалось создать временный файл для блока $block_counter" >&2
|
||||
return 1
|
||||
}
|
||||
temp_files="$temp_files $block_spec"
|
||||
prj_seen=0
|
||||
|
||||
# Сохраняем MLT строку в новый блок
|
||||
echo "$line" >> "$block_spec"
|
||||
;;
|
||||
|
||||
PRJ:*)
|
||||
# Начало описания проекта
|
||||
echo "$line" >> "$block_spec"
|
||||
prj_seen=1
|
||||
;;
|
||||
|
||||
DR:*|FL:*)
|
||||
# Описание директорий и файлов
|
||||
[ -z "$block_spec" ] && {
|
||||
echo "❌ DR: или FL: без MLT: блока" >&2
|
||||
return 1
|
||||
}
|
||||
echo "$line" >> "$block_spec"
|
||||
;;
|
||||
|
||||
*)
|
||||
# Неизвестные строки пропускаем с предупреждением
|
||||
echo "⚠️ Неизвестная директива, пропускаем: $line" >&2
|
||||
;;
|
||||
esac
|
||||
done < "$spec"
|
||||
|
||||
# Завершаем последний блок
|
||||
finish_block "$current_hosts" "$block_spec"
|
||||
|
||||
# Ждём завершения всех фоновых процессов
|
||||
if [ -n "$pids" ]; then
|
||||
echo "⏳ Ждём завершения всех деплоев..."
|
||||
local failed=0
|
||||
for pid in $pids; do
|
||||
if wait "$pid"; then
|
||||
echo "✓ Процесс $pid завершился успешно"
|
||||
else
|
||||
echo "✗ Процесс $pid завершился с ошибкой" >&2
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$failed" -eq 1 ]; then
|
||||
echo "⚠️ Некоторые деплои не удались. Логи в: $tmpdir"
|
||||
echo " Посмотреть: ls -la $tmpdir/*.log"
|
||||
# Не удаляем временную директорию при ошибках
|
||||
trap - EXIT INT TERM
|
||||
return 1
|
||||
else
|
||||
echo "✅ Все деплои успешно завершены!"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Нет хостов для деплоя"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# -------------------------- help -----------------------------
|
||||
# Короткая подсказка по доступным командам.
|
||||
pitools_help() {
|
||||
cat <<EOF
|
||||
PITOOLSv2 — Простой SSH-оркестратор для всех
|
||||
===========================================
|
||||
|
||||
Доступные команды:
|
||||
pinstall [-h|-u|-s|-d] ФАЙЛ... Создать файлы/директории
|
||||
-h, --here Унаследовать права от текущей папки
|
||||
-u, --user Стандартные права пользователя (644)
|
||||
-s, --script Исполняемые права (755)
|
||||
-d, --directory Создать директорию (не файл)
|
||||
.sh, .py, .js файлы автоматически получают 755!
|
||||
|
||||
piproject ФАЙЛ.prj Создать структуру проекта
|
||||
Формат:
|
||||
PRJ:имя_проекта # Корень проекта
|
||||
DR:подпапка # Создать папку
|
||||
FL:файл # Создать файл
|
||||
FL:путь/к/файлу # Файл с путём
|
||||
# Комментарии разрешены
|
||||
|
||||
deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ ФАЙЛ.prj Деплой на один хост
|
||||
Пример: deploypi server.ru vasya проект.prj
|
||||
|
||||
multideploypi ФАЙЛ_МУЛЬТИ.prj Деплой на несколько хостов
|
||||
Формат:
|
||||
MLT:хост1,хост2,хост3 # Хосты для этого блока
|
||||
PRJ:проект # Описание проекта
|
||||
DR:... # Папки
|
||||
FL:... # Файлы
|
||||
|
||||
pitools_help Показать эту справку
|
||||
|
||||
Примеры:
|
||||
pinstall script.sh # Создаёт script.sh с правами 755
|
||||
piproject мойпроект.prj # Создать локальный проект
|
||||
deploypi server1 vasya app.prj # Деплой на один сервер
|
||||
multideploypi infra.prj # Деплой на несколько серверов
|
||||
|
||||
Советы:
|
||||
- Если файл/папка уже существует, права будут обновлены
|
||||
- Ошибки не останавливают выполнение (только предупреждения)
|
||||
- Все пути безопасно обрабатываются
|
||||
EOF
|
||||
}
|
||||
|
||||
# Автоматическое выполнение команды если скрипт вызван напрямую
|
||||
if [ "$0" = "${BASH_SOURCE[0]:-$0}" ]; then
|
||||
case "${1:-}" in
|
||||
pinstall|piproject|deploypi|multideploypi|pitools_help)
|
||||
"$@"
|
||||
;;
|
||||
"")
|
||||
echo "PITOOLSv2 загружен успешно!"
|
||||
echo "Используйте 'pitools_help' для справки."
|
||||
echo ""
|
||||
echo "Быстрый старт:"
|
||||
echo " . ./pitoolsv2.sh # Загрузить в текущую сессию"
|
||||
echo " piproject пример.prj # Создать проект"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Неизвестная команда: $1" >&2
|
||||
echo "Используйте: pinstall, piproject, deploypi, multideploypi, pitools_help" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
629
pitoolsv3.sh
Normal file
629
pitoolsv3.sh
Normal file
|
|
@ -0,0 +1,629 @@
|
|||
#!/bin/bash
|
||||
# PITOOLSv3 — DevOps для бабушек и дедушек! V3
|
||||
# Автор: [ТЫ] + брат-АИ
|
||||
# Фикс от 17.12.2024: пути создаются правильно!
|
||||
# ==========================================================
|
||||
|
||||
# 🎯 ОСОБЕННОСТИ V3:
|
||||
# 1. Множественные файлы в одной строке FL:
|
||||
# 2. Вложенные пути в DR: (config/subconfig/nested)
|
||||
# 3. История изменений с комментариями
|
||||
# 4. Не пугает бабушек ошибками
|
||||
# 5. ВСЁ ПРОСТО И РАБОТАЕТ!
|
||||
|
||||
# ------------------------- pinstall --------------------------
|
||||
# pinstall [-h|--here] [-u|--user] [-s|--script] [-d|--directory] FILE...
|
||||
# Создаёт файлы/каталоги с умными правами.
|
||||
pinstall() {
|
||||
local mode=644 owner="$USER" group create_dir=0 verbose=1
|
||||
|
||||
# Получаем группу
|
||||
group="$(id -gn "$USER" 2>/dev/null || id -gn 2>/dev/null || echo "$USER")"
|
||||
|
||||
# Парсим флаги
|
||||
while [ $# -gt 0 ] && [ "${1#-}" != "$1" ]; do
|
||||
case "$1" in
|
||||
-h|--here)
|
||||
if [ -d . ]; then
|
||||
mode="$(stat -c '%a' . 2>/dev/null || echo 644)"
|
||||
owner="$(stat -c '%U' . 2>/dev/null || echo "$USER")"
|
||||
group="$(stat -c '%G' . 2>/dev/null || echo "$group")"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-u|--user)
|
||||
mode=644
|
||||
owner="$USER"
|
||||
group="$(id -gn "$USER" 2>/dev/null || id -gn 2>/dev/null || echo "$USER")"
|
||||
shift
|
||||
;;
|
||||
-s|--script)
|
||||
mode=755
|
||||
shift
|
||||
;;
|
||||
-d|--directory)
|
||||
create_dir=1
|
||||
shift
|
||||
;;
|
||||
-q|--quiet)
|
||||
verbose=0
|
||||
shift
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
echo "pinstall: неверный флаг: $1" >&2
|
||||
return 1
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ $# -eq 0 ] && {
|
||||
echo "Использование: pinstall [-h|-u|-s|-d|-q] ФАЙЛ..." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local created=0
|
||||
|
||||
for item in "$@"; do
|
||||
# Поддерживаем несколько файлов через пробел
|
||||
local items
|
||||
IFS=' ' read -ra items <<< "$item"
|
||||
|
||||
for f in "${items[@]}"; do
|
||||
[ -z "$f" ] && continue
|
||||
|
||||
local eff_mode="$mode"
|
||||
|
||||
# Авто-права для скриптов
|
||||
case "$(basename "$f")" in
|
||||
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
|
||||
eff_mode=755
|
||||
;;
|
||||
esac
|
||||
|
||||
# Убираем / в конце для проверки
|
||||
local clean_path="${f%/}"
|
||||
|
||||
# Существует?
|
||||
if [ -e "$clean_path" ]; then
|
||||
[ "$verbose" -eq 1 ] && {
|
||||
if [ -d "$clean_path" ]; then
|
||||
echo " 📂 Уже есть: $clean_path" >&2
|
||||
else
|
||||
echo " 📄 Уже есть: $clean_path" >&2
|
||||
fi
|
||||
}
|
||||
# Обновляем права если нужно
|
||||
chmod "$eff_mode" "$clean_path" 2>/dev/null
|
||||
chown "$owner:$group" "$clean_path" 2>/dev/null
|
||||
continue
|
||||
fi
|
||||
|
||||
# Определяем тип
|
||||
local is_dir="$create_dir"
|
||||
[ "$is_dir" -eq 0 ] && [[ "$f" == */ ]] && is_dir=1
|
||||
|
||||
if [ "$is_dir" -eq 1 ]; then
|
||||
# СОЗДАЁМ ДИРЕКТОРИЮ
|
||||
if mkdir -p "$clean_path" 2>/dev/null; then
|
||||
chmod "$eff_mode" "$clean_path" 2>/dev/null
|
||||
chown "$owner:$group" "$clean_path" 2>/dev/null
|
||||
[ "$verbose" -eq 1 ] && echo " 📂 Создана: $clean_path" >&2
|
||||
created=$((created + 1))
|
||||
else
|
||||
[ "$verbose" -eq 1 ] && echo " ❌ Ошибка: $clean_path" >&2
|
||||
fi
|
||||
else
|
||||
# СОЗДАЁМ ФАЙЛ
|
||||
# Сначала родительские директории
|
||||
local parent_dir="$(dirname "$f")"
|
||||
if [ "$parent_dir" != "." ] && [ ! -d "$parent_dir" ]; then
|
||||
mkdir -p "$parent_dir" 2>/dev/null
|
||||
fi
|
||||
|
||||
if touch "$f" 2>/dev/null; then
|
||||
chmod "$eff_mode" "$f" 2>/dev/null
|
||||
chown "$owner:$group" "$f" 2>/dev/null
|
||||
[ "$verbose" -eq 1 ] && {
|
||||
case "$(basename "$f")" in
|
||||
*.sh|*.py|*.pl|*.rb|*.go|*.js|Makefile|makefile|*.mk)
|
||||
echo " 🔥 755 → $f" >&2
|
||||
;;
|
||||
*)
|
||||
echo " 📄 Создан: $f" >&2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
created=$((created + 1))
|
||||
else
|
||||
[ "$verbose" -eq 1 ] && echo " ❌ Ошибка: $f" >&2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
[ "$verbose" -eq 1 ] && [ "$created" -gt 0 ] && echo "✅ Создано: $created" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------ piproject --------------------------
|
||||
# piproject SPEC.prj
|
||||
# ФИКС: Все пути создаются относительно корня правильно!
|
||||
piproject() {
|
||||
local spec="$1" root="" curdir="" line="" line_num=0
|
||||
|
||||
[ -z "$spec" ] && {
|
||||
echo "Использование: piproject ФАЙЛ.prj" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[ ! -f "$spec" ] && {
|
||||
echo "❌ Не найден: $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "🌳 Читаем: $spec"
|
||||
echo "══════════════════════════════════════"
|
||||
|
||||
# Проверяем формат
|
||||
if ! grep -q "^PRJ:" "$spec" 2>/dev/null; then
|
||||
echo "⚠️ Нет PRJ: в файле (но продолжим)" >&2
|
||||
fi
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line_num=$((line_num + 1))
|
||||
|
||||
# Чистим строку
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
|
||||
# Комментарий или пустая строка
|
||||
if [[ -z "$line" ]] || [[ "$line" == \#* ]]; then
|
||||
[[ "$line" == \#* ]] && [ ${#line} -lt 50 ] && echo " 💬 $line" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# ОБРАБОТКА СТРОК
|
||||
if [[ "$line" == PRJ:* ]]; then
|
||||
# НОВЫЙ ПРОЕКТ
|
||||
root="${line#PRJ:}"
|
||||
curdir="$root" # Текущая директория = корень
|
||||
|
||||
echo "📁 ПРОЕКТ: $root/"
|
||||
echo "──────────────────────────────────"
|
||||
|
||||
if pinstall -d -q "$root/"; then
|
||||
echo " 📂 Корень создан"
|
||||
fi
|
||||
|
||||
elif [[ "$line" == DR:* ]]; then
|
||||
# ДИРЕКТОРИЯ
|
||||
[ -z "$root" ] && {
|
||||
echo "❌ Строка $line_num: сначала нужен PRJ:" >&2
|
||||
continue
|
||||
}
|
||||
|
||||
local dir_part="${line#DR:}"
|
||||
|
||||
# Разбиваем несколько директорий через пробел
|
||||
local dir_items
|
||||
IFS=' ' read -ra dir_items <<< "$dir_part"
|
||||
|
||||
for dir_spec in "${dir_items[@]}"; do
|
||||
[ -z "$dir_spec" ] && continue
|
||||
|
||||
# Добавляем / если нет
|
||||
[[ "$dir_spec" != */ ]] && dir_spec="$dir_spec/"
|
||||
|
||||
# ВСЕГДА создаём от корня!
|
||||
local full_dir="$root/$dir_spec"
|
||||
|
||||
# Убираем возможные двойные слеши
|
||||
while [[ "$full_dir" == *//* ]]; do
|
||||
full_dir="${full_dir//\/\//\/}"
|
||||
done
|
||||
|
||||
# Создаём директорию
|
||||
if pinstall -d -q "${full_dir%/}"; then
|
||||
echo " 📂 Папка: $dir_spec"
|
||||
fi
|
||||
|
||||
# Запоминаем последнюю созданную папку для файлов без пути
|
||||
curdir="${full_dir%/}"
|
||||
done
|
||||
|
||||
elif [[ "$line" == FL:* ]]; then
|
||||
# ФАЙЛЫ
|
||||
[ -z "$root" ] && {
|
||||
echo "❌ Строка $line_num: сначала нужен PRJ:" >&2
|
||||
continue
|
||||
}
|
||||
|
||||
local file_part="${line#FL:}"
|
||||
|
||||
if [ -z "$file_part" ]; then
|
||||
echo "⚠️ Строка $line_num: пустой FL:" >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
# Разбиваем несколько файлов
|
||||
local file_items
|
||||
IFS=' ' read -ra file_items <<< "$file_part"
|
||||
|
||||
for file_spec in "${file_items[@]}"; do
|
||||
[ -z "$file_spec" ] && continue
|
||||
|
||||
local full_path=""
|
||||
|
||||
# Определяем путь
|
||||
if [[ "$file_spec" == */* ]]; then
|
||||
# Файл с путём - относительно КОРНЯ
|
||||
full_path="$root/$file_spec"
|
||||
|
||||
# Создаём родительские директории если нужно
|
||||
local parent_dir="${full_path%/*}"
|
||||
if [ ! -d "$parent_dir" ]; then
|
||||
pinstall -d -q "$parent_dir/" >/dev/null 2>&1
|
||||
fi
|
||||
else
|
||||
# Просто имя файла - в ТЕКУЩУЮ директорию
|
||||
[ -z "$curdir" ] && curdir="$root"
|
||||
full_path="$curdir/$file_spec"
|
||||
fi
|
||||
|
||||
# Убираем двойные слеши
|
||||
while [[ "$full_path" == *//* ]]; do
|
||||
full_path="${full_path//\/\//\/}"
|
||||
done
|
||||
|
||||
# Создаём файл
|
||||
if pinstall "$full_path"; then
|
||||
echo " 📄 Файл: $file_spec"
|
||||
fi
|
||||
done
|
||||
|
||||
else
|
||||
# Неизвестная директива
|
||||
echo "⚠️ Строка $line_num: непонятно - '$line'" >&2
|
||||
fi
|
||||
|
||||
done < "$spec"
|
||||
|
||||
echo "══════════════════════════════════════"
|
||||
if [ -n "$root" ]; then
|
||||
echo "✅ Готово! Проект: $root/"
|
||||
echo "📁 Структура:"
|
||||
find "$root" -type d 2>/dev/null | head -20
|
||||
else
|
||||
echo "ℹ️ Ничего не создано"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------ deploypi ---------------------------
|
||||
# deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ SPEC.prj
|
||||
deploypi() {
|
||||
local host="$1" user="$2" spec="$3"
|
||||
|
||||
[ -z "$host" ] || [ -z "$user" ] || [ -z "$spec" ] && {
|
||||
echo "Использование: deploypi ХОСТ ПОЛЬЗОВАТЕЛЬ ФАЙЛ.prj" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[ ! -f "$spec" ] && {
|
||||
echo "❌ Не найден: $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "🚀 ДЕПЛОЙ: $spec → $user@$host"
|
||||
echo "──────────────────────────────────"
|
||||
|
||||
# Имя файла
|
||||
local spec_name="project_$(date +%s).prj"
|
||||
|
||||
# Проверяем SSH
|
||||
echo "🔍 Проверяем связь..."
|
||||
if ! ssh -o ConnectTimeout=5 "$user@$host" "exit 0" 2>/dev/null; then
|
||||
echo "❌ Не могу подключиться к $user@$host" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Загружаем спецификацию
|
||||
echo "📤 Загружаем..."
|
||||
if ! scp "$spec" "$user@$host:~/$spec_name" 2>/dev/null; then
|
||||
echo "❌ Ошибка загрузки" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Определяем путь к себе
|
||||
local self_path="$0"
|
||||
[ "$self_path" = "bash" ] && self_path="pitoolsv3.sh"
|
||||
|
||||
# Загружаем pitoolsv3 если нет
|
||||
echo "⚙️ Настраиваем..."
|
||||
ssh "$user@$host" "
|
||||
# Скачиваем или создаём pitoolsv3
|
||||
if [ ! -f ~/pitoolsv3.sh ]; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -s 'https://raw.githubusercontent.com/пример/pitools/main/pitoolsv3.sh' -o ~/pitoolsv3.sh
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q -O ~/pitoolsv3.sh 'https://raw.githubusercontent.com/пример/pitools/main/pitoolsv3.sh'
|
||||
else
|
||||
# Создаём минимальную версию
|
||||
cat > ~/pitoolsv3.sh <<'MINI'
|
||||
#!/bin/bash
|
||||
# Минимальный PITOOLSv3
|
||||
pinstall() {
|
||||
for f in \"\$@\"; do
|
||||
if [[ \"\$f\" == */ ]]; then
|
||||
mkdir -p \"\${f%/}\" && echo \" 📂 \${f%/}\"
|
||||
else
|
||||
touch \"\$f\" && echo \" 📄 \$f\"
|
||||
fi
|
||||
done
|
||||
}
|
||||
piproject() {
|
||||
local spec=\"\$1\" root=\"\" curdir=\"\"
|
||||
while IFS= read -r line; do
|
||||
case \"\$line\" in
|
||||
PRJ:*) root=\"\${line#PRJ:}\"; mkdir -p \"\$root\" ;;
|
||||
DR:*) mkdir -p \"\$root/\${line#DR:}\" ;;
|
||||
FL:*) touch \"\$root/\${line#FL:}\" ;;
|
||||
esac
|
||||
done < \"\$spec\"
|
||||
echo \"✅ Готово: \$root/\"
|
||||
}
|
||||
MINI
|
||||
chmod +x ~/pitoolsv3.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
# Запускаем
|
||||
. ~/pitoolsv3.sh
|
||||
cd ~
|
||||
piproject \"$spec_name\"
|
||||
|
||||
# Убираем за собой
|
||||
rm -f \"$spec_name\"
|
||||
" 2>&1
|
||||
|
||||
local result=$?
|
||||
|
||||
if [ $result -eq 0 ]; then
|
||||
echo "✅ $host: деплой успешен!"
|
||||
else
|
||||
echo "❌ $host: ошибка деплоя" >&2
|
||||
fi
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# ---------------------- multideploypi ------------------------
|
||||
# multideploypi INFRA.prj
|
||||
multideploypi() {
|
||||
local spec="$1"
|
||||
|
||||
[ -z "$spec" ] && {
|
||||
echo "Использование: multideploypi ФАЙЛ.prj" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[ ! -f "$spec" ] && {
|
||||
echo "❌ Не найден: $spec" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "🌍 МУЛЬТИДЕПЛОЙ: $spec"
|
||||
echo "══════════════════════════════════════"
|
||||
|
||||
local current_hosts="" block_content="" in_block=0 block_num=0
|
||||
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line="${line#"${line%%[![:space:]]*}"}"
|
||||
line="${line%"${line##*[![:space:]]}"}"
|
||||
|
||||
if [[ "$line" == MLT:* ]]; then
|
||||
# Новый блок
|
||||
if [ -n "$current_hosts" ] && [ -n "$block_content" ]; then
|
||||
block_num=$((block_num + 1))
|
||||
echo "🔧 Блок $block_num → $(echo "$current_hosts" | tr ',' ' ' | wc -w) хост(ов)"
|
||||
|
||||
# Деплоим
|
||||
local hosts
|
||||
IFS=',' read -ra hosts <<< "$current_hosts"
|
||||
for host in "${hosts[@]}"; do
|
||||
host="${host#"${host%%[![:space:]]*}"}"
|
||||
host="${host%"${host##*[![:space:]]}"}"
|
||||
[ -z "$host" ] && continue
|
||||
|
||||
echo " 🚀 $host..."
|
||||
( deploypi "$host" "$USER" <(echo "$block_content") >/dev/null 2>&1 && \
|
||||
echo " ✅ Готово" || \
|
||||
echo " ❌ Ошибка" ) &
|
||||
done
|
||||
wait
|
||||
fi
|
||||
|
||||
current_hosts="${line#MLT:}"
|
||||
block_content=""
|
||||
|
||||
elif [[ "$line" == PRJ:* || "$line" == DR:* || "$line" == FL:* || \
|
||||
"$line" == "" || "$line" == \#* ]]; then
|
||||
# Добавляем в блок
|
||||
block_content="$block_content"$'\n'"$line"
|
||||
|
||||
else
|
||||
echo "⚠️ Пропускаем: $line" >&2
|
||||
fi
|
||||
|
||||
done < "$spec"
|
||||
|
||||
# Последний блок
|
||||
if [ -n "$current_hosts" ] && [ -n "$block_content" ]; then
|
||||
block_num=$((block_num + 1))
|
||||
echo "🔧 Блок $block_num → $(echo "$current_hosts" | tr ',' ' ' | wc -w) хост(ов)"
|
||||
|
||||
local hosts
|
||||
IFS=',' read -ra hosts <<< "$current_hosts"
|
||||
for host in "${hosts[@]}"; do
|
||||
host="${host#"${host%%[![:space:]]*}"}"
|
||||
host="${host%"${host##*[![:space:]]}"}"
|
||||
[ -z "$host" ] && continue
|
||||
|
||||
echo " 🚀 $host..."
|
||||
( deploypi "$host" "$USER" <(echo "$block_content") >/dev/null 2>&1 && \
|
||||
echo " ✅ Готово" || \
|
||||
echo " ❌ Ошибка" ) &
|
||||
done
|
||||
wait
|
||||
fi
|
||||
|
||||
echo "══════════════════════════════════════"
|
||||
echo "✅ Мультидеплой завершён!"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------- pifill & pifetch ---------------------
|
||||
pifill() {
|
||||
local url="$1"
|
||||
|
||||
[ -z "$url" ] && {
|
||||
echo "Использование: pifill ssh://user@host/path < файл" >&2
|
||||
echo " pifill ssh://user@host/path -c 'текст'" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[[ "$url" != ssh://* ]] && {
|
||||
echo "❌ Только ssh:// протокол" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local rest="${url#ssh://}"
|
||||
local user_host="${rest%%/*}"
|
||||
local path="${rest#*/}"
|
||||
local user="${user_host%%@*}"
|
||||
local host="${user_host#*@}"
|
||||
|
||||
echo "📤 Заливаем в $user@$host:$path"
|
||||
|
||||
if [ "$2" = "-c" ]; then
|
||||
ssh "$user@$host" "mkdir -p '$(dirname "$path")' && cat > '$path'" <<< "$3"
|
||||
else
|
||||
ssh "$user@$host" "mkdir -p '$(dirname "$path")' && cat > '$path'"
|
||||
fi
|
||||
|
||||
echo "✅ Готово!"
|
||||
}
|
||||
|
||||
pifetch() {
|
||||
local url="$1"
|
||||
|
||||
[ -z "$url" ] && {
|
||||
echo "Использование: pifetch ssh://user@host/path > файл" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
[[ "$url" != ssh://* ]] && {
|
||||
echo "❌ Только ssh:// протокол" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local rest="${url#ssh://}"
|
||||
local user_host="${rest%%/*}"
|
||||
local path="${rest#*/}"
|
||||
local user="${user_host%%@*}"
|
||||
local host="${user_host#*@}"
|
||||
|
||||
if [ "$2" = "-o" ]; then
|
||||
scp "$user@$host:$path" "$3"
|
||||
echo "✅ Скачан в $3"
|
||||
else
|
||||
ssh "$user@$host" "cat '$path'"
|
||||
fi
|
||||
}
|
||||
|
||||
# -------------------------- help -----------------------------
|
||||
pitools_help() {
|
||||
cat <<'EOF'
|
||||
🔥 PITOOLSv3 — DevOps для всех!
|
||||
|
||||
📚 КОМАНДЫ:
|
||||
pinstall [-опции] ФАЙЛ... Создать файлы/папки
|
||||
-h Права как у папки -u 644
|
||||
-s Скрипт (755) -d Папка
|
||||
-q Тихий режим
|
||||
|
||||
piproject файл.prj Создать проект
|
||||
Формат .prj:
|
||||
PRJ:имя # Корень
|
||||
DR:папка/внутри/ # Папка (можно несколько)
|
||||
FL:файл1 файл2 # Файлы (можно несколько)
|
||||
# Комментарии
|
||||
|
||||
deploypi хост юзер файл.prj Деплой на сервер
|
||||
multideploypi файл.prj Деплой на несколько серверов
|
||||
MLT:хост1,хост2 # Список серверов
|
||||
|
||||
pifill ssh://юзер@хост/путь Залить файл
|
||||
pifetch ssh://юзер@хост/путь Скачать файл
|
||||
|
||||
📝 ПРИМЕР:
|
||||
PRJ:мой_сайт
|
||||
DR:www/ config/ backup/
|
||||
FL:index.html style.css
|
||||
DR:scripts/
|
||||
FL:start.sh stop.sh
|
||||
|
||||
✨ Особенности V3:
|
||||
• Несколько файлов/папок в одной строке
|
||||
• Вложенные пути работают правильно
|
||||
• Не ругается если уже существует
|
||||
• Бабушка поймёт!
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ---------------------- автозапуск ---------------------------
|
||||
if [ "$0" = "${BASH_SOURCE[0]:-$0}" ]; then
|
||||
case "${1:-}" in
|
||||
pinstall|piproject|deploypi|multideploypi|pifill|pifetch|pitools_help)
|
||||
"$@"
|
||||
;;
|
||||
"test")
|
||||
# Тестовый прогон
|
||||
cat > test_simple.prj <<'TEST'
|
||||
PRJ:test_project
|
||||
DR:app/config db/logs/
|
||||
FL:settings.yaml database.conf
|
||||
DR:scripts/
|
||||
FL:start.sh stop.sh
|
||||
# Комментарий
|
||||
TEST
|
||||
echo "🧪 Тестовый прогон..."
|
||||
piproject test_simple.prj
|
||||
echo ""
|
||||
echo "📁 Результат:"
|
||||
find test_project -type f 2>/dev/null || echo "Не создано"
|
||||
;;
|
||||
"")
|
||||
echo "🔥 PITOOLSv3 загружен!"
|
||||
echo "💡 Используй 'pitools_help' для справки"
|
||||
echo ""
|
||||
echo "🚀 Быстрый старт:"
|
||||
echo " . ./pitoolsv3.sh"
|
||||
echo " echo 'PRJ:мой_проект' > проект.prj"
|
||||
echo " piproject проект.prj"
|
||||
;;
|
||||
*)
|
||||
echo "❌ Неизвестная команда: $1" >&2
|
||||
echo "💡 Используй 'pitools_help'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
Loading…
Add table
Add a link
Reference in a new issue