diff --git a/docs/configuration/napalm.md b/docs/configuration/napalm.md index 253bea297..e9fc91b72 100644 --- a/docs/configuration/napalm.md +++ b/docs/configuration/napalm.md @@ -1,5 +1,7 @@ # NAPALM Parameters +!!! **Note:** As of NetBox v3.5, NAPALM integration has been moved to a plugin and these configuration parameters are now deprecated. + ## NAPALM_USERNAME ## NAPALM_PASSWORD diff --git a/docs/features/api-integration.md b/docs/features/api-integration.md index 50c31ec4f..c3b78de47 100644 --- a/docs/features/api-integration.md +++ b/docs/features/api-integration.md @@ -36,6 +36,8 @@ To learn more about this feature, check out the [webhooks documentation](../inte To learn more about this feature, check out the [NAPALM documentation](../integrations/napalm.md). +As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. + ## Prometheus Metrics NetBox includes a special `/metrics` view which exposes metrics for a [Prometheus](https://prometheus.io/) scraper, powered by the open source [django-prometheus](https://github.com/korfuri/django-prometheus) library. To learn more about this feature, check out the [Prometheus metrics documentation](../integrations/prometheus-metrics.md). diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index 26a2bf917..dc6c38977 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -199,14 +199,6 @@ When you have finished modifying the configuration, remember to save the file. All Python packages required by NetBox are listed in `requirements.txt` and will be installed automatically. NetBox also supports some optional packages. If desired, these packages must be listed in `local_requirements.txt` within the NetBox root directory. -### NAPALM - -Integration with the [NAPALM automation](../integrations/napalm.md) library allows NetBox to fetch live data from devices and return it to a requester via its REST API. The `NAPALM_USERNAME` and `NAPALM_PASSWORD` configuration parameters define the credentials to be used when connecting to a device. - -```no-highlight -sudo sh -c "echo 'napalm' >> /opt/netbox/local_requirements.txt" -``` - ### Remote File Storage By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`. diff --git a/docs/integrations/napalm.md b/docs/integrations/napalm.md index 60d8014e2..e7e0f108c 100644 --- a/docs/integrations/napalm.md +++ b/docs/integrations/napalm.md @@ -1,74 +1,3 @@ # NAPALM -NetBox supports integration with the [NAPALM automation](https://github.com/napalm-automation/napalm) library. NAPALM allows NetBox to serve a proxy for operational data, fetching live data from network devices and returning it to a requester via its REST API. Note that NetBox does not store any NAPALM data locally. - -The NetBox UI will display tabs for status, LLDP neighbors, and configuration under the device view if the following conditions are met: - -* Device status is "Active" -* A primary IP has been assigned to the device -* A platform with a NAPALM driver has been assigned -* The authenticated user has the `dcim.napalm_read_device` permission - -!!! note - To enable this integration, the NAPALM library must be installed. See [installation steps](../../installation/3-netbox/#napalm) for more information. - -Below is an example REST API request and response: - -```no-highlight -GET /api/dcim/devices/1/napalm/?method=get_environment - -{ - "get_environment": { - ... - } -} -``` - -!!! note - To make NAPALM requests via the NetBox REST API, a NetBox user must have assigned a permission granting the `napalm_read` action for the device object type. - -## Authentication - -By default, the [`NAPALM_USERNAME`](../configuration/napalm.md#napalm_username) and [`NAPALM_PASSWORD`](../configuration/napalm.md#napalm_password) configuration parameters are used for NAPALM authentication. They can be overridden for an individual API call by specifying the `X-NAPALM-Username` and `X-NAPALM-Password` headers. - -``` -$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \ --H "Authorization: Token $TOKEN" \ --H "Content-Type: application/json" \ --H "Accept: application/json; indent=4" \ --H "X-NAPALM-Username: foo" \ --H "X-NAPALM-Password: bar" -``` - -## Method Support - -The list of supported NAPALM methods depends on the [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html#general-support-matrix) configured for the platform of a device. Because there is no granular mechanism in place for limiting potentially disruptive requests, NetBox supports only read-only [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods. - -## Multiple Methods - -It is possible to request the output of multiple NAPALM methods in a single API request by passing multiple `method` parameters. For example: - -```no-highlight -GET /api/dcim/devices/1/napalm/?method=get_ntp_servers&method=get_ntp_peers - -{ - "get_ntp_servers": { - ... - }, - "get_ntp_peers": { - ... - } -} -``` - -## Optional Arguments - -The behavior of NAPALM drivers can be adjusted according to the [optional arguments](https://napalm.readthedocs.io/en/latest/support/index.html#optional-arguments). NetBox exposes those arguments using headers prefixed with `X-NAPALM-`. For example, the SSH port is changed to 2222 in this API call: - -``` -$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \ --H "Authorization: Token $TOKEN" \ --H "Content-Type: application/json" \ --H "Accept: application/json; indent=4" \ --H "X-NAPALM-port: 2222" -``` +As of NetBox v3.5, NAPALM integration has been moved to a plugin. Please see the [netbox_napalm_plugin](https://github.com/netbox-community/netbox-napalm) for installation instructions. **Note:** All previously entered NAPALM configuration data will be saved and automatically imported by the new plugin. diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 464c3eb47..a3a478be5 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -51,7 +51,6 @@ class DataSourceForm(NetBoxModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Determine the selected backend type backend_type = get_field_value(self, 'type') backend = registry['data_backends'].get(backend_type) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 21b05fece..cd911f3cb 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -419,124 +419,6 @@ class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet): return serializers.DeviceWithConfigContextSerializer - @swagger_auto_schema( - manual_parameters=[ - Parameter( - name='method', - in_='query', - required=True, - type=openapi.TYPE_STRING - ) - ], - responses={'200': serializers.DeviceNAPALMSerializer} - ) - @action(detail=True, url_path='napalm') - def napalm(self, request, pk): - """ - Execute a NAPALM method on a Device - """ - device = get_object_or_404(self.queryset, pk=pk) - if not device.primary_ip: - raise ServiceUnavailable("This device does not have a primary IP address configured.") - if device.platform is None: - raise ServiceUnavailable("No platform is configured for this device.") - if not device.platform.napalm_driver: - raise ServiceUnavailable(f"No NAPALM driver is configured for this device's platform: {device.platform}.") - - # Check for primary IP address from NetBox object - if device.primary_ip: - host = str(device.primary_ip.address.ip) - else: - # Raise exception for no IP address and no Name if device.name does not exist - if not device.name: - raise ServiceUnavailable( - "This device does not have a primary IP address or device name to lookup configured." - ) - try: - # Attempt to complete a DNS name resolution if no primary_ip is set - host = socket.gethostbyname(device.name) - except socket.gaierror: - # Name lookup failure - raise ServiceUnavailable( - f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or " - f"setup name resolution.") - - # Check that NAPALM is installed - try: - import napalm - from napalm.base.exceptions import ModuleImportError - except ModuleNotFoundError as e: - if getattr(e, 'name') == 'napalm': - raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.") - raise e - - # Validate the configured driver - try: - driver = napalm.get_network_driver(device.platform.napalm_driver) - except ModuleImportError: - raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format( - device.platform, device.platform.napalm_driver - )) - - # Verify user permission - if not request.user.has_perm('dcim.napalm_read_device'): - return HttpResponseForbidden() - - napalm_methods = request.GET.getlist('method') - response = {m: None for m in napalm_methods} - - config = get_config() - username = config.NAPALM_USERNAME - password = config.NAPALM_PASSWORD - timeout = config.NAPALM_TIMEOUT - optional_args = config.NAPALM_ARGS.copy() - if device.platform.napalm_args is not None: - optional_args.update(device.platform.napalm_args) - - # Update NAPALM parameters according to the request headers - for header in request.headers: - if header[:9].lower() != 'x-napalm-': - continue - - key = header[9:] - if key.lower() == 'username': - username = request.headers[header] - elif key.lower() == 'password': - password = request.headers[header] - elif key: - optional_args[key.lower()] = request.headers[header] - - # Connect to the device - d = driver( - hostname=host, - username=username, - password=password, - timeout=timeout, - optional_args=optional_args - ) - try: - d.open() - except Exception as e: - raise ServiceUnavailable("Error connecting to the device at {}: {}".format(host, e)) - - # Validate and execute each specified NAPALM method - for method in napalm_methods: - if not hasattr(driver, method): - response[method] = {'error': 'Unknown NAPALM method'} - continue - if not method.startswith('get_'): - response[method] = {'error': 'Only get_* NAPALM methods are supported'} - continue - try: - response[method] = getattr(d, method)() - except NotImplementedError: - response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)} - except Exception as e: - response[method] = {'error': 'Method {} failed: {}'.format(method, e)} - d.close() - - return Response(response) - class VirtualDeviceContextViewSet(NetBoxModelViewSet): queryset = VirtualDeviceContext.objects.prefetch_related( diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index fd3f9425e..7a27ef110 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -806,7 +806,7 @@ class PlatformFilterSet(OrganizationalModelFilterSet): class Meta: model = Platform - fields = ['id', 'name', 'slug', 'napalm_driver', 'description'] + fields = ['id', 'name', 'slug', 'description'] class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet, LocalConfigContextFilterSet): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index ea7ab65cd..5b605dbb4 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -476,10 +476,6 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): queryset=Manufacturer.objects.all(), required=False ) - napalm_driver = forms.CharField( - max_length=50, - required=False - ) config_template = DynamicModelChoiceField( queryset=ConfigTemplate.objects.all(), required=False @@ -491,9 +487,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): model = Platform fieldsets = ( - (None, ('manufacturer', 'config_template', 'napalm_driver', 'description')), + (None, ('manufacturer', 'config_template', 'description')), ) - nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description') + nullable_fields = ('manufacturer', 'config_template', 'description') class DeviceBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index e495ec34d..a72bdbce9 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -342,7 +342,7 @@ class PlatformImportForm(NetBoxModelImportForm): class Meta: model = Platform fields = ( - 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags', + 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', ) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 34f91bbe8..74e697dde 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -451,19 +451,15 @@ class PlatformForm(NetBoxModelForm): fieldsets = ( ('Platform', ( - 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags', - + 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', )), ) class Meta: model = Platform fields = [ - 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags', + 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', ] - widgets = { - 'napalm_args': forms.Textarea(), - } class DeviceForm(TenancyForm, NetBoxModelForm): diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index bae4f030f..f70c729f4 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -172,7 +172,6 @@ class PlatformIndex(SearchIndex): fields = ( ('name', 100), ('slug', 110), - ('napalm_driver', 300), ('description', 500), ) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index f68960965..bed32251c 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -133,11 +133,11 @@ class PlatformTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = models.Platform fields = ( - 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'napalm_driver', - 'napalm_args', 'description', 'tags', 'actions', 'created', 'last_updated', + 'pk', 'id', 'name', 'manufacturer', 'device_count', 'vm_count', 'slug', 'config_template', 'description', + 'tags', 'actions', 'created', 'last_updated', ) default_columns = ( - 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'napalm_driver', 'description', + 'pk', 'name', 'manufacturer', 'device_count', 'vm_count', 'description', ) diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index c78b592d3..01ef4a87b 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -1469,9 +1469,9 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests): Manufacturer.objects.bulk_create(manufacturers) platforms = ( - Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], napalm_driver='driver-1', description='A'), - Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], napalm_driver='driver-2', description='B'), - Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], napalm_driver='driver-3', description='C'), + Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0], description='A'), + Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturers[1], description='B'), + Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturers[2], description='C'), ) Platform.objects.bulk_create(platforms) @@ -1487,10 +1487,6 @@ class PlatformTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'description': ['A', 'B']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_napalm_driver(self): - params = {'napalm_driver': ['driver-1', 'driver-2']} - self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_manufacturer(self): manufacturers = Manufacturer.objects.all()[:2] params = {'manufacturer_id': [manufacturers[0].pk, manufacturers[1].pk]} diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index bba91412d..eef78c6c6 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -1591,8 +1591,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase): 'name': 'Platform X', 'slug': 'platform-x', 'manufacturer': manufacturer.pk, - 'napalm_driver': 'junos', - 'napalm_args': None, 'description': 'A new platform', 'tags': [t.pk for t in tags], } @@ -1612,7 +1610,6 @@ class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase): ) cls.bulk_edit_data = { - 'napalm_driver': 'ios', 'description': 'New description', } diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 62359553d..38eb302a8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -2080,71 +2080,6 @@ class DeviceBulkRenameView(generic.BulkRenameView): table = tables.DeviceTable -# -# Device NAPALM views -# - -class NAPALMViewTab(ViewTab): - - def render(self, instance): - # Display NAPALM tabs only for devices which meet certain requirements - if not ( - instance.status == 'active' and - instance.primary_ip and - instance.platform and - instance.platform.napalm_driver - ): - return None - return super().render(instance) - - -@register_model_view(Device, 'status') -class DeviceStatusView(generic.ObjectView): - additional_permissions = ['dcim.napalm_read_device'] - queryset = Device.objects.all() - template_name = 'dcim/device/status.html' - tab = NAPALMViewTab( - label=_('Status'), - permission='dcim.napalm_read_device', - weight=3000 - ) - - -@register_model_view(Device, 'lldp_neighbors', path='lldp-neighbors') -class DeviceLLDPNeighborsView(generic.ObjectView): - additional_permissions = ['dcim.napalm_read_device'] - queryset = Device.objects.all() - template_name = 'dcim/device/lldp_neighbors.html' - tab = NAPALMViewTab( - label=_('LLDP Neighbors'), - permission='dcim.napalm_read_device', - weight=3100 - ) - - def get_extra_context(self, request, instance): - interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related( - '_path' - ).exclude( - type__in=NONCONNECTABLE_IFACE_TYPES - ) - - return { - 'interfaces': interfaces, - } - - -@register_model_view(Device, 'config') -class DeviceConfigView(generic.ObjectView): - additional_permissions = ['dcim.napalm_read_device'] - queryset = Device.objects.all() - template_name = 'dcim/device/config.html' - tab = NAPALMViewTab( - label=_('Config'), - permission='dcim.napalm_read_device', - weight=3200 - ) - - # # Modules # diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 837a8f2d3..18cc860b1 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -35,10 +35,6 @@ class ConfigRevisionAdmin(admin.ModelAdmin): 'fields': ('CUSTOM_VALIDATORS',), 'classes': ('monospace',), }), - ('NAPALM', { - 'fields': ('NAPALM_USERNAME', 'NAPALM_PASSWORD', 'NAPALM_TIMEOUT', 'NAPALM_ARGS'), - 'classes': ('monospace',), - }), ('User Preferences', { 'fields': ('DEFAULT_USER_PREFERENCES',), }), diff --git a/netbox/project-static/bundle.js b/netbox/project-static/bundle.js index 76a1581ad..6f651cd05 100644 --- a/netbox/project-static/bundle.js +++ b/netbox/project-static/bundle.js @@ -40,9 +40,6 @@ async function bundleGraphIQL() { async function bundleNetBox() { const entryPoints = { netbox: 'src/index.ts', - lldp: 'src/device/lldp.ts', - config: 'src/device/config.ts', - status: 'src/device/status.ts', }; try { const result = await esbuild.build({ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 384195df5..5481e38a3 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/src/device/config.ts b/netbox/project-static/src/device/config.ts deleted file mode 100644 index c9c19e8d3..000000000 --- a/netbox/project-static/src/device/config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createToast } from '../bs'; -import { apiGetBase, getNetboxData, hasError, toggleLoader } from '../util'; - -/** - * Initialize device config elements. - */ -function initConfig(): void { - toggleLoader('show'); - const url = getNetboxData('data-object-url'); - - if (url !== null) { - apiGetBase(url) - .then(data => { - if (hasError(data)) { - createToast('danger', 'Error Fetching Device Config', data.error).show(); - console.error(data.error); - return; - } else if (hasError>(data.get_config)) { - createToast('danger', 'Error Fetching Device Config', data.get_config.error).show(); - console.error(data.get_config.error); - return; - } else { - const configTypes = ['running', 'startup', 'candidate'] as DeviceConfigType[]; - - for (const configType of configTypes) { - const element = document.getElementById(`${configType}_config`); - if (element !== null) { - const config = data.get_config[configType]; - if (typeof config === 'string') { - // If the returned config is a string, set the element innerHTML as-is. - element.innerHTML = config; - } else { - // If the returned config is an object (dict), convert it to JSON. - element.innerHTML = JSON.stringify(data.get_config[configType], null, 2); - } - } - } - } - }) - .finally(() => { - toggleLoader('hide'); - }); - } -} - -if (document.readyState !== 'loading') { - initConfig(); -} else { - document.addEventListener('DOMContentLoaded', initConfig); -} diff --git a/netbox/project-static/src/device/lldp.ts b/netbox/project-static/src/device/lldp.ts deleted file mode 100644 index ebf71138c..000000000 --- a/netbox/project-static/src/device/lldp.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { createToast } from '../bs'; -import { getNetboxData, apiGetBase, hasError, isTruthy, toggleLoader } from '../util'; - -// Match an interface name that begins with a capital letter and is followed by at least one other -// alphabetic character, and ends with a forward-slash-separated numeric sequence such as 0/1/2. -const CISCO_IOS_PATTERN = new RegExp(/^([A-Z][A-Za-z]+)[^0-9]*([0-9/]+)$/); - -// Mapping of overrides to default Cisco IOS interface alias behavior (default behavior is to use -// the first two characters). -const CISCO_IOS_OVERRIDES = new Map([ - // Cisco IOS abbreviates 25G (TwentyFiveGigE) interfaces as 'Twe'. - ['TwentyFiveGigE', 'Twe'], -]); - -/** - * Get an attribute from a row's cell. - * - * @param row Interface row - * @param query CSS media query - * @param attr Cell attribute - */ -function getData(row: HTMLTableRowElement, query: string, attr: string): string | null { - return row.querySelector(query)?.getAttribute(attr) ?? null; -} - -/** - * Get preconfigured alias for given interface. Primarily for matching long-form Cisco IOS - * interface names with short-form Cisco IOS interface names. For example, `GigabitEthernet0/1/2` - * would become `Gi0/1/2`. - * - * This should probably be replaced with something in the primary application (Django), such as - * a database field attached to given interface types. However, this is a temporary measure to - * replace the functionality of this one-liner: - * - * @see https://github.com/netbox-community/netbox/blob/9cc4992fad2fe04ef0211d998c517414e8871d8c/netbox/templates/dcim/device/lldp_neighbors.html#L69 - * - * @param name Long-form/original interface name. - */ -function getInterfaceAlias(name: string | null): string | null { - if (name === null) { - return name; - } - if (name.match(CISCO_IOS_PATTERN)) { - // Extract the base name and numeric portions of the interface. For example, an input interface - // of `GigabitEthernet0/0/1` would result in an array of `['GigabitEthernet', '0/0/1']`. - const [base, numeric] = (name.match(CISCO_IOS_PATTERN) ?? []).slice(1, 3); - - if (isTruthy(base) && isTruthy(numeric)) { - // Check the override map and use its value if the base name is present in the map. - // Otherwise, use the first two characters of the base name. For example, - // `GigabitEthernet0/0/1` would become `Gi0/0/1`, but `TwentyFiveGigE0/0/1` would become - // `Twe0/0/1`. - const aliasBase = CISCO_IOS_OVERRIDES.get(base) || base.slice(0, 2); - return `${aliasBase}${numeric}`; - } - } - return name; -} - -/** - * Update row styles based on LLDP neighbor data. - */ -function updateRowStyle(data: LLDPNeighborDetail) { - for (const [fullIface, neighbors] of Object.entries(data.get_lldp_neighbors_detail)) { - const [iface] = fullIface.split('.'); - - const row = document.getElementById(iface) as Nullable; - - if (row !== null) { - for (const neighbor of neighbors) { - const deviceCell = row.querySelector('td.device'); - const interfaceCell = row.querySelector('td.interface'); - const configuredDevice = getData(row, 'td.configured_device', 'data'); - const configuredChassis = getData(row, 'td.configured_chassis', 'data-chassis'); - const configuredIface = getData(row, 'td.configured_interface', 'data'); - - const interfaceAlias = getInterfaceAlias(configuredIface); - - const remoteName = neighbor.remote_system_name ?? ''; - const remotePort = neighbor.remote_port ?? ''; - const [neighborDevice] = remoteName.split('.'); - const [neighborIface] = remotePort.split('.'); - - if (deviceCell !== null) { - deviceCell.innerText = neighborDevice; - } - - if (interfaceCell !== null) { - interfaceCell.innerText = neighborIface; - } - - // Interface has an LLDP neighbor, but the neighbor is not configured in NetBox. - const nonConfiguredDevice = !isTruthy(configuredDevice) && isTruthy(neighborDevice); - - // NetBox device or chassis matches LLDP neighbor. - const validNode = - configuredDevice === neighborDevice || configuredChassis === neighborDevice; - - // NetBox configured interface matches LLDP neighbor interface. - const validInterface = - configuredIface === neighborIface || interfaceAlias === neighborIface; - - if (nonConfiguredDevice) { - row.classList.add('info'); - } else if (validNode && validInterface) { - row.classList.add('success'); - } else { - row.classList.add('danger'); - } - } - } - } -} - -/** - * Initialize LLDP Neighbor fetching. - */ -function initLldpNeighbors() { - toggleLoader('show'); - const url = getNetboxData('object-url'); - if (url !== null) { - apiGetBase(url) - .then(data => { - if (hasError(data)) { - createToast('danger', 'Error Retrieving LLDP Neighbor Information', data.error).show(); - toggleLoader('hide'); - return; - } else { - updateRowStyle(data); - } - return; - }) - .finally(() => { - toggleLoader('hide'); - }); - } -} - -if (document.readyState !== 'loading') { - initLldpNeighbors(); -} else { - document.addEventListener('DOMContentLoaded', initLldpNeighbors); -} diff --git a/netbox/project-static/src/device/status.ts b/netbox/project-static/src/device/status.ts deleted file mode 100644 index 8261ebc82..000000000 --- a/netbox/project-static/src/device/status.ts +++ /dev/null @@ -1,379 +0,0 @@ -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import timezone from 'dayjs/plugin/timezone'; -import duration from 'dayjs/plugin/duration'; -import advancedFormat from 'dayjs/plugin/advancedFormat'; - -import { createToast } from '../bs'; -import { apiGetBase, getNetboxData, hasError, toggleLoader, createElement, cToF } from '../util'; - -type Uptime = { - utc: string; - zoned: string | null; - duration: string; -}; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(advancedFormat); -dayjs.extend(duration); - -const factKeys = [ - 'hostname', - 'fqdn', - 'vendor', - 'model', - 'serial_number', - 'os_version', -] as (keyof DeviceFacts)[]; - -type DurationKeys = 'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds'; -const formatKeys = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'] as DurationKeys[]; - -/** - * From a number of seconds that have elapsed since reboot, extract human-readable dates in the - * following formats: - * - Relative time since reboot (e.g. 1 month, 28 days, 1 hour, 30 seconds). - * - Time stamp in browser-relative timezone. - * - Time stamp in UTC. - * @param seconds Seconds since reboot. - */ -function getUptime(seconds: number): Uptime { - const relDate = new Date(); - - // Get the user's UTC offset, to determine if the user is in UTC or not. - const offset = relDate.getTimezoneOffset(); - const relNow = dayjs(relDate); - - // Get a dayjs object for the device reboot time (now - number of seconds). - const relThen = relNow.subtract(seconds, 'seconds'); - - // Get a human-readable version of the time in UTC. - const utc = relThen.tz('Etc/UTC').format('YYYY-MM-DD HH:MM:ss z'); - - // We only want to show the UTC time if the user is not already in UTC time. - let zoned = null; - if (offset !== 0) { - // If the user is not in UTC time, return a human-readable version in the user's timezone. - zoned = relThen.format('YYYY-MM-DD HH:MM:ss z'); - } - // Get a dayjs duration object to create a human-readable relative time string. - const between = dayjs.duration(seconds, 'seconds'); - - // Array of all non-zero-value duration properties. For example, if duration.year() is 0, we - // don't care about it and shouldn't show it to the user. - let parts = [] as string[]; - for (const key of formatKeys) { - // Get the property value. For example, duration.year(), duration.month(), etc. - const value = between[key](); - if (value === 1) { - // If the duration for this key is 1, drop the trailing 's'. For example, '1 seconds' would - // become '1 second'. - const label = key.replace(/s$/, ''); - parts = [...parts, `${value} ${label}`]; - } else if (value > 1) { - // If the duration for this key is more than one, add it to the array as-is. - parts = [...parts, `${value} ${key}`]; - } - } - // Set the duration to something safe, so we don't show 'undefined' or an empty string to the user. - let duration = 'None'; - if (parts.length > 0) { - // If the array actually has elements, reassign the duration to a human-readable version. - duration = parts.join(', '); - } - - return { utc, zoned, duration }; -} - -/** - * After the `get_facts` result is received, parse its content and update HTML elements - * accordingly. - * - * @param facts NAPALM Device Facts - */ -function processFacts(facts: DeviceFacts): void { - for (const key of factKeys) { - if (key in facts) { - // Find the target element which should have its innerHTML/innerText set to a NAPALM value. - const element = document.getElementById(key); - if (element !== null) { - element.innerHTML = String(facts[key]); - } - } - } - const { uptime } = facts; - const { utc, zoned, duration } = getUptime(uptime); - - // Find the duration (relative time) element and set its value. - const uptimeDurationElement = document.getElementById('uptime-duration'); - if (uptimeDurationElement !== null) { - uptimeDurationElement.innerHTML = duration; - } - // Find the time stamp element and set its value. - const uptimeElement = document.getElementById('uptime'); - if (uptimeElement !== null) { - if (zoned === null) { - // If the user is in UTC time, only add the UTC time stamp. - uptimeElement.innerHTML = utc; - } else { - // Otherwise, add both time stamps. - uptimeElement.innerHTML = [zoned, `${utc}`].join(''); - } - } -} - -/** - * Insert a title row before the next table row. The title row describes each environment key/value - * pair from the NAPALM response. - * - * @param next Next adjacent element. For example, if this is the CPU data, `next` would be the - * memory row. - * @param title1 Column 1 Title - * @param title2 Column 2 Title - */ -function insertTitleRow(next: E, title1: string, title2: string): void { - // Create cell element that contains the key title. - const col1Title = createElement('th', { innerText: title1 }, ['border-end', 'text-end']); - // Create cell element that contains the value title. - const col2Title = createElement('th', { innerText: title2 }, ['border-start', 'text-start']); - // Create title row element with the two header cells as children. - const titleRow = createElement('tr', {}, [], [col1Title, col2Title]); - // Insert the entire row just before the beginning of the next row (i.e., at the end of this row). - next.insertAdjacentElement('beforebegin', titleRow); -} - -/** - * Insert a "No Data" row, for when the NAPALM response doesn't contain this type of data. - * - * @param next Next adjacent element.For example, if this is the CPU data, `next` would be the - * memory row. - */ -function insertNoneRow>(next: E): void { - const none = createElement('td', { colSpan: '2', innerText: 'No Data' }, [ - 'text-muted', - 'text-center', - ]); - const titleRow = createElement('tr', {}, [], [none]); - if (next !== null) { - next.insertAdjacentElement('beforebegin', titleRow); - } -} - -function getNext(id: string): Nullable { - const element = document.getElementById(id); - if (element !== null) { - return element.nextElementSibling as Nullable; - } - return null; -} - -/** - * Create & insert table rows for each CPU in the NAPALM response. - * - * @param cpu NAPALM CPU data. - */ -function processCpu(cpu: DeviceEnvironment['cpu']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-cpu'); - if (typeof cpu !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Name', 'Usage'); - for (const [core, data] of Object.entries(cpu)) { - const usage = data['%usage']; - const kCell = createElement('td', { innerText: core }, ['border-end', 'text-end']); - const vCell = createElement('td', { innerText: `${usage} %` }, [ - 'border-start', - 'text-start', - ]); - const row = createElement('tr', {}, [], [kCell, vCell]); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for the memory in the NAPALM response. - * - * @param mem NAPALM memory data. - */ -function processMemory(mem: DeviceEnvironment['memory']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-memory'); - if (typeof mem !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Available', 'Used'); - const { available_ram: avail, used_ram: used } = mem; - const aCell = createElement('td', { innerText: avail }, ['border-end', 'text-end']); - const uCell = createElement('td', { innerText: used }, ['border-start', 'text-start']); - const row = createElement('tr', {}, [], [aCell, uCell]); - next.insertAdjacentElement('beforebegin', row); - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for each temperature sensor in the NAPALM response. - * - * @param temp NAPALM temperature data. - */ -function processTemp(temp: DeviceEnvironment['temperature']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-temperature'); - if (typeof temp !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Sensor', 'Value'); - for (const [sensor, data] of Object.entries(temp)) { - const tempC = data.temperature; - const tempF = cToF(tempC); - const innerHTML = `${tempC} °C ${tempF} °F`; - const status = data.is_alert ? 'warning' : data.is_critical ? 'danger' : 'success'; - const kCell = createElement('td', { innerText: sensor }, ['border-end', 'text-end']); - const vCell = createElement('td', { innerHTML }, ['border-start', 'text-start']); - const row = createElement('tr', {}, [`table-${status}`], [kCell, vCell]); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for each fan in the NAPALM response. - * - * @param fans NAPALM fan data. - */ -function processFans(fans: DeviceEnvironment['fans']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-fans'); - if (typeof fans !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'Fan', 'Status'); - for (const [fan, data] of Object.entries(fans)) { - const { status } = data; - const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']); - const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']); - const kCell = createElement('td', { innerText: fan }, ['border-end', 'text-end']); - const vCell = createElement( - 'td', - {}, - ['border-start', 'text-start'], - [status ? goodIcon : badIcon], - ); - const row = createElement( - 'tr', - {}, - [`table-${status ? 'success' : 'warning'}`], - [kCell, vCell], - ); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * Create & insert table rows for each PSU in the NAPALM response. - * - * @param power NAPALM power data. - */ -function processPower(power: DeviceEnvironment['power']): void { - // Find the next adjacent element, so we can insert elements before it. - const next = getNext('status-power'); - if (typeof power !== 'undefined') { - if (next !== null) { - insertTitleRow(next, 'PSU', 'Status'); - for (const [psu, data] of Object.entries(power)) { - const { status } = data; - const goodIcon = createElement('i', {}, ['mdi', 'mdi-check-bold', 'text-success']); - const badIcon = createElement('i', {}, ['mdi', 'mdi-close', 'text-warning']); - const kCell = createElement('td', { innerText: psu }, ['border-end', 'text-end']); - const vCell = createElement( - 'td', - {}, - ['border-start', 'text-start'], - [status ? goodIcon : badIcon], - ); - const row = createElement( - 'tr', - {}, - [`table-${status ? 'success' : 'warning'}`], - [kCell, vCell], - ); - next.insertAdjacentElement('beforebegin', row); - } - } - } else { - insertNoneRow(next); - } -} - -/** - * After the `get_environment` result is received, parse its content and update HTML elements - * accordingly. - * - * @param env NAPALM Device Environment - */ -function processEnvironment(env: DeviceEnvironment): void { - const { cpu, memory, temperature, fans, power } = env; - processCpu(cpu); - processMemory(memory); - processTemp(temperature); - processFans(fans); - processPower(power); -} - -/** - * Initialize NAPALM device status handlers. - */ -function initStatus(): void { - // Show loading state for both Facts & Environment cards. - toggleLoader('show'); - - const url = getNetboxData('data-object-url'); - - if (url !== null) { - apiGetBase(url) - .then(data => { - if (hasError(data)) { - // If the API returns an error, show it to the user. - createToast('danger', 'Error Fetching Device Status', data.error).show(); - } else { - if (!hasError(data.get_facts)) { - processFacts(data.get_facts); - } else { - // If the device facts data contains an error, show it to the user. - createToast('danger', 'Error Fetching Device Facts', data.get_facts.error).show(); - } - if (!hasError(data.get_environment)) { - processEnvironment(data.get_environment); - } else { - // If the device environment data contains an error, show it to the user. - createToast( - 'danger', - 'Error Fetching Device Environment Data', - data.get_environment.error, - ).show(); - } - } - return; - }) - .finally(() => toggleLoader('hide')); - } else { - toggleLoader('hide'); - } -} - -if (document.readyState !== 'loading') { - initStatus(); -} else { - document.addEventListener('DOMContentLoaded', initStatus); -} diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index 9f6ff100d..e1ada2e19 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -397,16 +397,6 @@ export function createElement< return element as HTMLElementTagNameMap[T]; } -/** - * Convert Celsius to Fahrenheit, for NAPALM temperature sensors. - * - * @param celsius Degrees in Celsius. - * @returns Degrees in Fahrenheit. - */ -export function cToF(celsius: number): number { - return Math.round((celsius * (9 / 5) + 32 + Number.EPSILON) * 10) / 10; -} - /** * Deduplicate an array of objects based on the value of a property. * diff --git a/netbox/templates/dcim/device/config.html b/netbox/templates/dcim/device/config.html deleted file mode 100644 index f3609d3a4..000000000 --- a/netbox/templates/dcim/device/config.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends 'dcim/device/base.html' %} -{% load static %} - -{% block title %}{{ object }} - Config{% endblock %} - -{% block head %} - -{% endblock %} - -{% block content %} -
-
-
-
-
- Loading... -
-
-
Device Configuration
-
- -
-
-

