Skip to content

Instantly share code, notes, and snippets.

@pclose
Last active September 17, 2025 17:41
Show Gist options
  • Select an option

  • Save pclose/ad231e700e167c5c9ed56b0aa8b9673c to your computer and use it in GitHub Desktop.

Select an option

Save pclose/ad231e700e167c5c9ed56b0aa8b9673c to your computer and use it in GitHub Desktop.
Wrapper for SWIS API -pete 2020-07-06
#!/usr/bin/python3
'''
usage: swsh.py [-h] [--login LOGIN] [--password PASSWORD] [--server SERVER]
[--verify VERIFY] [--output OUTPUT] [--input INPUT]
[--customprop [CUSTOMPROP [CUSTOMPROP ...]]]
[--pollers [POLLERS [POLLERS ...]]]
[--node-settings [NODE_SETTINGS [NODE_SETTINGS ...]]] [--debug]
[--log [LOG]] [--config [CONFIG]] [--generate-config]
[--col-order COL_ORDER]
{query,add,remove} [parameters [parameters ...]]
Wrapper for SWIS API -pete 2020-07-06
positional arguments:
{query,add,remove}
parameters Values to use SWQL statement or "key1 value1, ... ,
keyN valueN" node property pairs
optional arguments:
-h, --help show this help message and exit
--login LOGIN, -l LOGIN
username
--password PASSWORD, -p PASSWORD
password
--server SERVER, -s SERVER
Solarwinds API endpoint. hostname[:port]
--verify VERIFY Filepath to verify HTTPS connection with
--output OUTPUT, -o OUTPUT
Output file, defaults to stdout
--input INPUT, -i INPUT
Input file, defaults to stdin
--customprop [CUSTOMPROP [CUSTOMPROP ...]], -c [CUSTOMPROP [CUSTOMPROP ...]]
Custom property key value pairs to be set on deployed
node
--pollers [POLLERS [POLLERS ...]], -pl [POLLERS [POLLERS ...]]
Additional pollers to enable on node
--node-settings [NODE_SETTINGS [NODE_SETTINGS ...]]
Additional node setting key value pairs to add to the
node
--debug Increase logging to stdout
--log [LOG] Write results to a log file. Defaults to ./swsh.py.log
--config [CONFIG] Without setting this the program will attempt to read
from swsh.py.conf
--generate-config Write config file. By default this will write to
swsh.py.conf unless the --config option is present
--col-order COL_ORDER
Order of SWQL columns to put in CSV. One argument with
comma separated strings
Example usages:
1) Deploy a node
`python swsh.py add ipaddress 10.10.10.1 caption example.net`
2) Remove a node
`python swsh.py remove ipaddress 10.10.10.1`
3) Save SWQL output to CSV
`python swsh.py --output Mynodes.csv query "select caption, ipaddress from orion.nodes where ipaddress = @arg1" arg1 10.10.10.1`
4) Generate a config file with server connection settings
`python swsh.py --generate-config --server solarwinds.net:443 --login user`
Without passing --config the default location is automatically used on subsequent requests
5) Generate a config file with some default settings for adding nodes
`python swsh.py--generate-config --config add_settings.conf \
--server solarwinds.net:443 \
--login user --password pass \
add \
EntityType Orion.Nodes \
MachineType ICMP \
PollInterval 120 \
StatCollection 10 \
RediscoveryInterval 30 \
--customprop Environment Production \
--pollers N.Status.ICMP.Native N.ResponseTime.ICMP.Native
5a) So subsequent calls using this config file will pass the default parameters in addition
eg. `python swsh.py --config add_settings.conf add caption example2.net ipaddress 10.10.10.2`
'''
import orionsdk
from requests.exceptions import HTTPError
import csv, sys, re, logging, os, json
# Logging constants
LOG_FORMAT = "%(asctime)s-%(levelname)s: %(message)s"
LOG_DATE_FMT= "%Y-%m-%d %H:%M:%S"
LOG_LEVEL=logging.INFO
LOG_PATH=os.path.basename(sys.argv[0])+".log"
# Returns a dict for ["key", "value"] list
def prop_parms(d):
if not d: return
if len(d)%2!=0: raise ValueError("Parameters must be a space separated list of \"key\" \"values\"")
i,j,r = 0,len(d),{}
while i<j:
r[d[i]]=d[i+1]
i=i+2
return r
# Return a poller dict
def proc_pollerlist(pol,nodeid):
r=[]
for e in pol:
r.append({
'pollertype': e,
'netobject': 'N:' + nodeid,
'netobjecttype': 'N',
'netobjectid': nodeid,
'enabled': True
})
return r
# Node settings definitions
def proc_settings(settings,nodeid=None):
r=[]
for e in settings:
r.append({
"settingname":e,
"settingvalue":settings[e]})
if nodeid: r[-1]["nodeid"]=nodeid
return r
# Read from the config file generated from generate_config()
# args passed in will override the config file
def parse_config(filen, args):
if not os.access(filen, os.R_OK): return args
from configparser import ConfigParser
config = ConfigParser()
config.read(filen)
d = vars(args)
operation = d["operation"]
for e in config["main"]:
if args.__dict__[e] is not None: continue
args.__dict__[e]=config["main"][e]
for s in config.sections():
if s == "main": continue
op,sec = s.split(".")
if operation != op: continue
if sec=="pollers": # Special parsing for pollers
args.__dict__[sec]=config[s]["values"].split(",")
continue
subj = args.__dict__[sec]
args.__dict__[sec]={}
for e in config[s]:
args.__dict__[sec][e]=config[s][e]
if subj and len(subj)>0:
for k,v in prop_parms(subj).items():
args.__dict__[sec][k]=v
return args
# Write a config file
def generate_config(filen, args):
from configparser import ConfigParser
config = ConfigParser()
config.optionxform=lambda x:x # This stops the capitalization of the parms
argd = vars(args) # Turns the Namespace into a dict essentially
operation=argd["operation"]
config["main"]={}
for e in argd:
if argd[e] and isinstance(argd[e],list): # Create a section in the config file when we hit a list
secname = ".".join([operation,e])
config[secname]={}
if e == "pollers": # Pollers are a list, so they are special
config[secname]["values"]=",".join(argd[e])
continue
l=argd[e]
i,j = 0,len(l)
while i<j: # This is a modified prop_parms()
if i+1>=len(l):
config[secname][l[i]]=""
i=i+2
else:
config[secname][l[i]]=l[i+1]
i=i+2
elif argd[e]: # Passing None in here will cause problems later
if e in ["config", "generate_config", "operation"]: continue # Ignore options that generate this file essentially
config["main"][e]=str(argd[e])
with open(filen, "w") as f: config.write(f)
class swsh(orionsdk.SwisClient):
def __init__(self, hostname, username, password, port="17778", verify=False, log=None, debug=False):
super().__init__(hostname, username, password, verify)
if ":17778" in self.url: self.url=self.url.replace(":17778",":"+port) # Override hardcoded value in orionsdk
self.log = logging.getLogger("swsh_logger")
formatter = logging.Formatter(fmt=LOG_FORMAT, datefmt=LOG_DATE_FMT)
streamhandler = logging.StreamHandler(sys.stdout)
streamhandler.setFormatter(formatter)
self.log.addHandler(streamhandler)
self.log.setLevel(LOG_LEVEL)
if log:
filehandler = logging.FileHandler(log)
filehandler.setFormatter(formatter)
filehandler.setLevel(LOG_LEVEL)
self.log.addHandler(filehandler)
if debug:
self.log.setLevel(logging.DEBUG)
# Override orionsdk method to include logging
def _req(self, method, frag, data=None):
try:
resp = super()._req(method, frag, data)
except HTTPError as err:
self.log.error("{} {} {} {}".format(err.response.status_code, method, frag, repr(data)))
raise err
self.log.debug("request: {} {} {}, {}".format(resp.status_code, method, frag, repr(data)))
self.log.debug("response: {}".format(resp.text))
return resp
# Runs a SWQL statement and saves it in a CSV
def sh_query(self, query, output_fileh, col_order=None):
if len(query)>1:
qparms=prop_parms(query[1:])
qr=self.query(query[0], **qparms)
else:
qr = self.query(query[0])
if len(qr["results"])<1: return
o = csv.writer(output_fileh)
if col_order:
keys=[x.strip() for x in col_order.split(",")]
else:
keys = qr["results"][0].keys()
o.writerow(keys)
for i in qr["results"]:
o.writerow([i.get(x) for x in keys])
# Add a node
def sh_add(self, node_prop, custom_prop=[], pollers=[], node_settings=[]):
'''This will add or update a node. Unfortunately the API will let you create nodes with the same properties over and over, so this has some logic to try and prevent that'''
nprop,cprop,npol,nset = {},{},[],{}
# Override or append properties
if pollers: npol+=pollers
if node_prop:
if isinstance(node_prop,list): t = prop_parms(node_prop)
elif isinstance(node_prop,dict): t = node_prop
for e in t: nprop[e.lower()]=t[e]
if custom_prop:
if isinstance(custom_prop,list): t = prop_parms(custom_prop)
elif isinstance(custom_prop,dict): t = custom_prop
for e in t: cprop[e.lower()]=t[e]
if node_settings:
if isinstance(node_settings,list): t = prop_parms(node_settings)
elif isinstance(node_settings,dict): t = node_settings
for e in t: nset[e.lower()]=t[e]
# Check for existing nodes
ex_nodes = []
if "caption" in nprop:
t = self.search_node_uri(nprop["caption"])
if t: ex_nodes.append(t)
if "ipaddress" in nprop:
t = self.search_node_uri(nprop["ipaddress"])
if t: ex_nodes.append(t)
ex_nodes = list(set(ex_nodes))
# TODO: Other properties to consider here: DisplayName, DNS, NodeName, SystemName
did_we_update=False
# No nodes exist, create one
if len(ex_nodes)<1:
# Create node and save the NodeID value
nodeuri = self.create("Orion.Nodes", **nprop)
# A single node already exists, prepare to update its properties
elif len(ex_nodes)==1:
did_we_update=True
nodeuri = ex_nodes[0]
nodeid = re.search(r'(\d+)$', nodeuri).group(0)
# Filter out or enable pollers that already exist on the node
basepoller_uri=nodeuri.split("Orion.Nodes")[0]+"Orion.Pollers/PollerID={}"
qr = self.query("select pollerid,pollertype,enabled from orion.pollers where netobject = @netobjid", netobjid="N:"+nodeid)
for e in qr["results"]:
if e["pollertype"] in npol: # Consider pollers that are going to be added later
if e["enabled"]:
npol.remove(e["pollertype"]) # Remove from the list if it's enabled
else: # Update it if it is not enabled
npol.remove(e["pollertype"]) # Actually not sure if this is a valid condition without the API footgun
self.update(basepoller_uri.format(e["pollerid"]), enabled=True)
# Same for node settings
baseproperty_uri=nodeuri.split("Orion.Nodes")[0]+"Orion.NodeSettings/NodeSettingID={}"
qr = self.query("select settingvalue,nodeid,nodesettingid,settingname from orion.nodesettings where nodeid = @nodeid", nodeid=nodeid)
for e in qr["results"]:
if e["settingname"] in nset and nset[e["settingname"]]!=e["settingvalue"]:
t={}
t[e["settingname"]]=nset[e["settingname"]]
t=proc_settings(t)[0]
self.update(baseproperty_uri.format(e["nodesettingid"]), **t)
del nset[e["settingname"]]
elif len(ex_nodes)>1:
self.log.info("Multiple nodes already exist with this criteria, giving up {}".format(json.dumps(ex_nodes)))
return
nodeid = re.search(r'(\d+)$', nodeuri).group(0)
# Generate poller properties and add them to node
for e in proc_pollerlist(npol, nodeid):
self.create('Orion.Pollers', **e)
# Add NodeSettings
for e in proc_settings(nset, nodeid):
self.create('Orion.NodeSettings', **e)
# Update custom properties
if cprop!={}: self.update(nodeuri + "/CustomProperties", **cprop)
if did_we_update: self.log.info("Done updating node {} {}".format(nprop["caption"], nodeuri))
else: self.log.info("Done adding node {} {}".format(nprop["caption"], nodeuri))
def search_node_uri(self, node, criteria=None):
if criteria:
nquery="SELECT Uri FROM Orion.Nodes WHERE {} = @arg1".format(criteria)
elif re.match("^\d+(\.\d+){3}", node):
nquery="SELECT IPAddress, Caption, Uri FROM Orion.Nodes WHERE IPAddress = @arg1"
else: nquery="SELECT IPAddress, Caption, Uri FROM Orion.Nodes WHERE Caption = @arg1"
qr = self.query(nquery, arg1=node)
if len(qr["results"])==1:
return qr["results"][0]["Uri"]
elif len(qr["results"])>1:
self.log.info("Multiple nodes found {}, giving up".format(json.dumps(qr["results"])))
raise Exception("Multiple nodes found")
else:
return
def sh_remove(self, node):
nuri=None
if isinstance(node, list): node = node[0]
if isinstance(node, dict):
for e in node:
nuri = self.search_node_uri(node[e], e)
if nuri: break
elif node.startswith("swis://"): nuri=node
else:
nuri = self.search_node_uri(node)
if nuri:
self.delete(nuri)
self.log.info("Done removing node {} {}".format(node, nuri))
else:
self.log.info("No node found matching criteria")
if __name__ == "__main__":
usage_examples = '''
Example usages:
1) Deploy a node
`python %(prog)s add ipaddress 10.10.10.1 caption example.net`
2) Remove a node
`python %(prog)s remove ipaddress 10.10.10.1`
3) Save SWQL output to CSV
`python %(prog)s --output Mynodes.csv query "select caption, ipaddress from orion.nodes where ipaddress = @arg1" arg1 10.10.10.1`
4) Generate a config file with server connection settings
`python %(prog)s --generate-config --server solarwinds.net:443 --login user`
Without passing --config the default location is automatically used on subsequent requests
5) Generate a config file with some default settings for adding nodes
`python %(prog)s --generate-config --config add_settings.conf \\
--server solarwinds.net:443 \\
--login user --password pass \\
add \\
EntityType Orion.Nodes \\
MachineType ICMP \\
PollInterval 120 \\
StatCollection 10 \\
RediscoveryInterval 30 \\
--customprop Environment Production \\
--pollers N.Status.ICMP.Native N.ResponseTime.ICMP.Native
5a) So subsequent calls using this config file will pass the default parameters in addition
eg. `python %(prog)s --config add_settings.conf add caption example2.net ipaddress 10.10.10.2`
'''
import argparse
parser = argparse.ArgumentParser(prog="swsh.py", description='Wrapper for SWIS API -pete 2020-07-06', epilog=usage_examples, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('operation', choices=["query","add","remove"])
parser.add_argument('parameters', nargs="*", help="Values to use SWQL statement or \"key1 value1, ... , keyN valueN\" node property pairs")
parser.add_argument('--login', '-l', default=None, help="username")
parser.add_argument('--password', '-p', default=None, help="password")
parser.add_argument('--server', '-s', default="localhost", help="Solarwinds API endpoint. hostname[:port]")
parser.add_argument('--verify', help="Filepath to verify HTTPS connection with")
parser.add_argument("--output", "-o", help="Output file, defaults to stdout")
parser.add_argument("--input", "-i", help="Input file, defaults to stdin")
parser.add_argument("--customprop", "-c", nargs="*", help="Custom property key value pairs to be set on deployed node")
parser.add_argument("--pollers", "-pl", nargs="*", help="Additional pollers to enable on node")
parser.add_argument("--node-settings", nargs="*", help="Additional node setting key value pairs to add to the node")
parser.add_argument("--debug", action="store_true", help="Increase logging to stdout")
parser.add_argument("--log", nargs="?", const=LOG_PATH, help="Write results to a log file. Defaults to ./{}".format(LOG_PATH))
parser.add_argument("--config", nargs="?", default=os.path.basename(sys.argv[0])+".conf", help="Without setting this the program will attempt to read from {}".format(os.path.basename(sys.argv[0])+".conf"))
parser.add_argument("--generate-config",action="store_true", help="Write config file. By default this will write to {} unless the --config option is present".format(os.path.basename(sys.argv[0])+".conf"))
parser.add_argument("--col-order", help="Order of SWQL columns to put in CSV. One argument with comma separated strings") # This is not ideal, but neither is the API ¯\_(ツ)_/¯
args=parser.parse_args()
if args.generate_config:
generate_config(args.config, args)
sys.exit(0)
args = parse_config(args.config, args)
if args.password == None:
import getpass
args.password = getpass.getpass('Password:')
port = "17778"
server = args.server
if ":" in args.server:
port = args.server.split(":")[1]
server = args.server.split(":")[0]
swsh_client = swsh(server, args.login, args.password, port, args.verify, args.log, args.debug)
input_fileh, output_fileh = None, None
if args.input == "-": input_fileh = sys.stdin
elif args.input: input_fileh = open(args.input, "rb")
if args.output == "-": output_fileh = sys.stdout
elif args.output: output_fileh = open(args.output, "w")
if args.operation=="add":
swsh_client.sh_add(args.parameters, args.customprop, args.pollers, args.node_settings)
elif args.operation=="remove":
swsh_client.sh_remove(args.parameters)
elif args.operation=="query":
if input_fileh:
swql=[" ".join(input_fileh.readlines())]
swql+=args.parameters
elif args.parameters: swql=args.parameters
else:
print("SQL query needs to be provided")
parser.print_help()
sys.exit(1)
if not output_fileh: output_fileh = sys.stdout
swsh_client.sh_query(swql, output_fileh, args.col_order)
else:
parser.print_help()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment