auroraresearchlab / netbox-dns

Netbox Dns is a netbox plugin for managing zone, nameserver and record inventory.
MIT License
208 stars 20 forks source link

nameserver array is empty in netbox {{ data }} context data on zone create event. #302

Closed kemeris2000 closed 1 year ago

kemeris2000 commented 1 year ago

nameserver array is empty in netbox {{ data }} context data on zone create event. On zone update event for example array is filled.

{
        'id': 4,
        'url': '/api/plugins/netbox-dns/zones/44/',
        'name': 'zeusman4.lt',
        'view': '',
        'display': 'zeusman4.lt',
        'nameservers': [],
        'status': 'active',
        'description': '',
        'tags': [],
        'created': '2023-05-08T17:04:22.192959+03:00',
        'last_updated': '2023-05-08T17:04:22.192994+03:00',
        'default_ttl': 86400,
        'soa_ttl': 86400,
        'soa_mname': OrderedDict([('id', 1), ('url', '/api/plugins/netbox-dns/nameservers/1/'), ('display', 'ns1.zeusman.lt'), ('name', 'ns1.zeusman.lt')]),
        'soa_rname': 'admin.zeusman.lt',
        'soa_serial': 1683554663,
        'soa_serial_auto': True,
        'soa_refresh': 172800,
        'soa_retry': 7200,
        'soa_expire': 2592000,
        'soa_minimum': 3600,
        'custom_fields': {
            'task_netbox_rndc_exec': True
        }
    }
peteeckel commented 1 year ago

This is correct and a result of the way Django works internally.

The nameservers relation is a ManyToManyField, and thus it is not populated when the zone itself is created. So technically when the zone create event is triggered there are no nameservers yet, they get added in a separate step (which is why you see them in a zone update event).

This has some implications in other places in the code as well, but normally you don't see them - except when you handle events. There is, for instance, the following code in models.py:

@receiver(m2m_changed, sender=Zone.nameservers.through)
def update_ns_records(**kwargs):
    if kwargs.get("action") not in ["post_add", "post_remove"]:
        return

    zone = kwargs.get("instance")
    zone.update_ns_records()

This is necessary to internally handle updates to the name servers of a zone, because the nameservers are not necessarily up to date when a zone is created or updated. To create the NS records for a zone, we need to wait for the m2m_changed event for the name servers field.

This is also explained in https://github.com/netbox-community/netbox/issues/6284.

I'm not sure if there's a simple way to handle the issue, but I will look into it.

peteeckel commented 1 year ago

I just had a closer look at it and created a webhook debug setup that triggers on zone creation and update. Then I created a zone and got in total three events: The create event and two update events.

[1] Sun, 14 May 2023 15:41:46 GMT 127.0.0.1 "POST / HTTP/1.1" 200 -
Host: localhost:9000
Accept-Encoding: identity
Content-Type: application/json
Content-Length: 1449
User-Agent: python-urllib3/1.26.15

{
    "event": "created",
    "timestamp": "2023-05-14 15:41:46.214018+00:00",
    "model": "zone",
    "username": "admin",
    "request_id": "bb74c9b4-72ee-49f3-b797-57c0c3263cfc",
    "data": {
        "id": 61,
        "url": "/api/plugins/netbox-dns/zones/61/",
        "name": "zone31.example.com",
        "view": {
            "id": 2,
            "url": "/api/plugins/netbox-dns/views/2/",
            "display": "external",
            "name": "external"
        },
        "display": "[external] zone31.example.com",
        "nameservers": [],
        "status": "active",
        "description": "",
        "tags": [],
        "created": "2023-05-14T15:41:46.055321Z",
        "last_updated": "2023-05-14T15:41:46.055479Z",
        "default_ttl": 86400,
        "soa_ttl": 86400,
        "soa_mname": {
            "id": 1,
            "url": "/api/plugins/netbox-dns/nameservers/1/",
            "display": "ns1.example.com",
            "name": "ns1.example.com"
        },
        "soa_rname": "hostmaster.example.com",
        "soa_serial": 1684078907,
        "soa_serial_auto": true,
        "soa_refresh": 172800,
        "soa_retry": 7200,
        "soa_expire": 2592000,
        "soa_minimum": 3600,
        "custom_fields": {}
    },
    "snapshots": {
        "prechange": null,
        "postchange": {
            "created": "2023-05-14T15:41:46.055Z",
            "last_updated": "2023-05-14T15:41:46.055Z",
            "view": 2,
            "name": "zone31.example.com",
            "status": "active",
            "default_ttl": 86400,
            "soa_ttl": 86400,
            "soa_mname": 1,
            "soa_rname": "hostmaster.example.com",
            "soa_serial": 1684078907,
            "soa_refresh": 172800,
            "soa_retry": 7200,
            "soa_expire": 2592000,
            "soa_minimum": 3600,
            "soa_serial_auto": true,
            "description": "",
            "arpa_network": null,
            "nameservers": [],
            "custom_fields": {},
            "tags": []
        }
    }
}
Completed request #1
------------
[2] Sun, 14 May 2023 15:41:46 GMT 127.0.0.1 "POST / HTTP/1.1" 200 -
Host: localhost:9000
Accept-Encoding: identity
Content-Type: application/json
Content-Length: 1685
User-Agent: python-urllib3/1.26.15

