Automate post-import refresh and validation workflow
This commit is contained in:
+78
-50
@@ -6,15 +6,12 @@ post-import reset steps. It reports whether the test instance looks ready for
|
||||
Helpdesk and redMCP testing without changing remote state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -26,20 +23,30 @@ DEFAULT_MAILPIT_HOST = "192.168.1.105"
|
||||
DEFAULT_FILES_ROOT = "/var/lib/redmine/default/files"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CheckResult:
|
||||
status: str
|
||||
name: str
|
||||
detail: str
|
||||
def __init__(self, status, name, detail):
|
||||
self.status = status
|
||||
self.name = name
|
||||
self.detail = detail
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RemoteRedmine:
|
||||
ssh_host: str
|
||||
ssh_key: Path
|
||||
remote_redmine: str
|
||||
def __init__(self, ssh_host, ssh_key, remote_redmine, local=False):
|
||||
self.ssh_host = ssh_host
|
||||
self.ssh_key = ssh_key
|
||||
self.remote_redmine = remote_redmine
|
||||
self.local = local
|
||||
|
||||
def ssh(self, remote_command: str) -> subprocess.CompletedProcess[str]:
|
||||
def ssh(self, remote_command):
|
||||
if self.local:
|
||||
return subprocess.run(
|
||||
remote_command,
|
||||
shell=True,
|
||||
universal_newlines=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
return subprocess.run(
|
||||
[
|
||||
"ssh",
|
||||
@@ -50,43 +57,49 @@ class RemoteRedmine:
|
||||
self.ssh_host,
|
||||
remote_command,
|
||||
],
|
||||
text=True,
|
||||
universal_newlines=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def mysql_json_lines(self, sql: str) -> list[dict[str, Any]]:
|
||||
def mysql_json_lines(self, sql):
|
||||
result = self.mysql(sql)
|
||||
rows: list[dict[str, Any]] = []
|
||||
rows = []
|
||||
for line in result.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
rows.append(json.loads(bytes.fromhex(line.strip()).decode("utf-8")))
|
||||
return rows
|
||||
|
||||
def mysql(self, sql: str) -> str:
|
||||
def mysql(self, sql):
|
||||
command = self._mysql_runner_command()
|
||||
shell = True
|
||||
if not self.local:
|
||||
command = [
|
||||
"ssh",
|
||||
"-i",
|
||||
str(self.ssh_key),
|
||||
"-o",
|
||||
"IdentitiesOnly=yes",
|
||||
self.ssh_host,
|
||||
self._mysql_runner_command(),
|
||||
]
|
||||
shell = False
|
||||
result = subprocess.run(
|
||||
[
|
||||
"ssh",
|
||||
"-i",
|
||||
str(self.ssh_key),
|
||||
"-o",
|
||||
"IdentitiesOnly=yes",
|
||||
self.ssh_host,
|
||||
self._mysql_runner_command(),
|
||||
],
|
||||
command,
|
||||
input=sql,
|
||||
text=True,
|
||||
universal_newlines=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
shell=shell,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(result.stderr.strip() or "Remote MySQL command failed.")
|
||||
return result.stdout
|
||||
|
||||
def _mysql_runner_command(self) -> str:
|
||||
def _mysql_runner_command(self):
|
||||
ruby = (
|
||||
"require 'yaml'; "
|
||||
"c = YAML.load_file('config/database.yml')['production']; "
|
||||
@@ -99,18 +112,23 @@ class RemoteRedmine:
|
||||
return f"cd {shell_quote(self.remote_redmine)} && ruby -e {shell_quote(ruby)}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Read-only checks for the Redmine LAN test instance.")
|
||||
parser.add_argument("--ssh-host", default=os.getenv("REDMINE_SSH_HOST", DEFAULT_SSH_HOST))
|
||||
parser.add_argument("--ssh-key", type=Path, default=Path(os.getenv("REDMINE_SSH_KEY", str(DEFAULT_SSH_KEY))))
|
||||
parser.add_argument("--local", action="store_true", help="Validate local Redmine paths/database instead of using SSH.")
|
||||
parser.add_argument("--remote-redmine", default=os.getenv("REDMINE_REMOTE_PATH", DEFAULT_REMOTE_REDMINE))
|
||||
parser.add_argument("--mailpit-host", default=DEFAULT_MAILPIT_HOST)
|
||||
parser.add_argument("--files-root", default=DEFAULT_FILES_ROOT)
|
||||
parser.add_argument("--composer-bin", default=os.getenv("COMPOSER_BIN", "composer"))
|
||||
parser.add_argument(
|
||||
"--composer-bin",
|
||||
default=os.getenv("COMPOSER_BIN"),
|
||||
help="Optional Composer binary or composer.phar for redMCP validation.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
remote = RemoteRedmine(args.ssh_host, args.ssh_key, args.remote_redmine)
|
||||
checks: list[CheckResult] = []
|
||||
remote = RemoteRedmine(args.ssh_host, args.ssh_key, args.remote_redmine, local=args.local)
|
||||
checks = []
|
||||
|
||||
checks.extend(check_remote_basics(remote))
|
||||
checks.extend(check_mailpit_connectivity(remote, args.mailpit_host))
|
||||
@@ -127,8 +145,8 @@ def main() -> int:
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
def check_remote_basics(remote: RemoteRedmine) -> list[CheckResult]:
|
||||
results: list[CheckResult] = []
|
||||
def check_remote_basics(remote):
|
||||
results = []
|
||||
result = remote.ssh("printf ok")
|
||||
if result.returncode == 0 and result.stdout == "ok":
|
||||
results.append(CheckResult("OK", "SSH", f"connected to {remote.ssh_host}"))
|
||||
@@ -155,7 +173,7 @@ def check_remote_basics(remote: RemoteRedmine) -> list[CheckResult]:
|
||||
return results
|
||||
|
||||
|
||||
def check_mailpit_connectivity(remote: RemoteRedmine, host: str) -> list[CheckResult]:
|
||||
def check_mailpit_connectivity(remote, host):
|
||||
results = [
|
||||
tcp_check("Mailpit HTTP from local", host, 8025),
|
||||
tcp_check("Mailpit SMTP from local", host, 1025),
|
||||
@@ -181,7 +199,7 @@ def check_mailpit_connectivity(remote: RemoteRedmine, host: str) -> list[CheckRe
|
||||
return results
|
||||
|
||||
|
||||
def check_files_permissions(remote: RemoteRedmine, files_root: str) -> list[CheckResult]:
|
||||
def check_files_permissions(remote, files_root):
|
||||
command = (
|
||||
"ruby -e "
|
||||
+ shell_quote(
|
||||
@@ -208,8 +226,8 @@ def check_files_permissions(remote: RemoteRedmine, files_root: str) -> list[Chec
|
||||
return [CheckResult("OK", "Attachment directory permissions", detail)]
|
||||
|
||||
|
||||
def check_database_state(remote: RemoteRedmine, mailpit_host: str) -> list[CheckResult]:
|
||||
results: list[CheckResult] = []
|
||||
def check_database_state(remote, mailpit_host):
|
||||
results = []
|
||||
try:
|
||||
projects = remote.mysql_json_lines(
|
||||
"""
|
||||
@@ -219,12 +237,7 @@ WHERE identifier IN ('fud-helpdesk', 'fud-nohelpdesk')
|
||||
ORDER BY identifier;
|
||||
"""
|
||||
)
|
||||
found = {project["identifier"] for project in projects}
|
||||
missing = {"fud-helpdesk", "fud-nohelpdesk"} - found
|
||||
if missing:
|
||||
results.append(CheckResult("FAIL", "Controlled test projects", "missing " + ", ".join(sorted(missing))))
|
||||
else:
|
||||
results.append(CheckResult("OK", "Controlled test projects", ", ".join(sorted(found))))
|
||||
results.append(controlled_project_check(projects))
|
||||
|
||||
settings_rows = remote.mysql_json_lines(helpdesk_settings_sql())
|
||||
failures = helpdesk_setting_failures(settings_rows, mailpit_host)
|
||||
@@ -237,7 +250,7 @@ ORDER BY identifier;
|
||||
return results
|
||||
|
||||
|
||||
def helpdesk_settings_sql() -> str:
|
||||
def helpdesk_settings_sql():
|
||||
return """
|
||||
SELECT HEX(CAST(JSON_OBJECT(
|
||||
'identifier', p.identifier,
|
||||
@@ -264,7 +277,7 @@ ORDER BY p.identifier;
|
||||
"""
|
||||
|
||||
|
||||
def helpdesk_setting_failures(rows: list[dict[str, Any]], mailpit_host: str) -> list[str]:
|
||||
def helpdesk_setting_failures(rows, mailpit_host):
|
||||
expected = {
|
||||
"protocol": "pop3",
|
||||
"host": mailpit_host,
|
||||
@@ -281,7 +294,7 @@ def helpdesk_setting_failures(rows: list[dict[str, Any]], mailpit_host: str) ->
|
||||
"smtp_ssl": "0",
|
||||
"smtp_tls": "0",
|
||||
}
|
||||
failures: list[str] = []
|
||||
failures = []
|
||||
for row in rows:
|
||||
for key, value in expected.items():
|
||||
if row.get(key) != value:
|
||||
@@ -289,7 +302,22 @@ def helpdesk_setting_failures(rows: list[dict[str, Any]], mailpit_host: str) ->
|
||||
return failures
|
||||
|
||||
|
||||
def check_composer(composer_bin: str) -> CheckResult:
|
||||
def controlled_project_check(projects):
|
||||
found = {project["identifier"] for project in projects}
|
||||
missing = {"fud-helpdesk", "fud-nohelpdesk"} - found
|
||||
if missing:
|
||||
return CheckResult(
|
||||
"WARN",
|
||||
"Controlled test projects",
|
||||
"optional smoke-test project(s) missing after production clone: "
|
||||
+ ", ".join(sorted(missing)),
|
||||
)
|
||||
return CheckResult("OK", "Controlled test projects", ", ".join(sorted(found)))
|
||||
|
||||
|
||||
def check_composer(composer_bin):
|
||||
if not composer_bin:
|
||||
return CheckResult("WARN", "Composer validation", "skipped; pass --composer-bin to enable")
|
||||
composer_path = Path(composer_bin)
|
||||
composer_on_path = shutil.which(composer_bin)
|
||||
if composer_on_path is None and not composer_path.exists():
|
||||
@@ -302,7 +330,7 @@ def check_composer(composer_bin: str) -> CheckResult:
|
||||
command = [php, composer_bin, "validate", "--working-dir=redMCP"]
|
||||
result = subprocess.run(
|
||||
command,
|
||||
text=True,
|
||||
universal_newlines=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
@@ -312,7 +340,7 @@ def check_composer(composer_bin: str) -> CheckResult:
|
||||
return CheckResult("FAIL", "Composer validation", (result.stdout + result.stderr).strip())
|
||||
|
||||
|
||||
def tcp_check(name: str, host: str, port: int) -> CheckResult:
|
||||
def tcp_check(name, host, port):
|
||||
try:
|
||||
with socket.create_connection((host, port), timeout=5):
|
||||
return CheckResult("OK", name, f"{host}:{port}")
|
||||
@@ -320,7 +348,7 @@ def tcp_check(name: str, host: str, port: int) -> CheckResult:
|
||||
return CheckResult("FAIL", name, f"{host}:{port} {exc.__class__.__name__}: {exc}")
|
||||
|
||||
|
||||
def shell_quote(value: str) -> str:
|
||||
def shell_quote(value):
|
||||
return "'" + value.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user