You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
neutron/neutron/agent/linux/daemon.py

265 lines
8.1 KiB

# Copyright 2012 New Dream Network, LLC (DreamHost)
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import atexit
import fcntl
import grp
import logging as std_logging
from logging import handlers
import os
import pwd
import signal
import sys
from neutron_lib import exceptions
from oslo_log import log as logging
import setproctitle
import six
from neutron._i18n import _
LOG = logging.getLogger(__name__)
DEVNULL = object()
# Note: We can't use sys.std*.fileno() here. sys.std* objects may be
# random file-like objects that may not match the true system std* fds
# - and indeed may not even have a file descriptor at all (eg: test
# fixtures that monkey patch fixtures.StringStream onto sys.stdout).
# Below we always want the _real_ well-known 0,1,2 Unix fds during
# os.dup2 manipulation.
STDIN_FILENO = 0
STDOUT_FILENO = 1
STDERR_FILENO = 2
def setuid(user_id_or_name):
try:
new_uid = int(user_id_or_name)
except (TypeError, ValueError):
new_uid = pwd.getpwnam(user_id_or_name).pw_uid
if new_uid != 0:
try:
os.setuid(new_uid)
except OSError:
msg = _('Failed to set uid %s') % new_uid
LOG.critical(msg)
raise exceptions.FailToDropPrivilegesExit(msg)
def setgid(group_id_or_name):
try:
new_gid = int(group_id_or_name)
except (TypeError, ValueError):
new_gid = grp.getgrnam(group_id_or_name).gr_gid
if new_gid != 0:
try:
os.setgid(new_gid)
except OSError:
msg = _('Failed to set gid %s') % new_gid
LOG.critical(msg)
raise exceptions.FailToDropPrivilegesExit(msg)
def unwatch_log():
"""Replace WatchedFileHandler handlers by FileHandler ones.
Neutron logging uses WatchedFileHandler handlers but they do not
support privileges drop, this method replaces them by FileHandler
handlers supporting privileges drop.
"""
log_root = logging.getLogger(None).logger
to_replace = [h for h in log_root.handlers
if isinstance(h, handlers.WatchedFileHandler)]
for handler in to_replace:
# NOTE(cbrandily): we use default delay(=False) to ensure the log file
# is opened before privileges drop.
new_handler = std_logging.FileHandler(handler.baseFilename,
mode=handler.mode,
encoding=handler.encoding)
log_root.removeHandler(handler)
log_root.addHandler(new_handler)
def drop_privileges(user=None, group=None):
"""Drop privileges to user/group privileges."""
if user is None and group is None:
return
if os.geteuid() != 0:
msg = _('Root permissions are required to drop privileges.')
LOG.critical(msg)
raise exceptions.FailToDropPrivilegesExit(msg)
if group is not None:
try:
os.setgroups([])
except OSError:
msg = _('Failed to remove supplemental groups')
LOG.critical(msg)
raise exceptions.FailToDropPrivilegesExit(msg)
setgid(group)
if user is not None:
setuid(user)
LOG.info("Process runs with uid/gid: %(uid)s/%(gid)s",
{'uid': os.getuid(), 'gid': os.getgid()})
class Pidfile(object):
def __init__(self, pidfile, procname, uuid=None):
self.pidfile = pidfile
self.procname = procname
self.uuid = uuid
try:
self.fd = os.open(pidfile, os.O_CREAT | os.O_RDWR)
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
LOG.exception("Error while handling pidfile: %s", pidfile)
sys.exit(1)
def __str__(self):
return self.pidfile
def unlock(self):
fcntl.flock(self.fd, fcntl.LOCK_UN)
def write(self, pid):
os.ftruncate(self.fd, 0)
os.write(self.fd, six.b("%s" % pid))
os.fsync(self.fd)
def read(self):
try:
pid = int(os.read(self.fd, 128))
os.lseek(self.fd, 0, os.SEEK_SET)
return pid
except ValueError:
return
def is_running(self):
pid = self.read()
if not pid:
return False
cmdline = '/proc/%s/cmdline' % pid
try:
with open(cmdline, "r") as f:
exec_out = f.readline()
return self.procname in exec_out and (not self.uuid or
self.uuid in exec_out)
except IOError:
return False
class Daemon(object):
"""A generic daemon class.
Usage: subclass the Daemon class and override the run() method
"""
def __init__(self, pidfile, stdin=DEVNULL, stdout=DEVNULL,
stderr=DEVNULL, procname='python', uuid=None,
user=None, group=None):
"""Note: pidfile may be None."""
self.stdin = stdin
self.stdout = stdout
self.stderr = stderr
self.procname = procname
self.pidfile = (Pidfile(pidfile, procname, uuid)
if pidfile is not None else None)
self.user = user
self.group = group
def _fork(self):
try:
pid = os.fork()
if pid > 0:
os._exit(0)
except OSError:
LOG.exception('Fork failed')
sys.exit(1)
def daemonize(self):
"""Daemonize process by doing Stevens double fork."""
# flush any buffered data before fork/dup2.
if self.stdout is not DEVNULL:
self.stdout.flush()
if self.stderr is not DEVNULL:
self.stderr.flush()
# sys.std* may not match STD{OUT,ERR}_FILENO. Tough.
for f in (sys.stdout, sys.stderr):
f.flush()
# fork first time
self._fork()
# decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(0)
# fork second time
self._fork()
# redirect standard file descriptors
with open(os.devnull, 'w+') as devnull:
stdin = devnull if self.stdin is DEVNULL else self.stdin
stdout = devnull if self.stdout is DEVNULL else self.stdout
stderr = devnull if self.stderr is DEVNULL else self.stderr
os.dup2(stdin.fileno(), STDIN_FILENO)
os.dup2(stdout.fileno(), STDOUT_FILENO)
os.dup2(stderr.fileno(), STDERR_FILENO)
if self.pidfile is not None:
# write pidfile
atexit.register(self.delete_pid)
signal.signal(signal.SIGTERM, self.handle_sigterm)
self.pidfile.write(os.getpid())
def delete_pid(self):
if self.pidfile is not None:
os.remove(str(self.pidfile))
def handle_sigterm(self, signum, frame):
sys.exit(0)
def start(self):
"""Start the daemon."""
self._parent_proctitle = setproctitle.getproctitle()
if self.pidfile is not None and self.pidfile.is_running():
self.pidfile.unlock()
LOG.error('Pidfile %s already exist. Daemon already '
'running?', self.pidfile)
sys.exit(1)
# Start the daemon
self.daemonize()
self.run()
def _set_process_title(self):
proctitle = "%s (%s)" % (self.procname, self._parent_proctitle)
setproctitle.setproctitle(proctitle)
def run(self):
"""Override this method and call super().run when subclassing Daemon.
start() will call this method after the process has daemonized.
"""
self._set_process_title()
unwatch_log()
drop_privileges(self.user, self.group)