Blob Blame History Raw
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Authors:
#   Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019  Red Hat
# see file 'COPYING' for use and warranty information
#
# 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/>.

import os
import sys
import shutil
import tempfile
import argparse
import traceback
import subprocess


def parse_options():
    usage = "Usage: ansible-ipa-client-install [options] <ansible host>"

    parser = argparse.ArgumentParser(usage=usage)
    parser.add_argument("--version", dest="version",
                        action="store_true",
                        help="show program's version number and exit")
    parser.add_argument("-U", "--unattended", dest="unattended",
                        action="store_true",
                        help="unattended (un)installation never prompts the "
                        "user")
    parser.add_argument("--uninstall", dest="uninstall",
                        action="store_true",
                        help="uninstall an existing installation. The "
                        "uninstall can be run with --unattended option")
    # basic
    parser.add_argument("-p", "--principal", dest="principal",
                        default=None,
                        help="principal to use to join the IPA realm")
    parser.add_argument("--ca-cert-file", dest="ca_cert_file",
                        default=None,
                        help="load the CA certificate from this file")
    parser.add_argument("--ip-address", dest="ip_addresses",
                        metavar="IP_ADDRESS",
                        action='append', default=None,
                        help="Specify IP address that should be added to DNS. "
                        "This option can be used multiple times")
    parser.add_argument("--all-ip-addresses", dest="all_ip_addresses",
                        action='store_true',
                        help="All routable IP addresses configured on any "
                        "interface will be added to DNS")
    parser.add_argument("--domain", dest="domain",
                        default=None,
                        help="primary DNS domain of the IPA deployment (not "
                        "necessarily related to the current hostname)")
    parser.add_argument("--server", dest="servers",
                        metavar="SERVER",
                        action='append', default=None,
                        help="FQDN of IPA server")
    parser.add_argument("--realm", dest="realm",
                        default=None,
                        help="Kerberos realm name of the IPA deployment "
                        "(typically an upper-cased name of the primary DNS "
                        "domain)")
    parser.add_argument("--hostname", dest="hostname",
                        default=None,
                        help="The hostname of this machine (FQDN). If "
                        "specified, the hostname will be set and the system "
                        "configuration will be updated to persist over "
                        "reboot. By default the result of getfqdn() call "
                        "from Python's socket module is used.")
    # client
    parser.add_argument("-w", "--password", dest="password",
                        default=None,
                        help="password to join the IPA realm (assumes bulk "
                        "password unless principal is also set)")
    parser.add_argument("-W", dest="password_prompt",
                        action="store_true",
                        help="Prompt for a password to join the IPA realm")
    parser.add_argument("-f", "--force", dest="force",
                        action="store_true",
                        help="force setting of LDAP/Kerberos conf")
    parser.add_argument("--configure-firefox", dest="configure_firefox",
                        action="store_true",
                        help="configure Firefox to use IPA domain credentials")
    parser.add_argument("--firefox-dir", dest="firefox_dir",
                        default=None,
                        help="specify directory where Firefox is installed "
                        "(for example: '/usr/lib/firefox')")
    parser.add_argument("-k", "--keytab", dest="keytab",
                        default=None,
                        help="path to backed up keytab from previous "
                        "enrollment")
    parser.add_argument("--mkhomedir", dest="mkhomedir",
                        action="store_true",
                        help="create home directories for users on their "
                        "first login")
    parser.add_argument("--force-join", dest="force_join",
                        action="store_true",
                        help="Force client enrollment even if already "
                        "enrolled")
    parser.add_argument("--ntp-server", dest="ntp_servers",
                        metavar="NTP_SERVER",
                        action='append', default=None,
                        help="ntp server to use. This option can be used "
                        "multiple times")
    parser.add_argument("--ntp-pool", dest="ntp_pool",
                        default=None,
                        help="ntp server pool to use")
    parser.add_argument("-N", "--no-ntp", dest="no_ntp",
                        action="store_true",
                        help="do not configure ntp")
    parser.add_argument("--nisdomain", dest="nisdomain",
                        default=None,
                        help="NIS domain name")
    parser.add_argument("--no-nisdomain", dest="no_nisdomain",
                        action="store_true",
                        help="do not configure NIS domain name")
    parser.add_argument("--ssh-trust-dns", dest="ssh_trust_dns",
                        action="store_true",
                        help="configure OpenSSH client to trust DNS SSHFP "
                        "records")
    parser.add_argument("--no-ssh", dest="no_ssh",
                        action="store_true",
                        help="do not configure OpenSSH client")
    parser.add_argument("--no-sshd", dest="no_sshd",
                        action="store_true",
                        help="do not configure OpenSSH server")
    parser.add_argument("--no-sudo", dest="no_sudo",
                        action="store_true",
                        help="do not configure SSSD as data source for sudo")
    parser.add_argument("--no-dns-sshfp", dest="no_dns_sshfp",
                        action="store_true",
                        help="do not automatically create DNS SSHFP records")
    parser.add_argument("--kinit-attempts", dest="kinit_attempts",
                        type=int, default=None,
                        help="number of attempts to obtain host TGT (defaults "
                        "to 5)")
    # sssd
    parser.add_argument("--fixed-primary", dest="fixed_primary",
                        action="store_true",
                        help="Configure sssd to use fixed server as primary "
                        "IPA server")
    parser.add_argument("--permit", dest="permit",
                        action="store_true",
                        help="disable access rules by default, permit all "
                        "access")
    parser.add_argument("--enable-dns-updates", dest="enable_dns_updates",
                        action="store_true",
                        help="Configures the machine to attempt dns updates "
                        "when the ip address changes")
    parser.add_argument("--no-krb5-offline-passwords",
                        dest="no_krb5_offline_passwords",
                        action="store_true",
                        help="Configure SSSD not to store user password when "
                        "the server is offline")
    parser.add_argument("--preserve-sssd", dest="preserve_sssd",
                        action="store_true",
                        help="Preserve old SSSD configuration if possible")

    # automount
    parser.add_argument("--automount-location", dest="automount_location",
                        default=None,
                        help="Automount location")
    # logging and output
    parser.add_argument("-v", "--verbose", dest="verbose",
                        action="store_true",
                        help="print debugging information")
    parser.add_argument("-d", "--debug", dest="verbose",
                        action="store_true",
                        help="alias for --verbose (deprecated)")
    parser.add_argument("-q", "--quiet", dest="quiet",
                        action="store_true",
                        help="output only errors")
    parser.add_argument("--log-file", dest="log_file",
                        help="log to the given file")
    # ansible
    parser.add_argument("--ipaclient-use-otp", dest="ipaclient_use_otp",
                        choices=("yes", "no"), default=None,
                        help="The bool value defines if a one-time password "
                        "will be generated to join a new or existing host. "
                        "Default: no")
    parser.add_argument("--ipaclient-allow-repair",
                        dest="ipaclient_allow_repair",
                        choices=("yes", "no"), default=None,
                        help="The bool value defines if an already joined or "
                        "partly set-up client can be repaired. Default: no")
    parser.add_argument("--ipaclient-install-packages",
                        dest="ipaclient_install_packages",
                        choices=("yes", "no"), default=None,
                        help="The bool value defines if the needed packages "
                        "are installed on the node. Default: yes")
    # playbook
    parser.add_argument("--playbook-dir",
                        dest="playbook_dir",
                        default=None,
                        help="If defined will be used as to create inventory "
                        "file and playbook in. The files will not be removed "
                        "after the playbook processing ended.")
    parser.add_argument("--become-method",
                        dest="become_method",
                        default="sudo",
                        help="privilege escalation method to use "
                        "(default=sudo), use `ansible-doc -t become -l` to "
                        "list valid choices.")
    parser.add_argument("--ansible-verbose",
                        dest="ansible_verbose",
                        type=int, default=None,
                        help="privilege escalation method to use "
                        "(default=sudo), use `ansible-doc -t become -l` to "
                        "list valid choices.")

    options, args = parser.parse_known_args()

    if options.playbook_dir and not os.path.isdir(options.playbook_dir):
        parser.error("playbook dir does not exist")

    if options.password_prompt:
        parser.error("password prompt is not possible with ansible")
    if options.log_file:
        parser.error("log_file is not supported")

    if len(args) < 1:
        parser.error("ansible host not set")
    elif len(args) > 1:
        parser.error("too many arguments: %s" % ",".join(args))

    return options, args


