machine: add a more robust container check

Most containers do not have access to systemd, so using
systemd-detect-virt will fail.  We can check the cgroup
and sched files of the first process to reliably determine
if Moonraker is running within a container.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2021-10-28 19:38:15 -04:00
parent 179e9428c5
commit 28c8bf61da
1 changed files with 75 additions and 25 deletions

View File

@ -29,6 +29,8 @@ ALLOWED_SERVICES = [
"moonraker", "klipper", "webcamd", "MoonCord",
"KlipperScreen", "moonraker-telegram-bot"
]
CGROUP_PATH = "/proc/1/cgroup"
SCHED_PATH = "/proc/1/sched"
SYSTEMD_PATH = "/etc/systemd/system"
SD_CID_PATH = "/sys/block/mmcblk0/device/cid"
SD_CSD_PATH = "/sys/block/mmcblk0/device/csd"
@ -43,23 +45,15 @@ class Machine:
dist_info: Dict[str, Any]
dist_info = {'name': distro.name(pretty=True)}
dist_info.update(distro.info())
self.virt_type = "none"
self.inside_container = False
self.virt_id = "none"
self.system_info: Dict[str, Any] = {
'cpu_info': self._get_cpu_info(),
'sd_info': self._get_sdcard_info(),
'distribution': dist_info
'distribution': dist_info,
'virtualization': self._check_inside_container()
}
# Add system info to log rollover
sys_info_msg = "\nSystem Info:"
for header, info in self.system_info.items():
sys_info_msg += f"\n\n***{header}***"
if not isinstance(info, dict):
sys_info_msg += f"\n {repr(info)}"
else:
for key, val in info.items():
sys_info_msg += f"\n {key}: {val}"
self.server.add_log_rollover_item('system_info', sys_info_msg)
self._update_log_rollover(log=True)
self.available_services: Dict[str, Dict[str, str]] = {}
self.server.register_endpoint(
@ -92,6 +86,17 @@ class Machine:
self.init_evt = asyncio.Event()
event_loop.register_callback(self._initialize)
def _update_log_rollover(self, log: bool = False) -> None:
sys_info_msg = "\nSystem Info:"
for header, info in self.system_info.items():
sys_info_msg += f"\n\n***{header}***"
if not isinstance(info, dict):
sys_info_msg += f"\n {repr(info)}"
else:
for key, val in info.items():
sys_info_msg += f"\n {key}: {val}"
self.server.add_log_rollover_item('system_info', sys_info_msg, log=log)
async def wait_for_init(self, timeout: float = None) -> None:
try:
await asyncio.wait_for(self.init_evt.wait(), timeout)
@ -99,12 +104,14 @@ class Machine:
pass
async def _initialize(self):
if not self.inside_container:
await self._check_virt_status()
await self._find_active_services()
self._update_log_rollover()
async def _handle_machine_request(self, web_request: WebRequest) -> str:
ep = web_request.get_endpoint()
if self.virt_type == "container":
if self.inside_container:
raise self.server.error(
f"Cannot {ep.split('/')[-1]} from within a "
f"{self.virt_id} container")
@ -294,8 +301,50 @@ class Machine:
await self.update_service_status(notify=False)
self.init_evt.set()
def _check_inside_container(self) -> Dict[str, Any]:
cgroup_file = pathlib.Path(CGROUP_PATH)
virt_type = virt_id = "none"
if cgroup_file.exists():
try:
data = cgroup_file.read_text()
container_types = ["docker", "lxc"]
for ct in container_types:
if ct in data:
self.inside_container = True
virt_type = "container"
virt_id = ct
break
except Exception:
logging.exception(f"Error reading {CGROUP_PATH}")
# Fall back to process schedule check
if not self.inside_container:
sched_file = pathlib.Path(SCHED_PATH)
if sched_file.exists():
try:
data = sched_file.read_text().strip()
proc_name = data.split('\n')[0].split()[0]
if proc_name not in ["init", "systemd"]:
self.inside_container = True
virt_type = "container"
virt_id = "lxc"
if (
os.path.exists("/.dockerenv") or
os.path.exists("/.dockerinit")
):
virt_id = "docker"
except Exception:
logging.exception(f"Error reading {SCHED_PATH}")
self.virt_id = virt_id
return {
'virt_type': virt_type,
'virt_identifier': virt_id
}
async def _check_virt_status(self) -> None:
self.virt_id = self.virt_type = "none"
# Fallback virtualization check
virt_id = virt_type = "none"
shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
@ -307,31 +356,32 @@ class Machine:
except shell_cmd.error:
pass
else:
self.virt_id = resp.strip()
virt_id = resp.strip()
if self.virt_id != "none":
if virt_id != "none":
# Check explicitly for container virtualization
scmd = shell_cmd.build_shell_command(
"systemd-detect-virt --container")
try:
resp = await scmd.run_with_response()
except shell_cmd.error:
self.virt_type = "vm"
virt_type = "vm"
else:
if self.virt_id == resp.strip():
self.virt_type = "container"
if virt_id == resp.strip():
virt_type = "container"
else:
# Moonraker is run from within a VM inside a container
self.virt_type = "vm"
virt_type = "vm"
logging.info(
f"Virtualized Environment Detected, Type: {self.virt_type} "
f"id: {self.virt_id}")
f"Virtualized Environment Detected, Type: {virt_type} "
f"id: {virt_id}")
else:
logging.info("No Virtualization Detected")
self.virt_id = virt_id
self.system_info['virtualization'] = {
'virt_type': self.virt_type,
'virt_identifier': self.virt_id
'virt_type': virt_type,
'virt_identifier': virt_id
}
async def update_service_status(self, notify: bool = True) -> None: