URL: https://github.com/freeipa/freeipa/pull/2053
Author: rcritten
 Title: #2053: Add ipa-advise plugin to set the CRL master
Action: opened

PR body:
"""
This might still be a bit rough around the edges but is ready for a review.

Note that for sudo to work you need to allow whatever use you are 
authenticating as to be able to run systemctl, grep and sed (not a very nice 
combination at all). I could easily drop this and require root only, I was 
trying to be nice.

I ran the script output through a bash linter and it is generally ok.
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/2053/head:pr2053
git checkout pr2053
From c680c16f89d5ede61aab8f844c0fc8850faeed41 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Wed, 20 Jun 2018 18:14:52 -0400
Subject: [PATCH 1/2] Allow an advice script to override the shell used

Relying in /bin/sh is more generic but some commands may require
more advanced scripting found in other shells such as bash.

Related: https://fedorahosted.org/freeipa/ticket/5803

Signed-off-by: Rob Crittenden <rcrit...@redhat.com>
---
 ipaserver/advise/base.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/ipaserver/advise/base.py b/ipaserver/advise/base.py
index 40f2e65dc8..7c63250a00 100644
--- a/ipaserver/advise/base.py
+++ b/ipaserver/advise/base.py
@@ -408,6 +408,7 @@ class IpaAdvise(admintool.AdminTool):
     description = "Provides configuration advice for various use cases. To "\
                   "see the list of possible ADVICEs, run ipa-advise without "\
                   "any arguments."
+    shell = '/bin/sh'
 
     def __init__(self, options, args):
         super(IpaAdvise, self).__init__(options, args)
@@ -448,13 +449,13 @@ def print_config_list(self):
             for line in wrapped_description[1:]:
                 print("{off}{line}".format(off=' ' * len(prefix), line=line))
 
-    def print_header(self, header, print_shell=False):
+    def print_header(self, header, shell=None, print_shell=False):
         header_size = len(header)
 
         prefix = ''
         if print_shell:
             prefix = '# '
-            print('#!/bin/sh')
+            print('#!{}'.format(shell))
 
         # Do not print out empty header
         if header_size > 0:
@@ -482,7 +483,7 @@ def print_advice(self, keyword):
                 .format(adv=keyword.replace('_', '-')), 1)
 
         # Print out nicely formatted header
-        self.print_header(advice.description, print_shell=True)
+        self.print_header(advice.description, advice.shell, print_shell=True)
 
         # Set options so that plugin can use verbose/quiet options
         advice.set_options(self.options)

From 30d6aa1ee9d7b03878567035828f473686191c37 Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Wed, 20 Jun 2018 17:26:56 -0400
Subject: [PATCH 2/2] Add advice script for setting a CRL master

https://www.freeipa.org/page/V4/Promotion_to_CRL_generation_master

https://fedorahosted.org/freeipa/ticket/5803

Signed-off-by: Rob Crittenden <rcrit...@redhat.com>
---
 ipaserver/advise/plugins/crl_master.py | 348 +++++++++++++++++++++++++++++++++
 1 file changed, 348 insertions(+)
 create mode 100644 ipaserver/advise/plugins/crl_master.py

diff --git a/ipaserver/advise/plugins/crl_master.py b/ipaserver/advise/plugins/crl_master.py
new file mode 100644
index 0000000000..57fc5806cb
--- /dev/null
+++ b/ipaserver/advise/plugins/crl_master.py
@@ -0,0 +1,348 @@
+#
+# Copyright (C) 2018 FreeIPA Contributors see COPYING for license
+#
+
+from __future__ import absolute_import
+
+from ipalib import api
+from ipalib.plugable import Registry
+from ipaplatform import services
+from ipaserver.advise.base import Advice
+
+register = Registry()
+
+
+@register()
+class set_crl_master(Advice):
+    """
+    Set the CRL master among a list of masters
+    """
+
+    description = ("Instructions for configuring an IPA master to generate "
+                   "the Certificate Revocation List (CRL). "
+                   "Only one master may have this responsibilty at a time.")
+
+    shell = '/bin/bash'
+
+    pki_service_name = services.knownservices.pki_tomcatd.systemd_name
+
+    def get_info(self):
+        self.set_local_fn()
+        self.get_local_fn()
+        self.get_remote_fn()
+        self.log.exit_on_nonroot_euid()
+        self.parse_arguments()
+        self.check_ccache_not_empty()
+        self.get_principal()
+        self.initialize_local_variables()
+        self.check_hostname_is_in_masters()
+        self.find_ca_masters()
+        self.run_validate()
+        self.check_target_is_in_ca_masters()
+        self.get_local_enableCRLUpdates_setting()
+        self.get_remoteenableCRLUpdates_setting()
+        self.check_remote_status()
+        self.log.exit_on_predicate(
+            '[ ! -z "$REMOTE_ENABLED" ] && [ "$REMOTE_ENABLED" == "false" ]'
+            ' && [ "$FORCE" -ne 1 ]',
+            ['"$REMOTE_HOSTNAME" is not the CRL master']
+        )
+        self.set_local_enableCRLUpdates_setting()
+        self.set_remote_enableCRLUpdates_setting()
+        self.check_rollback()
+
+    def initialize_local_variables(self):
+        # SKIP_REMOTE is set if the remote is unavailable and --force
+        self.log.command('SKIP_REMOTE=0')
+        # ROLLBACK indicates a remote failure needs a local rollback
+        self.log.command('ROLLBACK=0')
+        # Don't assume that the $HOSTNAME passed in is sane
+        self.log.command('HOSTNAME=$(hostname -f)')
+        self.log.command('\n')
+        self.log.command(
+            'if [ $SUDO_ENABLED -eq 1 ]; then\n'
+            '    SUDO="sudo"\n'
+            '    SSH_CMD="/usr/bin/ssh -t ${PRINCIPAL}@"\n'
+            '    echo "Authenticating as ${PRINCIPAL}"\n'
+            'else\n'
+            '    SUDO=""\n'
+            '    SSH_CMD="/usr/bin/ssh root@"\n'
+            '    echo "Authenticating as root"\n'
+            'fi\n'
+        )
+        self.log.command('\n')
+
+    def parse_arguments(self):
+        self.log.exit_on_failed_command(
+            'OPTS=$(getopt -o "" --long force,sudo,validate -- "$@")\n'
+            'if [ $? != 0 ] ; then exit 1 ; fi\n'
+            'eval set -- "$OPTS"\n'
+            'FORCE=0\n'
+            'SUDO_ENABLED=0\n'
+            'VALIDATE=0\n'
+
+            '# Remote hostname is the remote master we are changing\n'
+            'while true; do\n'
+            '  case "$1" in\n'
+            '    --force ) FORCE=1; shift ;;\n'
+            '    --sudo )    SUDO_ENABLED=1; shift ;;\n'
+            '    --validate ) VALIDATE=1; shift ;;\n'
+            '    -- ) shift; break ;;\n'
+            '    * ) break ;;\n'
+            '  esac\n'
+            'done\n'
+            'REMOTE_HOSTNAME=$1',
+            ['Usage: $0 [--force] [--sudo] [--validate] IPA_master']
+        )
+        self.log.exit_on_predicate(
+            '[ -z "$REMOTE_HOSTNAME" ]',
+            ['Usage: $0 [--force] [--sudo] IPA_master',
+             '       $0 [--validate]'])
+
+    def check_ccache_not_empty(self):
+        self.log.comment('Check whether the credential cache is not empty')
+        self.log.exit_on_failed_command(
+            'klist > /dev/null 2>&1',
+            [
+                "Credential cache is empty",
+                'Use kinit as privileged user to obtain Kerberos credentials'
+            ])
+
+    def get_principal(self):
+        self.log.comment('Get the current prinicipal')
+        self.log.command(
+            'PRINCIPAL=$(klist | grep "Default principal:" | '
+            'awk "{ print \$3 }" | sed "s/@%s//")' % api.env.realm
+        )
+
+    def check_hostname_is_in_masters(self):
+        self.log.comment('Check whether the host is IPA master')
+        self.log.exit_on_failed_command(
+            'ipa server-find "$(hostname -f)" > /dev/null 2>&1',
+            ["This script can be run on IPA master only"])
+
+    def find_ca_masters(self):
+        self.log.comment('Find CA masters')
+        self.log.command(
+            'output=$(ipa server-find --servroles="CA server" 2>&1 '
+            '| grep name: | awk "{ print \$3 }")'
+        )
+        self.log.exit_on_predicate(
+            '[ "$?" -ne "0" ]',
+            ['Failed to find CA servers'])
+        self.log.command('servers=$output')
+
+    def check_target_is_in_ca_masters(self):
+        self.log.comment('Ensure provided hostname is an IPA CA master')
+        self.log.command(
+            'FOUND=0\n'
+            'for server in $servers; do\n'
+            '    if [ "$REMOTE_HOSTNAME" == "$server" ]; then FOUND=1; fi\n'
+            'done\n\n'
+        )
+        self.log.exit_on_predicate(
+            '[ "$FOUND" == "0" ]',
+            ['Failed to find $REMOTE_HOSTNAME in list of CA masters'])
+
+    def set_local_fn(self):
+        """Function for setting local value"""
+        self.log.command(
+            '# Arg order: oldvalue, newvalue\n'
+            'set_local()\n'
+            '{\n'
+            '    sed -i "s/'
+            '    ca.crl.MasterCRL.enableCRLUpdates=$1/'
+            '    ca.crl.MasterCRL.enableCRLUpdates=$2/" '
+            '    /etc/pki/pki-tomcat/ca/CS.cfg\n'
+        )
+        self.log.command(
+            '    if [ "$?" -ne "0" ]; then\n'
+            '        echo "Failed to change local CRL master" >&2\n'
+            '        echo "Restart the CA manually" >&2\n'
+            '        exit 1\n'
+            '    fi\n'
+        )
+        self.log.command('}\n')
+
+    def get_local_fn(self):
+        self.log.comment('Get local enableCRLUpdates setting')
+        self.log.command(
+            'get_local()\n'
+            '{\n'
+            '    LOCAL_ENABLED=$(grep ca.crl.MasterCRL.enableCRLUpdates '
+            '    /etc/pki/pki-tomcat/ca/CS.cfg |'
+            '    awk -F= \' { print $2 }\')\n',
+        )
+        self.log.command(
+            '    echo Current setting on "${HOSTNAME}": '
+            '    "${LOCAL_ENABLED}" >&2')
+        self.log.command('}\n')
+
+    def get_remote_fn(self):
+        self.log.comment('Retrieve remote enableCRLUpdates setting')
+        self.log.command(
+            '# Arg order: $REMOTE_HOST\n'
+            'get_remote()\n'
+            '{\n'
+            '    # We need to capture the output so fake a prompt here\n'
+            '    if [ ! -z "${SUDO}" ]; then\n'
+            '        echo -n "[sudo]: password for $PRINCIPAL: "\n'
+            '    fi\n'
+            '    REMOTE_ENABLED=\"$(${SSH_CMD}$1 ${SUDO} grep '
+            '    ca.crl.MasterCRL.enableCRLUpdates '
+            '    /etc/pki/pki-tomcat/ca/CS.cfg 2>/dev/null | '
+            '    awk -F= \'{ print $2 }\')\"\n'
+            '    echo \n'
+        )
+        self.log.command(
+            '    echo Current setting on $1: ${REMOTE_ENABLED} >&2'
+        )
+        self.log.command('}\n')
+
+    def run_validate(self):
+        self.log.comment('See if we are doing validation only')
+        self.log.command(
+            'if [ "$VALIDATE" == "1" ]; then\n'
+            '    get_local\n'
+            '    for server in $servers; do\n'
+            '    if [ "$HOSTNAME" != "$server" ]; then\n'
+            '        get_remote $server;\n'
+            '    fi\n'
+            '    done\n'
+            '    exit 0\n'
+            'fi')
+
+    def get_local_enableCRLUpdates_setting(self):
+        self.log.comment('Get local enableCRLUpdates setting')
+        self.log.exit_on_failed_command(
+            'LOCAL_ENABLED=$(grep ca.crl.MasterCRL.enableCRLUpdates '
+            '/etc/pki/pki-tomcat/ca/CS.cfg |'
+            'awk -F= \' { print $2 }\')\n',
+            ['Failed to get ca.crl.MasterCRL.enableCRLUpdates']
+        )
+        self.log.command(
+            'echo Current setting on "${HOSTNAME}": "${LOCAL_ENABLED}" >&2')
+        self.log.exit_on_predicate(
+            '[ "${LOCAL_ENABLED}" == "true" -a "${FORCE}" -ne 1 ]',
+            ['$HOSTNAME is already the CRL master']
+        )
+        self.log.command('echo "Force is enabled, continuing"')
+
+    def get_remoteenableCRLUpdates_setting(self):
+        self.log.comment('Get remote enableCRLUpdates setting')
+        self.log.command('get_remote $REMOTE_HOSTNAME')
+
+    def check_remote_status(self):
+        self.log.comment('See if the remote responded')
+        self.log.command(
+            'if [ -z "$REMOTE_ENABLED" ]; then\n'
+            '    echo "Unable to contact \"$REMOTE_HOSTNAME\"" >&2\n'
+            '    if [ "$FORCE" -eq 1 ]; then\n'
+            '        echo "Continuing anyway" >&2\n'
+            '        SKIP_REMOTE=1\n'
+            '    else\n'
+            '        exit 1\n'
+            '    fi\n'
+            'fi\n'
+        )
+
+    def set_local_enableCRLUpdates_setting(self):
+        self.log.comment('Set the local server to true')
+        self.stop_dogtag()
+        self.log.command('echo Updating value on "$HOSTNAME" to true >&2')
+        self.log.command('set_local "false" "true"')
+        self.start_dogtag()
+
+    def set_remote_enableCRLUpdates_setting(self):
+        self.log.comment('Set the remote server to false')
+        self.log.command(
+            'if [ "${SKIP_REMOTE}" -eq 0 ]; then\n'
+        )
+        self.stop_dogtag(remote=True, eol=' \\')
+
+        self.log.command('echo Updating value on "$1" to false >&2')
+        self.log.command(
+            '${SSH_CMD}${REMOTE_HOSTNAME} ${SUDO} sed -i "s/'
+            'ca.crl.MasterCRL.enableCRLUpdates=true/'
+            'ca.crl.MasterCRL.enableCRLUpdates=false/" '
+            '/etc/pki/pki-tomcat/ca/CS.cfg 2>/dev/null'
+        )
+        self.log.command(
+            'if [ "$?" -ne "0" ]; then\n'
+            '    echo "Failed to change remote CRL master" >&2\n'
+            '    ROLLBACK=1\n'
+            'fi\n'
+        )
+        self.start_dogtag(remote=True, eol=' \\')
+        self.log.command('fi')
+        self.log.comment('# end of SKIP_REMOTE')
+
+    def sleep(self, timeout):
+        self.log.command('sleep {}'.format(timeout))
+
+    def stop_dogtag(self, remote=False, eol=''):
+        if remote:
+            self.log.command('echo Stopping dogtag on '
+                             '"${REMOTE_HOSTNAME}" >&2')
+            self.log.command(
+                '${SSH_CMD}${REMOTE_HOSTNAME} ${SUDO} '
+                'systemctl stop %s 2>/dev/null\n' % self.pki_service_name
+            )
+        else:
+            self.log.command('echo Stopping dogtag on "${HOSTNAME}" >&2')
+            self.log.command(
+                'systemctl stop {}\n'.format(self.pki_service_name)
+            )
+        self.log.command(
+            'if [ "$?" -ne "0" ]; {eol}\n'
+            'then {eol}\n'
+            '    echo "Failed to stop dogtag." >&2; {eol}\n'
+            'exit 1; {eol}\n'
+            'fi\n'.format(eol=eol)
+        )
+        self.sleep(3)
+
+    def start_dogtag(self, remote=False, eol='', amp=''):
+        if remote:
+            self.log.command(
+                'if [ ! -z "${SUDO}" ]; then\n'
+                '    echo -n "Password: "\n'
+                'fi'
+            )
+            self.log.command('echo Starting dogtag on '
+                             '"${REMOTE_HOSTNAME}" >&2')
+            self.log.command(
+                '${SSH_CMD}${REMOTE_HOSTNAME} ${SUDO} '
+                'systemctl stop %s 2>/dev/null\n' % self.pki_service_name
+            )
+        else:
+            self.log.command('echo Starting dogtag on "${HOSTNAME}" >&2')
+            self.log.command(
+                'systemctl start {} {}'.format(self.pki_service_name, amp)
+            )
+        self.log.command(
+            'if [ "$?" -ne "0" ]; {eol}\n'
+            'then {eol}\n'
+            '    echo "Failed to start dogtag." >&2; {eol}\n'.format(eol=eol)
+        )
+        if remote:
+            self.log.command(
+                '    ROLLBACK=1; {eol}\n'
+                'fi {eol}\n'.format(eol=eol)
+            )
+        else:
+            self.log.command(
+                '    exit 1; {eol}\n'
+                'fi {eol}\n'.format(eol=eol)
+            )
+
+        # TODO: actually wait until dogtag is responsive
+        self.sleep(10)
+
+    def check_rollback(self):
+        self.log.comment('Rollback to previous state if necessary')
+        self.log.command('if [ "$ROLLBACK" == "0" ] ; then exit 0; fi')
+        self.log.command('echo Rolling back local value >&2')
+        self.stop_dogtag()
+        self.log.command('set_local "true" "$LOCAL_ENABLED"')
+        self.start_dogtag()
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org
Fedora Code of Conduct: https://getfedora.org/code-of-conduct.html
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedoraproject.org/archives/list/freeipa-devel@lists.fedorahosted.org/message/XUU2I4WAALB2DGKP3JH2PHFS5K5B2PDM/

Reply via email to