diff --git a/docs/api.rst b/docs/api.rst index 3481cca78..d3aa11ccf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -231,7 +231,8 @@ Process Control 'exitstatus': 0, 'stdout_logfile': '/path/to/stdout-log', 'stderr_logfile': '/path/to/stderr-log', - 'pid': 1} + 'pid': 1, + 'has_cgroups' 0} .. describe:: name @@ -286,6 +287,10 @@ Process Control UNIX process ID (PID) of the process, or 0 if the process is not running. + .. describe:: has_cgroups + + 1 if the process was configured to attach to at least one control + group, or 0 if not. .. automethod:: getAllProcessInfo diff --git a/supervisor/options.py b/supervisor/options.py index fc3f426e9..e46c72891 100644 --- a/supervisor/options.py +++ b/supervisor/options.py @@ -794,6 +794,7 @@ def processes_from_section(self, parser, section, group_name, serverurl = get(section, 'serverurl', None) if serverurl and serverurl.strip().upper() == 'AUTO': serverurl = None + cgroups = list_of_strings(get(section, 'cgroups', None)) umask = get(section, 'umask', None) if umask is not None: @@ -883,7 +884,8 @@ def processes_from_section(self, parser, section, group_name, exitcodes=exitcodes, redirect_stderr=redirect_stderr, environment=environment, - serverurl=serverurl) + serverurl=serverurl, + cgroups=cgroups) programs.append(pconfig) @@ -1600,7 +1602,7 @@ class ProcessConfig(Config): 'stderr_logfile_backups', 'stderr_logfile_maxbytes', 'stderr_events_enabled', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', - 'exitcodes', 'redirect_stderr' ] + 'exitcodes', 'redirect_stderr', 'cgroups' ] optional_param_names = [ 'environment', 'serverurl' ] def __init__(self, options, **params): diff --git a/supervisor/process.py b/supervisor/process.py index c33a68154..98061bb84 100644 --- a/supervisor/process.py +++ b/supervisor/process.py @@ -289,6 +289,19 @@ def _spawn_as_child(self, filename, argv): options.setpgrp() self._prepare_child_fds() # sending to fd 2 will put this output in the stderr log + + # Attach the child to cgroups before dropping privileges + # (may need to be root to do this). + msg = self.attach_cgroups() + if msg: + cgroups = self.config.cgroups + s = 'supervisor: error attaching process to cgroups %s ' % cgroups + options.write(2, s) + options.write(2, "(%s)\n" % msg) + # It would be great to actually affect parent state here + # (i.e. the parent should know that we haven't actually + # attached), but this only logs. + msg = self.set_uid() if msg: uid = self.config.uid @@ -491,6 +504,24 @@ def set_uid(self): msg = self.config.options.dropPrivileges(self.config.uid) return msg + def attach_cgroups(self): + # Doesn't undo in the event of partial failure (e.g. attach succeeds + # for one but not the other). + for cgroup in self.config.cgroups: + tasks_path = os.path.join(cgroup, "tasks") + if not os.path.isfile(tasks_path): + return "Can't find cgroup path %s" % tasks_path + try: + tasks = open(tasks_path, "w") + try: + # We assume that we're only called during process creation, + # so we're single-threaded and can write just our PID. + tasks.write(str(self.config.options.get_pid())) + finally: + tasks.close() + except IOError: + return "Couldn't attach process to cgroup %s" % tasks_path + def __cmp__(self, other): # sort by priority return cmp(self.config.priority, other.config.priority) diff --git a/supervisor/rpcinterface.py b/supervisor/rpcinterface.py index 92797a87f..80b675f60 100644 --- a/supervisor/rpcinterface.py +++ b/supervisor/rpcinterface.py @@ -527,6 +527,7 @@ def getProcessInfo(self, name): 'stdout_logfile':stdout_logfile, 'stderr_logfile':stderr_logfile, 'pid':process.pid, + 'has_cgroups':int(len(process.config.cgroups) > 0), } description = self._interpretProcessInfo(info) diff --git a/supervisor/tests/base.py b/supervisor/tests/base.py index 1a5c6de06..80953118b 100644 --- a/supervisor/tests/base.py +++ b/supervisor/tests/base.py @@ -484,7 +484,7 @@ def __init__(self, options, name, command, directory=None, umask=None, stderr_logfile_backups=0, stderr_logfile_maxbytes=0, redirect_stderr=False, stopsignal=None, stopwaitsecs=10, stopasgroup=False, killasgroup=False, - exitcodes=(0,2), environment=None, serverurl=None): + exitcodes=(0,2), environment=None, serverurl=None, cgroups=[]): self.options = options self.name = name self.command = command @@ -518,6 +518,7 @@ def __init__(self, options, name, command, directory=None, umask=None, self.umask = umask self.autochildlogs_created = False self.serverurl = serverurl + self.cgroups = cgroups def create_autochildlogs(self): self.autochildlogs_created = True diff --git a/supervisor/tests/test_options.py b/supervisor/tests/test_options.py index c3cf03a29..d1f579900 100644 --- a/supervisor/tests/test_options.py +++ b/supervisor/tests/test_options.py @@ -263,6 +263,7 @@ def test_options(self): numprocs = 2 command = /bin/cat autorestart=unexpected + cgroups=/path/one,/path/two """ % {'tempdir':tempfile.gettempdir()}) from supervisor import datatypes @@ -322,6 +323,7 @@ def test_options(self): self.assertEqual(proc1.directory, '/tmp') self.assertEqual(proc1.umask, 002) self.assertEqual(proc1.environment, dict(FAKE_ENV_VAR='/some/path')) + self.assertEqual(proc1.cgroups, []) cat2 = options.process_group_configs[1] self.assertEqual(cat2.name, 'cat2') @@ -343,6 +345,7 @@ def test_options(self): self.assertEqual(proc2.stdout_logfile_backups, 2) self.assertEqual(proc2.exitcodes, [0,2]) self.assertEqual(proc2.directory, None) + self.assertEqual(proc2.cgroups, []) cat3 = options.process_group_configs[2] self.assertEqual(cat3.name, 'cat3') @@ -364,6 +367,7 @@ def test_options(self): self.assertEqual(proc3.stopsignal, signal.SIGTERM) self.assertEqual(proc3.stopasgroup, True) self.assertEqual(proc3.killasgroup, True) + self.assertEqual(proc3.cgroups, []) cat4 = options.process_group_configs[3] self.assertEqual(cat4.name, 'cat4') @@ -386,6 +390,7 @@ def test_options(self): self.assertEqual(proc4_a.stopsignal, signal.SIGTERM) self.assertEqual(proc4_a.stopasgroup, False) self.assertEqual(proc4_a.killasgroup, False) + self.assertEqual(proc4_a.cgroups, ["/path/one", "/path/two"]) proc4_b = cat4.process_configs[1] self.assertEqual(proc4_b.name, 'fleeb_1') @@ -403,6 +408,7 @@ def test_options(self): self.assertEqual(proc4_b.stopsignal, signal.SIGTERM) self.assertEqual(proc4_b.stopasgroup, False) self.assertEqual(proc4_b.killasgroup, False) + self.assertEqual(proc4_b.cgroups, ["/path/one", "/path/two"]) here = os.path.abspath(os.getcwd()) self.assertEqual(instance.uid, 0) @@ -704,6 +710,7 @@ def test_processes_from_section(self): environment = KEY1=val1,KEY2=val2,KEY3=%(process_num)s numprocs = 2 process_name = %(group_name)s_%(program_name)s_%(process_num)02d + cgroups = foo,bar """) from supervisor.options import UnhosedConfigParser config = UnhosedConfigParser() @@ -730,6 +737,7 @@ def test_processes_from_section(self): self.assertEqual(pconfig.redirect_stderr, False) self.assertEqual(pconfig.environment, {'KEY1':'val1', 'KEY2':'val2', 'KEY3':'0'}) + self.assertEqual(pconfig.cgroups, ["foo", "bar"]) def test_processes_from_section_host_node_name_expansion(self): instance = self._makeOne() @@ -1445,7 +1453,7 @@ def _makeOne(self, *arg, **kw): 'stderr_events_enabled', 'stderr_logfile_backups', 'stderr_logfile_maxbytes', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes', - 'redirect_stderr', 'environment'): + 'redirect_stderr', 'environment', 'cgroups'): defaults[name] = name defaults.update(kw) return self._getTargetClass()(*arg, **defaults) @@ -1519,7 +1527,7 @@ def _makeOne(self, *arg, **kw): 'stderr_events_enabled', 'stderr_logfile_backups', 'stderr_logfile_maxbytes', 'stopsignal', 'stopwaitsecs', 'stopasgroup', 'killasgroup', 'exitcodes', - 'redirect_stderr', 'environment'): + 'redirect_stderr', 'environment', 'cgroups'): defaults[name] = name defaults.update(kw) return self._getTargetClass()(*arg, **defaults) diff --git a/supervisor/tests/test_process.py b/supervisor/tests/test_process.py index 57a6d504d..9e398f33c 100644 --- a/supervisor/tests/test_process.py +++ b/supervisor/tests/test_process.py @@ -904,6 +904,29 @@ def test_set_uid(self): self.assertEqual(options.privsdropped, 1) self.assertEqual(msg, None) + def test_attach_cgroups(self): + cgroups = ["/tmp/test_cg1", "/tmp/test_cg2"] + for cg in cgroups: + if os.path.exists(cg): + os.rmdir(cg) + os.makedirs(cg) + open(os.path.join(cg, "tasks"), "w").close() + + options = DummyOptions() + config = DummyPConfig(options, 'test', '/test', cgroups=cgroups) + instance = self._makeOne(config) + msg = instance.attach_cgroups() + for cg in cgroups: + tasks_path = os.path.join(cg, "tasks") + tasks = open(tasks_path) + try: + self.assertEqual(tasks.read().strip(), str(os.getpid())) + finally: + tasks.close() + os.remove(tasks_path) + os.rmdir(cg) + self.assertEqual(msg, None) + def test_cmp_bypriority(self): options = DummyOptions() config = DummyPConfig(options, 'notthere', '/notthere', diff --git a/supervisor/tests/test_supervisord.py b/supervisor/tests/test_supervisord.py index 9bc5322f4..14837daf2 100644 --- a/supervisor/tests/test_supervisord.py +++ b/supervisor/tests/test_supervisord.py @@ -261,7 +261,8 @@ def make_pconfig(name, command, **params): 'stopsignal': None, 'stopwaitsecs': 10, 'stopasgroup': False, 'killasgroup': False, - 'exitcodes': (0,2), 'environment': None, 'serverurl': None } + 'exitcodes': (0,2), 'environment': None, 'serverurl': None, + 'cgroups': None } result.update(params) return ProcessConfig(options, **result)