Просмотр исходного кода

Merge branch 'develop' into develop-2.4

Jeremy Stretch 7 лет назад
Родитель
Сommit
206c465e02

+ 2 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -6,6 +6,8 @@
     be able to accept.
 
     Please indicate the relevant feature request or bug report below.
+    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
+    FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
 -->
 ### Fixes:
 

+ 7 - 5
CONTRIBUTING.md

@@ -91,11 +91,13 @@ appropriate labels will be applied for categorization.
 
 ## Submitting Pull Requests
 
-* Be sure to open an issue before starting work on a pull request, and discuss
-your idea with the NetBox maintainers before beginning work​. This will help
-prevent wasting time on something that might we might not be able to implement.
-When suggesting a new feature, also make sure it won't conflict with any work
-that's already in progress.
+* Be sure to open an issue **before** starting work on a pull request, and
+discuss your idea with the NetBox maintainers before beginning work. This will
+help prevent wasting time on something that might we might not be able to
+implement. When suggesting a new feature, also make sure it won't conflict with
+any work that's already in progress.
+
+* Any pull request which does _not_ relate to an accepted issue will be closed.
 
 * When submitting a pull request, please be sure to work off of the `develop`
 branch, rather than `master`. The `develop` branch is used for ongoing

+ 15 - 12
netbox/dcim/api/views.py

@@ -260,7 +260,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
             import napalm
         except ImportError:
             raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
-        from napalm.base.exceptions import ConnectAuthError, ModuleImportError
+        from napalm.base.exceptions import ModuleImportError
 
         # Validate the configured driver
         try:
@@ -274,16 +274,8 @@ class DeviceViewSet(CustomFieldModelViewSet):
         if not request.user.has_perm('dcim.napalm_read'):
             return HttpResponseForbidden()
 
-        # Validate requested NAPALM methods
+        # Connect to the device
         napalm_methods = request.GET.getlist('method')
-        for method in napalm_methods:
-            if not hasattr(driver, method):
-                return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
-            elif not method.startswith('get_'):
-                return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
-
-        # Connect to the device and execute the requested methods
-        # TODO: Improve error handling
         response = OrderedDict([(m, None) for m in napalm_methods])
         ip_address = str(device.primary_ip.address.ip)
         optional_args = settings.NAPALM_ARGS.copy()
@@ -298,12 +290,23 @@ class DeviceViewSet(CustomFieldModelViewSet):
         )
         try:
             d.open()
-            for method in napalm_methods:
-                response[method] = getattr(d, method)()
         except Exception as e:
             raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, 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(driver)}
         d.close()
+
         return Response(response)
 
 

+ 1 - 1
netbox/dcim/filters.py

@@ -521,7 +521,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
             Q(name__icontains=value) |
             Q(serial__icontains=value.strip()) |
             Q(inventory_items__serial__icontains=value.strip()) |
-            Q(asset_tag=value.strip()) |
+            Q(asset_tag__icontains=value.strip()) |
             Q(comments__icontains=value)
         ).distinct()
 

+ 0 - 3
netbox/dcim/models.py

@@ -1085,9 +1085,6 @@ class DeviceRole(ChangeLoggedModel):
     def __str__(self):
         return self.name
 
-    def get_absolute_url(self):
-        return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
-
     def to_csv(self):
         return (
             self.name,

+ 1 - 6
netbox/dcim/signals.py

@@ -11,13 +11,8 @@ def assign_virtualchassis_master(instance, created, **kwargs):
     """
     When a VirtualChassis is created, automatically assign its master device to the VC.
     """
-    # Default to 1 but don't overwrite an existing position (see #2087)
-    if instance.master.vc_position is not None:
-        vc_position = instance.master.vc_position
-    else:
-        vc_position = 1
     if created:
-        Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=vc_position)
+        Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=None)
 
 
 @receiver(pre_delete, sender=VirtualChassis)

+ 0 - 1
netbox/dcim/tables.py

