Explorar o código

Merge pull request #1387 from digitalocean/develop

Release v2.1.1
Jeremy Stretch %!s(int64=8) %!d(string=hai) anos
pai
achega
9cc03aaa9a
Modificáronse 65 ficheiros con 225 adicións e 264 borrados
  1. 45 3
      docs/configuration/optional-settings.md
  2. 2 10
      docs/installation/ldap.md
  3. 7 6
      netbox/dcim/api/views.py
  4. 22 8
      netbox/dcim/filters.py
  5. 10 0
      netbox/dcim/models.py
  6. 0 5
      netbox/dcim/views.py
  7. 2 2
      netbox/extras/management/commands/run_inventory.py
  8. 2 2
      netbox/netbox/configuration.docker.py
  9. 10 3
      netbox/netbox/configuration.example.py
  10. 23 6
      netbox/netbox/settings.py
  11. 1 3
      netbox/templates/circuits/circuit.html
  12. 1 3
      netbox/templates/circuits/circuit_list.html
  13. 1 5
      netbox/templates/circuits/circuittermination_edit.html
  14. 1 3
      netbox/templates/circuits/circuittype_list.html
  15. 1 3
      netbox/templates/circuits/provider.html
  16. 1 3
      netbox/templates/circuits/provider_list.html
  17. 1 3
      netbox/templates/dcim/console_connections_list.html
  18. 1 3
      netbox/templates/dcim/consoleport_connect.html
  19. 1 3
      netbox/templates/dcim/consoleserverport_connect.html
  20. 1 1
      netbox/templates/dcim/device_edit.html
  21. 1 3
      netbox/templates/dcim/device_list.html
  22. 1 3
      netbox/templates/dcim/devicebay_populate.html
  23. 1 3
      netbox/templates/dcim/devicerole_list.html
  24. 1 3
      netbox/templates/dcim/devicetype.html
  25. 1 3
      netbox/templates/dcim/devicetype_list.html
  26. 1 3
      netbox/templates/dcim/interface_connections_list.html
  27. 0 64
      netbox/templates/dcim/ipaddress_assign.html
  28. 1 3
      netbox/templates/dcim/manufacturer_list.html
  29. 1 3
      netbox/templates/dcim/platform_list.html
  30. 1 3
      netbox/templates/dcim/power_connections_list.html
  31. 1 3
      netbox/templates/dcim/poweroutlet_connect.html
  32. 1 3
      netbox/templates/dcim/powerport_connect.html
  33. 3 5
      netbox/templates/dcim/rack.html
  34. 2 2
      netbox/templates/dcim/rack_elevation_list.html
  35. 1 3
      netbox/templates/dcim/rack_list.html
  36. 1 3
      netbox/templates/dcim/rackgroup_list.html
  37. 1 3
      netbox/templates/dcim/rackrole_list.html
  38. 1 3
      netbox/templates/dcim/region_list.html
  39. 1 3
      netbox/templates/dcim/site.html
  40. 1 3
      netbox/templates/dcim/site_list.html
  41. 8 0
      netbox/templates/home.html
  42. 1 3
      netbox/templates/ipam/aggregate.html
  43. 1 3
      netbox/templates/ipam/aggregate_list.html
  44. 3 5
      netbox/templates/ipam/ipaddress.html
  45. 1 3
      netbox/templates/ipam/ipaddress_assign.html
  46. 1 3
      netbox/templates/ipam/ipaddress_list.html
  47. 1 1
      netbox/templates/ipam/prefix_ipaddresses.html
  48. 1 3
      netbox/templates/ipam/prefix_list.html
  49. 1 3
      netbox/templates/ipam/rir_list.html
  50. 1 3
      netbox/templates/ipam/role_list.html
  51. 1 3
      netbox/templates/ipam/vlan.html
  52. 1 3
      netbox/templates/ipam/vlan_list.html
  53. 1 3
      netbox/templates/ipam/vlangroup_list.html
  54. 1 3
      netbox/templates/ipam/vrf.html
  55. 1 3
      netbox/templates/ipam/vrf_list.html
  56. 1 3
      netbox/templates/secrets/secret.html
  57. 1 3
      netbox/templates/secrets/secret_edit.html
  58. 1 3
      netbox/templates/secrets/secret_import.html
  59. 1 3
      netbox/templates/secrets/secret_list.html
  60. 1 3
      netbox/templates/secrets/secretrole_list.html
  61. 1 3
      netbox/templates/tenancy/tenant.html
  62. 1 3
      netbox/templates/tenancy/tenant_list.html
  63. 1 3
      netbox/templates/tenancy/tenantgroup_list.html
  64. 28 1
      netbox/utilities/api.py
  65. 10 0
      netbox/utilities/filters.py

+ 45 - 3
docs/configuration/optional-settings.md

@@ -135,11 +135,53 @@ An API consumer can request an arbitrary number of objects by appending the "lim
 
 ---
 
-## NETBOX_USERNAME
+## NAPALM_USERNAME
 
-## NETBOX_PASSWORD
+## NAPALM_PASSWORD
 
