vendor(qemu-ga): svos-dev's nh3-svos quiesce hook + its 6 controls beside the dispatcher
This commit is contained in:
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/python3
|
||||
"""Stop the installed SVOS services for a VM backup, then restore their prior state."""
|
||||
import argparse
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
SERVICES = ('svos-kernel.service', 'svos-interface.service')
|
||||
|
||||
class Quiesce:
|
||||
def __init__(self, directory, *, run=None):
|
||||
self.directory = Path(directory)
|
||||
self.directory.mkdir(mode=0o700, exist_ok=True)
|
||||
info = self.directory.lstat()
|
||||
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid() or stat.S_IMODE(info.st_mode) != 0o700:
|
||||
raise RuntimeError('backup state directory must be private and owned by this user')
|
||||
self.state = self.directory / 'state.json'
|
||||
self.run = run or self._run
|
||||
|
||||
@staticmethod
|
||||
def _run(args, **kwargs):
|
||||
return subprocess.run(['systemctl', *args], check=True, text=True, capture_output=True, **kwargs)
|
||||
|
||||
def status(self, unit, deadline):
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise TimeoutError('service quiesce deadline exceeded')
|
||||
answer = self.run(['show', unit, '--property=ActiveState,Result'], timeout=min(5, remaining))
|
||||
fields = dict(line.split('=', 1) for line in answer.stdout.splitlines() if '=' in line)
|
||||
return fields.get('ActiveState'), fields.get('Result')
|
||||
|
||||
def record(self, active):
|
||||
with tempfile.NamedTemporaryFile(mode='w', dir=self.directory, delete=False) as output:
|
||||
name = output.name
|
||||
json.dump({'version': 1, 'services': active}, output)
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.replace(name, self.state)
|
||||
self.sync_directory()
|
||||
|
||||
def sync_directory(self):
|
||||
fd = os.open(self.directory, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
|
||||
def freeze(self):
|
||||
if self.state.exists():
|
||||
raise RuntimeError('previous backup state remains; thaw before retrying freeze')
|
||||
deadline = time.monotonic() + 35
|
||||
active = []
|
||||
for unit in SERVICES:
|
||||
state, _ = self.status(unit, deadline)
|
||||
if state == 'active':
|
||||
active.append(unit)
|
||||
elif state not in ('inactive', 'failed'):
|
||||
raise RuntimeError('service is transitioning: ' + unit)
|
||||
self.record(active)
|
||||
try:
|
||||
if active:
|
||||
self.run(['stop', *active], timeout=max(0.1, deadline - time.monotonic()))
|
||||
for unit in active:
|
||||
state, result = self.status(unit, deadline)
|
||||
if state != 'inactive' or result != 'success':
|
||||
raise RuntimeError('service did not stop cleanly: ' + unit)
|
||||
except BaseException:
|
||||
if active:
|
||||
try:
|
||||
self.run(['start', '--no-block', *active], timeout=5)
|
||||
except Exception as error:
|
||||
print('backup recovery start refused: ' + type(error).__name__, file=sys.stderr)
|
||||
# Keep the original state until thaw confirms recovery, even if freeze failed.
|
||||
raise
|
||||
|
||||
def thaw(self):
|
||||
if not self.state.exists():
|
||||
return
|
||||
record = json.loads(self.state.read_text())
|
||||
active = record.get('services')
|
||||
if (set(record) != {'version', 'services'} or record['version'] != 1 or
|
||||
not isinstance(active, list) or any(unit not in SERVICES for unit in active) or
|
||||
len(active) != len(set(active))):
|
||||
raise RuntimeError('invalid backup recovery record')
|
||||
deadline = time.monotonic() + 15
|
||||
if active:
|
||||
self.run(['start', *active], timeout=10)
|
||||
for unit in active:
|
||||
if self.status(unit, deadline)[0] != 'active':
|
||||
raise RuntimeError('service did not resume: ' + unit)
|
||||
self.state.unlink()
|
||||
self.sync_directory()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('action', choices=('freeze', 'thaw'))
|
||||
parser.add_argument('--state-dir', default='/run/svos-backup')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
hook = Quiesce(args.state_dir)
|
||||
fd = os.open(hook.directory / 'lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
getattr(hook, args.action)()
|
||||
finally:
|
||||
os.close(fd)
|
||||
except Exception as error:
|
||||
print('SVOS backup ' + args.action + ' failed: ' + str(error), file=sys.stderr)
|
||||
return 1
|
||||
print('SVOS backup ' + args.action + ' complete')
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user