netbox-ping 0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ from netbox.plugins import PluginConfig
2
+
3
+ class Config(PluginConfig):
4
+ name = 'netbox_ping'
5
+ verbose_name = 'NetBox Ping'
6
+ description = 'Ping IPs and subnets'
7
+ version = '0.2'
8
+ author = 'Christian Rose'
9
+ default_settings = {
10
+ 'exclude_virtual_interfaces': True
11
+ }
12
+
13
+ # Register the custom table
14
+ ipaddress_table = 'netbox_ping.tables.CustomIPAddressTable'
15
+
16
+ # Define which models support custom fields
17
+ custom_field_models = ['ipaddress']
18
+
19
+ # API settings
20
+ base_url = 'netbox-ping'
21
+ default_app_config = 'netbox_ping.apps.NetBoxPingConfig'
22
+
23
+ config = Config
netbox_ping/config.py ADDED
@@ -0,0 +1,43 @@
1
+ from extras.choices import CustomFieldTypeChoices
2
+ from extras.models import CustomField, Tag
3
+ from django.db.models import Q
4
+
5
+ def create_custom_fields_and_tags():
6
+ """Create custom fields and tags needed by the plugin"""
7
+
8
+ # Create Up_Down custom field
9
+ custom_field, created = CustomField.objects.get_or_create(
10
+ name='Up_Down',
11
+ defaults={
12
+ 'type': CustomFieldTypeChoices.TYPE_BOOLEAN,
13
+ 'label': 'Up/Down Status',
14
+ 'description': 'Indicates if the IP is responding to ping',
15
+ 'required': False,
16
+ 'filter_logic': 'exact'
17
+ }
18
+ )
19
+
20
+ # Add the custom field to IPAddress content type if not already added
21
+ if created:
22
+ custom_field.content_types.add(
23
+ ContentType.objects.get(app_label='ipam', model='ipaddress')
24
+ )
25
+
26
+ # Create online/offline tags
27
+ Tag.objects.get_or_create(
28
+ name='online',
29
+ slug='online',
30
+ defaults={
31
+ 'description': 'IP is responding to ping',
32
+ 'color': '4CAF50' # Green color
33
+ }
34
+ )
35
+
36
+ Tag.objects.get_or_create(
37
+ name='offline',
38
+ slug='offline',
39
+ defaults={
40
+ 'description': 'IP is not responding to ping',
41
+ 'color': 'F44336' # Red color
42
+ }
43
+ )
netbox_ping/forms.py ADDED
@@ -0,0 +1,13 @@
1
+ from django import forms
2
+ from netbox.forms import NetBoxModelForm
3
+ from .models import PluginSettingsModel
4
+
5
+
6
+ class InterfaceComparisonForm(forms.Form):
7
+ add_to_device = forms.BooleanField(required=False)
8
+ remove_from_device = forms.BooleanField(required=False)
9
+
10
+ class PluginSettingsForm(NetBoxModelForm):
11
+ class Meta:
12
+ model = PluginSettingsModel
13
+ fields = ('update_tags',)
netbox_ping/hooks.py ADDED
@@ -0,0 +1,6 @@
1
+ from extras.plugins import PluginTemplateExtension
2
+ from . import tables
3
+
4
+ def override_ipaddress_table():
5
+ """Override the default IP address table to include Up_Down status"""
6
+ return tables.CustomIPAddressTable
@@ -0,0 +1,30 @@
1
+ from django.db import migrations, models
2
+ import django.db.models.deletion
3
+ import netbox.models.features
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ initial = True
8
+
9
+ dependencies = [
10
+ ('extras', '0001_initial'), # Base NetBox dependency
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='PluginSettingsModel',
16
+ fields=[
17
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
18
+ ('created', models.DateTimeField(auto_now_add=True)),
19
+ ('last_updated', models.DateTimeField(auto_now=True)),
20
+ ('custom_field_data', models.JSONField(blank=True, default=dict, null=True)),
21
+ ('update_tags', models.BooleanField(default=True, help_text='Whether to update tags when scanning IPs', verbose_name='Update Tags')),
22
+ ],
23
+ options={
24
+ 'verbose_name': 'Plugin Settings',
25
+ 'verbose_name_plural': 'Plugin Settings',
26
+ 'ordering': ['pk'],
27
+ },
28
+ bases=(netbox.models.features.ChangeLoggingMixin, models.Model),
29
+ ),
30
+ ]
File without changes
netbox_ping/models.py ADDED
@@ -0,0 +1,42 @@
1
+ from django.db import models
2
+ from netbox.models import NetBoxModel
3
+ from utilities.choices import ChoiceSet
4
+
5
+ class PluginSettingsModel(NetBoxModel):
6
+ """Store plugin settings"""
7
+ class Meta:
8
+ verbose_name = 'Plugin Settings'
9
+ verbose_name_plural = 'Plugin Settings'
10
+ ordering = ['pk']
11
+
12
+ # Required fields from NetBoxModel
13
+ id = models.BigAutoField(
14
+ primary_key=True
15
+ )
16
+ created = models.DateTimeField(
17
+ auto_now_add=True
18
+ )
19
+ last_updated = models.DateTimeField(
20
+ auto_now=True
21
+ )
22
+ custom_field_data = models.JSONField(
23
+ blank=True,
24
+ null=True,
25
+ default=dict
26
+ )
27
+
28
+ # Our custom fields
29
+ update_tags = models.BooleanField(
30
+ default=True,
31
+ verbose_name='Update Tags',
32
+ help_text='Whether to update tags when scanning IPs'
33
+ )
34
+
35
+ def __str__(self):
36
+ return "NetBox Ping Settings"
37
+
38
+ @classmethod
39
+ def get_settings(cls):
40
+ """Get or create settings"""
41
+ settings, _ = cls.objects.get_or_create(pk=1)
42
+ return settings
@@ -0,0 +1,17 @@
1
+ from netbox.plugins import PluginMenuItem, PluginMenuButton
2
+
3
+ menu_items = (
4
+ PluginMenuItem(
5
+ link='plugins:netbox_ping:ping_home',
6
+ link_text='Network Tools',
7
+ permissions=('ipam.view_prefix',),
8
+ buttons=(
9
+ PluginMenuButton(
10
+ link='plugins:netbox_ping:ping_home',
11
+ title='Network Tools',
12
+ icon_class='mdi mdi-lan',
13
+ permissions=('ipam.view_prefix',),
14
+ ),
15
+ ),
16
+ ),
17
+ )
netbox_ping/plugin.py ADDED
@@ -0,0 +1,83 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from extras.api.serializers import CustomFieldSerializer, TagSerializer
3
+ from extras.models import CustomField, Tag
4
+ from ipam.models import IPAddress
5
+ from rest_framework.exceptions import ValidationError
6
+
7
+ def initialize_plugin():
8
+ """Initialize plugin custom fields and tags using the API"""
9
+
10
+ # Get ContentType ID for IPAddress
11
+ ipaddress_ct_id = ContentType.objects.get_for_model(IPAddress).id
12
+
13
+ # Create Up_Down custom field
14
+ up_down_data = {
15
+ 'name': 'Up_Down',
16
+ 'type': 'boolean',
17
+ 'label': 'Up/Down Status',
18
+ 'description': 'Indicates if the IP is responding to ping',
19
+ 'required': False,
20
+ 'filter_logic': 'exact',
21
+ 'ui_visible': 'always',
22
+ 'ui_editable': 'yes',
23
+ 'is_cloneable': True,
24
+ 'weight': 100,
25
+ }
26
+
27
+ # Create Auto_discovered custom field
28
+ discovered_data = {
29
+ 'name': 'Auto_discovered',
30
+ 'type': 'date',
31
+ 'label': 'Auto Discovered',
32
+ 'description': 'Date when this IP was automatically discovered',
33
+ 'required': False,
34
+ 'filter_logic': 'exact',
35
+ 'ui_visible': 'always',
36
+ 'ui_editable': 'yes',
37
+ 'is_cloneable': True,
38
+ 'weight': 101,
39
+ }
40
+
41
+ # Create custom fields
42
+ for cf_data in [up_down_data, discovered_data]:
43
+ try:
44
+ custom_field = CustomField.objects.get(name=cf_data['name'])
45
+ except CustomField.DoesNotExist:
46
+ try:
47
+ custom_field = CustomField.objects.create(**cf_data)
48
+ custom_field.object_types.set([ipaddress_ct_id])
49
+ print(f"Created custom field: {custom_field.name}")
50
+ except Exception as e:
51
+ print(f"Failed to create custom field: {str(e)}")
52
+
53
+ # Create tags
54
+ tags_data = [
55
+ {
56
+ 'name': 'online',
57
+ 'slug': 'online',
58
+ 'description': 'IP is responding to ping',
59
+ 'color': '4CAF50'
60
+ },
61
+ {
62
+ 'name': 'offline',
63
+ 'slug': 'offline',
64
+ 'description': 'IP is not responding to ping',
65
+ 'color': 'F44336'
66
+ },
67
+ {
68
+ 'name': 'auto-discovered',
69
+ 'slug': 'auto-discovered',
70
+ 'description': 'IP was automatically discovered by scanning',
71
+ 'color': '2196F3' # Blue color
72
+ }
73
+ ]
74
+
75
+ for tag_data in tags_data:
76
+ try:
77
+ tag = Tag.objects.get(slug=tag_data['slug'])
78
+ except Tag.DoesNotExist:
79
+ try:
80
+ tag = Tag.objects.create(**tag_data)
81
+ print(f"Created tag: {tag.name}")
82
+ except Exception as e:
83
+ print(f"Failed to create tag: {str(e)}")
netbox_ping/signals.py ADDED
@@ -0,0 +1,48 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from django.apps import apps
3
+ from django.db.models.signals import post_migrate
4
+ from django.dispatch import receiver
5
+ from extras.choices import CustomFieldTypeChoices
6
+ from extras.models import CustomField, Tag
7
+
8
+ @receiver(post_migrate)
9
+ def create_custom_fields_and_tags(sender, **kwargs):
10
+ """
11
+ Create required custom fields and tags after database migrations complete
12
+ """
13
+ if sender.name == 'netbox_ping':
14
+ # Create Up_Down custom field
15
+ custom_field, _ = CustomField.objects.get_or_create(
16
+ name='Up_Down',
17
+ defaults={
18
+ 'type': CustomFieldTypeChoices.TYPE_BOOLEAN,
19
+ 'label': 'Up/Down Status',
20
+ 'description': 'Indicates if the IP is responding to ping',
21
+ 'required': False,
22
+ 'filter_logic': 'exact'
23
+ }
24
+ )
25
+
26
+ # Add the custom field to IPAddress content type
27
+ ipaddress_ct = ContentType.objects.get_for_model(apps.get_model('ipam', 'ipaddress'))
28
+ if ipaddress_ct not in custom_field.content_types.all():
29
+ custom_field.content_types.add(ipaddress_ct)
30
+
31
+ # Create online/offline tags
32
+ Tag.objects.get_or_create(
33
+ name='online',
34
+ slug='online',
35
+ defaults={
36
+ 'description': 'IP is responding to ping',
37
+ 'color': '4CAF50' # Green color
38
+ }
39
+ )
40
+
41
+ Tag.objects.get_or_create(
42
+ name='offline',
43
+ slug='offline',
44
+ defaults={
45
+ 'description': 'IP is not responding to ping',
46
+ 'color': 'F44336' # Red color
47
+ }
48
+ )
netbox_ping/tables.py ADDED
@@ -0,0 +1,22 @@
1
+ import django_tables2 as tables
2
+ from netbox.tables import NetBoxTable, columns
3
+ from ipam.models import IPAddress
4
+ from ipam.tables import IPAddressTable
5
+
6
+ class CustomIPAddressTable(IPAddressTable):
7
+ """Custom IP Address table that includes the Up_Down status"""
8
+
9
+ up_down = columns.BooleanColumn(
10
+ verbose_name='Ping Status',
11
+ accessor=tables.A('_custom_field_data__Up_Down'),
12
+ order_by='_custom_field_data__Up_Down',
13
+ )
14
+
15
+ online = columns.TagColumn(
16
+ verbose_name='Online/Offline'
17
+ )
18
+
19
+ class Meta(IPAddressTable.Meta):
20
+ model = IPAddress
21
+ fields = IPAddressTable.Meta.fields + ('up_down', 'online')
22
+ default_columns = ('address', 'status', 'up_down', 'online', 'tenant', 'assigned_object', 'description')
@@ -0,0 +1,15 @@
1
+ from netbox.plugins.template_content import PluginTemplateContent
2
+
3
+ class PrefixContent(PluginTemplateContent):
4
+ model = 'ipam.prefix'
5
+
6
+ def buttons(self):
7
+ prefix = self.context['object']
8
+ return f'''
9
+ <a href="/plugins/netbox_ping/ping-subnet/{prefix.id}/" class="btn btn-primary">
10
+ <span class="mdi mdi-lan"></span>
11
+ Ping Subnet
12
+ </a>
13
+ '''
14
+
15
+ template_content = [PrefixContent]
@@ -0,0 +1,15 @@
1
+ from netbox.plugins import PluginTemplateExtension
2
+
3
+ class PrefixExtension(PluginTemplateExtension):
4
+ model = 'ipam.prefix'
5
+
6
+ def buttons(self):
7
+ prefix = self.context['object']
8
+ return f'''
9
+ <a href="/plugins/netbox_ping/ping-subnet/{prefix.id}/" class="btn btn-primary">
10
+ <span class="mdi mdi-lan"></span>
11
+ Ping Subnet
12
+ </a>
13
+ '''
14
+
15
+ template_extensions = [PrefixExtension]
@@ -0,0 +1,4 @@
1
+ <a href="{% url 'plugins:netbox_ping:interface_comparison' device_id=device.id %}" class="btn btn-purple">
2
+ <i class="mdi mdi-sync" aria-hidden="true"></i>
3
+ Sync Interfaces
4
+ </a>
@@ -0,0 +1,161 @@
1
+ {% extends 'base/layout.html' %}
2
+
3
+ {% block title %}{{ device }} - Interface comparison{% endblock %}
4
+ {% block header %}
5
+ <nav class="breadcrumb-container px-3" aria-label="breadcrumb">
6
+ <ol class="breadcrumb">
7
+ <li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}">Device</a></li>
8
+ <li class="breadcrumb-item"><a href="{% url 'dcim:device_list' %}?site={{ device.site.slug }}">{{ device.site }}</a></li>
9
+ <li class="breadcrumb-item"><a href="{% url 'dcim:device' pk=device.id %}">{{ device }}</a></li>
10
+ </ol>
11
+ </nav>
12
+ {{ block.super }}
13
+ {% endblock %}
14
+
15
+ {% block content %}
16
+ <style>
17
+ .checkbox-group {
18
+ position: absolute;
19
+ }
20
+ </style>
21
+ <script>
22
+ function toggle(event) {
23
+ event = event || window.event;
24
+ var src = event.target || event.srcElement || event;
25
+ checkboxes = document.getElementsByName(src.id);
26
+ for(var checkbox of checkboxes) checkbox.checked = src.checked;
27
+ }
28
+
29
+ function uncheck(event) {
30
+ event = event || window.event;
31
+ var src = event.target || event.srcElement || event;
32
+ if (src.checked == false) {
33
+ document.getElementById(src.name).checked = false;
34
+ }
35
+ }
36
+ </script>
37
+
38
+ <p>
39
+ {% if templates_count == interfaces_count %}
40
+ The Device Type and Device have the same number of Interfaces.
41
+ {% else %}
42
+ The Device Type and Device have a different number of Interfaces.<br>
43
+ Device Type: {{ templates_count }}<br>
44
+ Device: {{ interfaces_count }}
45
+ {% endif %}
46
+ </p>
47
+
48
+ <form method="post">
49
+ <!-- Interface templates -->
50
+ {% csrf_token %}
51
+ <table class="table" style="width: 50%; float: left;">
52
+ <tr>
53
+ <th colspan="2">Device Type</th>
54
+ <th>Actions</th>
55
+ </tr>
56
+ <tr>
57
+ <th>Name</th>
58
+ <th>Type</th>
59
+ <th>
60
+ <label class="checkbox-group">
61
+ <input type="checkbox" id="add_to_device" onclick="toggle(this)">
62
+ Add
63
+ </label>
64
+ </th>
65
+ </tr>
66
+ {% for template, interface in comparison_items %}
67
+ {% if template %}
68
+ <tr {% if not interface %}class="success" data-mark-connected="true"{% endif %}>
69
+ <td>
70
+ {% if interface and template.name != interface.name %}
71
+ <span style="background-color: #cde8c2">{{ template.name }}</span>
72
+ {% else %}
73
+ {{ template.name }}
74
+ {% endif %}
75
+ </td>
76
+ <td>{{ template.type_display }}</td>
77
+ <td>
78
+ {% if not interface %}
79
+ <label class="checkbox-group">
80
+ <input type="checkbox" name="add_to_device" value="{{ template.id }}" onclick="uncheck(this)">
81
+ Add
82
+ </label>
83
+ {% endif %}
84
+ </td>
85
+ </tr>
86
+ {% else %}
87
+ <tr>
88
+ <td>&nbsp;</td>
89
+ <td>&nbsp;</td>
90
+ <td>&nbsp;</td>
91
+ </tr>
92
+ {% endif %}
93
+ {% endfor %}
94
+ </table>
95
+
96
+ <table class="table" style="width: 50%; float: right;">
97
+ <!-- Interfaces -->
98
+ <tr>
99
+ <th colspan="2">Device</th>
100
+ <th colspan="2">Actions</th>
101
+ </tr>
102
+ <tr>
103
+ <th>Name</th>
104
+ <th>Type</th>
105
+ <th>
106
+ <label class="checkbox-group">
107
+ <input type="checkbox" id="remove_from_device" onclick="toggle(this)">
108
+ Remove
109
+ </label>
110
+ </th>
111
+ <th>
112
+ <label class="checkbox-group">
113
+ <input type="checkbox" id="fix_name" onclick="toggle(this)">
114
+ Fix Name
115
+ </label>
116
+ </th>
117
+ </tr>
118
+ {% for template, interface in comparison_items %}
119
+ {% if interface %}
120
+ <tr {% if not template %}class="danger" data-enabled="disabled"{% endif %}>
121
+ <td>
122
+ {% if template and template.name != interface.name %}
123
+ <span style="background-color: #eab2b2">{{ interface.name }}</span>
124
+ {% else %}
125
+ {{ interface.name }}
126
+ {% endif %}
127
+ </td>
128
+ <td>{{ interface.type_display }}</td>
129
+ <td>
130
+ {% if not template %}
131
+ <label class="checkbox-group">
132
+ <input type="checkbox" name="remove_from_device" value="{{ interface.id }}" onclick="uncheck(this)">
133
+ Remove
134
+ </label>
135
+ {% endif %}
136
+ </td>
137
+ <td>
138
+ {% if template and template.name != interface.name %}
139
+ <label class="checkbox-group">
140
+ <input type="checkbox" name="fix_name" value="{{ interface.id }}" onclick="uncheck(this)">
141
+ Fix Name
142
+ </label>
143
+ {% endif %}
144
+ </td>
145
+ </tr>
146
+ {% else %}
147
+ <tr>
148
+ <td>&nbsp;</td>
149
+ <td>&nbsp;</td>
150
+ <td>&nbsp;</td>
151
+ <td>&nbsp;</td>
152
+ </tr>
153
+ {% endif %}
154
+ {% endfor %}
155
+ </table>
156
+ <div class="text-right">
157
+ <input type="submit" value="Apply Changes" class="btn btn-green">
158
+ </div>
159
+ </form>
160
+
161
+ {% endblock %}
@@ -0,0 +1,21 @@
1
+ <div class="panel panel-default">
2
+ <div class="panel-heading">
3
+ <strong>Interfaces</strong>
4
+ </div>
5
+ <div class="panel-body">
6
+ <table class="table table-hover panel-body attr-table">
7
+ <tr>
8
+ <td>Total Interfaces</td>
9
+ <td>{{ interfaces.count }}</td>
10
+ </tr>
11
+ <tr>
12
+ <td>Real Interfaces</td>
13
+ <td>{{ real_interfaces.count }}</td>
14
+ </tr>
15
+ <tr>
16
+ <td>Interface Templates</td>
17
+ <td>{{ interface_templates.count }}</td>
18
+ </tr>
19
+ </table>
20
+ </div>
21
+ </div>