diff --git a/README.md b/README.md index d1f1f5a..ae08c91 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ If you need help setting up a custom integration, you can create an [issue](http - [Scale Computing](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/scale-computing/) - [Snipe-IT](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snipe-it/) - [Snow License Manager](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snow-license-manager/) +- [Solarwinds Information Service](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/solarwinds-information-service/) - [Stairwell](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/stairwell/) - [Tailscale](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tailscale/) - [Tanium](https://github.com/runZeroInc/runzero-custom-integrations/blob/main/tanium/) diff --git a/docs/integrations.json b/docs/integrations.json index e9a1824..1ee5640 100644 --- a/docs/integrations.json +++ b/docs/integrations.json @@ -1,6 +1,6 @@ { - "lastUpdated": "2026-01-15T14:44:35.262088Z", - "totalIntegrations": 30, + "lastUpdated": "2026-01-15T15:30:47.444549Z", + "totalIntegrations": 31, "integrationDetails": [ { "name": "Moysle", @@ -176,6 +176,12 @@ "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snipe-it/README.md", "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/snipe-it/snipeit.star" }, + { + "name": "Solarwinds Information Service", + "type": "inbound", + "readme": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/solarwinds-information-service/README.md", + "integration": "https://github.com/runZeroInc/runzero-custom-integrations/blob/main/solarwinds-information-service/custom-integration-swis.star" + }, { "name": "Proxmox", "type": "inbound", diff --git a/solarwinds-information-service/README.md b/solarwinds-information-service/README.md new file mode 100644 index 0000000..1c28797 --- /dev/null +++ b/solarwinds-information-service/README.md @@ -0,0 +1,70 @@ +# Custom Integration: SolarWinds Orion - SolarWinds Information Service (SWIS) + +## Getting Started + +- Clone this repository + +``` +git clone https://github.com/runZeroInc/runzero-custom-integrations.git +``` + +## runZero requirements + +- Superuser access to the [Custom Integrations configuration](https://console.runzero.com/custom-integrations) in runZero + +## SolarWinds SWIS requirements + +**SWIS Instance URL** - The domain or IP of the Snow Atlas web server and the port number e.g. "https://my.solarwinds.instance:17774" (defined within the starlark script as `SWIS_BASE_URL`) + +**username** - account username for Solarwinds access (configured in Credentials section of runZero) + +**password** - account password (configured in Credentials section of runZero) + +## SolarWinds SWIS API Docs + +- [SWIS Cortex.Orion.Node Schema Reference](https://solarwinds.github.io/OrionSDK/schema/Cortex.Orion.Node.html) + +- [Orion SDK SWIS Docs](https://github.com/solarwinds/OrionSDK/wiki/About-SWIS) + +## Steps + +### Solarwinds configuration + +1. Determine the proper Solarwinds URL and port: + - Common Solarwinds ports are 17774 (default since v2024) and 17778 (the default in v2023 and prior). + - Assign the URL to `SWIS_BASE_URL` within the starlark script + - Determine proper username and password credentials for access. These will be configured in the Custom Integration credentials section within the runZero console. + +### runZero configuration + +1. (Make any neccessary changes to the script to align with your environment. + - Modify API calls as needed to filter assets + >- Determine the proper SWQL query needed to return the data set to import to runZero + >- Add this query to the integration script in the 'params' variable within the 'get_assets' function. + - Modify attribute mapping based on the data returned by the SWQL query as needed + >- The integration script outlines some common example attributes that could be brought in from Solarwinds but the attributes that are actually retrieved will be determined by the SWQL query passed in the API call params + >- Modify the asset attributes and custom attributes to match the data provided by the SWQL query following the pattern outlined in the script + >- For a list of "core" attributes that runZero maps, reference the Custom SDK documentation [here](https://runzeroinc.github.io/runzero-sdk-py/autoapi/runzero/types/_data_models_gen/index.html#runzero.types._data_models_gen.ImportAsset). All other attributes provided by Solarwinds should be mapped within 'Custom Attributes' +2. [Create the Credential for the Custom Integration](https://console.runzero.com/credentials) + - Select the type **Custom Integration Script Secrets** + - Both **access_key** and **access_secret** are required + - **access_key** corresponds to the username to access Solarwinds + - **access_secret** corresponds to the password to access Solarwinds +3. [Create the Custom Integration](https://console.runzero.com/custom-integrations/new) + - Add a Name (e.g. solarwinds) and Icon + - Toggle **Enable custom integration script** to input your finalized script + - Click **Validate** to ensure it has valide syntax + - Click **Save** to create the Custom Integration +4. [Create the Custom Integration task](https://console.runzero.com/ingest/custom/) + - Select the Credential and Custom Integration created in steps 2 and 3 + - Update the task schedule to recur at the desired timeframes + - Select the Explorer you'd like the Custom Integration to run from + - Click **Save** to start the task + + +### What's next? + +- You will see the task initilize on the [tasks](https://console.runzero.com/tasks) page like other integration tasks +- The task will update the existing assets with the data pulled from the Custom Integration source +- The task will create new assets for when there are no existing assets that meet merge criteria (hostname, MAC, IP, etc) +- You can search for assets enriched by this custom integration with the runZero search `custom_integration:` \ No newline at end of file diff --git a/solarwinds-information-service/config.json b/solarwinds-information-service/config.json new file mode 100644 index 0000000..301ec59 --- /dev/null +++ b/solarwinds-information-service/config.json @@ -0,0 +1 @@ +{ "name": "Solarwinds Information Service", "type": "inbound" } \ No newline at end of file diff --git a/solarwinds-information-service/custom-integration-swis.star b/solarwinds-information-service/custom-integration-swis.star new file mode 100644 index 0000000..d3014e1 --- /dev/null +++ b/solarwinds-information-service/custom-integration-swis.star @@ -0,0 +1,109 @@ +load('runzero.types', 'ImportAsset', 'NetworkInterface') +load('base64', base64_encode='encode', base64_decode='decode') +load('http', http_get='get', http_post='post', 'url_encode') +load('json', json_encode='encode', json_decode='decode') +load('net', 'ip_address') +load('uuid', 'new_uuid') + +SWIS_BASE_URL = 'https://localhost:17774' +RUNZERO_REDIRECT = 'https://console.runzero.com/' + +def build_assets(assets): + assets_import = [] + for asset in assets: + asset_id = str(asset.get('NodeId', str(new_uuid))) + hostname = asset.get('Fqdn', '') + os = asset.get('OsVersion', '') + vendor = asset.get('Vendor', '') + + # create the network interfaces + interfaces = [] + addresses = asset.get('IpAddress', []) + interface = build_network_interface(ips=[addresses], mac=None) + interfaces.append(interface) + + # Retrieve and map custom attributes + cpu_util = str(asset.get('CpuPercentUtilization', '')) + discovery_profile_id = str(asset.get('DiscoveryProfileId', '')) + mem_util_perc = str(asset.get('PercentMemoryUsed', '')) + mem_util = str(asset.get('MemoryUsed', '')) + pollers = asset.get('Pollers', '') + response_time = str(asset.get('ResponseTime', '')) + snmp_port = str(asset.get('SnmpPort', '')) + snmp_version = str(asset.get('SnmpVersion', '')) + status = asset.get('Status', '') + sys_object_id = asset.get('SysObjectId', '') + uptime = str(asset.get('Uptime', '')) + + custom_attributes = { + 'percentCpuUtilization': cpu_util, + 'discoveryProfileId': discovery_profile_id, + 'percentMemoryUtilization': mem_util_perc, + 'memoryUtilized': mem_util, + 'pollers': pollers, + 'responseTime': response_time, + 'snmp.port': snmp_port, + 'snmp.version': snmp_version, + 'status': status, + 'sysObjectId': sys_object_id, + 'uptime': uptime + } + + # Build assets for import + assets_import.append( + ImportAsset( + id=asset_id, + hostnames=[hostname], + os=os, + manufacturer=vendor, + networkInterfaces=interfaces, + customAttributes=custom_attributes + ) + ) + return assets_import + +def build_network_interface(ips, mac): + ip4s = [] + ip6s = [] + for ip in ips[:99]: + ip_addr = ip_address(ip) + if ip_addr.version == 4: + ip4s.append(ip_addr) + elif ip_addr.version == 6: + ip6s.append(ip_addr) + else: + continue + if not mac: + return NetworkInterface(ipv4Addresses=ip4s, ipv6Addresses=ip6s) + else: + return NetworkInterface(macAddress=mac, ipv4Addresses=ip4s, ipv6Addresses=ip6s) + +def get_assets(creds): + + url = SWIS_BASE_URL + 'SolarWinds/InformationService/v3/Json/Query?' + headers = {'Accept': 'application/json', + 'Authorization': 'Basic ' + creds} + # Populate the SWQL query to return desired assets and attributes in the params query value e.g. + # params = {'query': 'SELECT N.NodeID, N.OsVersion, N.Fqdn, N.Vendor, N.IPAddress, N.CpuPercentUtilization, N.DiscoveryProfileId, N.PercentMemoryUsed, N.MemoryUsed, N.Pollers, N.responseTime, N.snmp.port, N.snmp.version, N.status, N.sysObjectId, N.Uptime FROM Orion.Nodes'} + params = {'query': ''} + response = http_get(url, headers=headers, params=params) + if response.status_code != 200: + print('failed to retrieve assets', 'status code: ' + str(response.status_code)) + data = json_decode(response.body) + assets_all.extend(data) + + return assets_all + +def main(*args, **kwargs): + username = kwargs['access_key'] + password = kwargs['access_secret'] + b64_creds = base64_encode(username + ":" + password) + assets = get_assets(b64_creds) + + # Format asset list for import into runZero + import_assets = build_assets(assets) + if not import_assets: + print('no assets') + return None + + return import_assets