#!/usr/bin/python2
#
# Authors:
#   Rob Crittenden <rcritten@redhat.com>
#
# Copyright (C) 2009  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/>.

"""
Upgrade configuration files to a newer template.
"""

import sys
import re
import os
import shutil
import pwd
import fileinput
import ConfigParser

from ipalib import api
import SSSDConfig
import ipalib.util
import ipalib.errors
from ipaplatform import services
from ipaplatform.tasks import tasks
from ipapython import ipautil, sysrestore, version, certdb
from ipapython.config import IPAOptionParser
from ipapython.ipa_log_manager import *
from ipapython import certmonger
from ipapython import dogtag
from ipaplatform.paths import paths
from ipaserver.install import installutils
from ipaserver.install import dsinstance
from ipaserver.install import httpinstance
from ipaserver.install import memcacheinstance
from ipaserver.install import bindinstance
from ipaserver.install import service
from ipaserver.install import cainstance
from ipaserver.install import certs
from ipaserver.install import otpdinstance
from ipaserver.install import sysupgrade
from ipaserver.install import dnskeysyncinstance


def parse_options():
    parser = IPAOptionParser(version=version.VERSION)
    parser.add_option("-d", "--debug", dest="debug", action="store_true",
                      default=False, help="print debugging information")
    parser.add_option("-q", "--quiet", dest="quiet",
                      action="store_true",
                      default=False, help="Output only errors")

    options, args = parser.parse_args()
    safe_options = parser.get_safe_opts(options)

    return safe_options, options

class KpasswdInstance(service.SimpleServiceInstance):
    def __init__(self):
        service.SimpleServiceInstance.__init__(self, "ipa_kpasswd")

def uninstall_ipa_kpasswd():
    """
    We can't use the full service uninstaller because that will attempt
    to stop and disable the service which by now doesn't exist. We just
    want to clean up sysrestore.state to remove all references to
    ipa_kpasswd.
    """
    ipa_kpasswd = KpasswdInstance()

    running = ipa_kpasswd.restore_state("running")
    enabled = not ipa_kpasswd.restore_state("enabled")

    if enabled is not None and not enabled:
        ipa_kpasswd.remove()

def backup_file(filename, ext):
    """Make a backup of filename using ext as the extension. Do not overwrite
       previous backups."""
    if not os.path.isabs(filename):
        raise ValueError("Absolute path required")

    backupfile = filename + ".bak"
    (reldir, file) = os.path.split(filename)

    while os.path.exists(backupfile):
        backupfile = backupfile + "." + str(ext)

    try:
        shutil.copy2(filename, backupfile)
    except IOError, e:
        if e.errno == 2: # No such file or directory
            pass
        else:
            raise e

def update_conf(sub_dict, filename, template_filename):
    template = ipautil.template_file(template_filename, sub_dict)
    fd = open(filename, "w")
    fd.write(template)
    fd.close()

def find_hostname():
    """Find the hostname currently configured in ipa-rewrite.conf"""
    filename=paths.HTTPD_IPA_REWRITE_CONF

    if not ipautil.file_exists(filename):
        return None

    pattern = "^[\s#]*.*https:\/\/([A-Za-z0-9\.\-]*)\/.*"
    p = re.compile(pattern)
    for line in fileinput.input(filename):
        if p.search(line):
            fileinput.close()
            return p.search(line).group(1)
    fileinput.close()

    raise RuntimeError("Unable to determine the fully qualified hostname from %s" % filename)

def find_autoredirect(fqdn):
    """
    When upgrading ipa-rewrite.conf we need to see if the automatic redirect
    was disabled during install time (or afterward). So sift through the
    configuration file and see if we can determine the status.

    Returns True if autoredirect is enabled, False otherwise
    """
    filename = paths.HTTPD_IPA_REWRITE_CONF
    if os.path.exists(filename):
        pattern = "^RewriteRule \^/\$ https://%s/ipa/ui \[L,NC,R=301\]" % fqdn
        p = re.compile(pattern)
        for line in fileinput.input(filename):
            if p.search(line):
                fileinput.close()
                return True
        fileinput.close()
        return False
    return True

def find_version(filename):
    """Find the version of a configuration file

    If no VERSION entry exists in the file, returns 0.
    If the file does not exist, returns -1.
    """
    if os.path.exists(filename):
        pattern = "^[\s#]*VERSION\s+([0-9]+)\s+.*"
        p = re.compile(pattern)
        for line in fileinput.input(filename):
            if p.search(line):
                fileinput.close()
                return p.search(line).group(1)
        fileinput.close()

        # no VERSION found
        return 0
    else:
        return -1

def upgrade(sub_dict, filename, template, add=False):
    """
    Get the version from the current and template files and update the
    installed configuration file if there is a new template.

    If add is True then create a new configuration file.
    """
    old = int(find_version(filename))
    new = int(find_version(template))

    if old < 0 and not add:
        root_logger.error("%s not found." % filename)
        sys.exit(1)

    if new < 0:
        root_logger.error("%s not found." % template)

    if old == 0:
        # The original file does not have a VERSION entry. This means it's now
        # managed by IPA, but previously was not.
        root_logger.warning("%s is now managed by IPA. It will be "
            "overwritten. A backup of the original will be made.", filename)

    if old < new or (add and old == 0):
        backup_file(filename, new)
        update_conf(sub_dict, filename, template)
        root_logger.info("Upgraded %s to version %d", filename, new)

def check_certs():
    """Check ca.crt is in the right place, and try to fix if not"""
    root_logger.info('[Verifying that root certificate is published]')
    if not os.path.exists(paths.CA_CRT):
        ca_file = paths.ALIAS_CACERT_ASC
        if os.path.exists(ca_file):
            old_umask = os.umask(022)   # make sure its readable by httpd
            try:
                shutil.copyfile(ca_file, paths.CA_CRT)
            finally:
                os.umask(old_umask)
        else:
            root_logger.error("Missing Certification Authority file.")
            root_logger.error("You should place a copy of the CA certificate in /usr/share/ipa/html/ca.crt")
    else:
        root_logger.debug('Certificate file exists')

