From eee0fb7acf18b500ea35cd85838839aabf108c9f Mon Sep 17 00:00:00 2001 From: gerbenvandervries Date: Thu, 23 Mar 2023 15:31:39 +0100 Subject: [PATCH 1/2] work in progress, role the deploy LDAP2LISTSERV script and config, and crons --- roles/listserv/tasks/main.yml | 47 ++ roles/listserv/templates/LDAP2LISTSERV.bash | 832 ++++++++++++++++++++ roles/listserv/templates/LDAP2LISTSERV.cfg | 74 ++ single_role_playbooks/listserv.yml | 5 + 4 files changed, 958 insertions(+) create mode 100644 roles/listserv/templates/LDAP2LISTSERV.bash create mode 100644 roles/listserv/templates/LDAP2LISTSERV.cfg create mode 100644 single_role_playbooks/listserv.yml diff --git a/roles/listserv/tasks/main.yml b/roles/listserv/tasks/main.yml index c81cf5b7c..572c00240 100644 --- a/roles/listserv/tasks/main.yml +++ b/roles/listserv/tasks/main.yml @@ -1,3 +1,50 @@ --- +- name: Create directories for mailinglists script. + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0750' + owner: 'root' + group: 'root' + with_items: + - '/root/mailinglists' + become: true +- name: 'Install listserv script.' + ansible.builtin.copy: + src: "templates/{{ item }}" + dest: "/root/mailinglists/{{ item }}" + owner: 'root' + group: 'root' + mode: '0750' + with_items: + - 'LDAP2LISTSERV.bash' + - 'LDAP2LISTSERV.cfg' + become: true + +- name: 'Create cron job to fetch data from an LDAP and manage mailing list subscriptions of our users.' + ansible.builtin.cron: + name: Set cron job to fetch data from an LDAP and manage mailing list subscriptions. + weekday: '*' + hour: '12,15,18' + minute: '15' + user: 'root' + job: | + /bin/bash -c '/root/mailinglists/LDAP2LISTSERV.bash -l ERROR -u -n 2>&1 | /bin/logger' + cron_file: 'ldap-2-mailinglists' + disabled: True + become: true + +- name: 'cron job to backup mailing list ones a day.' + ansible.builtin.cron: + name: set cron job to backup mailing list. + weekday: '*' + hour: '09' + minute: '15' + user: 'root' + job: | + /bin/bash -c '/root/mailinglists/LDAP2LISTSERV.bash -l ERROR -u -n -b /root/mailinglists/listserv_backups 2>&1 | /bin/logger + cron_file: 'ldap-2-mailinglists' + disabled: True + become: true ... diff --git a/roles/listserv/templates/LDAP2LISTSERV.bash b/roles/listserv/templates/LDAP2LISTSERV.bash new file mode 100644 index 000000000..e1ed87539 --- /dev/null +++ b/roles/listserv/templates/LDAP2LISTSERV.bash @@ -0,0 +1,832 @@ +#!/bin/bash + +# +# Code Conventions: +# Indentation: TABs only +# Functions: camelCase +# Global Variables: lower_case_with_underscores +# Local Variables: _lower_case_with_underscores_and_prefixed_with_underscore +# Environment Variables: UPPER_CASE_WITH_UNDERSCORES +# + +# +## +### Environment and Bash sanity. +## +# +if [[ "${BASH_VERSINFO}" -lt 4 || "${BASH_VERSINFO[0]}" -lt 4 ]]; then + echo "Sorry, you need at least bash 4.x to use ${0}." >&2 + exit 1 +fi + +set -e # Exit if any subcommand or pipeline returns a non-zero exit status. +set -u # Raise exception if variable is unbound. Combined with set -e will halt execution when an unbound variable is encountered. +#set -o pipefail # Fail when any command in series of piped commands failed as opposed to only when the last command failed. + +umask 0077 + +export TMPDIR="${TMPDIR:-/tmp}" # Default to /tmp if $TMPDIR was not defined. +SCRIPT_NAME="$(basename ${0})" +SCRIPT_NAME="${SCRIPT_NAME%.*sh}" +INSTALLATION_DIR="$(cd -P "$(dirname "${0}")/.." && pwd)" +LIB_DIR="${INSTALLATION_DIR}/lib" +CFG_DIR="${INSTALLATION_DIR}/etc" +HOSTNAME_SHORT="$(hostname -s)" + +# +# Make sure dots are used as decimal separator. +# +LANG='en_US.UTF-8' +LC_NUMERIC="${LANG}" + +# +## +### Functions. +## +# +function showHelp() { + # + # Display commandline help on STDOUT. + # + cat <" "${SCRIPT_NAME}" "${_log_timestamp}" "${_log_level}" "${_problematic_line}" "${_problematic_function}") + local _log_line="${_log_line_prefix} ${_log_message}" + if [ ! -z "${mixed_stdouterr:-}" ]; then + _log_line="${_log_line} STD[OUT+ERR]: ${mixed_stdouterr}" + fi + if [ ${_status} -ne 0 ]; then + _log_line="${_log_line} (Exit status = ${_status})" + fi + + # + # Log to STDOUT (low prio <= 'WARN') or STDERR (high prio >= 'ERROR'). + # + if [[ ${_log_level_prio} -ge ${l4b_log_levels['ERROR']} || ${_status} -ne 0 ]]; then + printf '%s\n' "${_log_line}" > /dev/stderr + else + printf '%s\n' "${_log_line}" + fi + fi + + # + # Exit if this was a FATAL error. + # + if [ ${_log_level_prio} -ge ${l4b_log_levels['FATAL']} ]; then + # + # Reset trap and exit. + # + trap - EXIT + if [ ${_status} -ne 0 ]; then + exit ${_status} + else + exit 1 + fi + fi +} + + +function getSubscriptions () { + # + local _entitlement="${1}" + local _login_file="${TMPDIR}/${SCRIPT_NAME}/login_result.html" + local _cookie_file="${TMPDIR}/${SCRIPT_NAME}/cookiejar.txt" + local _subscribtions_file="${TMPDIR}/${SCRIPT_NAME}/${_entitlement}-subscriptions.list" + local _mailinglist="${entitlement_settings[${_entitlement}',mailing_list']}" + # + # Login to get cookie, which we store in our cookiejar. + # * LOGIN1 and X are required empty arguments. + # * Order of arguments is essential and not random. + # + mixed_stdouterr=$(curl --silent --show-error \ + --cookie-jar "${_cookie_file}" \ + --data-urlencode 'LOGIN1=' \ + --data-urlencode "Y=${listserv_admin_user}" \ + --data-urlencode "p=${listserv_admin_pass}" \ + --data-urlencode "e=Log+In" \ + --data-urlencode 'X=' \ + --output "${_login_file}" \ + "${listserv_api_webinterface_url}" 2>&1) \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to login as ${listserv_admin_user} on ${listserv_api_webinterface_url}." + + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Login as ${listserv_admin_user} on ${listserv_api_webinterface_url} succeeded. Cookies were stored in ${_cookie_file}." + # + # Use cookie from our jar to retrieve a subscribers list, + # which is pipe (|) separated. + # + mixed_stdouterr=$(curl --silent --show-error \ + --cookie "${_cookie_file}" \ + --data-urlencode "REPORT=${_mailinglist}" \ + --data-urlencode "Y=${listserv_admin_user}" \ + --data-urlencode '_charset_=UTF-8' \ + --data-urlencode 'z=2' \ + --data-urlencode 'CSV=|ALL' \ + --output "${_subscribtions_file}" \ + "${listserv_api_webinterface_url}" 2>&1) \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to download list of ${_mailinglist} subscribers from ${listserv_api_webinterface_url} to ${_subscribtions_file}." + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Downloaded list of ${_mailinglist} subscribers from ${listserv_api_webinterface_url} to ${_subscribtions_file}." + # + # Patch formatting issues. + # + echo >> ${_subscribtions_file} # Append missing line end character on last line. + perl -pi -e 's/"//g' "${_subscribtions_file}" # Remove all double quotes. + perl -pi -e 's/ \| /|/g' "${_subscribtions_file}" # Remove spaces that surround the value separator character (|). + perl -pi -e 's/\|$//' "${_subscribtions_file}" # Remove the bogus value separator character (|) at the end of each line. + # + # Parse subscriptions file. + # + local _regex='\(([^()]{1,})\)' + while IFS='|' read -r -a _subscriber_record_values; do + # + # * Skip blank lines. + # * Skip the header line: Email|Name + # + local _email="${_subscriber_record_values[0]:-}" + local _full_name="${_subscriber_record_values[1]:-}" + if [[ "${_email:-}" != '' ]] && [[ "${_full_name:-}" != '' ]] && [[ "${_email:-}" != 'Email' ]]; then + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Subscriber record contains email ${_email} and full name ${_full_name}." + if [[ "${_full_name}" =~ ${_regex} ]]; then + local _account_name="${BASH_REMATCH[1],,}" # Convert key on-the-fly to lowercase just in case. + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Found account name ${_account_name} and adding subscribed user to subscriptions hash." + subscriptions["${_account_name}"]="${_email}|${_full_name}" + else + log4Bash 'ERROR' "${LINENO}" "${FUNCNAME:-main}" '0' "No account name found at the end of full name ${_full_name}. Account name must be listed between round brackets at the end of the full name." + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' "Deleting ${_email} from the ${_mailinglist} mailing list, because full name '${_full_name}' is malformed and lacks account name." + sendListservCommand "${_mailinglist}" "${notify_users}" 'DELETE' "${_email}" + fi + fi + done < "${_subscribtions_file}" + # + # Optional: Make backup of mailinglist subscriptions. + # + if [[ ! -z "${backup_dir:-}" ]]; then + # + # Create directory and timestamp for backups. + # + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Making backup of ${_mailinglist} mailing list subscribers..." + mixed_stdouterr=$(mkdir -m 0700 -p "${backup_dir}" 2>&1) \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to create backup dir ${backup_dir}." + if [[ -d ${backup_dir} && -w ${backup_dir} ]]; then + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Backup dir ${backup_dir} is Ok." + else + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '0' "Backup dir ${backup_dir} cannot be used. Check path and permissions." + fi + local _backup_ts=`date "+%Y-%m-%d-T%H%M"` + local _backup_file="${backup_dir}/${_entitlement}-subscriptions-${_backup_ts}.list" + # + # We already have the list of subscribers as a temp file: mv this file to the backup dir. + # + mixed_stdouterr=$(mv "${_subscribtions_file}" "${_backup_file}" 2>&1) \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to create backup ${_backup_file}." + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' "Backup was saved to ${_backup_file}." + fi +} + +# +# Manage mailing list subscriptions. +# +function manageSubscriptions () { + # + local _entitlement="${1}" + local _mailinglist="${entitlement_settings[${_entitlement}',mailing_list']}" + local _ldif_file="${TMPDIR}/${SCRIPT_NAME}/${_entitlement}.ldif" + local _ldap_attr_regex='([^: ]{1,})(:{1,2}) ([^:]{1,})' + local _timestamp_regex='^([0-9]){4}([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})' # YYYYMMDDhhmm ignores any seocnds and timezomes at the end. + local _sn_regex='([^,]{1,}),[[:blank:]]*([^,]{1,})[[:blank:]]*' + local _subscription_regex='([^|]{1,})[|]([^|]{1,})' + # + # Query LDAP. + # + declare -A _accounts=() + mixed_stdouterr=$(ldapsearch -LLL -o ldif-wrap=no \ + -D "${entitlement_settings[${_entitlement}',ldap_user']}" \ + -w "${entitlement_settings[${_entitlement}',ldap_pass']}" \ + -b "${entitlement_settings[${_entitlement}',ldap_search_base']}" \ + "(ObjectClass=person)" ${ldap_fields} 2>&1 > "${_ldif_file}") \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "ldapsearch failed." + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "ldapsearch results were saved to ${_ldif_file}." + # + # Append the NULL character to the LDIF file, so we can detect that as EOF instead of a newline. + # + printf '\0' >> "${_ldif_file}" + # + # Substitute the blank line record separator with a # character and read records into an array. + # + IFS='#' read -r -d '' -a _ldif_records < <(sed 's/^$/#/' "${_ldif_file}") || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Parsing LDIF file (${_ldif_file}) into records failed." + # + # Loop over records in the array and create a faked-multi-dimensional hash. + # + local _ldif_record + for _ldif_record in "${_ldif_records[@]}"; do + # + # Remove trailing white space like the new line character. + # And skip blank lines. + # + _ldif_record="${_ldif_record%%[[:space:]]}" + [[ "${_ldif_record}" == '' ]] && continue + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "LDIF record contains: ${_ldif_record}" + # + # Parse record's key:value pairs. + # + local -A _directory_record_attributes=() + local _ldif_line + while IFS=$'\n' read -r _ldif_line; do + [[ "${_ldif_line}" == '' ]] && continue # Skip blank lines. + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' "LDIF key:value pair contains: ${_ldif_line}." + if [[ ${_ldif_line} =~ ${_ldap_attr_regex} ]]; then + local _key="${BASH_REMATCH[1],,}" # Convert key on-the-fly to lowercase. + local _sep="${BASH_REMATCH[2]}" + local _value="${BASH_REMATCH[3]}" + # + # Check if value was base64 encoded (double colon as separator) + # or plain text (single colon as separator) and decode if necessary. + # + if [[ "${_sep}" == '::' ]]; then + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' " decoding base64 encoded value..." + _value="$(printf '%s' "${_value}" | base64 -di)" + fi + # + # This may be a multi-valued attribute and therefore check if key already exists; + # When key already exists make sure we append instead of overwriting the existing value(s)! + # + if [[ ! -z "${_directory_record_attributes[${_key}]+isset}" ]]; then + _directory_record_attributes["${_key}"]="${_directory_record_attributes[${_key}]} ${_value}" + else + _directory_record_attributes["${_key}"]="${_value}" + fi + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' " key contains: ${_key}." + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' " value contains: ${_value}." + else + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "Failed to parse LDIF key:value pair (${_ldif_line})." + fi + done < <(printf '%s\n' "${_ldif_record}") || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Parsing LDIF record failed." + # + # Use processed LDIF record to manage subscription to mailing list. + # (required fields are: cn givenName sn mail loginExpirationTime loginDisabled sshPublicKey) + # + local _account_name + local _given_name='' + local _sur_name='' + local _family_name + local _middle_name + local _full_name + local _email + # + # Get account/login name (required). + # + if [[ ! -z "${_directory_record_attributes['dn']+isset}" ]]; then + # + # Parse account name (cn) from dn. + # + _account_name=$(dn2cnWithEntitlementPrefix "${_directory_record_attributes['dn']}") + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Processing _account_name: ${_account_name}." + _accounts["${_account_name}"]='found' + else + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "dn attribute (used as account name) missing for this ldif record." + fi + # + # Skip (functional) accounts [optional]. + # There is no LDAP label/attribute to detect if an account is a functional or regular one for a "real" user, + # therefore we currently rely on regular expressions to detect functional accounts based on naming schemes. + # + local _no_subscription_account_name_pattern + for _no_subscription_account_name_pattern in "${no_subscription_account_name_patterns[@]:-no_patterns_specified}"; do + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' "Checking if account: ${_account_name} matches pattern ${_no_subscription_account_name_pattern}." + if [[ "${_account_name}" =~ ${_no_subscription_account_name_pattern} ]]; then + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Skipping account: ${_account_name} matching pattern ${_no_subscription_account_name_pattern}." + continue 2 + else + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' "Account: ${_account_name} does not match pattern ${_no_subscription_account_name_pattern}." + fi + done + # + # Get user's name attributes and compile full "real" name (optional). + # + if [[ ! -z "${_directory_record_attributes['givenname']+isset}" ]]; then + _given_name="${_directory_record_attributes['givenname']}" + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Found givenName: ${_given_name}." + _full_name="${_given_name}" + else + log4Bash 'WARN' "${LINENO}" "${FUNCNAME:-main}" '0' "givenName attribute missing in ldif record for ${_account_name}." + fi + if [[ ! -z "${_directory_record_attributes['sn']+isset}" ]]; then + _sur_name="${_directory_record_attributes['sn']}" + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Found sn: ${_sur_name}." + # + # Check for comma separated middle name suffixed in the sn field due to lack of proper middle name field. + # + if [[ "${_sur_name}" =~ ${_sn_regex} ]]; then + _family_name="${BASH_REMATCH[1]}" + _middle_name="${BASH_REMATCH[2]}" + # + # Append family name (and middle) when present. + # + if [[ ! -z "${_full_name:-}" ]]; then + _full_name="${_full_name} ${_middle_name} ${_family_name}" + else + _full_name="${_middle_name} ${_family_name}" + fi + else + _family_name="${_sur_name}" + if [[ ! -z "${_full_name:-}" ]]; then + _full_name="${_full_name} ${_family_name}" + else + _full_name="${_family_name}" + fi + fi + else + log4Bash 'WARN' "${LINENO}" "${FUNCNAME:-main}" '0' "sn attribute missing in ldif record for ${_account_name}." + fi + # + # By default we assume all users must be subscribed. + # When any of the required attributes is missing or has a 'wrong' value (e.g. account expired), + # we switch _account_must_be_subscribed to 'no'. + # + local _account_must_be_subscribed='yes' + if [[ -z "${_directory_record_attributes['mail']+isset}" ]]; then + _account_must_be_subscribed='no' + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "mail attribute is missing/empty in ldif record for ${_account_name}." + else + _email="${_directory_record_attributes['mail']}" + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Found mail: ${_email}." + fi + if [[ -z "${_directory_record_attributes['sshpublickey']+isset}" ]]; then + _account_must_be_subscribed='no' + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "sshPublicKey attribute is empty in ldif record for ${_account_name}." + fi + if [[ ! -z "${_directory_record_attributes['logindisabled']+isset}" && "${_directory_record_attributes['logindisabled']}" == 'TRUE' ]]; then + _account_must_be_subscribed='no' + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "loginDisabled attribute is TRUE in ldif record for ${_account_name}." + fi + if [[ ! -z "${_directory_record_attributes['loginExpirationTime']+isset}" ]]; then + local _expiration_date="${_directory_record_attributes['loginExpirationTime']+isset}" + # + # Convert both loginExpirationTime as well as current date into seconds since the POSIX time Epoch (January 1st 1970). + # loginExpirationTime format example: "20170101165600Z" + # required format for conversion with date command: "2017-01-01 16:56" + # + local _expiration_date_in_seconds_since_epoch + if [[ "${_expiration_date}" =~ ${_timestamp_regex} ]]; then + local _YYYY="${BASH_REMATCH[1]}" + local _MM="${BASH_REMATCH[2]}" + local _DD="${BASH_REMATCH[3]}" + local _hh="${BASH_REMATCH[4]}" + local _mm="${BASH_REMATCH[5]}" + local _expiration_date_in_seconds_since_epoch=$(date -d "${YYYY}-${MM}-${DD} ${hh}:${mm}" '+%s') \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to convert loginExpirationTime into seconds since Epoch." + local _current_date_in_seconds_since_epoch=$(date '+%s') \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to convert current date into seconds since Epoch." + if [[ "${_expiration_date_in_seconds_since_epoch}" -lt "${current_date_in_seconds_since_epoch}" ]]; then + _account_must_be_subscribed='no' + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Account ${_account_name} expired on ${YYYY}-${MM}-${DD} ${hh}:${mm}." + else + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Account ${_account_name} has not expired." + fi + else + _account_must_be_subscribed='unknown' + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "loginExpirationTime in unsupported format (and failed to convert to seconds since epoch): ${_expiration_date}." + fi + fi + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' "_account_must_be_subscribed=${_account_must_be_subscribed} for this ldif record." + # + # Append account name in round brackets to full name and + # make sure the combination of e-mail address + full name with appended account name + # does not exceed the listserv field size limit for this combination, which is 80 bytes. + # Note that accented characters like é, ü, etc. + # take up twice the size as a regular character without accents, + # so we must count byte length and not plain regular string length. + # The complete combination of e-mail address and full name format contains: + # _email _given_name _middle_name _family_name (_account_name) + # _email <------------------_full_name-----------------------> + # We need the complete _account_name between round brackets, + # so we have to truncate the _given_name _middle_name _family_name combination + # if it exceeds: + # 80 minus 4 (for 2 spaces & round opening and closing brackets) and + # minus the byte length of _account_name and + # minus the byte length of _email + # + if [[ -n "${_full_name:-}" ]]; then + local _max_full_name_length + local _full_name_length + local _account_name_length + local _email_length + _full_name_length="$(printf '%s' "${_full_name}" | wc -c)" + _account_name_length="$(printf '%s' "${_account_name}" | wc -c)" + _email_length="$(printf '%s' "${_email:-}" | wc -c)" + _max_full_name_length=$((80 - 4 - ${_account_name_length} - ${_email_length})) + if [[ "${_full_name_length}" -gt "${_max_full_name_length}" ]]; then + # + # Use truncated name, because it is too long for listserv. + # + _full_name="$(printf '%s' "${_full_name}" | cut -b "1-$((${_max_full_name_length} - 3))")... (${_account_name})" + else + _full_name="${_full_name} (${_account_name})" + fi + else + _full_name="(${_account_name})" + fi + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Compiled full real name: ${_full_name:-}." + # + # subscriptions["${_account_name}"]="${_email}|${_full_name}" + # _full_name="${_given_name} ${_middle_name} ${_family_name} (${_account_name})" + # + if [[ ! -z "${subscriptions["${_account_name}"]+isset}" && "${_account_must_be_subscribed}" == 'no' ]]; then + _accounts["${_account_name}"]='disabled' + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Unsubscribing expired/inactive account ${_account_name} from '${_mailinglist}' mailing list..." + echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'DELETE' "${_email}" + elif [[ -z "${subscriptions["${_account_name}"]+isset}" && "${_account_must_be_subscribed}" == 'yes' ]]; then + _accounts["${_account_name}"]='active' + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Subscribing new account ${_account_name} to '${_mailinglist}' mailing list..." + echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'ADD' "${_email}" +"${_full_name}" + elif [[ ! -z "${subscriptions["${_account_name}"]+isset}" && "${_account_must_be_subscribed}" == 'yes' ]]; then + _accounts["${_account_name}"]='active' + if [[ "${subscriptions["${_account_name}"]}" =~ ${_subscription_regex} ]]; then + local _subscribed_email="${BASH_REMATCH[1]}" + local _subscribed_full_name="${BASH_REMATCH[2]}" + # + # Listserv converts the domain name part of the subscribed email address to UPPERCASE. + # Therefore we convert the complete email address variables to lowercase before comparing them. + # Hence we ignore lowercase vs. UPPERCASE for detecting a changed email address. + # + if [[ "${_subscribed_email,,}|${_subscribed_full_name}" == "${_email,,}|${_full_name}" ]]; then + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Subscription for ${_account_name} is still up to date." + elif [[ "${_subscribed_email,,}" != "${_email,,}" && "${_subscribed_full_name}" == "${_full_name}" ]]; then + # + # Update only the email address for this subscribed user. + # User will receive the a notification based on the CHANGE1 template. + # + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Updating email address for account ${_account_name}..." + echo "sendListservCommand" "${_mailinglist}" "${notify_users}" +'CHANGE' "${_subscribed_email}" "${_email}" + elif [[ "${_subscribed_email,,}" == "${_email,,}" && "${_subscribed_full_name}" != "${_full_name}" ]]; then + # + # Use hardcoded notify_users=0 to do a QUIET ADD, + # which will only update the name of the subscribed user, + # without sending a "welcome new user" notification email. + # + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Updating name for account ${_account_name}..." + echo "sendListservCommand" "${_mailinglist}" '0' 'ADD' "${_email}" +"${_full_name}" + else + # + # Both the email address as well as the full name of the user have changed. + # Mostly likely a (functional) account got recycled + # -> unsubscribe (delete) the old user and subscribe (add) the new user. + # + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Unsubscribing and resubscribing changed account ${_account_name}..." + echo "sendListservCommand" "${_mailinglist}" "${notify_users}" +'DELETE' "${_subscribed_email}" + echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'ADD' +"${_email}" "${_full_name}" + fi + else + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "Failed to split subscriber info using a pipe as separator for ${_account_name}. Contact an admin." + fi + elif [[ -z "${subscriptions["${_account_name}"]+isset}" && "${_account_must_be_subscribed}" == 'no' ]]; then + _accounts["${_account_name}"]='disabled' + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Skipping inactive account ${_account_name} that is not on the mailing list and should not be subscribed..." + else + _accounts["${_account_name}"]='strange' + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "I do not know how to handle ${_account_name}. Contact an admin." + fi + done + # + # Check if all subscribers still have an account for this entitlement + # and hence if they got processed using the code above. + # Unsubscribe any users who no longer can be found in the LDAP. + # + local _subscribed_account + for _subscribed_account in "${!subscriptions[@]}"; do + if [[ ! -z "${_accounts["${_subscribed_account}"]+isset}" ]]; then + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Subscribed account ${_subscribed_account} was found in the LDAP and processed." + else + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Subscribed account ${_subscribed_account} was not found in the LDAP and will be unsubscribed..." + if [[ "${subscriptions["${_subscribed_account}"]}" =~ ${_subscription_regex} ]]; then + local _subscribed_email="${BASH_REMATCH[1]}" + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Unsubscribing missing account ${_subscribed_account}..." + echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'DELETE' +"${_subscribed_email}" + else + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "Failed to split subscriber info using a pipe as separator for ${_subscribed_account}. Contact an admin." + fi + fi + done +} + +function sendListservCommand () { + # + local _mailinglist="${1}" + local _notify_users="${2}" + local _command="${3}" + local _email="${4}" + local _name="${5:-}" # optional; not required for DELETE command. + local _body + # + exit 0 + log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" 0 "Args: _mailinglist=${_mailinglist}|_notify_users=${_notify_users}|_command=${_command}|email=${_email}|_name=${_name}." + # + # Use QUIET in front of command to disable sending notification messages to users. + # The // at the beginning of the line is listserv syntax for a command that spans multiple lines. + # (Putting everything on one line may result in truncated data, when a line exceeds 80 characters.) + # + if [[ "${_notify_users}" -eq '0' ]]; then + _body='// QUIET ' + elif [[ "${_notify_users}" -eq '1' ]]; then + _body='// ' + else + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "I expected 0 or 1 for _notify_users, but got _notify_users=${_notify_users}. Contact an admin." + fi + _body="${_body} ${_command} ${_mailinglist} ${_email} ,\n${_name:-}" + + if [[ "${update_subscriptions}" -eq '1' ]]; then + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" 0 "Sending command (${_body}) by email on behalf of ${listserv_api_email_from} to ${listserv_api_email_to}..." + mixed_stdouterr="$(printf '%b\n' "${_body}" | mail -s '' -r "${listserv_api_email_from}" "${listserv_api_email_to}" 2>&1)" \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to send command (${_body}) by email on behalf of ${listserv_api_email_from} to ${listserv_api_email_to}." + else + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' "DRY RUN: would send command (${_body}) by email on behalf of ${listserv_api_email_from} to ${listserv_api_email_to}..." + fi +} + +# +# Extract a CN from a DN LDAP attribute. +# +function dn2cnWithEntitlementPrefix () { + # cn=umcg-someuser,ou=users,ou=umcg,o=rs + local _dn="$1" + local _cn='MIA' + local _regex='cn=([^, ]+)' + if [[ ${_dn} =~ ${_regex} ]]; then + _cn="${BASH_REMATCH[1]}" + fi + printf '%s' "${_cn}" +} + +# +## +### Variables. +## +# +mixed_stdouterr='' # global variable to capture output from commands for reporting in custom log messages. + +# +# Initialise Log4Bash logging with defaults. +# +l4b_log_level="${log_level:-INFO}" +declare -A l4b_log_levels=( + ['TRACE']='0' + ['DEBUG']='1' + ['INFO']='2' + ['WARN']='3' + ['ERROR']='4' + ['FATAL']='5' +) +l4b_log_level_prio="${l4b_log_levels[${l4b_log_level}]}" + +# +## +### Main. +## +# + +# +# Get commandline arguments. +# +declare update_subscriptions='0' +declare notify_users='0' +while getopts "l:b:unh" opt; do + case $opt in + h) + showHelp + ;; + b) + backup_dir="${OPTARG}" + ;; + u) + update_subscriptions='1' + ;; + n) + notify_users='1' + ;; + l) + l4b_log_level=${OPTARG^^} + l4b_log_level_prio=${l4b_log_levels[${l4b_log_level}]} + ;; + \?) + log4Bash "${LINENO}" "${FUNCNAME:-main}" '1' "Invalid option -${OPTARG}. Try $(basename $0) -h for help." + ;; + :) + log4Bash "${LINENO}" "${FUNCNAME:-main}" '1' "Option -${OPTARG} requires an argument. Try $(basename $0) -h for help." + ;; + esac +done + +# +# Source config file. See commandline help for details of config file format. +# +config_file="$(cd -P "$( dirname "$0" )" && pwd)/${SCRIPT_NAME}.cfg" +if [[ -r "${config_file}" && -f "${config_file}" ]]; then + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Sourcing config file ${config_file}..." + source "${config_file}" || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Cannot source ${config_file}." +else + log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "Config file ${config_file} missing or not accessible." +fi + +if [[ "${update_subscriptions}" -eq '1' ]]; then + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' 'Found option -u: will update mailing list subscriptions.' +else + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' 'Option -u not specified: will only perform a "dry run" to show what needs to be updated. Use -u to update subscriptions.' +fi +if [[ "${notify_users}" -eq '1' ]]; then + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' 'Found option -n: will notify users when they are added to / changed on / deleted from the mailing list.' +else + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' 'Option -n not specified: will not notify users.' +fi + +# +# Compile list of LDAP fields to retrieve. +# +ldap_fields='cn givenName sn mail loginExpirationTime loginDisabled sshPublicKey' +log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" '0' "ldap_fields to retrieve = ${ldap_fields}." + +# +# Create tmp dir. +# +mixed_stdouterr=$(mkdir -m 0700 -p "${TMPDIR}/${SCRIPT_NAME}/" 2>&1) \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to create tmp dir ${TMPDIR}/${SCRIPT_NAME}/." +# +# Process entitlement groups. +# +for entitlement in ${entitlements[@]}; do + log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" '0' "Processing entitlement ${entitlement}..." + # + # Get current subscribers from mailing list server. + # + declare -A subscriptions=() + getSubscriptions "${entitlement}" + # + # Query LDAP and add/update/delete subscriptions. + # +# manageSubscriptions "${entitlement}" +done + +# +# Cleanup. +# +if [ ${l4b_log_level_prio} -lt ${l4b_log_levels['INFO']} ]; then + log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" 0 "Debug mode: temporary files in ${TMPDIR}/${SCRIPT_NAME}/ won't be removed." +else + mixed_stdouterr=$(rm -Rf "${TMPDIR}/${SCRIPT_NAME}/" 2>&1) \ + || log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" "${?}" "Failed to remove tmp dir ${TMPDIR}/${SCRIPT_NAME}/." +fi + +# +# Reset trap and exit. +# +log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" 0 "Finished!" +trap - EXIT +exit 0 diff --git a/roles/listserv/templates/LDAP2LISTSERV.cfg b/roles/listserv/templates/LDAP2LISTSERV.cfg new file mode 100644 index 000000000..1c6959a36 --- /dev/null +++ b/roles/listserv/templates/LDAP2LISTSERV.cfg @@ -0,0 +1,74 @@ +$ANSIBLE_VAULT;1.2;AES256;all +36626139313861616663333137303765613062393135396133663564333162353835363331366338 +3364346565623163303563643733373834663763616339650a333535336437666432383938383339 +65386335346632323630393430636166343734366232373439643466373264333439333138663434 +6236373738343961300a386135316564346263343034316337626130643638326130343136363162 +39623865303864333362623064373938366364333738643333623664653239333261376531616232 +37643462633939373437333433313264313064383637393264356635386230376233323263366165 +31663063663063323165396566343730313634663366323363316564306532336332396163356238 +33343663356131616638666462653037316162326631633464616132396135353962363266386232 +64346666663131373666323863356265393737653466633336613834623930636539306534326162 +64363339636230366130623732336539383839396166336139326537656266363265373731326461 +33663036373734316431303830663739333830373363366361303730323636366238366239323966 +32313430383634633963623763396430663861316639313263326136383561363061666435626138 +62326233323639313738663137386438663633393763643137316138323762353066326461353237 +34366365373033666466623263363565303234343934303537383437393264346234643032656263 +37346435326530376634396364383762303238316661353561393636313138393638653336323539 +31333930343436323933626662616339383833636331386432633438623264393661346436663565 +61363466363735663662333861643633363135376631326432393932343163636563323238616164 +34663264616337303631646136363164633561656332623036336265363262313231306264383235 +65393762613533646132333236383730356231306566313437373930663965386439353732363034 +39336266626430353465633465396564333461623937333963323835336434666138343532356162 +61633230636235663830626565363563656434333163666661633964366437623566323838336135 +61303431613166653036336332633338366636623435656339356365376338653964623739326539 +32383639316530323238326162666538363837383538346364356239313261386264643538626364 +36376335656366633438633938313933353365323233343532376331326532373233633934353533 +65303039383434393131396534653835346136346339333862663663373631336535656362333432 +32393266616631623066663531383566616534326233303339353634623432653535366231353237 +35313034333864643730613230353366346163353835343136353835313065333639373434633561 +61336261316231623464376265313434623730306432353364303963323161373266623837616437 +36653834613266306338623831633334356437373932663634653762326239366438626266383333 +36633536636535613735393638323361633535306232356331356333383236633264613930353937 +32623536356535343662396331393666643839326166373233616337623031616165363331313236 +34326332373137306461366538646538313139353633663965343339656261393866356430313738 +30663630306635346330653262303564306164313233393235303265343331623036316262613333 +38656464663464366563643935393631356435353535613130373034663932386462303762306235 +31303564396161306363306335373464646361643733356462613130353536643034306334323030 +65383436393035623233393962623434626137336161333039333834623565613536376335336431 +65306139656361666564663731316562393232366566363466376533643239383337333865373430 +62613237336437323761326163383266626639623332663532396636316135373339373666643430 +62613433383335306365356635313638623932363364393561383639376431663562653634313534 +62613634333434383931636436636661353337356137656665316663383237653332373866656238 +63393261353661623965333533316530316538366166643235653234373934393433623535666632 +37343163363530386336343230376666343964316435313265363032663161653132303532323132 +61376134353966376534613335393533313331366236626331353433666264653763373336386564 +62303337643437336431383964303533326133633036623232346565643637636630646132396363 +65386565636332623064666439303263396566366130363136636363353431666434343661343066 +39633836633131323164376531643833333732666663386561616434636366613961663961613366 +33656264393361333565343637386366643561653630383739663635643861393661313434613661 +66376534313233336531363137363632356161373135316437336565313533363234613262626535 +39316639363739396437376563643562323566653533336534323361316432373065396533386139 +33383562666262663430663363343435323239363233613163396664303733323632353636353134 +32643162653433386430373364636631333232326263363538656635366238366236626236366135 +35386636313664666564353035323562343737643962653030396135623431613634383237383262 +66626237306536313430303362306336343537303262396465373937373363366139656661623333 +62623936613061306433636566393934323032366234386139356539623266396364383238323435 +62303934373131376537643337373932343963346331356238653965306132383561376163363664 +39646436396162343237303864393663633135396230353437383463393434373433623639303765 +36313734666366366531663535613235326462346436633432336137616364313061393838613565 +63376533386666383437633061663231666466356235653263353832646539613663626635363862 +62346536336262313262383366366132393063646563626339346131323331323864326439313665 +36623161306162393738316562616231313731373530353430396464323639313734333931366530 +36343763643664643561653539376532633663376232643164363731633162653538633063613364 +35393763663861646137383534636165613462383266323830626336336531316163663733633662 +39323235653636313937656437626161623935363837393235383035363762356433313434613063 +33396234393532303934613665366634343634393065373334373565306330336438343266633566 +30643833643637666531356431663363656233633139343936356434336363343230323265376666 +36666233373331316335623535316436663866656663323539633333613233383433376662623463 +34613435306561643863643730383432363831373237373539313736366532646462336136396537 +38316361323936346635396133396635643461663062396265636433383739336263356633653735 +64383935393532383434656238616530356130613833636565356163393365313730346638383462 +30653763393338383233333535373538653035383739326332363638346564343161373733666635 +31666662653066343831383638323833663761343033393631653737633630623531333230646336 +38643431666132336138626330373736373762326166326663333239636565393234613463666439 +65306130343236663930636164386239663864653963356464633362383664316430 diff --git a/single_role_playbooks/listserv.yml b/single_role_playbooks/listserv.yml new file mode 100644 index 000000000..9371f547d --- /dev/null +++ b/single_role_playbooks/listserv.yml @@ -0,0 +1,5 @@ +--- +- hosts: sys_admin_interface + roles: + - listserv +... From 4472c4dc7cef2678386714328c0fc06698a11b26 Mon Sep 17 00:00:00 2001 From: gerbenvandervries Date: Thu, 23 Mar 2023 16:57:14 +0100 Subject: [PATCH 2/2] remove test comments outs --- roles/listserv/templates/LDAP2LISTSERV.bash | 25 ++++++++------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/roles/listserv/templates/LDAP2LISTSERV.bash b/roles/listserv/templates/LDAP2LISTSERV.bash index e1ed87539..57bf32809 100644 --- a/roles/listserv/templates/LDAP2LISTSERV.bash +++ b/roles/listserv/templates/LDAP2LISTSERV.bash @@ -577,12 +577,11 @@ function manageSubscriptions () { if [[ ! -z "${subscriptions["${_account_name}"]+isset}" && "${_account_must_be_subscribed}" == 'no' ]]; then _accounts["${_account_name}"]='disabled' log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Unsubscribing expired/inactive account ${_account_name} from '${_mailinglist}' mailing list..." - echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'DELETE' "${_email}" + sendListservCommand "${_mailinglist}" "${notify_users}" 'DELETE' "${_email}" elif [[ -z "${subscriptions["${_account_name}"]+isset}" && "${_account_must_be_subscribed}" == 'yes' ]]; then _accounts["${_account_name}"]='active' log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Subscribing new account ${_account_name} to '${_mailinglist}' mailing list..." - echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'ADD' "${_email}" -"${_full_name}" + sendListservCommand "${_mailinglist}" "${notify_users}" 'ADD' "${_email}" "${_full_name}" elif [[ ! -z "${subscriptions["${_account_name}"]+isset}" && "${_account_must_be_subscribed}" == 'yes' ]]; then _accounts["${_account_name}"]='active' if [[ "${subscriptions["${_account_name}"]}" =~ ${_subscription_regex} ]]; then @@ -601,8 +600,7 @@ function manageSubscriptions () { # User will receive the a notification based on the CHANGE1 template. # log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Updating email address for account ${_account_name}..." - echo "sendListservCommand" "${_mailinglist}" "${notify_users}" -'CHANGE' "${_subscribed_email}" "${_email}" + sendListservCommand "${_mailinglist}" "${notify_users}" 'CHANGE' "${_subscribed_email}" "${_email}" elif [[ "${_subscribed_email,,}" == "${_email,,}" && "${_subscribed_full_name}" != "${_full_name}" ]]; then # # Use hardcoded notify_users=0 to do a QUIET ADD, @@ -610,8 +608,7 @@ function manageSubscriptions () { # without sending a "welcome new user" notification email. # log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Updating name for account ${_account_name}..." - echo "sendListservCommand" "${_mailinglist}" '0' 'ADD' "${_email}" -"${_full_name}" + sendListservCommand "${_mailinglist}" '0' 'ADD' "${_email}" "${_full_name}" else # # Both the email address as well as the full name of the user have changed. @@ -619,10 +616,8 @@ function manageSubscriptions () { # -> unsubscribe (delete) the old user and subscribe (add) the new user. # log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Unsubscribing and resubscribing changed account ${_account_name}..." - echo "sendListservCommand" "${_mailinglist}" "${notify_users}" -'DELETE' "${_subscribed_email}" - echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'ADD' -"${_email}" "${_full_name}" + sendListservCommand "${_mailinglist}" "${notify_users}" 'DELETE' "${_subscribed_email}" + sendListservCommand "${_mailinglist}" "${notify_users}" 'ADD' "${_email}" "${_full_name}" fi else log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "Failed to split subscriber info using a pipe as separator for ${_account_name}. Contact an admin." @@ -649,8 +644,7 @@ function manageSubscriptions () { if [[ "${subscriptions["${_subscribed_account}"]}" =~ ${_subscription_regex} ]]; then local _subscribed_email="${BASH_REMATCH[1]}" log4Bash 'DEBUG' "${LINENO}" "${FUNCNAME:-main}" '0' "Unsubscribing missing account ${_subscribed_account}..." - echo "sendListservCommand" "${_mailinglist}" "${notify_users}" 'DELETE' -"${_subscribed_email}" + sendListservCommand "${_mailinglist}" "${notify_users}" 'DELETE' "${_subscribed_email}" else log4Bash 'FATAL' "${LINENO}" "${FUNCNAME:-main}" '1' "Failed to split subscriber info using a pipe as separator for ${_subscribed_account}. Contact an admin." fi @@ -667,7 +661,6 @@ function sendListservCommand () { local _name="${5:-}" # optional; not required for DELETE command. local _body # - exit 0 log4Bash 'TRACE' "${LINENO}" "${FUNCNAME:-main}" 0 "Args: _mailinglist=${_mailinglist}|_notify_users=${_notify_users}|_command=${_command}|email=${_email}|_name=${_name}." # # Use QUIET in front of command to disable sending notification messages to users. @@ -811,7 +804,7 @@ for entitlement in ${entitlements[@]}; do # # Query LDAP and add/update/delete subscriptions. # -# manageSubscriptions "${entitlement}" + manageSubscriptions "${entitlement}" done # @@ -829,4 +822,4 @@ fi # log4Bash 'INFO' "${LINENO}" "${FUNCNAME:-main}" 0 "Finished!" trap - EXIT -exit 0 +exit 0 \ No newline at end of file