jimfunk / django-postgresql-netfields

Proper INET and CIDR fields for Django running on PostgreSQL
BSD 3-Clause "New" or "Revised" License
154 stars 65 forks source link

Lookup by host #85

Closed Someguy123 closed 5 years ago

Someguy123 commented 5 years ago

I've been playing with this project for the past few hours, and have been at a loss due to what seems like a major missing feature (or a completely undocumented feature)...

It should be possible to have a lookup such as __net_host or __net_address for direct IP lookups against an InetAddressField which has a CIDR prefix. This is already supported by PostgreSQL with WHERE HOST(my_inet_field) = '1.2.3.4'

Without this, I would have to store the IP address separately from the subnet (which defeats one of my main reasons for using the INET type) to be able to query them effectively by individual IP, without knowing the CIDR prefix beforehand.

Something which would operate similar to this:

SELECT  id, hostname, external_ip FROM servers;

   id    | hostname | external_ip
---------+----------+-------------
10       |     test | 1.2.3.4/22
11       |    test2 | 1.2.3.44/22

SELECT id, hostname, external_ip FROM servers WHERE HOST(external_ip) = '1.2.3.4';

   id    | hostname | external_ip
---------+----------+-------------
10       |     test | 1.2.3.4/22

It could be used like so:

Server.objects.get(external_ip__net_host='1.2.3.4')

Neither startswith nor endswith work for this, as they risk running into similar IP addresses, e.g. a query for 1.2.3.4 could bring results for 1.2.3.44 (starts with), or 11.2.3.4 (ends with).

jimfunk commented 5 years ago

Try:

Server.objects.get(external_ip='1.2.3.4')
Someguy123 commented 5 years ago

Try:

Server.objects.get(external_ip='1.2.3.4')

I had already tried this, but I simply get "matching query does not exist".

If it's any help, here's my django and netfields version:

django-netfields==0.9.0
Django==2.0.3
jimfunk commented 5 years ago

Ah, I misread. You want an In query. Try this:

Server.objects.get(external_ip__contains='1.2.3.4')
Someguy123 commented 5 years ago

Ah, I misread. You want an In query. Try this:

Server.objects.get(external_ip__contains='1.2.3.4')
 get() returned more than one Server -- it returned 2!

After trying it with filter() instead, it's of course matching both 1.2.3.4/22 and 1.2.3.44/22 - not an exact address match sadly.

jimfunk commented 5 years ago

Hmmm. I see.

This would require a new query type. I will take a look later.

Someguy123 commented 5 years ago

Hmmm. I see.

This would require a new query type. I will take a look later.

I thought so after looking through the code. I assumed that it should be as simple as adding a new lookup in lookups.py after noticing NetFieldDecoratorMixin uses HOST().

The lookup functions could do with some commenting so that it's easier for others to contribute to them - I had a difficult time figuring them out, otherwise I would've submitted a PR.

Thanks for the very fast response 👍

jimfunk commented 5 years ago

I just added a commit that implements _host. Can you try master and see if it works for you before I make a release?

To use it:

Server.objects.get(external_ip__host='1.2.3.4')
Someguy123 commented 5 years ago

It seems to work 👍

>>> Server.objects.filter(external_ip__host='1.2.3.4')
<QuerySet [<Server:  1.2.3.4/22 @ Sweden (owner: chris)>]>

>>> Server.objects.get(external_ip__host='1.2.3.4')
<Server: 1.2.3.4/22 @ Sweden (owner: chris)>

Thanks for implementing this so quickly

strikaco commented 5 years ago

Sorry to hijack but out of curiosity, what aspect of the initial problem is not served by __net_contains_or_equals?

It looks like you have a series of networks. You are trying to find which one(s?) a specific IP belongs to?

I think I have the same use case as you (found this issue while looking for examples), but not the same problem. The fact that you're asking this has me questioning if I've screwed something up myself.

This test passes:

class IP(models.Model):
    value = InetAddressField()
    objects = NetManager()

class IPTest(TestCase):
    def test_ip_creation(self):
        # Create a subnet
        ip, created = IP.objects.get_or_create(value='1.32.128.0/18')

        # Create an ip within it
        ip, created = IP.objects.get_or_create(value='1.32.128.22')

        # Create an unrelated subnet
        ip, created = IP.objects.get_or_create(value='5.101.221.0/24')

        # Pretend you have a single ip. Find all records containing it
        ips = IP.objects.filter(value__net_contains_or_equals='1.32.128.22')
        self.assertEqual(ips.count(), 2)

Printed values of ips:

[IPv4Interface('1.32.128.0/18'), IPv4Interface('1.32.128.22/32')]

What is different about your approach (or wrong with mine) that NCOE does not work? Or are you looking for a strict __net_equals operator?

jimfunk commented 5 years ago

The problem with the contains lookups is that you get unrelated matches for the use case. It's not networks that are being stored here, but combined network/netmask values referring to individual hosts. If I have 2 hosts in the same subnet, say 1.2.3.4/24 and 1.2.3.5/24, and I want to search for a record matching 1.2.3.4 without knowing it's prefix length, a common function of an IPAM, a contains lookup will return both records, since it matches the subnet, not the host.