def upgrade_pki(ca, fstore):
    """
    Update/add the dogtag proxy configuration. The IPA side of this is
    handled in ipa-pki-proxy.conf.

    This requires enabling SSL renegotiation.
    """
    configured_constants = dogtag.configured_constants()
    root_logger.info('[Verifying that CA proxy configuration is correct]')
    if not ca.is_configured():
        root_logger.info('CA is not configured')
        return

    http = httpinstance.HTTPInstance(fstore)
    http.enable_mod_nss_renegotiate()
    if not installutils.get_directive(configured_constants.CS_CFG_PATH,
                                      'proxy.securePort', '=') and \
            os.path.exists(paths.PKI_SETUP_PROXY):
        # update proxy configuration with stopped dogtag to prevent corruption
        # of CS.cfg
        ipautil.run([paths.PKI_SETUP_PROXY, '-pki_instance_root=/var/lib',
                     '-pki_instance_name=pki-ca','-subsystem_type=ca'])
        root_logger.debug('Proxy configuration updated')
    else:
        root_logger.debug('Proxy configuration up-to-date')

def update_dbmodules(realm, filename=paths.KRB5_CONF):
    newfile = []
    found_dbrealm = False
    found_realm = False
    prefix = ''

    root_logger.info('[Verifying that KDC configuration is using ipa-kdb backend]')
    st = os.stat(filename)
    fd = open(filename)

    lines = fd.readlines()
    fd.close()

    if '    db_library = ipadb.so\n' in lines:
        root_logger.debug('dbmodules already updated in %s', filename)
        return

    for line in lines:
        if line.startswith('[dbmodules]'):
            found_dbrealm = True
        if found_dbrealm and line.find(realm) > -1:
            found_realm = True
            prefix = '#'
        if found_dbrealm and line.find('}') > -1 and found_realm:
            found_realm = False
            newfile.append('#%s' % line)
            prefix = ''
            continue

        newfile.append('%s%s' % (prefix, line))

    # Append updated dbmodules information
    newfile.append('  %s = {\n' % realm)
    newfile.append('    db_library = ipadb.so\n')
    newfile.append('  }\n')

    # Write out new file
    fd = open(filename, 'w')
    fd.write("".join(newfile))
    fd.close()
    root_logger.debug('%s updated', filename)

def cleanup_kdc(fstore):
    """
    Clean up old KDC files if they exist. We need to remove the actual
    file and any references in the uninstall configuration.
    """
    root_logger.info('[Checking for deprecated KDC configuration files]')
    for file in ['kpasswd.keytab', 'ldappwd']:
        filename = os.path.join(paths.VAR_KERBEROS_KRB5KDC_DIR, file)
        installutils.remove_file(filename)
        if fstore.has_file(filename):
            fstore.untrack_file(filename)
            root_logger.debug('Uninstalling %s', filename)

def cleanup_adtrust(fstore):
    """
    Clean up any old Samba backup files that were deprecated.
    """

    root_logger.info('[Checking for deprecated backups of Samba '
                     'configuration files]')

    for backed_up_file in [paths.SMB_CONF]:
        if fstore.has_file(backed_up_file):
            fstore.untrack_file(backed_up_file)
            root_logger.debug('Removing %s from backup', backed_up_file)


def setup_firefox_extension(fstore):
    """Set up the Firefox configuration extension, if it's not set up yet
    """
    root_logger.info('[Setting up Firefox extension]')
    http = httpinstance.HTTPInstance(fstore)
    realm = api.env.realm
    domain = api.env.domain
    http.setup_firefox_extension(realm, domain)


def upgrade_ipa_profile(ca, domain, fqdn):
    """
    Update the IPA Profile provided by dogtag

    Returns True if restart is needed, False otherwise.
    """
    root_logger.info('[Verifying that CA service certificate profile is updated]')
    if ca.is_configured():
        ski = ca.enable_subject_key_identifier()
        if ski:
            root_logger.debug('Subject Key Identifier updated.')
        else:
            root_logger.debug('Subject Key Identifier already set.')
        san = ca.enable_subject_alternative_name()
        if san:
            root_logger.debug('Subject Alternative Name updated.')
        else:
            root_logger.debug('Subject Alternative Name already set.')
        audit = ca.set_audit_renewal()
        uri = ca.set_crl_ocsp_extensions(domain, fqdn)
        if audit or ski or san or uri:
            return True
    else:
        root_logger.info('CA is not configured')

    return False


def named_remove_deprecated_options():
    """
    From IPA 3.3, persistent search is a default mechanism for new DNS zone
    detection.

    Remove psearch, zone_refresh and cache_ttl options, as they have been
    deprecated in bind-dyndb-ldap configuration file.

    When some change in named.conf is done, this functions returns True.
    """

    root_logger.info('[Removing deprecated DNS configuration options]')

    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    deprecated_options = ['zone_refresh', 'psearch', 'cache_ttl']
    removed_options = []

    try:
        # Remove all the deprecated options
        for option in deprecated_options:
            value = bindinstance.named_conf_get_directive(option)

            if value is not None:
                bindinstance.named_conf_set_directive(option, None)
                removed_options.append(option)

    except IOError, e:
        root_logger.error('Cannot modify DNS configuration in %s: %s',
                          bindinstance.NAMED_CONF, e)

    # Log only the changed options
    if not removed_options:
        root_logger.debug('No changes made')
        return False

    root_logger.debug('The following configuration options have been removed: '
                      '{options}'.format(options = ', '.join(removed_options)))
    return True


