Repository: netbox-community/netbox

Issue:
Allow custom scripts to fail gracefully on errors detected by the scripts
### NetBox version

v3.3.9

### Feature type

Change to existing functionality

### Proposed functionality

It would be nice if a custom script (and I guess a report, too), would have the option to throw a special kind of Exception (say `AbortScript (msg: str)`, which stops the script, aborts the DB transaction, and stores the error `msg` so the caller can see the fact that the script was aborted and why.

Happy to open a PR to add this into the `_run_script()` function. If there is a way to do this, which I didn't find, I'm more than happy for a pointer :-)

### Use case

I wrote a scripts which should aid in provisioning things inside our network, which will do some sanity checks and if all is good set up a number of things (prefixes, IPs, sub-interfaces, IPs on them, etc.). The reason to do this inside a script is to do this in an atomic operation, so either the full provision process is done, or nothing is changed at all.

Now I was looking into exiting the script on an error and only found the option to throw any `Exception` which will produce a stack trace of all my internal Exceptions I caught and handled. When I throw a `AbortTransaction` Exception from the script I can't pass any message.

### Database changes

None.

### External dependencies

None.


Discussion:
Sounds good. It _might_ make sense to use the `AbortRequest` exception for this.

Resolving patch:
diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py
--- a/netbox/extras/scripts.py
+++ b/netbox/extras/scripts.py
@@ -21,7 +21,7 @@
 from extras.signals import clear_webhooks
 from ipam.formfields import IPAddressFormField, IPNetworkFormField
 from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator
-from utilities.exceptions import AbortTransaction
+from utilities.exceptions import AbortScript, AbortTransaction
 from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField
 from .context_managers import change_logging
 from .forms import ScriptForm
@@ -470,6 +470,14 @@ def _run_script():
         except AbortTransaction:
             script.log_info("Database changes have been reverted automatically.")
             clear_webhooks.send(request)
+        except AbortScript as e:
+            script.log_failure(
+                f"Script aborted with error: {e}"
+            )
+            script.log_info("Database changes have been reverted due to error.")
+            logger.error(f"Script aborted with error: {e}")
+            job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
+            clear_webhooks.send(request)
         except Exception as e:
             stacktrace = traceback.format_exc()
             script.log_failure(
diff --git a/netbox/utilities/exceptions.py b/netbox/utilities/exceptions.py
--- a/netbox/utilities/exceptions.py
+++ b/netbox/utilities/exceptions.py
@@ -24,6 +24,13 @@ def __init__(self, message):
         self.message = message
 
 
+class AbortScript(Exception):
+    """
+    Raised to cleanly abort a script.
+    """
+    pass
+
+
 class PermissionsViolation(Exception):
     """
     Raised when an operation was prevented because it would violate the


Test patch: