Add ParseCpuMask() utility function
[ganeti-local] / lib / utils.py
index 8972b2e..f846f3b 100644 (file)
@@ -49,8 +49,6 @@ import datetime
 import calendar
 import hmac
 import collections
-import struct
-import IN
 
 from cStringIO import StringIO
 
@@ -63,6 +61,7 @@ except ImportError:
 from ganeti import errors
 from ganeti import constants
 from ganeti import compat
+from ganeti import netutils
 
 
 _locksheld = []
@@ -84,18 +83,6 @@ X509_SIGNATURE = re.compile(r"^%s:\s*(?P<salt>%s+)/(?P<sign>%s+)$" %
 
 _VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
 
-# 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)
-
 # Certificate verification results
 (CERT_WARNING,
  CERT_ERROR) = range(1, 3)
@@ -616,19 +603,6 @@ def RunParts(dir_name, env=None, reset_env=False):
   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.
 
@@ -1080,85 +1054,6 @@ def MatchNameComponent(key, name_list, case_sensitive=True):
   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, socket.herror, socket.error), err:
-      # hostname not found in DNS, or other socket exception in the
-      # (code, description format)
-      raise errors.ResolverError(hostname, err.args[0], err.args[1])
-
-    return result
-
-  @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
-
-    """
-    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 ValidateServiceName(name):
   """Validate the given service name.
 
@@ -1183,16 +1078,6 @@ def ValidateServiceName(name):
   return name
 
 
-def GetHostInfo(name=None):
-  """Lookup host name and raise an OpPrereqError for failures"""
-
-  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)
-
-
 def ListVolumeGroups():
   """List volume groups and their size
 
@@ -1292,84 +1177,6 @@ def TryConvert(fn, val):
   return nv
 
 
-def _GenericIsValidIP(family, ip):
-  """Generic internal version of ip validation.
-
-  @type family: int
-  @param family: socket.AF_INET | socket.AF_INET6
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  try:
-    socket.inet_pton(family, ip)
-    return True
-  except socket.error:
-    return False
-
-
-def IsValidIP4(ip):
-  """Verifies an IPv4 address.
-
-  This function checks if the given address is a valid IPv4 address.
-
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  return _GenericIsValidIP(socket.AF_INET, ip)
-
-
-def IsValidIP6(ip):
-  """Verifies an IPv6 address.
-
-  This function checks if the given address is a valid IPv6 address.
-
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  return _GenericIsValidIP(socket.AF_INET6, ip)
-
-
-def IsValidIP(ip):
-  """Verifies an IP address.
-
-  This function checks if the given IP address (both IPv4 and IPv6) is valid.
-
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  return IsValidIP4(ip) or IsValidIP6(ip)
-
-
-def GetAddressFamily(ip):
-  """Get the address family of the given address.
-
-  @type ip: str
-  @param ip: ip address whose family will be returned
-  @rtype: int
-  @return: socket.AF_INET or socket.AF_INET6
-  @raise errors.GenericError: for invalid addresses
-
-  """
-  if IsValidIP6(ip):
-    return socket.AF_INET6
-  elif IsValidIP4(ip):
-    return socket.AF_INET
-  else:
-    raise errors.GenericError("Address %s not valid" % ip)
-
-
 def IsValidShellParam(word):
   """Verifies is the given word is safe from the shell's p.o.v.
 
@@ -1492,6 +1299,45 @@ def ParseUnit(input_string):
   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.
 
@@ -1609,7 +1455,7 @@ def AddHostToEtcHosts(hostname):
       L{constants.ETC_HOSTS}
 
   """
-  hi = HostInfo(name=hostname)
+  hi = netutils.HostInfo(name=hostname)
   SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
 
 
@@ -1666,7 +1512,7 @@ def RemoveHostFromEtcHosts(hostname):
       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())
 
@@ -1741,80 +1587,46 @@ def ShellQuoteArgs(args):
   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.
 
   """
-  try:
-    family = GetAddressFamily(target)
-  except errors.GenericError:
-    return False
+  INDENT_STR = "  "
 
-  sock = socket.socket(family, socket.SOCK_STREAM)
-  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.
 
-  This is done by trying to bind the given address. We return True if we
-  succeed or false if a socket.error is raised.
+    """
+    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)
 
-  """
-  family = GetAddressFamily(address)
-  s = socket.socket(family, socket.SOCK_DGRAM)
-  success = False
-  try:
-    try:
-      s.bind((address, 0))
-      success = True
-    except socket.error:
-      success = False
-  finally:
-    s.close()
-  return success
+    if args:
+      self._fh.write(txt % args)
+    else:
+      self._fh.write(txt)
+
+    self._fh.write("\n")
 
 
 def ListVisibleFiles(path):
@@ -2260,18 +2072,19 @@ def CloseFDs(noclose_fds=None):
     _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
@@ -2282,7 +2095,7 @@ def Mlockall():
   # 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
@@ -2578,30 +2391,6 @@ def MergeTime(timetuple):
   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.
 
@@ -3156,6 +2945,26 @@ def CalculateDirectorySize(path):
   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.