def named_set_minimum_connections():
    """
    Sets the minimal number of connections.

    When some change in named.conf is done, this functions returns True.
    """

    changed = False

    root_logger.info('[Ensuring minimal number of connections]')

    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return changed

    # make sure number of connections is right
    minimum_connections = 4

    try:
        connections = bindinstance.named_conf_get_directive('connections')
    except IOError, e:
        root_logger.debug('Cannot retrieve connections option from %s: %s',
                bindinstance.NAMED_CONF, e)
        return changed

    try:
        if connections is not None:
            connections = int(connections)
    except ValueError:
        # this should not happend, but there is some bad value in
        # "connections" option, bail out
        pass
    else:
        if connections is not None and connections < minimum_connections:
            try:
                bindinstance.named_conf_set_directive('connections',
                                                       minimum_connections)
                root_logger.debug('Connections set to %d', minimum_connections)
            except IOError, e:
                root_logger.error('Cannot update connections in %s: %s',
                        bindinstance.NAMED_CONF, e)
            else:
                changed = True

    if not changed:
        root_logger.debug('No changes made')

    return changed


def named_enable_serial_autoincrement():
    """
    Serial autoincrement is a requirement for zone transfers or DNSSEC. It
    should be enabled both for new installs and upgraded servers.

    When some change in named.conf is done, this functions returns True
    """
    changed = False

    root_logger.info('[Enabling serial autoincrement in DNS]')

    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return changed

    try:
        serial_autoincrement = bindinstance.named_conf_get_directive(
                                    'serial_autoincrement')
    except IOError, e:
        root_logger.debug('Cannot retrieve psearch option from %s: %s',
                bindinstance.NAMED_CONF, e)
        return changed
    else:
        serial_autoincrement = None if serial_autoincrement is None \
                else serial_autoincrement.lower()

    # enable SOA serial autoincrement
    if not sysupgrade.get_upgrade_state('named.conf', 'autoincrement_enabled'):
        if serial_autoincrement != 'yes':
            try:
                bindinstance.named_conf_set_directive('serial_autoincrement', 'yes')
            except IOError, e:
                root_logger.error('Cannot enable serial_autoincrement in %s: %s',
                        bindinstance.NAMED_CONF, e)
                return changed
            else:
                root_logger.debug('Serial autoincrement enabled')
                changed = True
        else:
            root_logger.debug('Serial autoincrement is alredy enabled')
        sysupgrade.set_upgrade_state('named.conf', 'autoincrement_enabled', True)
    else:
        root_logger.debug('Skip serial autoincrement check')

    return changed

def named_update_gssapi_configuration():
    """
    Update GSSAPI configuration in named.conf to a recent API.
    tkey-gssapi-credential and tkey-domain is replaced with tkey-gssapi-keytab.
    Details can be found in https://fedorahosted.org/freeipa/ticket/3429.

    When some change in named.conf is done, this functions returns True
    """

    root_logger.info('[Updating GSSAPI configuration in DNS]')

    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    if sysupgrade.get_upgrade_state('named.conf', 'gssapi_updated'):
        root_logger.debug('Skip GSSAPI configuration check')
        return False

    try:
        gssapi_keytab = bindinstance.named_conf_get_directive('tkey-gssapi-keytab',
                bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot retrieve tkey-gssapi-keytab option from %s: %s',
                bindinstance.NAMED_CONF, e)
        return False
    else:
        if gssapi_keytab:
            root_logger.debug('GSSAPI configuration already updated')
            sysupgrade.set_upgrade_state('named.conf', 'gssapi_updated', True)
            return False

    try:
        tkey_credential = bindinstance.named_conf_get_directive('tkey-gssapi-credential',
                bindinstance.NAMED_SECTION_OPTIONS)
        tkey_domain = bindinstance.named_conf_get_directive('tkey-domain',
                bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot retrieve tkey-gssapi-credential option from %s: %s',
                bindinstance.NAMED_CONF, e)
        return False

    if not tkey_credential or not tkey_domain:
        root_logger.error('Either tkey-gssapi-credential or tkey-domain is missing in %s. '
            'Skip update.', bindinstance.NAMED_CONF)
        return False

    try:
        bindinstance.named_conf_set_directive('tkey-gssapi-credential', None,
                                              bindinstance.NAMED_SECTION_OPTIONS)
        bindinstance.named_conf_set_directive('tkey-domain', None,
                                              bindinstance.NAMED_SECTION_OPTIONS)
        bindinstance.named_conf_set_directive('tkey-gssapi-keytab', paths.NAMED_KEYTAB,
                                              bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot update GSSAPI configuration in %s: %s',
                bindinstance.NAMED_CONF, e)
        return False
    else:
        root_logger.debug('GSSAPI configuration updated')

    sysupgrade.set_upgrade_state('named.conf', 'gssapi_updated', True)
    return True

def named_update_pid_file():
    """
    Make sure that named reads the pid file from the right file
    """
    root_logger.info('[Updating pid-file configuration in DNS]')

    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    if sysupgrade.get_upgrade_state('named.conf', 'pid-file_updated'):
        root_logger.debug('Skip pid-file configuration check')
        return False

    try:
        pid_file = bindinstance.named_conf_get_directive('pid-file',
                bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot retrieve pid-file option from %s: %s',
                bindinstance.NAMED_CONF, e)
        return False
    else:
        if pid_file:
            root_logger.debug('pid-file configuration already updated')
            sysupgrade.set_upgrade_state('named.conf', 'pid-file_updated', True)
            return False

    try:
        bindinstance.named_conf_set_directive('pid-file', paths.NAMED_PID,
                                              bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot update pid-file configuration in %s: %s',
                bindinstance.NAMED_CONF, e)
        return False
    else:
        root_logger.debug('pid-file configuration updated')

    sysupgrade.set_upgrade_state('named.conf', 'pid-file_updated', True)
    return True

