Skip to content

1. Deploying NSP Intents using Netbox#

Activity name Deploying NSP Intents with NetBox
Activity ID 57
Short Description Use NetBox for infrastructure management on top of NSP to create/update infrastructure intents.
Difficulty Advanced
Tools used Postman
Topology Nodes PE1, PE2, PE3, PE4, P1, P2
References Nokia Developer Portal (Sign up for a free account)
NSP Postman collections
Netbox Shell Documentation

1.1 Objective#

Netbox is a Source of Truth platform for recording the target state of your network infrastructure, both physical and logical. NSP is an intent-driven automation platform we'll be using to control the IP network domain, based on the information defined in Netbox. In this activity, you'll create a custom Netbox script to translate Netbox's version of the network state into NSP intents to control device configuration.

This is the rough flow chart of the solution we're going to construct, the Netbox custom script is where most of the effort will be focused.

flowchart LR
    nb(Netbox) --> |Event-Rule| sc(Netbox <br> Custom Script)
    sc -->|Restconf API| nsp(NSP)
    nsp --> |gNMI/NETCONF| net(Network)
    style sc fill:pink

1.2 Prerequisites#

  • NSP RESTCONF APIs using POSTMAN
  • Practical programming experience using python

1.3 NetBox#

1.3.1 DCIM#

The Data Center Infrastructure Management (DCIM) module in Netbox provides tools for documenting network infrastructure, including devices, racks, and cables. It centers around the Device model for hardware and the Interface model for network ports, allowing operators to track both physical connections and interface configurations in a centralized system.

DCIM's power lies in how it relates these components - Interfaces can be connected to other Interfaces via Cables, Devices can be mounted in Racks at specific positions, and everything can be organized by Site and Location. This creates a complete model of the physical network that serves as a source of truth for automation. The module also includes features for tracking power connections, console ports, and front/rear panel layouts of devices.

For network automation use-cases, the Device and Interface models are particularly valuable as they can be integrated with other systems (like NSP) to drive configuration changes based on the documented physical state in Netbox.

1.3.2 IPAM#

The IP Address Management (IPAM) module in Netbox provides tools for managing IP addresses, prefixes, VLANs, and VRFs. A key feature is the IPAddress model which represents individual IP addresses and their assignments. IPAddress objects can be directly associated with Interface objects from the DCIM module, creating a clear link between physical ports and their IP addressing. This relationship is crucial for network automation as it allows operators to track both the physical connectivity and logical addressing of network devices in one system. The IPAM module supports both IPv4 and IPv6 addressing, CIDR notation, and can enforce rules around address assignment and utilization. When integrated with the Interface model, it provides a complete view of both physical and logical network configurations that can be used as a source of truth for automation platforms.

1.4 Accessing Netbox#

Note

Login to the Netbox UI via the web interface:

URL: http://GROUP_NUMBER.srexperts.net:8000
Username: admin
Password: same password as the instance SSH or SROS and SRL nodes

1.5 Tasks#

You should read these tasks from top-to-bottom before beginning the activity.

It is tempting to skip ahead but tasks may require you to have completed previous tasks before tackling them.

1.5.1 Explore Netbox#

We've already done some work to load the Hackathon topology into Netbox - this includes specifying Manufactures and Device Types, creating Racks, installing Devices into racks with hostnames, Interfaces and connecting devices to one another.

1.5.1.1 Explore Core Netbox Components#

  • Browse the Devices section to see all network equipment
  • Examine the Racks view to understand physical infrastructure layout, use the Elevations menu option to view a visual representation of the rack front/rear.
  • Click into individual devices to view their basic information, browse the Interfaces tab to list interfaces and inspect their basic parameters.

1.5.1.2 Run the IP Allocation Custom Script#

You'll may notice that devices don't have IPs assigned to interface within Netbox. Because each lab environment has unique IP space, we have to dynamically add IP addresses to each Netbox instance. The next instructions will show you an example of how a custom script is run interactively and what they can do.

Run Netbox IP Assignment Script
  • Navigate to Customization > Scripts > Create IP interfaces on core nodes
  • In the Instance id field, put in your group number such as 10. Be sure this is correct, your NSP intents won't work unless this is correct.
  • Click the Run Script button.
  • This should result in successful results: IP Assignment Script Results

1.5.2 Explore the NSPs APIs#

1.5.2.1 Setup Postman Authentication#

  • Create an Environment
  • Populate the variables using the NSP credentials provided to you, don't just copy what's here: Postman Envvars
  • Update the Initial Authentication POST call with the following contents added to the Script tab:
    var jsonBody = JSON.parse(responseBody);
    pm.environment.set("token", jsonBody.access_token);
    
  • Run the Initial Authentication call and ensure a token is returned.

When the Initial Authentication call is made, the token should get updated in the environment variable. Once authenticated, the generated bearer-token will be used for follow-up API calls to authenticate. Be aware, that the token is only valid for a certain period of time. Either one must refresh it once in a while, or reauthenticate when the token has expired.

If you use an older version of POSTMAN, you will find the script in a tab called Tests.

Note

The POSTMAN collection hosted on the developer portal made available by the NSP product-team stores the auth-token under Globals. The script used in this activity stores the token in the environment instead, which allows toggling between different environments without the need to reconnect.

1.5.2.2 List all network ports#

List all network ports using RESTCONF GET 24.11 Network Functions > Network Infrastructure Management > Network Inventory > Get vs Find Restconf API samples> GET getAllPorts GetAllPorts Restconf API call

1.5.2.3 Search NSP Inventory#

Execute the Filter ethernet ports Postman example found in 24.11 Network Functions > Network Infrastructure Management > Network Inventory > Get vs Find Restconf API samples> POST Filter ethernet ports. Because this is using the nsp-inventory:find RPC using RESTCONF POST, we can pass JSON in the Body tab to filter for specific ports. Inspect the Body tab and adjust the xpath filter.

{
    "input" : {
        "xpath-filter": "/nsp-equipment:network/network-element[ne-name='g2-pe1']/hardware-component/port[contains('name', '1/1/c10')]",
        "include-meta": false
    }
}

Note

To keep things easy, we are filtering by ne-name and not by ne-id. In the example above the filter matches for port 1/1/c10 on PE1 for Group 2. Adjust the ne-name to match your group.

Inspect the results - note the equipment-extension-port:extension stanza in the results body doesn't have a deployments key. We'll come back to this later.

1.5.3 Create NSP ICM Templates#

We'll now set up some configuration templates we'll later reference in our Netbox script. Head over to the NSP UI and browse to Device Management > Configuration Intent Types.

1.5.3.1 Check ICM Intent Types#

Ensure our required intent-types have been imported into Device Management, the three types listed below should be in the table with a Success status.

  • port-connector_gsros_<version> - For basic configuration on the physical port (Admin state etc.)
  • icm-equipment-port-ethernet - For Ethernet configuration on the physical port (MTU, LLDP etc.)
  • icm-router-network-interface - For IP Interfaces added to the "Base" router (IPv4, IPv6 addresses etc.)

NSP Intent Types

1.5.3.2 Create ICM Templates#

We'll now create ICM Templates, these are the blueprints we'll use to create Intent deployments in our Netbox script.

  • Using the drop-down next to Device Management browse to Configuration Templates.
  • Create a template called Group 10 - NSP Activity 57 - Port Connector using intent-type port-connector_gsros_<version>. Click Release.
  • Repeat for the other two intent-types: icm-equipment-port-ethernet and icm-router-network-interface using similar names swapping the "Port Connector" part.

NSP Intent Template

1.5.3.3 Create ICM Configuration Deployments#

Create 3 ICM Configuration Deployments using your templates. Choose an unused port and do some basic port configuration:

Template Target Config
Connector Port NE: PE1
Port: 1/1/c10
Breakout: c1-100g
Ethernet Port NE: PE1
Port: 1/1/c10/1
keep defaults
Network Interface Interface Name: port_1/1/c10/1 Port Binding: port
Port 1/1/c10/1
IPv4 Primary Address: 1.6.20.25
IPv4 Primary Prefix Length: 31

Note

We'll use this naming scheme in the deployment script. This is the basic process we'll use via the API for the Netbox script.

NSP ICM Deployments

  • Re-execute the Filter Ethernet ports postman call from the previous step - can you see the deployments key? See the details of the deployment listed. This is how you should detect if an existing ICM Deployment already exists for a port.

1.5.4 Create Netbox Event Rule#

1.5.4.1 View the Dump Data Script#

Back in the Netbox UI, browse to Customization > Scripts and click on the Dump Data script. At the top there are tabs to show the script source and to show the Jobs - these are the instances of the script being executed. You can click the ID number of each job execution to view the script output.

NSP Dump Script

