Skip to content

Commit

Permalink
Merge pull request #1387 from digitalocean/develop
Browse files Browse the repository at this point in the history
Release v2.1.1
  • Loading branch information
jeremystretch authored Aug 2, 2017
2 parents 1bda56e + 1c1f406 commit 9cc03aa
Show file tree
Hide file tree
Showing 65 changed files with 225 additions and 264 deletions.
48 changes: 45 additions & 3 deletions docs/configuration/optional-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
12 changes: 2 additions & 10 deletions docs/installation/ldap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
```

Expand Down Expand Up @@ -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",
}
```
13 changes: 7 additions & 6 deletions netbox/dcim/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

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

Expand All @@ -21,7 +20,7 @@
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

Expand Down Expand Up @@ -272,15 +271,17 @@ def napalm(self, request, pk):
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)
Expand Down Expand Up @@ -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"
Expand Down
30 changes: 22 additions & 8 deletions netbox/dcim/filters.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import unicode_literals

import django_filters
from netaddr import EUI
from netaddr.core import AddrFormatError

from django.contrib.auth.models import User
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,
Expand Down Expand Up @@ -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)',
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)',
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -569,7 +581,8 @@ def _mac_address(self, queryset, name, value):
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()

Expand All @@ -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):
Expand Down
10 changes: 10 additions & 0 deletions netbox/dcim/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,16 @@ def get_available_units(self, u_height=1, rack_face=None, exclude=list()):

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)

Expand Down
5 changes: 0 additions & 5 deletions netbox/dcim/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,15 +417,10 @@ def get(self, request, pk):
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,
Expand Down
4 changes: 2 additions & 2 deletions netbox/extras/management/commands/run_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

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")
Expand Down
4 changes: 2 additions & 2 deletions netbox/netbox/configuration.docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
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)
Expand Down
13 changes: 10 additions & 3 deletions netbox/netbox/configuration.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,16 @@
# 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
Expand Down
29 changes: 23 additions & 6 deletions netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)


VERSION = '2.1.0'
VERSION = '2.1.1'

# Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
Expand Down Expand Up @@ -46,8 +46,12 @@
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')
Expand All @@ -56,6 +60,19 @@

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:
Expand All @@ -78,9 +95,9 @@
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 "
Expand Down
4 changes: 1 addition & 3 deletions netbox/templates/circuits/circuit.html
Original file line number Diff line number Diff line change
@@ -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">
Expand Down Expand Up @@ -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">
Expand Down
4 changes: 1 addition & 3 deletions netbox/templates/circuits/circuit_list.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
{% extends '_base.html' %}
{% load helpers %}

{% block title %}Circuits{% endblock %}

{% block content %}
<div class="pull-right">
{% if perms.circuits.add_circuit %}
Expand All @@ -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' %}
Expand Down
Loading

0 comments on commit 9cc03aa

Please sign in to comment.