def run_cmd(args):
    """
    Execute an external command.
    """
    p_out = subprocess.PIPE
    p_err = subprocess.STDOUT
    try:
        p = subprocess.Popen(args, stdout=p_out, stderr=p_err,
                             close_fds=True, bufsize=1,
                             universal_newlines=True)
        while True:
            line = p.stdout.readline()
            if p.poll() is not None and line == "":
                break
            sys.stdout.write(line)
    except KeyboardInterrupt:
        p.wait()
        raise
    else:
        p.wait()
        return p.returncode


def main(options, args):
    if options.playbook_dir:
        playbook_dir = options.playbook_dir
    else:
        temp_dir = tempfile.mkdtemp(prefix='ansible-ipa-client')
        playbook_dir = temp_dir

    inventory = os.path.join(playbook_dir, "ipaclient-inventory")
    playbook = os.path.join(playbook_dir, "ipaclient-playbook.yml")

    with open(inventory, 'w') as f:
        if options.servers:
            f.write("[ipaservers]\n")
            for server in options.servers:
                f.write("%s\n" % server)
            f.write("\n")
        f.write("[ipaclients]\n")
        f.write("%s\n" % args[0])
        f.write("\n")
        f.write("[ipaclients:vars]\n")
        # basic
        if options.principal:
            f.write("ipaadmin_principal=%s\n" % options.principal)
        if options.ca_cert_file:
            f.write("ipaclient_ca_cert_file=%s\n" % options.ca_cert_file)
        if options.ip_addresses:
            f.write("ipaclient_ip_addresses=%s\n" %
                    ",".join(options.ip_addresses))
        if options.all_ip_addresses:
            f.write("ipaclient_all_ip_addresses=yes\n")
        if options.domain:
            f.write("ipaclient_domain=%s\n" % options.domain)
        # --servers are handled above
        if options.realm:
            f.write("ipaclient_realm=%s\n" % options.realm)
        if options.hostname:
            f.write("ipaclient_hostname=%s\n" % options.hostname)
        # client
        if options.password:
            f.write("ipaadmin_password=%s\n" % options.password)
        if options.force:
            f.write("ipaclient_force=yes\n")
        if options.configure_firefox:
            f.write("ipaclient_configure_firefox=yes\n")
        if options.firefox_dir:
            f.write("ipaclient_firefox_dir=%s\n" % options.firefox_dir)
        if options.keytab:
            f.write("ipaclient_keytab=%s\n" % options.keytab)
        if options.mkhomedir:
            f.write("ipaclient_mkhomedir=yes\n")
        if options.force_join:
            f.write("ipaclient_force_join=%s\n" % options.force_join)
        if options.ntp_servers:
            f.write("ipaclient_ntp_servers=%s\n" %
                    ",".join(options.ntp_servers))
        if options.ntp_pool:
            f.write("ipaclient_ntp_pool=%s\n" % options.ntp_pool)
        if options.no_ntp:
            f.write("ipaclient_no_ntp=yes\n")
        if options.nisdomain:
            f.write("ipaclient_nisdomain=%s\n" % options.nisdomain)
        if options.no_nisdomain:
            f.write("ipaclient_no_nisdomain=yes\n")
        if options.ssh_trust_dns:
            f.write("ipaclient_ssh_trust_dns=yes\n")
        if options.no_ssh:
            f.write("ipaclient_no_ssh=yes\n")
        if options.no_sshd:
            f.write("ipaclient_no_sshd=yes\n")
        if options.no_sudo:
            f.write("ipaclient_no_sudo=yes\n")
        if options.no_dns_sshfp:
            f.write("ipaclient_no_dns_sshfp=yes\n")
        if options.kinit_attempts:
            f.write("ipaclient_kinit_attempts=%d\n" % options.kinit_attempts)
        # sssd
        if options.fixed_primary:
            f.write("ipasssd_fixed_primary=yes\n")
        if options.permit:
            f.write("ipasssd_permit=yes\n")
        if options.enable_dns_updates:
            f.write("ipasssd_enable_dns_updates=yes\n")
        if options.no_krb5_offline_passwords:
            f.write("ipasssd_no_krb5_offline_passwords=yes\n")
        if options.preserve_sssd:
            f.write("ipasssd_preserve_sssd=yes\n")
        # automount
        if options.automount_location:
            f.write("ipaclient_automount_location=%s\n" %
                    options.automount_location)
        # ansible
        if options.ipaclient_use_otp:
            f.write("ipaclient_use_otp=%s\n" % options.ipaclient_use_otp)
        if options.ipaclient_allow_repair:
            f.write("ipaclient_allow_repair=%s\n" %
                    options.ipaclient_allow_repair)
        if options.ipaclient_install_packages:
            f.write("ipaclient_install_packages=%s\n" %
                    options.ipaclient_install_packages)

    if options.uninstall:
        state = "absent"
    else:
        state = "present"

    with open(playbook, 'w') as f:
        f.write("---\n")
        f.write("- name: Playbook to configure IPA clients\n")
        f.write("  hosts: ipaclients\n")
        f.write("  become: true\n")
        if options.become_method:
            f.write("  become_method: %s\n" % options.become_method)
        f.write("\n")
        f.write("  roles:\n")
        f.write("  - role: ipaclient\n")
        f.write("    state: %s\n" % state)

    cmd = [ 'ansible-playbook' ]
    if options.ansible_verbose:
        cmd.append("-"+"v"*options.ansible_verbose)
    cmd.extend(['-i', inventory, playbook])
    try:
        returncode = run_cmd(cmd)
        if returncode != 0:
            raise RuntimeError()
    finally:
        if not options.playbook_dir:
            shutil.rmtree(temp_dir, ignore_errors=True)


options, args = parse_options()
try:
    main(options, args)
except KeyboardInterrupt:
    sys.exit(1)
except SystemExit as e:
    sys.exit(e)
except RuntimeError as e:
    sys.exit(e)
except Exception as e:
    if options.verbose:
        traceback.print_exc(file=sys.stdout)
    else:
        print("Re-run %s with --verbose option to get more information" %
              sys.argv[0])

    print("Unexpected error: %s" % str(e))
    sys.exit(1)