Parcourir la source

Merge branch 'develop' into api2

Conflicts:
	netbox/dcim/api/serializers.py
	netbox/dcim/api/urls.py
	netbox/dcim/api/views.py
	netbox/dcim/filters.py
	netbox/dcim/tables.py
	requirements.txt
Jeremy Stretch il y a 8 ans
Parent
commit
9b39ba169c

+ 28 - 0
.github/ISSUE_TEMPLATE.md

@@ -0,0 +1,28 @@
+<!--
+    Please note: GitHub issues are to be used only for feature requests
+    and bug reports. For installation assistance or general discussion,
+    please join us on the mailing list:
+
+        https://groups.google.com/forum/#!forum/netbox-discuss
+
+    Please indicate "bug report" or "feature request" below. Be sure to
+    search the existing set of issues (both open and closed) to see if
+    a similar issue has already been raised.
+-->
+### Issue type:
+
+<!--
+    If filing a bug, please indicate the version of Python and NetBox
+    you are running. (This is not necessary for feature requests.)
+-->
+**Python version:**
+**NetBox version:**
+
+<!--
+    If filing a bug, please record the exact steps taken to reproduce
+    the bug and any errors messages that are generated.
+
+    If filing a feature request, please precisely describe the data
+    model or workflow you would like to see implemented, and provide a
+    use case.
+-->

+ 14 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,14 @@
+<!--
+    Thank you for your interest in contributing to NetBox! Please note
+    that our contribution policy requires that a feature request or bug
+    report be opened for approval prior to filing a pull request. This
+    helps avoid wasting time and effort on something that we might not
+    be able to accept.
+
+    Please indicate the relevant feature request or bug report below.
+-->
+### Fixes:
+
+<!--
+    Please include a summary of the proposed changes below.
+-->

+ 76 - 47
CONTRIBUTING.md

@@ -1,84 +1,113 @@
 ## Getting Help
 
-If you encounter any issues installing or using NetBox, try one of the following resources to get assistance. Please
-**do not** open an issue on GitHub except to report bugs or request features.
+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.
 
-### Freenode IRC
+### Mailing List
 
-Join the #netbox channel on [Freenode IRC](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/).
+We have established a Google Groups Mailing List for issues and general
+discussion. This is the best forum for obtaining assistance with NetBox
+installation. You can find us [here](https://groups.google.com/forum/#!forum/netbox-discuss).
 
-### Mailing List
+### Freenode IRC
 
-We have established a Google Groups Mailing List for issues and general discussion. You can find us [here]( https://groups.google.com/forum/#!forum/netbox-discuss).
+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/).
 
 ## 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.
+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 IRC or Google Groups.
-**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:
+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:
 
     * The environment in which NetBox is running
-    * The exact steps that can be taken to reproduce the issue (if applicable)
-    * Any error messages returned
+    * The exact steps that can be taken to reproduce the issue (if
+      applicable)
+    * Any error messages generated
     * Screenshots (if applicable)
 
-* 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. 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 as spam. 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:
+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
 
-* Before filing a new feature request, propose it on IRC or Reddit first. Feedback you receive there will help validate
-and shape the proposed feature before filing a formal issue.
+* 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.
 
-* 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 chances are of being implemented. Overly broad feature
-requests will be closed.
+* 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
+implemented. Overly broad feature requests will be closed.
 
-* When submitting a feature request on GitHub, be sure to include the following:
+* 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 any changes necessary to the database schema
-    * Any third-party libraries or other resources which would be involved
+    * 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
 
 ## 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`. In NetBox, 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 netbox/`
-    * PEP 8 compliance is enforced, with the exception that lines may be greater than 80 characters in length
+    * All tests pass when run with `./manage.py test`
+    * PEP 8 compliance is enforced, with the exception that lines may be
+      greater than 80 characters in length

+ 3 - 3
docs/data-model/circuits.md

@@ -2,7 +2,7 @@ The circuits component of NetBox deals with the management of long-haul Internet
 
 # Providers
 
-A provider is any entity which provides some form of connectivity. This obviously includes carriers which offer Internet and private transit service. However, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
+A provider is any entity which provides some form of connectivity. While this obviously includes carriers which offer Internet and private transit service, it might also include Internet exchange (IX) points and even organizations with whom you peer directly.
 
 Each provider may be assigned an autonomous system number (ASN), an account number, and contact information.
 
@@ -14,7 +14,7 @@ A circuit represents a single physical data link connecting two endpoints. Each
 
 ### Circuit Types
 
-Circuits are classified by type. For example:
+Circuits are classified by type. For example, you might define circuit types for:
 
 * Internet transit
 * Out-of-band connectivity
@@ -27,7 +27,7 @@ Circuit types are fully customizable.
 
 A circuit may have one or two terminations, annotated as the "A" and "Z" sides of the circuit. A single-termination circuit can be used when you don't know (or care) about the far end of a circuit (for example, an Internet access circuit which connects to a transit provider). A dual-termination circuit is useful for tracking circuits which connect two sites.
 
-Each circuit termination can be tied to a site, or to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
+Each circuit termination is tied to a site, and optionally to a specific device and interface within that site. Each termination can be assigned a separate downstream and upstream speed independent from one another. Fields are also available to track cross-connect and patch panel details.
 
 !!! note
     A circuit represents a physical link, and cannot have more than two endpoints. When modeling a multi-point topology, each leg of the topology must be defined as a discrete circuit.

Fichier diff supprimé car celui-ci est trop grand
+ 36 - 24
docs/data-model/dcim.md


Fichier diff supprimé car celui-ci est trop grand
+ 16 - 8
docs/data-model/extras.md


+ 17 - 14
docs/data-model/ipam.md

@@ -6,11 +6,14 @@ A VRF object in NetBox represents a virtual routing and forwarding (VRF) domain
 
 Each VRF is assigned a name and a unique route distinguisher (RD). VRFs are an optional feature of NetBox: Any IP prefix or address not assigned to a VRF is said to belong to the "global" table.
 
+!!! note
+    By default, NetBox allows for overlapping IP space both in the global table and within each VRF. Unique space enforcement can be toggled per-VRF as well as in the global table using the `ENFORCE_GLOBAL_UNIQUE` configuration setting.
+
 ---
 
 # Aggregates
 
-IPv4 address space is organized as a hierarchy, with more-specific (smaller) prefix arranged as child nodes under less-specific (larger) prefixes. For example:
+IP address space is organized as a hierarchy, with more-specific (smaller) prefixes arranged as child nodes under less-specific (larger) prefixes. For example:
 
 * 10.0.0.0/8
     * 10.1.0.0/16
@@ -18,23 +21,23 @@ IPv4 address space is organized as a hierarchy, with more-specific (smaller) pre
 
 The root of the IPv4 hierarchy is 0.0.0.0/0, which encompasses all possible IPv4 addresses (and similarly, ::/0 for IPv6). However, even the largest organizations use only a small fraction of the global address space. Therefore, it makes sense to track in NetBox only the address space which is of interest to your organization.
 
-Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the RFC 1918 private IPv4 space. So, you might define three aggregates for this space:
+Aggregates serve as arbitrary top-level nodes in the IP space hierarchy. They allow you to easily construct your IP scheme without any clutter of unused address space. For instance, most organizations utilize some portion of the private IPv4 space set aside in RFC 1918. So, you might define three aggregates for this space:
 
 * 10.0.0.0/8
 * 172.16.0.0/12
 * 192.168.0.0/16
 
-Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space.
+Additionally, you might define an aggregate for each large swath of public IPv4 space your organization uses. You'd also create aggregates for both globally routable and unique local IPv6 space. (Most organizations will not have a need to track IPv6 link local space.)
 
-Any prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation.
+Prefixes you create in NetBox (discussed below) will be automatically organized under their respective aggregates. Any space within an aggregate which is not covered by an existing prefix will be annotated as available for allocation. Total utilization for each aggregate is displayed in the aggregates list.
 
-Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix.
+Aggregates cannot overlap with one another; they can only exist in parallel. For instance, you cannot define both 10.0.0.0/8 and 10.16.0.0/16 as aggregates, because they overlap. 10.16.0.0/16 in this example would be created as a prefix and automatically grouped under 10.0.0.0/8.
 
 ### RIRs
 
 Regional Internet Registries (RIRs) are responsible for the allocation of global address space. The five RIRs are ARIN, RIPE, APNIC, LACNIC, and AFRINIC. However, some address space has been set aside for private or internal use only, such as defined in RFCs 1918 and 6598. NetBox considers these RFCs as a sort of RIR as well; that is, an authority which "owns" certain address space.
 
-Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own).
+Each aggregate must be assigned to one RIR. You are free to define whichever RIRs you choose (or create your own). Each RIR can be annotated as representing only private space.
 
 ---
 
@@ -44,7 +47,7 @@ A prefix is an IPv4 or IPv6 network and mask expressed in CIDR notation (e.g. 19
 
 Each prefix may be assigned to one VRF; prefixes not assigned to a VRF are assigned to the "global" table. Prefixes are also organized under their respective aggregates, irrespective of VRF assignment.
 
-A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. This can be helpful is replicating real-world IP assignments. Each prefix may also be assigned a short description.
+A prefix may optionally be assigned to one VLAN; a VLAN may have multiple prefixes assigned to it. Each prefix may also be assigned a short description.
 
 ### Statuses
 
@@ -52,7 +55,7 @@ Each prefix is assigned an operational status. This is one of the following:
 
 * Container - A summary of child prefixes
 * Active - Provisioned and in use
-* Reserved - Earmarked for future use
+* Reserved - Designated for future use
 * Deprecated - No longer in use
 
 ### Roles
@@ -65,25 +68,25 @@ Whereas a status describes a prefix's operational state, a role describes its fu
 * Lab
 * Out-of-band
 
-Role assignment is optional and you are free to create as many as you'd like.
+Role assignment is optional and roles are fully customizable.
 
 ---
 
 # IP Addresses
 
-An IP address comprises a single address (either IPv4 or IPv6) and its mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
+An IP address comprises a single address (either IPv4 or IPv6) and its subnet mask. Its mask should match exactly how the IP address is configured on an interface in the real world.
 
 Like prefixes, an IP address can optionally be assigned to a VRF (or it will appear in the "global" table). IP addresses are automatically organized under parent prefixes within their respective VRFs. Each IP address can also be assigned a short description.
 
-Each IP address can optionally be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address.
+An IP address can be assigned to a device's interface; an interface may have multiple IP addresses assigned to it. Further, each device may have one of its interface IPs designated as its primary IP address (for both IPv4 and IPv6).
 
-One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily is denoting the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not currently supported.
+One IP address can be designated as the network address translation (NAT) IP address for exactly one other IP address. This is useful primarily to denote the public address for a private internal IP. Tracking one-to-many NAT (or PAT) assignments is not supported.
 
 ---
 
 # VLANs
 
-A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). VLANs may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
+A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). Each VLAN may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role, and may include a short description.
 
 ### VLAN Groups
 
@@ -93,4 +96,4 @@ VLAN groups can be employed for administrative organization within NetBox. Each
 
 # Services
 
-A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, SSH (TCP/22). A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any IP address.)
+A service represents a TCP or UDP service available on a device. Each service must be defined with a name, protocol, and port number; for example, "SSH (TCP/22)." A service may optionally be bound to one or more specific IP addresses belonging to a device. (If no IP addresses are bound, the service is assumed to be reachable via any assigned IP address.)

+ 2 - 2
docs/data-model/secrets.md

@@ -24,11 +24,11 @@ Roles are also used to control access to secrets. Each role is assigned an arbit
 
 Each user within NetBox can associate his or her account with an RSA public key. If activated by an administrator, this user key will contain a unique, encrypted copy of the AES master key needed to retrieve secret data.
 
-User keys may be created by users individually, however they are of no use until they have been activated by a user who already has access to retrieve secret data.
+User keys may be created by users individually, however they are of no use until they have been activated by a user who already possesses an active user key.
 
 ## Creating the First User Key
 
-When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the super user) must create a user key. This can be done by navigating to Profile > User Key.
+When NetBox is first installed, it contains no encryption keys. Before it can store secrets, a user (typically the superuser) must create a user key. This can be done by navigating to Profile > User Key.
 
 To create a user key, you can either generate a new RSA key pair, or upload the public key belonging to a pair you already have. If generating a new key pair, **you must save the private key** locally before saving your new user key. Once your user key has been created, its public key will be displayed under your profile.
 

+ 2 - 4
docs/data-model/tenancy.md

@@ -1,10 +1,8 @@
-NetBox supports the concept of individual tenants within its parent organization. Typically, these are used to represent individual customers or internal departments.
+NetBox supports the assignment of resources to tenant organizations. Typically, these are used to represent individual customers of or internal departments within the organization using NetBox.
 
 # Tenants
 
-A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance.
-
-The following objects can be assigned to tenants:
+A tenant represents a discrete organization. The following objects can be assigned to tenants:
 
 * Sites
 * Racks

+ 10 - 6
netbox/circuits/filters.py

@@ -10,8 +10,8 @@ from .models import Provider, Circuit, CircuitTermination, CircuitType
 
 
 class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -30,7 +30,9 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
         model = Provider
         fields = ['name', 'account', 'asn']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(name__icontains=value) |
             Q(account__icontains=value) |
@@ -39,8 +41,8 @@ class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
 
 class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     provider_id = django_filters.ModelMultipleChoiceFilter(
@@ -92,7 +94,9 @@ class CircuitFilter(CustomFieldFilterSet, django_filters.FilterSet):
         model = Circuit
         fields = ['install_date']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(cid__icontains=value) |
             Q(terminations__xconnect_id__icontains=value) |

+ 18 - 8
netbox/circuits/forms.py

@@ -64,6 +64,7 @@ class ProviderFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Provider
     q = forms.CharField(required=False, label='Search')
     site = FilterChoiceField(queryset=Site.objects.all(), to_field_name='slug')
+    asn = forms.IntegerField(required=False, label='ASN')
 
 
 #
@@ -128,14 +129,23 @@ class CircuitBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Circuit
     q = forms.CharField(required=False, label='Search')
-    type = FilterChoiceField(queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
-                             to_field_name='slug')
-    provider = FilterChoiceField(queryset=Provider.objects.annotate(filter_count=Count('circuits')),
-                                 to_field_name='slug')
-    tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('circuits')), to_field_name='slug',
-                               null_option=(0, 'None'))
-    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
-                             to_field_name='slug')
+    type = FilterChoiceField(
+        queryset=CircuitType.objects.annotate(filter_count=Count('circuits')),
+        to_field_name='slug'
+    )
+    provider = FilterChoiceField(
+        queryset=Provider.objects.annotate(filter_count=Count('circuits')),
+        to_field_name='slug'
+    )
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.annotate(filter_count=Count('circuits')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
+    site = FilterChoiceField(
+        queryset=Site.objects.annotate(filter_count=Count('circuit_terminations')),
+        to_field_name='slug'
+    )
 
 
 #

+ 56 - 41
netbox/dcim/filters.py

@@ -15,8 +15,8 @@ from .models import (
 
 
 class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     region_id = NullableModelMultipleChoiceFilter(
@@ -46,9 +46,16 @@ class SiteFilter(CustomFieldFilterSet, django_filters.FilterSet):
         model = Site
         fields = ['q', 'name', 'facility', 'asn']
 
-    def search(self, queryset, value):
-        qs_filter = Q(name__icontains=value) | Q(facility__icontains=value) | Q(physical_address__icontains=value) | \
-            Q(shipping_address__icontains=value) | Q(comments__icontains=value)
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
+        qs_filter = (
+            Q(name__icontains=value) |
+            Q(facility__icontains=value) |
+            Q(physical_address__icontains=value) |
+            Q(shipping_address__icontains=value) |
+            Q(comments__icontains=value)
+        )
         try:
             qs_filter |= Q(asn=int(value.strip()))
         except ValueError:
@@ -71,11 +78,12 @@ class RackGroupFilter(django_filters.FilterSet):
 
     class Meta:
         model = RackGroup
+        fields = ['name']
 
 
 class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -127,7 +135,9 @@ class RackFilter(CustomFieldFilterSet, django_filters.FilterSet):
         model = Rack
         fields = ['u_height']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(name__icontains=value) |
             Q(facility_id__icontains=value) |
@@ -148,8 +158,8 @@ class RackReservationFilter(django_filters.FilterSet):
 
 
 class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     manufacturer_id = django_filters.ModelMultipleChoiceFilter(
@@ -166,10 +176,13 @@ class DeviceTypeFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = DeviceType
-        fields = ['model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device',
-                  'subdevice_role']
+        fields = [
+            'model', 'part_number', 'u_height', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role',
+        ]
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(manufacturer__name__icontains=value) |
             Q(model__icontains=value) |
@@ -235,12 +248,12 @@ class DeviceBayTemplateFilter(DeviceTypeComponentFilterSet):
 
 
 class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
-    mac_address = django_filters.MethodFilter(
-        action='_mac_address',
+    mac_address = django_filters.CharFilter(
+        method='_mac_address',
         label='MAC address',
     )
     site_id = django_filters.ModelMultipleChoiceFilter(
@@ -340,7 +353,9 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
         model = Device
         fields = ['name', 'serial', 'asset_tag']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(name__icontains=value) |
             Q(serial__icontains=value.strip()) |
@@ -349,7 +364,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
             Q(comments__icontains=value)
         ).distinct()
 
-    def _mac_address(self, queryset, value):
+    def _mac_address(self, queryset, name, value):
         value = value.strip()
         if not value:
             return queryset
@@ -402,8 +417,8 @@ class PowerOutletFilter(DeviceComponentFilterSet):
 
 
 class InterfaceFilter(DeviceComponentFilterSet):
-    type = django_filters.MethodFilter(
-        action='filter_type',
+    type = django_filters.CharFilter(
+        method='filter_type',
         label='Interface type',
     )
 
@@ -411,7 +426,7 @@ class InterfaceFilter(DeviceComponentFilterSet):
         model = Interface
         fields = ['name']
 
-    def filter_type(self, queryset, value):
+    def filter_type(self, queryset, name, value):
         value = value.strip().lower()
         if value == 'physical':
             return queryset.exclude(form_factor__in=VIRTUAL_IFACE_TYPES)
@@ -437,51 +452,51 @@ class ModuleFilter(DeviceComponentFilterSet):
 
 
 class ConsoleConnectionFilter(django_filters.FilterSet):
-    site = django_filters.MethodFilter(
-        action='filter_site',
+    site = django_filters.CharFilter(
+        method='filter_site',
         label='Site (slug)',
     )
 
     class Meta:
         model = ConsoleServerPort
+        fields = []
 
-    def filter_site(self, queryset, value):
-        value = value.strip()
-        if not value:
+    def filter_site(self, queryset, name, value):
+        if not value.strip():
             return queryset
-        return queryset.filter(cs_port__device__rack__site__slug=value)
+        return queryset.filter(cs_port__device__site__slug=value)
 
 
 class PowerConnectionFilter(django_filters.FilterSet):
-    site = django_filters.MethodFilter(
-        action='filter_site',
+    site = django_filters.CharFilter(
+        method='filter_site',
         label='Site (slug)',
     )
 
     class Meta:
         model = PowerOutlet
+        fields = []
 
-    def filter_site(self, queryset, value):
-        value = value.strip()
-        if not value:
+    def filter_site(self, queryset, name, value):
+        if not value.strip():
             return queryset
-        return queryset.filter(power_outlet__device__rack__site__slug=value)
+        return queryset.filter(power_outlet__device__site__slug=value)
 
 
 class InterfaceConnectionFilter(django_filters.FilterSet):
-    site = django_filters.MethodFilter(
-        action='filter_site',
+    site = django_filters.CharFilter(
+        method='filter_site',
         label='Site (slug)',
     )
 
     class Meta:
         model = InterfaceConnection
+        fields = []
 
-    def filter_site(self, queryset, value):
-        value = value.strip()
-        if not value:
+    def filter_site(self, queryset, name, value):
+        if not value.strip():
             return queryset
         return queryset.filter(
-            Q(interface_a__device__rack__site__slug=value) |
-            Q(interface_b__device__rack__site__slug=value)
+            Q(interface_a__device__site__slug=value) |
+            Q(interface_b__device__site__slug=value)
         )

+ 24 - 10
netbox/dcim/forms.py

@@ -281,13 +281,25 @@ class RackBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class RackFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Rack
     q = forms.CharField(required=False, label='Search')
-    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('racks')), to_field_name='slug')
-    group_id = FilterChoiceField(queryset=RackGroup.objects.select_related('site')
-                                 .annotate(filter_count=Count('racks')), label='Rack group', null_option=(0, 'None'))
-    tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
-                               null_option=(0, 'None'))
-    role = FilterChoiceField(queryset=RackRole.objects.annotate(filter_count=Count('racks')), to_field_name='slug',
-                             null_option=(0, 'None'))
+    site = FilterChoiceField(
+        queryset=Site.objects.annotate(filter_count=Count('racks')),
+        to_field_name='slug'
+    )
+    group_id = FilterChoiceField(
+        queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks')),
+        label='Rack group',
+        null_option=(0, 'None')
+    )
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.annotate(filter_count=Count('racks')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
+    role = FilterChoiceField(
+        queryset=RackRole.objects.annotate(filter_count=Count('racks')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
 
 
 #
@@ -359,8 +371,10 @@ class DeviceTypeBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class DeviceTypeFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = DeviceType
     q = forms.CharField(required=False, label='Search')
-    manufacturer = FilterChoiceField(queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
-                                     to_field_name='slug')
+    manufacturer = FilterChoiceField(
+        queryset=Manufacturer.objects.annotate(filter_count=Count('device_types')),
+        to_field_name='slug'
+    )
 
 
 #
@@ -724,7 +738,7 @@ class DeviceFilterForm(BootstrapMixin, CustomFieldFilterForm):
     )
     rack_group_id = FilterChoiceField(
         queryset=RackGroup.objects.select_related('site').annotate(filter_count=Count('racks__devices')),
-        label='Rack Group',
+        label='Rack group',
     )
     role = FilterChoiceField(
         queryset=DeviceRole.objects.annotate(filter_count=Count('devices')),

+ 21 - 0
netbox/dcim/migrations/0032_device_increase_name_length.py

@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.4 on 2017-03-02 15:09
+from __future__ import unicode_literals
+
+from django.db import migrations
+import utilities.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0031_regions'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='device',
+            name='name',
+            field=utilities.fields.NullableCharField(blank=True, max_length=64, null=True, unique=True),
+        ),
+    ]

+ 1 - 1
netbox/dcim/models.py

@@ -915,7 +915,7 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
     device_role = models.ForeignKey('DeviceRole', related_name='devices', on_delete=models.PROTECT)
     tenant = models.ForeignKey(Tenant, blank=True, null=True, related_name='devices', on_delete=models.PROTECT)
     platform = models.ForeignKey('Platform', related_name='devices', blank=True, null=True, on_delete=models.SET_NULL)
-    name = NullableCharField(max_length=50, blank=True, null=True, unique=True)
+    name = NullableCharField(max_length=64, blank=True, null=True, unique=True)
     serial = models.CharField(max_length=50, blank=True, verbose_name='Serial number')
     asset_tag = NullableCharField(max_length=50, blank=True, null=True, unique=True, verbose_name='Asset tag',
                                   help_text='A unique tag used to identify this device')

+ 7 - 2
netbox/dcim/tables.py

@@ -94,6 +94,12 @@ STATUS_ICON = """
 {% endif %}
 """
 
+DEVICE_PRIMARY_IP = """
+{{ record.primary_ip6.address.ip|default:"" }}
+{% if record.primary_ip6 and record.primary_ip4 %}<br />{% endif %}
+{{ record.primary_ip4.address.ip|default:"" }}
+"""
+
 UTILIZATION_GRAPH = """
 {% load helpers %}
 {% utilization_graph value %}
@@ -106,7 +112,6 @@ UTILIZATION_GRAPH = """
 
 class RegionTable(BaseTable):
     pk = ToggleColumn()
-    # name = tables.LinkColumn(verbose_name='Name')
     name = tables.TemplateColumn(template_code=REGION_LINK, orderable=False)
     site_count = tables.Column(verbose_name='Sites')
     slug = tables.Column(verbose_name='Slug')
@@ -365,7 +370,7 @@ class DeviceTable(BaseTable):
     device_type = tables.LinkColumn('dcim:devicetype', args=[Accessor('device_type.pk')], verbose_name='Type',
                                     text=lambda record: record.device_type.full_name)
     primary_ip = tables.TemplateColumn(orderable=False, verbose_name='IP Address',
-                                       template_code="{{ record.primary_ip.address.ip }}")
+                                       template_code=DEVICE_PRIMARY_IP)
 
     class Meta(BaseTable.Meta):
         model = Device

+ 3 - 1
netbox/ipam/fields.py

@@ -6,7 +6,7 @@ from django.db import models
 from .formfields import IPFormField
 from .lookups import (
     EndsWith, IEndsWith, IRegex, IStartsWith, NetContained, NetContainedOrEqual, NetContains, NetContainsOrEquals,
-    NetHost, Regex, StartsWith,
+    NetHost, NetMaskLength, Regex, StartsWith,
 )
 
 
@@ -67,6 +67,7 @@ IPNetworkField.register_lookup(NetContainedOrEqual)
 IPNetworkField.register_lookup(NetContains)
 IPNetworkField.register_lookup(NetContainsOrEquals)
 IPNetworkField.register_lookup(NetHost)
+IPNetworkField.register_lookup(NetMaskLength)
 
 
 class IPAddressField(BaseIPField):
@@ -90,3 +91,4 @@ IPAddressField.register_lookup(NetContainedOrEqual)
 IPAddressField.register_lookup(NetContains)
 IPAddressField.register_lookup(NetContainsOrEquals)
 IPAddressField.register_lookup(NetHost)
+IPAddressField.register_lookup(NetMaskLength)

+ 53 - 56
netbox/ipam/filters.py

@@ -13,15 +13,10 @@ from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLAN
 
 
 class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
-    name = django_filters.CharFilter(
-        name='name',
-        lookup_type='icontains',
-        label='Name',
-    )
     tenant_id = NullableModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
@@ -34,7 +29,9 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
         label='Tenant (slug)',
     )
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(name__icontains=value) |
             Q(rd__icontains=value) |
@@ -43,7 +40,7 @@ class VRFFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = VRF
-        fields = ['rd']
+        fields = ['name', 'rd']
 
 
 class RIRFilter(django_filters.FilterSet):
@@ -54,8 +51,8 @@ class RIRFilter(django_filters.FilterSet):
 
 
 class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     rir_id = django_filters.ModelMultipleChoiceFilter(
@@ -74,7 +71,9 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
         model = Aggregate
         fields = ['family', 'date_added']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         qs_filter = Q(description__icontains=value)
         try:
             prefix = str(IPNetwork(value.strip()).cidr)
@@ -85,14 +84,18 @@ class AggregateFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
 
 class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
-    parent = django_filters.MethodFilter(
-        action='search_by_parent',
+    parent = django_filters.CharFilter(
+        method='search_by_parent',
         label='Parent prefix',
     )
+    mask_length = django_filters.NumberFilter(
+        method='filter_mask_length',
+        label='Mask length',
+    )
     vrf_id = NullableModelMultipleChoiceFilter(
         name='vrf_id',
         queryset=VRF.objects.all(),
@@ -151,7 +154,9 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         model = Prefix
         fields = ['family', 'status']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         qs_filter = Q(description__icontains=value)
         try:
             prefix = str(IPNetwork(value.strip()).cidr)
@@ -160,7 +165,7 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
             pass
         return queryset.filter(qs_filter)
 
-    def search_by_parent(self, queryset, value):
+    def search_by_parent(self, queryset, name, value):
         value = value.strip()
         if not value:
             return queryset
@@ -170,34 +175,25 @@ class PrefixFilter(CustomFieldFilterSet, django_filters.FilterSet):
         except AddrFormatError:
             return queryset.none()
 
-    def _tenant(self, queryset, value):
-        if str(value) == '':
+    def filter_mask_length(self, queryset, name, value):
+        if not value:
             return queryset
-        return queryset.filter(
-            Q(tenant__slug=value) |
-            Q(tenant__isnull=True, vrf__tenant__slug=value)
-        )
-
-    def _tenant_id(self, queryset, value):
-        try:
-            value = int(value)
-        except ValueError:
-            return queryset.none()
-        return queryset.filter(
-            Q(tenant__pk=value) |
-            Q(tenant__isnull=True, vrf__tenant__pk=value)
-        )
+        return queryset.filter(prefix__net_mask_length=value)
 
 
 class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
-    parent = django_filters.MethodFilter(
-        action='search_by_parent',
+    parent = django_filters.CharFilter(
+        method='search_by_parent',
         label='Parent prefix',
     )
+    mask_length = django_filters.NumberFilter(
+        method='filter_mask_length',
+        label='Mask length',
+    )
     vrf_id = NullableModelMultipleChoiceFilter(
         name='vrf_id',
         queryset=VRF.objects.all(),
@@ -239,9 +235,11 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = IPAddress
-        fields = ['q', 'family', 'status']
+        fields = ['family', 'status']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         qs_filter = Q(description__icontains=value)
         try:
             ipaddress = str(IPNetwork(value.strip()))
@@ -250,16 +248,21 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
             pass
         return queryset.filter(qs_filter)
 
-    def search_by_parent(self, queryset, value):
+    def search_by_parent(self, queryset, name, value):
         value = value.strip()
         if not value:
             return queryset
         try:
-            query = str(IPNetwork(value).cidr)
+            query = str(IPNetwork(value.strip()).cidr)
             return queryset.filter(address__net_contained_or_equal=query)
         except AddrFormatError:
             return queryset.none()
 
+    def filter_mask_length(self, queryset, name, value):
+        if not value:
+            return queryset
+        return queryset.filter(address__net_mask_length=value)
+
 
 class VLANGroupFilter(django_filters.FilterSet):
     site_id = NullableModelMultipleChoiceFilter(
@@ -276,11 +279,12 @@ class VLANGroupFilter(django_filters.FilterSet):
 
     class Meta:
         model = VLANGroup
+        fields = ['name']
 
 
 class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     site_id = NullableModelMultipleChoiceFilter(
@@ -305,15 +309,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         to_field_name='slug',
         label='Group',
     )
-    name = django_filters.CharFilter(
-        name='name',
-        lookup_type='icontains',
-        label='Name',
-    )
-    vid = django_filters.NumberFilter(
-        name='vid',
-        label='VLAN number (1-4095)',
-    )
     tenant_id = NullableModelMultipleChoiceFilter(
         name='tenant',
         queryset=Tenant.objects.all(),
@@ -339,12 +334,14 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = VLAN
-        fields = ['status']
+        fields = ['name', 'vid', 'status']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
         try:
-            qs_filter |= Q(vid=int(value))
+            qs_filter |= Q(vid=int(value.strip()))
         except ValueError:
             pass
         return queryset.filter(qs_filter)

+ 68 - 25
netbox/ipam/forms.py

@@ -21,6 +21,12 @@ IP_FAMILY_CHOICES = [
     (6, 'IPv6'),
 ]
 
+PREFIX_MASK_LENGTH_CHOICES = [
+    ('', '---------'),
+] + [(i, i) for i in range(1, 128)]
+
+IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
+
 
 #
 # VRFs
@@ -131,8 +137,11 @@ class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Aggregate
     q = forms.CharField(required=False, label='Search')
     family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
-    rir = FilterChoiceField(queryset=RIR.objects.annotate(filter_count=Count('aggregates')), to_field_name='slug',
-                            label='RIR')
+    rir = FilterChoiceField(
+        queryset=RIR.objects.annotate(filter_count=Count('aggregates')),
+        to_field_name='slug',
+        label='RIR'
+    )
 
 
 #
@@ -259,19 +268,33 @@ 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={
+    parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={
         'placeholder': 'Prefix',
     }))
-    family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
-    vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('prefixes')), to_field_name='rd',
-                            label='VRF', null_option=(0, 'Global'))
-    tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
-                               null_option=(0, 'None'))
+    family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
+    mask_length = forms.ChoiceField(required=False, choices=PREFIX_MASK_LENGTH_CHOICES, label='Mask length')
+    vrf = FilterChoiceField(
+        queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
+        to_field_name='rd',
+        label='VRF',
+        null_option=(0, 'Global')
+    )
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
     status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
-    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
-                             null_option=(0, 'None'))
-    role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('prefixes')), to_field_name='slug',
-                             null_option=(0, 'None'))
+    site = FilterChoiceField(
+        queryset=Site.objects.annotate(filter_count=Count('prefixes')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
+    role = FilterChoiceField(
+        queryset=Role.objects.annotate(filter_count=Count('prefixes')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
     expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
 
 
@@ -487,11 +510,19 @@ class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
     parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
         'placeholder': 'Prefix',
     }))
-    family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
-    vrf = FilterChoiceField(queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')), to_field_name='rd',
-                            label='VRF', null_option=(0, 'Global'))
-    tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
-                               to_field_name='slug', null_option=(0, 'None'))
+    family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
+    mask_length = forms.ChoiceField(required=False, choices=IPADDRESS_MASK_LENGTH_CHOICES, label='Mask length')
+    vrf = FilterChoiceField(
+        queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
+        to_field_name='rd',
+        label='VRF',
+        null_option=(0, 'Global')
+    )
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
     status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
 
 
@@ -603,15 +634,27 @@ def vlan_status_choices():
 class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = VLAN
     q = forms.CharField(required=False, label='Search')
-    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
-                             null_option=(0, 'Global'))
-    group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
-                                 null_option=(0, 'None'))
-    tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
-                               null_option=(0, 'None'))
+    site = FilterChoiceField(
+        queryset=Site.objects.annotate(filter_count=Count('vlans')),
+        to_field_name='slug',
+        null_option=(0, 'Global')
+    )
+    group_id = FilterChoiceField(
+        queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
+        label='VLAN group',
+        null_option=(0, 'None')
+    )
+    tenant = FilterChoiceField(
+        queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
     status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
-    role = FilterChoiceField(queryset=Role.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
-                             null_option=(0, 'None'))
+    role = FilterChoiceField(
+        queryset=Role.objects.annotate(filter_count=Count('vlans')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )
 
 
 #

+ 10 - 1
netbox/ipam/lookups.py

@@ -1,4 +1,4 @@
-from django.db.models import Lookup
+from django.db.models import Lookup, Transform, IntegerField
 from django.db.models.lookups import BuiltinLookup
 
 
@@ -87,3 +87,12 @@ class NetHost(Lookup):
             rhs_params[0] = rhs_params[0].split('/')[0]
         params = lhs_params + rhs_params
         return 'HOST(%s) = %s' % (lhs, rhs), params
+
+
+class NetMaskLength(Transform):
+    lookup_name = 'net_mask_length'
+    function = 'MASKLEN'
+
+    @property
+    def output_field(self):
+        return IntegerField()

+ 1 - 1
netbox/netbox/settings.py

@@ -12,7 +12,7 @@ except ImportError:
                                "the documentation.")
 
 
-VERSION = '1.8.5-dev'
+VERSION = '1.9.1-dev'
 
 # Import local configuration
 for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:

+ 5 - 3
netbox/secrets/filters.py

@@ -7,8 +7,8 @@ from dcim.models import Device
 
 
 class SecretFilter(django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     role_id = django_filters.ModelMultipleChoiceFilter(
@@ -38,7 +38,9 @@ class SecretFilter(django_filters.FilterSet):
         model = Secret
         fields = ['name']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(name__icontains=value) |
             Q(device__name__icontains=value)

+ 4 - 1
netbox/secrets/forms.py

@@ -96,7 +96,10 @@ class SecretBulkEditForm(BootstrapMixin, BulkEditForm):
 
 class SecretFilterForm(BootstrapMixin, forms.Form):
     q = forms.CharField(required=False, label='Search')
-    role = FilterChoiceField(queryset=SecretRole.objects.annotate(filter_count=Count('secrets')), to_field_name='slug')
+    role = FilterChoiceField(
+        queryset=SecretRole.objects.annotate(filter_count=Count('secrets')),
+        to_field_name='slug'
+    )
 
 
 #

+ 6 - 4
netbox/tenancy/filters.py

@@ -8,8 +8,8 @@ from .models import Tenant, TenantGroup
 
 
 class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
-    q = django_filters.MethodFilter(
-        action='search',
+    q = django_filters.CharFilter(
+        method='search',
         label='Search',
     )
     group_id = NullableModelMultipleChoiceFilter(
@@ -26,9 +26,11 @@ class TenantFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
     class Meta:
         model = Tenant
-        fields = ['q', 'group_id', 'group', 'name']
+        fields = ['name']
 
-    def search(self, queryset, value):
+    def search(self, queryset, name, value):
+        if not value.strip():
+            return queryset
         return queryset.filter(
             Q(name__icontains=value) |
             Q(description__icontains=value) |

+ 5 - 2
netbox/tenancy/forms.py

@@ -56,5 +56,8 @@ class TenantBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
 class TenantFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = Tenant
     q = forms.CharField(required=False, label='Search')
-    group = FilterChoiceField(queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
-                              to_field_name='slug', null_option=(0, 'None'))
+    group = FilterChoiceField(
+        queryset=TenantGroup.objects.annotate(filter_count=Count('tenants')),
+        to_field_name='slug',
+        null_option=(0, 'None')
+    )

+ 4 - 0
requirements.txt

@@ -2,7 +2,11 @@ cffi>=1.8
 cryptography>=1.4
 Django>=1.10
 django-debug-toolbar>=1.6
+<<<<<<< HEAD
 django-filter==0.15.3
+=======
+django-filter>=1.0.1
+>>>>>>> develop
 django-mptt==0.8.7
 django-rest-swagger==0.3.10
 django-tables2>=1.2.5