[PATCH 12/36] tuna: Add cpuset move command for process migration

John Kacur <[email protected]> Fri, 10 Jul 2026 10:14:50 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Implements 'tuna cpuset move' command to move processes into existing
cpusets. Supports two input methods:
- --threads: Thread list with pattern matching (e.g., kworker*)
- --pids: Comma-separated list of PIDs

Key features:
- Validates cpuset exists before attempting moves
- Gracefully handles invalid PIDs (returns False, continues)
- Reports success/failure counts
- Exits with error if all moves fail

Also adds existing_ok parameter to Cpuset class to suppress warnings
when accessing existing cpusets (follows os.makedirs convention).

Changes:
- tuna-cmd.py: Add cpuset move argument parser, handler, and dispatcher
- tuna/cpuset.py: Add existing_ok parameter to Cpuset.__init__() and
  create_cpuset() to allow accessing existing cpusets without warnings

Assisted-by: Claude:claude-sonnet-4-5
Signed-off-by: John Kacur <[email protected]>
---
 tuna-cmd.py    | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++
 tuna/cpuset.py | 22 ++++++++----
 2 files changed, 105 insertions(+), 7 deletions(-)

diff --git a/tuna-cmd.py b/tuna-cmd.py
index 2ff73426ba4f..0e70b9208665 100755
--- a/tuna-cmd.py
+++ b/tuna-cmd.py
@@ -292,6 +292,12 @@ def gen_parser():
     cpuset_destroy.add_argument('--skip-empty', action='store_true', help='When using --pattern, skip cpusets with no CPUs assigned')
     cpuset_destroy.add_argument('--no-recursive', dest='recursive', action='store_false', default=True, help='Only destroy top-level cpusets when using --pattern')
 
+    # cpuset move
+    cpuset_move = cpuset_subparser.add_parser('move', description='Move processes to a cpuset', help='Move processes to a cpuset')
+    cpuset_move.add_argument('-n', '--name', type=str, metavar='NAME', required=True, help='Cpuset name to move processes to')
+    cpuset_move.add_argument('-t', '--threads', **MODS['threads'])
+    cpuset_move.add_argument('-p', '--pids', dest='pid_list', type=str, metavar='PID-LIST', help='Comma-separated list of PIDs to move')
+
     return parser
 
 
@@ -1009,6 +1015,87 @@ def cpuset_destroy(name, pattern, force, skip_empty, recursive):
         sys.exit(1)
 
 
+def cpuset_move(name, thread_list, pid_list):
+    """Handler for 'tuna cpuset move' command.
+
+    Args:
+        name: Cpuset name to move processes to
+        thread_list: Thread list (already converted to list of PIDs by main())
+        pid_list: PID list string (comma-separated PIDs only)
+    """
+    # Check cgroup v2 support
+    ci = cpuset.CpusetsInit()
+    if not ci.supported:
+        print("Error: cgroup v2 cpusets not supported on this system", file=sys.stderr)
+        sys.exit(1)
+
+    # Validate: must specify at least one of thread_list or pid_list
+    if not thread_list and not pid_list:
+        print("Error: must specify either --threads or --pids", file=sys.stderr)
+        sys.exit(2)
+
+    # Check if cpuset exists
+    cpuset_path = os.path.join('/sys/fs/cgroup', name)
+    if not os.path.exists(cpuset_path):
+        print(f"Error: cpuset '{name}' does not exist", file=sys.stderr)
+        sys.exit(1)
+
+    # Create cpuset object for the target cpuset
+    try:
+        cs = cpuset.Cpuset(name, existing_ok=True)
+    except (OSError, ValueError) as e:
+        print(f"Error accessing cpuset '{name}': {e}", file=sys.stderr)
+        sys.exit(1)
+
+    # Build list of PIDs to move
+    pids = []
+
+    # Process thread_list if provided (already converted to list of PIDs by main())
+    if thread_list:
+        # thread_list is already a list of integers from main()'s conversion
+        pids.extend(thread_list)
+
+    # Process pid_list if provided (simple comma-separated PIDs)
+    if pid_list:
+        import re
+        pid_strings = [s.strip() for s in re.split(r'[,\s]+', pid_list) if s.strip()]
+        for pid_str in pid_strings:
+            if pid_str.isdigit():
+                pids.append(int(pid_str))
+            else:
+                print(f"Warning: ignoring invalid PID '{pid_str}'", file=sys.stderr)
+
+    # Remove duplicates
+    pids = list(set(pids))
+
+    if not pids:
+        print("Error: no valid PIDs to move", file=sys.stderr)
+        sys.exit(2)
+
+    # Move each PID to the cpuset
+    success_count = 0
+    fail_count = 0
+
+    for pid in pids:
+        if cs.write_pid(pid):
+            success_count += 1
+        else:
+            # write_pid returns False on failure
+            print(f"Warning: failed to move PID {pid} to cpuset '{name}'", file=sys.stderr)
+            fail_count += 1
+
+    # Print summary
+    print(f"Moved {success_count} process(es) to cpuset '{name}'", end='')
+    if fail_count > 0:
+        print(f" ({fail_count} failed)")
+    else:
+        print()
+
+    # Exit with error code if all moves failed
+    if success_count == 0:
+        sys.exit(1)
+
+
 def main():
     i18n_init()
     parser = gen_parser()