1.5.4.2 Create Dump Data Event Rule#

Netbox can create a rule that responds to changes for particular objects and trigger web hooks or script executions. We're going to create a testing rule that runs an existing script called DumpData - all this script does is print out the data parameter that's passed to the run() method of the script when the Event Rule is triggered.

  • Create an Event Rule under Operations > Event Rules with Action Type of Script set to the existing DumpData script.
  • Note the Action data field where you can provide arbitrary JSON that gets included with the data parameter, we'll use this later.
  • You can test this rule by making a change to a device's interface and inspecting the job log: Operations > Jobs > <id>. This output is a useful reference when creating your custom script.

Netbox Event Rule

1.5.5 Write Netbox Script#

Our goal is to have a python script that gets uploaded to Netbox and run whenever an interface object in Netbox is created or changed. Your challenge is to flesh out the skeleton script with all the required functionality to query NSP inventory, determine what action is needed and make calls to the NSP API to create ICM Deployments. You'll want to use Postman as your API reference to find the endpoints you need and what needs to be passed in them.

Netbox scripts should be written in your own development environment or IDE - VSCode is a great starting point.

1.5.5.1 Netbox Script Skeleton#

We've provided this skeleton script as a starting point - it provides the basic structure and allows you to run the script interactively (on the terminal) or after being uploaded to Netbox.

For running on the terminal, please ensure you've installed the requests pip package.

pip install requests

Tip

This skeleton script has some custom magic that means it can (mostly) be run outside the Netbox execution environment. This means participants can run the script on their local machine or on the jump host to test the NSP calls. Any calls that use the Netbox models will fail, comment those out and set static values during testing.

skeleton.py
# Copyright 2025 Nokia
# Licensed under the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause

import sys, json
import requests
from requests.auth import HTTPBasicAuth
import urllib3

urllib3.disable_warnings()

# Stub class to be used during interactive script execution
class ScriptStub(object):
    def log(self, message="", obj=None):
        if message:
            print(message)

        if obj:
            print(obj)

    def log_error(self, message="", obj=None):
        self.log(message=message, obj=obj)

    def log_info(self, message="", obj=None):
        self.log(message=message, obj=obj)

# Swap the classes
try:
    # We're inside the Netbox Environment
    from extras.scripts import Script as NetboxScriptCls
    from ipam.models import IPAddress
    from dcim.models import Interface

    ScriptCls = NetboxScriptCls
except ImportError:
    # We're not inside the Netbox Environment
    ScriptCls = ScriptStub

class NetboxNSPICM(ScriptCls):
    class Meta():
        name = "Netbox NSP ICM"
        description = "Script to sync Netbox interface configuration with NSP ICM Intents"

    def get_port_inventory(self, host, token, port):
        pass

    def run(self, data, commit=True):
        # I've done the authentication bit for you, it's boring anyway :)
        r = requests.post(f"https://{data.get('nsp_host')}/rest-gateway/rest/api/v1/auth/token",
                        data={"grant_type": "client_credentials"},
                        auth=HTTPBasicAuth(data.get("nsp_username"), data.get("nsp_password")),
                        verify=False
                        )

        if r.status_code > 200:
            self.log_error(message=f"Unable to authenticate with error: {r.json}")

        token = r.json()["access_token"]

        self.log_info(message=f"Authenticated with NSP! Status Code:{r.status_code}")

        # Hacker: you have the data, you have NSP, you have all you need.
        # - Find the interface in NSP inventory
        # - determine if there is an existing intent
        # - - if yes, update it and deploy
        # - - if not, create it and deploy

        pass

if __name__ == '__main__':
    # run the script interactively.
    if len(sys.argv) < 2:
        exit('Not enough arguments for interactive call - please pass in JSON data')

    script = NetboxNSPICM()
    p_data = json.load(open(sys.argv[1]))
    script.run(p_data)
sample_data.json
{
    "_occupied": true,
    "bridge": null,
    "cable": {
        "description": "",
        "display": "#18",
        "id": 18,
        "label": "",
        "url": "/api/dcim/cables/18/"
    },
    "cable_end": "A",
    "connected_endpoints": [
        {
            "_occupied": true,
            "cable": {
                "description": "",
                "display": "#18",
                "id": 18,
                "label": "",
                "url": "/api/dcim/cables/18/"
            },
            "description": "p1",
            "device": {
                "description": "",
                "display": "pe1",
                "id": 9,
                "name": "pe1",
                "url": "/api/dcim/devices/9/"
            },
            "display": "1/1/c8/1",
            "id": 119,
            "name": "1/1/c8/1",
            "url": "/api/dcim/interfaces/119/"
        }
    ],
    "connected_endpoints_reachable": true,
    "connected_endpoints_type": "dcim.interface",
    "count_fhrp_groups": 0,
    "count_ipaddresses": 2,
    "created": "2025-04-10T22:04:05.241912Z",
    "custom_fields": {},
    "description": "pe1",
    "device": {
        "description": "",
        "display": "p1",
        "id": 7,
        "name": "p1",
        "url": "/api/dcim/devices/7/"
    },
    "display": "1/1/c8/1",
    "display_url": "/dcim/interfaces/89/",
    "duplex": null,
    "enabled": true,
    "id": 89,
    "l2vpn_termination": null,
    "label": "",
    "lag": null,
    "last_updated": "2025-04-16T04:19:48.072746Z",
    "link_peers": [
        {
            "_occupied": true,
            "cable": {
                "description": "",
                "display": "#18",
                "id": 18,
                "label": "",
                "url": "/api/dcim/cables/18/"
            },
            "description": "p1",
            "device": {
                "description": "",
                "display": "pe1",
                "id": 9,
                "name": "pe1",
                "url": "/api/dcim/devices/9/"
            },
            "display": "1/1/c8/1",
            "id": 119,
            "name": "1/1/c8/1",
            "url": "/api/dcim/interfaces/119/"
        }
    ],
    "link_peers_type": "dcim.interface",
    "mac_address": null,
    "mac_addresses": [],
    "mark_connected": false,
    "mgmt_only": false,
    "mode": null,
    "module": null,
    "mtu": 9100,
    "name": "1/1/c8/1",
    "nsp_host": "SAMPLE_HOST",
    "nsp_password": "SAMPLE_PASSWORD",
    "nsp_username": "SAMPLE_USERNAME",
    "parent": null,
    "poe_mode": null,
    "poe_type": null,
    "primary_mac_address": null,
    "qinq_svlan": null,
    "rf_channel": null,
    "rf_channel_frequency": null,
    "rf_channel_width": null,
    "rf_role": null,
    "speed": null,
    "tagged_vlans": [],
    "tags": [],
    "tx_power": null,
    "type": {
        "label": "QSFP28 (100GE)",
        "value": "100gbase-x-qsfp28"
    },
    "untagged_vlan": null,
    "url": "/api/dcim/interfaces/89/",
    "vdcs": [],
    "vlan_translation_policy": null,
    "vrf": null,
    "wireless_lans": [],
    "wireless_link": null,
    "wwn": null
}
  • Copy the above sample_data.json and skeleton.py contents into their own files in your development environment. Edit the sample_data.json file and update the nsp_host, nsp_user and nsp_password keys to match your lab access details.
  • Run the skeleton script, passing in the sample_data.json file. You should get a message indicating successful authentication. If not, diagnose why.
> python3 ./skeleton.py sample_data.json
> Authenticated with NSP! Status Code:200

1.5.5.2 Your Turn#

There are some hints below about how you could structure your script, try your best without referring to this if you can.

Tip

1.5.5.3 Script format#

  • The netbox data parameter doesn't indicate the action type (create, update, delete) so some logic is required to determine the required action.
  • Write a method that can interface with the NSP Inventory API (Filter ethernet ports) explored earlier and search for Port/Ethernet/Interface deployments.
  • Write a method that can create/update/delete an NSP ICM Deployment given service data parameters and the ICM template name. You'll need to explore the NSP ICM Postman collection to find the best API call to make.
  • You'll need to create separate Port, Ethernet and Interface Deployments when the script runs.
  • Assemble the final logic in the run() method, pulling the required information from Netbox object queries (interface details like state, MTU, IP address etc.) and pass it to the ICM deployment target_data param.

You can test your script by running it interactively and pass in the sample_data.json file. As mentioned above, netbox object queries won't work - you'll need to stub those out for static values during testing. Example output from a script execution is shown below:

> python3 ./skeleton.py sample_data.json
> Authenticated with NSP! Status Code:200
> Querying NSP for Port Deployments...
> 1 Port deployment found
> Netbox interface exists, we need to update the NSP ICM Deployment
> Updating NSP ICM Deployment...
> SUCCESS:204 - updated NSP ICM Deployment for Port 1/1/c10.

