# Custom Scripts Custom scripting was introduced to provide a way for users to execute custom logic from within the NetBox UI. Custom scripts enable the user to directly and conveniently manipulate NetBox data in a prescribed fashion. They can be used to accomplish myriad tasks, such as: * Automatically populate new devices and cables in preparation for a new site deployment * Create a range of new reserved prefixes or IP addresses * Fetch data from an external source and import it to NetBox Custom scripts are Python code and exist outside of the official NetBox code base, so they can be updated and changed without interfering with the core NetBox installation. And because they're completely custom, there is no inherent limitation on what a script can accomplish. ## Writing Custom Scripts All custom scripts must inherit from the `extras.scripts.Script` base class. This class provides the functionality necessary to generate forms and log activity. ```python from extras.scripts import Script class MyScript(Script): ... ``` Scripts comprise two core components: a set of variables and a `run()` method. Variables allow your script to accept user input via the NetBox UI, but they are optional: If your script does not require any user input, there is no need to define any variables. The `run()` method is where your script's execution logic lives. (Note that your script can have as many methods as needed: this is merely the point of invocation for NetBox.) ```python class MyScript(Script): var1 = StringVar(...) var2 = IntegerVar(...) var3 = ObjectVar(...) def run(self, data, commit): ... ``` The `run()` method should accept two arguments: * `data` - A dictionary containing all of the variable data passed via the web form. * `commit` - A boolean indicating whether database changes will be committed. !!! note The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however beginning with v2.10 NetBox will require the `run()` method of every script to accept both arguments. (Either argument may still be ignored within the method.) Defining script variables is optional: You may create a script with only a `run()` method if no user input is needed. Any output generated by the script during its execution will be displayed under the "output" tab in the UI. ## Module Attributes ### `name` You can define `name` within a script module (the Python file which contains one or more scripts) to set the module name. If `name` is not defined, the module's file name will be used. ## Script Attributes Script attributes are defined under a class named `Meta` within the script. These are optional, but encouraged. ### `name` This is the human-friendly names of your script. If omitted, the class name will be used. ### `description` A human-friendly description of what your script does. ### `commit_default` The checkbox to commit database changes when executing a script is checked by default. Set `commit_default` to False under the script's Meta class to leave this option unchecked by default. ```python commit_default = False ``` ## Accessing Request Data Details of the current HTTP request (the one being made to execute the script) are available as the instance attribute `self.request`. This can be used to infer, for example, the user executing the script and the client IP address: ```python username = self.request.user.username ip_address = self.request.META.get('HTTP_X_FORWARDED_FOR') or \ self.request.META.get('REMOTE_ADDR') self.log_info(f"Running as user {username} (IP: {ip_address})...") ``` For a complete list of available request parameters, please see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/request-response/). ## Reading Data from Files The Script class provides two convenience methods for reading data from files: * `load_yaml` * `load_json` These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`). ## Logging The Script object provides a set of convenient functions for recording messages at different severity levels: * `log_debug` * `log_success` * `log_info` * `log_warning` * `log_failure` Log messages are returned to the user upon execution of the script. Markdown rendering is supported for log messages. ## Variable Reference ### Default Options All custom script variables support the following default options: * `default` - The field's default value * `description` - A brief user-friendly description of the field * `label` - The field name to be displayed in the rendered form * `required` - Indicates whether the field is mandatory (all fields are required by default) * `widget` - The class of form widget to use (see the [Django documentation](https://docs.djangoproject.com/en/stable/ref/forms/widgets/)) ### StringVar Stores a string of characters (i.e. text). Options include: * `min_length` - Minimum number of characters * `max_length` - Maximum number of characters * `regex` - A regular expression against which the provided value must match Note that `min_length` and `max_length` can be set to the same number to effect a fixed-length field. ### TextVar Arbitrary text of any length. Renders as a multi-line text input field. ### IntegerVar Stores a numeric integer. Options include: * `min_value` - Minimum value * `max_value` - Maximum value ### BooleanVar A true/false flag. This field has no options beyond the defaults listed above. ### ChoiceVar A set of choices from which the user can select one. * `choices` - A list of `(value, label)` tuples representing the available choices. For example: ```python CHOICES = ( ('n', 'North'), ('s', 'South'), ('e', 'East'), ('w', 'West') ) direction = ChoiceVar(choices=CHOICES) ``` In the example above, selecting the choice labeled "North" will submit the value `n`. ### MultiChoiceVar Similar to `ChoiceVar`, but allows for the selection of multiple choices. ### ObjectVar A particular object within NetBox. Each ObjectVar must specify a particular model, and allows the user to select one of the available instances. ObjectVar accepts several arguments, listed below. * `model` - The model class * `query_params` - A dictionary of query parameters to use when retrieving available options (optional) * `null_option` - A label representing a "null" or empty choice (optional) To limit the selections available within the list, additional query parameters can be passed as the `query_params` dictionary. For example, to show only devices with an "active" status: ```python device = ObjectVar( model=Device, query_params={ 'status': 'active' } ) ``` Multiple values can be specified by assigning a list to the dictionary key. It is also possible to reference the value of other fields in the form by prepending a dollar sign (`$`) to the variable's name. ```python region = ObjectVar( model=Region ) site = ObjectVar( model=Site, query_params={ 'region_id': '$region' } ) ``` ### MultiObjectVar Similar to `ObjectVar`, but allows for the selection of multiple objects. ### FileVar An uploaded file. Note that uploaded files are present in memory only for the duration of the script's execution: They will not be automatically saved for future use. The script is responsible for writing file contents to disk where necessary. ### IPAddressVar An IPv4 or IPv6 address, without a mask. Returns a `netaddr.IPAddress` object. ### IPAddressWithMaskVar An IPv4 or IPv6 address with a mask. Returns a `netaddr.IPNetwork` object which includes the mask. ### IPNetworkVar An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two attributes are available to validate the provided mask: * `min_prefix_length` - Minimum length of the mask * `max_prefix_length` - Maximum length of the mask ## Running Custom Scripts !!! note To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below. ![Adding the run action to a permission](../../media/admin_ui_run_permission.png) ### Via the Web UI Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button. ### Via the API To run a script via the REST API, issue a POST request to the script's endpoint specifying the form data and commitment. For example, to run a script named `example.MyReport`, we would make a request such as the following: ```no-highlight curl -X POST \ -H "Authorization: Token $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json; indent=4" \ http://netbox/api/extras/scripts/example.MyReport/ \ --data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}' ``` ## Example Below is an example script that creates new objects for a planned site. The user is prompted for three variables: * The name of the new site * The device model (a filtered list of defined device types) * The number of access switches to create These variables are presented as a web form to be completed by the user. Once submitted, the script's `run()` method is called to create the appropriate objects. ```python from django.utils.text import slugify from dcim.choices import DeviceStatusChoices, SiteStatusChoices from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site from extras.scripts import * class NewBranchScript(Script): class Meta: name = "New Branch" description = "Provision a new branch site" field_order = ['site_name', 'switch_count', 'switch_model'] site_name = StringVar( description="Name of the new site" ) switch_count = IntegerVar( description="Number of access switches to create" ) manufacturer = ObjectVar( model=Manufacturer, required=False ) switch_model = ObjectVar( description="Access switch model", model=DeviceType, query_params={ 'manufacturer_id': '$manufacturer' } ) def run(self, data, commit): # Create the new site site = Site( name=data['site_name'], slug=slugify(data['site_name']), status=SiteStatusChoices.STATUS_PLANNED ) site.save() self.log_success(f"Created new site: {site}") # Create access switches switch_role = DeviceRole.objects.get(name='Access Switch') for i in range(1, data['switch_count'] + 1): switch = Device( device_type=data['switch_model'], name=f'{site.slug}-switch{i}', site=site, status=DeviceStatusChoices.STATUS_PLANNED, device_role=switch_role ) switch.save() self.log_success(f"Created new switch: {switch}") # Generate a CSV table of new devices output = [ 'name,make,model' ] for switch in Device.objects.filter(site=site): attrs = [ switch.name, switch.device_type.manufacturer.name, switch.device_type.model ] output.append(','.join(attrs)) return '\n'.join(output) ```