def named_enable_dnssec():
    """
    Enable dnssec in named.conf
    """
    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    if not sysupgrade.get_upgrade_state('named.conf', 'dnssec_enabled'):
        root_logger.info('[Enabling "dnssec-enable" configuration in DNS]')
        try:
            bindinstance.named_conf_set_directive('dnssec-enable', 'yes',
                                                  bindinstance.NAMED_SECTION_OPTIONS,
                                                  str_val=False)
        except IOError, e:
            root_logger.error('Cannot update dnssec-enable configuration in %s: %s',
                    bindinstance.NAMED_CONF, e)
            return False
    else:
        root_logger.debug('dnssec-enabled in %s' % bindinstance.NAMED_CONF)

    sysupgrade.set_upgrade_state('named.conf', 'dnssec_enabled', True)
    return True

def named_validate_dnssec():
    """
    Disable dnssec validation in named.conf

    We can't let enable it by default, there can be non-valid dns forwarders
    which breaks DNSSEC validation
    """
    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    if (not sysupgrade.get_upgrade_state('named.conf', 'dnssec_validation_upgraded')
        and bindinstance.named_conf_get_directive(
                'dnssec-validation', bindinstance.NAMED_SECTION_OPTIONS,
                str_val=False) is None):
        # dnssec-validation is not configured, disable it
        root_logger.info('[Disabling "dnssec-validate" configuration in DNS]')
        try:
            bindinstance.named_conf_set_directive('dnssec-validation', 'no',
                                                  bindinstance.NAMED_SECTION_OPTIONS,
                                                  str_val=False)
        except IOError, e:
            root_logger.error('Cannot update dnssec-validate configuration in %s: %s',
                    bindinstance.NAMED_CONF, e)
            return False
    else:
        root_logger.debug('dnssec-validate already configured in %s' % bindinstance.NAMED_CONF)

    sysupgrade.set_upgrade_state('named.conf', 'dnssec_validation_upgraded', True)
    return True

def named_bindkey_file_option():
    """
    Add options bindkey_file to named.conf
    """
    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    if sysupgrade.get_upgrade_state('named.conf', 'bindkey-file_updated'):
        root_logger.debug('Skip bindkey-file configuration check')
        return False

    try:
        bindkey_file = bindinstance.named_conf_get_directive('bindkey-file',
                bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot retrieve bindkey-file option from %s: %s',
                bindinstance.NAMED_CONF, e)
        return False
    else:
        if bindkey_file:
            root_logger.debug('bindkey-file configuration already updated')
            sysupgrade.set_upgrade_state('named.conf', 'bindkey-file_updated', True)
            return False

    root_logger.info('[Setting "bindkeys-file" option in named.conf]')
    try:
        bindinstance.named_conf_set_directive('bindkeys-file',
                                              paths.NAMED_BINDKEYS_FILE,
                                              bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot update bindkeys-file configuration in %s: %s',
                bindinstance.NAMED_CONF, e)
        return False


    sysupgrade.set_upgrade_state('named.conf', 'bindkey-file_updated', True)
    return True

def named_managed_keys_dir_option():
    """
    Add options managed_keys_directory to named.conf
    """
    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    if sysupgrade.get_upgrade_state('named.conf', 'managed-keys-directory_updated'):
        root_logger.debug('Skip managed-keys-directory configuration check')
        return False

    try:
        managed_keys = bindinstance.named_conf_get_directive('managed-keys-directory',
                bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot retrieve managed-keys-directory option from %s: %s',
                bindinstance.NAMED_CONF, e)
        return False
    else:
        if managed_keys:
            root_logger.debug('managed_keys_directory configuration already updated')
            sysupgrade.set_upgrade_state('named.conf', 'managed-keys-directory_updated', True)
            return False

    root_logger.info('[Setting "managed-keys-directory" option in named.conf]')
    try:
        bindinstance.named_conf_set_directive('managed-keys-directory',
                                              paths.NAMED_MANAGED_KEYS_DIR,
                                              bindinstance.NAMED_SECTION_OPTIONS)
    except IOError, e:
        root_logger.error('Cannot update managed-keys-directory configuration in %s: %s',
                bindinstance.NAMED_CONF, e)
        return False


    sysupgrade.set_upgrade_state('named.conf', 'managed-keys-directory_updated', True)
    return True

def named_root_key_include():
    """
    Add options managed_keys_directory to named.conf
    """
    if not bindinstance.named_conf_exists():
        # DNS service may not be configured
        root_logger.info('DNS is not configured')
        return False

    if sysupgrade.get_upgrade_state('named.conf', 'root_key_updated'):
        root_logger.debug('Skip root key configuration check')
        return False

    try:
        root_key = bindinstance.named_conf_include_exists(paths.NAMED_ROOT_KEY)
    except IOError, e:
        root_logger.error('Cannot check root key include in %s: %s',
                bindinstance.NAMED_CONF, e)
        return False
    else:
        if root_key:
            root_logger.debug('root keys configuration already updated')
            sysupgrade.set_upgrade_state('named.conf', 'root_key_updated', True)
            return False

    root_logger.info('[Including named root key in named.conf]')
    try:
        bindinstance.named_conf_add_include(paths.NAMED_ROOT_KEY)
    except IOError, e:
        root_logger.error('Cannot update named root key include in %s: %s',
                bindinstance.NAMED_CONF, e)
        return False


    sysupgrade.set_upgrade_state('named.conf', 'root_key_updated', True)
    return True

