2024-04-01 10:40:20 +02:00

772 lines
25 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from NSCP import Settings, Registry, Core, log, log_debug, log_error, status
import plugin_pb2
from optparse import OptionParser
from sets import Set
import os
import traceback
#import string
from jinja2 import Template, Environment
import hashlib
helper = None
module_template = u"""# {{module.key}}
{{module.info.description}}
{%if module.ext_desc -%}
{{ module.ext_desc}}
{%- endif %}
{% if module.queries -%}
## List of commands
A list of all available queries (check commands)
{% set table = [] -%}
{% for key,query in module.queries|dictsort -%}
{% if query.info.description.startswith('Alternative name for:') -%}
{% set command = query.info.description[22:] -%}
{% do table.append([query.key|md_self_link, command|rst_link('query')]) -%}
{%- elif query.info.description.startswith('Alias for:') -%}
{% set command = query.info.description[11:] -%}
{% do table.append([query.key|md_self_link, command|rst_link('query')]) -%}
{%- else -%}
{% do table.append([query.key|md_self_link, query.info.description|firstline]) -%}
{%- endif %}
{%- endfor %}
{{table|rst_table('Command', 'Description')}}
{%- endif %}
{% if module.aliases -%}
## List of command aliases
A list of all short hand aliases for queries (check commands)
{% set table = [] -%}
{% for key,query in module.aliases|dictsort -%}
{% if query.info.description.startswith('Alternative name for:') -%}
{% set command = query.info.description[22:] -%}
{% do table.append([query.key, "Alias for: " + command|rst_link('query')]) -%}
{%- elif query.info.description.startswith('Alias for:') -%}
{% set command = query.info.description[11:] -%}
{% do table.append([query.key, "Alias for: " + command|rst_link('query')]) -%}
{%- else -%}
{% do table.append([query.key, query.info.description|firstline]) -%}
{%- endif %}
{%- endfor %}
{{table|rst_table('Command', 'Description')}}
{%- endif %}
{% if module.paths -%}
## List of Configuration
{% set table = [] -%}
{% set table_adv = [] -%}
{% set table_sam = [] -%}
{% for k,path in module.paths|dictsort -%}
{% set pkey = path.key|md_self_link -%}
{% for k,key in path.keys|dictsort -%}
{% set kkey = k|md_prefix_lnk(path.key)|md_self_link(k) -%}
{% if key.info.sample -%}
{% do table_sam.append([pkey, kkey, key.info.title|firstline]) -%}
{%- elif key.info.advanced -%}
{% do table_adv.append([pkey, kkey, key.info.title|firstline]) -%}
{%- else -%}
{% do table.append([pkey, kkey, key.info.title|firstline]) -%}
{%- endif %}
{%- endfor %}
{%- endfor %}
{% if table -%}
### Common Keys
{{table|rst_table('Path / Section', 'Key', 'Description')}}
{%- endif %}
{% if table_adv -%}
### Advanced keys
{{table_adv|rst_table('Path / Section', 'Key', 'Description')}}
{%- endif %}
{% if table_sam -%}
### Sample keys
{{table_sam|rst_table('Path / Section', 'Key', 'Description')}}
{%- endif %}
{%- endif %}
{% if module.sample %}
# Usage
_To edit the usage section please edit [this page](https://github.com/mickem/nscp-docs/blob/master/{{module.sample_source}})_
{{module.sample}}
{%- endif %}
{% if module.queries -%}
# Queries
A quick reference for all available queries (check commands) in the {{module.key}} module.
{% for k,query in module.queries|dictsort -%}
## {{query.key}}
{{query.info.description}}
{% if query.sample -%}
### Usage
_To edit these sample please edit [this page](https://github.com/mickem/nscp-docs/blob/master/{{query.sample_source}})_
{{query.sample}}
{%- endif %}
### Usage
{% set table = [] -%}
{% for help in query.params -%}{% if help.content_type == 4 -%}
{% do table.append([help.name|md_prefix_lnk(query.key)|md_self_link(help.name),'N/A', help.long_description|firstline]) %}{% else -%}
{% do table.append([help.name|md_prefix_lnk(query.key)|md_self_link(help.name),help.default_value, help.long_description|firstline]) %}{%- endif %}
{%- endfor %}
{{table|rst_table('Option', 'Default Value', 'Description')}}
{% for help in query.params -%}
<a name="{{help.name|md_prefix_lnk(query.key)}}"/>
### {{help.name}}
{% if help.default_value %}
**Deafult Value:** {{help.default_value}}
{%- endif %}
**Description:**
{%if help.ext -%}
{{help.ext.head}}
{{help.ext.data|rst_table}}
{{help.ext.tail}}
{% else -%}
{{help.long_description}}
{%- endif %}
{{'\n'}}
{%- endfor %}
{%- endfor %}
{%- endif %}
{% if module.paths -%}
# Configuration
{% for pkey,path in module.paths|dictsort -%}
<a name="{{path.key}}"/>
## {{path.info.title}}
{{path.info.description}}
```ini
# {{path.info.description|firstline}}
[{{path.key}}]
{% for kkey,key in path.keys|dictsort -%}
{% if key.info.default_value|extract_value -%}
{{kkey}}={{key.info.default_value|extract_value}}
{% endif %}
{%- endfor %}
```
{% set tbl = [] -%}
{% set pkey = path.key|md_self_link -%}
{% for k,key in path.keys|dictsort -%}
{% set kkey = k|md_prefix_lnk(path.key)|md_self_link(k) -%}
{% do tbl.append([kkey, key.info.default_value|extract_value, key.info.title|firstline]) -%}
{%- endfor %}
{{tbl|rst_table('Key', 'Default Value', 'Description')}}
{% for kkey,key in path.keys|dictsort %}
<a name="{{kkey|md_prefix_lnk(path.key)}}"/>
### {{kkey}}
**{{key.info.title|as_text}}**
{%if key.ext -%}
{{key.ext.head}}
{{key.ext.data|rst_table}}
{{key.ext.tail}}
{% else -%}
{{key.info.description|as_text}}
{%- endif %}
{% set table = [] -%}
{% do table.append(['Path:', path.key|md_self_link(path.key)]) -%}
{% do table.append(['Key:', kkey]) -%}
{% if key.info.advanced -%}
{% do table.append(['Advanced:', 'Yes (means it is not commonly used)']) -%}
{%- endif %}
{% if key.info.default_value|extract_value -%}
{% do table.append(['Default value:', '`' + key.info.default_value|extract_value + '`']) -%}
{% else %}
{% do table.append(['Default value:', '_N/A_']) -%}
{%- endif %}
{% if key.info.sample -%}
{% do table.append(['Sample key:', 'Yes (This section is only to show how this key is used)']) -%}
{%- endif %}
{% do table.append(['Used by:', ', '.join(path.info.plugin|sort)]) -%}
{{table|rst_table('Key', 'Description')}}
#### Sample
```
[{{path.key}}]
# {{key.info.title}}
{{kkey}}={{key.info.default_value|extract_value}}
```
{% endfor %}
{% endfor %}
{%- endif %}
"""
index_template = u"""
# Modules
{% set table = [] -%}
{% for key,module in plugins|dictsort -%}
{% do table.append([module.namespace, ('reference/' + module.namespace + '/' + module.key)|md_link(module.key), module.info.description|firstline]) -%}
{%- endfor %}
{{table|rst_table('Type', 'Module', 'Description')}}
# Queries
{% set table = [] -%}
{% for mk,module in plugins|dictsort -%}
{% for key,query in module.queries|dictsort -%}
{% set desc = query.info.description|firstline %}
{% if desc.startswith('Alternative name for:') -%}
{% set desc = query.info.description[22:]|rst_link('query') -%}
{%- elif desc.startswith('Legacy version of ') -%}
{% set desc = '**Deprecated** please use: ' + query.info.description[18:]|rst_link('query') -%}
{%- elif query.info.description.startswith('Alias for:') -%}
{% set desc = query.info.description[11:]|rst_link('query') -%}
{%- endif %}
{% do table.append([mk, ('reference/' + module.namespace + '/' + module.key + '#' + query.key)|md_link(query.key), desc]) -%}
{%- endfor %}
{%- endfor %}
{{table|rst_table('Module', 'Command', 'Description')}}
"""
samples_template = u""".. default-domain:: nscp
.. default-domain:: nscp
===========
All samples
===========
A collection of all sample commands
{% for mk,module in plugins|dictsort -%}
{% set vars = {'found': False} -%}
{% for qk,query in module.queries|dictsort -%}
{% if query.sample -%}
{% if vars.update({'found': True}) -%}{%- endif %}
{%- endif %}
{%- endfor %}
{% if vars.found -%}
{{mk|rst_heading('=')}}
{% for qk,query in module.queries|dictsort -%}
{% if query.sample -%}
{{qk|rst_heading}}
{{query.info.description|firstline}}
.. include:: ../samples/{{query.sample}}
{% endif %}
{%- endfor %}
{%- endif %}
{%- endfor %}
"""
def split_argllist(name, desc):
extdata = {}
spos = desc.find('\n\n')
if spos != -1:
epos = desc.find('\n\n', spos+2)
if epos != -1:
pos = desc.find('\t', spos+2, epos)
if pos != -1:
extdata['head'] = desc[:spos]
extdata['tail'] = desc[epos+2:]
data = desc[spos+2:epos]
rows = data.split('\n')
tbl = []
for r in rows:
tbl.append(r.split('\t'))
extdata['data'] = tbl
return extdata
class root_container(object):
paths = {}
commands = {}
aliases = {}
plugins = {}
windows_modules = ['CheckSystem', 'CheckDisk', 'NSClientServer', 'DotnetPlugins', 'CheckEventLog', 'CheckTaskSched', 'CheckWMI']
unix_modules = ['CheckSystemUnix']
check_modules = ['CheckExternalScripts', 'CheckHelpers', 'CheckLogFile', 'CheckMKClient', 'CheckMKServer', 'CheckNSCP']
client_modules = ['GraphiteClient', 'NRDPClient', 'NRPEClient', 'NRPEServer', 'NSCAClient', 'NSCAServer', 'NSClientServer', 'SMTPClient', 'SyslogClient']
generic_modules = ['CommandClient', 'DotnetPlugins', 'LUAScript', 'PythonScript', 'Scheduler', 'SimpleCache', 'SimpleFileWriter', 'WEBServer']
def __init__(self):
self.paths = {}
self.commands = {}
self.aliases = {}
self.plugins = {}
def append_key(self, info):
path = info.node.path
if path in self.paths:
self.paths[path].append_key(info)
else:
p = path_container()
p.append_key(info)
self.paths[path] = p
def append_path(self, info):
path = info.node.path
if not path in self.paths:
self.paths[path] = path_container(info)
def append_command(self, info):
name = info.name
if not name in self.commands:
self.commands[name] = command_container(info)
def append_alias(self, info):
name = info.name
if not name in self.commands:
self.aliases[name] = command_container(info)
def append_plugin(self, info, folder):
name = info.name
namespace = ''
if name in self.windows_modules:
namespace = 'windows'
elif name in self.unix_modules:
namespace = 'unix'
elif name in self.check_modules:
namespace = 'check'
elif name in self.client_modules:
namespace = 'client'
elif name in self.generic_modules:
namespace = 'generic'
else:
namespace = 'misc'
if not name in self.plugins:
self.plugins[name] = plugin_container(info, namespace)
spath = '%s/samples/%s_samples.md'%(folder, name)
self.plugins[name].sample = ''
if os.path.exists(spath):
with open(spath) as f:
self.plugins[name].sample = unicode(f.read(), 'utf8')
self.plugins[name].sample_source = 'samples/%s_samples.md'%(name)
spath = '%s/samples/%s_desc.md'%(folder, name)
self.plugins[name].ext_desc = ''
if os.path.exists(spath):
with open(spath) as f:
self.plugins[name].ext_desc = unicode(f.read(), 'utf8')
self.plugins[name].ext_desc_source = 'samples/%s_desc.md'%(name)
def get_hash(self):
ret = {}
ret['paths'] = self.paths
ret['commands'] = self.commands
ret['aliases'] = self.aliases
ret['plugins'] = self.plugins
return ret
class path_container(object):
keys = {}
info = None
def __init__(self, info = None):
self.keys = {}
self.info = info.info
def append_key(self, info):
extdata = split_argllist(info.node.key, info.info.description)
if extdata:
ninfo = {}
ninfo['info'] = info.info
ninfo['node'] = info.node
ninfo['ext'] = extdata
self.keys[info.node.key] = ninfo
else:
self.keys[info.node.key] = info
class command_container(object):
info = None
parameters = None
def __init__(self, info = None):
self.info = info.info
self.parameters = info.parameters
class plugin_container(object):
info = None
namespace = ''
def __init__(self, info = None, namespace = ''):
self.info = info.info
self.namespace = namespace
def first_line(string):
return string.strip().split('\n')[0]
def make_rst_link(name, type, title = None):
if title:
return ':%s:`%s<%s>`'%(type, title, name)
return ':%s:`%s`'%(type, name)
def make_md_link(name, title = None):
if title:
return '[%s](%s)'%(title, name)
return '[%s](%s)'%(name, name)
def make_md_self_link(name, title = None):
if title:
return '[%s](#%s)'%(title, name)
return '[%s](#%s)'%(name, name)
def make_md_code(name):
return '`%s`'%name
def make_md_prefix_lnk(value, prefix):
return '%s_%s'%(prefix, value)
def largest_value(a,b):
return map(lambda n: n[0] if len(n[0])>len(n[1]) else n[1], zip(a, b))
def extract_value(value):
if value.HasField("string_data"):
return value.string_data
if value.HasField("int_data"):
return '%d'%value.int_data
if value.HasField("bool_data"):
return "true" if value.bool_data else "false"
return ''
def as_text(value):
value = value.replace('\\', '\\\\')
value = value.replace('`', '\\`')
value = value.replace('|', '\\|')
return value
def block_pad(string, pad, prefix = ''):
string = string.strip()
if not string:
return ''
ret = ''
for line in string.split('\n'):
if line != '\n':
ret += (pad * ' ') + prefix + line + '\n'
else:
ret += line + '\n'
return ret.rstrip()
def render_rst_table(table, *args):
if not table:
return ''
if args:
table.insert(0, args)
ret = ''
maxcol = map(lambda a:len(a), reduce(lambda a,b: largest_value(a,b), table))
for idx, line in enumerate(table):
ret += '|' + ''.join(map(lambda a:' ' + a[1].ljust(a[0],' ') + ' |', zip(maxcol, line))) + '\n'
if idx == 0:
ret += '|' + ''.join(map(lambda a:'-' + ''.ljust(a[0],'-') + '-|', zip(maxcol, line))) + '\n'
return ret
def render_rst_heading(string, char='-', top=False):
if top:
return "\n".rjust(len(string)+1, char) + string + "\n".ljust(len(string)+1, char)
return string + "\n".ljust(len(string)+1, char)
def getcommonletters(strlist):
return ''.join([x[0] for x in zip(*strlist) \
if reduce(lambda a,b:(a == b) and a or None,x)])
def calculate_common_head(strlist):
strlist = strlist[:]
prev = None
while True:
common = getcommonletters(strlist)
if common == prev:
break
strlist.append(common)
prev = common
return getcommonletters(strlist)
def render_template(hash, template, filename):
data = template.render(hash).encode('utf8')
path = os.path.dirname(filename)
if not os.path.exists(path):
os.makedirs(path)
if os.path.exists(filename):
m1 = hashlib.sha256()
m1.update(data)
sha1 = m1.digest()
with open(filename) as f:
m2 = hashlib.sha256()
m2.update(f.read())
sha2 = m2.digest()
if sha1 == sha2:
log_debug("no changes detected in: %s"%filename)
return
log_debug('Writing file: %s'%filename)
f = open(filename,"wb")
f.write(data)
f.close()
class DocumentationHelper(object):
plugin_id = None
plugin_alias = None
script_alias = None
conf = None
registry = None
core = None
dir = None
folder = None
command_cache = {}
def __init__(self, plugin_id, plugin_alias, script_alias):
self.plugin_id = plugin_id
self.plugin_alias = plugin_alias
self.script_alias = script_alias
self.conf = Settings.get(self.plugin_id)
self.registry = Registry.get(self.plugin_id)
self.core = Core.get(self.plugin_id)
self.command_cache = {}
self.folder = None
def build_inventory_request(self, path = '/', recursive = True, keys = False):
message = plugin_pb2.SettingsRequestMessage()
payload = message.payload.add()
payload.plugin_id = self.plugin_id
payload.inventory.node.path = path
payload.inventory.recursive_fetch = recursive
payload.inventory.fetch_keys = keys
payload.inventory.fetch_paths = not keys
payload.inventory.fetch_samples = True
payload.inventory.descriptions = True
return message.SerializeToString()
def build_command_request(self, type = 1):
message = plugin_pb2.RegistryRequestMessage()
payload = message.payload.add()
payload.inventory.fetch_all = True
payload.inventory.type.append(type)
return message.SerializeToString()
def get_paths(self):
(code, data) = self.conf.query(self.build_inventory_request())
if code == 1:
message = plugin_pb2.SettingsResponseMessage()
message.ParseFromString(data)
for payload in message.payload:
if payload.inventory:
log_debug('Found %d paths'%len(payload.inventory))
return payload.inventory
return []
def get_keys(self, path):
(code, data) = self.conf.query(self.build_inventory_request(path, False, True))
if code == 1:
message = plugin_pb2.SettingsResponseMessage()
message.ParseFromString(data)
for payload in message.payload:
if payload.inventory:
log_debug('Found %d keys for %s'%(len(payload.inventory), path))
return payload.inventory
return []
def get_queries(self):
log_debug('Fetching queries...')
(code, data) = self.registry.query(self.build_command_request(1))
if code == 1:
message = plugin_pb2.RegistryResponseMessage()
message.ParseFromString(data)
for payload in message.payload:
if payload.inventory:
log_debug('Found %d queries'%len(payload.inventory))
return payload.inventory
log_error('No queries found')
return []
def get_query_aliases(self):
log_debug('Fetching aliases...')
(code, data) = self.registry.query(self.build_command_request(5))
if code == 1:
message = plugin_pb2.RegistryResponseMessage()
message.ParseFromString(data)
for payload in message.payload:
if payload.inventory:
log_debug('Found %d aliases'%len(payload.inventory))
return payload.inventory
log_error('No aliases found')
return []
def get_plugins(self):
log_debug('Fetching plugins...')
(code, data) = self.registry.query(self.build_command_request(7))
if code == 1:
message = plugin_pb2.RegistryResponseMessage()
message.ParseFromString(data)
for payload in message.payload:
if payload.inventory:
log_debug('Found %d plugins'%len(payload.inventory))
return payload.inventory
log_error('No plugins')
return []
def get_info(self):
root = root_container()
for p in self.get_plugins():
root.append_plugin(p, self.folder)
for p in self.get_paths():
root.append_path(p)
for k in self.get_keys(p.node.path):
root.append_key(k)
for p in self.get_queries():
root.append_command(p)
for p in self.get_query_aliases():
root.append_alias(p)
return root
def fetch_command(self, module, minfo, command, cinfo):
if command in self.command_cache:
return self.command_cache[command]
params = []
for p in cinfo.parameters.parameter:
extdata = split_argllist(p.name, p.long_description)
if extdata:
np = {}
np['long_description'] = p.long_description
np['default_value'] = p.default_value
np['name'] = p.name
np['ext'] = extdata
np['content_type'] = p.content_type
params.append(np)
else:
params.append(p)
cinfo.params = params
spath = '%s/samples/%s_%s_samples.md'%(self.folder, module, command)
cinfo.sample = ''
if os.path.exists(spath):
with open(spath) as f:
cinfo.sample = unicode(f.read(), 'utf8')
cinfo.sample_source = 'samples/%s_%s_samples.md'%(module, command)
self.command_cache[command] = cinfo
return cinfo
def generate_rst(self, input_dir, output_dir):
root = self.get_info()
i = 0
env = Environment(extensions=["jinja2.ext.do",])
env.filters['firstline'] = first_line
env.filters['rst_link'] = make_rst_link
env.filters['md_link'] = make_md_link
env.filters['md_prefix_lnk'] = make_md_prefix_lnk
env.filters['md_self_link'] = make_md_self_link
env.filters['md_code'] = make_md_code
env.filters['rst_table'] = render_rst_table
env.filters['rst_heading'] = render_rst_heading
env.filters['extract_value'] = extract_value
env.filters['block_pad'] = block_pad
env.filters['common_head'] = calculate_common_head
env.filters['as_text'] = as_text
for (module,minfo) in root.plugins.iteritems():
out_base_path = '%s/docs/'%output_dir
sample_base_path = '%s/docs/samples/'%output_dir
if minfo.namespace:
out_base_path = '%s/docs/reference/%s/'%(output_dir, minfo.namespace)
hash = root.get_hash()
minfo.key = module
minfo.queries = {}
sfile = '%s%s_samples.inc'%(sample_base_path, module)
if os.path.exists(sfile):
minfo.sample = os.path.basename(sfile)
sfile = '%s%s_samples.rst'%(sample_base_path, module)
if os.path.exists(sfile):
minfo.sample = os.path.basename(sfile)
for (c,cinfo) in sorted(root.commands.iteritems()):
if module in cinfo.info.plugin:
more_info = self.fetch_command(module, minfo, c,cinfo)
if more_info:
cinfo = more_info
sfile = '%s%s_%s_samples.inc'%(sample_base_path, module, c)
if os.path.exists(sfile):
cinfo.sample = os.path.basename(sfile)
#all_samples.append((module, command, sfile))
cinfo.key = c
minfo.queries[c] = cinfo
minfo.aliases = {}
for (c,cinfo) in sorted(root.aliases.iteritems()):
if module in cinfo.info.plugin:
cinfo.key = c
minfo.aliases[c] = cinfo
minfo.paths = {}
for (c,cinfo) in sorted(root.paths.iteritems()):
if module in cinfo.info.plugin:
cinfo.key = c
minfo.paths[c] = cinfo
hash['module'] = minfo
i=i+1
log_debug('Processing module: %d of %d [%s]'%(i, len(root.plugins), module))
template = env.from_string(module_template)
render_template(hash, template, '%s/%s.md'%(out_base_path, module))
hash = root.get_hash()
template = env.from_string(index_template)
render_template(hash, template, '%s/docs/reference/index.md'%output_dir)
log_debug('%s/samples/index.rst'%output_dir)
template = env.from_string(samples_template)
render_template(hash, template, '%s/samples/index.rst'%output_dir)
def main(self, args):
parser = OptionParser(prog="")
parser.add_option("-o", "--output", help="write report to FILE(s)")
parser.add_option("-i", "--input", help="Reference folder")
(options, args) = parser.parse_args(args=args)
self.folder = options.output
self.generate_rst(options.input, options.output)
def __main__(args):
global helper
helper.main(args);
return 0
def init(plugin_id, plugin_alias, script_alias):
global helper
helper = DocumentationHelper(plugin_id, plugin_alias, script_alias)
def shutdown():
global helper
helper = None