From c4be440cd1b6bf7829fa68981507070e482e50de Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 16 Aug 2018 12:21:24 -0400 Subject: [PATCH] Closes #2367: Remove deprecated RPCClient functionality --- netbox/dcim/api/serializers.py | 2 +- netbox/dcim/constants.py | 10 - netbox/dcim/fixtures/dcim.json | 6 +- netbox/dcim/fixtures/initial_data.json | 18 +- netbox/dcim/forms.py | 2 +- .../0062_remove_platform_rpc_client.py | 17 ++ netbox/dcim/models.py | 15 -- .../management/commands/run_inventory.py | 125 ---------- netbox/extras/rpc.py | 235 ------------------ 9 files changed, 27 insertions(+), 403 deletions(-) create mode 100644 netbox/dcim/migrations/0062_remove_platform_rpc_client.py delete mode 100644 netbox/extras/management/commands/run_inventory.py delete mode 100644 netbox/extras/rpc.py diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 26100c3a7..c357891b6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -344,7 +344,7 @@ class PlatformSerializer(ValidatedModelSerializer): class Meta: model = Platform - fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'rpc_client'] + fields = ['id', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args'] class NestedPlatformSerializer(WritableNestedSerializer): diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 2249f0e0d..0bec76bf3 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -243,13 +243,3 @@ CONNECTION_STATUS_CHOICES = [ [CONNECTION_STATUS_PLANNED, 'Planned'], [CONNECTION_STATUS_CONNECTED, 'Connected'], ] - -# Platform -> RPC client mappings -RPC_CLIENT_JUNIPER_JUNOS = 'juniper-junos' -RPC_CLIENT_CISCO_IOS = 'cisco-ios' -RPC_CLIENT_OPENGEAR = 'opengear' -RPC_CLIENT_CHOICES = [ - [RPC_CLIENT_JUNIPER_JUNOS, 'Juniper Junos (NETCONF)'], - [RPC_CLIENT_CISCO_IOS, 'Cisco IOS (SSH)'], - [RPC_CLIENT_OPENGEAR, 'Opengear (SSH)'], -] diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json index 761f1ba69..709995f95 100644 --- a/netbox/dcim/fixtures/dcim.json +++ b/netbox/dcim/fixtures/dcim.json @@ -1903,8 +1903,7 @@ "pk": 1, "fields": { "name": "Juniper Junos", - "slug": "juniper-junos", - "rpc_client": "juniper-junos" + "slug": "juniper-junos" } }, { @@ -1912,8 +1911,7 @@ "pk": 2, "fields": { "name": "Opengear", - "slug": "opengear", - "rpc_client": "opengear" + "slug": "opengear" } }, { diff --git a/netbox/dcim/fixtures/initial_data.json b/netbox/dcim/fixtures/initial_data.json index e765de227..83f79e3a3 100644 --- a/netbox/dcim/fixtures/initial_data.json +++ b/netbox/dcim/fixtures/initial_data.json @@ -149,8 +149,7 @@ "pk": 1, "fields": { "name": "Cisco IOS", - "slug": "cisco-ios", - "rpc_client": "cisco-ios" + "slug": "cisco-ios" } }, { @@ -158,8 +157,7 @@ "pk": 2, "fields": { "name": "Cisco NX-OS", - "slug": "cisco-nx-os", - "rpc_client": "" + "slug": "cisco-nx-os" } }, { @@ -167,8 +165,7 @@ "pk": 3, "fields": { "name": "Juniper Junos", - "slug": "juniper-junos", - "rpc_client": "juniper-junos" + "slug": "juniper-junos" } }, { @@ -176,8 +173,7 @@ "pk": 4, "fields": { "name": "Arista EOS", - "slug": "arista-eos", - "rpc_client": "" + "slug": "arista-eos" } }, { @@ -185,8 +181,7 @@ "pk": 5, "fields": { "name": "Linux", - "slug": "linux", - "rpc_client": "" + "slug": "linux" } }, { @@ -194,8 +189,7 @@ "pk": 6, "fields": { "name": "Opengear", - "slug": "opengear", - "rpc_client": "opengear" + "slug": "opengear" } } ] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 66ed2447f..30c798e9e 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -744,7 +744,7 @@ class PlatformForm(BootstrapMixin, forms.ModelForm): class Meta: model = Platform - fields = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'rpc_client'] + fields = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args'] widgets = { 'napalm_args': SmallTextarea(), } diff --git a/netbox/dcim/migrations/0062_remove_platform_rpc_client.py b/netbox/dcim/migrations/0062_remove_platform_rpc_client.py new file mode 100644 index 000000000..22a4900d1 --- /dev/null +++ b/netbox/dcim/migrations/0062_remove_platform_rpc_client.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.8 on 2018-08-16 16:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0061_platform_napalm_args'), + ] + + operations = [ + migrations.RemoveField( + model_name='platform', + name='rpc_client', + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 8fdc5e998..432d78977 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -17,7 +17,6 @@ from timezone_field import TimeZoneField from circuits.models import Circuit from extras.constants import OBJECTCHANGE_ACTION_DELETE, OBJECTCHANGE_ACTION_UPDATE from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange -from extras.rpc import RPC_CLIENTS from utilities.fields import ColorField, NullableCharField from utilities.managers import NaturalOrderByManager from utilities.models import ChangeLoggedModel @@ -1096,12 +1095,6 @@ class Platform(ChangeLoggedModel): verbose_name='NAPALM arguments', help_text='Additional arguments to pass when initiating the NAPALM driver (JSON format)' ) - rpc_client = models.CharField( - max_length=30, - choices=RPC_CLIENT_CHOICES, - blank=True, - verbose_name='Legacy RPC client' - ) csv_headers = ['name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args'] @@ -1507,14 +1500,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): def get_status_class(self): return STATUS_CLASSES[self.status] - def get_rpc_client(self): - """ - Return the appropriate RPC (e.g. NETCONF, ssh, etc.) client for this device's platform, if one is defined. - """ - if not self.platform: - return None - return RPC_CLIENTS.get(self.platform.rpc_client) - # # Console ports diff --git a/netbox/extras/management/commands/run_inventory.py b/netbox/extras/management/commands/run_inventory.py deleted file mode 100644 index e5ce1bbca..000000000 --- a/netbox/extras/management/commands/run_inventory.py +++ /dev/null @@ -1,125 +0,0 @@ -from getpass import getpass - -from django.conf import settings -from django.core.management.base import BaseCommand, CommandError -from django.db import transaction -from ncclient.transport.errors import AuthenticationError -from paramiko import AuthenticationException - -from dcim.models import DEVICE_STATUS_ACTIVE, Device, InventoryItem, Site - - -class Command(BaseCommand): - help = "Update inventory information for specified devices" - username = settings.NAPALM_USERNAME - password = settings.NAPALM_PASSWORD - - def add_arguments(self, parser): - parser.add_argument('-u', '--username', dest='username', help="Specify the username to use") - parser.add_argument('-p', '--password', action='store_true', default=False, help="Prompt for password to use") - parser.add_argument('-s', '--site', dest='site', action='append', - help="Filter devices by site (include argument once per site)") - parser.add_argument('-n', '--name', dest='name', help="Filter devices by name (regular expression)") - parser.add_argument('--full', action='store_true', default=False, help="For inventory update for all devices") - parser.add_argument('--fake', action='store_true', default=False, help="Do not actually update database") - - def handle(self, *args, **options): - - def create_inventory_items(inventory_items, parent=None): - for item in inventory_items: - i = InventoryItem(device=device, parent=parent, name=item['name'], part_id=item['part_id'], - serial=item['serial'], discovered=True) - i.save() - create_inventory_items(item.get('items', []), parent=i) - - # Credentials - if options['username']: - self.username = options['username'] - if options['password']: - self.password = getpass("Password: ") - - # Attempt to inventory only active devices - device_list = Device.objects.filter(status=DEVICE_STATUS_ACTIVE) - - # --site: Include only devices belonging to specified site(s) - if options['site']: - sites = Site.objects.filter(slug__in=options['site']) - if sites: - site_names = [s.name for s in sites] - self.stdout.write("Running inventory for these sites: {}".format(', '.join(site_names))) - else: - raise CommandError("One or more sites specified but none found.") - device_list = device_list.filter(site__in=sites) - - # --name: Filter devices by name matching a regex - if options['name']: - device_list = device_list.filter(name__iregex=options['name']) - - # --full: Gather inventory data for *all* devices - if options['full']: - self.stdout.write("WARNING: Running inventory for all devices! Prior data will be overwritten. (--full)") - - # --fake: Gathering data but not updating the database - if options['fake']: - self.stdout.write("WARNING: Inventory data will not be saved! (--fake)") - - device_count = device_list.count() - self.stdout.write("** Found {} devices...".format(device_count)) - - for i, device in enumerate(device_list, start=1): - - self.stdout.write("[{}/{}] {}: ".format(i, device_count, device.name), ending='') - - # Skip inactive devices - if not device.status: - self.stdout.write("Skipped (not active)") - continue - - # Skip devices without primary_ip set - if not device.primary_ip: - self.stdout.write("Skipped (no primary IP set)") - continue - - # Skip devices which have already been inventoried if not doing a full update - if device.serial and not options['full']: - self.stdout.write("Skipped (Serial: {})".format(device.serial)) - continue - - RPC = device.get_rpc_client() - if not RPC: - self.stdout.write("Skipped (no RPC client available for platform {})".format(device.platform)) - continue - - # Connect to device and retrieve inventory info - try: - with RPC(device, self.username, self.password) as rpc_client: - inventory = rpc_client.get_inventory() - except KeyboardInterrupt: - raise - except (AuthenticationError, AuthenticationException): - self.stdout.write("Authentication error!") - continue - except Exception as e: - self.stdout.write("Error: {}".format(e)) - continue - - if options['verbosity'] > 1: - self.stdout.write("") - self.stdout.write("\tSerial: {}".format(inventory['chassis']['serial'])) - self.stdout.write("\tDescription: {}".format(inventory['chassis']['description'])) - for item in inventory['items']: - self.stdout.write("\tItem: {} / {} ({})".format(item['name'], item['part_id'], - item['serial'])) - else: - self.stdout.write("{} ({})".format(inventory['chassis']['description'], inventory['chassis']['serial'])) - - if not options['fake']: - with transaction.atomic(): - # Update device serial - if device.serial != inventory['chassis']['serial']: - device.serial = inventory['chassis']['serial'] - device.save() - InventoryItem.objects.filter(device=device, discovered=True).delete() - create_inventory_items(inventory.get('items', [])) - - self.stdout.write("Finished!") diff --git a/netbox/extras/rpc.py b/netbox/extras/rpc.py deleted file mode 100644 index 60fc9208f..000000000 --- a/netbox/extras/rpc.py +++ /dev/null @@ -1,235 +0,0 @@ -import re -import time - -import paramiko -import xmltodict -from ncclient import manager - -CONNECT_TIMEOUT = 5 # seconds - - -class RPCClient(object): - - def __init__(self, device, username='', password=''): - self.username = username - self.password = password - try: - self.host = str(device.primary_ip.address.ip) - except AttributeError: - raise Exception("Specified device ({}) does not have a primary IP defined.".format(device)) - - def get_inventory(self): - """ - Returns a dictionary representing the device chassis and installed inventory items. - - { - 'chassis': { - 'serial': , - 'description': , - } - 'items': [ - { - 'name': , - 'part_id': , - 'serial': , - }, - ... - ] - } - """ - raise NotImplementedError("Feature not implemented for this platform.") - - -class SSHClient(RPCClient): - def __enter__(self): - - self.ssh = paramiko.SSHClient() - self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - self.ssh.connect( - self.host, - username=self.username, - password=self.password, - timeout=CONNECT_TIMEOUT, - allow_agent=False, - look_for_keys=False, - ) - except paramiko.AuthenticationException: - # Try default credentials if the configured creds don't work - try: - default_creds = self.default_credentials - if default_creds.get('username') and default_creds.get('password'): - self.ssh.connect( - self.host, - username=default_creds['username'], - password=default_creds['password'], - timeout=CONNECT_TIMEOUT, - allow_agent=False, - look_for_keys=False, - ) - else: - raise ValueError('default_credentials are incomplete.') - except AttributeError: - raise paramiko.AuthenticationException - - self.session = self.ssh.invoke_shell() - self.session.recv(1000) - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.ssh.close() - - def _send(self, cmd, pause=1): - self.session.send('{}\n'.format(cmd)) - data = '' - time.sleep(pause) - while self.session.recv_ready(): - data += self.session.recv(4096).decode() - if not data: - break - return data - - -class JunosNC(RPCClient): - """ - NETCONF client for Juniper Junos devices - """ - - def __enter__(self): - - # Initiate a connection to the device - self.manager = manager.connect(host=self.host, username=self.username, password=self.password, - hostkey_verify=False, timeout=CONNECT_TIMEOUT) - - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - - # Close the connection to the device - self.manager.close_session() - - def get_inventory(self): - - def glean_items(node, depth=0): - items = [] - items_list = node.get('chassis{}-module'.format('-sub' * depth), []) - # Junos like to return single children directly instead of as a single-item list - if hasattr(items_list, 'items'): - items_list = [items_list] - for item in items_list: - m = { - 'name': item['name'], - 'part_id': item.get('model-number') or item.get('part-number', ''), - 'serial': item.get('serial-number', ''), - } - child_items = glean_items(item, depth + 1) - if child_items: - m['items'] = child_items - items.append(m) - return items - - rpc_reply = self.manager.dispatch('get-chassis-inventory') - inventory_raw = xmltodict.parse(rpc_reply.xml)['rpc-reply']['chassis-inventory']['chassis'] - - result = dict() - - # Gather chassis data - result['chassis'] = { - 'serial': inventory_raw['serial-number'], - 'description': inventory_raw['description'], - } - - # Gather inventory items - result['items'] = glean_items(inventory_raw) - - return result - - -class IOSSSH(SSHClient): - """ - SSH client for Cisco IOS devices - """ - - def get_inventory(self): - def version(): - - def parse(cmd_out, rex): - for i in cmd_out: - match = re.search(rex, i) - if match: - return match.groups()[0] - - sh_ver = self._send('show version').split('\r\n') - return { - 'serial': parse(sh_ver, r'Processor board ID ([^\s]+)'), - 'description': parse(sh_ver, r'cisco ([^\s]+)') - } - - def items(chassis_serial=None): - cmd = self._send('show inventory').split('\r\n\r\n') - for i in cmd: - i_fmt = i.replace('\r\n', ' ') - try: - m_name = re.search(r'NAME: "([^"]+)"', i_fmt).group(1) - m_pid = re.search(r'PID: ([^\s]+)', i_fmt).group(1) - m_serial = re.search(r'SN: ([^\s]+)', i_fmt).group(1) - # Omit built-in items and those with no PID - if m_serial != chassis_serial and m_pid.lower() != 'unspecified': - yield { - 'name': m_name, - 'part_id': m_pid, - 'serial': m_serial, - } - except AttributeError: - continue - - self._send('term length 0') - sh_version = version() - - return { - 'chassis': sh_version, - 'items': list(items(chassis_serial=sh_version.get('serial'))) - } - - -class OpengearSSH(SSHClient): - """ - SSH client for Opengear devices - """ - default_credentials = { - 'username': 'root', - 'password': 'default', - } - - def get_inventory(self): - - try: - stdin, stdout, stderr = self.ssh.exec_command("showserial") - serial = stdout.readlines()[0].strip() - except Exception: - raise RuntimeError("Failed to glean chassis serial from device.") - # Older models don't provide serial info - if serial == "No serial number information available": - serial = '' - - try: - stdin, stdout, stderr = self.ssh.exec_command("config -g config.system.model") - description = stdout.readlines()[0].split(' ', 1)[1].strip() - except Exception: - raise RuntimeError("Failed to glean chassis description from device.") - - return { - 'chassis': { - 'serial': serial, - 'description': description, - }, - 'items': [], - } - - -# For mapping platform -> NC client -RPC_CLIENTS = { - 'juniper-junos': JunosNC, - 'cisco-ios': IOSSSH, - 'opengear': OpengearSSH, -}