def generate(name): ''' Summary: Generates a configuration file for a specified device. Takes: name: Device to generate configuration for ''' Device.get_devices_from_file() if os.path.isfile(METADATA_PATH + name + '_metadata.yaml'): with open(METADATA_PATH + name + '_metadata.yaml', 'r') as metadata: device_metadata = (yaml.safe_load(metadata)) else: log('[{}] Metadata does not exist'.format(name), 'info') sys.exit(1) device_os = Device.get_device_information(name)['os'] environment = MODULE_PATH + device_os + '/' j2_env = Environment(loader=FileSystemLoader(environment), trim_blocks=True, lstrip_blocks=True) for device, data in device_metadata.items(): configuration = j2_env.get_template('template.j2').render(data) with open(CONFIG_PATH + name + '_generated.txt', 'w') as generated_config_file: generated_config_file.write(configuration) log('[{}] Generated configuration'.format(name), 'info')
def check_configuration_line(device, device_connection, os, configuration_line): ''' Summary: Checks if configuration line has been pushed to device. Takes: device: Device name device_connection: Device connection object os: Operating system of device configuration_line: Configuration line to check for ''' with open(MODULE_PATH + os + '/commands.json') as command_file_temp: command_file = json.load(command_file_temp) configuration = device_connection.send_command( command_file['commands']['config']) if configuration_line in configuration: log( "[{}] Configuration check passed for '{}'".format( device, configuration_line), 'info') return False else: log( "[{}] Configuration check failed for '{}', rolling back". format(device, configuration_line), 'info') Configuration.rollback_configuration(device, os, device_connection) return True
def delete_rollback_configuration(device): ''' Summary: Delete rollback configuration once deployment succeeds.. Takes: device: Device name ''' os.remove(CONFIG_PATH + device + '_rollback.txt') log("[{}] Removed rollback file".format(device), 'info')
def close_connection(self, connection): ''' Summary: Closes an SSH connection to a device. Takes: connection: Connection object to close ''' connection.disconnect() log('[{}] Disconnected'.format(self.name), 'info')
def rollback_configuration(device, os, device_connection): ''' Summary: Performs a rollback of device configuration. Takes: device: Device name device_connection: Device connection object ''' try: with open(CONFIG_PATH + device + '_custom_commands.txt') as custom_commands_from_file: command_list_temp = custom_commands_from_file.read( ).splitlines() if os == 'cisco_ios': command_list = [ 'no ' + command for command in command_list_temp ] elif os == 'vyos': command_list = [ command.replace('set', 'delete') for command in command_list_temp ] device_connection.send_config_set(command_list) if os == 'vyos': device_connection.send_config_set(['commit', 'save']) elif os == 'cisco_ios': output = device_connection.send_command_timing( 'copy run start') if 'Destination filename' in output: device_connection.send_command_timing("\n", strip_prompt=False, strip_command=False) if 'Overwrite the previous' in output: device_connection.send_command_timing("\n", strip_prompt=False, strip_command=False) if 'Warning: Attempting to overwrite an NVRAM configuration previously written' in output: device_connection.send_command_timing("\n", strip_prompt=False, strip_command=False) log('[{}] Device configuration rolled back'.format(device), 'info') except AttributeError: log( '[{}] Could not send commands, device unreachable'.format( device), 'error')
def save_deployments_to_file(): ''' Summary: Saves deployment data to an encrypted file. ''' deployments_to_save = [] for deployment in deployments: for deployment_name, deployment_object in deployment.items(): deployments_to_save.append(deployment_object.all()) Crypt.create_encrypted_file('deployments', deployments_to_save) log('Saved deployments to file', 'info')
def save_devices_to_file(): ''' Summary: Saves device data to an encrypted file. ''' devices_to_save = [] for device in devices: for device_name, device_object in device.items(): devices_to_save.append(device_object.all()) Crypt.create_encrypted_file('devices', devices_to_save) log('Saved devices to file', 'info')
def get_devices_from_file(): ''' Summary: Gets device data from an encrypted file and creates an object stored in the devices list. ''' if os.path.isfile(DB_PATH + 'devices'): devices_temp_file = Crypt.get_encrypted_file_contents('devices') for device in devices_temp_file: device_object = Device(device['name'], device['ip'], device['username'], device['password'], device['os']) devices.append({device['name']: device_object}) log('Got devices from file', 'info') else: log('No devices to get from file', 'info')
def run(name): ''' Summary: Calls the run method on a Deployment object. Takes: name: Name of deployment to run ''' Device.get_devices_from_file() Deployment.get_deployments_from_file() for deployment in deployments: if name in deployment: deployment[name].run() Deployment.save_deployments_to_file() return log('[{}] Deployment does not exist'.format(name), 'error')
def remove(name): ''' Summary: Removes a device. Takes: name: Name of device to remove ''' Device.get_devices_from_file() for device in devices: if name in device: devices.remove(device) log('[{}] Device removed successfully'.format(name), 'info') Device.save_devices_to_file() return log('[{}] Device does not exist'.format(name), 'error')
def remove(name): ''' Summary: Removes a deployment. Takes: name: Name of deployment to remove ''' Deployment.get_deployments_from_file() for deployment in deployments: if name in deployment: deployments.remove(deployment) log('[{}] Deployment removed successfully'.format(name), 'info') Deployment.save_deployments_to_file() return log('[{}] Deployment does not exist'.format(name), 'error')
def deploy_custom_configuration(device): ''' Summary: Deploys custom configuration from a file to a device. Takes: device: Device to deploy configuration to ''' rolled_back = False device_information = Device.get_device_information(device) connection_object = Connection(device_information['name'], device_information['ip'], device_information['username'], device_information['password'], device_information['os']) device_connection = connection_object.get_connection() try: Configuration.snapshot_configuration(device, device_connection, device_information['os']) with open(CONFIG_PATH + device + '_custom_commands.txt') as custom_commands_from_file: command_list = custom_commands_from_file.read().splitlines() log('[{}] Pushing configuration...'.format(device), 'info') device_connection.send_config_set(command_list) Configuration.save_configuration(device_information['os'], device_connection) for command in command_list: if command != 'no shutdown' and rolled_back == False: rolled_back = Configuration.check_configuration_line( device, device_connection, device_information['os'], command) connection_object.close_connection(device_connection) Configuration.delete_rollback_configuration(device) except AttributeError: log('[{}] Could not send commands'.format(device), 'error')
def decrypt(filename): ''' Summary: Create a decrypted copy of a file in JSON format. Takes: filename: Name of decrypted file ''' key = Crypt.get_key() with open(DB_PATH + filename, 'rb') as encrypted_file: data = encrypted_file.read() fernet = Fernet(key) decrypted_data = literal_eval(fernet.decrypt(data).decode()) with open(DB_PATH + filename + '.decrypted', 'w') as decrypted_file: json.dump(decrypted_data, decrypted_file, indent=4) log('Generated decrypted file {}.decrypted'.format(filename), 'info')
def mark_configuration_as_deployed(device): ''' Summary: Marks a generated configuration file as deployed. Takes: device: Name of device to mark configuration as deployed for ''' with open(CONFIG_PATH + device + '_generated.txt') as generated_configuration_file: deployed_configuration = generated_configuration_file.read() with open( CONFIG_PATH + device + '_deployed_' + datetime.now().strftime('%Y-%m-%d_%H:%M:%S') + '.txt', 'w') as deployed_configuration_file: deployed_configuration_file.write(deployed_configuration) log('[{}] Marked generated configuration as deployed'.format(device), 'info')
def get_deployments_from_file(): ''' Summary: Gets deployment data from an encrypted file and creates an object stored in the deployments list. ''' if os.path.isfile(DB_PATH + 'deployments'): deployments_temp_file = Crypt.get_encrypted_file_contents( 'deployments') for deployment in deployments_temp_file: deployment_object = Deployment( deployment['name'], deployment['targets'], deployment['action'], deployment_id=deployment['deployment_id'], status=deployment['status'], ) deployments.append({deployment['name']: deployment_object}) log('Got deployments from file', 'info') else: log('No deployments to get from file', 'info')
def add(name, targets, action): ''' Summary: Adds a deployment. Takes: name: Name of deployment targets: Devices to target with the deployment action: Action to perform get|deploy_generated|deploy_custom ''' Deployment.get_deployments_from_file() for deployment in deployments: if name in deployment: log('[{}] Deployment already exists'.format(name), 'info') sys.exit(1) deployment_object = Deployment(name, list(targets), action) deployments.append({name: deployment_object}) log( '[{}] Deployment added successfully with ID {}'.format( name, deployment_object.deployment_id), 'info') Deployment.save_deployments_to_file()
def add(name, ip, username, password, os): ''' Summary: Adds a device. Takes: name: Name of device ip: Management IP address of device username: Username to authenticate against password: Password to authenticate with os: Operating system of device cisco_ios|vyos ''' Device.get_devices_from_file() for device in devices: if name in device: log('[{}] Device already exists'.format(name), 'info') sys.exit(1) device_object = Device(name, ip, username, password, os) devices.append({name: device_object}) log('[{}] Device added successfully'.format(name), 'info') Device.save_devices_to_file()
def add_from_file(filename): Device.get_devices_from_file() file_path = DB_PATH + filename if os.path.isfile(file_path): log("Adding devices from '{}'".format(file_path), 'info') with open(file_path, 'r') as devices_file: all_lines = [line.strip() for line in devices_file.readlines()] for device_attribute in range(0, len(all_lines), 5): device_exists = False for device in devices: if all_lines[device_attribute] in device: log( '[{}] Device already exists'.format( all_lines[device_attribute]), 'info') device_exists = True if device_exists == False: device_object = Device(all_lines[device_attribute], all_lines[device_attribute + 1], all_lines[device_attribute + 2], all_lines[device_attribute + 3], all_lines[device_attribute + 4]) devices.append( {all_lines[device_attribute]: device_object}) log( '[{}] Device added successfully'.format( all_lines[device_attribute]), 'info') Device.save_devices_to_file()
def deploy_generated_configuration(device): ''' Summary: Deploys configuration generated from device metadata to a device. Takes: device: Device to deploy configuration to ''' device_information = Device.get_device_information(device) connection_object = Connection(device_information['name'], device_information['ip'], device_information['username'], device_information['password'], device_information['os']) device_connection = connection_object.get_connection() try: Configuration.snapshot_configuration(device, device_connection, device_information['os']) log('[{}] Pushing configuration...'.format(device), 'info') device_connection.send_config_from_file(CONFIG_PATH + device + '_generated.txt') Configuration.save_configuration(device_information['os'], device_connection) pushed_successfully = Configuration.check_full_configuration( device, device_connection, device_information['os']) if pushed_successfully == True: Configuration.mark_configuration_as_deployed(device) Configuration.delete_rollback_configuration(device) connection_object.close_connection(device_connection) except AttributeError: log( '[{}] Could not send commands, device unreachable'.format( device), 'error')
def snapshot_configuration(device, device_connection, os): ''' Summary: Takes a snapshot of device configuration for rollback configuration. Takes: device: Device name ''' try: with open(MODULE_PATH + os + '/commands.json') as command_list_from_file: command_list = json.load(command_list_from_file) log('[{}] Creating configuration snapshot...'.format(device), 'info') output = device_connection.send_command( command_list['commands']['config']) configuration_lines = output.splitlines() with open(CONFIG_PATH + device + '_rollback.txt', 'w+') as configuration_file: for line in configuration_lines: configuration_file.write(line + '\n') log( '[{}] Configuration snapshot stored as {}_rollback.txt in {}'. format(device, device, CONFIG_PATH), 'info') except AttributeError: log( '[{}] Could not send commands, device unreachable'.format( device), 'error')
def get_connection(self): ''' Summary: Tests device connectivity then creates an SSH connection if successful. Returns: Connection object ''' ping_result = ping(self.ip, count=1) if 'Request timed out' not in str(ping_result): log('[{}] Reachable, getting connection...'.format(self.name), 'info') connection = Netmiko( self.ip, username=self.username, password=self.password, device_type=self.os ) log('[{}] Connected'.format(self.name), 'info') return connection else: log('[{}] Not reachable'.format(self.name), 'info') return
def run(self): ''' Summary: Runs a deployment. ''' log('[{}] Running deployment'.format(self.name), 'info') self.status = 'In progress' log('[{}] Updated deployment status to In progress'.format(self.name), 'info') if self.action == 'get': device_processes = [ Process(target=Configuration.get_configuration, args=(target_device, )) for target_device in self.targets ] elif self.action == 'deploy_generated': device_processes = [ Process(target=Configuration.deploy_generated_configuration, args=(target_device, )) for target_device in self.targets ] elif self.action == 'deploy_custom': device_processes = [ Process(target=Configuration.deploy_custom_configuration, args=(target_device, )) for target_device in self.targets ] for _process in device_processes: _process.start() for _process in device_processes: _process.join() self.status = 'Completed' log('[{}] Updated deployment status to Completed'.format(self.name), 'info') log('[{}] Deployment completed'.format(self.name), 'info')
def stored(): ''' Summary: Lists all stored configuration files. Takes: none ''' configurations = os.listdir(CONFIG_PATH) if configurations: log('Stored configuration files:', 'info') for configuration_file in configurations: if configuration_file != '__init__.py': log(configuration_file, 'info') else: log('No configuration files stored', 'info')
def get_configuration(device): ''' Summary: Gets current configuration from a device and stores it in a file. Takes: device: Device to get configuration from ''' device_information = Device.get_device_information(device) connection_object = Connection(device_information['name'], device_information['ip'], device_information['username'], device_information['password'], device_information['os']) device_connection = connection_object.get_connection() try: with open(MODULE_PATH + device_information['os'] + '/commands.json') as command_list_from_file: command_list = json.load(command_list_from_file) log('[{}] Getting device configuration...'.format(device), 'info') output = device_connection.send_command( command_list['commands']['config']) configuration_lines = output.splitlines() with open(CONFIG_PATH + device + '_latest.txt', 'w+') as configuration_file: for line in configuration_lines: configuration_file.write(line + '\n') log( '[{}] Device configuration stored as {}_latest.txt in {}'. format(device, device, CONFIG_PATH), 'info') connection_object.close_connection(device_connection) except AttributeError: log( '[{}] Could not send commands, device unreachable'.format( device), 'error')
def diff(config1, config2): ''' Summary: Outputs the difference between two device configuration files by comparing them line by line. Takes: config1: First configuration file config2: Configuration file to compare with ''' config1_lines = open(CONFIG_PATH + config1).read().splitlines() config2_lines = open(CONFIG_PATH + config2).read().splitlines() diff = difflib.unified_diff(config1_lines, config2_lines) log('Diff for [{}] < > [{}]'.format(config1, config2), 'info') for line in diff: if line[0] == '+' and line[1] != '+': log('\033[0;32m{}\033[m'.format(line), 'info') elif line[0] == '-' and line[1] != '-': log('\033[0;31m{}\033[m'.format(line), 'info')
def generate_key(): ''' Summary: Generate a key used for symmetric encryption, using the hash of a user entered password for the salt. Returns: Encryption key Encryption type: 128-bit AES ''' if os.path.isfile(KEY_PATH): log( "Using existing key '{}' for encryption/decryption".format( KEY_PATH), 'info') key = Crypt.get_key() return key else: log( "Attempting to use '{}' but no key found, create a new key or add an existing one" .format(KEY_PATH), 'info') password = getpass(prompt='New encryption key password: '******'wb') as encryption_key: encryption_key.write(key) log("Stored encryption key as '{}'".format(KEY_PATH), 'info') return key
def check_full_configuration(device, device_connection, os): ''' Summary: Checks if full configuration has been pushed to device. Takes: device: Device name device_connection: Device connection object os: Operating system of device ''' full_configuration_pushed = True do_not_check = ['!', ' no shutdown'] with open(MODULE_PATH + os + '/commands.json') as command_file_temp: command_file = json.load(command_file_temp) device_configuration = device_connection.send_command( command_file['commands']['config']) with open(CONFIG_PATH + device + '_generated.txt') as pushed_configuration_temp: pushed_configuration = pushed_configuration_temp.read().splitlines( ) log('[{}] Checking configuration...'.format(device), 'info') for configuration_line in pushed_configuration: if configuration_line not in device_configuration and configuration_line not in do_not_check: full_configuration_pushed = False if full_configuration_pushed == True: log('[{}] Configuration check was successful'.format(device), 'info') return True else: log( '[{}] Configuration check failed, check configuration manually' .format(device), 'error') return False
def view(name): ''' Summary: Prints attributes of a Deployment instance. Takes: name: Name of deployment to view information about ''' Deployment.get_deployments_from_file() for deployment in deployments: if name in deployment: deployment_dict = deployment[name].all() log('Name: ' + str(deployment_dict['name']), 'info') log('Targets: ' + str(deployment_dict['targets']), 'info') log('Action: ' + str(deployment_dict['action']), 'info') log('ID: ' + str(deployment_dict['deployment_id']), 'info') log('Status: ' + str(deployment_dict['status']), 'info') return log('[{}] Deployment does not exist'.format(name), 'error')
def view(name): ''' Summary: Prints attributes of a Device instance. Takes: name: Name of device to view information about ''' Device.get_devices_from_file() device_information = Device.get_device_information(name) if device_information != None: log('Name: ' + str(device_information['name']), 'info') log('IP: ' + str(device_information['ip']), 'info') log('Username: '******'username']), 'info') log('Password: '******'password']), 'info') log('OS: ' + str(device_information['os']), 'info') else: log('[{}] Device does not exist'.format(name), 'error')