Add method to update a disk object size
[ganeti-local] / lib / utils.py
index fc7dcdb..fda1f6b 100644 (file)
@@ -20,6 +20,7 @@
 
 
 """Ganeti small utilities
+
 """
 
 
@@ -34,14 +35,25 @@ import tempfile
 import shutil
 import errno
 import pwd
+import itertools
+import select
+import fcntl
+import resource
+
+from cStringIO import StringIO
 
 from ganeti import logger
 from ganeti import errors
+from ganeti import constants
 
 
 _locksheld = []
 _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$')
 
+debug = False
+no_fork = False
+
+
 class RunResult(object):
   """Simple class for holding the result of running external programs.
 
@@ -76,6 +88,10 @@ class RunResult(object):
     else:
       self.fail_reason = "unable to determine termination reason"
 
+    if debug and self.failed:
+      logger.Debug("Command '%s' failed (%s); output: %s" %
+                   (self.cmd, self.fail_reason, self.output))
+
   def _GetOutput(self):
     """Returns the combined stdout and stderr for easier usage.
 
@@ -87,7 +103,7 @@ class RunResult(object):
 
 def _GetLockFile(subsystem):
   """Compute the file name for a given lock name."""
-  return "/var/lock/ganeti_lock_%s" % subsystem
+  return "%s/ganeti_lock_%s" % (constants.LOCK_DIR, subsystem)
 
 
 def Lock(name, max_retries=None, debug=False):
@@ -204,6 +220,9 @@ def RunCmd(cmd):
   Returns: `RunResult` instance
 
   """
+  if no_fork:
+    raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
+
   if isinstance(cmd, list):
     cmd = [str(val) for val in cmd]
     strcmd = " ".join(cmd)
@@ -213,6 +232,7 @@ def RunCmd(cmd):
     shell = True
   env = os.environ.copy()
   env["LC_ALL"] = "C"
+  poller = select.poll()
   child = subprocess.Popen(cmd, shell=shell,
                            stderr=subprocess.PIPE,
                            stdout=subprocess.PIPE,
@@ -220,8 +240,35 @@ def RunCmd(cmd):
                            close_fds=True, env=env)
 
   child.stdin.close()
-  out = child.stdout.read()
-  err = child.stderr.read()
+  poller.register(child.stdout, select.POLLIN)
+  poller.register(child.stderr, select.POLLIN)
+  out = StringIO()
+  err = StringIO()
+  fdmap = {
+    child.stdout.fileno(): (out, child.stdout),
+    child.stderr.fileno(): (err, child.stderr),
+    }
+  for fd in fdmap:
+    status = fcntl.fcntl(fd, fcntl.F_GETFL)
+    fcntl.fcntl(fd, fcntl.F_SETFL, status | os.O_NONBLOCK)
+
+  while fdmap:
+    for fd, event in poller.poll():
+      if event & select.POLLIN or event & select.POLLPRI:
+        data = fdmap[fd][1].read()
+        # no data from read signifies EOF (the same as POLLHUP)
+        if not data:
+          poller.unregister(fd)
+          del fdmap[fd]
+          continue
+        fdmap[fd][0].write(data)
+      if (event & select.POLLNVAL or event & select.POLLHUP or
+          event & select.POLLERR):
+        poller.unregister(fd)
+        del fdmap[fd]
+
+  out = out.getvalue()
+  err = err.getvalue()
 
   status = child.wait()
   if status >= 0:
@@ -416,6 +463,12 @@ class HostInfo:
     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.
@@ -516,59 +569,6 @@ def NiceSort(name_list):
   return [tup[1] for tup in to_sort]
 
 
-def CheckDaemonAlive(pid_file, process_string):
-  """Check wether the specified daemon is alive.
-
-  Args:
-   - pid_file: file to read the daemon pid from, the file is
-               expected to contain only a single line containing
-               only the PID
-   - process_string: a substring that we expect to find in
-                     the command line of the daemon process
-
-  Returns:
-   - True if the daemon is judged to be alive (that is:
-      - the PID file exists, is readable and contains a number
-      - a process of the specified PID is running
-      - that process contains the specified string in its
-        command line
-      - the process is not in state Z (zombie))
-   - False otherwise
-
-  """
-  try:
-    pid_file = file(pid_file, 'r')
-    try:
-      pid = int(pid_file.readline())
-    finally:
-      pid_file.close()
-
-    cmdline_file_path = "/proc/%s/cmdline" % (pid)
-    cmdline_file = open(cmdline_file_path, 'r')
-    try:
-      cmdline = cmdline_file.readline()
-    finally:
-      cmdline_file.close()
-
-    if not process_string in cmdline:
-      return False
-
-    stat_file_path =  "/proc/%s/stat" % (pid)
-    stat_file = open(stat_file_path, 'r')
-    try:
-      process_state = stat_file.readline().split()[2]
-    finally:
-      stat_file.close()
-
-    if process_state == 'Z':
-      return False
-
-  except (IndexError, IOError, ValueError):
-    return False
-
-  return True
-
-
 def TryConvert(fn, val):
   """Try to convert a value ignoring errors.
 
@@ -723,21 +723,115 @@ def RemoveAuthorizedKey(file_name, key):
   key_fields = key.split()
 
   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
-  out = os.fdopen(fd, 'w')
   try:
-    f = open(file_name, 'r')
+    out = os.fdopen(fd, 'w')
     try:
-      for line in f:
-        # Ignore whitespace changes while comparing lines
-        if line.split() != key_fields:
+      f = open(file_name, 'r')
+      try:
+        for line in f:
+          # Ignore whitespace changes while comparing lines
+          if line.split() != key_fields:
+            out.write(line)
+
+        out.flush()
+        os.rename(tmpname, file_name)
+      finally:
+        f.close()
+    finally:
+      out.close()
+  except:
+    RemoveFile(tmpname)
+    raise
+
+
+def SetEtcHostsEntry(file_name, ip, hostname, aliases):
+  """Sets the name of an IP address and hostname in /etc/hosts.
+
+  """
+  # Ensure aliases are unique
+  aliases = UniqueSequence([hostname] + aliases)[1:]
+
+  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
+  try:
+    out = os.fdopen(fd, 'w')
+    try:
+      f = open(file_name, 'r')
+      try:
+        written = False
+        for line in f:
+          fields = line.split()
+          if fields and not fields[0].startswith('#') and ip == fields[0]:
+            continue
           out.write(line)
 
-      out.flush()
-      os.rename(tmpname, file_name)
+        out.write("%s\t%s" % (ip, hostname))
+        if aliases:
+          out.write(" %s" % ' '.join(aliases))
+        out.write('\n')
+
+        out.flush()
+        os.fsync(out)
+        os.rename(tmpname, file_name)
+      finally:
+        f.close()
     finally:
-      f.close()
-  finally:
-    out.close()
+      out.close()
+  except:
+    RemoveFile(tmpname)
+    raise
+
+
+def AddHostToEtcHosts(hostname):
+  """Wrapper around SetEtcHostsEntry.
+
+  """
+  hi = HostInfo(name=hostname)
+  SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
+
+
+def RemoveEtcHostsEntry(file_name, hostname):
+  """Removes a hostname from /etc/hosts.
+
+  IP addresses without names are removed from the file.
+  """
+  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
+  try:
+    out = os.fdopen(fd, 'w')
+    try:
+      f = open(file_name, 'r')
+      try:
+        for line in f:
+          fields = line.split()
+          if len(fields) > 1 and not fields[0].startswith('#'):
+            names = fields[1:]
+            if hostname in names:
+              while hostname in names:
+                names.remove(hostname)
+              if names:
+                out.write("%s %s\n" % (fields[0], ' '.join(names)))
+              continue
+
+          out.write(line)
+
+        out.flush()
+        os.fsync(out)
+        os.rename(tmpname, file_name)
+      finally:
+        f.close()
+    finally:
+      out.close()
+  except:
+    RemoveFile(tmpname)
+    raise
+
+
+def RemoveHostFromEtcHosts(hostname):
+  """Wrapper around RemoveEtcHostsEntry.
+
+  """
+  hi = HostInfo(name=hostname)
+  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
+  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
 
 
 def CreateBackup(file_name):
@@ -751,11 +845,11 @@ def CreateBackup(file_name):
                                 file_name)
 
   prefix = '%s.backup-%d.' % (os.path.basename(file_name), int(time.time()))
-  dir = os.path.dirname(file_name)
+  dir_name = os.path.dirname(file_name)
 
   fsrc = open(file_name, 'rb')
   try:
