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