def certificate_renewal_update(ca):
    """
    Update certmonger certificate renewal configuration.
    """
    dogtag_constants = dogtag.configured_constants()

    # bump version when requests is changed
    version = 2
    requests = (
        (
            dogtag_constants.ALIAS_DIR,
            'auditSigningCert cert-pki-ca',
            'dogtag-ipa-ca-renew-agent',
            'stop_pkicad',
            'renew_ca_cert',
            None,
        ),
        (
            dogtag_constants.ALIAS_DIR,
            'ocspSigningCert cert-pki-ca',
            'dogtag-ipa-ca-renew-agent',
            'stop_pkicad',
            'renew_ca_cert',
            None,
        ),
        (
            dogtag_constants.ALIAS_DIR,
            'subsystemCert cert-pki-ca',
            'dogtag-ipa-ca-renew-agent',
            'stop_pkicad',
            'renew_ca_cert',
            None,
        ),
        (
            dogtag_constants.ALIAS_DIR,
            'caSigningCert cert-pki-ca',
            'dogtag-ipa-ca-renew-agent',
            'stop_pkicad',
            'renew_ca_cert',
            'ipaCACertRenewal',
        ),
        (
            paths.HTTPD_ALIAS_DIR,
            'ipaCert',
            'dogtag-ipa-ca-renew-agent',
            None,
            'renew_ra_cert',
            None,
        ),
        (
            dogtag_constants.ALIAS_DIR,
            'Server-Cert cert-pki-ca',
            'dogtag-ipa-renew-agent',
            None,
            None,
            None,
        ),
    )

    root_logger.info("[Update certmonger certificate renewal configuration to "
                     "version %d]" % version)
    if not ca.is_configured():
        root_logger.info('CA is not configured')
        return False

    state = 'certificate_renewal_update_%d' % version
    if sysupgrade.get_upgrade_state('dogtag', state):
        return False

    # State not set, lets see if we are already configured
    for request in requests:
        nss_dir, nickname, ca_name, pre_command, post_command, profile = request
        criteria = {
            'cert-database': nss_dir,
            'cert-nickname': nickname,
            'ca-name': ca_name,
            'template-profile': profile,
        }
        request_id = certmonger.get_request_id(criteria)
        if request_id is None:
            break

        val = certmonger.get_request_value(request_id, 'cert-presave-command')
        if val is not None:
            val = val.split(' ', 1)[0]
            val = os.path.basename(val)
        if pre_command != val:
            break

        val = certmonger.get_request_value(request_id, 'cert-postsave-command')
        if val is not None:
            val = val.split(' ', 1)[0]
            val = os.path.basename(val)
        if post_command != val:
            break
    else:
        sysupgrade.set_upgrade_state('dogtag', state, True)
        root_logger.info("Certmonger certificate renewal configuration is "
                         "already at version %d" % version)
        return False

    # Ok, now we need to stop tracking, then we can start tracking them
    # again with new configuration:
    cainstance.stop_tracking_certificates(dogtag_constants)

    if not sysupgrade.get_upgrade_state('dogtag',
                                        'certificate_renewal_update_1'):
        filename = paths.CERTMONGER_CAS_CA_RENEWAL
        if os.path.exists(filename):
            with installutils.stopped_service('certmonger'):
                root_logger.info("Removing %s" % filename)
                installutils.remove_file(filename)

    ca.configure_certmonger_renewal()
    ca.configure_renewal()
    ca.configure_agent_renewal()
    ca.track_servercert()

    sysupgrade.set_upgrade_state('dogtag', state, True)
    root_logger.info("Certmonger certificate renewal configuration updated to "
                     "version %d" % version)
    return True

def copy_crl_file(old_path, new_path=None):
    """
    Copy CRL to new location, update permissions and SELinux context
    """
    if new_path is None:
        filename = os.path.basename(old_path)
        new_path = os.path.join(dogtag.configured_constants().CRL_PUBLISH_PATH,
                                filename)
    root_logger.debug('copy_crl_file: %s -> %s', old_path, new_path)

    if os.path.islink(old_path):
        # update symlink to the most most recent CRL file
        filename = os.path.basename(os.readlink(old_path))
        realpath = os.path.join(dogtag.configured_constants().CRL_PUBLISH_PATH,
                                filename)
        root_logger.debug('copy_crl_file: Create symlink %s -> %s',
            new_path, realpath)
        os.symlink(realpath, new_path)
    else:
        shutil.copy2(old_path, new_path)
        pent = pwd.getpwnam(cainstance.PKI_USER)
        os.chown(new_path, pent.pw_uid, pent.pw_gid)

    tasks.restore_context(new_path)

def migrate_crl_publish_dir(ca):
    """
    Move CRL publish dir from /var/lib/pki-ca/publish to IPA controlled tree:
    /var/lib/ipa/pki-ca/publish
    """
    root_logger.info('[Migrate CRL publish directory]')
    if sysupgrade.get_upgrade_state('dogtag', 'moved_crl_publish_dir'):
        root_logger.info('CRL tree already moved')
        return False

    if not ca.is_configured():
        root_logger.info('CA is not configured')
        return False

    caconfig = dogtag.configured_constants()

    try:
        old_publish_dir = installutils.get_directive(caconfig.CS_CFG_PATH,
            'ca.publish.publisher.instance.FileBaseCRLPublisher.directory',
            separator='=')
    except OSError, e:
        root_logger.error('Cannot read CA configuration file "%s": %s',
                caconfig.CS_CFG_PATH, e)
        return False

    # Prepare target publish dir (creation, permissions, SELinux context)
    # Run this every update to ensure proper values
    publishdir = ca.prepare_crl_publish_dir()

    if old_publish_dir == caconfig.CRL_PUBLISH_PATH:
        # publish dir is already updated
        root_logger.info('Publish directory already set to new location')
        sysupgrade.set_upgrade_state('dogtag', 'moved_crl_publish_dir', True)
        return False

    # Copy all CRLs to new directory
    root_logger.info('Copy all CRLs to new publish directory')
    try:
        crl_files_unsorted = cainstance.get_crl_files(old_publish_dir)
    except OSError, e:
        root_logger.error('Cannot move CRL files to new directory: %s', e)
    else:
        # Move CRL files at the end of the list to make sure that the actual
        # CRL files are copied first
        crl_files = sorted(crl_files_unsorted,
                           key=lambda f: os.path.islink(f))
        for f in crl_files:
            try:
                copy_crl_file(f)
            except Exception, e:
                root_logger.error('Cannot move CRL file to new directory: %s', e)

    try:
        installutils.set_directive(caconfig.CS_CFG_PATH,
            'ca.publish.publisher.instance.FileBaseCRLPublisher.directory',
            publishdir, quotes=False, separator='=')
    except OSError, e:
        root_logger.error('Cannot update CA configuration file "%s": %s',
                caconfig.CS_CFG_PATH, e)
        return False
    sysupgrade.set_upgrade_state('dogtag', 'moved_crl_publish_dir', True)
    root_logger.info('CRL publish directory has been migrated, '
                     'request pki-ca restart')
    return True


