import calendar
import hmac
import collections
-import struct
-import IN
from cStringIO import StringIO
try:
+ # pylint: disable-msg=F0401
import ctypes
except ImportError:
ctypes = None
from ganeti import errors
from ganeti import constants
from ganeti import compat
+from ganeti import netutils
_locksheld = []
HEX_CHAR_RE, HEX_CHAR_RE),
re.S | re.I)
-# Structure definition for getsockopt(SOL_SOCKET, SO_PEERCRED, ...):
-# struct ucred { pid_t pid; uid_t uid; gid_t gid; };
-#
-# The GNU C Library defines gid_t and uid_t to be "unsigned int" and
-# pid_t to "int".
-#
-# IEEE Std 1003.1-2008:
-# "nlink_t, uid_t, gid_t, and id_t shall be integer types"
-# "blksize_t, pid_t, and ssize_t shall be signed integer types"
-_STRUCT_UCRED = "iII"
-_STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED)
+_VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
# Certificate verification results
(CERT_WARNING,
return rr
-def GetSocketCredentials(sock):
- """Returns the credentials of the foreign process connected to a socket.
-
- @param sock: Unix socket
- @rtype: tuple; (number, number, number)
- @return: The PID, UID and GID of the connected foreign process.
-
- """
- peercred = sock.getsockopt(socket.SOL_SOCKET, IN.SO_PEERCRED,
- _STRUCT_UCRED_SIZE)
- return struct.unpack(_STRUCT_UCRED, peercred)
-
-
def RemoveFile(filename):
"""Remove a file ignoring some errors.
raise errors.TypeEnforcementError(msg)
+def _GetProcStatusPath(pid):
+ """Returns the path for a PID's proc status file.
+
+ @type pid: int
+ @param pid: Process ID
+ @rtype: string
+
+ """
+ return "/proc/%d/status" % pid
+
+
def IsProcessAlive(pid):
"""Check if a given pid exists on the system.
if pid <= 0:
return False
- proc_entry = "/proc/%d/status" % pid
# /proc in a multiprocessor environment can have strange behaviors.
# Retry the os.stat a few times until we get a good result.
try:
- return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5, args=[proc_entry])
+ return Retry(_TryStat, (0.01, 1.5, 0.1), 0.5,
+ args=[_GetProcStatusPath(pid)])
except RetryTimeout, err:
err.RaiseInner()
+def _ParseSigsetT(sigset):
+ """Parse a rendered sigset_t value.
+
+ This is the opposite of the Linux kernel's fs/proc/array.c:render_sigset_t
+ function.
+
+ @type sigset: string
+ @param sigset: Rendered signal set from /proc/$pid/status
+ @rtype: set
+ @return: Set of all enabled signal numbers
+
+ """
+ result = set()
+
+ signum = 0
+ for ch in reversed(sigset):
+ chv = int(ch, 16)
+
+ # The following could be done in a loop, but it's easier to read and
+ # understand in the unrolled form
+ if chv & 1:
+ result.add(signum + 1)
+ if chv & 2:
+ result.add(signum + 2)
+ if chv & 4:
+ result.add(signum + 3)
+ if chv & 8:
+ result.add(signum + 4)
+
+ signum += 4
+
+ return result
+
+
+def _GetProcStatusField(pstatus, field):
+ """Retrieves a field from the contents of a proc status file.
+
+ @type pstatus: string
+ @param pstatus: Contents of /proc/$pid/status
+ @type field: string
+ @param field: Name of field whose value should be returned
+ @rtype: string
+
+ """
+ for line in pstatus.splitlines():
+ parts = line.split(":", 1)
+
+ if len(parts) < 2 or parts[0] != field:
+ continue
+
+ return parts[1].strip()
+
+ return None
+
+
+def IsProcessHandlingSignal(pid, signum, status_path=None):
+ """Checks whether a process is handling a signal.
+
+ @type pid: int
+ @param pid: Process ID
+ @type signum: int
+ @param signum: Signal number
+ @rtype: bool
+
+ """
+ if status_path is None:
+ status_path = _GetProcStatusPath(pid)
+
+ try:
+ proc_status = ReadFile(status_path)
+ except EnvironmentError, err:
+ # In at least one case, reading /proc/$pid/status failed with ESRCH.
+ if err.errno in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL, errno.ESRCH):
+ return False
+ raise
+
+ sigcgt = _GetProcStatusField(proc_status, "SigCgt")
+ if sigcgt is None:
+ raise RuntimeError("%s is missing 'SigCgt' field" % status_path)
+
+ # Now check whether signal is handled
+ return signum in _ParseSigsetT(sigcgt)
+
+
def ReadPidFile(pidfile):
"""Read a pid from a file.
return None
-class HostInfo:
- """Class implementing resolver and hostname functionality
-
- """
- _VALID_NAME_RE = re.compile("^[a-z0-9._-]{1,255}$")
-
- def __init__(self, name=None):
- """Initialize the host name object.
-
- If the name argument is not passed, it will use this system's
- name.
-
- """
- if name is None:
- name = self.SysName()
-
- self.query = name
- self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
- self.ip = self.ipaddrs[0]
-
- def ShortName(self):
- """Returns the hostname without domain.
-
- """
- return self.name.split('.')[0]
-
- @staticmethod
- def SysName():
- """Return the current system's name.
-
- This is simply a wrapper over C{socket.gethostname()}.
-
- """
- return socket.gethostname()
-
- @staticmethod
- def LookupHostname(hostname):
- """Look up hostname
-
- @type hostname: str
- @param hostname: hostname to look up
-
- @rtype: tuple
- @return: a tuple (name, aliases, ipaddrs) as returned by
- C{socket.gethostbyname_ex}
- @raise errors.ResolverError: in case of errors in resolving
-
- """
- try:
- result = socket.gethostbyname_ex(hostname)
- except socket.gaierror, err:
- # hostname not found in DNS
- raise errors.ResolverError(hostname, err.args[0], err.args[1])
+def ValidateServiceName(name):
+ """Validate the given service name.
- return result
+ @type name: number or string
+ @param name: Service name or port specification
- @classmethod
- def NormalizeName(cls, hostname):
- """Validate and normalize the given hostname.
-
- @attention: the validation is a bit more relaxed than the standards
- require; most importantly, we allow underscores in names
- @raise errors.OpPrereqError: when the name is not valid
+ """
+ try:
+ numport = int(name)
+ except (ValueError, TypeError):
+ # Non-numeric service name
+ valid = _VALID_SERVICE_NAME_RE.match(name)
+ else:
+ # Numeric port (protocols other than TCP or UDP might need adjustments
+ # here)
+ valid = (numport >= 0 and numport < (1 << 16))
- """
- hostname = hostname.lower()
- if (not cls._VALID_NAME_RE.match(hostname) or
- # double-dots, meaning empty label
- ".." in hostname or
- # empty initial label
- hostname.startswith(".")):
- raise errors.OpPrereqError("Invalid hostname '%s'" % hostname,
- errors.ECODE_INVAL)
- if hostname.endswith("."):
- hostname = hostname.rstrip(".")
- return hostname
-
-
-def GetHostInfo(name=None):
- """Lookup host name and raise an OpPrereqError for failures"""
+ if not valid:
+ raise errors.OpPrereqError("Invalid service name '%s'" % name,
+ errors.ECODE_INVAL)
- try:
- return HostInfo(name)
- except errors.ResolverError, err:
- raise errors.OpPrereqError("The given name (%s) does not resolve: %s" %
- (err[0], err[2]), errors.ECODE_RESOLVER)
+ return name
def ListVolumeGroups():
return nv
-def IsValidIP(ip):
- """Verifies the syntax of an IPv4 address.
-
- This function checks if the IPv4 address passes is valid or not based
- on syntax (not IP range, class calculations, etc.).
-
- @type ip: str
- @param ip: the address to be checked
- @rtype: a regular expression match object
- @return: a regular expression match object, or None if the
- address is not valid
-
- """
- unit = "(0|[1-9]\d{0,2})"
- #TODO: convert and return only boolean
- return re.match("^%s\.%s\.%s\.%s$" % (unit, unit, unit, unit), ip)
-
-
def IsValidShellParam(word):
"""Verifies is the given word is safe from the shell's p.o.v.
return value
+def ParseCpuMask(cpu_mask):
+ """Parse a CPU mask definition and return the list of CPU IDs.
+
+ CPU mask format: comma-separated list of CPU IDs
+ or dash-separated ID ranges
+ Example: "0-2,5" -> "0,1,2,5"
+
+ @type cpu_mask: str
+ @param cpu_mask: CPU mask definition
+ @rtype: list of int
+ @return: list of CPU IDs
+
+ """
+ if not cpu_mask:
+ return []
+ cpu_list = []
+ for range_def in cpu_mask.split(","):
+ boundaries = range_def.split("-")
+ n_elements = len(boundaries)
+ if n_elements > 2:
+ raise errors.ParseError("Invalid CPU ID range definition"
+ " (only one hyphen allowed): %s" % range_def)
+ try:
+ lower = int(boundaries[0])
+ except (ValueError, TypeError), err:
+ raise errors.ParseError("Invalid CPU ID value for lower boundary of"
+ " CPU ID range: %s" % str(err))
+ try:
+ higher = int(boundaries[-1])
+ except (ValueError, TypeError), err:
+ raise errors.ParseError("Invalid CPU ID value for higher boundary of"
+ " CPU ID range: %s" % str(err))
+ if lower > higher:
+ raise errors.ParseError("Invalid CPU ID range definition"
+ " (%d > %d): %s" % (lower, higher, range_def))
+ cpu_list.extend(range(lower, higher + 1))
+ return cpu_list
+
+
def AddAuthorizedKey(file_name, key):
"""Adds an SSH public key to an authorized_keys file.
L{constants.ETC_HOSTS}
"""
- hi = HostInfo(name=hostname)
+ hi = netutils.HostInfo(name=hostname)
SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
L{constants.ETC_HOSTS}
"""
- hi = HostInfo(name=hostname)
+ hi = netutils.HostInfo(name=hostname)
RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
return ' '.join([ShellQuote(i) for i in args])
-def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
- """Simple ping implementation using TCP connect(2).
-
- Check if the given IP is reachable by doing attempting a TCP connect
- to it.
-
- @type target: str
- @param target: the IP or hostname to ping
- @type port: int
- @param port: the port to connect to
- @type timeout: int
- @param timeout: the timeout on the connection attempt
- @type live_port_needed: boolean
- @param live_port_needed: whether a closed port will cause the
- function to return failure, as if there was a timeout
- @type source: str or None
- @param source: if specified, will cause the connect to be made
- from this specific source address; failures to bind other
- than C{EADDRNOTAVAIL} will be ignored
+class ShellWriter:
+ """Helper class to write scripts with indentation.
"""
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ INDENT_STR = " "
- success = False
+ def __init__(self, fh):
+ """Initializes this class.
- if source is not None:
- try:
- sock.bind((source, 0))
- except socket.error, (errcode, _):
- if errcode == errno.EADDRNOTAVAIL:
- success = False
+ """
+ self._fh = fh
+ self._indent = 0
- sock.settimeout(timeout)
+ def IncIndent(self):
+ """Increase indentation level by 1.
- try:
- sock.connect((target, port))
- sock.close()
- success = True
- except socket.timeout:
- success = False
- except socket.error, (errcode, _):
- success = (not live_port_needed) and (errcode == errno.ECONNREFUSED)
+ """
+ self._indent += 1
- return success
+ def DecIndent(self):
+ """Decrease indentation level by 1.
+ """
+ assert self._indent > 0
+ self._indent -= 1
-def OwnIpAddress(address):
- """Check if the current host has the the given IP address.
+ def Write(self, txt, *args):
+ """Write line to output file.
- Currently this is done by TCP-pinging the address from the loopback
- address.
+ """
+ assert self._indent >= 0
- @type address: string
- @param address: the address to check
- @rtype: bool
- @return: True if we own the address
+ self._fh.write(self._indent * self.INDENT_STR)
- """
- return TcpPing(address, constants.DEFAULT_NODED_PORT,
- source=constants.LOCALHOST_IP_ADDRESS)
+ if args:
+ self._fh.write(txt % args)
+ else:
+ self._fh.write(txt)
+
+ self._fh.write("\n")
def ListVisibleFiles(path):
raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
" absolute/normalized: '%s'" % path)
files = [i for i in os.listdir(path) if not i.startswith(".")]
- files.sort()
return files
if err.errno != errno.EEXIST:
raise errors.GenericError("Cannot create needed directory"
" '%s': %s" % (dir_name, err))
+ try:
+ os.chmod(dir_name, dir_mode)
+ except EnvironmentError, err:
+ raise errors.GenericError("Cannot change directory permissions on"
+ " '%s': %s" % (dir_name, err))
if not os.path.isdir(dir_name):
raise errors.GenericError("%s is not a directory" % dir_name)
_CloseFDNoErr(fd)
-def Mlockall():
+def Mlockall(_ctypes=ctypes):
"""Lock current process' virtual address space into RAM.
This is equivalent to the C call mlockall(MCL_CURRENT|MCL_FUTURE),
see mlock(2) for more details. This function requires ctypes module.
+ @raises errors.NoCtypesError: if ctypes module is not found
+
"""
- if ctypes is None:
- logging.warning("Cannot set memory lock, ctypes module not found")
- return
+ if _ctypes is None:
+ raise errors.NoCtypesError()
- libc = ctypes.cdll.LoadLibrary("libc.so.6")
+ libc = _ctypes.cdll.LoadLibrary("libc.so.6")
if libc is None:
logging.error("Cannot set memory lock, ctypes cannot load libc")
return
# its value correctly, should the mlockall call fail, in order to see what
# the actual error code was.
# pylint: disable-msg=W0212
- libc.__errno_location.restype = ctypes.POINTER(ctypes.c_int)
+ libc.__errno_location.restype = _ctypes.POINTER(_ctypes.c_int)
if libc.mlockall(_MCL_CURRENT | _MCL_FUTURE):
# pylint: disable-msg=W0212
logging.debug("Memory lock set")
-def Daemonize(logfile):
+def Daemonize(logfile, run_uid, run_gid):
"""Daemonize the current process.
This detaches the current process from the controlling terminal and
@type logfile: str
@param logfile: the logfile to which we should redirect stdout/stderr
+ @type run_uid: int
+ @param run_uid: Run the child under this uid
+ @type run_gid: int
+ @param run_gid: Run the child under this gid
@rtype: int
@return: the value zero
pid = os.fork()
if (pid == 0): # The first child.
os.setsid()
+ # FIXME: When removing again and moving to start-stop-daemon privilege drop
+ # make sure to check for config permission and bail out when invoked
+ # with wrong user.
+ os.setgid(run_gid)
+ os.setuid(run_uid)
# this might fail
pid = os.fork() # Fork a second child.
if (pid == 0): # The second child.
return True
+def StopDaemon(name):
+ """Stop daemon
+
+ """
+ result = RunCmd([constants.DAEMON_UTIL, "stop", name])
+ if result.failed:
+ logging.error("Can't stop daemon '%s', failure %s, output: %s",
+ name, result.fail_reason, result.output)
+ return False
+
+ return True
+
+
def WritePidFile(name):
"""Write the current process pidfile.
"""
def _helper(pid, signal_, wait):
"""Simple helper to encapsulate the kill/waitpid sequence"""
- os.kill(pid, signal_)
- if wait:
+ if IgnoreProcessNotFound(os.kill, pid, signal_) and wait:
try:
os.waitpid(pid, os.WNOHANG)
except OSError:
return float(seconds) + (float(microseconds) * 0.000001)
-def GetDaemonPort(daemon_name):
- """Get the daemon port for this cluster.
-
- Note that this routine does not read a ganeti-specific file, but
- instead uses C{socket.getservbyname} to allow pre-customization of
- this parameter outside of Ganeti.
-
- @type daemon_name: string
- @param daemon_name: daemon name (in constants.DAEMONS_PORTS)
- @rtype: int
-
- """
- if daemon_name not in constants.DAEMONS_PORTS:
- raise errors.ProgrammerError("Unknown daemon: %s" % daemon_name)
-
- (proto, default_port) = constants.DAEMONS_PORTS[daemon_name]
- try:
- port = socket.getservbyname(daemon_name, proto)
- except socket.error:
- port = default_port
-
- return port
-
-
class LogFileHandler(logging.FileHandler):
"""Log handler that doesn't fallback to stderr.
return BytesToMebibyte(size)
+def GetMounts(filename=constants.PROC_MOUNTS):
+ """Returns the list of mounted filesystems.
+
+ This function is Linux-specific.
+
+ @param filename: path of mounts file (/proc/mounts by default)
+ @rtype: list of tuples
+ @return: list of mount entries (device, mountpoint, fstype, options)
+
+ """
+ # TODO(iustin): investigate non-Linux options (e.g. via mount output)
+ data = []
+ mountlines = ReadFile(filename).splitlines()
+ for line in mountlines:
+ device, mountpoint, fstype, options, _ = line.split(None, 4)
+ data.append((device, mountpoint, fstype, options))
+
+ return data
+
+
def GetFilesystemStats(path):
"""Returns the total and free space on a filesystem.
return bool(exitcode)
+def IgnoreProcessNotFound(fn, *args, **kwargs):
+ """Ignores ESRCH when calling a process-related function.
+
+ ESRCH is raised when a process is not found.
+
+ @rtype: bool
+ @return: Whether process was found
+
+ """
+ try:
+ fn(*args, **kwargs)
+ except EnvironmentError, err:
+ # Ignore ESRCH
+ if err.errno == errno.ESRCH:
+ return False
+ raise
+
+ return True
+
+
def IgnoreSignals(fn, *args, **kwargs):
"""Tries to call a function ignoring failures due to EINTR.
try:
return fn(*args, **kwargs)
except EnvironmentError, err:
- if err.errno != errno.EINTR:
+ if err.errno == errno.EINTR:
+ return None
+ else:
raise
except (select.error, socket.error), err:
# In python 2.6 and above select.error is an IOError, so it's handled
# above, in 2.5 and below it's not, and it's handled here.
- if not (err.args and err.args[0] == errno.EINTR):
+ if err.args and err.args[0] == errno.EINTR:
+ return None
+ else:
raise
-def LockedMethod(fn):
- """Synchronized object access decorator.
-
- This decorator is intended to protect access to an object using the
- object's own lock which is hardcoded to '_lock'.
-
- """
- def _LockDebug(*args, **kwargs):
- if debug_locks:
- logging.debug(*args, **kwargs)
-
- def wrapper(self, *args, **kwargs):
- # pylint: disable-msg=W0212
- assert hasattr(self, '_lock')
- lock = self._lock
- _LockDebug("Waiting for %s", lock)
- lock.acquire()
- try:
- _LockDebug("Acquired %s", lock)
- result = fn(self, *args, **kwargs)
- finally:
- _LockDebug("Releasing %s", lock)
- lock.release()
- _LockDebug("Released %s", lock)
- return result
- return wrapper
-
-
def LockFile(fd):
"""Locks a file using POSIX locks.
return time.strftime("%F %T", time.localtime(val))
+def FormatSeconds(secs):
+ """Formats seconds for easier reading.
+
+ @type secs: number
+ @param secs: Number of seconds
+ @rtype: string
+ @return: Formatted seconds (e.g. "2d 9h 19m 49s")
+
+ """
+ parts = []
+
+ secs = round(secs, 0)
+
+ if secs > 0:
+ # Negative values would be a bit tricky
+ for unit, one in [("d", 24 * 60 * 60), ("h", 60 * 60), ("m", 60)]:
+ (complete, secs) = divmod(secs, one)
+ if complete or parts:
+ parts.append("%d%s" % (complete, unit))
+
+ parts.append("%ds" % secs)
+
+ return " ".join(parts)
+
+
def ReadWatcherPauseFile(filename, now=None, remove_after=3600):
"""Reads the watcher pause file.