From d2736a635d0cd1e634c255fe1fd2bb50fac834d2 Mon Sep 17 00:00:00 2001 From: Oren de Lame Date: Wed, 14 Aug 2024 14:51:10 +0300 Subject: [PATCH 1/5] fix --- granulate-utils/linux/containers.py | 42 +++++++++++++++++++++++++++++ granulate_utils/linux/process.py | 30 +++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 granulate-utils/linux/containers.py diff --git a/granulate-utils/linux/containers.py b/granulate-utils/linux/containers.py new file mode 100644 index 00000000..32f3df7f --- /dev/null +++ b/granulate-utils/linux/containers.py @@ -0,0 +1,42 @@ +# +# Copyright (C) 2023 Intel Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import re +from typing import Optional + +from psutil import Process + +from granulate_utils.linux import process + +# ECS uses /ecs/uuid/container-id +# standard Docker uses /docker/container-id +# k8s uses /kubepods/{burstable,besteffort}/uuid/container-id +# there are some variations to the above formats, but generally, the container +# ID is always 64-hex digits. +CONTAINER_ID_PATTERN = re.compile(r"[a-f0-9]{64}") + + +def get_process_container_id(process: Process) -> Optional[str]: + """ + Gets the container ID of a running process, or None if not in a container. + :raises NoSuchProcess: If the process doesn't or no longer exists + """ + for proc_cgroup_line in process.get_process_cgroups(process): + found = CONTAINER_ID_PATTERN.findall(proc_cgroup_line.relative_path) + if found: + return found[-1] + + return None diff --git a/granulate_utils/linux/process.py b/granulate_utils/linux/process.py index c5fe1aab..59e7e980 100644 --- a/granulate_utils/linux/process.py +++ b/granulate_utils/linux/process.py @@ -171,3 +171,33 @@ def search_for_process(filter: Callable[[psutil.Process], bool]) -> Iterator[psu with contextlib.suppress(NoSuchProcess, AccessDenied): if is_process_running(proc) and filter(proc): yield proc + + +class ProcCgroupLine: + """ + The format of the line: hierarchy-ID:controller-list:relative-path + Example line: 1:cpu:/custom_cgroup + + relative-path - the path of the cgroup the process belongs to, relative to the hierarchy mount point + e.g. /sys/fs/cgroup/memory on v1 or just the cgroups v2 mount on v2 e.g /sys/fs/cgroup. + """ + + hier_id: str + controllers: List[str] + relative_path: str + + def __init__(self, procfs_line: str): + hier_id, controller_list, relative_path = procfs_line.split(":", maxsplit=2) + self.hier_id = hier_id + self.controllers = controller_list.split(",") + self.relative_path = relative_path + + +def get_process_cgroups(process: Optional[psutil.Process] = None) -> List[ProcCgroupLine]: + """ + Get the cgroups of a process in [(hier id., controllers, path)] parsed form. + If process is None, gets the cgroups of the current process. + """ + process = process or psutil.Process() + text = read_proc_file(process, "cgroup").decode() + return [ProcCgroupLine(line) for line in text.splitlines()] From 1edc3208ce6e0e660b2710669c2ba91b8c34fe24 Mon Sep 17 00:00:00 2001 From: Oren de Lame Date: Wed, 14 Aug 2024 14:54:48 +0300 Subject: [PATCH 2/5] fix --- granulate-utils/linux/containers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/granulate-utils/linux/containers.py b/granulate-utils/linux/containers.py index 32f3df7f..0b892e19 100644 --- a/granulate-utils/linux/containers.py +++ b/granulate-utils/linux/containers.py @@ -19,7 +19,7 @@ from psutil import Process -from granulate_utils.linux import process +from granulate_utils.linux.process import get_process_cgroups # ECS uses /ecs/uuid/container-id # standard Docker uses /docker/container-id @@ -34,7 +34,7 @@ def get_process_container_id(process: Process) -> Optional[str]: Gets the container ID of a running process, or None if not in a container. :raises NoSuchProcess: If the process doesn't or no longer exists """ - for proc_cgroup_line in process.get_process_cgroups(process): + for proc_cgroup_line in get_process_cgroups(process): found = CONTAINER_ID_PATTERN.findall(proc_cgroup_line.relative_path) if found: return found[-1] From 4176cc6f970ca6dbc13435a6a64673962501c8bf Mon Sep 17 00:00:00 2001 From: Amit Frenkel Date: Wed, 14 Aug 2024 15:34:52 +0300 Subject: [PATCH 3/5] sepreated the action of entering the target process ns from run_in_ns to expose this functionality as a standalone function --- granulate_utils/linux/ns.py | 44 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/granulate_utils/linux/ns.py b/granulate_utils/linux/ns.py index b34bb59b..c98b6a65 100644 --- a/granulate_utils/linux/ns.py +++ b/granulate_utils/linux/ns.py @@ -245,27 +245,13 @@ def run_in_ns( for ns in nstypes: assert_ns_str(ns) - # make sure "mnt" is last, once we change it our /proc is gone - nstypes = sorted(nstypes, key=lambda ns: 1 if ns == "mnt" else 0) ret: Union[T, _Sentinel] = _SENTINEL exc: Optional[BaseException] = None def _switch_and_run(): try: - global libc - if libc is None: - libc = ctypes.CDLL("libc.so.6") - - for nstype in nstypes: - if not is_same_ns(target_pid, nstype): - flag = NsType[nstype].value - if libc.unshare(flag) != 0: - raise ValueError(f"Failed to unshare({nstype})") - - with open(f"/proc/{target_pid}/ns/{nstype}", "r") as nsf: - if libc.setns(nsf.fileno(), flag) != 0: - raise ValueError(f"Failed to setns({nstype}) (to pid {target_pid})") + enter_process_ns(nstypes, target_pid) nonlocal ret ret = callback() @@ -332,3 +318,31 @@ def resolve_host_path(process: Process, ns_path: str, from_ancestor: bool = True Get a path in the host mount namespace pointing to path in process mount namespace. """ return resolve_proc_root_links(get_proc_root_path(process, from_ancestor=from_ancestor), ns_path) + + +def enter_process_ns( + nstypes: List[str], + target_pid: int = 1, +) -> None: + """ + Swaps to a set of the namespaces of a target process. + """ + + for ns in nstypes: + assert_ns_str(ns) + # make sure "mnt" is last, once we change it our /proc is gone + nstypes = sorted(nstypes, key=lambda ns: 1 if ns == "mnt" else 0) + + global libc + if libc is None: + libc = ctypes.CDLL("libc.so.6") + + for nstype in nstypes: + if not is_same_ns(target_pid, nstype): + flag = NsType[nstype].value + if libc.unshare(flag) != 0: + raise ValueError(f"Failed to unshare({nstype})") + + with open(f"/proc/{target_pid}/ns/{nstype}", "r") as nsf: + if libc.setns(nsf.fileno(), flag) != 0: + raise ValueError(f"Failed to setns({nstype}) (to pid {target_pid})") From bb59f517d4233f018f3d05a4947b839772f22745 Mon Sep 17 00:00:00 2001 From: Oren de Lame Date: Wed, 14 Aug 2024 16:25:33 +0300 Subject: [PATCH 4/5] fix --- .../linux/containers.py | 0 granulate_utils/linux/ns.py | 33 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) rename {granulate-utils => granulate_utils}/linux/containers.py (100%) diff --git a/granulate-utils/linux/containers.py b/granulate_utils/linux/containers.py similarity index 100% rename from granulate-utils/linux/containers.py rename to granulate_utils/linux/containers.py diff --git a/granulate_utils/linux/ns.py b/granulate_utils/linux/ns.py index b34bb59b..1be5a970 100644 --- a/granulate_utils/linux/ns.py +++ b/granulate_utils/linux/ns.py @@ -23,9 +23,10 @@ from threading import Thread from typing import Callable, Collection, List, Optional, TypeVar, Union -from psutil import NoSuchProcess, Process +from psutil import NoSuchProcess, Process, process_iter from granulate_utils.exceptions import UnsupportedNamespaceError +from granulate_utils.linux import containers T = TypeVar("T") @@ -332,3 +333,33 @@ def resolve_host_path(process: Process, ns_path: str, from_ancestor: bool = True Get a path in the host mount namespace pointing to path in process mount namespace. """ return resolve_proc_root_links(get_proc_root_path(process, from_ancestor=from_ancestor), ns_path) + + +def get_host_pid(nspid: int, container_id: str) -> Optional[int]: + assert len(container_id) == 64, f"Invalid container id {container_id!r}" + + pid_namespace = "" + running_processes = list(process_iter()) + + # Get the pid namespace of the given container + for process in running_processes: + try: + if container_id == containers.get_process_container_id(process): + pid_namespace = os.readlink(f"/proc/{process.pid}/ns/pid") + break + except (FileNotFoundError, NoSuchProcess): + continue + + if not pid_namespace: + return None + + # Here we need to find the process by comparing pid namespaces and not by comparing cgroups, because + # technically a process that's running in a container pid namespace doesn't have to share its cgroup + for process in running_processes: + try: + if os.readlink(f"/proc/{process.pid}/ns/pid") == pid_namespace and get_process_nspid(process) == nspid: + return process.pid + except (FileNotFoundError, NoSuchProcess, PermissionError): + continue + + return None From e734009b104a0f9a7f7acfa00b77aef8f09ea255 Mon Sep 17 00:00:00 2001 From: Amit Frenkel Date: Wed, 14 Aug 2024 17:26:00 +0300 Subject: [PATCH 5/5] remove newline --- granulate_utils/linux/ns.py | 1 - 1 file changed, 1 deletion(-) diff --git a/granulate_utils/linux/ns.py b/granulate_utils/linux/ns.py index 17c8e525..8d29e93b 100644 --- a/granulate_utils/linux/ns.py +++ b/granulate_utils/linux/ns.py @@ -358,7 +358,6 @@ def enter_process_ns( """ Swaps to a set of the namespaces of a target process. """ - for ns in nstypes: assert_ns_str(ns) # make sure "mnt" is last, once we change it our /proc is gone