Back to the main page

Import DSEE automount maps into IPA

Intro

This is python script to import DSEE (Directory Server Enterprise Edition) automount maps into FreeIPA.

Design

Prerequisite

A keytab is a file containing pairs of Kerberos principals and encrypted keys. Will need one for admin account, to be used in the script.
Example of genereting a keytab for Zarko, in the working directory.

-bash-4.2$ ipa-getkeytab -s ipa-server -p zarko@DOMAIN.COM -P -k zarko.kt New Principal Password: Verify Principal Password: Failed to parse result: Failed to decode GetKeytab Control. Retrying with pre-4.0 keytab retrieval method... Failed to retrieve encryption type Camellia-128 CTS mode with CMAC (#25) Failed to retrieve encryption type Camellia-256 CTS mode with CMAC (#26) Keytab successfully retrieved and stored in: zarko.kt

Example of using keytab

-bash-4.2$ klist klist: Credentials cache keyring 'persistent:485400000:485400000' not found -bash-4.2$ kinit -kt .ipa/admin.kt admin -bash-4.2$ klist Ticket cache: KEYRING:persistent:485400000:485400000 Default principal: admin@DOMIAN.COM Valid starting Expires Service principal 06/13/2019 13:42:57 06/14/2019 13:42:57 krbtgt/DOMAIN.COM@DOMAIN.COM

Implementation

[IPA zdudic@admin-host] /import/sascripts/ipa/dsee-ipa_automountmap_sync.py -h usage: dsee-ipa_automountmap_sync.py [-h] -d DSEE_MAP -i IPA_MAP -c CHANGES Sync automount maps from DSEE to IPA optional arguments: -h, --help show this help message and exit -d DSEE_MAP, --dsee_map DSEE_MAP DSEE indirect automount map -i IPA_MAP, --ipa_map IPA_MAP IPA indirect automount map -c CHANGES, --changes CHANGES Number of changes that are reason for frown

Cron jobs on admin-host, example:

# Import DSEE automount maps into IPA 05 15 * * * /import/sascripts/ipa/dsee-ipa_automountmap_sync.py -d auto_workbucket -i auto.workbucket -c 100 >/dev/null 35 15 * * * /import/sascripts/ipa/dsee-ipa_automountmap_sync.py -d auto_import -i auto.import -c 100 >/dev/null

Script

#!/some-path/python3.5 import os import sys import socket import getpass import argparse import logging, logging.handlers from time import gmtime, strftime import datetime import paramiko import subprocess import re import difflib import contextlib # modules for sending plain text email import smtplib from email.mime.text import MIMEText # # logging destination PROGRAM = os.path.basename(sys.argv[0]) # name of this script LOG_PATH = ("/var/log/" + PROGRAM) # put together "/var/log/" if not os.path.exists(LOG_PATH): # create LOG_PATH if doesn't exists os.makedirs(LOG_PATH) os.chown(LOG_PATH,-1,485400023) # root(-1 means no change):userg_sa IPA group os.chmod(LOG_PATH,0o775) # drwxrwxr-x parser = argparse.ArgumentParser( description="Sync automount maps from DSEE to IPA ", epilog='Brought to you by ZD') parser.add_argument("-d", "--dsee_map", help="DSEE indirect automount map", required=True) parser.add_argument("-i", "--ipa_map", help="IPA indirect automount map", required=True) parser.add_argument("-c", "--changes", help="Number of changes that are reason for frown", type=int, required=True) args = parser.parse_args() dsee_map=args.dsee_map ipa_map=args.ipa_map changes=args.changes # variables dsee_server = "dsee-server.domain.com" who_cares = "zarko@domain.com" automountmap_tmpfile = "/tmp/" + PROGRAM + ".automountmap." + dsee_map keys_tmpfile = "/tmp/" + PROGRAM + ".keys." + dsee_map keys_tmpfile_clean = "/tmp/" + PROGRAM + ".keys-clean." + dsee_map keys_info_tmpfile = "/tmp/" + PROGRAM + ".keys_info." + dsee_map ipa_keys_tmpfile = "/tmp/" + PROGRAM + ".ipa_keys." + ipa_map added_keys_tmpfile = "/tmp/" + PROGRAM + ".added_keys." + ipa_map removed_keys_tmpfile = "/tmp/" + PROGRAM + ".removed_keys." + ipa_map diff_keys_tmpfile = "/tmp/" + PROGRAM + ".diff_keys." + dsee_map + "_" + ipa_map to_add_keys = "/tmp/" + PROGRAM + ".to_add_keys." + ipa_map to_rm_keys = "/tmp/" + PROGRAM + ".to_rm_keys." + ipa_map # --- Define logging LOG_FILE = (LOG_PATH + "/" + datetime.datetime.now().strftime("%m-%d-%Y_%Hh%Mm%Ss")) # create logger (interface that script uses for logging) logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # create file handler (define log destination) handler = logging.handlers.TimedRotatingFileHandler(LOG_FILE, when='MIDNIGHT', backupCount=50, utc=False) # create formatter (define layout of logs) formatter = logging.Formatter('%(asctime)s:%(levelname)s: %(message)s') handler.setFormatter(formatter) # add handler to logger logger.addHandler(handler) logger.debug("") logger.debug("START AT : " + strftime("%a, %d %b %Y %H:%M:%S", gmtime())) logger.debug("Threshold for changes is " + str(changes) ) logger.debug("Importing DSEE automount map " + dsee_map + " into CA-LDAP " + ipa_map) group=os.getegid() if group != 485400023: logger.debug("Only member of userg_sa IPA group can run this") sys.exit("Only member of userg_sa IPA group can run this") else: logger.debug("Running by " + getpass.getuser()) print("Running by " + getpass.getuser()) def send_email_errors(body): """ Email errors Argument: email body """ message = MIMEText(body) # email body message['Subject'] = "[" + socket.gethostname() + ":" + PROGRAM + "]: Error has been reported" message['From'] = 'labops-support@domain.com' message['To'] = who_cares try: s = smtplib.SMTP(host="internal-mail-router.domain.com") s.send_message(message) s.quit() print("Email about error sent" ) logger.debug("Email about error sent" ) except SMTPException: logger.debug("Cannot send email about error") sys.exit("Cannot send email about error") def send_email_report(body): """ Email report about added, removed keys Argument: email body """ message = MIMEText(body) # email body message['Subject'] = "[" + socket.gethostname() + ":" + PROGRAM + "]:" + ipa_map + " automount maps keys changes" message['From'] = 'labops-support@domain.com' message['To'] = who_cares try: s = smtplib.SMTP(host="internal-mail-router.domain.com") s.send_message(message) s.quit() print("Email sent about key changes." ) logger.debug("Email sent about key changes.") except SMTPException: logger.debug("Cannot send email about key changes.") sys.exit("Cannot send email about key changes.") def admin_kinit(): """ Obtain and cache Kerberos ticket-granting ticket for admin admin is IPA administrator account """ try: print("Obtaining Kerberos ticket-granting ticket for admin (IPA Administrator)") logger.debug("Obtaining Kerberos ticket-granting ticket for admin (IPA Administrator)") subprocess.call(['sudo -u admin kinit -kt /homelocal/admin/.ipa/admin.kt admin'], shell=True) except subprocess.CalledProcessError as err: logger.debug("Admin account (IPA Administrator) cannot obtain Kerberos ticket-granting ticket" ) logger.debug("subprocess.CalledProcessError: {0}".format(err)) send_email_errors("Admin account (IPA Administrator) cannot obtain Kerberos ticket-granting ticket") sys.exit("subprocess.CalledProcessError: {0}".format(err)) def append_to_tmpfile(file_name, text): """ Append stdout to tmpfile Arguments: file name and text """ global automountmap_tmpfile original = sys.stdout sys.stdout = open(file_name, 'a') print(text) sys.stdout.close() sys.stdout = original def write_to_tmpfile(file_name, text): """ Write stdout to tmpfile Arguments: file name and text """ global automountmap_tmpfile original = sys.stdout sys.stdout = open(file_name, 'w+') print(text) sys.stdout.close() sys.stdout = original def get_dsee_automount_map(): """ Get DSEE automount map, SSH to dsee server as user who runs the script, a user must be able to SSH passwordless """ runner=getpass.getuser() sshkey="/home/" + runner + "/.ssh/id_rsa" # user's key for passwordless SSH client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname=dsee_server, username=runner, key_filename=sshkey) try: print("Getting " + dsee_map + " keys from " + dsee_server) logger.debug("Getting " + dsee_map + " keys from " + dsee_server) stdin, stdout, stderr = client.exec_command('ldaplist -l %s' % (dsee_map)) if stdout.channel.recv_exit_status() != 0: logger.debug("Cannot get " + dsee_map + " keys from " + dsee_server) send_email_errors("Cannot get " + dsee_map + " keys from " + dsee_server) sys.exit("Cannot get " + dsee_map + " keys from " + dsee_server) stdout=stdout.readlines() # this is list print(' '.join(stdout)) # print list, shorter write_to_tmpfile(automountmap_tmpfile, ' '.join(stdout)) logger.debug(' '.join(stdout)) except paramiko.ssh_exception.SSHException as ssherr: logger.debug("SSHException: {0}".format(ssherr)) send_email_errors("SSHException: {0}".format(ssherr)) sys.exit("SSHException: {0}".format(ssherr)) except paramiko.ssh_exception.ChannelException as cherr: logger.debug("paramiko.ssh_exception.ChannelException: {0}".format(cherr)) send_email_errors("paramiko.ssh_exception.ChannelException: {0}".format(cherr)) sys.exit("paramiko.ssh_exception.ChannelException: {0}".format(cherr)) except paramiko.ssh_exception.NoValidConnectionsError as err: logger.debug("NoValidConnectionsError: {0}".format(err)) send_email_errors("NoValidConnectionsError: {0}".format(err)) sys.exit("NoValidConnectionsError: {0}".format(err)) except paramiko.ssh_exception.PartialAuthentication as parerr: logger.debug("PartialAuthentication: {0}".format(parerr)) send_email_errors("PartialAuthentication: {0}".format(parerr)) sys.exit("PartialAuthentication: {0}".format(parerr)) close() def get_dsee_keys(): """ Extract key from automount map temp file, also remove fauly keys, like ones having equal sign '=' """ f = open(automountmap_tmpfile, 'r') try: for line in f: if re.search("automountKey:", line): key = line.split()[1] print("Key: " + key) append_to_tmpfile(keys_tmpfile, key) logger.debug("Key: " + key) f.close() except IOError as err: print("IOError: {0}".format(err)) send_email_errors("IOError: {0}".format(err)) sys.exit("IOError: {0}".format(err)) # need to remove fault keys, like ones having = ff = open(keys_tmpfile, 'r') try: for line in ff: if re.search("=", line) is None: append_to_tmpfile(keys_tmpfile_clean, line.split("\n")[0]) ff.close() except IOError as err: print("IOError: {0}".format(err)) send_email_errors("IOError: {0}".format(err)) sys.exit("IOError: {0}".format(err)) def get_ipa_keys(ipa_map): """ Get existing IPA (CA-LDAP) keys and put in the tmp file """ try: current_ipa_keys = subprocess.check_output(['ldapsearch -h ipa-server.domain.com -LLL \ -b automountmapname=%s,cn=default,cn=automount,dc=domain,dc=com \ -D "cn=directory manager" -w `echo abcdABCD | openssl enc -base64 -d` -LLL \ | grep automountKey | awk \'{print $2}\' \ | sort | uniq' % ipa_map], shell=True) write_to_tmpfile(ipa_keys_tmpfile, current_ipa_keys.decode()) except subprocess.CalledProcessError as err: logger.debug("Can't get existing IPA keys for " + ipa_map ) send_email_errors("Can't get existing IPA keys for " + ipa_map ) logger.debug("subprocess.CalledProcessError: {0}".format(err)) sys.exit("subprocess.CalledProcessError: {0}".format(err)) def diff_dsee_n_ipa_keys(): """ Diff from keys files, DSEE (keys_tmpfile) diff IPA (ipa_keys_tmpfile) Create one file with keys to be added into IPA Create another file with keys to be removed from IPA """ # remove blank lines logger.debug("Removing blank lines from " + keys_tmpfile) with open(keys_tmpfile, 'r') as f1: # this care of closing file later for line in sorted(f1): if line.strip(): # this isn't blank line with open(keys_tmpfile_clean + ".noblnk", 'a') as f1_noblnk: with contextlib.redirect_stdout(f1_noblnk): print(line.strip()) logger.debug("Removing blank lines from " + ipa_keys_tmpfile) with open(ipa_keys_tmpfile, 'r') as f2: for line in sorted(f2): if line.strip(): # this isn't blank line with open(ipa_keys_tmpfile + ".noblnk", 'a') as f2_noblnk: with contextlib.redirect_stdout(f2_noblnk): print(line.strip()) logger.debug("Doing diff between " + ipa_keys_tmpfile + ".noblnk" + " and " + keys_tmpfile_clean + ".noblnk" ) original = sys.stdout sys.stdout = open(diff_keys_tmpfile, 'w+') # diff ipa_keys dsee_keys diff = difflib.ndiff(open(ipa_keys_tmpfile + ".noblnk").readlines(),open(keys_tmpfile_clean + ".noblnk").readlines()) print (''.join(diff), end="") sys.stdout.close() sys.stdout = original logger.debug("Keys planned to be added and/or deleted:" ) with open(diff_keys_tmpfile, 'r') as f: for line in f: if re.search ("^-", line.strip()): with open(to_rm_keys, 'a') as f_remove: with contextlib.redirect_stdout(f_remove): print(line.strip().split()[1]) # to be removed from IPA logger.debug("To be removed: " + line.strip().split()[1]) if re.search ("^\+", line.strip()): with open(to_add_keys, 'a') as f_add: with contextlib.redirect_stdout(f_add): print(line.strip().split()[1]) # to be added to IPA logger.debug("To be added: " + line.strip().split()[1]) def get_dsee_keyinfo(): """ Get DSEE info for a key, that's mount option, nfs server and export SSh to dsee server as user who runs the script """ runner=getpass.getuser() sshkey="/home/" + runner + "/.ssh/id_rsa" client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname=dsee_server, username=runner, key_filename=sshkey) if os.path.exists(to_add_keys): f = open(to_add_keys, 'r') try: for key in f: print(" - Getting " + key.strip() + " info from " + dsee_server) logger.debug(" - Getting " + key.strip() + " info from " + dsee_server) stdin, stdout, stderr = client.exec_command('ldaplist -l %s %s |grep automountInformation' % (dsee_map, key)) stdout=stdout.readlines() # this is list logger.debug(' '.join(stdout)) keyinfo = (' '.join(stdout)).split("automountInformation:")[1].split("\n")[0] # extract key info print("Key Info: " + keyinfo) logger.debug("Key Info: " + keyinfo) append_to_tmpfile(keys_info_tmpfile, key.strip() + ":=:" + keyinfo.strip()) # :=: devides key and info except paramiko.ssh_exception.SSHException as ssherr: logger.debug("SSHException: {0}".format(ssherr)) send_email_errors("SSHException: {0}".format(ssherr)) sys.exit("SSHException: {0}".format(ssherr)) except paramiko.ssh_exception.ChannelException as cherr: logger.debug("paramiko.ssh_exception.ChannelException: {0}".format(cherr)) send_email_errors("paramiko.ssh_exception.ChannelException: {0}".format(cherr)) sys.exit("paramiko.ssh_exception.ChannelException: {0}".format(cherr)) except paramiko.ssh_exception.NoValidConnectionsError as err: logger.debug("NoValidConnectionsError: {0}".format(err)) send_email_errors("NoValidConnectionsError: {0}".format(err)) sys.exit("NoValidConnectionsError: {0}".format(err)) except paramiko.ssh_exception.PartialAuthentication as parerr: logger.debug("PartialAuthentication: {0}".format(parerr)) send_email_errors("PartialAuthentication: {0}".format(parerr)) sys.exit("PartialAuthentication: {0}".format(parerr)) close() def is_key_in_ipa(ipamap, key): """ Find if key already exists in IPA Arguments: IPA automount map, key - IPA automount is provided by runner - key is found by script Run as admin since admin has kerberos ticket """ return True if subprocess.call(['sudo -u admin ipa automountkey-show default %s --key=%s' % (ipamap, key)], shell=True) == 0 else False def add_key_in_ipa(): """ Add DSEE keys into IPA Run as admin since admin has kerberos ticket """ if os.path.exists(keys_info_tmpfile): check_number_of_added_keys() f = open(keys_info_tmpfile, 'r') for line in f: key = line.split(":=:")[0] info = line.split(":=:")[1].split("\n")[0] if not is_key_in_ipa(ipa_map, key): admin_kinit() print("Adding key " + key + " with info " + info + " into " + ipa_map ) logger.debug("Adding key " + key + " with info " + info + " into " + ipa_map ) logger.debug("-------------------------------------------------- ") try: subprocess.check_output(['sudo -u admin ipa automountkey-add default %s --key=%s --info="%s"' % (ipa_map, key, info)], shell=True) append_to_tmpfile(added_keys_tmpfile, "Added to " + ipa_map + " : " + key + " : " + info) except subprocess.CalledProcessError as err: logger.debug("There is problem with adding key " + key + " into " + ipa_map ) send_email_errors("There is problem with adding key " + key + " into " + ipa_map ) logger.debug("subprocess.CalledProcessError: {0}".format(err)) sys.exit("subprocess.CalledProcessError: {0}".format(err)) else: logger.debug("Key " + key + " already exists in " + ipa_map ) logger.debug("---------------------------------------------- ") f.close() def remove_key_from_ipa(): """ Remove keys (that are not in DSEE) from IPA """ if os.path.exists(to_rm_keys): check_number_of_removed_keys() f = open(to_rm_keys, 'r') for key in f: admin_kinit() print("Removing key " + key.strip() + " from IPA's automount map " + ipa_map ) logger.debug("Removing key " + key.strip() + " from IPA's automount map " + ipa_map ) logger.debug("-------------------------------------------------- ") try: subprocess.check_output(['sudo -u admin ipa automountkey-del default %s --key=%s' % (ipa_map, key)], shell=True) append_to_tmpfile(removed_keys_tmpfile, "Deleted from " + ipa_map + " : " + key.strip() ) except subprocess.CalledProcessError as err: logger.debug("There is problem with deleting key " + key + " from " + ipa_map ) send_email_errors("There is problem with deleting key " + key + " from " + ipa_map ) logger.debug("subprocess.CalledProcessError: {0}".format(err)) sys.exit("subprocess.CalledProcessError: {0}".format(err)) f.close() def check_number_of_added_keys(): """ Find how many added keys and compare with provided number of changes, if it's bigger then number of changes, then abort import and send email """ num_additions = len(open(to_add_keys).read().splitlines()) if ( num_additions > changes ): send_email_errors("Additions into " + ipa_map + " are supposed to be bigger then " + str(changes) + ". Aborting. Please check this." ) logger.debug("Additions into " + ipa_map + " are supposed to be bigger then " + str(changes) + ". Aborting. Please check this." ) sys.exit("Additions into " + ipa_map + " are supposed to be bigger then " + str(changes) + ". Aborting. Please check this." ) def check_number_of_removed_keys(): """ Find how many removed keys and compare with provided number of changes, if it's bigger then number of changes, then abort deletion and send email """ num_deletions = len(open(to_rm_keys).read().splitlines()) if ( num_deletions > changes ): send_email_errors("Deletions from " + ipa_map + " are supposed to be bigger then " + str(changes) + ". Aborting. Please check this." ) logger.debug("Deletions from " + ipa_map + " are supposed to be bigger then " + str(changes) + ". Aborting. Please check this." ) sys.exit("Deletions from " + ipa_map + " are supposed to be bigger then " + str(changes) + ". Aborting. Please check this." ) def email_added_report(): """ If there are added automount map keys, email report with the list of them """ if os.path.exists(added_keys_tmpfile): f = open(added_keys_tmpfile, 'r') lines = f.readlines() emailbody = ''.join(lines) send_email_report(emailbody) def email_deleted_report(): """ If there are deleted automount map keys, email report with the list of them """ if os.path.exists(removed_keys_tmpfile): f = open(removed_keys_tmpfile, 'r') lines = f.readlines() emailbody = ''.join(lines) send_email_report(emailbody) def rm_tmp_files(): """ Remove temp files """ if os.path.exists(automountmap_tmpfile): os.remove(automountmap_tmpfile) logger.debug("Removed " + automountmap_tmpfile) if os.path.exists(keys_tmpfile): os.remove(keys_tmpfile) logger.debug("Removed " + keys_tmpfile) if os.path.exists(keys_tmpfile_clean): os.remove(keys_tmpfile_clean) logger.debug("Removed " + keys_tmpfile_clean) if os.path.exists(added_keys_tmpfile): os.remove(added_keys_tmpfile) logger.debug("Removed " + added_keys_tmpfile) if os.path.exists(removed_keys_tmpfile): os.remove(removed_keys_tmpfile) logger.debug("Removed " + removed_keys_tmpfile) if os.path.exists(keys_info_tmpfile): os.remove(keys_info_tmpfile) logger.debug("Removed " + keys_info_tmpfile) if os.path.exists(diff_keys_tmpfile): os.remove(diff_keys_tmpfile) logger.debug("Removed " + diff_keys_tmpfile) if os.path.exists(to_add_keys): os.remove(to_add_keys) logger.debug("Removed " + to_add_keys) if os.path.exists(to_rm_keys): os.remove(to_rm_keys) logger.debug("Removed " + to_rm_keys) if os.path.exists(ipa_keys_tmpfile): os.remove(ipa_keys_tmpfile) logger.debug("Removed " + ipa_keys_tmpfile) if os.path.exists(keys_tmpfile_clean + ".noblnk"): os.remove(keys_tmpfile_clean + ".noblnk") logger.debug("Removed " + keys_tmpfile_clean + ".noblnk") if os.path.exists(ipa_keys_tmpfile + ".noblnk"): os.remove(ipa_keys_tmpfile + ".noblnk") logger.debug("Removed " + ipa_keys_tmpfile + ".noblnk") if __name__ == '__main__': rm_tmp_files() # just in case there are some left over admin_kinit() get_dsee_automount_map() get_dsee_keys() get_ipa_keys(ipa_map) diff_dsee_n_ipa_keys() get_dsee_keyinfo() add_key_in_ipa() remove_key_from_ipa() email_added_report() email_deleted_report() rm_tmp_files() logger.debug("FINISH AT : " + strftime("%a, %d %b %Y %H:%M:%S", gmtime())) sys.exit(0)



Back to the main page