Merge pull request #1259 from digitalocean/develop

Release v2.0.6
This commit is contained in:
Jeremy Stretch 2017-06-12 09:51:15 -04:00 committed by GitHub
commit 5c63a499d5
20 changed files with 272 additions and 71 deletions

View File

@ -83,6 +83,34 @@ Enforcement of unique IP space can be toggled on a per-VRF basis. To enforce uni
--- ---
## LOGGING
By default, all messages of INFO severity or higher will be logged to the console. Additionally, if `DEBUG` is False and email access has been configured, ERROR and CRITICAL messages will be emailed to the users defined in `ADMINS`.
The Django framework on which NetBox runs allows for the customization of logging, e.g. to write logs to file. Please consult the [Django logging documentation](https://docs.djangoproject.com/en/1.11/topics/logging/) for more information on configuring this setting. Below is an example which will write all INFO and higher messages to a file:
```
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': '/var/log/netbox.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
},
},
}
```
---
## LOGIN_REQUIRED ## LOGIN_REQUIRED
Default: False Default: False

View File

@ -28,6 +28,9 @@ Create a file in the same directory as `configuration.py` (typically `netbox/net
## General Server Configuration ## General Server Configuration
!!! info
When using Windows Server 2012 you may need to specify a port on `AUTH_LDAP_SERVER_URI`. Use `3269` for secure, or `3268` for non-secure.
```python ```python
import ldap import ldap
@ -49,11 +52,11 @@ AUTH_LDAP_BIND_PASSWORD = "demo"
LDAP_IGNORE_CERT_ERRORS = True LDAP_IGNORE_CERT_ERRORS = True
``` ```
!!! info
When using Windows Server 2012 you may need to specify a port on AUTH_LDAP_SERVER_URI - 3269 for secure, 3268 for non-secure.
## User Authentication ## User Authentication
!!! info
When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None.
```python ```python
from django_auth_ldap.config import LDAPSearch from django_auth_ldap.config import LDAPSearch
@ -73,9 +76,6 @@ AUTH_LDAP_USER_ATTR_MAP = {
} }
``` ```
!!! info
When using Windows Server 2012 AUTH_LDAP_USER_DN_TEMPLATE should be set to None.
# User Groups for Permissions # User Groups for Permissions
```python ```python
@ -109,12 +109,11 @@ AUTH_LDAP_GROUP_CACHE_TIMEOUT = 3600
* `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_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. * `is_superuser` - Users mapped to this group will be granted superuser status. Superusers are implicitly granted all permissions.
!!! info It is also possible map user attributes to Django attributes:
It is also possible map user attributes to Django attributes:
```no-highlight ```python
AUTH_LDAP_USER_ATTR_MAP = { AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName", "first_name": "givenName",
"last_name": "sn" "last_name": "sn",
} }
``` ```

View File

@ -52,6 +52,13 @@ Once the new code is in place, run the upgrade script (which may need to be run
# ./upgrade.sh # ./upgrade.sh
``` ```
!!! warning
The upgrade script will prefer Python3 and pip3 if both executables are available. To force it to use Python2 and pip, use the `-2` argument as below.
```no-highlight
# ./upgrade.sh -2
```
This script: This script:
* Installs or upgrades any new required Python packages * Installs or upgrades any new required Python packages

View File

@ -52,6 +52,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'asn', 'account', 'portal_url']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -107,6 +109,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description']
class Meta: class Meta:
ordering = ['provider', 'cid'] ordering = ['provider', 'cid']
unique_together = ['provider', 'cid'] unique_together = ['provider', 'cid']

View File

@ -280,6 +280,10 @@ class Site(CreatedUpdatedModel, CustomFieldModel):
objects = SiteManager() objects = SiteManager()
csv_headers = [
'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email',
]
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
@ -402,6 +406,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel):
objects = RackManager() objects = RackManager()
csv_headers = [
'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units',
]
class Meta: class Meta:
ordering = ['site', 'name'] ordering = ['site', 'name']
unique_together = [ unique_together = [
@ -981,6 +989,11 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
objects = DeviceManager() objects = DeviceManager()
csv_headers = [
'name', 'device_role', 'tenant', 'manufacturer', 'model_name', 'platform', 'serial', 'asset_tag', 'status',
'site', 'rack_group', 'rack_name', 'position', 'face',
]
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
unique_together = ['rack', 'position', 'face'] unique_together = ['rack', 'position', 'face']
@ -1096,6 +1109,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
self.asset_tag, self.asset_tag,
self.get_status_display(), self.get_status_display(),
self.site.name, self.site.name,
self.rack.group.name if self.rack and self.rack.group else None,
self.rack.name if self.rack else None, self.rack.name if self.rack else None,
self.position, self.position,
self.get_face_display(), self.get_face_display(),
@ -1162,6 +1176,8 @@ class ConsolePort(models.Model):
verbose_name='Console server port', blank=True, null=True) verbose_name='Console server port', blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
csv_headers = ['console_server', 'cs_port', 'device', 'console_port', 'connection_status']
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ['device', 'name']
unique_together = ['device', 'name'] unique_together = ['device', 'name']
@ -1231,6 +1247,8 @@ class PowerPort(models.Model):
blank=True, null=True) blank=True, null=True)
connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED)
csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status']
class Meta: class Meta:
ordering = ['device', 'name'] ordering = ['device', 'name']
unique_together = ['device', 'name'] unique_together = ['device', 'name']
@ -1392,6 +1410,8 @@ class InterfaceConnection(models.Model):
connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED, connection_status = models.BooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED,
verbose_name='Status') verbose_name='Status')
csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status']
def clean(self): def clean(self):
try: try:
if self.interface_a == self.interface_b: if self.interface_a == self.interface_b:

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from copy import deepcopy from copy import deepcopy
from difflib import SequenceMatcher
import re import re
from natsort import natsorted from natsort import natsorted
from operator import attrgetter from operator import attrgetter
@ -776,20 +777,14 @@ class DeviceView(View):
services = Service.objects.filter(device=device) services = Service.objects.filter(device=device)
secrets = device.secrets.all() secrets = device.secrets.all()
# Find any related devices for convenient linking in the UI # Find up to ten devices in the same site with the same functional role for quick reference.
related_devices = [] related_devices = Device.objects.filter(
if device.name: site=device.site, device_role=device.device_role
if re.match('.+[0-9]+$', device.name): ).exclude(
# Strip 1 or more trailing digits (e.g. core-switch1) pk=device.pk
base_name = re.match('(.*?)[0-9]+$', device.name).group(1) ).select_related(
elif re.match('.+\d[a-z]$', device.name.lower()): 'rack', 'device_type__manufacturer'
# Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3) )[:10]
base_name = re.match('(.*\d+)[a-z]$', device.name.lower()).group(1)
else:
base_name = None
if base_name:
related_devices = Device.objects.filter(name__istartswith=base_name).exclude(pk=device.pk)\
.select_related('rack', 'device_type__manufacturer')[:10]
# Show graph button on interfaces only if at least one graph has been created. # Show graph button on interfaces only if at least one graph has been created.
show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists() show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()

View File

@ -0,0 +1,62 @@
from __future__ import unicode_literals
import code
import platform
import sys
from django import get_version
from django.apps import apps
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db.models import Model
APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users']
BANNER_TEXT = """### NetBox interactive shell ({node})
### Python {python} | Django {django} | NetBox {netbox}
### lsmodels() will show available models. Use help(<model>) for more info.""".format(
node=platform.node(),
python=platform.python_version(),
django=get_version(),
netbox=settings.VERSION
)
class Command(BaseCommand):
help = "Start the Django shell with all NetBox models already imported"
django_models = {}
def _lsmodels(self):
for app, models in self.django_models.items():
app_name = apps.get_app_config(app).verbose_name
print('{}:'.format(app_name))
for m in models:
print(' {}'.format(m))
def get_namespace(self):
namespace = {}
# Gather Django models from each app
for app in APPS:
self.django_models[app] = []
app_models = sys.modules['{}.models'.format(app)]
for name in dir(app_models):
model = getattr(app_models, name)
try:
if issubclass(model, Model):
namespace[name] = model
self.django_models[app].append(name)
except TypeError:
pass
# Load convenience commands
namespace.update({
'lsmodels': self._lsmodels,
})
return namespace
def handle(self, **options):
shell = code.interact(banner=BANNER_TEXT, local=self.get_namespace())
return shell

View File

@ -180,6 +180,18 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
required=False, required=False,
label='Site', label='Site',
widget=forms.Select( widget=forms.Select(
attrs={'filter-for': 'vlan_group', 'nullable': 'true'}
)
)
vlan_group = ChainedModelChoiceField(
queryset=VLANGroup.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='VLAN group',
widget=APISelect(
api_url='/api/ipam/vlan-groups/?site_id={{site}}',
attrs={'filter-for': 'vlan', 'nullable': 'true'} attrs={'filter-for': 'vlan', 'nullable': 'true'}
) )
) )
@ -187,11 +199,12 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
queryset=VLAN.objects.all(), queryset=VLAN.objects.all(),
chains=( chains=(
('site', 'site'), ('site', 'site'),
('group', 'vlan_group'),
), ),
required=False, required=False,
label='VLAN', label='VLAN',
widget=APISelect( widget=APISelect(
api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name' api_url='/api/ipam/vlans/?site_id={{site}}&group_id={{vlan_group}}', display_field='display_name'
) )
) )
@ -200,6 +213,14 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant'] fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
initial = kwargs.get('initial', {})
if instance and instance.vlan is not None:
initial['vlan_group'] = instance.vlan.group
kwargs['initial'] = initial
super(PrefixForm, self).__init__(*args, **kwargs) super(PrefixForm, self).__init__(*args, **kwargs)
self.fields['vrf'].empty_label = 'Global' self.fields['vrf'].empty_label = 'Global'

