7ad4ca7b37bd429cbca6655caabee54b31ea8b51
[openstack-build/neutron-build.git] / neutron / agent / linux / daemon.py
1 # Copyright 2012 New Dream Network, LLC (DreamHost)
2 #
3 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
4 #    not use this file except in compliance with the License. You may obtain
5 #    a copy of the License at
6 #
7 #         http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #    Unless required by applicable law or agreed to in writing, software
10 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 #    License for the specific language governing permissions and limitations
13 #    under the License.
14
15 import atexit
16 import fcntl
17 import grp
18 import logging as std_logging
19 from logging import handlers
20 import os
21 import pwd
22 import signal
23 import sys
24
25 from oslo_log import log as logging
26
27 from neutron._i18n import _, _LE, _LI
28 from neutron.common import exceptions
29
30 LOG = logging.getLogger(__name__)
31
32 DEVNULL = object()
33
34 # Note: We can't use sys.std*.fileno() here.  sys.std* objects may be
35 # random file-like objects that may not match the true system std* fds
36 # - and indeed may not even have a file descriptor at all (eg: test
37 # fixtures that monkey patch fixtures.StringStream onto sys.stdout).
38 # Below we always want the _real_ well-known 0,1,2 Unix fds during
39 # os.dup2 manipulation.
40 STDIN_FILENO = 0
41 STDOUT_FILENO = 1
42 STDERR_FILENO = 2
43
44
45 def setuid(user_id_or_name):
46     try:
47         new_uid = int(user_id_or_name)
48     except (TypeError, ValueError):
49         new_uid = pwd.getpwnam(user_id_or_name).pw_uid
50     if new_uid != 0:
51         try:
52             os.setuid(new_uid)
53         except OSError:
54             msg = _('Failed to set uid %s') % new_uid
55             LOG.critical(msg)
56             raise exceptions.FailToDropPrivilegesExit(msg)
57
58
59 def setgid(group_id_or_name):
60     try:
61         new_gid = int(group_id_or_name)
62     except (TypeError, ValueError):
63         new_gid = grp.getgrnam(group_id_or_name).gr_gid
64     if new_gid != 0:
65         try:
66             os.setgid(new_gid)
67         except OSError:
68             msg = _('Failed to set gid %s') % new_gid
69             LOG.critical(msg)
70             raise exceptions.FailToDropPrivilegesExit(msg)
71
72
73 def unwatch_log():
74     """Replace WatchedFileHandler handlers by FileHandler ones.
75
76     Neutron logging uses WatchedFileHandler handlers but they do not
77     support privileges drop, this method replaces them by FileHandler
78     handlers supporting privileges drop.
79     """
80     log_root = logging.getLogger(None).logger
81     to_replace = [h for h in log_root.handlers
82                   if isinstance(h, handlers.WatchedFileHandler)]
83     for handler in to_replace:
84         # NOTE(cbrandily): we use default delay(=False) to ensure the log file
85         # is opened before privileges drop.
86         new_handler = std_logging.FileHandler(handler.baseFilename,
87                                               mode=handler.mode,
88                                               encoding=handler.encoding)
89         log_root.removeHandler(handler)
90         log_root.addHandler(new_handler)
91
92
93 def drop_privileges(user=None, group=None):
94     """Drop privileges to user/group privileges."""
95     if user is None and group is None:
96         return
97
98     if os.geteuid() != 0:
99         msg = _('Root permissions are required to drop privileges.')
100         LOG.critical(msg)
101         raise exceptions.FailToDropPrivilegesExit(msg)
102
103     if group is not None:
104         try:
105             os.setgroups([])
106         except OSError:
107             msg = _('Failed to remove supplemental groups')
108             LOG.critical(msg)
109             raise exceptions.FailToDropPrivilegesExit(msg)
110         setgid(group)
111
112     if user is not None:
113         setuid(user)
114
115     LOG.info(_LI("Process runs with uid/gid: %(uid)s/%(gid)s"),
116              {'uid': os.getuid(), 'gid': os.getgid()})
117
118
119 class Pidfile(object):
120     def __init__(self, pidfile, procname, uuid=None):
121         self.pidfile = pidfile
122         self.procname = procname
123         self.uuid = uuid
124         try:
125             self.fd = os.open(pidfile, os.O_CREAT | os.O_RDWR)
126             fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
127         except IOError:
128             LOG.exception(_LE("Error while handling pidfile: %s"), pidfile)
129             sys.exit(1)
130
131     def __str__(self):
132         return self.pidfile
133
134     def unlock(self):
135         fcntl.flock(self.fd, fcntl.LOCK_UN)
136
137     def write(self, pid):
138         os.ftruncate(self.fd, 0)
139         os.write(self.fd, "%d" % pid)
140         os.fsync(self.fd)
141
142     def read(self):
143         try:
144             pid = int(os.read(self.fd, 128))
145             os.lseek(self.fd, 0, os.SEEK_SET)
146             return pid
147         except ValueError:
148             return
149
150     def is_running(self):
151         pid = self.read()
152         if not pid:
153             return False
154
155         cmdline = '/proc/%s/cmdline' % pid
156         try:
157             with open(cmdline, "r") as f:
158                 exec_out = f.readline()
159             return self.procname in exec_out and (not self.uuid or
160                                                   self.uuid in exec_out)
161         except IOError:
162             return False
163
164
165 class Daemon(object):
166     """A generic daemon class.
167
168     Usage: subclass the Daemon class and override the run() method
169     """
170     def __init__(self, pidfile, stdin=DEVNULL, stdout=DEVNULL,
171                  stderr=DEVNULL, procname='python', uuid=None,
172                  user=None, group=None, watch_log=True):
173         """Note: pidfile may be None."""
174         self.stdin = stdin
175         self.stdout = stdout
176         self.stderr = stderr
177         self.procname = procname
178         self.pidfile = (Pidfile(pidfile, procname, uuid)
179                         if pidfile is not None else None)
180         self.user = user
181         self.group = group
182         self.watch_log = watch_log
183
184     def _fork(self):
185         try:
186             pid = os.fork()
187             if pid > 0:
188                 os._exit(0)
189         except OSError:
190             LOG.exception(_LE('Fork failed'))
191             sys.exit(1)
192
193     def daemonize(self):
194         """Daemonize process by doing Stevens double fork."""
195
196         # flush any buffered data before fork/dup2.
197         if self.stdout is not DEVNULL:
198             self.stdout.flush()
199         if self.stderr is not DEVNULL:
200             self.stderr.flush()
201         # sys.std* may not match STD{OUT,ERR}_FILENO.  Tough.
202         for f in (sys.stdout, sys.stderr):
203             f.flush()
204
205         # fork first time
206         self._fork()
207
208         # decouple from parent environment
209         os.chdir("/")
210         os.setsid()
211         os.umask(0)
212
213         # fork second time
214         self._fork()
215
216         # redirect standard file descriptors
217         with open(os.devnull, 'w+') as devnull:
218             stdin = devnull if self.stdin is DEVNULL else self.stdin
219             stdout = devnull if self.stdout is DEVNULL else self.stdout
220             stderr = devnull if self.stderr is DEVNULL else self.stderr
221             os.dup2(stdin.fileno(), STDIN_FILENO)
222             os.dup2(stdout.fileno(), STDOUT_FILENO)
223             os.dup2(stderr.fileno(), STDERR_FILENO)
224
225         if self.pidfile is not None:
226             # write pidfile
227             atexit.register(self.delete_pid)
228             signal.signal(signal.SIGTERM, self.handle_sigterm)
229             self.pidfile.write(os.getpid())
230
231     def delete_pid(self):
232         if self.pidfile is not None:
233             os.remove(str(self.pidfile))
234
235     def handle_sigterm(self, signum, frame):
236         sys.exit(0)
237
238     def start(self):
239         """Start the daemon."""
240
241         if self.pidfile is not None and self.pidfile.is_running():
242             self.pidfile.unlock()
243             LOG.error(_LE('Pidfile %s already exist. Daemon already '
244                           'running?'), self.pidfile)
245             sys.exit(1)
246
247         # Start the daemon
248         self.daemonize()
249         self.run()
250
251     def run(self):
252         """Override this method and call super().run when subclassing Daemon.
253
254         start() will call this method after the process has daemonized.
255         """
256         if not self.watch_log:
257             unwatch_log()
258         drop_privileges(self.user, self.group)