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