@@ -1229,6 +1316,9 @@ def main():
         elif args.cpuset_command == 'destroy':
             cpuset_destroy(args.name, args.pattern, args.force, args.skip_empty, args.recursive)
 
+        elif args.cpuset_command == 'move':
+            cpuset_move(args.name, args.thread_list, args.pid_list)
+
 
 if __name__ == '__main__':
     main()
diff --git a/tuna/cpuset.py b/tuna/cpuset.py
index 4a3a71416c53..1039eb6c2c49 100644
--- a/tuna/cpuset.py
+++ b/tuna/cpuset.py
@@ -22,7 +22,7 @@ class Cpuset:
 
     mpath = '/sys/fs/cgroup'
 
-    def __init__(self, name=None, auto_destroy=False):
+    def __init__(self, name=None, auto_destroy=False, existing_ok=False):
         """
         Initialize a cpuset
 
@@ -32,11 +32,14 @@ class Cpuset:
                          exiting a context manager (rteval style).
                          If False (default), cpuset persists after program exits
                          (tuna style).
+            existing_ok: If True, suppress warning when cpuset already exists.
+                        Use this when accessing an existing cpuset rather than
+                        creating a new one (default: False).
         """
         self.cpuset_name = None
         self._cpuset_path = None
         self.auto_destroy = auto_destroy
-        self.create_cpuset(name)
+        self.create_cpuset(name, existing_ok=existing_ok)
 
     @property
     def cpuset_path(self):
@@ -46,12 +49,16 @@ class Cpuset:
     def __str__(self):
         return f'cpuset_name={self.cpuset_name}, cpuset_path={self.cpuset_path}'
 
-    def create_cpuset(self, name=None):
+    def create_cpuset(self, name=None, existing_ok=False):
         """
         Create a cpuset (cgroup) below /sys/fs/cgroup
 
-        If the cpuset already exists, prints a warning and returns without
-        creating a new one.
+        If the cpuset already exists, prints a warning (unless existing_ok=True)
+        and returns without creating a new one.
+
+        Args:
+            name: Name of the cpuset to create
+            existing_ok: If True, don't warn when cpuset already exists
         """
         if name is None:
             raise ValueError("cpuset name cannot be None")
@@ -60,8 +67,9 @@ class Cpuset:
 
         # Check if cpuset already exists
         if os.path.exists(path):
-            logger.warning(f"Cpuset '{name}' already exists at {path}")
-            print(f"Warning: cpuset '{name}' already exists at {path}")
+            if not existing_ok:
+                logger.warning(f"Cpuset '{name}' already exists at {path}")
+                print(f"Warning: cpuset '{name}' already exists at {path}")
             self._cpuset_path = path
             return
 
-- 
2.54.0