netbox-change-control 0.3.0__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.
- netbox_change_control-0.3.0/LICENSE +21 -0
- netbox_change_control-0.3.0/PKG-INFO +159 -0
- netbox_change_control-0.3.0/README.md +135 -0
- netbox_change_control-0.3.0/netbox_change_control/__init__.py +87 -0
- netbox_change_control-0.3.0/netbox_change_control/api/__init__.py +0 -0
- netbox_change_control-0.3.0/netbox_change_control/api/serializers.py +251 -0
- netbox_change_control-0.3.0/netbox_change_control/api/urls.py +14 -0
- netbox_change_control-0.3.0/netbox_change_control/api/views.py +173 -0
- netbox_change_control-0.3.0/netbox_change_control/automerge.py +166 -0
- netbox_change_control-0.3.0/netbox_change_control/batching.py +102 -0
- netbox_change_control-0.3.0/netbox_change_control/checks.py +368 -0
- netbox_change_control-0.3.0/netbox_change_control/choices.py +110 -0
- netbox_change_control-0.3.0/netbox_change_control/conflicts.py +61 -0
- netbox_change_control-0.3.0/netbox_change_control/dashboard.py +65 -0
- netbox_change_control-0.3.0/netbox_change_control/diffs.py +133 -0
- netbox_change_control-0.3.0/netbox_change_control/events.py +83 -0
- netbox_change_control-0.3.0/netbox_change_control/filtersets.py +377 -0
- netbox_change_control-0.3.0/netbox_change_control/forms.py +434 -0
- netbox_change_control-0.3.0/netbox_change_control/jobs.py +51 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/0001_initial.py +203 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/0002_policy_condition_state.py +18 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/0003_changerequest_ref.py +18 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/0004_alter_changerequest_options.py +17 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/0005_changerequest_cached_conflicted_and_more.py +68 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/0006_remove_changerequestpolicy_created_and_more.py +21 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/0007_alter_changerequestpolicy_options.py +41 -0
- netbox_change_control-0.3.0/netbox_change_control/migrations/__init__.py +0 -0
- netbox_change_control-0.3.0/netbox_change_control/models/__init__.py +4 -0
- netbox_change_control-0.3.0/netbox_change_control/models/checks.py +94 -0
- netbox_change_control-0.3.0/netbox_change_control/models/comments.py +137 -0
- netbox_change_control-0.3.0/netbox_change_control/models/policies.py +213 -0
- netbox_change_control-0.3.0/netbox_change_control/models/requests.py +608 -0
- netbox_change_control-0.3.0/netbox_change_control/navigation.py +67 -0
- netbox_change_control-0.3.0/netbox_change_control/notifications.py +92 -0
- netbox_change_control-0.3.0/netbox_change_control/permissions.py +57 -0
- netbox_change_control-0.3.0/netbox_change_control/policy.py +447 -0
- netbox_change_control-0.3.0/netbox_change_control/search.py +114 -0
- netbox_change_control-0.3.0/netbox_change_control/signal_receivers.py +532 -0
- netbox_change_control-0.3.0/netbox_change_control/tables.py +178 -0
- netbox_change_control-0.3.0/netbox_change_control/template_content.py +121 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/changerequest.html +308 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/changerequest_changes.html +151 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/changerequest_reviews.html +14 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/branch_change_request.html +20 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/branch_change_request_body.html +23 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/branch_change_request_card.html +21 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/branch_no_request.html +20 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/branch_no_request_body.html +8 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/branch_no_request_card.html +19 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/comment_actions.html +21 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/review_form.html +39 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/inc/rule_reviewers.html +24 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/mergecheck.html +35 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/policy.html +73 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/policyrule.html +53 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/review.html +45 -0
- netbox_change_control-0.3.0/netbox_change_control/templates/netbox_change_control/widgets/my_reviews.html +21 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/__init__.py +0 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/base.py +151 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_api.py +326 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_automatic_behaviors.py +271 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_automerge_once.py +116 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_batching.py +178 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_branch_deletion.py +258 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_branch_page.py +237 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_cached_state.py +305 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_check_changelog.py +105 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_checks.py +837 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_comment_attribution.py +234 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_comments.py +376 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_conditions.py +205 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_conflicts.py +253 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_documented_constraints.py +143 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_documented_permissions.py +103 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_documented_settings.py +46 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_events.py +109 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_extensibility.py +222 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_filtersets.py +317 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_lifecycle_actions.py +462 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_list_actions.py +100 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_markdown.py +115 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_notifications.py +145 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_permissions.py +57 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_policy.py +285 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_protect_main.py +279 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_readiness.py +56 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_reviewer_display.py +180 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_scope.py +244 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_search.py +147 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_status.py +64 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_templates.py +190 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_validators.py +83 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_views.py +150 -0
- netbox_change_control-0.3.0/netbox_change_control/tests/test_windows.py +441 -0
- netbox_change_control-0.3.0/netbox_change_control/urls.py +74 -0
- netbox_change_control-0.3.0/netbox_change_control/validators.py +131 -0
- netbox_change_control-0.3.0/netbox_change_control/views/__init__.py +14 -0
- netbox_change_control-0.3.0/netbox_change_control/views/bulk.py +80 -0
- netbox_change_control-0.3.0/netbox_change_control/views/checks.py +73 -0
- netbox_change_control-0.3.0/netbox_change_control/views/comments.py +191 -0
- netbox_change_control-0.3.0/netbox_change_control/views/policies.py +86 -0
- netbox_change_control-0.3.0/netbox_change_control/views/requests.py +279 -0
- netbox_change_control-0.3.0/netbox_change_control/views/reviews.py +68 -0
- netbox_change_control-0.3.0/netbox_change_control.egg-info/PKG-INFO +159 -0
- netbox_change_control-0.3.0/netbox_change_control.egg-info/SOURCES.txt +108 -0
- netbox_change_control-0.3.0/netbox_change_control.egg-info/dependency_links.txt +1 -0
- netbox_change_control-0.3.0/netbox_change_control.egg-info/requires.txt +1 -0
- netbox_change_control-0.3.0/netbox_change_control.egg-info/top_level.txt +1 -0
- netbox_change_control-0.3.0/pyproject.toml +58 -0
- netbox_change_control-0.3.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Antoine Keranflech
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: netbox-change-control
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Policy-driven change control and mandatory review for NetBox branches
|
|
5
|
+
Author-email: Antoine Keranflech <antoine.keranflech@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/antoinekh/netbox-change-control
|
|
8
|
+
Project-URL: Documentation, https://github.com/antoinekh/netbox-change-control/blob/master/docs/index.md
|
|
9
|
+
Project-URL: Source, https://github.com/antoinekh/netbox-change-control
|
|
10
|
+
Project-URL: Changelog, https://github.com/antoinekh/netbox-change-control/blob/master/CHANGELOG.md
|
|
11
|
+
Project-URL: Issues, https://github.com/antoinekh/netbox-change-control/issues
|
|
12
|
+
Keywords: netbox,netbox-plugin,change-management,change-control,branching,approval-workflow,network-automation
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Intended Audience :: System Administrators
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: netboxlabs-netbox-branching<1.2,>=1.1.3
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
<div align="center">
|
|
26
|
+
<img src="docs/img/logo.svg" width="460" alt="netbox change control" />
|
|
27
|
+
<p><strong>Policy-driven change control and mandatory review for NetBox branches</strong></p>
|
|
28
|
+
<p>change requests • policies • checks • comments</p>
|
|
29
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" />
|
|
30
|
+
<img src="https://img.shields.io/badge/NetBox-%3E%3D%204.6.9%2C%20%3C%204.7-00857d" alt="NetBox compatibility" />
|
|
31
|
+
<img src="https://img.shields.io/badge/netbox--branching-%3E%3D%201.1.3-00857d" alt="netbox-branching compatibility" />
|
|
32
|
+
<img src="https://img.shields.io/badge/python-%3E%3D%203.12-blue" alt="Python version" />
|
|
33
|
+
<p>
|
|
34
|
+
<strong><a href="docs/index.md">Documentation</a></strong> |
|
|
35
|
+
<strong><a href="docs/installation.md">Install</a></strong> |
|
|
36
|
+
<strong><a href="docs/policies.md">Policies</a></strong> |
|
|
37
|
+
<strong><a href="docs/checks.md">Checks</a></strong> |
|
|
38
|
+
<strong><a href="CHANGELOG.md">Changelog</a></strong>
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
This plugin builds on [netbox-branching](https://github.com/netboxlabs/netbox-branching). A branch stages your changes; this plugin decides who must approve them and refuses the merge until they have.
|
|
43
|
+
|
|
44
|
+
The goal is change control that is **policy driven**: who must approve a change is decided by the objects it touches, not by whoever opened it. Around that sit two extension points, so the same gate can be driven by more than people. Pre-merge **checks** are pluggable, and an event fires on every status change, so a change request can call an external system, wait for a CI result, or ask a model to review the diff before anyone merges it. See [writing your own checks](docs/custom-checks.md), which includes an AI reviewer and a CI reporter.
|
|
45
|
+
|
|
46
|
+
It takes ideas from **[NetBox Labs change management](https://netboxlabs.com/docs/changes/)**, for policies and rules governing who must review a branch; from **GitHub**, for status checks that gate a merge independently of human approval and for review comments anchored to a specific change; and from **[Infrahub](https://docs.infrahub.app/topics/proposed-change)** by OpsMill, for treating a proposed change as a first-class object that carries its own validation.
|
|
47
|
+
|
|
48
|
+
> [!NOTE]
|
|
49
|
+
> **Independent community plugin.** Free, MIT licensed, not official, not certified and not endorsed by NetBox Labs. It bundles no netbox-branching code. You install that package yourself, and its own licence governs how you may use it.
|
|
50
|
+
>
|
|
51
|
+
> No commercial support. If you need a supported product, use the [NetBox Labs](https://netboxlabs.com/docs/changes/) change management plugin.
|
|
52
|
+
|
|
53
|
+
> [!WARNING]
|
|
54
|
+
> **Not stable yet.** The version is below 1.0. Models, settings and the REST API can still change between releases. Read the [changelog](CHANGELOG.md) before you upgrade.
|
|
55
|
+
|
|
56
|
+
<p align="center">
|
|
57
|
+
<a href="#how-it-works">How it works</a> |
|
|
58
|
+
<a href="#documentation">Documentation</a> |
|
|
59
|
+
<a href="#features">Features</a> |
|
|
60
|
+
<a href="#requirements">Requirements</a> |
|
|
61
|
+
<a href="#quick-install">Quick install</a>
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<p align="center">
|
|
65
|
+
<img src="docs/img/change-request-detail.png" width="800" alt="A change request, showing the approval status, the pre-merge checks and the conflict banner" />
|
|
66
|
+
</p>
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
1. Someone creates a branch and makes their changes inside it, as normal for netbox-branching.
|
|
71
|
+
2. They open a **change request** against that branch.
|
|
72
|
+
3. The plugin reads which object types the branch touches and attaches every **policy** whose scope matches. A policy can narrow further on the values of the objects themselves. The author cannot remove them.
|
|
73
|
+
4. Each policy contains **rules**. A rule says how many approvals it needs and who may give them.
|
|
74
|
+
5. Reviewers **approve**, request changes, or comment. They can also comment on one specific changed object.
|
|
75
|
+
6. Independently, the **pre-merge checks** named by those policies run. A required check that is not passing blocks the merge on its own.
|
|
76
|
+
7. Once every rule is satisfied and every required check passes, the **merge** button appears.
|
|
77
|
+
8. After the merge, the request is marked completed.
|
|
78
|
+
|
|
79
|
+
Two gates guard the merge and they are independent: the people gate (policies and reviews) and the machine gate (checks). A change can be approved by every reviewer and still be refused by a check.
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
Full documentation is in [`docs/`](docs/index.md).
|
|
84
|
+
|
|
85
|
+
| Page | Covers |
|
|
86
|
+
|---|---|
|
|
87
|
+
| [Installation and configuration](docs/installation.md) | Requirements, installing, and every setting. |
|
|
88
|
+
| [Policies and rules](docs/policies.md) | Scoping a policy and writing rules. |
|
|
89
|
+
| [Policy conditions](docs/policy-conditions.md) | Narrowing a policy on object values. |
|
|
90
|
+
| [Conflicts with main](docs/conflicts.md) | What counts as a real conflict, and how to resolve one. |
|
|
91
|
+
| [Change requests](docs/change-requests.md) | The lifecycle, and what survives a branch deletion. |
|
|
92
|
+
| [Reviews](docs/reviews.md) | Submitting reviews and commenting on individual changes. |
|
|
93
|
+
| [Pre-merge checks](docs/checks.md) | What checks are and which ship built in. |
|
|
94
|
+
| [Writing your own checks](docs/custom-checks.md) | The registry, an AI reviewer, and reporting from CI. |
|
|
95
|
+
| [Event rules](docs/event-rules.md) | Firing a webhook or a script on a change request. |
|
|
96
|
+
| [Merging, windows and auto-merge](docs/merging.md) | Change windows and automatic merging. |
|
|
97
|
+
| [Protecting main](docs/protect-main.md) | Requiring a branch, optionally for part of NetBox only. |
|
|
98
|
+
| [Automatic behaviours](docs/automation.md) | Stale reviews, reevaluation, notifications. |
|
|
99
|
+
| [Administration guide](docs/admin-guide.md) | Roles, the permission matrix, building policies, troubleshooting. |
|
|
100
|
+
| [Permissions](docs/permissions.md) | The short reference for every permission name. |
|
|
101
|
+
| [REST API](docs/api.md) | Every endpoint. |
|
|
102
|
+
| [Extending this plugin](docs/extending.md) | How another plugin adds content and checks. |
|
|
103
|
+
| [Design](docs/design.md) | Why it is built this way. |
|
|
104
|
+
|
|
105
|
+
## Features
|
|
106
|
+
|
|
107
|
+
| Feature | Status |
|
|
108
|
+
|---|---|
|
|
109
|
+
| Policies containing rules with a minimum approval count | Done |
|
|
110
|
+
| Rules naming reviewer groups and individual reviewers | Done |
|
|
111
|
+
| Policies attached automatically, scope-matched from the branch contents, following it as it changes, and not selectable by the author | Done |
|
|
112
|
+
| Policy conditions, narrowing a policy on the values of the changed objects | Done |
|
|
113
|
+
| Change requests with status and priority | Done |
|
|
114
|
+
| Reviews with approve, request changes, and comment | Done |
|
|
115
|
+
| Per-change comments on the branch diff, with threaded replies, in Markdown | Done |
|
|
116
|
+
| Merge button appears once approved | Done, on the change request and on the branch |
|
|
117
|
+
| Status set to **completed** after a successful merge | Done |
|
|
118
|
+
| Pre-merge gate, enforced regardless of `protect_main` | Done |
|
|
119
|
+
| `protect_main` blocks direct edits outside a branch, optionally scoped | Done, with a bypass permission |
|
|
120
|
+
| Stale review detection when the branch changes | Done |
|
|
121
|
+
| Approval invalidation when the branch changes after approval | Done |
|
|
122
|
+
| Policy reevaluation on rule, reviewer or group membership change | Done |
|
|
123
|
+
| Real conflicts with main, distinguished from a stale branching baseline | Done |
|
|
124
|
+
| Notifications to reviewers | Done, through NetBox's notification inbox |
|
|
125
|
+
| Pluggable pre-merge checks, in-process or reported over the REST API | Done |
|
|
126
|
+
| Change windows, with an override permission | Done |
|
|
127
|
+
| Automatic merge once every gate is satisfied | Done |
|
|
128
|
+
| Change request survives deletion of its branch | Done |
|
|
129
|
+
| REST API for every model | Done |
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
| Component | Version |
|
|
134
|
+
|---|---|
|
|
135
|
+
| NetBox | `>= 4.6.9, < 4.7` |
|
|
136
|
+
| netbox-branching | `>= 1.1.3, < 1.2` |
|
|
137
|
+
| Python | `>= 3.12` |
|
|
138
|
+
|
|
139
|
+
## Quick install
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# configuration/plugins.py
|
|
143
|
+
PLUGINS = [
|
|
144
|
+
'netbox_change_control',
|
|
145
|
+
'netbox_branching', # must stay last
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
PLUGINS_CONFIG = {
|
|
149
|
+
'netbox_branching': {
|
|
150
|
+
'exempt_models': ['netbox_change_control.*'], # required
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
./manage.py migrate netbox_change_control
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
See [Installation and configuration](docs/installation.md) for the detail, including why `exempt_models` is not optional.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="docs/img/logo.svg" width="460" alt="netbox change control" />
|
|
3
|
+
<p><strong>Policy-driven change control and mandatory review for NetBox branches</strong></p>
|
|
4
|
+
<p>change requests • policies • checks • comments</p>
|
|
5
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" />
|
|
6
|
+
<img src="https://img.shields.io/badge/NetBox-%3E%3D%204.6.9%2C%20%3C%204.7-00857d" alt="NetBox compatibility" />
|
|
7
|
+
<img src="https://img.shields.io/badge/netbox--branching-%3E%3D%201.1.3-00857d" alt="netbox-branching compatibility" />
|
|
8
|
+
<img src="https://img.shields.io/badge/python-%3E%3D%203.12-blue" alt="Python version" />
|
|
9
|
+
<p>
|
|
10
|
+
<strong><a href="docs/index.md">Documentation</a></strong> |
|
|
11
|
+
<strong><a href="docs/installation.md">Install</a></strong> |
|
|
12
|
+
<strong><a href="docs/policies.md">Policies</a></strong> |
|
|
13
|
+
<strong><a href="docs/checks.md">Checks</a></strong> |
|
|
14
|
+
<strong><a href="CHANGELOG.md">Changelog</a></strong>
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
This plugin builds on [netbox-branching](https://github.com/netboxlabs/netbox-branching). A branch stages your changes; this plugin decides who must approve them and refuses the merge until they have.
|
|
19
|
+
|
|
20
|
+
The goal is change control that is **policy driven**: who must approve a change is decided by the objects it touches, not by whoever opened it. Around that sit two extension points, so the same gate can be driven by more than people. Pre-merge **checks** are pluggable, and an event fires on every status change, so a change request can call an external system, wait for a CI result, or ask a model to review the diff before anyone merges it. See [writing your own checks](docs/custom-checks.md), which includes an AI reviewer and a CI reporter.
|
|
21
|
+
|
|
22
|
+
It takes ideas from **[NetBox Labs change management](https://netboxlabs.com/docs/changes/)**, for policies and rules governing who must review a branch; from **GitHub**, for status checks that gate a merge independently of human approval and for review comments anchored to a specific change; and from **[Infrahub](https://docs.infrahub.app/topics/proposed-change)** by OpsMill, for treating a proposed change as a first-class object that carries its own validation.
|
|
23
|
+
|
|
24
|
+
> [!NOTE]
|
|
25
|
+
> **Independent community plugin.** Free, MIT licensed, not official, not certified and not endorsed by NetBox Labs. It bundles no netbox-branching code. You install that package yourself, and its own licence governs how you may use it.
|
|
26
|
+
>
|
|
27
|
+
> No commercial support. If you need a supported product, use the [NetBox Labs](https://netboxlabs.com/docs/changes/) change management plugin.
|
|
28
|
+
|
|
29
|
+
> [!WARNING]
|
|
30
|
+
> **Not stable yet.** The version is below 1.0. Models, settings and the REST API can still change between releases. Read the [changelog](CHANGELOG.md) before you upgrade.
|
|
31
|
+
|
|
32
|
+
<p align="center">
|
|
33
|
+
<a href="#how-it-works">How it works</a> |
|
|
34
|
+
<a href="#documentation">Documentation</a> |
|
|
35
|
+
<a href="#features">Features</a> |
|
|
36
|
+
<a href="#requirements">Requirements</a> |
|
|
37
|
+
<a href="#quick-install">Quick install</a>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<img src="docs/img/change-request-detail.png" width="800" alt="A change request, showing the approval status, the pre-merge checks and the conflict banner" />
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
1. Someone creates a branch and makes their changes inside it, as normal for netbox-branching.
|
|
47
|
+
2. They open a **change request** against that branch.
|
|
48
|
+
3. The plugin reads which object types the branch touches and attaches every **policy** whose scope matches. A policy can narrow further on the values of the objects themselves. The author cannot remove them.
|
|
49
|
+
4. Each policy contains **rules**. A rule says how many approvals it needs and who may give them.
|
|
50
|
+
5. Reviewers **approve**, request changes, or comment. They can also comment on one specific changed object.
|
|
51
|
+
6. Independently, the **pre-merge checks** named by those policies run. A required check that is not passing blocks the merge on its own.
|
|
52
|
+
7. Once every rule is satisfied and every required check passes, the **merge** button appears.
|
|
53
|
+
8. After the merge, the request is marked completed.
|
|
54
|
+
|
|
55
|
+
Two gates guard the merge and they are independent: the people gate (policies and reviews) and the machine gate (checks). A change can be approved by every reviewer and still be refused by a check.
|
|
56
|
+
|
|
57
|
+
## Documentation
|
|
58
|
+
|
|
59
|
+
Full documentation is in [`docs/`](docs/index.md).
|
|
60
|
+
|
|
61
|
+
| Page | Covers |
|
|
62
|
+
|---|---|
|
|
63
|
+
| [Installation and configuration](docs/installation.md) | Requirements, installing, and every setting. |
|
|
64
|
+
| [Policies and rules](docs/policies.md) | Scoping a policy and writing rules. |
|
|
65
|
+
| [Policy conditions](docs/policy-conditions.md) | Narrowing a policy on object values. |
|
|
66
|
+
| [Conflicts with main](docs/conflicts.md) | What counts as a real conflict, and how to resolve one. |
|
|
67
|
+
| [Change requests](docs/change-requests.md) | The lifecycle, and what survives a branch deletion. |
|
|
68
|
+
| [Reviews](docs/reviews.md) | Submitting reviews and commenting on individual changes. |
|
|
69
|
+
| [Pre-merge checks](docs/checks.md) | What checks are and which ship built in. |
|
|
70
|
+
| [Writing your own checks](docs/custom-checks.md) | The registry, an AI reviewer, and reporting from CI. |
|
|
71
|
+
| [Event rules](docs/event-rules.md) | Firing a webhook or a script on a change request. |
|
|
72
|
+
| [Merging, windows and auto-merge](docs/merging.md) | Change windows and automatic merging. |
|
|
73
|
+
| [Protecting main](docs/protect-main.md) | Requiring a branch, optionally for part of NetBox only. |
|
|
74
|
+
| [Automatic behaviours](docs/automation.md) | Stale reviews, reevaluation, notifications. |
|
|
75
|
+
| [Administration guide](docs/admin-guide.md) | Roles, the permission matrix, building policies, troubleshooting. |
|
|
76
|
+
| [Permissions](docs/permissions.md) | The short reference for every permission name. |
|
|
77
|
+
| [REST API](docs/api.md) | Every endpoint. |
|
|
78
|
+
| [Extending this plugin](docs/extending.md) | How another plugin adds content and checks. |
|
|
79
|
+
| [Design](docs/design.md) | Why it is built this way. |
|
|
80
|
+
|
|
81
|
+
## Features
|
|
82
|
+
|
|
83
|
+
| Feature | Status |
|
|
84
|
+
|---|---|
|
|
85
|
+
| Policies containing rules with a minimum approval count | Done |
|
|
86
|
+
| Rules naming reviewer groups and individual reviewers | Done |
|
|
87
|
+
| Policies attached automatically, scope-matched from the branch contents, following it as it changes, and not selectable by the author | Done |
|
|
88
|
+
| Policy conditions, narrowing a policy on the values of the changed objects | Done |
|
|
89
|
+
| Change requests with status and priority | Done |
|
|
90
|
+
| Reviews with approve, request changes, and comment | Done |
|
|
91
|
+
| Per-change comments on the branch diff, with threaded replies, in Markdown | Done |
|
|
92
|
+
| Merge button appears once approved | Done, on the change request and on the branch |
|
|
93
|
+
| Status set to **completed** after a successful merge | Done |
|
|
94
|
+
| Pre-merge gate, enforced regardless of `protect_main` | Done |
|
|
95
|
+
| `protect_main` blocks direct edits outside a branch, optionally scoped | Done, with a bypass permission |
|
|
96
|
+
| Stale review detection when the branch changes | Done |
|
|
97
|
+
| Approval invalidation when the branch changes after approval | Done |
|
|
98
|
+
| Policy reevaluation on rule, reviewer or group membership change | Done |
|
|
99
|
+
| Real conflicts with main, distinguished from a stale branching baseline | Done |
|
|
100
|
+
| Notifications to reviewers | Done, through NetBox's notification inbox |
|
|
101
|
+
| Pluggable pre-merge checks, in-process or reported over the REST API | Done |
|
|
102
|
+
| Change windows, with an override permission | Done |
|
|
103
|
+
| Automatic merge once every gate is satisfied | Done |
|
|
104
|
+
| Change request survives deletion of its branch | Done |
|
|
105
|
+
| REST API for every model | Done |
|
|
106
|
+
|
|
107
|
+
## Requirements
|
|
108
|
+
|
|
109
|
+
| Component | Version |
|
|
110
|
+
|---|---|
|
|
111
|
+
| NetBox | `>= 4.6.9, < 4.7` |
|
|
112
|
+
| netbox-branching | `>= 1.1.3, < 1.2` |
|
|
113
|
+
| Python | `>= 3.12` |
|
|
114
|
+
|
|
115
|
+
## Quick install
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# configuration/plugins.py
|
|
119
|
+
PLUGINS = [
|
|
120
|
+
'netbox_change_control',
|
|
121
|
+
'netbox_branching', # must stay last
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
PLUGINS_CONFIG = {
|
|
125
|
+
'netbox_branching': {
|
|
126
|
+
'exempt_models': ['netbox_change_control.*'], # required
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
./manage.py migrate netbox_change_control
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
See [Installation and configuration](docs/installation.md) for the detail, including why `exempt_models` is not optional.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from netbox.plugins import PluginConfig, get_plugin_config
|
|
2
|
+
|
|
3
|
+
__version__ = '0.3.0'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ChangeControlConfig(PluginConfig):
|
|
7
|
+
name = 'netbox_change_control'
|
|
8
|
+
verbose_name = 'NetBox Change Control'
|
|
9
|
+
description = 'Policy-driven change control and mandatory review for NetBox branches'
|
|
10
|
+
version = __version__
|
|
11
|
+
base_url = 'change-control'
|
|
12
|
+
# NetBox refuses to load the plugin outside this range, which is the only place the
|
|
13
|
+
# requirement is enforced rather than documented. The floor is 4.6.9 because that is what
|
|
14
|
+
# is tested; below 4.6.5 the initial migration cannot even apply, since it depends on
|
|
15
|
+
# extras.0140, and below 4.6.0 on core.0024 and users.0016 as well.
|
|
16
|
+
min_version = '4.6.9'
|
|
17
|
+
max_version = '4.6.99'
|
|
18
|
+
default_settings = { # noqa: RUF012
|
|
19
|
+
# Block writes to branching-supported models outside of a branch. Users holding the
|
|
20
|
+
# netbox_change_control.bypass_policy permission are exempt.
|
|
21
|
+
'protect_main': False,
|
|
22
|
+
# Limit protect_main to specific models. Empty means every branching-supported model.
|
|
23
|
+
# Entries are 'app_label.modelname' or 'app_label.*', for example ['circuits.*'].
|
|
24
|
+
'protect_main_scope': [],
|
|
25
|
+
# Refuse to merge a branch which has no approved change request. This is the core
|
|
26
|
+
# guarantee of the plugin; disable it only to troubleshoot.
|
|
27
|
+
'enforce_merge_gate': True,
|
|
28
|
+
# Send a NetBox notification to the outstanding reviewers when a change request
|
|
29
|
+
# needs review, and to the requester when it is approved or rejected.
|
|
30
|
+
'notify_reviewers': True,
|
|
31
|
+
# Where a branch page shows its change request. 'right_page' puts it in a card in
|
|
32
|
+
# the right-hand column, beside branching's own cards; 'alerts' puts it in a band
|
|
33
|
+
# across the top of the page, above them. Naming both shows both, and an empty list
|
|
34
|
+
# shows neither.
|
|
35
|
+
'branch_page_placement': ['right_page'],
|
|
36
|
+
# Which built-in pre-merge checks to make available. True offers all of them, False
|
|
37
|
+
# none, and a list a subset. Registering only makes a check selectable: a policy
|
|
38
|
+
# decides where it applies, by naming it in its Checks field. Valid names:
|
|
39
|
+
# 'has-changes' the branch contains something to merge
|
|
40
|
+
# 'no-conflicts' no conflicts with main
|
|
41
|
+
# 'not-stale' the branch is recent enough to sync
|
|
42
|
+
# 'threads-resolved' every comment thread on the Changes tab is resolved
|
|
43
|
+
'enable_builtin_checks': True,
|
|
44
|
+
# Checks reported by an external system through the REST API. Each is created as
|
|
45
|
+
# pending and blocks the merge until a result is reported. Entries are a name, or a
|
|
46
|
+
# (name, label) pair.
|
|
47
|
+
'required_external_checks': [],
|
|
48
|
+
# Allow change requests to merge themselves once every gate is satisfied. Each
|
|
49
|
+
# request must also opt in individually. Turning this off disables the feature
|
|
50
|
+
# globally without editing any request, and stops the periodic job being registered.
|
|
51
|
+
'enable_auto_merge': True,
|
|
52
|
+
# Minutes between automatic merge sweeps. This bounds how late a change window can
|
|
53
|
+
# fire: at 10, a window opening at 21:00 merges by 21:09. Lower it for tighter
|
|
54
|
+
# windows, at the cost of one Job record per run. A change window shorter than this
|
|
55
|
+
# interval may never merge automatically, and says so on the change request. Takes
|
|
56
|
+
# effect when the worker restarts.
|
|
57
|
+
'auto_merge_interval': 10,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def ready(self):
|
|
61
|
+
super().ready()
|
|
62
|
+
|
|
63
|
+
from netbox_branching.models import Branch
|
|
64
|
+
|
|
65
|
+
# NetBox auto-imports a plugin's navigation, search and template_content modules, but
|
|
66
|
+
# not its dashboard module, so a widget is registered only if something imports it.
|
|
67
|
+
# Without this the My Reviews widget is missing from "Add widget", and a dashboard
|
|
68
|
+
# already holding one fails to load at all.
|
|
69
|
+
from . import dashboard, events, signal_receivers # noqa: F401
|
|
70
|
+
|
|
71
|
+
# Registering the job is what makes rqworker schedule it, so skipping the import
|
|
72
|
+
# when auto-merge is off means no periodic job and no Job records at all.
|
|
73
|
+
if get_plugin_config('netbox_change_control', 'enable_auto_merge'):
|
|
74
|
+
from . import jobs # noqa: F401
|
|
75
|
+
from .checks import register_builtin_checks
|
|
76
|
+
from .validators import require_approved_change_request
|
|
77
|
+
|
|
78
|
+
Branch.register_preaction_check(require_approved_change_request, 'merge')
|
|
79
|
+
|
|
80
|
+
# Registering makes a check available; a policy decides where it applies. True makes
|
|
81
|
+
# every built-in available, False none, and a list a subset.
|
|
82
|
+
selection = get_plugin_config('netbox_change_control', 'enable_builtin_checks')
|
|
83
|
+
if selection:
|
|
84
|
+
register_builtin_checks(list(selection) if isinstance(selection, (list, tuple, set)) else None)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
config = ChangeControlConfig
|
|
File without changes
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from netbox.api.serializers import NetBoxModelSerializer
|
|
2
|
+
from rest_framework import serializers
|
|
3
|
+
|
|
4
|
+
from netbox_change_control.models import (
|
|
5
|
+
ChangeComment,
|
|
6
|
+
ChangeRequest,
|
|
7
|
+
MergeCheck,
|
|
8
|
+
Policy,
|
|
9
|
+
PolicyRule,
|
|
10
|
+
Review,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
'ChangeCommentSerializer',
|
|
15
|
+
'ChangeRequestSerializer',
|
|
16
|
+
'MergeCheckSerializer',
|
|
17
|
+
'PolicyRuleSerializer',
|
|
18
|
+
'PolicySerializer',
|
|
19
|
+
'ReviewSerializer',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PolicySerializer(NetBoxModelSerializer):
|
|
24
|
+
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_change_control-api:policy-detail')
|
|
25
|
+
|
|
26
|
+
class Meta:
|
|
27
|
+
model = Policy
|
|
28
|
+
fields = (
|
|
29
|
+
'id',
|
|
30
|
+
'url',
|
|
31
|
+
'display',
|
|
32
|
+
'name',
|
|
33
|
+
'description',
|
|
34
|
+
'enabled',
|
|
35
|
+
'weight',
|
|
36
|
+
'object_types',
|
|
37
|
+
'conditions',
|
|
38
|
+
'condition_state',
|
|
39
|
+
'checks',
|
|
40
|
+
'comments',
|
|
41
|
+
'tags',
|
|
42
|
+
'custom_fields',
|
|
43
|
+
'created',
|
|
44
|
+
'last_updated',
|
|
45
|
+
)
|
|
46
|
+
brief_fields = ('id', 'url', 'display', 'name')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PolicyRuleSerializer(NetBoxModelSerializer):
|
|
50
|
+
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_change_control-api:policyrule-detail')
|
|
51
|
+
policy = PolicySerializer(nested=True)
|
|
52
|
+
|
|
53
|
+
class Meta:
|
|
54
|
+
model = PolicyRule
|
|
55
|
+
fields = (
|
|
56
|
+
'id',
|
|
57
|
+
'url',
|
|
58
|
+
'display',
|
|
59
|
+
'policy',
|
|
60
|
+
'name',
|
|
61
|
+
'min_reviews',
|
|
62
|
+
'groups',
|
|
63
|
+
'users',
|
|
64
|
+
'tags',
|
|
65
|
+
'custom_fields',
|
|
66
|
+
'created',
|
|
67
|
+
'last_updated',
|
|
68
|
+
)
|
|
69
|
+
brief_fields = ('id', 'url', 'display', 'name', 'min_reviews')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ChangeRequestSerializer(NetBoxModelSerializer):
|
|
73
|
+
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_change_control-api:changerequest-detail')
|
|
74
|
+
approved = serializers.BooleanField(source='is_approved', read_only=True)
|
|
75
|
+
branch_name = serializers.CharField(read_only=True)
|
|
76
|
+
branch_deleted = serializers.BooleanField(read_only=True)
|
|
77
|
+
ready_to_merge = serializers.BooleanField(source='is_ready_to_merge', read_only=True)
|
|
78
|
+
# Derived from the policy evaluation, so it is reported and never set. A writable status
|
|
79
|
+
# let a caller mark a request Completed, which is terminal and blocks its branch from ever
|
|
80
|
+
# merging. Use the abandon and reopen actions for the two transitions a person makes.
|
|
81
|
+
status = serializers.CharField(read_only=True)
|
|
82
|
+
merge_blocked_reason = serializers.CharField(read_only=True)
|
|
83
|
+
has_conflicts = serializers.BooleanField(read_only=True)
|
|
84
|
+
|
|
85
|
+
class Meta:
|
|
86
|
+
model = ChangeRequest
|
|
87
|
+
fields = (
|
|
88
|
+
'id',
|
|
89
|
+
'url',
|
|
90
|
+
'display',
|
|
91
|
+
'ref',
|
|
92
|
+
'branch',
|
|
93
|
+
'branch_name',
|
|
94
|
+
'branch_deleted',
|
|
95
|
+
'title',
|
|
96
|
+
'description',
|
|
97
|
+
'status',
|
|
98
|
+
'priority',
|
|
99
|
+
'requester',
|
|
100
|
+
'approved',
|
|
101
|
+
'ready_to_merge',
|
|
102
|
+
'merge_blocked_reason',
|
|
103
|
+
'has_conflicts',
|
|
104
|
+
'scheduled_start',
|
|
105
|
+
'scheduled_end',
|
|
106
|
+
'auto_merge',
|
|
107
|
+
'comments',
|
|
108
|
+
'tags',
|
|
109
|
+
'custom_fields',
|
|
110
|
+
'created',
|
|
111
|
+
'last_updated',
|
|
112
|
+
)
|
|
113
|
+
brief_fields = ('id', 'url', 'display', 'ref', 'title', 'status')
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ReviewSerializer(NetBoxModelSerializer):
|
|
117
|
+
"""
|
|
118
|
+
A review is a personal statement, so the API records it as the caller and never as
|
|
119
|
+
somebody else.
|
|
120
|
+
|
|
121
|
+
`reviewer` is read-only and defaults to the requesting user. Leaving it writable let any
|
|
122
|
+
token holding `add_review` post an approval attributed to a colleague, which defeats the
|
|
123
|
+
merge gate with one request. The object form does not offer the field for the same reason.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_change_control-api:review-detail')
|
|
127
|
+
reviewer = serializers.PrimaryKeyRelatedField(read_only=True)
|
|
128
|
+
|
|
129
|
+
class Meta:
|
|
130
|
+
model = Review
|
|
131
|
+
fields = (
|
|
132
|
+
'id',
|
|
133
|
+
'url',
|
|
134
|
+
'display',
|
|
135
|
+
'change_request',
|
|
136
|
+
'reviewer',
|
|
137
|
+
'decision',
|
|
138
|
+
'comment',
|
|
139
|
+
'tags',
|
|
140
|
+
'custom_fields',
|
|
141
|
+
'created',
|
|
142
|
+
'last_updated',
|
|
143
|
+
)
|
|
144
|
+
brief_fields = ('id', 'url', 'display', 'decision')
|
|
145
|
+
|
|
146
|
+
def validate(self, attrs):
|
|
147
|
+
"""
|
|
148
|
+
Attribute a new review to the caller.
|
|
149
|
+
|
|
150
|
+
A read-only field carrying a default is not enough, because DRF leaves a read-only
|
|
151
|
+
field out of validated_data entirely. The assignment also has to happen before
|
|
152
|
+
super(), since NetBox's ValidatedModelSerializer builds a model instance from these
|
|
153
|
+
attributes and calls full_clean() on it, which would reject the missing reviewer.
|
|
154
|
+
"""
|
|
155
|
+
# `not self.nested` matters: a nested serializer's to_internal_value returns a model
|
|
156
|
+
# instance rather than a dict, and assigning into it raises TypeError.
|
|
157
|
+
if self.instance is None and not self.nested:
|
|
158
|
+
request = self.context.get('request')
|
|
159
|
+
if request is not None:
|
|
160
|
+
attrs['reviewer'] = request.user
|
|
161
|
+
return super().validate(attrs)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class MergeCheckSerializer(NetBoxModelSerializer):
|
|
165
|
+
"""
|
|
166
|
+
External systems report a result by PATCHing status, summary and details_url onto an
|
|
167
|
+
existing check.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_change_control-api:mergecheck-detail')
|
|
171
|
+
blocks_merge = serializers.BooleanField(read_only=True)
|
|
172
|
+
# A reporter must not decide whether its own check counts. Requiredness comes from the
|
|
173
|
+
# configuration or the policy, and the gate reads it from there rather than from this row,
|
|
174
|
+
# so a writable field here would only mislead.
|
|
175
|
+
required = serializers.BooleanField(read_only=True)
|
|
176
|
+
# The check belongs to the change request which created it. Moving it elsewhere would
|
|
177
|
+
# detach a result from what it was measuring.
|
|
178
|
+
change_request = serializers.PrimaryKeyRelatedField(read_only=True)
|
|
179
|
+
|
|
180
|
+
class Meta:
|
|
181
|
+
model = MergeCheck
|
|
182
|
+
fields = (
|
|
183
|
+
'id',
|
|
184
|
+
'url',
|
|
185
|
+
'display',
|
|
186
|
+
'change_request',
|
|
187
|
+
'name',
|
|
188
|
+
'label',
|
|
189
|
+
'status',
|
|
190
|
+
'required',
|
|
191
|
+
'summary',
|
|
192
|
+
'details_url',
|
|
193
|
+
'completed',
|
|
194
|
+
'blocks_merge',
|
|
195
|
+
'tags',
|
|
196
|
+
'custom_fields',
|
|
197
|
+
'created',
|
|
198
|
+
'last_updated',
|
|
199
|
+
)
|
|
200
|
+
brief_fields = ('id', 'url', 'display', 'name', 'status')
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ChangeCommentSerializer(NetBoxModelSerializer):
|
|
204
|
+
"""
|
|
205
|
+
A comment is a personal statement, so the API records it as the caller and never as
|
|
206
|
+
somebody else.
|
|
207
|
+
|
|
208
|
+
`author` is read-only and defaults to the requesting user, for the same reason `reviewer`
|
|
209
|
+
is on ReviewSerializer. Leaving it writable let any token holding `add_changecomment` post
|
|
210
|
+
a comment attributed to a colleague, which is enough to fake a sign-off in the discussion
|
|
211
|
+
a reviewer reads before approving.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_change_control-api:changecomment-detail')
|
|
215
|
+
author = serializers.PrimaryKeyRelatedField(read_only=True)
|
|
216
|
+
|
|
217
|
+
def validate(self, attrs):
|
|
218
|
+
"""
|
|
219
|
+
Attribute a new comment to the caller.
|
|
220
|
+
|
|
221
|
+
A read-only field carrying a default is not enough, because DRF leaves a read-only
|
|
222
|
+
field out of validated_data entirely. The assignment also has to happen before
|
|
223
|
+
super(), since NetBox's ValidatedModelSerializer builds a model instance from these
|
|
224
|
+
attributes and calls full_clean() on it, which would reject the missing author.
|
|
225
|
+
"""
|
|
226
|
+
# `not self.nested` matters: a nested serializer's to_internal_value returns a model
|
|
227
|
+
# instance rather than a dict, and assigning into it raises TypeError.
|
|
228
|
+
if self.instance is None and not self.nested:
|
|
229
|
+
request = self.context.get('request')
|
|
230
|
+
if request is not None:
|
|
231
|
+
attrs['author'] = request.user
|
|
232
|
+
return super().validate(attrs)
|
|
233
|
+
|
|
234
|
+
class Meta:
|
|
235
|
+
model = ChangeComment
|
|
236
|
+
fields = (
|
|
237
|
+
'id',
|
|
238
|
+
'url',
|
|
239
|
+
'display',
|
|
240
|
+
'change_request',
|
|
241
|
+
'change_diff',
|
|
242
|
+
'parent',
|
|
243
|
+
'author',
|
|
244
|
+
'text',
|
|
245
|
+
'resolved',
|
|
246
|
+
'tags',
|
|
247
|
+
'custom_fields',
|
|
248
|
+
'created',
|
|
249
|
+
'last_updated',
|
|
250
|
+
)
|
|
251
|
+
brief_fields = ('id', 'url', 'display', 'text')
|