1#!/usr/bin/env python
2
3import argparse
4import logging
5import json
6import os
7from   pprint import pprint
8import re
9import sys
10
11def touch(filename):
12    open(filename, 'a').close()
13
14class daemonName:
15    def __init__( self ):
16        table = {
17            "Director": "Dir",
18            "StorageDaemon": "Sd",
19            "FileDaemon": "Fd"
20        }
21
22    @staticmethod
23    def getLong( string ):
24        if string.lower() == "director" or string.lower() == "dir":
25            return "Director"
26        elif string.lower() == "storagedaemon" or string.lower() == "sd":
27            return "StorageDaemon"
28        elif string.lower() == "filedaemon" or string.lower() == "fd":
29            return "FileDaemon"
30
31    @staticmethod
32    def getShort( string ):
33        if string.lower() == "bareos-dir" or string.lower() == "director" or string.lower() == "dir":
34            return "Dir"
35        elif string.lower() == "bareos-sd" or string.lower() == "storagedaemon" or string.lower() == "sd":
36            return "Sd"
37        elif string.lower() == "bareos-fd" or string.lower() == "filedaemon" or string.lower() == "fd":
38            return "Fd"
39        elif string.lower() == "bareos-console" or string.lower() == "bconsole" or string.lower() == "con":
40            return "Console"
41        elif string.lower() == "bareos-tray-monitor":
42            return "Console"
43
44    @staticmethod
45    def getLowShort(string):
46        return daemonName.getShort(string).lower()
47
48
49class BareosConfigurationSchema:
50    def __init__( self, json ):
51        self.format_version_min = 2
52        self.logger = logging.getLogger()
53        self.json = json
54        try:
55            self.format_version = json['format-version']
56        except KeyError as e:
57            raise
58        logger.info( "format-version: " + str(self.format_version) )
59        if self.format_version < self.format_version_min:
60            raise RuntimeError( "format-version is " + str(self.format_version) + ". Required: >= " + str(self.format_version_min) )
61
62    def open(self, filename = None, mode = 'r'):
63        if filename:
64            self.out=open( filename, mode )
65        else:
66            self.out=sys.stdout
67
68    def close(self):
69        if self.out != sys.stdout:
70            self.out.close()
71
72    def getDaemons(self):
73        return sorted(filter( None, self.json["resource"].keys()))
74
75    def getDatatypes(self):
76        try:
77            return sorted(filter( None, self.json["datatype"].keys()) )
78        except KeyError:
79            return
80
81    def convertCamelCase2Spaces(self, valueCC):
82        s1 = re.sub('([a-z0-9])([A-Z])', r'\1 \2', valueCC)
83        result=[]
84        for token in s1.split(' '):
85            u = token.upper()
86            # TODO: add LAN
87            if u in [ "ACL", "CA", "CN", "DB", "DH", "FD", "LMDB", "NDMP", "PSK", "SD", "SSL", "TLS", "VSS" ]:
88                token=u
89            result.append(token)
90        return " ".join( result )
91
92    def getDatatype(self, name):
93        return self.json["datatype"][name]
94
95    def getResources(self, daemon):
96        return sorted(filter( None, self.json["resource"][daemon].keys()) )
97
98    def getResource(self, daemon, resourcename):
99        return self.json["resource"][daemon][resourcename]
100
101    def getDefaultValue(self, data):
102        default=""
103        if data.get('default_value'):
104            default=data.get('default_value')
105            if data.get('platform_specific'):
106                default+=" (platform specific)"
107        return default
108
109    def getConvertedResources(self, daemon):
110        result = ""
111        for i in self.getResources(daemon):
112            result += i + "\n"
113        return result
114
115    def getResourceDirectives(self, daemon, resourcename):
116        return sorted(filter( None, self.getResource(daemon, resourcename).keys()) )
117
118    def getResourceDirective(self, daemon, resourcename, directive, deprecated=None):
119        # TODO:
120        # deprecated:
121        #   None:  include deprecated
122        #   False: exclude deprecated
123        #   True:  only deprecated
124        return BareosConfigurationSchemaDirective( self.json["resource"][daemon][resourcename][directive] )
125
126    def getConvertedResourceDirectives(self, daemon, resourcename):
127        # OVERWRITE
128        return None
129
130    def writeResourceDirectives(self, daemon, resourcename, filename=None):
131        self.open(filename, "w")
132        self.out.write(self.getConvertedResourceDirectives(daemon, resourcename))
133        self.close()
134
135    def getStringsWithModifiers(self, text, strings):
136        strings['text']=strings[text]
137        if strings[text]:
138            if strings.get('mo'):
139                return "%(mo)s%(text)s%(mc)s" % ( strings )
140            else:
141                return "%(text)s" % ( strings )
142        else:
143            return ""
144
145
146class BareosConfigurationSchemaDirective(dict):
147
148    def getDefaultValue( self ):
149        default=None
150        if dict.get( self, 'default_value' ):
151            if (dict.get( self, 'default_value' ) == "true") or (dict.get( self, 'default_value' ) == "on"):
152                default="yes"
153            elif (dict.get( self, 'default_value' ) == "false") or (dict.get( self, 'default_value' ) == "off"):
154                default="no"
155            else:
156                default=dict.get( self, 'default_value' )
157        return default
158
159    def getStartVersion( self ):
160        if dict.get( self, 'versions' ):
161            version = dict.get( self, 'versions' ).partition("-")[0]
162            if version:
163                return version
164
165    def getEndVersion( self ):
166        if dict.get( self, 'versions' ):
167            version = dict.get( self, 'versions' ).partition("-")[2]
168            if version:
169                return version
170
171    def get(self, key, default=None):
172        if key == "default_value" or key == "default":
173            return self.getDefaultValue()
174        elif key == "start_version":
175            if self.getStartVersion():
176                return self.getStartVersion()
177        elif key == "end_version":
178            if self.getEndVersion():
179                return self.getEndVersion()
180        return dict.get(self, key, default)
181
182
183class BareosConfigurationSchema2Latex(BareosConfigurationSchema):
184
185    def getConvertedResources(self, daemon):
186        result = "\\begin{itemize}\n"
187        for i in self.getResources(daemon):
188            if i:
189                result += "  \\item " + i + "\n"
190        result += "\\end{itemize}\n"
191        return result
192
193    def getLatexDatatypeRef( self, datatype ):
194        DataType="".join([x.capitalize() for x in datatype.split('_')])
195        return "\\dt{%(DataType)s}" % { 'DataType': DataType }
196
197    def getLatexDefaultValue( self, data ):
198        default=""
199        if data.get( 'default_value' ):
200            default=data.get( 'default_value' )
201            if data.get( 'platform_specific' ):
202                default+=" \\textit{\\small(platform specific)}"
203        return default
204
205    def getLatexDescription(self, data):
206        description = ""
207        if data.get('description'):
208            description = data.get('description').replace('_','\_')
209        return description
210
211    def getLatexTable(self, subtree, latexDefine="define%(key)s", latexLink="\\hyperlink{key%(key)s}{%(key)s}" ):
212        result="\\begin{center}\n"
213        result+="\\begin{longtable}{ l | l | l | l }\n"
214        result+="\\hline \n"
215        result+="\\multicolumn{1}{ c|}{\\textbf{%(name)-80s}} &\n" % { 'name': "configuration directive name" }
216        result+="\\multicolumn{1}{|c|}{\\textbf{%(type)-80s}} &\n" % { 'type': "type of data" }
217        result+="\\multicolumn{1}{|c|}{\\textbf{%(default)-80s}} &\n" % { 'default': "default value" }
218        result+="\\multicolumn{1}{|c }{\\textbf{%(remark)-80s}} \\\\ \n" % { 'remark': "remark" }
219        result+="\\hline \n"
220        result+="\\hline \n"
221
222        for key in sorted(filter( None, subtree.keys() ) ):
223            data=BareosConfigurationSchemaDirective(subtree[key])
224
225            strings={
226                'key': self.convertCamelCase2Spaces( key ),
227                'mc': "}",
228                'extra': [],
229                'default': self.getLatexDefaultValue( data ),
230            }
231
232            strings['directive_link'] = latexLink % strings
233
234            strings["datatype"] = self.getLatexDatatypeRef( data['datatype'] )
235            if data.get( 'equals' ):
236                strings["datatype"]="= %(datatype)s" % { 'datatype': strings["datatype"] }
237            else:
238                strings["datatype"]="\{ %(datatype)s \}" % { 'datatype': strings["datatype"] }
239
240            extra=[]
241            if data.get( 'alias' ):
242                extra.append("alias")
243                strings["mo"]="\\textit{"
244            if data.get( 'deprecated' ):
245                extra.append("deprecated")
246                strings["mo"]="\\textit{"
247            if data.get( 'required' ):
248                extra.append("required")
249                strings["mo"]="\\textbf{"
250            strings["extra"]=", ".join(extra)
251
252            define = "\\csgdef{resourceDirectiveDefined" + latexDefine + "}{yes}"
253            strings['define']= define % strings
254            strings["t_directive"] = self.getStringsWithModifiers( "directive_link", strings )
255            strings["t_datatype"] = self.getStringsWithModifiers( "datatype", strings )
256            strings["t_default"] = self.getStringsWithModifiers( "default", strings )
257            strings["t_extra"] = self.getStringsWithModifiers( "extra", strings )
258
259            result+="%(define)-80s\n" % ( strings )
260            result+="%(t_directive)-80s &\n" % ( strings )
261            result+="%(t_datatype)-80s &\n" % ( strings )
262            result+="%(t_default)-80s &\n" % ( strings )
263            result+="%(t_extra)s\n" % ( strings )
264            result+="\\\\ \n\n" % ( strings )
265
266        result+="\\hline \n"
267        result+="\\end{longtable}\n"
268        result+="\\end{center}\n"
269        result+="\n"
270        return result
271
272    def writeResourceDirectivesTable(self, daemon, resourcename, filename=None):
273        ds=daemonName.getShort(daemon)
274        self.open(filename, "w")
275        self.out.write( self.getLatexTable( self.json["resource"][daemon][resourcename], latexDefine=ds+resourcename+"%(key)s", latexLink="\\linkResourceDirective*{"+ds+"}{"+resourcename+"}{%(key)s}" ) )
276        self.close()
277
278    def writeDatatypeOptionsTable(self, filename=None):
279        self.open(filename, "w")
280        self.out.write(self.getLatexTable(self.getDatatype( "OPTIONS" )["values"], latexDefine="DatatypeOptions%(key)s" )
281        )
282        self.close()
283
284    def getConvertedResourceDirectives(self, daemon, resourcename):
285        result="\\begin{description}\n\n"
286        for directive in self.getResourceDirectives(daemon, resourcename):
287            data=self.getResourceDirective(daemon, resourcename, directive)
288
289            strings={
290                'daemon': daemonName.getShort( daemon ),
291                'resource': resourcename,
292                'directive': self.convertCamelCase2Spaces( directive ),
293                'datatype': self.getLatexDatatypeRef( data['datatype'] ),
294                'default': self.getLatexDefaultValue( data ),
295                'version': data.get( 'start_version', "" ),
296                'description': self.getLatexDescription(data),
297                'required': '',
298            }
299
300            if data.get( 'alias' ):
301                if not strings['description']:
302                    strings['description']="\\textit{This directive is an alias.}"
303            if data.get( 'deprecated' ):
304                # overwrites start_version
305                strings['version']="deprecated"
306            if data.get( 'required' ):
307                strings['required']="required"
308
309            result+="\\resourceDirective{%(daemon)s}{%(resource)s}{%(directive)s}{%(datatype)s}{%(required)s}{%(default)s}{%(version)s}{%(description)s}\n\n" % ( strings )
310        result+="\\end{description}\n\n"
311        return result
312
313    def writeResourceDirectives(self, daemon, resourcename, filename=None):
314        self.open(filename, "w")
315        self.out.write( self.getConvertedResourceDirectives( daemon, resourcename ) )
316        self.close()
317
318    def getResourceDirectiveDefs(self, daemon, resourcename):
319        result=""
320        for directive in self.getResourceDirectives(daemon, resourcename):
321            data=self.getResourceDirective(daemon, resourcename, directive)
322
323            strings={
324                'daemon': daemonName.getShort( daemon ),
325                'resource': resourcename,
326                'directive': self.convertCamelCase2Spaces( directive ),
327            }
328
329            result+="\\defDirective{%(daemon)s}{%(resource)s}{%(directive)s}{}{}{%%\n" % ( strings )
330            result+="}\n\n"
331        return result
332
333    def writeResourceDirectiveDefs(self, daemon, resourcename, filename=None):
334        self.open(filename, "w")
335        self.out.write( self.getResourceDirectiveDefs( daemon, resourcename ) )
336        self.close()
337
338
339class BareosConfigurationSchema2Sphinx(BareosConfigurationSchema):
340
341    def indent(self, text, amount, ch=' '):
342        padding = amount * ch
343        return ''.join(padding+line for line in text.splitlines(True))
344
345    def getLatexDatatypeRef( self, datatype ):
346        DataType="".join([x.capitalize() for x in datatype.split('_')])
347        return "\\dt{%(DataType)s}" % { 'DataType': DataType }
348
349    def getDefaultValue( self, data ):
350        default=""
351        if data.get( 'default_value' ):
352            default=data.get( 'default_value' )
353            if data.get( 'platform_specific' ):
354                default+=" *(platform specific)*"
355        return default
356
357    def getDescription(self, data):
358        description = ""
359        if data.get('description'):
360            description = self.indent(data.get('description'), 3)
361            #.replace('_','\_')
362        return description
363
364    def getConvertedResourceDirectives(self, daemon, resourcename):
365        logger = logging.getLogger()
366
367        result = ''
368        # only useful, when file is included by toctree.
369        #result='{}\n{}\n\n'.format(resourcename, len(resourcename) * '-')
370        for directive in self.getResourceDirectives(daemon,resourcename):
371            data=self.getResourceDirective(daemon, resourcename, directive)
372
373            strings={
374                'program': daemon,
375                'daemon': daemonName.getLowShort(daemon),
376                'resource': resourcename.lower(),
377                'directive': directive ,
378                'datatype': data['datatype'],
379                'default': self.getDefaultValue( data ),
380                'version': data.get( 'start_version', "" ),
381                'description': self.getDescription(data),
382                'required': '',
383            }
384
385            if data.get( 'alias' ):
386                if not strings['description']:
387                    strings['description']="   *This directive is an alias.*"
388
389            if data.get( 'deprecated' ):
390                # overwrites start_version
391                strings['version']="deprecated"
392
393            includefilename = '/manually_added_config_directive_descriptions/{daemon}-{resource}-{directive}.rst.inc'.format(**strings)
394
395
396            result += '.. config:option:: {daemon}/{resource}/{directive}\n\n'.format(**strings)
397
398            if data.get( 'required' ):
399                strings['required']="True"
400                result += '   :required: {required}\n'.format(**strings)
401
402            result += '   :type: {datatype}\n'.format(**strings)
403
404            if data.get( 'default_value' ):
405                result += '   :default: {default}\n'.format(**strings)
406
407            if strings.get('version'):
408                result += '   :version: {version}\n'.format(**strings)
409
410            result += '\n'
411
412            if strings['description']:
413                result += strings['description'] + '\n\n'
414
415            # make sure, file exists, so that there are no problems with include.
416            checkincludefilename = 'source/{}'.format(includefilename)
417            if not os.path.exists(checkincludefilename):
418                touch(checkincludefilename)
419
420            result += '   .. include:: {}\n\n'.format(includefilename)
421
422            result += '\n\n'
423
424        return result
425
426
427    def getHeader(self):
428        result  = '.. csv-table::\n'
429        result += '   :header: '
430        result += self.getHeaderColumns()
431        result += '\n\n'
432        return result
433
434    def getHeaderColumns(self):
435        columns = [
436            "configuration directive name",
437            "type of data",
438            "default value",
439            "remark"
440        ]
441
442        return '"{}"'.format('", "'.join(columns))
443
444
445    def getRows(self, daemon, resourcename, subtree, link):
446        result =''
447        for key in sorted(filter( None, subtree.keys() ) ):
448            data=BareosConfigurationSchemaDirective(subtree[key])
449
450            strings={
451                'key': self.convertCamelCase2Spaces( key ),
452                'extra': [],
453                'mc': '* ',
454                'default': self.getDefaultValue( data ),
455
456                'program': daemon,
457                'daemon': daemonName.getLowShort(daemon),
458                'resource': resourcename.lower(),
459                'directive': key,
460            }
461
462            strings['directive_link'] = link % strings
463
464            if data.get( 'equals' ):
465                strings["datatype"]="= %(datatype)s" % { 'datatype': data["datatype"] }
466            else:
467                strings["datatype"]="\{ %(datatype)s \}" % { 'datatype': data["datatype"] }
468
469            extra=[]
470            if data.get( 'alias' ):
471                extra.append("alias")
472                strings["mo"]="*"
473            if data.get( 'deprecated' ):
474                extra.append("deprecated")
475                strings["mo"]="*"
476            if data.get( 'required' ):
477                extra.append("required")
478                strings["mo"]="**"
479                strings["mc"]="** "
480            strings["extra"]=', '.join(extra)
481
482            strings["t_datatype"] = '"{}"'.format(self.getStringsWithModifiers( "datatype", strings ))
483            strings["t_default"] = '"{}"'.format(self.getStringsWithModifiers( "default", strings ))
484            strings["t_extra"] = '"{}"'.format(self.getStringsWithModifiers( "extra", strings ))
485
486            #result+='   %(directive_link)-60s, %(t_datatype)-20s, %(t_default)-20s, %(t_extra)s\n' % ( strings )
487            result+='   %(directive_link)-60s, %(t_datatype)s, %(t_default)s, %(t_extra)s\n' % ( strings )
488
489        return result
490
491
492    def getFooter(self):
493        result = "\n"
494        return result
495
496
497    def getTable(self, daemon, resourcename, subtree, link=':config:option:`%(daemon)s/%(resource)s/%(directive)s`\ ' ):
498        result  = self.getHeader()
499        result += self.getRows(daemon, resourcename, subtree, link)
500        result += self.getFooter()
501
502        return result
503
504    def writeResourceDirectivesTable(self, daemon, resourcename, filename=None):
505        ds=daemonName.getShort(daemon)
506        self.open(filename, "w")
507        self.out.write( self.getTable(daemon, resourcename, self.json["resource"][daemon][resourcename] ) )
508        self.close()
509
510
511
512def createLatex(data):
513    logger = logging.getLogger()
514
515    logger.info("Create LaTex files ...")
516
517    latex = BareosConfigurationSchema2Latex(data)
518
519    for daemon in latex.getDaemons():
520
521        #pprint(schema.getResources(daemon))
522        for resource in latex.getResources(daemon):
523            logger.info( "daemon: " + daemon + ", resource: " + resource )
524
525            #pprint(schema.getResource(daemon,resource))
526            latex.writeResourceDirectives(daemon, resource, "source/include/autogenerated/" + daemon.lower()+ "-resource-"+resource.lower()+"-description.tex")
527            latex.writeResourceDirectiveDefs(daemon, resource, "source/include/autogenerated/" + daemon.lower()+ "-resource-"+resource.lower()+"-defDirective.tex")
528            latex.writeResourceDirectivesTable(daemon, resource, "source/include/autogenerated/" + daemon.lower()+ "-resource-"+resource.lower()+"-table.tex")
529
530    if latex.getDatatypes():
531        print latex.getDatatypes()
532        #print schema.getDatatype( "OPTIONS" )
533        #print latex.getLatexTable( schema.getDatatype( "OPTIONS" )["values"], latexDefine="%(key)s", latexLink="\\linkResourceDirective{%(key)s}" )
534        latex.writeDatatypeOptionsTable( filename="source/include/autogenerated/datatype-options-table.tex" )
535
536
537
538def createSphinx(data):
539    logger = logging.getLogger()
540
541    logger.info("Create RST/Sphinx files ...")
542
543    sphinx = BareosConfigurationSchema2Sphinx(data)
544
545    for daemon in sphinx.getDaemons():
546
547        #pprint(schema.getResources(daemon))
548        for resource in sphinx.getResources(daemon):
549            logger.info( "daemon: " + daemon + ", resource: " + resource )
550
551            sphinx.writeResourceDirectives(daemon, resource, "source/include/autogenerated/" + daemon.lower()+ "-resource-"+resource.lower()+"-description.rst.inc")
552
553            sphinx.writeResourceDirectivesTable(daemon, resource, "source/include/autogenerated/" + daemon.lower()+ "-resource-"+resource.lower()+"-table.rst.inc")
554
555
556    #if sphinx.getDatatypes():
557    #    print sphinx.getDatatypes()
558
559
560
561if __name__ == '__main__':
562    logging.basicConfig(format='%(levelname)s %(module)s.%(funcName)s: %(message)s', level=logging.INFO)
563    logger = logging.getLogger()
564
565    parser = argparse.ArgumentParser()
566    parser.add_argument('-q', '--quiet', action='store_true', help="suppress logging output" )
567    parser.add_argument('-d', '--debug', action='store_true', help="enable debugging output" )
568    parser.add_argument('--latex', action='store_true', help="Create LaTex files." )
569    parser.add_argument('--sphinx', action='store_true', help="Create RST files for Sphinx." )
570    parser.add_argument("filename", help="json data file")
571    args = parser.parse_args()
572    if args.debug:
573        logger.setLevel(logging.DEBUG)
574
575    if args.quiet:
576        logger.setLevel(logging.CRITICAL)
577
578    with open(args.filename) as data_file:
579        data = json.load(data_file)
580    #pprint(data)
581
582    if not args.latex:
583        # default is sphinx
584        args.sphinx = True
585
586    if args.latex:
587        createLatex(data)
588
589    if args.sphinx:
590        createSphinx(data)
591