{
    "event": "updated",
    "timestamp": "2023-05-14 15:41:46.222865+00:00",
    "model": "zone",
    "username": "admin",
    "request_id": "bb74c9b4-72ee-49f3-b797-57c0c3263cfc",
    "data": {
        "id": 61,
        "url": "/api/plugins/netbox-dns/zones/61/",
        "name": "zone31.example.com",
        "view": {
            "id": 2,
            "url": "/api/plugins/netbox-dns/views/2/",
            "display": "external",
            "name": "external"
        },
        "display": "[external] zone31.example.com",
        "nameservers": [
            {
                "id": 2,
                "url": "/api/plugins/netbox-dns/nameservers/2/",
                "display": "ns2.example.com",
                "name": "ns2.example.com"
            },
            {
                "id": 3,
                "url": "/api/plugins/netbox-dns/nameservers/3/",
                "display": "ns3.example.com",
                "name": "ns3.example.com"
            }
        ],
        "status": "active",
        "description": "",
        "tags": [],
        "created": "2023-05-14T15:41:46.055321Z",
        "last_updated": "2023-05-14T15:41:46.140365Z",
        "default_ttl": 86400,
        "soa_ttl": 86400,
        "soa_mname": {
            "id": 1,
            "url": "/api/plugins/netbox-dns/nameservers/1/",
            "display": "ns1.example.com",
            "name": "ns1.example.com"
        },
        "soa_rname": "hostmaster.example.com",
        "soa_serial": 1684078907,
        "soa_serial_auto": true,
        "soa_refresh": 172800,
        "soa_retry": 7200,
        "soa_expire": 2592000,
        "soa_minimum": 3600,
        "custom_fields": {}
    },
    "snapshots": {
        "prechange": null,
        "postchange": {
            "created": "2023-05-14T15:41:46.055Z",
            "last_updated": "2023-05-14T15:41:46.140Z",
            "view": 2,
            "name": "zone31.example.com",
            "status": "active",
            "default_ttl": 86400,
            "soa_ttl": 86400,
            "soa_mname": 1,
            "soa_rname": "hostmaster.example.com",
            "soa_serial": 1684078907,
            "soa_refresh": 172800,
            "soa_retry": 7200,
            "soa_expire": 2592000,
            "soa_minimum": 3600,
            "soa_serial_auto": true,
            "description": "",
            "arpa_network": null,
            "nameservers": [
                2,
                3
            ],
            "custom_fields": {},
            "tags": []
        }
    }
}
Completed request #2
------------
[3] Sun, 14 May 2023 15:41:46 GMT 127.0.0.1 "POST / HTTP/1.1" 200 -
Host: localhost:9000
Accept-Encoding: identity
Content-Type: application/json
Content-Length: 1685
User-Agent: python-urllib3/1.26.15

{
    "event": "updated",
    "timestamp": "2023-05-14 15:41:46.223925+00:00",
    "model": "zone",
    "username": "admin",
    "request_id": "bb74c9b4-72ee-49f3-b797-57c0c3263cfc",
    "data": {
        "id": 61,
        "url": "/api/plugins/netbox-dns/zones/61/",
        "name": "zone31.example.com",
        "view": {
            "id": 2,
            "url": "/api/plugins/netbox-dns/views/2/",
            "display": "external",
            "name": "external"
        },
        "display": "[external] zone31.example.com",
        "nameservers": [
            {
                "id": 2,
                "url": "/api/plugins/netbox-dns/nameservers/2/",
                "display": "ns2.example.com",
                "name": "ns2.example.com"
            },
            {
                "id": 3,
                "url": "/api/plugins/netbox-dns/nameservers/3/",
                "display": "ns3.example.com",
                "name": "ns3.example.com"
            }
        ],
        "status": "active",
        "description": "",
        "tags": [],
        "created": "2023-05-14T15:41:46.055321Z",
        "last_updated": "2023-05-14T15:41:46.176236Z",
        "default_ttl": 86400,
        "soa_ttl": 86400,
        "soa_mname": {
            "id": 1,
            "url": "/api/plugins/netbox-dns/nameservers/1/",
            "display": "ns1.example.com",
            "name": "ns1.example.com"
        },
        "soa_rname": "hostmaster.example.com",
        "soa_serial": 1684078907,
        "soa_serial_auto": true,
        "soa_refresh": 172800,
        "soa_retry": 7200,
        "soa_expire": 2592000,
        "soa_minimum": 3600,
        "custom_fields": {}
    },
    "snapshots": {
        "prechange": null,
        "postchange": {
            "created": "2023-05-14T15:41:46.055Z",
            "last_updated": "2023-05-14T15:41:46.176Z",
            "view": 2,
            "name": "zone31.example.com",
            "status": "active",
            "default_ttl": 86400,
            "soa_ttl": 86400,
            "soa_mname": 1,
            "soa_rname": "hostmaster.example.com",
            "soa_serial": 1684078907,
            "soa_refresh": 172800,
            "soa_retry": 7200,
            "soa_expire": 2592000,
            "soa_minimum": 3600,
            "soa_serial_auto": true,
            "description": "",
            "arpa_network": null,
            "nameservers": [
                2,
                3
            ],
            "custom_fields": {},
            "tags": []
        }
    }
}
Completed request #3
------------

Because of the behaviour mentioned above, there will always be a zone update event for a new zone after the zone create event if the zone actually has name servers.

I also checked the events sent when objects are created with tags. Now it gets interesting: When I create a zone with a tag (but no nameserver), I get one event. When I do the same with an IPAM IP Address, I get one create event with the tags included. So there is a way to queue m2m updates properly. Let me see if we can jump on that train ... :-)

kemeris2000 commented 1 year ago

I just had a closer look at it and created a webhook debug setup that triggers on zone creation and update. Then I created a zone and got in total three events: The create event and two update events.

Because of the behaviour mentioned above, there will always be a zone update event for a new zone after the zone create event if the zone actually has name servers.

I also checked the events sent when objects are created with tags. Now it gets interesting: When I create a zone with a tag (but no nameserver), I get one event. When I do the same with an IPAM IP Address, I get one create event with the tags included. So there is a way to queue m2m updates properly. Let me see if we can jump on that train ... :-)

I observe the following:

case: 1 nameserver 0 tags result: create, update

case: 2 nameserver 0 tags result: create, update, update

case: 1 nameserver 1 tags result: create, update, update

case: 2 nameserver 1 tags result: create, update, update, update

In last case events "create" and first "update" produce identical "last_updated" timestamp.

I guess at the moment of script execution on create event, database table "netbox_dns_zone_nameservers" is not populated yet?

kemeris2000 commented 1 year ago

At the moment I see only one dirty solution to store zone "id" and "last_updated" values locally on disk on create event, and send zone to BIND on last update event if "id" matches, difference of "last_updated" is less 1 second or so and nameservers exist.

peteeckel commented 1 year ago

I delved a bit deeper into this yesterday, and I found some information that I currently can't piece together properly. Concentrating on tags first, because that is something that applies to all models, I found that for some reason tags are included in the create event for View and NameServer, but not for Record and Zone.

That indicates that some of NetBox DNS' internal processing interferes with the way NetBox works around the m2m_changed problem. I'll debug that further as soon as I can, this is really interesting.

peteeckel commented 1 year ago

At the moment I see only one dirty solution to store zone "id" and "last_updated" values locally on disk on create event, and send zone to BIND on last update event if "id" matches, difference of "last_updated" is less 1 second or so and nameservers exist.

This is very similar to the way NetBox implements the workaround ... they queue Webhooks and flush them once everything is done.

Considering my earlier advice not to flush every change to the name server database it's not that dirty at all, though I would really like to find the root cause of the incomplete create events.

kemeris2000 commented 1 year ago

I will wait for results of your attempt.

kemeris2000 commented 1 year ago

Hi peteeckel, any progress on this issue?

peteeckel commented 1 year ago

Hi @kemeris2000, there are three ways to deal with this:

  1. Wait until I find the time to work on it in my spare time,
  2. Find the solution and open a PR,
  3. Ask for a consultancy quotation.

Have a nice weekend!

peteeckel commented 1 year ago

Hi @kemeris2000, it does not look like this can be solved easily within NetBox DNS or even NetBox.

I ran some tests with custom signal listeners on post_save and m2m_changed events for Zone and Zone.nameservers.through, and the serialize_object() method on the zone object does not seem to have the nameservers array populated in either.

That means that the code within NetBox that triggers the Webhooks can't see them either, which explains why the Webhook data does not contain data for nameservers as well.

This might be an issue within NetBox (the serialize_object() method could be flawed in some way) or within Django (unlikely, but possible). I'm not sure what triggers the issue, but clearly something is not working as expected.

kemeris2000 commented 1 year ago

Thank you peteeckel for you time and effort! I guess it's not worth to investigate further. At the moment I propose simple workaround for everyone else who is interested: do not trigger any actions on create event and handle zone creation and modifications on update event. Netbox webhook condition filters out unneeded update events:

{
    "and": [
        {
            "attr": "nameservers",
            "negate": true,
            "value": null
        }
    ]
}