Porting code over from Github
This commit is contained in:
parent
bf29bdae42
commit
5f0ea9976f
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
build/*
|
||||
dist/*
|
||||
ndct.egg-info/*
|
||||
*/.DS_Store
|
93
README.md
93
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
|
||||
***
|
||||
|
BIN
ndct/.___init__.py
Normal file
BIN
ndct/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/._cli
Executable file
BIN
ndct/._cli
Executable file
Binary file not shown.
BIN
ndct/._core
Executable file
BIN
ndct/._core
Executable file
Binary file not shown.
BIN
ndct/._modules
Executable file
BIN
ndct/._modules
Executable file
Binary file not shown.
0
ndct/__init__.py
Normal file
0
ndct/__init__.py
Normal file
BIN
ndct/cli/.___init__.py
Normal file
BIN
ndct/cli/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/cli/._configuration.py
Normal file
BIN
ndct/cli/._configuration.py
Normal file
Binary file not shown.
BIN
ndct/cli/._crypt.py
Normal file
BIN
ndct/cli/._crypt.py
Normal file
Binary file not shown.
BIN
ndct/cli/._deployment.py
Normal file
BIN
ndct/cli/._deployment.py
Normal file
Binary file not shown.
BIN
ndct/cli/._device.py
Normal file
BIN
ndct/cli/._device.py
Normal file
Binary file not shown.
BIN
ndct/cli/._main.py
Normal file
BIN
ndct/cli/._main.py
Normal file
Binary file not shown.
0
ndct/cli/__init__.py
Normal file
0
ndct/cli/__init__.py
Normal file
93
ndct/cli/configuration.py
Normal file
93
ndct/cli/configuration.py
Normal 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
37
ndct/cli/crypt.py
Normal 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
102
ndct/cli/deployment.py
Normal 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
106
ndct/cli/device.py
Normal 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
20
ndct/cli/main.py
Normal 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
BIN
ndct/core/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/core/._banner.py
Normal file
BIN
ndct/core/._banner.py
Normal file
Binary file not shown.
BIN
ndct/core/._configuration.py
Normal file
BIN
ndct/core/._configuration.py
Normal file
Binary file not shown.
BIN
ndct/core/._configuration_files
Executable file
BIN
ndct/core/._configuration_files
Executable file
Binary file not shown.
BIN
ndct/core/._connection.py
Normal file
BIN
ndct/core/._connection.py
Normal file
Binary file not shown.
BIN
ndct/core/._crypt.py
Normal file
BIN
ndct/core/._crypt.py
Normal file
Binary file not shown.
BIN
ndct/core/._db
Executable file
BIN
ndct/core/._db
Executable file
Binary file not shown.
BIN
ndct/core/._deployment.py
Normal file
BIN
ndct/core/._deployment.py
Normal file
Binary file not shown.
BIN
ndct/core/._device.py
Normal file
BIN
ndct/core/._device.py
Normal file
Binary file not shown.
BIN
ndct/core/._device_metadata
Executable file
BIN
ndct/core/._device_metadata
Executable file
Binary file not shown.
BIN
ndct/core/._log.py
Normal file
BIN
ndct/core/._log.py
Normal file
Binary file not shown.
BIN
ndct/core/._logs
Executable file
BIN
ndct/core/._logs
Executable file
Binary file not shown.
BIN
ndct/core/._paths.py
Normal file
BIN
ndct/core/._paths.py
Normal file
Binary file not shown.
0
ndct/core/__init__.py
Normal file
0
ndct/core/__init__.py
Normal file
12
ndct/core/banner.py
Normal file
12
ndct/core/banner.py
Normal 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
328
ndct/core/configuration.py
Normal 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')
|
BIN
ndct/core/configuration_files/._R1_custom_commands.txt
Normal file
BIN
ndct/core/configuration_files/._R1_custom_commands.txt
Normal file
Binary file not shown.
BIN
ndct/core/configuration_files/._R1_generated.txt
Normal file
BIN
ndct/core/configuration_files/._R1_generated.txt
Normal file
Binary file not shown.
BIN
ndct/core/configuration_files/._R2_custom_commands.txt
Normal file
BIN
ndct/core/configuration_files/._R2_custom_commands.txt
Normal file
Binary file not shown.
BIN
ndct/core/configuration_files/._R2_generated.txt
Normal file
BIN
ndct/core/configuration_files/._R2_generated.txt
Normal file
Binary file not shown.
BIN
ndct/core/configuration_files/.___init__.py
Normal file
BIN
ndct/core/configuration_files/.___init__.py
Normal file
Binary file not shown.
2
ndct/core/configuration_files/R1_custom_commands.txt
Normal file
2
ndct/core/configuration_files/R1_custom_commands.txt
Normal file
@ -0,0 +1,2 @@
|
||||
router ospf 1
|
||||
network 11.11.11.11 0.0.0.0 area 0
|
14
ndct/core/configuration_files/R1_generated.txt
Normal file
14
ndct/core/configuration_files/R1_generated.txt
Normal 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
|
||||
!
|
1
ndct/core/configuration_files/R2_custom_commands.txt
Normal file
1
ndct/core/configuration_files/R2_custom_commands.txt
Normal file
@ -0,0 +1 @@
|
||||
set protocols ospf area 0 network '12.12.12.12/32'
|
6
ndct/core/configuration_files/R2_generated.txt
Normal file
6
ndct/core/configuration_files/R2_generated.txt
Normal 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'
|
0
ndct/core/configuration_files/__init__.py
Normal file
0
ndct/core/configuration_files/__init__.py
Normal file
53
ndct/core/connection.py
Normal file
53
ndct/core/connection.py
Normal 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
111
ndct/core/crypt.py
Normal 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
BIN
ndct/core/db/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/core/db/._deployments
Normal file
BIN
ndct/core/db/._deployments
Normal file
Binary file not shown.
BIN
ndct/core/db/._devices
Normal file
BIN
ndct/core/db/._devices
Normal file
Binary file not shown.
BIN
ndct/core/db/._example_devices_file.txt
Normal file
BIN
ndct/core/db/._example_devices_file.txt
Normal file
Binary file not shown.
BIN
ndct/core/db/._key.key
Normal file
BIN
ndct/core/db/._key.key
Normal file
Binary file not shown.
0
ndct/core/db/__init__.py
Normal file
0
ndct/core/db/__init__.py
Normal file
1
ndct/core/db/deployments
Normal file
1
ndct/core/db/deployments
Normal 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
1
ndct/core/db/devices
Normal file
@ -0,0 +1 @@
|
||||
gAAAAABevR-IVmPaNA1MBwD9ukWIWMbf2axg4PgO0AxYxZAl35eaWEiU2Er8Y1Hk__cyc7nZ8l0KQHWy-iJRNuMs2b93_yfmN54cv0NZk9HwpOaWmGv4Ejp2iQLjXvcq_NrX8djlDkTyu73S98yzEjJ9HJR8o2fMbdotW37JCURQyBpn_IzXdfCuN6IXxGClTVK0TBvhVEESYrHOapUKB_qchya42jwTNuam7RgVOl8IkyrJqKsGICKtvNmCxLmm1yik_wO1QLwHUrBPuP1r5jtiFIfY090BobOuERky-Za7-ICkFt8x-YUJAGeimP_ynMNAXRh6l15FVemyHKxyju8tgawtcngAiw==
|
15
ndct/core/db/example_devices_file.txt
Normal file
15
ndct/core/db/example_devices_file.txt
Normal 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
1
ndct/core/db/key.key
Normal file
@ -0,0 +1 @@
|
||||
ENR0-Y8N5ZH7ho6DEAI9xRe17jwVAXsuekLOEfED6Ic=
|
103
ndct/core/deployment.py
Normal file
103
ndct/core/deployment.py
Normal 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
86
ndct/core/device.py
Normal 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
|
BIN
ndct/core/device_metadata/._R1_metadata.yaml
Normal file
BIN
ndct/core/device_metadata/._R1_metadata.yaml
Normal file
Binary file not shown.
BIN
ndct/core/device_metadata/._R2_metadata.yaml
Normal file
BIN
ndct/core/device_metadata/._R2_metadata.yaml
Normal file
Binary file not shown.
BIN
ndct/core/device_metadata/.___init__.py
Normal file
BIN
ndct/core/device_metadata/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/core/device_metadata/._cisco_ios_example.yaml
Normal file
BIN
ndct/core/device_metadata/._cisco_ios_example.yaml
Normal file
Binary file not shown.
BIN
ndct/core/device_metadata/._vyos_example.yaml
Normal file
BIN
ndct/core/device_metadata/._vyos_example.yaml
Normal file
Binary file not shown.
26
ndct/core/device_metadata/R1_metadata.yaml
Normal file
26
ndct/core/device_metadata/R1_metadata.yaml
Normal 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
|
25
ndct/core/device_metadata/R2_metadata.yaml
Normal file
25
ndct/core/device_metadata/R2_metadata.yaml
Normal 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
|
0
ndct/core/device_metadata/__init__.py
Normal file
0
ndct/core/device_metadata/__init__.py
Normal file
26
ndct/core/device_metadata/cisco_ios_example.yaml
Normal file
26
ndct/core/device_metadata/cisco_ios_example.yaml
Normal 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
|
25
ndct/core/device_metadata/vyos_example.yaml
Normal file
25
ndct/core/device_metadata/vyos_example.yaml
Normal 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
42
ndct/core/log.py
Normal 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)
|
BIN
ndct/core/logs/.___init__.py
Normal file
BIN
ndct/core/logs/.___init__.py
Normal file
Binary file not shown.
0
ndct/core/logs/__init__.py
Normal file
0
ndct/core/logs/__init__.py
Normal file
19
ndct/core/paths.py
Normal file
19
ndct/core/paths.py
Normal 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
BIN
ndct/modules/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/modules/._cisco_ios
Executable file
BIN
ndct/modules/._cisco_ios
Executable file
Binary file not shown.
BIN
ndct/modules/._vyos
Executable file
BIN
ndct/modules/._vyos
Executable file
Binary file not shown.
0
ndct/modules/__init__.py
Normal file
0
ndct/modules/__init__.py
Normal file
BIN
ndct/modules/cisco_ios/.___init__.py
Normal file
BIN
ndct/modules/cisco_ios/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/modules/cisco_ios/._commands.json
Normal file
BIN
ndct/modules/cisco_ios/._commands.json
Normal file
Binary file not shown.
BIN
ndct/modules/cisco_ios/._template.j2
Normal file
BIN
ndct/modules/cisco_ios/._template.j2
Normal file
Binary file not shown.
0
ndct/modules/cisco_ios/__init__.py
Normal file
0
ndct/modules/cisco_ios/__init__.py
Normal file
7
ndct/modules/cisco_ios/commands.json
Normal file
7
ndct/modules/cisco_ios/commands.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"commands":
|
||||
{
|
||||
"config": "show run",
|
||||
"save_config": "copy run start"
|
||||
}
|
||||
}
|
28
ndct/modules/cisco_ios/template.j2
Normal file
28
ndct/modules/cisco_ios/template.j2
Normal 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 %}
|
||||
!
|
BIN
ndct/modules/vyos/.___init__.py
Normal file
BIN
ndct/modules/vyos/.___init__.py
Normal file
Binary file not shown.
BIN
ndct/modules/vyos/._commands.json
Normal file
BIN
ndct/modules/vyos/._commands.json
Normal file
Binary file not shown.
BIN
ndct/modules/vyos/._template.j2
Normal file
BIN
ndct/modules/vyos/._template.j2
Normal file
Binary file not shown.
0
ndct/modules/vyos/__init__.py
Normal file
0
ndct/modules/vyos/__init__.py
Normal file
7
ndct/modules/vyos/commands.json
Normal file
7
ndct/modules/vyos/commands.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"commands":
|
||||
{
|
||||
"config": "show configuration commands",
|
||||
"save_config": ["commit", "save", "exit"]
|
||||
}
|
||||
}
|
20
ndct/modules/vyos/template.j2
Normal file
20
ndct/modules/vyos/template.j2
Normal 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
29
setup.py
Normal 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']
|
||||
}
|
||||
)
|
Loading…
Reference in New Issue
Block a user