Source code for accre.bots.account_creator

"""
Robot to be run periodically from cron and handle RT user creation
tickets that come from the website. This semi-automates the steps
of creating the account including checking with the PI, adding the
user to various systems, emailing the new user, and updating the
ticket.

Account creation is run as a pipeline repurposing the ticket
priority number to indicate the stage of each new account request.
The stages are given in the pipeline description below:

"""
from datetime import datetime
import json
import logging
import re
import subprocess
import sys

import urllib3

from accre.account_management import (
    VUNetIDValidator,
    ClusterAccountCreator,
    repair_affiliate_group_membership,
    repair_ldap_group_membership,
    repair_user_slurm_associations,
    repair_user_slurm_acc_associations
)
from accre.config import get_config
from accre.database import VandyAdminDBClient
from accre.exceptions import ACCREError
from accre.logger import get_logger
from accre.rt_requests import Requestor


# Description of the creation pipeline to be added to each ticket and
# to document the pipeline stages. If the pipeline is changed make sure
# that this description matches the actual pipeline executed in the
# NewUser class below.
PIPELINE_DESCRIPTION = """\
* Priority 0 : New ticket 
* Priority 1 : VUNetID verified and cluster account does not exist
* Priotity 2 : Sent PI verification message, awaiting response from PI
* Priority 3 : Ready to be added to the database
* Priority 4 : Username added to database, ready for cluster account creation
* Priority 5 : Account created in POSIX. Ready for secondary group addition
  and slurm associations
* Priority 6 : Account fully created, ready to be updated in RT
* Priority 7 : Username updated in RT, ticket is ready to be closed

* Priority -1 : An error has occurred and manual intervention is required.
  After fixing the priority can be set back to a positive integer and
  the automated process may resume.

Note that ACCRE staff must manually change ticket priority to 3 after
verification for account creation to begin.

For priority 0 the ticket status should be 'new', for all others 'open',
otherwise the account creation bot will ignore a ticket.
"""
__doc__ += PIPELINE_DESCRIPTION

CONFIG = get_config()
LOGGER = get_logger()


[docs]def run_account_creator(): """ Function or CLI endpoint to run the account creator, searching through all RT tickets and performing appropriate steps in the account creation pipeline. To simply and rapidly process account requests, each pipeline stage is executed as a separate task in sequence and tickets are re-read each time. """ # Suppress certificate warnings from RT (TODO: fix the cert!) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) for stage in range(0, 8): for user in retrieve_new_users(stage=stage): try: user.process_stage(stage) except Exception: LOGGER.exception( 'Failed processing user request for %s at stage %d', user.vunetid, stage )
[docs]def retrieve_new_users(stage): """ Search all tickets for the given stage, and return a list of NewUser objects from the ticket data matching the given pipeline stage. :param int stage: Pipeline stage to check :returns: list of new user requests :rtype: list(NewUser) """ requestor = Requestor() status = 'open' owner = CONFIG['account-management']['rt-user'] queue = CONFIG['account-management']['rt-queue'] if stage == 0: owner = 'Nobody' status = 'new' query = ( "Status = '{0}' AND Priority = '{1}' AND " "Queue = '{2}' AND Owner = '{3}'" .format(status, stage, queue, owner) ) ticket_ids = list(requestor.search_tickets( query=query, filter_fun=lambda t: t[1].startswith('ACCRE New User:') )) new_users = [] for ticket_id in ticket_ids: try: new_users.append( NewUser(**requestor.ticket_account_bot_json(ticket_id)) ) except Exception: LOGGER.exception( "New user not extracted from ticket {0}".format(ticket_id) ) return new_users
[docs]class NewUser: """ Stores information about the new user and performs each step of the user creation pipeline """ def __init__(self, name, vunetid, email, group, ticket): self.name = name self.vunetid = vunetid self.email = email self.group = group self.ticket = ticket self.requestor = Requestor()
[docs] def process_stage(self, stage): """ Dispatch method for each pipeline stage """ LOGGER.info('Processing Stage %d account creation for %s ticket %s', stage, self.vunetid, self.ticket ) if stage == 0: self.add_pipeline_description() self.verify_account(stage) elif stage == 1: self.verify_pi(stage) elif stage == 2: self.maybe_remind_pi(stage) elif stage == 3: self.add_to_database(stage) elif stage == 4: self.create_cluster_account(stage) elif stage == 5: self.initial_account_repair(stage) elif stage == 6: self.update_rt_username(stage) elif stage == 7: self.close_ticket(stage) else: LOGGER.error('Invalid stage %d account creation for %s ticket %s', stage, self.vunetid, self.ticket )
[docs] def add_pipeline_description(self): """ Adds a comment to the ticket with a description of the creation pipeline and instructions to the administrator. """ msg = ( "The account creation bot has recognized this ticket as a new " "account request with the following user information: \n" "name: {0}\n" "vunetid: {1}\n" "email: {2}\n" "group: {3}\n\n" "This ticket will now be processed by priority setting " "according to the following pipeline:\n\n" .format(self.name, self.vunetid, self.email, self.group) ) msg += PIPELINE_DESCRIPTION self.requestor.ticket_add_comment(self.ticket, msg)
[docs] def verify_account(self, stage): """ Verify that this user is not already in the database and has an active VUNetID or robot equivalent and that the email matches """ with VUNetIDValidator() as client: if not client.exists(self.vunetid): self.fail_and_report( '{0} is not a valid VUNetID'.format(self.vunetid) ) return if client.is_locked(self.vunetid): self.fail_and_report( '{0} is a locked VUNetID'.format(self.vunetid) ) vunet_email = client.info(self.vunetid)['vanderbilt_email'] dbclient = VandyAdminDBClient() if dbclient.user_exists(self.vunetid): self.requestor.ticket_add_reply( self.ticket, 'It appears you already have an account on the ACCRE cluster. ' 'Please reply to this email if you need help logging in ' 'or a password reset.' ) self.fail_and_report( '{0} is already an ACCRE user.'.format(self.vunetid) ) return msg = ( '{0} ({1}) is not a current cluster user and has an active ' 'VUNetID, so you may proceed with the ' 'account creation assuming this VUNetID is valid for this person.' ' The email associated with this VUNetID is {2}.' .format(self.name, self.vunetid, vunet_email) ) self.requestor.ticket_add_comment(self.ticket, msg) self.requestor.ticket_change_owner( self.ticket, CONFIG['account-management']['rt-user'] ) self.requestor.ticket_change_priority(self.ticket, stage + 1) self.requestor.ticket_change_status(self.ticket, "open")
[docs] def verify_pi(self, stage): """ Request approval from the PI """ LOGGER.info("Requesting approval from PI!") self.requestor.ticket_add_reply( self.ticket, 'Dear faculty approver for {name},\n\n ' 'Please reply to this email (a simple yes will suffice) ' 'confirming that {name} is approved for a new user ' 'account in the {group} ACCRE group and covered under ' 'the terms of your Disclosure ' 'for this group.'.format(name=self.name, group=self.group) ) LOGGER.info( "Changing priority of ticket to {0}, " "indicating that it is awaiting approval from the PI." .format(stage + 1) ) self.requestor.ticket_change_priority(self.ticket, stage + 1)
[docs] def maybe_remind_pi(self, stage): """ Remind the PI to decide on approving the user if the ticket has not been updated in two days """ # Get days since last update tp = self.requestor.ticket_properties(self.ticket) last_update = tp["LastUpdated"] start = datetime.strptime(last_update, '%a %b %d %H:%M:%S %Y') delta = datetime.now() - start days = delta.total_seconds() / (3600.0 * 24.0) # convert to days # Get last ticket responder ticket_histories = self.requestor.ticket_history(self.ticket) last_response_string = "" for k in ticket_histories: for v in ticket_histories[k].splitlines(): if "Correspondence added" in v: # keep searching to get last response last_response_string = v owner = CONFIG['account-management']['rt-user'] if days > 2.0 and owner in last_response_string: LOGGER.info("Sending reminder re: {0} to PI!".format(self.ticket)) self.requestor.ticket_add_reply( self.ticket, 'Hello, this request still awaits your approval (see below). ' 'Thank you.\n\n ' 'Dear faculty approver for {name},\n\n ' 'Please reply "YES" to this email confirming ' 'that {name} is approved for a new user account ' 'in the {group} ACCRE group and covered under the terms ' 'of your Disclosure for this group.' .format(name=self.name, group=self.group) )
[docs] def add_to_database(self, stage): """ Add the new user to the database along with any secondary groups specified. """ LOGGER.info('Adding new user %s to the database', self.vunetid) try: with VUNetIDValidator() as client: uid = client.info(self.vunetid)['uid'] dbclient = VandyAdminDBClient() dbclient.add_user( vunetid=self.vunetid, user_id=uid, group=self.group, email=self.email, first_name=self.name.split()[0], last_name=self.name.split()[-1] ) except Exception as e: LOGGER.exception('Exception adding %s to the DB', self.vunetid) self.fail_and_report( 'Failed to add {0} to the database: {1}' .format(self.vunetid, str(e)) ) return msg = 'Created database account for new user' self.requestor.ticket_add_comment(self.ticket, msg) self.requestor.ticket_change_priority(self.ticket, stage + 1)
[docs] def create_cluster_account(self, stage): """ Initial creation of the POSIX account """ LOGGER.info('Creating POSIX account for %s', self.vunetid) try: cac = ClusterAccountCreator( vunetid=self.vunetid, ticket=self.ticket ) result = cac.create_cluster_account() if result['error']: self.fail_and_report( 'Problem creating POSIX account for {0}: {1}' .format(self.vunetid, result) ) return except Exception as e: LOGGER.exception( 'Failure creating POSIX account for %s', self.vunetid ) self.fail_and_report( 'Failure creating POSIX account for {0}: {1}' .format(self.vunetid, str(e)) ) return msg = 'Created POSIX account for new user' self.requestor.ticket_add_comment(self.ticket, msg) self.requestor.ticket_change_priority(self.ticket, stage + 1)
[docs] def initial_account_repair(self, stage): """ Perform initial account repairs, i.e. add secondary groups and required slurm associations. """ LOGGER.info( 'Adding secondary groups to DB from affiliates for %s', self.vunetid ) try: result = repair_affiliate_group_membership(self.vunetid) msg = 'Repaired affiliate groups: {0}'.format(result) self.requestor.ticket_add_comment(self.ticket, msg) except Exception as e: LOGGER.exception('Failed to add affiliate groups') self.fail_and_report('Failed to add affiliate groups') return LOGGER.info('Adding secondary groups to LDAP for %s', self.vunetid) try: result = repair_ldap_group_membership(self.vunetid) msg = 'Repaired secondary groups: {0}'.format(result) self.requestor.ticket_add_comment(self.ticket, msg) except Exception as e: LOGGER.exception('Failed to repair secondary groups') self.fail_and_report('Failed to repair secondary groups') return LOGGER.info("Adding slurm associations for %s", self.vunetid) try: result = repair_user_slurm_associations(self.vunetid) msg = 'Repaired slurm associations: {0}'.format(result) self.requestor.ticket_add_comment(self.ticket, msg) except Exception as e: LOGGER.exception('Failed to repair slurm associations') self.fail_and_report('Failed to repair slurm associations') return LOGGER.info("Adding accel slurm associations for %s", self.vunetid) try: result = repair_user_slurm_acc_associations(self.vunetid) msg = 'Repaired accel slurm associations: {0}'.format(result) self.requestor.ticket_add_comment(self.ticket, msg) except Exception as e: LOGGER.exception('Failed to repair accel slurm associations') self.fail_and_report('Failed to repair accel slurm associations') return self.requestor.ticket_change_priority(self.ticket, stage + 1)
[docs] def update_rt_username(self, stage): """ Change new user's RT name from their email to their vunetid """ LOGGER.info("Updating username in RT: {0}".format(self.ticket)) try: response = self.requestor.update_username(self.email, self.vunetid) except Exception as e: msg = 'Failed to connect to RT for username change' LOGGER.exception(msg) self.fail_and_report(msg) return if '200 Ok' not in response: self.fail_and_report( 'Failed to update username, RT response: {0}'.format(response) ) return fmt_response = "Output from RT username update:\n{0}".format(response) self.requestor.ticket_add_comment(self.ticket, fmt_response) self.requestor.ticket_change_priority(self.ticket, stage + 1)
[docs] def close_ticket(self, stage, threshold=1.0): """ Close the new user ticket if threshold days have passed with no ticket activity """ # Get days since last update tp = self.requestor.ticket_properties(self.ticket) last_update = tp["LastUpdated"] start = datetime.strptime(last_update, '%a %b %d %H:%M:%S %Y') delta = datetime.now() - start days = delta.total_seconds() / (3600.0 * 24.0) # convert to days if days > threshold: msg = ( "No reply on ticket {0} for at least {1} days. Closing!" .format(self.ticket, threshold) ) self.requestor.ticket_change_status( self.ticket, 'resolved' ) LOGGER.info(msg)
[docs] def fail_and_report(self, msg): """ Log failure messge, add to ticket, set ticket priority to -1 to stop pipeline """ LOGGER.error('Account creation error for %s: %s', self.vunetid, msg) msg += ( '\n\nPlease manually diagnose and fix this problem, ' 'then reset ticket priority to continue the creation pipeline.' ) self.requestor.ticket_add_comment(self.ticket, msg) self.requestor.ticket_change_priority(self.ticket, -1)