From 3b923c2e3bb3c33b3de7a63301bee18aa61d674a Mon Sep 17 00:00:00 2001 From: William Brown Date: Wed, 14 Sep 2016 13:07:10 +1000 Subject: [PATCH] Ticket 48984 - Add lib389 paths module Bug Description: lib389 needs a way to consume the paths discovered by defaults.inf. This allows us to consume that in the new installer, and in test cases provided by lib389 Fix Description: Add the paths module that searches wellknown locations for defaults.inf. If not found, we error. At the moment, this assumes all installs will follow the defaults.inf, and in the future we will add support for this to read from dse.ldif if an instance is specified. https://fedorahosted.org/389/ticket/48984 Author: wibrown Review by: ??? --- lib389/__init__.py | 85 +++++++++++++++++------------- lib389/_constants.py | 20 +++---- lib389/cli_adm/instance.py | 3 +- lib389/instance/options.py | 48 +++++++++-------- lib389/paths.py | 126 +++++++++++++++++++++++++++++++++++++++++++++ lib389/tests/paths_test.py | 36 +++++++++++++ lib389/tools.py | 4 +- 7 files changed, 253 insertions(+), 69 deletions(-) create mode 100644 lib389/paths.py create mode 100644 lib389/tests/paths_test.py diff --git a/lib389/__init__.py b/lib389/__init__.py index e16db6d..d83a44e 100644 --- a/lib389/__init__.py +++ b/lib389/__init__.py @@ -77,10 +77,9 @@ from lib389.utils import ( escapeDNValue, update_newhost_with_fqdn, formatInfData, - get_sbin_dir, - get_bin_dir, ensure_bytes, ensure_str) +from lib389.paths import Paths # mixin # from lib389.tools import DirSrvTools @@ -99,7 +98,8 @@ RE_DBMONATTRSUN = re.compile(r'^([a-zA-Z]+)-([a-zA-Z]+)$') # My logger log = logging.getLogger(__name__) - +# Initiate the paths object here. Should this be part of the DirSrv class +# for submodules? def wrapper(f, name): """ Wrapper of all superclass methods using lib389.Entry. @@ -403,10 +403,11 @@ class DirSrv(SimpleLDAPObject): self.timeout = timeout self.confdir = None + self.ds_paths = Paths() + # Reset the args (py.test reuses the args_instance for each test case) - args_instance[SER_DEPLOYED_DIR] = os.environ.get('PREFIX', '/') - args_instance[SER_BACKUP_INST_DIR] = os.environ.get('BACKUPDIR', - DEFAULT_BACKUPDIR) + args_instance[SER_DEPLOYED_DIR] = os.environ.get('PREFIX', self.ds_paths.prefix) + args_instance[SER_BACKUP_INST_DIR] = os.environ.get('BACKUPDIR', DEFAULT_BACKUPDIR) args_instance[SER_ROOT_DN] = DN_DM args_instance[SER_ROOT_PW] = PW_DM args_instance[SER_HOST] = LOCALHOST @@ -467,6 +468,10 @@ class DirSrv(SimpleLDAPObject): self.log.debug('SER_SERVERID_PROP not provided') # The lack of this value basically rules it out in most cases self.isLocal = False + self.ds_paths = Paths() + else: + self.ds_paths = Paths(args[SER_SERVERID_PROP]) + # Do we have ldapi settings? # Do we really need .strip() on this? @@ -747,7 +752,7 @@ class DirSrv(SimpleLDAPObject): sysconfig_head = confdir privconfig_head = None else: - sysconfig_head = prefix + ENV_SYSCONFIG_DIR + sysconfig_head = self.ds_paths.initconfig_dir privconfig_head = os.path.join(os.getenv('HOME'), ENV_LOCAL_DIR) if not os.path.isdir(sysconfig_head): privconfig_head = None @@ -838,7 +843,7 @@ class DirSrv(SimpleLDAPObject): """ DirSrvTools.lib389User(user=DEFAULT_USER) - prog = get_sbin_dir(None, self.prefix) + CMD_PATH_SETUP_DS + prog = os.path.join(self.ds_paths.sbin_dir, CMD_PATH_SETUP_DS) if not os.path.isfile(prog): log.error("Can't find file: %r, removing extension" % prog) @@ -866,12 +871,9 @@ class DirSrv(SimpleLDAPObject): # This may conflict in some tests, we may need to use /etc/host # aliases or we may need to use server id self.krb5_realm.create_principal(principal='ldap/%s' % self.host) - ktab = '%s/etc/dirsrv/slapd-%s/ldap.keytab' % (self.prefix, - self.serverid) - self.krb5_realm.create_keytab(principal='ldap/%s' % self.host, - keytab=ktab) - with open('%s/etc/sysconfig/dirsrv-%s' % - (self.prefix, self.serverid), 'a') as sfile: + ktab = '%s/etc/dirsrv/slapd-%s/ldap.keytab' % (self.prefix, self.serverid) + self.krb5_realm.create_keytab(principal='ldap/%s' % self.host, keytab=ktab) + with open('%s/dirsrv-%s' % (self.ds_paths.initconfig_dir, self.serverid), 'a') as sfile: sfile.write("\nKRB5_KTNAME=%s/etc/dirsrv/slapd-%s/" "ldap.keytab\nexport KRB5_KTNAME\n" % (self.prefix, self.serverid)) @@ -943,10 +945,9 @@ class DirSrv(SimpleLDAPObject): (self.serverid, self.host, self.port)) # Now time to remove the instance - prog = get_sbin_dir(None, self.prefix) + CMD_PATH_REMOVE_DS + prog = os.path.join(self.ds_paths.sbin_dir, CMD_PATH_REMOVE_DS) if (not self.prefix or self.prefix == '/') and os.geteuid() != 0: - raise ValueError("Error: without prefix deployment it is " + - "required to be root user") + raise ValueError("Error: without prefix deployment it is required to be root user") cmd = "%s -i %s%s" % (prog, DEFAULT_INST_HEAD, self.serverid) self.log.debug("running: %s " % cmd) try: @@ -1446,21 +1447,35 @@ class DirSrv(SimpleLDAPObject): def get_ldif_dir(self): """Return the server instance ldif directory.""" - try: - ldif_dir = self.getEntry(DN_CONFIG).__getattr__('nsslapd-ldifdir') - except: - ldif_dir = self.ldifdir - - return ldif_dir + return self.ds_paths.ldif_dir def get_bak_dir(self): """Return the server instance ldif directory.""" - try: - bak_dir = self.getEntry(DN_CONFIG).__getattr__('nsslapd-bakdir') - except: - bak_dir = self.bakdir + return self.ds_paths.backup_dir + + def get_local_state_dir(self): + return self.ds_paths.local_state_dir + + def get_sysconf_dir(self): + return self.ds_paths.sysconf_dir + + def get_initconfig_dir(self): + return self.ds_paths.initconfig_dir + + def get_sbin_dir(self): + return self.ds_paths.sbin_dir + + def get_bin_dir(self): + return self.ds_paths.bin_dir + + def get_plugin_dir(self): + return self.ds_paths.plugin_dir + + def get_tmp_dir(self): + return self.ds_paths.tmp_dir - return bak_dir + def has_asan(self): + return self.ds_paths.asan_enabled # # Get entries @@ -2428,7 +2443,7 @@ class DirSrv(SimpleLDAPObject): @return - True if import succeeded """ DirSrvTools.lib389User(user=DEFAULT_USER) - prog = get_sbin_dir(None, self.prefix) + LDIF2DB + prog = os.path.join(self.ds_paths.sbin_dir, LDIF2DB) if not bename and not suffixes: log.error("ldif2db: backend name or suffix missing") @@ -2477,7 +2492,7 @@ class DirSrv(SimpleLDAPObject): @return - True if export succeeded """ DirSrvTools.lib389User(user=DEFAULT_USER) - prog = get_sbin_dir(None, self.prefix) + DB2LDIF + prog = os.path.join(self.ds_paths.sbin_dir, DB2LDIF) if not bename and not suffixes: log.error("db2ldif: backend name or suffix missing") @@ -2518,7 +2533,7 @@ class DirSrv(SimpleLDAPObject): @return - True if the restore succeeded """ DirSrvTools.lib389User(user=DEFAULT_USER) - prog = get_sbin_dir(None, self.prefix) + BAK2DB + prog = os.path.join(self.ds_paths.sbin_dir, BAK2DB) if not archive_dir: log.error("bak2db: backup directory missing") @@ -2546,7 +2561,7 @@ class DirSrv(SimpleLDAPObject): @return - True if the backup succeeded """ DirSrvTools.lib389User(user=DEFAULT_USER) - prog = get_sbin_dir(None, self.prefix) + DB2BAK + prog = os.path.join(self.ds_paths.sbin_dir, DB2BAK) if not archive_dir: log.error("db2bak: backup directory missing") @@ -2575,7 +2590,7 @@ class DirSrv(SimpleLDAPObject): @return - True if reindexing succeeded """ DirSrvTools.lib389User(user=DEFAULT_USER) - prog = get_sbin_dir(None, self.prefix) + DB2INDEX + prog = os.path.join(self.ds_paths.sbin_dir, DB2INDEX) if not bename and not suffixes: log.error("db2index: missing required backend name or suffix") @@ -2614,7 +2629,7 @@ class DirSrv(SimpleLDAPObject): @return - dumped string """ DirSrvTools.lib389User(user=DEFAULT_USER) - prog = get_bin_dir(None, self.prefix) + DBSCAN + prog = os.path.join(self.ds_paths.bin_dir, DBSCAN) if not bename: log.error("dbscan: missing required backend name") @@ -2843,7 +2858,7 @@ class DirSrv(SimpleLDAPObject): @raise - OSError """ try: - os.system('dbgen.pl -s %s -n %d -o %s' % (suffix, num, ldif_file)) + os.system('%s -s %s -n %d -o %s' % (os.path.join(self.ds_paths.bin_dir, 'dbgen.pl'), suffix, num, ldif_file)) os.chmod(ldif_file, 0o644) if os.getuid() == 0: # root user - chown the ldif to the server user diff --git a/lib389/_constants.py b/lib389/_constants.py index fbfb4ae..262d092 100644 --- a/lib389/_constants.py +++ b/lib389/_constants.py @@ -73,8 +73,8 @@ DN_MONITOR_SNMP = "cn=snmp,cn=monitor" DN_MONITOR_LDBM = "cn=monitor,cn=ldbm database,cn=plugins,cn=config" -CMD_PATH_SETUP_DS = "/setup-ds.pl" -CMD_PATH_REMOVE_DS = "/remove-ds.pl" +CMD_PATH_SETUP_DS = "setup-ds.pl" +CMD_PATH_REMOVE_DS = "remove-ds.pl" # State of an DirSrv object DIRSRV_STATE_INIT = 'initial' @@ -96,8 +96,8 @@ DEFAULT_ENV_HEAD = 'dirsrv-' DEFAULT_CHANGELOG_NAME = "changelog5" DEFAULT_CHANGELOG_DB = 'changelogdb' -CONF_DIR = 'etc/dirsrv' -ENV_SYSCONFIG_DIR = '/etc/sysconfig' +# CONF_DIR = 'etc/dirsrv' +# ENV_SYSCONFIG_DIR = '/etc/sysconfig' ENV_LOCAL_DIR = '.dirsrv' # CONFIG file (/etc/sysconfig/dirsrv-* or @@ -132,12 +132,12 @@ DN_MBO_TASK = "cn=memberOf task,%s" % DN_TASKS DN_TOMB_FIXUP_TASK = "cn=fixup tombstones,%s" % DN_TASKS # Script Constants -LDIF2DB = '/ldif2db' -DB2LDIF = '/db2ldif' -BAK2DB = '/bak2db' -DB2BAK = '/db2bak' -DB2INDEX = '/db2index' -DBSCAN = '/dbscan' +LDIF2DB = 'ldif2db' +DB2LDIF = 'db2ldif' +BAK2DB = 'bak2db' +DB2BAK = 'db2bak' +DB2INDEX = 'db2index' +DBSCAN = 'dbscan' RDN_REPLICA = "cn=replica" diff --git a/lib389/cli_adm/instance.py b/lib389/cli_adm/instance.py index 7c1ac75..d6a04ee 100644 --- a/lib389/cli_adm/instance.py +++ b/lib389/cli_adm/instance.py @@ -115,6 +115,7 @@ def create_parser(subparsers): list_parser = subcommands.add_parser('list', help="List installed instances of Directory Server") list_parser.set_defaults(func=instance_list) + list_parser.set_defaults(noinst=True) start_parser = subcommands.add_parser('start', help="Start an instance of Directory Server, if it is not currently running") # start_parser.add_argument('instance', nargs=1, help="The name of the instance to start.") @@ -141,5 +142,5 @@ By setting this value you acknowledge and take responsibility for the fact this example_parser = subcommands.add_parser('example', help="Display an example ini answer file, with comments") example_parser.set_defaults(func=instance_example) - create_parser.set_defaults(noinst=True) + example_parser.set_defaults(noinst=True) diff --git a/lib389/instance/options.py b/lib389/instance/options.py index 8561fcc..21d8c27 100644 --- a/lib389/instance/options.py +++ b/lib389/instance/options.py @@ -9,6 +9,7 @@ import socket import sys import os +from lib389.paths import Paths MAJOR, MINOR, _, _, _ = sys.version_info @@ -38,6 +39,7 @@ format_keys = [ 'tmp_dir', ] +ds_paths = Paths() class Options2(object): # This stores the base options in a self._options dict. @@ -135,15 +137,15 @@ class Slapd2Base(Options2): self._type['instance_name'] = str self._helptext['instance_name'] = "The name of the instance. Cannot be changed post installation." - self._options['user'] = 'dirsrv' + self._options['user'] = ds_paths.user self._type['user'] = str self._helptext['user'] = "The user account ns-slapd will drop privileges to during operation." - self._options['group'] = 'dirsrv' + self._options['group'] = ds_paths.group self._type['group'] = str self._helptext['group'] = "The group ns-slapd will drop privilleges to during operation." - self._options['root_dn'] = 'cn=Directory Manager' + self._options['root_dn'] = ds_paths.root_dn self._type['root_dn'] = str self._helptext['root_dn'] = "The Distinquished Name of the Administrator account. This is equivalent to root of your Directory Server." @@ -151,7 +153,7 @@ class Slapd2Base(Options2): self._type['root_password'] = str self._helptext['root_password'] = "The password for the root_dn account. " - self._options['prefix'] = os.environ.get('PREFIX', "") + self._options['prefix'] = ds_paths.prefix self._type['prefix'] = str self._helptext['prefix'] = "The filesystem prefix for all other locations. Unless you are developing DS, you likely never need to set this. This value can be reffered to in other fields with {prefix}, and can be set with the environment variable PREFIX." @@ -164,72 +166,76 @@ class Slapd2Base(Options2): self._helptext['secure_port'] = "The TCP port that Directory Server will listen on for TLS secured LDAP connections." # In the future, make bin and sbin /usr/[s]bin, but we may need autotools assistance from Ds - self._options['bin_dir'] = "{prefix}/bin" + self._options['bin_dir'] = ds_paths.bin_dir self._type['bin_dir'] = str self._helptext['bin_dir'] = "The location Directory Server can find binaries. You should not need to alter this value." - self._options['sbin_dir'] = "{prefix}/sbin" + self._options['sbin_dir'] = ds_paths.sbin_dir self._type['sbin_dir'] = str self._helptext['sbin_dir'] = "The location Directory Server can find systemd administration binaries. You should not need to alter this value." - self._options['sysconf_dir'] = "{prefix}/etc" + self._options['sysconf_dir'] = ds_paths.sysconf_dir self._type['sysconf_dir'] = str self._helptext['sysconf_dir'] = "The location of the system configuration directory. You should not need to alter this value." + self._options['initconfig_dir'] = ds_paths.initconfig_dir + self._type['initconfig_dir'] = str + self._helptext['initconfig_dir'] = "The location of the system rc configuration directory. You should not need to alter this value." + # In the future, make bin and sbin /usr/[s]bin, but we may need autotools assistance from Ds - self._options['data_dir'] = "{prefix}/share" + self._options['data_dir'] = ds_paths.data_dir self._type['data_dir'] = str self._helptext['data_dir'] = "The location of shared static data. You should not need to alter this value." - self._options['local_state_dir'] = "{prefix}/var" + self._options['local_state_dir'] = ds_paths.local_state_dir self._type['local_state_dir'] = str self._helptext['local_state_dir'] = "The location prefix to variable data. You should not need to alter this value." - self._options['lib_dir'] = "{prefix}/usr/lib64/dirsrv" + self._options['lib_dir'] = ds_paths.lib_dir self._type['lib_dir'] = str self._helptext['lib_dir'] = "The location to Directory Server shared libraries. You should not need to alter this value." - self._options['cert_dir'] = "{sysconf_dir}/dirsrv/slapd-{instance_name}" + self._options['cert_dir'] = ds_paths.cert_dir self._type['cert_dir'] = str self._helptext['cert_dir'] = "The location where NSS will store certificates." - self._options['config_dir'] = "{sysconf_dir}/dirsrv/slapd-{instance_name}" + self._options['config_dir'] = ds_paths.config_dir self._type['config_dir'] = str self._helptext['config_dir'] = "The location where dse.ldif and other configuration will be stored. You should not need to alter this value." - self._options['inst_dir'] = "{local_state_dir}/lib/dirsrv/slapd-{instance_name}" + self._options['inst_dir'] = ds_paths.inst_dir self._type['inst_dir'] = str self._helptext['inst_dir'] = "The location of the Directory Server databases, ldif and backups. You should not need to alter this value." - self._options['backup_dir'] = "{inst_dir}/bak" + self._options['backup_dir'] = ds_paths.backup_dir self._type['backup_dir'] = str self._helptext['backup_dir'] = "The location where Directory Server will export and import backups from. You should not need to alter this value." - self._options['db_dir'] = "{inst_dir}/db" + self._options['db_dir'] = ds_paths.db_dir self._type['db_dir'] = str self._helptext['db_dir'] = "The location where Directory Server will store databases. You should not need to alter this value." - self._options['ldif_dir'] = "{inst_dir}/ldif" + self._options['ldif_dir'] = ds_paths.ldif_dir self._type['ldif_dir'] = str self._helptext['ldif_dir'] = "The location where Directory Server will export and import ldif from. You should not need to alter this value." - self._options['lock_dir'] = "{local_state_dir}/lock/dirsrv/slapd-{instance_name}" + self._options['lock_dir'] = ds_paths.lock_dir self._type['lock_dir'] = str self._helptext['lock_dir'] = "The location where Directory Server will store lock files. You should not need to alter this value." - self._options['log_dir'] = "{local_state_dir}/log/dirsrv/slapd-{instance_name}" + self._options['log_dir'] = ds_paths.log_dir self._type['log_dir'] = str self._helptext['log_dir'] = "The location where Directory Server will write log files. You should not need to alter this value." - self._options['run_dir'] = "{local_state_dir}/run/dirsrv" + self._options['run_dir'] = ds_paths.run_dir self._type['run_dir'] = str self._helptext['run_dir'] = "The location where Directory Server will create pid files. You should not need to alter this value." - self._options['schema_dir'] = "{config_dir}/schema" + self._options['schema_dir'] = ds_paths.schema_dir self._type['schema_dir'] = str self._helptext['schema_dir'] = "The location where Directory Server will store and write schema. You should not need to alter this value." - self._options['tmp_dir'] = "/tmp" + self._options['tmp_dir'] = ds_paths.tmp_dir self._type['tmp_dir'] = str self._helptext['tmp_dir'] = "The location where Directory Server will write temporary files. You should not need to alter this value." diff --git a/lib389/paths.py b/lib389/paths.py new file mode 100644 index 0000000..73a0d97 --- /dev/null +++ b/lib389/paths.py @@ -0,0 +1,126 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2016 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import sys +import os + +MAJOR, MINOR, _, _, _ = sys.version_info + +if MAJOR >= 3: + import configparser +else: + import ConfigParser as configparser + +# Read the paths from default.inf + +# Create a lazy eval class for paths. When we first access, we re-read +# the inf. This way, if we never request a path, we never need to read +# this file. IE remote installs, we shouldn't need to read this. + +# Could this actually become a defaults module, and merge with instance/options? +# Would it handle the versioning requirements and diff we need? + +DEFAULTS_PATH = [ + '/usr/share/dirsrv/inf/defaults.inf', + '/usr/local/share/dirsrv/inf/defaults.inf', + '/opt/dirsrv/share/dirsrv/inf/defaults.inf', + '/opt/local/share/dirsrv/inf/defaults.inf', + '/opt/share/dirsrv/inf/defaults.inf', +] + +MUST = [ + 'product', + 'version', + 'user', + 'group', + 'root_dn', + 'prefix', + 'bin_dir', + 'sbin_dir', + 'lib_dir', + 'data_dir', + 'tmp_dir', + 'sysconf_dir', + 'config_dir', + 'schema_dir', + 'cert_dir', + 'local_state_dir', + 'run_dir', + 'lock_dir', + 'log_dir', + 'inst_dir', + 'db_dir', + 'backup_dir', + 'ldif_dir', + 'initconfig_dir', +] + +SECTION = 'slapd' + +class Paths(object): + def __init__(self, serverid=None): + """ + Parses and uses a set of default paths from wellknown locations. The list + of keys available is from the MUST attribute in this module. + + To use this module: + + p = Paths() + p.bindir + + If the defaults.inf is NOT in a wellknown path, this will throw IOError + on the first attribute access. If this does not have a value defaults.inf + it will raise KeyError that the defaults.inf is not capable of supporting + this tool. + + This is lazy evaluated, so the file is read at the "last minute" then + the contents are cached. This means that remote tools that don't need + to know about paths, shouldn't need to have a copy of 389-ds-base + installed to remotely admin a server. + """ + self._defaults_cached = False + self._config = None + self._serverid = serverid + + def _get_defaults_loc(self, search_paths): + for spath in search_paths: + if os.path.isfile(spath): + return spath + raise IOError('defaults.inf not found in any wellknown location!') + + def _read_defaults(self): + spath = self._get_defaults_loc(DEFAULTS_PATH) + self._config = configparser.SafeConfigParser() + self._config.read([spath]) + self._defaults_cached = True + + def _validate_defaults(self): + if self._defaults_cached is False: + return False + for k in MUST: + if self._config.has_option(SECTION, k) is False: + raise KeyError('Invalid defaults.inf, missing key %s' % k) + return True + + def __getattr__(self, name): + if self._defaults_cached is False: + self._read_defaults() + self._validate_defaults() + if self._serverid is not None: + return self._config.get(SECTION, name).format(instance_name=self._serverid) + else: + return self._config.get(SECTION, name) + + def asan_enabled(self): + if self._defaults_cached is False: + self._read_defaults() + self._validate_defaults() + if self._config.has_option(SECTION, 'asan_enabled'): + if self._config.get(SECTION, 'asan_enabled') == '1': + return True + return False diff --git a/lib389/tests/paths_test.py b/lib389/tests/paths_test.py new file mode 100644 index 0000000..1f86103 --- /dev/null +++ b/lib389/tests/paths_test.py @@ -0,0 +1,36 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2016 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- +# + +from lib389.paths import Paths + +# Test that we can retrieve the settings from the paths object +def test_paths(): + # Make the paths object. + p = Paths() + # Get a value! + v = p.version + +# Test that if we make the path object, and we don't read a path from it +# the filecache state is False +def test_path_noread(): + p = Paths() + assert(p._defaults_cached is False) + p._read_defaults() + assert(p._defaults_cached is True) + +def test_path_exception(): + # Trigger the internal path find with a "bad location" and + # make sure that we get the exception + p = Paths() + try: + p._get_defaults_loc(search_paths=[]) + assert(False) + except IOError: + assert(True) + diff --git a/lib389/tools.py b/lib389/tools.py index 4223774..3d0dce0 100644 --- a/lib389/tools.py +++ b/lib389/tools.py @@ -922,9 +922,9 @@ class DirSrvTools(object): else: prog = '' if args['have_admin']: - prog = get_sbin_dir(sroot, prefix) + PATH_SETUP_DS_ADMIN + prog = get_sbin_dir(sroot, prefix) + '/' + PATH_SETUP_DS_ADMIN else: - prog = get_sbin_dir(sroot, prefix) + PATH_SETUP_DS + prog = get_sbin_dir(sroot, prefix) + '/' + PATH_SETUP_DS if not os.path.isfile(prog): log.error("Can't find file: %r, removing extension" % prog) -- 1.8.3.1