-If provided, NetBox will use these credentials to authenticate against devices when collecting data.
+NetBox will use these credentials when authenticating to remote devices via the [NAPALM library](https://napalm-automation.net/), if installed. Both parameters are optional.
+
+Note: If SSH public key authentication has been set up for the system account under which NetBox runs, these parameters are not needed.
+
+---
+
+## NAPALM_ARGS
+
+A dictionary of optional arguments to pass to NAPALM when instantiating a network driver. See the NAPALM documentation for a [complete list of optional arguments](http://napalm.readthedocs.io/en/latest/support/#optional-arguments). An example:
+
+```
+NAPALM_ARGS = {
+    'api_key': '472071a93b60a1bd1fafb401d9f8ef41',
+    'port': 2222,
+}
+```
+
+Note: Some platforms (e.g. Cisco IOS) require an argument named `secret` to be passed in addition to the normal password. If desired, you can use the configured `NAPALM_PASSWORD` as the value for this argument:
+
+```
+NAPALM_USERNAME = 'username'
+NAPALM_PASSWORD = 'MySecretPassword'
+NAPALM_ARGS = {
+    'secret': NAPALM_PASSWORD,
+    # Include any additional args here
+}
+```
+
+---
+
+## NAPALM_TIMEOUT
+
+Default: 30 seconds
+
+The amount of time (in seconds) to wait for NAPALM to connect to a device.
+
+---
+
+## NETBOX_USERNAME (Deprecated)
+
+## NETBOX_PASSWORD (Deprecated)
+
+These settings have been deprecated and will be removed in NetBox v2.2. Please use `NAPALM_USERNAME` and `NAPALM_PASSWORD` instead.
 
 ---
 

+ 2 - 10
docs/installation/ldap.md

@@ -72,7 +72,8 @@ AUTH_LDAP_USER_DN_TEMPLATE = "uid=%(user)s,ou=users,dc=example,dc=com"
 # You can map user attributes to Django attributes as so.
 AUTH_LDAP_USER_ATTR_MAP = {
     "first_name": "givenName",
-    "last_name": "sn"
+    "last_name": "sn",
+    "email": "mail"
 }
 ```
 
@@ -108,12 +109,3 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
 * `is_active` - All users must be mapped to at least this group to enable authentication. Without this, users cannot log in.
 * `is_staff` - Users mapped to this group are enabled for access to the administration tools; this is the equivalent of checking the "staff status" box on a manually created user. This doesn't grant any specific permissions.
 * `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
-
-It is also possible map user attributes to Django attributes:
-
-```python
-AUTH_LDAP_USER_ATTR_MAP = {
-    "first_name": "givenName",
-    "last_name": "sn",
-}
-```

+ 7 - 6
netbox/dcim/api/views.py

@@ -3,7 +3,6 @@ from collections import OrderedDict
 
 from rest_framework.decorators import detail_route
 from rest_framework.mixins import ListModelMixin
-from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
 
@@ -21,7 +20,7 @@ from dcim import filters
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
-from utilities.api import ServiceUnavailable, WritableSerializerMixin
+from utilities.api import IsAuthenticatedOrLoginNotRequired, ServiceUnavailable, WritableSerializerMixin
 from .exceptions import MissingFilterException
 from . import serializers
 
@@ -272,15 +271,17 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
         ip_address = str(device.primary_ip.address.ip)
         d = driver(
             hostname=ip_address,
-            username=settings.NETBOX_USERNAME,
-            password=settings.NETBOX_PASSWORD
+            username=settings.NAPALM_USERNAME,
+            password=settings.NAPALM_PASSWORD,
+            timeout=settings.NAPALM_TIMEOUT,
+            optional_args=settings.NAPALM_ARGS
         )
         try:
             d.open()
             for method in napalm_methods:
                 response[method] = getattr(d, method)()
         except Exception as e:
-            raise ServiceUnavailable("Error connecting to the device: {}".format(e))
+            raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
 
         d.close()
         return Response(response)
@@ -385,7 +386,7 @@ class ConnectedDeviceViewSet(ViewSet):
     * `peer-device`: The name of the peer device
     * `peer-interface`: The name of the peer interface
     """
-    permission_classes = [IsAuthenticated]
+    permission_classes = [IsAuthenticatedOrLoginNotRequired]
 
     def get_view_name(self):
         return "Connected Device Locator"

+ 22 - 8
netbox/dcim/filters.py

@@ -1,6 +1,7 @@
 from __future__ import unicode_literals
 
 import django_filters
+from netaddr import EUI
 from netaddr.core import AddrFormatError
 
 from django.contrib.auth.models import User
@@ -8,7 +9,7 @@ from django.db.models import Q
 
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
-from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
+from utilities.filters import NullableCharFieldFilter, NullableModelMultipleChoiceFilter, NumericInFilter
 from .models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
@@ -113,6 +114,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         label='Search',
     )
+    facility_id = NullableCharFieldFilter()
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
         label='Site (ID)',
@@ -156,7 +158,7 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = Rack
-        fields = ['facility_id', 'type', 'width', 'u_height', 'desc_units']
+        fields = ['type', 'width', 'u_height', 'desc_units']
 
     def search(self, queryset, name, value):
         if not value.strip():
@@ -383,6 +385,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Platform (slug)',
     )
+    name = NullableCharFieldFilter()
+    asset_tag = NullableCharFieldFilter()
     site_id = django_filters.ModelMultipleChoiceFilter(
         queryset=Site.objects.all(),
         label='Site (ID)',
@@ -439,25 +443,33 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = Device
-        fields = ['name', 'serial', 'asset_tag']
+        fields = ['serial']
 
     def search(self, queryset, name, value):
         if not value.strip():
             return queryset
-        return queryset.filter(
+        qs_filter = (
             Q(name__icontains=value) |
             Q(serial__icontains=value.strip()) |
             Q(inventory_items__serial__icontains=value.strip()) |
             Q(asset_tag=value.strip()) |
             Q(comments__icontains=value)
-        ).distinct()
+        )
+        # If the query value looks like a MAC address, search interfaces as well.
+        try:
+            mac = EUI(value.strip())
+            qs_filter |= Q(interfaces__mac_address=mac)
+        except AddrFormatError:
+            pass
+        return queryset.filter(qs_filter).distinct()
 
     def _mac_address(self, queryset, name, value):
         value = value.strip()
         if not value:
             return queryset
         try:
-            return queryset.filter(interfaces__mac_address=value).distinct()
+            mac = EUI(value.strip())
+            return queryset.filter(interfaces__mac_address=mac).distinct()
         except AddrFormatError:
             return queryset.none()
 
@@ -569,7 +581,8 @@ class InterfaceFilter(django_filters.FilterSet):
         if not value:
             return queryset
         try:
-            return queryset.filter(mac_address=value)
+            mac = EUI(value.strip())
+            return queryset.filter(mac_address=mac)
         except AddrFormatError:
             return queryset.none()
 
@@ -596,10 +609,11 @@ class InventoryItemFilter(DeviceComponentFilterSet):
         to_field_name='slug',
         label='Manufacturer (slug)',
     )
+    asset_tag = NullableCharFieldFilter()
 
     class Meta:
         model = InventoryItem
-        fields = ['name', 'part_id', 'serial', 'asset_tag', 'discovered']
+        fields = ['name', 'part_id', 'serial', 'discovered']
 
 
 class ConsoleConnectionFilter(django_filters.FilterSet):

+ 10 - 0
netbox/dcim/models.py

@@ -357,6 +357,16 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
 
         return list(reversed(available_units))
 
+    def get_reserved_units(self):
+        """
+        Return a dictionary mapping all reserved units within the rack to their reservation.
+        """
+        reserved_units = {}
+        for r in self.reservations.all():
+            for u in r.units:
+                reserved_units[u] = r
+        return reserved_units
+
     def get_0u_devices(self):
         return self.devices.filter(position=0)
 

+ 0 - 5
netbox/dcim/views.py

@@ -417,15 +417,10 @@ class RackView(View):
         prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
 
         reservations = RackReservation.objects.filter(rack=rack)
-        reserved_units = {}
-        for r in reservations:
-            for u in r.units:
-                reserved_units[u] = r
 
         return render(request, 'dcim/rack.html', {
             'rack': rack,
             'reservations': reservations,
-            'reserved_units': reserved_units,
             'nonracked_devices': nonracked_devices,
             'next_rack': next_rack,
             'prev_rack': prev_rack,

+ 2 - 2
netbox/extras/management/commands/run_inventory.py

@@ -13,8 +13,8 @@ from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
 
 class Command(BaseCommand):
     help = "Update inventory information for specified devices"
-    username = settings.NETBOX_USERNAME
-    password = settings.NETBOX_PASSWORD
+    username = settings.NAPALM_USERNAME
+    password = settings.NAPALM_PASSWORD
 
     def add_arguments(self, parser):
         parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")

+ 2 - 2
netbox/netbox/configuration.docker.py

@@ -60,8 +60,8 @@ BASE_PATH = os.environ.get('BASE_PATH', '')
 MAINTENANCE_MODE = os.environ.get('MAINTENANCE_MODE', False)
 
 # Credentials that NetBox will use to access live devices.
-NETBOX_USERNAME = os.environ.get('NETBOX_USERNAME', '')
-NETBOX_PASSWORD = os.environ.get('NETBOX_PASSWORD', '')
+NAPALM_USERNAME = os.environ.get('NAPALM_USERNAME', '')
+NAPALM_PASSWORD = os.environ.get('NAPALM_PASSWORD', '')
 
 # Determine how many objects to display per page within a list. (Default: 50)
 PAGINATE_COUNT = os.environ.get('PAGINATE_COUNT', 50)

+ 10 - 3
netbox/netbox/configuration.example.py

@@ -93,9 +93,16 @@ MAINTENANCE_MODE = False
 # all objects by specifying "?limit=0".
 MAX_PAGE_SIZE = 1000
 
-# Credentials that NetBox will use to access live devices (future use).
-NETBOX_USERNAME = ''
-NETBOX_PASSWORD = ''
+# Credentials that NetBox will uses to authenticate to devices when connecting via NAPALM.
+NAPALM_USERNAME = ''
+NAPALM_PASSWORD = ''
+
+# NAPALM timeout (in seconds). (Default: 30)
+NAPALM_TIMEOUT = 30
+
+# NAPALM optional arguments (see http://napalm.readthedocs.io/en/latest/support/#optional-arguments). Arguments must
+# be provided as a dictionary.
+NAPALM_ARGS = {}
 
 # Determine how many objects to display per page within a list. (Default: 50)
 PAGINATE_COUNT = 50

+ 23 - 6
netbox/netbox/settings.py

@@ -13,7 +13,7 @@ except ImportError:
     )
 
 
-VERSION = '2.1.0'
+VERSION = '2.1.1'
 
 # Import required configuration parameters
 ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
@@ -46,8 +46,12 @@ MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
 MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
 PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
 PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
-NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
-NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
+NAPALM_USERNAME = getattr(configuration, 'NAPALM_USERNAME', '')
+NAPALM_PASSWORD = getattr(configuration, 'NAPALM_PASSWORD', '')
+NAPALM_TIMEOUT = getattr(configuration, 'NAPALM_TIMEOUT', 30)
+NAPALM_ARGS = getattr(configuration, 'NAPALM_ARGS', {})
+NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')  # Deprecated
+NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')  # Deprecated
 SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
 SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
 SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
@@ -56,6 +60,19 @@ TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
 
 CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
 
+# Check for deprecated configuration parameters
+config_logger = logging.getLogger('configuration')
+config_logger.addHandler(logging.StreamHandler())
+config_logger.setLevel(logging.WARNING)
+if NETBOX_USERNAME:
+    config_logger.warning('NETBOX_USERNAME is deprecated and will be removed in v2.2. Please use NAPALM_USERNAME instead.')
+    if not NAPALM_USERNAME:
+        NAPALM_USERNAME = NETBOX_USERNAME
+if NETBOX_PASSWORD:
+    config_logger.warning('NETBOX_PASSWORD is deprecated and will be removed in v2.2. Please use NAPALM_PASSWORD instead.')
+    if not NAPALM_PASSWORD:
+        NAPALM_PASSWORD = NETBOX_PASSWORD
+
 # Attempt to import LDAP configuration if it has been defined
 LDAP_IGNORE_CERT_ERRORS = False
 try:
@@ -78,9 +95,9 @@ if LDAP_CONFIGURED:
         if LDAP_IGNORE_CERT_ERRORS:
             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
         # Enable logging for django_auth_ldap
-        logger = logging.getLogger('django_auth_ldap')
-        logger.addHandler(logging.StreamHandler())
-        logger.setLevel(logging.DEBUG)
+        ldap_logger = logging.getLogger('django_auth_ldap')
+        ldap_logger.addHandler(logging.StreamHandler())
+        ldap_logger.setLevel(logging.DEBUG)
     except ImportError:
         raise ImproperlyConfigured(
             "LDAP authentication has been configured, but django-auth-ldap is not installed. You can remove "

+ 1 - 3
netbox/templates/circuits/circuit.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -39,7 +37,7 @@
 		</a>
     {% endif %}
 </div>
-<h1>{{ circuit.provider }} - {{ circuit.cid }}</h1>
+<h1>{% block title %}{{ circuit.provider }} - {{ circuit.cid }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=circuit %}
 <div class="row">
 	<div class="col-md-6">

+ 1 - 3
netbox/templates/circuits/circuit_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Circuits{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.circuits.add_circuit %}
@@ -17,7 +15,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='circuits' %}
 </div>
-<h1>Circuits</h1>
+<h1>{% block title %}Circuits{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:circuit_bulk_edit' bulk_delete_url='circuits:circuit_bulk_delete' %}

+ 1 - 5
netbox/templates/circuits/circuittermination_edit.html

@@ -2,10 +2,6 @@
 {% load staticfiles %}
 {% load form_helpers %}
 
-{% block title %}
-    Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}
-{% endblock %}
-
 {% block content %}
     <form action="." method="post" class="form form-horizontal">
         {% csrf_token %}
@@ -14,7 +10,7 @@
         {% endfor %}
         <div class="row">
             <div class="col-md-6 col-md-offset-3">
-                <h3>Circuit {{ obj.circuit }} - Side {{ form.term_side.value }}</h3>
+                <h3>{% block title %}Circuit {{ obj.circuit }} - {{ form.term_side.value }} Side{% endblock %}</h3>
                 {% if form.non_field_errors %}
                     <div class="panel panel-danger">
                         <div class="panel-heading"><strong>Errors</strong></div>

+ 1 - 3
netbox/templates/circuits/circuittype_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Circuit Types{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.circuits.add_circuittype %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Circuit Types</h1>
+<h1>{% block title %}Circuit Types{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='circuits:circuittype_bulk_delete' %}

+ 1 - 3
netbox/templates/circuits/provider.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load helpers %}
 
-{% block title %}{{ provider }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -45,7 +43,7 @@
 		</a>
     {% endif %}
 </div>
-<h1>{{ provider }}</h1>
+<h1>{% block title %}{{ provider }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=provider %}
 <div class="row">
 	<div class="col-md-4">

+ 1 - 3
netbox/templates/circuits/provider_list.html

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}Providers{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.circuits.add_provider %}
@@ -16,7 +14,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='providers' %}
 </div>
-<h1>Providers</h1>
+<h1>{% block title %}Providers{% endblock %}</h1>
 <div class="row">
     <div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='circuits:provider_bulk_edit' bulk_delete_url='circuits:provider_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/console_connections_list.html

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}Console Connections{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.change_consoleport %}
@@ -12,7 +10,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='connections' %}
 </div>
-<h1>Console Connections</h1>
+<h1>{% block title %}Console Connections{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'responsive_table.html' %}

+ 1 - 3
netbox/templates/dcim/consoleport_connect.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load form_helpers %}
 
-{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}
-
 {% block content %}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
@@ -21,7 +19,7 @@
                 </div>
             {% endif %}
             <div class="panel panel-default">
-                <div class="panel-heading">Connect {{ consoleport.device }} {{ consoleport }}</div>
+                <div class="panel-heading">{% block title %}Connect {{ consoleport.device }} {{ consoleport }}{% endblock %}</div>
                 <div class="panel-body">
                     <ul class="nav nav-tabs" role="tablist">
                         <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

+ 1 - 3
netbox/templates/dcim/consoleserverport_connect.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load form_helpers %}
 
-{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}
-
 {% block content %}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
@@ -21,7 +19,7 @@
                 </div>
             {% endif %}
             <div class="panel panel-default">
-                <div class="panel-heading">Connect {{ consoleserverport.device }} {{ consoleserverport }}</div>
+                <div class="panel-heading">{% block title %}Connect {{ consoleserverport.device }} {{ consoleserverport }}{% endblock %}</div>
                 <div class="panel-body">
                     <ul class="nav nav-tabs" role="tablist">
                         <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

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

@@ -45,7 +45,7 @@
                         </p>
                     </div>
                 </div>
-            {% elif not obj.device_type.is_child_device %}
+            {% else %}
                 {% render_field form.face %}
                 {% render_field form.position %}
             {% endif %}

+ 1 - 3
netbox/templates/dcim/device_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Devices{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_device %}
@@ -17,7 +15,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='devices' %}
 </div>
-<h1>Devices</h1>
+<h1>{% block title %}Devices{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'dcim/inc/device_table.html' with bulk_edit_url='dcim:device_bulk_edit' bulk_delete_url='dcim:device_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/devicebay_populate.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load form_helpers %}
 
-{% block title %}Populate {{ device_bay }}{% endblock %}
-
 {% block content %}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
@@ -17,7 +15,7 @@
                 </div>
             {% endif %}
             <div class="panel panel-default">
-                <div class="panel-heading">Populate {{ device_bay }}</div>
+                <div class="panel-heading">{% block title %}Populate {{ device_bay }}{% endblock %}</div>
                 <div class="panel-body">
                     <div class="form-group">
                         <label class="col-md-3 control-label required">Parent Device</label>

+ 1 - 3
netbox/templates/dcim/devicerole_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Device Roles{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_devicerole %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Device Roles</h1>
+<h1>{% block title %}Device Roles{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:devicerole_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/devicetype.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-md-12">
@@ -31,7 +29,7 @@
     </div>
 {% endif %}
 
-<h1>{{ devicetype.manufacturer }} {{ devicetype.model }}</h1>
+<h1>{% block title %}{{ devicetype.manufacturer }} {{ devicetype.model }}{% endblock %}</h1>
 <div class="row">
     <div class="col-md-5">
         <div class="panel panel-default">

+ 1 - 3
netbox/templates/dcim/devicetype_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Device Types{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_devicetype %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Device Types</h1>
+<h1>{% block title %}Device Types{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:devicetype_bulk_edit' bulk_delete_url='dcim:devicetype_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/interface_connections_list.html

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}Interface Connections{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_interfaceconnection %}
@@ -12,7 +10,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='connections' %}
 </div>
-<h1>Interface Connections</h1>
+<h1>{% block title %}Interface Connections{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'responsive_table.html' %}

+ 0 - 64
netbox/templates/dcim/ipaddress_assign.html

@@ -1,64 +0,0 @@
-{% extends '_base.html' %}
-{% load form_helpers %}
-
-{% block title %}Assign a New IP Address{% endblock %}
-
-{% block content %}
-<form action="." method="post" class="form form-horizontal">
-    {% csrf_token %}
-    <div class="row">
-        <div class="col-md-6 col-md-offset-3">
-            {% if form.non_field_errors %}
-                <div class="panel panel-danger">
-                    <div class="panel-heading"><strong>Errors</strong></div>
-                    <div class="panel-body">
-                        {{ form.non_field_errors }}
-                    </div>
-                </div>
-            {% endif %}
-            <div class="panel panel-default">
-                <div class="panel-heading">
-                    <strong>IP Address</strong>
-                </div>
-                <div class="panel-body">
-                    {% render_field form.address %}
-                    {% render_field form.vrf %}
-                    {% render_field form.tenant %}
-                    {% render_field form.status %}
-                    {% render_field form.description %}
-                </div>
-            </div>
-            <div class="panel panel-default">
-                <div class="panel-heading">
-                    <strong>Interface Assignment</strong>
-                </div>
-                <div class="panel-body">
-                    <div class="form-group">
-                        <label class="col-md-3 control-label">Device</label>
-                        <div class="col-md-9">
-                            <p class="form-control-static">{{ device }}</p>
-                        </div>
-                    </div>
-                    {% render_field form.interface %}
-                    {% render_field form.set_as_primary %}
-                </div>
-            </div>
-            {% if form.custom_fields %}
-            <div class="panel panel-default">
-                <div class="panel-heading"><strong>Custom Fields</strong></div>
-                <div class="panel-body">
-                    {% render_custom_fields form %}
-                </div>
-            </div>
-            {% endif %}
-		    <div class="form-group">
-                <div class="col-md-9 col-md-offset-3">
-                    <button type="submit" name="_create" class="btn btn-primary">Create</button>
-                    <button type="submit" name="_addanother" class="btn btn-primary">Create and Add More</button>
-                    <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
-                </div>
-		    </div>
-        </div>
-    </div>
-</form>
-{% endblock %}

+ 1 - 3
netbox/templates/dcim/manufacturer_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Manufacturers{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_manufacturer %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Manufacturers</h1>
+<h1>{% block title %}Manufacturers{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:manufacturer_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/platform_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Platforms{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_platform %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Platforms</h1>
+<h1>{% block title %}Platforms{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:platform_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/power_connections_list.html

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}Power Connections{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.change_powerport %}
@@ -12,7 +10,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='connections' %}
 </div>
-<h1>Power Connections</h1>
+<h1>{% block title %}Power Connections{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'responsive_table.html' %}

+ 1 - 3
netbox/templates/dcim/poweroutlet_connect.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load form_helpers %}
 
-{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}
-
 {% block content %}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
@@ -21,7 +19,7 @@
                 </div>
             {% endif %}
             <div class="panel panel-default">
-                <div class="panel-heading">Connect {{ poweroutlet.device }} {{ poweroutlet }}</div>
+                <div class="panel-heading">{% block title %}Connect {{ poweroutlet.device }} {{ poweroutlet }}{% endblock %}</div>
                 <div class="panel-body">
                     <ul class="nav nav-tabs" role="tablist">
                         <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

+ 1 - 3
netbox/templates/dcim/powerport_connect.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load form_helpers %}
 
-{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}
-
 {% block content %}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
@@ -21,7 +19,7 @@
                 </div>
             {% endif %}
             <div class="panel panel-default">
-                <div class="panel-heading">Connect {{ powerport.device }} {{ powerport }}</div>
+                <div class="panel-heading">{% block title %}Connect {{ powerport.device }} {{ powerport }}{% endblock %}</div>
                 <div class="panel-body">
                     <ul class="nav nav-tabs" role="tablist">
                         <li role="presentation" class="active"><a href="#search" aria-controls="search" role="tab" data-toggle="tab">Search</a></li>

+ 3 - 5
netbox/templates/dcim/rack.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}{{ rack.site }} - Rack {{ rack.name }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -51,7 +49,7 @@
 		</a>
     {% endif %}
 </div>
-<h1>Rack {{ rack.name }}</h1>
+<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=rack %}
 <div class="row">
 	<div class="col-md-6">
@@ -261,13 +259,13 @@
           <div class="rack_header">
             <h4>Front</h4>
           </div>
-          {% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 %}
+          {% include 'dcim/inc/rack_elevation.html' with primary_face=front_elevation secondary_face=rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
       </div>
       <div class="col-md-6 col-sm-6 col-xs-12">
         <div class="rack_header">
             <h4>Rear</h4>
         </div>
-        {% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 %}
+        {% include 'dcim/inc/rack_elevation.html' with primary_face=rear_elevation secondary_face=front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
       </div>
     </div>
 </div>

+ 2 - 2
netbox/templates/dcim/rack_elevation_list.html

@@ -18,9 +18,9 @@
                             <p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
                         </div>
                         {% if face_id %}
-                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 %}
+                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
                         {% else %}
-                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 %}
+                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
                         {% endif %}
                         <div class="clearfix"></div>
                         <div class="rack_header">

+ 1 - 3
netbox/templates/dcim/rack_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Racks{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_rack %}
@@ -17,7 +15,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='racks' %}
 </div>
-<h1>Racks</h1>
+<h1>{% block title %}Racks{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:rack_bulk_edit' bulk_delete_url='dcim:rack_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/rackgroup_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Rack Groups{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_rackgroup %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Rack Groups</h1>
+<h1>{% block title %}Rack Groups{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackgroup_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/rackrole_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Rack Role{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_rackrole %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Rack Roles</h1>
+<h1>{% block title %}Rack Roles{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:rackrole_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/region_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Regions{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_region %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>{{ block.title }}</h1>
+<h1>{% block title %}Regions{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='dcim:region_bulk_delete' %}

+ 1 - 3
netbox/templates/dcim/site.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load helpers %}
 
-{% block title %}{{ site }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -50,7 +48,7 @@
 		</a>
     {% endif %}
 </div>
-<h1>{{ site.name }}</h1>
+<h1>{% block title %}{{ site }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=site %}
 <div class="row">
 	<div class="col-md-7">

+ 1 - 3
netbox/templates/dcim/site_list.html

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}Sites{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_site %}
@@ -16,7 +14,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='sites' %}
 </div>
-<h1>Sites</h1>
+<h1>{% block title %}Sites{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='dcim:site_bulk_edit' %}

+ 8 - 0
netbox/templates/home.html

@@ -1,6 +1,14 @@
 {% extends '_base.html' %}
 
 {% block content %}
+{% if settings.NETBOX_USERNAME or settings.NETBOX_PASSWORD %}
+    <div class="alert alert-warning alert-dismissable" role="alert">
+        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+            <span aria-hidden="true">&times;</span>
+        </button>
+        <strong>Warning:</strong> The <code>NETBOX_USERNAME</code> and <code>NETBOX_PASSWORD</code> configuration parameters have been deprecated. Please replace them in configuration.py with <code>NAPALM_USERNAME</code> and <code>NAPALM_PASSWORD</code>.
+    </div>
+{% endif %}
 {% include 'search_form.html' %}
 <div class="row">
     <div class="col-sm-6 col-md-4">

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

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}Aggregate: {{ aggregate }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -38,7 +36,7 @@
         </a>
     {% endif %}
 </div>
-<h1>{{ aggregate }}</h1>
+<h1>{% block title %}{{ aggregate }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=aggregate %}
 <div class="row">
 	<div class="col-md-6">

+ 1 - 3
netbox/templates/ipam/aggregate_list.html

@@ -2,8 +2,6 @@
 {% load humanize %}
 {% load helpers %}
 
-{% block title %}Aggregates{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.ipam.add_aggregate %}
@@ -18,7 +16,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='aggregates' %}
 </div>
-<h1>Aggregates</h1>
+<h1>{% block title %}Aggregates{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:aggregate_bulk_edit' bulk_delete_url='ipam:aggregate_bulk_delete' %}

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

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}{{ ipaddress }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -40,10 +38,10 @@
         </a>
     {% endif %}
 </div>
-<h1>{{ ipaddress }}</h1>
+<h1>{% block title %}{{ ipaddress }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=ipaddress %}
 <div class="row">
-	<div class="col-md-6">
+	<div class="col-md-4">
         <div class="panel panel-default">
             <div class="panel-heading">
                 <strong>IP Address</strong>
@@ -137,7 +135,7 @@
             {% include 'inc/custom_fields_panel.html' %}
         {% endwith %}
 	</div>
-	<div class="col-md-6">
+	<div class="col-md-8">
         {% include 'panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
         {% if duplicate_ips_table.rows %}
             {% include 'panel_table.html' with table=duplicate_ips_table heading='Duplicate IP Addresses' panel_class='danger' %}

+ 1 - 3
netbox/templates/ipam/ipaddress_assign.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load form_helpers %}
 
-{% block title %}Assign an IP Address{% endblock %}
-
 {% block content %}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
@@ -19,7 +17,7 @@
             {% endif %}
             <div class="panel panel-default">
                 <div class="panel-heading">
-                    <strong>Assign an IP Address</strong>
+                    <strong>{% block title %}Assign an IP Address{% endblock %}</strong>
                 </div>
                 <div class="panel-body">
                     <div class="form-group">

+ 1 - 3
netbox/templates/ipam/ipaddress_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}IP Addresses{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.ipam.add_ipaddress %}
@@ -17,7 +15,7 @@
 	{% endif %}
     {% include 'inc/export_button.html' with obj_type='IPs' %}
 </div>
-<h1>IP Addresses</h1>
+<h1>{% block title %}IP Addresses{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:ipaddress_bulk_edit' bulk_delete_url='ipam:ipaddress_bulk_delete' %}

+ 1 - 1
netbox/templates/ipam/prefix_ipaddresses.html

@@ -1,6 +1,6 @@
 {% extends '_base.html' %}
 
-{% block title %}{{ prefix }}{% endblock %}
+{% block title %}{{ prefix }} - IP Addresses{% endblock %}
 
 {% block content %}
 {% include 'ipam/inc/prefix_header.html' with active_tab='ip-addresses' %}

+ 1 - 3
netbox/templates/ipam/prefix_list.html

@@ -2,8 +2,6 @@
 {% load helpers %}
 {% load form_helpers %}
 
-{% block title %}Prefixes{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     <div class="btn-group" role="group">
@@ -22,7 +20,7 @@
 	{% endif %}
     {% include 'inc/export_button.html' with obj_type='prefixes' %}
 </div>
-<h1>Prefixes</h1>
+<h1>{% block title %}Prefixes{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %}

+ 1 - 3
netbox/templates/ipam/rir_list.html

@@ -2,8 +2,6 @@
 {% load humanize %}
 {% load helpers %}
 
-{% block title %}RIRs{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if request.GET.family == '6' %}
@@ -24,7 +22,7 @@
         </a>
     {% endif %}
 </div>
-<h1>RIRs</h1>
+<h1>{% block title %}RIRs{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:rir_bulk_delete' %}

+ 1 - 3
netbox/templates/ipam/role_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Prefix/VLAN Roles{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_devicerole %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Prefix/VLAN Roles</h1>
+<h1>{% block title %}Prefix/VLAN Roles{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:role_bulk_delete' %}

+ 1 - 3
netbox/templates/ipam/vlan.html

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}VLAN {{ vlan.display_name }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -43,7 +41,7 @@
         </a>
     {% endif %}
 </div>
-<h1>VLAN {{ vlan.display_name }}</h1>
+<h1>{% block title %}VLAN {{ vlan.display_name }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=vlan %}
 <div class="row">
 	<div class="col-md-6">

+ 1 - 3
netbox/templates/ipam/vlan_list.html

@@ -2,8 +2,6 @@
 {% load helpers %}
 {% load form_helpers %}
 
-{% block title %}VLANs{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.ipam.add_vlan %}
@@ -18,7 +16,7 @@
 	{% endif %}
     {% include 'inc/export_button.html' with obj_type='VLANs' %}
 </div>
-<h1>VLANs</h1>
+<h1>{% block title %}VLANs{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}

+ 1 - 3
netbox/templates/ipam/vlangroup_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}VLAN Groups{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.ipam.add_vlangroup %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>VLAN Groups</h1>
+<h1>{% block title %}VLAN Groups{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_delete_url='ipam:vlangroup_bulk_delete' %}

+ 1 - 3
netbox/templates/ipam/vrf.html

@@ -1,7 +1,5 @@
 {% extends '_base.html' %}
 
-{% block title %}VRF {{ vrf }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -37,7 +35,7 @@
         </a>
     {% endif %}
 </div>
-<h1>{{ vrf }}</h1>
+<h1>{% block title %}VRF {{ vrf }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=vrf %}
 <div class="row">
 	<div class="col-md-6">

+ 1 - 3
netbox/templates/ipam/vrf_list.html

@@ -2,8 +2,6 @@
 {% load helpers %}
 {% load form_helpers %}
 
-{% block title %}VRFs{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.ipam.add_vrf %}
@@ -18,7 +16,7 @@
 	{% endif %}
     {% include 'inc/export_button.html' with obj_type='VRFs' %}
 </div>
-<h1>VRFs</h1>
+<h1>{% block title %}VRFs{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='ipam:vrf_bulk_edit' bulk_delete_url='ipam:vrf_bulk_delete' %}

+ 1 - 3
netbox/templates/secrets/secret.html

@@ -2,8 +2,6 @@
 {% load static from staticfiles %}
 {% load secret_helpers %}
 
-{% block title %}Secret: {{ secret }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-md-12">
@@ -28,7 +26,7 @@
 		</a>
     {% endif %}
 </div>
-<h1>{{ secret }}</h1>
+<h1>{% block title %}{{ secret }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=secret %}
 <div class="row">
 	<div class="col-md-6">

+ 1 - 3
netbox/templates/secrets/secret_edit.html

@@ -2,15 +2,13 @@
 {% load static from staticfiles %}
 {% load form_helpers %}
 
-{% block title %}{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %}
-
 {% block content %}
 <form action="." method="post" class="form form-horizontal">
     {% csrf_token %}
     {{ form.private_key }}
     <div class="row">
         <div class="col-md-6 col-md-offset-3">
-            <h3>{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}</h3>
+            <h3>{% block title %}{% if secret.pk %}Editing {{ secret }}{% else %}Add a Secret{% endif %}{% endblock %}</h3>
             {% if form.non_field_errors %}
                 <div class="panel panel-danger">
                     <div class="panel-heading"><strong>Errors</strong></div>

+ 1 - 3
netbox/templates/secrets/secret_import.html

@@ -2,10 +2,8 @@
 {% load static from staticfiles %}
 {% load form_helpers %}
 
-{% block title %}Secret Import{% endblock %}
-
 {% block content %}
-<h1>Secret Import</h1>
+<h1>{% block title %}Secret Import{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-6">
         {% if form.non_field_errors %}

+ 1 - 3
netbox/templates/secrets/secret_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Secrets{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.secrets.add_secret %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Secrets</h1>
+<h1>{% block title %}Secrets{% endblock %}</h1>
 <div class="row">
     <div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='secrets:secret_bulk_edit' bulk_delete_url='secrets:secret_bulk_delete' %}

+ 1 - 3
netbox/templates/secrets/secretrole_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Secret Roles{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.dcim.add_devicerole %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Secret Roles</h1>
+<h1>{% block title %}Secret Roles{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='secrets:secretrole_bulk_delete' %}

+ 1 - 3
netbox/templates/tenancy/tenant.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}{{ tenant }}{% endblock %}
-
 {% block content %}
 <div class="row">
     <div class="col-sm-8 col-md-9">
@@ -41,7 +39,7 @@
 		</a>
     {% endif %}
 </div>
-<h1>{{ tenant }}</h1>
+<h1>{% block title %}{{ tenant }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=tenant %}
 <div class="row">
 	<div class="col-md-7">

+ 1 - 3
netbox/templates/tenancy/tenant_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Tenants{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.tenancy.add_tenant %}
@@ -17,7 +15,7 @@
     {% endif %}
     {% include 'inc/export_button.html' with obj_type='tenants' %}
 </div>
-<h1>Tenants</h1>
+<h1>{% block title %}Tenants{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-9">
         {% include 'utilities/obj_table.html' with bulk_edit_url='tenancy:tenant_bulk_edit' bulk_delete_url='tenancy:tenant_bulk_delete' %}

+ 1 - 3
netbox/templates/tenancy/tenantgroup_list.html

@@ -1,8 +1,6 @@
 {% extends '_base.html' %}
 {% load helpers %}
 
-{% block title %}Tenant Groups{% endblock %}
-
 {% block content %}
 <div class="pull-right">
     {% if perms.tenancy.add_tenantgroup %}
@@ -12,7 +10,7 @@
         </a>
     {% endif %}
 </div>
-<h1>Tenant Groups</h1>
+<h1>{% block title %}Tenant Groups{% endblock %}</h1>
 <div class="row">
 	<div class="col-md-12">
         {% include 'utilities/obj_table.html' with bulk_delete_url='tenancy:tenantgroup_bulk_delete' %}

+ 28 - 1
netbox/utilities/api.py

@@ -4,9 +4,10 @@ from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 
 from rest_framework import authentication, exceptions
+from rest_framework.compat import is_authenticated
 from rest_framework.exceptions import APIException
 from rest_framework.pagination import LimitOffsetPagination
-from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS
+from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS
 from rest_framework.serializers import Field, ValidationError
 
 from users.models import Token
@@ -20,6 +21,10 @@ class ServiceUnavailable(APIException):
     default_detail = "Service temporarily unavailable, please try again later."
 
 
+#
+# Authentication
+#
+
 class TokenAuthentication(authentication.TokenAuthentication):
     """
     A custom authentication scheme which enforces Token expiration times.
@@ -61,6 +66,20 @@ class TokenPermissions(DjangoModelPermissions):
         return super(TokenPermissions, self).has_permission(request, view)
 
 
+class IsAuthenticatedOrLoginNotRequired(BasePermission):
+    """
+    Returns True if the user is authenticated or LOGIN_REQUIRED is False.
+    """
+    def has_permission(self, request, view):
+        if not settings.LOGIN_REQUIRED:
+            return True
+        return request.user and is_authenticated(request.user)
+
+
+#
+# Serializers
+#
+
 class ChoiceFieldSerializer(Field):
     """
     Represent a ChoiceField as {'value': <DB value>, 'label': <string>}.
@@ -98,6 +117,10 @@ class ContentTypeFieldSerializer(Field):
             raise ValidationError("Invalid content type")
 
 
+#
+# Mixins
+#
+
 class ModelValidationMixin(object):
     """
     Enforce a model's validation through clean() when validating serializer data. This is necessary to ensure we're
@@ -119,6 +142,10 @@ class WritableSerializerMixin(object):
         return self.serializer_class
 
 
+#
+# Pagination
+#
+
 class OptionalLimitOffsetPagination(LimitOffsetPagination):
     """
     Override the stock paginator to allow setting limit=0 to disable pagination for a request. This returns all objects

+ 10 - 0
netbox/utilities/filters.py

@@ -19,6 +19,16 @@ class NumericInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
     pass
 
 
+class NullableCharFieldFilter(django_filters.CharFilter):
+    null_value = 'NULL'
+
+    def filter(self, qs, value):
+        if value != self.null_value:
+            return super(NullableCharFieldFilter, self).filter(qs, value)
+        qs = self.get_method(qs)(**{'{}__isnull'.format(self.name): True})
+        return qs.distinct() if self.distinct else qs
+
+
 class NullableModelMultipleChoiceField(forms.ModelMultipleChoiceField):
     """
     This field operates like a normal ModelMultipleChoiceField except that it allows for one additional choice which is