Преглед изворни кода

Merge pull request #2275 from digitalocean/develop

Release v2.3.7
Jeremy Stretch пре 7 година
родитељ
комит
a85e6370a8

+ 0 - 49
.github/ISSUE_TEMPLATE.md

@@ -1,49 +0,0 @@
-<!--
-    Before opening a new issue, please search through the existing issues to
-    see if your topic has already been addressed. Note that you may need to
-    remove the "is:open" filter from the search bar to include closed issues.
-
-    Check the appropriate type for your issue below by placing an x between the
-    brackets. For assistance with installation issues, or for any other issues
-    other than those listed below, please raise your topic for discussion on
-    our mailing list:
-
-        https://groups.google.com/forum/#!forum/netbox-discuss
-
-    Please note that issues which do not fall under any of the below categories
-    will be closed. Due to an excessive backlog of feature requests, we are
-    not currently accepting any proposals which extend NetBox's feature scope.
-
-    Do not prepend any sort of tag to your issue's title. An administrator will
-    review your issue and assign labels as appropriate.
---->
-### Issue type
-[ ] Feature request <!-- An enhancement of existing functionality -->
-[ ] Bug report      <!-- Unexpected or erroneous behavior -->
-[ ] Documentation   <!-- A modification to the documentation -->
-[ ] Housekeeping    <!-- Changes pertaining to the codebase itself -->
-
-<!--
-    Please describe the environment in which you are running NetBox. (Be sure
-    to verify that you are running the latest stable release of NetBox before
-    submitting a bug report.) If you are submitting a bug report and have made
-    any changes to the code base, please first validate that your bug can be
-    recreated while running an official release.
--->
-### Environment
-* Python version:  <!-- Example: 3.5.4 -->
-* NetBox version:  <!-- Example: 2.3.5 -->
-
-<!--
-    BUG REPORTS must include:
-        * A list of the steps needed for someone else to reproduce the bug
-        * A description of the expected and observed behavior
-        * Any relevant error messages (screenshots may also help)
-
-    FEATURE REQUESTS must include:
-        * A detailed description of the proposed functionality
-        * A use case for the new feature
-        * A rough description of any necessary changes to the database schema
-        * Any relevant third-party libraries which would be needed
--->
-### Description

+ 34 - 0
.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,34 @@
+---
+name: :bug: Bug Report
+about: Report a reproducible bug in the current release of NetBox
+---
+
+<!--
+    NOTE: This form is only for reproducible bugs. If you need assistance with
+    NetBox installation, or if you have a general question, DO NOT open an
+    issue. Instead, post to our mailing list:
+
+        https://groups.google.com/forum/#!forum/netbox-discuss
+
+    Please describe the environment in which you are running NetBox. Be sure
+    that you are running an unmodified instance of the latest stable release
+    before submitting a bug report.
+-->
+### Environment
+* Python version:  <!-- Example: 3.5.4 -->
+* NetBox version:  <!-- Example: 2.3.6 -->
+
+<!--
+    Describe in detail the steps that someone else can take to reproduce this
+    bug using the current stable release of NetBox (or the current beta release
+    where applicable).
+-->
+### Steps to Reproduce
+
+
+<!-- What did you expect to happen? -->
+### Expected Behavior
+
+
+<!-- What happened instead? -->
+### Observed Behavior

+ 17 - 0
.github/ISSUE_TEMPLATE/documentation_change.md

@@ -0,0 +1,17 @@
+---
+name: :book: Documentation Change
+about: Suggest an addition or modification to the NetBox documentation
+---
+
+<!--
+    Please indicate the nature of the change by placing an X in one of the
+    boxes below.
+-->
+### Change Type
+[ ] Addition
+[ ] Correction
+[ ] Deprecation
+[ ] Cleanup (formatting, typos, etc.)
+
+<!-- Describe the proposed change(s). -->
+### Proposed Changes

+ 53 - 0
.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,53 @@
+---
+name: :new: Feature Request
+about: Propose a new NetBox feature or enhancement
+---
+
+<!--
+    NOTE: This form is only for proposing specific new features or enhancements.
+    If you have a general idea or question, please post to our mailing list
+    instead of opening an issue:
+
+        https://groups.google.com/forum/#!forum/netbox-discuss
+
+    NOTE: Due to an excessive backlog of feature requests, we are not currently
+    accepting any proposals which significantly extend NetBox's feature scope.
+
+    Please describe the environment in which you are running NetBox. Be sure
+    that you are running an unmodified instance of the latest stable release
+    before submitting a bug report.
+-->
+### Environment
+* Python version:  <!-- Example: 3.5.4 -->
+* NetBox version:  <!-- Example: 2.3.6 -->
+
+<!--
+    Describe in detail the new functionality you are proposing. Include any
+    specific changes to work flows, data models, or the user interface.
+-->
+### Proposed Functionality
+
+
+<!--
+    Convey an example use case for your proposed feature. Write from the
+    perspective of a NetBox user who would benefit from the proposed
+    functionality and describe how.
+--->
+### Use Case
+
+
+<!--
+    Note any changes to the database schema necessary to support the new
+    feature. For example, does the proposal require adding a new model or
+    field? (Not all new features require database changes.)
+--->
+### Database Changes
+
+
+<!--
+    List any new dependencies on external libraries or services that this new
+    feature would introduce. For example, does the proposal require the
+    installation of a new Python package? (Not all new features introduce new
+    dependencies.)
+-->
+### External Dependencies

