diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index 1a13353b5..c7c2eb8ed 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -1,12 +1,11 @@ # Installation -**Debian/Ubuntu** +**Ubuntu** Python 3: ```no-highlight # apt-get install -y python3 python3-dev python3-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev -# update-alternatives --install /usr/bin/python python /usr/bin/python3 1 ``` Python 2: @@ -15,7 +14,7 @@ Python 2: # apt-get install -y python2.7 python-dev python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev libssl-dev zlib1g-dev ``` -**CentOS/RHEL** +**CentOS** Python 3: @@ -57,13 +56,13 @@ Create the base directory for the NetBox installation. For this guide, we'll use If `git` is not already installed, install it: -**Debian/Ubuntu** +**Ubuntu** ```no-highlight # apt-get install -y git ``` -**CentOS/RHEL** +**CentOS** ```no-highlight # yum install -y git @@ -150,11 +149,14 @@ You may use the script located at `netbox/generate_secret_key.py` to generate a # Run Database Migrations -Before NetBox can run, we need to install the database schema. This is done by running `./manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example): +!!! warning + The examples on the rest of this page call the `python` executable, which will be Python2 on most systems. Replace this with `python3` if you're running NetBox on Python3. + +Before NetBox can run, we need to install the database schema. This is done by running `python manage.py migrate` from the `netbox` directory (`/opt/netbox/netbox/` in our example): ```no-highlight # cd /opt/netbox/netbox/ -# ./manage.py migrate +# python manage.py migrate Operations to perform: Apply all migrations: dcim, sessions, admin, ipam, utilities, auth, circuits, contenttypes, extras, secrets, users Running migrations: @@ -172,7 +174,7 @@ If this step results in a PostgreSQL authentication error, ensure that the usern NetBox does not come with any predefined user accounts. You'll need to create a super user to be able to log into NetBox: ```no-highlight -# ./manage.py createsuperuser +# python manage.py createsuperuser Username: admin Email address: admin@example.com Password: @@ -183,7 +185,7 @@ Superuser created successfully. # Collect Static Files ```no-highlight -# ./manage.py collectstatic --no-input +# python manage.py collectstatic --no-input You have requested to collect static files at the destination location as specified in your settings: @@ -204,7 +206,7 @@ NetBox ships with some initial data to help you get started: RIR definitions, co This step is optional. It's perfectly fine to start using NetBox without using this initial data if you'd rather create everything from scratch. ```no-highlight -# ./manage.py loaddata initial_data +# python manage.py loaddata initial_data Installed 43 object(s) from 4 fixture(s) ``` @@ -213,7 +215,7 @@ Installed 43 object(s) from 4 fixture(s) At this point, NetBox should be able to run. We can verify this by starting a development instance: ```no-highlight -# ./manage.py runserver 0.0.0.0:8000 --insecure +# python manage.py runserver 0.0.0.0:8000 --insecure Performing system checks... System check identified no issues (0 silenced). diff --git a/docs/installation/postgresql.md b/docs/installation/postgresql.md index 543a0a2cf..75c754707 100644 --- a/docs/installation/postgresql.md +++ b/docs/installation/postgresql.md @@ -1,15 +1,18 @@ NetBox requires a PostgreSQL database to store data. (Please note that MySQL is not supported, as NetBox leverages PostgreSQL's built-in [network address types](https://www.postgresql.org/docs/9.1/static/datatype-net-types.html).) +!!! note + The installation instructions provided here have been tested to work on Ubuntu 16.04 and CentOS 6.9. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. + # Installation -**Debian/Ubuntu** +**Ubuntu** ```no-highlight # apt-get update # apt-get install -y postgresql libpq-dev ``` -**CentOS/RHEL** +**CentOS** ```no-highlight # yum install -y postgresql postgresql-server postgresql-devel diff --git a/docs/installation/web-server.md b/docs/installation/web-server.md index 7cbfad2e4..9da487f13 100644 --- a/docs/installation/web-server.md +++ b/docs/installation/web-server.md @@ -3,7 +3,7 @@ We'll set up a simple WSGI front end using [gunicorn](http://gunicorn.org/) for the purposes of this guide. For web servers, we provide example configurations for both [nginx](https://www.nginx.com/resources/wiki/) and [Apache](http://httpd.apache.org/docs/2.4). (You are of course free to use whichever combination of HTTP and WSGI services you'd like.) We'll also use [supervisord](http://supervisord.org/) to enable service persistence. !!! info - Only Debian/Ubuntu instructions are provided here, but the installation process for CentOS/RHEL does not differ much. Please consult the documentation for those distributions for details. + For the sake of brevity, only Ubuntu 16.04 instructions are provided here, but this sort of web server and WSGI configuration is not unique to NetBox. Please consult your distribution's documentation for assistance if needed. ```no-highlight # apt-get install -y gunicorn supervisor diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 46472b228..89f7a598f 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -220,7 +220,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm label='Interface', widget=APISelect( api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical', - disabled_indicator='is_connected' + disabled_indicator='connection' ) ) diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index 12a7dc298..7dd72ad9d 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ # Providers url(r'^providers/$', views.ProviderListView.as_view(), name='provider_list'), - url(r'^providers/add/$', views.ProviderEditView.as_view(), name='provider_add'), + url(r'^providers/add/$', views.ProviderCreateView.as_view(), name='provider_add'), url(r'^providers/import/$', views.ProviderBulkImportView.as_view(), name='provider_import'), url(r'^providers/edit/$', views.ProviderBulkEditView.as_view(), name='provider_bulk_edit'), url(r'^providers/delete/$', views.ProviderBulkDeleteView.as_view(), name='provider_bulk_delete'), @@ -20,13 +20,13 @@ urlpatterns = [ # Circuit types url(r'^circuit-types/$', views.CircuitTypeListView.as_view(), name='circuittype_list'), - url(r'^circuit-types/add/$', views.CircuitTypeEditView.as_view(), name='circuittype_add'), + url(r'^circuit-types/add/$', views.CircuitTypeCreateView.as_view(), name='circuittype_add'), url(r'^circuit-types/delete/$', views.CircuitTypeBulkDeleteView.as_view(), name='circuittype_bulk_delete'), url(r'^circuit-types/(?P[\w-]+)/edit/$', views.CircuitTypeEditView.as_view(), name='circuittype_edit'), # Circuits url(r'^circuits/$', views.CircuitListView.as_view(), name='circuit_list'), - url(r'^circuits/add/$', views.CircuitEditView.as_view(), name='circuit_add'), + url(r'^circuits/add/$', views.CircuitCreateView.as_view(), name='circuit_add'), url(r'^circuits/import/$', views.CircuitBulkImportView.as_view(), name='circuit_import'), url(r'^circuits/edit/$', views.CircuitBulkEditView.as_view(), name='circuit_bulk_edit'), url(r'^circuits/delete/$', views.CircuitBulkDeleteView.as_view(), name='circuit_bulk_delete'), @@ -36,7 +36,7 @@ urlpatterns = [ url(r'^circuits/(?P\d+)/terminations/swap/$', views.circuit_terminations_swap, name='circuit_terminations_swap'), # Circuit terminations - url(r'^circuits/(?P\d+)/terminations/add/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'), + url(r'^circuits/(?P\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), url(r'^circuit-terminations/(?P\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), url(r'^circuit-terminations/(?P\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 35e37f4c5..eda37340d 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -49,14 +49,18 @@ class ProviderView(View): }) -class ProviderEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_provider' +class ProviderCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_provider' model = Provider form_class = forms.ProviderForm template_name = 'circuits/provider_edit.html' default_return_url = 'circuits:provider_list' +class ProviderEditView(ProviderCreateView): + permission_required = 'circuits.change_provider' + + class ProviderDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_provider' model = Provider @@ -96,8 +100,8 @@ class CircuitTypeListView(ObjectListView): template_name = 'circuits/circuittype_list.html' -class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_circuittype' +class CircuitTypeCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_circuittype' model = CircuitType form_class = forms.CircuitTypeForm @@ -105,6 +109,10 @@ class CircuitTypeEditView(PermissionRequiredMixin, ObjectEditView): return reverse('circuits:circuittype_list') +class CircuitTypeEditView(CircuitTypeCreateView): + permission_required = 'circuits.change_circuittype' + + class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'circuits.delete_circuittype' cls = CircuitType @@ -146,14 +154,18 @@ class CircuitView(View): }) -class CircuitEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_circuit' +class CircuitCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_circuit' model = Circuit form_class = forms.CircuitForm template_name = 'circuits/circuit_edit.html' default_return_url = 'circuits:circuit_list' +class CircuitEditView(CircuitCreateView): + permission_required = 'circuits.change_circuit' + + class CircuitDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_circuit' model = Circuit @@ -232,8 +244,8 @@ def circuit_terminations_swap(request, pk): # Circuit terminations # -class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'circuits.change_circuittermination' +class CircuitTerminationCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'circuits.add_circuittermination' model = CircuitTermination form_class = forms.CircuitTerminationForm template_name = 'circuits/circuittermination_edit.html' @@ -247,6 +259,10 @@ class CircuitTerminationEditView(PermissionRequiredMixin, ObjectEditView): return obj.circuit.get_absolute_url() +class CircuitTerminationEditView(CircuitTerminationCreateView): + permission_required = 'circuits.change_circuittermination' + + class CircuitTerminationDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'circuits.delete_circuittermination' model = CircuitTermination diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 6aef5808c..e05ffec50 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -13,8 +13,8 @@ from tenancy.forms import TenancyForm from tenancy.models import Tenant from utilities.forms import ( APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, - ChainedFieldsMixin, ChainedModelChoiceField, CommentField, CSVChoiceField, ExpandableNameField, FilterChoiceField, - FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, + ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ConfirmationForm, CSVChoiceField, ExpandableNameField, + FilterChoiceField, FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SmallTextarea, SlugField, FilterTreeNodeMultipleChoiceField, ) from .formfields import MACAddressFormField @@ -1174,6 +1174,10 @@ class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms. } +class ConsoleServerPortBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=ConsoleServerPort.objects.all(), widget=forms.MultipleHiddenInput) + + # # Power ports # @@ -1431,6 +1435,10 @@ class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): } +class PowerOutletBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=PowerOutlet.objects.all(), widget=forms.MultipleHiddenInput) + + # # Interfaces # @@ -1508,6 +1516,10 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): self.fields['lag'].choices = [] +class InterfaceBulkDisconnectForm(ConfirmationForm): + pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) + + # # Interface connections # @@ -1594,9 +1606,10 @@ class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelFor ] # Mark connected interfaces as disabled - self.fields['interface_b'].choices = [ - (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset - ] + if self.data.get('device_b'): + self.fields['interface_b'].choices = [ + (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in self.fields['interface_b'].queryset + ] class InterfaceConnectionCSVForm(forms.ModelForm): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 775daeabf..53031ebbe 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.conf.urls import url from extras.views import ImageAttachmentEditView -from ipam.views import ServiceEditView +from ipam.views import ServiceCreateView from secrets.views import secret_add from .models import Device, Rack, Site from . import views @@ -14,13 +14,13 @@ urlpatterns = [ # Regions url(r'^regions/$', views.RegionListView.as_view(), name='region_list'), - url(r'^regions/add/$', views.RegionEditView.as_view(), name='region_add'), + url(r'^regions/add/$', views.RegionCreateView.as_view(), name='region_add'), url(r'^regions/delete/$', views.RegionBulkDeleteView.as_view(), name='region_bulk_delete'), url(r'^regions/(?P\d+)/edit/$', views.RegionEditView.as_view(), name='region_edit'), # Sites url(r'^sites/$', views.SiteListView.as_view(), name='site_list'), - url(r'^sites/add/$', views.SiteEditView.as_view(), name='site_add'), + url(r'^sites/add/$', views.SiteCreateView.as_view(), name='site_add'), url(r'^sites/import/$', views.SiteBulkImportView.as_view(), name='site_import'), url(r'^sites/edit/$', views.SiteBulkEditView.as_view(), name='site_bulk_edit'), url(r'^sites/(?P[\w-]+)/$', views.SiteView.as_view(), name='site'), @@ -30,13 +30,13 @@ urlpatterns = [ # Rack groups url(r'^rack-groups/$', views.RackGroupListView.as_view(), name='rackgroup_list'), - url(r'^rack-groups/add/$', views.RackGroupEditView.as_view(), name='rackgroup_add'), + url(r'^rack-groups/add/$', views.RackGroupCreateView.as_view(), name='rackgroup_add'), url(r'^rack-groups/delete/$', views.RackGroupBulkDeleteView.as_view(), name='rackgroup_bulk_delete'), url(r'^rack-groups/(?P\d+)/edit/$', views.RackGroupEditView.as_view(), name='rackgroup_edit'), # Rack roles url(r'^rack-roles/$', views.RackRoleListView.as_view(), name='rackrole_list'), - url(r'^rack-roles/add/$', views.RackRoleEditView.as_view(), name='rackrole_add'), + url(r'^rack-roles/add/$', views.RackRoleCreateView.as_view(), name='rackrole_add'), url(r'^rack-roles/delete/$', views.RackRoleBulkDeleteView.as_view(), name='rackrole_bulk_delete'), url(r'^rack-roles/(?P\d+)/edit/$', views.RackRoleEditView.as_view(), name='rackrole_edit'), @@ -56,18 +56,18 @@ urlpatterns = [ url(r'^racks/(?P\d+)/$', views.RackView.as_view(), name='rack'), url(r'^racks/(?P\d+)/edit/$', views.RackEditView.as_view(), name='rack_edit'), url(r'^racks/(?P\d+)/delete/$', views.RackDeleteView.as_view(), name='rack_delete'), - url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationEditView.as_view(), name='rack_add_reservation'), + url(r'^racks/(?P\d+)/reservations/add/$', views.RackReservationCreateView.as_view(), name='rack_add_reservation'), url(r'^racks/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='rack_add_image', kwargs={'model': Rack}), # Manufacturers url(r'^manufacturers/$', views.ManufacturerListView.as_view(), name='manufacturer_list'), - url(r'^manufacturers/add/$', views.ManufacturerEditView.as_view(), name='manufacturer_add'), + url(r'^manufacturers/add/$', views.ManufacturerCreateView.as_view(), name='manufacturer_add'), url(r'^manufacturers/delete/$', views.ManufacturerBulkDeleteView.as_view(), name='manufacturer_bulk_delete'), url(r'^manufacturers/(?P[\w-]+)/edit/$', views.ManufacturerEditView.as_view(), name='manufacturer_edit'), # Device types url(r'^device-types/$', views.DeviceTypeListView.as_view(), name='devicetype_list'), - url(r'^device-types/add/$', views.DeviceTypeEditView.as_view(), name='devicetype_add'), + url(r'^device-types/add/$', views.DeviceTypeCreateView.as_view(), name='devicetype_add'), url(r'^device-types/edit/$', views.DeviceTypeBulkEditView.as_view(), name='devicetype_bulk_edit'), url(r'^device-types/delete/$', views.DeviceTypeBulkDeleteView.as_view(), name='devicetype_bulk_delete'), url(r'^device-types/(?P\d+)/$', views.DeviceTypeView.as_view(), name='devicetype'), @@ -75,45 +75,45 @@ urlpatterns = [ url(r'^device-types/(?P\d+)/delete/$', views.DeviceTypeDeleteView.as_view(), name='devicetype_delete'), # Console port templates - url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateAddView.as_view(), name='devicetype_add_consoleport'), + url(r'^device-types/(?P\d+)/console-ports/add/$', views.ConsolePortTemplateCreateView.as_view(), name='devicetype_add_consoleport'), url(r'^device-types/(?P\d+)/console-ports/delete/$', views.ConsolePortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleport'), # Console server port templates - url(r'^device-types/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateAddView.as_view(), name='devicetype_add_consoleserverport'), + url(r'^device-types/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortTemplateCreateView.as_view(), name='devicetype_add_consoleserverport'), url(r'^device-types/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_consoleserverport'), # Power port templates - url(r'^device-types/(?P\d+)/power-ports/add/$', views.PowerPortTemplateAddView.as_view(), name='devicetype_add_powerport'), + url(r'^device-types/(?P\d+)/power-ports/add/$', views.PowerPortTemplateCreateView.as_view(), name='devicetype_add_powerport'), url(r'^device-types/(?P\d+)/power-ports/delete/$', views.PowerPortTemplateBulkDeleteView.as_view(), name='devicetype_delete_powerport'), # Power outlet templates - url(r'^device-types/(?P\d+)/power-outlets/add/$', views.PowerOutletTemplateAddView.as_view(), name='devicetype_add_poweroutlet'), + url(r'^device-types/(?P\d+)/power-outlets/add/$', views.PowerOutletTemplateCreateView.as_view(), name='devicetype_add_poweroutlet'), url(r'^device-types/(?P\d+)/power-outlets/delete/$', views.PowerOutletTemplateBulkDeleteView.as_view(), name='devicetype_delete_poweroutlet'), # Interface templates - url(r'^device-types/(?P\d+)/interfaces/add/$', views.InterfaceTemplateAddView.as_view(), name='devicetype_add_interface'), + url(r'^device-types/(?P\d+)/interfaces/add/$', views.InterfaceTemplateCreateView.as_view(), name='devicetype_add_interface'), url(r'^device-types/(?P\d+)/interfaces/edit/$', views.InterfaceTemplateBulkEditView.as_view(), name='devicetype_bulkedit_interface'), url(r'^device-types/(?P\d+)/interfaces/delete/$', views.InterfaceTemplateBulkDeleteView.as_view(), name='devicetype_delete_interface'), # Device bay templates - url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateAddView.as_view(), name='devicetype_add_devicebay'), + url(r'^device-types/(?P\d+)/device-bays/add/$', views.DeviceBayTemplateCreateView.as_view(), name='devicetype_add_devicebay'), url(r'^device-types/(?P\d+)/device-bays/delete/$', views.DeviceBayTemplateBulkDeleteView.as_view(), name='devicetype_delete_devicebay'), # Device roles url(r'^device-roles/$', views.DeviceRoleListView.as_view(), name='devicerole_list'), - url(r'^device-roles/add/$', views.DeviceRoleEditView.as_view(), name='devicerole_add'), + url(r'^device-roles/add/$', views.DeviceRoleCreateView.as_view(), name='devicerole_add'), url(r'^device-roles/delete/$', views.DeviceRoleBulkDeleteView.as_view(), name='devicerole_bulk_delete'), url(r'^device-roles/(?P[\w-]+)/edit/$', views.DeviceRoleEditView.as_view(), name='devicerole_edit'), # Platforms url(r'^platforms/$', views.PlatformListView.as_view(), name='platform_list'), - url(r'^platforms/add/$', views.PlatformEditView.as_view(), name='platform_add'), + url(r'^platforms/add/$', views.PlatformCreateView.as_view(), name='platform_add'), url(r'^platforms/delete/$', views.PlatformBulkDeleteView.as_view(), name='platform_bulk_delete'), url(r'^platforms/(?P[\w-]+)/edit/$', views.PlatformEditView.as_view(), name='platform_edit'), # Devices url(r'^devices/$', views.DeviceListView.as_view(), name='device_list'), - url(r'^devices/add/$', views.DeviceEditView.as_view(), name='device_add'), + url(r'^devices/add/$', views.DeviceCreateView.as_view(), name='device_add'), url(r'^devices/import/$', views.DeviceBulkImportView.as_view(), name='device_import'), url(r'^devices/import/child-devices/$', views.ChildDeviceBulkImportView.as_view(), name='device_import_child'), url(r'^devices/edit/$', views.DeviceBulkEditView.as_view(), name='device_bulk_edit'), @@ -124,12 +124,12 @@ urlpatterns = [ url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P\d+)/lldp-neighbors/$', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'), url(r'^devices/(?P\d+)/add-secret/$', secret_add, name='device_addsecret'), - url(r'^devices/(?P\d+)/services/assign/$', ServiceEditView.as_view(), name='service_assign'), + url(r'^devices/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='service_assign'), url(r'^devices/(?P\d+)/images/add/$', ImageAttachmentEditView.as_view(), name='device_add_image', kwargs={'model': Device}), # Console ports url(r'^devices/console-ports/add/$', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), - url(r'^devices/(?P\d+)/console-ports/add/$', views.ConsolePortAddView.as_view(), name='consoleport_add'), + url(r'^devices/(?P\d+)/console-ports/add/$', views.ConsolePortCreateView.as_view(), name='consoleport_add'), url(r'^devices/(?P\d+)/console-ports/delete/$', views.ConsolePortBulkDeleteView.as_view(), name='consoleport_bulk_delete'), url(r'^console-ports/(?P\d+)/connect/$', views.consoleport_connect, name='consoleport_connect'), url(r'^console-ports/(?P\d+)/disconnect/$', views.consoleport_disconnect, name='consoleport_disconnect'), @@ -138,7 +138,8 @@ urlpatterns = [ # Console server ports url(r'^devices/console-server-ports/add/$', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), - url(r'^devices/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortAddView.as_view(), name='consoleserverport_add'), + url(r'^devices/(?P\d+)/console-server-ports/add/$', views.ConsoleServerPortCreateView.as_view(), name='consoleserverport_add'), + url(r'^devices/(?P\d+)/console-server-ports/disconnect/$', views.ConsoleServerPortBulkDisconnectView.as_view(), name='consoleserverport_bulk_disconnect'), url(r'^devices/(?P\d+)/console-server-ports/delete/$', views.ConsoleServerPortBulkDeleteView.as_view(), name='consoleserverport_bulk_delete'), url(r'^console-server-ports/(?P\d+)/connect/$', views.consoleserverport_connect, name='consoleserverport_connect'), url(r'^console-server-ports/(?P\d+)/disconnect/$', views.consoleserverport_disconnect, name='consoleserverport_disconnect'), @@ -147,7 +148,7 @@ urlpatterns = [ # Power ports url(r'^devices/power-ports/add/$', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), - url(r'^devices/(?P\d+)/power-ports/add/$', views.PowerPortAddView.as_view(), name='powerport_add'), + url(r'^devices/(?P\d+)/power-ports/add/$', views.PowerPortCreateView.as_view(), name='powerport_add'), url(r'^devices/(?P\d+)/power-ports/delete/$', views.PowerPortBulkDeleteView.as_view(), name='powerport_bulk_delete'), url(r'^power-ports/(?P\d+)/connect/$', views.powerport_connect, name='powerport_connect'), url(r'^power-ports/(?P\d+)/disconnect/$', views.powerport_disconnect, name='powerport_disconnect'), @@ -156,7 +157,8 @@ urlpatterns = [ # Power outlets url(r'^devices/power-outlets/add/$', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), - url(r'^devices/(?P\d+)/power-outlets/add/$', views.PowerOutletAddView.as_view(), name='poweroutlet_add'), + url(r'^devices/(?P\d+)/power-outlets/add/$', views.PowerOutletCreateView.as_view(), name='poweroutlet_add'), + url(r'^devices/(?P\d+)/power-outlets/disconnect/$', views.PowerOutletBulkDisconnectView.as_view(), name='poweroutlet_bulk_disconnect'), url(r'^devices/(?P\d+)/power-outlets/delete/$', views.PowerOutletBulkDeleteView.as_view(), name='poweroutlet_bulk_delete'), url(r'^power-outlets/(?P\d+)/connect/$', views.poweroutlet_connect, name='poweroutlet_connect'), url(r'^power-outlets/(?P\d+)/disconnect/$', views.poweroutlet_disconnect, name='poweroutlet_disconnect'), @@ -165,8 +167,9 @@ urlpatterns = [ # Interfaces url(r'^devices/interfaces/add/$', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), - url(r'^devices/(?P\d+)/interfaces/add/$', views.InterfaceAddView.as_view(), name='interface_add'), + url(r'^devices/(?P\d+)/interfaces/add/$', views.InterfaceCreateView.as_view(), name='interface_add'), url(r'^devices/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), + url(r'^devices/(?P\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), url(r'^devices/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), url(r'^devices/(?P\d+)/interface-connections/add/$', views.interfaceconnection_add, name='interfaceconnection_add'), url(r'^interface-connections/(?P\d+)/delete/$', views.interfaceconnection_delete, name='interfaceconnection_delete'), @@ -175,7 +178,7 @@ urlpatterns = [ # Device bays url(r'^devices/device-bays/add/$', views.DeviceBulkAddDeviceBayView.as_view(), name='device_bulk_add_devicebay'), - url(r'^devices/(?P\d+)/bays/add/$', views.DeviceBayAddView.as_view(), name='devicebay_add'), + url(r'^devices/(?P\d+)/bays/add/$', views.DeviceBayCreateView.as_view(), name='devicebay_add'), url(r'^devices/(?P\d+)/bays/delete/$', views.DeviceBayBulkDeleteView.as_view(), name='devicebay_bulk_delete'), url(r'^device-bays/(?P\d+)/edit/$', views.DeviceBayEditView.as_view(), name='devicebay_edit'), url(r'^device-bays/(?P\d+)/delete/$', views.DeviceBayDeleteView.as_view(), name='devicebay_delete'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 40b18e33a..e6b77cb59 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals from copy import deepcopy -from difflib import SequenceMatcher import re from natsort import natsorted from operator import attrgetter @@ -9,7 +8,7 @@ from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.paginator import EmptyPage, PageNotAnInteger -from django.db.models import Count +from django.db.models import Count, Q from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -142,6 +141,44 @@ class ComponentDeleteView(ObjectDeleteView): return obj.device.get_absolute_url() +class BulkDisconnectView(View): + """ + An extendable view for disconnection console/power/interface components in bulk. + """ + model = None + form = None + template_name = 'dcim/bulk_disconnect.html' + + def disconnect_objects(self, objects): + raise NotImplementedError() + + def post(self, request, pk): + + device = get_object_or_404(Device, pk=pk) + selected_objects = [] + + if '_confirm' in request.POST: + form = self.form(request.POST) + if form.is_valid(): + count = self.disconnect_objects(form.cleaned_data['pk']) + messages.success(request, "Disconnected {} {} on {}".format( + count, self.model._meta.verbose_name_plural, device + )) + return redirect(device.get_absolute_url()) + + else: + form = self.form(initial={'pk': request.POST.getlist('pk')}) + selected_objects = self.model.objects.filter(pk__in=form.initial['pk']) + + return render(request, self.template_name, { + 'form': form, + 'device': device, + 'obj_type_plural': self.model._meta.verbose_name_plural, + 'selected_objects': selected_objects, + 'return_url': device.get_absolute_url(), + }) + + # # Regions # @@ -152,8 +189,8 @@ class RegionListView(ObjectListView): template_name = 'dcim/region_list.html' -class RegionEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_region' +class RegionCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_region' model = Region form_class = forms.RegionForm @@ -161,6 +198,10 @@ class RegionEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:region_list') +class RegionEditView(RegionCreateView): + permission_required = 'dcim.change_region' + + class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_region' cls = Region @@ -204,14 +245,18 @@ class SiteView(View): }) -class SiteEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_site' +class SiteCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_site' model = Site form_class = forms.SiteForm template_name = 'dcim/site_edit.html' default_return_url = 'dcim:site_list' +class SiteEditView(SiteCreateView): + permission_required = 'dcim.change_site' + + class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_site' model = Site @@ -246,8 +291,8 @@ class RackGroupListView(ObjectListView): template_name = 'dcim/rackgroup_list.html' -class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rackgroup' +class RackGroupCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rackgroup' model = RackGroup form_class = forms.RackGroupForm @@ -255,6 +300,10 @@ class RackGroupEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:rackgroup_list') +class RackGroupEditView(RackGroupCreateView): + permission_required = 'dcim.change_rackgroup' + + class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackgroup' cls = RackGroup @@ -272,8 +321,8 @@ class RackRoleListView(ObjectListView): template_name = 'dcim/rackrole_list.html' -class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rackrole' +class RackRoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rackrole' model = RackRole form_class = forms.RackRoleForm @@ -281,6 +330,10 @@ class RackRoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:rackrole_list') +class RackRoleEditView(RackRoleCreateView): + permission_required = 'dcim.change_rackrole' + + class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_rackrole' cls = RackRole @@ -374,14 +427,18 @@ class RackView(View): }) -class RackEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rack' +class RackCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rack' model = Rack form_class = forms.RackForm template_name = 'dcim/rack_edit.html' default_return_url = 'dcim:rack_list' +class RackEditView(RackCreateView): + permission_required = 'dcim.change_rack' + + class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_rack' model = Rack @@ -423,8 +480,8 @@ class RackReservationListView(ObjectListView): template_name = 'dcim/rackreservation_list.html' -class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_rackreservation' +class RackReservationCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_rackreservation' model = RackReservation form_class = forms.RackReservationForm @@ -438,6 +495,10 @@ class RackReservationEditView(PermissionRequiredMixin, ObjectEditView): return obj.rack.get_absolute_url() +class RackReservationEditView(RackReservationCreateView): + permission_required = 'dcim.change_rackreservation' + + class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_rackreservation' model = RackReservation @@ -462,8 +523,8 @@ class ManufacturerListView(ObjectListView): template_name = 'dcim/manufacturer_list.html' -class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_manufacturer' +class ManufacturerCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_manufacturer' model = Manufacturer form_class = forms.ManufacturerForm @@ -471,6 +532,10 @@ class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:manufacturer_list') +class ManufacturerEditView(ManufacturerCreateView): + permission_required = 'dcim.change_manufacturer' + + class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_manufacturer' cls = Manufacturer @@ -542,14 +607,18 @@ class DeviceTypeView(View): }) -class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_devicetype' +class DeviceTypeCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_devicetype' model = DeviceType form_class = forms.DeviceTypeForm template_name = 'dcim/devicetype_edit.html' default_return_url = 'dcim:devicetype_list' +class DeviceTypeEditView(DeviceTypeCreateView): + permission_required = 'dcim.change_devicetype' + + class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_devicetype' model = DeviceType @@ -576,7 +645,7 @@ class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Device type components # -class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsolePortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleporttemplate' parent_model = DeviceType parent_field = 'device_type' @@ -593,7 +662,7 @@ class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView) parent_cls = DeviceType -class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsoleServerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverporttemplate' parent_model = DeviceType parent_field = 'device_type' @@ -608,7 +677,7 @@ class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDelet parent_cls = DeviceType -class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerPortTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerporttemplate' parent_model = DeviceType parent_field = 'device_type' @@ -623,7 +692,7 @@ class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): parent_cls = DeviceType -class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerOutletTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlettemplate' parent_model = DeviceType parent_field = 'device_type' @@ -638,7 +707,7 @@ class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView) parent_cls = DeviceType -class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class InterfaceTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interfacetemplate' parent_model = DeviceType parent_field = 'device_type' @@ -661,7 +730,7 @@ class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): parent_cls = DeviceType -class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView): +class DeviceBayTemplateCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebaytemplate' parent_model = DeviceType parent_field = 'device_type' @@ -686,8 +755,8 @@ class DeviceRoleListView(ObjectListView): template_name = 'dcim/devicerole_list.html' -class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_devicerole' +class DeviceRoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_devicerole' model = DeviceRole form_class = forms.DeviceRoleForm @@ -695,6 +764,10 @@ class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:devicerole_list') +class DeviceRoleEditView(DeviceRoleCreateView): + permission_required = 'dcim.change_devicerole' + + class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_devicerole' cls = DeviceRole @@ -711,8 +784,8 @@ class PlatformListView(ObjectListView): template_name = 'dcim/platform_list.html' -class PlatformEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_platform' +class PlatformCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_platform' model = Platform form_class = forms.PlatformForm @@ -720,6 +793,10 @@ class PlatformEditView(PermissionRequiredMixin, ObjectEditView): return reverse('dcim:platform_list') +class PlatformEditView(PlatformCreateView): + permission_required = 'dcim.change_platform' + + class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_platform' cls = Platform @@ -843,14 +920,18 @@ class DeviceLLDPNeighborsView(View): }) -class DeviceEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'dcim.change_device' +class DeviceCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'dcim.add_device' model = Device form_class = forms.DeviceForm template_name = 'dcim/device_edit.html' default_return_url = 'dcim:device_list' +class DeviceEditView(DeviceCreateView): + permission_required = 'dcim.change_device' + + class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_device' model = Device @@ -904,7 +985,7 @@ class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Console ports # -class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsolePortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleport' parent_model = Device parent_field = 'device' @@ -1017,7 +1098,7 @@ class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): # Console server ports # -class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView): +class ConsoleServerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_consoleserverport' parent_model = Device parent_field = 'device' @@ -1116,6 +1197,15 @@ class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView): model = ConsoleServerPort +class ConsoleServerPortBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): + permission_required = 'dcim.change_consoleserverport' + model = ConsoleServerPort + form = forms.ConsoleServerPortBulkDisconnectForm + + def disconnect_objects(self, cs_ports): + return ConsolePort.objects.filter(cs_port__in=cs_ports).update(cs_port=None, connection_status=None) + + class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_consoleserverport' cls = ConsoleServerPort @@ -1126,7 +1216,7 @@ class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Power ports # -class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerPortCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_powerport' parent_model = Device parent_field = 'device' @@ -1239,7 +1329,7 @@ class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): # Power outlets # -class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView): +class PowerOutletCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_poweroutlet' parent_model = Device parent_field = 'device' @@ -1338,6 +1428,17 @@ class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView): model = PowerOutlet +class PowerOutletBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): + permission_required = 'dcim.change_poweroutlet' + model = PowerOutlet + form = forms.PowerOutletBulkDisconnectForm + + def disconnect_objects(self, power_outlets): + return PowerPort.objects.filter(power_outlet__in=power_outlets).update( + power_outlet=None, connection_status=None + ) + + class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'dcim.delete_poweroutlet' cls = PowerOutlet @@ -1348,7 +1449,7 @@ class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Interfaces # -class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView): +class InterfaceCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_interface' parent_model = Device parent_field = 'device' @@ -1368,6 +1469,18 @@ class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView): model = Interface +class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): + permission_required = 'dcim.change_interface' + model = Interface + form = forms.InterfaceBulkDisconnectForm + + def disconnect_objects(self, interfaces): + count, _ = InterfaceConnection.objects.filter( + Q(interface_a__in=interfaces) | Q(interface_b__in=interfaces) + ).delete() + return count + + class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): permission_required = 'dcim.change_interface' cls = Interface @@ -1386,7 +1499,7 @@ class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Device bays # -class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView): +class DeviceBayCreateView(PermissionRequiredMixin, ComponentCreateView): permission_required = 'dcim.add_devicebay' parent_model = Device parent_field = 'device' diff --git a/netbox/extras/models.py b/netbox/extras/models.py index bea8a664b..ade251c94 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -371,7 +371,8 @@ class TopologyMap(models.Model): # Add all circuits to the graph for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices): peer_termination = termination.get_peer_termination() - if peer_termination is not None and peer_termination.interface.device in devices: + if (peer_termination is not None and peer_termination.interface is not None and + peer_termination.interface.device in devices): graph.edge(termination.interface.device.name, peer_termination.interface.device.name, color='blue') return graph.pipe(format=img_format) diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index b4db64d81..78deadd45 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from django import forms +from django.core.exceptions import MultipleObjectsReturned from django.db.models import Count from dcim.models import Site, Rack, Device, Interface @@ -301,6 +302,10 @@ class PrefixCSVForm(forms.ModelForm): )) else: raise forms.ValidationError("Global VLAN {} not found in group {}".format(vlan_vid, vlan_group)) + except MultipleObjectsReturned: + raise forms.ValidationError( + "Multiple VLANs with VID {} found in group {}".format(vlan_vid, vlan_group) + ) elif vlan_vid: try: self.instance.vlan = VLAN.objects.get(site=site, group__isnull=True, vid=vlan_vid) @@ -309,6 +314,8 @@ class PrefixCSVForm(forms.ModelForm): raise forms.ValidationError("VLAN {} not found in site {}".format(vlan_vid, site)) else: raise forms.ValidationError("Global VLAN {} not found".format(vlan_vid)) + except MultipleObjectsReturned: + raise forms.ValidationError("Multiple VLANs with VID {} found".format(vlan_vid)) class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm): @@ -490,7 +497,7 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm) initial['interface_site'] = instance.interface.device.site initial['interface_rack'] = instance.interface.device.rack initial['interface_device'] = instance.interface.device - if instance and instance.nat_inside is not None: + if instance and instance.nat_inside and instance.nat_inside.device is not None: initial['nat_site'] = instance.nat_inside.device.site initial['nat_rack'] = instance.nat_inside.device.rack initial['nat_device'] = instance.nat_inside.device @@ -582,7 +589,7 @@ class IPAddressCSVForm(forms.ModelForm): } ) status = CSVChoiceField( - choices=PREFIX_STATUS_CHOICES, + choices=IPADDRESS_STATUS_CHOICES, help_text='Operational status' ) device = FlexibleModelChoiceField( diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index d28bf8a13..15634c0ee 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ # VRFs url(r'^vrfs/$', views.VRFListView.as_view(), name='vrf_list'), - url(r'^vrfs/add/$', views.VRFEditView.as_view(), name='vrf_add'), + url(r'^vrfs/add/$', views.VRFCreateView.as_view(), name='vrf_add'), url(r'^vrfs/import/$', views.VRFBulkImportView.as_view(), name='vrf_import'), url(r'^vrfs/edit/$', views.VRFBulkEditView.as_view(), name='vrf_bulk_edit'), url(r'^vrfs/delete/$', views.VRFBulkDeleteView.as_view(), name='vrf_bulk_delete'), @@ -20,13 +20,13 @@ urlpatterns = [ # RIRs url(r'^rirs/$', views.RIRListView.as_view(), name='rir_list'), - url(r'^rirs/add/$', views.RIREditView.as_view(), name='rir_add'), + url(r'^rirs/add/$', views.RIRCreateView.as_view(), name='rir_add'), url(r'^rirs/delete/$', views.RIRBulkDeleteView.as_view(), name='rir_bulk_delete'), url(r'^rirs/(?P[\w-]+)/edit/$', views.RIREditView.as_view(), name='rir_edit'), # Aggregates url(r'^aggregates/$', views.AggregateListView.as_view(), name='aggregate_list'), - url(r'^aggregates/add/$', views.AggregateEditView.as_view(), name='aggregate_add'), + url(r'^aggregates/add/$', views.AggregateCreateView.as_view(), name='aggregate_add'), url(r'^aggregates/import/$', views.AggregateBulkImportView.as_view(), name='aggregate_import'), url(r'^aggregates/edit/$', views.AggregateBulkEditView.as_view(), name='aggregate_bulk_edit'), url(r'^aggregates/delete/$', views.AggregateBulkDeleteView.as_view(), name='aggregate_bulk_delete'), @@ -36,13 +36,13 @@ urlpatterns = [ # Roles url(r'^roles/$', views.RoleListView.as_view(), name='role_list'), - url(r'^roles/add/$', views.RoleEditView.as_view(), name='role_add'), + url(r'^roles/add/$', views.RoleCreateView.as_view(), name='role_add'), url(r'^roles/delete/$', views.RoleBulkDeleteView.as_view(), name='role_bulk_delete'), url(r'^roles/(?P[\w-]+)/edit/$', views.RoleEditView.as_view(), name='role_edit'), # Prefixes url(r'^prefixes/$', views.PrefixListView.as_view(), name='prefix_list'), - url(r'^prefixes/add/$', views.PrefixEditView.as_view(), name='prefix_add'), + url(r'^prefixes/add/$', views.PrefixCreateView.as_view(), name='prefix_add'), url(r'^prefixes/import/$', views.PrefixBulkImportView.as_view(), name='prefix_import'), url(r'^prefixes/edit/$', views.PrefixBulkEditView.as_view(), name='prefix_bulk_edit'), url(r'^prefixes/delete/$', views.PrefixBulkDeleteView.as_view(), name='prefix_bulk_delete'), @@ -53,8 +53,8 @@ urlpatterns = [ # IP addresses url(r'^ip-addresses/$', views.IPAddressListView.as_view(), name='ipaddress_list'), - url(r'^ip-addresses/add/$', views.IPAddressEditView.as_view(), name='ipaddress_add'), - url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkAddView.as_view(), name='ipaddress_bulk_add'), + url(r'^ip-addresses/add/$', views.IPAddressCreateView.as_view(), name='ipaddress_add'), + url(r'^ip-addresses/bulk-add/$', views.IPAddressBulkCreateView.as_view(), name='ipaddress_bulk_add'), url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'), url(r'^ip-addresses/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'), url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'), @@ -64,13 +64,13 @@ urlpatterns = [ # VLAN groups url(r'^vlan-groups/$', views.VLANGroupListView.as_view(), name='vlangroup_list'), - url(r'^vlan-groups/add/$', views.VLANGroupEditView.as_view(), name='vlangroup_add'), + url(r'^vlan-groups/add/$', views.VLANGroupCreateView.as_view(), name='vlangroup_add'), url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), url(r'^vlan-groups/(?P\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), # VLANs url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), - url(r'^vlans/add/$', views.VLANEditView.as_view(), name='vlan_add'), + url(r'^vlans/add/$', views.VLANCreateView.as_view(), name='vlan_add'), url(r'^vlans/import/$', views.VLANBulkImportView.as_view(), name='vlan_import'), url(r'^vlans/edit/$', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'), url(r'^vlans/delete/$', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index f8fe0535a..3cc54d252 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -13,7 +13,7 @@ from django.views.generic import View from dcim.models import Device from utilities.paginator import EnhancedPaginator from utilities.views import ( - BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, + BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) from . import filters, forms, tables from .models import ( @@ -114,14 +114,18 @@ class VRFView(View): }) -class VRFEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_vrf' +class VRFCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_vrf' model = VRF form_class = forms.VRFForm template_name = 'ipam/vrf_edit.html' default_return_url = 'ipam:vrf_list' +class VRFEditView(VRFCreateView): + permission_required = 'ipam.change_vrf' + + class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_vrf' model = VRF @@ -239,8 +243,8 @@ class RIRListView(ObjectListView): } -class RIREditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_rir' +class RIRCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_rir' model = RIR form_class = forms.RIRForm @@ -248,6 +252,10 @@ class RIREditView(PermissionRequiredMixin, ObjectEditView): return reverse('ipam:rir_list') +class RIREditView(RIRCreateView): + permission_required = 'ipam.change_rir' + + class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_rir' cls = RIR @@ -324,14 +332,18 @@ class AggregateView(View): }) -class AggregateEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_aggregate' +class AggregateCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_aggregate' model = Aggregate form_class = forms.AggregateForm template_name = 'ipam/aggregate_edit.html' default_return_url = 'ipam:aggregate_list' +class AggregateEditView(AggregateCreateView): + permission_required = 'ipam.change_aggregate' + + class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_aggregate' model = Aggregate @@ -371,8 +383,8 @@ class RoleListView(ObjectListView): template_name = 'ipam/role_list.html' -class RoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_role' +class RoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_role' model = Role form_class = forms.RoleForm @@ -380,6 +392,10 @@ class RoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('ipam:role_list') +class RoleEditView(RoleCreateView): + permission_required = 'ipam.change_role' + + class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_role' cls = Role @@ -519,14 +535,18 @@ class PrefixIPAddressesView(View): }) -class PrefixEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_prefix' +class PrefixCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_prefix' model = Prefix form_class = forms.PrefixForm template_name = 'ipam/prefix_edit.html' default_return_url = 'ipam:prefix_list' +class PrefixEditView(PrefixCreateView): + permission_required = 'ipam.change_prefix' + + class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_prefix' model = Prefix @@ -612,21 +632,25 @@ class IPAddressView(View): }) -class IPAddressEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_ipaddress' +class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_ipaddress' model = IPAddress form_class = forms.IPAddressForm template_name = 'ipam/ipaddress_edit.html' default_return_url = 'ipam:ipaddress_list' +class IPAddressEditView(IPAddressCreateView): + permission_required = 'ipam.change_ipaddress' + + class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_ipaddress' model = IPAddress default_return_url = 'ipam:ipaddress_list' -class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView): +class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView): permission_required = 'ipam.add_ipaddress' pattern_form = forms.IPAddressPatternForm model_form = forms.IPAddressBulkAddForm @@ -683,8 +707,8 @@ class VLANGroupListView(ObjectListView): template_name = 'ipam/vlangroup_list.html' -class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_vlangroup' +class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_vlangroup' model = VLANGroup form_class = forms.VLANGroupForm @@ -692,6 +716,10 @@ class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView): return reverse('ipam:vlangroup_list') +class VLANGroupEditView(VLANGroupCreateView): + permission_required = 'ipam.change_vlangroup' + + class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'ipam.delete_vlangroup' cls = VLANGroup @@ -728,14 +756,18 @@ class VLANView(View): }) -class VLANEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_vlan' +class VLANCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_vlan' model = VLAN form_class = forms.VLANForm template_name = 'ipam/vlan_edit.html' default_return_url = 'ipam:vlan_list' +class VLANEditView(VLANCreateView): + permission_required = 'ipam.change_vlan' + + class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_vlan' model = VLAN @@ -769,8 +801,8 @@ class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): # Services # -class ServiceEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'ipam.change_service' +class ServiceCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'ipam.add_service' model = Service form_class = forms.ServiceForm template_name = 'ipam/service_edit.html' @@ -784,6 +816,10 @@ class ServiceEditView(PermissionRequiredMixin, ObjectEditView): return obj.device.get_absolute_url() +class ServiceEditView(ServiceCreateView): + permission_required = 'ipam.change_service' + + class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'ipam.delete_service' model = Service diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4ee969ac9..5bd4d2f6e 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -13,7 +13,7 @@ except ImportError: ) -VERSION = '2.0.6' +VERSION = '2.0.7' # Import required configuration parameters ALLOWED_HOSTS = DATABASE = SECRET_KEY = None diff --git a/netbox/project-static/js/livesearch.js b/netbox/project-static/js/livesearch.js index f387372b4..e00aefbaf 100644 --- a/netbox/project-static/js/livesearch.js +++ b/netbox/project-static/js/livesearch.js @@ -1,6 +1,7 @@ $(document).ready(function() { var search_field = $('#id_livesearch'); var real_field = $('#id_' + search_field.attr('data-field')); + var select_fields = $('#select select'); var search_key = search_field.attr('data-key'); var label = search_field.attr('data-label'); if (!label) { @@ -40,13 +41,22 @@ $(document).ready(function() { select: function(event, ui) { event.preventDefault(); search_field.val(ui.item.label); + select_fields.val(''); + select_fields.attr('disabled', 'disabled'); real_field.empty(); real_field.append($("").attr('value', ui.item.value).text(ui.item.label)); real_field.change(); - // If the field has a parent helper, reset the parent to no selection - $('select[filter-for="' + real_field.attr('name') + '"]').val(''); + // Disable parent selection fields + // $('select[filter-for="' + real_field.attr('name') + '"]').val(''); }, minLength: 4, delay: 500 }); + + search_field.change(function() { + if (!search_field.val()) { + select_fields.removeAttr('disabled'); + select_fields.val(''); + } + }); }); diff --git a/netbox/secrets/urls.py b/netbox/secrets/urls.py index b28198a2f..961e0f0ed 100644 --- a/netbox/secrets/urls.py +++ b/netbox/secrets/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ # Secret roles url(r'^secret-roles/$', views.SecretRoleListView.as_view(), name='secretrole_list'), - url(r'^secret-roles/add/$', views.SecretRoleEditView.as_view(), name='secretrole_add'), + url(r'^secret-roles/add/$', views.SecretRoleCreateView.as_view(), name='secretrole_add'), url(r'^secret-roles/delete/$', views.SecretRoleBulkDeleteView.as_view(), name='secretrole_bulk_delete'), url(r'^secret-roles/(?P[\w-]+)/edit/$', views.SecretRoleEditView.as_view(), name='secretrole_edit'), diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index e046f1dbc..ac4226358 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -40,8 +40,8 @@ class SecretRoleListView(ObjectListView): template_name = 'secrets/secretrole_list.html' -class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): - permission_required = 'secrets.change_secretrole' +class SecretRoleCreateView(PermissionRequiredMixin, ObjectEditView): + permission_required = 'secrets.add_secretrole' model = SecretRole form_class = forms.SecretRoleForm @@ -49,6 +49,10 @@ class SecretRoleEditView(PermissionRequiredMixin, ObjectEditView): return reverse('secrets:secretrole_list') +class SecretRoleEditView(SecretRoleCreateView): + permission_required = 'secrets.change_secretrole' + + class SecretRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): permission_required = 'secrets.delete_secretrole' cls = SecretRole diff --git a/netbox/templates/dcim/bulk_disconnect.html b/netbox/templates/dcim/bulk_disconnect.html new file mode 100644 index 000000000..82cc86a7a --- /dev/null +++ b/netbox/templates/dcim/bulk_disconnect.html @@ -0,0 +1,13 @@ +{% extends 'utilities/confirmation_form.html' %} +{% load helpers %} + +{% block title %}Disconnect {{ obj_type_plural|bettertitle }}{% endblock %} + +{% block message %} +

Are you sure you want to disconnect all {{ selected_objects|length }} of these {{ obj_type_plural }} on {{ device }}?

+
    + {% for obj in selected_objects %} +
  • {{ obj }}
  • + {% endfor %} +
+{% endblock %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index bafcdd224..a6e5d1dbe 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -424,12 +424,17 @@