Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to add directory #1400

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 86 additions & 5 deletions supervisor/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,9 @@ def process_config(self, do_usage=True):
"""Process configuration data structure.

This includes reading config file if necessary, setting defaults etc.
"""
"""
if self.configfile:
self.process_config_file(do_usage)

self.process_config_file(do_usage)
# Copy config options to attributes of self. This only fills
# in options that aren't already set from the command line.
for name, confname in self.names_list:
Expand Down Expand Up @@ -484,7 +483,7 @@ def default_configfile(self):
def realize(self, *arg, **kw):
Options.realize(self, *arg, **kw)
section = self.configroot.supervisord

# Additional checking of user option; set uid and gid
if self.user is not None:
try:
Expand Down Expand Up @@ -520,6 +519,7 @@ def realize(self, *arg, **kw):
self.serverurl = None

self.server_configs = sconfigs = section.server_configs
self.directories = section.directories

# we need to set a fallback serverurl that process.spawn can use

Expand Down Expand Up @@ -670,6 +670,7 @@ def get(opt, default, **kwargs):
env.update(proc.environment)
proc.environment = env
section.server_configs = self.server_configs_from_parser(parser)
section.directories = self.directories_from_parser(parser)
section.profile_options = None
return section

Expand Down Expand Up @@ -1132,6 +1133,29 @@ def server_configs_from_parser(self, parser):

return configs

def directories_from_parser(self, parser):
"""Read [directory:x] sections from parser."""
get = parser.saneget
directories = []
all_sections = parser.sections()
for section in all_sections:
if not section.startswith('directory:'):
continue
name = section.split(':', 1)[1]
path = get(section, "path", None)
if path is None:
raise ValueError('[%s] section requires a value for "path"')
path = normalize_path(path)
create = boolean(get(section, "create", "false"))
mode_str = get(section, "mode", "777")
try:
mode = octal_type(mode_str)
except (TypeError, ValueError):
raise ValueError("Invalid mode value %s" % mode_str)
directory = DirectoryConfig(name, path, create, mode)
directories.append(directory)
return directories

def daemonize(self):
self.poller.before_daemonize()
self._daemonize()
Expand Down Expand Up @@ -1496,7 +1520,19 @@ def make_logger(self):
self.logger.warn(msg)
for msg in self.parse_infos:
self.logger.info(msg)


def check_directories(self):
# must be called after realize() and after supervisor does setuid()
for directory in self.directories:
try:
if directory.verify_exists():
self.logger.info(
"created directory (%s) %s"
% (directory.name, directory.path)
)
except ValueError as error:
self.usage(str(error))

def make_http_servers(self, supervisord):
from supervisor.http import make_http_servers
return make_http_servers(self, supervisord)
Expand Down Expand Up @@ -2060,6 +2096,51 @@ def make_group(self):
from supervisor.process import FastCGIProcessGroup
return FastCGIProcessGroup(self)


class DirectoryConfig(object):
"""Configuration for a required directory."""

def __init__(self, name, path, create, mode):
self.name = name
self.path = path
self.create = create
self.mode = mode

def __repr__(self):
return "DirectoryConfig({!r}, {!r}, {!r}, 0o{:o})".format(
self.name, self.path, self.create, self.mode
)

def __eq__(self, other):
return (
isinstance(other, DirectoryConfig)
and self.name == other.name
and self.path == other.path
and self.create == other.create
and self.mode == other.mode
)

def verify_exists(self):
"""Verify the directory exists, and potentially try to
create it. Return True if directory was created."""
if os.path.exists(self.path):
if not os.path.isdir(self.path):
raise ValueError(
"required directory (%s) %s is not a directory"
% (self.name, self.path)
)
else:
if self.create:
os.makedirs(self.path, self.mode)
return True
else:
raise ValueError(
"required directory (%s) %s does not exist"
% (self.name, self.path)
)
return False


def readFile(filename, offset, length):
""" Read length bytes from the file named by filename starting at
offset """
Expand Down
4 changes: 3 additions & 1 deletion supervisor/supervisord.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def main(self):
# first request
self.options.cleanup_fds()

self.options.set_uid_or_exit()
self.options.set_uid_or_exit()

if self.options.first:
self.options.set_rlimits_or_exit()
Expand All @@ -71,6 +71,8 @@ def main(self):
# delay logger instantiation until after setuid
self.options.make_logger()

self.options.check_directories()

if not self.options.nocleanup:
# clean up old automatic logs
self.options.clear_autochildlogdir()
Expand Down
3 changes: 3 additions & 0 deletions supervisor/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ def get_socket_map(self):
def make_logger(self):
pass

def check_directories(self):
pass

def clear_autochildlogdir(self):
self.autochildlogdir_cleared = True

Expand Down
112 changes: 106 additions & 6 deletions supervisor/tests/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import shutil
import errno
import platform
import tempfile

from supervisor.compat import StringIO
from supervisor.compat import as_bytes
Expand Down Expand Up @@ -3255,13 +3256,36 @@ def test_drop_privileges_nonroot_different_user(self):
msg = instance.drop_privileges(42)
self.assertEqual(msg, "Can't drop privilege as nonroot user")

def test_daemonize_notifies_poller_before_and_after_fork(self):
def test_directories_from_parser(self):
from supervisor.options import DirectoryConfig
text = lstrip("""\
[directory:dir1]
path = /foo/bar