View File

@ -89,6 +89,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
verbose_name = 'VRF' verbose_name = 'VRF'
@ -146,6 +148,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['prefix', 'rir', 'date_added', 'description']
class Meta: class Meta:
ordering = ['family', 'prefix'] ordering = ['family', 'prefix']
@ -200,7 +204,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel):
def get_utilization(self): def get_utilization(self):
""" """
Determine the utilization rate of the aggregate prefix and return it as a percentage. Determine the prefix utilization of the aggregate and return it as a percentage.
""" """
child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix)) child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
# Remove overlapping prefixes from list of children # Remove overlapping prefixes from list of children
@ -297,6 +301,10 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
objects = PrefixQuerySet.as_manager() objects = PrefixQuerySet.as_manager()
csv_headers = [
'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description',
]
class Meta: class Meta:
ordering = ['vrf', 'family', 'prefix'] ordering = ['vrf', 'family', 'prefix']
verbose_name_plural = 'prefixes' verbose_name_plural = 'prefixes'
@ -307,9 +315,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('ipam:prefix', args=[self.pk]) return reverse('ipam:prefix', args=[self.pk])
def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def clean(self): def clean(self):
if self.prefix: if self.prefix:
@ -357,6 +362,22 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
self.description, self.description,
]) ])
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
def get_duplicates(self):
return Prefix.objects.filter(vrf=self.vrf, prefix=str(self.prefix)).exclude(pk=self.pk)
def get_utilization(self):
"""
Determine the utilization of the prefix and return it as a percentage.
"""
child_count = IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf).count()
prefix_size = self.prefix.size
if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
prefix_size -= 2
return int(float(child_count) / prefix_size * 100)
@property @property
def new_subnet(self): def new_subnet(self):
if self.family == 4: if self.family == 4:
@ -368,9 +389,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
return None return None
def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status]
class IPAddressManager(models.Manager): class IPAddressManager(models.Manager):
@ -414,6 +432,8 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
objects = IPAddressManager() objects = IPAddressManager()
csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description']
class Meta: class Meta:
ordering = ['family', 'address'] ordering = ['family', 'address']
verbose_name = 'IP address' verbose_name = 'IP address'
@ -452,11 +472,12 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
def to_csv(self): def to_csv(self):
# Determine if this IP is primary for a Device # Determine if this IP is primary for a Device
is_primary = False
if self.family == 4 and getattr(self, 'primary_ip4_for', False): if self.family == 4 and getattr(self, 'primary_ip4_for', False):
is_primary = True is_primary = True
elif self.family == 6 and getattr(self, 'primary_ip6_for', False): elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
is_primary = True is_primary = True
else:
is_primary = False
return csv_format([ return csv_format([
self.address, self.address,
@ -527,6 +548,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['site', 'group_name', 'vid', 'name', 'tenant', 'status', 'role', 'description']
class Meta: class Meta:
ordering = ['site', 'group', 'vid'] ordering = ['site', 'group', 'vid']
unique_together = [ unique_together = [

View File

@ -34,7 +34,7 @@ RIR_ACTIONS = """
UTILIZATION_GRAPH = """ UTILIZATION_GRAPH = """
{% load helpers %} {% load helpers %}
{% utilization_graph value %} {% if record.pk %}{% utilization_graph value %}{% else %}&mdash;{% endif %}
""" """
ROLE_ACTIONS = """ ROLE_ACTIONS = """
@ -241,6 +241,7 @@ class PrefixTable(BaseTable):
prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}})
status = tables.TemplateColumn(STATUS_LABEL) status = tables.TemplateColumn(STATUS_LABEL)
vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF') vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='IP Usage')
tenant = tables.TemplateColumn(TENANT_LINK) tenant = tables.TemplateColumn(TENANT_LINK)
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')])
vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN')
@ -248,7 +249,7 @@ class PrefixTable(BaseTable):
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Prefix model = Prefix
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description')
row_attrs = { row_attrs = {
'class': lambda record: 'success' if not record.pk else '', 'class': lambda record: 'success' if not record.pk else '',
} }

View File

@ -58,6 +58,11 @@ CORS_ORIGIN_REGEX_WHITELIST = [
# r'^(https?://)?(\w+\.)?example\.com$', # r'^(https?://)?(\w+\.)?example\.com$',
] ]
# Set to True to enable server debugging. WARNING: Debugging introduces a substantial performance penalty and may reveal
# sensitive information about your installation. Only enable debugging while performing testing. Never enable debugging
# on a production system.
DEBUG = False
# Email settings # Email settings
EMAIL = { EMAIL = {
'SERVER': 'localhost', 'SERVER': 'localhost',
@ -72,6 +77,10 @@ EMAIL = {
# (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. # (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True.
ENFORCE_GLOBAL_UNIQUE = False ENFORCE_GLOBAL_UNIQUE = False
# Enable custom logging. Please see the Django documentation for detailed guidance on configuring custom logs:
# https://docs.djangoproject.com/en/1.11/topics/logging/
LOGGING = {}
# Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users # Setting this to True will permit only authenticated users to access any part of NetBox. By default, anonymous users
# are permitted to access most data in NetBox (excluding secrets) but not make any changes. # are permitted to access most data in NetBox (excluding secrets) but not make any changes.
LOGIN_REQUIRED = False LOGIN_REQUIRED = False

View File

@ -13,9 +13,9 @@ except ImportError:
) )
VERSION = '2.0.5' VERSION = '2.0.6'
# Import local configuration # Import required configuration parameters
ALLOWED_HOSTS = DATABASE = SECRET_KEY = None ALLOWED_HOSTS = DATABASE = SECRET_KEY = None
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
try: try:
@ -25,33 +25,35 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
"Mandatory setting {} is missing from configuration.py.".format(setting) "Mandatory setting {} is missing from configuration.py.".format(setting)
) )
# Default configurations # Import optional configuration parameters
ADMINS = getattr(configuration, 'ADMINS', []) ADMINS = getattr(configuration, 'ADMINS', [])
DEBUG = getattr(configuration, 'DEBUG', False) BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
EMAIL = getattr(configuration, 'EMAIL', {}) BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
BASE_PATH = getattr(configuration, 'BASE_PATH', '') BASE_PATH = getattr(configuration, 'BASE_PATH', '')
if BASE_PATH: if BASE_PATH:
BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
DEBUG = getattr(configuration, 'DEBUG', False)
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
EMAIL = getattr(configuration, 'EMAIL', {})
LOGGING = getattr(configuration, 'LOGGING', {})
LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False)
MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False) MAINTENANCE_MODE = getattr(configuration, 'MAINTENANCE_MODE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50) PAGINATE_COUNT = getattr(configuration, 'PAGINATE_COUNT', 50)
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False)
NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '') NETBOX_USERNAME = getattr(configuration, 'NETBOX_USERNAME', '')
NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '') NETBOX_PASSWORD = getattr(configuration, 'NETBOX_PASSWORD', '')
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False) SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False) TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
ENFORCE_GLOBAL_UNIQUE = getattr(configuration, 'ENFORCE_GLOBAL_UNIQUE', False)
MAX_PAGE_SIZE = getattr(configuration, 'MAX_PAGE_SIZE', 1000)
CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False)
CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', [])
CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', [])
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Attempt to import LDAP configuration if it has been defined # Attempt to import LDAP configuration if it has been defined

View File

@ -291,6 +291,7 @@ class Secret(CreatedUpdatedModel):
hash = models.CharField(max_length=128, editable=False) hash = models.CharField(max_length=128, editable=False)
plaintext = None plaintext = None
csv_headers = ['device', 'role', 'name', 'plaintext']
class Meta: class Meta:
ordering = ['device', 'role', 'name'] ordering = ['device', 'role', 'name']

View File

@ -57,6 +57,12 @@
<a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a> <a href="{% url 'ipam:aggregate_list' %}?rir={{ aggregate.rir.slug }}">{{ aggregate.rir }}</a>
</td> </td>
</tr> </tr>
<tr>
<td>Utilization</td>
<td>
{{ aggregate.get_utilization }}%
</td>
</tr>
<tr> <tr>
<td>Date Added</td> <td>Date Added</td>
<td> <td>

View File

@ -121,8 +121,8 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>IP Addresses</td> <td>Utilization</td>
<td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }}</a></td> <td><a href="{% url 'ipam:prefix_ipaddresses' pk=prefix.pk %}">{{ ipaddress_count }} IP addresses</a> ({{ prefix.get_utilization }}%)</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -8,13 +8,19 @@
{% render_field form.prefix %} {% render_field form.prefix %}
{% render_field form.status %} {% render_field form.status %}
{% render_field form.vrf %} {% render_field form.vrf %}
{% render_field form.site %}
{% render_field form.vlan %}
{% render_field form.role %} {% render_field form.role %}
{% render_field form.description %} {% render_field form.description %}
{% render_field form.is_pool %} {% render_field form.is_pool %}
</div> </div>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading"><strong>Site/VLAN Assignment</strong></div>
<div class="panel-body">
{% render_field form.site %}
{% render_field form.vlan_group %}
{% render_field form.vlan %}
</div>
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Tenancy</strong></div> <div class="panel-heading"><strong>Tenancy</strong></div>
<div class="panel-body"> <div class="panel-body">

View File

@ -41,6 +41,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel):
comments = models.TextField(blank=True) comments = models.TextField(blank=True)
custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id')
csv_headers = ['name', 'slug', 'group', 'description']
class Meta: class Meta:
ordering = ['group', 'name'] ordering = ['group', 'name']

View File

@ -478,8 +478,8 @@ class ChainedFieldsMixin(forms.BaseForm):
filters_dict = {} filters_dict = {}
for (db_field, parent_field) in field.chains: for (db_field, parent_field) in field.chains:
if self.is_bound and self.data.get(parent_field): if self.is_bound and parent_field in self.data:
filters_dict[db_field] = self.data[parent_field] filters_dict[db_field] = self.data[parent_field] or None
elif self.initial.get(parent_field): elif self.initial.get(parent_field):
filters_dict[db_field] = self.initial[parent_field] filters_dict[db_field] = self.initial[parent_field]
elif self.fields[parent_field].widget.attrs.get('nullable'): elif self.fields[parent_field].widget.attrs.get('nullable'):

View File

@ -102,7 +102,9 @@ class ObjectListView(View):
.format(et.name)) .format(et.name))
# Fall back to built-in CSV export # Fall back to built-in CSV export
elif 'export' in request.GET and hasattr(model, 'to_csv'): elif 'export' in request.GET and hasattr(model, 'to_csv'):
output = '\n'.join([obj.to_csv() for obj in self.queryset]) headers = getattr(model, 'csv_headers', None)
output = ','.join(headers) + '\n' if headers else ''
output += '\n'.join([obj.to_csv() for obj in self.queryset])
response = HttpResponse( response = HttpResponse(
output, output,
content_type='text/csv' content_type='text/csv'

View File

@ -5,6 +5,25 @@
# Once the script completes, remember to restart the WSGI service (e.g. # Once the script completes, remember to restart the WSGI service (e.g.
# gunicorn or uWSGI). # gunicorn or uWSGI).
# Determine which version of Python/pip to use. Default to v3 (if available)
# but allow the user to force v2.
PYTHON="python3"
PIP="pip3"
type $PYTHON >/dev/null 2>&1 && type $PIP >/dev/null 2>&1 || PYTHON="python" PIP="pip"
while getopts ":2" opt; do
case $opt in
2)
PYTHON="python"
PIP="pip"
echo "Forcing Python/pip v2"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit
;;
esac
done
# Optionally use sudo if not already root, and always prompt for password # Optionally use sudo if not already root, and always prompt for password
# before running the command # before running the command
PREFIX="sudo -k " PREFIX="sudo -k "
@ -20,12 +39,6 @@ COMMAND="${PREFIX}find . -name \"*.pyc\" -delete"
echo "Cleaning up stale Python bytecode ($COMMAND)..." echo "Cleaning up stale Python bytecode ($COMMAND)..."
eval $COMMAND eval $COMMAND
# Prefer python3/pip3
PYTHON="python3"
type $PYTHON >/dev/null 2>&1 || PYTHON="python"
PIP="pip3"
type $PIP >/dev/null 2>&1 || PIP="pip"
# Install any new Python packages # Install any new Python packages
COMMAND="${PREFIX}${PIP} install -r requirements.txt --upgrade" COMMAND="${PREFIX}${PIP} install -r requirements.txt --upgrade"
echo "Updating required Python packages ($COMMAND)..." echo "Updating required Python packages ($COMMAND)..."