/neatx/lib/utils.py

http://neatx.googlecode.com/ · Python · 879 lines · 519 code · 110 blank · 250 comment · 51 complexity · 88607fa91b74fd422c3cb8e304f2305b MD5 · raw file

  1. #
  2. #
  3. # Copyright (C) 2009 Google Inc.
  4. #
  5. # This program is free software; you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation; either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but
  11. # WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. # General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  18. # 02110-1301, USA.
  19. """Module for utility functions"""
  20. import errno
  21. import fcntl
  22. import logging
  23. import logging.handlers
  24. import os
  25. import os.path
  26. import pwd
  27. import resource
  28. import re
  29. import signal
  30. import sys
  31. import syslog
  32. import tempfile
  33. import termios
  34. import time
  35. from neatx import constants
  36. from neatx import errors
  37. _SHELL_UNQUOTED_RE = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
  38. try:
  39. DEV_NULL = os.devnull
  40. except AttributeError:
  41. DEV_NULL = "/dev/null"
  42. class CustomSyslogHandler(logging.Handler):
  43. """Custom syslog handler.
  44. logging.handlers.SysLogHandler doesn't support splitting exceptions into
  45. several lines.
  46. This class should only be used once per process.
  47. """
  48. _LEVEL_MAP = {
  49. logging.CRITICAL: syslog.LOG_CRIT,
  50. logging.ERROR: syslog.LOG_ERR,
  51. logging.WARNING: syslog.LOG_WARNING,
  52. logging.INFO: syslog.LOG_INFO,
  53. logging.DEBUG: syslog.LOG_DEBUG,
  54. }
  55. def __init__(self, ident):
  56. """Initializes instances.
  57. @type ident: string
  58. @param ident: String prepended to every message
  59. """
  60. logging.Handler.__init__(self)
  61. syslog.openlog(ident, syslog.LOG_PID, syslog.LOG_USER)
  62. def close(self):
  63. syslog.closelog()
  64. def _MapLogLevel(self, levelno):
  65. """Maps log level to syslog.
  66. @type levelno: int
  67. @param levelno: Log level
  68. Default is LOG_DEBUG.
  69. """
  70. return self._LEVEL_MAP.get(levelno, syslog.LOG_DEBUG)
  71. def emit(self, record):
  72. """Send a log record to syslog.
  73. @type record: logging.LogRecord
  74. @param record: Log record
  75. """
  76. msg = self.format(record)
  77. if record.exc_info:
  78. messages = msg.split(os.linesep)
  79. else:
  80. messages = [msg]
  81. priority = self._MapLogLevel(record.levelno)
  82. for msg in messages:
  83. syslog.syslog(priority, msg)
  84. class LoggingSetupOptions(object):
  85. def __init__(self, debug, logtostderr):
  86. """Initializes logging setup options class.
  87. @type debug: bool
  88. @param debug: Whether to enable debug log messages
  89. @type logtostderr: bool
  90. @param logtostderr: Whether to write log messages to stderr
  91. """
  92. self.debug = debug
  93. self.logtostderr = logtostderr
  94. class LoggingSetup(object):
  95. """Logging setup class.
  96. This class should only be used once per process.
  97. """
  98. def __init__(self, program):
  99. """Configures the logging module
  100. @type program: str
  101. @param program: the name under which we should log messages
  102. """
  103. self._program = program
  104. self._options = None
  105. self._root_logger = None
  106. self._stderr_handler = None
  107. self._syslog_handler = None
  108. def Init(self):
  109. """Initializes the logging module.
  110. """
  111. assert not self._root_logger
  112. assert not self._stderr_handler
  113. assert not self._syslog_handler
  114. # Get root logger
  115. self._root_logger = self._InitRootLogger()
  116. # Create stderr handler
  117. self._stderr_handler = logging.StreamHandler(sys.stderr)
  118. # Create syslog handler
  119. self._syslog_handler = CustomSyslogHandler(self._program)
  120. self._ConfigureHandlers()
  121. def SetOptions(self, options):
  122. """Configure logging setup.
  123. @type options: L{LoggingSetupOptions}
  124. @param options: Configuration object
  125. """
  126. self._options = options
  127. self._ConfigureHandlers()
  128. @staticmethod
  129. def _InitRootLogger():
  130. """Initializes and returns the root logger.
  131. """
  132. root_logger = logging.getLogger("")
  133. root_logger.setLevel(logging.NOTSET)
  134. # Remove all previously setup handlers
  135. for handler in root_logger.handlers:
  136. root_logger.removeHandler(handler)
  137. handler.close()
  138. return root_logger
  139. def _ConfigureHandlers(self):
  140. """Set formatters and levels for handlers.
  141. """
  142. stderr_level = None
  143. syslog_level = None
  144. if self._options is None:
  145. # Log error and above
  146. stderr_level = logging.ERROR
  147. else:
  148. if self._options.debug:
  149. # Log everything
  150. level = logging.NOTSET
  151. else:
  152. # Log info and above (e.g. error)
  153. level = logging.INFO
  154. if self._options.logtostderr:
  155. stderr_level = level
  156. else:
  157. syslog_level = level
  158. # Configure handlers
  159. self._ConfigureSingleHandler(self._stderr_handler, stderr_level, False)
  160. self._ConfigureSingleHandler(self._syslog_handler, syslog_level, True)
  161. def _ConfigureSingleHandler(self, handler, level, is_syslog):
  162. """Configures a handler based on the parameters.
  163. """
  164. if level is None:
  165. self._root_logger.removeHandler(handler)
  166. return
  167. debug = self._options is not None and self._options.debug
  168. fmt = self._GetMessageFormat(self._program, is_syslog, debug)
  169. handler.setLevel(level)
  170. handler.setFormatter(logging.Formatter(fmt))
  171. self._root_logger.addHandler(handler)
  172. @staticmethod
  173. def _GetMessageFormat(program, is_syslog, debug):
  174. """Returns message format.
  175. """
  176. assert "%" not in program
  177. if is_syslog:
  178. fmt = ""
  179. else:
  180. fmt = "%(asctime)s: " + program + " pid=%(process)d "
  181. fmt += "%(levelname)s"
  182. if debug:
  183. fmt += " %(module)s:%(lineno)s"
  184. fmt += " %(message)s"
  185. return fmt
  186. def WithoutTerminalEcho(fd, fn, *args, **kwargs):
  187. """Calls function with ECHO flag disabled on passed file descriptor.
  188. @type fd: file or int
  189. @param fd: File descriptor
  190. @type fn: callable
  191. @param fn: Called function
  192. """
  193. assert callable(fn)
  194. # Keep old terminal settings
  195. try:
  196. old = termios.tcgetattr(fd)
  197. except termios.error, err:
  198. if err.args[0] not in (errno.ENOTTY, errno.EINVAL):
  199. raise
  200. old = None
  201. if old is not None:
  202. new = old[:]
  203. # Disable the echo flag in lflags (index 3)
  204. new[3] &= ~termios.ECHO
  205. termios.tcsetattr(fd, termios.TCSADRAIN, new)
  206. try:
  207. return fn(*args, **kwargs)
  208. finally:
  209. if old is not None:
  210. termios.tcsetattr(fd, termios.TCSADRAIN, old)
  211. def NormalizeSpace(text):
  212. """Replace all whitespace (\\s) with a single space.
  213. Whitespace at start and end are also removed.
  214. """
  215. return re.sub(r"\s+", " ", text).strip()
  216. def RemoveFile(filename):
  217. """Remove a file ignoring some errors.
  218. Remove a file, ignoring non-existing ones or directories. Other
  219. errors are passed.
  220. @type filename: str
  221. @param filename: the file to be removed
  222. """
  223. try:
  224. os.unlink(filename)
  225. except OSError, err:
  226. if err.errno not in (errno.ENOENT, errno.EISDIR):
  227. raise
  228. def SetCloseOnExecFlag(fd, enable):
  229. """Sets or unsets the close-on-exec flag on a file descriptor.
  230. @type fd: int
  231. @param fd: File descriptor
  232. @type enable: bool
  233. @param enable: Whether to set or unset it.
  234. """
  235. flags = fcntl.fcntl(fd, fcntl.F_GETFD)
  236. if enable:
  237. flags |= fcntl.FD_CLOEXEC
  238. else:
  239. flags &= ~fcntl.FD_CLOEXEC
  240. fcntl.fcntl(fd, fcntl.F_SETFD, flags)
  241. def SetNonblockFlag(fd, enable):
  242. """Sets or unsets the O_NONBLOCK flag on on a file descriptor.
  243. @type fd: int
  244. @param fd: File descriptor
  245. @type enable: bool
  246. @param enable: Whether to set or unset it
  247. """
  248. flags = fcntl.fcntl(fd, fcntl.F_GETFL)
  249. if enable:
  250. flags |= os.O_NONBLOCK
  251. else:
  252. flags &= ~os.O_NONBLOCK
  253. fcntl.fcntl(fd, fcntl.F_SETFL, flags)
  254. def ListVisibleFiles(path):
  255. """Returns a list of visible files in a directory.
  256. @type path: str
  257. @param path: the directory to enumerate
  258. @rtype: list
  259. @return: the list of all files not starting with a dot
  260. """
  261. files = [i for i in os.listdir(path) if not i.startswith(".")]
  262. files.sort()
  263. return files
  264. def WriteFile(file_name, fn=None, data=None,
  265. mode=None, uid=-1, gid=-1):
  266. """(Over)write a file atomically.
  267. The file_name and either fn (a function taking one argument, the
  268. file descriptor, and which should write the data to it) or data (the
  269. contents of the file) must be passed. The other arguments are
  270. optional and allow setting the file mode, owner and group of the file.
  271. If the function (WriteFile) doesn't raise an exception, it has succeeded and
  272. the target file has the new contents. If the function has raised an
  273. exception, an existing target file should be unmodified and the temporary
  274. file should be removed.
  275. @type file_name: str
  276. @param file_name: the target filename
  277. @type fn: callable
  278. @param fn: content writing function, called with
  279. file descriptor as parameter
  280. @type data: str
  281. @param data: contents of the file
  282. @type mode: int
  283. @param mode: file mode
  284. @type uid: int
  285. @param uid: the owner of the file
  286. @type gid: int
  287. @param gid: the group of the file
  288. @raise errors.ProgrammerError: if any of the arguments are not valid
  289. """
  290. if not os.path.isabs(file_name):
  291. raise errors.ProgrammerError("Path passed to WriteFile is not"
  292. " absolute: '%s'" % file_name)
  293. if [fn, data].count(None) != 1:
  294. raise errors.ProgrammerError("fn or data required")
  295. dir_name, base_name = os.path.split(file_name)
  296. fd, new_name = tempfile.mkstemp(prefix=".tmp", suffix=base_name,
  297. dir=dir_name)
  298. try:
  299. if uid != -1 or gid != -1:
  300. os.chown(new_name, uid, gid)
  301. if mode:
  302. os.chmod(new_name, mode)
  303. if data is not None:
  304. os.write(fd, data)
  305. else:
  306. fn(fd)
  307. os.fsync(fd)
  308. os.rename(new_name, file_name)
  309. finally:
  310. os.close(fd)
  311. # Make sure temporary file is removed in any case
  312. RemoveFile(new_name)
  313. def FormatTable(data, columns):
  314. """Formats a list of input data as a table.
  315. Columns must be passed via the C{columns} parameter. It must be a list of
  316. tuples, each containing the column caption, width and a function to retrieve
  317. the value. If the width is negative, the value is aligned to the right. The
  318. column function is called for every item.
  319. Example:
  320. >>> columns = [
  321. ("Name", 10, lambda item: item[0]),
  322. ("Value", -5, lambda item: item[1]),
  323. ]
  324. >>> data = [("Row%d" % i, i) for i in xrange(3)]
  325. >>> print "\\n".join(utils.FormatTable(data, columns))
  326. Name Value
  327. ---------- -----
  328. Row0 0
  329. Row1 1
  330. Row2 2
  331. @type data: list
  332. @param data: Input data
  333. @type columns: list of tuples
  334. @param columns: Column definitions
  335. @rtype: list of strings
  336. @return: Rows as strings
  337. """
  338. col_width = []
  339. header_row = []
  340. dashes_row = []
  341. format_fields = []
  342. for idx, (header, width, _) in enumerate(columns):
  343. if idx == (len(columns) - 1) and width >= 0:
  344. # Last column
  345. col_width.append(None)
  346. fmt = "%s"
  347. else:
  348. col_width.append(abs(width))
  349. if width < 0:
  350. fmt = "%*s"
  351. else:
  352. fmt = "%-*s"
  353. format_fields.append(fmt)
  354. if col_width[idx] is not None:
  355. header_row.append(col_width[idx])
  356. dashes_row.append(col_width[idx])
  357. header_row.append(header)
  358. dashes_row.append(abs(width) * "-")
  359. format = " ".join(format_fields)
  360. rows = [header_row, dashes_row]
  361. for item in data:
  362. row = []
  363. for idx, (_, width, fn) in enumerate(columns):
  364. if col_width[idx] is not None:
  365. row.append(col_width[idx])
  366. row.append(fn(item))
  367. rows.append(row)
  368. return [format % tuple(row) for row in rows]
  369. class RetryTimeout(Exception):
  370. """Retry loop timed out.
  371. """
  372. class RetryAgain(Exception):
  373. """Retry again.
  374. """
  375. def Retry(fn, start, factor, limit, timeout, _time=time):
  376. """Call a function repeatedly until it succeeds.
  377. The function C{fn} is called repeatedly until it doesn't throw L{RetryAgain}
  378. anymore. Between calls a delay starting at C{start} and multiplied by
  379. C{factor} on each run until it's above C{limit} is inserted. After a total of
  380. C{timeout} seconds, the retry loop fails with L{RetryTimeout}.
  381. @type fn: callable
  382. @param fn: Function to be called (no parameters supported)
  383. @type start: float
  384. @param start: Initial value for delay
  385. @type factor: float
  386. @param factor: Factor for delay increase
  387. @type limit: float
  388. @param limit: Upper limit for delay
  389. @type timeout: float
  390. @param timeout: Total timeout
  391. @return: Return value of function
  392. """
  393. assert start > 0
  394. assert factor > 1.0
  395. assert limit >= 0
  396. end_time = _time.time() + timeout
  397. delay = start
  398. while True:
  399. try:
  400. return fn()
  401. except RetryAgain:
  402. pass
  403. if _time.time() > end_time:
  404. raise RetryTimeout()
  405. _time.sleep(delay)
  406. if delay < limit:
  407. delay *= factor
  408. def ShellQuote(value):
  409. """Quotes shell argument according to POSIX.
  410. @type value: str
  411. @param value: the argument to be quoted
  412. @rtype: str
  413. @return: the quoted value
  414. """
  415. if _SHELL_UNQUOTED_RE.match(value):
  416. return value
  417. else:
  418. return "'%s'" % value.replace("'", "'\\''")
  419. def ShellQuoteArgs(args):
  420. """Quotes a list of shell arguments.
  421. @type args: list
  422. @param args: list of arguments to be quoted
  423. @rtype: str
  424. @return: the quoted arguments concatenaned with spaces
  425. """
  426. return " ".join([ShellQuote(i) for i in args])
  427. def CloseFd(fd, retries=5):
  428. """Close a file descriptor ignoring errors.
  429. @type fd: int
  430. @param fd: File descriptor
  431. @type retries: int
  432. @param retries: How many retries to make, in case we get any
  433. other error than EBADF
  434. """
  435. while retries > 0:
  436. retries -= 1
  437. try:
  438. os.close(fd)
  439. except OSError, err:
  440. if err.errno != errno.EBADF:
  441. continue
  442. break
  443. def GetMaxFd():
  444. """Determine max file descriptor number.
  445. @rtype: int
  446. @return: Max file descriptor number
  447. """
  448. maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
  449. if maxfd == resource.RLIM_INFINITY:
  450. # Default maximum for the number of available file descriptors.
  451. maxfd = 1024
  452. try:
  453. maxfd = os.sysconf("SC_OPEN_MAX")
  454. except ValueError:
  455. pass
  456. if maxfd < 0:
  457. maxfd = 1024
  458. return maxfd
  459. def StartDaemon(fn):
  460. """Start a daemon process.
  461. Starts a daemon process by double-forking and invoking a function. The
  462. function should then use exec*(2) to start the process.
  463. """
  464. # First fork
  465. pid = os.fork()
  466. if pid != 0:
  467. # Parent process
  468. # Try to avoid zombies
  469. try:
  470. os.waitpid(pid, 0)
  471. except OSError:
  472. pass
  473. return
  474. # First child process
  475. os.setsid()
  476. # Second fork
  477. pid = os.fork()
  478. if pid != 0:
  479. # Second parent process
  480. os._exit(0)
  481. # Second child process
  482. os.chdir("/")
  483. os.umask(077)
  484. # Close all file descriptors
  485. for fd in xrange(GetMaxFd()):
  486. CloseFd(fd)
  487. # Open /dev/null
  488. fd = os.open(DEV_NULL, os.O_RDWR)
  489. # Redirect stdio to /dev/null
  490. os.dup2(fd, constants.STDIN_FILENO)
  491. os.dup2(fd, constants.STDOUT_FILENO)
  492. os.dup2(fd, constants.STDERR_FILENO)
  493. try:
  494. # Call function starting daemon
  495. fn()
  496. os._exit(0)
  497. except (SystemExit, KeyboardInterrupt):
  498. raise
  499. except:
  500. os._exit(1)
  501. def CallWithSignalHandlers(sigtbl, fn, *args, **kwargs):
  502. previous = {}
  503. try:
  504. for (signum, handler) in sigtbl.iteritems():
  505. # Setup handler
  506. prev_handler = signal.signal(signum, handler)
  507. try:
  508. previous[signum] = prev_handler
  509. except Exception:
  510. # Restore previous handler
  511. signal.signal(signum, prev_handler)
  512. raise
  513. return fn(*args, **kwargs)
  514. finally:
  515. for (signum, prev_handler) in previous.items():
  516. signal.signal(signum, prev_handler)
  517. # If successful, remove from dict
  518. del previous[signum]
  519. assert not previous
  520. def _GetSignalNumberTable(_signal=signal):
  521. table = {}
  522. for name in dir(_signal):
  523. if name.startswith("SIG") and not name.startswith("SIG_"):
  524. signum = getattr(_signal, name)
  525. if isinstance(signum, (int, long)):
  526. table[signum] = name
  527. return table
  528. def GetSignalName(signum, _signal=signal):
  529. """Returns signal name by signal number.
  530. If the signal number is not known, "Signal 123" is returned (with the passed
  531. signal number instead of 123).
  532. @type signum: int
  533. @param signum: Signal number
  534. @rtype: str
  535. @return: Signal name
  536. """
  537. # This table could be cached
  538. table = _GetSignalNumberTable(_signal=_signal)
  539. try:
  540. return table[signum]
  541. except KeyError:
  542. return "Signal %s" % signum
  543. def _ConvertVersionPart(value):
  544. """Tries to convert a value to an integer.
  545. """
  546. try:
  547. return int(value)
  548. except ValueError:
  549. return value
  550. def _GetVersionSplitter(sep, count):
  551. """Returns callable to split wanted parts from version.
  552. @type sep: string
  553. @param sep: String of separator characters
  554. @type count: int
  555. @param count: How many parts to return
  556. """
  557. assert sep
  558. assert count == -1 or count > 0
  559. # str.split is a lot faster but doesn't provide the right semantics when
  560. # the caller wants more than one possible separator.
  561. #if len(sep) == 1:
  562. # if count == -1:
  563. # return lambda ver: ver.split(sep)
  564. # else:
  565. # return lambda ver: ver.split(sep, count)[:count]
  566. re_split = re.compile("[%s]" % re.escape(sep)).split
  567. if count == -1:
  568. return lambda ver: re_split(ver)
  569. else:
  570. return lambda ver: re_split(ver, count)[:count]
  571. def GetVersionComparator(sep, count=-1):
  572. """Returns a cmp-compatible function to compare two version strings.
  573. @type sep: string
  574. @param sep: String of separator characters, similar to strsep(3)
  575. @type count: int
  576. @param count: How many parts to compare (-1 for all)
  577. """
  578. # TODO: Support for versions such as "1.2~alpha0"
  579. split_fn = _GetVersionSplitter(sep, count)
  580. split_version = lambda ver: map(_ConvertVersionPart, split_fn(ver))
  581. return lambda x, y: cmp(split_version(x), split_version(y))
  582. def ParseVersion(version, sep, digits):
  583. """Parses a version and converts it to a number.
  584. Example:
  585. >>> ParseVersion("3.3.7.2-1", ".-", [2, 2, 2])
  586. 30307
  587. >>> ParseVersion("3.3.5.2-1", ".-", [2, 2, 4])
  588. 3030005
  589. >>> ParseVersion("3.3.9.2-1", ".-", [2, 2, 2, 2, 2])
  590. 303090201
  591. >>> ParseVersion("12.1", ".-", [2, 2, 2, 2])
  592. 12010000
  593. >>> ParseVersion("23.193", ".-", [2, 4])
  594. 230193
  595. @type version: str
  596. @param version: Version string
  597. @type sep: string
  598. @param sep: String of separator characters, similar to strsep(3)
  599. @type digits: list
  600. @param digits: List of digits to be used per part
  601. """
  602. split_fn = _GetVersionSplitter(sep, len(digits))
  603. parts = split_fn(version)
  604. version = 0
  605. total_exp = 0
  606. for idx, exp in reversed(list(enumerate(digits))):
  607. try:
  608. value = int(parts[idx])
  609. except IndexError:
  610. value = 0
  611. if value > 10 ** exp:
  612. raise ValueError("Version part %s (%r) too long for %s digits" %
  613. (idx, value, exp))
  614. version += (10 ** total_exp) * value
  615. total_exp += exp
  616. return version
  617. def FormatVersion(version, sep, digits):
  618. """Format version number.
  619. @type version: int
  620. @param version: Version number (as returned by L{ParseVersion})
  621. @type sep: string
  622. @param sep: Separator string between digits
  623. @type digits: list
  624. @param digits: List of digits used per part in the version numbers
  625. """
  626. # TODO: Implement support for more than one separator
  627. parts = []
  628. next = version
  629. for exp in reversed(digits):
  630. (next, value) = divmod(next, 10 ** exp)
  631. parts.append(str(value))
  632. if next > 0:
  633. raise ValueError("Invalid version number (%r) for given digits (%r)" %
  634. (version, digits))
  635. parts.reverse()
  636. return sep.join(parts)
  637. def LogFunctionWithPrefix(fn, prefix):
  638. return lambda msg, *args, **kwargs: fn(prefix + msg, *args, **kwargs)
  639. def GetExitcodeSignal(status):
  640. if status < 0:
  641. return (None, -status)
  642. else:
  643. return (status, None)
  644. def GetCurrentUserName():
  645. """Returns the name of the user that owns the current process.
  646. """
  647. return pwd.getpwuid(os.getuid())[0]