├── icon2.ico ├── SNAP.py ├── README.md ├── Settings.py ├── message_handler.py ├── TroubleshootTab.py ├── RouterTab.py └── LoadTab.py /icon2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JTHern/SNAP---Code/HEAD/icon2.ico -------------------------------------------------------------------------------- /SNAP.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import Settings 3 | from LoadTab import LoadPage 4 | from PyQt5.QtGui import QIcon 5 | from PyQt5.QtWidgets import (QApplication, QMainWindow, QTabWidget, QVBoxLayout, QWidget) 6 | from RouterTab import RouterInfo 7 | from TroubleshootTab import Troubleshoot 8 | 9 | __author__ = "Jason Hernandez" 10 | __copyright__ = "Copyright 2018" 11 | __credits__ = ["KTByers - Netmiko", "PyQt5"] 12 | __license__ = "MIT" 13 | __version__ = "1.0" 14 | __email__ = "JThern@github" 15 | 16 | 17 | class SNAPWindow(QMainWindow): 18 | def __init__(self, parent=None): 19 | super(SNAPWindow, self).__init__(parent) 20 | central_widget = QWidget() 21 | self.setWindowTitle('SNAP') 22 | self.setWindowIcon(QIcon('icon2.png')) 23 | lay = QVBoxLayout(central_widget) 24 | self.setCentralWidget(central_widget) 25 | Settings.creds() 26 | tab_widget = QTabWidget() 27 | lay.addWidget(tab_widget) # Tab Layout below. 28 | tab_widget.addTab(RouterInfo(), "Router Info") 29 | tab_widget.addTab(LoadPage(), "Load Page") 30 | tab_widget.addTab(Troubleshoot(), "Troubleshoot") 31 | tab_widget.addTab(Settings.AboutTab(), "About") 32 | 33 | if __name__ == "__main__": 34 | app = QApplication(sys.argv) 35 | snap = SNAPWindow() 36 | snap.show() 37 | sys.exit(app.exec_()) 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SNAP 2 | A Simple Network Automation Program to load a Cisco router from nothing to something somewhat quickly. 3 | ![SNAP](https://github.com/JTHern/SNAP---Dist/blob/master/images/Snap1.PNG) 4 | 5 | # How does it work: 6 | Open SNAP. 7 | 8 | # Router Info: 9 | Select your method for connection. 10 | (Console)(Telnet)(SSH) 11 | Enter the username (usernames will not be saved) 12 | enter the password (password will not be saved) 13 | Then select the COM port (if using Console) or IP if using telnet or ssh. 14 | 15 | Click Verify. 16 | 17 | This will then use the credentials on the device. 18 | It will automatically make sure the Cisco Router is above IOS 15.4 (this was for my use but it wont affect anything.) 19 | 20 | # Load Page: 21 | ![snap](https://github.com/JTHern/SNAP---Dist/blob/master/images/Snap2.PNG) 22 | * Make sure the config does not have conf t, enable, building.., or end in it. 23 | 24 | Click open to select the config you want to load. Then Click Load, watch as the config loads line by line. 25 | 26 | Or pull the current config on the device. (This will automatically save the config in the same directory as SNAP) 27 | 28 | If you want to completely erase the Cisco Router there is also a Zeroize feature. (This was useful for me because reasons.) 29 | 30 | # Troubleshoot 31 | ![snap](https://github.com/JTHern/SNAP---Dist/blob/master/images/Snap3.PNG) 32 | ping [Enter the ip into the empty field] - Pings from the Cisco Router, not your machine. 33 | Traceroute [Enter the ip into the empty field] - Traceroutes from the Cisco router, not your machine. 34 | 35 | Routes - prints the output of [show ip route] to the Snap Screen. 36 | Interaces - prints the output of [show ip interface brief] to the Snap Screen. 37 | DMPVN - prints the output of [show crypto ikev2 sa] to the Snap Screen. 38 | OSPF - prints the output of [show ip ospf neigh] to the Snap Screen. 39 | EIGRP - prints the output of [show ip eigrp neigh] to the Snap Screen. 40 | 41 | # About 42 | 43 | Dependencies: 44 | Netmiko (and all of its dependencies), PyQT5. 45 | -------------------------------------------------------------------------------- /Settings.py: -------------------------------------------------------------------------------- 1 | from message_handler import LoggingMessageHandler 2 | from PyQt5.QtWidgets import (QGridLayout, QPlainTextEdit, QWidget) 3 | 4 | __author__ = "Jason Hernandez" 5 | __copyright__ = "Copyright 2018" 6 | __credits__ = ["KTByers - Netmiko", "PyQt5"] 7 | __license__ = "MIT" 8 | __version__ = "1.0" 9 | __email__ = "JThern@github" 10 | 11 | 12 | def creds(): 13 | global device 14 | device = [] 15 | 16 | 17 | class AboutTab(QWidget): 18 | label = "RouterInfo" 19 | 20 | def __init__(self, parent=None): 21 | """ Initialise the page. """ 22 | 23 | super().__init__(parent) 24 | '''sets up a grid layout for the tab''' 25 | layout = QGridLayout() # Page will use a grid layout. 26 | 27 | self._log_viewer = QPlainTextEdit(readOnly=True) 28 | self._log_viewer.setStyleSheet("background-color: #1E1E1E") 29 | layout.addWidget(self._log_viewer, 0, 0, 5, 1) 30 | 31 | self.logger = LoggingMessageHandler(bool(), self._log_viewer) 32 | self.logger.clear() 33 | self.logger.title('SNAP') # Description of the application the different message types create different colors. 34 | self.logger.user_exception('Version: 1.0') 35 | self.logger.message('Author: Jason Hernandez') 36 | self.logger.user_exception('\nMIT License: Copyright (c) 2018 Jason Hernandez\n' 37 | 'Permission is hereby granted, free of charge, to any person obtaining a copy\n' 38 | 'of this software and associated documentation files (the "Software"), to deal\n' 39 | 'in the Software without restriction, including without limitation the rights\n' 40 | 'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n' 41 | 'copies of the Software, and to permit persons to whom the Software is\n' 42 | 'furnished to do so, subject to the following conditions:\n\n' 43 | 'The above copyright notice and this permission notice shall be included in all\n' 44 | 'copies or substantial portions of the Software.\n\n' 45 | 'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, \n' 46 | 'EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES \n' 47 | 'OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND \n' 48 | 'NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT \n' 49 | 'HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, \n' 50 | 'WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, \n' 51 | 'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER \n' 52 | 'DEALINGS IN THE SOFTWARE.\n') 53 | 54 | self.setLayout(layout) # Displays the layout 55 | -------------------------------------------------------------------------------- /message_handler.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtGui import (QColor) 2 | from PyQt5.QtWidgets import QAbstractSlider 3 | 4 | 5 | class MessageHandler: 6 | """ The MessageHandler class handles progress and verbose progress 7 | messages. This base implementation issues messages to the console. 8 | """ 9 | 10 | def __init__(self, quiet, verbose): 11 | """ Initialise the object. quiet is set if all progress messages 12 | should be disabled. verbose is set if verbose progress messages should 13 | be enabled. Messages do not have trailing newlines. 14 | """ 15 | 16 | self.quiet = quiet 17 | self.verbose = verbose 18 | 19 | def progress_message(self, message): 20 | """ Handle a progress message. """ 21 | 22 | if not self.quiet: 23 | self.message(message) 24 | 25 | def verbose_message(self, message): 26 | """ Handle a verbose progress message. """ 27 | 28 | if self.verbose: 29 | self.progress_message(message) 30 | 31 | def message(self, message): 32 | """ Handle a message. This method may be reimplemented to send the 33 | message to somewhere other that stdout. 34 | """ 35 | 36 | print(message) 37 | 38 | 39 | class LoggingMessageHandler(MessageHandler): 40 | """ A message handler that captures user messages and displays them in a widget.""" 41 | 42 | def __init__(self, verbose, viewer): 43 | """ Initialise the object. """ 44 | 45 | super().__init__(quiet=False, verbose=verbose) 46 | 47 | self._viewer = viewer 48 | 49 | self._default_format = self._viewer.currentCharFormat() 50 | self._default_format.setForeground(QColor('#1E90FF')) # blue 51 | 52 | self._error_format = self._viewer.currentCharFormat() 53 | self._error_format.setForeground(QColor('#F8F8FF')) # white 54 | 55 | self._status_format = self._viewer.currentCharFormat() 56 | self._status_format.setForeground(QColor('#000000')) # black 57 | 58 | self._title_format = self._viewer.currentCharFormat() 59 | self._title_format.setForeground(QColor('#1E90FF')) # blue 60 | self._title_format.setFontPointSize(20) 61 | 62 | def clear(self): 63 | """ Clear the viewer. """ 64 | 65 | self._viewer.setPlainText('') 66 | 67 | def status_message(self, message): 68 | """ Add a status message to the viewer. """ 69 | 70 | self._append_text(message, self._status_format) 71 | 72 | def user_exception(self, e): 73 | """ Add a user exception to the viewer. """ 74 | 75 | self._append_text(e, self._error_format) 76 | 77 | if self.verbose and e.detail != '': 78 | self._append_text(e.detail, self._error_format) 79 | 80 | def message(self, message): 81 | """ Reimplemented to handle progress messages. """ 82 | 83 | self._append_text(message, self._default_format) 84 | 85 | def title(self, title): 86 | """ Reimplemented to handle progress messages. """ 87 | 88 | self._append_text(title, self._title_format) 89 | 90 | def _append_text(self, text, char_format): 91 | """ Append text to the viewer using a specific character format. """ 92 | 93 | viewer = self._viewer 94 | 95 | viewer.setCurrentCharFormat(char_format) 96 | viewer.appendPlainText(text) 97 | viewer.setCurrentCharFormat(self._default_format) 98 | 99 | # Make sure the new text is visible. 100 | viewer.verticalScrollBar().triggerAction( 101 | QAbstractSlider.SliderToMaximum) 102 | -------------------------------------------------------------------------------- /TroubleshootTab.py: -------------------------------------------------------------------------------- 1 | import Settings 2 | from message_handler import LoggingMessageHandler 3 | from netmiko import ConnectHandler 4 | from netmiko.ssh_exception import NetMikoTimeoutException, NetMikoAuthenticationException 5 | from PyQt5.QtCore import (QThread, pyqtSignal) 6 | from PyQt5.QtWidgets import (QGridLayout, QLineEdit, QPlainTextEdit, QPushButton, QWidget) 7 | 8 | __author__ = "Jason Hernandez" 9 | __copyright__ = "Copyright 2018" 10 | __credits__ = ["KTByers - Netmiko", "PyQt5"] 11 | __license__ = "MIT" 12 | __version__ = "1.0" 13 | __email__ = "JThern@github" 14 | 15 | 16 | class CommandThread(QThread): 17 | signal = pyqtSignal('PyQt_PyObject') 18 | # This Class threads the connection so we don't freeze the entire program waiting on the connection to take place. 19 | 20 | def __init__(self): 21 | QThread.__init__(self) 22 | self.command = '' 23 | 24 | # run method gets called when we start() the thread 25 | def run(self): 26 | if self.command == '': 27 | self.signal.emit("No command to run.") 28 | return 29 | if Settings.device == []: 30 | self.signal.emit("Enter Credentials on Router Info tab.") 31 | self.signal.emit("Once entered click Verify.") 32 | return 33 | if Settings.device['device_type'] == 'cisco_ios_serial': 34 | device = Settings.device 35 | try: 36 | router = ConnectHandler(**device) # Connect to the Device 37 | router.enable() 38 | result = router.send_command(self.command) 39 | self.signal.emit(result) 40 | router.disconnect() 41 | except ValueError: 42 | self.signal.emit("Console Error: Make sure you have connectivity and try again.") 43 | except TimeoutError: 44 | self.signal.emit("Timeout Error: Make sure you are still connected") 45 | except NetMikoTimeoutException: 46 | self.signal.emit("Timeout Error: Make sure you are still connected") 47 | except NetMikoAuthenticationException: 48 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 49 | else: 50 | device = Settings.device 51 | try: 52 | router = ConnectHandler(**device) # Connect to the Device 53 | router.enable() 54 | result = router.send_command(self.command) 55 | self.signal.emit(result) 56 | router.disconnect() 57 | except ValueError: 58 | self.signal.emit("User does not have permission to make these changes.") 59 | except TimeoutError: 60 | self.signal.emit("Telnet Error: Make sure the IP address is correct.") 61 | except NetMikoTimeoutException: 62 | self.signal.emit("SSH Error: Make sure the IP address is correct.") 63 | except NetMikoAuthenticationException: 64 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 65 | 66 | 67 | class Troubleshoot(QWidget): 68 | """ The GUI for the build page of a project. """ 69 | 70 | # The page's label. 71 | label = "Troubleshooting" 72 | 73 | def __init__(self): 74 | """ Initialise the page. """ 75 | 76 | super().__init__() 77 | layout = QGridLayout() 78 | 79 | self._log_viewer = QPlainTextEdit(readOnly=True) # the message window 80 | layout.addWidget(self._log_viewer, 0, 0, 3, 5) 81 | 82 | self.ping = QPushButton("Ping", clicked=self._ping) 83 | self.ping.setToolTip("ping [x.x.x.x]") 84 | layout.addWidget(self.ping, 4, 0) 85 | 86 | self.traceroute = QPushButton("Traceroute", clicked=self._traceroute) 87 | self.traceroute.setToolTip("traceroute [x.x.x.x]") 88 | layout.addWidget(self.traceroute, 4, 1) 89 | 90 | self.ip = QLineEdit() 91 | self.ip.setToolTip("[x.x.x.x]") 92 | layout.addWidget(self.ip, 4, 2, 1, 3) 93 | 94 | self.routes = QPushButton("Routes", clicked=self._routes) 95 | self.routes.setToolTip("show ip route") 96 | layout.addWidget(self.routes, 5, 0) 97 | 98 | self.interfaces = QPushButton("Interfaces", clicked=self._interfaces) 99 | self.interfaces.setToolTip("show ip interface brief") 100 | layout.addWidget(self.interfaces, 5, 1) 101 | 102 | self.dmvpn = QPushButton("DMVPN", clicked=self._dmvpn) 103 | self.dmvpn.setToolTip("show crypto ikev2 sa") 104 | layout.addWidget(self.dmvpn, 5, 2) 105 | 106 | self.ospf = QPushButton("OSPF", clicked=self._ospf) 107 | self.ospf.setToolTip("show ip ospf neighbor") 108 | layout.addWidget(self.ospf, 5, 3) 109 | 110 | self.eigrp = QPushButton("EIGRP", clicked=self._eigrp) 111 | self.eigrp.setToolTip("show ip eigrp neigh") 112 | layout.addWidget(self.eigrp, 5, 4) 113 | 114 | self.command_thread = CommandThread() 115 | self.command_thread.signal.connect(self.finished) 116 | 117 | self.setLayout(layout) # Displays the layout 118 | 119 | '''The fields below allow for actions to take place based on the above input and button pushes.''' 120 | 121 | def _ping(self, _): 122 | """ Invoked when the user clicks the ping button. """ 123 | logger = LoggingMessageHandler(bool(), self._log_viewer) 124 | if self.ip.text() == '': 125 | logger.clear() 126 | logger.status_message("No IP to ping.") 127 | return 128 | else: 129 | command = f'ping {self.ip.text()}' 130 | self.command_thread.command = command 131 | logger.status_message("Running....") 132 | self.command_thread.start() 133 | 134 | def _traceroute(self, _): 135 | """ Invoked when the user clicks the traceroute button. """ 136 | logger = LoggingMessageHandler(bool(), self._log_viewer) 137 | if self.ip.text() == '': 138 | logger.clear() 139 | logger.status_message("No IP to traceroute.") 140 | return 141 | else: 142 | command = f'traceroute {self.ip.text()}' 143 | self.command_thread.command = command 144 | logger.status_message("Running....") 145 | self.command_thread.start() 146 | 147 | def _routes(self, _): 148 | """ Invoked when the user clicks the routes button. """ 149 | logger = LoggingMessageHandler(bool(), self._log_viewer) 150 | command = 'show ip route' 151 | self.command_thread.command = command 152 | logger.clear() 153 | logger.status_message("Running....") 154 | self.ping.setEnabled(False) 155 | self.traceroute.setEnabled(False) 156 | self.ip.setEnabled(False) 157 | self.routes.setEnabled(False) 158 | self.interfaces.setEnabled(False) 159 | self.dmvpn.setEnabled(False) 160 | self.ospf.setEnabled(False) 161 | self.eigrp.setEnabled(False) 162 | self.command_thread.start() 163 | 164 | def _interfaces(self, _): 165 | """ Invoked when the user clicks the interfaces button. """ 166 | logger = LoggingMessageHandler(bool(), self._log_viewer) 167 | command = 'show ip interface brief' 168 | self.command_thread.command = command 169 | logger.clear() 170 | logger.status_message("Running....") 171 | self.ping.setEnabled(False) 172 | self.traceroute.setEnabled(False) 173 | self.ip.setEnabled(False) 174 | self.routes.setEnabled(False) 175 | self.interfaces.setEnabled(False) 176 | self.dmvpn.setEnabled(False) 177 | self.ospf.setEnabled(False) 178 | self.eigrp.setEnabled(False) 179 | self.command_thread.start() 180 | 181 | def _dmvpn(self, _): 182 | """ Invoked when the user clicks the dmvpn button. """ 183 | logger = LoggingMessageHandler(bool(), self._log_viewer) 184 | command = 'show crypto ikev2 sa' 185 | self.command_thread.command = command 186 | logger.clear() 187 | logger.status_message("Running....") 188 | self.ping.setEnabled(False) 189 | self.traceroute.setEnabled(False) 190 | self.ip.setEnabled(False) 191 | self.routes.setEnabled(False) 192 | self.interfaces.setEnabled(False) 193 | self.dmvpn.setEnabled(False) 194 | self.ospf.setEnabled(False) 195 | self.eigrp.setEnabled(False) 196 | self.command_thread.start() 197 | 198 | def _ospf(self, _): 199 | """ Invoked when the user clicks the ospf button. """ 200 | logger = LoggingMessageHandler(bool(), self._log_viewer) 201 | command = 'show ip ospf neigh' 202 | self.command_thread.command = command 203 | logger.clear() 204 | logger.status_message("Running....") 205 | self.ping.setEnabled(False) 206 | self.traceroute.setEnabled(False) 207 | self.ip.setEnabled(False) 208 | self.routes.setEnabled(False) 209 | self.interfaces.setEnabled(False) 210 | self.dmvpn.setEnabled(False) 211 | self.ospf.setEnabled(False) 212 | self.eigrp.setEnabled(False) 213 | self.command_thread.start() 214 | 215 | def _eigrp(self, _): 216 | """ Invoked when the user clicks the eigrp button. """ 217 | logger = LoggingMessageHandler(bool(), self._log_viewer) 218 | command = 'show ip eigrp neigh' 219 | self.command_thread.command = command 220 | logger.clear() 221 | logger.status_message("Running....") 222 | self.ping.setEnabled(False) 223 | self.traceroute.setEnabled(False) 224 | self.ip.setEnabled(False) 225 | self.routes.setEnabled(False) 226 | self.interfaces.setEnabled(False) 227 | self.dmvpn.setEnabled(False) 228 | self.ospf.setEnabled(False) 229 | self.eigrp.setEnabled(False) 230 | self.command_thread.start() 231 | 232 | def finished(self, result): # Pull all messages into the main thread so we can see them. 233 | logger = LoggingMessageHandler(bool(), self._log_viewer) 234 | self.ping.setEnabled(True) 235 | self.traceroute.setEnabled(True) 236 | self.ip.setEnabled(True) 237 | self.routes.setEnabled(True) 238 | self.interfaces.setEnabled(True) 239 | self.dmvpn.setEnabled(True) 240 | self.ospf.setEnabled(True) 241 | self.eigrp.setEnabled(True) 242 | if result == '': 243 | logger.status_message('Process not running.') 244 | else: 245 | logger.status_message(result) 246 | -------------------------------------------------------------------------------- /RouterTab.py: -------------------------------------------------------------------------------- 1 | import Settings 2 | import serial.tools.list_ports 3 | import re 4 | from message_handler import LoggingMessageHandler 5 | from netmiko import ConnectHandler 6 | from netmiko.ssh_exception import NetMikoTimeoutException, NetMikoAuthenticationException 7 | from PyQt5.QtCore import Qt, QThread, pyqtSignal 8 | from PyQt5.QtWidgets import (QCheckBox, QHBoxLayout, QGridLayout, QGroupBox, QLabel, QLineEdit, QPlainTextEdit, 9 | QPushButton, QWidget) 10 | 11 | __author__ = "Jason Hernandez" 12 | __copyright__ = "Copyright 2018" 13 | __credits__ = ["KTByers - Netmiko", "PyQt5"] 14 | __license__ = "MIT" 15 | __version__ = "1.0" 16 | __email__ = "JThern@github" 17 | 18 | 19 | class Thread(QThread): 20 | signal = pyqtSignal('PyQt_PyObject') 21 | # This Class threads the connection so we don't freeze the entire program waiting on the connection to take place. 22 | 23 | def __init__(self): 24 | QThread.__init__(self) 25 | self.device = {} 26 | 27 | @staticmethod 28 | def get_version_cisco(show_ver): 29 | match = re.search(r"1.*", show_ver) 30 | return float(match.group(0)) 31 | 32 | # run method gets called when we start() the thread 33 | def run(self): 34 | if self.device['device_type'] == 'cisco_ios_serial': 35 | try: 36 | console = ConnectHandler(**self.device) # Connect to the Device to verify credentials. 37 | config_mode = console.check_config_mode() 38 | if config_mode is True: 39 | console.exit_config_mode() 40 | self.signal.emit('Router was in config mode try again.') 41 | return 42 | console.enable() # Enter Enable Mode 43 | show_ver = console.send_command('show run | inc version 1') 44 | self.signal.emit(show_ver) 45 | version = self.get_version_cisco(show_ver) 46 | if version >= 15.4: 47 | pass 48 | else: 49 | show_flash = console.send_command('dir flash: | i .bin') 50 | self.signal.emit('----ERROR----\n' 51 | ' Old version of IOS Detected\n' 52 | ' Correct version may be in flash:') 53 | self.signal.emit(show_flash) 54 | self.signal.emit('\n>========== Reminder ==========<\n' 55 | 'Ensure the config you plan to load is compatible\n' 56 | ' with the version of IOS you are using.\n ' 57 | 'If not contact the help desk.\n' 58 | '>========== Reminder ==========<\n') 59 | console.disconnect() # disconnect so it can be modified later on the LoadTab 60 | self.signal.emit('Credentials Verified on Console!') # Congrats you made it 61 | except ValueError: # most likely com port fault. 62 | self.signal.emit('...COM Port does not appear to be working. \nTry one of these:') 63 | ports = list(serial.tools.list_ports.comports()) # You used the wrong one here let me help you. 64 | for p in ports: # may just automate this in the future using this method if only one port is found 65 | self.signal.emit(p[0]) 66 | return 67 | except NetMikoAuthenticationException: # Exactly what it says in the error. 68 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 69 | return 70 | elif self.device['device_type'] == 'cisco_ios_telnet': 71 | try: 72 | telnet = ConnectHandler(**self.device) # Connect to the Device to verify credentials. 73 | telnet.enable() # Enter Enable Mode 74 | if telnet == True: 75 | telnet.send_command('exit') 76 | show_ver = telnet.send_command('show run | inc version 1') 77 | self.signal.emit(show_ver) 78 | version = self.get_version_cisco(show_ver) 79 | if version >= 15.4: 80 | pass 81 | else: 82 | show_flash = telnet.send_command('dir flash: | i .bin') 83 | self.signal.emit('----ERROR----\n' 84 | ' Old version of IOS Detected\n' 85 | ' Correct version may be in flash:') 86 | self.signal.emit(show_flash) 87 | self.signal.emit('\n>========== Reminder ==========<\n' 88 | 'Ensure the config you plan to load is compatible\n' 89 | ' with the version of IOS you are using.\n ' 90 | 'If not contact the help desk.\n' 91 | '>========== Reminder ==========<\n') 92 | telnet.disconnect() 93 | self.signal.emit('Credentials Verified on Telnet!') 94 | except TimeoutError: # Exactly what it says in the error. 95 | self.signal.emit("Telnet Error: Make sure the IP address is correct.") 96 | return 97 | except NetMikoAuthenticationException: # Exactly what it says in the error. 98 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 99 | return 100 | elif self.device['device_type'] == 'cisco_ios': 101 | try: 102 | ssh = ConnectHandler(**self.device) # Connect to the Device to verify credentials. 103 | ssh.enable() # Enter Enable Mode 104 | if ssh == True: 105 | ssh.send_command('exit') 106 | show_ver = ssh.send_command('show run | inc version 1') 107 | self.signal.emit(show_ver) 108 | version = self.get_version_cisco(show_ver) 109 | if version >= 15.4: 110 | pass 111 | else: 112 | show_flash = ssh.send_command('dir flash: | i .bin') 113 | self.signal.emit('----ERROR----\n' 114 | ' Old version of IOS Detected\n' 115 | ' Correct version may be in flash:') 116 | self.signal.emit(show_flash) 117 | self.signal.emit('\n>========== Reminder ==========<\n' 118 | 'Ensure the config you plan to load is compatible\n' 119 | ' with the version of IOS you are using.\n ' 120 | 'If not contact the help desk.\n' 121 | '>========== Reminder ==========<\n') 122 | ssh.disconnect() 123 | self.signal.emit('Credentials Verified on SSH!') 124 | except NetMikoTimeoutException: # Exactly what it says in the error. 125 | self.signal.emit("SSH Error: Make sure the IP address is correct.") 126 | return 127 | except NetMikoAuthenticationException: # Exactly what it says in the error. 128 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 129 | return 130 | else: 131 | return 132 | 133 | 134 | class RouterInfo(QWidget): 135 | label = "RouterInfo" 136 | 137 | def __init__(self, parent=None): 138 | """ Initialise the page. """ 139 | 140 | super().__init__(parent) 141 | '''sets up a grid layout for the tab''' 142 | layout = QGridLayout() # Page will use a grid layout. 143 | 144 | '''Connection method''' 145 | self.con_method = '' # Once the connection method is selected it is stored in this variable. 146 | con_method_sel = QGroupBox("Connection method") 147 | con_method_sel_layout = QHBoxLayout() # Lays out the below buttons horizontally. 148 | 149 | self._console_button = QCheckBox("Console", checked=False, stateChanged=self._console_button) 150 | self._console_button.setToolTip("If connecting over Serial - COM port needed.") 151 | con_method_sel_layout.addWidget(self._console_button) 152 | 153 | self._telnet_button = QCheckBox("Telnet", checked=False, stateChanged=self._telnet_button) 154 | self._telnet_button.setToolTip("If connecting over Telnet. - IP needed") 155 | con_method_sel_layout.addWidget(self._telnet_button) 156 | 157 | self._ssh_button = QCheckBox("SSH", checked=False, stateChanged=self._ssh_button) 158 | self._ssh_button.setToolTip("If connecting over SSH. - IP needed") 159 | con_method_sel_layout.addWidget(self._ssh_button) 160 | 161 | con_method_sel.setLayout(con_method_sel_layout) 162 | layout.addWidget(con_method_sel, 0, 1) 163 | 164 | '''The username field and label''' 165 | label1 = QLabel(' Username') 166 | layout.addWidget(label1, 1, 0) 167 | self.username = QLineEdit() 168 | layout.addWidget(self.username, 1, 1) 169 | 170 | '''The password field and label''' 171 | label2 = QLabel(' Password') 172 | layout.addWidget(label2, 2, 0) 173 | self.password = QLineEdit() 174 | self.password.setEchoMode(QLineEdit.Password) # turn off echo mode for password 175 | 176 | layout.addWidget(self.password, 2, 1) 177 | 178 | '''The ip field and label''' 179 | label3 = QLabel(' COM Port or IP') 180 | layout.addWidget(label3, 3, 0) 181 | self.ip = QLineEdit() 182 | self.ip.setToolTip("Com Port = COM1 or IP = 192.168.0.1") 183 | layout.addWidget(self.ip, 3, 1) 184 | 185 | '''The verify pushbutton''' 186 | self.verify_button = QPushButton("Verify", clicked=self._verify) 187 | self.verify_button.setToolTip("Verify - Connects using provided credentials.") 188 | layout.addWidget(self.verify_button, 3, 2) 189 | self.verify_thread = Thread() 190 | self.verify_thread.signal.connect(self.finished) 191 | 192 | label0 = QLabel('Once credentials are set click Verify.') # label at bottom of screen 193 | layout.addWidget(label0, 4, 1) 194 | 195 | '''The output screen''' 196 | self._log_viewer = QPlainTextEdit(readOnly=True) 197 | layout.addWidget(self._log_viewer, 5, 1, 1, 1) 198 | 199 | self.setLayout(layout) # Displays the layout 200 | 201 | '''The fields below allow for actions to take place based on the above input and button pushes.''' 202 | 203 | def _verify(self): # We want to verify the information in a new thread so we don't freeze up the entire app. 204 | logger = LoggingMessageHandler(bool(), self._log_viewer) 205 | if self.ip.text() == '' or self.username.text() == '' or self.password.text() == '': 206 | logger.clear() 207 | logger.status_message("All Fields must be Completed.") 208 | return 209 | if self.con_method == 'cisco_ios_serial': 210 | if 'COM' not in self.ip.text().upper(): 211 | logger.clear() 212 | logger.status_message("Com Port field requires COM1 or COM2 or COM3 etc...") 213 | return 214 | device = { 215 | 'device_type': self.con_method, 216 | 'global_delay_factor': 2, 217 | 'username': self.username.text(), 218 | 'password': self.password.text(), 219 | 'serial_settings': { 220 | 'port': self.ip.text()} 221 | } 222 | Settings.device = device 223 | self.verify_thread.device = device 224 | elif self.con_method == 'cisco_ios_telnet': 225 | device = { 226 | 'device_type': self.con_method, 227 | 'ip': self.ip.text(), 228 | 'username': self.username.text(), 229 | 'password': self.password.text() 230 | } 231 | Settings.device = device 232 | self.verify_thread.device = device 233 | elif self.con_method == 'cisco_ios': 234 | device = { 235 | 'device_type': self.con_method, 236 | 'ip': self.ip.text(), 237 | 'username': self.username.text(), 238 | 'password': self.password.text() 239 | } 240 | Settings.device = device 241 | self.verify_thread.device = device 242 | else: 243 | logger.clear() 244 | logger.status_message("Please select a connection method.") 245 | return 246 | 247 | self.verify_button.setEnabled(False) # Disables the pushButton 248 | logger.status_message("Verifying... (This may take a while on Console)") 249 | self.verify_thread.start() 250 | 251 | def finished(self, result): 252 | logger = LoggingMessageHandler(bool(), self._log_viewer) 253 | logger.status_message(result) 254 | self.verify_button.setEnabled(True) # Enable the pushButton 255 | 256 | def _console_button(self, state): 257 | """ if console is checked uncheck the others """ 258 | 259 | if state == Qt.Checked: 260 | self._telnet_button.setCheckState(Qt.Unchecked) 261 | self._ssh_button.setCheckState(Qt.Unchecked) 262 | self.con_method = 'cisco_ios_serial' 263 | 264 | def _telnet_button(self, state): 265 | """ if telnet is checked uncheck the others """ 266 | 267 | if state == Qt.Checked: 268 | self._console_button.setCheckState(Qt.Unchecked) 269 | self._ssh_button.setCheckState(Qt.Unchecked) 270 | self.con_method = 'cisco_ios_telnet' 271 | 272 | def _ssh_button(self, state): 273 | """ if ssh is checked uncheck the others """ 274 | 275 | if state == Qt.Checked: 276 | self._console_button.setCheckState(Qt.Unchecked) 277 | self._telnet_button.setCheckState(Qt.Unchecked) 278 | self.con_method = 'cisco_ios' 279 | -------------------------------------------------------------------------------- /LoadTab.py: -------------------------------------------------------------------------------- 1 | import Settings 2 | from datetime import datetime 3 | from time import sleep 4 | from message_handler import LoggingMessageHandler 5 | from netmiko import ConnectHandler 6 | from netmiko.ssh_exception import NetMikoTimeoutException, NetMikoAuthenticationException 7 | from PyQt5.QtCore import QThread, pyqtSignal 8 | from PyQt5.QtWidgets import QFileDialog, QGridLayout, QPlainTextEdit, QPushButton, QWidget, QMessageBox 9 | from serial.serialutil import SerialException 10 | 11 | __author__ = "Jason Hernandez" 12 | __copyright__ = "Copyright 2018" 13 | __credits__ = ["KTByers - Netmiko", "PyQt5"] 14 | __license__ = "MIT" 15 | __version__ = "1.0" 16 | __email__ = "JThern@github" 17 | 18 | 19 | class LoadThread(QThread): 20 | signal = pyqtSignal('PyQt_PyObject') 21 | # This Class threads the connection so we don't freeze the entire program waiting on the connection to take place. 22 | 23 | def __init__(self): 24 | QThread.__init__(self) 25 | self.config = '' 26 | 27 | # run method gets called when we start() the thread 28 | def run(self): 29 | if self.config == '': 30 | self.signal.emit("No config to load.") 31 | return 32 | if Settings.device == []: 33 | self.signal.emit("Enter Credentials on Router Info tab.") 34 | self.signal.emit("Once entered click Verify.") 35 | return 36 | if Settings.device['device_type'] == 'cisco_ios_serial': 37 | device = Settings.device 38 | try: 39 | router = ConnectHandler(**device) # Connect to the Device 40 | self.signal.emit('...connected...') 41 | router.enable() 42 | router.config_mode() 43 | for line in self.config.splitlines(): 44 | self.signal.emit(line) 45 | router.send_command(line, delay_factor=3, auto_find_prompt=False) 46 | router.exit_config_mode() 47 | new_config = router.send_command('show run') 48 | self.signal.emit(new_config) 49 | router.send_command('wr') 50 | router.config_mode() 51 | router.disconnect() 52 | self.signal.emit('Load Complete') 53 | except SerialException: 54 | self.signal.emit("Console Error: Make sure you have connectivity.") 55 | except OSError: 56 | self.signal.emit("Console Error: Make sure you have connectivity.") 57 | except ValueError: 58 | self.signal.emit("Console Error: Make sure you have connectivity.") 59 | except NetMikoTimeoutException: 60 | self.signal.emit("Timeout Error: Make sure you are still connected") 61 | except NetMikoAuthenticationException: 62 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 63 | else: 64 | device = Settings.device 65 | try: 66 | router = ConnectHandler(**device) # Connect to the Device 67 | self.signal.emit('...connected...') 68 | router.enable() 69 | self.signal.emit('...this may take a while...') 70 | router.config_mode() 71 | for line in self.config.splitlines(): 72 | self.signal.emit(line) 73 | router.send_command(line, delay_factor=2, auto_find_prompt=False) 74 | router.exit_config_mode() 75 | new_config = router.send_command('show run') 76 | self.signal.emit(new_config) 77 | router.send_command('wr') 78 | router.disconnect() 79 | self.signal.emit('Load Complete') 80 | except OSError: 81 | self.signal.emit("Verify connection") 82 | except ValueError: 83 | self.signal.emit("User does not have permission to make these changes.") 84 | except NetMikoTimeoutException: 85 | self.signal.emit("SSH Error: Make sure the IP address is correct.") 86 | except NetMikoAuthenticationException: 87 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 88 | 89 | 90 | class BackupThread(QThread): 91 | signal = pyqtSignal('PyQt_PyObject') 92 | # This Class threads the connection so we don't freeze the entire program waiting on the connection to take place. 93 | 94 | def __init__(self): 95 | QThread.__init__(self) 96 | 97 | # run method gets called when we start() the thread 98 | def run(self): 99 | today = datetime.now().strftime('%Y%m%d-%H%M') 100 | if Settings.device == []: 101 | self.signal.emit("Enter Credentials on Router Info tab.") 102 | self.signal.emit("Once entered click Verify.") 103 | return 104 | if Settings.device['device_type'] == 'cisco_ios_serial': 105 | device = Settings.device 106 | try: 107 | save_file = open(f'Backup Config {today}.txt', mode='w') 108 | router = ConnectHandler(**device) # Connect to the Device 109 | self.signal.emit('...connected...') 110 | router.enable() 111 | self.signal.emit('...this may take a while...') 112 | config = router.send_command('show run') 113 | self.signal.emit(config) 114 | save_file.write(config) 115 | save_file.close() 116 | router.disconnect() 117 | self.signal.emit('Configuration pulled') 118 | except SerialException: 119 | self.signal.emit("Console Error: Make sure you have connectivity.") 120 | except OSError: 121 | self.signal.emit("Console Error: Make sure you have connectivity.") 122 | except ValueError: 123 | self.signal.emit("Console Error: Make sure you have connectivity.") 124 | except NetMikoTimeoutException: 125 | self.signal.emit("Timeout Error: Make sure you are still connected") 126 | except NetMikoAuthenticationException: 127 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 128 | else: 129 | device = Settings.device 130 | try: 131 | save_file = open(f'Backup Config {today}.txt', mode='w') 132 | self.signal.emit('Connecting....') 133 | router = ConnectHandler(**device) # Connect to the Device 134 | self.signal.emit('...connected...') 135 | router.enable() 136 | self.signal.emit('...this may take a while...') 137 | config = router.send_command('show run') 138 | self.signal.emit(config) 139 | save_file.write(config) 140 | save_file.close() 141 | router.disconnect() 142 | self.signal.emit('Configuration pulled') 143 | except ValueError: 144 | self.signal.emit("User does not have permission to make these changes.") 145 | except TimeoutError: 146 | self.signal.emit("Telnet Error: Make sure the IP address is correct.") 147 | except NetMikoTimeoutException: 148 | self.signal.emit("SSH Error: Make sure the IP address is correct.") 149 | except NetMikoAuthenticationException: 150 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 151 | 152 | 153 | class ZeroizeThread(QThread): 154 | signal = pyqtSignal('PyQt_PyObject') 155 | # This Class threads the connection so we don't freeze the entire program waiting on the connection to take place. 156 | 157 | def __init__(self): 158 | QThread.__init__(self) 159 | 160 | # run method gets called when we start() the thread 161 | def run(self): 162 | if Settings.device == []: 163 | self.signal.emit("Enter Credentials on Router Info tab.") 164 | self.signal.emit("Once entered click Verify.") 165 | return 166 | if Settings.device['device_type'] == 'cisco_ios_serial': 167 | device = Settings.device 168 | try: 169 | router = ConnectHandler(**device) # Connect to the Device 170 | router.enable() 171 | self.signal.emit('...connected...') 172 | erase = router.send_command_timing('wr er') 173 | if 'Erasing' in erase: 174 | router.send_command_timing('y') 175 | self.signal.emit('Erase succeed') 176 | else: 177 | self.signal.emit('erase fail') 178 | sleep(5) 179 | reload = router.send_command_timing('reload') 180 | if 'Proceed' in reload: 181 | router.send_command_timing('y') 182 | self.signal.emit('Reload succeed') 183 | else: 184 | self.signal.emit('Reload fail') 185 | self.signal.emit('Router reloading....\n' 186 | 'After Reboot, username and password can be anything.\n' 187 | 'Ensure the correct com port is selected.\n' 188 | 'Reboot times may vary allow for 3-5 minutes.') 189 | router.disconnect() 190 | except SerialException: 191 | self.signal.emit("Console Error: Make sure you have connectivity.") 192 | except OSError: 193 | self.signal.emit("Console Error: Make sure you have connectivity.") 194 | except ValueError: 195 | self.signal.emit("Console Error: Make sure you have connectivity.") 196 | except NetMikoTimeoutException: 197 | self.signal.emit("Timeout Error: Make sure you are still connected") 198 | except NetMikoAuthenticationException: 199 | self.signal.emit("Check your username/password. Make sure you have an account on this device.") 200 | else: 201 | self.signal.emit("Zeroize only possible over Console.") 202 | 203 | 204 | class LoadPage(QWidget): 205 | label = "Load" 206 | 207 | def __init__(self, parent=None): 208 | """ Initialise the page. """ 209 | 210 | super().__init__(parent) 211 | layout = QGridLayout() # page will use a grid layout 212 | 213 | self.config = '' # this variable overwritten after config is opened. 214 | '''Pulls from router tab hopefully''' 215 | 216 | self._log_viewer = QPlainTextEdit(readOnly=True) 217 | layout.addWidget(self._log_viewer, 0, 0, 5, 1) 218 | 219 | self.openfile = QPushButton("Open", clicked=self._open) 220 | self.openfile.setToolTip("Open a text Config.") 221 | layout.addWidget(self.openfile, 0, 1) 222 | 223 | self._backup_config = QPushButton("Pull Current\nConfiguration", clicked=self._backup) 224 | self._backup_config.setToolTip("Optional - Pull the current config from the device will save as Backup Config.") 225 | layout.addWidget(self._backup_config, 1, 1) 226 | self.backup_thread = BackupThread() 227 | self.backup_thread.signal.connect(self.finished) 228 | 229 | self.load = QPushButton("Load", clicked=self._load) # The load button which carries out the load logic. 230 | self.load.setToolTip("Load a new configuration.") 231 | layout.addWidget(self.load, 2, 1) 232 | self.load_thread = LoadThread() 233 | self.load_thread.signal.connect(self.finished) 234 | 235 | self.zero = QPushButton("Zeroize", clicked=self._zero) # The Zero button which carries out the zeroize logic. 236 | self.zero.setToolTip("Restore the router to factory default settings.") 237 | layout.addWidget(self.zero, 4, 1) 238 | self.zero_thread = ZeroizeThread() 239 | self.zero_thread.signal.connect(self.finished) 240 | 241 | self.setLayout(layout) # Displays the layout 242 | 243 | '''The fields below allow for actions to take place based on the above input and button pushes.''' 244 | 245 | def _open(self, _): 246 | """ Invoked when the user clicks the open button. """ 247 | logger = LoggingMessageHandler(bool(), self._log_viewer) 248 | fltr = "Text or Config (*.txt *.cfg)" 249 | obj = QFileDialog.getOpenFileName(self, 'Config to Load', '', fltr) 250 | if obj[0] == '': 251 | return 252 | with open(obj[0], 'r') as file: 253 | config = file.read() 254 | logger.clear() 255 | logger.status_message('>======= Configuration Preview ======<\n') 256 | logger.status_message(config) 257 | logger.status_message('>============= Reminder =============<\n' 258 | 'Remove extra lines from the text file, such as:\n ' 259 | ' enable \n config t \n building \n etc...') 260 | self.load_thread.config = config 261 | 262 | def _backup(self, state): # backup button triggers the backup thread to start 263 | logger = LoggingMessageHandler(bool(), self._log_viewer) 264 | logger.clear() 265 | logger.status_message('Connecting....') 266 | self.openfile.setEnabled(False) 267 | self._backup_config.setEnabled(False) # turn off the buttons so accidents don't happen. 268 | self.load.setEnabled(False) 269 | self.zero.setEnabled(False) 270 | self.backup_thread.start() 271 | 272 | def _load(self): # load button triggers the backup thread to start 273 | logger = LoggingMessageHandler(bool(), self._log_viewer) 274 | logger.clear() 275 | logger.status_message('Loading Configuration....') 276 | self.openfile.setEnabled(False) 277 | self._backup_config.setEnabled(False) # turn off the buttons so accidents don't happen. 278 | self.load.setEnabled(False) 279 | self.zero.setEnabled(False) 280 | self.load_thread.start() 281 | 282 | def _zero(self,): 283 | zero_msg = "Are you sure you want Zero the router?" 284 | reply = QMessageBox.question(self, 'Zero?', zero_msg, QMessageBox.Yes, QMessageBox.No) 285 | 286 | if reply == QMessageBox.Yes: 287 | logger = LoggingMessageHandler(bool(), self._log_viewer) 288 | logger.clear() 289 | logger.status_message('Zeroizing router...') 290 | self.openfile.setEnabled(False) 291 | self._backup_config.setEnabled(False) # turn off the buttons so accidents don't happen. 292 | self.load.setEnabled(False) 293 | self.zero.setEnabled(False) 294 | self.zero_thread.start() 295 | else: 296 | return 297 | 298 | def finished(self, result): # Pull all messages into the main thread so we can see them. 299 | logger = LoggingMessageHandler(bool(), self._log_viewer) 300 | logger.status_message(result) 301 | self.openfile.setEnabled(True) 302 | self._backup_config.setEnabled(True) # turn the buttons on again. 303 | self.load.setEnabled(True) 304 | self.zero.setEnabled(True) 305 | --------------------------------------------------------------------------------