Merge pull request #4462 from netbox-community/4460-webhook-listener

Closes #4460: Add a webhook listener for troubleshooting
This commit is contained in:
Jeremy Stretch 2020-04-07 13:56:35 -04:00 committed by GitHub
commit eb2e960af4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 119 additions and 0 deletions

View File

@ -71,3 +71,36 @@ If no body template is specified, the request body will be populated with a JSON
When a change is detected, any resulting webhooks are placed into a Redis queue for processing. This allows the user's request to complete without needing to wait for the outgoing webhook(s) to be processed. The webhooks are then extracted from the queue by the `rqworker` process and HTTP requests are sent to their respective destinations. The current webhook queue and any failed webhooks can be inspected in the admin UI under Django RQ > Queues.
A request is considered successful if the response has a 2XX status code; otherwise, the request is marked as having failed. Failed requests may be retried manually via the admin UI.
## Troubleshooting
To assist with verifying that the content of outgoing webhooks is rendered correctly, NetBox provides a simple HTTP listener that can be run locally to receive and display webhook requests. First, modify the target URL of the desired webhook to `http://localhost:9000/`. This will instruct NetBox to send the request to the local server on TCP port 9000. Then, start the webhook receiver service from the NetBox root directory:
```no-highlight
$ python netbox/manage.py webhook_receiver
Listening on port http://localhost:9000. Stop with CONTROL-C.
```
You can test the receiver itself by sending any HTTP request to it. For example:
```no-highlight
$ curl -X POST http://localhost:9000 --data '{"foo": "bar"}'
```
The server will print output similar to the following:
```no-highlight
[1] Tue, 07 Apr 2020 17:44:02 GMT 127.0.0.1 "POST / HTTP/1.1" 200 -
Host: localhost:9000
User-Agent: curl/7.58.0
Accept: */*
Content-Length: 14
Content-Type: application/x-www-form-urlencoded
{"foo": "bar"}
------------
```
Note that `webhook_receiver` does not actually _do_ anything with the information received: It merely prints the request headers and body for inspection.
Now, when the NetBox webhook is triggered and processed, you should see its headers and content appear in the terminal where the webhook receiver is listening. If you don't, check that the `rqworker` process is running and that webhook events are being placed into the queue (visible under the NetBox admin UI).

View File

@ -7,6 +7,7 @@
* [#3676](https://github.com/netbox-community/netbox/issues/3676) - Reference VRF by name rather than RD during IP/prefix import
* [#4147](https://github.com/netbox-community/netbox/issues/4147) - Use absolute URLs in rack elevation SVG renderings
* [#4448](https://github.com/netbox-community/netbox/issues/4448) - Allow connecting cables between two circuit terminations
* [#4460](https://github.com/netbox-community/netbox/issues/4460) - Add the `webhook_receiver` management command to assist in troubleshooting outgoing webhooks
### Bug Fixes

View File

@ -0,0 +1,85 @@
import sys
from http.server import HTTPServer, BaseHTTPRequestHandler
from django.core.management.base import BaseCommand
request_counter = 1
class WebhookHandler(BaseHTTPRequestHandler):
show_headers = True
def __getattr__(self, item):
# Return the same method for any type of HTTP request (GET, POST, etc.)
if item.startswith('do_'):
return self.do_ANY
raise AttributeError
def log_message(self, format_str, *args):
global request_counter
print("[{}] {} {} {}".format(
request_counter,
self.date_time_string(),
self.address_string(),
format_str % args
))
def do_ANY(self):
global request_counter
# Send a 200 response regardless of the request content
self.send_response(200)
self.end_headers()
self.wfile.write(b'Webhook received!\n')
request_counter += 1
# Print the request headers to stdout
if self.show_headers:
for k, v in self.headers.items():
print('{}: {}'.format(k, v))
print()
# Print the request body (if any)
content_length = self.headers.get('Content-Length')
if content_length is not None:
body = self.rfile.read(int(content_length))
print(body.decode('utf-8'))
else:
print('(No body)')
print('------------')
class Command(BaseCommand):
help = "Start a simple listener to display received HTTP requests"
default_port = 9000
def add_arguments(self, parser):
parser.add_argument(
'--port', type=int, default=self.default_port,
help="Optional port number (default: {})".format(self.default_port)
)
parser.add_argument(
"--no-headers", action='store_true', dest='no_headers',
help="Hide HTTP request headers"
)
def handle(self, *args, **options):
port = options['port']
quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
WebhookHandler.show_headers = not options['no_headers']
self.stdout.write('Listening on port http://localhost:{}. Stop with {}.'.format(port, quit_command))
httpd = HTTPServer(('localhost', port), WebhookHandler)
try:
httpd.serve_forever()
except KeyboardInterrupt:
self.stdout.write("\nExiting...")