X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/8f7650695fbfd198b4128efee726f07ae6bd8ac5..eb0f0ce076ff4a47fa3cb3c61efa70a4d9181b9a:/lib/utils.py diff --git a/lib/utils.py b/lib/utils.py index 509f8be..ceacc9b 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -39,10 +39,10 @@ import itertools import select import fcntl import resource +import logging from cStringIO import StringIO -from ganeti import logger from ganeti import errors from ganeti import constants @@ -51,6 +51,7 @@ _locksheld = [] _re_shell_unquoted = re.compile('^[-.,=:/_+@A-Za-z0-9]+$') debug = False +no_fork = False class RunResult(object): @@ -87,9 +88,9 @@ 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)) + if self.failed: + logging.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. @@ -100,113 +101,6 @@ class RunResult(object): output = property(_GetOutput, None, None, "Return full output") -def _GetLockFile(subsystem): - """Compute the file name for a given lock name.""" - return "%s/ganeti_lock_%s" % (constants.LOCK_DIR, subsystem) - - -def Lock(name, max_retries=None, debug=False): - """Lock a given subsystem. - - In case the lock is already held by an alive process, the function - will sleep indefintely and poll with a one second interval. - - When the optional integer argument 'max_retries' is passed with a - non-zero value, the function will sleep only for this number of - times, and then it will will raise a LockError if the lock can't be - acquired. Passing in a negative number will cause only one try to - get the lock. Passing a positive number will make the function retry - for approximately that number of seconds. - - """ - lockfile = _GetLockFile(name) - - if name in _locksheld: - raise errors.LockError('Lock "%s" already held!' % (name,)) - - errcount = 0 - - retries = 0 - while True: - try: - fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_SYNC) - break - except OSError, creat_err: - if creat_err.errno != errno.EEXIST: - raise errors.LockError("Can't create the lock file. Error '%s'." % - str(creat_err)) - - try: - pf = open(lockfile, 'r') - except IOError, open_err: - errcount += 1 - if errcount >= 5: - raise errors.LockError("Lock file exists but cannot be opened." - " Error: '%s'." % str(open_err)) - time.sleep(1) - continue - - try: - pid = int(pf.read()) - except ValueError: - raise errors.LockError("Invalid pid string in %s" % - (lockfile,)) - - if not IsProcessAlive(pid): - 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" - " time, aborting.") - if retries == 5 and (debug or sys.stdin.isatty()): - logger.ToStderr("Waiting for '%s' lock from pid %d..." % (name, pid)) - - time.sleep(1) - retries += 1 - continue - - os.write(fd, '%d\n' % (os.getpid(),)) - os.close(fd) - - _locksheld.append(name) - - -def Unlock(name): - """Unlock a given subsystem. - - """ - lockfile = _GetLockFile(name) - - try: - fd = os.open(lockfile, os.O_RDONLY) - except OSError: - raise errors.LockError('Lock "%s" not held.' % (name,)) - - f = os.fdopen(fd, 'r') - pid_str = f.read() - - try: - pid = int(pid_str) - except ValueError: - raise errors.LockError('Unable to determine PID of locking process.') - - if pid != os.getpid(): - raise errors.LockError('Lock not held by me (%d != %d)' % - (os.getpid(), pid,)) - - os.unlink(lockfile) - _locksheld.remove(name) - - -def LockCleanup(): - """Remove all locks. - - """ - for lock in _locksheld: - Unlock(lock) - - def RunCmd(cmd): """Execute a (shell) command. @@ -219,6 +113,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) @@ -226,6 +123,7 @@ def RunCmd(cmd): else: strcmd = cmd shell = True + logging.debug("RunCmd '%s'", strcmd) env = os.environ.copy() env["LC_ALL"] = "C" poller = select.poll() @@ -277,30 +175,6 @@ def RunCmd(cmd): return RunResult(exitcode, signal, out, err, strcmd) -def RunCmdUnlocked(cmd): - """Execute a shell command without the 'cmd' lock. - - This variant of `RunCmd()` drops the 'cmd' lock before running the - command and re-aquires it afterwards, thus it can be used to call - other ganeti commands. - - The argument and return values are the same as for the `RunCmd()` - function. - - Args: - cmd - command to run. (str) - - Returns: - `RunResult` - - """ - Unlock('cmd') - ret = RunCmd(cmd) - Lock('cmd') - - return ret - - def RemoveFile(filename): """Remove a file ignoring some errors. @@ -385,8 +259,7 @@ def CheckDict(target, template, logname=None): target[k] = template[k] if missing and logname: - logger.Debug('%s missing keys %s' % - (logname, ', '.join(missing))) + logging.warning('%s missing keys %s', logname, ', '.join(missing)) def IsProcessAlive(pid): @@ -513,7 +386,7 @@ def ListVolumeGroups(): name, size = line.split() size = int(float(size)) except (IndexError, ValueError), err: - logger.Error("Invalid output from vgs (%s): %s" % (err, line)) + logging.error("Invalid output from vgs (%s): %s", err, line) continue retval[name] = size @@ -565,59 +438,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. @@ -830,6 +650,14 @@ def SetEtcHostsEntry(file_name, ip, hostname, aliases): 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. @@ -866,6 +694,15 @@ def RemoveEtcHostsEntry(file_name, hostname): 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): """Creates a backup of a file. @@ -991,7 +828,9 @@ def NewUUID(): def WriteFile(file_name, fn=None, data=None, mode=None, uid=-1, gid=-1, - atime=None, mtime=None): + atime=None, mtime=None, close=True, + dry_run=False, backup=False, + prewrite=None, postwrite=None): """(Over)write a file atomically. The file_name and either fn (a function taking one argument, the @@ -1005,6 +844,22 @@ def WriteFile(file_name, fn=None, data=None, exception, an existing target file should be unmodified and the temporary file should be removed. + Args: + file_name: New filename + fn: Content writing function, called with file descriptor as parameter + data: Content as string + mode: File mode + uid: Owner + gid: Group + atime: Access time + mtime: Modification time + close: Whether to close file after writing it + prewrite: Function object called before writing content + postwrite: Function object called after writing content + + Returns: + None if "close" parameter evaluates to True, otherwise file descriptor. + """ if not os.path.isabs(file_name): raise errors.ProgrammerError("Path passed to WriteFile is not" @@ -1017,6 +872,8 @@ def WriteFile(file_name, fn=None, data=None, 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) @@ -1027,18 +884,50 @@ def WriteFile(file_name, fn=None, data=None, os.chown(new_name, uid, gid) if mode: os.chmod(new_name, mode) + if callable(prewrite): + prewrite(fd) if data is not None: os.write(fd, data) else: fn(fd) + if callable(postwrite): + postwrite(fd) os.fsync(fd) if atime is not None and mtime is not None: os.utime(new_name, (atime, mtime)) - os.rename(new_name, file_name) + if not dry_run: + os.rename(new_name, file_name) finally: - os.close(fd) + if close: + os.close(fd) + result = None + else: + result = fd RemoveFile(new_name) + return result + + +def FirstFree(seq, base=0): + """Returns the first non-existing integer from seq. + + The seq argument should be a sorted list of positive integers. The + first time the index of an element is smaller than the element + value, the index will be returned. + + The base argument is used to start at a different offset, + i.e. [3, 4, 6] with offset=3 will return 5. + + Example: [0, 1, 3] will return 2. + + """ + for idx, elem in enumerate(seq): + assert elem >= base, "Passed element is higher than base offset" + if elem > idx + base: + # idx is not used + return idx + base + return None + def all(seq, pred=bool): "Returns True if pred(x) is True for every element in the iterable" @@ -1083,7 +972,7 @@ def TestDelay(duration): return True -def Daemonize(logfile): +def Daemonize(logfile, noclose_fds=None): """Daemonize the current process. This detaches the current process from the controlling terminal and @@ -1123,6 +1012,8 @@ def Daemonize(logfile): # 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) @@ -1132,3 +1023,74 @@ def Daemonize(logfile): 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 + + +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 wrapper(self, *args, **kwargs): + assert hasattr(self, '_lock') + lock = self._lock + lock.acquire() + try: + result = fn(self, *args, **kwargs) + finally: + lock.release() + return result + return wrapper + + +def LockFile(fd): + """Locks a file using POSIX locks. + + """ + try: + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError, err: + if err.errno == errno.EAGAIN: + raise errors.LockError("File already locked") + raise