diff --git a/swift/common/utils.py b/swift/common/utils.py index 36c755065c..d5692cada3 100644 --- a/swift/common/utils.py +++ b/swift/common/utils.py @@ -5820,3 +5820,30 @@ def get_db_files(db_path): continue results.append(os.path.join(db_dir, f)) return sorted(results) + + +def systemd_notify(logger=None): + """ + Notify the service manager that started this process, if it is + systemd-compatible, that this process correctly started. To do so, + it communicates through a Unix socket stored in environment variable + NOTIFY_SOCKET. More information can be found in systemd documentation: + https://www.freedesktop.org/software/systemd/man/sd_notify.html + + :param logger: a logger object + """ + msg = b'READY=1' + notify_socket = os.getenv('NOTIFY_SOCKET') + if notify_socket: + if notify_socket.startswith('@'): + # abstract namespace socket + notify_socket = '\0%s' % notify_socket[1:] + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + with closing(sock): + try: + sock.connect(notify_socket) + sock.sendall(msg) + del os.environ['NOTIFY_SOCKET'] + except EnvironmentError: + if logger: + logger.debug("Systemd notification failed", exc_info=True) diff --git a/swift/common/wsgi.py b/swift/common/wsgi.py index d544d9d2c0..8582b22e17 100644 --- a/swift/common/wsgi.py +++ b/swift/common/wsgi.py @@ -45,7 +45,7 @@ from swift.common.swob import Request, wsgi_quote, wsgi_unquote, \ from swift.common.utils import capture_stdio, disable_fallocate, \ drop_privileges, get_logger, NullLogger, config_true_value, \ validate_configuration, get_hub, config_auto_int_value, \ - reiterate, clean_up_daemon_hygiene + reiterate, clean_up_daemon_hygiene, systemd_notify SIGNUM_TO_NAME = {getattr(signal, n): n for n in dir(signal) if n.startswith('SIG') and '_' not in n} @@ -1185,6 +1185,9 @@ def run_wsgi(conf_path, app_section, *args, **kwargs): os.write(reexec_signal_fd, str(os.getpid()).encode('utf8')) os.close(reexec_signal_fd) + # Finally, signal systemd (if appropriate) that process started properly. + systemd_notify(logger=logger) + no_fork_sock = strategy.no_fork_sock() if no_fork_sock: run_server(conf, logger, no_fork_sock, global_conf=global_conf) diff --git a/test/unit/common/test_utils.py b/test/unit/common/test_utils.py index 2039fa5b11..6c4a94f9cc 100644 --- a/test/unit/common/test_utils.py +++ b/test/unit/common/test_utils.py @@ -4372,6 +4372,69 @@ cluster_dfw1 = http://dfw1.host/v1/ utils.load_pkg_resource(*args) self.assertEqual("Unhandled URI scheme: 'nog'", str(cm.exception)) + def test_systemd_notify(self): + m_sock = mock.Mock(connect=mock.Mock(), sendall=mock.Mock()) + with mock.patch('swift.common.utils.socket.socket', + return_value=m_sock) as m_socket: + # No notification socket + m_socket.reset_mock() + m_sock.reset_mock() + utils.systemd_notify() + self.assertEqual(m_socket.call_count, 0) + self.assertEqual(m_sock.connect.call_count, 0) + self.assertEqual(m_sock.sendall.call_count, 0) + + # File notification socket + m_socket.reset_mock() + m_sock.reset_mock() + os.environ['NOTIFY_SOCKET'] = 'foobar' + utils.systemd_notify() + m_socket.assert_called_once_with(socket.AF_UNIX, socket.SOCK_DGRAM) + m_sock.connect.assert_called_once_with('foobar') + m_sock.sendall.assert_called_once_with(b'READY=1') + self.assertNotIn('NOTIFY_SOCKET', os.environ) + + # Abstract notification socket + m_socket.reset_mock() + m_sock.reset_mock() + os.environ['NOTIFY_SOCKET'] = '@foobar' + utils.systemd_notify() + m_socket.assert_called_once_with(socket.AF_UNIX, socket.SOCK_DGRAM) + m_sock.connect.assert_called_once_with('\0foobar') + m_sock.sendall.assert_called_once_with(b'READY=1') + self.assertNotIn('NOTIFY_SOCKET', os.environ) + + # Test logger with connection error + m_sock = mock.Mock(connect=mock.Mock(side_effect=EnvironmentError), + sendall=mock.Mock()) + m_logger = mock.Mock(debug=mock.Mock()) + with mock.patch('swift.common.utils.socket.socket', + return_value=m_sock) as m_socket: + os.environ['NOTIFY_SOCKET'] = '@foobar' + m_sock.reset_mock() + m_logger.reset_mock() + utils.systemd_notify() + self.assertEqual(0, m_sock.sendall.call_count) + self.assertEqual(0, m_logger.debug.call_count) + + m_sock.reset_mock() + m_logger.reset_mock() + utils.systemd_notify(logger=m_logger) + self.assertEqual(0, m_sock.sendall.call_count) + m_logger.debug.assert_called_once_with( + "Systemd notification failed", exc_info=True) + + # Test it for real + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + sock.settimeout(5) + sock.bind('\0foobar') + os.environ['NOTIFY_SOCKET'] = '@foobar' + utils.systemd_notify() + msg = sock.recv(512) + sock.close() + self.assertEqual(msg, b'READY=1') + self.assertNotIn('NOTIFY_SOCKET', os.environ) + class ResellerConfReader(unittest.TestCase):