def ca_enable_pkix(ca):
    root_logger.info('[Enable PKIX certificate path discovery and validation]')
    if sysupgrade.get_upgrade_state('dogtag', 'pkix_enabled'):
        root_logger.info('PKIX already enabled')
        return False

    if not ca.is_configured():
        root_logger.info('CA is not configured')
        return False

    ca.enable_pkix()
    sysupgrade.set_upgrade_state('dogtag', 'pkix_enabled', True)

    return True


def add_ca_dns_records():
    root_logger.info('[Add missing CA DNS records]')

    if sysupgrade.get_upgrade_state('dns', 'ipa_ca_records'):
        root_logger.info('IPA CA DNS records already processed')
        return

    if not api.Backend.ldap2.isconnected():
        try:
            api.Backend.ldap2.connect(autobind=True)
        except ipalib.errors.PublicError, e:
            root_logger.error(
                "Cannot connect to LDAP to add DNS records: %s", e)
            return

    ret = api.Command['dns_is_enabled']()
    if not ret['result']:
        root_logger.info('DNS is not configured')
        sysupgrade.set_upgrade_state('dns', 'ipa_ca_records', True)
        return

    bind = bindinstance.BindInstance()

    bind.convert_ipa_ca_cnames(api.env.domain)

    # DNS is enabled, so let bindinstance find out if CA is enabled
    # and let it add the record in that case
    bind.add_ipa_ca_dns_records(api.env.host, api.env.domain,
                                ca_configured=None)

    sysupgrade.set_upgrade_state('dns', 'ipa_ca_records', True)


def find_subject_base():
    """
    Try to find the current value of certificate subject base.
    1) Look in sysupgrade first
    2) If no value is found there, look in DS (start DS if necessary)
    3) Last resort, look in the certmap.conf itself
    4) If all fails, log loudly and return None
    """
    root_logger.debug('Trying to find certificate subject base in sysupgrade')
    subject_base = sysupgrade.get_upgrade_state('certmap.conf', 'subject_base')

    if subject_base:
        root_logger.debug(
            'Found certificate subject base in sysupgrade: %s',
            subject_base
        )
        return subject_base

    root_logger.debug('Unable to find certificate subject base in sysupgrade')
    root_logger.debug('Trying to find certificate subject base in DS')

    ds_is_running = services.knownservices.dirsrv.is_running()
    if not ds_is_running:
        try:
            services.knownservices.dirsrv.start()
        except ipautil.CalledProcessError as e:
            root_logger.error('Cannot start DS to find certificate '
                              'subject base: %s', e)
        else:
            ds_is_running = True

    if ds_is_running:
        try:
            api.Backend.ldap2.connect(autobind=True)
        except ipalib.errors.PublicError, e:
            root_logger.error('Cannot connect to DS to find certificate '
                              'subject base: %s', e)
        else:
            ret = api.Command['config_show']()
            api.Backend.ldap2.disconnect()
            subject_base = str(ret['result']['ipacertificatesubjectbase'][0])
            root_logger.debug(
                'Found certificate subject base in DS: %s',
                subject_base
            )

    if not subject_base:
        root_logger.debug('Unable to find certificate subject base in DS')
        root_logger.debug('Trying to find certificate subject base in '
                          'certmap.conf')

        certmap_dir = dsinstance.config_dirname(
            dsinstance.realm_to_serverid(api.env.realm)
        )
        try:
            with open(os.path.join(certmap_dir, 'certmap.conf')) as f:
                for line in f:
                    if line.startswith('certmap ipaca'):
                        subject_base = line.strip().split(',')[-1]
                        root_logger.debug(
                            'Found certificate subject base in certmap.conf: '
                            '%s',
                            subject_base
                        )

        except IOError as e:
            root_logger.error('Cannot open certmap.conf to find certificate '
                              'subject base: %s', e.strerror)

    if subject_base:
        sysupgrade.set_upgrade_state(
            'certmap.conf',
            'subject_base',
            subject_base
        )
        return subject_base

    root_logger.debug('Unable to find certificate subject base in '
                      'certmap.conf')
    root_logger.error('Unable to determine certificate subject base. '
                      'certmap.conf will not be updated.')


def uninstall_selfsign(ds, http):
    root_logger.info('[Removing self-signed CA]')
    """Replace self-signed CA by a CA-less install"""
    if api.env.ra_plugin != 'selfsign':
        root_logger.debug('Self-signed CA is not installed')
        return

    root_logger.warning(
        'Removing self-signed CA. Certificates will need to managed manually.')
    p = ConfigParser.SafeConfigParser()
    p.read(paths.IPA_DEFAULT_CONF)
    p.set('global', 'enable_ra', 'False')
    p.set('global', 'ra_plugin', 'none')
    with open(paths.IPA_DEFAULT_CONF, 'w') as f:
        p.write(f)

    ds.stop_tracking_certificates()
    http.stop_tracking_certificates()


