From 09423acb6574a3773d7783f9ddec022bed3539c8 Mon Sep 17 00:00:00 2001 From: Jan Cholasta Date: Nov 11 2016 11:17:25 +0000 Subject: install: migrate client install to the new class hierarchy Migrate ipa-client-install from the custom script to the new installer class hierarchy classes. https://fedorahosted.org/freeipa/ticket/6392 Reviewed-By: Martin Basti --- diff --git a/client/ipa-client-install b/client/ipa-client-install index 9ce2697..2771184 100755 --- a/client/ipa-client-install +++ b/client/ipa-client-install @@ -19,232 +19,6 @@ # along with this program. If not, see . # -from __future__ import print_function +from ipaclient.install import ipa_client_install -import sys -import os - -from optparse import SUPPRESS_HELP, OptionGroup, OptionValueError - -from ipaclient.install import client -from ipapython.ipa_log_manager import standard_logging_setup, root_logger -from ipaplatform.paths import paths -from ipapython import version -from ipapython.admintool import ScriptError -from ipapython.config import IPAOptionParser -from ipalib import x509 -from ipalib.util import normalize_hostname, validate_domain_name - - -def parse_options(): - def validate_ca_cert_file_option(option, opt, value, parser): - if not os.path.exists(value): - raise OptionValueError("%s option '%s' does not exist" % (opt, value)) - if not os.path.isfile(value): - raise OptionValueError("%s option '%s' is not a file" % (opt, value)) - if not os.path.isabs(value): - raise OptionValueError("%s option '%s' is not an absolute file path" % (opt, value)) - - try: - x509.load_certificate_from_file(value) - except Exception: - raise OptionValueError("%s option '%s' is not a valid certificate file" % (opt, value)) - - parser.values.ca_cert_file = value - - def kinit_attempts_callback(option, opt, value, parser): - if value < 1: - raise OptionValueError( - "Option %s expects an integer greater than 0." - % opt) - - parser.values.kinit_attempts = value - - parser = IPAOptionParser(version=version.VERSION) - - basic_group = OptionGroup(parser, "basic options") - basic_group.add_option("--domain", dest="domain", help="domain name") - basic_group.add_option("--server", dest="server", help="FQDN of IPA server", action="append") - basic_group.add_option("--realm", dest="realm_name", help="realm name") - basic_group.add_option("--fixed-primary", dest="primary", action="store_true", - default=False, help="Configure sssd to use fixed server as primary IPA server") - basic_group.add_option("-p", "--principal", dest="principal", - help="principal to use to join the IPA realm") - basic_group.add_option("-w", "--password", dest="password", sensitive=True, - help="password to join the IPA realm (assumes bulk " - "password unless principal is also set)") - basic_group.add_option("-k", "--keytab", dest="keytab", - help="path to backed up keytab from previous enrollment") - basic_group.add_option("-W", dest="prompt_password", action="store_true", - default=False, - help="Prompt for a password to join the IPA realm") - basic_group.add_option("--mkhomedir", dest="mkhomedir", - action="store_true", default=False, - help="create home directories for users on their first login") - basic_group.add_option("", "--hostname", dest="hostname", - 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.") - basic_group.add_option("", "--force-join", dest="force_join", - action="store_true", default=False, - help="Force client enrollment even if already enrolled") - basic_group.add_option("--ntp-server", dest="ntp_servers", action="append", - help="ntp server to use. This option can be used " - "multiple times") - basic_group.add_option("-N", "--no-ntp", action="store_false", - help="do not configure ntp", default=True, dest="conf_ntp") - basic_group.add_option("", "--force-ntpd", dest="force_ntpd", - action="store_true", default=False, - help="Stop and disable any time&date synchronization services besides ntpd") - basic_group.add_option("--nisdomain", dest="nisdomain", - help="NIS domain name") - basic_group.add_option("--no-nisdomain", action="store_true", default=False, - help="do not configure NIS domain name", - dest="no_nisdomain") - basic_group.add_option("--ssh-trust-dns", dest="trust_sshfp", default=False, action="store_true", - help="configure OpenSSH client to trust DNS SSHFP records") - basic_group.add_option("--no-ssh", dest="conf_ssh", default=True, action="store_false", - help="do not configure OpenSSH client") - basic_group.add_option("--no-sshd", dest="conf_sshd", default=True, action="store_false", - help="do not configure OpenSSH server") - basic_group.add_option("--no-sudo", dest="conf_sudo", default=True, - action="store_false", - help="do not configure SSSD as data source for sudo") - basic_group.add_option("--no-dns-sshfp", dest="create_sshfp", default=True, action="store_false", - help="do not automatically create DNS SSHFP records") - basic_group.add_option("--noac", dest="no_ac", default=False, action="store_true", - help="do not modify the nsswitch.conf and PAM configuration") - basic_group.add_option("-f", "--force", dest="force", action="store_true", - default=False, help="force setting of LDAP/Kerberos conf") - basic_group.add_option('--kinit-attempts', dest='kinit_attempts', - action='callback', type='int', default=5, - callback=kinit_attempts_callback, - help=("number of attempts to obtain host TGT" - " (defaults to %default).")) - basic_group.add_option("-d", "--debug", dest="debug", action="store_true", - default=False, help="print debugging information") - basic_group.add_option("-U", "--unattended", dest="unattended", - action="store_true", - help="unattended (un)installation never prompts the user") - basic_group.add_option("--ca-cert-file", dest="ca_cert_file", - type="string", action="callback", callback=validate_ca_cert_file_option, - help="load the CA certificate from this file") - basic_group.add_option("--request-cert", dest="request_cert", - action="store_true", default=False, - help="request certificate for the machine") - # --on-master is used in ipa-server-install and ipa-replica-install - # only, it isn't meant to be used on clients. - basic_group.add_option("--on-master", dest="on_master", action="store_true", - help=SUPPRESS_HELP, default=False) - basic_group.add_option("--automount-location", dest="location", - help="Automount location") - basic_group.add_option("--configure-firefox", dest="configure_firefox", - action="store_true", default=False, - help="configure Firefox to use IPA domain credentials") - basic_group.add_option("--firefox-dir", dest="firefox_dir", default=None, - help="specify directory where Firefox is installed (for example: '/usr/lib/firefox')") - basic_group.add_option("--ip-address", dest="ip_addresses", default=[], - action="append", help="Specify IP address that should be added to DNS." - " This option can be used multiple times") - basic_group.add_option("--all-ip-addresses", dest="all_ip_addresses", - default=False, action="store_true", help="All routable IP" - " addresses configured on any inteface will be added to DNS") - parser.add_option_group(basic_group) - - sssd_group = OptionGroup(parser, "SSSD options") - sssd_group.add_option("--permit", dest="permit", - action="store_true", default=False, - help="disable access rules by default, permit all access.") - sssd_group.add_option("", "--enable-dns-updates", dest="dns_updates", - action="store_true", default=False, - help="Configures the machine to attempt dns updates when the ip address changes.") - sssd_group.add_option("--no-krb5-offline-passwords", dest="krb5_offline_passwords", - action="store_false", default=True, - help="Configure SSSD not to store user password when the server is offline") - sssd_group.add_option("-S", "--no-sssd", dest="sssd", - action="store_false", default=True, - help="Do not configure the client to use SSSD for authentication") - sssd_group.add_option("--preserve-sssd", dest="preserve_sssd", - action="store_true", default=False, - help="Preserve old SSSD configuration if possible") - parser.add_option_group(sssd_group) - - uninstall_group = OptionGroup(parser, "uninstall options") - uninstall_group.add_option("", "--uninstall", dest="uninstall", action="store_true", - default=False, help="uninstall an existing installation. The uninstall can " \ - "be run with --unattended option") - parser.add_option_group(uninstall_group) - - options, _args = parser.parse_args() - safe_opts = parser.get_safe_opts(options) - - if (options.server and not options.domain): - parser.error("--server cannot be used without providing --domain") - - if options.domain: - try: - validate_domain_name(options.domain) - except ValueError as ex: - parser.error("invalid domain name: %s" % ex) - options.domain = normalize_hostname(options.domain) - - if options.force_ntpd and not options.conf_ntp: - parser.error("--force-ntpd cannot be used together with --no-ntp") - - if options.firefox_dir and not options.configure_firefox: - parser.error("--firefox-dir cannot be used without --configure-firefox option") - - if options.no_nisdomain and options.nisdomain: - parser.error("--no-nisdomain cannot be used together with --nisdomain") - - if options.ip_addresses: - if options.dns_updates: - parser.error("--ip-address cannot be used together with" - " --enable-dns-updates") - - if options.all_ip_addresses: - parser.error("--ip-address cannot be used together with" - " --all-ip-addresses") - - return safe_opts, options - - -def logging_setup(options): - log_file = paths.IPACLIENT_INSTALL_LOG - - if options.uninstall: - log_file = paths.IPACLIENT_UNINSTALL_LOG - - standard_logging_setup( - filename=log_file, verbose=True, debug=options.debug, - console_format='%(message)s') - - -def main(): - safe_options, options = parse_options() - - logging_setup(options) - root_logger.debug( - '%s was invoked with options: %s', sys.argv[0], safe_options) - root_logger.debug("missing options might be asked for interactively later") - root_logger.debug('IPA version %s' % version.VENDOR_VERSION) - - if options.uninstall: - client.uninstall_check(options) - client.uninstall(options) - else: - client.install_check(options) - client.install(options) - -if __name__ == "__main__": - try: - sys.exit(main()) - except ScriptError as e: - if e.rval != client.SUCCESS and e.msg: - root_logger.error(e.msg) - sys.exit(e.rval) - except KeyboardInterrupt: - sys.exit(1) - except RuntimeError as e: - sys.exit(e) +ipa_client_install.run() diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index 3f124a6..b24a989 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -50,6 +50,7 @@ from ipalib.rpc import delete_persistent_client_session_data from ipalib.util import ( broadcast_ip_address_warning, network_ip_address_warning, + normalize_hostname, verify_host_resolvable, ) from ipaplatform import services @@ -67,7 +68,8 @@ from ipapython.admintool import ScriptError from ipapython.dn import DN from ipapython.install import typing from ipapython.install.core import knob -from ipapython.ipa_log_manager import root_logger +from ipapython.install.common import step +from ipapython.ipa_log_manager import log_mgr, root_logger from ipapython.ipautil import ( CalledProcessError, dir_exists, @@ -3309,6 +3311,41 @@ def uninstall(options): raise ScriptError(rval=rv) +def init(installer): + try: + installer.debug = log_mgr.get_handler('console').level == 'debug' + except KeyError: + installer.debug = True + installer.unattended = not installer.interactive + + if installer.domain_name: + installer.domain = normalize_hostname(installer.domain_name) + else: + installer.domain = None + installer.server = installer.servers + installer.realm = installer.realm_name + installer.primary = installer.fixed_primary + if installer.principal: + installer.password = installer.admin_password + else: + installer.password = installer.host_password + installer.hostname = installer.host_name + installer.conf_ntp = not installer.no_ntp + installer.trust_sshfp = installer.ssh_trust_dns + installer.conf_ssh = not installer.no_ssh + installer.conf_sshd = not installer.no_sshd + installer.conf_sudo = not installer.no_sudo + installer.create_sshfp = not installer.no_dns_sshfp + if installer.ca_cert_files: + installer.ca_cert_file = installer.ca_cert_files[-1] + else: + installer.ca_cert_file = None + installer.location = installer.automount_location + installer.dns_updates = installer.enable_dns_updates + installer.krb5_offline_passwords = not installer.no_krb5_offline_passwords + installer.sssd = not installer.no_sssd + + class ClientInstallInterface(hostname_.HostNameInstallInterface, service.ServiceAdminInstallInterface): """ @@ -3492,3 +3529,91 @@ class ClientInstallInterface(hostname_.HostNameInstallInterface, raise RuntimeError( "--ip-address cannot be used together with" "--all-ip-addresses") + + +class ClientInstall(ClientInstallInterface, + automount.AutomountInstallInterface): + """ + Client installer + """ + + ca_cert_files = knob( + bases=ClientInstallInterface.ca_cert_files, + ) + + @ca_cert_files.validator + def ca_cert_files(self, value): + if not os.path.exists(value): + raise ValueError("'%s' does not exist" % value) + if not os.path.isfile(value): + raise ValueError("'%s' is not a file" % value) + if not os.path.isabs(value): + raise ValueError("'%s' is not an absolute file path" % value) + + try: + x509.load_certificate_from_file(value) + except Exception: + raise ValueError("'%s' is not a valid certificate file" % value) + + @property + def prompt_password(self): + return self.interactive + + automount_location = knob( + bases=automount.AutomountInstallInterface.automount_location, + default=None, + ) + + no_ac = knob( + None, + description="do not modify the nsswitch.conf and PAM configuration", + cli_names='--noac', + ) + + force = knob( + None, + description="force setting of LDAP/Kerberos conf", + cli_names=[None, '-f'], + ) + + on_master = False + + configure_firefox = knob( + None, + description="configure Firefox to use IPA domain credentials", + ) + + firefox_dir = knob( + str, None, + description="specify directory where Firefox is installed (for " + "example: '/usr/lib/firefox')", + ) + + no_sssd = knob( + None, + description="Do not configure the client to use SSSD for " + "authentication", + cli_names=[None, '-S'], + ) + + def __init__(self, **kwargs): + super(ClientInstall, self).__init__(**kwargs) + + if self.firefox_dir and not self.configure_firefox: + raise RuntimeError( + "--firefox-dir cannot be used without --configure-firefox " + "option") + + @step() + def main(self): + init(self) + install_check(self) + yield + install(self) + + @main.uninstaller + def main(self): + init(self) + uninstall_check(self) + yield + uninstall(self) diff --git a/ipaclient/install/ipa_client_install.py b/ipaclient/install/ipa_client_install.py new file mode 100644 index 0000000..4ac7cf5 --- /dev/null +++ b/ipaclient/install/ipa_client_install.py @@ -0,0 +1,66 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +from ipaclient.install import client +from ipaplatform.paths import paths +from ipapython.install import cli +from ipapython.install.core import knob + + +class StandaloneClientInstall(client.ClientInstall): + no_host_dns = False + no_wait_for_dns = False + + principal = knob( + bases=client.ClientInstall.principal, + cli_names=list(client.ClientInstall.principal.cli_names) + ['-p'], + ) + + password = knob( + str, None, + sensitive=True, + description="password to join the IPA realm (assumes bulk password " + "unless principal is also set)", + cli_names=[None, '-w'], + ) + + @property + def admin_password(self): + if self.principal: + return self.password + + return super(StandaloneClientInstall, self).admin_password + + @property + def host_password(self): + if not self.principal: + return self.password + + return super(StandaloneClientInstall, self).host_password + + prompt_password = knob( + None, + description="Prompt for a password to join the IPA realm", + cli_names='-W', + ) + + on_master = knob( + None, + deprecated=True, + ) + + +ClientInstall = cli.install_tool( + StandaloneClientInstall, + command_name='ipa-client-install', + log_file_name=paths.IPACLIENT_INSTALL_LOG, + debug_option=True, + verbose=True, + console_format='%(message)s', + uninstall_log_file_name=paths.IPACLIENT_UNINSTALL_LOG, +) + + +def run(): + ClientInstall.run_cli()