1.5.5.4 Useful Netbox script snippets#

The Netbox Shell Documentation is a good reference for how to query and filter models in Custom scripts. You can also access the API docs directly in Netbox (a good way to see keys on each model) at <netbox_url>/api/.

  • Lookup a device's system address using the Netbox API.

    Get System Address
    i = [i for i in 
            list(IPAddress.objects.filter(
                interface__device__name="pe1", interface__name="system")) 
            if ':' not in str(i.address) # only ipv4 address
        ][0]
    

  • Retrieve a Netbox interface from the id parameter in the data param:

    Get Interface by ID
    nb_interface = list(Interface.objects.filter(id=data.get('id')))
    

  • Get the IP addresses (IPv4 and IPv6) for a given Interface:

    Get IP addresses for an interface by Interface ID
    ip_addresses = list(IPAddress.objects.filter(interface_id=data.get('id')))
    

1.5.6 Deploy to Netbox#

Once you have a basic script that can create/update/delete NSP ICM intent deployments based on the sample data, we can setup the Event Rule in Netbox.

1.5.6.1 Upload Script#

First we need to upload your script to Netbox: * Browse to Customization > Scripts > Add and upload your script source. Now to hook the script up to an event rule. You can either reuse the Event Rule we created earlier, or create a second one which might be better for debugging - when an interface is changed, you'll have a Netbox Job output that the script was executed with from the other Event Rule.

1.5.6.2 Create Event Rule#

  • Browse to Operations > Event Rules in Netbox
  • Create an event rule that will trigger your script.
  • Note the Action Data section - provide the following JSON snippet customized with your NSP's login details and host IP.
{"nsp_host":"nsp.srexperts.net","nsp_password":"<NSP_PASSWORD>","nsp_username":"<NSP_USERNAME>>"}

Final Event Rule

1.5.6.3 Test#

Now you can test! Choose a device interface, make a change and test your script. Some example changes:

  • Change the Netbox interface state to "disabled" to ensure a Port Connector ICM deployment disables the interface.
  • Change the MTU of a Netbox interface to ensure the Ethernet ICM deployment changes the ethernet MTU.
  • Add an IP address to an existing interface and ensure a Network Interface ICM is created.
  • Delete an interface that already has ICM Deployments, ensure that all three Deployments are removed from NSP.

Note

To access the Netbox interface navigate to Devices > DEVICE COMPONENTS > Interfaces.

1.5.7 Solution#

Here is an example solution that can create and update NSP ICM Intents:

Example Solution
# Copyright 2025 Nokia
# Licensed under the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause

import sys, json
from ipaddress import ip_address

import requests
from requests.auth import HTTPBasicAuth
import urllib3

urllib3.disable_warnings()


class ScriptStub(object):
    """Stub class to be used during interactive script execution.

    This class provides basic logging functionality when running the script outside
    the Netbox environment. It mimics the logging interface of the Netbox Script class.
    """
    def log(self, message="", obj=None):
        """Log a message and optionally an object.

        Args:
            message (str, optional): Message to log. Defaults to "".
            obj (any, optional): Object to log. Defaults to None.
        """
        if message:
            print(message)

        if obj:
            print(obj)

    def log_error(self, message="", obj=None):
        """Log an error message and optionally an object.

        Args:
            message (str, optional): Error message to log. Defaults to "".
            obj (any, optional): Object to log. Defaults to None.
        """
        self.log(message=message, obj=obj)

    def log_info(self, message="", obj=None):
        """Log an info message and optionally an object.

        Args:
            message (str, optional): Info message to log. Defaults to "".
            obj (any, optional): Object to log. Defaults to None.
        """
        self.log(message=message, obj=obj)


# Swap the classes if we're not inside the Netbox Script environment.
try:
    # We're inside the Netbox Environment
    from extras.scripts import Script as NetboxScriptCls
    from ipam.models import IPAddress
    from dcim.models import Interface

    ScriptCls = NetboxScriptCls
except ImportError:
    ScriptCls = ScriptStub