-                    
-
-

-                    
-
-

-                    
-
-
-
-
-
-{% endblock %} - -{% block data %} - -{% endblock %} diff --git a/netbox/templates/dcim/device/lldp_neighbors.html b/netbox/templates/dcim/device/lldp_neighbors.html deleted file mode 100644 index 2be6aba4d..000000000 --- a/netbox/templates/dcim/device/lldp_neighbors.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends 'dcim/device/base.html' %} -{% load static %} - -{% block title %}{{ object }} - LLDP Neighbors{% endblock %} - -{% block head %} - -{% endblock %} - -{% block content %} -
-
-
- Loading... -
-
-
-
LLDP Neighbors
-
-
- - - - - - - - - - - - {% for iface in interfaces %} - - - {% with peer=iface.connected_endpoints.0 %} - {% if peer.device %} - - - {% elif peer.circuit %} - {% with circuit=peer.circuit %} - - {% endwith %} - {% else %} - - {% endif %} - {% endwith %} - - - - {% endfor %} - -
InterfaceConfigured DeviceConfigured InterfaceLLDP DeviceLLDP Interface
{{ iface }} - {{ peer.device }} - - {{ peer }} - - - {{ circuit.provider }} {{ circuit }} - None
-
-
-{% endblock %} - -{% block data %} - -{% endblock %} diff --git a/netbox/templates/dcim/device/status.html b/netbox/templates/dcim/device/status.html deleted file mode 100644 index 51dd7d27e..000000000 --- a/netbox/templates/dcim/device/status.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends 'dcim/device/base.html' %} -{% load static %} - -{% block title %}{{ object }} - Status{% endblock %} - -{% block head %} - -{% endblock %} - -{% block content %} -
-
-
-
-
- Loading... -
-
-
Device Facts
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Hostname
FQDN
Vendor
Model
Serial Number
OS Version
Uptime -
-
-
-
-
-
-
-
-
-
- Loading... -
-
-
Environment
-
- - - - - - - - - - - - - - - - - - -
CPU
Memory
Temperature
Fans
Power
-
-
-
-
-{% endblock %} - -{% block data %} - -{% endblock %} diff --git a/netbox/templates/dcim/platform.html b/netbox/templates/dcim/platform.html index 5123699d4..a834ed7e9 100644 --- a/netbox/templates/dcim/platform.html +++ b/netbox/templates/dcim/platform.html @@ -43,20 +43,10 @@ Config Template {{ object.config_template|linkify|placeholder }} - - NAPALM Driver - {{ object.napalm_driver|placeholder }} - {% include 'inc/panels/tags.html' %} -
-
NAPALM Arguments
-
-
{{ object.napalm_args|json }}
-
-
{% plugin_left_page object %}