mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into develop-2.8
This commit is contained in:
@ -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.
|
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.
|
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).
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
# NetBox v2.7 Release Notes
|
# NetBox v2.7 Release Notes
|
||||||
|
|
||||||
## v2.7.12 (FUTURE)
|
## v2.7.12 (2020-04-08)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#3676](https://github.com/netbox-community/netbox/issues/3676) - Reference VRF by name rather than RD during IP/prefix import
|
* [#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
|
* [#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
|
* [#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
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#4395](https://github.com/netbox-community/netbox/issues/4395) - Fix typing of count_ipaddresses on interface serializer
|
||||||
* [#4418](https://github.com/netbox-community/netbox/issues/4418) - Fail cleanly when trying to import multiple device types simultaneously
|
* [#4418](https://github.com/netbox-community/netbox/issues/4418) - Fail cleanly when trying to import multiple device types simultaneously
|
||||||
* [#4438](https://github.com/netbox-community/netbox/issues/4438) - Fix exception when disconnecting a cable from a power feed
|
* [#4438](https://github.com/netbox-community/netbox/issues/4438) - Fix exception when disconnecting a cable from a power feed
|
||||||
* [#4439](https://github.com/netbox-community/netbox/issues/4439) - Tweak display of unset custom integer fields
|
* [#4439](https://github.com/netbox-community/netbox/issues/4439) - Tweak display of unset custom integer fields
|
||||||
|
@ -533,6 +533,7 @@ class InterfaceSerializer(TaggitSerializer, ConnectedEndpointSerializer):
|
|||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
cable = NestedCableSerializer(read_only=True)
|
||||||
tags = TagListSerializerField(required=False)
|
tags = TagListSerializerField(required=False)
|
||||||
|
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from netbox.admin import admin_site
|
|
||||||
from utilities.forms import LaxURLField
|
from utilities.forms import LaxURLField
|
||||||
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook
|
from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, ReportResult, Webhook
|
||||||
from .reports import get_report
|
from .reports import get_report
|
||||||
@ -35,7 +34,7 @@ class WebhookForm(forms.ModelForm):
|
|||||||
order_content_types(self.fields['obj_type'])
|
order_content_types(self.fields['obj_type'])
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Webhook, site=admin_site)
|
@admin.register(Webhook)
|
||||||
class WebhookAdmin(admin.ModelAdmin):
|
class WebhookAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
|
'name', 'models', 'payload_url', 'http_content_type', 'enabled', 'type_create', 'type_update', 'type_delete',
|
||||||
@ -93,7 +92,7 @@ class CustomFieldChoiceAdmin(admin.TabularInline):
|
|||||||
extra = 5
|
extra = 5
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomField, site=admin_site)
|
@admin.register(CustomField)
|
||||||
class CustomFieldAdmin(admin.ModelAdmin):
|
class CustomFieldAdmin(admin.ModelAdmin):
|
||||||
inlines = [CustomFieldChoiceAdmin]
|
inlines = [CustomFieldChoiceAdmin]
|
||||||
list_display = [
|
list_display = [
|
||||||
@ -135,7 +134,7 @@ class CustomLinkForm(forms.ModelForm):
|
|||||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CustomLink, site=admin_site)
|
@admin.register(CustomLink)
|
||||||
class CustomLinkAdmin(admin.ModelAdmin):
|
class CustomLinkAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'name', 'content_type', 'group_name', 'weight',
|
'name', 'content_type', 'group_name', 'weight',
|
||||||
@ -150,7 +149,7 @@ class CustomLinkAdmin(admin.ModelAdmin):
|
|||||||
# Graphs
|
# Graphs
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(Graph, site=admin_site)
|
@admin.register(Graph)
|
||||||
class GraphAdmin(admin.ModelAdmin):
|
class GraphAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'name', 'type', 'weight', 'template_language', 'source',
|
'name', 'type', 'weight', 'template_language', 'source',
|
||||||
@ -178,7 +177,7 @@ class ExportTemplateForm(forms.ModelForm):
|
|||||||
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
self.fields['content_type'].choices.insert(0, ('', '---------'))
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ExportTemplate, site=admin_site)
|
@admin.register(ExportTemplate)
|
||||||
class ExportTemplateAdmin(admin.ModelAdmin):
|
class ExportTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'name', 'content_type', 'description', 'mime_type', 'file_extension',
|
'name', 'content_type', 'description', 'mime_type', 'file_extension',
|
||||||
@ -193,7 +192,7 @@ class ExportTemplateAdmin(admin.ModelAdmin):
|
|||||||
# Reports
|
# Reports
|
||||||
#
|
#
|
||||||
|
|
||||||
@admin.register(ReportResult, site=admin_site)
|
@admin.register(ReportResult)
|
||||||
class ReportResultAdmin(admin.ModelAdmin):
|
class ReportResultAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
'report', 'active', 'created', 'user', 'passing',
|
'report', 'active', 'created', 'user', 'passing',
|
||||||
|
85
netbox/extras/management/commands/webhook_receiver.py
Normal file
85
netbox/extras/management/commands/webhook_receiver.py
Normal 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...")
|
@ -1,21 +1,14 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.admin import AdminSite
|
from django.contrib.admin import site as admin_site
|
||||||
from django.contrib.auth.admin import GroupAdmin, UserAdmin
|
from taggit.models import Tag
|
||||||
from django.contrib.auth.models import Group, User
|
|
||||||
|
|
||||||
|
|
||||||
class NetBoxAdminSite(AdminSite):
|
# Override default AdminSite attributes so we can avoid creating and
|
||||||
"""
|
# registering our own class
|
||||||
Custom admin site
|
admin_site.site_header = 'NetBox Administration'
|
||||||
"""
|
admin_site.site_title = 'NetBox'
|
||||||
site_header = 'NetBox Administration'
|
admin_site.site_url = '/{}'.format(settings.BASE_PATH)
|
||||||
site_title = 'NetBox'
|
admin_site.index_template = 'admin/index.html'
|
||||||
site_url = '/{}'.format(settings.BASE_PATH)
|
|
||||||
index_template = 'admin/index.html'
|
|
||||||
|
|
||||||
|
# Unregister the unused stock Tag model provided by django-taggit
|
||||||
admin_site = NetBoxAdminSite(name='admin')
|
admin_site.unregister(Tag)
|
||||||
|
|
||||||
# Register external models
|
|
||||||
admin_site.register(Group, GroupAdmin)
|
|
||||||
admin_site.register(User, UserAdmin)
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
from django.contrib import admin, messages
|
from django.contrib import admin, messages
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
from netbox.admin import admin_site
|
|
||||||
from .forms import ActivateUserKeyForm
|
from .forms import ActivateUserKeyForm
|
||||||
from .models import UserKey
|
from .models import UserKey
|
||||||
|
|
||||||
|
|
||||||
@admin.register(UserKey, site=admin_site)
|
@admin.register(UserKey)
|
||||||
class UserKeyAdmin(admin.ModelAdmin):
|
class UserKeyAdmin(admin.ModelAdmin):
|
||||||
actions = ['activate_selected']
|
actions = ['activate_selected']
|
||||||
list_display = ['user', 'is_filled', 'is_active', 'created']
|
list_display = ['user', 'is_filled', 'is_active', 'created']
|
||||||
|
@ -3,14 +3,13 @@ from django.contrib import admin
|
|||||||
from django.contrib.auth.admin import UserAdmin as UserAdmin_
|
from django.contrib.auth.admin import UserAdmin as UserAdmin_
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from netbox.admin import admin_site
|
|
||||||
from .models import Token
|
from .models import Token
|
||||||
|
|
||||||
# Unregister the built-in UserAdmin so that we can use our custom admin view below
|
# Unregister the built-in UserAdmin so that we can use our custom admin view below
|
||||||
admin_site.unregister(User)
|
admin.site.unregister(User)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(User, site=admin_site)
|
@admin.register(User)
|
||||||
class UserAdmin(UserAdmin_):
|
class UserAdmin(UserAdmin_):
|
||||||
list_display = [
|
list_display = [
|
||||||
'username', 'email', 'first_name', 'last_name', 'is_superuser', 'is_staff', 'is_active'
|
'username', 'email', 'first_name', 'last_name', 'is_superuser', 'is_staff', 'is_active'
|
||||||
@ -30,7 +29,7 @@ class TokenAdminForm(forms.ModelForm):
|
|||||||
model = Token
|
model = Token
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Token, site=admin_site)
|
@admin.register(Token)
|
||||||
class TokenAdmin(admin.ModelAdmin):
|
class TokenAdmin(admin.ModelAdmin):
|
||||||
form = TokenAdminForm
|
form = TokenAdminForm
|
||||||
list_display = [
|
list_display = [
|
||||||
|
Reference in New Issue
Block a user