Automating IP Allocation with SolarWinds IPAM and Python
When you are building automated VM provisioning pipelines, one of the first friction points you hit is IP address management. The VM needs a static IP before it boots — it goes directly into the kickstart config (for RHEL) or Autounattend.xml (for Windows). Without IPAM integration, that means someone has to open the SolarWinds console, find an available IP in the right subnet, write it down, mark it as used, and hand it off to whoever is running the provisioning script. That is a manual step in the middle of an automated workflow, and it is a reliable source of errors: duplicate IPs, IPs that got allocated but never marked used, IPs marked used for VMs that were decommissioned months ago.
The goal here is simple: the provisioning script calls IPAM, gets an available IP, marks it used, and moves on. No human in the loop.
SolarWinds IPAM Overview
SolarWinds Orion IPAM tracks the status of every IP address in every subnet you have configured. Each IP has one of four statuses: Available, Used, Reserved, or Transient. The data is queryable through SWIS — the SolarWinds Information Service — which is the REST API underlying everything in Orion. SWIS accepts SQL-like queries against the Orion data model and also exposes invoke-style calls for operations like claiming an IP or changing its status.
The Python SDK for this is orionsdk.
Setting Up orionsdk
pip install orionsdk requests
Connection setup, pulling credentials from environment variables:
import os
import requests
from orionsdk import SwisClient
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
sw_server = os.environ['SW_HOSTNAME']
sw_username = os.environ['SW_USERNAME']
sw_password = os.environ['SW_PASSWORD']
swis = SwisClient(sw_server, sw_username, sw_password, verify=False)
The verify=False is because most internal SolarWinds instances use self-signed certs. Suppressing the urllib3 warning keeps the output clean. In production, you should pin the cert instead of disabling verification entirely — but in practice, most internal tooling runs with verify=False and nobody fixes it until it becomes a compliance issue.
Getting the First Available IP
orionsdk exposes an invoke method for calling SWIS verbs. GetFirstAvailableIP takes a subnet address and a CIDR prefix length and returns the first IP in that subnet with Available status.
def get_first_available_ip(swis, subnet_ip='10.10.0.0', subnet_cidr=24):
"""Claim the first available IP in a subnet via SWIS IPAM API."""
ip = swis.invoke('IPAM.SubnetManagement', 'GetFirstAvailableIP', subnet_ip, subnet_cidr)
return ip
Checking and Setting IP Status
SWIS queries look like SQL but they run against the Orion schema, not a real database. The IPAM.IPNode table has the per-IP records. Status comes back as an integer, so you need to map it yourself.
def get_ip_status(swis, ip):
result = swis.query("""
SELECT i.IPAddress, i.Status
FROM IPAM.IPNode i
WHERE i.IPAddress = @ip
""", ip=ip)
status_map = {1: 'Used', 2: 'Available', 3: 'Transient', 4: 'Reserved'}
raw = result['results'][0]['Status']
return status_map[raw]
def set_ip_used(swis, ip):
swis.invoke('IPAM.SubnetManagement', 'ChangeIPStatus', ip, 'Used')
Getting Subnet Info for an IP
The kickstart or Autounattend config needs more than just the IP — it also needs the netmask. You can pull subnet info with a join against IPAM.Subnet:
def get_subnet_for_ip(swis, ip):
result = swis.query("""
SELECT s.Address, s.AddressMask, s.CIDR
FROM IPAM.Subnet s
INNER JOIN IPAM.IPNode i ON s.SubnetId = i.SubnetId
WHERE i.IPAddress = @ip
""", ip=ip)
r = result['results'][0]
return {'address': r['Address'], 'netmask': r['AddressMask'], 'cidr': r['CIDR']}
The Full Allocation Workflow
Putting it together into a single function that the provisioning script can call:
def allocate_ip(swis, subnet_ip, subnet_cidr):
"""Allocate an IP from IPAM. Returns (ip, netmask, cidr) or raises."""
ip = get_first_available_ip(swis, subnet_ip, subnet_cidr)
status = get_ip_status(swis, ip)
if status != 'Available':
raise RuntimeError(f"Expected Available, got {status} for {ip}")
set_ip_used(swis, ip)
# Confirm
new_status = get_ip_status(swis, ip)
if new_status != 'Used':
raise RuntimeError(f"Status update failed for {ip}: still {new_status}")
subnet = get_subnet_for_ip(swis, ip)
return ip, subnet['netmask'], subnet['cidr']
The status check before and after ChangeIPStatus is defensive. GetFirstAvailableIP should return an Available IP, but verifying it before marking it used catches any surprises. Confirming the status after ChangeIPStatus catches cases where the invoke call returns without error but the update did not actually take.
Integration with VM Provisioning
This plugs into a Packer or vSphere provisioning script as the first step. The script calls allocate_ip.py, captures the output as JSON, and passes IP and netmask as build variables:
# In the provisioning shell script
RESULT=$(python3 allocate_ip.py --subnet 10.10.20.0 --cidr 24)
IP=$(echo $RESULT | jq -r '.ip')
NETMASK=$(echo $RESULT | jq -r '.netmask')
packer build \
-var "vm_ip=$IP" \
-var "vm_netmask=$NETMASK" \
rhel7_build.json
Inside the Packer template, vm_ip and vm_netmask get substituted into the kickstart template for RHEL or into Autounattend.xml for Windows. The VM boots already knowing its own IP address. No manual lookup, no hand-off, no possibility of forgetting to mark the IP as used.
Error Handling and Edge Cases
Subnet full. When GetFirstAvailableIP has nothing to return, it returns null. Your wrapper needs to check for that and raise a meaningful error rather than passing null into downstream steps.
Race condition. This is the bigger problem. If two provisioning jobs run simultaneously against the same subnet, both can call GetFirstAvailableIP and get the same IP back before either has called ChangeIPStatus. You end up with two VMs claiming the same address.
There are a few ways to deal with this:
- Use the
Transientstatus as a reservation mechanism. Mark the IP Transient immediately afterGetFirstAvailableIP, before doing any other work. IfGetFirstAvailableIPrespects Transient status (it should), a concurrent request will skip that IP and get the next one. Then update from Transient to Used once provisioning is confirmed. - Add a mutex or lock in your provisioning coordinator. If you have a central service managing provisioning jobs, serialize the IPAM allocation step there. This is cleaner than relying on SWIS behavior, but it requires having a coordination layer in the first place.
In practice, if your provisioning volume is low enough that two jobs rarely overlap, you may never hit this. But it is worth knowing the gap is there.
Honest Take
SWIS query syntax looks like SQL and mostly behaves like SQL, but it is not SQL. The Orion schema documentation is buried in the SDK docs and supplementary guides, and you will spend time just figuring out the right table and column names. IPAM.IPNode, IPAM.Subnet, and IPAM.SubnetManagement are the relevant ones for this use case, but discovering that takes some digging.
The invoke interface for GetFirstAvailableIP and ChangeIPStatus is not always stable between Orion versions. If you upgrade Orion, test your integration before assuming it still works.
That said, once it is working, automating away the manual IPAM step removes a real bottleneck from the provisioning workflow. The alternative — someone opening the SolarWinds UI every time a VM gets provisioned — does not scale, and it introduces exactly the kind of human error that automated workflows are supposed to eliminate. Worth the setup cost.