URL: https://github.com/freeipa/freeipa/pull/215
Author: jumitche
 Title: #215: Add script to setup krb5 NFS exports
Action: synchronized

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/215/head:pr215
git checkout pr215
From 99c8c50dd7f1cf106b9480c1805339eb2382f18c Mon Sep 17 00:00:00 2001
From: Justin Mitchell <jumit...@redhat.com>
Date: Tue, 8 Nov 2016 11:15:57 +0000
Subject: [PATCH 1/2] Add script to setup krb5 NFS exports

---
 client/Makefile.am          |   1 +
 client/ipa-client-nfsexport | 814 ++++++++++++++++++++++++++++++++++++++++++++
 freeipa.spec.in             |   1 +
 3 files changed, 816 insertions(+)
 create mode 100755 client/ipa-client-nfsexport

diff --git a/client/Makefile.am b/client/Makefile.am
index 30adafd..8996fd5 100644
--- a/client/Makefile.am
+++ b/client/Makefile.am
@@ -45,6 +45,7 @@ sbin_PROGRAMS =			\
 sbin_SCRIPTS =			\
 	ipa-client-install	\
 	ipa-client-automount	\
+	ipa-client-nfsexport	\
 	ipa-certupdate		\
 	$(NULL)
 
diff --git a/client/ipa-client-nfsexport b/client/ipa-client-nfsexport
new file mode 100755
index 0000000..ef47942
--- /dev/null
+++ b/client/ipa-client-nfsexport
@@ -0,0 +1,814 @@
+#!/usr/bin/python -E
+#
+# Configure an IPA/AD client system to serve Kerberos NFS4
+#
+# Author: Justin Mitchell <jumit...@redhat.com>
+#
+# Copyright (C) 2016 Red Hat, 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 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 <http://www.gnu.org/licenses/>.
+#
+#
+## Clients must also do:
+# ipa service-add nfs/client.mydomain
+# ipa-getkeytab -s ipa.mydomain -p nfs/client.mydomain -k /etc/krb5.keytab
+# systemctl start nfs-client.target
+# optionally: ipa-client-automount
+
+from __future__ import print_function
+
+try:
+    import sys
+    import os
+    import time
+    import tempfile
+    import dns
+    import socket
+    import netaddr
+    import logging
+    import subprocess
+    import tempfile
+    import ConfigParser
+    import re
+
+    from dns import resolver, rdatatype
+    from dns.exception import DNSException
+    from argparse import ArgumentParser
+    from subprocess import CalledProcessError, check_output, check_call
+
+except ImportError as e:
+    print("""\
+There was a problem importing one of the required Python modules. The
+error was:
+
+    %s
+""" % e, file=sys.stderr)
+    sys.exit(1)
+
+
+class Paths:
+    """Collection of pathnames and executables to use"""
+    IPA_CLI = "/usr/bin/ipa"
+    IPA_GETKEYTAB = "/usr/sbin/ipa-getkeytab"
+    KLIST = "/usr/bin/klist"
+    KINIT = "/usr/bin/kinit"
+    IPA_DEFAULT_CONF = "/etc/ipa/default.conf"
+    RESOLV_CONF = "/etc/resolv.conf"
+    EXPORTS = "/var/lib/nfs/etab"
+    KEYTAB = "/etc/krb5.keytab"
+    EXPORTSFILE = "/etc/exports.d/krb5.exports"
+    EXPORTFS = "/usr/sbin/exportfs"
+    SYSTEMCTL = "/usr/bin/systemctl"
+    IPACONFIG = "/etc/ipa/default.conf"
+    KRB5CONFIG = "/etc/krb5.conf"
+    DNF = "/usr/bin/dnf"
+
+
+def parse_options():
+    parser = ArgumentParser()
+
+    parser.add_argument("--domain", dest="domain", help="domain name")
+    parser.add_argument("--server", dest="server", help="IPA server", action="append")
+    parser.add_argument("--export", dest="exports", help="NFS mount exports", action="append")
+    parser.add_argument("--realm", dest="realm", help="realm name")
+    parser.add_argument("--hostname", dest="hostname", help="The hostname of this machine (FQDN)")
+    parser.add_argument("--username", dest="username", help="Kerberos Username")
+    parser.add_argument("--force", action="store_true", 
+            help="Perform actions even if unneccessary")
+    parser.add_argument("-v", "--verbose", help="Increase Verbosity", action="count")
+    parser.add_argument("--automount", dest="automount", default=None, action="store_true", 
+            help="Configure mounts for automount use")
+    parser.add_argument("--noautomount", dest="automount", default=None, action="store_false", 
+            help="Do not configure mounts for automount use")
+
+    options = parser.parse_args()
+
+    if options.verbose > 0:
+        logging.getLogger().setLevel(logging.DEBUG)
+
+    return options
+
+
+def have_keytab( hostname, service='host', realm=None ):
+    """Test if we have been configured for any realm by the existance
+        of a host key in the default keytab"""
+
+    principal = '%s/%s' % (service, hostname)
+    if realm:
+        principal = '%s/%s@%s' % (service, hostname, realm.upper())
+
+    logging.debug("Checking for principal %s in keytab" % principal )
+
+    try:
+        out = subprocess.check_output([ Paths.KLIST, '-k'], stderr=devnull)
+
+        if out.find(principal) != -1:
+                return True
+
+    except CalledProcessError as e:
+        logging.debug("klist Error ret=%d %s" , e.returncode, e)
+        return False
+
+    return False
+
+def get_hostname():
+    """Get the system hostname"""
+    return socket.getfqdn()
+
+def valid_ip(addr):
+    """Test if {addr} is valid format for an IP address"""
+    return netaddr.valid_ipv4(addr) or netaddr.valid_ipv6(addr)
+
+def get_resolver_domains():
+    """Extract any likely looking domains from resolv.conf"""
+    domains = []
+    domain = None
+    try:
+        fp = open(Paths.RESOLV_CONF, 'r')
+        lines = fp.readlines()
+        fp.close()
+
+        for line in lines:
+            if line.lower().startswith('domain'):
+                domain = line.split()[-1]
+            elif line.lower().startswith('search'):
+                domains += line.split()[1:]
+    except:
+        pass
+
+    logging.debug(" resolv.conf search domains: " + str(domains))
+    if domain:
+        logging.debug(" - resolv.conf domain: " + domain)
+        domains.append(domain)
+    return domains
+
+            
+def dns_search_srv(domain, srv_record_name, default_port):
+    """Search for SRV records in the domain"""
+    qname = '%s.%s' % (srv_record_name, domain)
+    try:
+        answers = resolver.query(qname, rdatatype.SRV)
+    except DNSException, e:
+        answers = []
+
+    logging.debug("  SRV query for " + qname )
+
+    servers = []
+    for answer in answers:
+        logging.debug("   - target=" + str(answer.target) + " port=" + str(answer.port))
+        host = str(answer.target).rstrip(".")
+        if not host:
+            continue;
+
+        if default_port is not None and answer.port != default_port:
+            host = "%s:%s" % (host, str(answer.port))
+        servers.append( host )
+    if not answers:
+        logging.debug("   - No answers")
+
+    return servers
+
+        
+def search_servers( domain ):
+    """Build a list of possible domain names,
+       then search it for LDAP servers """
+    domains = []
+
+    if domain and not valid_ip(domain):
+        p = domain.find(".")
+        if p != -1:
+            domains.append( domain.lower() )
+
+    rd = get_resolver_domains()
+    for d in rd:
+        if d.lower() not in domains:
+            domains.append(d.lower())
+
+    logging.debug("LDAP Search Domain List: %s", domains)
+
+    servers = []
+    tried = set()
+    for d in domains:
+        if d in tried:
+            continue
+        tried.add(d)
+
+        p = d.find(".")
+        while p != -1:
+            found = dns_search_srv(d, '_ldap._tcp', 389)
+            if found:
+                for f in found:
+                    servers.append( f )
+                break
+            d = d[p+1:]
+            p = d.find(".")
+
+    return servers
+
+def search_realm(hostname):
+    """Search dns for kerberos TXT records"""
+    logging.debug("Searching DNS for Kerberos Realm...")
+
+    domain = None
+    realm = None
+    if hostname and not valid_ip(hostname):
+        p = hostname.find(".")
+        if p != -1:
+            domain = hostname[p+1:]
+
+    if not domain:
+        raise ValueError("bad hostname")
+
+    qname = "_kerberos." + domain
+    if not qname.endswith('.'):
+        qname += '.'
+
+    logging.debug("  TXT query for %s" , qname)
+    try:
+        answers = resolver.query(qname, rdatatype.TXT)
+    except DNSException, e:
+        raise
+
+    for answer in answers:
+        logging.debug("   - Answer: %s" , answer.strings)
+        try:
+            return answer.strings[0]
+        except:
+            pass
+
+    raise RuntimeError("Not found")
+
+
+def user_input(prompt, default=None, allow_empty=True):
+    """Prompt the user for some input, with optional default value"""
+
+    if isinstance(default, basestring):
+        ret = raw_input("%s [%s]: " % (prompt, default))
+        if not ret.strip():
+            return default
+        else:
+            return ret
+    elif isinstance(default, bool):
+        if default:
+            choice = "yes"
+        else:
+            choice = "no"
+        while True:
+            ret = raw_input("%s [%s]: " % (prompt, choice))
+            if not ret.strip():
+                return default
+            elif ret.lower()[0] == "y":
+                return True
+            elif ret.lower()[0] == "n":
+                return False
+    elif isinstance(default, int):
+        while True:
+            try:
+                ret = raw_input("%s [%s]: " % (prompt, choice))
+                if not ret.strip():
+                    return default
+                ret = int(ret)
+            except ValueError:
+                pass
+            else:
+                return ret
+    else:
+        while True:
+            ret = raw_input("%s: " % prompt)
+            if allow_empty or (ret and ret.strip()):
+                return ret
+
+def search_exports():
+    """Grab a list of all currently exported domains"""
+    exports = []
+    try:
+        fp = open(Paths.EXPORTS, 'r')
+        lines = fp.readlines()
+        fp.close()
+
+        logging.debug("Searching exports file %s", Paths.EXPORTS)
+        for line in lines:
+            if line.lower().startswith('#'):
+                continue
+            domain = line.split()[0]
+            if not domain in exports:
+                exports.append(domain)
+        return exports
+    except:
+        raise
+
+def krb5_fetchinfo():
+    """Try and extract info from klist"""
+    try:
+        cmd = [ Paths.KLIST ]
+        ret = check_output(cmd)
+        matches = re.findall(r'Default principal: (.*)@(.*)\n', ret)
+        logging.debug("results: %s" % matches)
+        return dict( username=matches[0][0], realm=matches[0][1] )
+    except:
+        logging.debug("klist extract failed: %s", sys.exc_info()[1])
+
+
+def krb5_valid(ccache=None):
+    """Test we have a TGT cached"""
+    try:
+        cmd = [ Paths.KLIST, '-s' ]
+        if ccache:
+            cmd += [ '-c', ccache ];
+        ret = check_call(cmd)
+        if ret > 0:
+            logging.debug("No current valid keys found in ccache")
+            return False
+        else:
+            logging.debug("Found valid ccache")
+            return True
+    except:
+        logging.debug("klist error: %s", sys.exc_info())
+        return False
+
+def krb5_init( username, realm=None, force=False, ccache=None):
+    """Test and login to realm"""
+    if krb5_valid(ccache) and not force:
+        return
+
+    if realm:
+        principal = "%s@%s" % (username, realm)
+    else:
+        principal = username
+
+    try:
+        cmd = [ Paths.KINIT ]
+        if ccache:
+            cmd += [ '-c', ccache ]
+        cmd += [ principal ]
+        ret = check_call(cmd, stdin=sys.stdin, stdout=sys.stdout)
+        if ret != 0:
+            logging.error("kinit returned %d", ret)
+    except CalledProcessError as e:
+        raise
+
+
+def ipa_service_exists( hostname, service='nfs', realm=None):
+    """Ask IPA if this service principal exists"""
+    principal = '%s/%s' % ( service, hostname )
+    try: 
+        out = check_output([ Paths.IPA_CLI, 'service-show', principal ], stderr=devnull)
+        return True
+
+    except CalledProcessError as e:
+        logging.debug("Failed to find service %s" % principal)
+        return False
+
+def ipa_service_add( hostname, service='nfs', realm=None, force=False):
+    """Create this service principal via IPA"""
+    principal = '%s/%s' % ( service, hostname )
+    logging.debug("Adding service %s" % principal)
+    cmd = [ Paths.IPA_CLI, 'service-add', principal ]
+    if force:
+        cmd.append('--force')
+    try: 
+        out = check_output( cmd )
+
+    except CalledProcessError as e:
+        logging.error("'%s' failed with retcode %d: %s" % (cmd[0], e.returncode, e.output))
+        raise
+
+def fetch_keytab(hostname, service='nfs', server=None, keytab=None):
+    """Fetch a service key"""
+    principal = '%s/%s' % (service, hostname)
+    cmd = [ Paths.IPA_GETKEYTAB, '-p', principal ]
+    if server:
+        cmd += [ '-s', server ]
+    if keytab:
+        cmd += [ '-k', keytab ]
+
+    try: 
+        out = check_output(cmd)
+    except:
+        raise
+
+
+def service_restart(service, force=False):
+    """Check if a systemd service is enabled, if needed enable and run it"""
+
+    # Is it already enabled, or do we not care
+    enabled = False
+    if not force:
+        cmd = [ Paths.SYSTEMCTL, 'is-enabled', service ]
+        try:
+            ret = check_call(cmd, stdout=devnull, stderr=devnull)
+            if ret == 0:
+                enabled = True
+        except CalledProcessError as e:
+            pass
+
+    # Enable it
+    if not enabled:
+        cmd = [ Paths.SYSTEMCTL, 'enable', service ]
+        try:
+            ret = check_call(cmd, stdout=devnull, stderr=devnull)
+        except CalledProcessError as e:
+            logging.error("Error enabling service %s" % service)
+            raise
+   
+    # run/restart it
+    if force:
+        cmd = [ Paths.SYSTEMCTL, 'restart', service ]
+    else:
+        cmd = [ Paths.SYSTEMCTL, 'try-restart', service ]
+        
+    try:
+        ret = check_call(cmd, stdout=devnull, stderr=devnull)
+    except CalledProcessError as e:
+        logging.error("Error restarting service %s" % service)
+        raise
+
+
+def update_exports():
+    """Update the exports with exportfs"""
+    cmd = [ Paths.EXPORTFS , '-a' ]
+    logging.debug("Updating exports...")
+    try:
+        ret = check_output( cmd )
+        logging.debug(ret)
+    except:
+        raise
+
+def load_ipa_config(pathname):
+    """Parse an IPA config file and return a dict of the values we found"""
+    config = ConfigParser.ConfigParser()
+
+    values = dict()
+    try:
+        config.read([pathname])
+    except:
+        raise
+
+    try:
+        values['realm'] = config.get('global', 'realm')
+    except:
+        pass
+
+    try:
+        values['server'] = config.get('global', 'server')
+    except:
+        pass
+
+    try:
+        values['hostname'] = config.get('global', 'host')
+    except:
+        pass
+
+    if not values:
+        raise EOFError('Empty Config')
+
+    return values
+
+
+def mapadd( hostname, directory ):
+    parts = directory.rsplit('/', 1)
+    if parts[1]:
+        mapname = 'auto.%s' % parts[1]
+    else:
+        mapname = 'auto.%s' % directory
+
+    logging.debug("Adding map '%s'  %s:%s" % (mapname, hostname, directory))
+
+    try:
+        # Create the map
+        logging.debug("Create map '%s'" % mapname)
+        cmd = [ Paths.IPA_CLI, 'automountmap-add', 'default', mapname ]
+        ret = check_output(cmd, stderr=devnull)
+
+        # set the directory and make a sub of auto.master
+        logging.debug("Add map directory '%s'" % directory)
+        cmd = [ Paths.IPA_CLI, 'automountkey-add', 'default', '--key', directory, '--info', mapname, 'auto.master' ]
+        ret = check_output(cmd, stderr=devnull)
+
+        # Now set the mapping
+        logging.debug("Set map key '%s'" % hostname)
+        cmd = [ Paths.IPA_CLI, 'automountkey-add', 'default', '--key', '*', '--info', "-fstype=nfs4,rw,sec=krb5p,soft,rsize=8192,wsize=8192 %s:%s/&" % (hostname, directory), mapname ]
+        ret = check_output(cmd, stderr=devnull)
+
+    except CalledProcessError as e:
+        logging.debug("Error creating map %s: %s " % (mapname, e) )
+        raise
+
+
+
+
+def main():
+    env={"PATH":"/bin:/sbin:/usr/kerberos/bin:/usr/kerberos/sbin:/usr/bin:/usr/sbin"}
+
+    tool = 'yum'
+    if os.path.exists( Paths.DNF ):
+        tool = 'dnf'
+
+    # basic sanity checks first
+    if not os.path.exists( Paths.IPA_CLI ):
+        logging.error("%s not found. Try '%s install ipa-admintools' first" % (Paths.IPA_CLI, tool))
+        sys.exit(1)
+
+    if not os.path.exists( Paths.IPA_GETKEYTAB ):
+        logging.error("%s not found. Try '%s install ipa-client' first" % (Paths.IPA_GETKEYTAB, tool))
+        sys.exit(1)
+
+    if not os.path.exists( Paths.KLIST ):
+        logging.error("%s not found. Try '%s install krb5-workstation' first" % (Paths.KLIST, tool))
+        sys.exit(1)
+
+    if not os.path.exists( Paths.KINIT ):
+        logging.error("%s not found. Try '%s install krb5-workstation' first" % ( Paths.KINIT, tool))
+        sys.exit(1)
+
+    if not os.path.exists( Paths.EXPORTFS ):
+        logging.error("%s not found. Try '%s install nfs-utils' first" % ( Paths.EXPORTFS, tool))
+        sys.exit(1)
+
+
+
+    # Check for cmdline options
+    options = parse_options()
+
+    # commandline provided options take precidence, so assign them first
+    hostname = options.hostname
+    realm = options.realm
+    username = options.username
+    servers = []
+    exports = []
+
+    if options.server:
+        servers.append( options.server )
+
+    # Can we get hostname, username, realm, etc from the ipa config file ?
+    ipaconf = dict()
+    try:
+        ipaconf = load_ipa_config(Paths.IPACONFIG)
+    except:
+        logging.debug("load config failed: %s" % sys.exc_value);
+        pass
+
+    # If we have them, try using the ipa config values next
+    if not hostname:
+        try: 
+            hostname = ipaconf['hostname']
+        except:
+            pass
+
+    if not realm:
+        try:
+            realm = ipaconf['realm']
+        except:
+            pass
+
+    if not servers:
+        try:
+            newserver = ipaconf['server']
+            servers.append( newserver )
+        except:
+            pass
+
+
+    # still no luck with hostname, ask the system
+    if not hostname:
+        hostname = get_hostname()
+
+    # All attempts have failed, give in and ask the user
+    if not hostname:
+        print("Unable to determine hostname, and not provided by --hostname")
+        hostname = user_input("Enter hostname", allow_empty=False)
+
+
+    # Check if we have a key cached for this hostname
+    # if not then we probably dont have IPA/AD setup yet
+    if not have_keytab(hostname, 'host'):
+        sys.exit("Host key not found. run ipa-client-install first ?")
+
+    # We still don't know the realm, check in DNS
+    if not realm:
+        try:
+            realm = search_realm(hostname)
+        except:
+            pass
+
+    # Maybe we have signed in already and that can tell us?
+    if not realm:
+        logging.debug("Checking klist for realm info")
+        try:
+            kinfo = krb5_fetchinfo()
+            realm = kinfo['realm']
+            if not username:
+                username = kinfo['username']
+        except:
+            pass
+
+    # We cant find a realm so ask the user
+    if not realm:
+        domain = None
+        if hostname and not valid_ip(hostname):
+            p = hostname.find(".")
+            if p != -1:
+                domain = hostname[p+1:]
+        print("Unable to determine realm, and not provided by --realm")
+        realm = user_input("Kerberos Realm", allow_empty=False, default=domain.upper())
+
+
+    # Not manual, check in DNS for it
+    if not servers:
+        logging.debug("Searching for IPA/LDAP servers...")
+        servers += search_servers( realm )
+
+    # still havent found it, demand one
+    if not servers:
+        print("Unable to determine IPA/LDAP server, and not provided by --server")
+        servers.append( user_input("IPA/LDAP Server", allow_empty=False) )
+
+    # Grab a list of what is already exported on this system
+    try:
+        exported = search_exports()
+    except:
+        pass
+
+    # Has the user given a manual list of directories to export
+    if options.exports:
+        exports += options.exports
+
+    # Ask the user for some exports
+    if not exports:
+        print("Enter any directories to export... Enter to finish")
+        while True:
+            e = user_input("Add export")
+            if not e:
+                break
+            exports.append(e)
+
+    realm = realm.upper()
+
+    if exports and options.automount is None:
+        print("Do you wish to enable automount ability for these mounts?")
+        options.automount = user_input("Configure automount", default=False)
+
+    # summary of results
+    print()
+    print("Setting up Kerberized NFS with the following settings:")
+    print("Hostname: " , hostname )
+    print("Realm: " , realm )
+    print("Server List: " , servers  )
+    print("Automount: ", options.automount )
+
+    mountlist = exports[:]
+
+    # lets sanity check the exports list whilst we are printing it
+    if not exports:
+        print("Skipping directory exports")
+    else:
+        print("Exports List: ")
+        for d in exports[:]:
+            response = None
+            if d in exported:
+                response = 'Already exported'
+            elif not os.path.exists(d):
+                response = 'does not exist'
+                mountlist.remove(d)
+            elif not os.path.isdir(d):
+                response = 'Not a directory'
+                mountlist.remove(d)
+
+            if response:
+                if not options.force:
+                    response += ", Ignored"
+                    exports.remove(d)
+                print(" - %s (%s)" % ( d, response ))
+            else:
+                print(" - %s" % d)
+
+
+    # Ask if this seems okay
+    print()
+    if not user_input("Continue to configure the system with these values?", default=False):
+        print("Abandoning.")
+        sys.exit(0)
+
+    # Okay, lets do it then...
+
+    # If they are not signed in then use a temporary ccache
+    ccache = None
+    if not krb5_valid():
+        ccache_dir = tempfile.mkdtemp(prefix='krbcc')
+        ccache = os.path.join(ccache_dir, 'ccache')
+
+    if not krb5_valid(ccache=ccache) and not username:
+        print("Enter principal that has permission to add services to this realm")
+        username = user_input("Admin username", allow_empty=False)
+
+    # Make sure we are signed in
+    if not krb5_valid(ccache=ccache):
+        try:
+            krb5_init(username, realm, force=options.force, ccache=ccache)
+        except:
+            if not options.force:
+                sys.exit(1)
+        if ccache:
+            os.environ['KRB5CCNAME'] = ccache
+
+    # Check if there is an nfs service key, create if we have to
+    if options.force or not ipa_service_exists(hostname, service='nfs'):
+        try:
+            ipa_service_add(hostname, service='nfs', force=options.force)
+        except:
+            if not options.force:
+                sys.exit(1)
+    else:
+        logging.info("Service nfs/%s already exists" % hostname)
+
+    # check if we have the nfs server key, fetch it if we dont
+    if options.force or not have_keytab(hostname, service='nfs', realm=realm):
+        logging.debug("Fetching keytab entry")
+        try:
+            fetch_keytab(hostname, service='nfs', server=servers[0], keytab=Paths.KEYTAB)
+        except CalledProcessError as e:
+            logging.debug("'%s' failed with retcode %d: %s" % (e.cmd, e.returncode, e.output))
+
+    else:
+        logging.info("Already have the keytab cached, skipping")
+
+    # Check if the directories we wish to export are already exported
+    if not exports:
+        logging.info("No directories to export")
+
+    # This is somewhat naieve for now, creates a new exports.d file
+    for d in exports:
+        if options.force or d not in exported:
+            logging.debug("Exporting %s", d)
+            fp = open(Paths.EXPORTSFILE, 'a')
+            fp.write( "%s *(rw,sec=sys:krb5:krb5i:krb5p)\n" % ( d ) )
+            fp.close()
+        else:
+            logging.debug("Path %s is already exported, skipping", d)
+    
+    if options.force or exports:
+        try:
+            update_exports()
+        except CalledProcessError as e:
+            logging.error("'%s' failed with retcode %d: %s" % (e.cmd, e.returncode, e.output))
+
+    if options.automount:
+        logging.debug("Configuring automount")
+        for d in mountlist:
+            try:
+                mapadd( hostname, d )
+            except:
+                logging.error("Adding automount map for %s failed" % d)
+
+
+    # Restart any services
+    try:
+        service_restart('nfs-server', force=options.force)
+    except:
+        pass
+
+
+    # Clean up any temporary stuff we made
+    try:
+        if ccache:
+            os.unlink(ccache)
+    except:
+        pass
+
+    try:
+        if ccache_dir:
+            os.rmdir(ccache_dir)
+    except:
+        pass
+
+    print("Finished.")
+
+
+
+# Setup the logger, default to only error messages
+logging.basicConfig(level=logging.INFO, format='%(message)s')
+
+# use this to suppress error messages from subprocesses	
+devnull = open(os.devnull, 'w')
+
+# boilerplate to launch main and handle the fallout
+try:
+    if __name__ == "__main__":
+        sys.exit(main())
+except SystemExit as e:
+    sys.exit(e)
+except KeyboardInterrupt:
+    sys.exit(1)
+except RuntimeError as e:
+    sys.exit(e)
diff --git a/freeipa.spec.in b/freeipa.spec.in
index fbe7ff9..271d9d2 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -1269,6 +1269,7 @@ fi
 %license COPYING
 %{_sbindir}/ipa-client-install
 %{_sbindir}/ipa-client-automount
+%{_sbindir}/ipa-client-nfsexport
 %{_sbindir}/ipa-certupdate
 %{_sbindir}/ipa-getkeytab
 %{_sbindir}/ipa-rmkeytab

From 5ca055d2b7ced3bc06a4077ff0b4c08ca10761ef Mon Sep 17 00:00:00 2001
From: Justin Mitchell <jumit...@redhat.com>
Date: Thu, 10 Nov 2016 16:07:58 +0000
Subject: [PATCH 2/2] Clean up the script to pass pylint cleanly

---
 client/ipa-client-nfsexport | 162 +++++++++++++++++++++-----------------------
 1 file changed, 78 insertions(+), 84 deletions(-)

diff --git a/client/ipa-client-nfsexport b/client/ipa-client-nfsexport
index ef47942..9a0e244 100755
--- a/client/ipa-client-nfsexport
+++ b/client/ipa-client-nfsexport
@@ -31,9 +31,7 @@ from __future__ import print_function
 try:
     import sys
     import os
-    import time
     import tempfile
-    import dns
     import socket
     import netaddr
     import logging
@@ -57,7 +55,7 @@ error was:
     sys.exit(1)
 
 
-class Paths:
+class Paths(object):
     """Collection of pathnames and executables to use"""
     IPA_CLI = "/usr/bin/ipa"
     IPA_GETKEYTAB = "/usr/sbin/ipa-getkeytab"