-    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir)
+    (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name)
     fdst = os.fdopen(fd, 'wb')
     try:
       shutil.copyfileobj(fsrc, fdst)
@@ -784,25 +878,29 @@ def ShellQuoteArgs(args):
   return ' '.join([ShellQuote(i) for i in args])
 
 
-
-def TcpPing(source, target, port, timeout=10, live_port_needed=False):
+def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
   """Simple ping implementation using TCP connect(2).
 
-  Try to do a TCP connect(2) from the specified source IP to the specified
-  target IP and the specified target port. If live_port_needed is set to true,
-  requires the remote end to accept the connection. The timeout is specified
-  in seconds and defaults to 10 seconds
+  Try to do a TCP connect(2) from an optional source IP to the
+  specified target IP and the specified target port. If the optional
+  parameter live_port_needed is set to true, requires the remote end
+  to accept the connection. The timeout is specified in seconds and
+  defaults to 10 seconds. If the source optional argument is not
+  passed, the source address selection is left to the kernel,
+  otherwise we try to connect using the passed address (failures to
+  bind other than EADDRNOTAVAIL will be ignored).
 
   """
   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
   sucess = False
 
-  try:
-    sock.bind((source, 0))
-  except socket.error, (errcode, errstring):
-    if errcode == errno.EADDRNOTAVAIL:
-      success = False
+  if source is not None:
+    try:
+      sock.bind((source, 0))
+    except socket.error, (errcode, errstring):
+      if errcode == errno.EADDRNOTAVAIL:
+        success = False
 
   sock.settimeout(timeout)
 
@@ -822,21 +920,33 @@ def ListVisibleFiles(path):
   """Returns a list of all visible files in a directory.
 
   """
