#!/usr/bin/python # -*- coding: utf-8 -*- # Authors: # Thomas Woerner # # 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 . import os import sys import shutil import tempfile import argparse import traceback import subprocess def parse_options(): usage = "Usage: ansible-ipa-client-install [options] " 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)