@@ -84,12 +82,12 @@ def parse_options():
     parser.add_argument("--realm", dest="realm", help="realm name")
     parser.add_argument("--hostname", dest="hostname", help="The hostname of this machine (FQDN)")
     parser.add_argument("--username", dest="username", help="Kerberos Username")
-    parser.add_argument("--force", action="store_true", 
+    parser.add_argument("--force", action="store_true",
             help="Perform actions even if unneccessary")
     parser.add_argument("-v", "--verbose", help="Increase Verbosity", action="count")
-    parser.add_argument("--automount", dest="automount", default=None, action="store_true", 
+    parser.add_argument("--automount", dest="automount", default=None, action="store_true",
             help="Configure mounts for automount use")
-    parser.add_argument("--noautomount", dest="automount", default=None, action="store_false", 
+    parser.add_argument("--noautomount", dest="automount", default=None, action="store_false",
             help="Do not configure mounts for automount use")
 
     options = parser.parse_args()
@@ -108,7 +106,7 @@ def have_keytab( hostname, service='host', realm=None ):
     if realm:
         principal = '%s/%s@%s' % (service, hostname, realm.upper())
 
-    logging.debug("Checking for principal %s in keytab" % principal )
+    logging.debug("Checking for principal %s in keytab", principal )
 
     try:
         out = subprocess.check_output([ Paths.KLIST, '-k'], stderr=devnull)
@@ -117,7 +115,7 @@ def have_keytab( hostname, service='host', realm=None ):
                 return True
 
     except CalledProcessError as e:
-        logging.debug("klist Error ret=%d %s" , e.returncode, e)
+        logging.debug("klist Error ret=%d %s", e.returncode, e)
         return False
 
     return False
@@ -144,7 +142,7 @@ def get_resolver_domains():
                 domain = line.split()[-1]
             elif line.lower().startswith('search'):
                 domains += line.split()[1:]
-    except:
+    except StandardError:
         pass
 
     logging.debug(" resolv.conf search domains: " + str(domains))
@@ -153,13 +151,13 @@ def get_resolver_domains():
         domains.append(domain)
     return domains
 
-            
+
 def dns_search_srv(domain, srv_record_name, default_port):
     """Search for SRV records in the domain"""
     qname = '%s.%s' % (srv_record_name, domain)
     try:
         answers = resolver.query(qname, rdatatype.SRV)
-    except DNSException, e:
+    except DNSException:
         answers = []
 
     logging.debug("  SRV query for " + qname )
@@ -169,7 +167,7 @@ def dns_search_srv(domain, srv_record_name, default_port):
         logging.debug("   - target=" + str(answer.target) + " port=" + str(answer.port))
         host = str(answer.target).rstrip(".")
         if not host:
-            continue;
+            continue
 
         if default_port is not None and answer.port != default_port:
             host = "%s:%s" % (host, str(answer.port))
@@ -179,7 +177,7 @@ def dns_search_srv(domain, srv_record_name, default_port):
 
     return servers
 
-        
+
 def search_servers( domain ):
     """Build a list of possible domain names,
        then search it for LDAP servers """
@@ -221,7 +219,6 @@ def search_realm(hostname):
     logging.debug("Searching DNS for Kerberos Realm...")
 
     domain = None
-    realm = None
     if hostname and not valid_ip(hostname):
         p = hostname.find(".")
         if p != -1:
@@ -237,14 +234,14 @@ def search_realm(hostname):
     logging.debug("  TXT query for %s" , qname)
     try:
         answers = resolver.query(qname, rdatatype.TXT)
-    except DNSException, e:
+    except DNSException:
         raise
 
     for answer in answers:
         logging.debug("   - Answer: %s" , answer.strings)
         try:
             return answer.strings[0]
-        except:
+        except LookupError:
             pass
 
     raise RuntimeError("Not found")
@@ -305,7 +302,7 @@ def search_exports():
             if not domain in exports:
                 exports.append(domain)
         return exports
-    except:
+    except StandardError:
         raise
 
 def krb5_fetchinfo():
@@ -314,9 +311,9 @@ def krb5_fetchinfo():
         cmd = [ Paths.KLIST ]
         ret = check_output(cmd)
         matches = re.findall(r'Default principal: (.*)@(.*)\n', ret)
-        logging.debug("results: %s" % matches)
+        logging.debug("results: %s" , matches)
         return dict( username=matches[0][0], realm=matches[0][1] )
-    except:
+    except StandardError:
         logging.debug("klist extract failed: %s", sys.exc_info()[1])
 
 
@@ -325,7 +322,7 @@ def krb5_valid(ccache=None):
     try:
         cmd = [ Paths.KLIST, '-s' ]
         if ccache:
-            cmd += [ '-c', ccache ];
+            cmd += [ '-c', ccache ]
         ret = check_call(cmd)
         if ret > 0:
             logging.debug("No current valid keys found in ccache")
@@ -333,7 +330,7 @@ def krb5_valid(ccache=None):
         else:
             logging.debug("Found valid ccache")
             return True
-    except:
+    except CalledProcessError:
         logging.debug("klist error: %s", sys.exc_info())
         return False
 
@@ -355,33 +352,33 @@ def krb5_init( username, realm=None, force=False, ccache=None):
         ret = check_call(cmd, stdin=sys.stdin, stdout=sys.stdout)
         if ret != 0:
             logging.error("kinit returned %d", ret)
-    except CalledProcessError as e:
+    except CalledProcessError:
         raise
 
 
 def ipa_service_exists( hostname, service='nfs', realm=None):
     """Ask IPA if this service principal exists"""
     principal = '%s/%s' % ( service, hostname )
-    try: 
-        out = check_output([ Paths.IPA_CLI, 'service-show', principal ], stderr=devnull)
+    try:
+        check_output([ Paths.IPA_CLI, 'service-show', principal ], stderr=devnull)
         return True
 
-    except CalledProcessError as e:
-        logging.debug("Failed to find service %s" % principal)
+    except CalledProcessError:
+        logging.debug("Failed to find service %s", principal)
         return False
 
 def ipa_service_add( hostname, service='nfs', realm=None, force=False):
     """Create this service principal via IPA"""
     principal = '%s/%s' % ( service, hostname )
-    logging.debug("Adding service %s" % principal)
+    logging.debug("Adding service %s", principal)
     cmd = [ Paths.IPA_CLI, 'service-add', principal ]
     if force:
         cmd.append('--force')
-    try: 
-        out = check_output( cmd )
+    try:
+        check_output( cmd )
 
     except CalledProcessError as e:
-        logging.error("'%s' failed with retcode %d: %s" % (cmd[0], e.returncode, e.output))
+        logging.error("'%s' failed with retcode %d: %s", cmd[0], e.returncode, e.output)
         raise
 
 def fetch_keytab(hostname, service='nfs', server=None, keytab=None):
@@ -393,9 +390,9 @@ def fetch_keytab(hostname, service='nfs', server=None, keytab=None):
     if keytab:
         cmd += [ '-k', keytab ]
 
-    try: 
-        out = check_output(cmd)
-    except:
+    try:
+        check_output(cmd)
+    except CalledProcessError:
         raise
 
 
@@ -410,7 +407,7 @@ def service_restart(service, force=False):
             ret = check_call(cmd, stdout=devnull, stderr=devnull)
             if ret == 0:
                 enabled = True
-        except CalledProcessError as e:
+        except CalledProcessError:
             pass
 
     # Enable it
@@ -418,20 +415,20 @@ def service_restart(service, force=False):
         cmd = [ Paths.SYSTEMCTL, 'enable', service ]
         try:
             ret = check_call(cmd, stdout=devnull, stderr=devnull)
-        except CalledProcessError as e:
-            logging.error("Error enabling service %s" % service)
+        except CalledProcessError:
+            logging.error("Error enabling service %s", service)
             raise
-   
+
     # run/restart it
     if force:
         cmd = [ Paths.SYSTEMCTL, 'restart', service ]
     else:
         cmd = [ Paths.SYSTEMCTL, 'try-restart', service ]
-        
+
     try:
         ret = check_call(cmd, stdout=devnull, stderr=devnull)
-    except CalledProcessError as e:
-        logging.error("Error restarting service %s" % service)
+    except CalledProcessError:
+        logging.error("Error restarting service %s", service)
         raise
 
 
@@ -442,7 +439,7 @@ def update_exports():
     try:
         ret = check_output( cmd )
         logging.debug(ret)
-    except:
+    except CalledProcessError:
         raise
 
 def load_ipa_config(pathname):
@@ -452,22 +449,22 @@ def load_ipa_config(pathname):
     values = dict()
     try:
         config.read([pathname])
-    except:
+    except ConfigParser.Error:
         raise
 
     try:
         values['realm'] = config.get('global', 'realm')
-    except:
+    except ConfigParser.Error:
         pass
 
     try:
         values['server'] = config.get('global', 'server')
-    except:
+    except ConfigParser.Error:
         pass
 
     try:
         values['hostname'] = config.get('global', 'host')
-    except:
+    except ConfigParser.Error:
         pass
 
     if not values:
@@ -483,57 +480,55 @@ def mapadd( hostname, directory ):
     else:
         mapname = 'auto.%s' % directory
 
-    logging.debug("Adding map '%s'  %s:%s" % (mapname, hostname, directory))
+    logging.debug("Adding map '%s'  %s:%s" , mapname, hostname, directory)
 
     try:
         # Create the map
-        logging.debug("Create map '%s'" % mapname)
+        logging.debug("Create map '%s'", mapname)
         cmd = [ Paths.IPA_CLI, 'automountmap-add', 'default', mapname ]
-        ret = check_output(cmd, stderr=devnull)
+        check_output(cmd, stderr=devnull)
 
         # set the directory and make a sub of auto.master
-        logging.debug("Add map directory '%s'" % directory)
+        logging.debug("Add map directory '%s'", directory)
         cmd = [ Paths.IPA_CLI, 'automountkey-add', 'default', '--key', directory, '--info', mapname, 'auto.master' ]
-        ret = check_output(cmd, stderr=devnull)
+        check_output(cmd, stderr=devnull)
 
         # Now set the mapping
-        logging.debug("Set map key '%s'" % hostname)
+        logging.debug("Set map key '%s'", hostname)
         cmd = [ Paths.IPA_CLI, 'automountkey-add', 'default', '--key', '*', '--info', "-fstype=nfs4,rw,sec=krb5p,soft,rsize=8192,wsize=8192 %s:%s/&" % (hostname, directory), mapname ]
-        ret = check_output(cmd, stderr=devnull)
+        check_output(cmd, stderr=devnull)
 
     except CalledProcessError as e:
-        logging.debug("Error creating map %s: %s " % (mapname, e) )
+        logging.debug("Error creating map %s: %s ", mapname, e)
         raise
 
 
 
 
 def main():
-    env={"PATH":"/bin:/sbin:/usr/kerberos/bin:/usr/kerberos/sbin:/usr/bin:/usr/sbin"}
-
     tool = 'yum'
     if os.path.exists( Paths.DNF ):
         tool = 'dnf'
 
     # basic sanity checks first
     if not os.path.exists( Paths.IPA_CLI ):
-        logging.error("%s not found. Try '%s install ipa-admintools' first" % (Paths.IPA_CLI, tool))
+        logging.error("%s not found. Try '%s install ipa-admintools' first", Paths.IPA_CLI, tool)
         sys.exit(1)
 
     if not os.path.exists( Paths.IPA_GETKEYTAB ):
-        logging.error("%s not found. Try '%s install ipa-client' first" % (Paths.IPA_GETKEYTAB, tool))
+        logging.error("%s not found. Try '%s install ipa-client' first", Paths.IPA_GETKEYTAB, tool)
         sys.exit(1)
 
     if not os.path.exists( Paths.KLIST ):
-        logging.error("%s not found. Try '%s install krb5-workstation' first" % (Paths.KLIST, tool))
+        logging.error("%s not found. Try '%s install krb5-workstation' first", Paths.KLIST, tool)
         sys.exit(1)
 
     if not os.path.exists( Paths.KINIT ):
-        logging.error("%s not found. Try '%s install krb5-workstation' first" % ( Paths.KINIT, tool))
+        logging.error("%s not found. Try '%s install krb5-workstation' first", Paths.KINIT, tool)
         sys.exit(1)
 
     if not os.path.exists( Paths.EXPORTFS ):
-        logging.error("%s not found. Try '%s install nfs-utils' first" % ( Paths.EXPORTFS, tool))
+        logging.error("%s not found. Try '%s install nfs-utils' first", Paths.EXPORTFS, tool)
         sys.exit(1)
 
 
@@ -555,28 +550,27 @@ def main():
     ipaconf = dict()
     try:
         ipaconf = load_ipa_config(Paths.IPACONFIG)
-    except:
-        logging.debug("load config failed: %s" % sys.exc_value);
-        pass
+    except StandardError:
+        logging.debug("load config failed: %s", sys.exc_value)
 
     # If we have them, try using the ipa config values next
     if not hostname:
-        try: 
+        try:
             hostname = ipaconf['hostname']
-        except:
+        except LookupError:
             pass
 
     if not realm:
         try:
             realm = ipaconf['realm']
-        except:
+        except LookupError:
             pass
 
     if not servers:
         try:
             newserver = ipaconf['server']
             servers.append( newserver )
-        except:
+        except LookupError:
             pass
 
 
@@ -599,7 +593,7 @@ def main():
     if not realm:
         try:
             realm = search_realm(hostname)
-        except:
+        except StandardError:
             pass
 
     # Maybe we have signed in already and that can tell us?
@@ -610,14 +604,14 @@ def main():
             realm = kinfo['realm']
             if not username:
                 username = kinfo['username']
-        except:
+        except StandardError:
             pass
 
     # We cant find a realm so ask the user
     if not realm:
         domain = None
         if hostname and not valid_ip(hostname):
-            p = hostname.find(".")
+            p = str(hostname).find(".")
             if p != -1:
                 domain = hostname[p+1:]
         print("Unable to determine realm, and not provided by --realm")
@@ -637,7 +631,7 @@ def main():
     # Grab a list of what is already exported on this system
     try:
         exported = search_exports()
-    except:
+    except StandardError:
         pass
 
     # Has the user given a manual list of directories to export
@@ -653,7 +647,7 @@ def main():
                 break
             exports.append(e)
 
-    realm = realm.upper()
+    realm = str(realm).upper()
 
     if exports and options.automount is None:
         print("Do you wish to enable automount ability for these mounts?")
@@ -716,7 +710,7 @@ def main():
     if not krb5_valid(ccache=ccache):
         try:
             krb5_init(username, realm, force=options.force, ccache=ccache)
-        except:
+        except CalledProcessError:
             if not options.force:
                 sys.exit(1)
         if ccache:
@@ -726,11 +720,11 @@ def main():
     if options.force or not ipa_service_exists(hostname, service='nfs'):
         try:
             ipa_service_add(hostname, service='nfs', force=options.force)
-        except:
+        except CalledProcessError:
             if not options.force:
                 sys.exit(1)
     else:
-        logging.info("Service nfs/%s already exists" % hostname)
+        logging.info("Service nfs/%s already exists", hostname)
 
     # check if we have the nfs server key, fetch it if we dont
     if options.force or not have_keytab(hostname, service='nfs', realm=realm):
@@ -738,7 +732,7 @@ def main():
         try:
             fetch_keytab(hostname, service='nfs', server=servers[0], keytab=Paths.KEYTAB)
         except CalledProcessError as e:
-            logging.debug("'%s' failed with retcode %d: %s" % (e.cmd, e.returncode, e.output))
+            logging.debug("'%s' failed with retcode %d: %s", e.cmd, e.returncode, e.output)
 
     else:
         logging.info("Already have the keytab cached, skipping")
@@ -756,26 +750,26 @@ def main():
             fp.close()
         else:
             logging.debug("Path %s is already exported, skipping", d)
-    
+
     if options.force or exports:
         try:
             update_exports()
         except CalledProcessError as e:
-            logging.error("'%s' failed with retcode %d: %s" % (e.cmd, e.returncode, e.output))
+            logging.error("'%s' failed with retcode %d: %s", e.cmd, e.returncode, e.output)
 
     if options.automount:
         logging.debug("Configuring automount")
         for d in mountlist:
             try:
                 mapadd( hostname, d )
-            except:
-                logging.error("Adding automount map for %s failed" % d)
+            except CalledProcessError:
+                logging.error("Adding automount map for %s failed", d)
 
 
     # Restart any services
     try:
         service_restart('nfs-server', force=options.force)
-    except:
+    except CalledProcessError:
         pass
 
 
@@ -783,13 +777,13 @@ def main():
     try:
         if ccache:
             os.unlink(ccache)
-    except:
+    except OSError:
         pass
 
     try:
         if ccache_dir:
             os.rmdir(ccache_dir)
-    except:
+    except OSError:
         pass
 
     print("Finished.")
@@ -799,7 +793,7 @@ def main():
 # Setup the logger, default to only error messages
 logging.basicConfig(level=logging.INFO, format='%(message)s')
 
-# use this to suppress error messages from subprocesses	
+# use this to suppress error messages from subprocesses
 devnull = open(os.devnull, 'w')
 
 # boilerplate to launch main and handle the fallout
-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to