+ 16 - 0
.github/ISSUE_TEMPLATE/housekeeping.md

@@ -0,0 +1,16 @@
+---
+name: :house: Housekeeping
+about: A change pertaining to the codebase itself
+---
+
+<!--
+    NOTE: This type of issue should be opened only by those reasonably familiar
+    with NetBox's code base and interested in contributing to its development.
+
+    Describe the proposed change(s) in detail.
+-->
+### Proposed Changes
+
+
+<!-- Provide justification for the proposed change(s). -->
+### Justification -->

+ 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

@@ -267,7 +267,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:
@@ -281,16 +281,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)
         d = driver(
@@ -302,12 +294,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

@@ -509,7 +509,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

@@ -781,9 +781,6 @@ class DeviceRole(models.Model):
     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

@@ -408,7 +408,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

@@ -196,8 +196,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

+ 29 - 11
netbox/ipam/tables.py

@@ -194,17 +194,35 @@ class RIRTable(BaseTable):
 
 
 class RIRDetailTable(RIRTable):
-    stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
-                                footer=lambda table: sum(r.stats['total'] for r in table.data))
-    stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
-                                 footer=lambda table: sum(r.stats['active'] for r in table.data))
-    stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved',
-                                   footer=lambda table: sum(r.stats['reserved'] for r in table.data))
-    stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated',
-                                     footer=lambda table: sum(r.stats['deprecated'] for r in table.data))
-    stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
-                                    footer=lambda table: sum(r.stats['available'] for r in table.data))
-    utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
+    stats_total = tables.Column(
+        accessor='stats.total',
+        verbose_name='Total',
+        footer=lambda table: sum(r.stats['total'] for r in table.data)
+    )
+    stats_active = tables.Column(
+        accessor='stats.active',
+        verbose_name='Active',
+        footer=lambda table: sum(r.stats['active'] for r in table.data)
+    )
+    stats_reserved = tables.Column(
+        accessor='stats.reserved',
+        verbose_name='Reserved',
+        footer=lambda table: sum(r.stats['reserved'] for r in table.data)
+    )
+    stats_deprecated = tables.Column(
+        accessor='stats.deprecated',
+        verbose_name='Deprecated',
+        footer=lambda table: sum(r.stats['deprecated'] for r in table.data)
+    )
+    stats_available = tables.Column(
+        accessor='stats.available',
+        verbose_name='Available',
+        footer=lambda table: sum(r.stats['available'] for r in table.data)
+    )
+    utilization = tables.TemplateColumn(
+        template_code=RIR_UTILIZATION,
+        verbose_name='Utilization'
+    )
 
     class Meta(RIRTable.Meta):
         fields = (

+ 14 - 22
netbox/ipam/views.py

@@ -192,9 +192,15 @@ class RIRListView(ObjectListView):
                 queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
 
                 # Find all consumed space for each prefix status (we ignore containers for this purpose).
-                active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
-                reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
-                deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
+                active_prefixes = netaddr.cidr_merge(
+                    [p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)]
+                )
+                reserved_prefixes = netaddr.cidr_merge(
+                    [p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)]
+                )
+                deprecated_prefixes = netaddr.cidr_merge(
+                    [p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)]
+                )
 
                 # Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
                 available_prefixes = (
@@ -205,11 +211,11 @@ class RIRListView(ObjectListView):
                 )
 
                 # Add the size of each metric to the RIR total.
-                stats['total'] += aggregate.prefix.size / denominator
-                stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
-                stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
-                stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
-                stats['available'] += available_prefixes.size / denominator
+                stats['total'] += int(aggregate.prefix.size / denominator)
+                stats['active'] += int(netaddr.IPSet(active_prefixes).size / denominator)
+                stats['reserved'] += int(netaddr.IPSet(reserved_prefixes).size / denominator)
+                stats['deprecated'] += int(netaddr.IPSet(deprecated_prefixes).size / denominator)
+                stats['available'] += int(available_prefixes.size / denominator)
 
             # Calculate the percentage of total space for each prefix status.
             total = float(stats['total'])
