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