PageRenderTime 45ms CodeModel.GetById 16ms app.highlight 24ms RepoModel.GetById 1ms app.codeStats 0ms

/system-config-services-0.101.7/src/scservices/core/legacy/services.py

#
Python | 582 lines | 454 code | 85 blank | 43 comment | 31 complexity | 9d8e1bd87680cf0fa6768b37fe2a4556 MD5 | raw file
  1# -*- coding: utf-8 -*-
  2
  3# services.py: services
  4#
  5# Copyright Š 2007 - 2009, 2011 Red Hat, Inc.
  6#
  7# This program is free software; you can redistribute it and/or modify
  8# it under the terms of the GNU General Public License as published by
  9# the Free Software Foundation; either version 2 of the License, or
 10# (at your option) any later version.
 11#
 12# This program is distributed in the hope that it will be useful,
 13# but WITHOUT ANY WARRANTY; without even the implied warranty of
 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15# GNU General Public License for more details.
 16#
 17# You should have received a copy of the GNU General Public License
 18# along with this program; if not, write to the Free Software
 19# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 20#
 21# Authors:
 22# Nils Philippsen <nils@redhat.com>
 23
 24import os
 25import sys
 26import re
 27import gobject
 28import gamin
 29
 30from ..util import getstatusoutput
 31from .asynccmd import AsyncCmdQueue
 32
 33from .servicesinfo import SysVServiceInfo, XinetdServiceInfo, InvalidServiceInfoException
 34
 35# make pydoc work by working around cyclic dependency
 36
 37try:
 38    import serviceherders
 39except AttributeError:
 40    pass
 41
 42from slip.util.hookable import HookableSet
 43
 44SVC_STATUS_REFRESHING = 0
 45SVC_STATUS_UNKNOWN = 1
 46SVC_STATUS_STOPPED = 2
 47SVC_STATUS_RUNNING = 3
 48SVC_STATUS_DEAD = 4
 49
 50SVC_ENABLED_REFRESHING = 0
 51SVC_ENABLED_ERROR = 1
 52SVC_ENABLED_YES = 2
 53SVC_ENABLED_NO = 3
 54SVC_ENABLED_CUSTOM = 4
 55
 56
 57class InvalidServiceException(Exception):
 58
 59    pass
 60
 61
 62class Service(object):
 63
 64    """Represents an abstract service."""
 65
 66    def __init__(self, name, mon, herder):
 67
 68        super(Service, self).__init__()
 69        self.name = name
 70        self.mon = mon
 71        self.herder = herder
 72        self._asynccmdqueue = AsyncCmdQueue()
 73        self.conf_updates_running = 0
 74
 75    def __repr__(self):
 76        return "<%s.%s object at %s: \"%s\">" % (self.__class__.__module__,
 77                self.__class__.__name__, hex(id(self)), self.name)
 78
 79    def notify_herder(self, change):
 80        """Notify the herder of a change."""
 81
 82        if self.herder:
 83            self.herder.notify(change, self)
 84
 85    def load(self):
 86        """Load configuration from disk synchronously."""
 87
 88        mainloop = gobject.MainLoop()
 89        self._async_load(self._sync_load_finished, mainloop)
 90        mainloop.run()
 91
 92    def _sync_load_finished(self, mainloop, __exception__=None):
 93        mainloop.quit()
 94        if __exception__ == None:
 95            self.valid = True
 96        else:
 97            self.valid = False
 98
 99    def async_load(self):
