[PATCH 22/36] tuna: Add cpuset modify command

John Kacur <[email protected]> Fri, 10 Jul 2026 10:15:00 -0400
Newsgroups org.kernel.vger.linux-rt-users
Message-ID <[email protected]>
Add ability to modify existing cpusets without destroying them:

- Add/remove CPUs: --add-cpus and --remove-cpus options
- Change memory nodes: -m/--memory-nodes option
- Toggle partition type: --isolated/--no-isolated options
- Multiple modifications can be combined in a single command
- Shows before/after summary of changes

New Cpuset class methods:
- get_cpus(): Get current CPU assignment
- get_memnode(): Get current memory node assignment
- get_partition_type(): Get current partition type
- modify_cpus(add_cpus, remove_cpus): Atomically modify CPU assignment
  (CPUs are added before removal to avoid zero-CPU scenarios)

Example usage:
  tuna cpuset modify tuna0 --add-cpus 8-11
  tuna cpuset modify tuna0 --remove-cpus 0-1 --add-cpus 4-7
  tuna cpuset modify tuna0 --memory-nodes 0-1 --isolated

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

diff --git a/tuna-cmd.py b/tuna-cmd.py
index 62553bf51869..557a07891183 100755
--- a/tuna-cmd.py
+++ b/tuna-cmd.py
@@ -311,6 +311,19 @@ def gen_parser():
     cpuset_status.add_argument('--show-empty', action='store_true', help='Include cpusets with no CPUs assigned (default: skip empty)')
     cpuset_status.add_argument('--no-recursive', dest='recursive', action='store_false', default=True, help='Only show top-level cpusets')
 
+    # cpuset modify
+    cpuset_modify = cpuset_subparser.add_parser('modify', description='Modify an existing cpuset', help='Modify cpuset configuration')
+    cpuset_modify.add_argument('name', type=str, metavar='NAME', help='Cpuset name to modify')
+    cpuset_modify.add_argument('--add-cpus', dest='add_cpu_list', type=str, metavar='CPU-LIST',
+                                help='CPUs to add to the cpuset (e.g., 4-7 or 4,5,6,7)')
+    cpuset_modify.add_argument('--remove-cpus', dest='remove_cpu_list', type=str, metavar='CPU-LIST',
+                                help='CPUs to remove from the cpuset (e.g., 0-1 or 0,1)')
+    cpuset_modify.add_argument('-m', '--memory-nodes', type=str, metavar='MEM-NODES', dest='memory_nodes',
+                                help='Set memory nodes (e.g., "0" or "0-1")')
+    partition_group = cpuset_modify.add_mutually_exclusive_group()
+    partition_group.add_argument('--isolated', action='store_true', help='Set CPU partition to isolated')
+    partition_group.add_argument('--no-isolated', action='store_true', help='Set CPU partition to member (not isolated)')
+
     return parser
 
 
@@ -1410,6 +1423,79 @@ def cpuset_status(pattern, show_empty, recursive):
         print("  These critical system processes should not be in custom cpusets!", file=sys.stderr)
 
 
+def cpuset_modify(name, add_cpus, remove_cpus, memory_nodes, isolated, no_isolated):
+    """Handler for 'tuna cpuset modify' command.
+
+    Args:
+        name: Cpuset name to modify
+        add_cpus: List of CPU numbers to add (or None)
+        remove_cpus: List of CPU numbers to remove (or None)
+        memory_nodes: Memory nodes string to set (or None to keep current)
+        isolated: If True, set partition to isolated
+        no_isolated: If True, set partition to member
+    """
+    # 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 name
+    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)
+
+    # At least one modification must be specified
+    if not any([add_cpus, remove_cpus, memory_nodes is not None, isolated, no_isolated]):
+        print("Error: at least one modification option must be specified", file=sys.stderr)
+        print("  --add-cpus, --remove-cpus, --memory-nodes, --isolated, or --no-isolated", file=sys.stderr)
+        sys.exit(1)
+
+    # Can't specify both --isolated and --no-isolated
+    if isolated and no_isolated:
+        print("Error: --isolated and --no-isolated are mutually exclusive", file=sys.stderr)
+        sys.exit(1)
+
+    try:
+        # Create Cpuset object for existing cpuset
+        cs = cpuset.Cpuset(name, existing_ok=True)
+
+        changes = []
+
+        # Modify CPUs if requested
+        if add_cpus or remove_cpus:
+            old_cpus = cs.get_cpus()
+            cs.modify_cpus(add_cpus=add_cpus, remove_cpus=remove_cpus)
+            new_cpus = cs.get_cpus()
+            changes.append(f"CPUs: {old_cpus or '(none)'} → {new_cpus or '(none)'}")
+
+        # Modify memory nodes if requested
+        if memory_nodes is not None:
+            old_mems = cs.get_memnode()
+            cs.write_memnode(memory_nodes)
+            changes.append(f"Memory nodes: {old_mems or '(none)'} → {memory_nodes}")
+
+        # Modify partition type if requested
+        if isolated:
+            old_partition = cs.get_partition_type()
+            cs.set_cpu_exclusive()
+            changes.append(f"Partition: {old_partition} → isolated")
+        elif no_isolated:
+            old_partition = cs.get_partition_type()
+            cs.unset_cpu_exclusive()
+            changes.append(f"Partition: {old_partition} → member")
+
+        # Print summary of changes
+        print(f"Modified cpuset '{name}':")
+        for change in changes:
+            print(f"  {change}")
+
+    except (OSError, ValueError) as e:
+        print(f"Error modifying cpuset: {e}", file=sys.stderr)
+        sys.exit(1)
+
+
 def main():
     i18n_init()
     parser = gen_parser()
@@ -1639,6 +1725,18 @@ def main():
         elif args.cpuset_command == 'status':
             cpuset_status(args.pattern, args.show_empty, args.recursive)
 
+        elif args.cpuset_command == 'modify':
+            # Parse CPU lists if provided
+            add_cpus = None
+            remove_cpus = None
+            if args.add_cpu_list:
+                add_cpus = tuna.cpustring_to_list(args.add_cpu_list)
+            if args.remove_cpu_list:
+                remove_cpus = tuna.cpustring_to_list(args.remove_cpu_list)
+
+            cpuset_modify(args.name, add_cpus, remove_cpus, args.memory_nodes,
+                         args.isolated, args.no_isolated)
+
 
 if __name__ == '__main__':
     main()
diff --git a/tuna/cpuset.py b/tuna/cpuset.py
index 79f86d8cfe9f..3f9b0534a397 100644
--- a/tuna/cpuset.py
+++ b/tuna/cpuset.py
@@ -264,6 +264,72 @@ class Cpuset:
         with open(path, 'r', encoding='utf-8') as f:
             return [line.strip() for line in f if line.strip()]
 
+    def get_cpus(self):
+        """
+        Get the current CPU assignment as a string
+        Returns string like "0-3" or "0,2,4-7" or empty string if none assigned
+        """
+        path = os.path.join(self._cpuset_path, "cpuset.cpus")
+        with open(path, 'r', encoding='utf-8') as f:
+            return f.read().strip()
+
+    def get_memnode(self):
+        """
+        Get the current memory node assignment as a string
+        Returns string like "0" or "0-1" or empty string if none assigned
+        """
+        path = os.path.join(self._cpuset_path, "cpuset.mems")
+        with open(path, 'r', encoding='utf-8') as f:
+            return f.read().strip()
+
+    def get_partition_type(self):
+        """
+        Get the current partition type
+        Returns 'isolated', 'member', or 'root' depending on cpuset.cpus.partition
+        Returns None if partition file doesn't exist
+        """
+        path = os.path.join(self._cpuset_path, "cpuset.cpus.partition")
+        if not os.path.exists(path):
+            return None
+        with open(path, 'r', encoding='utf-8') as f:
+            return f.read().strip()
+
+    def modify_cpus(self, add_cpus=None, remove_cpus=None):
+        """
+        Modify CPU assignment by adding and/or removing CPUs
+
+        Args:
+            add_cpus: List of CPU numbers to add (e.g., [4, 5, 6, 7])
+            remove_cpus: List of CPU numbers to remove (e.g., [0, 1])
+
+        Note: CPUs are added before removal. This is important when replacing CPUs
+        to avoid having zero CPUs assigned (which may cause errors).
+        """
+        from tuna import tuna
+
+        # Get current CPUs
+        current_cpus_str = self.get_cpus()
+        if current_cpus_str:
+            current_cpus = set(tuna.cpustring_to_list(current_cpus_str))
+        else:
+            current_cpus = set()
+
+        # Add new CPUs
+        if add_cpus:
+            current_cpus.update(add_cpus)
+
+        # Remove CPUs
+        if remove_cpus:
+            current_cpus.difference_update(remove_cpus)
+
+        # Convert back to string and assign
+        if current_cpus:
+            new_cpus_str = tuna.collapse_cpulist(sorted(current_cpus))
+            self.assign_cpus(new_cpus_str)
+        else:
+            # If no CPUs left, assign empty string
+            self.assign_cpus("")
+
 
 class CpusetsInit(Cpuset):
     """
-- 
2.54.0