[directory:dir2]
path = /foo/new
create = True

[directory:dir3]
path = /foo/new2
create = True
mode = 755
""")
from supervisor.options import UnhosedConfigParser
config = UnhosedConfigParser()
config.read_string(text)
instance = self._makeOne()
instance._daemonize = lambda: None
instance.poller = Mock()
instance.daemonize()
instance.poller.before_daemonize.assert_called_once_with()
instance.poller.after_daemonize.assert_called_once_with()
directories = instance.directories_from_parser(config)

self.assertEqual(
directories,
[
DirectoryConfig('dir1', '/foo/bar', False, 0o777),
DirectoryConfig('dir2', '/foo/new', True, 0o777),
DirectoryConfig('dir3', '/foo/new2', True, 0o755)
]
)


class ProcessConfigTests(unittest.TestCase):
def _getTargetClass(self):
Expand Down Expand Up @@ -3732,6 +3756,82 @@ def test_split_namespec(self):
self.assertEqual(s('group:'), ('group', None))
self.assertEqual(s('group:*'), ('group', None))


class TestDirectories(unittest.TestCase):

def setUp(self):
self.temp_dir = tempfile.mkdtemp("supervisor", "testdir")

def tearDown(self):
shutil.rmtree(self.temp_dir)

def test_repr(self):
from supervisor.options import DirectoryConfig
directory_config = DirectoryConfig(
"foo",
"/foo/bar",
True,
0o777
)
self.assertEqual(
repr(directory_config),
"DirectoryConfig('foo', '/foo/bar', True, 0o777)"
)

def check_directory_exists(self):
from supervisor.options import DirectoryConfig
directory_config = DirectoryConfig(
"foo",
self.temp_dir,
False,
0o777
)
# Should return false if directory exists and isn't created in the check
self.assertFalse(directory_config.verify_exists())

def check_directory_does_not_exist(self):
from supervisor.options import DirectoryConfig
directory = os.path.join(self.temp_dir, "nothing_here")
directory_config = DirectoryConfig(
"foo",
directory,
False,
0o777
)
with self.assertRaises(ValueError):
# Directory doesn't exist, should raise ValueError
directory_config.verify_exists()

def check_directory_not_a_directory(self):
from supervisor.options import DirectoryConfig
directory = os.path.join(self.temp_dir, "foo")
with open(directory, "w") as fh:
fh.write("test")
directory_config = DirectoryConfig(
"foo",
directory,
False,
0o777
)
with self.assertRaises(ValueError):
# Path exists, but not a directory
directory_config.verify_exists()

def check_create(self):
from supervisor.options import DirectoryConfig
directory = os.path.join(self.temp_dir, "foo/bar")
self.assertFalse(os.path.exists(directory))
directory_config = DirectoryConfig(
"foo",
directory,
True,
0o777
)
# With create = True, the directory will be created
self.assertTrue(directory_config.verify_exists())
self.assertTrue(os.path.isdir(directory))


def test_suite():
return unittest.findTestCases(sys.modules[__name__])

Expand Down