@@ -432,7 +432,6 @@ class DeviceBayTemplateTable(BaseTable):
 
 class DeviceRoleTable(BaseTable):
     pk = ToggleColumn()
-    name = tables.LinkColumn(verbose_name='Name')
     device_count = tables.TemplateColumn(
         template_code=DEVICEROLE_DEVICE_COUNT,
         accessor=Accessor('devices.count'),

+ 2 - 1
netbox/ipam/api/views.py

@@ -193,8 +193,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
 
             # Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
             available_ips = iter(available_ips)
+            prefix_length = prefix.prefix.prefixlen
             for requested_ip in requested_ips:
-                requested_ip['address'] = next(available_ips)
+                requested_ip['address'] = '{}/{}'.format(next(available_ips), prefix_length)
                 requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
 
             # Initialize the serializer with a list or a single object depending on what was requested

+ 2 - 1
netbox/secrets/forms.py

@@ -157,7 +157,8 @@ class UserKeyForm(BootstrapMixin, forms.ModelForm):
         model = UserKey
         fields = ['public_key']
         help_texts = {
-            'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption.",
+            'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption. "
+                          "Please note that passphrase-protected keys are not supported.",
         }
 
     def clean_public_key(self):

+ 1 - 1
netbox/templates/dcim/device.html

@@ -219,7 +219,7 @@
                     <tr>
                         <td>Role</td>
                         <td>
-                            <a href="{{ device.device_role.get_absolute_url }}">{{ device.device_role }}</a>
+                            <a href="{% url 'dcim:device_list' %}?role={{ device.device_role.slug }}">{{ device.device_role }}</a>
                         </td>
                     </tr>
                     <tr>

+ 4 - 3
netbox/templates/ipam/ipaddress.html

@@ -77,10 +77,11 @@
                     <td>Tenant</td>
                     <td>
                         {% if ipaddress.tenant %}
+                            {% if ipaddress.tenant.group %}
+                                <a href="{{ ipaddress.tenant.group.get_absolute_url }}">{{ ipaddress.tenant.group }}</a>
+                                <i class="fa fa-angle-right"></i>
+                            {% endif %}
                             <a href="{{ ipaddress.tenant.get_absolute_url }}">{{ ipaddress.tenant }}</a>
-                        {% elif ipaddress.vrf.tenant %}
-                            <a href="{{ ipaddress.vrf.tenant.get_absolute_url }}">{{ ipaddress.vrf.tenant }}</a>
-                            <label class="label label-info">Inherited</label>
                         {% else %}
                             <span class="text-muted">None</span>
                         {% endif %}

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

@@ -99,13 +99,6 @@
                                     <i class="fa fa-angle-right"></i>
                                 {% endif %}
                                 <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
-                            {% elif prefix.vrf.tenant %}
-                                {% if prefix.vrf.tenant.group %}
-                                    <a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group }}</a>
-                                    <i class="fa fa-angle-right"></i>
-                                {% endif %}
-                                <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
-                                <label class="label label-info">Inherited</label>
                             {% else %}
                                 <span class="text-muted">None</span>
                             {% endif %}

+ 2 - 8
netbox/tenancy/views.py

@@ -75,14 +75,8 @@ class TenantView(View):
             'rackreservation_count': RackReservation.objects.filter(tenant=tenant).count(),
             'device_count': Device.objects.filter(tenant=tenant).count(),
             'vrf_count': VRF.objects.filter(tenant=tenant).count(),
-            'prefix_count': Prefix.objects.filter(
-                Q(tenant=tenant) |
-                Q(tenant__isnull=True, vrf__tenant=tenant)
-            ).count(),
-            'ipaddress_count': IPAddress.objects.filter(
-                Q(tenant=tenant) |
-                Q(tenant__isnull=True, vrf__tenant=tenant)
-            ).count(),
+            'prefix_count': Prefix.objects.filter(tenant=tenant).count(),
+            'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(),
             'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
             'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
             'virtualmachine_count': VirtualMachine.objects.filter(tenant=tenant).count(),