Add owner-only update controls, a read-only SSH checker and independent systemd deployment services. Build before maintenance; back up code and database; restore on failed deployment. Preserve runtime configuration and gate business writes during the switch.
71 lines
2.3 KiB
Python
71 lines
2.3 KiB
Python
"""Persistent state shared by the independent update services."""
|
|
from __future__ import annotations
|
|
|
|
import fcntl
|
|
import json
|
|
import os
|
|
from contextlib import contextmanager
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Iterator
|
|
|
|
CONFIG_FILE = Path('/etc/replypier-updater/config.json')
|
|
BUSY = {'queued', 'preparing', 'building', 'backing_up', 'deploying', 'migrating', 'verifying', 'rolling_back'}
|
|
|
|
|
|
def now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def config() -> dict[str, Any]:
|
|
return json.loads(CONFIG_FILE.read_text())
|
|
|
|
|
|
def directory(cfg: dict[str, Any]) -> Path:
|
|
return Path(cfg['state_dir'])
|
|
|
|
|
|
def read(cfg: dict[str, Any]) -> dict[str, Any]:
|
|
p = directory(cfg) / 'status.json'
|
|
return json.loads(p.read_text()) if p.exists() else {
|
|
'current': {'revision': cfg['initial_revision'], 'subject': '已安装版本'},
|
|
'latest': None, 'checked_at': None, 'checking': False,
|
|
'check_error': None, 'update': {'status': 'idle', 'message': ''},
|
|
}
|
|
|
|
|
|
def write(cfg: dict[str, Any], state: dict[str, Any]) -> None:
|
|
target = directory(cfg) / 'status.json'
|
|
temp = target.with_suffix('.tmp')
|
|
with temp.open('w') as out:
|
|
json.dump(state, out, ensure_ascii=False, indent=2)
|
|
out.flush()
|
|
os.fsync(out.fileno())
|
|
temp.chmod(0o644)
|
|
temp.replace(target)
|
|
|
|
|
|
@contextmanager
|
|
def transaction(cfg: dict[str, Any]) -> Iterator[dict[str, Any]]:
|
|
directory(cfg).mkdir(parents=True, exist_ok=True)
|
|
with (directory(cfg) / 'state.lock').open('a') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
state = read(cfg)
|
|
yield state
|
|
write(cfg, state)
|
|
|
|
|
|
def progress(cfg: dict[str, Any], status: str, message: str, **values: Any) -> None:
|
|
with transaction(cfg) as state:
|
|
state['update'].update(status=status, message=message, **values)
|
|
|
|
|
|
def public_status(cfg: dict[str, Any]) -> dict[str, Any]:
|
|
state = read(cfg)
|
|
current = state['current']['revision']
|
|
latest = (state.get('latest') or {}).get('revision')
|
|
return {**state, 'configured': True, 'can_manage': True,
|
|
'repository_url': cfg['web_url'], 'branch': cfg['branch'],
|
|
'check_interval_minutes': 30,
|
|
'update_available': bool(latest and latest != current and not state.get('check_error'))}
|