#!/usr/bin/env python # Configure clustered Samba nodes # Copyright (C) Martin Schwenke 2010 # 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 3 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, see . import os import sys import optparse # Newer argparse not yet in RHEL5/EPEL. import ConfigParser import logging import re import subprocess import errno import filecmp import shutil import glob import string lib_dir = "lib" sys.path.append(lib_dir) import logging def process_args(): usage = "usage: %prog [options] configfile" parser = optparse.OptionParser(usage=usage) parser.add_option("-t", "--templates", action="store", dest="templates", default="templates/rhel", help="directory containing templates") parser.add_option("-f", "--force", action="store_true", dest="force", default=False, help="install configuration files even if unchanged",) parser.add_option("-r", "--no-reload", action="store_true", dest="no_reload", default=False, help="""don't run the "reload" script for services - this is usually done to make services reload their configuration after any changes""") parser.add_option("-n", "--simulate", action="store_true", dest="simulate", default=False, help="""don't actually install configuration files - this will leave the configuration files in the temporary staging area - implies -r""") parser.add_option("-s", "--staging", action="store", dest="staging", default="staging", help="directory to stage the files to be installed") parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0, help="print information and actions taken to stdout") parser.add_option("-l", "--log-file", action="store", dest="log_file", default=None, metavar="FILE", help="append information and actions taken to FILE") (options, args) = parser.parse_args() if len(args) != 1: parser.error("configuration file must be specified") options.config = args[0] return options def setup_logging(): global options logger = logging.getLogger("cluster-configure") logger.setLevel(logging.ERROR) sh = logging.StreamHandler(sys.stderr) sh.handleError = lambda x : (logging.shutdown(), sys.exit()) logger.addHandler(sh) if options.verbose == 1: logger.setLevel(logging.WARNING) elif options.verbose == 2: logger.setLevel(logging.INFO) elif options.verbose >= 3: logger.setLevel(logging.DEBUG) if options.log_file is not None: fh = logging.FileHandler(options.log_file) # The formatting option %(funcName)s would be useful here but # it only appeared in Python 2.5 and this script will be run # on Python 2.4. formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s") fh.setFormatter(formatter) logger.addHandler(fh) return logger def _rpm_parse_version(v): ret = [] for x in re.split("[^A-Za-z0-9]", v): try: ret.append(int(x)) except ValueError: ret.append(x) return ret config = None options = None logger = None class Package(object): def __init__(self, config, directory): self.config = config self.directory = directory self.version = None self.platform = None self.version_dir = None self.files = list() self.files_to_install = list() m = re.match("\d\d\.(.*)", directory) if m is None: raise RuntimeError, \ ("invalid template package directory %s" % directory) self.name = m.group(1) self.stage = os.path.join(options.staging, self.name) def _get_version(self): if os.path.exists("/etc/redhat-release"): p = subprocess.Popen(["rpm", "-q", self.name], stdout=subprocess.PIPE) out = p.communicate()[0] status = p.wait() if status == 0: out.replace(self.name + "-", "") self.version = string.strip(out) self.platform = "rpm" logger.debug("_get_version: package %s has version %s", self.name, self.version) return True elif os.path.exists("/etc/debian_version"): p = subprocess.Popen(["dpkg-query", "-W", "-f", "${Version}", self.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = p.communicate()[0] status = p.wait() if status == 0 and len(out) > 0: self.version = string.strip(out) self.platform = "deb" logger.debug("_get_version: package %s has version %s", self.name, self.version) return True logger.info("_get_version: package %s is not installed", self.name) return False # Currently Red Hat specific... def _check_version(self, min=None, max=None): v = _rpm_parse_version(self.version) return min is None or \ (_rpm_parse_version(min) <= v and \ (max is None or v < _rpm_parse_version(max))) def find_version_dir(self): if self._get_version(): pdir = os.path.join(options.templates, self.directory) versions = sorted(os.listdir(pdir)) # FIXME: filter out any without #, since they're meaningless. versions.reverse() # bare "#" comes last since it is the default for i in versions: try: (min, max) = map(lambda x: len(x) and x or None, i.split("#")) except ValueError: logger.warn("_find_version_dir: skipping invalid version subdirectory %s" % i) continue if self._check_version(min, max): self.version_dir = os.path.join(pdir, i) logger.info("_find_version_dir: found version directory %s" % self.version_dir) return True return False def _find_template_files(self, fdir): """Find the available templates in the given file directory fdir.""" for (root, dirs, files) in os.walk(fdir): for f in files: # os.path.relpath is not available in older Python frel = os.path.join(root, f).replace(fdir + "/", "", 1) logger.debug("_find_template_files: add template file %s" % frel) self.files.append(frel) def _substitute_template(self, contents): """Expand the given template fdir/file into the staging area.""" logger.debug("_expand_template: subsitute variables into %s", file) # Find variables in template and substitute values. variables = sorted(set(re.findall("!!((\w+)((=)([^!]*))?)!!", contents))) # r is the default replacement value for v. for (a, v, x, e, r) in variables: try: r = self.config.get("package:" + self.name, v) except (ConfigParser.NoSectionError, ConfigParser.NoOptionError), s: # This is the equals sign. if it is there then we have default. if e == "": raise contents = contents.replace("!!%s!!" % a, r) # Find plugin expressions in template and subsitute values. exprs = re.findall("!!%([^!]+)!!", contents) for e in exprs: (m, f, rest) = re.split("[:(]", e, 2) foo = 'plugins["%s"].%s(config, "%s", %s' % (m, f, self.name, rest) r = eval(foo) if r is None: return None contents = contents.replace("!!%%%s!!" % e, r) # Find general python expressions in template and subsitute values. exprs = re.findall("!!\|([^!]+)!!", contents) for e in exprs: r = eval(e) if r is None: return None contents = contents.replace("!!|%s!!" % e, r) return contents def _expand_template(self, fdir, file): """Expand the given template fdir/file into the staging area.""" logger.debug("_expand_template: subsitute variables into %s", file) # Read input file. src = os.path.join(fdir, file) f = open(src) contents = f.read() f.close() contents = self._substitute_template(contents) # Ensure output directory exists in staging area. dst = os.path.join(self.stage, file) try: os.makedirs(os.path.dirname(dst)) except OSError, exc: if exc.errno == errno.EEXIST: pass else: raise # Write output file into staging area, unless it is None, # which means to remove the file if it exists. if contents is not None: f = open(dst, "w") f.write(contents) f.close() else: try: os.remove(dst) except OSError, exc: if exc.errno == errno.ENOENT: pass else: raise def _would_install_file(self, file): """Check if a file should be installed from the staging area because it is different to the currently installed file (or if there is no installed file).""" src = os.path.join(self.stage, file) dst = os.path.join("/", file) if not os.path.exists(src) and not os.path.exists(dst): logger.debug("_would_install_file: skip install of %s (missing)", dst) return False try: if not options.force and filecmp.cmp(src, dst, shallow=False): logger.debug("_would_install_file: skip install of %s (unchanged)", dst) return False except OSError, exc: if exc.errno == errno.ENOENT: pass else: raise logger.info("_would_install_file: would install file %s", dst) return True def would_install_files(self): """For the templates in our packages files area, expand each template into the staging area and check if it would be installed due to a change. Return True if any files would be installed, False otherwise.""" fdir = os.path.join(self.version_dir, "files") shutil.rmtree(self.stage, ignore_errors=True) self._find_template_files(fdir) for f in self.files: self._expand_template(fdir, f) for f in self.files: if self._would_install_file(f): self.files_to_install.append(f) return self.files_to_install def _install_file(self, file): """Install file from the staging area .""" src = os.path.join(self.stage, file) dst = os.path.join("/", file) if os.path.exists(src): logger.info("_install_file: install file %s", dst) shutil.copy2(src, dst) else: logger.info("_install_file: remove file %s", dst) try: os.remove(dst) except OSError, exc: if exc.errno == errno.ENOENT: pass else: raise def install_files(self): """Install the list of files from self.files_to_install.""" for f in self.files_to_install: self._install_file(f) def run_event(self, event): """Run the given event script for the service, if present.""" es = os.path.join(self.version_dir, "events", event) if os.path.exists(es) and os.access(es, os.X_OK): p = subprocess.Popen([es], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out = p.communicate()[0] status = p.wait() if status == 0: logger.info("_run_event: successfully ran event script %s", es) else: logger.error("""_run_event: error running event script "%s": "%s" """, es , string.strip(out)) else: logger.debug("_run_event: no event script %s in %s", event, self.version_dir) plugins = {} def load_plugins(): global plugins plugin_dir = "plugins" sys.path.append(plugin_dir) for f in map(lambda x: os.path.splitext(os.path.basename(x))[0], glob.glob(os.path.join(plugin_dir, "*.py"))): plugins[f] = __import__(f) def ctdb_socket(): ret = os.getenv('CTDB_SOCKET') if ret is None: ctdb = '/usr/bin/ctdb' if os.path.exists(ctdb): cmd = "strings " + ctdb + " | grep -E '/ctdbd?\.socket$'" p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) out = p.communicate()[0] status = p.wait() if status == 0: ret = string.rstrip(out) else: logger.warning('Failed to find socket path in "' + ctdb + '" - falling back to default') else: logger.warning('Failed to find "' + ctdb + '" - falling back to default') if ret is None: ret = '/var/run/ctdb/ctdbd.socket' return ret def main(): global config, options, logger options = process_args() logger = setup_logging() load_plugins() os.environ["PATH"] = os.getcwd() + ":" + os.environ["PATH"] logger.debug("main: read configuration from %s", options.config) config = ConfigParser.SafeConfigParser() config.readfp(open(options.config)) # Run the check function in every plugin that defines it. config_status = True for p in plugins.iterkeys(): func = getattr(plugins[p], "check", None) if callable(func): if not func(config): config_status = False if not config_status: logger.error("main: exiting due to previous configuration errors") return 1 # Process templates. for d in sorted(os.listdir(options.templates)): try: p = Package(config, d) if p.find_version_dir(): if p.would_install_files() and not options.simulate: p.run_event("pre") p.install_files() p.run_event("post") except RuntimeError, s: logger.info("main: ignoring %s", s) logging.shutdown() if __name__ == '__main__': sys.exit(main())