Navigation


source: freeDiameter/contrib/tools/csv_to_fd @ 1463:8f6c77f24b1a

Last change on this file since 1463:8f6c77f24b1a was 1463:8f6c77f24b1a, checked in by Luke Mewburn <luke@mewburn.net>, 2 years ago

csv_to_fd: add QoSFilterRule. style fixes

Support derived type QoSFilterRule from RFC 7155 section 4.1.1
Minor code refactor.
Expand comments, removing UTF-8 chars and encoding requirement.

  • Property exe set to *
File size: 14.2 KB
Line 
1#!/usr/bin/env python
2
3"""
4Convert CSV files containing RADIUS or Diameter AVP tables
5into various formats.
6
7Format of the CSV files is one of:
8- Row per 3GPP AVP tables:
9    Name, Code, Section, DataType, Must, May, ShouldNot, MustNot [, ...]
10    - Name:
11        AVP Name. String, validated as ALPHA *(ALPHA / DIGIT / "-")
12        per RFC 6733 section 3.2.
13    - Code:
14        AVP Code. Integer, 0..4294967295.
15    - Section:
16        Section in relevant standard. String.
17    - DataType:
18        AVP Data Type. String, validated per basic and derived types in:
19            - RFC 6733 section 4.2
20            - RFC 6733 section 4.3
21            - RFC 7155 section 4.1
22    - Must, May, ShouldNot, MustNot:
23        Flags, possibly comma or space separated: M, V
24
25- Comment row. First cell:
26    # Comment text      Comment text
27    #=                  Header row of ====
28    #                   Blank line
29
30- Parameter row:
31    @Parameter,Value [, ...]
32  Supported Parameter terms:
33    standard    Standard name. E.g. '3GPP TS 29.272', 'RFC 6733'.
34    vendor      Vendor number.
35
36"""
37
38from __future__ import print_function
39from __future__ import with_statement
40
41import abc
42import csv
43import collections
44import json
45import re
46import optparse
47import sys
48
49CSV_COLUMN_NAMES = [
50    'name',
51    'code',
52    'section',
53    'datatype',
54    'must',
55    'may',
56    'shouldnot',
57    'mustnot',
58    'encrypt',
59]
60
61DERIVED_TO_BASE = {
62    'Address':          'OctetString',  # RFC 6733 section 4.3.1
63    'Time':             'OctetString',  # RFC 6733 section 4.3.1
64    'UTF8String':       'OctetString',  # RFC 6733 section 4.3.1
65    'DiameterIdentity': 'OctetString',  # RFC 6733 section 4.3.1
66    'DiameterURI':      'OctetString',  # RFC 6733 section 4.3.1
67    'Enumerated':       'Integer32',    # RFC 6733 section 4.3.1
68    'IPFilterRule':     'OctetString',  # RFC 6733 section 4.3.1
69    'QoSFilterRule':    'OctetString',  # RFC 7155 section 4.1.1
70}
71
72# See https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers
73VENDOR_TO_NAME = {
74    0:      '',
75    193:    'Ericsson',
76    8164:   'Starent',
77    10415:  '3GPP',
78}
79
80
81class Avp(object):
82    """Store an AVP row."""
83
84    # Regex to validate avp-name per RFC 6733 section 3.2,
85    # with changes:
86    # - Allow avp-name to start with numbers (for 3GPP)
87    # - Allow '.' in avp-name, for existing dict_dcca_3gpp usage.
88# TODO: if starts with digit, ensure contains a letter somewhere?
89    _name_re = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9-\.]*$')
90
91    __slots__ = CSV_COLUMN_NAMES + [
92        'filename', 'linenum', 'standard', 'vendor', ]
93
94    def __init__(self, name, code, section, datatype,
95                 must, may, shouldnot, mustnot, encrypt,
96                 filename=None, linenum=0, standard=None, vendor=0):
97        # Members from CSV row
98        self.name = name
99        self.code = int(code)
100        self.section = section
101        self.datatype = datatype
102        self.must = must
103        self.may = may
104        self.shouldnot = shouldnot
105        self.mustnot = mustnot
106        self.encrypt = encrypt
107        # Members from file state
108        self.filename = filename
109        self.linenum = linenum
110        self.standard = standard
111        self.vendor = vendor
112        # Validate CSV fields
113        if not self._name_re.match(self.name):
114            raise ValueError('Invalid AVP name "{}"'.format(self.name))
115        if (self.code < 0 or self.code > 4294967295):
116            raise ValueError('Invalid AVP code {}'.format(self.code))
117        if (self.datatype not in (
118                'OctetString', 'Integer32', 'Integer64', 'Unsigned32',
119                'Unsigned64', 'Float32', 'Float64', 'Grouped')
120                and self.datatype not in DERIVED_TO_BASE):
121            raise ValueError('Invalid AVP data type "{}"'.format(
122                self.datatype))
123# TODO: validate must, may, shouldnot, mustnot
124
125    @property
126    def __dict__(self):
127        return {s: getattr(self, s) for s in self.__slots__}
128
129
130class Processor(object):
131    """Interface for processor of Avp"""
132
133    __metaclass__ = abc.ABCMeta
134
135    @classmethod
136    def cls_name(cls):
137        """Return the name, lower-case, without "processor" suffix."""
138        suffix = 'processor'
139        name = cls.__name__.lower()
140        if name.endswith(suffix):
141            return name[:-len(suffix)]
142        return name
143
144    @classmethod
145    def cls_desc(cls):
146        """Return the first line of the docstring."""
147        if cls.__doc__ is None:
148            return ""
149        return cls.__doc__.split('\n')[0]
150
151    @abc.abstractmethod
152    def next_file(self, filename):
153        """Called when a file is opened."""
154        pass
155
156    @abc.abstractmethod
157    def avp(self, avp):
158        """Process a validated Avp."""
159        pass
160
161    @abc.abstractmethod
162    def comment(self, comment, filename, linenum):
163        """Process a comment row:
164            #comment,
165        """
166        pass
167
168    @abc.abstractmethod
169    def generate(self):
170        """Invoked after all rows processed."""
171        pass
172
173
174class DebugProcessor(Processor):
175    """Display the CSV parsing"""
176
177    def next_file(self, filename):
178        print('File: {}'.format(filename))
179
180    def avp(self, avp):
181        avpdict = vars(avp)
182        print('AVP: {name}, {code}, {datatype}'.format(**avpdict))
183
184    def comment(self, comment, filename, linenum):
185        print('Comment: {}'.format(comment))
186
187    def generate(self):
188        print('Generate')
189
190
191class NoopProcessor(Processor):
192    """Validate the CSV; no other output"""
193
194    def next_file(self, filename):
195        pass
196
197    def avp(self, avp):
198        pass
199
200    def comment(self, comment, filename, linenum):
201        pass
202
203    def generate(self):
204        pass
205
206
207class FdcProcessor(Processor):
208    """Generate freeDiameter C code
209
210    Comment cells are parsed as:
211        # text comment  /* text comment */
212        #=              /*==============*/
213        #               [blank line]
214    """
215
216    COMMENT_WIDTH = 64
217
218    def __init__(self):
219        self.lines = []
220
221    def next_file(self, filename):
222        print('/* CSV file: {} */'.format(filename))
223
224    def avp(self, avp):
225        comment = '{name}, {datatype}, code {code}'.format(**vars(avp))
226        if '' != avp.section:
227            comment += ', section {}'.format(avp.section)
228        self.add_comment(comment)
229        self.add('\t{')
230        self.add('\t\tstruct dict_avp_data data = {')
231# TODO: remove comments?
232        self.add('\t\t\t{},\t/* Code */'.format(avp.code))
233        self.add('\t\t\t{},\t/* Vendor */'.format(avp.vendor))
234        self.add('\t\t\t\"{}\",\t/* Name */'.format(avp.name))
235        self.add('\t\t\t{},\t/* Fixed flags */'.format(
236            self.build_flags(', '.join([avp.must, avp.mustnot]))))
237        self.add('\t\t\t{},\t/* Fixed flag values */'.format(
238            self.build_flags(avp.must)))
239# TODO: add trailing comma?
240        self.add('\t\t\tAVP_TYPE_{}\t/* base type of data */'.format(
241            DERIVED_TO_BASE.get(avp.datatype, avp.datatype).upper()))
242        self.add('\t\t};')
243        avp_type = 'NULL'
244        if 'Enumerated' == avp.datatype:
245            self.add('\t\tstruct dict_object\t*type;')
246            vendor_prefix = ''
247            if avp.vendor != 0:
248                vendor_prefix = '{}/'.format(VENDOR_TO_NAME[avp.vendor])
249            self.add(
250                '\t\tstruct dict_type_data\t tdata = {{ AVP_TYPE_INTEGER32, '
251                '"Enumerated({prefix}{name})", NULL, NULL, NULL }};'.format(
252                    prefix=vendor_prefix, name=avp.name))
253# XXX: add enumerated values
254            self.add('\t\tCHECK_dict_new(DICT_TYPE, &tdata, NULL, &type);')
255            avp_type = "type"
256        elif avp.datatype in DERIVED_TO_BASE:
257            avp_type = '{}_type'.format(avp.datatype)
258        self.add('\t\tCHECK_dict_new(DICT_AVP, &data, {}, NULL);'.format(
259            avp_type))
260# TODO: remove ; on scope brace
261        self.add('\t};')
262        self.add('')
263
264    def comment(self, comment, filename, linenum):
265        if '' == comment:
266            self.add('')
267        elif '=' == comment:
268            self.add_header()
269        elif comment.startswith(' '):
270            self.add_comment(comment[1:])
271        else:
272            raise ValueError('Unsupported comment "{}"'.format(comment))
273
274    def generate(self):
275        self.print_header()
276        self.print_comment('Start of generated data.')
277        self.print_comment('')
278        self.print_comment('The following is created automatically with:')
279        self.print_comment('    csv_to_fd -p {}'.format(self.cls_name()))
280        self.print_comment('Changes will be lost during the next update.')
281        self.print_comment('Do not modify;'
282                           ' modify the source .csv file instead.')
283        self.print_header()
284        print('')
285        print('\n'.join(self.lines))
286        self.print_header()
287        self.print_comment('End of generated data.')
288        self.print_header()
289
290    def build_flags(self, flags):
291        result = []
292        if 'V' in flags:
293            result.append('AVP_FLAG_VENDOR')
294        if 'M' in flags:
295            result.append('AVP_FLAG_MANDATORY')
296        return ' |'.join(result)
297
298    def add(self, line):
299        self.lines.append(line)
300
301    def add_comment(self, comment):
302        self.lines.append(self.format_comment(comment))
303
304    def add_header(self):
305        self.lines.append(self.format_header())
306
307    def format_comment(self, comment):
308        return '\t/* {:<{width}} */'.format(comment, width=self.COMMENT_WIDTH)
309
310    def format_header(self):
311        return '\t/*={:=<{width}}=*/'.format('', width=self.COMMENT_WIDTH)
312
313    def print_comment(self, comment):
314        print(self.format_comment(comment))
315
316    def print_header(self):
317        print(self.format_header())
318
319
320class JsonProcessor(Processor):
321    """Generate freeDiameter JSON object
322    """
323
324    def __init__(self):
325        self.avps = []
326
327    def next_file(self, filename):
328        pass
329
330    def avp(self, avp):
331        flags = collections.OrderedDict([
332            ('Must',    self.build_flags(avp.must)),
333            ('MustNot', self.build_flags(avp.mustnot)),
334        ])
335        row = collections.OrderedDict([
336            ('Code',    avp.code),
337            ('Flags',   flags),
338            ('Name',    avp.name),
339            ('Type',    avp.datatype),
340            ('Vendor',  avp.vendor),
341        ])
342        self.avps.append(row)
343
344    def comment(self, comment, filename, linenum):
345        pass
346
347    def generate(self):
348        doc = {"AVPs": self.avps}
349        print(json.dumps(doc, indent=2))
350
351    def build_flags(self, flags):
352        result = []
353        if 'V' in flags:
354            result.append('V')
355        if 'M' in flags:
356            result.append('M')
357        return ''.join(result)
358
359
360def main():
361
362    # Build dict of name: NameProcessor
363    processors = {
364        cls.cls_name(): cls
365        for cls in Processor.__subclasses__()
366        }
367
368    # Build Processor name to desc
369    processor_help = '\n'.join(
370        ['  {:8} {}'.format(key, processors[key].cls_desc())
371         for key in sorted(processors)])
372
373    # Custom OptionParser with improved help
374    class MyParser(optparse.OptionParser):
375        """Custom OptionParser without epilog formatting."""
376        def format_help(self, formatter=None):
377            return """\
378{}
379Supported PROCESSOR options:
380{}
381""".format(
382                optparse.OptionParser.format_help(self, formatter),
383                processor_help)
384
385    # Parse options
386    parser = MyParser(
387        description="""\
388Convert CSV files containing RADIUS or Diameter AVP tables
389into various formats using the specified processor PROCESSOR.
390""")
391
392    parser.add_option(
393        '-p', '--processor',
394        default='noop',
395        help='AVP processor. One of: {}. [%default]'.format(
396             ', '.join(processors.keys())))
397    (opts, args) = parser.parse_args()
398    if len(args) < 1:
399        parser.error('Incorrect number of arguments')
400
401    # Find processor
402    try:
403        avpproc = processors[opts.processor]()
404    except KeyError as e:
405        parser.error('Unknown processor "{}"'.format(opts.processor))
406
407    # dict of [vendor][code] : Avp
408    avp_codes = collections.defaultdict(dict)
409
410    # Process files
411    for filename in args:
412        avpproc.next_file(filename)
413        with open(filename, 'r') as csvfile:
414            csvdata = csv.DictReader(csvfile, CSV_COLUMN_NAMES)
415            linenum = 0
416            standard = ''
417            vendor = 0
418            for row in csvdata:
419                linenum += 1
420                try:
421                    if row['name'] in (None, '', 'Attribute Name'):
422                        continue
423                    elif row['name'].startswith('#'):
424                        comment = row['name'][1:]
425                        avpproc.comment(comment, filename, linenum)
426                    elif row['name'].startswith('@'):
427                        parameter = row['name'][1:]
428                        value = row['code']
429                        if False:
430                            pass
431                        elif 'standard' == parameter:
432                            standard = value
433                        elif 'vendor' == parameter:
434                            vendor = int(value)
435                        else:
436                            raise ValueError('Unknown parameter "{}"'.format(
437                                parameter))
438                    else:
439                        avp = Avp(filename=filename, linenum=linenum,
440                                  standard=standard, vendor=vendor,
441                                  **row)
442                        # Ensure AVP vendor/code not already defined
443                        if avp.code in avp_codes[avp.vendor]:
444                            conflict = avp_codes[avp.vendor][avp.code]
445                            raise ValueError(
446                                'AVP vendor {} code {} already present'
447                                ' in file "{}" line {}'.format(
448                                    avp.vendor, avp.code,
449                                    conflict.filename, conflict.linenum))
450                        avp_codes[avp.vendor][avp.code] = avp
451                        # Process AVP
452                        avpproc.avp(avp)
453                except ValueError as e:
454                    sys.stderr.write('CSV file "{}" line {}: {}: {}\n'.format(
455                        filename, linenum, e.__class__.__name__, e))
456                    sys.exit(1)
457
458    # Generate result
459    avpproc.generate()
460
461
462if '__main__' == __name__:
463    main()
464
465# vim: set et sw=4 sts=4 :
Note: See TracBrowser for help on using the repository browser.