mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-30 12:33:02 +02:00
Similar to the run_multiline() method, sometimes files contain more than one line and a common read_multiline() quickly becomes useful. This is currently only used by ietf-system for reading /etc/resolv.conf.head. Also, fix pylint warning for missing encoding to all open() calls. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
213 lines
6.2 KiB
Python
213 lines
6.2 KiB
Python
import abc
|
|
import datetime
|
|
import functools
|
|
import json
|
|
import os
|
|
import subprocess
|
|
|
|
from . import common
|
|
|
|
|
|
HOST = None
|
|
|
|
|
|
class Host(abc.ABC):
|
|
"""Host system API"""
|
|
|
|
@abc.abstractmethod
|
|
def now(self):
|
|
"""Get the current time as a `datetime`"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def run(self, cmd, default=None, log=True):
|
|
"""Get stdout of cmd
|
|
|
|
Run cmd, provided as an argv, and return the output it
|
|
produces. On error, return the default value if provided,
|
|
otherwise raise an exception.
|
|
|
|
"""
|
|
pass
|
|
|
|
def run_multiline(self, cmd, default=None):
|
|
"""Get lines of stdout of cmd"""
|
|
try:
|
|
txt = self.run(tuple(cmd), log=(default is None))
|
|
return txt.splitlines()
|
|
except:
|
|
if default is not None:
|
|
return default
|
|
raise
|
|
|
|
def run_json(self, cmd, default=None):
|
|
"""Get JSON object from stdout of cmd"""
|
|
try:
|
|
txt = self.run(tuple(cmd), log=(default is None))
|
|
return json.loads(txt)
|
|
except:
|
|
if default is not None:
|
|
return default
|
|
raise
|
|
|
|
@abc.abstractmethod
|
|
def read(self, path):
|
|
"""Get the contents of path
|
|
|
|
Returns `None` if the file is not readable for any reason.
|
|
|
|
"""
|
|
pass
|
|
|
|
def read_multiline(self, path, default=None):
|
|
"""Get lines of content from path
|
|
|
|
Returns a list of lines from the file, or default if not readable.
|
|
"""
|
|
try:
|
|
txt = self.read(path)
|
|
return txt.splitlines() if txt is not None else default
|
|
except:
|
|
if default is not None:
|
|
return default
|
|
raise
|
|
|
|
def read_json(self, path, default=None):
|
|
"""Get JSON object from path """
|
|
try:
|
|
txt = self.read(path)
|
|
return json.loads(txt)
|
|
except:
|
|
if default is not None:
|
|
return default
|
|
raise
|
|
|
|
|
|
class Localhost(Host):
|
|
def now(self):
|
|
return datetime.datetime.now(tz=datetime.timezone.utc)
|
|
|
|
@functools.cache
|
|
def run(self, cmd, default=None, log=True):
|
|
try:
|
|
result = subprocess.run(cmd, check=True, text=True,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.DEVNULL)
|
|
return result.stdout
|
|
except subprocess.CalledProcessError as err:
|
|
if default is not None:
|
|
return default
|
|
|
|
if log:
|
|
common.LOG.error(f"Failed to run {err}")
|
|
raise
|
|
|
|
def read(self, path):
|
|
try:
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
data = f.read().strip()
|
|
return data
|
|
except FileNotFoundError:
|
|
# This is considered OK
|
|
pass
|
|
except IOError:
|
|
common.LOG.error(f"Failed to read \"{path}\"")
|
|
|
|
return None
|
|
|
|
|
|
class Remotehost(Localhost):
|
|
def __init__(self, prefix, capdir):
|
|
super().__init__()
|
|
self.prefix = tuple(prefix.split()) if prefix else tuple()
|
|
self.capdir = capdir
|
|
if capdir:
|
|
for subdir in ("rootfs", "run"):
|
|
os.makedirs(os.path.join(capdir, subdir), exist_ok=True)
|
|
|
|
def now(self):
|
|
timestamp = self._run(["date", "-u", "+%s"], default=None, log=True)
|
|
if self.capdir:
|
|
path = os.path.join(self.capdir, "timestamp")
|
|
with open(path, "w", encoding='utf-8') as f:
|
|
f.write(f"{timestamp}\n")
|
|
pass
|
|
|
|
return datetime.datetime.fromtimestamp(int(timestamp), datetime.timezone.utc)
|
|
|
|
def _run(self, cmd, default, log):
|
|
# Assume that the wrapper acts like ssh(1) and simply concats
|
|
# arguments to a single string. Therefore, we must quoute
|
|
# arguments containing spaces so that commands like `vtysh -c
|
|
# "show ip route json"` work as expected.
|
|
cmd = " ".join([arg if " " not in arg else f"\"{arg}\"" for arg in cmd])
|
|
return super().run(self.prefix + (cmd,), default, log)
|
|
|
|
def run(self, cmd, default=None, log=True):
|
|
if not self.capdir:
|
|
return self._run(cmd, default, log)
|
|
|
|
storedpath = os.path.join(self.capdir, "run", Replayhost.SlugOf(cmd))
|
|
if os.path.exists(storedpath):
|
|
with open(storedpath, 'r', encoding='utf-8') as f:
|
|
return f.read()
|
|
|
|
out = self._run(cmd, default, log)
|
|
with open(storedpath, "w", encoding='utf-8') as f:
|
|
f.write(out)
|
|
|
|
return out
|
|
|
|
def read(self, path):
|
|
out = self._run(("cat", path), default="", log=False)
|
|
|
|
if self.capdir:
|
|
dirname = os.path.join(self.capdir, "rootfs", os.path.dirname(path[1:]))
|
|
filname = os.path.join(self.capdir, "rootfs", path[1:])
|
|
os.makedirs(dirname, exist_ok=True)
|
|
with open(filname, "w", encoding='utf-8') as f:
|
|
f.write(out)
|
|
|
|
return out
|
|
|
|
|
|
class Replayhost(Host):
|
|
def SlugOf(cmd):
|
|
return "_".join(cmd).replace("/", "+").replace(" ", "-")
|
|
|
|
def __init__(self, replaydir):
|
|
self.replaydir = replaydir
|
|
|
|
def now(self):
|
|
path = os.path.join(self.replaydir, "timestamp")
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
timestamp = f.read().strip()
|
|
return datetime.datetime.fromtimestamp(int(timestamp), datetime.timezone.utc)
|
|
|
|
def run(self, cmd, default=None, log=True):
|
|
path = os.path.join(self.replaydir, "run", Replayhost.SlugOf(cmd))
|
|
|
|
try:
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
return f.read()
|
|
except:
|
|
if default is not None:
|
|
return default
|
|
|
|
if log:
|
|
common.LOG.error(f"No recording found for run \"{path}\"")
|
|
raise
|
|
|
|
def read(self, path):
|
|
path = os.path.join(self.replaydir, "rootfs", path[1:])
|
|
try:
|
|
with open(path, 'r', encoding='utf-8') as f:
|
|
return f.read()
|
|
except FileNotFoundError:
|
|
# This is considered OK
|
|
pass
|
|
except:
|
|
common.LOG.error(f"No recording found for file \"{path}\"")
|
|
raise
|