diff --git a/src/lib/Bcfg2/Client/Tools/WinAction.py b/src/lib/Bcfg2/Client/Tools/WinAction.py new file mode 100755 index 0000000000..33f943c801 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/WinAction.py @@ -0,0 +1,79 @@ +"""WinAction driver""" +import subprocess + +import Bcfg2.Client.Tools +from Bcfg2.Utils import safe_input + + +class WinAction(Bcfg2.Client.Tools.Tool): + """Implement Actions""" + name = 'WinAction' + __handles__ = [('Action', None)] + __req__ = {'Action': ['name', 'timing', 'when', 'command', 'status']} + + def RunAction(self, entry): + """This method handles command execution and status return.""" + shell = False + shell_string = '' + if entry.get('shell', 'false') == 'true': + shell = True + shell_string = '(in shell) ' + + if not Bcfg2.Options.setup.dry_run: + if Bcfg2.Options.setup.interactive: + prompt = ('Run Action %s%s, %s: (y/N): ' % + (shell_string, entry.get('name'), + entry.get('command'))) + ans = safe_input(prompt) + if ans not in ['y', 'Y']: + return False + if Bcfg2.Options.setup.service_mode == 'build': + if entry.get('build', 'true') == 'false': + self.logger.debug("Action: Deferring execution of %s due " + "to build mode" % entry.get('command')) + return False + self.logger.debug("Running Action %s %s" % + (shell_string, entry.get('name'))) + rv = self.cmd.run(entry.get('command'), shell=shell, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False) + self.logger.debug("Action: %s got return code %s" % + (entry.get('command'), rv.retval)) + entry.set('rc', str(rv.retval)) + return entry.get('status', 'check') == 'ignore' or rv.success + else: + self.logger.debug("In dryrun mode: not running action: %s" % + (entry.get('name'))) + return False + + def VerifyAction(self, dummy, _): + """Actions always verify true.""" + return True + + def InstallAction(self, entry): + """Run actions as pre-checks for bundle installation.""" + if entry.get('timing') != 'post': + return self.RunAction(entry) + return True + + def BundleUpdated(self, bundle): + """Run postinstalls when bundles have been updated.""" + states = dict() + for action in bundle.findall("Action"): + if action.get('timing') in ['post', 'both']: + if not self._install_allowed(action): + continue + states[action] = self.RunAction(action) + return states + + def BundleNotUpdated(self, bundle): + """Run Actions when bundles have not been updated.""" + states = dict() + for action in bundle.findall("Action"): + if (action.get('timing') in ['post', 'both'] and + action.get('when') != 'modified'): + if not self._install_allowed(action): + continue + states[action] = self.RunAction(action) + return states diff --git a/src/lib/Bcfg2/Client/Tools/WinFS.py b/src/lib/Bcfg2/Client/Tools/WinFS.py new file mode 100755 index 0000000000..4be1530eeb --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/WinFS.py @@ -0,0 +1,229 @@ +"""All Windows Type client support for Bcfg2.""" +import sys +import os +import stat +import shutil +import tempfile +import Bcfg2.Options +import Bcfg2.Client.Tools +from Bcfg2.Compat import unicode, b64decode # pylint: disable=W0622 + + +class WinFS(Bcfg2.Client.Tools.Tool): + """Windows File support code.""" + name = 'WinFS' + __handles__ = [('Path', 'file'), ('Path', 'nonexistent')] + + def __init__(self, config): + Bcfg2.Client.Tools.Tool.__init__(self, config) + self.__req__ = dict(Path=dict()) + self.__req__['Path']['file'] = ['name', 'mode', 'owner', 'group'] + self.__req__['Path']['nonexistent'] = ['name', 'recursive'] + + def _getFilePath(self, entry): + """Evaluates the enviroment Variables and returns the file path""" + file_path = os.path.expandvars(os.path.normpath(entry.get('name')[1:])) + if not file_path[1] == ':': + self.logger.info( + "Skipping \"%s\" because it doesnt look like a " + "Windows Path" % + file_path) + return False + return file_path + + def VerifyPath(self, entry, _): + """Path always verify true.""" + file_path = self._getFilePath(entry) + if not file_path: + return False + + if entry.get('type') == 'nonexistent': + if os.path.exists(file_path): + self.logger.debug("WinFS: %s exists but should not" % + file_path) + return False + return True + + ondisk = self._exists(file_path) + tempdata = self._get_data(entry)[0] + if isinstance(tempdata, str) and str != unicode: + tempdatasize = len(tempdata) + else: + tempdatasize = len(tempdata.encode(Bcfg2.Options.setup.encoding)) + + different = False + content = None + if not ondisk: + # first, see if the target file exists at all; if not, + # they're clearly different + different = True + content = "" + elif tempdatasize != ondisk[stat.ST_SIZE]: + # next, see if the size of the target file is different + # from the size of the desired content + different = True + else: + # finally, read in the target file and compare them + # directly. comparison could be done with a checksum, + # which might be faster for big binary files, but slower + # for everything else + try: + content = open(file_path).read() + except UnicodeDecodeError: + content = open(file_path, + encoding=Bcfg2.Options.setup.encoding).read() + except IOError: + self.logger.error("Windows: Failed to read %s: %s" % + (file_path, sys.exc_info()[1])) + return False + different = str(content) != str(tempdata) + return not different + + def InstallPath(self, entry): + """Install device entries.""" + file_path = self._getFilePath(entry) + ename = entry.get('name') + print("Handeling %s" % file_path) + if not file_path: + return False + if entry.get('type') == "nonexistent": + recursive = entry.get('recursive', '').lower() == 'true' + if recursive: + # ensure that configuration spec is consistent first + for struct in self.config.getchildren(): + for el in struct.getchildren(): + if (el.tag == 'Path' and + el.get('type') != 'nonexistent' and + el.get('name').startswith(ename)): + self.logger.error('POSIX: Not removing %s. One ' + 'or more files in this ' + 'directory are specified ' + 'in your configuration.' % + file_path) + return False + try: + self._remove(file_path, recursive=recursive) + rv = True + except OSError: + err = sys.exc_info()[1] + self.logger.error('WinFS: Failed to renive %s: %s' % + (file_path, err)) + rv = False + return rv + + self.logger.debug("Installing: " + file_path) + if not os.path.exists(os.path.dirname(file_path)): + if not self._makedirs(path=file_path): + return False + newfile = self._write_tmpfile(entry, file_path) + if not newfile: + return False + rv = True + if not self._rename_tmpfile(newfile, file_path): + rv = False + + return rv + + def _makedirs(self, path): + """ os.makedirs helpfully creates all parent directories for us.""" + rv = True + try: + os.makedirs(os.path.dirname(path)) + except OSError: + err = sys.exc_info()[1] + self.logger.error('Windows: Failed to create directory %s: %s' % + (path, err)) + rv = False + return rv + + def _write_tmpfile(self, entry, file_path): + """ Write the file data to a temp file """ + filedata = self._get_data(entry)[0] + # get a temp file to write to that is in the same directory as + # the existing file in order to preserve any permissions + # protections on that directory, and also to avoid issues with + # /tmp set nosetuid while creating files that are supposed to + # be setuid + try: + (newfd, newfile) = \ + tempfile.mkstemp(prefix=os.path.basename(file_path), + dir=os.path.dirname(file_path)) + except OSError: + err = sys.exc_info()[1] + self.logger.error( + "Windows: Failed to create " + "temp file in %s: %s" % (file_path, err)) + return False + try: + if isinstance(filedata, str) and str != unicode: + os.fdopen(newfd, 'w').write(filedata) + else: + os.fdopen(newfd, 'wb').write( + filedata.encode(Bcfg2.Options.setup.encoding)) + except (OSError, IOError): + err = sys.exc_info()[1] + self.logger.error( + "Windows: Failed to open temp file %s for writing " + "%s: %s" % + (newfile, file_path, err)) + return False + return newfile + + def _get_data(self, entry): + """ Get a tuple of (, ) for the given entry """ + is_binary = entry.get('encoding', 'ascii') == 'base64' + if entry.get('empty', 'false') == 'true' or not entry.text: + tempdata = '' + elif is_binary: + tempdata = b64decode(entry.text) + else: + tempdata = entry.text + if isinstance(tempdata, unicode) and unicode != str: + try: + tempdata = tempdata.encode(Bcfg2.Options.setup.encoding) + except UnicodeEncodeError: + err = sys.exc_info()[1] + self.logger.error("Windows: Error encoding file %s: %s" % + (entry.get('name'), err)) + return (tempdata, is_binary) + + def _rename_tmpfile(self, newfile, file_path): + """ Rename the given file to the appropriate filename for entry """ + try: + if os.path.isfile(file_path): + os.unlink(file_path) + os.rename(newfile, file_path) + return True + except OSError: + err = sys.exc_info()[1] + self.logger.error( + "Windows: Failed to rename temp file %s to %s: %s" + % (newfile, file_path, err)) + try: + os.unlink(newfile) + except OSError: + err = sys.exc_info()[1] + self.logger.error( + "Windows: Could not remove temp file %s: %s" % + (newfile, err)) + return False + + def _exists(self, file_path): + """ check for existing paths and optionally remove them. if + the path exists, return the lstat of it """ + try: + return os.lstat(file_path) + except OSError: + return None + + def _remove(self, file_path, recursive=True): + """ Remove a Path entry, whatever that takes """ + if os.path.islink(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + if recursive: + shutil.rmtree(file_path) + else: + os.rmdir(file_path) + else: + os.unlink(file_path) diff --git a/src/lib/Bcfg2/Client/Tools/WinService.py b/src/lib/Bcfg2/Client/Tools/WinService.py new file mode 100755 index 0000000000..64d4b7b717 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/WinService.py @@ -0,0 +1,92 @@ +"""WinService support for Bcfg2.""" + +import subprocess + +import Bcfg2.Client.Tools +import Bcfg2.Client.XML + + +class WinService(Bcfg2.Client.Tools.SvcTool): + """WinService service support for Bcfg2.""" + name = 'WinService' + __handles__ = [('Service', 'windows')] + __req__ = {'Service': ['name', 'status']} + + def get_svc_command(self, service, action): + return "powershell.exe %s-Service %s" % (action, service.get('name')) + + def VerifyService(self, entry, _): + """Verify Service status for entry""" + + if entry.get('status') == 'ignore': + return True + + try: + output = self.cmd.run('powershell.exe (Get-Service %s).Status' % + (entry.get('name')), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False).stdout.splitlines()[0] + except IndexError: + self.logger.error("Service %s not an Windows service" % + entry.get('name')) + return False + + if output is None: + # service does not exist + entry.set('current_status', 'off') + status = False + elif output.lower() == 'running': + # service is running + entry.set('current_status', 'on') + if entry.get('status') == 'off': + status = False + else: + status = True + else: + # service is not running + entry.set('current_status', 'off') + if entry.get('status') == 'on': + status = False + else: + status = True + + return status + + def InstallService(self, entry): + """Install Service for entry.""" + if entry.get('status') == 'on': + cmd = "start" + elif entry.get('status') == 'off': + cmd = "stop" + return self.cmd.run(self.get_svc_command(entry, cmd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False).success + + def restart_service(self, service): + """Restart a service. + + :param service: The service entry to modify + :type service: lxml.etree._Element + :returns: Bcfg2.Utils.ExecutorResult - The return value from + :class:`Bcfg2.Utils.Executor.run` + """ + self.logger.debug('Restarting service %s' % service.get('name')) + restart_target = service.get('target', 'restart') + return self.cmd.run(self.get_svc_command(service, restart_target), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False) + + def FindExtra(self): + """Locate extra Windows services.""" + services = self.cmd.run("powershell.exe " + "\"Get-WMIObject win32_service -Filter " + "\\\"StartMode = 'auto'\\\"" + " | Format-Table Name -HideTableHeaders\"", + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False).stdout.splitlines() + return [Bcfg2.Client.XML.Element('Service', name=name, type='windows') + for name in services] diff --git a/src/lib/Bcfg2/Client/__init__.py b/src/lib/Bcfg2/Client/__init__.py index dc4dfb983e..c748aa2553 100644 --- a/src/lib/Bcfg2/Client/__init__.py +++ b/src/lib/Bcfg2/Client/__init__.py @@ -4,7 +4,6 @@ import sys import stat import time -import fcntl import socket import fnmatch import logging @@ -13,10 +12,11 @@ import copy import Bcfg2.Logger import Bcfg2.Options +import subprocess from Bcfg2.Client import XML from Bcfg2.Client import Proxy from Bcfg2.Client import Tools -from Bcfg2.Utils import locked, Executor, safe_input +from Bcfg2.Utils import Flock, Executor, safe_input from Bcfg2.version import __version__ # pylint: disable=W0622 from Bcfg2.Compat import xmlrpclib, walk_packages, any, all, cmp @@ -158,7 +158,7 @@ def __init__(self): self.tools = [] self.times = dict() self.times['initialization'] = time.time() - + self.lock = Flock(Bcfg2.Options.setup.lockfile, self.logger) if Bcfg2.Options.setup.bundle_quick: if (not Bcfg2.Options.setup.only_bundles and not Bcfg2.Options.setup.except_bundles): @@ -196,7 +196,11 @@ def run_probe(self, probe): self.logger.info("Running probe %s" % name) ret = XML.Element("probe-data", name=name, source=probe.get('source')) try: - scripthandle, scriptname = tempfile.mkstemp() + fileending = '' + if os.name == 'nt': + (interpreter, fileending) = \ + probe.attrib.get('interpreter').split('*') + (scripthandle, scriptname) = tempfile.mkstemp(suffix=fileending) if sys.hexversion >= 0x03000000: script = os.fdopen(scripthandle, 'w', encoding=Bcfg2.Options.setup.encoding) @@ -210,11 +214,17 @@ def run_probe(self, probe): else: script.write(probe.text.encode('utf-8')) script.close() - os.chmod(scriptname, - stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | - stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | - stat.S_IWUSR) # 0755 - rv = self.cmd.run(scriptname) + if os.name == 'nt': + rv = self.cmd.run([interpreter, scriptname], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=False) + else: + os.chmod(scriptname, + stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | + stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | + stat.S_IWUSR) # 0755 + rv = self.cmd.run(scriptname) if rv.stderr: self.logger.warning("Probe %s has error output: %s" % (name, rv.stderr)) @@ -409,15 +419,15 @@ def run(self): if not Bcfg2.Options.setup.no_lock: # check lock here try: - lockfile = open(Bcfg2.Options.setup.lockfile, 'w') - if locked(lockfile.fileno()): + if self.lock.islocked() and not self.lock.ownlock(): self.fatal_error("Another instance of Bcfg2 is running. " "If you want to bypass the check, run " "with the -O/--no-lock option") + else: + self.lock.acquire() except SystemExit: raise except: - lockfile = None self.logger.error("Failed to open lockfile %s: %s" % (Bcfg2.Options.setup.lockfile, sys.exc_info()[1])) @@ -425,16 +435,6 @@ def run(self): # execute the configuration self.Execute() - if not Bcfg2.Options.setup.no_lock: - # unlock here - if lockfile: - try: - fcntl.lockf(lockfile.fileno(), fcntl.LOCK_UN) - os.remove(Bcfg2.Options.setup.lockfile) - except OSError: - self.logger.error("Failed to unlock lockfile %s" % - lockfile.name) - if (not Bcfg2.Options.setup.file and not Bcfg2.Options.setup.bundle_quick): # upload statistics diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py index 1c2420ccf9..986d5183ae 100644 --- a/src/lib/Bcfg2/Compat.py +++ b/src/lib/Bcfg2/Compat.py @@ -72,6 +72,26 @@ except ImportError: import http.client as httplib +try: + import fcntl +except ImportError: + fcntl = None + +try: + import termios +except ImportError: + termios = None + +try: + import pwd +except ImportError: + pwd = None + +try: + import grp +except ImportError: + grp = None + try: unicode = unicode except NameError: diff --git a/src/lib/Bcfg2/Logger.py b/src/lib/Bcfg2/Logger.py index e5f316a186..7b859eba9e 100644 --- a/src/lib/Bcfg2/Logger.py +++ b/src/lib/Bcfg2/Logger.py @@ -1,14 +1,12 @@ """Bcfg2 logging support""" - import copy -import fcntl import logging import logging.handlers import math import socket import struct import sys -import termios +from Bcfg2.Compat import fcntl, termios import Bcfg2.Options logging.raiseExceptions = 0 diff --git a/src/lib/Bcfg2/Options/Types.py b/src/lib/Bcfg2/Options/Types.py index ad2e04f10f..37ecd7c026 100644 --- a/src/lib/Bcfg2/Options/Types.py +++ b/src/lib/Bcfg2/Options/Types.py @@ -3,9 +3,7 @@ import os import re -import pwd -import grp -from Bcfg2.Compat import literal_eval +from Bcfg2.Compat import literal_eval, pwd, grp _COMMA_SPLIT_RE = re.compile(r'\s*,\s*') diff --git a/src/lib/Bcfg2/Utils.py b/src/lib/Bcfg2/Utils.py index 2fdc0c3e0d..275614129a 100644 --- a/src/lib/Bcfg2/Utils.py +++ b/src/lib/Bcfg2/Utils.py @@ -1,8 +1,6 @@ """ Miscellaneous useful utility functions, classes, etc., that are used by both client and server. Stuff that doesn't fit anywhere else. """ - -import fcntl import logging import os import re @@ -11,7 +9,7 @@ import sys import subprocess import threading -from Bcfg2.Compat import input, any # pylint: disable=W0622 +from Bcfg2.Compat import input, any, fcntl # pylint: disable=W0622 class ClassName(object): @@ -86,18 +84,92 @@ def __str__(self): return "[%s]" % self.str -def locked(fd): - """ Acquire a lock on a file. +class Flock(object): + '''Class to handle creating and removing (pid) lockfiles''' - :param fd: The file descriptor to lock - :type fd: int - :returns: bool - True if the file is already locked, False - otherwise """ - try: - fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError: - return True - return False + # custom exceptions + class FileLockAcquisitionError(Exception): + """Exception if we fail to acquire the lock""" + pass + + class FileLockReleaseError(Exception): + """Exception if we fail to release the lock""" + pass + + # convenience callables for formatting + def addr(self): + """Function which returns the pid""" + return '%d' % (self.pid) + + def fddr(self): + """Function which returns the path and the pid""" + return '<%s %s>' % (self.path, self.addr()) + + def pddr(self, lock): + """Function which returns the path and the pid of a specific lock""" + return '<%s %s>' % (self.path, lock['pid']) + + def __init__(self, path, logger): + self.pid = os.getpid() + self.path = path + self.logger = logger + + def acquire(self): + """Acquire a lock, returning self if successful, False otherwise""" + if self.islocked(): + lock = self._readlock() + raise Exception("Previous lock detected: %s" % self.pddr(lock)) + try: + file_handle = open(self.path, 'w') + file_handle.write(self.addr()) + file_handle.close() + self.logger.debug('Acquired lock: %s' % self.fddr()) + except IOError, exception: + if os.path.isfile(self.path): + try: + os.unlink(self.path) + except OSError: + pass + raise (self.FileLockAcquisitionError( + "Error acquiring lock '%s': %s" % (self.fddr(), + exception))) + + def release(self): + """Release lock, returning self""" + if self.ownlock(): + try: + os.unlink(self.path) + self.logger.debug('Released lock: %s' % self.fddr()) + except Exception, exception: + raise(self.FileLockReleaseError( + "Error releasing lock: '%s': %s" % (self.fddr(), + exception))) + + def _readlock(self): + """Internal method to read lock info""" + lock = {} + file_handle = open(self.path) + data = file_handle.read().rstrip() + file_handle.close() + lock['pid'] = data + return lock + + def islocked(self): + """Check if we already have a lock""" + try: + lock = self._readlock() + return self.fddr() != self.pddr(lock) + except IOError: + return False + + def ownlock(self): + """Check if we own the lock""" + lock = self._readlock() + return (self.fddr() == self.pddr(lock)) + + def __del__(self): + """Magic method to clean up lock when program exits""" + self.release() class ExecutorResult(object): @@ -260,6 +332,19 @@ def run(self, command, inputdata=None, timeout=None, **kwargs): timer.cancel() +def locked(fd): + """ Acquire a lock on a file. + :param fd: The file descriptor to lock + :type fd: int + :returns: bool - True if the file is already locked, False + otherwise """ + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except IOError: + return True + return False + + def list2range(lst): ''' convert a list of integers to a set of human-readable ranges. e.g.: