meta-flow: Autogenerate mf_field data structures.
[cascardo/ovs.git] / build-aux / extract-ofp-fields
1 #! /usr/bin/python
2
3 import sys
4 import os.path
5 import re
6
7 line = ""
8
9 # Maps from user-friendly version number to its protocol encoding.
10 VERSION = {"1.0": 0x01,
11            "1.1": 0x02,
12            "1.2": 0x03,
13            "1.3": 0x04,
14            "1.4": 0x05,
15            "1.5": 0x06}
16
17 TYPES = {"u8": 1,
18          "be16": 2,
19          "be32": 4,
20          "MAC": 6,
21          "be64": 8,
22          "IPv6": 16}
23
24 FORMATTING = {"decimal":            ("MFS_DECIMAL",      1, 8),
25               "hexadecimal":        ("MFS_HEXADECIMAL",  1, 8),
26               "Ethernet":           ("MFS_ETHERNET",     6, 6),
27               "IPv4":               ("MFS_IPV4",         4, 4),
28               "IPv6":               ("MFS_IPV6",        16,16),
29               "OpenFlow 1.0 port":  ("MFS_OFP_PORT",     2, 2),
30               "OpenFlow 1.1+ port": ("MFS_OFP_PORT_OXM", 4, 4),
31               "frag":               ("MFS_FRAG",         1, 1),
32               "tunnel flags":       ("MFS_TNL_FLAGS",    2, 2),
33               "TCP flags":          ("MFS_TCP_FLAGS",    2, 2)}
34
35 PREREQS = {"none": "MFP_NONE",
36           "ARP": "MFP_ARP",
37           "VLAN VID": "MFP_VLAN_VID",
38           "IPv4": "MFP_IPV4",
39           "IPv6": "MFP_IPV6",
40           "IPv4/IPv6": "MFP_IP_ANY",
41           "MPLS": "MFP_MPLS",
42           "TCP": "MFP_TCP",
43           "UDP": "MFP_UDP",
44           "SCTP": "MFP_SCTP",
45           "ICMPv4": "MFP_ICMPV4",
46           "ICMPv6": "MFP_ICMPV6",
47           "ND": "MFP_ND",
48           "ND solicit": "MFP_ND_SOLICIT",
49           "ND advert": "MFP_ND_ADVERT"}
50
51 # Maps a name prefix to an oxm_class.
52 # If a name matches more than one prefix, the longest one is used.
53 OXM_CLASSES = {"NXM_OF_": 0x0000,
54                "NXM_NX_": 0x0001,
55                "OXM_OF_": 0x8000,
56                "OXM_OF_PKT_REG": 0x8001}
57 def oxm_name_to_class(name):
58     prefix = ''
59     class_ = None
60     for p, c in OXM_CLASSES.iteritems():
61         if name.startswith(p) and len(p) > len(prefix):
62             prefix = p
63             class_ = c
64     return class_
65
66 def decode_version_range(range):
67     if range in VERSION:
68         return (VERSION[range], VERSION[range])
69     elif range.endswith('+'):
70         return (VERSION[range[:-1]], max(VERSION.values()))
71     else:
72         a, b = re.match(r'^([^-]+)-([^-]+)$', range).groups()
73         return (VERSION[a], VERSION[b])
74
75 def get_line():
76     global line
77     global line_number
78     line = input_file.readline()
79     line_number += 1
80     if line == "":
81         fatal("unexpected end of input")
82
83 n_errors = 0
84 def error(msg):
85     global n_errors
86     sys.stderr.write("%s:%d: %s\n" % (file_name, line_number, msg))
87     n_errors += 1
88
89 def fatal(msg):
90     error(msg)
91     sys.exit(1)
92
93 def usage():
94     argv0 = os.path.basename(sys.argv[0])
95     print '''\
96 %(argv0)s, for extracting OpenFlow field properties from meta-flow.h
97 usage: %(argv0)s INPUT
98   where INPUT points to lib/meta-flow.h in the source directory.
99 The output written to stdout is intended to be saved as lib/meta-flow.inc,
100 which lib/meta-flow.c \"#include\"s.\
101 ''' % {"argv0": argv0}
102     sys.exit(0)
103
104 def make_sizeof(s):
105     m = re.match(r'(.*) up to (.*)', s)
106     if m:
107         struct, member = m.groups()
108         return "offsetof(%s, %s)" % (struct, member)
109     else:
110         return "sizeof(%s)" % s
111
112 def parse_oxm(s, prefix, n_bytes):
113     if s == 'none':
114         return None
115
116     m = re.match('([A-Z0-9_]+)\(([0-9]+)\) since(?: OF(1\.[0-9]+) and)? v([12]\.[0-9]+)$', s)
117     if not m:
118         fatal("%s: syntax error parsing %s" % (s, prefix))
119         
120     name, code, of_version, ovs_version = m.groups()
121
122     class_ = oxm_name_to_class(name)
123     if class_ is None:
124         fatal("unknown OXM class for %s" % name)
125     header = ("NXM_HEADER(0x%04x, %s, %d)" % (class_, code, n_bytes))
126
127     if of_version:
128         if of_version not in VERSION:
129             fatal("%s: unknown OpenFlow version %s" % (name, of_version))
130         of_version_nr = VERSION[of_version]
131         if of_version_nr < VERSION['1.2']:
132             fatal("%s: claimed version %s predates OXM" % (name, of_version))
133     else:
134         of_version_nr = 0
135
136     return (header, name, of_version_nr, ovs_version)
137
138 def parse_field(mff, comment):
139     f = {'mff': mff}
140
141     # First line of comment is the field name.
142     m = re.match(r'"([^"]+)"(?:\s+\(aka "([^"]+)"\))?(?:\s+\(.*\))?\.', comment[0])
143     if not m:
144         fatal("%s lacks field name" % mff)
145     f['name'], f['extra_name'] = m.groups()
146
147     # Find the last blank line the comment.  The field definitions
148     # start after that.
149     blank = None
150     for i in range(len(comment)):
151         if not comment[i]:
152             blank = i
153     if not blank:
154         fatal("%s: missing blank line in comment" % mff)
155
156     d = {}
157     for key in ("Type", "Maskable", "Formatting", "Prerequisites",
158                 "Access", "Prefix lookup member",
159                 "OXM", "NXM", "OF1.0", "OF1.1"):
160         d[key] = None
161     for fline in comment[blank + 1:]:
162         m = re.match(r'([^:]+):\s+(.*)\.$', fline)
163         if not m:
164             fatal("%s: syntax error parsing key-value pair as part of %s"
165                   % (fline, mff))
166         key, value = m.groups()
167         if key not in d:
168             fatal("%s: unknown key" % key)
169         elif key == 'Code point':
170             d[key] += [value]
171         elif d[key] is not None:
172             fatal("%s: duplicate key" % key)
173         d[key] = value
174     for key, value in d.iteritems():
175         if not value and key not in ("OF1.0", "OF1.1",
176                                      "Prefix lookup member", "Notes"):
177             fatal("%s: missing %s" % (mff, key))
178
179     m = re.match(r'([a-zA-Z0-9]+)(?: \(low ([0-9]+) bits\))?$', d['Type'])
180     if not m:
181         fatal("%s: syntax error in type" % mff)
182     type_ = m.group(1)
183     if type_ not in TYPES:
184         fatal("%s: unknown type %s" % (mff, d['Type']))
185     f['n_bytes'] = TYPES[type_]
186     if m.group(2):
187         f['n_bits'] = int(m.group(2))
188         if f['n_bits'] > f['n_bytes'] * 8:
189             fatal("%s: more bits (%d) than field size (%d)"
190                   % (mff, f['n_bits'], 8 * f['n_bytes']))
191     else:
192         f['n_bits'] = 8 * f['n_bytes']
193
194     if d['Maskable'] == 'no':
195         f['mask'] = 'MFM_NONE'
196     elif d['Maskable'] == 'bitwise':
197         f['mask'] = 'MFM_FULLY'
198     else:
199         fatal("%s: unknown maskable %s" % (mff, d['Maskable']))
200
201     fmt = FORMATTING.get(d['Formatting'])
202     if not fmt:
203         fatal("%s: unknown format %s" % (mff, d['Formatting']))
204     if f['n_bytes'] < fmt[1] or f['n_bytes'] > fmt[2]:
205         fatal("%s: %d-byte field can't be formatted as %s"
206               % (mff, f['n_bytes'], d['Formatting']))
207     f['string'] = fmt[0]
208
209     f['prereqs'] = PREREQS.get(d['Prerequisites'])
210     if not f['prereqs']:
211         fatal("%s: unknown prerequisites %s" % (mff, d['Prerequisites']))
212
213     if d['Access'] == 'read-only':
214         f['writable'] = False
215     elif d['Access'] == 'read/write':
216         f['writable'] = True
217     else:
218         fatal("%s: unknown access %s" % (mff, d['Access']))
219
220     f['OF1.0'] = d['OF1.0']
221     if not d['OF1.0'] in (None, 'exact match', 'CIDR mask'):
222         fatal("%s: unknown OF1.0 match type %s" % (mff, d['OF1.0']))
223         
224     f['OF1.1'] = d['OF1.1']
225     if not d['OF1.1'] in (None, 'exact match', 'bitwise mask'):
226         fatal("%s: unknown OF1.1 match type %s" % (mff, d['OF1.1']))
227
228     f['OXM'] = parse_oxm(d['OXM'], 'OXM', f['n_bytes'])
229     f['NXM'] = parse_oxm(d['NXM'], 'NXM', f['n_bytes'])
230
231     f['prefix'] = d["Prefix lookup member"]
232
233     return f
234
235 def protocols_to_c(protocols):
236     if protocols == set(['of10', 'of11', 'oxm']):
237         return 'OFPUTIL_P_ANY'
238     elif protocols == set(['of11', 'oxm']):
239         return 'OFPUTIL_P_NXM_OF11_UP'
240     elif protocols == set(['oxm']):
241         return 'OFPUTIL_P_NXM_OXM_ANY'
242     elif protocols == set([]):
243         return 'OFPUTIL_P_NONE'
244     else:
245         assert False        
246
247 def extract_ofp_fields():
248     global line
249
250     fields = []
251
252     while True:
253         get_line()
254         if re.match('enum.*mf_field_id', line):
255             break
256
257     while True:
258         get_line()
259         first_line_number = line_number
260         here = '%s:%d' % (file_name, line_number)
261         if (line.startswith('/*')
262             or line.startswith(' *')
263             or line.startswith('#')
264             or not line
265             or line.isspace()):
266             continue
267         elif re.match('}', line) or re.match('\s+MFF_N_IDS', line):
268             break
269
270         # Parse the comment preceding an MFF_ constant into 'comment',
271         # one line to an array element.
272         line = line.strip()
273         if not line.startswith('/*'):
274             fatal("unexpected syntax between fields")
275         line = line[1:]
276         comment = []
277         end = False
278         while not end:
279             line = line.strip()
280             if line.startswith('*/'):
281                 get_line()
282                 break
283             if not line.startswith('*'):
284                 fatal("unexpected syntax within field")
285
286             line = line[1:]
287             if line.startswith(' '):
288                 line = line[1:]
289             if line.startswith(' ') and comment:
290                 continuation = True
291                 line = line.lstrip()
292             else:
293                 continuation = False
294
295             if line.endswith('*/'):
296                 line = line[:-2].rstrip()
297                 end = True
298             else:
299                 end = False
300
301             if continuation:
302                 comment[-1] += " " + line
303             else:
304                 comment += [line]
305             get_line()
306
307         # Drop blank lines at each end of comment.
308         while comment and not comment[0]:
309             comment = comment[1:]
310         while comment and not comment[-1]:
311             comment = comment[:-1]
312
313         # Parse the MFF_ constant(s).
314         mffs = []
315         while True:
316             m = re.match('\s+(MFF_[A-Z0-9_]+),?\s?$', line)
317             if not m:
318                 break
319             mffs += [m.group(1)]
320             get_line()
321         if not mffs:
322             fatal("unexpected syntax looking for MFF_ constants")
323
324         if len(mffs) > 1 or '<N>' in comment[0]:
325             for mff in mffs:
326                 # Extract trailing integer.
327                 m = re.match('.*[^0-9]([0-9]+)$', mff)
328                 if not m:
329                     fatal("%s lacks numeric suffix in register group" % mff)
330                 n = m.group(1)
331
332                 # Search-and-replace <N> within the comment,
333                 # and drop lines that have <x> for x != n.
334                 instance = []
335                 for x in comment:
336                     y = x.replace('<N>', n)
337                     if re.search('<[0-9]+>', y):
338                         if ('<%s>' % n) not in y:
339                             continue
340                         y = re.sub('<[0-9]+>', '', y)
341                     instance += [y.strip()]
342                 fields += [parse_field(mff, instance)]
343         else:
344             fields += [parse_field(mffs[0], comment)]
345         continue
346
347     input_file.close()
348
349     if n_errors:
350         sys.exit(1)
351
352     output = []
353     output += ["/* Generated automatically; do not modify!     "
354                "-*- buffer-read-only: t -*- */"]
355     output += [""]
356
357     for f in fields:
358         output += ["{"]
359         output += ["    %s," % f['mff']]
360         if f['extra_name']:
361             output += ["    \"%s\", \"%s\"," % (f['name'], f['extra_name'])]
362         else:
363             output += ["    \"%s\", NULL," % f['name']]
364         output += ["    %d, %d," % (f['n_bytes'], f['n_bits'])]
365
366         if f['writable']:
367             rw = 'true'
368         else:
369             rw = 'false'
370         output += ["    %s, %s, %s, %s,"
371                    % (f['mask'], f['string'], f['prereqs'], rw)]
372
373         nxm = f['NXM']
374         oxm = f['OXM']
375         if not nxm:
376             nxm = oxm
377         elif not oxm:
378             oxm = nxm
379         if nxm:
380             output += ["    %s, \"%s\"," % (nxm[0], nxm[1])]
381             output += ["    %s, \"%s\", %s," % (oxm[0], oxm[1], oxm[2])]
382         else:
383             output += ["    0, NULL, 0, NULL, 0, /* no NXM or OXM */"]
384
385         of10 = f['OF1.0']
386         of11 = f['OF1.1']
387         if f['mff'] in ('MFF_DL_VLAN', 'MFF_DL_VLAN_PCP'):
388             # MFF_DL_VLAN and MFF_DL_VLAN_PCP don't exactly correspond to
389             # OF1.1, nor do they have NXM or OXM assignments, but their
390             # meanings can be expressed in every protocol, which is the goal of
391             # this member.
392             protocols = set(["of10", "of11", "oxm"])
393         else:
394             protocols = set([])
395             if of10:
396                 protocols |= set(["of10"])
397             if of11:
398                 protocols |= set(["of11"])
399             if nxm or oxm:
400                 protocols |= set(["oxm"])
401
402         if f['mask'] == 'MFM_FULLY':
403             cidr_protocols = protocols.copy()
404             bitwise_protocols = protocols.copy()
405
406             if of10 == 'exact match':
407                 bitwise_protocols -= set(['of10'])
408                 cidr_protocols -= set(['of10'])
409             elif of10 == 'CIDR mask':
410                 bitwise_protocols -= set(['of10'])
411             else:
412                 assert of10 is None
413
414             if of11 == 'exact match':
415                 bitwise_protocols -= set(['of11'])
416                 cidr_protocols -= set(['of11'])
417             else:
418                 assert of11 in (None, 'bitwise mask')
419         else:
420             assert f['mask'] == 'MFM_NONE'
421             cidr_protocols = set([])
422             bitwise_protocols = set([])
423
424         output += ["    %s," % protocols_to_c(protocols)]
425         output += ["    %s," % protocols_to_c(cidr_protocols)]
426         output += ["    %s," % protocols_to_c(bitwise_protocols)]
427         
428         if f['prefix']:
429             output += ["    FLOW_U32OFS(%s)," % f['prefix']]
430         else:
431             output += ["    -1, /* not usable for prefix lookup */"]
432
433         output += ["},"]
434
435     if n_errors:
436         sys.exit(1)
437
438     return output
439
440
441 if __name__ == '__main__':
442     if '--help' in sys.argv:
443         usage()
444     elif len(sys.argv) != 2:
445         sys.stderr.write("exactly one non-option argument required; "
446                          "use --help for help\n")
447         sys.exit(1)
448     else:
449         global file_name
450         global input_file
451         global line_number
452         file_name = sys.argv[1]
453         input_file = open(file_name)
454         line_number = 0
455
456         for oline in extract_ofp_fields():
457             print oline
458