Просмотр исходного кода

Merge pull request #1708 from digitalocean/develop

Release v2.2.5
Jeremy Stretch 8 лет назад
Родитель
Сommit
a5a7358d26
100 измененных файлов с 773 добавлено и 596 удалено
  1. 16 9
      .github/ISSUE_TEMPLATE.md
  2. 63 73
      CONTRIBUTING.md
  3. 2 3
      netbox/circuits/api/views.py
  4. 0 1
      netbox/circuits/filters.py
  5. 0 1
      netbox/circuits/forms.py
  6. 1 1
      netbox/circuits/models.py
  7. 1 3
      netbox/circuits/tables.py
  8. 6 5
      netbox/circuits/tests/test_api.py
  9. 0 1
      netbox/circuits/urls.py
  10. 2 1
      netbox/circuits/views.py
  11. 10 6
      netbox/dcim/api/serializers.py
  12. 18 9
      netbox/dcim/api/views.py
  13. 10 8
      netbox/dcim/filters.py
  14. 1 2
      netbox/dcim/formfields.py
  15. 10 8
      netbox/dcim/forms.py
  16. 28 4
      netbox/dcim/models.py
  17. 4 4
      netbox/dcim/querysets.py
  18. 0 1
      netbox/dcim/tables.py
  19. 9 9
      netbox/dcim/tests/test_api.py
  20. 1 2
      netbox/dcim/urls.py
  21. 8 7
      netbox/dcim/views.py
  22. 14 6
      netbox/extras/api/customfields.py
  23. 2 4
      netbox/extras/api/serializers.py
  24. 3 4
      netbox/extras/api/views.py
  25. 2 2
      netbox/extras/filters.py
  26. 3 4
      netbox/extras/forms.py
  27. 0 1
      netbox/extras/management/commands/nbshell.py
  28. 2 2
      netbox/extras/management/commands/run_inventory.py
  29. 0 1
      netbox/extras/management/commands/runreport.py
  30. 2 1
      netbox/extras/migrations/0008_reports.py
  31. 2 1
      netbox/extras/models.py
  32. 2 1
      netbox/extras/reports.py
  33. 2 2
      netbox/extras/rpc.py
  34. 4 4
      netbox/extras/tests/test_api.py
  35. 5 7
      netbox/extras/tests/test_customfields.py
  36. 0 1
      netbox/extras/urls.py
  37. 1 1
      netbox/extras/views.py
  38. 4 3
      netbox/ipam/api/serializers.py
  39. 4 5
      netbox/ipam/api/views.py
  40. 11 1
      netbox/ipam/constants.py
  41. 1 2
      netbox/ipam/fields.py
  42. 31 12
      netbox/ipam/filters.py
  43. 1 2
      netbox/ipam/formfields.py
  44. 8 6
      netbox/ipam/forms.py
  45. 6 6
      netbox/ipam/models.py
  46. 17 2
      netbox/ipam/tables.py
  47. 4 6
      netbox/ipam/tests/test_api.py
  48. 0 1
      netbox/ipam/tests/test_models.py
  49. 1 0
      netbox/ipam/urls.py
  50. 53 15
      netbox/ipam/views.py
  51. 0 1
      netbox/netbox/forms.py
  52. 2 1
      netbox/netbox/settings.py
  53. 2 8
      netbox/netbox/urls.py
  54. 4 26
      netbox/netbox/views.py
  55. 0 9
      netbox/netbox/wsgi.py
  56. 3 0
      netbox/project-static/css/base.css
  57. 2 3
      netbox/secrets/api/views.py
  58. 1 2
      netbox/secrets/filters.py
  59. 0 1
      netbox/secrets/forms.py
  60. 4 1
      netbox/secrets/models.py
  61. 0 2
      netbox/secrets/tables.py
  62. 3 4
      netbox/secrets/tests/test_api.py
  63. 1 2
      netbox/secrets/tests/test_models.py
  64. 0 1
      netbox/secrets/urls.py
  65. 1 0
      netbox/secrets/views.py
  66. 13 6
      netbox/templates/500.html
  67. 1 1
      netbox/templates/circuits/circuit.html
  68. 100 68
      netbox/templates/dcim/device.html
  69. 1 1
      netbox/templates/dcim/devicebay_populate.html
  70. 2 2
      netbox/templates/dcim/inc/consoleport.html
  71. 3 4
      netbox/templates/dcim/inc/consoleserverport.html
  72. 1 1
      netbox/templates/dcim/inc/device_header.html
  73. 2 2
      netbox/templates/dcim/inc/devicebay.html
  74. 80 61
      netbox/templates/dcim/inc/interface.html
  75. 1 1
      netbox/templates/dcim/inc/inventoryitem.html
  76. 4 5
      netbox/templates/dcim/inc/poweroutlet.html
  77. 2 2
      netbox/templates/dcim/inc/powerport.html
  78. 4 4
      netbox/templates/dcim/rack.html
  79. 2 2
      netbox/templates/dcim/site.html
  80. 18 0
      netbox/templates/exceptions/import_error.html
  81. 12 0
      netbox/templates/exceptions/permission_error.html
  82. 17 0
      netbox/templates/exceptions/programming_error.html
  83. 14 2
      netbox/templates/ipam/inc/ipadress_edit_header.html
  84. 48 0
      netbox/templates/ipam/ipaddress_assign.html
  85. 3 3
      netbox/templates/ipam/prefix.html
  86. 4 4
      netbox/templates/ipam/vlan.html
  87. 0 70
      netbox/templates/virtualization/inc/interface.html
  88. 35 19
      netbox/templates/virtualization/virtualmachine.html
  89. 0 1
      netbox/tenancy/filters.py
  90. 0 2
      netbox/tenancy/tables.py
  91. 2 3
      netbox/tenancy/tests/test_api.py
  92. 0 1
      netbox/tenancy/urls.py
  93. 1 1
      netbox/tenancy/views.py
  94. 0 1
      netbox/users/api/serializers.py
  95. 1 1
      netbox/users/forms.py
  96. 2 1
      netbox/users/models.py
  97. 0 1
      netbox/users/urls.py
  98. 1 2
      netbox/utilities/api.py
  99. 0 1
      netbox/utilities/fields.py
  100. 1 1
      netbox/utilities/filters.py

+ 16 - 9
.github/ISSUE_TEMPLATE.md

@@ -4,23 +4,30 @@
     remove the "is:open" filter from the search bar to include closed issues.
     remove the "is:open" filter from the search bar to include closed issues.
 
 
     Check the appropriate type for your issue below by placing an x between the
     Check the appropriate type for your issue below by placing an x between the
-    brackets. If none of the below apply, please raise your issue for
-    discussion on our mailing list:
+    brackets. For assistance with installation issues, or for any other issues
+    other than those listed below, please raise your topic for discussion on
+    our mailing list:
 
 
         https://groups.google.com/forum/#!forum/netbox-discuss
         https://groups.google.com/forum/#!forum/netbox-discuss
 
 
     Please note that issues which do not fall under any of the below categories
     Please note that issues which do not fall under any of the below categories
-    will be closed.
+    will be closed. Due to an excessive backlog of feature requests, we are
+    not currently accepting any proposals which extend NetBox's feature scope.
+
+    Do not prepend any sort of tag to your issue's title. An administrator will
+    review your issue and assign labels as appropriate.
 --->
 --->
 ### Issue type
 ### Issue type
-[ ] Feature request <!-- Requesting the implementation of a new feature -->
-[ ] Bug report      <!-- Reporting unexpected or erroneous behavior -->
-[ ] Documentation   <!-- Proposing a modification to the documentation -->
+[ ] Feature request <!-- An enhancement of existing functionality -->
+[ ] Bug report      <!-- Unexpected or erroneous behavior -->
+[ ] Documentation   <!-- A modification to the documentation -->
 
 
 <!--
 <!--
     Please describe the environment in which you are running NetBox. (Be sure
     Please describe the environment in which you are running NetBox. (Be sure
     to verify that you are running the latest stable release of NetBox before
     to verify that you are running the latest stable release of NetBox before
-    submitting a bug report.)
+    submitting a bug report.) If you are submitting a bug report and have made
+    any changes to the code base, please first validate that your bug can be
+    recreated while running an official release.
 -->
 -->
 ### Environment
 ### Environment
 * Python version:  <!-- Example: 3.5.4 -->
 * Python version:  <!-- Example: 3.5.4 -->
@@ -28,8 +35,8 @@
 
 
 <!--
 <!--
     BUG REPORTS must include:
     BUG REPORTS must include:
-        * A list of the steps needed to reproduce the bug
-        * A description of the expected behavior
+        * A list of the steps needed for someone else to reproduce the bug
+        * A description of the expected and observed behavior
         * Any relevant error messages (screenshots may also help)
         * Any relevant error messages (screenshots may also help)
 
 
     FEATURE REQUESTS must include:
     FEATURE REQUESTS must include:

+ 63 - 73
CONTRIBUTING.md

@@ -1,8 +1,8 @@
 ## Getting Help
 ## Getting Help
 
 
 If you encounter any issues installing or using NetBox, try one of the
 If you encounter any issues installing or using NetBox, try one of the
-following resources to get assistance. Please **do not** open a GitHub
-issue except to report bugs or request features.
+following resources to get assistance. Please **do not** open a GitHub issue
+except to report bugs or request features.
 
 
 ### Mailing List
 ### Mailing List
 
 
@@ -13,35 +13,32 @@ installation. You can find us [here](https://groups.google.com/forum/#!forum/net
 ### Freenode IRC
 ### Freenode IRC
 
 
 For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
 For real-time discussion, you can join the #netbox channel on [Freenode](https://freenode.net/).
-You can connect to Freenode at irc.freenode.net using an IRC client, or
-you can use their [webchat client](https://webchat.freenode.net/).
+You can connect to Freenode at irc.freenode.net using an IRC client, or you can
+use their [webchat client](https://webchat.freenode.net/).
 
 
 ## Reporting Bugs
 ## Reporting Bugs
 
 
 * First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
 * First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases) of
-NetBox. If you're running an older version, it's possible that the bug
-has already been fixed.
-
-* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has
-already been reported. If you think you may be experiencing a reported
-issue that hasn't already been resolved, please click "add a reaction"
-in the top right corner of the issue and add a thumbs up (+1). You might
-also want to add a comment describing how it's affecting your
-installation. This will allow us to prioritize bugs based on how many
-users are affected.
-
-* If you haven't found an existing issue that describes your suspected
-bug, please inquire about it on the mailing list. **Do not** file an
-issue until you have received confirmation that it is in fact a bug.
-Invalid issues are very distracting and slow the pace at which NetBox is
-developed.
-
-* When submitting an issue, please be as descriptive as possible. Be
-sure to include:
+NetBox. If you're running an older version, it's possible that the bug has
+already been fixed.
+
+* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the bug you've found has already
+been reported. If you think you may be experiencing a reported issue that
+hasn't already been resolved, please click "add a reaction" in the top right
+corner of the issue and add a thumbs up (+1). You mightalso want to add a
+comment describing how it's affecting your installation. This will allow us to
+prioritize bugs based on how many users are affected.
+
+* If you haven't found an existing issue that describes your suspected bug,
+please inquire about it on the mailing list. **Do not** file an issue until you
+have received confirmation that it is in fact a bug. Invalid issues are very
+distracting and slow the pace at which NetBox is developed.
+
+* When submitting an issue, please be as descriptive as possible. Be sure to
+include:
 
 
     * The environment in which NetBox is running
     * The environment in which NetBox is running
-    * The exact steps that can be taken to reproduce the issue (if
-      applicable)
+    * The exact steps that can be taken to reproduce the issue (if applicable)
     * Any error messages generated
     * Any error messages generated
     * Screenshots (if applicable)
     * Screenshots (if applicable)
 
 
@@ -49,71 +46,64 @@ sure to include:
 The issue will be reviewed by a moderator after submission and the appropriate
 The issue will be reviewed by a moderator after submission and the appropriate
 labels will be applied.
 labels will be applied.
 
 
-* Keep in mind that we prioritize bugs based on their severity and how
-much work is required to resolve them. It may take some time for someone
-to address your issue.
+* Keep in mind that we prioritize bugs based on their severity and how much
+work is required to resolve them. It may take some time for someone to address
+your issue.
 
 
 ## Feature Requests
 ## Feature Requests
 
 
-* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're
-requesting is already listed. (Be sure to search closed issues as well,
-since some feature requests are rejected.) If the feature you'd like to
-see has already been requested, click "add a reaction" in the top right
-corner of the issue and add a thumbs up (+1). This ensures that the
-issue has a better chance of making it onto the roadmap. Also feel free
-to add a comment with any additional justification for the feature.
-(However, note that comments with no substance other than a "+1" will be
-deleted. Please use GitHub's reactions feature to indicate your
-support.)
-
-* While suggestions for new features are welcome, it's important to
-limit the scope of NetBox's feature set to avoid feature creep. For
-example, the following features would be firmly out of scope for NetBox:
-
-    * Ticket management
-    * Network state monitoring
-    * Acting as a DNS server
-    * Acting as an authentication server
+* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues) to see if the feature you're requesting
+is already listed. (Be sure to search closed issues as well, since some
+feature requests have been rejected.) If the feature you'd like to see has
+already been requested and is open, click "add a reaction" in the top right
+corner of the issue and add a thumbs up (+1). This ensures that the issue has
+a better chance of receiving attention. Also feel free to add a comment with
+any additional justification for the feature. (However, note that comments with
+no substance other than a "+1" will be deleted. Please use GitHub's reactions
+feature to indicate your support.)
+
+* Due to an excessive backlog of feature requests, we are not currently
+accepting any proposals which substantially extend NetBox's functionality
+beyond its current feature set. This includes the introduction of any new views
+or models which have not already been proposed in an existing feature request.
 
 
 * Before filing a new feature request, consider raising your idea on the
 * Before filing a new feature request, consider raising your idea on the
-mailing list first. Feedback you receive there will help validate and
-shape the proposed feature before filing a formal issue.
+mailing list first. Feedback you receive there will help validate and shape the
+proposed feature before filing a formal issue.
 
 
-* Good feature requests are very narrowly defined. Be sure to enumerate
-specific functionality and data schema. The more effort you put into
-writing a feature request, the better its chance is of being
+* Good feature requests are very narrowly defined. Be sure to thoroughly
+describe the functionality and data model(s) being proposed. The more effort
+you put into writing a feature request, the better its chance is of being
 implemented. Overly broad feature requests will be closed.
 implemented. Overly broad feature requests will be closed.
 
 
 * When submitting a feature request on GitHub, be sure to include the
 * When submitting a feature request on GitHub, be sure to include the
 following:
 following:
 
 
     * A detailed description of the proposed functionality
     * A detailed description of the proposed functionality
-    * A use case for the feature; who would use it and what value it
-      would add to NetBox
-    * A rough description of changes necessary to the database schema
-      (if applicable)
-    * Any third-party libraries or other resources which would be
-      involved
-
-* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue title.
-The issue will be reviewed by a moderator after submission and the appropriate
-labels will be applied.
+    * A use case for the feature; who would use it and what value it would add
+      to NetBox
+    * A rough description of changes necessary to the database schema (if
+      applicable)
+    * Any third-party libraries or other resources which would be involved
+
+* Please avoid prepending any sort of tag (e.g. "[Feature]") to the issue
+title. The issue will be reviewed by a moderator after submission and the
+appropriate labels will be applied.
 
 
 ## Submitting Pull Requests
 ## Submitting Pull Requests
 
 
-* Be sure to open an issue before starting work on a pull request, and
-discuss your idea with the NetBox maintainers before beginning work​.
-This will help prevent wasting time on something that might we might not
-be able to implement. When suggesting a new feature, also make sure it
-won't conflict with any work that's already in progress.
+* Be sure to open an issue before starting work on a pull request, and discuss
+your idea with the NetBox maintainers before beginning work​. This will help
+prevent wasting time on something that might we might not be able to implement.
+When suggesting a new feature, also make sure it won't conflict with any work
+that's already in progress.
 
 
-* When submitting a pull request, please be sure to work off of the
-`develop` branch, rather than `master`. In NetBox, the `develop` branch
-is used for ongoing development, while `master` is used for tagging new
-stable releases.
+* When submitting a pull request, please be sure to work off of the `develop`
+branch, rather than `master`. The `develop` branch is used for ongoing
+development, while `master` is used for tagging new stable releases.
 
 
-* All code submissions should meet the following criteria (CI will
-enforce these checks):
+* All code submissions should meet the following criteria (CI will enforce
+these checks):
 
 
     * Python syntax is valid
     * Python syntax is valid
     * All tests pass when run with `./manage.py test`
     * All tests pass when run with `./manage.py test`

+ 2 - 3
netbox/circuits/api/views.py

@@ -1,16 +1,15 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+from django.shortcuts import get_object_or_404
 from rest_framework.decorators import detail_route
 from rest_framework.decorators import detail_route
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.viewsets import ModelViewSet
 from rest_framework.viewsets import ModelViewSet
 
 
-from django.shortcuts import get_object_or_404
-
 from circuits import filters
 from circuits import filters
 from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
 from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
-from extras.models import Graph, GRAPH_TYPE_PROVIDER
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
+from extras.models import Graph, GRAPH_TYPE_PROVIDER
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from . import serializers
 from . import serializers
 
 

+ 0 - 1
netbox/circuits/filters.py

@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import django_filters
 import django_filters
-
 from django.db.models import Q
 from django.db.models import Q
 
 
 from dcim.models import Site
 from dcim.models import Site

+ 0 - 1
netbox/circuits/forms.py

@@ -11,7 +11,6 @@ from utilities.forms import (
     APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
     APISelect, BootstrapMixin, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, FilterChoiceField,
     SmallTextarea, SlugField,
     SmallTextarea, SlugField,
 )
 )