@@ -229,20 +235,6 @@ class RIRListView(ObjectListView):
 
         return rirs
 
-    def extra_context(self):
-
-        totals = {
-            'total': sum([rir.stats['total'] for rir in self.queryset]),
-            'active': sum([rir.stats['active'] for rir in self.queryset]),
-            'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
-            'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
-            'available': sum([rir.stats['available'] for rir in self.queryset]),
-        }
-
-        return {
-            'totals': totals,
-        }
-
 
 class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
     permission_required = 'ipam.add_rir'

+ 1 - 1
netbox/netbox/settings.py

@@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
         DeprecationWarning
     )
 
-VERSION = '2.3.6'
+VERSION = '2.3.7'
 
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 

+ 2 - 0
netbox/netbox/urls.py

@@ -74,3 +74,5 @@ if settings.DEBUG:
 urlpatterns = [
     url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
 ]
+
+handler500 = 'utilities.views.server_error'

+ 8 - 1
netbox/project-static/css/base.css

@@ -372,12 +372,19 @@ table.reports td.method {
     font-family: monospace;
     padding-left: 30px;
 }
-table.reports td.stats label {
+td.report-stats label {
     display: inline-block;
     line-height: 14px;
     margin-bottom: 0;
     min-width: 40px;
 }
+table.report th {
+    position: relative;
+}
+table.report th a {
+    position: absolute;
+    top: -51px;
+}
 
 /* AJAX loader */
 .loading {

+ 2 - 1
netbox/secrets/forms.py

@@ -153,7 +153,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

@@ -146,7 +146,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>

+ 56 - 46
netbox/templates/extras/report.html

@@ -29,63 +29,73 @@
                 <p class="lead">{{ report.description }}</p>
             {% endif %}
             {% if report.result %}
-                <p>Last run: {{ report.result.created }}</p>
-            {% else %}
-                <p class="text-muted">Last run: Never</p>
+                <p>Last run: <strong>{{ report.result.created }}</strong></p>
             {% endif %}
-        </div>
-        <div class="col-md-9">
             {% if report.result %}
-                <table class="table table-hover">
-                    <thead>
-                        <tr>
-                            <th>Time</th>
-                            <th>Level</th>
-                            <th>Object</th>
-                            <th>Message</th>
-                        </tr>
-                    </thead>
-                    {% for method, data in report.result.data.items %}
-                        <tr>
-                            <th colspan="4"><a name="{{ method }}"></a>{{ method }}</th>
-                        </tr>
-                        {% for time, level, obj, url, message in data.log %}
-                            <tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
-                                <td>{{ time }}</td>
-                                <td>
-                                    <label class="label label-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
-                                </td>
-                                <td>
-                                    {% if obj and url %}
-                                        <a href="{{ url }}">{{ obj }}</a>
-                                    {% elif obj %}
-                                        {{ obj }}
-                                    {% endif %}
+                <div class="panel panel-default">
+                    <div class="panel-heading">
+                        <strong>Report Methods</strong>
+                    </div>
+                    <table class="table table-hover panel-body">
+                        {% for method, data in report.result.data.items %}
+                            <tr>
+                                <td><code><a href="#{{ method }}">{{ method }}</a></code></td>
+                                <td class="text-right report-stats">
+                                    <label class="label label-success">{{ data.success }}</label>
+                                    <label class="label label-info">{{ data.info }}</label>
+                                    <label class="label label-warning">{{ data.warning }}</label>
+                                    <label class="label label-danger">{{ data.failure }}</label>
                                 </td>
-                                <td>{{ message }}</td>
                             </tr>
                         {% endfor %}
-                    {% endfor %}
-                </table>
+                    </table>
+                </div>
+                <div class="panel panel-default">
+                    <div class="panel-heading">
+                        <strong>Report Results</strong>
+                    </div>
+                    <table class="table table-hover panel-body report">
+                        <thead>
+                            <tr class="table-headings">
+                                <th>Time</th>
+                                <th>Level</th>
+                                <th>Object</th>
+                                <th>Message</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            {% for method, data in report.result.data.items %}
+                                <tr>
+                                    <th colspan="4" style="font-family: monospace">
+                                        <a name="{{ method }}"></a>{{ method }}
+                                    </th>
+                                </tr>
+                                {% for time, level, obj, url, message in data.log %}
+                                    <tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
+                                        <td>{{ time }}</td>
+                                        <td>
+                                            <label class="label label-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
+                                        </td>
+                                        <td>
+                                            {% if obj and url %}
+                                                <a href="{{ url }}">{{ obj }}</a>
+                                            {% elif obj %}
+                                                {{ obj }}
+                                            {% endif %}
+                                        </td>
+                                        <td>{{ message }}</td>
+                                    </tr>
+                                {% endfor %}
+                            {% endfor %}
+                        </tbody>
+                    </table>
+                </div>
             {% else %}
                 <div class="well">No results are available for this report. Please run the report first.</div>
             {% endif %}
         </div>
         <div class="col-md-3">
             {% if report.result %}
-                <div class="panel panel-default">
-                    <div class="panel-heading">
-                        <strong>Methods</strong>
-                    </div>
-                    <ul class="list-group">
-                        {% for method, data in report.result.data.items %}
-                            <li class="list-group-item">
-                                <a href="#{{ method }}">{{ method }}</a>
-                                <span class="badge">{{ data.log|length }}</span>
-                            </li>
-                        {% endfor %}
-                    </ul>
-                </div>
             {% endif %}
         </div>
     </div>

+ 2 - 2
netbox/templates/extras/report_list.html

@@ -38,7 +38,7 @@
                                         <td colspan="3" class="method">
                                             {{ method }}
                                         </td>
-                                        <td class="text-right stats">
+                                        <td class="text-right report-stats">
                                             <label class="label label-success">{{ stats.success }}</label>
                                             <label class="label label-info">{{ stats.info }}</label>
                                             <label class="label label-warning">{{ stats.warning }}</label>
@@ -69,7 +69,7 @@
                                 <a href="#report.{{ report.name }}" class="list-group-item">
                                     <i class="fa fa-list-alt"></i> {{ report.name }}
                                     <div class="pull-right">
-                                        {% include 'extras/inc/report_label.html' %}
+                                        {% include 'extras/inc/report_label.html' with result=report.result %}
                                     </div>
                                 </a>
                             {% endfor %}

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

@@ -65,10 +65,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

@@ -35,13 +35,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

@@ -78,14 +78,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(),

+ 11 - 14
netbox/utilities/middleware.py

@@ -5,9 +5,10 @@ import sys
 from django.conf import settings
 from django.db import ProgrammingError
 from django.http import Http404, HttpResponseRedirect
-from django.shortcuts import render
 from django.urls import reverse
 
+from .views import server_error
+
 BASE_PATH = getattr(settings, 'BASE_PATH', False)
 LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
 
@@ -65,23 +66,19 @@ class ExceptionHandlingMiddleware(object):
         if isinstance(exception, Http404):
             return
 
-        # Determine the type of exception
+        # Determine the type of exception. If it's a common issue, return a custom error page with instructions.
+        custom_template = None
         if isinstance(exception, ProgrammingError):
-            template_name = 'exceptions/programming_error.html'
+            custom_template = 'exceptions/programming_error.html'
         elif isinstance(exception, ImportError):
-            template_name = 'exceptions/import_error.html'
+            custom_template = 'exceptions/import_error.html'
         elif (
             sys.version_info[0] >= 3 and isinstance(exception, PermissionError)
         ) or (
             isinstance(exception, OSError) and exception.errno == 13
         ):
-            template_name = 'exceptions/permission_error.html'
-        else:
-            template_name = '500.html'
-
-        # Return an error message
-        type_, error, traceback = sys.exc_info()
-        return render(request, template_name, {
-            'exception': str(type_),
-            'error': error,
-        }, status=500)
+            custom_template = 'exceptions/permission_error.html'
+
+        # Return a custom error message, or fall back to Django's default 500 error handling
+        if custom_template:
+            return server_error(request, template_name=custom_template)

+ 23 - 1
netbox/utilities/views.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 from collections import OrderedDict
 from copy import deepcopy
+import sys
 
 from django.conf import settings
 from django.contrib import messages
@@ -10,12 +11,16 @@ from django.core.exceptions import ValidationError
 from django.db import transaction, IntegrityError
 from django.db.models import ProtectedError
 from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
+from django.http import HttpResponseServerError
 from django.shortcuts import get_object_or_404, redirect, render
-from django.template.exceptions import TemplateSyntaxError
+from django.template import loader
+from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError
 from django.urls import reverse
 from django.utils.html import escape
 from django.utils.http import is_safe_url
 from django.utils.safestring import mark_safe
+from django.views.decorators.csrf import requires_csrf_token
+from django.views.defaults import ERROR_500_TEMPLATE_NAME
 from django.views.generic import View
 from django_tables2 import RequestConfig
 
@@ -858,3 +863,20 @@ class BulkComponentCreateView(View):
             'table': table,
             'return_url': reverse(self.default_return_url),
         })
+
+
+@requires_csrf_token
+def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
+    """
+    Custom 500 handler to provide additional context when rendering 500.html.
+    """
+    try:
+        template = loader.get_template(template_name)
+    except TemplateDoesNotExist:
+        return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
+    type_, error, traceback = sys.exc_info()
+
+    return HttpResponseServerError(template.render({
+        'exception': str(type_),
+        'error': error,
+    }))