100        """Load configuration from disk asynchronously, notify herder on completion."""
101
102        self.notify_herder(serviceherders.SVC_CONF_UPDATING)
103        return self._async_load(self._async_load_finished)
104
105    def _async_load_finished(self, __exception__=None):
106        self.notify_herder(serviceherders.SVC_CONF_CHANGED)
107
108    def _async_load_ready(self, cmd, callback, *p, **k):
109        try:
110            self._async_load_process(cmd)
111        except Exception, e:
112            k["__exception__"] = e
113        self.conf_updates_running -= 1
114        callback(*p, **k)
115
116    def _async_load(self, callback, *p, **k):
117        """Load configuration from disk asynchronously."""
118
119        raise NotImplementedError
120
121    def _async_load_process(self, cmd):
122        """Process asynchronously loaded configuration."""
123
124        raise NotImplementedError
125
126
127class ChkconfigService(Service):
128
129    """Represents an abstract service handled with chkconfig."""
130
131    _chkconfig_invocation = "LC_ALL=C /sbin/chkconfig"
132    _chkconfig_version_re = \
133        re.compile(r"^chkconfig\s+version\s+(?P<version>[\d\.]+)\s*$",
134                   re.IGNORECASE)
135
136    def __init__(self, name, mon, herder):
137
138        super(ChkconfigService, self).__init__(name, mon, herder)
139        self._chkconfig_running = 0
140
141        if not hasattr(ChkconfigService, "chkconfig_version"):
142
143            # determine chkconfig version, we only want to use some features
144            # from certain versions on
145
146            ckver_pipe = os.popen("%s -v" %
147                                   ChkconfigService._chkconfig_invocation)
148            for line in ckver_pipe:
149                line = line.strip()
150                m = ChkconfigService._chkconfig_version_re.match(line)
151                if m:
152                    version = m.group("version")
153                    ChkconfigService.chkconfig_version = \
154                            tuple(map(lambda x: int(x), version.split(".")))
155                    break
156
157            ckver_pipe.close()
158
159    @property
160    def chkconfig_invocation(self):
161        if not hasattr(self.__class__, "_chkconfig_actual_invocation"):
162            invoke = [ChkconfigService._chkconfig_invocation]
163
164            # --type <sysv|xinetd> exists in 1.3.40 and later
165
166            if ChkconfigService.chkconfig_version >= (1, 3, 40):
167                invoke.append("--type %s" % self.chkconfig_type)
168
169            self.__class__._chkconfig_actual_invocation = " ".join(invoke)
170
171        return self.__class__._chkconfig_actual_invocation
172
173    def is_chkconfig_running(self):
174        return self._chkconfig_running > 0
175
176    def _change_enablement_ready(self, cmd):
177        self._chkconfig_running = max((0, self._chkconfig_running - 1))
178
179    def _change_enablement(self, change):
180        self._chkconfig_running += 1
181        self._asynccmdqueue.queue("%s \"%s\" \"%s\""
182                                   % (self.chkconfig_invocation, self.name,
183                                  change), ready_cb=self._change_enablement_ready)
184
185    def enable(self):
186        """Enable this service."""
187
188        self._change_enablement("on")
189
190    def disable(self):
191        """Disable this service."""
192
193        self._change_enablement("off")
194
195    def get_enabled(self):
196        raise NotImplementedError
197
198
199class SysVService(ChkconfigService):
200
201    """Represents a service handled by SysVinit."""
202
203    chkconfig_type = "sysv"
204
205    init_list_re = \
206        re.compile(r"^(?P<name>\S+)\s+0:(?P<r0>off|on)\s+1:(?P<r1>off|on)\s+"
207                    "2:(?P<r2>off|on)\s+3:(?P<r3>off|on)\s+4:(?P<r4>off|on)\s+"
208                    "5:(?P<r5>off|on)\s+6:(?P<r6>off|on)\s*$", re.MULTILINE)
209
210    no_chkconfig_re = \
211        re.compile(r"^service (?P<name>.*) does not support chkconfig$",
212                re.MULTILINE)
213    chkconfig_error_re = \
214        re.compile(r"^error reading information on service (?P<name>.*):.*$",
215                re.MULTILINE)
216    chkconfig_unconfigured_re = \
217        re.compile(r"^service (?P<name>.*) supports chkconfig, but is not "
218                    "referenced in any runlevel "
219                    "\(run 'chkconfig --add (?P=name)'\)$", re.MULTILINE)
220
221    _fallback_default_runlevels = set((2, 3, 4, 5))
222
223    def __init__(self, name, mon, herder):
224        super(SysVService, self).__init__(name, mon, herder)
225
226        try:
227            self.info = SysVServiceInfo(name)
228        except InvalidServiceInfoException:
229            raise InvalidServiceException
230
231        # property: self.runlevels = HookableSet ()
232
233        self.configured = False
234
235        self.status_updates_running = 0
236        self.status = SVC_STATUS_UNKNOWN
237        self.status_output = None
238
239        self.valid = False
240
241        self._status_asynccmdqueue = AsyncCmdQueue()
242
243        if self.info.pidfiles:
244            self.pidfiles = set(self.info.pidfiles)
245            self.pids = set()
246            self.pids_pidfiles = {}
247
248            for file in self.info.pidfiles:
249                self.mon.watch_file(file, self._pidfile_changed)
250        else:
251
252            # no pidfile(s), watch /var/lock/subsys/...
253
254            self.mon.watch_file("/var/lock/subsys/%s" % self.name,
255                                self._var_lock_subsys_changed)
256
257    def _async_load(self, callback, *p, **k):
258        """Load configuration from disk asynchronously."""
259
260        p = (callback, ) + p
261        self._asynccmdqueue.queue("%s --list \"%s\"" %
262                                   (self.chkconfig_invocation, self.name),
263                                  combined_stdout=True,
264                                  ready_cb=self._async_load_ready,
265                                  ready_args=p, ready_kwargs=k)
266        self.conf_updates_running += 1
267
268    def _async_load_process(self, cmd):
269        """Process asynchronously loaded configuration."""
270
271        exitcode = cmd.exitcode
272        output = cmd.output
273
274        if exitcode != 0:
275            if self.no_chkconfig_re.search(output)\
276                 or self.chkconfig_error_re.search(output):
277                raise InvalidServiceException(output)
278            elif self.chkconfig_unconfigured_re.search(output):
279                self.configured = False
280                return
281            else:
282                # the service might have been deleted, let the herder take care
283                # of it
284                return
285
286        m = self.init_list_re.search(output)
287        if not m or m.group("name") != self.name:
288            raise output
289        runlevels = set()
290        for runlevel in xrange(1, 6):
291            if m.group("r%d" % runlevel) == "on":
292                runlevels.add(runlevel)
293
294        # disable save hook temporarily to avoid endless loops
295
296        self.runlevels.hooks_enabled = False
297        self.runlevels.clear()
298        self.runlevels.update(runlevels)
299        self.runlevels.hooks_enabled = True
300
301        self.configured = True
302        self.conf_updates_running -= 1
303
304    def _runlevels_save_hook(self):
305        """Save runlevel configuration to disk."""
306
307        runlevel_changes = {"on": [], "off": []}
308
309        for i in xrange(0, 7):
310            runlevel_changes[i in self._runlevels and "on" or "off"
311                             ].append(str(i))
312
313        for what in ("on", "off"):
314            if not len(runlevel_changes[what]):
315                continue
316            (status, output) = getstatusoutput("%s --level %s %s %s 2>&1"
317                     % (self.chkconfig_invocation,
318                    "".join(runlevel_changes[what]), self.name, what))
319            if status != 0:
320                raise OSError("Saving service '%s' failed, command was "
321                              "'%s --level %s %s %s'.\n"
322                              "Output was:\n"
323                              "%s" % (self.name, self.chkconfig_invocation,
324                                      "".join(runlevel_changes[what]),
325                                      self.name, what, output))
326
327        self.configured = True
328
329    def _get_runlevels(self):
330        if not hasattr(self, "_runlevels"):
331            self._runlevels = HookableSet()
332            self._runlevels.add_hook(self._runlevels_save_hook)
333        return self._runlevels
334
335    def _set_runlevels(self, runlevels):
336        self.runlevels
337        if self._runlevels != runlevels:
338            self._runlevels.freeze_hooks()
339            self._runlevels.clear()
340            self._runlevels.update(runlevels)
341            self._runlevels.thaw_hooks()
342
343    runlevels = property(_get_runlevels, _set_runlevels)
344
345    def _var_lock_subsys_changed(self, path, action, *p):
346        if action != gamin.GAMEndExist:
347            self.async_status_update()
348
349    def _pidfile_changed(self, path, action, *p):
350        if action in (gamin.GAMCreated, gamin.GAMChanged, gamin.GAMExists):
351            self._watch_pidfile(path)
352        elif action == gamin.GAMDeleted:
353            if len(self.pids) == 0:
354                self.async_status_update()
355            self._unwatch_pidfile(path)
356
357    def _watch_pidfile(self, path):
358        self._unwatch_pidfile(path)
359        try:
360            pidfile = open(path, "r")
361        except IOError:
362            return
363
364        for line in pidfile:
365            for _pid in line.split():
366                try:
367                    pid = int(_pid)
368                    self._watch_pid(pid, path)
369                except ValueError:
370                    pass
371
372        pidfile.close()
373
374    def _unwatch_pidfile(self, path):
375        unwatch_pids = set()
376        for pid in self.pids:
377            if path in self.pids_pidfiles[pid]:
378                unwatch_pids.add(pid)
379        for pid in unwatch_pids:
380            self._unwatch_pid(pid, path)
381
382    def _proc_pid_changed(self, path, action, *p):
383        if action != gamin.GAMEndExist:
384            self.async_status_update()
385
386    def _watch_pid(self, pid, pidfile):
387        if pid not in self.pids:
388            self.pids.add(pid)
389            self.mon.watch_file("/proc/%d" % pid, self._proc_pid_changed)
390        if not self.pids_pidfiles.has_key(pid):
391            self.pids_pidfiles[pid] = set()
392        self.pids_pidfiles[pid].add(pidfile)
393
394    def _unwatch_pid(self, pid, pidfile):
395        self.pids_pidfiles[pid].discard(pidfile)
396        if len(self.pids_pidfiles[pid]) == 0:
397            del self.pids_pidfiles[pid]
398            self.pids.discard(pid)
399            self.mon.stop_watch("/proc/%d" % pid)
400
401    def async_status_update(self):
402        """Determine service status asynchronously."""
403
404        return self._async_status_update(self._async_status_update_finished)
405
406    def _async_status_update_finished(self):
407        self.notify_herder(serviceherders.SVC_STATUS_CHANGED)
408
409    def _async_status_update(self, callback, *p, **k):
410        p = (callback, ) + p
411        self._status_asynccmdqueue.queue("env LC_ALL=C /sbin/service "
412                                          "\"%s\" status" % self.name,
413                                         combined_stdout=True,
414                                         ready_cb=self._status_update_ready,
415                                         ready_args=p, ready_kwargs=k)
416        self.status_updates_running += 1
417        self.status = SVC_STATUS_REFRESHING
418
419    def _status_update_ready(self, cmd, callback, *p, **k):
420        self.status_updates_running -= 1
421        if self.status_updates_running <= 0:
422            self.status_updates_running = 0
423            self.status = SVC_STATUS_UNKNOWN
424        self._status_update_process(cmd)
425        callback(*p, **k)
426
427    def _status_update_process(self, cmd):
428        """Process asynchronously determined service status."""
429
430        exitcode = cmd.exitcode
431
432        if exitcode == 0:
433            self.status = SVC_STATUS_RUNNING
434        elif exitcode == 1 or exitcode == 2:
435            self.status = SVC_STATUS_DEAD
436        elif exitcode == 3:
437            self.status = SVC_STATUS_STOPPED
438        else:
439            self.status = SVC_STATUS_UNKNOWN
440
441        # print "%s: %s: %d" % (cmd, self.name, self.status)
442
443        self.status_output = cmd.output
444
445    def get_enabled(self):
446        """Determines the enablement state of a service."""
447
448        if self.conf_updates_running > 0:
449            return SVC_ENABLED_REFRESHING
450        if len(self.runlevels) == 0:
451            return SVC_ENABLED_NO
452
453        # if len (self.info.startrunlevels) > 0 \
454        #        and self.runlevels == self.info.startrunlevels \
455        #        or self.runlevels == self._fallback_default_runlevels:
456
457        if self.runlevels == self._fallback_default_runlevels:
458            return SVC_ENABLED_YES
459        else:
460            return SVC_ENABLED_CUSTOM
461
462    def _change_status(self, change):
463        if change in ("start", "stop", "restart"):
464            self.status = SVC_STATUS_REFRESHING
465
466        # no callback, we let the herder handle that
467
468        self._asynccmdqueue.queue("env LC_ALL=C /sbin/service \"%s\" \"%s\"" %
469                                   (self.name, change))
470
471    def start(self):
472        """Start this service."""
473
474        self._change_status("start")
475
476    def stop(self):
477        """Stop this service."""
478
479        self._change_status("stop")
480
481    def restart(self):
482        """Restart this service."""
483
484        self._change_status("restart")
485
486    def reload(self):
487        """Reload this service."""
488
489        self._change_status("reload")
490
491
492class XinetdService(ChkconfigService):
493
494    """Represents a service handled by xinetd."""
495
496    chkconfig_type = "xinetd"
497
498    xinetd_list_re = re.compile(r"^(?P<name>\S+)\s+(?P<enabled>off|on)\s*$",
499            re.MULTILINE)
500
501    def __init__(self, name, mon, herder):
502        super(XinetdService, self).__init__(name, mon, herder)
503
504        try:
505            self.info = XinetdServiceInfo(name)
506        except InvalidServiceInfoException:
507            raise InvalidServiceException
508
509        # property: self.enabled = None
510
511        self.load()
512
513    def _async_load(self, callback, *p, **k):
514        """Load configuration from disk asynchronously."""
515
516        p = (callback, ) + p
517
518        self._asynccmdqueue.queue("%s --list %s" % 
519                                   (self.chkconfig_invocation, self.name),
520                                  combined_stdout=True,
521                                  ready_cb=self._async_load_ready,
522                                  ready_args=p, ready_kwargs=k)
523        self.conf_updates_running += 1
524
525    def _async_load_process(self, cmd):
526        """Process asynchronously loaded configuration."""
527
528        exitcode = cmd.exitcode
529        output = cmd.output
530
531        if exitcode != 0:
532            if self.no_chkconfig_re.search(output)\
533                 or self.chkconfig_error_re.search(output):
534                raise InvalidServiceException(output)
535            elif self.chkconfig_unconfigured_re.search(output):
536                self.configured = False
537                return
538            else:
539
540                # service might have been deleted, let the herder take care of it
541
542                return
543
544        m = self.xinetd_list_re.search(output)
545        if not m or m.group("name") != self.name:
546            print >> sys.stderr, "%s: unable to parse chkconfig output:\n" \
547                                  "%s" % (self, output)
548            self._enabled = None
549        else:
550            self._enabled = m.group("enabled") == "on"
551
552    def _get_enabled(self):
553        if not hasattr(self, "_enabled"):
554            self._enabled = None
555        return self._enabled
556
557    def _set_enabled(self, enabled):
558        old_enabled = getattr(self, "_enabled", None)
559        self._enabled = enabled
560        if old_enabled != enabled:
561            (status, output) = getstatusoutput("%s %s %s 2>&1" %
562                                                (self.chkconfig_invocation,
563                                                 self.name, self.enabled and \
564                                                  "on" or "off"))
565            if status != 0:
566                raise OSError("Saving service '%(name)s' failed, command was "
567                               "'%(invocation)s %(name)s %(action)s 2>&1'." % \
568                                    {"name": self.name,
569                                     "invocation": self.chkconfig_invocation,
570                                     "action": self.enabled and "on" or "off"})
571
572    enabled = property(_get_enabled, _set_enabled)
573
574    def get_enabled(self):
575        if self.conf_updates_running > 0:
576            return SVC_ENABLED_REFRESHING
577        elif self.enabled == None:
578            return SVC_ENABLED_ERROR
579        return self.enabled and SVC_ENABLED_YES or SVC_ENABLED_NO
580
581
582service_classes = [SysVService, XinetdService]