def per_device_work(session, enable_pass): """ This function contains the code that should be executed on each device that this script connects to. It is called after establishing a connection to each device in the loop above. You can either put your own code here, or if there is a single-device version of a script that performs the correct task, it can be imported and called here, essentially making this script connect to all the devices in the chosen CSV file and then running a single-device script on each of them. """ script = session.script interesting_keys = ['HARDWARE', 'HOSTNAME', 'MODEL', 'VERSION', 'SERIAL', 'UPTIME', 'LAST_REBOOT_REASON', 'IMAGE'] # Validate device is of a proper OS supported_os = ['IOS', 'NXOS', 'ASA'] session.start_cisco_session(enable_pass=enable_pass) session.validate_os(supported_os) # Select the appropriate template to process show version data if session.os == 'IOS': ver_template_file = script.get_template('cisco_ios_show_version.template') elif session.os == 'NXOS': ver_template_file = script.get_template('cisco_nxos_show_version.template') elif session.os == 'ASA': ver_template_file = script.get_template('cisco_asa_show_version.template') else: raise sessions.UnsupportedOSError("{0} isn't a supported OS.".format(session.os)) # Process Show Version data raw_version = session.get_command_output('show version') fsm_output = utilities.textfsm_parse_to_dict(raw_version, ver_template_file) if len(fsm_output) > 1: raise sessions.InteractionError("Received multiple entries from a single device, which should not happen.") else: ver_info = fsm_output[0] # For NXOS get parse 'show inventory' for model and serial number if session.os == 'NXOS': ver_info['HOSTNAME'] = session.hostname logger.debug("<M_SCRIPT> NXOS device, getting 'show inventory'.") raw_inv = session.get_command_output('show inventory') inv_template_file = script.get_template('cisco_nxos_show_inventory.template') inv_info = utilities.textfsm_parse_to_dict(raw_inv, inv_template_file) for entry in inv_info: if entry['NAME'] == "Chassis": logger.debug("<M_SCRIPT> Adding {0} as model number".format(entry['PID'])) ver_info['MODEL'] = entry['PID'] logger.debug("<M_SCRIPT> Adding {0} as serial number".format(entry['SN'])) ver_info['SERIAL'] = entry['SN'] break elif session.os == 'ASA': logger.debug("<M_SCRIPT> ASA device, writing 'N/A' for last reboot reason.") # For ASA put a N/A reload reason since ASA doesn't have this output ver_info['LAST_REBOOT_REASON'] = "N/A" # If we don't have a model number in older 'show ver' extract it from the hardware column. if not ver_info['MODEL']: model = ver_info['HARDWARE'].split(',')[0] logger.debug("<M_SCRIPT> ASA device without model, using {0}".format(model)) ver_info['MODEL'] = model elif session.os == 'IOS': # Expand multiple serial numbers found in stacks, or just remove lists for serial and model if only 1 device logger.debug("<M_SCRIPT> IOS device, writing list of serials/models to separate entries") num_in_stack = len(ver_info['SERIAL']) if len(ver_info['MODEL']) != num_in_stack: # For older IOS, we may not get a model number, but we'll pick up the hardware. As long as this happens # when only 1 serial is detected (not a switch stack), then just use the HARDWARE for the model number. if len(ver_info['MODEL']) == 0 and num_in_stack == 1 and ver_info['HARDWARE']: ver_info['MODEL'] = [ver_info['HARDWARE']] else: logger.debug("<M_SCRIPT> List of Serials & Models aren't the same length. Likely TextFSM parsing problem.") raise sessions.InteractionError("Received {0} serial nums and only {1} model nums in output." .format(num_in_stack, len(ver_info['MODEL']))) new_output = [] for x in range(num_in_stack): stack_subset = dict((key, ver_info[key]) for key in interesting_keys) stack_subset['HOSTNAME'] = "{0}-{1}".format(ver_info['HOSTNAME'], x+1) stack_subset['SERIAL'] = ver_info['SERIAL'][x] stack_subset['MODEL'] = ver_info['MODEL'][x] new_output.append(stack_subset) logger.debug("Created an entry for {0}/{1}".format(stack_subset['MODEL'], stack_subset['SERIAL'])) fsm_output = new_output # Create output data structure with only the keys that we need. inv_data = [] logger.debug("Creating list of dictionaries to return, and adding manufacture dates.") for entry in fsm_output: subset = dict((key, entry[key]) for key in interesting_keys) subset['MANUFACTURE_DATE'] = get_manufacture_date(subset['SERIAL']) inv_data.append(subset) # End session on the Cisco device session.end_cisco_session() return inv_data
def update_helpers(session, check_mode, old_helpers, new_helpers, remove_old_helpers): script = session.script # A list of supported OSes that this script is configured to handle. supported_os = ["IOS", "NXOS"] # Create data structure to record helper IPs that we find that aren't in our list that we are either looking for # or adding. unrecognized_helpers = [["Hostname", "Interface", "Helper IP"]] if session.os not in supported_os: logger.debug( "<UPDATE_HELPER> OS is {0}, which is not in supported OS list of {1}" .format(session.os, supported_os)) raise sessions.UnsupportedOSError( "This device's OS is {0}, which is not a supported OS for this script which " "only supports: {1}).".format(session.os, supported_os)) # Save our "Before" configuration. before_filename = session.create_output_filename("1-show-run-BEFORE") session.write_output_to_file("show run", before_filename) # Open the "Before" configuration and parse it with TextFSM to find helper-addresses with open(before_filename, 'r') as config_file: run_config = config_file.read() if session.os == "IOS": template_file = script.get_template( "cisco_ios_show_run_helper.template") else: template_file = script.get_template( "cisco_nxos_show_run_dhcp_relay.template") result = utilities.textfsm_parse_to_list(run_config, template_file) if check_mode: os.remove(before_filename) # Create a dictionary that will let us get a set of configured helpers under each interface. intfs_with_helpers = {} for entry in result: interface = entry[0] helper = entry[1] vrf = entry[2] if interface in intfs_with_helpers: intfs_with_helpers[interface]["helpers"].add(helper) else: intfs_with_helpers[interface] = { "vrf": "{}".format(vrf), "helpers": {helper} } # Check if helper is unrecognized and needs to be recorded if helper not in old_helpers and helper not in new_helpers: unknown_line = [session.hostname, interface, helper, vrf] unrecognized_helpers.append(unknown_line) logger.debug("<UPDATE_HELPER> Adding {} to unknown helpers".format( str(unknown_line))) logger.debug("<UPDATE_HELPER> Interfaces with helpers:\n{}".format( str(intfs_with_helpers))) # Figure out which interfaces need additional helpers need_to_update = [] for interface in intfs_with_helpers: configured_helpers = intfs_with_helpers[interface]["helpers"] vrf = intfs_with_helpers[interface]["vrf"] helper_matches = configured_helpers.intersection(old_helpers) if helper_matches: needed_new_helpers = set(new_helpers).difference( configured_helpers) if remove_old_helpers: need_to_update.append( (interface, vrf, needed_new_helpers, helper_matches)) else: need_to_update.append((interface, vrf, needed_new_helpers, {})) logger.debug("<UPDATE_HELPER> Required Updates:\n{}".format( str(need_to_update))) # If we have anything we need to update, build out required config commands, depending on device OS. update_commands = [] if session.os == "IOS": for entry in need_to_update: interface = entry[0] vrf = entry[1] helpers_to_add = entry[2] helpers_to_remove = entry[3] if helpers_to_add or helpers_to_remove: update_commands.append("interface {}".format(interface)) for helper in helpers_to_add: if vrf == "": update_commands.append( "ip helper-address {}".format(helper)) elif vrf == "global": update_commands.append( "ip helper-address global {}".format(helper)) else: update_commands.append( "ip helper-address vrf {} {}".format(vrf, helper)) for helper in helpers_to_remove: if vrf == "": update_commands.append( "no ip helper-address {}".format(helper)) elif vrf == "global": update_commands.append( "no ip helper-address global {}".format(helper)) else: update_commands.append( "no ip helper-address vrf {} {}".format( vrf, helper)) else: for entry in need_to_update: interface = entry[0] vrf = entry[1] helpers_to_add = entry[2] helpers_to_remove = entry[3] if helpers_to_add or helpers_to_remove: update_commands.append("interface {}".format(interface)) for helper in helpers_to_add: if vrf == "": update_commands.append( "ip dhcp relay address {}".format(helper)) else: update_commands.append( "ip dhcp relay address {} use-vrf {}".format( helper, vrf)) for helper in helpers_to_remove: if vrf == "": update_commands.append( "no ip dhcp relay address {}".format(helper)) else: update_commands.append( "no ip dhcp relay address {} use-vrf {}".format( helper, vrf)) # Send config commands to the device and save the session. if update_commands: if check_mode: # If in Check Mode, only generate config updates and write to a file. logger.debug("<UPDATE_HELPER> CHECK MODE: Generating config") command_string = "" command_string += "configure terminal\n" for command in update_commands: command_string += "{}\n".format(command.strip()) command_string += "end\n" config_filename = session.create_output_filename("PROPOSED_CONFIG") with open(config_filename, 'w') as output_file: output_file.write(command_string) else: config_filename = session.create_output_filename( "2-CONFIG_RESULTS") session.send_config_commands(update_commands, output_filename=config_filename) session.save() # Save our "After" configuration. after_filename = session.create_output_filename("3-show-run-AFTER") session.write_output_to_file("show run", after_filename)