From 3865ca483fad871b93711962d86a5abe653fb316 Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Mon, 10 Jan 2011 20:44:41 +0100 Subject: [PATCH] utils: Move I/O-related code into separate file Signed-off-by: Michael Hanselmann <hansmi@google.com> Reviewed-by: Iustin Pop <iustin@google.com> --- Makefile.am | 2 + lib/backend.py | 4 +- lib/utils/__init__.py | 729 +---------------------------- lib/utils/io.py | 762 +++++++++++++++++++++++++++++++ test/ganeti.utils.io_unittest.py | 694 ++++++++++++++++++++++++++++ test/ganeti.utils_unittest.py | 682 +-------------------------- 6 files changed, 1473 insertions(+), 1400 deletions(-) create mode 100644 lib/utils/io.py create mode 100755 test/ganeti.utils.io_unittest.py diff --git a/Makefile.am b/Makefile.am index 7c44969db..9675addf6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -216,6 +216,7 @@ utils_PYTHON = \ lib/utils/algo.py \ lib/utils/filelock.py \ lib/utils/hash.py \ + lib/utils/io.py \ lib/utils/log.py \ lib/utils/mlock.py \ lib/utils/retry.py \ @@ -489,6 +490,7 @@ python_tests = \ test/ganeti.utils.algo_unittest.py \ test/ganeti.utils.filelock_unittest.py \ test/ganeti.utils.hash_unittest.py \ + test/ganeti.utils.io_unittest.py \ test/ganeti.utils.mlock_unittest.py \ test/ganeti.utils.retry_unittest.py \ test/ganeti.utils.text_unittest.py \ diff --git a/lib/backend.py b/lib/backend.py index c79dd9eda..36a44b303 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -2512,7 +2512,7 @@ def _EnsureJobQueueFile(file_name): def JobQueueUpdate(file_name, content): """Updates a file in the queue directory. - This is just a wrapper over L{utils.WriteFile}, with proper + This is just a wrapper over L{utils.io.WriteFile}, with proper checking. @type file_name: str @@ -3399,7 +3399,7 @@ class DevCacheManager(object): def RemoveCache(cls, dev_path): """Remove data for a dev_path. - This is just a wrapper over L{utils.RemoveFile} with a converted + This is just a wrapper over L{utils.io.RemoveFile} with a converted path name and logging. @type dev_path: str diff --git a/lib/utils/__init__.py b/lib/utils/__init__.py index a7b603341..f13cca359 100644 --- a/lib/utils/__init__.py +++ b/lib/utils/__init__.py @@ -61,6 +61,7 @@ from ganeti.utils.log import * # pylint: disable-msg=W0401 from ganeti.utils.hash import * # pylint: disable-msg=W0401 from ganeti.utils.wrapper import * # pylint: disable-msg=W0401 from ganeti.utils.filelock import * # pylint: disable-msg=W0401 +from ganeti.utils.io import * # pylint: disable-msg=W0401 #: when set to True, L{RunCmd} is disabled @@ -701,85 +702,6 @@ def RunParts(dir_name, env=None, reset_env=False): return rr -def RemoveFile(filename): - """Remove a file ignoring some errors. - - Remove a file, ignoring non-existing ones or directories. Other - errors are passed. - - @type filename: str - @param filename: the file to be removed - - """ - try: - os.unlink(filename) - except OSError, err: - if err.errno not in (errno.ENOENT, errno.EISDIR): - raise - - -def RemoveDir(dirname): - """Remove an empty directory. - - Remove a directory, ignoring non-existing ones. - Other errors are passed. This includes the case, - where the directory is not empty, so it can't be removed. - - @type dirname: str - @param dirname: the empty directory to be removed - - """ - try: - os.rmdir(dirname) - except OSError, err: - if err.errno != errno.ENOENT: - raise - - -def RenameFile(old, new, mkdir=False, mkdir_mode=0750): - """Renames a file. - - @type old: string - @param old: Original path - @type new: string - @param new: New path - @type mkdir: bool - @param mkdir: Whether to create target directory if it doesn't exist - @type mkdir_mode: int - @param mkdir_mode: Mode for newly created directories - - """ - try: - return os.rename(old, new) - except OSError, err: - # In at least one use case of this function, the job queue, directory - # creation is very rare. Checking for the directory before renaming is not - # as efficient. - if mkdir and err.errno == errno.ENOENT: - # Create directory and try again - Makedirs(os.path.dirname(new), mode=mkdir_mode) - - return os.rename(old, new) - - raise - - -def Makedirs(path, mode=0750): - """Super-mkdir; create a leaf directory and all intermediate ones. - - This is a wrapper around C{os.makedirs} adding error handling not implemented - before Python 2.5. - - """ - try: - os.makedirs(path, mode) - except OSError, err: - # Ignore EEXIST. This is only handled in os.makedirs as included in - # Python 2.5 and above. - if err.errno != errno.EEXIST or not os.path.exists(path): - raise - - def ResetTempfileModule(): """Resets the random name generator of the tempfile module. @@ -1002,63 +924,6 @@ def IsProcessHandlingSignal(pid, signum, status_path=None): return signum in _ParseSigsetT(sigcgt) -def ReadPidFile(pidfile): - """Read a pid from a file. - - @type pidfile: string - @param pidfile: path to the file containing the pid - @rtype: int - @return: The process id, if the file exists and contains a valid PID, - otherwise 0 - - """ - try: - raw_data = ReadOneLineFile(pidfile) - except EnvironmentError, err: - if err.errno != errno.ENOENT: - logging.exception("Can't read pid file") - return 0 - - try: - pid = int(raw_data) - except (TypeError, ValueError), err: - logging.info("Can't parse pid file contents", exc_info=True) - return 0 - - return pid - - -def ReadLockedPidFile(path): - """Reads a locked PID file. - - This can be used together with L{StartDaemon}. - - @type path: string - @param path: Path to PID file - @return: PID as integer or, if file was unlocked or couldn't be opened, None - - """ - try: - fd = os.open(path, os.O_RDONLY) - except EnvironmentError, err: - if err.errno == errno.ENOENT: - # PID file doesn't exist - return None - raise - - try: - try: - # Try to acquire lock - LockFile(fd) - except errors.LockError: - # Couldn't lock, daemon is running - return int(os.read(fd, 100)) - finally: - os.close(fd) - - return None - - def ValidateServiceName(name): """Validate the given service name. @@ -1225,72 +1090,6 @@ def ParseCpuMask(cpu_mask): return cpu_list -def AddAuthorizedKey(file_obj, key): - """Adds an SSH public key to an authorized_keys file. - - @type file_obj: str or file handle - @param file_obj: path to authorized_keys file - @type key: str - @param key: string containing key - - """ - key_fields = key.split() - - if isinstance(file_obj, basestring): - f = open(file_obj, 'a+') - else: - f = file_obj - - try: - nl = True - for line in f: - # Ignore whitespace changes - if line.split() == key_fields: - break - nl = line.endswith('\n') - else: - if not nl: - f.write("\n") - f.write(key.rstrip('\r\n')) - f.write("\n") - f.flush() - finally: - f.close() - - -def RemoveAuthorizedKey(file_name, key): - """Removes an SSH public key from an authorized_keys file. - - @type file_name: str - @param file_name: path to authorized_keys file - @type key: str - @param key: string containing key - - """ - key_fields = key.split() - - 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: - # 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. @@ -1391,66 +1190,6 @@ def RemoveHostFromEtcHosts(hostname): RemoveEtcHostsEntry(constants.ETC_HOSTS, hostname.split(".")[0]) -def TimestampForFilename(): - """Returns the current time formatted for filenames. - - The format doesn't contain colons as some shells and applications treat them - as separators. Uses the local timezone. - - """ - return time.strftime("%Y-%m-%d_%H_%M_%S") - - -def CreateBackup(file_name): - """Creates a backup of a file. - - @type file_name: str - @param file_name: file to be backed up - @rtype: str - @return: the path to the newly created backup - @raise errors.ProgrammerError: for invalid file names - - """ - if not os.path.isfile(file_name): - raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" % - file_name) - - prefix = ("%s.backup-%s." % - (os.path.basename(file_name), TimestampForFilename())) - dir_name = os.path.dirname(file_name) - - fsrc = open(file_name, 'rb') - try: - (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name) - fdst = os.fdopen(fd, 'wb') - try: - logging.debug("Backing up %s at %s", file_name, backup_name) - shutil.copyfileobj(fsrc, fdst) - finally: - fdst.close() - finally: - fsrc.close() - - return backup_name - - -def ListVisibleFiles(path): - """Returns a list of visible files in a directory. - - @type path: str - @param path: the directory to enumerate - @rtype: list - @return: the list of all files not starting with a dot - @raise ProgrammerError: if L{path} is not an absolue and normalized path - - """ - if not IsNormAbsPath(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(".")] - return files - - def GetHomeDir(user, default=None): """Try to get the homedir of the given user. @@ -1483,227 +1222,6 @@ def NewUUID(): return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n") -def EnsureDirs(dirs): - """Make required directories, if they don't exist. - - @param dirs: list of tuples (dir_name, dir_mode) - @type dirs: list of (string, integer) - - """ - for dir_name, dir_mode in dirs: - try: - os.mkdir(dir_name, dir_mode) - except EnvironmentError, err: - 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) - - -def ReadFile(file_name, size=-1): - """Reads a file. - - @type size: int - @param size: Read at most size bytes (if negative, entire file) - @rtype: str - @return: the (possibly partial) content of the file - - """ - f = open(file_name, "r") - try: - return f.read(size) - finally: - f.close() - - -def WriteFile(file_name, fn=None, data=None, - mode=None, uid=-1, gid=-1, - 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 - 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 function has raised an - exception, an existing target file should be unmodified and the - temporary file should be removed. - - @type file_name: str - @param file_name: the target filename - @type fn: callable - @param fn: content writing function, called with - file descriptor as parameter - @type data: str - @param data: contents of the file - @type mode: int - @param mode: file mode - @type uid: int - @param uid: the owner of the file - @type gid: int - @param gid: the group of the file - @type atime: int - @param atime: a custom access time to be set on the file - @type mtime: int - @param mtime: a custom modification time to be set on the file - @type close: boolean - @param close: whether to close file after writing it - @type prewrite: callable - @param prewrite: function to be called before writing content - @type postwrite: callable - @param postwrite: function to be called after writing content - - @rtype: None or int - @return: None if the 'close' parameter evaluates to True, - otherwise the file descriptor - - @raise errors.ProgrammerError: if any of the arguments are not valid - - """ - if 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) - do_remove = True - # 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 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)) - if not dry_run: - os.rename(new_name, file_name) - do_remove = False - finally: - if close: - os.close(fd) - result = None - else: - result = fd - if do_remove: - RemoveFile(new_name) - - return result - - -def GetFileID(path=None, fd=None): - """Returns the file 'id', i.e. the dev/inode and mtime information. - - Either the path to the file or the fd must be given. - - @param path: the file path - @param fd: a file descriptor - @return: a tuple of (device number, inode number, mtime) - - """ - if [path, fd].count(None) != 1: - raise errors.ProgrammerError("One and only one of fd/path must be given") - - if fd is None: - st = os.stat(path) - else: - st = os.fstat(fd) - - return (st.st_dev, st.st_ino, st.st_mtime) - - -def VerifyFileID(fi_disk, fi_ours): - """Verifies that two file IDs are matching. - - Differences in the inode/device are not accepted, but and older - timestamp for fi_disk is accepted. - - @param fi_disk: tuple (dev, inode, mtime) representing the actual - file data - @param fi_ours: tuple (dev, inode, mtime) representing the last - written file data - @rtype: boolean - - """ - (d1, i1, m1) = fi_disk - (d2, i2, m2) = fi_ours - - return (d1, i1) == (d2, i2) and m1 <= m2 - - -def SafeWriteFile(file_name, file_id, **kwargs): - """Wraper over L{WriteFile} that locks the target file. - - By keeping the target file locked during WriteFile, we ensure that - cooperating writers will safely serialise access to the file. - - @type file_name: str - @param file_name: the target filename - @type file_id: tuple - @param file_id: a result from L{GetFileID} - - """ - fd = os.open(file_name, os.O_RDONLY | os.O_CREAT) - try: - LockFile(fd) - if file_id is not None: - disk_id = GetFileID(fd=fd) - if not VerifyFileID(disk_id, file_id): - raise errors.LockError("Cannot overwrite file %s, it has been modified" - " since last written" % file_name) - return WriteFile(file_name, **kwargs) - finally: - os.close(fd) - - -def ReadOneLineFile(file_name, strict=False): - """Return the first non-empty line from a file. - - @type strict: boolean - @param strict: if True, abort if the file has more than one - non-empty line - - """ - file_lines = ReadFile(file_name).splitlines() - full_lines = filter(bool, file_lines) - if not file_lines or not full_lines: - raise errors.GenericError("No data in one-liner file %s" % file_name) - elif strict and len(full_lines) > 1: - raise errors.GenericError("Too many lines in one-liner file %s" % - file_name) - return full_lines[0] - - def FirstFree(seq, base=0): """Returns the first non-existing integer from seq. @@ -1904,19 +1422,6 @@ def Daemonize(logfile): return wpipe -def DaemonPidFileName(name): - """Compute a ganeti pid file absolute path - - @type name: str - @param name: the daemon name - @rtype: str - @return: the full path to the pidfile corresponding to the given - daemon name - - """ - return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name) - - def EnsureDaemon(name): """Check for and start daemon if not alive. @@ -1943,49 +1448,6 @@ def StopDaemon(name): return True -def WritePidFile(pidfile): - """Write the current process pidfile. - - @type pidfile: string - @param pidfile: the path to the file to be written - @raise errors.LockError: if the pid file already exists and - points to a live process - @rtype: int - @return: the file descriptor of the lock file; do not close this unless - you want to unlock the pid file - - """ - # We don't rename nor truncate the file to not drop locks under - # existing processes - fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600) - - # Lock the PID file (and fail if not possible to do so). Any code - # wanting to send a signal to the daemon should try to lock the PID - # file before reading it. If acquiring the lock succeeds, the daemon is - # no longer running and the signal should not be sent. - LockFile(fd_pidfile) - - os.write(fd_pidfile, "%d\n" % os.getpid()) - - return fd_pidfile - - -def RemovePidFile(pidfile): - """Remove the current process pidfile. - - Any errors are ignored. - - @type pidfile: string - @param pidfile: Path to the file to be removed - - """ - # TODO: we could check here that the file contains our pid - try: - RemoveFile(pidfile) - except Exception: # pylint: disable-msg=W0703 - pass - - def KillProcess(pid, signal_=signal.SIGTERM, timeout=30, waitpid=False): """Kill a process given by its pid. @@ -2049,40 +1511,6 @@ def KillProcess(pid, signal_=signal.SIGTERM, timeout=30, _helper(pid, signal.SIGKILL, waitpid) -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. - - @type name: str - @param name: the name to look for - @type search_path: str - @param search_path: location to start at - @type test: callable - @param test: a function taking one argument that should return True - if the a given object is valid; the default value is - os.path.exists, causing only existing files to be returned - @rtype: str or None - @return: full path to the object if found, None otherwise - - """ - # validate the filename mask - if constants.EXT_PLUGIN_MASK.match(name) is None: - logging.critical("Invalid value passed for external script name: '%s'", - name) - return None - - for dir_name in search_path: - # FIXME: investigate switch to PathJoin - item_name = os.path.sep.join([dir_name, name]) - # check the user test and that we're indeed resolving to the given - # basename - if test(item_name) and os.path.basename(item_name) == name: - return item_name - return None - - def CheckVolumeGroupSize(vglist, vgname, minsize): """Checks if the volume group list is valid. @@ -2144,71 +1572,6 @@ def MergeTime(timetuple): return float(seconds) + (float(microseconds) * 0.000001) -def IsNormAbsPath(path): - """Check whether a path is absolute and also normalized - - This avoids things like /dir/../../other/path to be valid. - - """ - return os.path.normpath(path) == path and os.path.isabs(path) - - -def PathJoin(*args): - """Safe-join a list of path components. - - Requirements: - - the first argument must be an absolute path - - no component in the path must have backtracking (e.g. /../), - since we check for normalization at the end - - @param args: the path components to be joined - @raise ValueError: for invalid paths - - """ - # ensure we're having at least one path passed in - assert args - # ensure the first component is an absolute and normalized path name - root = args[0] - if not IsNormAbsPath(root): - raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0])) - result = os.path.join(*args) - # ensure that the whole path is normalized - if not IsNormAbsPath(result): - raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args)) - # check that we're still under the original prefix - prefix = os.path.commonprefix([root, result]) - if prefix != root: - raise ValueError("Error: path joining resulted in different prefix" - " (%s != %s)" % (prefix, root)) - return result - - -def TailFile(fname, lines=20): - """Return the last lines from a file. - - @note: this function will only read and parse the last 4KB of - the file; if the lines are very long, it could be that less - than the requested number of lines are returned - - @param fname: the file name - @type lines: int - @param lines: the (maximum) number of lines to return - - """ - fd = open(fname, "r") - try: - fd.seek(0, 2) - pos = fd.tell() - pos = max(0, pos-4096) - fd.seek(pos, 0) - raw_data = fd.read() - finally: - fd.close() - - rows = raw_data.splitlines() - return rows[-lines:] - - def _ParseAsn1Generalizedtime(value): """Parses an ASN1 GENERALIZEDTIME timestamp as used by pyOpenSSL. @@ -2436,37 +1799,6 @@ def FindMatch(data, name): return None -def BytesToMebibyte(value): - """Converts bytes to mebibytes. - - @type value: int - @param value: Value in bytes - @rtype: int - @return: Value in mebibytes - - """ - return int(round(value / (1024.0 * 1024.0), 0)) - - -def CalculateDirectorySize(path): - """Calculates the size of a directory recursively. - - @type path: string - @param path: Path to directory - @rtype: int - @return: Size in mebibytes - - """ - size = 0 - - for (curpath, _, files) in os.walk(path): - for filename in files: - st = os.lstat(PathJoin(curpath, filename)) - size += st.st_size - - return BytesToMebibyte(size) - - def GetMounts(filename=constants.PROC_MOUNTS): """Returns the list of mounted filesystems. @@ -2487,22 +1819,6 @@ def GetMounts(filename=constants.PROC_MOUNTS): return data -def GetFilesystemStats(path): - """Returns the total and free space on a filesystem. - - @type path: string - @param path: Path on filesystem to be examined - @rtype: int - @return: tuple of (Total space, Free space) in mebibytes - - """ - st = os.statvfs(path) - - fsize = BytesToMebibyte(st.f_bavail * st.f_frsize) - tsize = BytesToMebibyte(st.f_blocks * st.f_frsize) - return (tsize, fsize) - - def RunInSeparateProcess(fn, *args): """Runs a function in a separate process. @@ -2550,49 +1866,6 @@ def RunInSeparateProcess(fn, *args): return bool(exitcode) -def ReadWatcherPauseFile(filename, now=None, remove_after=3600): - """Reads the watcher pause file. - - @type filename: string - @param filename: Path to watcher pause file - @type now: None, float or int - @param now: Current time as Unix timestamp - @type remove_after: int - @param remove_after: Remove watcher pause file after specified amount of - seconds past the pause end time - - """ - if now is None: - now = time.time() - - try: - value = ReadFile(filename) - except IOError, err: - if err.errno != errno.ENOENT: - raise - value = None - - if value is not None: - try: - value = int(value) - except ValueError: - logging.warning(("Watcher pause file (%s) contains invalid value," - " removing it"), filename) - RemoveFile(filename) - value = None - - if value is not None: - # Remove file if it's outdated - if now > (value + remove_after): - RemoveFile(filename) - value = None - - elif now > value: - value = None - - return value - - def GenerateSelfSignedX509Cert(common_name, validity): """Generates a self-signed X509 certificate. diff --git a/lib/utils/io.py b/lib/utils/io.py new file mode 100644 index 000000000..eef7bb762 --- /dev/null +++ b/lib/utils/io.py @@ -0,0 +1,762 @@ +# +# + +# Copyright (C) 2006, 2007, 2010, 2011 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +"""Utility functions for I/O. + +""" + +import os +import logging +import shutil +import tempfile +import errno +import time + +from ganeti import errors +from ganeti import constants +from ganeti.utils import filelock + + +def ReadFile(file_name, size=-1): + """Reads a file. + + @type size: int + @param size: Read at most size bytes (if negative, entire file) + @rtype: str + @return: the (possibly partial) content of the file + + """ + f = open(file_name, "r") + try: + return f.read(size) + finally: + f.close() + + +def WriteFile(file_name, fn=None, data=None, + mode=None, uid=-1, gid=-1, + 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 + 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 function has raised an + exception, an existing target file should be unmodified and the + temporary file should be removed. + + @type file_name: str + @param file_name: the target filename + @type fn: callable + @param fn: content writing function, called with + file descriptor as parameter + @type data: str + @param data: contents of the file + @type mode: int + @param mode: file mode + @type uid: int + @param uid: the owner of the file + @type gid: int + @param gid: the group of the file + @type atime: int + @param atime: a custom access time to be set on the file + @type mtime: int + @param mtime: a custom modification time to be set on the file + @type close: boolean + @param close: whether to close file after writing it + @type prewrite: callable + @param prewrite: function to be called before writing content + @type postwrite: callable + @param postwrite: function to be called after writing content + + @rtype: None or int + @return: None if the 'close' parameter evaluates to True, + otherwise the file descriptor + + @raise errors.ProgrammerError: if any of the arguments are not valid + + """ + if 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) + do_remove = True + # 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 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)) + if not dry_run: + os.rename(new_name, file_name) + do_remove = False + finally: + if close: + os.close(fd) + result = None + else: + result = fd + if do_remove: + RemoveFile(new_name) + + return result + + +def GetFileID(path=None, fd=None): + """Returns the file 'id', i.e. the dev/inode and mtime information. + + Either the path to the file or the fd must be given. + + @param path: the file path + @param fd: a file descriptor + @return: a tuple of (device number, inode number, mtime) + + """ + if [path, fd].count(None) != 1: + raise errors.ProgrammerError("One and only one of fd/path must be given") + + if fd is None: + st = os.stat(path) + else: + st = os.fstat(fd) + + return (st.st_dev, st.st_ino, st.st_mtime) + + +def VerifyFileID(fi_disk, fi_ours): + """Verifies that two file IDs are matching. + + Differences in the inode/device are not accepted, but and older + timestamp for fi_disk is accepted. + + @param fi_disk: tuple (dev, inode, mtime) representing the actual + file data + @param fi_ours: tuple (dev, inode, mtime) representing the last + written file data + @rtype: boolean + + """ + (d1, i1, m1) = fi_disk + (d2, i2, m2) = fi_ours + + return (d1, i1) == (d2, i2) and m1 <= m2 + + +def SafeWriteFile(file_name, file_id, **kwargs): + """Wraper over L{WriteFile} that locks the target file. + + By keeping the target file locked during WriteFile, we ensure that + cooperating writers will safely serialise access to the file. + + @type file_name: str + @param file_name: the target filename + @type file_id: tuple + @param file_id: a result from L{GetFileID} + + """ + fd = os.open(file_name, os.O_RDONLY | os.O_CREAT) + try: + filelock.LockFile(fd) + if file_id is not None: + disk_id = GetFileID(fd=fd) + if not VerifyFileID(disk_id, file_id): + raise errors.LockError("Cannot overwrite file %s, it has been modified" + " since last written" % file_name) + return WriteFile(file_name, **kwargs) + finally: + os.close(fd) + + +def ReadOneLineFile(file_name, strict=False): + """Return the first non-empty line from a file. + + @type strict: boolean + @param strict: if True, abort if the file has more than one + non-empty line + + """ + file_lines = ReadFile(file_name).splitlines() + full_lines = filter(bool, file_lines) + if not file_lines or not full_lines: + raise errors.GenericError("No data in one-liner file %s" % file_name) + elif strict and len(full_lines) > 1: + raise errors.GenericError("Too many lines in one-liner file %s" % + file_name) + return full_lines[0] + + +def RemoveFile(filename): + """Remove a file ignoring some errors. + + Remove a file, ignoring non-existing ones or directories. Other + errors are passed. + + @type filename: str + @param filename: the file to be removed + + """ + try: + os.unlink(filename) + except OSError, err: + if err.errno not in (errno.ENOENT, errno.EISDIR): + raise + + +def RemoveDir(dirname): + """Remove an empty directory. + + Remove a directory, ignoring non-existing ones. + Other errors are passed. This includes the case, + where the directory is not empty, so it can't be removed. + + @type dirname: str + @param dirname: the empty directory to be removed + + """ + try: + os.rmdir(dirname) + except OSError, err: + if err.errno != errno.ENOENT: + raise + + +def RenameFile(old, new, mkdir=False, mkdir_mode=0750): + """Renames a file. + + @type old: string + @param old: Original path + @type new: string + @param new: New path + @type mkdir: bool + @param mkdir: Whether to create target directory if it doesn't exist + @type mkdir_mode: int + @param mkdir_mode: Mode for newly created directories + + """ + try: + return os.rename(old, new) + except OSError, err: + # In at least one use case of this function, the job queue, directory + # creation is very rare. Checking for the directory before renaming is not + # as efficient. + if mkdir and err.errno == errno.ENOENT: + # Create directory and try again + Makedirs(os.path.dirname(new), mode=mkdir_mode) + + return os.rename(old, new) + + raise + + +def Makedirs(path, mode=0750): + """Super-mkdir; create a leaf directory and all intermediate ones. + + This is a wrapper around C{os.makedirs} adding error handling not implemented + before Python 2.5. + + """ + try: + os.makedirs(path, mode) + except OSError, err: + # Ignore EEXIST. This is only handled in os.makedirs as included in + # Python 2.5 and above. + if err.errno != errno.EEXIST or not os.path.exists(path): + raise + + +def TimestampForFilename(): + """Returns the current time formatted for filenames. + + The format doesn't contain colons as some shells and applications treat them + as separators. Uses the local timezone. + + """ + return time.strftime("%Y-%m-%d_%H_%M_%S") + + +def CreateBackup(file_name): + """Creates a backup of a file. + + @type file_name: str + @param file_name: file to be backed up + @rtype: str + @return: the path to the newly created backup + @raise errors.ProgrammerError: for invalid file names + + """ + if not os.path.isfile(file_name): + raise errors.ProgrammerError("Can't make a backup of a non-file '%s'" % + file_name) + + prefix = ("%s.backup-%s." % + (os.path.basename(file_name), TimestampForFilename())) + dir_name = os.path.dirname(file_name) + + fsrc = open(file_name, 'rb') + try: + (fd, backup_name) = tempfile.mkstemp(prefix=prefix, dir=dir_name) + fdst = os.fdopen(fd, 'wb') + try: + logging.debug("Backing up %s at %s", file_name, backup_name) + shutil.copyfileobj(fsrc, fdst) + finally: + fdst.close() + finally: + fsrc.close() + + return backup_name + + +def ListVisibleFiles(path): + """Returns a list of visible files in a directory. + + @type path: str + @param path: the directory to enumerate + @rtype: list + @return: the list of all files not starting with a dot + @raise ProgrammerError: if L{path} is not an absolue and normalized path + + """ + if not IsNormAbsPath(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(".")] + return files + + +def EnsureDirs(dirs): + """Make required directories, if they don't exist. + + @param dirs: list of tuples (dir_name, dir_mode) + @type dirs: list of (string, integer) + + """ + for dir_name, dir_mode in dirs: + try: + os.mkdir(dir_name, dir_mode) + except EnvironmentError, err: + 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) + + +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. + + @type name: str + @param name: the name to look for + @type search_path: str + @param search_path: location to start at + @type test: callable + @param test: a function taking one argument that should return True + if the a given object is valid; the default value is + os.path.exists, causing only existing files to be returned + @rtype: str or None + @return: full path to the object if found, None otherwise + + """ + # validate the filename mask + if constants.EXT_PLUGIN_MASK.match(name) is None: + logging.critical("Invalid value passed for external script name: '%s'", + name) + return None + + for dir_name in search_path: + # FIXME: investigate switch to PathJoin + item_name = os.path.sep.join([dir_name, name]) + # check the user test and that we're indeed resolving to the given + # basename + if test(item_name) and os.path.basename(item_name) == name: + return item_name + return None + + +def IsNormAbsPath(path): + """Check whether a path is absolute and also normalized + + This avoids things like /dir/../../other/path to be valid. + + """ + return os.path.normpath(path) == path and os.path.isabs(path) + + +def PathJoin(*args): + """Safe-join a list of path components. + + Requirements: + - the first argument must be an absolute path + - no component in the path must have backtracking (e.g. /../), + since we check for normalization at the end + + @param args: the path components to be joined + @raise ValueError: for invalid paths + + """ + # ensure we're having at least one path passed in + assert args + # ensure the first component is an absolute and normalized path name + root = args[0] + if not IsNormAbsPath(root): + raise ValueError("Invalid parameter to PathJoin: '%s'" % str(args[0])) + result = os.path.join(*args) + # ensure that the whole path is normalized + if not IsNormAbsPath(result): + raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args)) + # check that we're still under the original prefix + prefix = os.path.commonprefix([root, result]) + if prefix != root: + raise ValueError("Error: path joining resulted in different prefix" + " (%s != %s)" % (prefix, root)) + return result + + +def TailFile(fname, lines=20): + """Return the last lines from a file. + + @note: this function will only read and parse the last 4KB of + the file; if the lines are very long, it could be that less + than the requested number of lines are returned + + @param fname: the file name + @type lines: int + @param lines: the (maximum) number of lines to return + + """ + fd = open(fname, "r") + try: + fd.seek(0, 2) + pos = fd.tell() + pos = max(0, pos-4096) + fd.seek(pos, 0) + raw_data = fd.read() + finally: + fd.close() + + rows = raw_data.splitlines() + return rows[-lines:] + + +def BytesToMebibyte(value): + """Converts bytes to mebibytes. + + @type value: int + @param value: Value in bytes + @rtype: int + @return: Value in mebibytes + + """ + return int(round(value / (1024.0 * 1024.0), 0)) + + +def CalculateDirectorySize(path): + """Calculates the size of a directory recursively. + + @type path: string + @param path: Path to directory + @rtype: int + @return: Size in mebibytes + + """ + size = 0 + + for (curpath, _, files) in os.walk(path): + for filename in files: + st = os.lstat(PathJoin(curpath, filename)) + size += st.st_size + + return BytesToMebibyte(size) + + +def GetFilesystemStats(path): + """Returns the total and free space on a filesystem. + + @type path: string + @param path: Path on filesystem to be examined + @rtype: int + @return: tuple of (Total space, Free space) in mebibytes + + """ + st = os.statvfs(path) + + fsize = BytesToMebibyte(st.f_bavail * st.f_frsize) + tsize = BytesToMebibyte(st.f_blocks * st.f_frsize) + return (tsize, fsize) + + +def ReadPidFile(pidfile): + """Read a pid from a file. + + @type pidfile: string + @param pidfile: path to the file containing the pid + @rtype: int + @return: The process id, if the file exists and contains a valid PID, + otherwise 0 + + """ + try: + raw_data = ReadOneLineFile(pidfile) + except EnvironmentError, err: + if err.errno != errno.ENOENT: + logging.exception("Can't read pid file") + return 0 + + try: + pid = int(raw_data) + except (TypeError, ValueError), err: + logging.info("Can't parse pid file contents", exc_info=True) + return 0 + + return pid + + +def ReadLockedPidFile(path): + """Reads a locked PID file. + + This can be used together with L{utils.StartDaemon}. + + @type path: string + @param path: Path to PID file + @return: PID as integer or, if file was unlocked or couldn't be opened, None + + """ + try: + fd = os.open(path, os.O_RDONLY) + except EnvironmentError, err: + if err.errno == errno.ENOENT: + # PID file doesn't exist + return None + raise + + try: + try: + # Try to acquire lock + filelock.LockFile(fd) + except errors.LockError: + # Couldn't lock, daemon is running + return int(os.read(fd, 100)) + finally: + os.close(fd) + + return None + + +def AddAuthorizedKey(file_obj, key): + """Adds an SSH public key to an authorized_keys file. + + @type file_obj: str or file handle + @param file_obj: path to authorized_keys file + @type key: str + @param key: string containing key + + """ + key_fields = key.split() + + if isinstance(file_obj, basestring): + f = open(file_obj, 'a+') + else: + f = file_obj + + try: + nl = True + for line in f: + # Ignore whitespace changes + if line.split() == key_fields: + break + nl = line.endswith('\n') + else: + if not nl: + f.write("\n") + f.write(key.rstrip('\r\n')) + f.write("\n") + f.flush() + finally: + f.close() + + +def RemoveAuthorizedKey(file_name, key): + """Removes an SSH public key from an authorized_keys file. + + @type file_name: str + @param file_name: path to authorized_keys file + @type key: str + @param key: string containing key + + """ + key_fields = key.split() + + 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: + # 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 DaemonPidFileName(name): + """Compute a ganeti pid file absolute path + + @type name: str + @param name: the daemon name + @rtype: str + @return: the full path to the pidfile corresponding to the given + daemon name + + """ + return PathJoin(constants.RUN_GANETI_DIR, "%s.pid" % name) + + +def WritePidFile(pidfile): + """Write the current process pidfile. + + @type pidfile: string + @param pidfile: the path to the file to be written + @raise errors.LockError: if the pid file already exists and + points to a live process + @rtype: int + @return: the file descriptor of the lock file; do not close this unless + you want to unlock the pid file + + """ + # We don't rename nor truncate the file to not drop locks under + # existing processes + fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600) + + # Lock the PID file (and fail if not possible to do so). Any code + # wanting to send a signal to the daemon should try to lock the PID + # file before reading it. If acquiring the lock succeeds, the daemon is + # no longer running and the signal should not be sent. + filelock.LockFile(fd_pidfile) + + os.write(fd_pidfile, "%d\n" % os.getpid()) + + return fd_pidfile + + +def RemovePidFile(pidfile): + """Remove the current process pidfile. + + Any errors are ignored. + + @type pidfile: string + @param pidfile: Path to the file to be removed + + """ + # TODO: we could check here that the file contains our pid + try: + RemoveFile(pidfile) + except Exception: # pylint: disable-msg=W0703 + pass + + +def ReadWatcherPauseFile(filename, now=None, remove_after=3600): + """Reads the watcher pause file. + + @type filename: string + @param filename: Path to watcher pause file + @type now: None, float or int + @param now: Current time as Unix timestamp + @type remove_after: int + @param remove_after: Remove watcher pause file after specified amount of + seconds past the pause end time + + """ + if now is None: + now = time.time() + + try: + value = ReadFile(filename) + except IOError, err: + if err.errno != errno.ENOENT: + raise + value = None + + if value is not None: + try: + value = int(value) + except ValueError: + logging.warning(("Watcher pause file (%s) contains invalid value," + " removing it"), filename) + RemoveFile(filename) + value = None + + if value is not None: + # Remove file if it's outdated + if now > (value + remove_after): + RemoveFile(filename) + value = None + + elif now > value: + value = None + + return value diff --git a/test/ganeti.utils.io_unittest.py b/test/ganeti.utils.io_unittest.py new file mode 100755 index 000000000..b83b51e52 --- /dev/null +++ b/test/ganeti.utils.io_unittest.py @@ -0,0 +1,694 @@ +#!/usr/bin/python +# + +# Copyright (C) 2006, 2007, 2010, 2011 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Script for testing ganeti.utils.io""" + +import os +import tempfile +import unittest +import shutil +import glob +import time + +from ganeti import constants +from ganeti import utils +from ganeti import compat +from ganeti import errors + +import testutils + + +class TestReadFile(testutils.GanetiTestCase): + def testReadAll(self): + data = utils.ReadFile(self._TestDataFilename("cert1.pem")) + self.assertEqual(len(data), 814) + + h = compat.md5_hash() + h.update(data) + self.assertEqual(h.hexdigest(), "a491efb3efe56a0535f924d5f8680fd4") + + def testReadSize(self): + data = utils.ReadFile(self._TestDataFilename("cert1.pem"), + size=100) + self.assertEqual(len(data), 100) + + h = compat.md5_hash() + h.update(data) + self.assertEqual(h.hexdigest(), "893772354e4e690b9efd073eed433ce7") + + def testError(self): + self.assertRaises(EnvironmentError, utils.ReadFile, + "/dev/null/does-not-exist") + + +class TestReadOneLineFile(testutils.GanetiTestCase): + def setUp(self): + testutils.GanetiTestCase.setUp(self) + + def testDefault(self): + data = utils.ReadOneLineFile(self._TestDataFilename("cert1.pem")) + self.assertEqual(len(data), 27) + self.assertEqual(data, "-----BEGIN CERTIFICATE-----") + + def testNotStrict(self): + data = utils.ReadOneLineFile(self._TestDataFilename("cert1.pem"), + strict=False) + self.assertEqual(len(data), 27) + self.assertEqual(data, "-----BEGIN CERTIFICATE-----") + + def testStrictFailure(self): + self.assertRaises(errors.GenericError, utils.ReadOneLineFile, + self._TestDataFilename("cert1.pem"), strict=True) + + def testLongLine(self): + dummydata = (1024 * "Hello World! ") + myfile = self._CreateTempFile() + utils.WriteFile(myfile, data=dummydata) + datastrict = utils.ReadOneLineFile(myfile, strict=True) + datalax = utils.ReadOneLineFile(myfile, strict=False) + self.assertEqual(dummydata, datastrict) + self.assertEqual(dummydata, datalax) + + def testNewline(self): + myfile = self._CreateTempFile() + myline = "myline" + for nl in ["", "\n", "\r\n"]: + dummydata = "%s%s" % (myline, nl) + utils.WriteFile(myfile, data=dummydata) + datalax = utils.ReadOneLineFile(myfile, strict=False) + self.assertEqual(myline, datalax) + datastrict = utils.ReadOneLineFile(myfile, strict=True) + self.assertEqual(myline, datastrict) + + def testWhitespaceAndMultipleLines(self): + myfile = self._CreateTempFile() + for nl in ["", "\n", "\r\n"]: + for ws in [" ", "\t", "\t\t \t", "\t "]: + dummydata = (1024 * ("Foo bar baz %s%s" % (ws, nl))) + utils.WriteFile(myfile, data=dummydata) + datalax = utils.ReadOneLineFile(myfile, strict=False) + if nl: + self.assert_(set("\r\n") & set(dummydata)) + self.assertRaises(errors.GenericError, utils.ReadOneLineFile, + myfile, strict=True) + explen = len("Foo bar baz ") + len(ws) + self.assertEqual(len(datalax), explen) + self.assertEqual(datalax, dummydata[:explen]) + self.assertFalse(set("\r\n") & set(datalax)) + else: + datastrict = utils.ReadOneLineFile(myfile, strict=True) + self.assertEqual(dummydata, datastrict) + self.assertEqual(dummydata, datalax) + + def testEmptylines(self): + myfile = self._CreateTempFile() + myline = "myline" + for nl in ["\n", "\r\n"]: + for ol in ["", "otherline"]: + dummydata = "%s%s%s%s%s%s" % (nl, nl, myline, nl, ol, nl) + utils.WriteFile(myfile, data=dummydata) + self.assert_(set("\r\n") & set(dummydata)) + datalax = utils.ReadOneLineFile(myfile, strict=False) + self.assertEqual(myline, datalax) + if ol: + self.assertRaises(errors.GenericError, utils.ReadOneLineFile, + myfile, strict=True) + else: + datastrict = utils.ReadOneLineFile(myfile, strict=True) + self.assertEqual(myline, datastrict) + + def testEmptyfile(self): + myfile = self._CreateTempFile() + self.assertRaises(errors.GenericError, utils.ReadOneLineFile, myfile) + + +class TestTimestampForFilename(unittest.TestCase): + def test(self): + self.assert_("." not in utils.TimestampForFilename()) + self.assert_(":" not in utils.TimestampForFilename()) + + +class TestCreateBackup(testutils.GanetiTestCase): + def setUp(self): + testutils.GanetiTestCase.setUp(self) + + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + testutils.GanetiTestCase.tearDown(self) + + shutil.rmtree(self.tmpdir) + + def testEmpty(self): + filename = utils.PathJoin(self.tmpdir, "config.data") + utils.WriteFile(filename, data="") + bname = utils.CreateBackup(filename) + self.assertFileContent(bname, "") + self.assertEqual(len(glob.glob("%s*" % filename)), 2) + utils.CreateBackup(filename) + self.assertEqual(len(glob.glob("%s*" % filename)), 3) + utils.CreateBackup(filename) + self.assertEqual(len(glob.glob("%s*" % filename)), 4) + + fifoname = utils.PathJoin(self.tmpdir, "fifo") + os.mkfifo(fifoname) + self.assertRaises(errors.ProgrammerError, utils.CreateBackup, fifoname) + + def testContent(self): + bkpcount = 0 + for data in ["", "X", "Hello World!\n" * 100, "Binary data\0\x01\x02\n"]: + for rep in [1, 2, 10, 127]: + testdata = data * rep + + filename = utils.PathJoin(self.tmpdir, "test.data_") + utils.WriteFile(filename, data=testdata) + self.assertFileContent(filename, testdata) + + for _ in range(3): + bname = utils.CreateBackup(filename) + bkpcount += 1 + self.assertFileContent(bname, testdata) + self.assertEqual(len(glob.glob("%s*" % filename)), 1 + bkpcount) + + +class TestListVisibleFiles(unittest.TestCase): + """Test case for ListVisibleFiles""" + + def setUp(self): + self.path = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.path) + + def _CreateFiles(self, files): + for name in files: + utils.WriteFile(os.path.join(self.path, name), data="test") + + def _test(self, files, expected): + self._CreateFiles(files) + found = utils.ListVisibleFiles(self.path) + self.assertEqual(set(found), set(expected)) + + def testAllVisible(self): + files = ["a", "b", "c"] + expected = files + self._test(files, expected) + + def testNoneVisible(self): + files = [".a", ".b", ".c"] + expected = [] + self._test(files, expected) + + def testSomeVisible(self): + files = ["a", "b", ".c"] + expected = ["a", "b"] + self._test(files, expected) + + def testNonAbsolutePath(self): + self.failUnlessRaises(errors.ProgrammerError, utils.ListVisibleFiles, + "abc") + + def testNonNormalizedPath(self): + self.failUnlessRaises(errors.ProgrammerError, utils.ListVisibleFiles, + "/bin/../tmp") + + +class TestWriteFile(unittest.TestCase): + def setUp(self): + self.tfile = tempfile.NamedTemporaryFile() + self.did_pre = False + self.did_post = False + self.did_write = False + + def markPre(self, fd): + self.did_pre = True + + def markPost(self, fd): + self.did_post = True + + def markWrite(self, fd): + self.did_write = True + + def testWrite(self): + data = "abc" + utils.WriteFile(self.tfile.name, data=data) + self.assertEqual(utils.ReadFile(self.tfile.name), data) + + def testErrors(self): + self.assertRaises(errors.ProgrammerError, utils.WriteFile, + self.tfile.name, data="test", fn=lambda fd: None) + self.assertRaises(errors.ProgrammerError, utils.WriteFile, self.tfile.name) + self.assertRaises(errors.ProgrammerError, utils.WriteFile, + self.tfile.name, data="test", atime=0) + + def testCalls(self): + utils.WriteFile(self.tfile.name, fn=self.markWrite, + prewrite=self.markPre, postwrite=self.markPost) + self.assertTrue(self.did_pre) + self.assertTrue(self.did_post) + self.assertTrue(self.did_write) + + def testDryRun(self): + orig = "abc" + self.tfile.write(orig) + self.tfile.flush() + utils.WriteFile(self.tfile.name, data="hello", dry_run=True) + self.assertEqual(utils.ReadFile(self.tfile.name), orig) + + def testTimes(self): + f = self.tfile.name + for at, mt in [(0, 0), (1000, 1000), (2000, 3000), + (int(time.time()), 5000)]: + utils.WriteFile(f, data="hello", atime=at, mtime=mt) + st = os.stat(f) + self.assertEqual(st.st_atime, at) + self.assertEqual(st.st_mtime, mt) + + def testNoClose(self): + data = "hello" + self.assertEqual(utils.WriteFile(self.tfile.name, data="abc"), None) + fd = utils.WriteFile(self.tfile.name, data=data, close=False) + try: + os.lseek(fd, 0, 0) + self.assertEqual(os.read(fd, 4096), data) + finally: + os.close(fd) + + +class TestFileID(testutils.GanetiTestCase): + def testEquality(self): + name = self._CreateTempFile() + oldi = utils.GetFileID(path=name) + self.failUnless(utils.VerifyFileID(oldi, oldi)) + + def testUpdate(self): + name = self._CreateTempFile() + oldi = utils.GetFileID(path=name) + os.utime(name, None) + fd = os.open(name, os.O_RDWR) + try: + newi = utils.GetFileID(fd=fd) + self.failUnless(utils.VerifyFileID(oldi, newi)) + self.failUnless(utils.VerifyFileID(newi, oldi)) + finally: + os.close(fd) + + def testWriteFile(self): + name = self._CreateTempFile() + oldi = utils.GetFileID(path=name) + mtime = oldi[2] + os.utime(name, (mtime + 10, mtime + 10)) + self.assertRaises(errors.LockError, utils.SafeWriteFile, name, + oldi, data="") + os.utime(name, (mtime - 10, mtime - 10)) + utils.SafeWriteFile(name, oldi, data="") + oldi = utils.GetFileID(path=name) + mtime = oldi[2] + os.utime(name, (mtime + 10, mtime + 10)) + # this doesn't raise, since we passed None + utils.SafeWriteFile(name, None, data="") + + def testError(self): + t = tempfile.NamedTemporaryFile() + self.assertRaises(errors.ProgrammerError, utils.GetFileID, + path=t.name, fd=t.fileno()) + + +class TestRemoveFile(unittest.TestCase): + """Test case for the RemoveFile function""" + + def setUp(self): + """Create a temp dir and file for each case""" + self.tmpdir = tempfile.mkdtemp('', 'ganeti-unittest-') + fd, self.tmpfile = tempfile.mkstemp('', '', self.tmpdir) + os.close(fd) + + def tearDown(self): + if os.path.exists(self.tmpfile): + os.unlink(self.tmpfile) + os.rmdir(self.tmpdir) + + def testIgnoreDirs(self): + """Test that RemoveFile() ignores directories""" + self.assertEqual(None, utils.RemoveFile(self.tmpdir)) + + def testIgnoreNotExisting(self): + """Test that RemoveFile() ignores non-existing files""" + utils.RemoveFile(self.tmpfile) + utils.RemoveFile(self.tmpfile) + + def testRemoveFile(self): + """Test that RemoveFile does remove a file""" + utils.RemoveFile(self.tmpfile) + if os.path.exists(self.tmpfile): + self.fail("File '%s' not removed" % self.tmpfile) + + def testRemoveSymlink(self): + """Test that RemoveFile does remove symlinks""" + symlink = self.tmpdir + "/symlink" + os.symlink("no-such-file", symlink) + utils.RemoveFile(symlink) + if os.path.exists(symlink): + self.fail("File '%s' not removed" % symlink) + os.symlink(self.tmpfile, symlink) + utils.RemoveFile(symlink) + if os.path.exists(symlink): + self.fail("File '%s' not removed" % symlink) + + +class TestRemoveDir(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + try: + shutil.rmtree(self.tmpdir) + except EnvironmentError: + pass + + def testEmptyDir(self): + utils.RemoveDir(self.tmpdir) + self.assertFalse(os.path.isdir(self.tmpdir)) + + def testNonEmptyDir(self): + self.tmpfile = os.path.join(self.tmpdir, "test1") + open(self.tmpfile, "w").close() + self.assertRaises(EnvironmentError, utils.RemoveDir, self.tmpdir) + + +class TestRename(unittest.TestCase): + """Test case for RenameFile""" + + def setUp(self): + """Create a temporary directory""" + self.tmpdir = tempfile.mkdtemp() + self.tmpfile = os.path.join(self.tmpdir, "test1") + + # Touch the file + open(self.tmpfile, "w").close() + + def tearDown(self): + """Remove temporary directory""" + shutil.rmtree(self.tmpdir) + + def testSimpleRename1(self): + """Simple rename 1""" + utils.RenameFile(self.tmpfile, os.path.join(self.tmpdir, "xyz")) + self.assert_(os.path.isfile(os.path.join(self.tmpdir, "xyz"))) + + def testSimpleRename2(self): + """Simple rename 2""" + utils.RenameFile(self.tmpfile, os.path.join(self.tmpdir, "xyz"), + mkdir=True) + self.assert_(os.path.isfile(os.path.join(self.tmpdir, "xyz"))) + + def testRenameMkdir(self): + """Rename with mkdir""" + utils.RenameFile(self.tmpfile, os.path.join(self.tmpdir, "test/xyz"), + mkdir=True) + self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test"))) + self.assert_(os.path.isfile(os.path.join(self.tmpdir, "test/xyz"))) + + utils.RenameFile(os.path.join(self.tmpdir, "test/xyz"), + os.path.join(self.tmpdir, "test/foo/bar/baz"), + mkdir=True) + self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test"))) + self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test/foo/bar"))) + self.assert_(os.path.isfile(os.path.join(self.tmpdir, "test/foo/bar/baz"))) + + +class TestMakedirs(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def testNonExisting(self): + path = utils.PathJoin(self.tmpdir, "foo") + utils.Makedirs(path) + self.assert_(os.path.isdir(path)) + + def testExisting(self): + path = utils.PathJoin(self.tmpdir, "foo") + os.mkdir(path) + utils.Makedirs(path) + self.assert_(os.path.isdir(path)) + + def testRecursiveNonExisting(self): + path = utils.PathJoin(self.tmpdir, "foo/bar/baz") + utils.Makedirs(path) + self.assert_(os.path.isdir(path)) + + def testRecursiveExisting(self): + path = utils.PathJoin(self.tmpdir, "B/moo/xyz") + self.assertFalse(os.path.exists(path)) + os.mkdir(utils.PathJoin(self.tmpdir, "B")) + utils.Makedirs(path) + self.assert_(os.path.isdir(path)) + + +class TestEnsureDirs(unittest.TestCase): + """Tests for EnsureDirs""" + + def setUp(self): + self.dir = tempfile.mkdtemp() + self.old_umask = os.umask(0777) + + def testEnsureDirs(self): + utils.EnsureDirs([ + (utils.PathJoin(self.dir, "foo"), 0777), + (utils.PathJoin(self.dir, "bar"), 0000), + ]) + self.assertEquals(os.stat(utils.PathJoin(self.dir, "foo"))[0] & 0777, 0777) + self.assertEquals(os.stat(utils.PathJoin(self.dir, "bar"))[0] & 0777, 0000) + + def tearDown(self): + os.rmdir(utils.PathJoin(self.dir, "foo")) + os.rmdir(utils.PathJoin(self.dir, "bar")) + os.rmdir(self.dir) + os.umask(self.old_umask) + + +class TestIsNormAbsPath(unittest.TestCase): + """Testing case for IsNormAbsPath""" + + def _pathTestHelper(self, path, result): + if result: + self.assert_(utils.IsNormAbsPath(path), + "Path %s should result absolute and normalized" % path) + else: + self.assertFalse(utils.IsNormAbsPath(path), + "Path %s should not result absolute and normalized" % path) + + def testBase(self): + self._pathTestHelper("/etc", True) + self._pathTestHelper("/srv", True) + self._pathTestHelper("etc", False) + self._pathTestHelper("/etc/../root", False) + self._pathTestHelper("/etc/", False) + + +class TestPathJoin(unittest.TestCase): + """Testing case for PathJoin""" + + def testBasicItems(self): + mlist = ["/a", "b", "c"] + self.failUnlessEqual(utils.PathJoin(*mlist), "/".join(mlist)) + + def testNonAbsPrefix(self): + self.failUnlessRaises(ValueError, utils.PathJoin, "a", "b") + + def testBackTrack(self): + self.failUnlessRaises(ValueError, utils.PathJoin, "/a", "b/../c") + + def testMultiAbs(self): + self.failUnlessRaises(ValueError, utils.PathJoin, "/a", "/b") + + +class TestTailFile(testutils.GanetiTestCase): + """Test case for the TailFile function""" + + def testEmpty(self): + fname = self._CreateTempFile() + self.failUnlessEqual(utils.TailFile(fname), []) + self.failUnlessEqual(utils.TailFile(fname, lines=25), []) + + def testAllLines(self): + data = ["test %d" % i for i in range(30)] + for i in range(30): + fname = self._CreateTempFile() + fd = open(fname, "w") + fd.write("\n".join(data[:i])) + if i > 0: + fd.write("\n") + fd.close() + self.failUnlessEqual(utils.TailFile(fname, lines=i), data[:i]) + + def testPartialLines(self): + data = ["test %d" % i for i in range(30)] + fname = self._CreateTempFile() + fd = open(fname, "w") + fd.write("\n".join(data)) + fd.write("\n") + fd.close() + for i in range(1, 30): + self.failUnlessEqual(utils.TailFile(fname, lines=i), data[-i:]) + + def testBigFile(self): + data = ["test %d" % i for i in range(30)] + fname = self._CreateTempFile() + fd = open(fname, "w") + fd.write("X" * 1048576) + fd.write("\n") + fd.write("\n".join(data)) + fd.write("\n") + fd.close() + for i in range(1, 30): + self.failUnlessEqual(utils.TailFile(fname, lines=i), data[-i:]) + + +class TestPidFileFunctions(unittest.TestCase): + """Tests for WritePidFile, RemovePidFile and ReadPidFile""" + + def setUp(self): + self.dir = tempfile.mkdtemp() + self.f_dpn = lambda name: os.path.join(self.dir, "%s.pid" % name) + + def testPidFileFunctions(self): + pid_file = self.f_dpn('test') + fd = utils.WritePidFile(self.f_dpn('test')) + self.failUnless(os.path.exists(pid_file), + "PID file should have been created") + read_pid = utils.ReadPidFile(pid_file) + self.failUnlessEqual(read_pid, os.getpid()) + self.failUnless(utils.IsProcessAlive(read_pid)) + self.failUnlessRaises(errors.LockError, utils.WritePidFile, + self.f_dpn('test')) + os.close(fd) + utils.RemovePidFile(self.f_dpn("test")) + self.failIf(os.path.exists(pid_file), + "PID file should not exist anymore") + self.failUnlessEqual(utils.ReadPidFile(pid_file), 0, + "ReadPidFile should return 0 for missing pid file") + fh = open(pid_file, "w") + fh.write("blah\n") + fh.close() + self.failUnlessEqual(utils.ReadPidFile(pid_file), 0, + "ReadPidFile should return 0 for invalid pid file") + # but now, even with the file existing, we should be able to lock it + fd = utils.WritePidFile(self.f_dpn('test')) + os.close(fd) + utils.RemovePidFile(self.f_dpn("test")) + self.failIf(os.path.exists(pid_file), + "PID file should not exist anymore") + + def testKill(self): + pid_file = self.f_dpn('child') + r_fd, w_fd = os.pipe() + new_pid = os.fork() + if new_pid == 0: #child + utils.WritePidFile(self.f_dpn('child')) + os.write(w_fd, 'a') + signal.pause() + os._exit(0) + return + # else we are in the parent + # wait until the child has written the pid file + os.read(r_fd, 1) + read_pid = utils.ReadPidFile(pid_file) + self.failUnlessEqual(read_pid, new_pid) + self.failUnless(utils.IsProcessAlive(new_pid)) + utils.KillProcess(new_pid, waitpid=True) + self.failIf(utils.IsProcessAlive(new_pid)) + utils.RemovePidFile(self.f_dpn('child')) + self.failUnlessRaises(errors.ProgrammerError, utils.KillProcess, 0) + + def tearDown(self): + shutil.rmtree(self.dir) + + +class TestSshKeys(testutils.GanetiTestCase): + """Test case for the AddAuthorizedKey function""" + + KEY_A = 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a' + KEY_B = ('command="/usr/bin/fooserver -t --verbose",from="198.51.100.4" ' + 'ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b') + + def setUp(self): + testutils.GanetiTestCase.setUp(self) + self.tmpname = self._CreateTempFile() + handle = open(self.tmpname, 'w') + try: + handle.write("%s\n" % TestSshKeys.KEY_A) + handle.write("%s\n" % TestSshKeys.KEY_B) + finally: + handle.close() + + def testAddingNewKey(self): + utils.AddAuthorizedKey(self.tmpname, + 'ssh-dss AAAAB3NzaC1kc3MAAACB root@test') + + self.assertFileContent(self.tmpname, + "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" + 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' + " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n" + "ssh-dss AAAAB3NzaC1kc3MAAACB root@test\n") + + def testAddingAlmostButNotCompletelyTheSameKey(self): + utils.AddAuthorizedKey(self.tmpname, + 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@test') + + self.assertFileContent(self.tmpname, + "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" + 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' + " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n" + "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@test\n") + + def testAddingExistingKeyWithSomeMoreSpaces(self): + utils.AddAuthorizedKey(self.tmpname, + 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a') + + self.assertFileContent(self.tmpname, + "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" + 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' + " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n") + + def testRemovingExistingKeyWithSomeMoreSpaces(self): + utils.RemoveAuthorizedKey(self.tmpname, + 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a') + + self.assertFileContent(self.tmpname, + 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' + " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n") + + def testRemovingNonExistingKey(self): + utils.RemoveAuthorizedKey(self.tmpname, + 'ssh-dss AAAAB3Nsdfj230xxjxJjsjwjsjdjU root@test') + + self.assertFileContent(self.tmpname, + "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" + 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' + " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n") + + +if __name__ == "__main__": + testutils.GanetiTestProgram() diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py index e7b0298f8..c1cb0b6a2 100755 --- a/test/ganeti.utils_unittest.py +++ b/test/ganeti.utils_unittest.py @@ -46,10 +46,10 @@ from ganeti import constants from ganeti import compat from ganeti import utils from ganeti import errors -from ganeti.utils import RunCmd, RemoveFile, \ - ListVisibleFiles, FirstFree, \ - TailFile, RunParts, PathJoin, \ - ReadOneLineFile, SetEtcHostsEntry, RemoveEtcHostsEntry +from ganeti.utils import RunCmd, \ + FirstFree, \ + RunParts, \ + SetEtcHostsEntry, RemoveEtcHostsEntry class TestIsProcessAlive(unittest.TestCase): @@ -112,7 +112,7 @@ class TestIsProcessHandlingSignal(unittest.TestCase): self.assertEqual(result, value.strip()) def test(self): - sp = PathJoin(self.tmpdir, "status") + sp = utils.PathJoin(self.tmpdir, "status") utils.WriteFile(sp, data="\n".join([ "Name: bash", @@ -130,7 +130,7 @@ class TestIsProcessHandlingSignal(unittest.TestCase): self.assert_(utils.IsProcessHandlingSignal(1234, 10, status_path=sp)) def testNoSigCgt(self): - sp = PathJoin(self.tmpdir, "status") + sp = utils.PathJoin(self.tmpdir, "status") utils.WriteFile(sp, data="\n".join([ "Name: bash", @@ -140,7 +140,7 @@ class TestIsProcessHandlingSignal(unittest.TestCase): 1234, 10, status_path=sp) def testNoSuchFile(self): - sp = PathJoin(self.tmpdir, "notexist") + sp = utils.PathJoin(self.tmpdir, "notexist") self.assertFalse(utils.IsProcessHandlingSignal(1234, 10, status_path=sp)) @@ -168,68 +168,6 @@ class TestIsProcessHandlingSignal(unittest.TestCase): self.assert_(utils.RunInSeparateProcess(self._TestRealProcess)) -class TestPidFileFunctions(unittest.TestCase): - """Tests for WritePidFile, RemovePidFile and ReadPidFile""" - - def setUp(self): - self.dir = tempfile.mkdtemp() - self.f_dpn = lambda name: os.path.join(self.dir, "%s.pid" % name) - - def testPidFileFunctions(self): - pid_file = self.f_dpn('test') - fd = utils.WritePidFile(self.f_dpn('test')) - self.failUnless(os.path.exists(pid_file), - "PID file should have been created") - read_pid = utils.ReadPidFile(pid_file) - self.failUnlessEqual(read_pid, os.getpid()) - self.failUnless(utils.IsProcessAlive(read_pid)) - self.failUnlessRaises(errors.LockError, utils.WritePidFile, - self.f_dpn('test')) - os.close(fd) - utils.RemovePidFile(self.f_dpn("test")) - self.failIf(os.path.exists(pid_file), - "PID file should not exist anymore") - self.failUnlessEqual(utils.ReadPidFile(pid_file), 0, - "ReadPidFile should return 0 for missing pid file") - fh = open(pid_file, "w") - fh.write("blah\n") - fh.close() - self.failUnlessEqual(utils.ReadPidFile(pid_file), 0, - "ReadPidFile should return 0 for invalid pid file") - # but now, even with the file existing, we should be able to lock it - fd = utils.WritePidFile(self.f_dpn('test')) - os.close(fd) - utils.RemovePidFile(self.f_dpn("test")) - self.failIf(os.path.exists(pid_file), - "PID file should not exist anymore") - - def testKill(self): - pid_file = self.f_dpn('child') - r_fd, w_fd = os.pipe() - new_pid = os.fork() - if new_pid == 0: #child - utils.WritePidFile(self.f_dpn('child')) - os.write(w_fd, 'a') - signal.pause() - os._exit(0) - return - # else we are in the parent - # wait until the child has written the pid file - os.read(r_fd, 1) - read_pid = utils.ReadPidFile(pid_file) - self.failUnlessEqual(read_pid, new_pid) - self.failUnless(utils.IsProcessAlive(new_pid)) - utils.KillProcess(new_pid, waitpid=True) - self.failIf(utils.IsProcessAlive(new_pid)) - utils.RemovePidFile(self.f_dpn('child')) - self.failUnlessRaises(errors.ProgrammerError, utils.KillProcess, 0) - - def tearDown(self): - for name in os.listdir(self.dir): - os.unlink(os.path.join(self.dir, name)) - os.rmdir(self.dir) - - class TestRunCmd(testutils.GanetiTestCase): """Testing case for the RunCmd function""" @@ -627,263 +565,6 @@ class TestStartDaemon(testutils.GanetiTestCase): os.close(fd) -class TestRemoveFile(unittest.TestCase): - """Test case for the RemoveFile function""" - - def setUp(self): - """Create a temp dir and file for each case""" - self.tmpdir = tempfile.mkdtemp('', 'ganeti-unittest-') - fd, self.tmpfile = tempfile.mkstemp('', '', self.tmpdir) - os.close(fd) - - def tearDown(self): - if os.path.exists(self.tmpfile): - os.unlink(self.tmpfile) - os.rmdir(self.tmpdir) - - def testIgnoreDirs(self): - """Test that RemoveFile() ignores directories""" - self.assertEqual(None, RemoveFile(self.tmpdir)) - - def testIgnoreNotExisting(self): - """Test that RemoveFile() ignores non-existing files""" - RemoveFile(self.tmpfile) - RemoveFile(self.tmpfile) - - def testRemoveFile(self): - """Test that RemoveFile does remove a file""" - RemoveFile(self.tmpfile) - if os.path.exists(self.tmpfile): - self.fail("File '%s' not removed" % self.tmpfile) - - def testRemoveSymlink(self): - """Test that RemoveFile does remove symlinks""" - symlink = self.tmpdir + "/symlink" - os.symlink("no-such-file", symlink) - RemoveFile(symlink) - if os.path.exists(symlink): - self.fail("File '%s' not removed" % symlink) - os.symlink(self.tmpfile, symlink) - RemoveFile(symlink) - if os.path.exists(symlink): - self.fail("File '%s' not removed" % symlink) - - -class TestRemoveDir(unittest.TestCase): - def setUp(self): - self.tmpdir = tempfile.mkdtemp() - - def tearDown(self): - try: - shutil.rmtree(self.tmpdir) - except EnvironmentError: - pass - - def testEmptyDir(self): - utils.RemoveDir(self.tmpdir) - self.assertFalse(os.path.isdir(self.tmpdir)) - - def testNonEmptyDir(self): - self.tmpfile = os.path.join(self.tmpdir, "test1") - open(self.tmpfile, "w").close() - self.assertRaises(EnvironmentError, utils.RemoveDir, self.tmpdir) - - -class TestRename(unittest.TestCase): - """Test case for RenameFile""" - - def setUp(self): - """Create a temporary directory""" - self.tmpdir = tempfile.mkdtemp() - self.tmpfile = os.path.join(self.tmpdir, "test1") - - # Touch the file - open(self.tmpfile, "w").close() - - def tearDown(self): - """Remove temporary directory""" - shutil.rmtree(self.tmpdir) - - def testSimpleRename1(self): - """Simple rename 1""" - utils.RenameFile(self.tmpfile, os.path.join(self.tmpdir, "xyz")) - self.assert_(os.path.isfile(os.path.join(self.tmpdir, "xyz"))) - - def testSimpleRename2(self): - """Simple rename 2""" - utils.RenameFile(self.tmpfile, os.path.join(self.tmpdir, "xyz"), - mkdir=True) - self.assert_(os.path.isfile(os.path.join(self.tmpdir, "xyz"))) - - def testRenameMkdir(self): - """Rename with mkdir""" - utils.RenameFile(self.tmpfile, os.path.join(self.tmpdir, "test/xyz"), - mkdir=True) - self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test"))) - self.assert_(os.path.isfile(os.path.join(self.tmpdir, "test/xyz"))) - - utils.RenameFile(os.path.join(self.tmpdir, "test/xyz"), - os.path.join(self.tmpdir, "test/foo/bar/baz"), - mkdir=True) - self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test"))) - self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test/foo/bar"))) - self.assert_(os.path.isfile(os.path.join(self.tmpdir, "test/foo/bar/baz"))) - - -class TestReadFile(testutils.GanetiTestCase): - - def testReadAll(self): - data = utils.ReadFile(self._TestDataFilename("cert1.pem")) - self.assertEqual(len(data), 814) - - h = compat.md5_hash() - h.update(data) - self.assertEqual(h.hexdigest(), "a491efb3efe56a0535f924d5f8680fd4") - - def testReadSize(self): - data = utils.ReadFile(self._TestDataFilename("cert1.pem"), - size=100) - self.assertEqual(len(data), 100) - - h = compat.md5_hash() - h.update(data) - self.assertEqual(h.hexdigest(), "893772354e4e690b9efd073eed433ce7") - - def testError(self): - self.assertRaises(EnvironmentError, utils.ReadFile, - "/dev/null/does-not-exist") - - -class TestReadOneLineFile(testutils.GanetiTestCase): - - def setUp(self): - testutils.GanetiTestCase.setUp(self) - - def testDefault(self): - data = ReadOneLineFile(self._TestDataFilename("cert1.pem")) - self.assertEqual(len(data), 27) - self.assertEqual(data, "-----BEGIN CERTIFICATE-----") - - def testNotStrict(self): - data = ReadOneLineFile(self._TestDataFilename("cert1.pem"), strict=False) - self.assertEqual(len(data), 27) - self.assertEqual(data, "-----BEGIN CERTIFICATE-----") - - def testStrictFailure(self): - self.assertRaises(errors.GenericError, ReadOneLineFile, - self._TestDataFilename("cert1.pem"), strict=True) - - def testLongLine(self): - dummydata = (1024 * "Hello World! ") - myfile = self._CreateTempFile() - utils.WriteFile(myfile, data=dummydata) - datastrict = ReadOneLineFile(myfile, strict=True) - datalax = ReadOneLineFile(myfile, strict=False) - self.assertEqual(dummydata, datastrict) - self.assertEqual(dummydata, datalax) - - def testNewline(self): - myfile = self._CreateTempFile() - myline = "myline" - for nl in ["", "\n", "\r\n"]: - dummydata = "%s%s" % (myline, nl) - utils.WriteFile(myfile, data=dummydata) - datalax = ReadOneLineFile(myfile, strict=False) - self.assertEqual(myline, datalax) - datastrict = ReadOneLineFile(myfile, strict=True) - self.assertEqual(myline, datastrict) - - def testWhitespaceAndMultipleLines(self): - myfile = self._CreateTempFile() - for nl in ["", "\n", "\r\n"]: - for ws in [" ", "\t", "\t\t \t", "\t "]: - dummydata = (1024 * ("Foo bar baz %s%s" % (ws, nl))) - utils.WriteFile(myfile, data=dummydata) - datalax = ReadOneLineFile(myfile, strict=False) - if nl: - self.assert_(set("\r\n") & set(dummydata)) - self.assertRaises(errors.GenericError, ReadOneLineFile, - myfile, strict=True) - explen = len("Foo bar baz ") + len(ws) - self.assertEqual(len(datalax), explen) - self.assertEqual(datalax, dummydata[:explen]) - self.assertFalse(set("\r\n") & set(datalax)) - else: - datastrict = ReadOneLineFile(myfile, strict=True) - self.assertEqual(dummydata, datastrict) - self.assertEqual(dummydata, datalax) - - def testEmptylines(self): - myfile = self._CreateTempFile() - myline = "myline" - for nl in ["\n", "\r\n"]: - for ol in ["", "otherline"]: - dummydata = "%s%s%s%s%s%s" % (nl, nl, myline, nl, ol, nl) - utils.WriteFile(myfile, data=dummydata) - self.assert_(set("\r\n") & set(dummydata)) - datalax = ReadOneLineFile(myfile, strict=False) - self.assertEqual(myline, datalax) - if ol: - self.assertRaises(errors.GenericError, ReadOneLineFile, - myfile, strict=True) - else: - datastrict = ReadOneLineFile(myfile, strict=True) - self.assertEqual(myline, datastrict) - - def testEmptyfile(self): - myfile = self._CreateTempFile() - self.assertRaises(errors.GenericError, ReadOneLineFile, myfile) - - -class TestTimestampForFilename(unittest.TestCase): - def test(self): - self.assert_("." not in utils.TimestampForFilename()) - self.assert_(":" not in utils.TimestampForFilename()) - - -class TestCreateBackup(testutils.GanetiTestCase): - def setUp(self): - testutils.GanetiTestCase.setUp(self) - - self.tmpdir = tempfile.mkdtemp() - - def tearDown(self): - testutils.GanetiTestCase.tearDown(self) - - shutil.rmtree(self.tmpdir) - - def testEmpty(self): - filename = PathJoin(self.tmpdir, "config.data") - utils.WriteFile(filename, data="") - bname = utils.CreateBackup(filename) - self.assertFileContent(bname, "") - self.assertEqual(len(glob.glob("%s*" % filename)), 2) - utils.CreateBackup(filename) - self.assertEqual(len(glob.glob("%s*" % filename)), 3) - utils.CreateBackup(filename) - self.assertEqual(len(glob.glob("%s*" % filename)), 4) - - fifoname = PathJoin(self.tmpdir, "fifo") - os.mkfifo(fifoname) - self.assertRaises(errors.ProgrammerError, utils.CreateBackup, fifoname) - - def testContent(self): - bkpcount = 0 - for data in ["", "X", "Hello World!\n" * 100, "Binary data\0\x01\x02\n"]: - for rep in [1, 2, 10, 127]: - testdata = data * rep - - filename = PathJoin(self.tmpdir, "test.data_") - utils.WriteFile(filename, data=testdata) - self.assertFileContent(filename, testdata) - - for _ in range(3): - bname = utils.CreateBackup(filename) - bkpcount += 1 - self.assertFileContent(bname, testdata) - self.assertEqual(len(glob.glob("%s*" % filename)), 1 + bkpcount) - - class TestParseCpuMask(unittest.TestCase): """Test case for the ParseCpuMask function.""" @@ -897,70 +578,6 @@ class TestParseCpuMask(unittest.TestCase): self.assertRaises(errors.ParseError, utils.ParseCpuMask, data) -class TestSshKeys(testutils.GanetiTestCase): - """Test case for the AddAuthorizedKey function""" - - KEY_A = 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a' - KEY_B = ('command="/usr/bin/fooserver -t --verbose",from="198.51.100.4" ' - 'ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b') - - def setUp(self): - testutils.GanetiTestCase.setUp(self) - self.tmpname = self._CreateTempFile() - handle = open(self.tmpname, 'w') - try: - handle.write("%s\n" % TestSshKeys.KEY_A) - handle.write("%s\n" % TestSshKeys.KEY_B) - finally: - handle.close() - - def testAddingNewKey(self): - utils.AddAuthorizedKey(self.tmpname, - 'ssh-dss AAAAB3NzaC1kc3MAAACB root@test') - - self.assertFileContent(self.tmpname, - "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" - 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' - " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n" - "ssh-dss AAAAB3NzaC1kc3MAAACB root@test\n") - - def testAddingAlmostButNotCompletelyTheSameKey(self): - utils.AddAuthorizedKey(self.tmpname, - 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@test') - - self.assertFileContent(self.tmpname, - "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" - 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' - " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n" - "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@test\n") - - def testAddingExistingKeyWithSomeMoreSpaces(self): - utils.AddAuthorizedKey(self.tmpname, - 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a') - - self.assertFileContent(self.tmpname, - "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" - 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' - " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n") - - def testRemovingExistingKeyWithSomeMoreSpaces(self): - utils.RemoveAuthorizedKey(self.tmpname, - 'ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a') - - self.assertFileContent(self.tmpname, - 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' - " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n") - - def testRemovingNonExistingKey(self): - utils.RemoveAuthorizedKey(self.tmpname, - 'ssh-dss AAAAB3Nsdfj230xxjxJjsjwjsjdjU root@test') - - self.assertFileContent(self.tmpname, - "ssh-dss AAAAB3NzaC1w5256closdj32mZaQU root@key-a\n" - 'command="/usr/bin/fooserver -t --verbose",from="198.51.100.4"' - " ssh-dss AAAAB3NzaC1w520smc01ms0jfJs22 root@key-b\n") - - class TestEtcHosts(testutils.GanetiTestCase): """Test functions modifying /etc/hosts""" @@ -1062,48 +679,6 @@ class TestGetMounts(unittest.TestCase): ("none", "/proc", "proc", "rw,nosuid,nodev,noexec,relatime"), ]) - -class TestListVisibleFiles(unittest.TestCase): - """Test case for ListVisibleFiles""" - - def setUp(self): - self.path = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.path) - - def _CreateFiles(self, files): - for name in files: - utils.WriteFile(os.path.join(self.path, name), data="test") - - def _test(self, files, expected): - self._CreateFiles(files) - found = ListVisibleFiles(self.path) - self.assertEqual(set(found), set(expected)) - - def testAllVisible(self): - files = ["a", "b", "c"] - expected = files - self._test(files, expected) - - def testNoneVisible(self): - files = [".a", ".b", ".c"] - expected = [] - self._test(files, expected) - - def testSomeVisible(self): - files = ["a", "b", ".c"] - expected = ["a", "b"] - self._test(files, expected) - - def testNonAbsolutePath(self): - self.failUnlessRaises(errors.ProgrammerError, ListVisibleFiles, "abc") - - def testNonNormalizedPath(self): - self.failUnlessRaises(errors.ProgrammerError, ListVisibleFiles, - "/bin/../tmp") - - class TestNewUUID(unittest.TestCase): """Test case for NewUUID""" @@ -1123,48 +698,6 @@ class TestFirstFree(unittest.TestCase): self.failUnlessRaises(AssertionError, FirstFree, [0, 3, 4, 6], base=3) -class TestTailFile(testutils.GanetiTestCase): - """Test case for the TailFile function""" - - def testEmpty(self): - fname = self._CreateTempFile() - self.failUnlessEqual(TailFile(fname), []) - self.failUnlessEqual(TailFile(fname, lines=25), []) - - def testAllLines(self): - data = ["test %d" % i for i in range(30)] - for i in range(30): - fname = self._CreateTempFile() - fd = open(fname, "w") - fd.write("\n".join(data[:i])) - if i > 0: - fd.write("\n") - fd.close() - self.failUnlessEqual(TailFile(fname, lines=i), data[:i]) - - def testPartialLines(self): - data = ["test %d" % i for i in range(30)] - fname = self._CreateTempFile() - fd = open(fname, "w") - fd.write("\n".join(data)) - fd.write("\n") - fd.close() - for i in range(1, 30): - self.failUnlessEqual(TailFile(fname, lines=i), data[-i:]) - - def testBigFile(self): - data = ["test %d" % i for i in range(30)] - fname = self._CreateTempFile() - fd = open(fname, "w") - fd.write("X" * 1048576) - fd.write("\n") - fd.write("\n".join(data)) - fd.write("\n") - fd.close() - for i in range(1, 30): - self.failUnlessEqual(TailFile(fname, lines=i), data[-i:]) - - class TestTimeFunctions(unittest.TestCase): """Test case for time functions""" @@ -1266,25 +799,6 @@ class TestForceDictType(unittest.TestCase): {"b": "hello"}, {"b": "no-such-type"}) -class TestIsNormAbsPath(unittest.TestCase): - """Testing case for IsNormAbsPath""" - - def _pathTestHelper(self, path, result): - if result: - self.assert_(utils.IsNormAbsPath(path), - "Path %s should result absolute and normalized" % path) - else: - self.assertFalse(utils.IsNormAbsPath(path), - "Path %s should not result absolute and normalized" % path) - - def testBase(self): - self._pathTestHelper('/etc', True) - self._pathTestHelper('/srv', True) - self._pathTestHelper('etc', False) - self._pathTestHelper('/etc/../root', False) - self._pathTestHelper('/etc/', False) - - class RunInSeparateProcess(unittest.TestCase): def test(self): for exp in [True, False]: @@ -1370,23 +884,6 @@ class TestGenerateSelfSignedX509Cert(unittest.TestCase): self.assert_(self._checkCertificate(cert1)) -class TestPathJoin(unittest.TestCase): - """Testing case for PathJoin""" - - def testBasicItems(self): - mlist = ["/a", "b", "c"] - self.failUnlessEqual(PathJoin(*mlist), "/".join(mlist)) - - def testNonAbsPrefix(self): - self.failUnlessRaises(ValueError, PathJoin, "a", "b") - - def testBackTrack(self): - self.failUnlessRaises(ValueError, PathJoin, "/a", "b/../c") - - def testMultiAbs(self): - self.failUnlessRaises(ValueError, PathJoin, "/a", "/b") - - class TestValidateServiceName(unittest.TestCase): def testValid(self): testnames = [ @@ -1531,37 +1028,6 @@ class TestSignX509Certificate(unittest.TestCase): pem, self.KEY_OTHER) -class TestMakedirs(unittest.TestCase): - def setUp(self): - self.tmpdir = tempfile.mkdtemp() - - def tearDown(self): - shutil.rmtree(self.tmpdir) - - def testNonExisting(self): - path = PathJoin(self.tmpdir, "foo") - utils.Makedirs(path) - self.assert_(os.path.isdir(path)) - - def testExisting(self): - path = PathJoin(self.tmpdir, "foo") - os.mkdir(path) - utils.Makedirs(path) - self.assert_(os.path.isdir(path)) - - def testRecursiveNonExisting(self): - path = PathJoin(self.tmpdir, "foo/bar/baz") - utils.Makedirs(path) - self.assert_(os.path.isdir(path)) - - def testRecursiveExisting(self): - path = PathJoin(self.tmpdir, "B/moo/xyz") - self.assertFalse(os.path.exists(path)) - os.mkdir(PathJoin(self.tmpdir, "B")) - utils.Makedirs(path) - self.assert_(os.path.isdir(path)) - - class TestReadLockedPidFile(unittest.TestCase): def setUp(self): self.tmpdir = tempfile.mkdtemp() @@ -1570,16 +1036,16 @@ class TestReadLockedPidFile(unittest.TestCase): shutil.rmtree(self.tmpdir) def testNonExistent(self): - path = PathJoin(self.tmpdir, "nonexist") + path = utils.PathJoin(self.tmpdir, "nonexist") self.assert_(utils.ReadLockedPidFile(path) is None) def testUnlocked(self): - path = PathJoin(self.tmpdir, "pid") + path = utils.PathJoin(self.tmpdir, "pid") utils.WriteFile(path, data="123") self.assert_(utils.ReadLockedPidFile(path) is None) def testLocked(self): - path = PathJoin(self.tmpdir, "pid") + path = utils.PathJoin(self.tmpdir, "pid") utils.WriteFile(path, data="123") fl = utils.FileLock.Open(path) @@ -1593,8 +1059,8 @@ class TestReadLockedPidFile(unittest.TestCase): self.assert_(utils.ReadLockedPidFile(path) is None) def testError(self): - path = PathJoin(self.tmpdir, "foobar", "pid") - utils.WriteFile(PathJoin(self.tmpdir, "foobar"), data="") + path = utils.PathJoin(self.tmpdir, "foobar", "pid") + utils.WriteFile(utils.PathJoin(self.tmpdir, "foobar"), data="") # open(2) should return ENOTDIR self.assertRaises(EnvironmentError, utils.ReadLockedPidFile, path) @@ -1653,28 +1119,6 @@ class TestVerifyCertificateInner(unittest.TestCase): self.assertEqual(errcode, utils.CERT_ERROR) -class TestEnsureDirs(unittest.TestCase): - """Tests for EnsureDirs""" - - def setUp(self): - self.dir = tempfile.mkdtemp() - self.old_umask = os.umask(0777) - - def testEnsureDirs(self): - utils.EnsureDirs([ - (PathJoin(self.dir, "foo"), 0777), - (PathJoin(self.dir, "bar"), 0000), - ]) - self.assertEquals(os.stat(PathJoin(self.dir, "foo"))[0] & 0777, 0777) - self.assertEquals(os.stat(PathJoin(self.dir, "bar"))[0] & 0777, 0000) - - def tearDown(self): - os.rmdir(PathJoin(self.dir, "foo")) - os.rmdir(PathJoin(self.dir, "bar")) - os.rmdir(self.dir) - os.umask(self.old_umask) - - class TestFindMatch(unittest.TestCase): def test(self): data = { @@ -1705,45 +1149,6 @@ class TestFindMatch(unittest.TestCase): self.assert_(utils.FindMatch(data, "Hello World") is None) -class TestFileID(testutils.GanetiTestCase): - def testEquality(self): - name = self._CreateTempFile() - oldi = utils.GetFileID(path=name) - self.failUnless(utils.VerifyFileID(oldi, oldi)) - - def testUpdate(self): - name = self._CreateTempFile() - oldi = utils.GetFileID(path=name) - os.utime(name, None) - fd = os.open(name, os.O_RDWR) - try: - newi = utils.GetFileID(fd=fd) - self.failUnless(utils.VerifyFileID(oldi, newi)) - self.failUnless(utils.VerifyFileID(newi, oldi)) - finally: - os.close(fd) - - def testWriteFile(self): - name = self._CreateTempFile() - oldi = utils.GetFileID(path=name) - mtime = oldi[2] - os.utime(name, (mtime + 10, mtime + 10)) - self.assertRaises(errors.LockError, utils.SafeWriteFile, name, - oldi, data="") - os.utime(name, (mtime - 10, mtime - 10)) - utils.SafeWriteFile(name, oldi, data="") - oldi = utils.GetFileID(path=name) - mtime = oldi[2] - os.utime(name, (mtime + 10, mtime + 10)) - # this doesn't raise, since we passed None - utils.SafeWriteFile(name, None, data="") - - def testError(self): - t = tempfile.NamedTemporaryFile() - self.assertRaises(errors.ProgrammerError, utils.GetFileID, - path=t.name, fd=t.fileno()) - - class TimeMock: def __init__(self, values): self.values = values @@ -1807,68 +1212,5 @@ class TestBuildShellCmd(unittest.TestCase): self.assertEqual(utils.BuildShellCmd("ls %s", "ab"), "ls ab") -class TestWriteFile(unittest.TestCase): - def setUp(self): - self.tfile = tempfile.NamedTemporaryFile() - self.did_pre = False - self.did_post = False - self.did_write = False - - def markPre(self, fd): - self.did_pre = True - - def markPost(self, fd): - self.did_post = True - - def markWrite(self, fd): - self.did_write = True - - def testWrite(self): - data = "abc" - utils.WriteFile(self.tfile.name, data=data) - self.assertEqual(utils.ReadFile(self.tfile.name), data) - - def testErrors(self): - self.assertRaises(errors.ProgrammerError, utils.WriteFile, - self.tfile.name, data="test", fn=lambda fd: None) - self.assertRaises(errors.ProgrammerError, utils.WriteFile, self.tfile.name) - self.assertRaises(errors.ProgrammerError, utils.WriteFile, - self.tfile.name, data="test", atime=0) - - def testCalls(self): - utils.WriteFile(self.tfile.name, fn=self.markWrite, - prewrite=self.markPre, postwrite=self.markPost) - self.assertTrue(self.did_pre) - self.assertTrue(self.did_post) - self.assertTrue(self.did_write) - - def testDryRun(self): - orig = "abc" - self.tfile.write(orig) - self.tfile.flush() - utils.WriteFile(self.tfile.name, data="hello", dry_run=True) - self.assertEqual(utils.ReadFile(self.tfile.name), orig) - - def testTimes(self): - f = self.tfile.name - for at, mt in [(0, 0), (1000, 1000), (2000, 3000), - (int(time.time()), 5000)]: - utils.WriteFile(f, data="hello", atime=at, mtime=mt) - st = os.stat(f) - self.assertEqual(st.st_atime, at) - self.assertEqual(st.st_mtime, mt) - - - def testNoClose(self): - data = "hello" - self.assertEqual(utils.WriteFile(self.tfile.name, data="abc"), None) - fd = utils.WriteFile(self.tfile.name, data=data, close=False) - try: - os.lseek(fd, 0, 0) - self.assertEqual(os.read(fd, 4096), data) - finally: - os.close(fd) - - if __name__ == '__main__': testutils.GanetiTestProgram() -- GitLab