소스 검색

10520 remove Napalm code references (#11768)

* 10520 remove all Napalm code references

* 10520 remove lldp

* 10520 remove config, status - rebuild js

* 10520 re-add config parameters

* 10520 re-add serializer

* 10520 update docs
Arthur Hanson 3 년 전
부모
커밋
36771e821c

+ 2 - 0
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

+ 2 - 0
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).

+ 0 - 8
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`.

+ 1 - 72
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.

+ 0 - 1
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)

+ 0 - 118
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(

+ 1 - 1
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):

+ 2 - 6
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):

+ 1 - 1
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',
         )
 
 

+ 2 - 6
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):

+ 0 - 1
netbox/dcim/search.py

@@ -172,7 +172,6 @@ class PlatformIndex(SearchIndex):
     fields = (
         ('name', 100),
         ('slug', 110),
-        ('napalm_driver', 300),
         ('description', 500),
     )
 

+ 3 - 3
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',
         )
 
 

+ 3 - 7
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]}

+ 0 - 3
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',
         }
 

+ 0 - 65
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
 #

+ 0 - 4
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',),
         }),

+ 0 - 3
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({

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
netbox/project-static/dist/netbox.js.map


+ 0 - 50
netbox/project-static/src/device/config.ts

@@ -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<DeviceConfig>(url)
-      .then(data => {
-        if (hasError(data)) {
-          createToast('danger', 'Error Fetching Device Config', data.error).show();
-          console.error(data.error);
-          return;
-        } else if (hasError<Required<DeviceConfig['get_config']>>(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);
-}

+ 0 - 143
netbox/project-static/src/device/lldp.ts

@@ -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<string, string>([
-  // 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<HTMLTableRowElement>;
-
-    if (row !== null) {
-      for (const neighbor of neighbors) {
-        const deviceCell = row.querySelector<HTMLTableCellElement>('td.device');
-        const interfaceCell = row.querySelector<HTMLTableCellElement>('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<LLDPNeighborDetail>(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);
-}

+ 0 - 379
netbox/project-static/src/device/status.ts

@@ -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, `<span class="fst-italic d-block">${utc}</span>`].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<E extends HTMLElement>(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<E extends Nullable<HTMLElement>>(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<E extends HTMLElement>(id: string): Nullable<E> {
-  const element = document.getElementById(id);
-  if (element !== null) {
-    return element.nextElementSibling as Nullable<E>;
-  }
-  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<HTMLTableRowElement>('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<HTMLTableRowElement>('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<HTMLTableRowElement>('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 <span class="ms-1 text-muted small">${tempF} °F</span>`;
-        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<HTMLTableRowElement>('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<HTMLTableRowElement>('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<DeviceStatus>(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);
-}

+ 0 - 10
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.
  *

+ 0 - 45
netbox/templates/dcim/device/config.html

@@ -1,45 +0,0 @@
-{% extends 'dcim/device/base.html' %}
-{% load static %}
-
-{% block title %}{{ object }} - Config{% endblock %}
-
-{% block head %}
-<script type="text/javascript" src="{% static 'config.js' %}" onerror="window.location='{% url 'media_failure' %}?filename=config.js'"></script>
-{% endblock %}
-
-{% block content %}
-<div class="row">
-    <div class="col">
-        <div class="card">
-            <div class="card-overlay">
-                <div class="spinner-border" role="status">
-                    <span class="visually-hidden">Loading...</span>
-                </div>
-            </div>
-            <h5 class="card-header">Device Configuration</h5>
-            <div class="card-body">
-                <ul class="nav nav-tabs" role="tablist">
-                    <li role="presentation"><a class="nav-link active" href="#running" aria-controls="running" role="tab" data-bs-toggle="tab">Running</a></li>
-                    <li role="presentation"><a class="nav-link" href="#startup" aria-controls="startup" role="tab" data-bs-toggle="tab">Startup</a></li>
-                    <li role="presentation"><a class="nav-link" href="#candidate" aria-controls="candidate" role="tab" data-bs-toggle="tab">Candidate</a></li>
-                </ul>
-                <div class="tab-content p-3">
-                    <div role="tabpanel" class="tab-pane active" id="running">
-                        <pre id="running_config"></pre>
-                    </div>
-                    <div role="tabpanel" class="tab-pane" id="startup">
-                        <pre id="startup_config"></pre>
-                    </div>
-                    <div role="tabpanel" class="tab-pane" id="candidate">
-                        <pre id="candidate_config"></pre>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-{% endblock %}
-
-{% block data %}
-<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_config"></span>
-{% endblock %}

+ 0 - 66
netbox/templates/dcim/device/lldp_neighbors.html

