Przeglądaj źródła

Merge pull request #1708 from digitalocean/develop

Release v2.2.5
Jeremy Stretch 8 lat temu
rodzic
commit
a5a7358d26
100 zmienionych plików z 773 dodań i 596 usunięć
  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.
 
     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
 
     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
-[ ] 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
     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
 * Python version:  <!-- Example: 3.5.4 -->
@@ -28,8 +35,8 @@
 
 <!--
     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)
 
     FEATURE REQUESTS must include:

+ 63 - 73
CONTRIBUTING.md

@@ -1,8 +1,8 @@
 ## Getting Help
 
 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
 
@@ -13,35 +13,32 @@ installation. You can find us [here](https://groups.google.com/forum/#!forum/net
 ### Freenode IRC
 
 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
 
 * 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 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
     * Screenshots (if applicable)
 
@@ -49,71 +46,64 @@ sure to include:
 The issue will be reviewed by a moderator after submission and the appropriate
 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
 
-* 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
-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.
 
 * When submitting a feature request on GitHub, be sure to include the
 following:
 
     * 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
 
-* 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
     * 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 django.shortcuts import get_object_or_404
 from rest_framework.decorators import detail_route
 from rest_framework.response import Response
 from rest_framework.viewsets import ModelViewSet
 
-from django.shortcuts import get_object_or_404
-
 from circuits import filters
 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.views import CustomFieldModelViewSet
+from extras.models import Graph, GRAPH_TYPE_PROVIDER
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from . import serializers
 

+ 0 - 1
netbox/circuits/filters.py

@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 
 import django_filters
-
 from django.db.models import Q
 
 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,
     SmallTextarea, SlugField,
 )
-
 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 extras.models import CustomFieldModel, CustomFieldValue
 from tenancy.models import Tenant
-from utilities.utils import csv_format
 from utilities.models import CreatedUpdatedModel
+from utilities.utils import csv_format
 from .constants import *
 
 

+ 1 - 3
netbox/circuits/tables.py

@@ -1,14 +1,12 @@
 from __future__ import unicode_literals
 
 import django_tables2 as tables
-from django_tables2.utils import Accessor
-
 from django.utils.safestring import mark_safe
+from django_tables2.utils import Accessor
 
 from utilities.tables import BaseTable, ToggleColumn
 from .models import Circuit, CircuitType, Provider
 
-
 CIRCUITTYPE_ACTIONS = """
 {% 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>

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

@@ -1,14 +1,15 @@
 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.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 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 utilities.tests import HttpStatusMixin
 

+ 0 - 1
netbox/circuits/urls.py

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

+ 2 - 1
netbox/circuits/views.py

@@ -15,7 +15,8 @@ from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 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 collections import OrderedDict
 
 from rest_framework import serializers
 from rest_framework.validators import UniqueTogetherValidator
 
-from ipam.models import IPAddress
 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 (
-    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 ipam.models import IPAddress
 from tenancy.api.serializers import NestedTenantSerializer
 from utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
 from virtualization.models import Cluster

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

@@ -1,28 +1,30 @@
 from __future__ import unicode_literals
+
 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.mixins import ListModelMixin
 from rest_framework.response import Response
 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 (
     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,
 )
-from dcim import filters
 from extras.api.serializers import RenderedGraphSerializer
 from extras.api.views import CustomFieldModelViewSet
 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 .exceptions import MissingFilterException
 
 
 #
@@ -256,12 +258,19 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet):
                 device.platform
             ))
 
-        # Check that NAPALM is installed and verify the configured driver
+        # Check that NAPALM is installed
         try:
             import napalm
-            from napalm_base.exceptions import ConnectAuthError, ModuleImportError
         except ImportError:
             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:
             driver = napalm.get_network_driver(device.platform.napalm_driver)
         except ModuleImportError:

+ 10 - 8
netbox/dcim/filters.py

@@ -1,22 +1,23 @@
 from __future__ import unicode_literals
 
 import django_filters
-from netaddr import EUI
-from netaddr.core import AddrFormatError
-
 from django.contrib.auth.models import User
 from django.db.models import Q
+from netaddr import EUI
+from netaddr.core import AddrFormatError
 
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from utilities.filters import NullableCharFieldFilter, NumericInFilter
 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 (
     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)',
     )
     status = django_filters.MultipleChoiceFilter(
-        choices=STATUS_CHOICES
+        choices=STATUS_CHOICES,
+        null_value=None
     )
     is_full_depth = django_filters.BooleanFilter(
         name='device_type__is_full_depth',

+ 1 - 2
netbox/dcim/formfields.py

@@ -1,9 +1,8 @@
 from __future__ import unicode_literals
 
-from netaddr import EUI, AddrFormatError
-
 from django import forms
 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 mptt.forms import TreeNodeChoiceField
 import re
 
 from django import forms
 from django.contrib.auth.models import User
 from django.contrib.postgres.forms.array import SimpleArrayField
 from django.db.models import Count, Q
+from mptt.forms import TreeNodeChoiceField
 
 from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
 from ipam.models import IPAddress
@@ -19,17 +19,19 @@ from utilities.forms import (
     SlugField, FilterTreeNodeMultipleChoiceField,
 )
 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 .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+\}'
 
 

+ 28 - 4
netbox/dcim/models.py

@@ -1,12 +1,10 @@
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 from itertools import count, groupby
 
-from mptt.models import MPTTModel, TreeForeignKey
-
 from django.conf import settings
 from django.contrib.auth.models import User
-from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.fields import GenericRelation
 from django.contrib.postgres.fields import ArrayField
 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.urls import reverse
 from django.utils.encoding import python_2_unicode_compatible
+from mptt.models import MPTTModel, TreeForeignKey
 
 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 tenancy.models import Tenant
 from utilities.fields import ColorField, NullableCharField
@@ -1118,6 +1117,15 @@ class ConsoleServerPort(models.Model):
     def __str__(self):
         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
@@ -1183,6 +1191,15 @@ class PowerOutlet(models.Model):
     def __str__(self):
         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
@@ -1239,6 +1256,13 @@ class Interface(models.Model):
 
     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
         if self.device and self.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]+)')"
         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)"
         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,
 )
 
-
 REGION_LINK = """
 {% if record.get_children %}
     <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 rest_framework import status
-from rest_framework.test import APITestCase
-
 from django.contrib.auth.models import User
 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 (
     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 users.models import Token
@@ -1432,7 +1432,7 @@ class ConsoleServerPortTest(HttpStatusMixin, APITestCase):
         site = Site.objects.create(name='Test Site 1', slug='test-site-1')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         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(
             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')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         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(
             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')
         manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
         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(
             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 ipam.views import ServiceCreateView
 from secrets.views import secret_add
-from .models import Device, Rack, Site
 from . import views
-
+from .models import Device, Rack, Site
 
 app_name = 'dcim'
 urlpatterns = [

+ 8 - 7
netbox/dcim/views.py

@@ -1,6 +1,5 @@
 from __future__ import unicode_literals
-import re
-from natsort import natsorted
+
 from operator import attrgetter
 
 from django.contrib import messages
@@ -15,10 +14,11 @@ from django.utils.html import escape
 from django.utils.http import urlencode
 from django.utils.safestring import mark_safe
 from django.views.generic import View
+from natsort import natsorted
 
-from ipam.models import Prefix, Service, VLAN
 from circuits.models import Circuit
 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.paginator import EnhancedPaginator
 from utilities.views import (
@@ -26,11 +26,12 @@ from utilities.views import (
     ComponentEditView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 from . import filters, forms, tables
+from .constants import CONNECTION_STATUS_CONNECTED
 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 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.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
 
 
@@ -39,6 +38,15 @@ class CustomFieldsSerializer(serializers.BaseSerializer):
             # Data validation
             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
                 if cf.type == CF_TYPE_BOOLEAN and value not in [True, False, 1, 0]:
                     raise ValidationError(

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

@@ -1,14 +1,12 @@
 from __future__ import unicode_literals
 
 from django.core.exceptions import ObjectDoesNotExist
-
 from rest_framework import serializers
 
 from dcim.api.serializers import NestedDeviceSerializer, NestedRackSerializer, NestedSiteSerializer
 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 utilities.api import ChoiceFieldSerializer, ContentTypeFieldSerializer, ValidatedModelSerializer
 

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

@@ -1,14 +1,13 @@
 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.exceptions import PermissionDenied
 from rest_framework.response import Response
 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.models import CustomField, ExportTemplate, Graph, ImageAttachment, ReportResult, TopologyMap, UserAction
 from extras.reports import get_report, get_reports

+ 2 - 2
netbox/extras/filters.py

@@ -1,12 +1,12 @@
 from __future__ import unicode_literals
 
 import django_filters
-
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 
 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):

+ 3 - 4
netbox/extras/forms.py

@@ -1,14 +1,13 @@
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 
 from django import forms
 from django.contrib.contenttypes.models import ContentType
 
 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):

+ 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.db.models import Model
 
-
 APPS = ['circuits', 'dcim', 'extras', 'ipam', 'secrets', 'tenancy', 'users', 'virtualization']
 
 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 getpass import getpass
-from ncclient.transport.errors import AuthenticationError
-from paramiko import AuthenticationException
 
 from django.conf import settings
 from django.core.management.base import BaseCommand, CommandError
 from django.db import transaction
+from ncclient.transport.errors import AuthenticationError
+from paramiko import AuthenticationException
 
 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.utils import timezone
 
-from extras.models import ReportResult
 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
 from __future__ import unicode_literals
 from distutils.version import StrictVersion
+import re
 
 from django.conf import settings
 import django.contrib.postgres.fields.jsonb
@@ -18,7 +19,7 @@ def verify_postgresql_version(apps, schema_editor):
         with connection.cursor() as cursor:
             cursor.execute("SELECT VERSION()")
             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'):
                 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 collections import OrderedDict
 from datetime import date
-import graphviz
 
+import graphviz
 from django.contrib.auth.models import User
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType

+ 2 - 1
netbox/extras/reports.py

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

+ 2 - 2
netbox/extras/rpc.py

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

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

@@ -1,14 +1,14 @@
 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.contenttypes.models import ContentType
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 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 utilities.tests import HttpStatusMixin
 

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

@@ -1,19 +1,17 @@
 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.contenttypes.models import ContentType
 from django.test import TestCase
 from django.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 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 utilities.tests import HttpStatusMixin
 

+ 0 - 1
netbox/extras/urls.py

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

+ 1 - 1
netbox/extras/views.py

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

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

@@ -1,4 +1,5 @@
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 
 from rest_framework import serializers
@@ -6,10 +7,10 @@ from rest_framework.validators import UniqueTogetherValidator
 
 from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer
 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 utilities.api import ChoiceFieldSerializer, ValidatedModelSerializer
 from virtualization.api.serializers import NestedVirtualMachineSerializer

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

@@ -1,17 +1,16 @@
 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.decorators import detail_route
 from rest_framework.exceptions import PermissionDenied
 from rest_framework.response import Response
 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 ipam import filters
+from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from utilities.api import FieldChoicesViewSet, WritableSerializerMixin
 from . import serializers
 

+ 11 - 1
netbox/ipam/constants.py

@@ -61,7 +61,7 @@ VLAN_STATUS_CHOICES = (
     (VLAN_STATUS_DEPRECATED, 'Deprecated')
 )
 
-# Bootstrap CSS classes for various statuses
+# Bootstrap CSS classes
 STATUS_CHOICE_CLASSES = {
     0: 'default',
     1: 'primary',
@@ -70,6 +70,16 @@ STATUS_CHOICE_CLASSES = {
     4: 'warning',
     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_PROTOCOL_TCP = 6

+ 1 - 2
netbox/ipam/fields.py

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

+ 31 - 12
netbox/ipam/filters.py

@@ -1,20 +1,17 @@
 from __future__ import unicode_literals
 
 import django_filters
+from django.db.models import Q
 from netaddr import IPNetwork
 from netaddr.core import AddrFormatError
 
-from django.db.models import Q
-
 from dcim.models import Site, Device, Interface
 from extras.filters import CustomFieldFilterSet
 from tenancy.models import Tenant
 from utilities.filters import NumericInFilter
 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):
@@ -102,9 +99,18 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         method='search',
         label='Search',
     )
+    # TODO: Deprecate in v2.3.0
     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(
         method='filter_mask_length',
@@ -159,7 +165,8 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Role (slug)',
     )
     status = django_filters.MultipleChoiceFilter(
-        choices=PREFIX_STATUS_CHOICES
+        choices=PREFIX_STATUS_CHOICES,
+        null_value=None
     )
 
     class Meta:
@@ -177,7 +184,17 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
             pass
         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()
         if not value:
             return queryset
@@ -254,7 +271,8 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Interface (ID)',
     )
     status = django_filters.MultipleChoiceFilter(
-        choices=IPADDRESS_STATUS_CHOICES
+        choices=IPADDRESS_STATUS_CHOICES,
+        null_value=None
     )
     role = django_filters.MultipleChoiceFilter(
         choices=IPADDRESS_ROLE_CHOICES
@@ -353,7 +371,8 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Role (slug)',
     )
     status = django_filters.MultipleChoiceFilter(
-        choices=VLAN_STATUS_CHOICES
+        choices=VLAN_STATUS_CHOICES,
+        null_value=None
     )
 
     class Meta:

+ 1 - 2
netbox/ipam/formfields.py

@@ -1,9 +1,8 @@
 from __future__ import unicode_literals
 
-from netaddr import IPNetwork, AddrFormatError
-
 from django import forms
 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,
 )
 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 = [
     ('', 'All'),
@@ -362,7 +359,7 @@ def prefix_status_choices():
 class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Prefix
     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',
     }))
     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']
 
 
+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():
     status_counts = {}
     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
-import netaddr
 
+import netaddr
 from django.conf import settings
 from django.contrib.contenttypes.fields import GenericRelation
-from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
@@ -286,7 +285,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
         """
         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):
         """
@@ -315,9 +314,7 @@ class Prefix(CreatedUpdatedModel, CustomFieldModel):
             child_prefixes = netaddr.IPSet([p.prefix for p in queryset])
             return int(float(child_prefixes.size) / self.prefix.size * 100)
         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
             if self.family == 4 and self.prefix.prefixlen < 31 and not self.is_pool:
                 prefix_size -= 2
@@ -461,6 +458,9 @@ class IPAddress(CreatedUpdatedModel, CustomFieldModel):
     def get_status_class(self):
         return STATUS_CHOICE_CLASSES[self.status]
 
+    def get_role_class(self):
+        return ROLE_CHOICE_CLASSES[self.role]
+
 
 @python_2_unicode_compatible
 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 .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
 
-
 RIR_UTILIZATION = """
 <div class="progress">
     {% if record.stats.total %}
@@ -77,6 +76,10 @@ IPADDRESS_LINK = """
 {% 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 = """
 {% if record.interface %}
     <a href="{{ record.interface.parent.get_absolute_url }}">{{ record.interface.parent }}</a>
@@ -269,8 +272,8 @@ class PrefixDetailTable(PrefixTable):
 class IPAddressTable(BaseTable):
     pk = ToggleColumn()
     address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address')
-    status = tables.TemplateColumn(STATUS_LABEL)
     vrf = tables.TemplateColumn(VRF_LINK, verbose_name='VRF')
+    status = tables.TemplateColumn(STATUS_LABEL)
     tenant = tables.TemplateColumn(TENANT_LINK)
     parent = tables.TemplateColumn(IPADDRESS_PARENT, 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
 #

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

@@ -1,16 +1,14 @@
 from __future__ import unicode_literals
 
+from django.contrib.auth.models import User
+from django.urls import reverse
 from netaddr import IPNetwork
 from rest_framework import status
 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 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 utilities.tests import HttpStatusMixin
 

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

@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 
 import netaddr
-
 from django.core.exceptions import ValidationError
 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/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/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+)/edit/$', views.IPAddressEditView.as_view(), name='ipaddress_edit'),
     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 django_tables2 import RequestConfig
 import netaddr
-
 from django.conf import settings
 from django.contrib.auth.mixins import PermissionRequiredMixin
 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.views.generic import View
+from django_tables2 import RequestConfig
 
 from dcim.models import Device, Interface
 from utilities.paginator import EnhancedPaginator
@@ -17,11 +16,8 @@ from utilities.views import (
 )
 from virtualization.models import VirtualMachine
 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):
@@ -459,9 +455,7 @@ class PrefixView(View):
             aggregate = None
 
         # 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 = Prefix.objects.filter(
@@ -517,6 +511,7 @@ class PrefixView(View):
             'parent_prefix_table': parent_prefix_table,
             'child_prefix_table': child_prefix_table,
             'duplicate_prefix_table': duplicate_prefix_table,
+            'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf or '0', prefix.prefix),
             'permissions': permissions,
             'return_url': prefix.get_absolute_url(),
         })
@@ -529,9 +524,7 @@ class PrefixIPAddressesView(View):
         prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
 
         # 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'
         )
         ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
@@ -557,7 +550,7 @@ class PrefixIPAddressesView(View):
             'prefix': prefix,
             'ip_table': ip_table,
             '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'
 
 
+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):
     permission_required = 'ipam.delete_ipaddress'
     model = IPAddress

+ 0 - 1
netbox/netbox/forms.py

@@ -4,7 +4,6 @@ from django import forms
 
 from utilities.forms import BootstrapMixin
 
-
 OBJ_TYPE_CHOICES = (
     ('', 'All Objects'),
     ('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__)))
 
@@ -148,6 +148,7 @@ MIDDLEWARE = (
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
     'django.middleware.security.SecurityMiddleware',
+    'utilities.middleware.ExceptionHandlingMiddleware',
     'utilities.middleware.LoginRequiredMiddleware',
     'utilities.middleware.APIVersionMiddleware',
 )

+ 2 - 8
netbox/netbox/urls.py

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

+ 4 - 26
netbox/netbox/views.py

@@ -1,13 +1,12 @@
 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.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.models import Circuit, Provider
@@ -30,7 +29,6 @@ from virtualization.models import Cluster, VirtualMachine
 from virtualization.tables import ClusterTable, VirtualMachineDetailTable
 from .forms import SearchForm
 
-
 SEARCH_MAX_RESULTS = 15
 SEARCH_TYPES = OrderedDict((
     # Circuits
@@ -247,23 +245,3 @@ class APIRootView(APIView):
             ('tenancy', reverse('tenancy-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
 
 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 */
+.text-nowrap {
+    white-space: nowrap;
+}
 .banner-bottom {
     margin-bottom: 50px;
 }

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

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

+ 1 - 2
netbox/secrets/filters.py

@@ -1,12 +1,11 @@
 from __future__ import unicode_literals
 
 import django_filters
-
 from django.db.models import Q
 
-from .models import Secret, SecretRole
 from dcim.models import Device
 from utilities.filters import NumericInFilter
+from .models import Secret, SecretRole
 
 
 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.PublicKey import RSA
-
 from django import forms
 from django.db.models import Count
 

+ 4 - 1
netbox/secrets/models.py

@@ -1,10 +1,10 @@
 from __future__ import unicode_literals
+
 import os
 
 from Crypto.Cipher import AES, PKCS1_OAEP
 from Crypto.PublicKey import RSA
 from Crypto.Util import strxor
-
 from django.conf import settings
 from django.contrib.auth.hashers import make_password, check_password
 from django.contrib.auth.models import Group, User
@@ -286,6 +286,9 @@ class Secret(CreatedUpdatedModel):
         super(Secret, self).__init__(*args, **kwargs)
 
     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:
             return '{} for {}'.format(self.role, self.device)
         return 'Secret'

+ 0 - 2
netbox/secrets/tables.py

@@ -3,10 +3,8 @@ from __future__ import unicode_literals
 import django_tables2 as tables
 
 from utilities.tables import BaseTable, ToggleColumn
-
 from .models import SecretRole, Secret
 
-
 SECRETROLE_ACTIONS = """
 {% 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>

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

@@ -1,18 +1,17 @@
 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.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
 from secrets.models import Secret, SecretRole, SessionKey, UserKey
 from users.models import Token
 from utilities.tests import HttpStatusMixin
 
-
 # Dummy RSA key pair for testing use only
 PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
 MIIEowIBAAKCAQEA97wPWxpq5cClRu8Ssq609ZLfyx6E8ln/v/PdFZ7fxxmA4k+z

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

@@ -1,14 +1,13 @@
 from __future__ import unicode_literals
 
 from Crypto.PublicKey import RSA
-
 from django.conf import settings
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
 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.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key
 
 
 class UserKeyTestCase(TestCase):

+ 0 - 1
netbox/secrets/urls.py

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

+ 1 - 0
netbox/secrets/views.py

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

+ 13 - 6
netbox/templates/500.html

@@ -12,7 +12,7 @@
 <body>
     <div class="container-fluid">
         <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-heading">
                         <strong>
@@ -21,13 +21,20 @@
                         </strong>
                     </div>
                     <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 />
 {{ 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">
                             <a href="{% url 'home' %}" class="btn btn-primary">Home Page</a>
                         </div>

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

@@ -65,7 +65,7 @@
                     <td>
                         {% if circuit.tenant %}
                             {% 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>
                             {% endif %}
                             <a href="{{ circuit.tenant.get_absolute_url }}">{{ circuit.tenant }}</a>

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

@@ -7,7 +7,7 @@
 {% block content %}
 {% include 'dcim/inc/device_header.html' with active_tab='info' %}
 <div class="row">
-	<div class="col-md-5">
+	<div class="col-md-6">
         <div class="panel panel-default">
             <div class="panel-heading">
                 <strong>Device</strong>
@@ -28,10 +28,10 @@
                     <td>
                         {% if device.rack %}
                             {% 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>
                             {% 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 %}
                             <span class="text-muted">None</span>
                         {% endif %}
@@ -42,7 +42,7 @@
                     <td>
                         {% if device.parent_bay %}
                             {% 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 %}
                                     (U{{ parent.position }} / {{ parent.get_face_display }})
                                 {% endif %}
@@ -61,7 +61,7 @@
                     <td>
                         {% if device.tenant %}
                             {% 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>
                             {% endif %}
                             <a href="{{ device.tenant.get_absolute_url }}">{{ device.tenant }}</a>
@@ -172,58 +172,20 @@
         {% with device.get_custom_fields as custom_fields %}
             {% include 'inc/custom_fields_panel.html' %}
         {% 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-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>
-            {% 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="col-md-6">
         <div class="panel panel-default">
             <div class="panel-heading">
                 <strong>Console / Power</strong>
@@ -273,17 +235,57 @@
                 </div>
             {% endif %}
         </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 %}
-                    <span class="text-muted">None</span>
+                    <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-heading">
+                <strong>Services</strong>
             </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 class="panel panel-default">
             <div class="panel-heading">
@@ -326,7 +328,9 @@
             {% endif %}
         </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 perms.dcim.delete_devicebay %}
                 <form method="post" action="{% url 'dcim:devicebay_bulk_delete' pk=device.pk %}">
@@ -350,7 +354,7 @@
                 </div>
                 <table class="table table-hover panel-body component-list">
                     {% for devicebay in device_bays %}
-                        {% include 'dcim/inc/devicebay.html' with selectable=True %}
+                        {% include 'dcim/inc/devicebay.html' %}
                     {% empty %}
                         <tr>
                             <td colspan="4">No device bays defined</td>
@@ -405,11 +409,23 @@
                     </div>
                 </div>
                 <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 %}
-                        {% include 'dcim/inc/interface.html' with selectable=True %}
+                        {% include 'dcim/inc/interface.html' %}
                     {% empty %}
                         <tr>
-                            <td colspan="4">No interfaces defined</td>
+                            <td colspan="8">No interfaces defined</td>
                         </tr>
                     {% endfor %}
                 </table>
@@ -467,8 +483,16 @@
                     </div>
                 </div>
                 <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 %}
-                        {% include 'dcim/inc/consoleserverport.html' with selectable=True %}
+                        {% include 'dcim/inc/consoleserverport.html' %}
                     {% empty %}
                         <tr>
                             <td colspan="4">No console server ports defined</td>
@@ -524,12 +548,20 @@
                     </div>
                 </div>
                 <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 %}
-                        {% include 'dcim/inc/poweroutlet.html' with selectable=True %}
+                        {% include 'dcim/inc/poweroutlet.html' %}
                     {% empty %}
                         <tr>
                             <td colspan="4">No power outlets defined</td>
-                        </tr>
+                        </tr> text-nowrap
                     {% endfor %}
                 </table>
                 {% 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">
                         <label class="col-md-3 control-label required">Bay</label>
                         <div class="col-md-9">
-                            <p class="form-control-static">{{ device_bay.name }}</p>
+                            <p class="form-control-static">{{ device_bay }}</p>
                         </div>
                     </div>
                     {% 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 %}">
     <td>
-        <i class="fa fa-fw fa-keyboard-o"></i> {{ cp.name }}
+        <i class="fa fa-fw fa-keyboard-o"></i> {{ cp }}
     </td>
     {% if cp.cs_port %}
         <td>
             <a href="{% url 'dcim:device' pk=cp.cs_port.device.pk %}">{{ cp.cs_port.device }}</a>
         </td>
         <td>
-            {{ cp.cs_port.name }}
+            {{ cp.cs_port }}
         </td>
     {% else %}
         <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 %}">
-    {% 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">
             <input name="pk" type="checkbox" value="{{ csp.pk }}" />
         </td>
@@ -7,20 +7,19 @@
     <td>
         <i class="fa fa-fw fa-keyboard-o"></i> {{ csp }}
     </td>
-    <td></td>
     {% if csp.connected_console %}
         <td>
             <a href="{% url 'dcim:device' pk=csp.connected_console.device.pk %}">{{ csp.connected_console.device }}</a>
         </td>
         <td>
-            {{ csp.connected_console.name }}
+            {{ csp.connected_console }}
         </td>
     {% else %}
         <td colspan="2">
             <span class="text-muted">Not connected</span>
         </td>
     {% endif %}
-    <td colspan="2" class="text-right">
+    <td class="text-right">
         {% if perms.dcim.change_consoleserverport %}
             {% if csp.connected_console %}
                 {% if csp.connected_console.connection_status %}

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

@@ -8,7 +8,7 @@
         {% endif %}
         {% if device.parent_bay %}
             <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 %}
         <li>{{ device }}</li>
     </ol>

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

@@ -1,11 +1,11 @@
 <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">
             <input name="pk" type="checkbox" value="{{ devicebay.pk }}" />
         </td>
     {% endif %}
     <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>
     {% if devicebay.installed_device %}
         <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">
             <input name="pk" type="checkbox" value="{{ iface.pk }}" />
         </td>
     {% 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>
-        <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 %}
-            <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 %}
     </td>
+
+    {# Description #}
+    <td>{{ iface.description }}</td>
+
+    {# MTU #}
     <td>{{ iface.mtu|default:"" }}</td>
+
+    {# MAC address #}
     <td>{{ iface.mac_address|default:"" }}</td>
+
+    {# Connection or type #}
     {% if iface.is_lag %}
         <td colspan="2" class="text-muted">
             LAG interface<br />
@@ -55,7 +71,9 @@
             <span class="text-muted">Not connected</span>
         </td>
     {% endif %}
-    <td colspan="2" class="text-right">
+
+    {# Buttons #}
+    <td class="text-right text-nowrap">
         {% if show_graphs %}
             {% 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">
@@ -96,7 +114,7 @@
                     </a>
                 {% 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>
             </a>
         {% endif %}
@@ -106,62 +124,63 @@
                     <i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
                 </button>
             {% 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>
                 </a>
             {% endif %}
         {% endif %}
     </td>
 </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 %}
-                <td colspan="7" class="subtable">
+                <span class="text-muted">Global</span>
             {% 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>
-    <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>{{ item.manufacturer|default:"" }}</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 %}">
-    {% 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">
             <input name="pk" type="checkbox" value="{{ po.pk }}" />
         </td>
     {% endif %}
     <td>
-        <i class="fa fa-fw fa-bolt"></i> {{ po.name }}
+        <i class="fa fa-fw fa-bolt"></i> {{ po }}
     </td>
-    <td></td>
     {% if po.connected_port %}
         <td>
             <a href="{% url 'dcim:device' pk=po.connected_port.device.pk %}">{{ po.connected_port.device }}</a>
         </td>
         <td>
-            {{ po.connected_port.name }}
+            {{ po.connected_port }}
         </td>
     {% else %}
         <td colspan="2">
             <span class="text-muted">Not connected</span>
         </td>
     {% endif %}
-    <td colspan="2" class="text-right">
+    <td class="text-right">
         {% if perms.dcim.change_poweroutlet %}
             {% if po.connected_port %}
                 {% 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 %}">
     <td>
-        <i class="fa fa-fw fa-bolt"></i> {{ pp.name }}
+        <i class="fa fa-fw fa-bolt"></i> {{ pp }}
     </td>
     {% if pp.power_outlet %}
         <td>
             <a href="{% url 'dcim:device' pk=pp.power_outlet.device.pk %}">{{ pp.power_outlet.device }}</a>
         </td>
         <td>
-            {{ pp.power_outlet.name }}
+            {{ pp.power_outlet }}
         </td>
     {% else %}
         <td colspan="2">

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

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

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

@@ -76,7 +76,7 @@
                     <td>
                         {% if site.tenant %}
                             {% 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>
                             {% endif %}
                             <a href="{{ site.tenant.get_absolute_url }}">{{ site.tenant }}</a>
@@ -221,7 +221,7 @@
                 <table class="table table-hover panel-body">
                     {% for rg in rack_groups %}
                         <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>
                         </tr>
                     {% 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">
-    <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>

+ 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>
                         {% if prefix.tenant %}
                             {% 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>
                             {% endif %}
                             <a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
                         {% elif prefix.vrf.tenant %}
                             {% 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>
                             {% endif %}
                             <a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
@@ -75,7 +75,7 @@
                     <td>
                         {% if prefix.vlan %}
                             {% 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>
                             {% endif %}
                             <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>
             {% endif %}
             {% 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 %}
-            <li>{{ vlan.name }} ({{ vlan.vid }})</li>
+            <li>{{ vlan }}</li>
         </ol>
     </div>
     <div class="col-sm-4 col-md-3">
@@ -68,7 +68,7 @@
                     <td>Group</td>
                     <td>
                         {% 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 %}
                             <span class="text-muted">None</span>
                         {% endif %}
@@ -87,7 +87,7 @@
                     <td>
                         {% if vlan.tenant %}
                             {% 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>
                             {% endif %}
                             <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>
 {% include 'inc/created_updated.html' with obj=vm %}
 <div class="row">
-	<div class="col-md-5">
+	<div class="col-md-6">
         <div class="panel panel-default">
             <div class="panel-heading">
                 <strong>Virtual Machine</strong>
@@ -49,7 +49,7 @@
             <table class="table table-hover panel-body attr-table">
                 <tr>
                     <td>Name</td>
-                    <td>{{ vm.name }}</td>
+                    <td>{{ vm }}</td>
                 </tr>
                 <tr>
                     <td>Status</td>
@@ -82,7 +82,7 @@
                     <td>
                         {% if vm.tenant %}
                             {% 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>
                             {% endif %}
                             <a href="{{ vm.tenant.get_absolute_url }}">{{ vm.tenant }}</a>
@@ -123,6 +123,21 @@
                 </tr>
             </table>
         </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-heading">
                 <strong>Cluster</strong>
@@ -204,21 +219,10 @@
                 </div>
             {% endif %}
         </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-7">
+</div>
+<div class="row">
+    <div class="col-md-12">
         {% if perms.dcim.change_interface or perms.dcim.delete_interface %}
             <form method="post">
             {% csrf_token %}
@@ -244,11 +248,23 @@
                 </div>
             </div>
             <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 %}
-                    {% include 'virtualization/inc/interface.html' with selectable=True %}
+                    {% include 'dcim/inc/interface.html' with device=vm %}
                 {% empty %}
                     <tr>
-                        <td colspan="4">No interfaces defined</td>
+                        <td colspan="6">No interfaces defined</td>
                     </tr>
                 {% endfor %}
             </table>

+ 0 - 1
netbox/tenancy/filters.py

@@ -1,7 +1,6 @@
 from __future__ import unicode_literals
 
 import django_filters
-
 from django.db.models import Q
 
 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
 
 from utilities.tables import BaseTable, ToggleColumn
-
 from .models import Tenant, TenantGroup
 
-
 TENANTGROUP_ACTIONS = """
 {% 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>

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

@@ -1,10 +1,9 @@
 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.urls import reverse
+from rest_framework import status
+from rest_framework.test import APITestCase
 
 from tenancy.models import Tenant, TenantGroup
 from users.models import Token

+ 0 - 1
netbox/tenancy/urls.py

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

+ 1 - 1
netbox/tenancy/views.py

@@ -13,8 +13,8 @@ from utilities.views import (
     BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
 )
 from virtualization.models import VirtualMachine
-from .models import Tenant, TenantGroup
 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 django.contrib.auth.models import User
-
 from rest_framework import serializers
 
 

+ 1 - 1
netbox/users/forms.py

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

+ 2 - 1
netbox/users/models.py

@@ -1,12 +1,13 @@
 from __future__ import unicode_literals
+
 import binascii
 import os
 
 from django.contrib.auth.models import User
 from django.core.validators import MinLengthValidator
 from django.db import models
-from django.utils.encoding import python_2_unicode_compatible
 from django.utils import timezone
+from django.utils.encoding import 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
 
-
 app_name = 'user'
 urlpatterns = [
 

+ 1 - 2
netbox/utilities/api.py

@@ -1,17 +1,16 @@
 from __future__ import unicode_literals
+
 from collections import OrderedDict
 
 from django.conf import settings
 from django.contrib.contenttypes.models import ContentType
 from django.http import Http404
-
 from rest_framework.exceptions import APIException
 from rest_framework.permissions import BasePermission
 from rest_framework.response import Response
 from rest_framework.serializers import Field, ModelSerializer, ValidationError
 from rest_framework.viewsets import ViewSet
 
-
 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
 
-
 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
 
-import django_filters
 import itertools
 
+import django_filters
 from django import forms
 from django.utils.encoding import force_text
 

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików