Closes #2367: Remove deprecated RPCClient functionality

This commit is contained in:
Jeremy Stretch 2018-08-16 12:21:24 -04:00
parent 7145f86a6e
commit c4be440cd1
9 changed files with 27 additions and 403 deletions

View File

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

View File

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

View File

@ -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"
}
},
{

View File

@ -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"
}
}
]

View File

@ -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(),
}

View File

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

View File

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

View File

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

View File

@ -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': <str>,
'description': <str>,
}
'items': [
{
'name': <str>,
'part_id': <str>,
'serial': <str>,
},
...
]
}
"""
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,
}