Scripting in presets

User defined scripts can now be used in presets allowing dynamic values
and complex provisioning workflow.
This commit is contained in:
Zaid Abdulla committed 2016-03-26 02:16:37 +03:00
1 parent ba33fc0fe0
commit 48d9adbaff
7 files changed
+441 -117

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
###
# Copyright 2013-2016 Zaid Abdulla
#
# GenieACS is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# GenieACS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
###
vm = require 'vm'
crypto = require 'crypto'
db = require './db'
REFRESH = 3000
nextRefresh = 0
hash = null
presets = null
scripts = null
UNLOCK_SCRIPT = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end'
EXTEND_SCRIPT = 'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("pexpire", KEYS[1], ARGV[2]) else return 0 end'
lock = (lockName, ttl, callback) ->
token = Math.random().toString(36).slice(2)
unlockOrExtend = (ttl) ->
if not ttl
db.redisClient.eval(UNLOCK_SCRIPT, 1, lockName, token, (err, res) ->
throw err or new Error('Lock expired') if err or not res
)
else
db.redisClient.eval(EXTEND_SCRIPT, 1, lockName, token, ttl, (err, res) ->
throw err or new Error('Lock expired') if err or not res
)
return
db.redisClient.set(lockName, token, 'NX', 'PX', ttl, (err, res) ->
if err or res
return callback(err, unlockOrExtend)
setTimeout(() ->
return lock(lockName, ttl, callback)
, 200)
)
computeHash = () ->
# MD5 hash for presets and scripts for detecting changes
h = crypto.createHash('md5')
for p in presets
h.update(JSON.stringify(p.name))
h.update(JSON.stringify(p.precondition))
h.update(JSON.stringify(p.provisions))
for s in Object.keys(scripts).sort()
h.update(JSON.stringify(s))
h.update(JSON.stringify(scripts[s].source))
return h.digest('hex')
refresh = (callback) ->
now = Date.now()
db.redisClient.get('presets_hash', (err, res) ->
return callback(err) if err
if hash? and res == hash
nextRefresh = now + REFRESH
return callback()
lock('presets_hash_lock', 3000, (err, unlockOrExtend) ->
counter = 2
db.presetsCollection.find().toArray((err, res) ->
if err
callback(err) if -- counter >= 0
counter = 0
return
res.sort((a, b) ->
if a.weight == b.weight
return a._id > b._id
else
return a.weight - b.weight
)
presets = []
for preset in res
precondition = JSON.parse(preset.precondition)
provisions = preset.provisions or []
# Generate provisions from the old configuration format
for c in preset.configurations
switch c.type
when 'age'
provisions.push(['refresh', c.name, c.age * -1000])
when 'value'
provisions.push(['value', c.name, c.value])
when 'add_tag'
provisions.push(['tag', c.tag, true])
when 'delete_tag'
provisions.push(['tag', c.tag, false])
when 'script'
provisions.push([c.name].concat(c.args or []))
else
throw new Error("Unknown configuration type #{c.type}")
presets.push({name: preset._id, precondition: precondition, provisions: provisions})
if -- counter == 0
computeHash()
db.redisClient.setex("presets_hash", 300, hash, (err) ->
unlockOrExtend(0)
nextRefresh = now + REFRESH
return callback()
)
)
db.scriptsCollection.find().toArray((err, res) ->
if err
callback(err) if -- counter >= 0
counter = 0
return
scripts = {}
for r in res
scripts[r._id] = {}
scripts[r._id].source = r.source
scripts[r._id].md5 = r.md5
scripts[r._id].compiled = vm.Script(r.source, {filename: r._id, timeout: 50})
if -- counter == 0
computeHash()
db.redisClient.setex("presets_hash", 300, hash, (err) ->
unlockOrExtend(0)
nextRefresh = now + REFRESH
return callback()
)
)
)
)
getPresets = (callback) ->
if Date.now() < nextRefresh
return callback(null, hash, presets)
refresh((err) ->
return callback(err, hash, presets)
)
getScripts = (callback) ->
if Date.now() < nextRefresh
return callback(null, hash, scripts)
refresh((err) ->
return callback(err, hash, scripts)
)
exports.getPresets = getPresets
exports.getScripts = getScripts
+10 -3
View File
@@ -44,10 +44,10 @@ zlib = require 'zlib'
config = require './config'
common = require './common'
soap = require './soap'
db = require './db'
session = require './session'
query = require './query'
device = require './device'
cache = require './cache'
writeResponse = (currentRequest, res) ->
@@ -104,7 +104,11 @@ inform = (currentRequest, cwmpRequest) ->
applyPresets = (currentRequest) ->
db.getPresetsObjectsAliases((presets, objects, aliases) ->
cache.getPresets((err, presetsHash, presets) ->
throw err if err
currentRequest.sessionData.presetsHash = presetsHash
parameters = {}
for p in presets
for a in Object.keys(query.queryProjection(p.precondition))
@@ -126,10 +130,12 @@ applyPresets = (currentRequest) ->
for p in presets
if query.test(parameters, p.precondition)
session.addPreset(currentRequest.sessionData, p._id, p)
session.addProvisions(currentRequest.sessionData, p.provisions)
session.rpcRequest(currentRequest.sessionData, null, (err, id, rpcRequest) ->
throw err if err
if not rpcRequest?
session.clearProvisions(currentRequest.sessionData)
sendRpcRequest(currentRequest, id, rpcRequest)
)
)
@@ -140,6 +146,7 @@ nextRpc = (currentRequest) ->
session.rpcRequest(currentRequest.sessionData, null, (err, id, rpcRequest) ->
throw err if err
if not rpcRequest?
session.clearProvisions(currentRequest.sessionData)
return applyPresets(currentRequest)
sendRpcRequest(currentRequest, id, rpcRequest)
+9 -1
View File
@@ -26,10 +26,11 @@ tasksCollection = null
devicesCollection = null
presetsCollection = null
objectsCollection = null
scriptsCollection = null
connect = (callback) ->
callbackCounter = 6
callbackCounter = 7
mongodb.MongoClient.connect(config.get('MONGODB_CONNECTION_URL'), {db:{w:1},server:{autoReconnect:true}}, (err, db) ->
return callback(err) if err
exports.mongoDb = db
@@ -71,6 +72,13 @@ connect = (callback) ->
return callback(err)
)
db.collection('scripts', (err, collection) ->
exports.scriptsCollection = scriptsCollection = collection
if --callbackCounter == 0 or err
callbackCounter = 0
return callback(err)
)
exports.redisClient = redisClient = redis.createClient(config.get('REDIS_PORT'), config.get('REDIS_HOST'))
redisClient.select(config.get('REDIS_DB'), (err) ->
if --callbackCounter == 0 or err
+37
View File
@@ -41,6 +41,7 @@
url = require 'url'
mongodb = require 'mongodb'
querystring = require 'querystring'
crypto = require 'crypto'
config = require './config'
common = require './common'
@@ -58,6 +59,7 @@ FILES_REGEX = /^\/files\/([a-zA-Z0-9\%\!\*\'\(\)\;\:\@\&\=\+\$\,\?\#\[\]\-\_\.\~
PING_REGEX = /^\/ping\/([a-zA-Z0-9\-\_\.]+)\/?$/
QUERY_REGEX = /^\/([a-zA-Z0-9_]+s)\/?$/
DELETE_DEVICE_REGEX = /^\/devices\/([a-zA-Z0-9\-\_\%]+)\/?$/
SCRIPTS_REGEX = /^\/scripts\/([a-zA-Z0-9\-\_\%]+)\/?$/
errorToString = (err) ->
@@ -149,6 +151,41 @@ listener = (request, response) ->
else
response.writeHead 405, {'Allow': 'PUT, DELETE'}
response.end('405 Method Not Allowed')
else if SCRIPTS_REGEX.test(urlParts.pathname)
scriptName = querystring.unescape(SCRIPTS_REGEX.exec(urlParts.pathname)[1])
if request.method == 'PUT'
object = {
_id: scriptName
source: body.toString()
md5: crypto.createHash('md5').update(body).digest('hex')
}
db.scriptsCollection.save(object, (err) ->
db.redisClient.del('presets_hash', (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(errorToString(err))
return
response.writeHead(200)
response.end()
)
else if request.method == 'DELETE'
db.scriptsCollection.remove({'_id' : scriptName}, (err) ->
db.redisClient.del('presets_hash', (err) ->
throw err if err
)
if err
response.writeHead(500)
response.end(errorToString(err))
return
response.writeHead(200)
response.end()
)
else
response.writeHead 405, {'Allow': 'PUT, DELETE'}
response.end('405 Method Not Allowed')
else if TAGS_REGEX.test(urlParts.pathname)
r = TAGS_REGEX.exec(urlParts.pathname)
deviceId = querystring.unescape(r[1])
-46
View File
@@ -1,46 +0,0 @@
###
# Copyright 2013-2016 Zaid Abdulla
#
# GenieACS is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# GenieACS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
###
common = require './common'
processProvision = (type, args, callback) ->
switch type
when 'refresh'
declarations = []
path = common.parsePath(args[0])
declaration = [args[1], null, null, null, null, null, args[1]]
for i in [path.length...14] by 1
path = path.slice()
path.length = i
declarations.push([path].concat(declaration))
return callback(null, declarations)
when 'value'
declarations = [[common.parsePath(args[0]), args[1], null, null, null, null, null, args[1]]]
return callback(null, declarations)
when 'tag'
declarations = [[['Tags', args[0]], null, null, null, null, null, null, null, [args[1], 'xsd:boolean']]]
return callback(null, declarations)
else
return callback(new Error("Unknown provision type '#{type}'"))
exports.processProvision = processProvision
+121
View File
@@ -0,0 +1,121 @@
###
# Copyright 2013-2016 Zaid Abdulla
#
# GenieACS is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# GenieACS is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with GenieACS. If not, see <http://www.gnu.org/licenses/>.
###
vm = require 'vm'
common = require './common'
device = require './device'
# Used for throwing to exit user script
class Declare;
sandbox = {
deviceData: null
args: null
revision: null
maxRevision: null
declarations: null
context: vm.createContext()
}
sandbox.context.declare = (decs) ->
if ++ sandbox.revision > sandbox.maxRevision + 1
throw new Error('Declare function should not be called from within a try/catch block')
sandbox.declarations = decs
if sandbox.revision == sandbox.maxRevision + 1
throw new Declare()
res = {}
for k, v of decs
path = common.parsePath(k)
all = device.getAll(sandbox.deviceData, path, sandbox.revision)
res[k] = {}
for a in all
r = {}
r.exist = a[1] if v.exist?
r.type = a.slice(3, 5) if v.type?
r.writable = a.slice(5, 7) if v.writable?
r.value = a.slice(7, 9) if v.value? and a[8]?
p = a[0].join('.')
if p == k
res[k] = r
else
res[k][p] = r
return res
Object.freeze(sandbox.context)
# Convert from friendly declaration format
toIndexDeclaration = (keyDeclaraiton) ->
indexDeclaraiton = []
if keyDeclaraiton['exist']?
if Array.isArray(keyDeclaraiton['exist'])
indexDeclaraiton[0] = keyDeclaraiton['exist'][0]
else
indexDeclaraiton[0] = keyDeclaraiton['exist']
if keyDeclaraiton['type']?
if Array.isArray(keyDeclaraiton['type'])
indexDeclaraiton[2] = keyDeclaraiton['type'][0]
else
indexDeclaraiton[2] = keyDeclaraiton['type']
if keyDeclaraiton['writable']?
if Array.isArray(keyDeclaraiton['writable'])
indexDeclaraiton[4] = keyDeclaraiton['writable'][0]
else
indexDeclaraiton[4] = keyDeclaraiton['writable']
if keyDeclaraiton['value']?
if Array.isArray(keyDeclaraiton['value'])
indexDeclaraiton[6] = keyDeclaraiton['value'][0]
indexDeclaraiton[7] = keyDeclaraiton['value'][1]
else
indexDeclaraiton[6] = keyDeclaraiton['value']
return indexDeclaraiton
run = (script, args, deviceData, revision, declarations) ->
sandbox.deviceData = deviceData
sandbox.args = args
sandbox.maxRevision = revision
sandbox.revision = 0
sandbox.declarations = []
try
script.runInNewContext(sandbox.context)
catch err
throw err if err not instanceof Declare
for k, v of sandbox.declarations
declarations.push([common.parsePath(k)].concat(toIndexDeclaration(v)))
return true
for k, v of sandbox.declarations
declarations.push([common.parsePath(k)].concat(toIndexDeclaration(v)))
return false
exports.run = run
+89 -67
View File
@@ -21,7 +21,8 @@ config = require './config'
common = require './common'
db = require './db'
device = require './device'
provisions = require './provisions'
sandbox = require './sandbox'
cache = require './cache'
init = (deviceId, cwmpVersion, timeout) ->
@@ -35,8 +36,8 @@ init = (deviceId, cwmpVersion, timeout) ->
deviceData : deviceData
cwmpVersion : cwmpVersion
timeout : timeout
presets : {}
rpcCount : 0
provisions: []
counter : [0, 0, 0] # rpc, revision, batch
}
return sessionData
@@ -65,26 +66,47 @@ loadParameters = (sessionData, toLoad, callback) ->
)
inform = (sessionData, rpcRequest, callback) ->
timestamp = sessionData.timestamp
device.set(sessionData.deviceData, ['DeviceID', 'Manufacturer'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.Manufacturer, 'xsd:string']])
device.set(sessionData.deviceData, ['DeviceID', 'OUI'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.OUI, 'xsd:string']])
device.set(sessionData.deviceData, ['DeviceID', 'ProductClass'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.ProductClass, 'xsd:string']])
device.set(sessionData.deviceData, ['DeviceID', 'SerialNumber'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcRequest.deviceId.SerialNumber, 'xsd:string']])
generateRpcId = (sessionData) ->
if sessionData.counter[0] > 255 or sessionData.counter[1] > 15 or sessionData.counter[2] > 15
throw new Error('Too many RPCs')
for p in rpcRequest.parameterList
return sessionData.counter[2].toString(16).slice(-1) +
sessionData.counter[1].toString(16).slice(-1) +
('0' + sessionData.counter[0].toString(16)).slice(-2)
inform = (sessionData, rpcReq, callback) ->
timestamp = sessionData.timestamp
device.set(sessionData.deviceData, ['DeviceID', 'Manufacturer'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.Manufacturer, 'xsd:string']])
device.set(sessionData.deviceData, ['DeviceID', 'OUI'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.OUI, 'xsd:string']])
device.set(sessionData.deviceData, ['DeviceID', 'ProductClass'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.ProductClass, 'xsd:string']])
device.set(sessionData.deviceData, ['DeviceID', 'SerialNumber'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [rpcReq.deviceId.SerialNumber, 'xsd:string']])
for p in rpcReq.parameterList
device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, null, null, timestamp, p.slice(1)])
device.set(sessionData.deviceData, ['Events', 'Inform'], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [timestamp, 'xsd:dateTime']])
for e in rpcRequest.event
for e in rpcReq.event
device.set(sessionData.deviceData, ['Events', e.replace(' ', '_')], 1, [timestamp, 1, timestamp, 0, timestamp, 0, timestamp, [timestamp, 'xsd:dateTime']])
return callback(null, {type : 'InformResponse'})
addPreset = (sessionData, name, preset) ->
sessionData.presets[name] = preset
addProvisions = (sessionData, provisions) ->
sessionData.provisions = sessionData.provisions.concat(provisions)
if sessionData.counter[1] > 0
sessionData.counter[2] += 1
sessionData.counter[1] = 0
sessionData.counter[0] = 0
clearProvisions = (sessionData) ->
sessionData.provisions = []
if sessionData.counter[1] > 0
sessionData.counter[2] += 1
sessionData.counter[1] = 0
sessionData.counter[0] = 0
generateRpcRequest = (sessionData, declarations, callback) ->
@@ -214,26 +236,26 @@ generateRpcRequest = (sessionData, declarations, callback) ->
return
if res.gpn?
rpcRequest = {
rpcReq = {
type: 'GetParameterNames'
parameterPath: res.gpn[0].join('.')
nextLevel: true
}
return callback(null, rpcRequest)
return callback(null, rpcReq)
if res.gpv?
rpcRequest = {
rpcReq = {
type: 'GetParameterValues'
parameterNames: (p.join('.') for p in res.gpv.slice(0, config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId)))
}
return callback(null, rpcRequest)
return callback(null, rpcReq)
if res.spv?
rpcRequest = {
rpcReq = {
type: 'SetParameterValues'
parameterList: ([p[0].join('.'), p[1], p[2]] for p in res.spv.slice(0, config.get('TASK_PARAMETERS_BATCH_SIZE', sessionData.deviceId)))
}
return callback(null, rpcRequest)
return callback(null, rpcReq)
return cb()
@@ -263,74 +285,73 @@ generateRpcRequest = (sessionData, declarations, callback) ->
rpcRequest = (sessionData, allDeclarations, callback) ->
presetNames = Object.keys(sessionData.presets)
presetNames.sort((a, b) ->
if sessionData.presets[a].weight == sessionData.presets[b].weight
return a > b
else
return sessionData.presets[a].weight - sessionData.presets[b].weight
)
allDeclarations = allDeclarations?.slice() ? []
counter = 1
for presetName in presetNames
do (presetName) ->
for provision in sessionData.presets[presetName].provisions
++ counter
provisions.processProvision(provision[0], provision[1..], (err, declarations) ->
if err
callback(err) if -- counter >= 0
counter = 0
return
nextRevision = false
allDeclarations = allDeclarations.concat(declarations)
cache.getScripts((err, presetsHash, scripts) ->
return callback(err) if err or presetsHash != sessionData.presetsHash
if -- counter == 0
generateRpcRequest(sessionData, allDeclarations, (err, rpcRequest) ->
sessionData.rpcRequest = rpcRequest
if err or not rpcRequest?
return callback(err)
for provision in sessionData.provisions
if not scripts[provision[0]]?
switch provision[0]
when 'refresh'
path = common.parsePath(provision[1])
for i in [path.length...16] by 1
path.length = i
allDeclarations.push([path.slice(), provision[2], null, null, null, null, null, provision[2]])
when 'value'
allDeclarations.push([[common.parsePath(provision[1]), provision[2], null, null, null, null, null, provision[2]]])
when 'tag'
allDeclarations.push([[['Tags', provision[1]], null, null, null, null, null, null, null, [provision[2], 'xsd:boolean']]])
continue
callback(err, sessionData.rpcCount++, rpcRequest)
)
return
)
nextRevision ||= sandbox.run(scripts[provision[0]].compiled, provision.slice(1), sessionData.deviceData, sessionData.counter[1], allDeclarations)
if -- counter == 0
generateRpcRequest(sessionData, allDeclarations, (err, rpcRequest) ->
sessionData.rpcRequest = rpcRequest
if err or not rpcRequest?
return callback(err)
generateRpcRequest(sessionData, allDeclarations, (err, rpcReq) ->
sessionData.rpcRequest = rpcReq
return callback(err) if err
callback(err, sessionData.rpcCount++, rpcRequest)
if not rpcReq?
if not nextRevision
return callback()
sessionData.counter[0] = 0
sessionData.counter[1] += 1
return rpcRequest(sessionData, null, callback)
callback(err, generateRpcId(sessionData), rpcReq)
)
return
)
return
rpcResponse = (sessionData, id, rpcResponse, callback) ->
# TODO verify ID
rpcRequest = sessionData.rpcRequest
rpcResponse = (sessionData, id, rpcRes, callback) ->
if id != generateRpcId(sessionData)
return callback(new Error('Request ID not recognized'))
++ sessionData.counter[0]
rpcReq = sessionData.rpcRequest
sessionData.rpcRequest = null
timestamp = sessionData.timestamp
switch rpcResponse.type
switch rpcRes.type
when 'GetParameterValuesResponse'
return callback(new Error('Response type does not match request type')) if rpcRequest.type isnt 'GetParameterValues'
return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'GetParameterValues'
for p in rpcResponse.parameterList
for p in rpcRes.parameterList
device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, null, null, timestamp, p.slice(1)])
when 'GetParameterNamesResponse'
return callback(new Error('Response type does not match request type')) if rpcRequest.type isnt 'GetParameterNames'
return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'GetParameterNames'
device.set(sessionData.deviceData, common.parsePath(rpcRequest.parameterPath).concat(null), 1, timestamp)
device.set(sessionData.deviceData, common.parsePath(rpcReq.parameterPath).concat(null), 1, timestamp)
for p in rpcResponse.parameterList
for p in rpcRes.parameterList
if common.endsWith(p[0], '.')
path = common.parsePath(p[0][0...-1])
if not rpcRequest.nextLevel
if not rpcReq.nextLevel
device.set(sessionData.deviceData, path.conact(null), 1, timestamp)
device.set(sessionData.deviceData, path, 1, [timestamp, 1, timestamp, 1, timestamp, if p[1] then 1 else 0])
@@ -338,9 +359,9 @@ rpcResponse = (sessionData, id, rpcResponse, callback) ->
device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, timestamp, if p[1] then 1 else 0])
when 'SetParameterValuesResponse'
return callback(new Error('Response type does not match request type')) if rpcRequest.type isnt 'SetParameterValues'
return callback(new Error('Response type does not match request type')) if rpcReq.type isnt 'SetParameterValues'
for p in rpcRequest.parameterList
for p in rpcReq.parameterList
device.set(sessionData.deviceData, common.parsePath(p[0]), 1, [timestamp, 1, timestamp, 0, timestamp, 1, timestamp, p.slice(1)])
else
@@ -410,7 +431,8 @@ end = (sessionData, callback) ->
exports.init = init
exports.inform = inform
exports.addPreset = addPreset
exports.addProvisions = addProvisions
exports.clearProvisions = clearProvisions
exports.rpcRequest = rpcRequest
exports.rpcResponse = rpcResponse
exports.rpcFault = rpcFault