Porting code over from Github

This commit is contained in:
Dom 2024-06-09 18:22:26 +00:00
parent bf29bdae42
commit 5f0ea9976f
90 changed files with 1442 additions and 1 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
build/*
dist/*
ndct.egg-info/*
*/.DS_Store

View File

@ -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
***

BIN
ndct/.___init__.py Normal file

Binary file not shown.

BIN
ndct/._cli Executable file

Binary file not shown.

BIN
ndct/._core Executable file

Binary file not shown.

BIN
ndct/._modules Executable file

Binary file not shown.

0
ndct/__init__.py Normal file
View File

BIN
ndct/cli/.___init__.py Normal file

Binary file not shown.

BIN
ndct/cli/._configuration.py Normal file

Binary file not shown.

BIN
ndct/cli/._crypt.py Normal file

Binary file not shown.

BIN
ndct/cli/._deployment.py Normal file

Binary file not shown.

BIN
ndct/cli/._device.py Normal file

Binary file not shown.

BIN
ndct/cli/._main.py Normal file

Binary file not shown.

0
ndct/cli/__init__.py Normal file
View File

93
ndct/cli/configuration.py Normal file
View File

@ -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)

37
ndct/cli/crypt.py Normal file
View File

@ -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)

102
ndct/cli/deployment.py Normal file
View File

@ -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)

106
ndct/cli/device.py Normal file
View File

@ -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)

20
ndct/cli/main.py Normal file
View File

@ -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')

BIN
ndct/core/.___init__.py Normal file

Binary file not shown.

BIN
ndct/core/._banner.py Normal file

Binary file not shown.

Binary file not shown.

BIN
ndct/core/._configuration_files Executable file

Binary file not shown.

BIN
ndct/core/._connection.py Normal file

Binary file not shown.

BIN
ndct/core/._crypt.py Normal file

Binary file not shown.

BIN
ndct/core/._db Executable file

Binary file not shown.

BIN
ndct/core/._deployment.py Normal file

Binary file not shown.

BIN
ndct/core/._device.py Normal file

Binary file not shown.

BIN
ndct/core/._device_metadata Executable file

Binary file not shown.

BIN
ndct/core/._log.py Normal file

Binary file not shown.

BIN
ndct/core/._logs Executable file

Binary file not shown.

BIN
ndct/core/._paths.py Normal file

Binary file not shown.

0
ndct/core/__init__.py Normal file
View File

12
ndct/core/banner.py Normal file
View File

@ -0,0 +1,12 @@
def banner():
print('''
Created by: github.com/m4cfarlane
_ _ ____ ____ _____
| \ | | _ \ / ___|_ _|
| \| | | | | | | |
| |\ | |_| | |___ | |
|_| \_|____/ \____| |_|
Network Device Configuration Tool
A configuration tool for efficient network device management.
''')

328
ndct/core/configuration.py Normal file
View File

@ -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')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
router ospf 1
network 11.11.11.11 0.0.0.0 area 0

View File

@ -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
!

View File

@ -0,0 +1 @@
set protocols ospf area 0 network '12.12.12.12/32'

View File

@ -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'

53
ndct/core/connection.py Normal file
View File

@ -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')

111
ndct/core/crypt.py Normal file
View File

@ -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

BIN
ndct/core/db/.___init__.py Normal file

Binary file not shown.

BIN
ndct/core/db/._deployments Normal file

Binary file not shown.

BIN
ndct/core/db/._devices Normal file

Binary file not shown.

Binary file not shown.

BIN
ndct/core/db/._key.key Normal file

Binary file not shown.

0
ndct/core/db/__init__.py Normal file
View File

1
ndct/core/db/deployments Normal file
View File

@ -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==

1
ndct/core/db/devices Normal file
View File

@ -0,0 +1 @@
gAAAAABevR-IVmPaNA1MBwD9ukWIWMbf2axg4PgO0AxYxZAl35eaWEiU2Er8Y1Hk__cyc7nZ8l0KQHWy-iJRNuMs2b93_yfmN54cv0NZk9HwpOaWmGv4Ejp2iQLjXvcq_NrX8djlDkTyu73S98yzEjJ9HJR8o2fMbdotW37JCURQyBpn_IzXdfCuN6IXxGClTVK0TBvhVEESYrHOapUKB_qchya42jwTNuam7RgVOl8IkyrJqKsGICKtvNmCxLmm1yik_wO1QLwHUrBPuP1r5jtiFIfY090BobOuERky-Za7-ICkFt8x-YUJAGeimP_ynMNAXRh6l15FVemyHKxyju8tgawtcngAiw==

View File

@ -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

1
ndct/core/db/key.key Normal file
View File

@ -0,0 +1 @@
ENR0-Y8N5ZH7ho6DEAI9xRe17jwVAXsuekLOEfED6Ic=

103
ndct/core/deployment.py Normal file
View File

@ -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')

86
ndct/core/device.py Normal file
View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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

View File

@ -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

View File

View File

@ -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

View File

@ -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

42
ndct/core/log.py Normal file
View File

@ -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)

Binary file not shown.

View File

19
ndct/core/paths.py Normal file
View File

@ -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/'

BIN
ndct/modules/.___init__.py Normal file

Binary file not shown.

BIN
ndct/modules/._cisco_ios Executable file

Binary file not shown.

BIN
ndct/modules/._vyos Executable file

Binary file not shown.

0
ndct/modules/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

View File

@ -0,0 +1,7 @@
{
"commands":
{
"config": "show run",
"save_config": "copy run start"
}
}

View File

@ -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 %}
!

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

View File

@ -0,0 +1,7 @@
{
"commands":
{
"config": "show configuration commands",
"save_config": ["commit", "save", "exit"]
}
}

View File

@ -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 %}

29
setup.py Normal file
View File

@ -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']
}
)