Kaynağa Gözat

Merge branch 'develop' into 3872-limit-related-ips

hSaria 6 yıl önce
ebeveyn
işleme
03b10b6f73

+ 65 - 0
docs/additional-features/napalm.md

@@ -0,0 +1,65 @@
+# NAPALM
+
+NetBox supports integration with the [NAPALM automation](https://napalm-automation.net/) library. NAPALM allows NetBox to fetch live data from devices and return it to a requester via its REST API.
+
+!!! info
+    To enable the integration, the NAPALM library must be installed. See [installation steps](../../installation/2-netbox/#napalm-automation-optional) for more information.
+
+```
+GET /api/dcim/devices/1/napalm/?method=get_environment
+
+{
+    "get_environment": {
+        ...
+    }
+}
+```
+
+## Authentication
+
+By default, the [`NAPALM_USERNAME`](../../configuration/optional-settings/#napalm_username) and [`NAPALM_PASSWORD`](../../configuration/optional-settings/#napalm_password) are used for NAPALM authentication. They can be overridden for an individual API call through the `X-NAPALM-Username` and `X-NAPALM-Password` headers.
+
+```
+$ curl "http://localhost/api/dcim/devices/1/napalm/?method=get_environment" \
+-H "Authorization: Token f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
+-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. NetBox only supports [get](https://napalm.readthedocs.io/en/latest/support/index.html#getters-support-matrix) methods.
+
+## Multiple Methods
+
+More than one method in an API call can be invoked by adding multiple `method` parameters. For example:
+
+```
+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 instance, 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 f4b378553dacfcfd44c5a0b9ae49b57e29c552b5" \
+-H "Content-Type: application/json" \
+-H "Accept: application/json; indent=4" \
+-H "X-NAPALM-port: 2222"
+```

+ 7 - 1
docs/release-notes/version-2.6.md

@@ -2,8 +2,13 @@
 
 ## Enhancements
 
+* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger
 * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
+* [#2113](https://github.com/netbox-community/netbox/issues/2113) - Allow NAPALM driver settings to be changed with request headers
+* [#2589](https://github.com/netbox-community/netbox/issues/2589) - Toggle for showing available prefixes/ip addresses
+* [#3090](https://github.com/netbox-community/netbox/issues/3090) - Add filter field for device interfaces
 * [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
+* [#3440](https://github.com/netbox-community/netbox/issues/3440) - Add total length to cable trace
 * [#3851](https://github.com/netbox-community/netbox/issues/3851) - Allow passing initial data to custom script forms
 
 ## Bug Fixes
@@ -13,7 +18,8 @@
 * [#3856](https://github.com/netbox-community/netbox/issues/3856) - Allow filtering VM interfaces by multiple MAC addresses
 * [#3857](https://github.com/netbox-community/netbox/issues/3857) - Fix group custom links rendering
 * [#3862](https://github.com/netbox-community/netbox/issues/3862) - Allow filtering device components by multiple device names
-* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Limit number of related IPs
+* [#3864](https://github.com/netbox-community/netbox/issues/3864) - Disallow /0 masks
+* [#3872](https://github.com/netbox-community/netbox/issues/3872) - Paginate related IPs of an address
 
 ---
 

+ 1 - 0
mkdocs.yml

@@ -35,6 +35,7 @@ pages:
         - Custom Scripts: 'additional-features/custom-scripts.md'
         - Export Templates: 'additional-features/export-templates.md'
         - Graphs: 'additional-features/graphs.md'
+        - NAPALM: 'additional-features/napalm.md'
         - Prometheus Metrics: 'additional-features/prometheus-metrics.md'
         - Reports: 'additional-features/reports.md'
         - Tags: 'additional-features/tags.md'

+ 4 - 0
netbox/dcim/api/serializers.py

@@ -370,6 +370,10 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
         return obj.get_config_context()
 
 
+class DeviceNAPALMSerializer(serializers.Serializer):
+    method = serializers.DictField()
+
+
 class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     cable = NestedCableSerializer(read_only=True)

+ 29 - 2
netbox/dcim/api/views.py

@@ -358,6 +358,17 @@ class DeviceViewSet(CustomFieldModelViewSet):
 
         return Response(serializer.data)
 
+    @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):
         """
@@ -396,13 +407,29 @@ class DeviceViewSet(CustomFieldModelViewSet):
         napalm_methods = request.GET.getlist('method')
         response = OrderedDict([(m, None) for m in napalm_methods])
         ip_address = str(device.primary_ip.address.ip)
+        username = settings.NAPALM_USERNAME
+        password = settings.NAPALM_PASSWORD
         optional_args = settings.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]
+
         d = driver(
             hostname=ip_address,
-            username=settings.NAPALM_USERNAME,
-            password=settings.NAPALM_PASSWORD,
+            username=username,
+            password=password,
             timeout=settings.NAPALM_TIMEOUT,
             optional_args=optional_args
         )

+ 2 - 0
netbox/dcim/models.py

@@ -2950,6 +2950,8 @@ class Cable(ChangeLoggedModel):
         # Store the given length (if any) in meters for use in database ordering
         if self.length and self.length_unit:
             self._abs_length = to_meters(self.length, self.length_unit)
+        else:
+            self._abs_length = None
 
         # Store the parent Device for the A and B terminations (if applicable) to enable filtering
         if hasattr(self.termination_a, 'device'):

+ 4 - 1
netbox/dcim/views.py

@@ -1754,10 +1754,13 @@ class CableTraceView(PermissionRequiredMixin, View):
     def get(self, request, model, pk):
 
         obj = get_object_or_404(model, pk=pk)
+        trace = obj.trace(follow_circuits=True)
+        total_length = sum([entry[1]._abs_length for entry in trace if entry[1] and entry[1]._abs_length])
 
         return render(request, 'dcim/cable_trace.html', {
             'obj': obj,
-            'trace': obj.trace(follow_circuits=True),
+            'trace': trace,
+            'total_length': total_length,
         })
 
 

+ 18 - 0
netbox/ipam/models.py

@@ -177,6 +177,12 @@ class Aggregate(ChangeLoggedModel, CustomFieldModel):
             # Clear host bits from prefix
             self.prefix = self.prefix.cidr
 
+            # /0 masks are not acceptable
+            if self.prefix.prefixlen == 0:
+                raise ValidationError({
+                    'prefix': "Cannot create aggregate with /0 mask."
+                })
+
             # Ensure that the aggregate being added is not covered by an existing aggregate
             covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
             if self.pk:
@@ -347,6 +353,12 @@ class Prefix(ChangeLoggedModel, CustomFieldModel):
 
         if self.prefix:
 
+            # /0 masks are not acceptable
+            if self.prefix.prefixlen == 0:
+                raise ValidationError({
+                    'prefix': "Cannot create prefix with /0 mask."
+                })
+
             # Disallow host masks
             if self.prefix.version == 4 and self.prefix.prefixlen == 32:
                 raise ValidationError({
@@ -622,6 +634,12 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
 
         if self.address:
 
+            # /0 masks are not acceptable
+            if self.address.prefixlen == 0:
+                raise ValidationError({
+                    'address': "Cannot create IP address with /0 mask."
+                })
+
             # Enforce unique IP space (if applicable)
             if self.role not in IPADDRESS_ROLES_NONUNIQUE and ((
                 self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE

+ 13 - 4
netbox/ipam/views.py

@@ -333,7 +333,10 @@ class AggregateView(PermissionRequiredMixin, View):
         ).annotate_depth(
             limit=0
         )
-        child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
+
+        # Add available prefixes to the table if requested
+        if request.GET.get('show_available', 'true') == 'true':
+            child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
 
         prefix_table = tables.PrefixDetailTable(child_prefixes)
         if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
@@ -356,6 +359,7 @@ class AggregateView(PermissionRequiredMixin, View):
             'aggregate': aggregate,
             'prefix_table': prefix_table,
             'permissions': permissions,
+            'show_available': request.GET.get('show_available', 'true') == 'true',
         })
 
 
@@ -511,8 +515,8 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
             'site', 'vlan', 'role',
         ).annotate_depth(limit=0)
 
-        # Annotate available prefixes
-        if child_prefixes:
+        # Add available prefixes to the table if requested
+        if child_prefixes and request.GET.get('show_available', 'true') == 'true':
             child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
 
         prefix_table = tables.PrefixDetailTable(child_prefixes)
@@ -539,6 +543,7 @@ class PrefixPrefixesView(PermissionRequiredMixin, View):
             'permissions': permissions,
             'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
             'active_tab': 'prefixes',
+            'show_available': request.GET.get('show_available', 'true') == 'true',
         })
 
 
@@ -553,7 +558,10 @@ class PrefixIPAddressesView(PermissionRequiredMixin, View):
         ipaddresses = prefix.get_child_ips().prefetch_related(
             'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
         )
-        ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
+
+        # Add available IP addresses to the table if requested
+        if request.GET.get('show_available', 'true') == 'true':
+            ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
 
         ip_table = tables.IPAddressTable(ipaddresses)
         if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
@@ -579,6 +587,7 @@ class PrefixIPAddressesView(PermissionRequiredMixin, View):
             'permissions': permissions,
             'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
             'active_tab': 'ip-addresses',
+            'show_available': request.GET.get('show_available', 'true') == 'true',
         })
 
 

+ 4 - 4
netbox/project-static/js/forms.js

@@ -7,7 +7,7 @@ $(document).ready(function() {
 
     // "Toggle" checkbox for object lists (PK column)
     $('input:checkbox.toggle').click(function() {
-        $(this).closest('table').find('input:checkbox[name=pk]').prop('checked', $(this).prop('checked'));
+        $(this).closest('table').find('input:checkbox[name=pk]:visible').prop('checked', $(this).prop('checked'));
 
         // Show the "select all" box if present
         if ($(this).is(':checked')) {
@@ -400,8 +400,8 @@ $(document).ready(function() {
     window.addEventListener('hashchange', headerOffsetScroll);
 
     // Offset between the preview window and the window edges
-    const IMAGE_PREVIEW_OFFSET_X = 20
-    const IMAGE_PREVIEW_OFFSET_Y = 10
+    const IMAGE_PREVIEW_OFFSET_X = 20;
+    const IMAGE_PREVIEW_OFFSET_Y = 10;
 
     // Preview an image attachment when the link is hovered over
     $('a.image-preview').on('mouseover', function(e) {
@@ -435,6 +435,6 @@ $(document).ready(function() {
 
     // Fade the image out; it will be deleted when another one is previewed
     $('a.image-preview').on('mouseout', function() {
-        $('#image-preview-window').fadeOut('fast')
+        $('#image-preview-window').fadeOut('fast');
     });
 });

+ 30 - 0
netbox/project-static/js/interface_toggles.js

@@ -0,0 +1,30 @@
+// Toggle the display of IP addresses under interfaces
+$('button.toggle-ips').click(function() {
+    var selected = $(this).attr('selected');
+    if (selected) {
+        $('#interfaces_table tr.ipaddresses').hide();
+    } else {
+        $('#interfaces_table tr.ipaddresses').show();
+    }
+    $(this).attr('selected', !selected);
+    $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
+    return false;
+});
+
+// Inteface filtering
+$('input.interface-filter').on('input', function() {
+    var filter = new RegExp(this.value);
+
+    for (interface of $(this).closest('form').find('tbody > tr')) {
+        // Slice off 'interface_' at the start of the ID
+        if (filter && filter.test(interface.id.slice(10))) {
+            // Match the toggle in case the filter now matches the interface
+            $(interface).find('input:checkbox[name=pk]').prop('checked', $('input.toggle').prop('checked'));
+            $(interface).show();
+        } else {
+            // Uncheck to prevent actions from including it when it doesn't match
+            $(interface).find('input:checkbox[name=pk]').prop('checked', false);
+            $(interface).hide();
+        }
+    }
+});

+ 4 - 1
netbox/templates/dcim/cable_trace.html

@@ -10,7 +10,10 @@
         <div class="col-md-4 col-md-offset-1 text-center">
             <h4>Near End</h4>
         </div>
-        <div class="col-md-4 col-md-offset-3 text-center">
+        <div class="col-md-3 text-center">
+            {% if total_length %}<h5>Total length: {{ total_length|floatformat:"-2" }} Meters<h5>{% endif %}
+        </div>
+        <div class="col-md-4 text-center">
             <h4>Far End</h4>
         </div>
     </div>

+ 4 - 12
netbox/templates/dcim/device.html

@@ -556,6 +556,9 @@
                                 <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
                             </button>
                         </div>
+                        <div class="col-md-2 pull-right noprint">
+                            <input class="form-control interface-filter" type="text" placeholder="Filter" title="RegEx-enabled" style="height: 23px" />
+                        </div>
                     </div>
                     <table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
                         <thead>
@@ -900,19 +903,8 @@ function toggleConnection(elem) {
 $(".cable-toggle").click(function() {
     return toggleConnection($(this));
 });
-// Toggle the display of IP addresses under interfaces
-$('button.toggle-ips').click(function() {
-    var selected = $(this).attr('selected');
-    if (selected) {
-        $('#interfaces_table tr.ipaddresses').hide();
-    } else {
-        $('#interfaces_table tr.ipaddresses').show();
-    }
-    $(this).attr('selected', !selected);
-    $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
-    return false;
-});
 </script>
+<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
 <script src="{% static 'js/graphs.js' %}?v{{ settings.VERSION }}"></script>
 <script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}

+ 1 - 0
netbox/templates/ipam/aggregate.html

@@ -40,6 +40,7 @@
     </div>
     <h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
     {% include 'inc/created_updated.html' with obj=aggregate %}
+    {% include 'ipam/inc/toggle_available.html' %}
     <div class="pull-right noprint">
         {% custom_links aggregate %}
     </div>

+ 9 - 0
netbox/templates/ipam/inc/toggle_available.html

@@ -0,0 +1,9 @@
+{% load helpers %}
+{% if show_available is not None %}
+    <div class="pull-right">
+        <div class="btn-group" role="group">
+            <a href="{{ request.path }}{% querystring request show_available='true' %}" class="btn btn-default{% if show_available %} active disabled{% endif %}">Show available</a>
+            <a href="{{ request.path }}{% querystring request show_available='false' %}" class="btn btn-default{% if not show_available %} active disabled{% endif %}">Hide available</a>
+        </div>
+    </div>
+{% endif %}

+ 1 - 0
netbox/templates/ipam/prefix.html

@@ -53,6 +53,7 @@
     </div>
     <h1>{% block title %}{{ prefix }}{% endblock %}</h1>
     {% include 'inc/created_updated.html' with obj=prefix %}
+    {% include 'ipam/inc/toggle_available.html' %}
     <div class="pull-right noprint">
         {% custom_links prefix %}
     </div>

+ 5 - 14
netbox/templates/virtualization/virtualmachine.html

@@ -1,5 +1,6 @@
 {% extends '_base.html' %}
 {% load custom_links %}
+{% load static %}
 {% load helpers %}
 
 {% block header %}
@@ -253,6 +254,9 @@
                         <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
                     </button>
                 </div>
+                <div class="col-md-2 pull-right noprint">
+                    <input class="form-control interface-filter" type="text" placeholder="Filter" title="RegEx-enabled" style="height: 23px" />
+                </div>
             </div>
             <table id="interfaces_table" class="table table-hover table-headings panel-body component-list">
                 <thead>
@@ -312,18 +316,5 @@
 {% endblock %}
 
 {% block javascript %}
-<script type="text/javascript">
-// Toggle the display of IP addresses under interfaces
-$('button.toggle-ips').click(function() {
-    var selected = $(this).attr('selected');
-    if (selected) {
-        $('#interfaces_table tr.ipaddresses').hide();
-    } else {
-        $('#interfaces_table tr.ipaddresses').show();
-    }
-    $(this).attr('selected', !selected);
-    $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
-    return false;
-});
-</script>
+<script src="{% static 'js/interface_toggles.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}