netbox-interface-name-rules 1.2.0__tar.gz → 1.2.2__tar.gz

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.
Files changed (56) hide show
  1. {netbox_interface_name_rules-1.2.0/netbox_interface_name_rules.egg-info → netbox_interface_name_rules-1.2.2}/PKG-INFO +29 -1
  2. netbox_interface_name_rules-1.2.0/PKG-INFO → netbox_interface_name_rules-1.2.2/README.md +20 -15
  3. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/__init__.py +3 -1
  4. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/api/serializers.py +6 -7
  5. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/engine.py +1 -2
  6. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/models.py +0 -1
  7. netbox_interface_name_rules-1.2.2/netbox_interface_name_rules/templates/netbox_interface_name_rules/interfacenamerule.html +115 -0
  8. netbox_interface_name_rules-1.2.2/netbox_interface_name_rules/templates/netbox_interface_name_rules/interfacenamerule_list.html +164 -0
  9. netbox_interface_name_rules-1.2.2/netbox_interface_name_rules/templates/netbox_interface_name_rules/rule_apply.html +180 -0
  10. netbox_interface_name_rules-1.2.2/netbox_interface_name_rules/templates/netbox_interface_name_rules/rule_apply_detail.html +210 -0
  11. netbox_interface_name_rules-1.2.2/netbox_interface_name_rules/templates/netbox_interface_name_rules/rule_test.html +258 -0
  12. netbox_interface_name_rules-1.2.2/netbox_interface_name_rules/tests/test_engine_advanced.py +1510 -0
  13. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_misc.py +307 -1
  14. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_regex.py +1 -1
  15. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_signals.py +248 -0
  16. netbox_interface_name_rules-1.2.2/netbox_interface_name_rules/tests/test_views.py +614 -0
  17. netbox_interface_name_rules-1.2.0/README.md → netbox_interface_name_rules-1.2.2/netbox_interface_name_rules.egg-info/PKG-INFO +42 -0
  18. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules.egg-info/SOURCES.txt +5 -0
  19. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/pyproject.toml +37 -3
  20. netbox_interface_name_rules-1.2.0/netbox_interface_name_rules/tests/test_engine_advanced.py +0 -680
  21. netbox_interface_name_rules-1.2.0/netbox_interface_name_rules/tests/test_views.py +0 -328
  22. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/LICENSE +0 -0
  23. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/api/__init__.py +0 -0
  24. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/api/urls.py +0 -0
  25. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/api/views.py +0 -0
  26. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/filters.py +0 -0
  27. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/forms.py +1 -1
  28. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/jobs.py +0 -0
  29. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0001_initial.py +0 -0
  30. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0002_regex_pattern_matching.py +0 -0
  31. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0003_constraints.py +0 -0
  32. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0004_nulls_distinct.py +0 -0
  33. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0005_platform.py +0 -0
  34. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0006_alter_interfacenamerule_options.py +0 -0
  35. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0007_alter_optional_fks_set_null.py +0 -0
  36. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0008_constraint_nonempty_pattern.py +0 -0
  37. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0009_interfacenamerule_enabled.py +0 -0
  38. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0010_interfacenamerule_applies_to_device_interfaces.py +0 -0
  39. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0011_remove_interfacenamerule_interfacenamerule_module_type_mode_check_and_more.py +0 -0
  40. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/0012_remove_interfacenamerule_interfacenamerule_unique_exact_and_more.py +0 -0
  41. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/migrations/__init__.py +0 -0
  42. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/navigation.py +0 -0
  43. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/signals.py +0 -0
  44. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tables.py +0 -0
  45. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/__init__.py +0 -0
  46. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_api.py +0 -0
  47. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_device_rules.py +0 -0
  48. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_e2e.py +0 -0
  49. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_engine.py +0 -0
  50. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/tests/test_rules.py +0 -0
  51. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/urls.py +0 -0
  52. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/utils.py +0 -0
  53. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules/views.py +0 -0
  54. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules.egg-info/dependency_links.txt +0 -0
  55. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/netbox_interface_name_rules.egg-info/top_level.txt +0 -0
  56. {netbox_interface_name_rules-1.2.0 → netbox_interface_name_rules-1.2.2}/setup.cfg +0 -0
@@ -1,12 +1,21 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-interface-name-rules
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: NetBox plugin for automatic interface renaming when modules are installed
5
+ Author: Marcin Zieba
5
6
  License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://marcinpsk.github.io/netbox-InterfaceNameRules-plugin/
8
+ Project-URL: Repository, https://github.com/marcinpsk/netbox-InterfaceNameRules-plugin
9
+ Project-URL: Issues, https://github.com/marcinpsk/netbox-InterfaceNameRules-plugin/issues
10
+ Project-URL: Documentation, https://marcinpsk.github.io/netbox-InterfaceNameRules-plugin/
11
+ Project-URL: Changelog, https://github.com/marcinpsk/netbox-InterfaceNameRules-plugin/blob/main/CHANGELOG.md
12
+ Keywords: netbox,plugin,dcim,interface,naming,modules,transceiver,automation
6
13
  Classifier: Programming Language :: Python :: 3
7
14
  Classifier: Programming Language :: Python :: 3.12
8
15
  Classifier: Programming Language :: Python :: 3.13
9
16
  Classifier: Framework :: Django
17
+ Classifier: Environment :: Plugins
18
+ Classifier: Topic :: System :: Networking
10
19
  Requires-Python: >=3.12.0
11
20
  Description-Content-Type: text/markdown
12
21
  License-File: LICENSE
@@ -14,6 +23,10 @@ Dynamic: license-file
14
23
 
15
24
  # NetBox Interface Name Rules Plugin
16
25
 
26
+ <p align="center">
27
+ <img src="https://raw.githubusercontent.com/marcinpsk/netbox-InterfaceNameRules-plugin/main/docs/icon.svg" alt="NetBox Interface Name Rules" width="80"/>
28
+ </p>
29
+
17
30
  [![PyPI](https://img.shields.io/pypi/v/netbox-interface-name-rules)](https://pypi.org/project/netbox-interface-name-rules/)
18
31
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/netbox-interface-name-rules)](https://pypi.org/project/netbox-interface-name-rules/)
19
32
  [![CI](https://img.shields.io/github/actions/workflow/status/marcinpsk/netbox-InterfaceNameRules-plugin/test.yaml?branch=main&label=tests)](https://github.com/marcinpsk/netbox-InterfaceNameRules-plugin/actions/workflows/test.yaml)
@@ -63,6 +76,21 @@ Add to `configuration.py`:
63
76
  PLUGINS = ['netbox_interface_name_rules']
64
77
  ```
65
78
 
79
+ ## Configuration
80
+
81
+ Rules are managed through the NetBox UI under **Plugins → Interface Name Rules**, or via the REST API at `/api/plugins/interface-name-rules/rules/`.
82
+
83
+ See the [full configuration guide](https://marcinpsk.github.io/netbox-InterfaceNameRules-plugin/configuration/) for all rule fields, priority scoring, and template variable reference.
84
+
85
+ ## Screenshots
86
+
87
+ <p align="center">
88
+ <img src="https://raw.githubusercontent.com/marcinpsk/netbox-InterfaceNameRules-plugin/main/docs/screenshots/01-rule-list.png" alt="Rule list" width="700"/>
89
+ </p>
90
+ <p align="center">
91
+ <img src="https://raw.githubusercontent.com/marcinpsk/netbox-InterfaceNameRules-plugin/main/docs/screenshots/11-apply-rule-preview.png" alt="Apply-rules preview" width="700"/>
92
+ </p>
93
+
66
94
  ## Compatibility
67
95
 
68
96
  - NetBox ≥ 4.2.0
@@ -1,19 +1,9 @@
1
- Metadata-Version: 2.4
2
- Name: netbox-interface-name-rules
3
- Version: 1.2.0
4
- Summary: NetBox plugin for automatic interface renaming when modules are installed
5
- License-Expression: Apache-2.0
6
- Classifier: Programming Language :: Python :: 3
7
- Classifier: Programming Language :: Python :: 3.12
8
- Classifier: Programming Language :: Python :: 3.13
9
- Classifier: Framework :: Django
10
- Requires-Python: >=3.12.0
11
- Description-Content-Type: text/markdown
12
- License-File: LICENSE
13
- Dynamic: license-file
14
-
15
1
  # NetBox Interface Name Rules Plugin
16
2
 
3
+ <p align="center">
4
+ <img src="https://raw.githubusercontent.com/marcinpsk/netbox-InterfaceNameRules-plugin/main/docs/icon.svg" alt="NetBox Interface Name Rules" width="80"/>
5
+ </p>
6
+
17
7
  [![PyPI](https://img.shields.io/pypi/v/netbox-interface-name-rules)](https://pypi.org/project/netbox-interface-name-rules/)
18
8
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/netbox-interface-name-rules)](https://pypi.org/project/netbox-interface-name-rules/)
19
9
  [![CI](https://img.shields.io/github/actions/workflow/status/marcinpsk/netbox-InterfaceNameRules-plugin/test.yaml?branch=main&label=tests)](https://github.com/marcinpsk/netbox-InterfaceNameRules-plugin/actions/workflows/test.yaml)
@@ -63,6 +53,21 @@ Add to `configuration.py`:
63
53
  PLUGINS = ['netbox_interface_name_rules']
64
54
  ```
65
55
 
56
+ ## Configuration
57
+
58
+ Rules are managed through the NetBox UI under **Plugins → Interface Name Rules**, or via the REST API at `/api/plugins/interface-name-rules/rules/`.
59
+
60
+ See the [full configuration guide](https://marcinpsk.github.io/netbox-InterfaceNameRules-plugin/configuration/) for all rule fields, priority scoring, and template variable reference.
61
+
62
+ ## Screenshots
63
+
64
+ <p align="center">
65
+ <img src="https://raw.githubusercontent.com/marcinpsk/netbox-InterfaceNameRules-plugin/main/docs/screenshots/01-rule-list.png" alt="Rule list" width="700"/>
66
+ </p>
67
+ <p align="center">
68
+ <img src="https://raw.githubusercontent.com/marcinpsk/netbox-InterfaceNameRules-plugin/main/docs/screenshots/11-apply-rule-preview.png" alt="Apply-rules preview" width="700"/>
69
+ </p>
70
+
66
71
  ## Compatibility
67
72
 
68
73
  - NetBox ≥ 4.2.0
@@ -81,4 +86,4 @@ Apache 2.0
81
86
 
82
87
  See [CONTRIBUTING.md](CONTRIBUTING.md) for how to submit code or interface name rules.
83
88
 
84
- Community-contributed rules for various vendors are in the [`contrib/`](contrib/) directory.
89
+ Community-contributed rules for various vendors are in the [`contrib/`](contrib/) directory.
@@ -2,7 +2,7 @@
2
2
  # Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3
3
  from netbox.plugins import PluginConfig
4
4
 
5
- __version__ = "1.2.0"
5
+ __version__ = "1.2.2"
6
6
 
7
7
 
8
8
  class InterfaceNameRulesConfig(PluginConfig):
@@ -16,6 +16,8 @@ class InterfaceNameRulesConfig(PluginConfig):
16
16
  min_version = "4.2.0"
17
17
  required_settings = []
18
18
  default_settings = {}
19
+ author = "Marcin Zieba"
20
+ author_email = "marcinpsk@gmail.com"
19
21
 
20
22
  def ready(self):
21
23
  """Connect signal handlers after all apps are loaded."""
@@ -1,8 +1,7 @@
1
1
  # SPDX-License-Identifier: Apache-2.0
2
2
  # Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3
- from rest_framework import serializers
4
-
5
3
  from netbox.api.serializers import NetBoxModelSerializer
4
+ from rest_framework import serializers
6
5
 
7
6
  from netbox_interface_name_rules.models import InterfaceNameRule
8
7
 
@@ -43,25 +42,25 @@ class InterfaceNameRuleSerializer(NetBoxModelSerializer):
43
42
 
44
43
  if is_device_level:
45
44
  # Device-level rules must not have a module_type FK
46
- if module_type:
45
+ if module_type: # pragma: no cover # model.clean() runs first
47
46
  raise serializers.ValidationError(
48
47
  {"module_type": "Module type must be empty for device-level interface rules."}
49
48
  )
50
49
  elif is_regex:
51
- if not pattern:
50
+ if not pattern: # pragma: no cover # model.clean() runs first
52
51
  raise serializers.ValidationError(
53
52
  {"module_type_pattern": "Regex pattern is required when regex mode is enabled."}
54
53
  )
55
- if module_type:
54
+ if module_type: # pragma: no cover # model.clean() runs first
56
55
  raise serializers.ValidationError(
57
56
  {"module_type": "Cannot set both module_type and module_type_pattern. Choose one."}
58
57
  )
59
58
  else:
60
- if not module_type:
59
+ if not module_type: # pragma: no cover # model.clean() runs first
61
60
  raise serializers.ValidationError(
62
61
  {"module_type": "module_type is required when regex mode is disabled."}
63
62
  )
64
- if pattern:
63
+ if pattern: # pragma: no cover # model.clean() runs first
65
64
  raise serializers.ValidationError(
66
65
  {"module_type_pattern": "Cannot set module_type_pattern when regex mode is disabled."}
67
66
  )
@@ -831,5 +831,4 @@ def evaluate_name_template(template: str, variables: dict) -> str:
831
831
  except (SyntaxError, TypeError) as e:
832
832
  raise ValueError(f"Invalid arithmetic expression '{expr}': {e}") from e
833
833
 
834
- result = re.sub(r"\{([^}]+)\}", _eval_expr, result)
835
- return result
834
+ return re.sub(r"\{([^}]+)\}", _eval_expr, result)
@@ -9,7 +9,6 @@ from django.urls import reverse
9
9
  from netbox.models import NetBoxModel
10
10
  from taggit.managers import TaggableManager
11
11
 
12
-
13
12
  _REDOS_PATTERN = re.compile(r"(\+\*|\*\+|\?\?|\)\s*[\+\*\?]\s*[\+\*\?]|\)\s*\{[^{}]+\}\s*[\+\*\?])")
14
13
 
15
14
 
@@ -0,0 +1,115 @@
1
+ {# SPDX-License-Identifier: Apache-2.0 #}
2
+ {# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com> #}
3
+ {% extends 'generic/object.html' %}
4
+ {% load helpers %}
5
+ {% load plugins %}
6
+
7
+ {% block extra_controls %}
8
+ <a href="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_test' %}?rule_id={{ object.pk }}"
9
+ class="btn btn-outline-secondary">
10
+ <i class="mdi mdi-flask-outline me-1"></i>Test in Build Rule
11
+ </a>
12
+ {% if perms.netbox_interface_name_rules.add_interfacenamerule %}
13
+ <a href="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_duplicate' object.pk %}"
14
+ class="btn btn-success">
15
+ <i class="mdi mdi-content-copy me-1"></i>Duplicate
16
+ </a>
17
+ {% endif %}
18
+ {% endblock extra_controls %}
19
+
20
+ {% block content %}
21
+ {# ── Priority callout ───────────────────────────────────────────────────────── #}
22
+ <div class="alert alert-info d-flex align-items-start mb-3" role="alert">
23
+ <i class="mdi mdi-information-outline me-2 mt-1 fs-5"></i>
24
+ <div>
25
+ <strong>Priority score: {{ object.specificity_score }}</strong>
26
+ <span class="text-muted ms-1">({{ object.specificity_label }})</span>
27
+ <br>
28
+ <small>
29
+ This score is <em>auto-computed</em> — you cannot set it directly.
30
+ It controls which rule wins when multiple rules match the same module.
31
+ <strong>Higher score = higher priority.</strong>
32
+ Exact FK rules always outrank regex rules (score 1000+&nbsp;vs max ≤&nbsp;955).
33
+ Among regex rules, adding scope fields raises the score:
34
+ <code>parent_module_type</code>&nbsp;+4×100,
35
+ <code>device_type</code>&nbsp;+2×100,
36
+ <code>platform</code>&nbsp;+1×100,
37
+ plus pattern length.
38
+ <a data-bs-toggle="collapse" href="#priority-detail" class="ms-1">More…</a>
39
+ </small>
40
+ <div class="collapse mt-2" id="priority-detail">
41
+ <table class="table table-sm table-bordered mb-0 small">
42
+ <thead class="table-light"><tr><th>Rule type</th><th>Score formula</th><th>Range</th></tr></thead>
43
+ <tbody>
44
+ <tr><td>Exact FK (module_type)</td><td>1000 + scope</td><td>1000 – 1007</td></tr>
45
+ <tr><td>Regex (module_type_pattern)</td><td>scope × 100 + len(pattern)</td><td>0 – 955</td></tr>
46
+ </tbody>
47
+ </table>
48
+ <p class="small mb-0 mt-1">
49
+ Scope bits: <code>parent_module_type</code>=4, <code>device_type</code>=2, <code>platform</code>=1.
50
+ Two rules with the same score: lowest <code>pk</code> (oldest rule) wins.
51
+ </p>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="row mb-3">
57
+ <div class="col col-md-12">
58
+ <div class="card">
59
+ <table class="table table-hover attr-table">
60
+ <thead>
61
+ <tr>
62
+ <th>Module Type</th>
63
+ <th>Parent Module Type</th>
64
+ <th>Device Type</th>
65
+ <th>Platform</th>
66
+ <th>Name Template</th>
67
+ <th>Channels</th>
68
+ <th>Channel Start</th>
69
+ <th>Description</th>
70
+ </tr>
71
+ </thead>
72
+ <tbody>
73
+ <tr>
74
+ <td>
75
+ {% if object.module_type_is_regex %}
76
+ <code>{{ object.module_type_pattern }}</code>
77
+ <span class="badge text-bg-info">regex</span>
78
+ {% elif object.module_type %}
79
+ <a href="{{ object.module_type.get_absolute_url }}">{{ object.module_type }}</a>
80
+ {% else %}
81
+ <span class="text-muted">—</span>
82
+ {% endif %}
83
+ </td>
84
+ <td>
85
+ {% if object.parent_module_type %}
86
+ <a href="{{ object.parent_module_type.get_absolute_url }}">{{ object.parent_module_type }}</a>
87
+ {% else %}
88
+ <span class="text-muted">Any parent</span>
89
+ {% endif %}
90
+ </td>
91
+ <td>
92
+ {% if object.device_type %}
93
+ <a href="{{ object.device_type.get_absolute_url }}">{{ object.device_type }}</a>
94
+ {% else %}
95
+ <span class="text-muted">Any device</span>
96
+ {% endif %}
97
+ </td>
98
+ <td>
99
+ {% if object.platform %}
100
+ <a href="{{ object.platform.get_absolute_url }}">{{ object.platform }}</a>
101
+ {% else %}
102
+ <span class="text-muted">Any platform</span>
103
+ {% endif %}
104
+ </td>
105
+ <td><code>{{ object.name_template }}</code></td>
106
+ <td>{{ object.channel_count|default:"—" }}</td>
107
+ <td>{{ object.channel_start }}</td>
108
+ <td>{{ object.description|default:"—" }}</td>
109
+ </tr>
110
+ </tbody>
111
+ </table>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ {% endblock %}
@@ -0,0 +1,164 @@
1
+ {# SPDX-License-Identifier: Apache-2.0 #}
2
+ {# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com> #}
3
+ {% extends 'generic/object_list.html' %}
4
+
5
+ {% block content %}
6
+ <div class="card mb-3" id="interface-name-rules-help">
7
+ <div class="card-header" role="button" tabindex="0" style="cursor: pointer;"
8
+ id="rules-help-header" aria-expanded="true" aria-controls="rules-help-body"
9
+ onclick="toggleRulesHelp()"
10
+ onkeydown="if(event.key==='Enter'||event.key===' '){toggleRulesHelp();event.preventDefault();}">
11
+ <h5 class="card-title mb-0 d-flex align-items-center">
12
+ <i class="mdi mdi-information-outline me-2"></i>
13
+ Interface Name Rules Reference
14
+ <i class="mdi ms-auto" id="rules-help-chevron"></i>
15
+ </h5>
16
+ </div>
17
+ <div id="rules-help-body" class="card-body">
18
+ <p>Define post-install interface rename rules for module types where NetBox's
19
+ position-based naming can't produce the correct interface name.</p>
20
+ <p>Covers <strong>converter offset</strong> (e.g., CVR-X2-SFP + GLC-T),
21
+ <strong>breakout transceivers</strong> (e.g., QSFP+ 4x10G with channel numbering),
22
+ <strong>platform-specific naming</strong> (e.g., Juniper et-/xe-/ge- prefixes),
23
+ and <strong>Virtual Chassis device port renaming</strong> (e.g., Gi0/1 → Gi2/1 on VC member 2).</p>
24
+
25
+ <p><strong>Signal-driven:</strong> Rules run automatically via two Django signals:</p>
26
+ <ul>
27
+ <li><code>post_save</code> on <strong>Module</strong> — fires after a module is installed into a device bay (primary path for transceiver/linecard naming).</li>
28
+ <li><code>post_save</code> on <strong>Device</strong> — fires after VC position changes (<code>vc_position</code> or <code>virtual_chassis</code> modified); triggers device-level interface renaming for VC members.</li>
29
+ </ul>
30
+ <p>Both paths use <code>on_commit</code> deferred execution so that all interfaces are in the database before renaming begins.</p>
31
+
32
+ <p><strong>Device interface rules</strong> (<code>Applies to Device Interfaces</code> checkbox) target native device-type interfaces
33
+ (<code>module=None</code>) rather than module-installed interfaces. Use these to rename VC member ports when the device
34
+ joins a stack or changes position. The <code>Module Type Pattern</code> field acts as an interface-name filter (regex) instead of a module type selector.</p>
35
+
36
+ <h6 class="mt-3">Supported Template Variables</h6>
37
+ <table class="table table-sm table-bordered" style="max-width: 750px;">
38
+ <thead>
39
+ <tr><th style="width: 200px;">Variable</th><th>Rule type</th><th>Description</th></tr>
40
+ </thead>
41
+ <tbody>
42
+ <tr><td><code>{slot}</code></td><td>Module</td><td>Top-level slot/module bay position (from grandparent or parent)</td></tr>
43
+ <tr><td><code>{bay_position}</code></td><td>Module</td><td>Position of the bay this module is installed into (raw, e.g., "swp1")</td></tr>
44
+ <tr><td><code>{bay_position_num}</code></td><td>Module</td><td>Numeric suffix of bay position (e.g., "swp1" &rarr; "1")</td></tr>
45
+ <tr><td><code>{parent_bay_position}</code></td><td>Module</td><td>Position of the parent module's bay</td></tr>
46
+ <tr><td><code>{sfp_slot}</code></td><td>Module</td><td>Numeric sub-bay index within the parent module</td></tr>
47
+ <tr><td><code>{base}</code></td><td>Both</td><td>Original interface name from the template (or current name for device-level rules)</td></tr>
48
+ <tr><td><code>{port}</code></td><td>Device</td><td>Segment after the last "/" in the current interface name (e.g., "Gi0/1" &rarr; "1"); full name if no "/"</td></tr>
49
+ <tr><td><code>{channel}</code></td><td>Module</td><td>Breakout channel number (iterated from Ch. Start to Ch. Start + Channels - 1)</td></tr>
50
+ <tr><td><code>{vc_position}</code></td><td>Both</td><td>Virtual Chassis member position (<code>device.vc_position</code>); requires device to be in a VC</td></tr>
51
+ </tbody>
52
+ </table>
53
+
54
+ <h6 class="mt-3">Arithmetic Expressions</h6>
55
+ <p>Arithmetic is supported inside braces:
56
+ <code>{8 + ({parent_bay_position} - 1) * 2 + {sfp_slot}}</code></p>
57
+
58
+ <h6 class="mt-3">Examples</h6>
59
+ <ul class="mb-0">
60
+ <li><strong>Converter offset:</strong> GLC-T in CVR-X2-SFP &rarr;
61
+ <code>GigabitEthernet{slot}/{8 + ({parent_bay_position} - 1) * 2 + {sfp_slot}}</code></li>
62
+ <li><strong>Breakout:</strong> QSFP-4X10G-LR &rarr; <code>{base}:{channel}</code>
63
+ (Channels=4, Ch. Start=0)</li>
64
+ <li><strong>Platform naming:</strong> QSFP-100G-LR4 on ACX7024 &rarr;
65
+ <code>et-0/0/{bay_position}</code> (scoped via Parent Device Type)</li>
66
+ <li><strong>UfiSpace breakout:</strong> QSFP-100G-LR4 on S9610-36D &rarr;
67
+ <code>swp{bay_position_num}s{channel}</code> (Channels=2, Ch. Start=1)</li>
68
+ <li><strong>Virtual Chassis linecard:</strong> VC-LINECARD on stack member &rarr;
69
+ <code>Gi{vc_position}/{bay_position_num}</code> (requires device to be in a Virtual Chassis)</li>
70
+ <li><strong>VC device ports (Cisco):</strong> GigabitEthernet1/0/3 on stack member 2 &rarr;
71
+ <code>GigabitEthernet{vc_position}/0/{port}</code> &rarr; GigabitEthernet2/0/3
72
+ (device-level rule, pattern <code>GigabitEthernet\d+/\d+/\d+</code>)</li>
73
+ <li><strong>VC device ports (Juniper):</strong> ge-0/0/2 on VC member 1 &rarr;
74
+ <code>ge-{vc_position}/0/{port}</code> &rarr; ge-1/0/2
75
+ (device-level rule, pattern <code>ge-\d+/\d+/\d+</code>)</li>
76
+ <li><strong>VC device ports (Arista):</strong> Ethernet1/3 on VC member 2 &rarr;
77
+ <code>Ethernet{vc_position}/{port}</code> &rarr; Ethernet2/3
78
+ (device-level rule, pattern <code>Ethernet\d+/\d+</code>)</li>
79
+ </ul>
80
+
81
+ {% if supports_module_path %}
82
+ <div class="alert alert-success mt-3 mb-0">
83
+ <i class="mdi mdi-check-circle me-1"></i>
84
+ <strong><code>{module_path}</code> supported:</strong>
85
+ Platform naming rules (e.g., <code>et-0/0/{bay_position}</code>)
86
+ may be replaceable by using <code>{module_path}</code> in module type interface templates directly.
87
+ Converter offset and breakout rules are still needed.
88
+ </div>
89
+ {% else %}
90
+ <div class="alert alert-warning mt-3 mb-0">
91
+ <i class="mdi mdi-information-outline me-1"></i>
92
+ <strong><code>{module_path}</code> not available:</strong>
93
+ Platform naming rules
94
+ (e.g., Juniper <code>et-0/0/{bay_position}</code>) are required for correct interface naming.
95
+ Upgrade NetBox to get native <code>{module_path}</code> support.
96
+ </div>
97
+ {% endif %}
98
+ </div>
99
+ </div>
100
+ <script>
101
+ function toggleRulesHelp() {
102
+ const body = document.getElementById('rules-help-body');
103
+ const header = document.getElementById('rules-help-header');
104
+ const chevron = document.getElementById('rules-help-chevron');
105
+ const collapsed = body.style.display === 'none';
106
+ body.style.display = collapsed ? '' : 'none';
107
+ chevron.className = collapsed ? 'mdi ms-auto mdi-chevron-up' : 'mdi ms-auto mdi-chevron-down';
108
+ header.setAttribute('aria-expanded', collapsed ? 'true' : 'false');
109
+ localStorage.setItem('rulesHelpCollapsed', collapsed ? '' : '1');
110
+ }
111
+ document.addEventListener('DOMContentLoaded', function() {
112
+ const collapsed = localStorage.getItem('rulesHelpCollapsed') === '1';
113
+ const body = document.getElementById('rules-help-body');
114
+ const header = document.getElementById('rules-help-header');
115
+ const chevron = document.getElementById('rules-help-chevron');
116
+ body.style.display = collapsed ? 'none' : '';
117
+ chevron.className = collapsed ? 'mdi ms-auto mdi-chevron-down' : 'mdi ms-auto mdi-chevron-up';
118
+ header.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
119
+
120
+ // AJAX toggle: click handler on toggle buttons (no form — avoids nested-form 405 bug)
121
+ document.addEventListener('click', function(e) {
122
+ const btn = e.target.closest('.toggle-btn[data-toggle-url]');
123
+ if (!btn) return;
124
+ const url = btn.dataset.toggleUrl;
125
+ const csrfToken =
126
+ document.querySelector('input[name="csrfmiddlewaretoken"]')?.value ||
127
+ (document.cookie.match(/(?:^|;\s*)csrftoken=([^;]+)/) || [])[1] || '';
128
+ btn.disabled = true;
129
+ fetch(url, {
130
+ method: 'POST',
131
+ headers: {'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': csrfToken},
132
+ credentials: 'same-origin',
133
+ }).then(r => {
134
+ if (!r.ok) throw new Error('HTTP ' + r.status);
135
+ return r.json();
136
+ }).then(data => {
137
+ btn.disabled = false;
138
+ const row = btn.closest('tr');
139
+ if (data.enabled) {
140
+ btn.className = 'btn btn-sm btn-success toggle-btn';
141
+ btn.title = 'Enabled — click to disable';
142
+ btn.setAttribute('aria-pressed', 'true');
143
+ btn.setAttribute('aria-label', 'Enabled — click to disable');
144
+ const iconOn = btn.querySelector('i');
145
+ if (iconOn) iconOn.className = 'mdi mdi-check';
146
+ if (row) row.classList.remove('text-muted', 'opacity-50');
147
+ } else {
148
+ btn.className = 'btn btn-sm btn-secondary toggle-btn';
149
+ btn.title = 'Disabled — click to enable';
150
+ btn.setAttribute('aria-pressed', 'false');
151
+ btn.setAttribute('aria-label', 'Disabled — click to enable');
152
+ const iconOff = btn.querySelector('i');
153
+ if (iconOff) iconOff.className = 'mdi mdi-minus';
154
+ if (row) row.classList.add('text-muted', 'opacity-50');
155
+ }
156
+ }).catch(() => {
157
+ btn.disabled = false;
158
+ window.location.reload();
159
+ });
160
+ });
161
+ });
162
+ </script>
163
+ {{ block.super }}
164
+ {% endblock %}
@@ -0,0 +1,180 @@
1
+ {# SPDX-License-Identifier: Apache-2.0 #}
2
+ {# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com> #}
3
+ {% extends 'generic/_base.html' %}
4
+
5
+ {% block title %}Apply Rules{% endblock %}
6
+
7
+ {% block content %}
8
+ <p class="text-muted mb-3">
9
+ Use this page to <strong>retroactively rename interfaces that were installed before the matching rule existed</strong>.
10
+ Interfaces installed <em>after</em> a rule is active are renamed automatically at install time — you don't need
11
+ to use Apply Rules for them.
12
+ </p>
13
+ <p class="text-muted mb-3">
14
+ Select a rule and click <strong>Preview &amp; Apply</strong> to see which currently installed interfaces
15
+ would be renamed, then optionally apply the changes (synchronously up to {{ batch_limit }} interfaces,
16
+ or as a background job for larger sets).
17
+ </p>
18
+
19
+ {% if rules %}
20
+ <div class="card">
21
+ <div class="card-header d-flex align-items-center gap-2 flex-wrap">
22
+ <h5 class="card-title mb-0 me-2"><i class="mdi mdi-format-list-bulleted me-2"></i>Available Rules</h5>
23
+ <button id="scan-all-btn" class="btn btn-sm btn-outline-secondary" title="Check all rules to see if any currently installed interfaces would be renamed">
24
+ <i class="mdi mdi-magnify me-1"></i>Check all rules
25
+ </button>
26
+ <span id="scan-progress" class="text-muted small" aria-live="polite" role="status" style="display:none"></span>
27
+ <div class="ms-auto">
28
+ <input id="rule-search" type="search" class="form-control form-control-sm" placeholder="Filter rules…"
29
+ style="width:240px" aria-label="Filter rules">
30
+ </div>
31
+ </div>
32
+ <div class="card-body p-0">
33
+ <table class="table table-hover mb-0" id="rules-table">
34
+ <thead>
35
+ <tr>
36
+ <th>#</th>
37
+ <th>Type</th>
38
+ <th>Module Type</th>
39
+ <th>Parent Module Type</th>
40
+ <th>Device Type</th>
41
+ <th>Platform</th>
42
+ <th>Name Template</th>
43
+ <th>Ch.</th>
44
+ <th title="Click 'Check' to scan installed interfaces and see if any would be renamed by this rule. This scan runs on demand because it may be slow for rules matching many modules.">Can rename</th>
45
+ <th></th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ {% for rule in rules %}
50
+ <tr>
51
+ <td><a href="{{ rule.get_absolute_url }}">{{ rule.pk }}</a></td>
52
+ <td>
53
+ {% if rule.module_type_is_regex %}
54
+ <span class="badge text-bg-info">regex</span>
55
+ {% else %}
56
+ <span class="badge text-bg-success">exact</span>
57
+ {% endif %}
58
+ </td>
59
+ <td>
60
+ {% if rule.module_type_is_regex %}
61
+ <code>{{ rule.module_type_pattern }}</code>
62
+ {% elif rule.module_type %}
63
+ <a href="{{ rule.module_type.get_absolute_url }}">{{ rule.module_type.model }}</a>
64
+ {% else %}
65
+ <span class="text-muted">—</span>
66
+ {% endif %}
67
+ </td>
68
+ <td>
69
+ {% if rule.parent_module_type %}
70
+ <a href="{{ rule.parent_module_type.get_absolute_url }}">{{ rule.parent_module_type.model }}</a>
71
+ {% else %}
72
+ <span class="text-muted">Any</span>
73
+ {% endif %}
74
+ </td>
75
+ <td>
76
+ {% if rule.device_type %}
77
+ <a href="{{ rule.device_type.get_absolute_url }}">{{ rule.device_type.model }}</a>
78
+ {% else %}
79
+ <span class="text-muted">Any</span>
80
+ {% endif %}
81
+ </td>
82
+ <td>
83
+ {% if rule.platform %}
84
+ <a href="{{ rule.platform.get_absolute_url }}">{{ rule.platform.name }}</a>
85
+ {% else %}
86
+ <span class="text-muted">Any</span>
87
+ {% endif %}
88
+ </td>
89
+ <td><code>{{ rule.name_template }}</code></td>
90
+ <td>{% if rule.channel_count %}{{ rule.channel_count }}{% else %}<span class="text-muted">—</span>{% endif %}</td>
91
+ <td class="applicable-cell" data-pk="{{ rule.pk }}"
92
+ data-url="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_applicable' rule.pk %}">
93
+ <button class="btn btn-sm btn-outline-secondary scan-btn py-0 px-2"
94
+ data-pk="{{ rule.pk }}"
95
+ title="Scan installed interfaces to see if any would be renamed">
96
+ <i class="mdi mdi-magnify"></i> Check
97
+ </button>
98
+ </td>
99
+ <td>
100
+ <a href="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_apply_detail' rule.pk %}"
101
+ class="btn btn-sm btn-outline-primary">
102
+ <i class="mdi mdi-eye me-1"></i>Preview &amp; Apply
103
+ </a>
104
+ <a href="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_test' %}?rule_id={{ rule.pk }}"
105
+ class="btn btn-sm btn-outline-secondary" title="Test in Rule Builder" aria-label="Test in Rule Builder">
106
+ <i class="mdi mdi-flask-outline"></i>
107
+ </a>
108
+ </td>
109
+ </tr>
110
+ {% endfor %}
111
+ </tbody>
112
+ </table>
113
+ </div>
114
+ </div>
115
+
116
+ <script>
117
+ (function () {
118
+ // --- filter ---
119
+ const input = document.getElementById('rule-search');
120
+ const rows = document.querySelectorAll('#rules-table tbody tr');
121
+ input.addEventListener('input', function () {
122
+ const q = this.value.toLowerCase();
123
+ rows.forEach(r => { r.style.display = r.textContent.toLowerCase().includes(q) ? '' : 'none'; });
124
+ });
125
+
126
+ // --- per-rule scan ---
127
+ function renderResult(cell, applicable) {
128
+ const pk = cell.dataset.pk;
129
+ const recheck = `<button class="btn btn-sm btn-link p-0 ms-1 rescan-btn" data-pk="${pk}" title="Re-check"><i class="mdi mdi-refresh"></i></button>`;
130
+ if (applicable === true) {
131
+ // light green tint — interfaces would be renamed
132
+ cell.innerHTML = `<span class="badge fw-normal" style="background:rgba(25,135,84,.15);color:#0f5132;border:1px solid rgba(25,135,84,.3)">Yes</span>${recheck}`;
133
+ } else if (applicable === false) {
134
+ // light muted tint — nothing to rename
135
+ cell.innerHTML = `<span class="badge fw-normal" style="background:rgba(108,117,125,.1);color:#6c757d;border:1px solid rgba(108,117,125,.25)">No</span>${recheck}`;
136
+ } else {
137
+ // light red tint — scan error
138
+ cell.innerHTML = `<span class="badge fw-normal" style="background:rgba(220,53,69,.12);color:#842029;border:1px solid rgba(220,53,69,.25)" title="Scan failed">Error</span>${recheck}`;
139
+ }
140
+ cell.querySelector('.rescan-btn').addEventListener('click', () => scanCell(cell));
141
+ }
142
+
143
+ // Returns a Promise that resolves when the fetch + render completes.
144
+ function scanCell(cell) {
145
+ const url = cell.dataset.url;
146
+ cell.innerHTML = '<span class="spinner-border spinner-border-sm text-secondary" role="status"></span>';
147
+ return fetch(url)
148
+ .then(r => { if (!r.ok) { renderResult(cell, null); return; } return r.json(); })
149
+ .then(d => { if (d) renderResult(cell, d.applicable); })
150
+ .catch(() => renderResult(cell, null));
151
+ }
152
+
153
+ // bind initial scan buttons
154
+ document.querySelectorAll('.scan-btn').forEach(btn => {
155
+ btn.addEventListener('click', function () { scanCell(this.closest('td')); });
156
+ });
157
+
158
+ // --- scan all (serial, awaiting each fetch before moving to the next) ---
159
+ document.getElementById('scan-all-btn').addEventListener('click', async function () {
160
+ const cells = Array.from(document.querySelectorAll('.applicable-cell'))
161
+ .filter(cell => cell.closest('tr').style.display !== 'none');
162
+ const progress = document.getElementById('scan-progress');
163
+ this.disabled = true;
164
+ progress.style.display = '';
165
+ for (let i = 0; i < cells.length; i++) {
166
+ progress.textContent = `Checking ${i + 1} / ${cells.length}…`;
167
+ await scanCell(cells[i]);
168
+ }
169
+ progress.textContent = `Done — ${cells.length} rules checked.`;
170
+ this.disabled = false;
171
+ });
172
+ })();
173
+ </script>
174
+ {% else %}
175
+ <div class="alert alert-info">
176
+ No rules defined yet.
177
+ <a href="{% url 'plugins:netbox_interface_name_rules:interfacenamerule_add' %}">Add a rule</a> to get started.
178
+ </div>
179
+ {% endif %}
180
+ {% endblock %}