make gnt-modify work with new HVM parameters
[ganeti-local] / lib / utils.py
index a71cd1f..9a5613f 100644 (file)
@@ -36,14 +36,22 @@ 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
+
 class RunResult(object):
   """Simple class for holding the result of running external programs.
 
@@ -78,6 +86,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.
 
@@ -89,10 +101,10 @@ 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):
+def Lock(name, max_retries=None, debug=False, autoclean=True):
   """Lock a given subsystem.
 
   In case the lock is already held by an alive process, the function
@@ -112,6 +124,7 @@ def Lock(name, max_retries=None, debug=False):
     raise errors.LockError('Lock "%s" already held!' % (name,))
 
   errcount = 0
+  cleanupcount = 0
 
   retries = 0
   while True:
@@ -140,8 +153,18 @@ def Lock(name, max_retries=None, debug=False):
                                (lockfile,))
 
       if not IsProcessAlive(pid):
-        raise errors.LockError("Stale lockfile %s for pid %d?" %
-                               (lockfile, pid))
+        if autoclean:
+          cleanupcount += 1
+          if cleanupcount >= 5:
+            raise errors.LockError, ("Too many stale lock cleanups! Check"
+                                     " what process is dying.")
+          logger.Error('Stale lockfile %s for pid %d, autocleaned.' %
+                       (lockfile, pid))
+          RemoveFile(lockfile)
+          continue
+        else:
+          raise errors.LockError("Stale lockfile %s for pid %d?" %
+                                 (lockfile, pid))
 
       if max_retries and max_retries <= retries:
         raise errors.LockError("Can't acquire lock during the specified"
@@ -215,6 +238,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,
@@ -222,8 +246,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:
@@ -752,36 +803,47 @@ def RemoveAuthorizedKey(file_name, key):
     raise
 
 
-def AddEtcHostsEntry(file_name, hostname, ip):
-  """Adds an IP address and hostname to /etc/hosts.
+def SetEtcHostsEntry(file_name, ip, hostname, aliases):
+  """Sets the name of an IP address and hostname in /etc/hosts.
 
   """
-  f = open(file_name, 'a+')
+  # Ensure aliases are unique
+  aliases = UniqueSequence([hostname] + aliases)[1:]
+
+  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
   try:
-    nl = True
-    for line in f:
-      fields = line.split()
-      if len(fields) < 2 or fields[0].startswith('#'):
-        continue
-      if fields[0] == ip and hostname in fields[1:]:
-        break
-      nl = line.endswith('\n')
-    else:
-      if not nl:
-        f.write("\n")
-      f.write(ip)
-      f.write(' ')
-      f.write(hostname)
-      f.write("\n")
-      f.flush()
-  finally:
-    f.close()
+    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.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:
+      out.close()
+  except:
+    RemoveFile(tmpname)
+    raise
 
 
 def RemoveEtcHostsEntry(file_name, hostname):
   """Removes a hostname from /etc/hosts.
 
-  IP addresses without hostnames are removed from the file.
+  IP addresses without names are removed from the file.
   """
   fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
   try:
@@ -797,14 +859,13 @@ def RemoveEtcHostsEntry(file_name, hostname):
               while hostname in names:
                 names.remove(hostname)
               if names:
-                out.write(fields[0])
-                out.write(' ')
-                out.write(' '.join(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()
@@ -859,25 +920,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)
 
@@ -897,7 +962,9 @@ 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(user, default=None):
@@ -995,3 +1062,109 @@ def any(seq, pred=bool):
   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