-  return [i for i in os.listdir(path) if not i.startswith(".")]
+  files = [i for i in os.listdir(path) if not i.startswith(".")]
+  files.sort()
+  return files
 
 
-def GetHomeDir(uid, default=None):
-  """Try to get the homedir of the given user id.
+def GetHomeDir(user, default=None):
+  """Try to get the homedir of the given user.
+
+  The user can be passed either as a string (denoting the name) or as
+  an integer (denoting the user id). If the user is not found, the
+  'default' argument is returned, which defaults to None.
 
   """
   try:
-    result = pwd.getpwuid(uid)
+    if isinstance(user, basestring):
+      result = pwd.getpwnam(user)
+    elif isinstance(user, (int, long)):
+      result = pwd.getpwuid(user)
+    else:
+      raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
+                                   type(user))
   except KeyError:
     return default
   return result.pw_dir
 
 
-def GetUUID():
+def NewUUID():
   """Returns a random UUID.
 
   """
@@ -845,3 +955,194 @@ def GetUUID():
     return f.read(128).rstrip("\n")
   finally:
     f.close()
+
+
+def WriteFile(file_name, fn=None, data=None,
+              mode=None, uid=-1, gid=-1,
+              atime=None, mtime=None,
+              check_abspath=True, dry_run=False, backup=False):
+  """(Over)write a file atomically.
+
+  The file_name and either fn (a function taking one argument, the
+  file descriptor, and which should write the data to it) or data (the
+  contents of the file) must be passed. The other arguments are
+  optional and allow setting the file mode, owner and group, and the
+  mtime/atime of the file.
+
+  If the function doesn't raise an exception, it has succeeded and the
+  target file has the new contents. If the file has raised an
+  exception, an existing target file should be unmodified and the
+  temporary file should be removed.
+
+  """
+  if check_abspath and not os.path.isabs(file_name):
+    raise errors.ProgrammerError("Path passed to WriteFile is not"
+                                 " absolute: '%s'" % file_name)
+
+  if [fn, data].count(None) != 1:
+    raise errors.ProgrammerError("fn or data required")
+
+  if [atime, mtime].count(None) == 1:
+    raise errors.ProgrammerError("Both atime and mtime must be either"
+                                 " set or None")
+
+  if backup and not dry_run and os.path.isfile(file_name):
+    CreateBackup(file_name)
+
+  dir_name, base_name = os.path.split(file_name)
+  fd, new_name = tempfile.mkstemp('.new', base_name, dir_name)
+  # here we need to make sure we remove the temp file, if any error
+  # leaves it in place
+  try:
+    if uid != -1 or gid != -1:
+      os.chown(new_name, uid, gid)
+    if mode:
+      os.chmod(new_name, mode)
+    if data is not None:
+      os.write(fd, data)
+    else:
+      fn(fd)
+    os.fsync(fd)
+    if atime is not None and mtime is not None:
+      os.utime(new_name, (atime, mtime))
+    if not dry_run:
+      os.rename(new_name, file_name)
+  finally:
+    os.close(fd)
+    RemoveFile(new_name)
+
+
+def all(seq, pred=bool):
+  "Returns True if pred(x) is True for every element in the iterable"
+  for elem in itertools.ifilterfalse(pred, seq):
+    return False
+  return True
+
+
+def any(seq, pred=bool):
+  "Returns True if pred(x) is True for at least one element in the iterable"
+  for elem in itertools.ifilter(pred, seq):
+    return True
+  return False
+
+
+def UniqueSequence(seq):
+  """Returns a list with unique elements.
+
+  Element order is preserved.
+  """
+  seen = set()
+  return [i for i in seq if i not in seen and not seen.add(i)]
+
+
+def IsValidMac(mac):
+  """Predicate to check if a MAC address is valid.
+
+  Checks wether the supplied MAC address is formally correct, only
+  accepts colon separated format.
+  """
+  mac_check = re.compile("^([0-9a-f]{2}(:|$)){6}$")
+  return mac_check.match(mac) is not None
+
+
+def TestDelay(duration):
+  """Sleep for a fixed amount of time.
+
+  """
+  if duration < 0:
+    return False
+  time.sleep(duration)
+  return True
+
+
+def Daemonize(logfile, noclose_fds=None):
+  """Daemonize the current process.
+
+  This detaches the current process from the controlling terminal and
+  runs it in the background as a daemon.
+
+  """
+  UMASK = 077
+  WORKDIR = "/"
+  # Default maximum for the number of available file descriptors.
+  if 'SC_OPEN_MAX' in os.sysconf_names:
+    try:
+      MAXFD = os.sysconf('SC_OPEN_MAX')
+      if MAXFD < 0:
+        MAXFD = 1024
+    except OSError:
+      MAXFD = 1024
+  else:
+    MAXFD = 1024
+
+  # this might fail
+  pid = os.fork()
+  if (pid == 0):  # The first child.
+    os.setsid()
+    # this might fail
+    pid = os.fork() # Fork a second child.
+    if (pid == 0):  # The second child.
+      os.chdir(WORKDIR)
+      os.umask(UMASK)
+    else:
+      # exit() or _exit()?  See below.
+      os._exit(0) # Exit parent (the first child) of the second child.
+  else:
+    os._exit(0) # Exit parent of the first child.
+  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+  if (maxfd == resource.RLIM_INFINITY):
+    maxfd = MAXFD
+
+  # Iterate through and close all file descriptors.
+  for fd in range(0, maxfd):
+    if noclose_fds and fd in noclose_fds:
+      continue
+    try:
+      os.close(fd)
+    except OSError: # ERROR, fd wasn't open to begin with (ignored)
+      pass
+  os.open(logfile, os.O_RDWR|os.O_CREAT|os.O_APPEND, 0600)
+  # Duplicate standard input to standard output and standard error.
+  os.dup2(0, 1)     # standard output (1)
+  os.dup2(0, 2)     # standard error (2)
+  return 0
+
+
+def FindFile(name, search_path, test=os.path.exists):
+  """Look for a filesystem object in a given path.
+
+  This is an abstract method to search for filesystem object (files,
+  dirs) under a given search path.
+
+  Args:
+    - name: the name to look for
+    - search_path: list of directory names
+    - test: the test which the full path must satisfy
+      (defaults to os.path.exists)
+
+  Returns:
+    - full path to the item if found
+    - None otherwise
+
+  """
+  for dir_name in search_path:
+    item_name = os.path.sep.join([dir_name, name])
+    if test(item_name):
+      return item_name
+  return None
+
+
+def CheckVolumeGroupSize(vglist, vgname, minsize):
+  """Checks if the volume group list is valid.
+
+  A non-None return value means there's an error, and the return value
+  is the error message.
+
+  """
+  vgsize = vglist.get(vgname, None)
+  if vgsize is None:
+    return "volume group '%s' missing" % vgname
+  elif vgsize < minsize:
+    return ("volume group '%s' too small (%s MiB required, %d MiB found)" %
+            (vgname, minsize, vgsize))
+  return None