mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Closes #2367: Remove deprecated RPCClient functionality
This commit is contained in:
parent
7145f86a6e
commit
c4be440cd1
@ -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):
|
||||
|
@ -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)'],
|
||||
]
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -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(),
|
||||
}
|
||||
|
17
netbox/dcim/migrations/0062_remove_platform_rpc_client.py
Normal file
17
netbox/dcim/migrations/0062_remove_platform_rpc_client.py
Normal 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',
|
||||
),
|
||||
]
|
@ -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
|
||||
|
@ -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!")
|
@ -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,
|
||||
}
|
Loading…
Reference in New Issue
Block a user