def mask_named_regular():
    """Disable named, we need to run only named-pkcs11, running both named and
    named-pkcs can cause unexpected errors"""
    if not sysupgrade.get_upgrade_state('dns', 'regular_named_masked'):
        if bindinstance.named_conf_exists():
            root_logger.info('[Masking named]')
            named = services.service('named-regular')
            try:
                named.stop()
            except Exception as e:
                root_logger.warning('Unable to stop named service (%s)', e)

            try:
                named.mask()
            except Exception as e:
                root_logger.warning('Unable to mask named service (%s)', e)

            return True

        sysupgrade.set_upgrade_state('dns', 'regular_named_masked', True)

    return False



def fix_schema_file_syntax():
    """Fix syntax errors in schema files

    https://fedorahosted.org/freeipa/ticket/3578
    """
    root_logger.info('[Fix DS schema file syntax]')

    # This is not handled by normal schema updates, because pre-1.3.2 DS will
    # ignore (auto-fix) these syntax errors, and 1.3.2 and above will choke on
    # them before checking dynamic schema updates.

    if sysupgrade.get_upgrade_state('ds', 'fix_schema_syntax'):
        root_logger.info('Syntax already fixed')
        return

    serverid = dsinstance.realm_to_serverid(api.env.realm)
    ds_dir = dsinstance.config_dirname(serverid)

    # 1. 60ipadns.ldif: Add parenthesis to idnsRecord

    filename = os.path.join(ds_dir, 'schema', '60ipadns.ldif')
    result_lines = []
    with open(filename) as file:
        for line in file:
            line = line.strip('\n')
            if (line.startswith('objectClasses:') and
                    "NAME 'idnsRecord'" in line and
                    line.count('(') == 2 and
                    line.count(')') == 1):
                root_logger.debug('Add closing parenthesis in idnsRecord')
                line += ' )'
            result_lines.append(line)

    with open(filename, 'w') as file:
        file.write('\n'.join(result_lines))

    # 2. 65ipasudo.ldif: Remove extra dollar from ipaSudoRule

    filename = os.path.join(ds_dir, 'schema', '65ipasudo.ldif')
    result_lines = []
    with open(filename) as file:
        for line in file:
            line = line.strip('\n')
            if (line.startswith('objectClasses:') and
                    "NAME 'ipaSudoRule'" in line):
                root_logger.debug('Remove extra dollar sign in ipaSudoRule')
                line = line.replace('$$', '$')
            result_lines.append(line)

    with open(filename, 'w') as file:
        file.write('\n'.join(result_lines))

    # Done

    sysupgrade.set_upgrade_state('ds', 'fix_schema_syntax', True)


def set_sssd_domain_option(option, value):
    sssdconfig = SSSDConfig.SSSDConfig()
    sssdconfig.import_config()
    domain = sssdconfig.get_domain(str(api.env.domain))
    domain.set_option(option, value)
    sssdconfig.save_domain(domain)
    sssdconfig.write(paths.SSSD_CONF)


def remove_ds_ra_cert(subject_base):
    root_logger.info('[Removing RA cert from DS NSS database]')

    if sysupgrade.get_upgrade_state('ds', 'remove_ra_cert'):
        root_logger.info('RA cert already removed')
        return

    dbdir = dsinstance.config_dirname(
        dsinstance.realm_to_serverid(api.env.realm))
    dsdb = certs.CertDB(api.env.realm, nssdir=dbdir, subject_base=subject_base)

    nickname = 'CN=IPA RA,%s' % subject_base
    cert = dsdb.get_cert_from_db(nickname)
    if cert:
        dsdb.delete_cert(nickname)

    sysupgrade.set_upgrade_state('ds', 'remove_ra_cert', True)


def fix_trust_flags():
    root_logger.info('[Fixing trust flags in %s]' % paths.HTTPD_ALIAS_DIR)

    if sysupgrade.get_upgrade_state('http', 'fix_trust_flags'):
        root_logger.info("Trust flags already processed")
        return

    if not api.Backend.ldap2.isconnected():
        try:
            api.Backend.ldap2.connect(autobind=True)
        except ipalib.errors.PublicError, e:
            root_logger.error("Cannot connect to LDAP: %s", e)
            return

    if not api.Command.ca_is_enabled()['result']:
        root_logger.info("CA is not enabled")
        return

    db = certs.CertDB(api.env.realm)
    nickname = certdb.get_ca_nickname(api.env.realm)
    cert = db.get_cert_from_db(nickname)
    if cert:
        db.trust_root_cert(nickname, 'CT,C,C')

    sysupgrade.set_upgrade_state('http', 'fix_trust_flags', True)


def main():
    """
    Get some basics about the system. If getting those basics fail then
    this is likely because the machine isn't currently an IPA server so
    exit gracefully.
    """

    if not os.geteuid()==0:
        sys.exit("\nYou must be root to run this script.\n")

    if not installutils.is_ipa_configured():
        sys.exit(0)

    safe_options, options = parse_options()

    verbose = not options.quiet
    if options.debug:
        console_format = '%(levelname)s: %(message)s'
    else:
        console_format = '%(message)s'

    standard_logging_setup(paths.IPAUPGRADE_LOG, debug=options.debug,
        verbose=verbose, console_format=console_format, filemode='a')
    root_logger.debug('%s was invoked with options: %s' % (sys.argv[0], safe_options))
    root_logger.debug('IPA version %s' % version.VENDOR_VERSION)

    fstore = sysrestore.FileStore(paths.SYSRESTORE)

    api.bootstrap(context='restart', in_server=True)
    api.finalize()

    fqdn = find_hostname()
    if fqdn is None:
        # ipa-rewrite.conf doesn't exist, nothing to do
        sys.exit(0)

    # Ok, we are an IPA server, do the additional tests

    check_certs()

    auto_redirect = find_autoredirect(fqdn)
    configured_constants = dogtag.configured_constants()
    sub_dict = dict(
        REALM=api.env.realm,
        FQDN=fqdn,
        AUTOREDIR='' if auto_redirect else '#',
        CRL_PUBLISH_PATH=configured_constants.CRL_PUBLISH_PATH,
        DOGTAG_PORT=configured_constants.AJP_PORT,
        CLONE='#'
    )

    subject_base = find_subject_base()
    if subject_base:
        sub_dict['SUBJECT_BASE'] = subject_base

    ca = cainstance.CAInstance(api.env.realm, certs.NSS_DIR)
    ca.backup_config()

    with installutils.stopped_service(configured_constants.SERVICE_NAME,
            configured_constants.PKI_INSTANCE_NAME):
        # migrate CRL publish dir before the location in ipa.conf is updated
        ca_restart = migrate_crl_publish_dir(ca)

        if ca.is_configured():
            crl = installutils.get_directive(configured_constants.CS_CFG_PATH,
                    'ca.crl.MasterCRL.enableCRLUpdates', '=')
            sub_dict['CLONE']='#' if crl.lower() == 'true' else ''

        ds_serverid = dsinstance.realm_to_serverid(api.env.realm)
        ds_dirname = dsinstance.config_dirname(ds_serverid)

        upgrade(sub_dict, paths.HTTPD_IPA_CONF, ipautil.SHARE_DIR + "ipa.conf")
        upgrade(sub_dict, paths.HTTPD_IPA_REWRITE_CONF, ipautil.SHARE_DIR + "ipa-rewrite.conf")
        if ca.is_configured():
            upgrade(sub_dict, paths.HTTPD_IPA_PKI_PROXY_CONF, ipautil.SHARE_DIR + "ipa-pki-proxy.conf", add=True)
        else:
            if ipautil.file_exists(paths.HTTPD_IPA_PKI_PROXY_CONF):
                os.remove(paths.HTTPD_IPA_PKI_PROXY_CONF)
        if subject_base:
            upgrade(
                sub_dict,
                os.path.join(ds_dirname, "certmap.conf"),
                os.path.join(ipautil.SHARE_DIR, "certmap.conf.template")
            )
        upgrade_pki(ca, fstore)

    update_dbmodules(api.env.realm)
    uninstall_ipa_kpasswd()

    removed_sysconfig_file = paths.SYSCONFIG_HTTPD
    if fstore.has_file(removed_sysconfig_file):
        root_logger.info('Restoring %s as it is no longer required',
            removed_sysconfig_file)
        fstore.restore_file(removed_sysconfig_file)

    http = httpinstance.HTTPInstance(fstore)
    http.remove_httpd_ccache()
    http.configure_selinux_for_httpd()
    http.change_mod_nss_port_from_http()

    http.stop()
    fix_trust_flags()
    http.start()

    ds = dsinstance.DsInstance()
    ds.configure_dirsrv_ccache()

    ds.stop(ds_serverid)
    fix_schema_file_syntax()
    remove_ds_ra_cert(subject_base)
    ds.start(ds_serverid)

    uninstall_selfsign(ds, http)

    simple_service_list = (
        (memcacheinstance.MemcacheInstance(), 'MEMCACHE'),
        (otpdinstance.OtpdInstance(), 'OTPD'),
    )

    for service, ldap_name in simple_service_list:
        service.ldapi = True
        try:
            if not service.is_configured():
                # 389-ds needs to be running to create the memcache instance
                # because we record the new service in cn=masters.
                ds.start()
                service.create_instance(ldap_name, fqdn, None,
                                        ipautil.realm_to_suffix(api.env.realm),
                                        realm=api.env.realm)
        except ipalib.errors.DuplicateEntry:
            pass

    # install DNSKeySync service only if DNS is configured on server
    if bindinstance.named_conf_exists():
            dnskeysyncd = dnskeysyncinstance.DNSKeySyncInstance(fstore, ldapi=True)
            if not dnskeysyncd.is_configured():
                ds.start()
                dnskeysyncd.create_instance(fqdn, api.env.realm)
                dnskeysyncd.start_dnskeysyncd()

    cleanup_kdc(fstore)
    cleanup_adtrust(fstore)
    setup_firefox_extension(fstore)
    add_ca_dns_records()

    # Any of the following functions returns True iff the named.conf file
    # has been altered
    named_conf_changes = (
                          named_remove_deprecated_options(),
                          named_set_minimum_connections(),
                          named_enable_serial_autoincrement(),
                          named_update_gssapi_configuration(),
                          named_update_pid_file(),
                          named_enable_dnssec(),
                          named_validate_dnssec(),
                          named_bindkey_file_option(),
                          named_managed_keys_dir_option(),
                          named_root_key_include(),
                          mask_named_regular(),
                         )

    if any(named_conf_changes):
        # configuration has changed, restart the name server
        root_logger.info('Changes to named.conf have been made, restart named')
        bind = bindinstance.BindInstance(fstore)
        try:
            bind.restart()
        except ipautil.CalledProcessError, e:
            root_logger.error("Failed to restart %s: %s", bind.service_name, e)

    ca_restart = any([
        ca_restart,
        upgrade_ipa_profile(ca, api.env.domain, fqdn),
        certificate_renewal_update(ca),
        ca_enable_pkix(ca),
    ])

    if ca_restart:
        root_logger.info('pki-ca configuration changed, restart pki-ca')
        try:
            ca.restart(dogtag.configured_constants().PKI_INSTANCE_NAME)
        except ipautil.CalledProcessError, e:
            root_logger.error("Failed to restart %s: %s", ca.service_name, e)

    set_sssd_domain_option('ipa_server_mode', 'True')

if __name__ == '__main__':
    installutils.run_script(main, operation_name='ipa-upgradeconfig')
