From a8fdb8de8248fe24f382e44b05293405b0b309ac Mon Sep 17 00:00:00 2001 From: Jan Cholasta Date: Nov 11 2016 11:17:25 +0000 Subject: install: introduce installer class hierarchy Add class hierarchy which allows inherting knob definitions between the various client and server install scripts. https://fedorahosted.org/freeipa/ticket/6392 Reviewed-By: Martin Basti --- diff --git a/freeipa.spec.in b/freeipa.spec.in index b49f937..7dbbf87 100644 --- a/freeipa.spec.in +++ b/freeipa.spec.in @@ -1311,7 +1311,9 @@ fi %dir %{python_sitelib}/ipapython/secrets %{python_sitelib}/ipapython/secrets/*.py* %dir %{python_sitelib}/ipalib -%{python_sitelib}/ipalib/* +%{python_sitelib}/ipalib/*.py* +%dir %{python_sitelib}/ipalib/install +%{python_sitelib}/ipalib/install/*.py* %dir %{python_sitelib}/ipaplatform %{python_sitelib}/ipaplatform/* %{python_sitelib}/ipapython-*.egg-info diff --git a/ipaclient/install/automount.py b/ipaclient/install/automount.py new file mode 100644 index 0000000..bb72045 --- /dev/null +++ b/ipaclient/install/automount.py @@ -0,0 +1,27 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +Automount installer module +""" + +from ipalib.install import service +from ipalib.install.service import enroll_only +from ipapython.install.core import knob + + +class AutomountInstallInterface(service.ServiceInstallInterface): + """ + Interface of the automount installer + + Knobs defined here will be available in: + * ipa-client-install + * ipa-client-automount + """ + + automount_location = knob( + str, 'default', + description="Automount location", + ) + automount_location = enroll_only(automount_location) diff --git a/ipaclient/install/client.py b/ipaclient/install/client.py index c246402..3f124a6 100644 --- a/ipaclient/install/client.py +++ b/ipaclient/install/client.py @@ -43,6 +43,9 @@ from ipalib import ( x509, ) from ipalib.constants import CACERT +from ipalib.install import hostname as hostname_ +from ipalib.install import service +from ipalib.install.service import enroll_only, prepare_only from ipalib.rpc import delete_persistent_client_session_data from ipalib.util import ( broadcast_ip_address_warning, @@ -62,6 +65,8 @@ from ipapython import ( ) 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.ipautil import ( CalledProcessError, @@ -74,6 +79,10 @@ from ipapython.ipautil import ( ) from ipapython.ssh import SSHPublicKey +from . import automount + +NoneType = type(None) + SUCCESS = 0 CLIENT_INSTALL_ERROR = 1 CLIENT_NOT_CONFIGURED = 2 @@ -3298,3 +3307,188 @@ def uninstall(options): if rv: raise ScriptError(rval=rv) + + +class ClientInstallInterface(hostname_.HostNameInstallInterface, + service.ServiceAdminInstallInterface): + """ + Interface of the client installer + + Knobs defined here will be available in: + * ipa-client-install + * ipa-server-install + * ipa-replica-prepare + * ipa-replica-install + """ + + fixed_primary = knob( + None, + description="Configure sssd to use fixed server as primary IPA server", + ) + fixed_primary = enroll_only(fixed_primary) + + principal = knob( + bases=service.ServiceAdminInstallInterface.principal, + description="principal to use to join the IPA realm", + ) + principal = enroll_only(principal) + + host_password = knob( + str, None, + sensitive=True, + ) + host_password = enroll_only(host_password) + + keytab = knob( + str, None, + description="path to backed up keytab from previous enrollment", + cli_names=[None, '-k'], + ) + keytab = enroll_only(keytab) + + mkhomedir = knob( + None, + description="create home directories for users on their first login", + ) + mkhomedir = enroll_only(mkhomedir) + + force_join = knob( + None, + description="Force client enrollment even if already enrolled", + ) + force_join = enroll_only(force_join) + + ntp_servers = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], None, + description="ntp server to use. This option can be used multiple " + "times", + cli_names='--ntp-server', + cli_metavar='NTP_SERVER', + ) + ntp_servers = enroll_only(ntp_servers) + + no_ntp = knob( + None, + description="do not configure ntp", + cli_names=[None, '-N'], + ) + no_ntp = enroll_only(no_ntp) + + force_ntpd = knob( + None, + description="Stop and disable any time&date synchronization services " + "besides ntpd", + ) + force_ntpd = enroll_only(force_ntpd) + + nisdomain = knob( + str, None, + description="NIS domain name", + ) + nisdomain = enroll_only(nisdomain) + + no_nisdomain = knob( + None, + description="do not configure NIS domain name", + ) + no_nisdomain = enroll_only(no_nisdomain) + + ssh_trust_dns = knob( + None, + description="configure OpenSSH client to trust DNS SSHFP records", + ) + ssh_trust_dns = enroll_only(ssh_trust_dns) + + no_ssh = knob( + None, + description="do not configure OpenSSH client", + ) + no_ssh = enroll_only(no_ssh) + + no_sshd = knob( + None, + description="do not configure OpenSSH server", + ) + no_sshd = enroll_only(no_sshd) + + no_sudo = knob( + None, + description="do not configure SSSD as data source for sudo", + ) + no_sudo = enroll_only(no_sudo) + + no_dns_sshfp = knob( + None, + description="do not automatically create DNS SSHFP records", + ) + no_dns_sshfp = enroll_only(no_dns_sshfp) + + kinit_attempts = knob( + int, 5, + description="number of attempts to obtain host TGT (defaults to 5).", + ) + kinit_attempts = enroll_only(kinit_attempts) + + @kinit_attempts.validator + def kinit_attempts(self, value): + if value < 1: + raise ValueError("expects an integer greater than 0.") + + request_cert = knob( + None, + description="request certificate for the machine", + ) + request_cert = prepare_only(request_cert) + + permit = knob( + None, + description="disable access rules by default, permit all access.", + ) + permit = enroll_only(permit) + + enable_dns_updates = knob( + None, + description="Configures the machine to attempt dns updates when the " + "ip address changes.", + ) + enable_dns_updates = enroll_only(enable_dns_updates) + + no_krb5_offline_passwords = knob( + None, + description="Configure SSSD not to store user password when the " + "server is offline", + ) + no_krb5_offline_passwords = enroll_only(no_krb5_offline_passwords) + + preserve_sssd = knob( + None, + description="Preserve old SSSD configuration if possible", + ) + preserve_sssd = enroll_only(preserve_sssd) + + def __init__(self, **kwargs): + super(ClientInstallInterface, self).__init__(**kwargs) + + if self.servers and not self.domain_name: + raise RuntimeError( + "--server cannot be used without providing --domain") + + if self.force_ntpd and self.no_ntp: + raise RuntimeError( + "--force-ntpd cannot be used together with --no-ntp") + + if self.no_nisdomain and self.nisdomain: + raise RuntimeError( + "--no-nisdomain cannot be used together with --nisdomain") + + if self.ip_addresses: + if self.enable_dns_updates: + raise RuntimeError( + "--ip-address cannot be used together with" + " --enable-dns-updates") + + if self.all_ip_addresses: + raise RuntimeError( + "--ip-address cannot be used together with" + "--all-ip-addresses") diff --git a/ipalib/install/__init__.py b/ipalib/install/__init__.py new file mode 100644 index 0000000..f5633f3 --- /dev/null +++ b/ipalib/install/__init__.py @@ -0,0 +1,3 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# diff --git a/ipalib/install/hostname.py b/ipalib/install/hostname.py new file mode 100644 index 0000000..74c569d --- /dev/null +++ b/ipalib/install/hostname.py @@ -0,0 +1,59 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +Host name installer module +""" + +from ipapython.install import typing +from ipapython.install.core import knob +from ipapython.ipautil import CheckedIPAddress + +from . import service +from .service import prepare_only + + +class HostNameInstallInterface(service.ServiceInstallInterface): + """ + Interface common to all service installers which create DNS address + records for `host_name` + """ + + ip_addresses = knob( + # pylint: disable=invalid-sequence-index + typing.List[CheckedIPAddress], None, + description="Specify IP address that should be added to DNS. This " + "option can be used multiple times", + cli_names='--ip-address', + cli_metavar='IP_ADDRESS', + ) + ip_addresses = prepare_only(ip_addresses) + + @ip_addresses.validator + def ip_addresses(self, values): + for value in values: + try: + CheckedIPAddress(value, match_local=True) + except Exception as e: + raise ValueError("invalid IP address {0}: {1}".format( + value, e)) + + all_ip_addresses = knob( + None, + description="All routable IP addresses configured on any inteface " + "will be added to DNS", + ) + all_ip_addresses = prepare_only(all_ip_addresses) + + no_host_dns = knob( + None, + description="Do not use DNS for hostname lookup during installation", + ) + no_host_dns = prepare_only(no_host_dns) + + no_wait_for_dns = knob( + None, + description="do not wait until the host is resolvable in DNS", + ) + no_wait_for_dns = prepare_only(no_wait_for_dns) diff --git a/ipalib/install/service.py b/ipalib/install/service.py new file mode 100644 index 0000000..2544e5b --- /dev/null +++ b/ipalib/install/service.py @@ -0,0 +1,178 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +Base service installer module +""" + +from ipalib.util import validate_domain_name +from ipapython.install import common, core, typing +from ipapython.install.core import knob + + +def prepare_only(obj): + """ + Decorator which makes an installer attribute appear only in the prepare + phase of the install + """ + obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'enroll'} + return obj + + +def enroll_only(obj): + """ + Decorator which makes an installer attribute appear only in the enroll + phase of the install + """ + obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'prepare'} + return obj + + +def master_install_only(obj): + """ + Decorator which makes an installer attribute appear only in master install + """ + obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'replica_install'} + return obj + + +def replica_install_only(obj): + """ + Decorator which makes an installer attribute appear only in replica install + """ + obj.__exclude__ = getattr(obj, '__exclude__', set()) | {'master_install'} + return obj + + +def _does(cls, arg): + def remove(name): + def removed(self): + raise AttributeError(name) + + return property(removed) + + return type( + cls.__name__, + (cls,), + { + n: remove(n) for n in dir(cls) + if arg in getattr(getattr(cls, n), '__exclude__', set()) + } + ) + + +def prepares(cls): + """ + Returns installer class stripped of attributes not related to the prepare + phase of the install + """ + return _does(cls, 'prepare') + + +def enrolls(cls): + """ + Returns installer class stripped of attributes not related to the enroll + phase of the install + """ + return _does(cls, 'enroll') + + +def installs_master(cls): + """ + Returns installer class stripped of attributes not related to master + install + """ + return _does(cls, 'master_install') + + +def installs_replica(cls): + """ + Returns installer class stripped of attributes not related to replica + install + """ + return _does(cls, 'replica_install') + + +class ServiceInstallInterface(common.Installable, + common.Interactive, + core.Composite): + """ + Interface common to all service installers + """ + + domain_name = knob( + str, None, + description="domain name", + cli_names='--domain', + ) + + @domain_name.validator + def domain_name(self, value): + validate_domain_name(value) + + servers = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], None, + description="FQDN of IPA server", + cli_names='--server', + cli_metavar='SERVER', + ) + + realm_name = knob( + str, None, + description="realm name", + cli_names='--realm', + ) + + host_name = knob( + str, None, + description="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.", + cli_names='--hostname', + ) + + ca_cert_files = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], None, + description="load the CA certificate from this file", + cli_names='--ca-cert-file', + cli_metavar='FILE', + ) + + replica_file = knob( + str, None, + description="a file generated by ipa-replica-prepare", + ) + replica_file = enroll_only(replica_file) + replica_file = replica_install_only(replica_file) + + dm_password = knob( + str, None, + sensitive=True, + description="Directory Manager password (for the existing master)", + ) + dm_password = enroll_only(dm_password) + dm_password = replica_install_only(dm_password) + + +class ServiceAdminInstallInterface(ServiceInstallInterface): + """ + Interface common to all service installers which require admin user + authentication + """ + + principal = knob( + str, None, + ) + principal = enroll_only(principal) + principal = replica_install_only(principal) + + admin_password = knob( + str, None, + sensitive=True, + ) + admin_password = enroll_only(admin_password) + admin_password = replica_install_only(admin_password) diff --git a/ipalib/setup.py b/ipalib/setup.py index 982a783..a828c37 100644 --- a/ipalib/setup.py +++ b/ipalib/setup.py @@ -34,5 +34,6 @@ if __name__ == '__main__': package_dir={'ipalib': ''}, packages=[ "ipalib", + "ipalib.install", ], ) diff --git a/ipaserver/install/ca.py b/ipaserver/install/ca.py index 10352c9..0d80a3f 100644 --- a/ipaserver/install/ca.py +++ b/ipaserver/install/ca.py @@ -2,11 +2,24 @@ # Copyright (C) 2015 FreeIPA Contributors see COPYING for license # +""" +CA installer module +""" + from __future__ import print_function +import enum import os.path -from ipaserver.install import cainstance, custodiainstance, dsinstance, bindinstance +import six + +from ipalib.install.service import enroll_only, master_install_only, replica_install_only +from ipapython.install import typing +from ipapython.install.core import knob +from ipaserver.install import (cainstance, + custodiainstance, + dsinstance, + bindinstance) from ipapython import ipautil, certdb from ipapython.admintool import ScriptError from ipaplatform import services @@ -17,6 +30,19 @@ from ipalib import api, certstore, x509 from ipapython.dn import DN from ipapython.ipa_log_manager import root_logger +from . import conncheck, dogtag + +if six.PY3: + unicode = str + +VALID_SUBJECT_ATTRS = ['st', 'o', 'ou', 'dnqualifier', 'c', + 'serialnumber', 'l', 'title', 'sn', 'givenname', + 'initials', 'generationqualifier', 'dc', 'mail', + 'uid', 'postaladdress', 'postalcode', 'postofficebox', + 'houseidentifier', 'e', 'street', 'pseudonym', + 'incorporationlocality', 'incorporationstate', + 'incorporationcountry', 'businesscategory'] + external_cert_file = None external_ca_file = None @@ -270,3 +296,109 @@ def uninstall(): ca_instance.stop_tracking_certificates() if ca_instance.is_configured(): ca_instance.uninstall() + + +class ExternalCAType(enum.Enum): + GENERIC = 'generic' + MS_CS = 'ms-cs' + + +class CASigningAlgorithm(enum.Enum): + SHA1_WITH_RSA = 'SHA1withRSA' + SHA_256_WITH_RSA = 'SHA256withRSA' + SHA_512_WITH_RSA = 'SHA512withRSA' + + +class CAInstallInterface(dogtag.DogtagInstallInterface, + conncheck.ConnCheckInterface): + """ + Interface of the CA installer + + Knobs defined here will be available in: + * ipa-server-install + * ipa-replica-prepare + * ipa-replica-install + * ipa-ca-install + """ + + principal = knob( + bases=conncheck.ConnCheckInterface.principal, + description="User allowed to manage replicas", + cli_names=( + list(conncheck.ConnCheckInterface.principal.cli_names) + ['-P']), + ) + principal = enroll_only(principal) + principal = replica_install_only(principal) + + admin_password = knob( + bases=conncheck.ConnCheckInterface.admin_password, + description="Admin user Kerberos password used for connection check", + cli_names=( + list(conncheck.ConnCheckInterface.admin_password.cli_names) + + ['-w']), + ) + admin_password = enroll_only(admin_password) + admin_password = replica_install_only(admin_password) + + external_ca = knob( + None, + description=("Generate a CSR for the IPA CA certificate to be signed " + "by an external CA"), + ) + external_ca = master_install_only(external_ca) + + external_ca_type = knob( + ExternalCAType, None, + description="Type of the external CA", + ) + external_ca_type = master_install_only(external_ca_type) + + external_cert_files = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], None, + description=("File containing the IPA CA certificate and the external " + "CA certificate chain"), + cli_names='--external-cert-file', + cli_deprecated_names=['--external_cert_file', '--external_ca_file'], + cli_metavar='FILE', + ) + external_cert_files = master_install_only(external_cert_files) + + @external_cert_files.validator + def external_cert_files(self, value): + if any(not os.path.isabs(path) for path in value): + raise ValueError("must use an absolute path") + + subject = knob( + str, None, + description="The certificate subject base (default O=)", + ) + subject = master_install_only(subject) + + @subject.validator + def subject(self, value): + v = unicode(value, 'utf-8') + if any(ord(c) < 0x20 for c in v): + raise ValueError("must not contain control characters") + if '&' in v: + raise ValueError("must not contain an ampersand (\"&\")") + try: + dn = DN(v) + for rdn in dn: + if rdn.attr.lower() not in VALID_SUBJECT_ATTRS: + raise ValueError("invalid attribute: \"%s\"" % rdn.attr) + except ValueError as e: + raise ValueError("invalid subject base format: %s" % e) + + ca_signing_algorithm = knob( + CASigningAlgorithm, None, + description="Signing algorithm of the IPA CA certificate", + ) + ca_signing_algorithm = master_install_only(ca_signing_algorithm) + + skip_schema_check = knob( + None, + description="skip check for updated CA DS schema on the remote master", + ) + skip_schema_check = enroll_only(skip_schema_check) + skip_schema_check = replica_install_only(skip_schema_check) diff --git a/ipaserver/install/conncheck.py b/ipaserver/install/conncheck.py new file mode 100644 index 0000000..132e2fe --- /dev/null +++ b/ipaserver/install/conncheck.py @@ -0,0 +1,25 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +Connection check module +""" + +from ipalib.install import service +from ipalib.install.service import enroll_only, replica_install_only +from ipapython.install.core import knob + + +class ConnCheckInterface(service.ServiceAdminInstallInterface): + """ + Interface common to all installers which perform connection check to the + remote master. + """ + + skip_conncheck = knob( + None, + description="skip connection check to remote master", + ) + skip_conncheck = enroll_only(skip_conncheck) + skip_conncheck = replica_install_only(skip_conncheck) diff --git a/ipaserver/install/dns.py b/ipaserver/install/dns.py index 23fde1b..5b40c03 100644 --- a/ipaserver/install/dns.py +++ b/ipaserver/install/dns.py @@ -2,11 +2,19 @@ # Copyright (C) 2015 FreeIPA Contributors see COPYING for license # +""" +DNS installer module +""" + from __future__ import absolute_import from __future__ import print_function +import enum + # absolute import is necessary because IPA module dns clashes with python-dns from dns import resolver +import six + import sys from subprocess import CalledProcessError @@ -14,6 +22,8 @@ from subprocess import CalledProcessError from ipalib import api from ipalib import errors from ipalib import util +from ipalib.install import hostname +from ipalib.install.service import enroll_only, prepare_only from ipaplatform.paths import paths from ipaplatform.constants import constants from ipaplatform import services @@ -21,6 +31,9 @@ from ipapython import ipautil from ipapython import sysrestore from ipapython import dnsutil from ipapython.dn import DN +from ipapython.dnsutil import check_zone_overlap +from ipapython.install import typing +from ipapython.install.core import knob from ipapython.ipa_log_manager import root_logger from ipapython.admintool import ScriptError from ipapython.ipautil import user_input @@ -32,6 +45,9 @@ from ipaserver.install import dnskeysyncinstance from ipaserver.install import odsexporterinstance from ipaserver.install import opendnssecinstance +if six.PY3: + unicode = str + ip_addresses = [] reverse_zones = [] @@ -392,3 +408,119 @@ def uninstall(): dnskeysync = dnskeysyncinstance.DNSKeySyncInstance(fstore) if dnskeysync.is_configured(): dnskeysync.uninstall() + + +class DNSForwardPolicy(enum.Enum): + ONLY = 'only' + FIRST = 'first' + + +class DNSInstallInterface(hostname.HostNameInstallInterface): + """ + Interface of the DNS installer + + Knobs defined here will be available in: + * ipa-server-install + * ipa-replica-prepare + * ipa-replica-install + * ipa-dns-install + """ + + allow_zone_overlap = knob( + None, + description="Create DNS zone even if it already exists", + ) + allow_zone_overlap = prepare_only(allow_zone_overlap) + + reverse_zones = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], [], + description=("The reverse DNS zone to use. This option can be used " + "multiple times"), + cli_names='--reverse-zone', + cli_metavar='REVERSE_ZONE', + ) + reverse_zones = prepare_only(reverse_zones) + + @reverse_zones.validator + def reverse_zones(self, values): + if not self.allow_zone_overlap: + for zone in values: + check_zone_overlap(zone) + + no_reverse = knob( + None, + description="Do not create new reverse DNS zone", + ) + no_reverse = prepare_only(no_reverse) + + auto_reverse = knob( + None, + description="Create necessary reverse zones", + ) + auto_reverse = prepare_only(auto_reverse) + + zonemgr = knob( + str, None, + description=("DNS zone manager e-mail address. Defaults to " + "hostmaster@DOMAIN"), + ) + zonemgr = prepare_only(zonemgr) + + @zonemgr.validator + def zonemgr(self, value): + # validate the value first + try: + # IDNA support requires unicode + encoding = getattr(sys.stdin, 'encoding', None) + if encoding is None: + encoding = 'utf-8' + value = value.decode(encoding) + bindinstance.validate_zonemgr_str(value) + except ValueError as e: + # FIXME we can do this in better way + # https://fedorahosted.org/freeipa/ticket/4804 + # decode to proper stderr encoding + stderr_encoding = getattr(sys.stderr, 'encoding', None) + if stderr_encoding is None: + stderr_encoding = 'utf-8' + error = unicode(e).encode(stderr_encoding) + raise ValueError(error) + + forwarders = knob( + # pylint: disable=invalid-sequence-index + typing.List[ipautil.CheckedIPAddress], None, + description=("Add a DNS forwarder. This option can be used multiple " + "times"), + cli_names='--forwarder', + ) + forwarders = enroll_only(forwarders) + + no_forwarders = knob( + None, + description="Do not add any DNS forwarders, use root servers instead", + ) + no_forwarders = enroll_only(no_forwarders) + + auto_forwarders = knob( + None, + description="Use DNS forwarders configured in /etc/resolv.conf", + ) + auto_forwarders = enroll_only(auto_forwarders) + + forward_policy = knob( + DNSForwardPolicy, None, + description=("DNS forwarding policy for global forwarders"), + ) + forward_policy = enroll_only(forward_policy) + + no_dnssec_validation = knob( + None, + description="Disable DNSSEC validation", + ) + no_dnssec_validation = enroll_only(no_dnssec_validation) + + dnssec_master = False + disable_dnssec_master = False + kasp_db_file = None + force = False diff --git a/ipaserver/install/dogtag.py b/ipaserver/install/dogtag.py new file mode 100644 index 0000000..a5c2a5a --- /dev/null +++ b/ipaserver/install/dogtag.py @@ -0,0 +1,25 @@ +# +# Copyright (C) 2016 FreeIPA Contributors see COPYING for license +# + +""" +Dogtag-based service installer module +""" + +from ipalib.install import service +from ipalib.install.service import prepare_only, replica_install_only +from ipapython.install.core import knob + + +class DogtagInstallInterface(service.ServiceInstallInterface): + """ + Interface common to all Dogtag-based service installers + """ + + ca_file = knob( + str, None, + description="location of CA PKCS#12 file", + cli_metavar='FILE', + ) + ca_file = prepare_only(ca_file) + ca_file = replica_install_only(ca_file) diff --git a/ipaserver/install/kra.py b/ipaserver/install/kra.py index 17da924..58a6a73 100644 --- a/ipaserver/install/kra.py +++ b/ipaserver/install/kra.py @@ -2,6 +2,10 @@ # Copyright (C) 2015 FreeIPA Contributors see COPYING for license # +""" +KRA installer module +""" + import os import shutil @@ -15,7 +19,9 @@ from ipaserver.install import custodiainstance from ipaserver.install import cainstance from ipaserver.install import krainstance from ipaserver.install import dsinstance -from ipaserver.install import service +from ipaserver.install import service as _service + +from . import dogtag def install_check(api, replica_config, options): @@ -109,7 +115,7 @@ def install(api, replica_config, options): ra_only=ra_only, promote=promote) - service.print_msg("Restarting the directory server") + _service.print_msg("Restarting the directory server") ds = dsinstance.DsInstance() ds.restart() @@ -134,3 +140,15 @@ def uninstall(standalone): kra.stop_tracking_certificates(stop_certmonger=not standalone) if kra.is_installed(): kra.uninstall() + + +class KRAInstallInterface(dogtag.DogtagInstallInterface): + """ + Interface of the KRA installer + + Knobs defined here will be available in: + * ipa-server-install + * ipa-replica-prepare + * ipa-replica-install + * ipa-kra-install + """ diff --git a/ipaserver/install/server/__init__.py b/ipaserver/install/server/__init__.py index da2ceec..a8b56d6 100644 --- a/ipaserver/install/server/__init__.py +++ b/ipaserver/install/server/__init__.py @@ -2,7 +2,539 @@ # Copyright (C) 2015 FreeIPA Contributors see COPYING for license # +""" +Server installer module +""" + +from __future__ import print_function + +import collections +import os.path +import random + +from ipaclient.install import client +from ipalib import constants +from ipalib.install.service import (enroll_only, + master_install_only, + prepare_only, + replica_install_only) +from ipalib.util import validate_domain_name +from ipapython import ipautil +from ipapython.dnsutil import check_zone_overlap +from ipapython.install import typing +from ipapython.install.core import knob + +from .install import validate_admin_password, validate_dm_password from .install import Server from .replicainstall import Replica from .upgrade import upgrade_check, upgrade + +from .. import ca, conncheck, dns, kra + + +class ServerInstallInterface(client.ClientInstallInterface, + ca.CAInstallInterface, + kra.KRAInstallInterface, + dns.DNSInstallInterface, + conncheck.ConnCheckInterface): + """ + Interface of server installers + + Knobs defined here will be available in: + * ipa-server-install + * ipa-replica-prepare + * ipa-replica-install + """ + + force_join = False + kinit_attempts = 1 + fixed_primary = True + ntp_servers = None + force_ntpd = False + permit = False + enable_dns_updates = False + no_krb5_offline_passwords = False + preserve_sssd = False + + domain_name = knob( + bases=client.ClientInstallInterface.domain_name, + # pylint: disable=no-member + cli_names=(list(client.ClientInstallInterface.domain_name.cli_names) + + ['-n']), + ) + domain_name = replica_install_only(domain_name) + + new_domain_name = knob( + bases=client.ClientInstallInterface.domain_name, + cli_names=['--domain', '-n'], + cli_metavar='DOMAIN_NAME', + ) + new_domain_name = master_install_only(new_domain_name) + + @new_domain_name.validator + def new_domain_name(self, value): + validate_domain_name(value) + if (self.setup_dns and + not self.allow_zone_overlap): # pylint: disable=no-member + print("Checking DNS domain %s, please wait ..." % value) + check_zone_overlap(value, False) + + servers = knob( + bases=client.ClientInstallInterface.servers, + description="fully qualified name of IPA server to enroll to", + ) + servers = enroll_only(servers) + + realm_name = knob( + bases=client.ClientInstallInterface.realm_name, + cli_names=(list(client.ClientInstallInterface.realm_name.cli_names) + + ['-r']), + ) + + host_name = knob( + bases=client.ClientInstallInterface.host_name, + description="fully qualified name of this host", + ) + + ca_cert_files = knob( + bases=client.ClientInstallInterface.ca_cert_files, + description="File containing CA certificates for the service " + "certificate files", + cli_deprecated_names='--root-ca-file', + ) + ca_cert_files = prepare_only(ca_cert_files) + + new_dm_password = knob( + str, None, + sensitive=True, + description="Directory Manager password", + cli_names='--dm-password', + cli_metavar='DM_PASSWORD', + ) + new_dm_password = master_install_only(new_dm_password) + + @new_dm_password.validator + def new_dm_password(self, value): + validate_dm_password(value) + + ip_addresses = knob( + bases=client.ClientInstallInterface.ip_addresses, + description="Server IP Address. This option can be used multiple " + "times", + ) + + principal = knob( + bases=client.ClientInstallInterface.principal, + description="User Principal allowed to promote replicas and join IPA " + "realm", + cli_names=(list(client.ClientInstallInterface.principal.cli_names) + + ['-P']), + ) + principal = replica_install_only(principal) + + admin_password = knob( + bases=client.ClientInstallInterface.admin_password, + description="Kerberos password for the specified admin principal", + ) + admin_password = replica_install_only(admin_password) + + new_admin_password = knob( + str, None, + sensitive=True, + description="admin user kerberos password", + cli_names='--admin-password', + cli_metavar='ADMIN_PASSWORD', + ) + new_admin_password = master_install_only(new_admin_password) + + @new_admin_password.validator + def new_admin_password(self, value): + validate_admin_password(value) + + master_password = knob( + str, None, + sensitive=True, + deprecated=True, + description="kerberos master password (normally autogenerated)", + ) + master_password = master_install_only(master_password) + + domain_level = knob( + int, constants.MAX_DOMAIN_LEVEL, + description="IPA domain level", + deprecated=True, + ) + domain_level = master_install_only(domain_level) + + @domain_level.validator + def domain_level(self, value): + # Check that Domain Level is within the allowed range + if value < constants.MIN_DOMAIN_LEVEL: + raise ValueError( + "Domain Level cannot be lower than {0}".format( + constants.MIN_DOMAIN_LEVEL)) + elif value > constants.MAX_DOMAIN_LEVEL: + raise ValueError( + "Domain Level cannot be higher than {0}".format( + constants.MAX_DOMAIN_LEVEL)) + + setup_ca = knob( + None, + description="configure a dogtag CA", + ) + setup_ca = enroll_only(setup_ca) + + setup_kra = knob( + None, + description="configure a dogtag KRA", + ) + setup_kra = enroll_only(setup_kra) + + setup_dns = knob( + None, + description="configure bind with our zone", + ) + setup_dns = enroll_only(setup_dns) + + idstart = knob( + int, random.randint(1, 10000) * 200000, + description="The starting value for the IDs range (default random)", + ) + idstart = master_install_only(idstart) + + idmax = knob( + int, + description=("The max value for the IDs range (default: " + "idstart+199999)"), + ) + idmax = master_install_only(idmax) + + @idmax.default_getter + def idmax(self): + return self.idstart + 200000 - 1 + + no_hbac_allow = knob( + None, + description="Don't install allow_all HBAC rule", + cli_deprecated_names='--no_hbac_allow', + ) + no_hbac_allow = master_install_only(no_hbac_allow) + + ignore_topology_disconnect = knob( + None, + description="do not check whether server uninstall disconnects the " + "topology (domain level 1+)", + ) + ignore_topology_disconnect = master_install_only(ignore_topology_disconnect) + + ignore_last_of_role = knob( + None, + description="do not check whether server uninstall removes last " + "CA/DNS server or DNSSec master (domain level 1+)", + ) + ignore_last_of_role = master_install_only(ignore_last_of_role) + + no_pkinit = knob( + None, + description="disables pkinit setup steps", + ) + no_pkinit = prepare_only(no_pkinit) + + no_ui_redirect = knob( + None, + description="Do not automatically redirect to the Web UI", + ) + no_ui_redirect = enroll_only(no_ui_redirect) + + ssh_trust_dns = knob( + None, + description="configure OpenSSH client to trust DNS SSHFP records", + ) + ssh_trust_dns = enroll_only(ssh_trust_dns) + + no_ssh = knob( + None, + description="do not configure OpenSSH client", + ) + no_ssh = enroll_only(no_ssh) + + no_sshd = knob( + None, + description="do not configure OpenSSH server", + ) + no_sshd = enroll_only(no_sshd) + + no_dns_sshfp = knob( + None, + description="Do not automatically create DNS SSHFP records", + ) + no_dns_sshfp = enroll_only(no_dns_sshfp) + + dirsrv_config_file = knob( + str, None, + description="The path to LDIF file that will be used to modify " + "configuration of dse.ldif during installation of the " + "directory server instance", + cli_metavar='FILE', + ) + dirsrv_config_file = enroll_only(dirsrv_config_file) + + @dirsrv_config_file.validator + def dirsrv_config_file(self, value): + if not os.path.exists(value): + raise ValueError("File %s does not exist." % value) + + dirsrv_cert_files = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], None, + description=("File containing the Directory Server SSL certificate " + "and private key"), + cli_names='--dirsrv-cert-file', + cli_deprecated_names='--dirsrv_pkcs12', + cli_metavar='FILE', + ) + dirsrv_cert_files = prepare_only(dirsrv_cert_files) + + http_cert_files = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], None, + description=("File containing the Apache Server SSL certificate and " + "private key"), + cli_names='--http-cert-file', + cli_deprecated_names='--http_pkcs12', + cli_metavar='FILE', + ) + http_cert_files = prepare_only(http_cert_files) + + pkinit_cert_files = knob( + # pylint: disable=invalid-sequence-index + typing.List[str], None, + description=("File containing the Kerberos KDC SSL certificate and " + "private key"), + cli_names='--pkinit-cert-file', + cli_deprecated_names='--pkinit_pkcs12', + cli_metavar='FILE', + ) + pkinit_cert_files = prepare_only(pkinit_cert_files) + + dirsrv_pin = knob( + str, None, + sensitive=True, + description="The password to unlock the Directory Server private key", + cli_deprecated_names='--dirsrv_pin', + cli_metavar='PIN', + ) + dirsrv_pin = prepare_only(dirsrv_pin) + + http_pin = knob( + str, None, + sensitive=True, + description="The password to unlock the Apache Server private key", + cli_deprecated_names='--http_pin', + cli_metavar='PIN', + ) + http_pin = prepare_only(http_pin) + + pkinit_pin = knob( + str, None, + sensitive=True, + description="The password to unlock the Kerberos KDC private key", + cli_deprecated_names='--pkinit_pin', + cli_metavar='PIN', + ) + pkinit_pin = prepare_only(pkinit_pin) + + dirsrv_cert_name = knob( + str, None, + description="Name of the Directory Server SSL certificate to install", + cli_metavar='NAME', + ) + dirsrv_cert_name = prepare_only(dirsrv_cert_name) + + http_cert_name = knob( + str, None, + description="Name of the Apache Server SSL certificate to install", + cli_metavar='NAME', + ) + http_cert_name = prepare_only(http_cert_name) + + pkinit_cert_name = knob( + str, None, + description="Name of the Kerberos KDC SSL certificate to install", + cli_metavar='NAME', + ) + pkinit_cert_name = prepare_only(pkinit_cert_name) + + def __init__(self, **kwargs): + super(ServerInstallInterface, self).__init__(**kwargs) + + # If any of the key file options are selected, all are required. + cert_file_req = (self.dirsrv_cert_files, self.http_cert_files) + cert_file_opt = (self.pkinit_cert_files,) + if any(cert_file_req + cert_file_opt) and not all(cert_file_req): + raise RuntimeError( + "--dirsrv-cert-file and --http-cert-file are required if any " + "key file options are used.") + + if not self.interactive: + if self.dirsrv_cert_files and self.dirsrv_pin is None: + raise RuntimeError( + "You must specify --dirsrv-pin with --dirsrv-cert-file") + if self.http_cert_files and self.http_pin is None: + raise RuntimeError( + "You must specify --http-pin with --http-cert-file") + if self.pkinit_cert_files and self.pkinit_pin is None: + raise RuntimeError( + "You must specify --pkinit-pin with --pkinit-cert-file") + + if not self.setup_dns: + if self.forwarders: + raise RuntimeError( + "You cannot specify a --forwarder option without the " + "--setup-dns option") + if self.auto_forwarders: + raise RuntimeError( + "You cannot specify a --auto-forwarders option without " + "the --setup-dns option") + if self.no_forwarders: + raise RuntimeError( + "You cannot specify a --no-forwarders option without the " + "--setup-dns option") + if self.forward_policy: + raise RuntimeError( + "You cannot specify a --forward-policy option without the " + "--setup-dns option") + if self.reverse_zones: + raise RuntimeError( + "You cannot specify a --reverse-zone option without the " + "--setup-dns option") + if self.auto_reverse: + raise RuntimeError( + "You cannot specify a --auto-reverse option without the " + "--setup-dns option") + if self.no_reverse: + raise RuntimeError( + "You cannot specify a --no-reverse option without the " + "--setup-dns option") + if self.no_dnssec_validation: + raise RuntimeError( + "You cannot specify a --no-dnssec-validation option " + "without the --setup-dns option") + elif self.forwarders and self.no_forwarders: + raise RuntimeError( + "You cannot specify a --forwarder option together with " + "--no-forwarders") + elif self.auto_forwarders and self.no_forwarders: + raise RuntimeError( + "You cannot specify a --auto-forwarders option together with " + "--no-forwarders") + elif self.reverse_zones and self.no_reverse: + raise RuntimeError( + "You cannot specify a --reverse-zone option together with " + "--no-reverse") + elif self.auto_reverse and self.no_reverse: + raise RuntimeError( + "You cannot specify a --auto-reverse option together with " + "--no-reverse") + + if not hasattr(self, 'replica_file'): + if self.external_cert_files and self.dirsrv_cert_files: + raise RuntimeError( + "Service certificate file options cannot be used with the " + "external CA options.") + + if self.external_ca_type and not self.external_ca: + raise RuntimeError( + "You cannot specify --external-ca-type without " + "--external-ca") + + if self.uninstalling: + if (self.realm_name or self.new_admin_password or + self.master_password): + raise RuntimeError( + "In uninstall mode, -a, -r and -P options are not " + "allowed") + elif not self.interactive: + if (not self.realm_name or not self.new_dm_password or + not self.new_admin_password): + raise RuntimeError( + "In unattended mode you need to provide at least -r, " + "-p and -a options") + if self.setup_dns: + if (not self.forwarders and + not self.no_forwarders and + not self.auto_forwarders): + raise RuntimeError( + "You must specify at least one of --forwarder, " + "--auto-forwarders, or --no-forwarders options") + + any_ignore_option_true = any( + [self.ignore_topology_disconnect, self.ignore_last_of_role]) + if any_ignore_option_true and not self.uninstalling: + raise RuntimeError( + "'--ignore-topology-disconnect/--ignore-last-of-role' " + "options can be used only during uninstallation") + + if self.idmax < self.idstart: + raise RuntimeError( + "idmax (%s) cannot be smaller than idstart (%s)" % + (self.idmax, self.idstart)) + else: + cert_file_req = (self.dirsrv_cert_files, self.http_cert_files) + cert_file_opt = (self.pkinit_cert_files,) + + if self.replica_file is None: + # If any of the PKCS#12 options are selected, all are required. + if any(cert_file_req + cert_file_opt) and not all(cert_file_req): + raise RuntimeError( + "--dirsrv-cert-file and --http-cert-file are required " + "if any PKCS#12 options are used") + + if self.servers and not self.domain_name: + raise RuntimeError( + "The --server option cannot be used without providing " + "domain via the --domain option") + + else: + if not ipautil.file_exists(self.replica_file): + raise RuntimeError( + "Replica file %s does not exist" % self.replica_file) + + if any(cert_file_req + cert_file_opt): + raise RuntimeError( + "You cannot specify any of --dirsrv-cert-file, " + "--http-cert-file, or --pkinit-cert-file together " + "with replica file") + + CLIKnob = collections.namedtuple('CLIKnob', ('value', 'name')) + + conflicting_knobs = ( + CLIKnob(self.realm_name, '--realm'), + CLIKnob(self.domain_name, '--domain'), + CLIKnob(self.host_name, '--hostname'), + CLIKnob(self.servers, '--server'), + CLIKnob(self.principal, '--principal'), + ) + + if any([k.value is not None for k in conflicting_knobs]): + conflicting_knob_names = [ + knob.name for knob in conflicting_knobs + if knob.value is not None + ] + + raise RuntimeError( + "You cannot specify '{0}' option(s) with replica file." + .format(", ".join(conflicting_knob_names)) + ) + + if self.setup_dns: + if (not self.forwarders and + not self.no_forwarders and + not self.auto_forwarders): + raise RuntimeError( + "You must specify at least one of --forwarder, " + "--auto-forwarders, or --no-forwarders options") + + # Automatically disable pkinit w/ dogtag until that is supported + self.no_pkinit = True