diff --git a/.gitignore b/.gitignore index b23c122..88bd791 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ bin/ include/ lib/ +dist/ .ropeproject/ */htmlcov/ *.egg-info/ diff --git a/README.md b/README.md index 0e53131..1358fc4 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,11 @@ import subprocess from cgroups import Cgroup -# First we create the cgroup 'charlie' and we set it's cpu and memory limits +# First we create the cgroup 'charlie' and we set it's cpu, memory and swap limits cg = Cgroup('charlie') cg.set_cpu_limit(50) cg.set_memory_limit(500) +cg.set_memsw_limit(500) # Then we a create a function to add a process in the cgroup def in_my_cgroup(): @@ -176,6 +177,38 @@ If you don't provide an argument to this method, the menthod will set the memory *unit* is the unit used for the limit. Available choices are 'bytes', 'kilobytes', 'megabytes' and 'gigabytes'. Default is 'megabytes'. +**Cgroup.memsw_limit** + +Get the swap+memory limit of the cgroup in bytes + +**Cgroup.set_memsw_limit(limit, unit='megabytes')** + +Set the swap+memory limit of the cgroup. +The function uses the `memory.memsw.limit_in_bytes` hierarchy. + +*limit* is the limit you want to set. +If you don't provide an argument to this method, the menthod will set the memory limit to the default memory limit (ie. no limit) + +*unit* is the unit used for the limit. Available choices are 'bytes', 'kilobytes', 'megabytes' and 'gigabytes'. Default is 'megabytes'. + +Note: this paramether can not be less then memory.limit_in_bytes, +because it sums memory and swap usage. + +**Cgroup.swappiness** + +Get the swappiness option of the cgroup + +**Cgroup.set_swappiness(swappiness=60)** + +swappiness may be in range 1..100 +vm.swappiness = 0 Version 3.5 and over: disables swapping. Prior to version 3.5: The kernel will swap only to avoid an out of memory condition. + +vm.swappiness = 1 Version 3.5 and over: Minimum swappiness without disabling it entirely + +vm.swappiness = 60 The default value. + +vm.swappiness = 100 The kernel will swap aggressively. + ```python from cgroups import Cgroup @@ -187,6 +220,17 @@ cg.set_memory_limit(50) # Reset the limit cg.set_memory_limit('charlie') + +cg.set_swappiness(swappiness=0) + +swappiness = cg.swappiness + +#getting the swap-memory limit of the current cgroup +limit_in_bytes = cg.memsw_limit + +#set the swap-memory limit of the current cgroup +cg.set_memsw_limit(100) + ``` @@ -207,3 +251,10 @@ from cgroups import Cgroup cg = Cgroup('charlie') cg.delete() ``` + +**Console tool cgc** +```bash +sudo cgc useradd $(whoami) + +cgc run bash --cpu 10 --mem 20 --no-swap +``` diff --git a/cgroups/cgc.py b/cgroups/cgc.py new file mode 100644 index 0000000..af42e82 --- /dev/null +++ b/cgroups/cgc.py @@ -0,0 +1,153 @@ +#! /usr/bin/env python +# -*- coding:utf-8 -*- + +from __future__ import unicode_literals +from __future__ import print_function + +import os +import getpass +import logging +import argparse + +import subprocess + +from cgroups.common import BASE_CGROUPS, CgroupsException +from cgroups.user import get_user_info +from cgroups.user import create_user_cgroups + +from cgroups import Cgroup + +logger = logging.getLogger(__name__) + +def run_command_with_cgroups_options(command,cpu=None,mem=None,swapless=False,cgroup=None): + if cgroup is None: + cg_name = '' + if swapless: + cg_name += 'swapless' + cg_name += '_cpu'+str(cpu) + if mem != -1: + cg_name += '_mem'+str(mem) + # print (cg_name) + + else: + cg_name = cgroup + + cg = Cgroup(cg_name) + + if cpu is not None: + cg.set_cpu_limit(cpu) + + if mem is not None: + cg.set_memory_limit(mem) + + if swapless: + cg.set_swappiness(0) + + def preexec_fn (): + pid = os.getpid() + print ("starting {} with pid {}".format(command,pid)) + cg.add(pid) + + process = subprocess.Popen([command],preexec_fn=preexec_fn) + process.wait() + +def main(): + parser = argparse.ArgumentParser( + prog="cgc", + description="cli tool for managing processes using Control Groups Linux mechanism" + ) + + parser.add_argument( + '-v', + '--verbose', + dest='verbose', + action='store', + help='verbose actions level (DEBUG,INFO,WARN), default=INFO', + default="INFO", + ) + + parser.add_argument( + 'action', + choices=('useradd','run','cgadd','addpid','rmpid','cgls','frz','ufrz'), + help="action to do", + ) + + parser.add_argument( + '--command', + dest='command', + action='store', + type=str, + help='command to run with some cgroup configuration', + default=None, + ) + parser.add_argument( + '--no-swap', + dest='no_swap', + action='store_true', + help='run command with swap memmory limit set to 0 bytes' + ) + parser.add_argument( + '--cpu', + dest='cpu', + action='store', + type=int, + help='run command with some percent of cpu-resource', + default=None, + ) + parser.add_argument( + '--mem', + dest='mem', + action='store', + type=int, + help='run command with memory limit', + default=None + ) + parser.add_argument( + '--cgroup', + dest='cgroup', + action='store', + type=str, + help='cgroup name', + default=None, + ) + + parser.add_argument( + '--user', + dest='user', + action='store', + help='username to grant privileges to use cgroups', + default=None, + ) + + args = parser.parse_args() + + print (args) + + # Logging + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s') + logstream = logging.StreamHandler() + logstream.setFormatter(formatter) + logger.addHandler(logstream) + if args.verbose == 'DEBUG': + logger.setLevel(logging.DEBUG) + elif args.verbose == 'INFO': + logger.setLevel(logging.INFO) + elif args.verbose == 'WARN': + logger.setLevel(logging.WARN) + else: + logger.setLevel(logging.ERROR) + logger.debug('Logging level: %s' % args.verbose) + + print (args) + + if args.action == "run": + print ("runing command {}".format(args.command)) + run_command_with_cgroups_options(args.command,cpu=args.cpu,mem=args.mem,swapless=args.no_swap if args.no_swap else False,cgroup=args.cgroup) + + elif args.action == "useradd": + print ("adding user {}".format(args.user)) + create_user_cgroups(args.user) + +if __name__ == "__main__": + main() diff --git a/cgroups/cgroup.py b/cgroups/cgroup.py index 93cc059..d648669 100644 --- a/cgroups/cgroup.py +++ b/cgroups/cgroup.py @@ -18,6 +18,8 @@ MEMORY_DEFAULT = -1 CPU_DEFAULT = 1024 +SWAPPINESS_DEFAULT = 60 + class Cgroup(object): @@ -122,7 +124,6 @@ def _format_cpu_value(self, limit=None): value = int(round(CPU_DEFAULT * limit)) return value - def set_cpu_limit(self, limit=None): if 'cpu' in self.cgroups: value = self._format_cpu_value(limit) @@ -168,7 +169,6 @@ def _format_memory_value(self, unit, limit=None): value = limit * 1024 * 1024 * 1024 return value - def set_memory_limit(self, limit=None, unit='megabytes'): if 'memory' in self.cgroups: value = self._format_memory_value(unit, limit) @@ -191,3 +191,116 @@ def memory_limit(self): return value else: return None + + def set_memsw_limit(self,limit=None,unit='megabytes'): + if 'memory' in self.cgroups: + value = self._format_memory_value(unit,limit) + swap_limit_file = self._get_cgroup_file( + 'memory', 'memory.memsw.limit_in_bytes') + with open(swap_limit_file,'w+') as f: + f.write('%s\n' % value) + else: + raise CgroupsException( + 'MEMORY hierarchy not available in this cgroup') + + @property + def memsw_limit(self): + if 'memory' in self.cgroups: + swap_limit_file = self._get_cgroup_file( + 'memory', 'memory.memsw.limit_in_bytes') + with open(swap_limit_file,'r+') as f: + value = f.read().split('\n')[0] + value = int(int(value)/ 1024 / 1024) + return value + else: + return None + + def set_swappiness(self,swappiness=SWAPPINESS_DEFAULT): + if 'memory' in self.cgroups: + swappiness = int(swappiness) + if swappiness<0 and swappiness>100: + raise CgroupsException("swappiness value must be in range 1..100") + value = swappiness + swappiness_file = self._get_cgroup_file( + 'memory', 'memory.swappiness') + with open(swappiness_file,'w+') as f: + f.write('%s\n' % value) + else: + raise CgroupsException( + 'MEMORY hierarchy not available in this cgroup') + + @property + def swappiness(self): + if 'memory' in self.cgroups: + swappiness_file = self._get_cgroup_file( + 'memory', 'memory.swappiness') + with open(swappiness_file,'r+') as f: + value = f.read().split('\n')[0] + value = int(value) + return value + else: + return None + + @property + def is_under_oom(self): + if 'memory' in self.cgroups: + oom_control_file = self._get_cgroup_file( + 'memory', 'memory.oom_control') + with open(oom_control_file,'r+') as f: + oom_kill_disable,under_oom = map(int,f.read().split('\n')) + oom_kill_disable = True if oom_kill_disable == 1 else False + + under_oom = True if under_oom == 1 else False + + return under_oom + else: + return None + + def set_omm_kill(self,allow=True): + if 'memory' in self.cgroups: + oom_control_file = self._get_cgroup_file( + 'memory', 'memory.oom_control') + oom_kill_disable = 0 if allow else 1 + under_oom = 1 if self.is_under_oom else 0 + with open(oom_control_file,'w+') as f: + f.write("oom_kill_disable %s\nunder_oom %s\n" % (oom_kill_disable,under_oom)) + else: + raise CgroupsException( + 'MEMORY hierarchy not available in this cgroup') + + def allow_kill_under_oom(self): + self.set_omm_kill(allow=True) + + def disallow_kill_under_oom(self): + self.set_omm_kill(allow=False) + + # FREEZER + + @property + def freeze_state(self): + if 'freezer' in self.cgroups: + freezer_state_file = self._get_cgroup_file( + 'freezer', 'freezer.state') + with open(freezer_state_file,'r+') as f: + state = f.read().split('\n')[0] + return state + else: + return None + + def set_freezer_state(self,state): + if state not in ('THAWED','FROZEN'): + raise CgroupsException("there is two values of freezer.state paramener available - THAWED|FROZEN.") + if 'freezer' in self.cgroups: + freezer_state_file = self._get_cgroup_file( + 'freezer', 'freezer.state') + with open(freezer_state_file,'w+') as f: + f.write("%s\n" % state) + else: + raise CgroupsException( + 'FREEZER hierarchy not available in this cgroup') + + def freeze(self): + self.set_freezer_state(state="FROZEN") + + def unfreeze(self): + self.set_freezer_state(state="THAWED") diff --git a/setup.py b/setup.py index 2365f53..95a82c9 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ entry_points={ 'console_scripts': [ 'user_cgroups = cgroups.user:main', + 'cgc = cgroups.cgc:main' ] }, )