From 627b7fa7639a8e652d28d292560702fdec1eefd9 Mon Sep 17 00:00:00 2001 From: Pavel Korovin
Date: Mon, 11 Jul 2016 02:41:44 +0300 Subject: [PATCH] Enable serving NetBox from non-root location with URL_PREFIX configuration option --- netbox/circuits/forms.py | 10 +++-- netbox/dcim/forms.py | 32 ++++++++------- netbox/ipam/forms.py | 10 +++-- netbox/netbox/configuration.example.py | 9 +++++ netbox/netbox/settings.py | 22 ++++++----- netbox/netbox/urls.py | 55 ++++++++++++++------------ netbox/templates/_base.html | 4 +- 7 files changed, 86 insertions(+), 56 deletions(-) diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 7046b8ec3..ff5f6e2b2 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.conf import settings from django.db.models import Count from dcim.models import Site, Device, Interface, Rack, IFACE_FF_VIRTUAL @@ -9,6 +10,9 @@ from utilities.forms import ( from .models import Circuit, CircuitType, Provider +url_prefix = '' +if settings.URL_PREFIX.strip('/'): + url_prefix = '/{0}'.format(settings.URL_PREFIX.strip('/')) # # Providers @@ -82,16 +86,16 @@ class CircuitTypeBulkDeleteForm(ConfirmationForm): class CircuitForm(forms.ModelForm, BootstrapMixin): site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) rack = forms.ModelChoiceField(queryset=Rack.objects.all(), required=False, label='Rack', - widget=APISelect(api_url='/api/dcim/racks/?site_id={{site}}', + widget=APISelect(api_url=url_prefix + '/api/dcim/racks/?site_id={{site}}', attrs={'filter-for': 'device'})) device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device', - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/?rack_id={{rack}}', attrs={'filter-for': 'interface'})) livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( query_key='q', query_url='dcim-api:device_list', field_to_update='device') ) interface = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Interface', - widget=APISelect(api_url='/api/dcim/devices/{{device}}/interfaces/?type=physical', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/{{device}}/interfaces/?type=physical', disabled_indicator='is_connected')) comments = CommentField() diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index bc3c8d8e8..4670a4ac2 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1,6 +1,7 @@ import re from django import forms +from django.conf import settings from django.db.models import Count, Q from ipam.models import IPAddress @@ -25,6 +26,9 @@ FORM_STATUS_CHOICES += STATUS_CHOICES DEVICE_BY_PK_RE = '{\d+\}' +url_prefix = '' +if settings.URL_PREFIX.strip('/'): + url_prefix = '/{0}'.format(settings.URL_PREFIX.strip('/')) def get_device_by_name_or_pk(name): """ @@ -105,7 +109,7 @@ class RackGroupFilterForm(forms.Form, BootstrapMixin): class RackForm(forms.ModelForm, BootstrapMixin): group = forms.ModelChoiceField(queryset=RackGroup.objects.all(), required=False, label='Group', widget=APISelect( - api_url='/api/dcim/rack-groups/?site_id={{site}}', + api_url=url_prefix + '/api/dcim/rack-groups/?site_id={{site}}', )) comments = CommentField() @@ -330,18 +334,18 @@ class PlatformBulkDeleteForm(ConfirmationForm): class DeviceForm(forms.ModelForm, BootstrapMixin): site = forms.ModelChoiceField(queryset=Site.objects.all(), widget=forms.Select(attrs={'filter-for': 'rack'})) rack = forms.ModelChoiceField(queryset=Rack.objects.all(), widget=APISelect( - api_url='/api/dcim/racks/?site_id={{site}}', + api_url=url_prefix + '/api/dcim/racks/?site_id={{site}}', display_field='display_name', attrs={'filter-for': 'position'} )) position = forms.TypedChoiceField(required=False, empty_value=None, help_text="For multi-U devices, this is the lowest occupied rack unit.", - widget=APISelect(api_url='/api/dcim/racks/{{rack}}/rack-units/?face={{face}}', + widget=APISelect(api_url=url_prefix + '/api/dcim/racks/{{rack}}/rack-units/?face={{face}}', disabled_indicator='device')) manufacturer = forms.ModelChoiceField(queryset=Manufacturer.objects.all(), widget=forms.Select(attrs={'filter-for': 'device_type'})) device_type = forms.ModelChoiceField(queryset=DeviceType.objects.all(), label='Device type', widget=APISelect( - api_url='/api/dcim/device-types/?manufacturer_id={{manufacturer}}', + api_url=url_prefix + '/api/dcim/device-types/?manufacturer_id={{manufacturer}}', display_field='model' )) comments = CommentField() @@ -614,13 +618,13 @@ class ConsolePortConnectionForm(forms.ModelForm, BootstrapMixin): rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False, widget=forms.Select(attrs={'filter-for': 'console_server'})) console_server = forms.ModelChoiceField(queryset=Device.objects.all(), label='Console Server', required=False, - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_console_server=True', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/?rack_id={{rack}}&is_console_server=True', attrs={'filter-for': 'cs_port'})) livesearch = forms.CharField(required=False, label='Console Server', widget=Livesearch( query_key='q', query_url='dcim-api:device_list', field_to_update='console_server') ) cs_port = forms.ModelChoiceField(queryset=ConsoleServerPort.objects.all(), label='Port', - widget=APISelect(api_url='/api/dcim/devices/{{console_server}}/console-server-ports/', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/{{console_server}}/console-server-ports/', disabled_indicator='connected_console')) class Meta: @@ -681,13 +685,13 @@ class ConsoleServerPortConnectionForm(forms.Form, BootstrapMixin): rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False, widget=forms.Select(attrs={'filter-for': 'device'})) device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False, - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/?rack_id={{rack}}', attrs={'filter-for': 'port'})) livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( query_key='q', query_url='dcim-api:device_list', field_to_update='device') ) port = forms.ModelChoiceField(queryset=ConsolePort.objects.all(), label='Port', - widget=APISelect(api_url='/api/dcim/devices/{{device}}/console-ports/', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/{{device}}/console-ports/', disabled_indicator='cs_port')) connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status', widget=forms.Select(choices=CONNECTION_STATUS_CHOICES)) @@ -810,13 +814,13 @@ class PowerPortConnectionForm(forms.ModelForm, BootstrapMixin): rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False, widget=forms.Select(attrs={'filter-for': 'pdu'})) pdu = forms.ModelChoiceField(queryset=Device.objects.all(), label='PDU', required=False, - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}&is_pdu=True', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/?rack_id={{rack}}&is_pdu=True', attrs={'filter-for': 'power_outlet'})) livesearch = forms.CharField(required=False, label='PDU', widget=Livesearch( query_key='q', query_url='dcim-api:device_list', field_to_update='pdu') ) power_outlet = forms.ModelChoiceField(queryset=PowerOutlet.objects.all(), label='Outlet', - widget=APISelect(api_url='/api/dcim/devices/{{pdu}}/power-outlets/', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/{{pdu}}/power-outlets/', disabled_indicator='connected_port')) class Meta: @@ -877,13 +881,13 @@ class PowerOutletConnectionForm(forms.Form, BootstrapMixin): rack = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False, widget=forms.Select(attrs={'filter-for': 'device'})) device = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False, - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack}}', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/?rack_id={{rack}}', attrs={'filter-for': 'port'})) livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( query_key='q', query_url='dcim-api:device_list', field_to_update='device') ) port = forms.ModelChoiceField(queryset=PowerPort.objects.all(), label='Port', - widget=APISelect(api_url='/api/dcim/devices/{{device}}/power-ports/', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/{{device}}/power-ports/', disabled_indicator='power_outlet')) connection_status = forms.BooleanField(required=False, initial=CONNECTION_STATUS_CONNECTED, label='Status', widget=forms.Select(choices=CONNECTION_STATUS_CHOICES)) @@ -952,13 +956,13 @@ class InterfaceConnectionForm(forms.ModelForm, BootstrapMixin): rack_b = forms.ModelChoiceField(queryset=Rack.objects.all(), label='Rack', required=False, widget=forms.Select(attrs={'filter-for': 'device_b'})) device_b = forms.ModelChoiceField(queryset=Device.objects.all(), label='Device', required=False, - widget=APISelect(api_url='/api/dcim/devices/?rack_id={{rack_b}}', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/?rack_id={{rack_b}}', attrs={'filter-for': 'interface_b'})) livesearch = forms.CharField(required=False, label='Device', widget=Livesearch( query_key='q', query_url='dcim-api:device_list', field_to_update='device_b') ) interface_b = forms.ModelChoiceField(queryset=Interface.objects.all(), label='Interface', - widget=APISelect(api_url='/api/dcim/devices/{{device_b}}/interfaces/?type=physical', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/{{device_b}}/interfaces/?type=physical', disabled_indicator='is_connected')) class Meta: diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 0c7a411cd..539f586e8 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1,6 +1,7 @@ from netaddr import IPNetwork from django import forms +from django.conf import settings from django.db.models import Count from dcim.models import Site, Device, Interface @@ -16,6 +17,9 @@ from .models import ( FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES +url_prefix = '' +if settings.URL_PREFIX.strip('/'): + url_prefix = '/{0}'.format(settings.URL_PREFIX.strip('/')) # # VRFs @@ -144,7 +148,7 @@ class PrefixForm(forms.ModelForm, BootstrapMixin): site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(attrs={'filter-for': 'vlan'})) vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN', - widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}', + widget=APISelect(api_url=url_prefix + '/api/ipam/vlans/?site_id={{site}}', display_field='display_name')) class Meta: @@ -270,13 +274,13 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin): nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(attrs={'filter-for': 'nat_device'})) nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device', - widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}', + widget=APISelect(api_url=url_prefix + '/api/dcim/devices/?site_id={{nat_site}}', attrs={'filter-for': 'nat_inside'})) livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch( query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address') ) nat_inside = forms.ModelChoiceField(queryset=IPAddress.objects.all(), required=False, label='NAT (Inside)', - widget=APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}', + widget=APISelect(api_url=url_prefix + '/api/ipam/ip-addresses/?device_id={{nat_device}}', display_field='address')) class Meta: diff --git a/netbox/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 96e605859..d4e79fea7 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -32,6 +32,15 @@ SECRET_KEY = '' # # ######################### +# URL prefix if NetBox is served from non-root location, e.g. http://mysite/netbox/ +# By default, URL_PREFIX = '/', i.e. NetBox is served from root location +#URL_PREFIX = '/netbox' + +# Directory and URL from which static files will be served +# for more info, see https://docs.djangoproject.com/en/1.9/howto/static-files/ +#STATIC_ROOT = '/var/www/htdocs/netbox/static/' +#STATIC_URL = '/static/netbox/' + # Specify one or more name and email address tuples representing NetBox administrators. These people will be notified of # application errors (assuming correct email settings are provided). ADMINS = [ diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e8b0b2289..8cd097dc1 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,15 @@ for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: raise ImproperlyConfigured("Mandatory setting {} is missing from configuration.py. Please define it per the " "documentation.".format(setting)) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + # Default configurations +URL_PREFIX = getattr(configuration, 'URL_PREFIX', '/') +url_prefix = '' +if URL_PREFIX.strip('/'): + url_prefix = '/{0}'.format(URL_PREFIX.strip('/')) +STATIC_ROOT = getattr(configuration, 'STATIC_ROOT', BASE_DIR + '/static/') +STATIC_URL = getattr(configuration, 'STATIC_URL', '/static' + url_prefix + '/') ADMINS = getattr(configuration, 'ADMINS', []) DEBUG = getattr(configuration, 'DEBUG', False) EMAIL = getattr(configuration, 'EMAIL', {}) @@ -69,8 +77,6 @@ if LDAP_CONFIGURED: raise ImproperlyConfigured("LDAP authentication has been configured, but django-auth-ldap is not installed. " "You can remove netbox/ldap.py to disable LDAP.") -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - # Database configuration.DATABASE.update({'ENGINE': 'django.db.backends.postgresql'}) DATABASES = { @@ -153,9 +159,7 @@ USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ -STATIC_ROOT = BASE_DIR + '/static/' -STATIC_URL = '/static/' +# https://docs.djangoproject.com/en/1.9/howto/static-files/ STATICFILES_DIRS = ( os.path.join(BASE_DIR, "project-static"), ) @@ -166,9 +170,9 @@ MESSAGE_TAGS = { } # Authentication URLs -LOGIN_URL = '/login/' -LOGIN_REDIRECT_URL = '/' -LOGOUT_URL = '/logout/' +LOGIN_URL = url_prefix + '/login/' +LOGIN_REDIRECT_URL = url_prefix + '/' +LOGOUT_URL = url_prefix + '/logout/' # Secrets SECRETS_MIN_PUBKEY_SIZE = 2048 @@ -180,7 +184,7 @@ REST_FRAMEWORK = { # Swagger settings (API docs) SWAGGER_SETTINGS = { - 'base_path': '{}/api/docs'.format(ALLOWED_HOSTS[0]), + 'base_path': '{}'.format(ALLOWED_HOSTS[0]) + url_prefix + '/api/docs', } diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index a7f908544..5d0fdaffe 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.conf.urls import include, url from django.contrib import admin from django.views.defaults import page_not_found @@ -5,36 +6,40 @@ from django.views.defaults import page_not_found from views import home, trigger_500 from users.views import login, logout +url_prefix = '' +if settings.URL_PREFIX.strip('/'): + url_prefix = '{0}/'.format(settings.URL_PREFIX.strip('/')) urlpatterns = [ + url(r'^{0}'.format(url_prefix), + include([ + # Default page + url(r'^$', home, name='home'), - # Default page - url(r'^$', home, name='home'), + # Login/logout + url(r'^login/$', login, name='login'), + url(r'^logout/$', logout, name='logout'), - # Login/logout - url(r'^login/$', login, name='login'), - url(r'^logout/$', logout, name='logout'), + # Apps + url(r'^circuits/', include('circuits.urls', namespace='circuits')), + url(r'^dcim/', include('dcim.urls', namespace='dcim')), + url(r'^ipam/', include('ipam.urls', namespace='ipam')), + url(r'^secrets/', include('secrets.urls', namespace='secrets')), + url(r'^profile/', include('users.urls', namespace='users')), - # Apps - url(r'^circuits/', include('circuits.urls', namespace='circuits')), - url(r'^dcim/', include('dcim.urls', namespace='dcim')), - url(r'^ipam/', include('ipam.urls', namespace='ipam')), - url(r'^secrets/', include('secrets.urls', namespace='secrets')), - url(r'^profile/', include('users.urls', namespace='users')), + # API + url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), + url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')), + url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), + url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), + url(r'^api/docs/', include('rest_framework_swagger.urls')), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - # API - url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), - url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')), - url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), - url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), - url(r'^api/docs/', include('rest_framework_swagger.urls')), - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - - # Error testing - url(r'^404/$', page_not_found), - url(r'^500/$', trigger_500), - - # Admin - url(r'^admin/', include(admin.site.urls)), + # Error testing + url(r'^404/$', page_not_found), + url(r'^500/$', trigger_500), + # Admin + url(r'^admin/', include(admin.site.urls)), + ])), ] diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 438ffdff5..2b8a74d48 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -19,7 +19,7 @@ - NetBox + NetBox