Last active
October 11, 2016 08:30
-
-
Save crazed/7203476 to your computer and use it in GitHub Desktop.
sample using trigger with a custom loader
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import logging | |
import xml.etree.ElementTree as ET | |
import re | |
import types | |
from uuid import uuid4 | |
import cyclone.httpclient | |
import cyclone.jsonrpc | |
import cyclone.xmlrpc | |
# trigger imports | |
import trigger.utils.xmltodict as xmltodict | |
from trigger.cmds import Commando as CommandoBase | |
from trigger.netdevices.loader import BaseLoader | |
from trigger.utils import strip_juniper_namespace | |
from twisted.internet import defer, task, reactor | |
from twisted.python import log | |
# custom client into centralized data store | |
import optopus | |
class MyLoader(BaseLoader): | |
is_usable = True | |
def get_data(self, data_source): | |
client = optopus.Client() | |
data = client.network_nodes | |
devices = [] | |
# map optopus data to what trigger expects | |
mapping = { 'nodeName': 'hostname' } | |
fact_mapping = { | |
'site': 'location', | |
'manufacturer': 'boardmanufacturer', | |
} | |
# expression used to create a deviceType attribute | |
exp = re.compile('ops-([a-z]+)\d+\..*\.shuttercorp\.net') | |
for device_data in data: | |
device_data = device_data['network_node'] | |
netdev_data = {'adminStatus': 'PRODUCTION'} | |
for key, value in mapping.items(): | |
if value in device_data: | |
netdev_data[key] = str(device_data[value]) | |
for key, value in fact_mapping.items(): | |
if value in device_data['facts']: | |
netdev_data[key] = str(device_data['facts'][value]) | |
m = exp.match(device_data['hostname']) | |
if m: | |
if m.group(1) == 'fw': | |
devtype = 'firewall' | |
elif m.group(1) == 'lb': | |
devtype = 'loadbalancer' | |
else: | |
devtype = m.group(1) | |
netdev_data['deviceType'] = devtype.upper() | |
devices.append(netdev_data) | |
return devices | |
def load_data_source(self, data_source, **kwargs): | |
return self.get_data(data_source) | |
# Setup our loader with tigger | |
import trigger | |
trigger.conf.global_settings.NETDEVICES_LOADERS = [ 'test.MyLoader' ] | |
nd = trigger.netdevices.NetDevices() | |
class ReactorlessCommando(CommandoBase): | |
""" | |
A reactor-less Commando subclass. | |
This allows multiple instances to coexist, with the side-effect that you | |
have to manage the reactor start/stop manually. | |
""" | |
def _start(self): | |
"""Initializes ``all_done`` instead of starting the reactor""" | |
log.msg("._start() called") | |
self.all_done = False | |
def _stop(self): | |
"""Sets ``all_done`` to True instead of stopping the reactor""" | |
log.msg("._stop() called") | |
self.all_done = True | |
def run(self): | |
""" | |
We've overloaded the run method to return a Deferred task object. | |
""" | |
log.msg(".run() called") | |
# This is the default behavior | |
super(ReactorlessCommando, self).run() | |
# Setup a deferred to hold the delayed result and not return it until | |
# it's done. This object will be populated with the value of the | |
# results once all commands have been executed on all devices. | |
d = defer.Deferred() | |
# Add monitor_result as a callback | |
d.addCallback(self.monitor_result, reactor) | |
# Tell the reactor to call the callback above when it starts | |
reactor.callWhenRunning(d.callback, reactor) | |
return d | |
def monitor_result(self, result, reactor): | |
""" | |
Loop periodically or until the factory stops to check if we're | |
``all_done`` and then return the results. | |
""" | |
# Once we're done, return the results | |
if self.all_done: | |
return self.results | |
# Otherwise tell the reactor to call me again after 0.5 seconds. | |
return task.deferLater(reactor, 0.5, self.monitor_result, result, reactor) | |
class SwitcherooCommand(ReactorlessCommando): | |
def to_juniper(self, device, commands=None, extra=None): | |
""" | |
The default Commando stuff will attempt to wrap my commands in | |
<command> elements. We do not want this here, since we are using the | |
XMLRPC API directly. | |
""" | |
return self.commands | |
def from_juniper(self, data, device): | |
""" | |
Convert xml to json for user friendly-ness | |
""" | |
results = {} | |
for xml in data: | |
jdata = xmltodict.parse( | |
ET.tostring(xml), | |
postprocessor=strip_juniper_namespace, | |
xml_attribs=False) | |
results.update(jdata['rpc-reply']) | |
self.store_results(device, results) | |
from cyclone.web import HTTPError | |
class JsonrpcHandler(cyclone.jsonrpc.JsonrpcRequestHandler): | |
RESULTS = {} | |
def prepare(self): | |
try: | |
print self.request.body | |
self.json_request = cyclone.escape.json_decode(self.request.body) | |
except Exception, e: | |
log.msg("Bad Request: %s" % str(e)) | |
raise HTTPError(400) | |
def post(self, *args): | |
self._auto_finish = False | |
try: | |
req = self.json_request | |
jsonid = req["id"] | |
method = req["method"] | |
assert isinstance(method, types.StringTypes), \ | |
"Invalid method type: %s" % type(method) | |
params = req["params"] | |
assert isinstance(params, (types.ListType, types.TupleType)), \ | |
"Invalid params type: %s" % type(params) | |
except Exception, e: | |
log.msg("Bad Request: %s" % str(e)) | |
raise HTTPError(400) | |
self.jsonid = jsonid | |
self.method = method | |
self.params = params | |
function = getattr(self, "jsonrpc_%s" % method, None) | |
if callable(function): | |
args = list(args) + params | |
d = defer.maybeDeferred(function, *args) | |
d.addBoth(self._cbResult, jsonid) | |
else: | |
self._cbResult(AttributeError("method not found: %s" % method), | |
jsonid) | |
def store_results(self, results, id): | |
self.RESULTS[id] = results | |
def jsonrpc_match(self, match_dict): | |
devices = nd.match(**match_dict) | |
ret = {} | |
for dev in devices: | |
ret[dev.nodeName] = { | |
'site': dev.site, | |
'manufacturer': dev.manufacturer, | |
'serialNumber': dev.serialNumber, | |
} | |
return {'devices': ret, 'match': match_dict} | |
def jsonrpc_get_results(self, id): | |
if id in self.RESULTS: | |
return self.RESULTS[id] | |
else: | |
return None | |
def jsonrpc_run(self, xml_string, kwargs): | |
devices = [] | |
if 'hosts' in kwargs: | |
for host in kwargs['hosts']: | |
devices.append(nd[host]) | |
elif 'match' in kwargs: | |
print kwargs['match'] | |
devices = nd.match(**kwargs['match']) | |
else: | |
log.msg("Request did not have hosts or match arguments!") | |
raise HTTPError(400) | |
self.RESULTS.pop(self.jsonid, None) | |
commands = [ET.fromstring(xml_string)] | |
print devices | |
action = SwitcherooCommand(commands=commands, devices=devices) | |
d = action.run() | |
d.addCallback(self.store_results, self.jsonid) | |
return {'id': self.jsonid} | |
def main(): | |
log.startLogging(sys.stdout, setStdout=False) | |
application = cyclone.web.Application([ | |
(r"/jsonrpc", JsonrpcHandler), | |
]) | |
reactor.listenTCP(8889, application) | |
reactor.run() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment