diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..25c945b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/* +dist/* +ndct.egg-info/* +*/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 3225d06..bab4209 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,93 @@ -# NDCT +# NDCT (Network Device Configuration Tool) +A configuration tool for efficient network device management. +## Features + +###### Generate network device configuration (Juniper Junos, Cisco IOS, Cisco Nexus, Cisco ASA) + - Multivendor device configuration generation from a single YAML file + +###### Deployments + - Push configuration + - Pull configuration + - Get device information + +###### Interface + - CLI + +###### File encryption + - AES 128-bit + +###### Full operation logging + +# Libraries +> + +# Installation +``` + $ cd NDCT + $ python3 setup.py install +``` + +# Commands +``` + $ ndct _____ + + configuration Configuration actions + crypt Crypt actions + deployment Deployment actions + device Device actions +``` +Add single device + +ndct device add -n DeviceName -i 192.168.1.1 -u username -p password -o cisco_ios|vyos +*** + +Add multiple devices from a file + +ndct device add-from-file -f example.txt +*** + +Remove device + +ndct device remove -n DeviceName +*** + +View device + +ndct device view -n DeviceName +*** + +Add deployment + +ndct deployment add -n DeploymentName -t Target1 Target2 -a get|deploy_generated|deploy_custom +*** + +Remove deployment + +ndct deployment remove -n DeploymentName +*** + +View deployment + +ndct deployment view -n DeploymentName +*** + +Run deployment + +ndct deployment run -n DeploymentName +*** + +List stored configuration files + +ndct configuration stored +*** + +Config diff + +ndct configuration diff -c1 R1_generated.txt -c2 R2_generated.txt +*** + +Generate config + +ndct configuration generate -n MyDevice +*** diff --git a/ndct/.___init__.py b/ndct/.___init__.py new file mode 100644 index 0000000..ee00ce5 Binary files /dev/null and b/ndct/.___init__.py differ diff --git a/ndct/._cli b/ndct/._cli new file mode 100755 index 0000000..ee2b6c2 Binary files /dev/null and b/ndct/._cli differ diff --git a/ndct/._core b/ndct/._core new file mode 100755 index 0000000..0d9b0f3 Binary files /dev/null and b/ndct/._core differ diff --git a/ndct/._modules b/ndct/._modules new file mode 100755 index 0000000..e1592e8 Binary files /dev/null and b/ndct/._modules differ diff --git a/ndct/__init__.py b/ndct/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/cli/.___init__.py b/ndct/cli/.___init__.py new file mode 100644 index 0000000..ff583ff Binary files /dev/null and b/ndct/cli/.___init__.py differ diff --git a/ndct/cli/._configuration.py b/ndct/cli/._configuration.py new file mode 100644 index 0000000..93a51fa Binary files /dev/null and b/ndct/cli/._configuration.py differ diff --git a/ndct/cli/._crypt.py b/ndct/cli/._crypt.py new file mode 100644 index 0000000..386ee7b Binary files /dev/null and b/ndct/cli/._crypt.py differ diff --git a/ndct/cli/._deployment.py b/ndct/cli/._deployment.py new file mode 100644 index 0000000..9c494d8 Binary files /dev/null and b/ndct/cli/._deployment.py differ diff --git a/ndct/cli/._device.py b/ndct/cli/._device.py new file mode 100644 index 0000000..b38e131 Binary files /dev/null and b/ndct/cli/._device.py differ diff --git a/ndct/cli/._main.py b/ndct/cli/._main.py new file mode 100644 index 0000000..f8f3366 Binary files /dev/null and b/ndct/cli/._main.py differ diff --git a/ndct/cli/__init__.py b/ndct/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/cli/configuration.py b/ndct/cli/configuration.py new file mode 100644 index 0000000..d55c2f1 --- /dev/null +++ b/ndct/cli/configuration.py @@ -0,0 +1,93 @@ +import click +import os +import difflib +import yaml +import sys + +from jinja2 import Environment, FileSystemLoader +from ndct.core.device import Device +from ndct.core.log import log +from ndct.core.paths import METADATA_PATH, MODULE_PATH, CONFIG_PATH + +@click.command(short_help = 'Generate a configuration') +@click.option('-n', '--name', help='Name', required = True) +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') + +@click.command(short_help = 'List configuration files') +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') + +@click.command(short_help = 'Show the difference between two configuration files') +@click.option('-c1', '--config1', help='Configuration file 1', required = True) +@click.option('-c2', '--config2', help='Configuration file 2', required = True) +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') + +@click.group(short_help = 'Configuration commands') +def configuration(): + pass + +configuration.add_command(generate) +configuration.add_command(stored) +configuration.add_command(diff) \ No newline at end of file diff --git a/ndct/cli/crypt.py b/ndct/cli/crypt.py new file mode 100644 index 0000000..3817bf7 --- /dev/null +++ b/ndct/cli/crypt.py @@ -0,0 +1,37 @@ +import click +import json + +from ast import literal_eval +from cryptography.fernet import Fernet +from ndct.core.crypt import Crypt +from ndct.core.log import log +from ndct.core.paths import DB_PATH + +@click.command(short_help = 'Decrypt a file') +@click.option('-f', '--filename', type = click.Choice(['devices', 'deployments']), help = 'Filename', required = True) +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') + +@click.group(short_help = 'Crypt commands') +def crypt(): + pass + +crypt.add_command(decrypt) \ No newline at end of file diff --git a/ndct/cli/deployment.py b/ndct/cli/deployment.py new file mode 100644 index 0000000..6bda315 --- /dev/null +++ b/ndct/cli/deployment.py @@ -0,0 +1,102 @@ +import click +import sys + +from ndct.core.deployment import Deployment, deployments +from ndct.core.device import Device, devices +from ndct.core.log import log + +@click.command(short_help = 'Add a deployment') +@click.option('-n', '--name', help = 'Name', required = True) +@click.option('-t', '--targets', nargs = 0, help = 'Devices to deploy to', required = True) +@click.option('-a', '--action', type = click.Choice(['get', 'deploy_generated', 'deploy_custom']), help = 'Deployment action', required = True) +@click.argument('targets', nargs = -1) +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() + +@click.command(short_help = 'Remove a deployment') +@click.option('-n', '--name', help = 'Name', required = True) +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') + +@click.command(short_help = 'View a deployment') +@click.option('-n', '--name', help = 'Name', required = True) +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') + +@click.command(short_help = 'Run a deployment') +@click.option('-n', '--name', help = 'Name', required = True) +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') + +@click.group(short_help = 'Deployment commands') +def deployment(): + pass + +deployment.add_command(add) +deployment.add_command(remove) +deployment.add_command(view) +deployment.add_command(run) \ No newline at end of file diff --git a/ndct/cli/device.py b/ndct/cli/device.py new file mode 100644 index 0000000..f74e15c --- /dev/null +++ b/ndct/cli/device.py @@ -0,0 +1,106 @@ +import click +import sys +import os + +from ndct.core.device import Device, devices +from ndct.core.log import log +from ndct.core.paths import DB_PATH + +@click.command(short_help = 'Add a device') +@click.option('-n', '--name', help = 'Name', required = True) +@click.option('-i', '--ip', help = 'IP address', required = True) +@click.option('-u', '--username', help = 'Username to authenticate against', required = True) +@click.option('-p', '--password', help = 'Password to authenticate with', required = True) +@click.option('-o', '--os', type = click.Choice(['cisco_ios', 'vyos']), help = 'Operating system', required = True) +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() + +@click.command(short_help = 'Add devices from a file') +@click.option('-f', '--filename', help = 'File to add devices from', required = True) +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() + +@click.command(short_help = 'Remove a device') +@click.option('-n', '--name', help = 'Name', required = True) +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') + +@click.command(short_help = 'View a device') +@click.option('-n', '--name', help = 'Name', required = True) +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: ' + str(device_information['username']), 'info') + log('Password: ' + str(device_information['password']), 'info') + log('OS: ' + str(device_information['os']), 'info') + else: + log('[{}] Device does not exist'.format(name), 'error') + +@click.group(short_help = 'Device commands') +def device(): + pass + +device.add_command(add) +device.add_command(add_from_file) +device.add_command(remove) +device.add_command(view) \ No newline at end of file diff --git a/ndct/cli/main.py b/ndct/cli/main.py new file mode 100644 index 0000000..8b36be7 --- /dev/null +++ b/ndct/cli/main.py @@ -0,0 +1,20 @@ +import click + +from ndct.cli import crypt +from ndct.cli import device +from ndct.cli import deployment +from ndct.cli import configuration +from ndct.core.banner import banner + +@click.group() +def main(): + ''' + Summary: + Network Device Configuration Tool. + ''' + banner() + +main.add_command(crypt.crypt, name = 'crypt') +main.add_command(device.device, name = 'device') +main.add_command(deployment.deployment, name = 'deployment') +main.add_command(configuration.configuration, name = 'configuration') \ No newline at end of file diff --git a/ndct/core/.___init__.py b/ndct/core/.___init__.py new file mode 100644 index 0000000..cf50bb7 Binary files /dev/null and b/ndct/core/.___init__.py differ diff --git a/ndct/core/._banner.py b/ndct/core/._banner.py new file mode 100644 index 0000000..d9ce4c8 Binary files /dev/null and b/ndct/core/._banner.py differ diff --git a/ndct/core/._configuration.py b/ndct/core/._configuration.py new file mode 100644 index 0000000..c059868 Binary files /dev/null and b/ndct/core/._configuration.py differ diff --git a/ndct/core/._configuration_files b/ndct/core/._configuration_files new file mode 100755 index 0000000..d94909a Binary files /dev/null and b/ndct/core/._configuration_files differ diff --git a/ndct/core/._connection.py b/ndct/core/._connection.py new file mode 100644 index 0000000..1449296 Binary files /dev/null and b/ndct/core/._connection.py differ diff --git a/ndct/core/._crypt.py b/ndct/core/._crypt.py new file mode 100644 index 0000000..6f8d87d Binary files /dev/null and b/ndct/core/._crypt.py differ diff --git a/ndct/core/._db b/ndct/core/._db new file mode 100755 index 0000000..d848bb9 Binary files /dev/null and b/ndct/core/._db differ diff --git a/ndct/core/._deployment.py b/ndct/core/._deployment.py new file mode 100644 index 0000000..ae4e386 Binary files /dev/null and b/ndct/core/._deployment.py differ diff --git a/ndct/core/._device.py b/ndct/core/._device.py new file mode 100644 index 0000000..da9deba Binary files /dev/null and b/ndct/core/._device.py differ diff --git a/ndct/core/._device_metadata b/ndct/core/._device_metadata new file mode 100755 index 0000000..6079919 Binary files /dev/null and b/ndct/core/._device_metadata differ diff --git a/ndct/core/._log.py b/ndct/core/._log.py new file mode 100644 index 0000000..1bc77bf Binary files /dev/null and b/ndct/core/._log.py differ diff --git a/ndct/core/._logs b/ndct/core/._logs new file mode 100755 index 0000000..dfef0c9 Binary files /dev/null and b/ndct/core/._logs differ diff --git a/ndct/core/._paths.py b/ndct/core/._paths.py new file mode 100644 index 0000000..9b915e1 Binary files /dev/null and b/ndct/core/._paths.py differ diff --git a/ndct/core/__init__.py b/ndct/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/core/banner.py b/ndct/core/banner.py new file mode 100644 index 0000000..aeac141 --- /dev/null +++ b/ndct/core/banner.py @@ -0,0 +1,12 @@ +def banner(): + print(''' + Created by: github.com/m4cfarlane + _ _ ____ ____ _____ + | \ | | _ \ / ___|_ _| + | \| | | | | | | | + | |\ | |_| | |___ | | + |_| \_|____/ \____| |_| + + Network Device Configuration Tool + A configuration tool for efficient network device management. + ''') \ No newline at end of file diff --git a/ndct/core/configuration.py b/ndct/core/configuration.py new file mode 100644 index 0000000..ccded29 --- /dev/null +++ b/ndct/core/configuration.py @@ -0,0 +1,328 @@ +import json +import os + +from datetime import datetime +from ndct.core.connection import Connection +from ndct.core.device import Device +from ndct.core.log import log +from ndct.core.paths import MODULE_PATH, CONFIG_PATH + +class Configuration: + @staticmethod + 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') + + @staticmethod + 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') + + @staticmethod + 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') + + @staticmethod + 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 + + @staticmethod + 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 + + @staticmethod + def save_configuration(os, device_connection): + ''' + Summary: + Saves device configuration persistently. + + Takes: + os: Operating system of device + device_connection: device_connection: Device connection object + ''' + with open(MODULE_PATH + os + '/commands.json') as command_file_temp: + command_file = json.load(command_file_temp) + + save_command = command_file['commands']['save_config'] + + if os == 'vyos': + device_connection.send_config_set(save_command) + elif os == 'cisco_ios': + output = device_connection.send_command_timing(save_command) + 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 + ) + + @staticmethod + 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') + + @staticmethod + 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') + + @staticmethod + 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') + + @staticmethod + 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') diff --git a/ndct/core/configuration_files/._R1_custom_commands.txt b/ndct/core/configuration_files/._R1_custom_commands.txt new file mode 100644 index 0000000..889328e Binary files /dev/null and b/ndct/core/configuration_files/._R1_custom_commands.txt differ diff --git a/ndct/core/configuration_files/._R1_generated.txt b/ndct/core/configuration_files/._R1_generated.txt new file mode 100644 index 0000000..dabcbe3 Binary files /dev/null and b/ndct/core/configuration_files/._R1_generated.txt differ diff --git a/ndct/core/configuration_files/._R2_custom_commands.txt b/ndct/core/configuration_files/._R2_custom_commands.txt new file mode 100644 index 0000000..176720b Binary files /dev/null and b/ndct/core/configuration_files/._R2_custom_commands.txt differ diff --git a/ndct/core/configuration_files/._R2_generated.txt b/ndct/core/configuration_files/._R2_generated.txt new file mode 100644 index 0000000..83cc476 Binary files /dev/null and b/ndct/core/configuration_files/._R2_generated.txt differ diff --git a/ndct/core/configuration_files/.___init__.py b/ndct/core/configuration_files/.___init__.py new file mode 100644 index 0000000..210723c Binary files /dev/null and b/ndct/core/configuration_files/.___init__.py differ diff --git a/ndct/core/configuration_files/R1_custom_commands.txt b/ndct/core/configuration_files/R1_custom_commands.txt new file mode 100644 index 0000000..1b11dbf --- /dev/null +++ b/ndct/core/configuration_files/R1_custom_commands.txt @@ -0,0 +1,2 @@ +router ospf 1 +network 11.11.11.11 0.0.0.0 area 0 \ No newline at end of file diff --git a/ndct/core/configuration_files/R1_generated.txt b/ndct/core/configuration_files/R1_generated.txt new file mode 100644 index 0000000..43affe8 --- /dev/null +++ b/ndct/core/configuration_files/R1_generated.txt @@ -0,0 +1,14 @@ +hostname R1 +! +interface Loopback0 + description Loopback + ip address 11.11.11.11 255.255.255.255 + no shutdown +! +router bgp 65001 + neighbor 192.168.21.202 remote-as 65002 + network 11.11.11.11 mask 255.255.255.255 +! +router ospf 1 + network 192.168.21.0 0.0.0.255 area 0 +! \ No newline at end of file diff --git a/ndct/core/configuration_files/R2_custom_commands.txt b/ndct/core/configuration_files/R2_custom_commands.txt new file mode 100644 index 0000000..aed19be --- /dev/null +++ b/ndct/core/configuration_files/R2_custom_commands.txt @@ -0,0 +1 @@ +set protocols ospf area 0 network '12.12.12.12/32' \ No newline at end of file diff --git a/ndct/core/configuration_files/R2_generated.txt b/ndct/core/configuration_files/R2_generated.txt new file mode 100644 index 0000000..5937cd2 --- /dev/null +++ b/ndct/core/configuration_files/R2_generated.txt @@ -0,0 +1,6 @@ +set system host-name 'R2' +set interfaces loopback lo address '12.12.12.12/32' +set interfaces loopback lo description 'Loopback' +set protocols bgp 65002 neighbor 192.168.21.201 remote-as '65001' +set protocols bgp 65002 network '12.12.12.12/32' +set protocols ospf area 0 network '192.168.21.0/24' diff --git a/ndct/core/configuration_files/__init__.py b/ndct/core/configuration_files/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/core/connection.py b/ndct/core/connection.py new file mode 100644 index 0000000..cb534a6 --- /dev/null +++ b/ndct/core/connection.py @@ -0,0 +1,53 @@ +from netmiko import Netmiko +from pythonping import ping +from ndct.core.device import Device +from ndct.core.log import log + +class Connection(Device): + def __init__(self, name, ip, username, password, os): + ''' + Takes: + ip: IP address of the device + username: Username to authentication against + password: Password to authenticate with + os: Operating system of the device + ''' + super().__init__(name, ip, username, password, os) + + 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 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') \ No newline at end of file diff --git a/ndct/core/crypt.py b/ndct/core/crypt.py new file mode 100644 index 0000000..552494c --- /dev/null +++ b/ndct/core/crypt.py @@ -0,0 +1,111 @@ +import base64 +import os +import hashlib +import json + +from ast import literal_eval +from getpass import getpass +from cryptography.fernet import Fernet +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from ndct.core.paths import KEY_PATH, DB_PATH +from ndct.core.log import log + +class Crypt: + @staticmethod + 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: ') + password_bytes = password.encode() + salt = os.urandom(16) + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=100000, + backend=default_backend() + ) + key = base64.urlsafe_b64encode(kdf.derive(password_bytes)) + + with open(KEY_PATH, 'wb') as encryption_key: + encryption_key.write(key) + + log("Stored encryption key as '{}'".format(KEY_PATH), 'info') + + return key + + @staticmethod + def get_key(): + ''' + Summary: + Get stored encryption key. + + Returns: + Encryption key + + Encryption type: + 128-bit AES + ''' + with open(KEY_PATH, 'rb') as encryption_key: + key = encryption_key.read() + + return key + + @staticmethod + def create_encrypted_file(filename, data): + ''' + Summary: + Create an encrypted file from data that is passed to the function. + + Takes: + filename: Name of encrypted file + data: Data to encrypt + ''' + key = Crypt.generate_key() + + fernet = Fernet(key) + encrypted_data = fernet.encrypt(str(data).encode()) + + with open(DB_PATH + filename, 'wb') as encrypted_file: + encrypted_file.write(encrypted_data) + + @staticmethod + def get_encrypted_file_contents(filename): + ''' + Summary: + Get the contents of an encrypted file to enable use of the stored data. + + Takes: + filename: Name of file to get decrypted contents of + + Returns: + Decrypted file contents + ''' + key = Crypt.generate_key() + + with open(DB_PATH + filename, 'rb') as encrypted_file: + data = encrypted_file.read() + + fernet = Fernet(key) + temp_data = fernet.decrypt(data).decode() + temp_data_list = literal_eval(temp_data) + + return temp_data_list \ No newline at end of file diff --git a/ndct/core/db/.___init__.py b/ndct/core/db/.___init__.py new file mode 100644 index 0000000..708f2ef Binary files /dev/null and b/ndct/core/db/.___init__.py differ diff --git a/ndct/core/db/._deployments b/ndct/core/db/._deployments new file mode 100644 index 0000000..74ab357 Binary files /dev/null and b/ndct/core/db/._deployments differ diff --git a/ndct/core/db/._devices b/ndct/core/db/._devices new file mode 100644 index 0000000..0fc0775 Binary files /dev/null and b/ndct/core/db/._devices differ diff --git a/ndct/core/db/._example_devices_file.txt b/ndct/core/db/._example_devices_file.txt new file mode 100644 index 0000000..349f92c Binary files /dev/null and b/ndct/core/db/._example_devices_file.txt differ diff --git a/ndct/core/db/._key.key b/ndct/core/db/._key.key new file mode 100644 index 0000000..b38a595 Binary files /dev/null and b/ndct/core/db/._key.key differ diff --git a/ndct/core/db/__init__.py b/ndct/core/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/core/db/deployments b/ndct/core/db/deployments new file mode 100644 index 0000000..ff85032 --- /dev/null +++ b/ndct/core/db/deployments @@ -0,0 +1 @@ +gAAAAABevSjq3Y7_nWkDlUWzLjMnKBkn29IiaA-GKTCL-dCdgSm0klcLCWLlYN_lpVTsrIY9QHQpGjZGCWGyMC3YFRDcZC4xxQ8nWgOU26xH5ypa2wBZrqGcCNpktcxRUNDl-NXOy9FOrYVzJFYMNMd_krmAya9NJcKX3AgKlBtKVZQGYpTYDOx4tBJ3roCOKRJEM_ZP5tPCvbI9FdBj4XjoeW1Rzz5i_wIY53k8aKhL8I6FDZXVFtg9s_5ph8I4St43kXVUuViGsbc2J_6R0Nw_xgs0vCMpxFZXgxxP_SAtG-Ux-ApVPjL6MibNbn-qgfzBqf3FaRp8o1N7-hT0FgD2Xv3dreXIbiYKKi8FFjdYtMkP0pB0ZUpES131QfSFoic5X4cQ7Rdi6ml6avVgjitsePLv-eBxDULrT0UrHPEyD_NjPugRNvYWlzaW0NBsyYkveXxsZuZdsQ9mlTcrnNcehCMAnzTvi7hnANhYU2gmraPskCmoe8CsmFNZC82S-jkLfHVo4CAsnnRmcGkJ_VaZWr659Uk9Txj0gBkBP5QVMTnJ1gFjzHjytKUNRVELk4rn4d0NixM3fRCxAGXIxiqnUKlsi-FFabOjrbOaWSmJIbDt64wLkSgGotfvz5j2ilTMArEzWO3xEGh9E4rbWg-b1cczdx2_IW5eh38NIpNImWAGmkhMHunGYGW6VoYRZpln-3X3Okt5eNOW4f1EeG0LroVzy71LgA== \ No newline at end of file diff --git a/ndct/core/db/devices b/ndct/core/db/devices new file mode 100644 index 0000000..52846ac --- /dev/null +++ b/ndct/core/db/devices @@ -0,0 +1 @@ +gAAAAABevR-IVmPaNA1MBwD9ukWIWMbf2axg4PgO0AxYxZAl35eaWEiU2Er8Y1Hk__cyc7nZ8l0KQHWy-iJRNuMs2b93_yfmN54cv0NZk9HwpOaWmGv4Ejp2iQLjXvcq_NrX8djlDkTyu73S98yzEjJ9HJR8o2fMbdotW37JCURQyBpn_IzXdfCuN6IXxGClTVK0TBvhVEESYrHOapUKB_qchya42jwTNuam7RgVOl8IkyrJqKsGICKtvNmCxLmm1yik_wO1QLwHUrBPuP1r5jtiFIfY090BobOuERky-Za7-ICkFt8x-YUJAGeimP_ynMNAXRh6l15FVemyHKxyju8tgawtcngAiw== \ No newline at end of file diff --git a/ndct/core/db/example_devices_file.txt b/ndct/core/db/example_devices_file.txt new file mode 100644 index 0000000..f637375 --- /dev/null +++ b/ndct/core/db/example_devices_file.txt @@ -0,0 +1,15 @@ +R7 +10.1.1.1 +admin +password +cisco_ios +R8 +10.2.2.2 +admin +password +cisco_ios +R9 +10.3.3.3 +admin +password +vyos \ No newline at end of file diff --git a/ndct/core/db/key.key b/ndct/core/db/key.key new file mode 100644 index 0000000..c6a0bc9 --- /dev/null +++ b/ndct/core/db/key.key @@ -0,0 +1 @@ +ENR0-Y8N5ZH7ho6DEAI9xRe17jwVAXsuekLOEfED6Ic= \ No newline at end of file diff --git a/ndct/core/deployment.py b/ndct/core/deployment.py new file mode 100644 index 0000000..2ca9e9e --- /dev/null +++ b/ndct/core/deployment.py @@ -0,0 +1,103 @@ +import uuid +import os + +from multiprocessing import Process +from ndct.core.configuration import Configuration +from ndct.core.crypt import Crypt +from ndct.core.log import log +from ndct.core.paths import DB_PATH + +deployments = [] + +class Deployment: + def __init__(self, name, targets, action, deployment_id=str(uuid.uuid4()), status='Not started'): + ''' + Takes: + name: Deployment name + targets: Target devices + action: Action of deployment + deployment_id: Unique identifier of deployment + status: Deployment status + attribute: Deployment attribute - for 'get' action deployments + ''' + self.name = name + self.targets = targets + self.action = action + self.deployment_id = deployment_id + self.status = status + + def all(self): + ''' + Summary: + Gets the contents of a Deployment instance. + + Returns: + Deployment instance contents in dictionairy form + ''' + return self.__dict__ + + 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') + + @staticmethod + 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') + + @staticmethod + 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') diff --git a/ndct/core/device.py b/ndct/core/device.py new file mode 100644 index 0000000..dcda296 --- /dev/null +++ b/ndct/core/device.py @@ -0,0 +1,86 @@ +import os + +from ndct.core.crypt import Crypt +from ndct.core.log import log +from ndct.core.paths import DB_PATH + +devices = [] + +class Device(): + def __init__(self, name, ip, username, password, os): + ''' + Takes: + name: Device name + ip: IP address of the device + user: Username to use for device connection + password: Password to use for device connection authentication + os: Operating system of the device + ''' + self.name = name + self.ip = ip + self.username = username + self.password = password + self.os = os + + def all(self): + ''' + Summary: + Gets the contents of a Device instance. + + Returns: + Device instance contents in dictionairy form + ''' + return self.__dict__ + + @staticmethod + 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') + + @staticmethod + 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') + + @staticmethod + def get_device_information(device_name): + ''' + Summary: + Gets the contents of a Device instance. + + Returns: + Device instance contents in dictionairy form. + ''' + for device in devices: + if device_name in device: + device_information = device[device_name].all() + return device_information + + return None diff --git a/ndct/core/device_metadata/._R1_metadata.yaml b/ndct/core/device_metadata/._R1_metadata.yaml new file mode 100644 index 0000000..dfe39fd Binary files /dev/null and b/ndct/core/device_metadata/._R1_metadata.yaml differ diff --git a/ndct/core/device_metadata/._R2_metadata.yaml b/ndct/core/device_metadata/._R2_metadata.yaml new file mode 100644 index 0000000..d73ae3c Binary files /dev/null and b/ndct/core/device_metadata/._R2_metadata.yaml differ diff --git a/ndct/core/device_metadata/.___init__.py b/ndct/core/device_metadata/.___init__.py new file mode 100644 index 0000000..d890055 Binary files /dev/null and b/ndct/core/device_metadata/.___init__.py differ diff --git a/ndct/core/device_metadata/._cisco_ios_example.yaml b/ndct/core/device_metadata/._cisco_ios_example.yaml new file mode 100644 index 0000000..8e5b88b Binary files /dev/null and b/ndct/core/device_metadata/._cisco_ios_example.yaml differ diff --git a/ndct/core/device_metadata/._vyos_example.yaml b/ndct/core/device_metadata/._vyos_example.yaml new file mode 100644 index 0000000..43813cf Binary files /dev/null and b/ndct/core/device_metadata/._vyos_example.yaml differ diff --git a/ndct/core/device_metadata/R1_metadata.yaml b/ndct/core/device_metadata/R1_metadata.yaml new file mode 100644 index 0000000..4efef2e --- /dev/null +++ b/ndct/core/device_metadata/R1_metadata.yaml @@ -0,0 +1,26 @@ +R1: + hostname: R1 + interfaces: + Loopback0: + description: Loopback + ip_address: 11.11.11.11 + subnet_mask: 255.255.255.255 + + bgp: + as: 65001 + peers: + R2: + neighbor_ip: 192.168.21.202 + neighbor_as: 65002 + networks: + network1: + subnet: 11.11.11.11 + subnet_mask: 255.255.255.255 + + ospf: + process: 1 + networks: + network1: + area: 0 + subnet: 192.168.21.0 + wildcard_mask: 0.0.0.255 \ No newline at end of file diff --git a/ndct/core/device_metadata/R2_metadata.yaml b/ndct/core/device_metadata/R2_metadata.yaml new file mode 100644 index 0000000..6cf1278 --- /dev/null +++ b/ndct/core/device_metadata/R2_metadata.yaml @@ -0,0 +1,25 @@ +R2: + hostname: R2 + interfaces: + lo: + description: Loopback + ip_address: 12.12.12.12 + subnet_mask: 32 + + bgp: + as: 65002 + peers: + R1: + neighbor_ip: 192.168.21.201 + neighbor_as: 65001 + networks: + network1: + subnet: 12.12.12.12 + subnet_mask: 32 + + ospf: + networks: + network1: + area: 0 + subnet: 192.168.21.0 + subnet_mask: 24 \ No newline at end of file diff --git a/ndct/core/device_metadata/__init__.py b/ndct/core/device_metadata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/core/device_metadata/cisco_ios_example.yaml b/ndct/core/device_metadata/cisco_ios_example.yaml new file mode 100644 index 0000000..09dcab6 --- /dev/null +++ b/ndct/core/device_metadata/cisco_ios_example.yaml @@ -0,0 +1,26 @@ +ROUTER: + hostname: X + interfaces: + int1: + description: X + ip_address: X + subnet_mask: X + + bgp: + as: X + peers: + X: + neighbor_ip: X + neighbor_as: X + networks: + network1: + subnet: X + subnet_mask: X + + ospf: + process: X + networks: + network1: + area: X + subnet: X + wildcard_mask: X \ No newline at end of file diff --git a/ndct/core/device_metadata/vyos_example.yaml b/ndct/core/device_metadata/vyos_example.yaml new file mode 100644 index 0000000..25d9e9e --- /dev/null +++ b/ndct/core/device_metadata/vyos_example.yaml @@ -0,0 +1,25 @@ +ROUTER: + hostname: X + interfaces: + int1: + description: X + ip_address: X + subnet_mask: X + + bgp: + as: X + peers: + X: + neighbor_ip: X + neighbor_as: X + networks: + network1: + subnet: X + subnet_mask: X + + ospf: + networks: + network1: + area: X + subnet: X + subnet_mask: X \ No newline at end of file diff --git a/ndct/core/log.py b/ndct/core/log.py new file mode 100644 index 0000000..13806bb --- /dev/null +++ b/ndct/core/log.py @@ -0,0 +1,42 @@ +import logging +import sys +import os + +from datetime import datetime +from ndct.core.paths import LOGGING_PATH + +def log(log_message, level): + ''' + Summary: + Logs information to logfile at the specified level. + + Takes: + log_message: Information to log + level: Level of which to log the information at + ''' + logger = logging.getLogger('ndct-logger') + + log_message_types = { + 'debug': logger.debug, + 'info': logger.info, + 'warning': logger.warning, + 'error': logger.error, + 'critical': logger.critical + } + + if not logger.handlers: + logger.setLevel(logging.DEBUG) + + formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s') + + file_handler = logging.FileHandler(LOGGING_PATH + 'log_' + datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + '.log') + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(formatter) + + logger.addHandler(file_handler) + logger.addHandler(stream_handler) + + log_message_types[level](log_message) diff --git a/ndct/core/logs/.___init__.py b/ndct/core/logs/.___init__.py new file mode 100644 index 0000000..13e74b2 Binary files /dev/null and b/ndct/core/logs/.___init__.py differ diff --git a/ndct/core/logs/__init__.py b/ndct/core/logs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/core/paths.py b/ndct/core/paths.py new file mode 100644 index 0000000..d81f026 --- /dev/null +++ b/ndct/core/paths.py @@ -0,0 +1,19 @@ +import os + +current_path = os.path.dirname(__file__) + +#For compatability with any operating system +#KEY_PATH = current_path + '/db/key.key' +#MODULE_PATH = os.path.dirname(current_path) + '/modules/' +#CONFIG_PATH = current_path + '/configuration_files/' +#METADATA_PATH = current_path + '/device_metadata/' +#LOGGING_PATH = current_path + '/logs/' +#DB_PATH = current_path + '/db/' + +#For testing only +KEY_PATH = 'Documents/Python/NDCT/ndct/core/db/key.key' +MODULE_PATH = 'Documents/Python/NDCT/ndct/modules/' +CONFIG_PATH = 'Documents/Python/NDCT/ndct/core/configuration_files/' +METADATA_PATH = 'Documents/Python/NDCT/ndct/core/device_metadata/' +LOGGING_PATH = 'Documents/Python/NDCT/ndct/core/logs/' +DB_PATH = 'Documents/Python/NDCT/ndct/core/db/' \ No newline at end of file diff --git a/ndct/modules/.___init__.py b/ndct/modules/.___init__.py new file mode 100644 index 0000000..ffc2a4f Binary files /dev/null and b/ndct/modules/.___init__.py differ diff --git a/ndct/modules/._cisco_ios b/ndct/modules/._cisco_ios new file mode 100755 index 0000000..5eae792 Binary files /dev/null and b/ndct/modules/._cisco_ios differ diff --git a/ndct/modules/._vyos b/ndct/modules/._vyos new file mode 100755 index 0000000..a1a7056 Binary files /dev/null and b/ndct/modules/._vyos differ diff --git a/ndct/modules/__init__.py b/ndct/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/modules/cisco_ios/.___init__.py b/ndct/modules/cisco_ios/.___init__.py new file mode 100644 index 0000000..a54a5a8 Binary files /dev/null and b/ndct/modules/cisco_ios/.___init__.py differ diff --git a/ndct/modules/cisco_ios/._commands.json b/ndct/modules/cisco_ios/._commands.json new file mode 100644 index 0000000..7942833 Binary files /dev/null and b/ndct/modules/cisco_ios/._commands.json differ diff --git a/ndct/modules/cisco_ios/._template.j2 b/ndct/modules/cisco_ios/._template.j2 new file mode 100644 index 0000000..ecc9a59 Binary files /dev/null and b/ndct/modules/cisco_ios/._template.j2 differ diff --git a/ndct/modules/cisco_ios/__init__.py b/ndct/modules/cisco_ios/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/modules/cisco_ios/commands.json b/ndct/modules/cisco_ios/commands.json new file mode 100644 index 0000000..9093d39 --- /dev/null +++ b/ndct/modules/cisco_ios/commands.json @@ -0,0 +1,7 @@ +{ + "commands": + { + "config": "show run", + "save_config": "copy run start" + } +} \ No newline at end of file diff --git a/ndct/modules/cisco_ios/template.j2 b/ndct/modules/cisco_ios/template.j2 new file mode 100644 index 0000000..bddfd24 --- /dev/null +++ b/ndct/modules/cisco_ios/template.j2 @@ -0,0 +1,28 @@ +hostname {{hostname}} +! +{% if interfaces %} +{% for interface, config in interfaces.items() %} +interface {{interface}} + description {{config['description']}} + ip address {{config['ip_address']}} {{config['subnet_mask']}} + no shutdown +! +{% endfor %} +{% endif %} +{% if bgp %} +router bgp {{bgp['as']}} +{% for peer, peer_config in bgp['peers'].items() %} + neighbor {{peer_config['neighbor_ip']}} remote-as {{peer_config['neighbor_as']}} +{% endfor %} +{% for network, network_config in bgp['networks'].items() %} + network {{network_config['subnet']}} mask {{network_config['subnet_mask']}} +{% endfor %} +{% endif %} +! +{% if ospf %} +router ospf {{ospf['process']}} +{% for network, network_config in ospf['networks'].items() %} + network {{network_config['subnet']}} {{network_config['wildcard_mask']}} area {{network_config['area']}} +{% endfor %} +{% endif %} +! \ No newline at end of file diff --git a/ndct/modules/vyos/.___init__.py b/ndct/modules/vyos/.___init__.py new file mode 100644 index 0000000..95d311d Binary files /dev/null and b/ndct/modules/vyos/.___init__.py differ diff --git a/ndct/modules/vyos/._commands.json b/ndct/modules/vyos/._commands.json new file mode 100644 index 0000000..3e4b943 Binary files /dev/null and b/ndct/modules/vyos/._commands.json differ diff --git a/ndct/modules/vyos/._template.j2 b/ndct/modules/vyos/._template.j2 new file mode 100644 index 0000000..5c3577a Binary files /dev/null and b/ndct/modules/vyos/._template.j2 differ diff --git a/ndct/modules/vyos/__init__.py b/ndct/modules/vyos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ndct/modules/vyos/commands.json b/ndct/modules/vyos/commands.json new file mode 100644 index 0000000..45abd6c --- /dev/null +++ b/ndct/modules/vyos/commands.json @@ -0,0 +1,7 @@ +{ + "commands": + { + "config": "show configuration commands", + "save_config": ["commit", "save", "exit"] + } +} \ No newline at end of file diff --git a/ndct/modules/vyos/template.j2 b/ndct/modules/vyos/template.j2 new file mode 100644 index 0000000..5190831 --- /dev/null +++ b/ndct/modules/vyos/template.j2 @@ -0,0 +1,20 @@ +set system host-name '{{hostname}}' +{% if interfaces %} +{% for interface, interface_config in interfaces.items() %} +set interfaces {% if interface[0:2] == 'lo' %}loopback{% elif interface[0:3] == 'eth' %}ethernet{% endif %} {{interface}} address '{{interface_config['ip_address']}}/{{interface_config['subnet_mask']}}' +set interfaces {% if interface[0:2] == 'lo' %}loopback{% elif interface[0:3] == 'eth' %}ethernet{% endif %} {{interface}} description '{{interface_config['description']}}' +{% endfor %} +{% endif %} +{% if bgp %} +{% for peer, peer_config in bgp['peers'].items() %} +set protocols bgp {{bgp['as']}} neighbor {{peer_config['neighbor_ip']}} remote-as '{{peer_config['neighbor_as']}}' +{% endfor %} +{% for network, network_config in bgp['networks'].items() %} +set protocols bgp {{bgp['as']}} network '{{network_config['subnet']}}/{{network_config['subnet_mask']}}' +{% endfor %} +{% endif %} +{% if ospf %} +{% for network, network_config in ospf['networks'].items() %} +set protocols ospf area {{network_config['area']}} network '{{network_config['subnet']}}/{{network_config['subnet_mask']}}' +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c844969 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup, find_packages + +setup( + name='ndct', + + # Information + version='1.0', + description='A configuration management tool for network device orchestration.', + long_description='A configuration management tool for network device orchestration through the use of deployments.', + + # Author details + author='Dominic James Macfarlane', + author_email='m021859g@student.staffs.ac.uk', + + # Packages + packages=find_packages(exclude=[""]), + + package_data={ + "": ["*.json", "*.j2", "*.yaml", "*.txt"], + }, + + # Dependencies + install_requires=['netmiko>=2.3.3', 'Jinja2>=2.10', 'cryptography>=2.6.1', 'Click>=7.0', 'PyYAML>=5.1.2', 'pythonping>=1.0.5'], + + # Entry points + entry_points={ + 'console_scripts': ['ndct = ndct.cli.main:main'] + } +) \ No newline at end of file