-
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 
 

+ 1 - 1
netbox/circuits/models.py

@@ -8,8 +8,8 @@ from django.utils.encoding import python_2_unicode_compatible
 from dcim.fields import ASNField
 from dcim.fields import ASNField
 from extras.models import CustomFieldModel, CustomFieldValue
 from extras.models import CustomFieldModel, CustomFieldValue
 from tenancy.models import Tenant
 from tenancy.models import Tenant
-from utilities.utils import csv_format
 from utilities.models import CreatedUpdatedModel
 from utilities.models import CreatedUpdatedModel
+from utilities.utils import csv_format
 from .constants import *
 from .constants import *
 
 
 
 

+ 1 - 3
netbox/circuits/tables.py

@@ -1,14 +1,12 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import django_tables2 as tables
 import django_tables2 as tables
-from django_tables2.utils import Accessor
-
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
+from django_tables2.utils import Accessor
 
 
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
 from .models import Circuit, CircuitType, Provider
 from .models import Circuit, CircuitType, Provider
 
 
-
 CIRCUITTYPE_ACTIONS = """
 CIRCUITTYPE_ACTIONS = """
 {% if perms.circuit.change_circuittype %}
 {% if perms.circuit.change_circuittype %}
     <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
     <a href="{% url 'circuits:circuittype_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>

+ 6 - 5
netbox/circuits/tests/test_api.py

@@ -1,14 +1,15 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from rest_framework import status
-from rest_framework.test import APITestCase
-
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.urls import reverse
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 
+from circuits.constants import TERM_SIDE_A, TERM_SIDE_Z
+from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
 from dcim.models import Site
 from dcim.models import Site
-from extras.models import Graph, GRAPH_TYPE_PROVIDER
-from circuits.models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
+from extras.constants import GRAPH_TYPE_PROVIDER
+from extras.models import Graph
 from users.models import Token
 from users.models import Token
 from utilities.tests import HttpStatusMixin
 from utilities.tests import HttpStatusMixin
 
 

+ 0 - 1
netbox/circuits/urls.py

@@ -4,7 +4,6 @@ from django.conf.urls import url
 
 
 from . import views
 from . import views
 
 
-
 app_name = 'circuits'
 app_name = 'circuits'
 urlpatterns = [
 urlpatterns = [
 
 

+ 2 - 1
netbox/circuits/views.py

@@ -15,7 +15,8 @@ from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from . import filters, forms, tables
 from . import filters, forms, tables
-from .models import Circuit, CircuitTermination, CircuitType, Provider, TERM_SIDE_A, TERM_SIDE_Z
+from .constants import TERM_SIDE_A, TERM_SIDE_Z
+from .models import Circuit, CircuitTermination, CircuitType, Provider
 
 
 
 
 #
 #

+ 10 - 6
netbox/dcim/api/serializers.py

@@ -1,19 +1,23 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from rest_framework import serializers
 from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 from rest_framework.validators import UniqueTogetherValidator
 
 
-from ipam.models import IPAddress
 from circuits.models import Circuit, CircuitTermination
 from circuits.models import Circuit, CircuitTermination
+from dcim.constants import (
+    CONNECTION_STATUS_CHOICES, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
+    RACK_WIDTH_CHOICES, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
+)
 from dcim.models import (
 from dcim.models import (
-    CONNECTION_STATUS_CHOICES, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
-    DeviceBay, DeviceBayTemplate, DeviceType, DeviceRole, IFACE_FF_CHOICES, IFACE_ORDERING_CHOICES, Interface,
-    InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate,
-    PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RACK_FACE_CHOICES, RACK_TYPE_CHOICES,
-    RACK_WIDTH_CHOICES, Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHOICES,
+    ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
+    DeviceBayTemplate, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
+    InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
+    RackReservation, RackRole, Region, Site,
 )
 )
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
+from ipam.models import IPAddress
 from tenancy.api.serializers import NestedTenantSerializer
 from tenancy.api.serializers import NestedTenantSerializer
 from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
 from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
 from virtualization.models import Cluster
 from virtualization.models import Cluster

+ 18 - 9
netbox/dcim/api/views.py

@@ -1,28 +1,30 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from collections import OrderedDict
 
 
+from django.conf import settings
+from django.http import HttpResponseBadRequest, HttpResponseForbidden
+from django.shortcuts import get_object_or_404
 from rest_framework.decorators import detail_route
 from rest_framework.decorators import detail_route
 from rest_framework.mixins import ListModelMixin
 from rest_framework.mixins import ListModelMixin
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
 from rest_framework.viewsets import GenericViewSet, ModelViewSet, ViewSet
 
 
-from django.conf import settings
-from django.http import HttpResponseBadRequest, HttpResponseForbidden
-from django.shortcuts import get_object_or_404
-
+from dcim import filters
 from dcim.models import (
 from dcim.models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
     DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
     InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
     InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
     RackReservation, RackRole, Region, Site,
     RackReservation, RackRole, Region, Site,
 )
 )
-from dcim import filters
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
 from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
-from utilities.api import IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin
-from .exceptions import MissingFilterException
+from utilities.api import (
+    IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ServiceUnavailable, WritableSerializerMixin,
+)
 from . import serializers
 from . import serializers
+from .exceptions import MissingFilterException
 
 
 
 
 #
 #
@@ -256,12 +258,19 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
                 device.platform
                 device.platform
             ))
             ))
 
 
-        # Check that NAPALM is installed and verify the configured driver
+        # Check that NAPALM is installed
         try:
         try:
             import napalm
             import napalm
-            from napalm_base.exceptions import ConnectAuthError, ModuleImportError
         except ImportError:
         except ImportError:
             raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
             raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
+
+        # TODO: Remove support for NAPALM < 2.0
+        try:
+            from napalm.base.exceptions import ConnectAuthError, ModuleImportError
+        except ImportError:
+            from napalm_base.exceptions import ConnectAuthError, ModuleImportError
+
+        # Validate the configured driver
         try:
         try:
             driver = napalm.get_network_driver(device.platform.napalm_driver)
             driver = napalm.get_network_driver(device.platform.napalm_driver)
         except ModuleImportError:
         except ModuleImportError:

+ 10 - 8
netbox/dcim/filters.py

@@ -1,22 +1,23 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import django_filters
 import django_filters
-from netaddr import EUI
-from netaddr.core import AddrFormatError
-
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.db.models import Q
 from django.db.models import Q
+from netaddr import EUI
+from netaddr.core import AddrFormatError
 
 
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.filters import NullableCharFieldFilter, NumericInFilter
 from utilities.filters import NullableCharFieldFilter, NumericInFilter
 from virtualization.models import Cluster
 from virtualization.models import Cluster
+from .constants import (
+    IFACE_FF_LAG, NONCONNECTABLE_IFACE_TYPES, STATUS_CHOICES, VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
+)
 from .models import (
 from .models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
-    DeviceBayTemplate, DeviceRole, DeviceType, STATUS_CHOICES, IFACE_FF_LAG, Interface, InterfaceConnection,
-    InterfaceTemplate, Manufacturer, InventoryItem, NONCONNECTABLE_IFACE_TYPES, Platform, PowerOutlet,
-    PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site,
-    VIRTUAL_IFACE_TYPES, WIRELESS_IFACE_TYPES,
+    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
+    InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
+    RackReservation, RackRole, Region, Site,
 )
 )
 
 
 
 
@@ -424,7 +425,8 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Device model (slug)',
         label='Device model (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=STATUS_CHOICES
+        choices=STATUS_CHOICES,
+        null_value=None
     )
     )
     is_full_depth = django_filters.BooleanFilter(
     is_full_depth = django_filters.BooleanFilter(
         name='device_type__is_full_depth',
         name='device_type__is_full_depth',

+ 1 - 2
netbox/dcim/formfields.py

@@ -1,9 +1,8 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from netaddr import EUI, AddrFormatError
-
 from django import forms
 from django import forms
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
+from netaddr import EUI, AddrFormatError
 
 
 
 
 #
 #

+ 10 - 8
netbox/dcim/forms.py

@@ -1,12 +1,12 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from mptt.forms import TreeNodeChoiceField
 import re
 import re
 
 
 from django import forms
 from django import forms
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.db.models import Count, Q
 from django.db.models import Count, Q
+from mptt.forms import TreeNodeChoiceField
 
 
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from ipam.models import IPAddress
 from ipam.models import IPAddress
@@ -19,17 +19,19 @@ from utilities.forms import (
     SlugField, FilterTreeNodeMultipleChoiceField,
     SlugField, FilterTreeNodeMultipleChoiceField,
 )
 )
 from virtualization.models import Cluster
 from virtualization.models import Cluster
+from .constants import (
+    CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES,
+    RACK_FACE_CHOICES, RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, STATUS_CHOICES,
+    SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
+)
 from .formfields import MACAddressFormField
 from .formfields import MACAddressFormField
 from .models import (
 from .models import (
-    DeviceBay, DeviceBayTemplate, CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, ConsolePort,
-    ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface,
-    IFACE_FF_CHOICES, IFACE_FF_LAG, IFACE_ORDERING_CHOICES, InterfaceConnection, InterfaceTemplate, Manufacturer,
-    InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, RACK_FACE_CHOICES,
-    RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, Rack, RackGroup, RackReservation, RackRole, RACK_WIDTH_19IN, RACK_WIDTH_23IN,
-    Region, Site, STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES,
+    DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
+    Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem,
+    Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation,
+    RackRole, Region, Site,
 )
 )
 
 
-
 DEVICE_BY_PK_RE = '{\d+\}'
 DEVICE_BY_PK_RE = '{\d+\}'
 
 
 
 

+ 28 - 4
netbox/dcim/models.py

@@ -1,12 +1,10 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from collections import OrderedDict
 from itertools import count, groupby
 from itertools import count, groupby
 
 
-from mptt.models import MPTTModel, TreeForeignKey
-
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.postgres.fields import ArrayField
 from django.contrib.postgres.fields import ArrayField
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
@@ -15,9 +13,10 @@ from django.db import models
 from django.db.models import Count, Q, ObjectDoesNotExist
 from django.db.models import Count, Q, ObjectDoesNotExist
 from django.urls import reverse
 from django.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.encoding import python_2_unicode_compatible
+from mptt.models import MPTTModel, TreeForeignKey
 
 
 from circuits.models import Circuit
 from circuits.models import Circuit
-from extras.models import CustomFieldModel, CustomField, CustomFieldValue, ImageAttachment
+from extras.models import CustomFieldModel, CustomFieldValue, ImageAttachment
 from extras.rpc import RPC_CLIENTS
 from extras.rpc import RPC_CLIENTS
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.fields import ColorField, NullableCharField
 from utilities.fields import ColorField, NullableCharField
@@ -1118,6 +1117,15 @@ class ConsoleServerPort(models.Model):
     def __str__(self):
     def __str__(self):
         return self.name
         return self.name
 
 
+    def clean(self):
+
+        # Check that the parent device's DeviceType is a console server
+        device_type = self.device.device_type
+        if not device_type.is_console_server:
+            raise ValidationError("The {} {} device type not support assignment of console server ports.".format(
+                device_type.manufacturer, device_type
+            ))
+
 
 
 #
 #
 # Power ports
 # Power ports
@@ -1183,6 +1191,15 @@ class PowerOutlet(models.Model):
     def __str__(self):
     def __str__(self):
         return self.name
         return self.name
 
 
+    def clean(self):
+
+        # Check that the parent device's DeviceType is a PDU
+        device_type = self.device.device_type
+        if not device_type.is_pdu:
+            raise ValidationError("The {} {} device type not support assignment of power outlets.".format(
+                device_type.manufacturer, device_type
+            ))
+
 
 
 #
 #
 # Interfaces
 # Interfaces
@@ -1239,6 +1256,13 @@ class Interface(models.Model):
 
 
     def clean(self):
     def clean(self):
 
 
+        # Check that the parent device's DeviceType is a network device
+        device_type = self.device.device_type
+        if not device_type.is_network_device:
+            raise ValidationError("The {} {} device type not support assignment of network interfaces.".format(
+                device_type.manufacturer, device_type
+            ))
+
         # An Interface must belong to a Device *or* to a VirtualMachine
         # An Interface must belong to a Device *or* to a VirtualMachine
         if self.device and self.virtual_machine:
         if self.device and self.virtual_machine:
             raise ValidationError("An interface cannot belong to both a device and a virtual machine.")
             raise ValidationError("An interface cannot belong to both a device and a virtual machine.")

+ 4 - 4
netbox/dcim/querysets.py

@@ -44,10 +44,10 @@ class InterfaceQuerySet(QuerySet):
 
 
         TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
         TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
         ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
         ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
-        SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
-        SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
-        POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
-        SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
+        SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?([0-9]+)\/') AS integer)"
+        SUBSLOT_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/)([0-9]+)') AS integer), 0)"
+        POSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)"
+        SUBPOSITION_RE = r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)?(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)"
         CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
         CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
         VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
         VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
 
 