@@ -1,66 +0,0 @@
-{% extends 'dcim/device/base.html' %}
-{% load static %}
-
-{% block title %}{{ object }} - LLDP Neighbors{% endblock %}
-
-{% block head %}
-<script type="text/javascript" src="{% static 'lldp.js' %}" onerror="window.location='{% url 'media_failure' %}?filename=lldp.js'"></script>
-{% endblock %}
-
-{% block content %}
-    <div class="card">
-        <div class="card-overlay">
-            <div class="spinner-border" role="status">
-                <span class="visually-hidden">Loading...</span>
-            </div>
-        </div>
-        <div class="card-header">
-            <h5 class="d-inline">LLDP Neighbors</h5>
-        </div>
-        <div class="card-body">
-            <table class="table table-hover">
-                <thead>
-                    <tr>
-                        <th>Interface</th>
-                        <th>Configured Device</th>
-                        <th>Configured Interface</th>
-                        <th>LLDP Device</th>
-                        <th>LLDP Interface</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    {% for iface in interfaces %}
-                        <tr id="{{ iface.name }}">
-                            <td>{{ iface }}</td>
-                            {% with peer=iface.connected_endpoints.0 %}
-                              {% if peer.device %}
-                                  <td class="configured_device" data="{{ peer.device.name }}" data-chassis="{{ peer.device.virtual_chassis.name }}">
-                                      <a href="{% url 'dcim:device' pk=peer.device.pk %}">{{ peer.device }}</a>
-                                  </td>
-                                  <td class="configured_interface" data="{{ peer.name }}">
-                                      <span title="{{ peer.get_type_display }}">{{ peer }}</span>
-                                  </td>
-                              {% elif peer.circuit %}
-                                  {% with circuit=peer.circuit %}
-                                      <td colspan="2">
-                                          <i class="mdi mdi-lightning-bolt" title="Circuit"></i>
-                                          <a href="{{ circuit.get_absolute_url }}">{{ circuit.provider }} {{ circuit }}</a>
-                                      </td>
-                                  {% endwith %}
-                              {% else %}
-                                  <td class="text-muted" colspan="2">None</td>
-                              {% endif %}
-                            {% endwith %}
-                            <td class="device"></td>
-                            <td class="interface"></td>
-                        </tr>
-                    {% endfor %}
-                </tbody>
-            </table>
-        </div>
-    </div>
-{% endblock %}
-
-{% block data %}
-<span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_lldp_neighbors_detail"></span>
-{% endblock %}

+ 0 - 93
netbox/templates/dcim/device/status.html

@@ -1,93 +0,0 @@
-{% extends 'dcim/device/base.html' %}
-{% load static %}
-
-{% block title %}{{ object }} - Status{% endblock %}
-
-{% block head %}
-<script type="text/javascript" src="{% static 'status.js' %}" onerror="window.location='{% url 'media_failure' %}?filename=status.js'"></script>
-{% endblock %}
-
-{% block content %}
-    <div class="row">
-        <div class="col col-md-6">
-            <div class="card">
-                <div class="card-overlay">
-                    <div class="spinner-border" role="status">
-                        <span class="visually-hidden">Loading...</span>
-                    </div>
-                </div>
-                <h5 class="card-header">Device Facts</h5>
-                <div class="card-body">
-                    <table class="table">
-                        <tr>
-                            <th scope="row">Hostname</th>
-                            <td id="hostname"></td>
-                        </tr>
-                        <tr>
-                            <th scope="row">FQDN</th>
-                            <td id="fqdn"></td>
-                        </tr>
-                        <tr>
-                            <th scope="row">Vendor</th>
-                            <td id="vendor"></td>
-                        </tr>
-                        <tr>
-                            <th scope="row">Model</th>
-                            <td id="model"></td>
-                        </tr>
-                        <tr>
-                            <th scope="row">Serial Number</th>
-                            <td id="serial_number" class="text-monospace"></td>
-                        </tr>
-                        <tr>
-                            <th scope="row">OS Version</th>
-                            <td id="os_version"></td>
-                        </tr>
-                        <tr class="align-middle">
-                            <th scope="row">Uptime</th>
-                            <td>
-                                <div id="uptime-duration"></div>
-                                <div id="uptime" class="small text-muted"></div>
-                            </td>
-                        </tr>
-                    </table>
-                </div>
-            </div>
-        </div>
-        <div class="col col-md-6">
-            <div class="card">
-                <div class="card-overlay">
-                    <div class="spinner-border" role="status">
-                        <span class="visually-hidden">Loading...</span>
-                    </div>
-                </div>
-                <h5 class="card-header">Environment</h5>
-                <div class="card-body">
-                    <table class="table">
-                        <tr id="status-cpu">
-                            <th colspan="2"><i class="mdi mdi-gauge"></i> CPU</th>
-                        </tr>
-                        <tr id="status-memory">
-                            <th colspan="2"><i class="mdi mdi-chip"></i> Memory</th>
-                        </tr>
-                        <tr id="status-temperature">
-                            <th colspan="2"><i class="mdi mdi-thermometer"></i> Temperature</th>
-                        </tr>
-                        <tr id="status-fans">
-                            <th colspan="2"><i class="mdi mdi-fan"></i> Fans</th>
-                        </tr>
-                        <tr id="status-power">
-                            <th colspan="2"><i class="mdi mdi-power"></i> Power</th>
-                        </tr>
-                        <tr class="napalm-table-placeholder d-none invisible">
-                        </tr>
-                    </table>
-                </div>
-            </div>
-        </div>
-    </div>
-{% endblock %}
-
-{% block data %}
-    <span data-object-url="{% url 'dcim-api:device-napalm' pk=object.pk %}?method=get_facts&method=get_environment"></span>
-{% endblock %}

+ 0 - 10
netbox/templates/dcim/platform.html

@@ -43,20 +43,10 @@
             <th scope="row">Config Template</th>
             <td>{{ object.config_template|linkify|placeholder }}</td>
           </tr>
-          <tr>
-            <th scope="row">NAPALM Driver</th>
-            <td>{{ object.napalm_driver|placeholder }}</td>
-          </tr>
         </table>
       </div>
     </div>
     {% include 'inc/panels/tags.html' %}
-    <div class="card">
-      <h5 class="card-header">NAPALM Arguments</h5>
-      <div class="card-body">
-        <pre>{{ object.napalm_args|json }}</pre>
-      </div>
-    </div>
     {% plugin_left_page object %}
 	</div>
 	<div class="col col-md-6">

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.