From 2b921c21ffba96a619ac18ede1820f3d2d4956a2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Jun 2017 10:12:39 -0400 Subject: [PATCH 01/11] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4228f1b27..d46f4b226 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.0.5' +VERSION = '2.0.6-dev' # Import local configuration ALLOWED_HOSTS = DATABASE = SECRET_KEY = None From f3e997ea39105d5032d02c54d26b459fd4d0c5c7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Jun 2017 12:37:25 -0400 Subject: [PATCH 02/11] Closes #40: Added IP utilization graph to prefix list --- netbox/ipam/models.py | 24 +++++++++++++++++------- netbox/ipam/tables.py | 5 +++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 04f193dd4..40e3fefde 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -200,7 +200,7 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): 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)) # Remove overlapping prefixes from list of children @@ -307,9 +307,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): def get_absolute_url(self): 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): if self.prefix: @@ -357,6 +354,22 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): 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 def new_subnet(self): if self.family == 4: @@ -368,9 +381,6 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1)) return None - def get_status_class(self): - return STATUS_CHOICE_CLASSES[self.status] - class IPAddressManager(models.Manager): diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 64b7d62fa..767bd2cec 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -34,7 +34,7 @@ RIR_ACTIONS = """ UTILIZATION_GRAPH = """ {% load helpers %} -{% utilization_graph value %} +{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %} """ ROLE_ACTIONS = """ @@ -241,6 +241,7 @@ class PrefixTable(BaseTable): prefix = tables.TemplateColumn(PREFIX_LINK, attrs={'th': {'style': 'padding-left: 17px'}}) status = tables.TemplateColumn(STATUS_LABEL) 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) site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) vlan = tables.LinkColumn('ipam:vlan', args=[Accessor('vlan.pk')], verbose_name='VLAN') @@ -248,7 +249,7 @@ class PrefixTable(BaseTable): class Meta(BaseTable.Meta): 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 = { 'class': lambda record: 'success' if not record.pk else '', } From 4f95926cbd956c070dad61be05bcf8afebd40aad Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Jun 2017 12:48:49 -0400 Subject: [PATCH 03/11] Added utilization percetange to aggregate and prefix views --- netbox/templates/ipam/aggregate.html | 6 ++++++ netbox/templates/ipam/prefix.html | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index bd5801f0e..82961d3bf 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -57,6 +57,12 @@ {{ aggregate.rir }} + + Utilization + + {{ aggregate.get_utilization }}% + + Date Added diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html index b2b996fce..23a996d82 100644 --- a/netbox/templates/ipam/prefix.html +++ b/netbox/templates/ipam/prefix.html @@ -121,8 +121,8 @@ - IP Addresses - {{ ipaddress_count }} + Utilization + {{ ipaddress_count }} IP addresses ({{ prefix.get_utilization }}%) From 787899257090cba501a1cdae3a3881d4f36a6958 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 8 Jun 2017 16:38:25 -0400 Subject: [PATCH 04/11] First stab at an interactive shell which pre-imports all models --- netbox/extras/management/commands/nbshell.py | 62 ++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 netbox/extras/management/commands/nbshell.py diff --git a/netbox/extras/management/commands/nbshell.py b/netbox/extras/management/commands/nbshell.py new file mode 100644 index 000000000..48448c16f --- /dev/null +++ b/netbox/extras/management/commands/nbshell.py @@ -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() 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 From 0a929f29714d3465e3734f72985f44825dfbe8db Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Jun 2017 12:13:47 -0400 Subject: [PATCH 05/11] Fixes #1253: Improved upgrade.sh to allow forcing Python2 --- docs/installation/upgrading.md | 7 +++++++ upgrade.sh | 25 +++++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index 6d0db728d..02dbb878f 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -52,6 +52,13 @@ Once the new code is in place, run the upgrade script (which may need to be run # ./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: * Installs or upgrades any new required Python packages diff --git a/upgrade.sh b/upgrade.sh index f8ce22a6f..24189a424 100755 --- a/upgrade.sh +++ b/upgrade.sh @@ -5,6 +5,25 @@ # Once the script completes, remember to restart the WSGI service (e.g. # 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 # before running the command PREFIX="sudo -k " @@ -20,12 +39,6 @@ COMMAND="${PREFIX}find . -name \"*.pyc\" -delete" echo "Cleaning up stale Python bytecode ($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 COMMAND="${PREFIX}${PIP} install -r requirements.txt --upgrade" echo "Updating required Python packages ($COMMAND)..." From 9e1d03b3837e05f7f6ccfb391b84a87da0648296 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Jun 2017 12:19:32 -0400 Subject: [PATCH 06/11] Formatting cleanup --- docs/installation/ldap.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/installation/ldap.md b/docs/installation/ldap.md index 2555ccb12..0d546863e 100644 --- a/docs/installation/ldap.md +++ b/docs/installation/ldap.md @@ -28,6 +28,9 @@ Create a file in the same directory as `configuration.py` (typically `netbox/net ## 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 import ldap @@ -49,11 +52,11 @@ AUTH_LDAP_BIND_PASSWORD = "demo" 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 +!!! info + When using Windows Server, `2012 AUTH_LDAP_USER_DN_TEMPLATE` should be set to None. + ```python 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 ```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_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 - AUTH_LDAP_USER_ATTR_MAP = { - "first_name": "givenName", - "last_name": "sn" - } - ``` +```python +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": "givenName", + "last_name": "sn", +} +``` From 05a796faf14f5a36349a2582adfbf4ae9a5289df Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Jun 2017 14:15:12 -0400 Subject: [PATCH 07/11] Closes #704: Allow filtering VLANs by group when editing prefixes --- netbox/ipam/forms.py | 23 ++++++++++++++++++++++- netbox/templates/ipam/prefix_edit.html | 10 ++++++++-- netbox/utilities/forms.py | 4 ++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 82e489fa8..b4db64d81 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -180,6 +180,18 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): required=False, label='Site', 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'} ) ) @@ -187,11 +199,12 @@ class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm): queryset=VLAN.objects.all(), chains=( ('site', 'site'), + ('group', 'vlan_group'), ), required=False, label='VLAN', 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'] 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) self.fields['vrf'].empty_label = 'Global' diff --git a/netbox/templates/ipam/prefix_edit.html b/netbox/templates/ipam/prefix_edit.html index 089335c02..938a75da3 100644 --- a/netbox/templates/ipam/prefix_edit.html +++ b/netbox/templates/ipam/prefix_edit.html @@ -8,13 +8,19 @@ {% render_field form.prefix %} {% render_field form.status %} {% render_field form.vrf %} - {% render_field form.site %} - {% render_field form.vlan %} {% render_field form.role %} {% render_field form.description %} {% render_field form.is_pool %} +
+
Site/VLAN Assignment
+
+ {% render_field form.site %} + {% render_field form.vlan_group %} + {% render_field form.vlan %} +
+
Tenancy
diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 231de52e5..16c40a727 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -478,8 +478,8 @@ class ChainedFieldsMixin(forms.BaseForm): filters_dict = {} for (db_field, parent_field) in field.chains: - if self.is_bound and self.data.get(parent_field): - filters_dict[db_field] = self.data[parent_field] + if self.is_bound and parent_field in self.data: + filters_dict[db_field] = self.data[parent_field] or None elif self.initial.get(parent_field): filters_dict[db_field] = self.initial[parent_field] elif self.fields[parent_field].widget.attrs.get('nullable'): From 8a849ebeffcebe519238678c956730ee3582736a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Jun 2017 15:03:10 -0400 Subject: [PATCH 08/11] Closes #990: Enable logging configuration in configuration.py --- docs/configuration/optional-settings.md | 28 ++++++++++++++++++ netbox/netbox/configuration.example.py | 9 ++++++ netbox/netbox/settings.py | 38 +++++++++++++------------ 3 files changed, 57 insertions(+), 18 deletions(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index 6c68ca386..05e60dcac 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -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 Default: False diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index bc255bac3..2e08090c7 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -58,6 +58,11 @@ CORS_ORIGIN_REGEX_WHITELIST = [ # 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 = { 'SERVER': 'localhost', @@ -72,6 +77,10 @@ EMAIL = { # (all prefixes and IP addresses not assigned to a VRF), set ENFORCE_GLOBAL_UNIQUE to True. 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 # are permitted to access most data in NetBox (excluding secrets) but not make any changes. LOGIN_REQUIRED = False diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index d46f4b226..5d72b9b84 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -15,7 +15,7 @@ except ImportError: VERSION = '2.0.6-dev' -# Import local configuration +# Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: try: @@ -25,33 +25,35 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: "Mandatory setting {} is missing from configuration.py.".format(setting) ) -# Default configurations +# Import optional configuration parameters ADMINS = getattr(configuration, 'ADMINS', []) -DEBUG = getattr(configuration, 'DEBUG', False) -EMAIL = getattr(configuration, 'EMAIL', {}) -LOGIN_REQUIRED = getattr(configuration, 'LOGIN_REQUIRED', False) +BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False) +BANNER_TOP = getattr(configuration, 'BANNER_TOP', False) BASE_PATH = getattr(configuration, 'BASE_PATH', '') if BASE_PATH: 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) +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', '') -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') -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') -BANNER_TOP = getattr(configuration, 'BANNER_TOP', False) -BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False) -PREFER_IPV4 = getattr(configuration, 'PREFER_IPV4', False) -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', []) +SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') +TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a') +TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC') + CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # Attempt to import LDAP configuration if it has been defined From 08883d86ef7e7914804f058024c7ed3f09b186c1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Jun 2017 16:24:59 -0400 Subject: [PATCH 09/11] Closes #913: Added headers to object CSV exports --- netbox/circuits/models.py | 4 ++++ netbox/dcim/models.py | 20 ++++++++++++++++++++ netbox/ipam/models.py | 15 ++++++++++++++- netbox/secrets/models.py | 1 + netbox/tenancy/models.py | 2 ++ netbox/utilities/views.py | 4 +++- 6 files changed, 44 insertions(+), 2 deletions(-) diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 7da5c4f73..44018ae1c 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -52,6 +52,8 @@ class Provider(CreatedUpdatedModel, CustomFieldModel): comments = models.TextField(blank=True) 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: ordering = ['name'] @@ -107,6 +109,8 @@ class Circuit(CreatedUpdatedModel, CustomFieldModel): comments = models.TextField(blank=True) 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: ordering = ['provider', 'cid'] unique_together = ['provider', 'cid'] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index bbc27b8d9..6411c6bff 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -280,6 +280,10 @@ class Site(CreatedUpdatedModel, CustomFieldModel): objects = SiteManager() + csv_headers = [ + 'name', 'slug', 'region', 'tenant', 'facility', 'asn', 'contact_name', 'contact_phone', 'contact_email', + ] + class Meta: ordering = ['name'] @@ -402,6 +406,10 @@ class Rack(CreatedUpdatedModel, CustomFieldModel): objects = RackManager() + csv_headers = [ + 'site', 'group_name', 'name', 'facility_id', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units', + ] + class Meta: ordering = ['site', 'name'] unique_together = [ @@ -981,6 +989,11 @@ class Device(CreatedUpdatedModel, CustomFieldModel): 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: ordering = ['name'] unique_together = ['rack', 'position', 'face'] @@ -1096,6 +1109,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel): self.asset_tag, self.get_status_display(), 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.position, self.get_face_display(), @@ -1162,6 +1176,8 @@ class ConsolePort(models.Model): verbose_name='Console server port', blank=True, null=True) 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: ordering = ['device', 'name'] unique_together = ['device', 'name'] @@ -1231,6 +1247,8 @@ class PowerPort(models.Model): blank=True, null=True) connection_status = models.NullBooleanField(choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED) + csv_headers = ['pdu', 'power_outlet', 'device', 'power_port', 'connection_status'] + class Meta: ordering = ['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, verbose_name='Status') + csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status'] + def clean(self): try: if self.interface_a == self.interface_b: diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 40e3fefde..89ee0facc 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -89,6 +89,8 @@ class VRF(CreatedUpdatedModel, CustomFieldModel): description = models.CharField(max_length=100, blank=True) 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: ordering = ['name'] verbose_name = 'VRF' @@ -146,6 +148,8 @@ class Aggregate(CreatedUpdatedModel, CustomFieldModel): description = models.CharField(max_length=100, blank=True) custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + csv_headers = ['prefix', 'rir', 'date_added', 'description'] + class Meta: ordering = ['family', 'prefix'] @@ -297,6 +301,10 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel): objects = PrefixQuerySet.as_manager() + csv_headers = [ + 'prefix', 'vrf', 'tenant', 'site', 'vlan_group', 'vlan_vid', 'status', 'role', 'is_pool', 'description', + ] + class Meta: ordering = ['vrf', 'family', 'prefix'] verbose_name_plural = 'prefixes' @@ -424,6 +432,8 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): objects = IPAddressManager() + csv_headers = ['address', 'vrf', 'tenant', 'status', 'device', 'interface_name', 'is_primary', 'description'] + class Meta: ordering = ['family', 'address'] verbose_name = 'IP address' @@ -462,11 +472,12 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel): def to_csv(self): # Determine if this IP is primary for a Device - is_primary = False if self.family == 4 and getattr(self, 'primary_ip4_for', False): is_primary = True elif self.family == 6 and getattr(self, 'primary_ip6_for', False): is_primary = True + else: + is_primary = False return csv_format([ self.address, @@ -537,6 +548,8 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel): description = models.CharField(max_length=100, blank=True) 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: ordering = ['site', 'group', 'vid'] unique_together = [ diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index bf423fdf6..e8eb2ff45 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -291,6 +291,7 @@ class Secret(CreatedUpdatedModel): hash = models.CharField(max_length=128, editable=False) plaintext = None + csv_headers = ['device', 'role', 'name', 'plaintext'] class Meta: ordering = ['device', 'role', 'name'] diff --git a/netbox/tenancy/models.py b/netbox/tenancy/models.py index ea3405df9..a43327a06 100644 --- a/netbox/tenancy/models.py +++ b/netbox/tenancy/models.py @@ -41,6 +41,8 @@ class Tenant(CreatedUpdatedModel, CustomFieldModel): comments = models.TextField(blank=True) custom_field_values = GenericRelation(CustomFieldValue, content_type_field='obj_type', object_id_field='obj_id') + csv_headers = ['name', 'slug', 'group', 'description'] + class Meta: ordering = ['group', 'name'] diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 7e95469f1..223ef43a9 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -102,7 +102,9 @@ class ObjectListView(View): .format(et.name)) # Fall back to built-in CSV export 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( output, content_type='text/csv' From cfff69a715c285b39d8b7bff40c0cf654ffb3046 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 9 Jun 2017 17:04:09 -0400 Subject: [PATCH 10/11] Closes #1180: Simplified the process of finding related devices when viewing a device --- netbox/dcim/views.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 01957ccbb..40b18e33a 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from copy import deepcopy +from difflib import SequenceMatcher import re from natsort import natsorted from operator import attrgetter @@ -776,20 +777,14 @@ class DeviceView(View): services = Service.objects.filter(device=device) secrets = device.secrets.all() - # Find any related devices for convenient linking in the UI - related_devices = [] - if device.name: - if re.match('.+[0-9]+$', device.name): - # Strip 1 or more trailing digits (e.g. core-switch1) - base_name = re.match('(.*?)[0-9]+$', device.name).group(1) - elif re.match('.+\d[a-z]$', device.name.lower()): - # Strip a trailing letter if preceded by a digit (e.g. dist-switch3a -> dist-switch3) - 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] + # Find up to ten devices in the same site with the same functional role for quick reference. + related_devices = Device.objects.filter( + site=device.site, device_role=device.device_role + ).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_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists() From 3a2c5b318a74a552fb87c6cea56b9c60fc9c359c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 12 Jun 2017 09:44:09 -0400 Subject: [PATCH 11/11] Release v2.0.6 --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 5d72b9b84..4ee969ac9 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.0.6-dev' +VERSION = '2.0.6' # Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None