]>
Commit | Line | Data |
---|---|---|
a4ce8b25 BP |
1 | #! /usr/bin/python |
2 | ||
96fee5e0 | 3 | import getopt |
a4ce8b25 BP |
4 | import sys |
5 | import os.path | |
6 | import re | |
96fee5e0 BP |
7 | import xml.dom.minidom |
8 | import build.nroff | |
a4ce8b25 BP |
9 | |
10 | line = "" | |
11 | ||
12 | # Maps from user-friendly version number to its protocol encoding. | |
13 | VERSION = {"1.0": 0x01, | |
14 | "1.1": 0x02, | |
15 | "1.2": 0x03, | |
16 | "1.3": 0x04, | |
17 | "1.4": 0x05, | |
18 | "1.5": 0x06} | |
4ab66562 | 19 | VERSION_REVERSE = dict((v,k) for k, v in VERSION.items()) |
a4ce8b25 | 20 | |
9558d2a5 JG |
21 | TYPES = {"u8": (1, False), |
22 | "be16": (2, False), | |
23 | "be32": (4, False), | |
24 | "MAC": (6, False), | |
25 | "be64": (8, False), | |
e56e30ac | 26 | "be128": (16, False), |
9558d2a5 | 27 | "tunnelMD": (124, True)} |
a4ce8b25 | 28 | |
1734bf29 JG |
29 | FORMATTING = {"decimal": ("MFS_DECIMAL", 1, 8), |
30 | "hexadecimal": ("MFS_HEXADECIMAL", 1, 127), | |
07659514 | 31 | "ct state": ("MFS_CT_STATE", 4, 4), |
1734bf29 JG |
32 | "Ethernet": ("MFS_ETHERNET", 6, 6), |
33 | "IPv4": ("MFS_IPV4", 4, 4), | |
34 | "IPv6": ("MFS_IPV6", 16, 16), | |
35 | "OpenFlow 1.0 port": ("MFS_OFP_PORT", 2, 2), | |
36 | "OpenFlow 1.1+ port": ("MFS_OFP_PORT_OXM", 4, 4), | |
37 | "frag": ("MFS_FRAG", 1, 1), | |
38 | "tunnel flags": ("MFS_TNL_FLAGS", 2, 2), | |
3d4b2e6e JS |
39 | "TCP flags": ("MFS_TCP_FLAGS", 2, 2), |
40 | "packet type": ("MFS_PACKET_TYPE", 4, 4)} | |
a4ce8b25 BP |
41 | |
42 | PREREQS = {"none": "MFP_NONE", | |
beb75a40 | 43 | "Ethernet": "MFP_ETHERNET", |
56134136 JS |
44 | "ARP": "MFP_ARP", |
45 | "VLAN VID": "MFP_VLAN_VID", | |
46 | "IPv4": "MFP_IPV4", | |
47 | "IPv6": "MFP_IPV6", | |
48 | "IPv4/IPv6": "MFP_IP_ANY", | |
3d2fbd70 | 49 | "NSH": "MFP_NSH", |
daf4d3c1 | 50 | "CT": "MFP_CT_VALID", |
56134136 JS |
51 | "MPLS": "MFP_MPLS", |
52 | "TCP": "MFP_TCP", | |
53 | "UDP": "MFP_UDP", | |
54 | "SCTP": "MFP_SCTP", | |
55 | "ICMPv4": "MFP_ICMPV4", | |
56 | "ICMPv6": "MFP_ICMPV6", | |
57 | "ND": "MFP_ND", | |
58 | "ND solicit": "MFP_ND_SOLICIT", | |
59 | "ND advert": "MFP_ND_ADVERT"} | |
a4ce8b25 | 60 | |
508a9338 BP |
61 | # Maps a name prefix into an (experimenter ID, class) pair, so: |
62 | # | |
63 | # - Standard OXM classes are written as (0, <oxm_class>) | |
64 | # | |
65 | # - Experimenter OXM classes are written as (<oxm_vender>, 0xffff) | |
66 | # | |
a4ce8b25 | 67 | # If a name matches more than one prefix, the longest one is used. |
ffc317b9 BP |
68 | OXM_CLASSES = {"NXM_OF_": (0, 0x0000, 'extension'), |
69 | "NXM_NX_": (0, 0x0001, 'extension'), | |
70 | "NXOXM_NSH_": (0x005ad650, 0xffff, 'extension'), | |
71 | "OXM_OF_": (0, 0x8000, 'standard'), | |
72 | "OXM_OF_PKT_REG": (0, 0x8001, 'standard'), | |
73 | "ONFOXM_ET_": (0x4f4e4600, 0xffff, 'standard'), | |
9b2b8497 | 74 | "ERICOXM_OF_": (0, 0x1000, 'extension'), |
508a9338 BP |
75 | |
76 | # This is the experimenter OXM class for Nicira, which is the | |
77 | # one that OVS would be using instead of NXM_OF_ and NXM_NX_ | |
78 | # if OVS didn't have those grandfathered in. It is currently | |
79 | # used only to test support for experimenter OXM, since there | |
80 | # are barely any real uses of experimenter OXM in the wild. | |
ffc317b9 | 81 | "NXOXM_ET_": (0x00002320, 0xffff, 'extension')} |
56134136 | 82 | |
a4ce8b25 BP |
83 | def oxm_name_to_class(name): |
84 | prefix = '' | |
85 | class_ = None | |
1421f682 | 86 | for p, c in OXM_CLASSES.items(): |
a4ce8b25 BP |
87 | if name.startswith(p) and len(p) > len(prefix): |
88 | prefix = p | |
89 | class_ = c | |
90 | return class_ | |
91 | ||
56134136 | 92 | |
ffc317b9 BP |
93 | def is_standard_oxm(name): |
94 | oxm_vendor, oxm_class, oxm_class_type = oxm_name_to_class(name) | |
95 | return oxm_class_type == 'standard' | |
96 | ||
97 | ||
a4ce8b25 BP |
98 | def decode_version_range(range): |
99 | if range in VERSION: | |
100 | return (VERSION[range], VERSION[range]) | |
101 | elif range.endswith('+'): | |
102 | return (VERSION[range[:-1]], max(VERSION.values())) | |
103 | else: | |
104 | a, b = re.match(r'^([^-]+)-([^-]+)$', range).groups() | |
105 | return (VERSION[a], VERSION[b]) | |
106 | ||
56134136 | 107 | |
a4ce8b25 BP |
108 | def get_line(): |
109 | global line | |
110 | global line_number | |
111 | line = input_file.readline() | |
112 | line_number += 1 | |
113 | if line == "": | |
114 | fatal("unexpected end of input") | |
115 | ||
56134136 | 116 | |
a4ce8b25 | 117 | n_errors = 0 |
56134136 JS |
118 | |
119 | ||
a4ce8b25 BP |
120 | def error(msg): |
121 | global n_errors | |
122 | sys.stderr.write("%s:%d: %s\n" % (file_name, line_number, msg)) | |
123 | n_errors += 1 | |
124 | ||
56134136 | 125 | |
a4ce8b25 BP |
126 | def fatal(msg): |
127 | error(msg) | |
128 | sys.exit(1) | |
129 | ||
56134136 | 130 | |
a4ce8b25 BP |
131 | def usage(): |
132 | argv0 = os.path.basename(sys.argv[0]) | |
1421f682 | 133 | print('''\ |
a4ce8b25 | 134 | %(argv0)s, for extracting OpenFlow field properties from meta-flow.h |
178742f9 | 135 | usage: %(argv0)s INPUT [--meta-flow | --nx-match] |
a4ce8b25 | 136 | where INPUT points to lib/meta-flow.h in the source directory. |
178742f9 BP |
137 | Depending on the option given, the output written to stdout is intended to be |
138 | saved either as lib/meta-flow.inc or lib/nx-match.inc for the respective C | |
139 | file to #include.\ | |
1421f682 | 140 | ''' % {"argv0": argv0}) |
a4ce8b25 BP |
141 | sys.exit(0) |
142 | ||
56134136 | 143 | |
a4ce8b25 BP |
144 | def make_sizeof(s): |
145 | m = re.match(r'(.*) up to (.*)', s) | |
146 | if m: | |
147 | struct, member = m.groups() | |
148 | return "offsetof(%s, %s)" % (struct, member) | |
149 | else: | |
150 | return "sizeof(%s)" % s | |
151 | ||
56134136 | 152 | |
e6556fe3 | 153 | def parse_oxms(s, prefix, n_bytes): |
a4ce8b25 | 154 | if s == 'none': |
e6556fe3 BP |
155 | return () |
156 | ||
157 | return tuple(parse_oxm(s2.strip(), prefix, n_bytes) for s2 in s.split(',')) | |
a4ce8b25 | 158 | |
56134136 | 159 | |
8c101866 JS |
160 | match_types = dict() |
161 | ||
162 | ||
e6556fe3 | 163 | def parse_oxm(s, prefix, n_bytes): |
8c101866 JS |
164 | global match_types |
165 | ||
a4ce8b25 BP |
166 | m = re.match('([A-Z0-9_]+)\(([0-9]+)\) since(?: OF(1\.[0-9]+) and)? v([12]\.[0-9]+)$', s) |
167 | if not m: | |
168 | fatal("%s: syntax error parsing %s" % (s, prefix)) | |
56134136 | 169 | |
508a9338 | 170 | name, oxm_type, of_version, ovs_version = m.groups() |
a4ce8b25 BP |
171 | |
172 | class_ = oxm_name_to_class(name) | |
173 | if class_ is None: | |
174 | fatal("unknown OXM class for %s" % name) | |
ffc317b9 | 175 | oxm_vendor, oxm_class, oxm_class_type = class_ |
508a9338 | 176 | |
8c101866 JS |
177 | if class_ in match_types: |
178 | if oxm_type in match_types[class_]: | |
179 | fatal("duplicate match type for %s (conflicts with %s)" % | |
180 | (name, match_types[class_][oxm_type])) | |
181 | else: | |
182 | match_types[class_] = dict() | |
183 | match_types[class_][oxm_type] = name | |
184 | ||
508a9338 BP |
185 | # Normally the oxm_length is the size of the field, but for experimenter |
186 | # OXMs oxm_length also includes the 4-byte experimenter ID. | |
187 | oxm_length = n_bytes | |
188 | if oxm_class == 0xffff: | |
189 | oxm_length += 4 | |
190 | ||
96fee5e0 | 191 | header = (oxm_vendor, oxm_class, int(oxm_type), oxm_length) |
a4ce8b25 BP |
192 | |
193 | if of_version: | |
ffc317b9 BP |
194 | if oxm_class_type == 'extension': |
195 | fatal("%s: OXM extension can't have OpenFlow version" % name) | |
a4ce8b25 BP |
196 | if of_version not in VERSION: |
197 | fatal("%s: unknown OpenFlow version %s" % (name, of_version)) | |
198 | of_version_nr = VERSION[of_version] | |
199 | if of_version_nr < VERSION['1.2']: | |
200 | fatal("%s: claimed version %s predates OXM" % (name, of_version)) | |
201 | else: | |
ffc317b9 BP |
202 | if oxm_class_type == 'standard': |
203 | fatal("%s: missing OpenFlow version number" % name) | |
a4ce8b25 BP |
204 | of_version_nr = 0 |
205 | ||
206 | return (header, name, of_version_nr, ovs_version) | |
207 | ||
56134136 | 208 | |
a4ce8b25 BP |
209 | def parse_field(mff, comment): |
210 | f = {'mff': mff} | |
211 | ||
212 | # First line of comment is the field name. | |
213 | m = re.match(r'"([^"]+)"(?:\s+\(aka "([^"]+)"\))?(?:\s+\(.*\))?\.', comment[0]) | |
214 | if not m: | |
215 | fatal("%s lacks field name" % mff) | |
216 | f['name'], f['extra_name'] = m.groups() | |
217 | ||
218 | # Find the last blank line the comment. The field definitions | |
219 | # start after that. | |
220 | blank = None | |
221 | for i in range(len(comment)): | |
222 | if not comment[i]: | |
223 | blank = i | |
224 | if not blank: | |
225 | fatal("%s: missing blank line in comment" % mff) | |
226 | ||
227 | d = {} | |
228 | for key in ("Type", "Maskable", "Formatting", "Prerequisites", | |
229 | "Access", "Prefix lookup member", | |
230 | "OXM", "NXM", "OF1.0", "OF1.1"): | |
231 | d[key] = None | |
232 | for fline in comment[blank + 1:]: | |
233 | m = re.match(r'([^:]+):\s+(.*)\.$', fline) | |
234 | if not m: | |
235 | fatal("%s: syntax error parsing key-value pair as part of %s" | |
236 | % (fline, mff)) | |
237 | key, value = m.groups() | |
238 | if key not in d: | |
239 | fatal("%s: unknown key" % key) | |
240 | elif key == 'Code point': | |
241 | d[key] += [value] | |
242 | elif d[key] is not None: | |
243 | fatal("%s: duplicate key" % key) | |
244 | d[key] = value | |
1421f682 | 245 | for key, value in d.items(): |
a4ce8b25 BP |
246 | if not value and key not in ("OF1.0", "OF1.1", |
247 | "Prefix lookup member", "Notes"): | |
248 | fatal("%s: missing %s" % (mff, key)) | |
249 | ||
250 | m = re.match(r'([a-zA-Z0-9]+)(?: \(low ([0-9]+) bits\))?$', d['Type']) | |
251 | if not m: | |
252 | fatal("%s: syntax error in type" % mff) | |
253 | type_ = m.group(1) | |
254 | if type_ not in TYPES: | |
255 | fatal("%s: unknown type %s" % (mff, d['Type'])) | |
f047e846 JG |
256 | |
257 | f['n_bytes'] = TYPES[type_][0] | |
a4ce8b25 BP |
258 | if m.group(2): |
259 | f['n_bits'] = int(m.group(2)) | |
260 | if f['n_bits'] > f['n_bytes'] * 8: | |
261 | fatal("%s: more bits (%d) than field size (%d)" | |
262 | % (mff, f['n_bits'], 8 * f['n_bytes'])) | |
263 | else: | |
264 | f['n_bits'] = 8 * f['n_bytes'] | |
f047e846 | 265 | f['variable'] = TYPES[type_][1] |
a4ce8b25 BP |
266 | |
267 | if d['Maskable'] == 'no': | |
268 | f['mask'] = 'MFM_NONE' | |
269 | elif d['Maskable'] == 'bitwise': | |
270 | f['mask'] = 'MFM_FULLY' | |
271 | else: | |
272 | fatal("%s: unknown maskable %s" % (mff, d['Maskable'])) | |
273 | ||
274 | fmt = FORMATTING.get(d['Formatting']) | |
275 | if not fmt: | |
276 | fatal("%s: unknown format %s" % (mff, d['Formatting'])) | |
96fee5e0 | 277 | f['formatting'] = d['Formatting'] |
a4ce8b25 BP |
278 | if f['n_bytes'] < fmt[1] or f['n_bytes'] > fmt[2]: |
279 | fatal("%s: %d-byte field can't be formatted as %s" | |
280 | % (mff, f['n_bytes'], d['Formatting'])) | |
281 | f['string'] = fmt[0] | |
282 | ||
96fee5e0 BP |
283 | f['prereqs'] = d['Prerequisites'] |
284 | if f['prereqs'] not in PREREQS: | |
a4ce8b25 BP |
285 | fatal("%s: unknown prerequisites %s" % (mff, d['Prerequisites'])) |
286 | ||
287 | if d['Access'] == 'read-only': | |
288 | f['writable'] = False | |
289 | elif d['Access'] == 'read/write': | |
290 | f['writable'] = True | |
291 | else: | |
292 | fatal("%s: unknown access %s" % (mff, d['Access'])) | |
293 | ||
294 | f['OF1.0'] = d['OF1.0'] | |
295 | if not d['OF1.0'] in (None, 'exact match', 'CIDR mask'): | |
296 | fatal("%s: unknown OF1.0 match type %s" % (mff, d['OF1.0'])) | |
56134136 | 297 | |
a4ce8b25 BP |
298 | f['OF1.1'] = d['OF1.1'] |
299 | if not d['OF1.1'] in (None, 'exact match', 'bitwise mask'): | |
300 | fatal("%s: unknown OF1.1 match type %s" % (mff, d['OF1.1'])) | |
301 | ||
e6556fe3 BP |
302 | f['OXM'] = (parse_oxms(d['OXM'], 'OXM', f['n_bytes']) + |
303 | parse_oxms(d['NXM'], 'NXM', f['n_bytes'])) | |
a4ce8b25 BP |
304 | |
305 | f['prefix'] = d["Prefix lookup member"] | |
306 | ||
307 | return f | |
308 | ||
56134136 | 309 | |
a4ce8b25 BP |
310 | def protocols_to_c(protocols): |
311 | if protocols == set(['of10', 'of11', 'oxm']): | |
312 | return 'OFPUTIL_P_ANY' | |
313 | elif protocols == set(['of11', 'oxm']): | |
314 | return 'OFPUTIL_P_NXM_OF11_UP' | |
315 | elif protocols == set(['oxm']): | |
316 | return 'OFPUTIL_P_NXM_OXM_ANY' | |
317 | elif protocols == set([]): | |
318 | return 'OFPUTIL_P_NONE' | |
319 | else: | |
56134136 JS |
320 | assert False |
321 | ||
a4ce8b25 | 322 | |
96fee5e0 BP |
323 | def autogen_c_comment(): |
324 | return [ | |
325 | "/* Generated automatically; do not modify! -*- buffer-read-only: t -*- */", | |
326 | ""] | |
327 | ||
328 | def make_meta_flow(meta_flow_h): | |
329 | fields = extract_ofp_fields(meta_flow_h) | |
330 | output = autogen_c_comment() | |
178742f9 BP |
331 | for f in fields: |
332 | output += ["{"] | |
333 | output += [" %s," % f['mff']] | |
334 | if f['extra_name']: | |
335 | output += [" \"%s\", \"%s\"," % (f['name'], f['extra_name'])] | |
336 | else: | |
337 | output += [" \"%s\", NULL," % f['name']] | |
f047e846 JG |
338 | |
339 | if f['variable']: | |
340 | variable = 'true' | |
341 | else: | |
342 | variable = 'false' | |
343 | output += [" %d, %d, %s," % (f['n_bytes'], f['n_bits'], variable)] | |
178742f9 BP |
344 | |
345 | if f['writable']: | |
346 | rw = 'true' | |
347 | else: | |
348 | rw = 'false' | |
04f48a68 | 349 | output += [" %s, %s, %s, %s, false," |
96fee5e0 | 350 | % (f['mask'], f['string'], PREREQS[f['prereqs']], rw)] |
178742f9 | 351 | |
178742f9 | 352 | oxm = f['OXM'] |
178742f9 BP |
353 | of10 = f['OF1.0'] |
354 | of11 = f['OF1.1'] | |
355 | if f['mff'] in ('MFF_DL_VLAN', 'MFF_DL_VLAN_PCP'): | |
356 | # MFF_DL_VLAN and MFF_DL_VLAN_PCP don't exactly correspond to | |
357 | # OF1.1, nor do they have NXM or OXM assignments, but their | |
358 | # meanings can be expressed in every protocol, which is the goal of | |
359 | # this member. | |
360 | protocols = set(["of10", "of11", "oxm"]) | |
361 | else: | |
362 | protocols = set([]) | |
363 | if of10: | |
364 | protocols |= set(["of10"]) | |
365 | if of11: | |
366 | protocols |= set(["of11"]) | |
e6556fe3 | 367 | if oxm: |
178742f9 BP |
368 | protocols |= set(["oxm"]) |
369 | ||
370 | if f['mask'] == 'MFM_FULLY': | |
371 | cidr_protocols = protocols.copy() | |
372 | bitwise_protocols = protocols.copy() | |
373 | ||
374 | if of10 == 'exact match': | |
375 | bitwise_protocols -= set(['of10']) | |
376 | cidr_protocols -= set(['of10']) | |
377 | elif of10 == 'CIDR mask': | |
378 | bitwise_protocols -= set(['of10']) | |
379 | else: | |
380 | assert of10 is None | |
381 | ||
382 | if of11 == 'exact match': | |
383 | bitwise_protocols -= set(['of11']) | |
384 | cidr_protocols -= set(['of11']) | |
385 | else: | |
386 | assert of11 in (None, 'bitwise mask') | |
387 | else: | |
388 | assert f['mask'] == 'MFM_NONE' | |
389 | cidr_protocols = set([]) | |
390 | bitwise_protocols = set([]) | |
391 | ||
392 | output += [" %s," % protocols_to_c(protocols)] | |
393 | output += [" %s," % protocols_to_c(cidr_protocols)] | |
394 | output += [" %s," % protocols_to_c(bitwise_protocols)] | |
56134136 | 395 | |
178742f9 BP |
396 | if f['prefix']: |
397 | output += [" FLOW_U32OFS(%s)," % f['prefix']] | |
398 | else: | |
399 | output += [" -1, /* not usable for prefix lookup */"] | |
400 | ||
aafee638 | 401 | output += ["},"] |
96fee5e0 BP |
402 | for oline in output: |
403 | print(oline) | |
178742f9 | 404 | |
56134136 | 405 | |
96fee5e0 BP |
406 | def make_nx_match(meta_flow_h): |
407 | fields = extract_ofp_fields(meta_flow_h) | |
408 | output = autogen_c_comment() | |
1421f682 | 409 | print("static struct nxm_field_index all_nxm_fields[] = {") |
178742f9 | 410 | for f in fields: |
e6556fe3 BP |
411 | # Sort by OpenFlow version number (nx-match.c depends on this). |
412 | for oxm in sorted(f['OXM'], key=lambda x: x[2]): | |
96fee5e0 | 413 | header = ("NXM_HEADER(0x%x,0x%x,%s,0,%d)" % oxm[0]) |
1421f682 | 414 | print("""{ .nf = { %s, %d, "%s", %s } },""" % ( |
96fee5e0 | 415 | header, oxm[2], oxm[1], f['mff'])) |
1421f682 | 416 | print("};") |
96fee5e0 BP |
417 | for oline in output: |
418 | print(oline) | |
178742f9 | 419 | |
56134136 | 420 | |
96fee5e0 BP |
421 | def extract_ofp_fields(fn): |
422 | global file_name | |
423 | global input_file | |
424 | global line_number | |
a4ce8b25 BP |
425 | global line |
426 | ||
96fee5e0 BP |
427 | file_name = fn |
428 | input_file = open(file_name) | |
429 | line_number = 0 | |
430 | ||
a4ce8b25 BP |
431 | fields = [] |
432 | ||
433 | while True: | |
434 | get_line() | |
435 | if re.match('enum.*mf_field_id', line): | |
436 | break | |
437 | ||
438 | while True: | |
439 | get_line() | |
440 | first_line_number = line_number | |
441 | here = '%s:%d' % (file_name, line_number) | |
442 | if (line.startswith('/*') | |
443 | or line.startswith(' *') | |
444 | or line.startswith('#') | |
445 | or not line | |
446 | or line.isspace()): | |
447 | continue | |
448 | elif re.match('}', line) or re.match('\s+MFF_N_IDS', line): | |
449 | break | |
450 | ||
451 | # Parse the comment preceding an MFF_ constant into 'comment', | |
452 | # one line to an array element. | |
453 | line = line.strip() | |
454 | if not line.startswith('/*'): | |
455 | fatal("unexpected syntax between fields") | |
456 | line = line[1:] | |
457 | comment = [] | |
458 | end = False | |
459 | while not end: | |
460 | line = line.strip() | |
461 | if line.startswith('*/'): | |
462 | get_line() | |
463 | break | |
464 | if not line.startswith('*'): | |
465 | fatal("unexpected syntax within field") | |
466 | ||
467 | line = line[1:] | |
468 | if line.startswith(' '): | |
469 | line = line[1:] | |
470 | if line.startswith(' ') and comment: | |
471 | continuation = True | |
472 | line = line.lstrip() | |
473 | else: | |
474 | continuation = False | |
475 | ||
476 | if line.endswith('*/'): | |
477 | line = line[:-2].rstrip() | |
478 | end = True | |
479 | else: | |
480 | end = False | |
481 | ||
482 | if continuation: | |
483 | comment[-1] += " " + line | |
484 | else: | |
485 | comment += [line] | |
486 | get_line() | |
487 | ||
488 | # Drop blank lines at each end of comment. | |
489 | while comment and not comment[0]: | |
490 | comment = comment[1:] | |
491 | while comment and not comment[-1]: | |
492 | comment = comment[:-1] | |
493 | ||
494 | # Parse the MFF_ constant(s). | |
495 | mffs = [] | |
496 | while True: | |
497 | m = re.match('\s+(MFF_[A-Z0-9_]+),?\s?$', line) | |
498 | if not m: | |
499 | break | |
500 | mffs += [m.group(1)] | |
501 | get_line() | |
502 | if not mffs: | |
503 | fatal("unexpected syntax looking for MFF_ constants") | |
504 | ||
505 | if len(mffs) > 1 or '<N>' in comment[0]: | |
506 | for mff in mffs: | |
507 | # Extract trailing integer. | |
508 | m = re.match('.*[^0-9]([0-9]+)$', mff) | |
509 | if not m: | |
510 | fatal("%s lacks numeric suffix in register group" % mff) | |
511 | n = m.group(1) | |
512 | ||
513 | # Search-and-replace <N> within the comment, | |
514 | # and drop lines that have <x> for x != n. | |
515 | instance = [] | |
516 | for x in comment: | |
517 | y = x.replace('<N>', n) | |
518 | if re.search('<[0-9]+>', y): | |
519 | if ('<%s>' % n) not in y: | |
520 | continue | |
521 | y = re.sub('<[0-9]+>', '', y) | |
522 | instance += [y.strip()] | |
523 | fields += [parse_field(mff, instance)] | |
524 | else: | |
525 | fields += [parse_field(mffs[0], comment)] | |
526 | continue | |
527 | ||
528 | input_file.close() | |
529 | ||
530 | if n_errors: | |
531 | sys.exit(1) | |
532 | ||
96fee5e0 BP |
533 | return fields |
534 | \f | |
535 | ## ------------------------ ## | |
536 | ## Documentation Generation ## | |
537 | ## ------------------------ ## | |
538 | ||
539 | def field_to_xml(field_node, f, body, summary): | |
540 | f["used"] = True | |
541 | ||
542 | # Summary. | |
543 | if field_node.hasAttribute('internal'): | |
544 | return | |
545 | ||
546 | min_of_version = None | |
547 | min_ovs_version = None | |
548 | for header, name, of_version_nr, ovs_version_s in f['OXM']: | |
ffc317b9 | 549 | if (is_standard_oxm(name) |
96fee5e0 BP |
550 | and (min_ovs_version is None or of_version_nr < min_of_version)): |
551 | min_of_version = of_version_nr | |
552 | ovs_version = [int(x) for x in ovs_version_s.split('.')] | |
553 | if min_ovs_version is None or ovs_version < min_ovs_version: | |
554 | min_ovs_version = ovs_version | |
555 | summary += ["\\fB%s\\fR" % f["name"]] | |
556 | if f["extra_name"]: | |
557 | summary += [" aka \\fB%s\\fR" % f["extra_name"]] | |
558 | summary += [";%d" % f["n_bytes"]] | |
559 | if f["n_bits"] != 8 * f["n_bytes"]: | |
560 | summary += [" (low %d bits)" % f["n_bits"]] | |
561 | summary += [";%s;" % {"MFM_NONE": "no", "MFM_FULLY": "yes"}[f["mask"]]] | |
562 | summary += ["%s;" % {True: "yes", False: "no"}[f["writable"]]] | |
563 | summary += ["%s;" % f["prereqs"]] | |
564 | support = [] | |
565 | if min_of_version is not None: | |
566 | support += ["OF %s+" % VERSION_REVERSE[min_of_version]] | |
567 | if min_ovs_version is not None: | |
568 | support += ["OVS %s+" % '.'.join([str(x) for x in min_ovs_version])] | |
569 | summary += ' and '.join(support) | |
570 | summary += ["\n"] | |
571 | ||
572 | # Full description. | |
573 | if field_node.hasAttribute('hidden'): | |
574 | return | |
575 | ||
576 | title = field_node.attributes['title'].nodeValue | |
577 | ||
578 | body += [""".PP | |
579 | \\fB%s Field\\fR | |
580 | .TS | |
581 | tab(;); | |
582 | l lx. | |
583 | """ % title] | |
584 | ||
585 | body += ["Name:;\\fB%s\\fR" % f["name"]] | |
586 | if f["extra_name"]: | |
587 | body += [" (aka \\fB%s\\fR)" % f["extra_name"]] | |
588 | body += ['\n'] | |
589 | ||
590 | body += ["Width:;"] | |
591 | if f["n_bits"] != 8 * f["n_bytes"]: | |
592 | body += ["%d bits (only the least-significant %d bits " | |
593 | "may be nonzero)" % (f["n_bytes"] * 8, f["n_bits"])] | |
594 | elif f["n_bits"] <= 128: | |
595 | body += ["%d bits" % f["n_bits"]] | |
178742f9 | 596 | else: |
96fee5e0 BP |
597 | body += ["%d bits (%d bytes)" % (f["n_bits"], f["n_bits"] / 8)] |
598 | body += ['\n'] | |
599 | ||
600 | body += ["Format:;%s\n" % f["formatting"]] | |
601 | ||
602 | masks = {"MFM_NONE": "not maskable", | |
603 | "MFM_FULLY": "arbitrary bitwise masks"} | |
604 | body += ["Masking:;%s\n" % masks[f["mask"]]] | |
605 | body += ["Prerequisites:;%s\n" % f["prereqs"]] | |
606 | ||
607 | access = {True: "read/write", | |
608 | False: "read-only"}[f["writable"]] | |
609 | body += ["Access:;%s\n" % access] | |
610 | ||
611 | of10 = {None: "not supported", | |
612 | "exact match": "yes (exact match only)", | |
613 | "CIDR mask": "yes (CIDR match only)"} | |
614 | body += ["OpenFlow 1.0:;%s\n" % of10[f["OF1.0"]]] | |
615 | ||
616 | of11 = {None: "not supported", | |
617 | "exact match": "yes (exact match only)", | |
618 | "bitwise mask": "yes"} | |
619 | body += ["OpenFlow 1.1:;%s\n" % of11[f["OF1.1"]]] | |
620 | ||
621 | oxms = [] | |
ffc317b9 | 622 | for header, name, of_version_nr, ovs_version in [x for x in sorted(f['OXM'], key=lambda x: x[2]) if is_standard_oxm(x[1])]: |
96fee5e0 BP |
623 | of_version = VERSION_REVERSE[of_version_nr] |
624 | oxms += [r"\fB%s\fR (%d) since OpenFlow %s and Open vSwitch %s" % (name, header[2], of_version, ovs_version)] | |
625 | if not oxms: | |
626 | oxms = ['none'] | |
627 | body += ['OXM:;T{\n%s\nT}\n' % r'\[char59] '.join(oxms)] | |
628 | ||
629 | nxms = [] | |
ffc317b9 | 630 | for header, name, of_version_nr, ovs_version in [x for x in sorted(f['OXM'], key=lambda x: x[2]) if not is_standard_oxm(x[1])]: |
96fee5e0 BP |
631 | nxms += [r"\fB%s\fR (%d) since Open vSwitch %s" % (name, header[2], ovs_version)] |
632 | if not nxms: | |
633 | nxms = ['none'] | |
634 | body += ['NXM:;T{\n%s\nT}\n' % r'\[char59] '.join(nxms)] | |
635 | ||
636 | body += [".TE\n"] | |
637 | ||
638 | body += ['.PP\n'] | |
639 | body += [build.nroff.block_xml_to_nroff(field_node.childNodes)] | |
640 | ||
641 | def group_xml_to_nroff(group_node, fields): | |
642 | title = group_node.attributes['title'].nodeValue | |
643 | ||
644 | summary = [] | |
645 | body = [] | |
646 | for node in group_node.childNodes: | |
647 | if node.nodeType == node.ELEMENT_NODE and node.tagName == 'field': | |
648 | id_ = node.attributes['id'].nodeValue | |
649 | field_to_xml(node, fields[id_], body, summary) | |
650 | else: | |
651 | body += [build.nroff.block_xml_to_nroff([node])] | |
652 | ||
653 | content = [ | |
654 | '.bp\n', | |
655 | '.SH \"%s\"\n' % build.nroff.text_to_nroff(title.upper() + " FIELDS"), | |
656 | '.SS "Summary:"\n', | |
657 | '.TS\n', | |
658 | 'tab(;);\n', | |
659 | 'l l l l l l l.\n', | |
660 | 'Name;Bytes;Mask;RW?;Prereqs;NXM/OXM Support\n', | |
661 | '\_;\_;\_;\_;\_;\_\n'] | |
662 | content += summary | |
663 | content += ['.TE\n'] | |
664 | content += body | |
665 | return ''.join(content) | |
666 | ||
667 | def make_oxm_classes_xml(document): | |
668 | s = '''tab(;); | |
669 | l l l. | |
670 | Prefix;Vendor;Class | |
671 | \_;\_;\_ | |
672 | ''' | |
673 | for key in sorted(OXM_CLASSES, key=OXM_CLASSES.get): | |
ffc317b9 | 674 | vendor, class_, class_type = OXM_CLASSES.get(key) |
96fee5e0 BP |
675 | s += r"\fB%s\fR;" % key.rstrip('_') |
676 | if vendor: | |
677 | s += r"\fL0x%08x\fR;" % vendor | |
678 | else: | |
679 | s += "(none);" | |
680 | s += r"\fL0x%04x\fR;" % class_ | |
681 | s += "\n" | |
682 | e = document.createElement('tbl') | |
683 | e.appendChild(document.createTextNode(s)) | |
684 | return e | |
685 | ||
686 | def recursively_replace(node, name, replacement): | |
687 | for child in node.childNodes: | |
688 | if child.nodeType == node.ELEMENT_NODE: | |
689 | if child.tagName == name: | |
690 | node.replaceChild(replacement, child) | |
691 | else: | |
692 | recursively_replace(child, name, replacement) | |
693 | ||
694 | def make_ovs_fields(meta_flow_h, meta_flow_xml): | |
695 | fields = extract_ofp_fields(meta_flow_h) | |
696 | fields_map = {} | |
697 | for f in fields: | |
698 | fields_map[f['mff']] = f | |
699 | ||
700 | document = xml.dom.minidom.parse(meta_flow_xml) | |
701 | doc = document.documentElement | |
702 | ||
703 | global version | |
704 | if version == None: | |
705 | version = "UNKNOWN" | |
706 | ||
707 | print('''\ | |
708 | '\\" tp | |
709 | .\\" -*- mode: troff; coding: utf-8 -*- | |
710 | .TH "ovs\-fields" 7 "%s" "Open vSwitch" "Open vSwitch Manual" | |
711 | .fp 5 L CR \\" Make fixed-width font available as \\fL. | |
712 | .de ST | |
713 | . PP | |
714 | . RS -0.15in | |
715 | . I "\\\\$1" | |
716 | . RE | |
717 | .. | |
718 | ||
719 | .de SU | |
720 | . PP | |
721 | . I "\\\\$1" | |
722 | .. | |
723 | ||
724 | .de IQ | |
725 | . br | |
726 | . ns | |
727 | . IP "\\\\$1" | |
728 | .. | |
b404f002 BP |
729 | |
730 | .de TQ | |
731 | . br | |
732 | . ns | |
733 | . TP "\\\\$1" | |
734 | .. | |
96fee5e0 BP |
735 | .de URL |
736 | \\\\$2 \\(laURL: \\\\$1 \\(ra\\\\$3 | |
737 | .. | |
738 | .if \\n[.g] .mso www.tmac | |
739 | .SH NAME | |
740 | ovs\-fields \- protocol header fields in OpenFlow and Open vSwitch | |
741 | . | |
742 | .PP | |
d34a1cc0 | 743 | ''' % version) |
96fee5e0 BP |
744 | |
745 | recursively_replace(doc, 'oxm_classes', make_oxm_classes_xml(document)) | |
746 | ||
747 | s = '' | |
748 | for node in doc.childNodes: | |
749 | if node.nodeType == node.ELEMENT_NODE and node.tagName == "group": | |
750 | s += group_xml_to_nroff(node, fields_map) | |
751 | elif node.nodeType == node.TEXT_NODE: | |
752 | assert node.data.isspace() | |
753 | elif node.nodeType == node.COMMENT_NODE: | |
754 | pass | |
755 | else: | |
756 | s += build.nroff.block_xml_to_nroff([node]) | |
a4ce8b25 | 757 | |
96fee5e0 BP |
758 | for f in fields: |
759 | if "used" not in f: | |
8889a8cc BP |
760 | fatal("%s: field not documented " |
761 | "(please add documentation in lib/meta-flow.xml)" | |
762 | % f["mff"]) | |
96fee5e0 BP |
763 | if n_errors: |
764 | sys.exit(1) | |
a4ce8b25 | 765 | |
96fee5e0 BP |
766 | output = [] |
767 | for oline in s.split("\n"): | |
768 | oline = oline.strip() | |
6e06e050 BP |
769 | |
770 | # Life is easier with nroff if we don't try to feed it Unicode. | |
771 | # Fortunately, we only use a few characters outside the ASCII range. | |
772 | oline = oline.replace(u'\u2208', r'\[mo]') | |
773 | oline = oline.replace(u'\u2260', r'\[!=]') | |
774 | oline = oline.replace(u'\u2264', r'\[<=]') | |
775 | oline = oline.replace(u'\u2265', r'\[>=]') | |
776 | oline = oline.replace(u'\u00d7', r'\[mu]') | |
96fee5e0 BP |
777 | if len(oline): |
778 | output += [oline] | |
779 | ||
780 | # nroff tends to ignore .bp requests if they come after .PP requests, | |
781 | # so remove .PPs that precede .bp. | |
782 | for i in range(len(output)): | |
783 | if output[i] == '.bp': | |
784 | j = i - 1 | |
785 | while j >= 0 and output[j] == '.PP': | |
786 | output[j] = None | |
787 | j -= 1 | |
788 | for i in range(len(output)): | |
789 | if output[i] is not None: | |
790 | print(output[i]) | |
791 | \f | |
792 | ## ------------ ## | |
793 | ## Main Program ## | |
794 | ## ------------ ## | |
795 | ||
796 | if __name__ == "__main__": | |
797 | argv0 = sys.argv[0] | |
798 | try: | |
799 | options, args = getopt.gnu_getopt(sys.argv[1:], 'h', | |
800 | ['help', 'ovs-version=']) | |
52e4a477 | 801 | except getopt.GetoptError as geo: |
96fee5e0 BP |
802 | sys.stderr.write("%s: %s\n" % (argv0, geo.msg)) |
803 | sys.exit(1) | |
a4ce8b25 | 804 | |
96fee5e0 BP |
805 | global version |
806 | version = None | |
807 | for key, value in options: | |
808 | if key in ['-h', '--help']: | |
809 | usage() | |
810 | elif key == '--ovs-version': | |
811 | version = value | |
812 | else: | |
813 | sys.exit(0) | |
814 | ||
815 | if not args: | |
816 | sys.stderr.write("%s: missing command argument " | |
817 | "(use --help for help)\n" % argv0) | |
a4ce8b25 | 818 | sys.exit(1) |
96fee5e0 BP |
819 | |
820 | commands = {"meta-flow": (make_meta_flow, 1), | |
821 | "nx-match": (make_nx_match, 1), | |
822 | "ovs-fields": (make_ovs_fields, 2)} | |
823 | ||
824 | if not args[0] in commands: | |
825 | sys.stderr.write("%s: unknown command \"%s\" " | |
826 | "(use --help for help)\n" % (argv0, args[0])) | |
178742f9 | 827 | sys.exit(1) |
96fee5e0 BP |
828 | |
829 | func, n_args = commands[args[0]] | |
830 | if len(args) - 1 != n_args: | |
831 | sys.stderr.write("%s: \"%s\" requires %d arguments but %d " | |
832 | "provided\n" | |
833 | % (argv0, args[0], n_args, len(args) - 1)) | |
834 | sys.exit(1) | |
835 | ||
836 | func(*args[1:]) |