summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Utils.py
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-03-21 14:35:04 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-03-21 14:35:04 -0400
commit38f99b402d3118e2c26ff42d72ddf9b666adac13 (patch)
tree705d18bed66337192816325facb1733280291ff5 /src/lib/Bcfg2/Utils.py
parenta3f2a731bbef5d9c2033eed23432aa7792d47989 (diff)
parent71d7285c405bd639f1f9f2642ea8fb567b97caec (diff)
downloadbcfg2-38f99b402d3118e2c26ff42d72ddf9b666adac13.tar.gz
bcfg2-38f99b402d3118e2c26ff42d72ddf9b666adac13.tar.bz2
bcfg2-38f99b402d3118e2c26ff42d72ddf9b666adac13.zip
Merge branch '1.3.2' into maint
Conflicts: src/lib/Bcfg2/Client/Tools/__init__.py src/sbin/bcfg2-test testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIX/Test__init.py testsuite/Testsrc/Testlib/TestClient/TestTools/TestPOSIXUsers.py testsuite/Testsrc/test_code_checks.py
Diffstat (limited to 'src/lib/Bcfg2/Utils.py')
-rw-r--r--src/lib/Bcfg2/Utils.py225
1 files changed, 225 insertions, 0 deletions
diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py
new file mode 100644
index 000000000..7d7d26d5d
--- /dev/null
+++ b/src/lib/Bcfg2/Utils.py
@@ -0,0 +1,225 @@
+""" Miscellaneous useful utility functions, classes, etc., that are
+used by both client and server. Stuff that doesn't fit anywhere
+else. """
+
+import fcntl
+import logging
+import threading
+import subprocess
+from Bcfg2.Compat import any # pylint: disable=W0622
+
+
+class ClassName(object):
+ """ This very simple descriptor class exists only to get the name
+ of the owner class. This is used because, for historical reasons,
+ we expect every server plugin and every client tool to have a
+ ``name`` attribute that is in almost all cases the same as the
+ ``__class__.__name__`` attribute of the plugin object. This makes
+ that more dynamic so that each plugin and tool isn't repeating its own
+ name."""
+
+ def __get__(self, inst, owner):
+ return owner.__name__
+
+
+class PackedDigitRange(object):
+ """ Representation of a set of integer ranges. A range is
+ described by a comma-delimited string of integers and ranges,
+ e.g.::
+
+ 1,10-12,15-20
+
+ Ranges are inclusive on both bounds, and may include 0. Negative
+ numbers are not supported."""
+
+ def __init__(self, *ranges):
+ """ May be instantiated in one of two ways::
+
+ PackedDigitRange(<comma-delimited list of ranges>)
+
+ Or::
+
+ PackedDigitRange(<int_or_range>[, <int_or_range>[, ...]])
+
+ E.g., both of the following are valid::
+
+ PackedDigitRange("1-5,7, 10-12")
+ PackedDigitRange("1-5", 7, "10-12")
+ """
+ self.ranges = []
+ self.ints = []
+ self.str = ",".join(str(r) for r in ranges)
+ if len(ranges) == 1 and "," in ranges[0]:
+ ranges = ranges[0].split(",")
+ for item in ranges:
+ item = str(item).strip()
+ if item.endswith("-"):
+ self.ranges.append((int(item[:-1]), None))
+ elif '-' in str(item):
+ self.ranges.append(tuple(int(x) for x in item.split('-')))
+ else:
+ self.ints.append(int(item))
+
+ def includes(self, other):
+ """ Return True if ``other`` is included in this range.
+ Functionally equivalent to ``other in range``, which should be
+ used instead. """
+ return other in self
+
+ def __contains__(self, other):
+ other = int(other)
+ if other in self.ints:
+ return True
+ return any((end is None and other >= start) or
+ (end is not None and other >= start and other <= end)
+ for start, end in self.ranges)
+
+ def __repr__(self):
+ return "%s:%s" % (self.__class__.__name__, str(self))
+
+ def __str__(self):
+ return "[%s]" % self.str
+
+ def __len__(self):
+ return sum(r[1] - r[0] + 1 for r in self.ranges) + len(self.ints)
+
+
+def locked(fd):
+ """ Acquire a lock on a file.
+
+ :param fd: The file descriptor to lock
+ :type fd: int
+ :returns: bool - True if the file is already locked, False
+ otherwise """
+ try:
+ fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except IOError:
+ return True
+ return False
+
+
+class ExecutorResult(object):
+ """ Returned as the result of a call to
+ :func:`Bcfg2.Utils.Executor.run`. The result can be accessed via
+ the instance variables, documented below, as a boolean (which is
+ equivalent to :attr:`Bcfg2.Utils.ExecutorResult.success`), or as a
+ tuple, which, for backwards compatibility, is equivalent to
+ ``(result.retval, result.stdout.splitlines())``."""
+
+ def __init__(self, stdout, stderr, retval):
+ #: The output of the command
+ self.stdout = stdout
+
+ #: The error produced by the command
+ self.stderr = stderr
+
+ #: The return value of the command.
+ self.retval = retval
+
+ #: Whether or not the command was successful. If the
+ #: ExecutorResult is used as a boolean, ``success`` is
+ #: returned.
+ self.success = retval == 0
+
+ #: A friendly error message
+ self.error = None
+ if self.retval:
+ if self.stderr:
+ self.error = "%s (rv: %s)" % (self.stderr, self.retval)
+ elif self.stdout:
+ self.error = "%s (rv: %s)" % (self.stdout, self.retval)
+ else:
+ self.error = "No output or error; return value %s" % \
+ self.retval
+
+ def __repr__(self):
+ if self.error:
+ return "Errored command result: %s" % self.error
+ elif self.stdout:
+ return "Successful command result: %s" % self.stdout
+ else:
+ return "Successful command result: No output"
+
+ def __getitem__(self, idx):
+ """ This provides compatibility with the old Executor, which
+ returned a tuple of (return value, stdout split by lines). """
+ return (self.retval, self.stdout.splitlines())[idx]
+
+ def __nonzero__(self):
+ return self.__bool__()
+
+ def __bool__(self):
+ return self.success
+
+
+class Executor(object):
+ """ A convenient way to run external commands with
+ :class:`subprocess.Popen` """
+
+ def __init__(self, timeout=None):
+ """
+ :param timeout: Set a default timeout for all commands run by
+ this Executor object
+ :type timeout: float
+ """
+ self.logger = logging.getLogger(self.__class__.__name__)
+ self.timeout = timeout
+
+ def _timeout(self, proc):
+ """ A function suitable for passing to
+ :class:`threading.Timer` that kills the given process.
+
+ :param proc: The process to kill upon timeout.
+ :type proc: subprocess.Popen
+ :returns: None """
+ if proc.poll() == None:
+ try:
+ proc.kill()
+ self.logger.warning("Process exceeeded timeout, killing")
+ except OSError:
+ pass
+
+ def run(self, command, inputdata=None, shell=False, timeout=None):
+ """ Run a command, given as a list, optionally giving it the
+ specified input data.
+
+ :param command: The command to run, as a list (preferred) or
+ as a string. See :class:`subprocess.Popen` for
+ details.
+ :type command: list or string
+ :param inputdata: Data to pass to the command on stdin
+ :type inputdata: string
+ :param shell: Run the given command in a shell (not recommended)
+ :type shell: bool
+ :param timeout: Kill the command if it runs longer than this
+ many seconds. Set to 0 or -1 to explicitly
+ override a default timeout.
+ :type timeout: float
+ :returns: :class:`Bcfg2.Utils.ExecutorResult`
+ """
+ if isinstance(command, str):
+ cmdstr = command
+ else:
+ cmdstr = " ".join(command)
+ self.logger.debug("Running: %s" % cmdstr)
+ proc = subprocess.Popen(command, shell=shell, bufsize=16384,
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE, close_fds=True)
+ if timeout is None:
+ timeout = self.timeout
+ if timeout is not None:
+ timer = threading.Timer(float(timeout), self._timeout, [proc])
+ timer.start()
+ try:
+ if inputdata:
+ for line in inputdata.splitlines():
+ self.logger.debug('> %s' % line)
+ (stdout, stderr) = proc.communicate(input=inputdata)
+ for line in stdout.splitlines(): # pylint: disable=E1103
+ self.logger.debug('< %s' % line)
+ for line in stderr.splitlines(): # pylint: disable=E1103
+ self.logger.info(line)
+ return ExecutorResult(stdout, stderr, proc.wait())
+ finally:
+ if timeout is not None:
+ timer.cancel()