+ 0 - 1
netbox/dcim/tables.py

@@ -10,7 +10,6 @@ from .models import (
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
     PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, Region, Site,
 )
 )
 
 
-
 REGION_LINK = """
 REGION_LINK = """
 {% if record.get_children %}
 {% if record.get_children %}
     <span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>
     <span style="padding-left: {{ record.get_ancestors|length }}0px "><i class="fa fa-caret-right"></i>

+ 9 - 9
netbox/dcim/tests/test_api.py

@@ -1,16 +1,16 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from rest_framework import status
-from rest_framework.test import APITestCase
-
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.urls import reverse
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 
+from dcim.constants import IFACE_FF_LAG, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT
 from dcim.models import (
 from dcim.models import (
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
     ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
-    DeviceBayTemplate, DeviceRole, DeviceType, IFACE_FF_LAG, Interface, InterfaceConnection, InterfaceTemplate,
-    Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
-    RackReservation, RackRole, Region, Site, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT,
+    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
+    InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
+    RackReservation, RackRole, Region, Site,
 )
 )
 from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
 from users.models import Token
 from users.models import Token
@@ -1432,7 +1432,7 @@ class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         devicetype = DeviceType.objects.create(
         devicetype = DeviceType.objects.create(
-            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
+            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_console_server=True
         )
         )
         devicerole = DeviceRole.objects.create(
         devicerole = DeviceRole.objects.create(
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -1590,7 +1590,7 @@ class PowerOutletTest(HttpStatusMixin, APITestCase):
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         devicetype = DeviceType.objects.create(
         devicetype = DeviceType.objects.create(
-            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
+            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_pdu=True
         )
         )
         devicerole = DeviceRole.objects.create(
         devicerole = DeviceRole.objects.create(
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
@@ -1667,7 +1667,7 @@ class InterfaceTest(HttpStatusMixin, APITestCase):
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         devicetype = DeviceType.objects.create(
         devicetype = DeviceType.objects.create(
-            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
+            manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1', is_network_device=True
         )
         )
         devicerole = DeviceRole.objects.create(
         devicerole = DeviceRole.objects.create(
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
             name='Test Device Role 1', slug='test-device-role-1', color='ff0000'

+ 1 - 2
netbox/dcim/urls.py

@@ -5,9 +5,8 @@ from django.conf.urls import url
 from extras.views import ImageAttachmentEditView
 from extras.views import ImageAttachmentEditView
 from ipam.views import ServiceCreateView
 from ipam.views import ServiceCreateView
 from secrets.views import secret_add
 from secrets.views import secret_add
-from .models import Device, Rack, Site
 from . import views
 from . import views
-
+from .models import Device, Rack, Site
 
 
 app_name = 'dcim'
 app_name = 'dcim'
 urlpatterns = [
 urlpatterns = [

+ 8 - 7
netbox/dcim/views.py

@@ -1,6 +1,5 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-import re
-from natsort import natsorted
+
 from operator import attrgetter
 from operator import attrgetter
 
 
 from django.contrib import messages
 from django.contrib import messages
@@ -15,10 +14,11 @@ from django.utils.html import escape
 from django.utils.http import urlencode
 from django.utils.http import urlencode
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 from django.views.generic import View
+from natsort import natsorted
 
 
-from ipam.models import Prefix, Service, VLAN
 from circuits.models import Circuit
 from circuits.models import Circuit
 from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
 from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
+from ipam.models import Prefix, Service, VLAN
 from utilities.forms import ConfirmationForm
 from utilities.forms import ConfirmationForm
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
 from utilities.views import (
 from utilities.views import (
@@ -26,11 +26,12 @@ from utilities.views import (
     ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
     ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from . import filters, forms, tables
 from . import filters, forms, tables
+from .constants import CONNECTION_STATUS_CONNECTED
 from .models import (
 from .models import (
-    CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
-    DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
-    Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
-    RackGroup, RackReservation, RackRole, Region, Site,
+    ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
+    DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer,
+    InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
+    RackReservation, RackRole, Region, Site,
 )
 )
 
 
 
 

+ 14 - 6
netbox/extras/api/customfields.py

@@ -1,15 +1,14 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-from datetime import datetime
 
 
-from rest_framework import serializers
-from rest_framework.exceptions import ValidationError
+from datetime import datetime
 
 
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.db import transaction
 from django.db import transaction
+from rest_framework import serializers
+from rest_framework.exceptions import ValidationError
 
 
-from extras.models import (
-    CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CustomField, CustomFieldChoice, CustomFieldValue,
-)
+from extras.constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT
+from extras.models import CustomField, CustomFieldChoice, CustomFieldValue
 from utilities.api import ValidatedModelSerializer
 from utilities.api import ValidatedModelSerializer
 
 
 
 
@@ -39,6 +38,15 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
             # Data validation
             # Data validation
             if value not in [None, '']:
             if value not in [None, '']:
 
 
+                # Validate integer
+                if cf.type == CF_TYPE_INTEGER:
+                    try:
+                        int(value)
+                    except ValueError:
+                        raise ValidationError(
+                            "Invalid value for integer field {}: {}".format(field_name, value)
+                        )
+
                 # Validate boolean
                 # Validate boolean
                 if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
                 if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
                     raise ValidationError(
                     raise ValidationError(

+ 2 - 4
netbox/extras/api/serializers.py

@@ -1,14 +1,12 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
-
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
 from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
 from dcim.models import Device, Rack, Site
 from dcim.models import Device, Rack, Site
-from extras.models import (
-    ACTION_CHOICES, ExportTemplate, Graph, GRAPH_TYPE_CHOICES, ImageAttachment, ReportResult, TopologyMap, UserAction,
-)
+from extras.constants import ACTION_CHOICES, GRAPH_TYPE_CHOICES
+from extras.models import ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
 from users.api.serializers import NestedUserSerializer
 from users.api.serializers import NestedUserSerializer
 from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
 from utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
 
 

+ 3 - 4
netbox/extras/api/views.py

@@ -1,14 +1,13 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+from django.contrib.contenttypes.models import ContentType
+from django.http import Http404, HttpResponse
+from django.shortcuts import get_object_or_404
 from rest_framework.decorators import detail_route
 from rest_framework.decorators import detail_route
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
 from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet, ViewSet
 
 
-from django.contrib.contenttypes.models import ContentType
-from django.http import Http404, HttpResponse
-from django.shortcuts import get_object_or_404
-
 from extras import filters
 from extras import filters
 from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
 from extras.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
 from extras.reports import get_report, get_reports
 from extras.reports import get_report, get_reports

+ 2 - 2
netbox/extras/filters.py

@@ -1,12 +1,12 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import django_filters
 import django_filters
-
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 
 
 from dcim.models import Site
 from dcim.models import Site
-from .models import CF_TYPE_SELECT, CustomField, Graph, ExportTemplate, TopologyMap, UserAction
+from .constants import CF_TYPE_SELECT
+from .models import CustomField, Graph, ExportTemplate, TopologyMap, UserAction
 
 
 
 
 class CustomFieldFilter(django_filters.Filter):
 class CustomFieldFilter(django_filters.Filter):

+ 3 - 4
netbox/extras/forms.py

@@ -1,14 +1,13 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from django import forms
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 
 
 from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
 from utilities.forms import BootstrapMixin, BulkEditForm, LaxURLField
-from .models import (
-    CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL, CustomField, CustomFieldValue,
-    ImageAttachment,
-)
+from .constants import CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_INTEGER, CF_TYPE_SELECT, CF_TYPE_URL
+from .models import CustomField, CustomFieldValue, ImageAttachment
 
 
 
 
 def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):
 def get_custom_fields_for_model(content_type, filterable_only=False, bulk_edit=False):

+ 0 - 1
netbox/extras/management/commands/nbshell.py

@@ -10,7 +10,6 @@ from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 from django.db.models import Model
 from django.db.models import Model
 
 
-
 APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
 APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
 
 
 BANNER_TEXT = """### NetBox interactive shell ({node})
 BANNER_TEXT = """### NetBox interactive shell ({node})

+ 2 - 2
netbox/extras/management/commands/run_inventory.py

@@ -1,12 +1,12 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from getpass import getpass
 from getpass import getpass
-from ncclient.transport.errors import AuthenticationError
-from paramiko import AuthenticationException
 
 
 from django.conf import settings
 from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
 from django.core.management.base import BaseCommand, CommandError
 from django.db import transaction
 from django.db import transaction
+from ncclient.transport.errors import AuthenticationError
+from paramiko import AuthenticationException
 
 
 from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
 from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE
 
 

+ 0 - 1
netbox/extras/management/commands/runreport.py

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 from django.utils import timezone
 
 
-from extras.models import ReportResult
 from extras.reports import get_reports
 from extras.reports import get_reports
 
 
 
 

+ 2 - 1
netbox/extras/migrations/0008_reports.py

@@ -2,6 +2,7 @@
 # Generated by Django 1.11.4 on 2017-09-26 21:25
 # Generated by Django 1.11.4 on 2017-09-26 21:25
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 from distutils.version import StrictVersion
 from distutils.version import StrictVersion
+import re
 
 
 from django.conf import settings
 from django.conf import settings
 import django.contrib.postgres.fields.jsonb
 import django.contrib.postgres.fields.jsonb
@@ -18,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
         with connection.cursor() as cursor:
         with connection.cursor() as cursor:
             cursor.execute("SELECT VERSION()")
             cursor.execute("SELECT VERSION()")
             row = cursor.fetchone()
             row = cursor.fetchone()
-            pg_version = row[0].split()[1]
+            pg_version = re.match('^PostgreSQL (\d+\.\d+(\.\d+)?)', row[0]).group(1)
             if StrictVersion(pg_version) < StrictVersion('9.4.0'):
             if StrictVersion(pg_version) < StrictVersion('9.4.0'):
                 raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
                 raise Exception("PostgreSQL 9.4.0 or higher is required ({} found). Upgrade PostgreSQL and then run migrations again.".format(pg_version))
 
 

+ 2 - 1
netbox/extras/models.py

@@ -1,8 +1,9 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from collections import OrderedDict
 from datetime import date
 from datetime import date
-import graphviz
 
 
+import graphviz
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType

+ 2 - 1
netbox/extras/reports.py

@@ -1,8 +1,9 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-from collections import OrderedDict
+
 import importlib
 import importlib
 import inspect
 import inspect
 import pkgutil
 import pkgutil
+from collections import OrderedDict
 
 
 from django.conf import settings
 from django.conf import settings
 from django.utils import timezone
 from django.utils import timezone

+ 2 - 2
netbox/extras/rpc.py

@@ -1,11 +1,11 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 import re
 import re
 import time
 import time
 
 
-from ncclient import manager
 import paramiko
 import paramiko
 import xmltodict
 import xmltodict
-
+from ncclient import manager
 
 
 CONNECT_TIMEOUT = 5  # seconds
 CONNECT_TIMEOUT = 5  # seconds
 
 

+ 4 - 4
netbox/extras/tests/test_api.py

@@ -1,14 +1,14 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from rest_framework import status
-from rest_framework.test import APITestCase
-
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.urls import reverse
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 
 from dcim.models import Device
 from dcim.models import Device
-from extras.models import Graph, GRAPH_TYPE_SITE, ExportTemplate
+from extras.constants import GRAPH_TYPE_SITE
+from extras.models import Graph, ExportTemplate
 from users.models import Token
 from users.models import Token
 from utilities.tests import HttpStatusMixin
 from utilities.tests import HttpStatusMixin
 
 

+ 5 - 7
netbox/extras/tests/test_customfields.py

@@ -1,19 +1,17 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-from datetime import date
 
 
-from rest_framework import status
-from rest_framework.test import APITestCase
+from datetime import date
 
 
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from django.test import TestCase
 from django.urls import reverse
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 
 from dcim.models import Site
 from dcim.models import Site
-from extras.models import (
-    CustomField, CustomFieldValue, CustomFieldChoice, CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE,
-    CF_TYPE_SELECT, CF_TYPE_URL,
-)
+from extras.constants import CF_TYPE_TEXT, CF_TYPE_INTEGER, CF_TYPE_BOOLEAN, CF_TYPE_DATE, CF_TYPE_SELECT, CF_TYPE_URL
+from extras.models import CustomField, CustomFieldValue, CustomFieldChoice
 from users.models import Token
 from users.models import Token
 from utilities.tests import HttpStatusMixin
 from utilities.tests import HttpStatusMixin
 
 

+ 0 - 1
netbox/extras/urls.py

@@ -4,7 +4,6 @@ from django.conf.urls import url
 
 
 from extras import views
 from extras import views
 
 
-
 app_name = 'extras'
 app_name = 'extras'
 urlpatterns = [
 urlpatterns = [
 
 

+ 1 - 1
netbox/extras/views.py

@@ -1,7 +1,7 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib import messages
 from django.contrib import messages
+from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.http import Http404
 from django.http import Http404
 from django.shortcuts import get_object_or_404, redirect, render
 from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe

+ 4 - 3
netbox/ipam/api/serializers.py

@@ -1,4 +1,5 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from rest_framework import serializers
 from rest_framework import serializers
@@ -6,10 +7,10 @@ from rest_framework.validators import UniqueTogetherValidator
 
 
 from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
 from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
 from extras.api.customfields import CustomFieldModelSerializer
 from extras.api.customfields import CustomFieldModelSerializer
-from ipam.models import (
-    Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, Prefix,
-    PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
+from ipam.constants import (
+    IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, IP_PROTOCOL_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES,
 )
 )
+from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from tenancy.api.serializers import NestedTenantSerializer
 from tenancy.api.serializers import NestedTenantSerializer
 from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
 from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
 from virtualization.api.serializers import NestedVirtualMachineSerializer
 from virtualization.api.serializers import NestedVirtualMachineSerializer

+ 4 - 5
netbox/ipam/api/views.py

@@ -1,17 +1,16 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+from django.conf import settings
+from django.shortcuts import get_object_or_404
 from rest_framework import status
 from rest_framework import status
 from rest_framework.decorators import detail_route
 from rest_framework.decorators import detail_route
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.viewsets import ModelViewSet
 from rest_framework.viewsets import ModelViewSet
 
 
-from django.conf import settings
-from django.shortcuts import get_object_or_404
-
-from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
-from ipam import filters
 from extras.api.views import CustomFieldModelViewSet
 from extras.api.views import CustomFieldModelViewSet
+from ipam import filters
+from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from . import serializers
 from . import serializers
 
 

+ 11 - 1
netbox/ipam/constants.py

@@ -61,7 +61,7 @@ VLAN_STATUS_CHOICES = (
     (VLAN_STATUS_DEPRECATED, 'Deprecated')
     (VLAN_STATUS_DEPRECATED, 'Deprecated')
 )
 )
 
 
-# Bootstrap CSS classes for various statuses
+# Bootstrap CSS classes
 STATUS_CHOICE_CLASSES = {
 STATUS_CHOICE_CLASSES = {
     0: 'default',
     0: 'default',
     1: 'primary',
     1: 'primary',
@@ -70,6 +70,16 @@ STATUS_CHOICE_CLASSES = {
     4: 'warning',
     4: 'warning',
     5: 'success',
     5: 'success',
 }
 }
+ROLE_CHOICE_CLASSES = {
+    10: 'default',
+    20: 'primary',
+    30: 'warning',
+    40: 'success',
+    41: 'success',
+    42: 'success',
+    43: 'success',
+    44: 'success',
+}
 
 
 # IP protocols (for services)
 # IP protocols (for services)
 IP_PROTOCOL_TCP = 6
 IP_PROTOCOL_TCP = 6

+ 1 - 2
netbox/ipam/fields.py

@@ -1,9 +1,8 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from netaddr import IPNetwork
-
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.db import models
 from django.db import models
+from netaddr import IPNetwork
 
 
 from .formfields import IPFormField
 from .formfields import IPFormField
 from .lookups import (
 from .lookups import (

+ 31 - 12
netbox/ipam/filters.py

@@ -1,20 +1,17 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import django_filters
 import django_filters
+from django.db.models import Q
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 from netaddr.core import AddrFormatError
 from netaddr.core import AddrFormatError
 
 
-from django.db.models import Q
-
 from dcim.models import Site, Device, Interface
 from dcim.models import Site, Device, Interface
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.filters import NumericInFilter
 from utilities.filters import NumericInFilter
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
-from .models import (
-    Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
-    Service, VLAN, VLAN_STATUS_CHOICES, VLANGroup, VRF,
-)
+from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
+from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 
 
 class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
 class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
@@ -102,9 +99,18 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         method='search',
         label='Search',
         label='Search',
     )
     )
+    # TODO: Deprecate in v2.3.0
     parent = django_filters.CharFilter(
     parent = django_filters.CharFilter(
-        method='search_by_parent',
-        label='Parent prefix',
+        method='search_within_include',
+        label='Parent prefix (deprecated)',
+    )
+    within = django_filters.CharFilter(
+        method='search_within',
+        label='Within prefix',
+    )
+    within_include = django_filters.CharFilter(
+        method='search_within_include',
+        label='Within and including prefix',
     )
     )
     mask_length = django_filters.NumberFilter(
     mask_length = django_filters.NumberFilter(
         method='filter_mask_length',
         method='filter_mask_length',
@@ -159,7 +165,8 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Role (slug)',
         label='Role (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=PREFIX_STATUS_CHOICES
+        choices=PREFIX_STATUS_CHOICES,
+        null_value=None
     )
     )
 
 
     class Meta:
     class Meta:
@@ -177,7 +184,17 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
             pass
             pass
         return queryset.filter(qs_filter)
         return queryset.filter(qs_filter)
 
 
-    def search_by_parent(self, queryset, name, value):
+    def search_within(self, queryset, name, value):
+        value = value.strip()
+        if not value:
+            return queryset
+        try:
+            query = str(IPNetwork(value).cidr)
+            return queryset.filter(prefix__net_contained=query)
+        except (AddrFormatError, ValueError):
+            return queryset.none()
+
+    def search_within_include(self, queryset, name, value):
         value = value.strip()
         value = value.strip()
         if not value:
         if not value:
             return queryset
             return queryset
@@ -254,7 +271,8 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Interface (ID)',
         label='Interface (ID)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=IPADDRESS_STATUS_CHOICES
+        choices=IPADDRESS_STATUS_CHOICES,
+        null_value=None
     )
     )
     role = django_filters.MultipleChoiceFilter(
     role = django_filters.MultipleChoiceFilter(
         choices=IPADDRESS_ROLE_CHOICES
         choices=IPADDRESS_ROLE_CHOICES
@@ -353,7 +371,8 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Role (slug)',
         label='Role (slug)',
     )
     )
     status = django_filters.MultipleChoiceFilter(
     status = django_filters.MultipleChoiceFilter(
-        choices=VLAN_STATUS_CHOICES
+        choices=VLAN_STATUS_CHOICES,
+        null_value=None
     )
     )
 
 
     class Meta:
     class Meta:

+ 1 - 2
netbox/ipam/formfields.py

@@ -1,9 +1,8 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from netaddr import IPNetwork, AddrFormatError
-
 from django import forms
 from django import forms
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
+from netaddr import IPNetwork, AddrFormatError
 
 
 
 
 #
 #

+ 8 - 6
netbox/ipam/forms.py

@@ -14,11 +14,8 @@ from utilities.forms import (
     add_blank_choice,
     add_blank_choice,
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
-from .models import (
-    Aggregate, IPAddress, IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role,
-    Service, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
-)
-
+from .constants import IPADDRESS_ROLE_CHOICES, IPADDRESS_STATUS_CHOICES, PREFIX_STATUS_CHOICES, VLAN_STATUS_CHOICES
+from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 IP_FAMILY_CHOICES = [
 IP_FAMILY_CHOICES = [
     ('', 'All'),
     ('', 'All'),
@@ -362,7 +359,7 @@ def prefix_status_choices():
 class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
 class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Prefix
     model = Prefix
     q = forms.CharField(required=False, label='Search')
     q = forms.CharField(required=False, label='Search')
-    parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={
+    within_include = forms.CharField(required=False, label='Search within', widget=forms.TextInput(attrs={
         'placeholder': 'Prefix',
         'placeholder': 'Prefix',
     }))
     }))
     family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
     family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
@@ -691,6 +688,11 @@ class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
         nullable_fields = ['vrf', 'role', 'tenant', 'description']
         nullable_fields = ['vrf', 'role', 'tenant', 'description']
 
 
 
 
+class IPAddressAssignForm(BootstrapMixin, forms.Form):
+    vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
+    address = forms.CharField(label='IP Address')
+
+
 def ipaddress_status_choices():
 def ipaddress_status_choices():
     status_counts = {}
     status_counts = {}
     for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
     for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):

+ 6 - 6
netbox/ipam/models.py

@@ -1,9 +1,8 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-import netaddr
 
 
+import netaddr
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.contenttypes.fields import GenericRelation
-from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
@@ -286,7 +285,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
         """
         """
         Return all IPAddresses within this Prefix.
         Return all IPAddresses within this Prefix.
         """
         """
-        return IPAddress.objects.filter(address__net_contained_or_equal=str(self.prefix), vrf=self.vrf)
+        return IPAddress.objects.filter(address__net_host_contained=str(self.prefix), vrf=self.vrf)
 
 
     def get_available_ips(self):
     def get_available_ips(self):
         """
         """
@@ -315,9 +314,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
             child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
             child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
             return int(float(child_prefixes.size) / self.prefix.size * 100)
             return int(float(child_prefixes.size) / self.prefix.size * 100)
         else:
         else:
-            child_count = IPAddress.objects.filter(
-                address__net_contained_or_equal=str(self.prefix), vrf=self.vrf
-            ).count()
+            child_count = self.get_child_ips().count()
             prefix_size = self.prefix.size
             prefix_size = self.prefix.size
             if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
             if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
                 prefix_size -= 2
                 prefix_size -= 2
@@ -461,6 +458,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     def get_status_class(self):
     def get_status_class(self):
         return STATUS_CHOICE_CLASSES[self.status]
         return STATUS_CHOICE_CLASSES[self.status]
 
 
+    def get_role_class(self):
+        return ROLE_CHOICE_CLASSES[self.role]
+
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible
 class VLANGroup(models.Model):
 class VLANGroup(models.Model):

+ 17 - 2
netbox/ipam/tables.py

@@ -6,7 +6,6 @@ from django_tables2.utils import Accessor
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 
 
-
 RIR_UTILIZATION = """
 RIR_UTILIZATION = """
 <div class="progress">
 <div class="progress">
     {% if record.stats.total %}
     {% if record.stats.total %}
@@ -77,6 +76,10 @@ IPADDRESS_LINK = """
 {% endif %}
 {% endif %}
 """
 """
 
 
+IPADDRESS_ASSIGN_LINK = """
+<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
+"""
+
 IPADDRESS_PARENT = """
 IPADDRESS_PARENT = """
 {% if record.interface %}
 {% if record.interface %}
     <a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
     <a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
@@ -269,8 +272,8 @@ class PrefixDetailTable(PrefixTable):
 class IPAddressTable(BaseTable):
 class IPAddressTable(BaseTable):
     pk = ToggleColumn()
     pk = ToggleColumn()
     address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
     address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
-    status = tables.TemplateColumn(STATUS_LABEL)
     vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
     vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
+    status = tables.TemplateColumn(STATUS_LABEL)
     tenant = tables.TemplateColumn(TENANT_LINK)
     tenant = tables.TemplateColumn(TENANT_LINK)
     parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
     parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
     interface = tables.Column(orderable=False)
     interface = tables.Column(orderable=False)
@@ -294,6 +297,18 @@ class IPAddressDetailTable(IPAddressTable):
         )
         )
 
 
 
 
+class IPAddressAssignTable(BaseTable):
+    address = tables.TemplateColumn(IPADDRESS_ASSIGN_LINK, verbose_name='IP Address')
+    status = tables.TemplateColumn(STATUS_LABEL)
+    parent = tables.TemplateColumn(IPADDRESS_PARENT, orderable=False)
+    interface = tables.Column(orderable=False)
+
+    class Meta(BaseTable.Meta):
+        model = IPAddress
+        fields = ('address', 'vrf', 'status', 'role', 'tenant', 'parent', 'interface')
+        orderable = False
+
+
 #
 #
 # VLAN groups
 # VLAN groups
 #
 #

+ 4 - 6
netbox/ipam/tests/test_api.py

@@ -1,16 +1,14 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
+from django.contrib.auth.models import User
+from django.urls import reverse
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 from rest_framework import status
 from rest_framework import status
 from rest_framework.test import APITestCase
 from rest_framework.test import APITestCase
 
 
-from django.contrib.auth.models import User
-from django.urls import reverse
-
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
-from ipam.models import (
-    Aggregate, IPAddress, IP_PROTOCOL_TCP, IP_PROTOCOL_UDP, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF,
-)
+from ipam.constants import IP_PROTOCOL_TCP, IP_PROTOCOL_UDP
+from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from users.models import Token
 from users.models import Token
 from utilities.tests import HttpStatusMixin
 from utilities.tests import HttpStatusMixin
 
 

+ 0 - 1
netbox/ipam/tests/test_models.py

@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import netaddr
 import netaddr
-
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.test import TestCase, override_settings
 from django.test import TestCase, override_settings
 
 

+ 1 - 0
netbox/ipam/urls.py

@@ -60,6 +60,7 @@ urlpatterns = [
     url(r'^ip-addresses/import/$', views.IPAddressBulkImportView.as_view(), name='ipaddress_import'),
     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/edit/$', views.IPAddressBulkEditView.as_view(), name='ipaddress_bulk_edit'),
     url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
     url(r'^ip-addresses/delete/$', views.IPAddressBulkDeleteView.as_view(), name='ipaddress_bulk_delete'),
+    url(r'^ip-addresses/assign/$', views.IPAddressAssignView.as_view(), name='ipaddress_assign'),
     url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
     url(r'^ip-addresses/(?P<pk>\d+)/$', views.IPAddressView.as_view(), name='ipaddress'),
     url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
     url(r'^ip-addresses/(?P<pk>\d+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
     url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),
     url(r'^ip-addresses/(?P<pk>\d+)/delete/$', views.IPAddressDeleteView.as_view(), name='ipaddress_delete'),

+ 53 - 15
netbox/ipam/views.py

@@ -1,14 +1,13 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django_tables2 import RequestConfig
 import netaddr
 import netaddr
-
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.contrib.auth.mixins import PermissionRequiredMixin
 from django.db.models import Count, Q
 from django.db.models import Count, Q
-from django.shortcuts import get_object_or_404, render
+from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 from django.views.generic import View
 from django.views.generic import View
+from django_tables2 import RequestConfig
 
 
 from dcim.models import Device, Interface
 from dcim.models import Device, Interface
 from utilities.paginator import EnhancedPaginator
 from utilities.paginator import EnhancedPaginator
@@ -17,11 +16,8 @@ from utilities.views import (
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
 from . import filters, forms, tables
 from . import filters, forms, tables
-from .constants import IPADDRESS_ROLE_ANYCAST
-from .models import (
-    Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
-    Service, VLAN, VLANGroup, VRF,
-)
+from .constants import IPADDRESS_ROLE_ANYCAST, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED
+from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 
 
 def add_available_prefixes(parent, prefix_list):
 def add_available_prefixes(parent, prefix_list):
@@ -459,9 +455,7 @@ class PrefixView(View):
             aggregate = None
             aggregate = None
 
 
         # Count child IP addresses
         # Count child IP addresses
-        ipaddress_count = IPAddress.objects.filter(
-            vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
-        ).count()
+        ipaddress_count = prefix.get_child_ips().count()
 
 
         # Parent prefixes table
         # Parent prefixes table
         parent_prefixes = Prefix.objects.filter(
         parent_prefixes = Prefix.objects.filter(
@@ -517,6 +511,7 @@ class PrefixView(View):
             'parent_prefix_table': parent_prefix_table,
             'parent_prefix_table': parent_prefix_table,
             'child_prefix_table': child_prefix_table,
             'child_prefix_table': child_prefix_table,
             'duplicate_prefix_table': duplicate_prefix_table,
             'duplicate_prefix_table': duplicate_prefix_table,
+            'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf or '0', prefix.prefix),
             'permissions': permissions,
             'permissions': permissions,
             'return_url': prefix.get_absolute_url(),
             'return_url': prefix.get_absolute_url(),
         })
         })
@@ -529,9 +524,7 @@ class PrefixIPAddressesView(View):
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
 
 
         # Find all IPAddresses belonging to this Prefix
         # Find all IPAddresses belonging to this Prefix
-        ipaddresses = IPAddress.objects.filter(
-            vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix)
-        ).select_related(
+        ipaddresses = prefix.get_child_ips().select_related(
             'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
             'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
         )
         )
         ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
         ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
@@ -557,7 +550,7 @@ class PrefixIPAddressesView(View):
             'prefix': prefix,
             'prefix': prefix,
             'ip_table': ip_table,
             'ip_table': ip_table,
             'permissions': permissions,
             'permissions': permissions,
-            'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf or '0', prefix.prefix),
+            'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
         })
         })
 
 
 
 
@@ -693,6 +686,51 @@ class IPAddressEditView(IPAddressCreateView):
     permission_required = 'ipam.change_ipaddress'
     permission_required = 'ipam.change_ipaddress'
 
 
 
 
+class IPAddressAssignView(PermissionRequiredMixin, View):
+    """
+    Search for IPAddresses to be assigned to an Interface.
+    """
+    permission_required = 'ipam.change_ipaddress'
+
+    def dispatch(self, request, *args, **kwargs):
+
+        # Redirect user if an interface has not been provided
+        if 'interface' not in request.GET:
+            return redirect('ipam:ipaddress_add')
+
+        return super(IPAddressAssignView, self).dispatch(request, *args, **kwargs)
+
+    def get(self, request):
+
+        form = forms.IPAddressAssignForm()
+
+        return render(request, 'ipam/ipaddress_assign.html', {
+            'form': form,
+            'return_url': request.GET.get('return_url', ''),
+        })
+
+    def post(self, request):
+
+        form = forms.IPAddressAssignForm(request.POST)
+        table = None
+
+        if form.is_valid():
+
+            queryset = IPAddress.objects.select_related(
+                'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
+            ).filter(
+                vrf=form.cleaned_data['vrf'],
+                address__net_host=form.cleaned_data['address'],
+            )
+            table = tables.IPAddressAssignTable(queryset)
+
+        return render(request, 'ipam/ipaddress_assign.html', {
+            'form': form,
+            'table': table,
+            'return_url': request.GET.get('return_url', ''),
+        })
+
+
 class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
 class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
     permission_required = 'ipam.delete_ipaddress'
     permission_required = 'ipam.delete_ipaddress'
     model = IPAddress
     model = IPAddress

+ 0 - 1
netbox/netbox/forms.py

@@ -4,7 +4,6 @@ from django import forms
 
 
 from utilities.forms import BootstrapMixin
 from utilities.forms import BootstrapMixin
 
 
-
 OBJ_TYPE_CHOICES = (
 OBJ_TYPE_CHOICES = (
     ('', 'All Objects'),
     ('', 'All Objects'),
     ('Circuits', (
     ('Circuits', (

+ 2 - 1
netbox/netbox/settings.py

@@ -13,7 +13,7 @@ except ImportError:
     )
     )
 
 
 
 
-VERSION = '2.2.4'
+VERSION = '2.2.5'
 
 
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
 
@@ -148,6 +148,7 @@ MIDDLEWARE = (
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'django.middleware.security.SecurityMiddleware',
     'django.middleware.security.SecurityMiddleware',
+    'utilities.middleware.ExceptionHandlingMiddleware',
     'utilities.middleware.LoginRequiredMiddleware',
     'utilities.middleware.LoginRequiredMiddleware',
     'utilities.middleware.APIVersionMiddleware',
     'utilities.middleware.APIVersionMiddleware',
 )
 )

+ 2 - 8
netbox/netbox/urls.py

@@ -1,17 +1,14 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from rest_framework_swagger.views import get_swagger_view
-
 from django.conf import settings
 from django.conf import settings
 from django.conf.urls import include, url
 from django.conf.urls import include, url
 from django.contrib import admin
 from django.contrib import admin
 from django.views.static import serve
 from django.views.static import serve
+from rest_framework_swagger.views import get_swagger_view
 
 
-from netbox.views import APIRootView, handle_500, HomeView, SearchView, trigger_500
+from netbox.views import APIRootView, HomeView, SearchView
 from users.views import LoginView, LogoutView
 from users.views import LoginView, LogoutView
 
 
-
-handler500 = handle_500
 swagger_view = get_swagger_view(title='NetBox API')
 swagger_view = get_swagger_view(title='NetBox API')
 
 
 _patterns = [
 _patterns = [
@@ -48,9 +45,6 @@ _patterns = [
     # Serving static media in Django to pipe it through LoginRequiredMiddleware
     # Serving static media in Django to pipe it through LoginRequiredMiddleware
     url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
     url(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
 
 
-    # Error testing
-    url(r'^500/$', trigger_500),
-
     # Admin
     # Admin
     url(r'^admin/', admin.site.urls),
     url(r'^admin/', admin.site.urls),
 
 

+ 4 - 26
netbox/netbox/views.py

@@ -1,13 +1,12 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-from collections import OrderedDict
-import sys
 
 
-from rest_framework.views import APIView
-from rest_framework.response import Response
-from rest_framework.reverse import reverse
+from collections import OrderedDict
 
 
 from django.shortcuts import render
 from django.shortcuts import render
 from django.views.generic import View
 from django.views.generic import View
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework.views import APIView
 
 
 from circuits.filters import CircuitFilter, ProviderFilter
 from circuits.filters import CircuitFilter, ProviderFilter
 from circuits.models import Circuit, Provider
 from circuits.models import Circuit, Provider
@@ -30,7 +29,6 @@ from virtualization.models import Cluster, VirtualMachine
 from virtualization.tables import ClusterTable, VirtualMachineDetailTable
 from virtualization.tables import ClusterTable, VirtualMachineDetailTable
 from .forms import SearchForm
 from .forms import SearchForm
 
 
-
 SEARCH_MAX_RESULTS = 15
 SEARCH_MAX_RESULTS = 15
 SEARCH_TYPES = OrderedDict((
 SEARCH_TYPES = OrderedDict((
     # Circuits
     # Circuits
@@ -247,23 +245,3 @@ class APIRootView(APIView):
             ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
             ('tenancy', reverse('tenancy-api:api-root', request=request, format=format)),
             ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
             ('virtualization', reverse('virtualization-api:api-root', request=request, format=format)),
         )))
         )))
-
-
-def handle_500(request):
-    """
-    Custom server error handler
-    """
-    type_, error, traceback = sys.exc_info()
-    return render(request, '500.html', {
-        'exception': str(type_),
-        'error': error,
-    }, status=500)
-
-
-def trigger_500(request):
-    """
-    Hot-wired method of triggering a server error to test reporting
-    """
-    raise Exception(
-        "Congratulations, you've triggered an exception! Go tell all your friends what an exceptional person you are."
-    )

+ 0 - 9
netbox/netbox/wsgi.py

@@ -1,12 +1,3 @@
-"""
-WSGI config for do_ipam project.
-
-It exposes the WSGI callable as a module-level variable named ``application``.
-
-For more information on this file, see
-https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
-"""
-
 import os
 import os
 
 
 from django.core.wsgi import get_wsgi_application
 from django.core.wsgi import get_wsgi_application

+ 3 - 0
netbox/project-static/css/base.css

@@ -395,6 +395,9 @@ table.reports td.stats label {
 }
 }
 
 
 /* Misc */
 /* Misc */
+.text-nowrap {
+    white-space: nowrap;
+}
 .banner-bottom {
 .banner-bottom {
     margin-bottom: 50px;
     margin-bottom: 50px;
 }
 }

+ 2 - 3
netbox/secrets/api/views.py

@@ -1,21 +1,20 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 import base64
 import base64
 
 
 from Crypto.PublicKey import RSA
 from Crypto.PublicKey import RSA
+from django.http import HttpResponseBadRequest
 from rest_framework.exceptions import ValidationError
 from rest_framework.exceptions import ValidationError
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.viewsets import ModelViewSet, ViewSet
 from rest_framework.viewsets import ModelViewSet, ViewSet
 
 
-from django.http import HttpResponseBadRequest
-
 from secrets import filters
 from secrets import filters
 from secrets.exceptions import InvalidKey
 from secrets.exceptions import InvalidKey
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from . import serializers
 from . import serializers
 
 
-
 ERR_USERKEY_MISSING = "No UserKey found for the current user."
 ERR_USERKEY_MISSING = "No UserKey found for the current user."
 ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
 ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption."
 ERR_PRIVKEY_MISSING = "Private key was not provided."
 ERR_PRIVKEY_MISSING = "Private key was not provided."

+ 1 - 2
netbox/secrets/filters.py

@@ -1,12 +1,11 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import django_filters
 import django_filters
-
 from django.db.models import Q
 from django.db.models import Q
 
 
-from .models import Secret, SecretRole
 from dcim.models import Device
 from dcim.models import Device
 from utilities.filters import NumericInFilter
 from utilities.filters import NumericInFilter
+from .models import Secret, SecretRole
 
 
 
 
 class SecretRoleFilter(django_filters.FilterSet):
 class SecretRoleFilter(django_filters.FilterSet):

+ 0 - 1
netbox/secrets/forms.py

@@ -2,7 +2,6 @@ from __future__ import unicode_literals
 
 
 from Crypto.Cipher import PKCS1_OAEP
 from Crypto.Cipher import PKCS1_OAEP
 from Crypto.PublicKey import RSA
 from Crypto.PublicKey import RSA
-
 from django import forms
 from django import forms
 from django.db.models import Count
 from django.db.models import Count
 
 

+ 4 - 1
netbox/secrets/models.py

@@ -1,10 +1,10 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 import os
 import os
 
 
 from Crypto.Cipher import AES, PKCS1_OAEP
 from Crypto.Cipher import AES, PKCS1_OAEP
 from Crypto.PublicKey import RSA
 from Crypto.PublicKey import RSA
 from Crypto.Util import strxor
 from Crypto.Util import strxor
-
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth.hashers import make_password, check_password
 from django.contrib.auth.hashers import make_password, check_password
 from django.contrib.auth.models import Group, User
 from django.contrib.auth.models import Group, User
@@ -286,6 +286,9 @@ class Secret(CreatedUpdatedModel):
         super(Secret, self).__init__(*args, **kwargs)
         super(Secret, self).__init__(*args, **kwargs)
 
 
     def __str__(self):
     def __str__(self):
+        if self.role and self.device and self.name:
+            return '{} for {} ({})'.format(self.role, self.device, self.name)
+        # Return role and device if no name is set
         if self.role and self.device:
         if self.role and self.device:
             return '{} for {}'.format(self.role, self.device)
             return '{} for {}'.format(self.role, self.device)
         return 'Secret'
         return 'Secret'

+ 0 - 2
netbox/secrets/tables.py

@@ -3,10 +3,8 @@ from __future__ import unicode_literals
 import django_tables2 as tables
 import django_tables2 as tables
 
 
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
-
 from .models import SecretRole, Secret
 from .models import SecretRole, Secret
 
 
-
 SECRETROLE_ACTIONS = """
 SECRETROLE_ACTIONS = """
 {% if perms.secrets.change_secretrole %}
 {% if perms.secrets.change_secretrole %}
     <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
     <a href="{% url 'secrets:secretrole_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>

+ 3 - 4
netbox/secrets/tests/test_api.py

@@ -1,18 +1,17 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
-import base64
 
 
-from rest_framework import status
-from rest_framework.test import APITestCase
+import base64
 
 
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.urls import reverse
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
 from users.models import Token
 from users.models import Token
 from utilities.tests import HttpStatusMixin
 from utilities.tests import HttpStatusMixin
 
 
-
 # Dummy RSA key pair for testing use only
 # Dummy RSA key pair for testing use only
 PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
 PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
 MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z
 MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z

+ 1 - 2
netbox/secrets/tests/test_models.py

@@ -1,14 +1,13 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from Crypto.PublicKey import RSA
 from Crypto.PublicKey import RSA
-
 from django.conf import settings
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
 from django.test import TestCase
 from django.test import TestCase
 
 
-from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
 from secrets.hashers import SecretValidationHasher
 from secrets.hashers import SecretValidationHasher
+from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
 
 
 
 
 class UserKeyTestCase(TestCase):
 class UserKeyTestCase(TestCase):

+ 0 - 1
netbox/secrets/urls.py

@@ -4,7 +4,6 @@ from django.conf.urls import url
 
 
 from . import views
 from . import views
 
 
-
 app_name = 'secrets'
 app_name = 'secrets'
 urlpatterns = [
 urlpatterns = [
 
 

+ 1 - 0
netbox/secrets/views.py

@@ -1,4 +1,5 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 import base64
 import base64
 
 
 from django.contrib import messages
 from django.contrib import messages

+ 13 - 6
netbox/templates/500.html

@@ -12,7 +12,7 @@
 <body>
 <body>
     <div class="container-fluid">
     <div class="container-fluid">
         <div class="row">
         <div class="row">
-            <div class="col-md-4 col-md-offset-4">
+            <div class="col-md-6 col-md-offset-3">
                 <div class="panel panel-danger" style="margin-top: 200px">
                 <div class="panel panel-danger" style="margin-top: 200px">
                     <div class="panel-heading">
                     <div class="panel-heading">
                         <strong>
                         <strong>
@@ -21,13 +21,20 @@
                         </strong>
                         </strong>
                     </div>
                     </div>
                     <div class="panel-body">
                     <div class="panel-body">
-                        <p>There was a problem with your request. This error has been logged and administrative staff have
-                        been notified. Please return to the home page and try again.</p>
-                        <p>If you are responsible for this installation, please consider
-                        <a href="https://github.com/digitalocean/netbox/issues">filing a bug report</a>. Additional
-                        information is provided below:</p>
+                        {% block message %}
+                            <p>
+                                There was a problem with your request. Please contact an administrator.
+                            </p>
+                        {% endblock %}
+                        <hr />
+                        <p>
+                            The complete exception is provided below:
+                        </p>
 <pre><strong>{{ exception }}</strong><br />
 <pre><strong>{{ exception }}</strong><br />
 {{ error }}</pre>
 {{ error }}</pre>
+                        <p>
+                            If further assistance is required, please post to the <a href="https://groups.google.com/forum/#!forum/netbox-discuss">NetBox mailing list</a>.
+                        </p>
                         <div class="text-right">
                         <div class="text-right">
                             <a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
                             <a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
                         </div>
                         </div>

+ 1 - 1
netbox/templates/circuits/circuit.html

@@ -65,7 +65,7 @@
                     <td>
                     <td>
                         {% if circuit.tenant %}
                         {% if circuit.tenant %}
                             {% if circuit.tenant.group %}
                             {% if circuit.tenant.group %}
-                                <a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group.name }}</a>
+                                <a href="{{ circuit.tenant.group.get_absolute_url }}">{{ circuit.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>
                             <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>

+ 100 - 68
netbox/templates/dcim/device.html

@@ -7,7 +7,7 @@
 {% block content %}
 {% block content %}
 {% include 'dcim/inc/device_header.html' with active_tab='info' %}
 {% include 'dcim/inc/device_header.html' with active_tab='info' %}
 <div class="row">
 <div class="row">
-	<div class="col-md-5">
+	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Device</strong>
                 <strong>Device</strong>
@@ -28,10 +28,10 @@
                     <td>
                     <td>
                         {% if device.rack %}
                         {% if device.rack %}
                             {% if device.rack.group %}
                             {% if device.rack.group %}
-                                <a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group.name }}</a>
+                                <a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
-                            <a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack.name }}</a>{% if device.rack.facility_id %} ({{ device.rack.facility_id }}){% endif %}
+                            <a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -42,7 +42,7 @@
                     <td>
                     <td>
                         {% if device.parent_bay %}
                         {% if device.parent_bay %}
                             {% with device.parent_bay.device as parent %}
                             {% with device.parent_bay.device as parent %}
-                                <a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay.name }}
+                                <a href="{{ parent.get_absolute_url }}">{{ parent }}</a> <i class="fa fa-angle-right"></i> {{ device.parent_bay }}
                                 {% if parent.position %}
                                 {% if parent.position %}
                                     (U{{ parent.position }} / {{ parent.get_face_display }})
                                     (U{{ parent.position }} / {{ parent.get_face_display }})
                                 {% endif %}
                                 {% endif %}
@@ -61,7 +61,7 @@
                     <td>
                     <td>
                         {% if device.tenant %}
                         {% if device.tenant %}
                             {% if device.tenant.group %}
                             {% if device.tenant.group %}
-                                <a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group.name }}</a>
+                                <a href="{{ device.tenant.group.get_absolute_url }}">{{ device.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
                             <a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
@@ -172,58 +172,20 @@
         {% with device.get_custom_fields as custom_fields %}
         {% with device.get_custom_fields as custom_fields %}
             {% include 'inc/custom_fields_panel.html' %}
             {% include 'inc/custom_fields_panel.html' %}
         {% endwith %}
         {% endwith %}
-        {% if request.user.is_authenticated %}
-            <div class="panel panel-default">
-                <div class="panel-heading">
-                    <strong>Secrets</strong>
-                </div>
-                {% if secrets %}
-                    <table class="table table-hover panel-body">
-                        {% for secret in secrets %}
-                            {% include 'secrets/inc/secret_tr.html' %}
-                        {% endfor %}
-                    </table>
-                {% else %}
-                    <div class="panel-body text-muted">
-                        None found
-                    </div>
-                {% endif %}
-                {% if perms.secrets.add_secret %}
-                    <form id="secret_form">
-                        {% csrf_token %}
-                    </form>
-                    <div class="panel-footer text-right">
-                        <a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary">
-                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
-                            Add secret
-                        </a>
-                    </div>
-                {% endif %}
-            </div>
-        {% endif %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
-                <strong>Services</strong>
+                <strong>Comments</strong>
+            </div>
+            <div class="panel-body">
+                {% if device.comments %}
+                    {{ device.comments|gfm }}
+                {% else %}
+                    <span class="text-muted">None</span>
+                {% endif %}
             </div>
             </div>
-            {% if services %}
-                <table class="table table-hover panel-body">
-                    {% for service in services %}
-                        {% include 'ipam/inc/service.html' %}
-                    {% endfor %}
-                </table>
-            {% else %}
-                <div class="panel-body text-muted">
-                    None
-                </div>
-            {% endif %}
-            {% if perms.ipam.add_service %}
-                <div class="panel-footer text-right">
-                    <a href="{% url 'dcim:device_service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
-                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
-                    </a>
-                </div>
-            {% endif %}
         </div>
         </div>
+    </div>
+    <div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Console / Power</strong>
                 <strong>Console / Power</strong>
@@ -273,17 +235,57 @@
                 </div>
                 </div>
             {% endif %}
             {% endif %}
         </div>
         </div>
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Comments</strong>
-            </div>
-            <div class="panel-body">
-                {% if device.comments %}
-                    {{ device.comments|gfm }}
+        {% if request.user.is_authenticated %}
+            <div class="panel panel-default">
+                <div class="panel-heading">
+                    <strong>Secrets</strong>
+                </div>
+                {% if secrets %}
+                    <table class="table table-hover panel-body">
+                        {% for secret in secrets %}
+                            {% include 'secrets/inc/secret_tr.html' %}
+                        {% endfor %}
+                    </table>
                 {% else %}
                 {% else %}
-                    <span class="text-muted">None</span>
+                    <div class="panel-body text-muted">
+                        None found
+                    </div>
                 {% endif %}
                 {% endif %}
+                {% if perms.secrets.add_secret %}
+                    <form id="secret_form">
+                        {% csrf_token %}
+                    </form>
+                    <div class="panel-footer text-right">
+                        <a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary">
+                            <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
+                            Add secret
+                        </a>
+                    </div>
+                {% endif %}
+            </div>
+        {% endif %}
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Services</strong>
             </div>
             </div>
+            {% if services %}
+                <table class="table table-hover panel-body">
+                    {% for service in services %}
+                        {% include 'ipam/inc/service.html' %}
+                    {% endfor %}
+                </table>
+            {% else %}
+                <div class="panel-body text-muted">
+                    None
+                </div>
+            {% endif %}
+            {% if perms.ipam.add_service %}
+                <div class="panel-footer text-right">
+                    <a href="{% url 'dcim:device_service_assign' device=device.pk %}" class="btn btn-xs btn-primary">
+                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign service
+                    </a>
+                </div>
+            {% endif %}
         </div>
         </div>
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
@@ -326,7 +328,9 @@
             {% endif %}
             {% endif %}
         </div>
         </div>
 	</div>
 	</div>
-	<div class="col-md-7">
+</div>
+<div class="row">
+	<div class="col-md-12">
         {% if device_bays or device.device_type.is_parent_device %}
         {% if device_bays or device.device_type.is_parent_device %}
             {% if perms.dcim.delete_devicebay %}
             {% if perms.dcim.delete_devicebay %}
                 <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
                 <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
@@ -350,7 +354,7 @@
                 </div>
                 </div>
                 <table class="table table-hover panel-body component-list">
                 <table class="table table-hover panel-body component-list">
                     {% for devicebay in device_bays %}
                     {% for devicebay in device_bays %}
-                        {% include 'dcim/inc/devicebay.html' with selectable=True %}
+                        {% include 'dcim/inc/devicebay.html' %}
                     {% empty %}
                     {% empty %}
                         <tr>
                         <tr>
                             <td colspan="4">No device bays defined</td>
                             <td colspan="4">No device bays defined</td>
@@ -405,11 +409,23 @@
                     </div>
                     </div>
                 </div>
                 </div>
                 <table id="interfaces_table" class="table table-hover panel-body component-list">
                 <table id="interfaces_table" class="table table-hover panel-body component-list">
+                    <tr class="table-headings">
+                        {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
+                            <th></th>
+                        {% endif %}
+                        <th>Name</th>
+                        <th>LAG</th>
+                        <th>Description</th>
+                        <th>MTU</th>
+                        <th>MAC Address</th>
+                        <th colspan="2">Connection</th>
+                        <th></th>
+                    </tr>
                     {% for iface in interfaces %}
                     {% for iface in interfaces %}
-                        {% include 'dcim/inc/interface.html' with selectable=True %}
+                        {% include 'dcim/inc/interface.html' %}
                     {% empty %}
                     {% empty %}
                         <tr>
                         <tr>
-                            <td colspan="4">No interfaces defined</td>
+                            <td colspan="8">No interfaces defined</td>
                         </tr>
                         </tr>
                     {% endfor %}
                     {% endfor %}
                 </table>
                 </table>
@@ -467,8 +483,16 @@
                     </div>
                     </div>
                 </div>
                 </div>
                 <table class="table table-hover panel-body component-list">
                 <table class="table table-hover panel-body component-list">
+                    <tr class="table-headings">
+                        {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
+                            <th></th>
+                        {% endif %}
+                        <th>Name</th>
+                        <th colspan="2">Connection</th>
+                        <th></th>
+                    </tr>
                     {% for csp in cs_ports %}
                     {% for csp in cs_ports %}
-                        {% include 'dcim/inc/consoleserverport.html' with selectable=True %}
+                        {% include 'dcim/inc/consoleserverport.html' %}
                     {% empty %}
                     {% empty %}
                         <tr>
                         <tr>
                             <td colspan="4">No console server ports defined</td>
                             <td colspan="4">No console server ports defined</td>
@@ -524,12 +548,20 @@
                     </div>
                     </div>
                 </div>
                 </div>
                 <table class="table table-hover panel-body component-list">
                 <table class="table table-hover panel-body component-list">
+                    <tr class="table-headings">
+                        {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
+                            <th></th>
+                        {% endif %}
+                        <th>Name</th>
+                        <th colspan="2">Connection</th>
+                        <th></th>
+                    </tr>
                     {% for po in power_outlets %}
                     {% for po in power_outlets %}
-                        {% include 'dcim/inc/poweroutlet.html' with selectable=True %}
+                        {% include 'dcim/inc/poweroutlet.html' %}
                     {% empty %}
                     {% empty %}
                         <tr>
                         <tr>
                             <td colspan="4">No power outlets defined</td>
                             <td colspan="4">No power outlets defined</td>
-                        </tr>
+                        </tr> text-nowrap
                     {% endfor %}
                     {% endfor %}
                 </table>
                 </table>
                 {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}
                 {% if perms.dcim.add_poweroutlet or perms.dcim.delete_poweroutlet %}

+ 1 - 1
netbox/templates/dcim/devicebay_populate.html

@@ -26,7 +26,7 @@
                     <div class="form-group">
                     <div class="form-group">
                         <label class="col-md-3 control-label required">Bay</label>
                         <label class="col-md-3 control-label required">Bay</label>
                         <div class="col-md-9">
                         <div class="col-md-9">
-                            <p class="form-control-static">{{ device_bay.name }}</p>
+                            <p class="form-control-static">{{ device_bay }}</p>
                         </div>
                         </div>
                     </div>
                     </div>
                     {% render_form form %}
                     {% render_form form %}

+ 2 - 2
netbox/templates/dcim/inc/consoleport.html

@@ -1,13 +1,13 @@
 <tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
 <tr class="consoleport{% if cp.cs_port and not cp.connection_status %} info{% endif %}">
     <td>
     <td>
-        <i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
+        <i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
     </td>
     </td>
     {% if cp.cs_port %}
     {% if cp.cs_port %}
         <td>
         <td>
             <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
             <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
         </td>
         </td>
         <td>
         <td>
-            {{ cp.cs_port.name }}
+            {{ cp.cs_port }}
         </td>
         </td>
     {% else %}
     {% else %}
         <td colspan="2">
         <td colspan="2">

+ 3 - 4
netbox/templates/dcim/inc/consoleserverport.html

@@ -1,5 +1,5 @@
 <tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
 <tr class="consoleserverport{% if csp.connected_console and not csp.connected_console.connection_status %} info{% endif %}">
-    {% if selectable and perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
+    {% if perms.dcim.change_consoleserverport or perms.dcim.delete_consoleserverport %}
         <td class="pk">
         <td class="pk">
             <input name="pk" type="checkbox" value="{{ csp.pk }}" />
             <input name="pk" type="checkbox" value="{{ csp.pk }}" />
         </td>
         </td>
@@ -7,20 +7,19 @@
     <td>
     <td>
         <i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
         <i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
     </td>
     </td>
-    <td></td>
     {% if csp.connected_console %}
     {% if csp.connected_console %}
         <td>
         <td>
             <a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
             <a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
         </td>
         </td>
         <td>
         <td>
-            {{ csp.connected_console.name }}
+            {{ csp.connected_console }}
         </td>
         </td>
     {% else %}
     {% else %}
         <td colspan="2">
         <td colspan="2">
             <span class="text-muted">Not connected</span>
             <span class="text-muted">Not connected</span>
         </td>
         </td>
     {% endif %}
     {% endif %}
-    <td colspan="2" class="text-right">
+    <td class="text-right">
         {% if perms.dcim.change_consoleserverport %}
         {% if perms.dcim.change_consoleserverport %}
             {% if csp.connected_console %}
             {% if csp.connected_console %}
                 {% if csp.connected_console.connection_status %}
                 {% if csp.connected_console.connection_status %}

+ 1 - 1
netbox/templates/dcim/inc/device_header.html

@@ -8,7 +8,7 @@
         {% endif %}
         {% endif %}
         {% if device.parent_bay %}
         {% if device.parent_bay %}
             <li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
             <li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
-            <li>{{ device.parent_bay.name }}</li>
+            <li>{{ device.parent_bay }}</li>
         {% endif %}
         {% endif %}
         <li>{{ device }}</li>
         <li>{{ device }}</li>
     </ol>
     </ol>

+ 2 - 2
netbox/templates/dcim/inc/devicebay.html

@@ -1,11 +1,11 @@
 <tr class="devicebay">
 <tr class="devicebay">
-    {% if selectable and perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
+    {% if perms.dcim.change_devicebay or perms.dcim.delete_devicebay %}
         <td class="pk">
         <td class="pk">
             <input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
             <input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
         </td>
         </td>
     {% endif %}
     {% endif %}
     <td>
     <td>
-        <i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay.name }}
+        <i class="fa fa-fw fa-{% if devicebay.installed_device %}dot-circle-o{% else %}circle-o{% endif %}"></i> {{ devicebay }}
     </td>
     </td>
     {% if devicebay.installed_device %}
     {% if devicebay.installed_device %}
         <td>
         <td>

+ 80 - 61
netbox/templates/dcim/inc/interface.html

@@ -1,21 +1,37 @@
-<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}">
-    {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
+<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% endif %}" id="iface_{{ iface.name }}">
+
+    {# Checkbox #}
+    {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
         <td class="pk">
         <td class="pk">
             <input name="pk" type="checkbox" value="{{ iface.pk }}" />
             <input name="pk" type="checkbox" value="{{ iface.pk }}" />
         </td>
         </td>
     {% endif %}
     {% endif %}
+
+    {# Icon and name #}
+    <td>
+        <span title="{{ iface.get_form_factor_display }}">
+            <i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_lag %}align-justify{% elif iface.is_virtual %}circle{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
+            {{ iface }}
+        </span>
+    </td>
+
+    {# LAG #}
     <td>
     <td>
-        <i class="fa fa-fw fa-{% if iface.mgmt_only %}wrench{% elif iface.is_virtual %}square{% elif iface.is_wireless %}wifi{% else %}exchange{% endif %}"></i>
-        <span title="{{ iface.get_form_factor_display }}">{{ iface.name }}</span>
         {% if iface.lag %}
         {% if iface.lag %}
-            <span class="label label-primary">{{ iface.lag.name }}</span>
-        {% endif %}
-        {% if iface.description %}
-            <i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
+            <a href="#iface_{{ iface.lag }}" class="label label-default">{{ iface.lag }}</a>
         {% endif %}
         {% endif %}
     </td>
     </td>
+
+    {# Description #}
+    <td>{{ iface.description }}</td>
+
+    {# MTU #}
     <td>{{ iface.mtu|default:"" }}</td>
     <td>{{ iface.mtu|default:"" }}</td>
+
+    {# MAC address #}
     <td>{{ iface.mac_address|default:"" }}</td>
     <td>{{ iface.mac_address|default:"" }}</td>
+
+    {# Connection or type #}
     {% if iface.is_lag %}
     {% if iface.is_lag %}
         <td colspan="2" class="text-muted">
         <td colspan="2" class="text-muted">
             LAG interface<br />
             LAG interface<br />
@@ -55,7 +71,9 @@
             <span class="text-muted">Not connected</span>
             <span class="text-muted">Not connected</span>
         </td>
         </td>
     {% endif %}
     {% endif %}
-    <td colspan="2" class="text-right">
+
+    {# Buttons #}
+    <td class="text-right text-nowrap">
         {% if show_graphs %}
         {% if show_graphs %}
             {% if iface.circuit_termination or iface.connection %}
             {% if iface.circuit_termination or iface.connection %}
                 <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
                 <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
@@ -96,7 +114,7 @@
                     </a>
                     </a>
                 {% endif %}
                 {% endif %}
             {% endif %}
             {% endif %}
-            <a href="{% url 'dcim:interface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface">
+            <a href="{% if iface.device %}{% url 'dcim:interface_edit' pk=iface.pk %}{% else %}{% url 'virtualization:interface_edit' pk=iface.pk %}{% endif %}" class="btn btn-info btn-xs" title="Edit interface">
                 <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
                 <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
             </a>
             </a>
         {% endif %}
         {% endif %}
@@ -106,62 +124,63 @@
                     <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
                     <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
                 </button>
                 </button>
             {% else %}
             {% else %}
-                <a href="{% url 'dcim:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
+                <a href="{% if iface.device %}{% url 'dcim:interface_delete' pk=iface.pk %}{% else %}{% url 'virtualization:interface_delete' pk=iface.pk %}{% endif %}" class="btn btn-danger btn-xs" title="Delete interface">
                     <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
                     <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
                 </a>
                 </a>
             {% endif %}
             {% endif %}
         {% endif %}
         {% endif %}
     </td>
     </td>
 </tr>
 </tr>
-{% with iface.ip_addresses.all as ipaddresses %}
-    {% if ipaddresses %}
-        <tr class="ipaddress">
-            {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
-                <td></td>
-                <td colspan="6" class="subtable">
+{% for ip in iface.ip_addresses.all %}
+    <tr class="ipaddress">
+
+        {# Placeholder #}
+        {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
+            <td></td>
+        {% endif %}
+
+        {# IP address #}
+        <td colspan="2">
+            <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
+        </td>
+
+        {# Primary, status, role #}
+        <td>
+            {% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
+                <span class="label label-success">Primary</span>
+            {% endif %}
+            <span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
+            {% if ip.role %}
+                <span class="label label-{{ ip.get_role_class }}">{{ ip.get_role_display }}</span>
+            {% endif %}
+        </td>
+
+        {# VRF #}
+        <td colspan="2">
+            {% if ip.vrf %}
+                <a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}" title="{{ ip.vrf.rd }}">{{ ip.vrf.name }}</a>
             {% else %}
             {% else %}
-                <td colspan="7" class="subtable">
+                <span class="text-muted">Global</span>
             {% endif %}
             {% endif %}
-            <table class="table table-hover">
-                {% for ip in ipaddresses %}
-                    <tr>
-                        <td>
-                            <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
-                            {% if ip.description %}
-                                <i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
-                            {% endif %}
-                        </td>
-                        <td>
-                            {% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
-                                <span class="label label-success">Primary</span>
-                            {% endif %}
-                        </td>
-                        <td>
-                            {% if ip.vrf %}
-                                <a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
-                            {% else %}
-                                <span class="text-muted">Global table</span>
-                            {% endif %}
-                        </td>
-                        <td>
-                            <span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
-                        </td>
-                        <td class="text-right">
-                            {% if perms.ipam.change_ipaddress %}
-                                <a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
-                                    <i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
-                                </a>
-                            {% endif %}
-                            {% if perms.ipam.delete_ipaddress %}
-                                <a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
-                                    <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
-                                </a>
-                            {% endif %}
-                        </td>
-                    </tr>
-                {% endfor %}
-            </table>
-            </td>
-        </tr>
-    {% endif %}
-{% endwith %}
+        </td>
+
+        {# Description #}
+        <td colspan="2">
+            {{ ip.description }}
+        </td>
+
+        {# Buttons #}
+        <td class="text-right text-nowrap">
+            {% if perms.ipam.change_ipaddress %}
+                <a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
+                    <i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
+                </a>
+            {% endif %}
+            {% if perms.ipam.delete_ipaddress %}
+                <a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
+                    <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
+                </a>
+            {% endif %}
+        </td>
+    </tr>
+{% endfor %}

+ 1 - 1
netbox/templates/dcim/inc/inventoryitem.html

@@ -1,5 +1,5 @@
 <tr>
 <tr>
-    <td style="padding-left: {{ indent|add:5 }}px">{{ item.name }}</td>
+    <td style="padding-left: {{ indent|add:5 }}px">{{ item }}</td>
     <td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
     <td>{% if not item.discovered %}<i class="fa fa-asterisk" title="Manually created"></i>{% endif %}</td>
     <td>{{ item.manufacturer|default:"" }}</td>
     <td>{{ item.manufacturer|default:"" }}</td>
     <td>{{ item.part_id }}</td>
     <td>{{ item.part_id }}</td>

+ 4 - 5
netbox/templates/dcim/inc/poweroutlet.html

@@ -1,26 +1,25 @@
 <tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
 <tr class="poweroutlet{% if po.connected_port and not po.connected_port.connection_status %} info{% endif %}">
-    {% if selectable and perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
+    {% if perms.dcim.change_poweroutlet or perms.dcim.delete_poweroutlet %}
         <td class="pk">
         <td class="pk">
             <input name="pk" type="checkbox" value="{{ po.pk }}" />
             <input name="pk" type="checkbox" value="{{ po.pk }}" />
         </td>
         </td>
     {% endif %}
     {% endif %}
     <td>
     <td>
-        <i class="fa fa-fw fa-bolt"></i> {{ po.name }}
+        <i class="fa fa-fw fa-bolt"></i> {{ po }}
     </td>
     </td>
-    <td></td>
     {% if po.connected_port %}
     {% if po.connected_port %}
         <td>
         <td>
             <a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
             <a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
         </td>
         </td>
         <td>
         <td>
-            {{ po.connected_port.name }}
+            {{ po.connected_port }}
         </td>
         </td>
     {% else %}
     {% else %}
         <td colspan="2">
         <td colspan="2">
             <span class="text-muted">Not connected</span>
             <span class="text-muted">Not connected</span>
         </td>
         </td>
     {% endif %}
     {% endif %}
-    <td colspan="2" class="text-right">
+    <td class="text-right">
         {% if perms.dcim.change_poweroutlet %}
         {% if perms.dcim.change_poweroutlet %}
             {% if po.connected_port %}
             {% if po.connected_port %}
                 {% if po.connected_port.connection_status %}
                 {% if po.connected_port.connection_status %}

+ 2 - 2
netbox/templates/dcim/inc/powerport.html

@@ -1,13 +1,13 @@
 <tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
 <tr class="powerport{% if pp.power_outlet and not pp.connection_status %} info{% endif %}">
     <td>
     <td>
-        <i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
+        <i class="fa fa-fw fa-bolt"></i> {{ pp }}
     </td>
     </td>
     {% if pp.power_outlet %}
     {% if pp.power_outlet %}
         <td>
         <td>
             <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
             <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
         </td>
         </td>
         <td>
         <td>
-            {{ pp.power_outlet.name }}
+            {{ pp.power_outlet }}
         </td>
         </td>
     {% else %}
     {% else %}
         <td colspan="2">
         <td colspan="2">

+ 4 - 4
netbox/templates/dcim/rack.html

@@ -49,7 +49,7 @@
 		</a>
 		</a>
     {% endif %}
     {% endif %}
 </div>
 </div>
-<h1>{% block title %}Rack {{ rack.name }}{% endblock %}</h1>
+<h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=rack %}
 {% include 'inc/created_updated.html' with obj=rack %}
 <div class="row">
 <div class="row">
 	<div class="col-md-6">
 	<div class="col-md-6">
@@ -72,7 +72,7 @@
                     <td>Group</td>
                     <td>Group</td>
                     <td>
                     <td>
                         {% if rack.group %}
                         {% if rack.group %}
-                            <a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group.name }}</a>
+                            <a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -93,7 +93,7 @@
                     <td>
                     <td>
                         {% if rack.tenant %}
                         {% if rack.tenant %}
                             {% if rack.tenant.group %}
                             {% if rack.tenant.group %}
-                                <a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group.name }}</a>
+                                <a href="{{ rack.tenant.group.get_absolute_url }}">{{ rack.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
                             <a href="{{ rack.tenant.get_absolute_url }}">{{ rack.tenant }}</a>
@@ -173,7 +173,7 @@
                     {% for device in nonracked_devices %}
                     {% for device in nonracked_devices %}
                         <tr{% if device.device_type.u_height %} class="warning"{% endif %}>
                         <tr{% if device.device_type.u_height %} class="warning"{% endif %}>
                             <td>
                             <td>
-                                <a href="{% url 'dcim:device' pk=device.pk %}">{{ device.name }}</a>
+                                <a href="{% url 'dcim:device' pk=device.pk %}">{{ device }}</a>
                             </td>
                             </td>
                             <td>{{ device.device_role }}</td>
                             <td>{{ device.device_role }}</td>
                             <td>{{ device.device_type.full_name }}</td>
                             <td>{{ device.device_type.full_name }}</td>

+ 2 - 2
netbox/templates/dcim/site.html

@@ -76,7 +76,7 @@
                     <td>
                     <td>
                         {% if site.tenant %}
                         {% if site.tenant %}
                             {% if site.tenant.group %}
                             {% if site.tenant.group %}
-                                <a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group.name }}</a>
+                                <a href="{{ site.tenant.group.get_absolute_url }}">{{ site.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
                             <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
@@ -221,7 +221,7 @@
                 <table class="table table-hover panel-body">
                 <table class="table table-hover panel-body">
                     {% for rg in rack_groups %}
                     {% for rg in rack_groups %}
                         <tr>
                         <tr>
-                            <td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg.name }}</a></td>
+                            <td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
                             <td>{{ rg.rack_count }}</td>
                             <td>{{ rg.rack_count }}</td>
                         </tr>
                         </tr>
                     {% endfor %}
                     {% endfor %}

+ 18 - 0
netbox/templates/exceptions/import_error.html

@@ -0,0 +1,18 @@
+{% extends '500.html' %}
+
+{% block message %}
+    <p>
+        A module import error occurred during this request. Common causes include the following:
+    </p>
+    <p>
+        <i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be missing one or more required
+        Python packages. These packages are listed in <code>requirements.txt</code> and are normally installed as part
+        of the installation or upgrade process. To verify installed packages, run <code>pip freeze</code> from the
+        console and compare the output to the list of required packages.
+    </p>
+    <p>
+        <i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation has recently been upgraded,
+        check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is
+        running.
+    </p>
+{% endblock %}

+ 12 - 0
netbox/templates/exceptions/permission_error.html

@@ -0,0 +1,12 @@
+{% extends '500.html' %}
+
+{% block message %}
+    <p>
+        A file permission error was detected while processing this request. Common causes include the following:
+    </p>
+    <p>
+        <i class="fa fa-warning"></i> <strong>Insufficient write permission to the media root</strong> - The configured
+        media root is <code>{{ settings.MEDIA_ROOT }}</code>. Ensure that the user NetBox runs as has access to write
+        files to all locations within this path.
+    </p>
+{% endblock %}

+ 17 - 0
netbox/templates/exceptions/programming_error.html

@@ -0,0 +1,17 @@
+{% extends '500.html' %}
+
+{% block message %}
+    <p>
+        A database programming error was detected while processing this request. Common causes include the following:
+    </p>
+    <p>
+        <i class="fa fa-warning"></i> <strong>Database migrations missing</strong> - When upgrading to a new NetBox release, the upgrade script must
+        be run to apply any new database migrations. You can run migrations manually by executing
+        <code>python3 manage.py migrate</code> from the command line.
+    </p>
+    <p>
+        <i class="fa fa-warning"></i> <strong>Unsupported PostgreSQL version</strong> - Ensure that PostgreSQL version 9.4 or higher is in use. You
+        can check this by connecting to the database using NetBox's credentials and issuing a query for
+        <code>SELECT VERSION()</code>.
+    </p>
+{% endblock %}

+ 14 - 2
netbox/templates/ipam/inc/ipadress_edit_header.html

@@ -1,4 +1,16 @@
+{% load helpers %}
+
 <ul class="nav nav-tabs" style="margin-bottom: 20px">
 <ul class="nav nav-tabs" style="margin-bottom: 20px">
-    <li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_add' %}">Individual</a></li>
-    <li role="presentation"{% if active_tab == 'bulk_add' %} class="active"{% endif %}><a href="{% url 'ipam:ipaddress_bulk_add' %}">Bulk</a></li>
+    <li role="presentation"{% if active_tab == 'add' %} class="active"{% endif %}>
+        <a href="{% url 'ipam:ipaddress_add' %}{% querystring request %}">New IP</a>
+    </li>
+    {% if 'interface' in request.GET %}
+        <li role="presentation"{% if active_tab == 'assign' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:ipaddress_assign' %}{% querystring request %}">Assign IP</a>
+        </li>
+    {% else %}
+        <li role="presentation"{% if active_tab == 'bulk_add' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:ipaddress_bulk_add' %}{% querystring request %}">Bulk Create</a>
+        </li>
+    {% endif %}
 </ul>
 </ul>

+ 48 - 0
netbox/templates/ipam/ipaddress_assign.html

@@ -0,0 +1,48 @@
+{% extends 'utilities/obj_edit.html' %}
+{% load static from staticfiles %}
+{% load form_helpers %}
+{% load helpers %}
+
+{% block content %}
+    <form action="{% querystring request %}" method="post" class="form form-horizontal">
+        {% csrf_token %}
+        {% for field in form.hidden_fields %}
+            {{ field }}
+        {% endfor %}
+        <div class="row">
+            <div class="col-md-6 col-md-offset-3">
+                <h3>Assign an IP Address</h3>
+                {% include 'ipam/inc/ipadress_edit_header.html' with active_tab='assign' %}
+                {% if form.non_field_errors %}
+                    <div class="panel panel-danger">
+                        <div class="panel-heading"><strong>Errors</strong></div>
+                        <div class="panel-body">
+                            {{ form.non_field_errors }}
+                        </div>
+                    </div>
+                {% endif %}
+            <div class="panel panel-default">
+                <div class="panel-heading"><strong>Select IP Address</strong></div>
+                <div class="panel-body">
+                    {% render_field form.vrf %}
+                    {% render_field form.address %}
+                </div>
+            </div>
+            </div>
+        </div>
+        <div class="row">
+            <div class="col-md-6 col-md-offset-3 text-right">
+                <button type="submit" class="btn btn-primary">Search</button>
+                <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
+            </div>
+        </div>
+    </form>
+    {% if table %}
+        <div class="row">
+            <div class="col-md-10 col-md-offset-1" style="margin-top: 20px">
+                <h3>Search Results</h3>
+                {% include 'utilities/obj_table.html' with table_template='panel_table.html' %}
+            </div>
+        </div>
+    {% endif %}
+{% endblock %}

+ 3 - 3
netbox/templates/ipam/prefix.html

@@ -30,13 +30,13 @@
                     <td>
                     <td>
                         {% if prefix.tenant %}
                         {% if prefix.tenant %}
                             {% if prefix.tenant.group %}
                             {% if prefix.tenant.group %}
-                                <a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group.name }}</a>
+                                <a href="{{ prefix.tenant.group.get_absolute_url }}">{{ prefix.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
                             <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
                         {% elif prefix.vrf.tenant %}
                         {% elif prefix.vrf.tenant %}
                             {% if prefix.vrf.tenant.group %}
                             {% if prefix.vrf.tenant.group %}
-                                <a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group.name }}</a>
+                                <a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
                             <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
@@ -75,7 +75,7 @@
                     <td>
                     <td>
                         {% if prefix.vlan %}
                         {% if prefix.vlan %}
                             {% if prefix.vlan.group %}
                             {% if prefix.vlan.group %}
-                                <a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group.name }}</a>
+                                <a href="{{ prefix.vlan.group.get_absolute_url }}">{{ prefix.vlan.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>
                             <a href="{% url 'ipam:vlan' pk=prefix.vlan.pk %}">{{ prefix.vlan.display_name }}</a>

+ 4 - 4
netbox/templates/ipam/vlan.html

@@ -9,9 +9,9 @@
                 <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
                 <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
             {% endif %}
             {% endif %}
             {% if vlan.group %}
             {% if vlan.group %}
-                <li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
+                <li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group }}</a></li>
             {% endif %}
             {% endif %}
-            <li>{{ vlan.name }} ({{ vlan.vid }})</li>
+            <li>{{ vlan }}</li>
         </ol>
         </ol>
     </div>
     </div>
     <div class="col-sm-4 col-md-3">
     <div class="col-sm-4 col-md-3">
@@ -68,7 +68,7 @@
                     <td>Group</td>
                     <td>Group</td>
                     <td>
                     <td>
                         {% if vlan.group %}
                         {% if vlan.group %}
-                            <a href="{{ vlan.group.get_absolute_url }}">{{ vlan.group.name }}</a>
+                            <a href="{{ vlan.group.get_absolute_url }}">{{ vlan.group }}</a>
                         {% else %}
                         {% else %}
                             <span class="text-muted">None</span>
                             <span class="text-muted">None</span>
                         {% endif %}
                         {% endif %}
@@ -87,7 +87,7 @@
                     <td>
                     <td>
                         {% if vlan.tenant %}
                         {% if vlan.tenant %}
                             {% if vlan.tenant.group %}
                             {% if vlan.tenant.group %}
-                                <a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group.name }}</a>
+                                <a href="{{ vlan.tenant.group.get_absolute_url }}">{{ vlan.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>
                             <a href="{{ vlan.tenant.get_absolute_url }}">{{ vlan.tenant }}</a>

+ 0 - 70
netbox/templates/virtualization/inc/interface.html

@@ -1,70 +0,0 @@
-<tr class="interface{% if not iface.enabled %} danger{% endif %}">
-    {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
-        <td class="pk">
-            <input name="pk" type="checkbox" value="{{ iface.pk }}" />
-        </td>
-    {% endif %}
-    <td>
-        <i class="fa fa-fw fa-square"></i> <span>{{ iface.name }}</span>
-        {% if iface.description %}
-            <i class="fa fa-fw fa-comment-o" title="{{ iface.description }}"></i>
-        {% endif %}
-    </td>
-    <td>{{ iface.mtu|default:"" }}</td>
-    <td>{{ iface.mac_address|default:"" }}</td>
-    <td class="text-right">
-        {% if perms.ipam.add_ipaddress %}
-            <a href="{% url 'ipam:ipaddress_add' %}?interface={{ iface.pk }}&return_url={{ vm.get_absolute_url }}" class="btn btn-xs btn-success" title="Add IP address">
-                <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
-            </a>
-        {% endif %}
-        {% if perms.dcim.change_interface %}
-            <a href="{% url 'virtualization:interface_edit' pk=iface.pk %}" class="btn btn-info btn-xs" title="Edit interface">
-                <i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
-            </a>
-        {% endif %}
-        {% if perms.dcim.delete_interface %}
-            <a href="{% url 'virtualization:interface_delete' pk=iface.pk %}" class="btn btn-danger btn-xs" title="Delete interface">
-                <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
-            </a>
-        {% endif %}
-    </td>
-</tr>
-{% for ip in iface.ip_addresses.all %}
-    <tr class="ipaddress">
-        {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
-            <td></td>
-        {% endif %}
-        <td>
-            <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
-            {% if ip.description %}
-                <i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
-            {% endif %}
-            {% if vm.primary_ip4 == ip or vm.primary_ip6 == ip %}
-                <span class="label label-success">Primary</span>
-            {% endif %}
-        </td>
-        <td class="text-right">
-            {% if ip.vrf %}
-                <a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
-            {% else %}
-                <span class="text-muted">Global</span>
-            {% endif %}
-        </td>
-        <td>
-            <span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
-        </td>
-        <td class="text-right">
-            {% if perms.ipam.change_ipaddress %}
-                <a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ vm.get_absolute_url }}" class="btn btn-info btn-xs">
-                    <i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
-                </a>
-            {% endif %}
-            {% if perms.ipam.delete_ipaddress %}
-                <a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ vm.get_absolute_url }}" class="btn btn-danger btn-xs">
-                    <i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
-                </a>
-            {% endif %}
-        </td>
-    </tr>
-{% endfor %}

+ 35 - 19
netbox/templates/virtualization/virtualmachine.html

@@ -41,7 +41,7 @@
 <h1>{% block title %}{{ vm }}{% endblock %}</h1>
 <h1>{% block title %}{{ vm }}{% endblock %}</h1>
 {% include 'inc/created_updated.html' with obj=vm %}
 {% include 'inc/created_updated.html' with obj=vm %}
 <div class="row">
 <div class="row">
-	<div class="col-md-5">
+	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Virtual Machine</strong>
                 <strong>Virtual Machine</strong>
@@ -49,7 +49,7 @@
             <table class="table table-hover panel-body attr-table">
             <table class="table table-hover panel-body attr-table">
                 <tr>
                 <tr>
                     <td>Name</td>
                     <td>Name</td>
-                    <td>{{ vm.name }}</td>
+                    <td>{{ vm }}</td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                     <td>Status</td>
                     <td>Status</td>
@@ -82,7 +82,7 @@
                     <td>
                     <td>
                         {% if vm.tenant %}
                         {% if vm.tenant %}
                             {% if vm.tenant.group %}
                             {% if vm.tenant.group %}
-                                <a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group.name }}</a>
+                                <a href="{{ vm.tenant.group.get_absolute_url }}">{{ vm.tenant.group }}</a>
                                 <i class="fa fa-angle-right"></i>
                                 <i class="fa fa-angle-right"></i>
                             {% endif %}
                             {% endif %}
                             <a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
                             <a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
@@ -123,6 +123,21 @@
                 </tr>
                 </tr>
             </table>
             </table>
         </div>
         </div>
+        {% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                <strong>Comments</strong>
+            </div>
+            <div class="panel-body">
+                {% if vm.comments %}
+                    {{ vm.comments|gfm }}
+                {% else %}
+                    <span class="text-muted">None</span>
+                {% endif %}
+            </div>
+        </div>
+    </div>
+	<div class="col-md-6">
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading">
             <div class="panel-heading">
                 <strong>Cluster</strong>
                 <strong>Cluster</strong>
@@ -204,21 +219,10 @@
                 </div>
                 </div>
             {% endif %}
             {% endif %}
         </div>
         </div>
-        {% include 'inc/custom_fields_panel.html' with custom_fields=vm.get_custom_fields %}
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Comments</strong>
-            </div>
-            <div class="panel-body">
-                {% if vm.comments %}
-                    {{ vm.comments|gfm }}
-                {% else %}
-                    <span class="text-muted">None</span>
-                {% endif %}
-            </div>
-        </div>
     </div>
     </div>
-    <div class="col-md-7">
+</div>
+<div class="row">
+    <div class="col-md-12">
         {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
         {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
             <form method="post">
             <form method="post">
             {% csrf_token %}
             {% csrf_token %}
@@ -244,11 +248,23 @@
                 </div>
                 </div>
             </div>
             </div>
             <table id="interfaces_table" class="table table-hover panel-body component-list">
             <table id="interfaces_table" class="table table-hover panel-body component-list">
+                <tr class="table-headings">
+                    {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
+                        <th></th>
+                    {% endif %}
+                    <th>Name</th>
+                    <th>LAG</th>
+                    <th>Description</th>
+                    <th>MTU</th>
+                    <th>MAC Address</th>
+                    <th colspan="2">Connection</th>
+                    <th></th>
+                </tr>
                 {% for iface in interfaces %}
                 {% for iface in interfaces %}
-                    {% include 'virtualization/inc/interface.html' with selectable=True %}
+                    {% include 'dcim/inc/interface.html' with device=vm %}
                 {% empty %}
                 {% empty %}
                     <tr>
                     <tr>
-                        <td colspan="4">No interfaces defined</td>
+                        <td colspan="6">No interfaces defined</td>
                     </tr>
                     </tr>
                 {% endfor %}
                 {% endfor %}
             </table>
             </table>

+ 0 - 1
netbox/tenancy/filters.py

@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 import django_filters
 import django_filters
-
 from django.db.models import Q
 from django.db.models import Q
 
 
 from extras.filters import CustomFieldFilterSet
 from extras.filters import CustomFieldFilterSet

+ 0 - 2
netbox/tenancy/tables.py

@@ -3,10 +3,8 @@ from __future__ import unicode_literals
 import django_tables2 as tables
 import django_tables2 as tables
 
 
 from utilities.tables import BaseTable, ToggleColumn
 from utilities.tables import BaseTable, ToggleColumn
-
 from .models import Tenant, TenantGroup
 from .models import Tenant, TenantGroup
 
 
-
 TENANTGROUP_ACTIONS = """
 TENANTGROUP_ACTIONS = """
 {% if perms.tenancy.change_tenantgroup %}
 {% if perms.tenancy.change_tenantgroup %}
     <a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>
     <a href="{% url 'tenancy:tenantgroup_edit' slug=record.slug %}" class="btn btn-xs btn-warning"><i class="glyphicon glyphicon-pencil" aria-hidden="true"></i></a>

+ 2 - 3
netbox/tenancy/tests/test_api.py

@@ -1,10 +1,9 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from rest_framework import status
-from rest_framework.test import APITestCase
-
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.urls import reverse
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from users.models import Token
 from users.models import Token

+ 0 - 1
netbox/tenancy/urls.py

@@ -4,7 +4,6 @@ from django.conf.urls import url
 
 
 from . import views
 from . import views
 
 
-
 app_name = 'tenancy'
 app_name = 'tenancy'
 urlpatterns = [
 urlpatterns = [
 
 

+ 1 - 1
netbox/tenancy/views.py

@@ -13,8 +13,8 @@ from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 )
 from virtualization.models import VirtualMachine
 from virtualization.models import VirtualMachine
-from .models import Tenant, TenantGroup
 from . import filters, forms, tables
 from . import filters, forms, tables
+from .models import Tenant, TenantGroup
 
 
 
 
 #
 #

+ 0 - 1
netbox/users/api/serializers.py

@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
-
 from rest_framework import serializers
 from rest_framework import serializers
 
 
 
 

+ 1 - 1
netbox/users/forms.py

@@ -1,7 +1,7 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
 from django import forms
 from django import forms
+from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
 
 
 from utilities.forms import BootstrapMixin
 from utilities.forms import BootstrapMixin
 from .models import Token
 from .models import Token

+ 2 - 1
netbox/users/models.py

@@ -1,12 +1,13 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 import binascii
 import binascii
 import os
 import os
 
 
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from django.core.validators import MinLengthValidator
 from django.core.validators import MinLengthValidator
 from django.db import models
 from django.db import models
-from django.utils.encoding import python_2_unicode_compatible
 from django.utils import timezone
 from django.utils import timezone
+from django.utils.encoding import python_2_unicode_compatible
 
 
 
 
 @python_2_unicode_compatible
 @python_2_unicode_compatible

+ 0 - 1
netbox/users/urls.py

@@ -4,7 +4,6 @@ from django.conf.urls import url
 
 
 from . import views
 from . import views
 
 
-
 app_name = 'user'
 app_name = 'user'
 urlpatterns = [
 urlpatterns = [
 
 

+ 1 - 2
netbox/utilities/api.py

@@ -1,17 +1,16 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from django.conf import settings
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.http import Http404
 from django.http import Http404
-
 from rest_framework.exceptions import APIException
 from rest_framework.exceptions import APIException
 from rest_framework.permissions import BasePermission
 from rest_framework.permissions import BasePermission
 from rest_framework.response import Response
 from rest_framework.response import Response
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.viewsets import ViewSet
 from rest_framework.viewsets import ViewSet
 
 
-
 WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
 WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete']
 
 
 
 

+ 0 - 1
netbox/utilities/fields.py

@@ -5,7 +5,6 @@ from django.db import models
 
 
 from .forms import ColorSelect
 from .forms import ColorSelect
 
 
-
 validate_color = RegexValidator('^[0-9a-f]{6}$', 'Enter a valid hexadecimal RGB color code.', 'invalid')
 validate_color = RegexValidator('^[0-9a-f]{6}$', 'Enter a valid hexadecimal RGB color code.', 'invalid')
 
 
 
 

+ 1 - 1
netbox/utilities/filters.py

@@ -1,8 +1,8 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-import django_filters
 import itertools
 import itertools
 
 
+import django_filters
 from django import forms
 from django import forms
 from django.utils.encoding import force_text
 from django.utils.encoding import force_text
 
 

Некоторые файлы не были показаны из-за большого количества измененных файлов