70 lines
2.3 KiB
Python
70 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'))}
|