Merge remote-tracking branch 'origin/master' into ovn4
[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, 127),
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 into an (experimenter ID, class) pair, so:
52 #
53 #      - Standard OXM classes are written as (0, <oxm_class>)
54 #
55 #      - Experimenter OXM classes are written as (<oxm_vender>, 0xffff)
56 #
57 # If a name matches more than one prefix, the longest one is used.
58 OXM_CLASSES = {"NXM_OF_":        (0,          0x0000),
59                "NXM_NX_":        (0,          0x0001),
60                "OXM_OF_":        (0,          0x8000),
61                "OXM_OF_PKT_REG": (0,          0x8001),
62                "ONFOXM_ET_":     (0x4f4e4600, 0xffff),
63
64                # This is the experimenter OXM class for Nicira, which is the
65                # one that OVS would be using instead of NXM_OF_ and NXM_NX_
66                # if OVS didn't have those grandfathered in.  It is currently
67                # used only to test support for experimenter OXM, since there
68                # are barely any real uses of experimenter OXM in the wild.
69                "NXOXM_ET_":      (0x00002320, 0xffff)}
70
71
72 def oxm_name_to_class(name):
73     prefix = ''
74     class_ = None
75     for p, c in OXM_CLASSES.items():
76         if name.startswith(p) and len(p) > len(prefix):
77             prefix = p
78             class_ = c
79     return class_
80
81
82 def decode_version_range(range):
83     if range in VERSION:
84         return (VERSION[range], VERSION[range])
85     elif range.endswith('+'):
86         return (VERSION[range[:-1]], max(VERSION.values()))
87     else:
88         a, b = re.match(r'^([^-]+)-([^-]+)$', range).groups()
89         return (VERSION[a], VERSION[b])
90
91
92 def get_line():
93     global line
94     global line_number
95     line = input_file.readline()
96     line_number += 1
97     if line == "":
98         fatal("unexpected end of input")
99
100
101 n_errors = 0
102
103
104 def error(msg):
105     global n_errors
106     sys.stderr.write("%s:%d: %s\n" % (file_name, line_number, msg))
107     n_errors += 1
108
109
110 def fatal(msg):
111     error(msg)
112     sys.exit(1)
113
114
115 def usage():
116     argv0 = os.path.basename(sys.argv[0])
117     print('''\
118 %(argv0)s, for extracting OpenFlow field properties from meta-flow.h
119 usage: %(argv0)s INPUT [--meta-flow | --nx-match]
120   where INPUT points to lib/meta-flow.h in the source directory.
121 Depending on the option given, the output written to stdout is intended to be
122 saved either as lib/meta-flow.inc or lib/nx-match.inc for the respective C
123 file to #include.\
124 ''' % {"argv0": argv0})
125     sys.exit(0)
126
127
128 def make_sizeof(s):
129     m = re.match(r'(.*) up to (.*)', s)
130     if m:
131         struct, member = m.groups()
132         return "offsetof(%s, %s)" % (struct, member)
133     else:
134         return "sizeof(%s)" % s
135
136
137 def parse_oxms(s, prefix, n_bytes):
138     if s == 'none':
139         return ()
140
141     return tuple(parse_oxm(s2.strip(), prefix, n_bytes) for s2 in s.split(','))
142
143
144 match_types = dict()
145
146
147 def parse_oxm(s, prefix, n_bytes):
148     global match_types
149
150     m = re.match('([A-Z0-9_]+)\(([0-9]+)\) since(?: OF(1\.[0-9]+) and)? v([12]\.[0-9]+)$', s)
151     if not m:
152         fatal("%s: syntax error parsing %s" % (s, prefix))
153
154     name, oxm_type, of_version, ovs_version = m.groups()
155
156     class_ = oxm_name_to_class(name)
157     if class_ is None:
158         fatal("unknown OXM class for %s" % name)
159     oxm_vendor, oxm_class = class_
160
161     if class_ in match_types:
162         if oxm_type in match_types[class_]:
163             fatal("duplicate match type for %s (conflicts with %s)" %
164                   (name, match_types[class_][oxm_type]))
165     else:
166         match_types[class_] = dict()
167     match_types[class_][oxm_type] = name
168
169     # Normally the oxm_length is the size of the field, but for experimenter
170     # OXMs oxm_length also includes the 4-byte experimenter ID.
171     oxm_length = n_bytes
172     if oxm_class == 0xffff:
173         oxm_length += 4
174
175     header = ("NXM_HEADER(0x%x,0x%x,%s,0,%d)"
176               % (oxm_vendor, oxm_class, oxm_type, oxm_length))
177
178     if of_version:
179         if of_version not in VERSION:
180             fatal("%s: unknown OpenFlow version %s" % (name, of_version))
181         of_version_nr = VERSION[of_version]
182         if of_version_nr < VERSION['1.2']:
183             fatal("%s: claimed version %s predates OXM" % (name, of_version))
184     else:
185         of_version_nr = 0
186
187     return (header, name, of_version_nr, ovs_version)
188
189
190 def parse_field(mff, comment):
191     f = {'mff': mff}
192
193     # First line of comment is the field name.
194     m = re.match(r'"([^"]+)"(?:\s+\(aka "([^"]+)"\))?(?:\s+\(.*\))?\.', comment[0])
195     if not m:
196         fatal("%s lacks field name" % mff)
197     f['name'], f['extra_name'] = m.groups()
198
199     # Find the last blank line the comment.  The field definitions
200     # start after that.
201     blank = None
202     for i in range(len(comment)):
203         if not comment[i]:
204             blank = i
205     if not blank:
206         fatal("%s: missing blank line in comment" % mff)
207
208     d = {}
209     for key in ("Type", "Maskable", "Formatting", "Prerequisites",
210                 "Access", "Prefix lookup member",
211                 "OXM", "NXM", "OF1.0", "OF1.1"):
212         d[key] = None
213     for fline in comment[blank + 1:]:
214         m = re.match(r'([^:]+):\s+(.*)\.$', fline)
215         if not m:
216             fatal("%s: syntax error parsing key-value pair as part of %s"
217                   % (fline, mff))
218         key, value = m.groups()
219         if key not in d:
220             fatal("%s: unknown key" % key)
221         elif key == 'Code point':
222             d[key] += [value]
223         elif d[key] is not None:
224             fatal("%s: duplicate key" % key)
225         d[key] = value
226     for key, value in d.items():
227         if not value and key not in ("OF1.0", "OF1.1",
228                                      "Prefix lookup member", "Notes"):
229             fatal("%s: missing %s" % (mff, key))
230
231     m = re.match(r'([a-zA-Z0-9]+)(?: \(low ([0-9]+) bits\))?$', d['Type'])
232     if not m:
233         fatal("%s: syntax error in type" % mff)
234     type_ = m.group(1)
235     if type_ not in TYPES:
236         fatal("%s: unknown type %s" % (mff, d['Type']))
237     f['n_bytes'] = TYPES[type_]
238     if m.group(2):
239         f['n_bits'] = int(m.group(2))
240         if f['n_bits'] > f['n_bytes'] * 8:
241             fatal("%s: more bits (%d) than field size (%d)"
242                   % (mff, f['n_bits'], 8 * f['n_bytes']))
243     else:
244         f['n_bits'] = 8 * f['n_bytes']
245
246     if d['Maskable'] == 'no':
247         f['mask'] = 'MFM_NONE'
248     elif d['Maskable'] == 'bitwise':
249         f['mask'] = 'MFM_FULLY'
250     else:
251         fatal("%s: unknown maskable %s" % (mff, d['Maskable']))
252
253     fmt = FORMATTING.get(d['Formatting'])
254     if not fmt:
255         fatal("%s: unknown format %s" % (mff, d['Formatting']))
256     if f['n_bytes'] < fmt[1] or f['n_bytes'] > fmt[2]:
257         fatal("%s: %d-byte field can't be formatted as %s"
258               % (mff, f['n_bytes'], d['Formatting']))
259     f['string'] = fmt[0]
260
261     f['prereqs'] = PREREQS.get(d['Prerequisites'])
262     if not f['prereqs']:
263         fatal("%s: unknown prerequisites %s" % (mff, d['Prerequisites']))
264
265     if d['Access'] == 'read-only':
266         f['writable'] = False
267     elif d['Access'] == 'read/write':
268         f['writable'] = True
269     else:
270         fatal("%s: unknown access %s" % (mff, d['Access']))
271
272     f['OF1.0'] = d['OF1.0']
273     if not d['OF1.0'] in (None, 'exact match', 'CIDR mask'):
274         fatal("%s: unknown OF1.0 match type %s" % (mff, d['OF1.0']))
275
276     f['OF1.1'] = d['OF1.1']
277     if not d['OF1.1'] in (None, 'exact match', 'bitwise mask'):
278         fatal("%s: unknown OF1.1 match type %s" % (mff, d['OF1.1']))
279
280     f['OXM'] = (parse_oxms(d['OXM'], 'OXM', f['n_bytes']) +
281                 parse_oxms(d['NXM'], 'NXM', f['n_bytes']))
282
283     f['prefix'] = d["Prefix lookup member"]
284
285     return f
286
287
288 def protocols_to_c(protocols):
289     if protocols == set(['of10', 'of11', 'oxm']):
290         return 'OFPUTIL_P_ANY'
291     elif protocols == set(['of11', 'oxm']):
292         return 'OFPUTIL_P_NXM_OF11_UP'
293     elif protocols == set(['oxm']):
294         return 'OFPUTIL_P_NXM_OXM_ANY'
295     elif protocols == set([]):
296         return 'OFPUTIL_P_NONE'
297     else:
298         assert False
299
300
301 def make_meta_flow(fields):
302     output = []
303     for f in fields:
304         output += ["{"]
305         output += ["    %s," % f['mff']]
306         if f['extra_name']:
307             output += ["    \"%s\", \"%s\"," % (f['name'], f['extra_name'])]
308         else:
309             output += ["    \"%s\", NULL," % f['name']]
310         output += ["    %d, %d," % (f['n_bytes'], f['n_bits'])]
311
312         if f['writable']:
313             rw = 'true'
314         else:
315             rw = 'false'
316         output += ["    %s, %s, %s, %s,"
317                    % (f['mask'], f['string'], f['prereqs'], rw)]
318
319         oxm = f['OXM']
320         of10 = f['OF1.0']
321         of11 = f['OF1.1']
322         if f['mff'] in ('MFF_DL_VLAN', 'MFF_DL_VLAN_PCP'):
323             # MFF_DL_VLAN and MFF_DL_VLAN_PCP don't exactly correspond to
324             # OF1.1, nor do they have NXM or OXM assignments, but their
325             # meanings can be expressed in every protocol, which is the goal of
326             # this member.
327             protocols = set(["of10", "of11", "oxm"])
328         else:
329             protocols = set([])
330             if of10:
331                 protocols |= set(["of10"])
332             if of11:
333                 protocols |= set(["of11"])
334             if oxm:
335                 protocols |= set(["oxm"])
336
337         if f['mask'] == 'MFM_FULLY':
338             cidr_protocols = protocols.copy()
339             bitwise_protocols = protocols.copy()
340
341             if of10 == 'exact match':
342                 bitwise_protocols -= set(['of10'])
343                 cidr_protocols -= set(['of10'])
344             elif of10 == 'CIDR mask':
345                 bitwise_protocols -= set(['of10'])
346             else:
347                 assert of10 is None
348
349             if of11 == 'exact match':
350                 bitwise_protocols -= set(['of11'])
351                 cidr_protocols -= set(['of11'])
352             else:
353                 assert of11 in (None, 'bitwise mask')
354         else:
355             assert f['mask'] == 'MFM_NONE'
356             cidr_protocols = set([])
357             bitwise_protocols = set([])
358
359         output += ["    %s," % protocols_to_c(protocols)]
360         output += ["    %s," % protocols_to_c(cidr_protocols)]
361         output += ["    %s," % protocols_to_c(bitwise_protocols)]
362
363         if f['prefix']:
364             output += ["    FLOW_U32OFS(%s)," % f['prefix']]
365         else:
366             output += ["    -1, /* not usable for prefix lookup */"]
367
368         output += ["},"]
369     return output
370
371
372 def make_nx_match(fields):
373     output = []
374     print("static struct nxm_field_index all_nxm_fields[] = {")
375     for f in fields:
376         # Sort by OpenFlow version number (nx-match.c depends on this).
377         for oxm in sorted(f['OXM'], key=lambda x: x[2]):
378             print("""{ .nf = { %s, %d, "%s", %s } },""" % (
379                 oxm[0], oxm[2], oxm[1], f['mff']))
380     print("};")
381     return output
382
383
384 def extract_ofp_fields(mode):
385     global line
386
387     fields = []
388
389     while True:
390         get_line()
391         if re.match('enum.*mf_field_id', line):
392             break
393
394     while True:
395         get_line()
396         first_line_number = line_number
397         here = '%s:%d' % (file_name, line_number)
398         if (line.startswith('/*')
399             or line.startswith(' *')
400             or line.startswith('#')
401             or not line
402             or line.isspace()):
403             continue
404         elif re.match('}', line) or re.match('\s+MFF_N_IDS', line):
405             break
406
407         # Parse the comment preceding an MFF_ constant into 'comment',
408         # one line to an array element.
409         line = line.strip()
410         if not line.startswith('/*'):
411             fatal("unexpected syntax between fields")
412         line = line[1:]
413         comment = []
414         end = False
415         while not end:
416             line = line.strip()
417             if line.startswith('*/'):
418                 get_line()
419                 break
420             if not line.startswith('*'):
421                 fatal("unexpected syntax within field")
422
423             line = line[1:]
424             if line.startswith(' '):
425                 line = line[1:]
426             if line.startswith(' ') and comment:
427                 continuation = True
428                 line = line.lstrip()
429             else:
430                 continuation = False
431
432             if line.endswith('*/'):
433                 line = line[:-2].rstrip()
434                 end = True
435             else:
436                 end = False
437
438             if continuation:
439                 comment[-1] += " " + line
440             else:
441                 comment += [line]
442             get_line()
443
444         # Drop blank lines at each end of comment.
445         while comment and not comment[0]:
446             comment = comment[1:]
447         while comment and not comment[-1]:
448             comment = comment[:-1]
449
450         # Parse the MFF_ constant(s).
451         mffs = []
452         while True:
453             m = re.match('\s+(MFF_[A-Z0-9_]+),?\s?$', line)
454             if not m:
455                 break
456             mffs += [m.group(1)]
457             get_line()
458         if not mffs:
459             fatal("unexpected syntax looking for MFF_ constants")
460
461         if len(mffs) > 1 or '<N>' in comment[0]:
462             for mff in mffs:
463                 # Extract trailing integer.
464                 m = re.match('.*[^0-9]([0-9]+)$', mff)
465                 if not m:
466                     fatal("%s lacks numeric suffix in register group" % mff)
467                 n = m.group(1)
468
469                 # Search-and-replace <N> within the comment,
470                 # and drop lines that have <x> for x != n.
471                 instance = []
472                 for x in comment:
473                     y = x.replace('<N>', n)
474                     if re.search('<[0-9]+>', y):
475                         if ('<%s>' % n) not in y:
476                             continue
477                         y = re.sub('<[0-9]+>', '', y)
478                     instance += [y.strip()]
479                 fields += [parse_field(mff, instance)]
480         else:
481             fields += [parse_field(mffs[0], comment)]
482         continue
483
484     input_file.close()
485
486     if n_errors:
487         sys.exit(1)
488
489     print("""\
490 /* Generated automatically; do not modify!    "-*- buffer-read-only: t -*- */
491 """)
492
493     if mode == '--meta-flow':
494         output = make_meta_flow(fields)
495     elif mode == '--nx-match':
496         output = make_nx_match(fields)
497     else:
498         assert False
499
500     return output
501
502
503 if __name__ == '__main__':
504     if '--help' in sys.argv:
505         usage()
506     elif len(sys.argv) != 3:
507         sys.stderr.write("exactly two arguments required; "
508                          "use --help for help\n")
509         sys.exit(1)
510     elif sys.argv[2] in ('--meta-flow', '--nx-match'):
511         global file_name
512         global input_file
513         global line_number
514         file_name = sys.argv[1]
515         input_file = open(file_name)
516         line_number = 0
517
518         for oline in extract_ofp_fields(sys.argv[2]):
519             print(oline)
520     else:
521         sys.stderr.write("invalid arguments; use --help for help\n")
522         sys.exit(1)