class NetboxNSPICM(ScriptCls):
    """Netbox script to sync interface configuration with NSP ICM Intents.

    This script handles the synchronization of Netbox interface configurations with
    Nokia Network Services Platform (NSP) Intent Configuration Management (ICM).
    It manages port connector and ethernet intents based on Netbox interface changes.
    """

    class Meta:
        """Metadata for the Netbox script.

        This class defines the script's name and description as it appears in the Netbox UI.
        """
        name = "Netbox NSP ICM"
        description = "Script to sync Netbox interface configuration with NSP ICM Intents"

    def get_device_system_ip(self, device_name):
        """Get the system IP address for a device.

        Args:
            device_name (str): Name of the device to get system IP for.

        Returns:
            str: The system IP address (ipv6) without subnet mask.
        """
        i = [i for i in list(IPAddress.objects.filter(interface__device__name=device_name, interface__name="system")) if '.' not in str(i.address)]

        return str(i[0]).split("/")[0] #only return IP, not mask.


    def execute_port_deployment(self, data, token, op_name, template_name, **kwargs):
        """Execute a port deployment operation in NSP ICM.

        This method handles creating, updating, or deleting port deployments in NSP ICM.
        The payload format varies based on the operation type.

        Args:
            data (dict): Configuration data containing NSP host and credentials.
            token (str): Authentication token for NSP API.
            op_name (str): Operation name ('create-deployment', 'update-deployment', or 'delete-deployment').
            template_name (str): Name of the template to use for deployment.
            **kwargs: Additional arguments:
                - target_data (str): JSON string containing deployment data (for create/update).
                - target (dict): Target configuration containing 'target' and 'target-identifier-value' (for create/update).
                - target_path (str): Path to the deployment to delete (for delete).

        Returns:
            dict: Response from NSP API if successful, None if failed.
        """
        payload = {"input": {"deployments": [{}]}}
        if op_name == "create-deployments" or op_name == "update-deployments":
            payload['input']['deployments'][0]['deployment-action'] = "deploy"
            payload['input']['deployments'][0]['template-name'] = template_name
            payload['input']['deployments'][0]['target-data'] = kwargs.get("target_data")
            payload['input']['deployments'][0]['targets'] = [kwargs.get("target")]

        if op_name == "delete-deployments":
            # NOOP because there's an issue in deleting intents
            return
            # payload['input']['delete-option'] = 'network-and-nsp'
            # payload['input']['deployments'][0] = {
            #     "deployment": f"/nsp-icm:icm/deployments/deployment[template-name='{template_name}'][target='{kwargs.get('target_path')}'][target-identifier-value='{kwargs.get('target_data')}']"
            # }

        response = requests.post(
            f"https://{data.get('nsp_host')}/restconf/operations/nsp-icm:{op_name}",
            headers={"Authorization": f"Bearer {token}"},
            json=payload,
            verify=False
        )

        if response.status_code > 200:
            self.log_error(message=f"Error executing deployment [{op_name}][{template_name}] deployment: {response.json()}")
            return None
        else:
            self.log_info(message=f"Success executing deployment [{op_name}][{template_name}]")
            return response.json()


    def get_port_inventory(self, host, token, ne_id, port):
        """Get port inventory information from NSP.

        Args:
            host (str): NSP host address.
            token (str): Authentication token for NSP API.
            port (str): Port name to get inventory for.

        Returns:
            list: List of deployment data for the port, empty list if not found or error.
        """
        find_query = {
            "input": {
                "xpath-filter": f"/nsp-equipment:network/network-element[ne-id='{ne_id}']/hardware-component/port/equipment-extension-port:extension/deployment[target-identifier-value='{port}']"
            }
        }

        find_r = requests.post(f"https://{host}/restconf/operations/nsp-inventory:find",
                               headers={"Authorization": f"Bearer {token}"},
                               json=find_query,
                               verify=False
                               )
        if find_r.status_code > 200:
            return []

        if find_r.json()["nsp-inventory:output"]["total-count"] == 0:
            return []

        return find_r.json()["nsp-inventory:output"]["data"]

    def get_ip_interface_inventory(self, host, token, ne_id, id):
        """Get IP interface inventory information from NSP.

        Args:
            host (str): NSP host address.
            token (str): Authentication token for NSP API.
            ne_id (str): Network element ID.
            id (str): Interface id.
        """
        # the format we'll use for the interface name is `port126` ~ `port<netbox_id_number>`
        find_query = {
            "input": {
                "xpath-filter": f"/nsp-equipment:network/network-element[ne-id='{ne_id}']/equipment-extension-ne:extension/deployment[target-identifier-value='port{id}']"
            }
        }

        find_r = requests.post(f"https://{host}/restconf/operations/nsp-inventory:find",
                               headers={"Authorization": f"Bearer {token}"},
                               json=find_query,
                               verify=False
                               )
        if find_r.status_code > 200:
            return []

        if find_r.json()["nsp-inventory:output"]["total-count"] == 0:
            return []

        return find_r.json()["nsp-inventory:output"]["data"]

    def determine_intent_action(self, nb_interface_exists, intent_exists):
        """Determine the action to take based on Netbox interface and intent existence.

        Args:
            nb_interface_exists (bool): Whether the interface exists in Netbox.
            intent_exists (bool): Whether the intent exists in NSP.

        Returns:
            str: One of 'create-deployments', 'update-deployments', or 'delete-deployments'.
                 Returns None for unexpected states.
        """
        if not intent_exists and nb_interface_exists:
            return "create-deployments"
        elif intent_exists and nb_interface_exists:
            return "update-deployments"
        elif intent_exists and not nb_interface_exists:
            return "delete-deployments"
        else:
            # This case shouldn't happen in normal operation
            self.log_error(message="No action required - no intent and no netbox interface exists.")
            return "none" # shouldn't trigger a deployment change

    def run(self, data, commit=True):
        """Main execution method for the script.

        This method handles the synchronization of Netbox interface configurations with NSP ICM.
        It manages port connector, ethernet, and network interface intents based on the state of the Netbox interface.

        Args:
            data (dict): Configuration data containing interface details and NSP credentials.
            commit (bool, optional): Whether to commit changes. Defaults to True.

        Returns:
            None
        """
        r = requests.post(f"https://{data.get('nsp_host')}/rest-gateway/rest/api/v1/auth/token",
                          data={"grant_type": "client_credentials"},
                          auth=HTTPBasicAuth(data.get("nsp_username"), data.get("nsp_password")),
                          verify=False
                          )

        if r.status_code > 200:
            self.log_error(message=f"Unable to authenticate with error: {r.json}")

        token = r.json()["access_token"]

        self.log_info(message=f"Authenticated with NSP! Status Code:{r.status_code}")

        # Get device system ipv6 which is the NE-ID.
        device_system_ip = self.get_device_system_ip(data.get('device').get('name'))

        # assume we're only dealing with 1 port connector per port
        port_name_ether = data.get("name")  # e.g 1/1/c1/1
        port_name_connector = "/".join(port_name_ether.split("/")[:3])  # e.g. 1/1/c1
        port_id = data.get('id')

        # Check existence in both systems
        self.log_info(message="[NSP] Getting connector inventory")
        connector_inventory = self.get_port_inventory(data.get('nsp_host'), token, device_system_ip, port_name_connector)
        self.log_info(message=f"[NSP] Found {len(connector_inventory)} Ports matching target.")

        self.log_info(message="[NSP] Getting ethernet inventory")
        ethernet_inventory = self.get_port_inventory(data.get('nsp_host'), token, device_system_ip, port_name_ether)
        self.log_info(message=f"[NSP] Found {len(ethernet_inventory)} Ethernet interfaces matching target.")

        self.log_info(message="[NSP] Getting network interface inventory")
        network_inventory = self.get_ip_interface_inventory(data.get('nsp_host'), token, device_system_ip, port_id)
        self.log_info(message=f"[NSP] Found {len(network_inventory)} IP Interfaces matching target.")

        # List with the interface in question
        # e.g. nb_interface_exists = [<Interface:id=89>]
        nb_interface_exists = list(Interface.objects.filter(id=data.get('id')))

        # Get IP addresses from Netbox
        # e.g. ip_addresses = ["192.0.11.2/31", "2001:db8:11::2/127"]
        ip_addresses = None
        if nb_interface_exists:
            ip_addresses = list(IPAddress.objects.filter(interface__id=data.get('id')))
            ip_addresses = [str(ip.address) for ip in ip_addresses]

        # Determine actions for each intent
        connector_action = self.determine_intent_action(bool(nb_interface_exists), bool(connector_inventory))
        ethernet_action = self.determine_intent_action(bool(nb_interface_exists), bool(ethernet_inventory))
        network_action = self.determine_intent_action(bool(nb_interface_exists), bool(network_inventory))

        # deployment data
        port_mode = "network"  # TODO: if len(connected_endpoints) > 0, network, else access
        port_state = "enable" if data.get("enabled") else "disable"
        port_mtu = data.get("mtu")

        # Handle connector intent
        if connector_action:
            if connector_action == "delete-deployments":
                result_connector = self.execute_port_deployment(
                    data, token, connector_action, "NSP-Activity-57-Port-Connector",
                    target_path=connector_inventory[0].get("target"),
                    target_data=port_name_connector
                )
                self.log_info(message=f"[NSP] Connector Deployment Delete: {result_connector}")
            else:
                result_connector = self.execute_port_deployment(
                    data, token, connector_action, "NSP-Activity-57-Port-Connector",
                    target_data=f"{{\"port\":{{\"admin-state\":\"{port_state}\",\"connector\":{{\"breakout\":\"c1-100g\"}}}}}}",
                    target={
                        "target": f"/nsp-equipment:network/network-element[ne-id='{device_system_ip}']/hardware-component/port[component-id='shelf=1/cardSlot=1/card=1/mdaSlot=1/mda=1/port={port_name_connector}']",
                        "target-identifier-value": port_name_connector
                    }
                )
                self.log_info(message=f"[NSP] Connector Deployment: {result_connector}")

        # Handle ethernet intent
        if ethernet_action:
            if ethernet_action == "delete-deployments":
                result_eth = self.execute_port_deployment(
                    data, token, ethernet_action, "NSP-Activity-57-Port-Ethernet",
                    target_path=ethernet_inventory[0].get("target"),
                    target_data=port_name_ether
                )
                self.log_info(message=f"[NSP] Eth Deployment Delete: {result_eth}")
            else:
                result_eth = self.execute_port_deployment(
                    data, token, ethernet_action, "NSP-Activity-57-Port-Ethernet",
                    target_data=f"{{\"port\":{{\"admin-state\":\"{port_state}\",\"ethernet\":{{\"mode\":\"{port_mode}\",\"mtu\":\"{port_mtu}\",\"encap-type\":\"null\"}}}}}}",
                    target={
                        'target': f"/nsp-equipment:network/network-element[ne-id='{device_system_ip}']/hardware-component/port[component-id='shelf=1/cardSlot=1/card=1/mdaSlot=1/mda=1/port={port_name_connector}/breakout-port={port_name_ether}']",
                        "target-identifier-value": port_name_ether
                    }
                )
                self.log_info(message=f"[NSP] Ethernet Deployment: {result_eth}")

        # Handle network interface intent
        if network_action:
            if network_action == "delete-deployments":
                result_network = self.execute_port_deployment(
                    data, token, network_action, "NSP-Activity-57-Network-Interface",
                    target_path=network_inventory[0].get("target"),
                    target_data=f"port{port_id}"
                )
                self.log_info(message=f"[NSP] Interface Deployment Delete: {result_network}")
            else:
                # Only create/update network interface if we have IP addresses
                if ip_addresses:
                    ipv4 = [i for i in ip_addresses if "." in i][0].split("/") # split mask and IP
                    ipv6 = [i for i in ip_addresses if ":" in i][0].split("/")
                    result_network = self.execute_port_deployment(
                        data, token, network_action, "NSP-Activity-57-Network-Interface",
                        target_data=f"{{\"interface\":{{\"admin-state\":\"{port_state}\",\"port-binding\":\"port\",\"port\":\"{port_name_ether}\",\"ipv4\":{{\"primary\":{{\"address\":\"{ipv4[0]}\",\"prefix-length\":{ipv4[1]}}}}},\"ipv6\":{{\"address\":[{{\"ipv6-address\":\"{ipv6[0]}\",\"prefix-length\":{ipv6[1]}}}]}}}}}}",
                        target={
                            'target': f"/nsp-equipment:network/network-element[ne-id='{device_system_ip}']",
                            "target-identifier-value": f"port{port_id}"
                        }
                    )
                    self.log_info(message=f"[NSP] Interface Deployment: {result_network}")

        return None


if __name__ == '__main__':
    # run the script interactively.
    if len(sys.argv) < 2:
        exit('Not enough arguments for interactive call - please pass in JSON data')

    script = NetboxNSPICM()
    p_data = json.load(open(sys.argv[1]))
    try:
        script.run(p_data)
    except NameError as e:
        if "IPAddress" in str(e) or "Interface" in str(e) or "Device" in str(e):
            print("### Note ### Netbox ORM calls such as Interface.objects.filter() can't be run interactively, stub them for "
                  "testing instead.")
            raise(e)

Do you feel you have achieved something?
Was the difficulty level graded appropriately?
How do you rate this activity?