Skip to content

Commit eaa597c

Browse files
committed
Rework traffic-rules
Previous implementation did not take how VLANs are configured into account, this lead to a pretty hefty change. VLANs are using fake bridge, this bridge is then included in a parent brige. As long as packets are inside that bridge and are not going out of it, the packets are not tagged. Therefore, the vlanid cannot be used to match the packets in OVS datapath. The only workaround found is to create the rules for each ports, but this also means rules for non-tagged traffic as they were done previously would apply to the VLAN ports as well. We therefore have to apply rules to each matching ports in all cases. So what that version is doing is: - TODO
1 parent f9a0d07 commit eaa597c

File tree

4 files changed

+953
-682
lines changed

4 files changed

+953
-682
lines changed

SOURCES/etc/xapi.d/plugins/sdncontroller.py

Lines changed: 174 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,26 @@
99

1010
OPENFLOW_PROTOCOL = "OpenFlow11"
1111
OVS_OFCTL_CMD = "ovs-ofctl"
12+
OVS_VSCTL_CMD = "ovs-vsctl"
1213
VALID_PROTOCOLS = {"tcp", "udp", "icmp", "ip", "arp"}
1314
PROTOCOLS_WITH_PORTS = {"tcp", "udp"}
1415

1516
# error codes
1617
E_PARSER = 1
1718
E_PARAMS = 2
18-
E_BUILDER = 3
19-
E_OVSCALL = 4
19+
E_OVSCALL = 3
20+
E_PORTS = 4
21+
22+
# rules names per direction
23+
TO = 0
24+
FROM = 1
25+
2026

2127
def log_and_raise_error(code, desc):
2228
_LOGGER.error(desc)
2329
raise_plugin_error(code, desc)
2430

31+
2532
# All functions starting with `parse_` are helper functions compatible with the `Parser` class.
2633
# Each should accept `args` as input and return either (result, None) on success,
2734
# or (None, error_message) on failure.
@@ -39,7 +46,9 @@ def parse_bridge(self):
3946
log_and_raise_error(E_PARSER, "bridge parameter is missing")
4047

4148
if not BRIDGE_REGEX.match(bridge):
42-
log_and_raise_error(E_PARSER, "'{}' is not a valid bridge name".format(bridge))
49+
log_and_raise_error(
50+
E_PARSER, "'{}' is not a valid bridge name".format(bridge)
51+
)
4352

4453
return bridge
4554

@@ -50,7 +59,10 @@ def parse_mac(self):
5059
if mac_addr is None:
5160
return None
5261

53-
if not MAC_REGEX.match(mac_addr) or MAC_REGEX.match(mac_addr).group(0) != mac_addr:
62+
if (
63+
not MAC_REGEX.match(mac_addr)
64+
or MAC_REGEX.match(mac_addr).group(0) != mac_addr
65+
):
5466
log_and_raise_error(E_PARSER, "'{}' is not a valid MAC".format(mac_addr))
5567

5668
return mac_addr
@@ -60,13 +72,15 @@ def parse_iprange(self):
6072
r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
6173
r"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/\d{1,2})?$"
6274
)
63-
ip_range = self.args.get("iprange")
75+
ip_range = self.args.get("ipRange")
6476

6577
if ip_range is None:
66-
log_and_raise_error(E_PARSER, "ip range parameter is missing")
78+
log_and_raise_error(E_PARSER, "ipRange parameter is missing")
6779

6880
if not IPRANGE_REGEX.match(ip_range):
69-
log_and_raise_error(E_PARSER, "'{}' is not a valid IP range".format(ip_range))
81+
log_and_raise_error(
82+
E_PARSER, "'{}' is not a valid IP range".format(ip_range)
83+
)
7084

7185
return ip_range
7286

@@ -81,7 +95,9 @@ def parse_direction(self):
8195
has_from = "from" in dir
8296

8397
if not (has_to or has_from):
84-
log_and_raise_error(E_PARSER, "'{}' is not a valid direction".format(direction))
98+
log_and_raise_error(
99+
E_PARSER, "'{}' is not a valid direction".format(direction)
100+
)
85101

86102
return (has_to, has_from)
87103

@@ -94,7 +110,9 @@ def parse_protocol(self):
94110
protocol = protocol.lower()
95111

96112
if protocol not in VALID_PROTOCOLS:
97-
log_and_raise_error(E_PARSER, "'{}' is not a supported protocol".format(protocol))
113+
log_and_raise_error(
114+
E_PARSER, "'{}' is not a supported protocol".format(protocol)
115+
)
98116

99117
return protocol
100118

@@ -117,7 +135,10 @@ def parse_allow(self):
117135
log_and_raise_error(E_PARSER, "allow parameter is missing")
118136

119137
if allow.lower() not in ["true", "false"]:
120-
log_and_raise_error(E_PARSER, "allow parameter should be true or false, not '{}'".format(allow))
138+
log_and_raise_error(
139+
E_PARSER,
140+
"allow parameter should be true or false, not '{}'".format(allow),
141+
)
121142

122143
if allow.lower() == "true":
123144
return True
@@ -135,7 +156,9 @@ def parse_priority(self):
135156
raise ValueError
136157
return priority
137158
except ValueError:
138-
log_and_raise_error(E_PARSER, "'{}' is not a valid priority".format(priority))
159+
log_and_raise_error(
160+
E_PARSER, "'{}' is not a valid priority".format(priority)
161+
)
139162

140163
def read(self, key, parse_fn, dests=None):
141164
# parse_fn can return a single value or a tuple of values.
@@ -147,13 +170,17 @@ def read(self, key, parse_fn, dests=None):
147170
if not isinstance(val, tuple):
148171
log_and_raise_error(
149172
E_PARSER,
150-
"Internal error: parse {}: doesn't return a tuple while dests is set".format(key)
173+
"Internal error: parse {}: doesn't return a tuple while dests is set".format(
174+
key
175+
),
151176
)
152177

153178
if len(dests) != len(val):
154179
log_and_raise_error(
155180
E_PARSER,
156-
"Internal error: parse {}: dests is {} while {} was expected".format(key, len(dests), len(val))
181+
"Internal error: parse {}: dests is {} while {} was expected".format(
182+
key, len(dests), len(val)
183+
),
157184
)
158185

159186
for d, v in zip(dests, val):
@@ -163,6 +190,7 @@ def read(self, key, parse_fn, dests=None):
163190

164191
self.values[key] = val
165192

193+
166194
def json_error(name, desc):
167195
return json.dumps(
168196
{
@@ -173,41 +201,102 @@ def json_error(name, desc):
173201
}
174202
)
175203

176-
def build_rules_string(args):
177-
rules = []
178204

179-
# tcp,dl_dst=26:bf:26:f0:4f:50,nw_src=192.168.38.0/24,tp_src=22 actions=NORMA
180-
if args["has_from"]:
181-
rule = ""
182-
if "priority" in args and args["priority"]:
183-
rule += "priority=" + args["priority"] + ","
184-
rule += args["protocol"].lower()
185-
if "mac" in args and args["mac"]:
186-
rule += ",dl_dst=" + args["mac"]
187-
rule += ",nw_src=" + args["iprange"]
188-
if "protocol" in args and args["protocol"] in PROTOCOLS_WITH_PORTS:
189-
rule += ",tp_src=" + args["port"]
190-
if "allow" in args:
191-
rule += ",actions=" + ("normal" if args["allow"] else "drop")
192-
rules += [rule]
193-
194-
# tcp,in_port=3,dl_src=26:bf:26:f0:4f:50,nw_dst=192.168.38.0/24,tp_dst=2
195-
if args["has_to"]:
196-
rule = ""
197-
if "priority" in args and args["priority"]:
198-
rule += "priority=" + args["priority"] + ","
199-
rule += args["protocol"].lower()
200-
if "mac" in args and args["mac"]:
201-
rule += ",dl_src=" + args["mac"]
202-
rule += ",nw_dst=" + args["iprange"]
203-
if "protocol" in args and args["protocol"] in PROTOCOLS_WITH_PORTS:
204-
rule += ",tp_dst=" + args["port"]
205-
if "allow" in args:
206-
rule += ",actions=" + ("normal" if args["allow"] else "drop")
207-
rules += [rule]
205+
def run_vsctl_cmd(args):
206+
vsctl_cmd = [OVS_VSCTL_CMD] + args
207+
cmd = run_command(vsctl_cmd, check=False)
208+
if cmd["returncode"] != 0:
209+
log_and_raise_error(
210+
E_OVSCALL,
211+
"Error running ovs-vsctl command: %s: %s"
212+
% (format(vsctl_cmd), cmd["stderr"]),
213+
)
214+
return cmd["stdout"]
215+
216+
217+
def update_args_from_ovs(args):
218+
# get parent bridge to apply ruels to
219+
args["parent-bridge"] = run_vsctl_cmd(["br-to-parent", args["bridge"]]).rstrip()
208220

221+
# get ports names for our actual bridge, be it fake (vlan) or real (no vlan)
222+
ifs_in_bridge = run_vsctl_cmd(["list-ports", args["bridge"]]).split()
223+
224+
# get the list of all ports and filter them with the ports names to get all interfaces
225+
ports_j = json.loads(run_vsctl_cmd(["--format=json", "list", "port"]))
226+
ports = [dict(zip(ports_j["headings"], row)) for row in ports_j["data"]]
227+
interfaces = []
228+
for port in ports:
229+
if port["name"] in ifs_in_bridge:
230+
interfaces.append(port["interfaces"])
231+
if len(interfaces) == 0:
232+
return
233+
234+
# get the list of all interfaces, filter with what we found previously and get their ofports number
235+
if_j = json.loads(run_vsctl_cmd(["--format=json", "list", "interface"]))
236+
ifs = [dict(zip(if_j["headings"], row)) for row in if_j["data"]]
237+
ofports = []
238+
for interface in ifs:
239+
# port 65534 is the internal port for the bridge, we don't want to use it
240+
if interface["_uuid"] in interfaces and interface["ofport"] != 65534:
241+
ofports.append(interface["ofport"])
242+
243+
args["ofports"] = ofports
244+
245+
246+
def build_rules_strings(args):
247+
rules = []
248+
if args.get("has_to"):
249+
if args.get("ofports"):
250+
for ofport in args["ofports"]:
251+
rules.append(build_rule_string(TO, ofport, args))
252+
253+
if args.get("has_from"):
254+
if args.get("ofports"):
255+
for ofport in args["ofports"]:
256+
rules.append(build_rule_string(FROM, ofport, args))
209257
return rules
210258

259+
260+
def build_rule_string(direction, ofport, args):
261+
rule = ""
262+
# To / From
263+
rule_parts = {
264+
"priority": ("priority", "priority"),
265+
"protocol": (None, None),
266+
"ofport": ("in_port", "in_port"),
267+
"mac": ("dl_src", "dl_dst"),
268+
"iprange": ("nw_dst", "nw_src"),
269+
"port": ("tp_dst", "tp_src"),
270+
"allow": ("actions", "actions"),
271+
}
272+
273+
if args.get("priority"):
274+
rule += "priority={}".format(args["priority"]) + ","
275+
rule += args["protocol"]
276+
if ofport:
277+
rule += "," + rule_parts["ofport"][direction] + "={}".format(ofport)
278+
if args.get("mac"):
279+
rule += "," + rule_parts["mac"][direction] + "={}".format(args["mac"])
280+
rule += "," + rule_parts["iprange"][direction] + "={}".format(args["iprange"])
281+
if args.get("port"):
282+
rule += "," + rule_parts["port"][direction] + "={}".format(args["port"])
283+
if "allow" in args: # optional in case of del_rule
284+
rule += ",actions={}".format("normal" if args["allow"] else "drop")
285+
return rule
286+
287+
288+
def run_ofctl_cmd(cmd, bridge, rule):
289+
ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, cmd, bridge, rule]
290+
cmd = run_command(ofctl_cmd, check=False)
291+
if cmd["returncode"] != 0:
292+
log_and_raise_error(
293+
E_OVSCALL,
294+
"Error running ovs-ofctl command: %s: %s"
295+
% (format(ofctl_cmd), cmd["stderr"]),
296+
)
297+
_LOGGER.info("Applied rule: {}".format(ofctl_cmd))
298+
299+
211300
@error_wrapped
212301
def add_rule(_session, args):
213302
_LOGGER.info("Calling add rule with args {}".format(args))
@@ -223,31 +312,39 @@ def add_rule(_session, args):
223312
parser.read("allow", parser.parse_allow)
224313
parser.read("priority", parser.parse_priority)
225314
except XenAPIPlugin.Failure as e:
226-
log_and_raise_error(E_PARSER, "add_rule: Failed to get parameters: {}".format(e.params[1]))
315+
log_and_raise_error(
316+
E_PARSER, "add_rule: Failed to get parameters: {}".format(e.params[1])
317+
)
227318

228319
if parser.errors:
229-
log_and_raise_error(E_PARSER, "add_rule: Failed to get parameters: {}".format(parser.errors))
320+
log_and_raise_error(
321+
E_PARSER, "add_rule: Failed to get parameters: {}".format(parser.errors)
322+
)
230323

231324
rule_args = parser.values
232-
_LOGGER.info("successfully parsed: {}".format(rule_args))
233325

234326
# sanity check
235327
if rule_args["protocol"] in PROTOCOLS_WITH_PORTS and not rule_args["port"]:
236-
log_and_raise_error(E_PARAMS, "add_rule: No port provided, tcp and udp requires one")
328+
log_and_raise_error(
329+
E_PARAMS, "add_rule: No port provided, tcp and udp requires one"
330+
)
237331

238-
ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, "add-flow", rule_args["bridge"]]
332+
# update vlanid first, as bridge could be replaced by parent bridge
333+
update_args_from_ovs(rule_args)
334+
if rule_args["ofports"] is None:
335+
log_and_raise_error(
336+
E_PORTS, "No ports found for bridge: {}".format(rule_args["bridge"])
337+
)
239338

240339
# We can now build the open flow rule
241-
rules = build_rules_string(rule_args)
340+
rules = build_rules_strings(rule_args)
341+
_LOGGER.info("Built rules: {}".format(rules))
242342

243343
if not rules:
244-
log_and_raise_error(E_BUILDER, "add_rule: No rules were build")
344+
log_and_raise_error(E_PORTS, "add_rule: No rules were build")
245345

246346
for rule in rules:
247-
cmd = run_command(ofctl_cmd + [rule], check=False)
248-
if cmd['returncode'] != 0:
249-
log_and_raise_error(E_OVSCALL, "Error adding rule: %s: %s" % (format(ofctl_cmd + [rule]), cmd['stderr']))
250-
_LOGGER.info("Added rule: {}".format(ofctl_cmd + [rule]))
347+
run_ofctl_cmd("add-flow", rule_args["parent-bridge"], rule)
251348

252349
return json.dumps(True)
253350

@@ -265,28 +362,33 @@ def del_rule(_session, args):
265362
parser.read("iprange", parser.parse_iprange)
266363
parser.read("port", parser.parse_port)
267364
except XenAPIPlugin.Failure as e:
268-
log_and_raise_error(E_PARSER, "del_rule: Failed to get parameters: {}".format(e.params[1]))
365+
log_and_raise_error(
366+
E_PARSER, "del_rule: Failed to get parameters: {}".format(e.params[1])
367+
)
269368

270369
if parser.errors:
271-
log_and_raise_error(E_PARSER, "del_rule: Failed to get parameters: {}".format(parser.errors))
370+
log_and_raise_error(
371+
E_PARSER, "del_rule: Failed to get parameters: {}".format(parser.errors)
372+
)
272373

273374
rule_args = parser.values
274375
_LOGGER.info("successfully parsed: {}".format(rule_args))
275376

276377
# sanity check
277378
if rule_args["protocol"] in PROTOCOLS_WITH_PORTS and not rule_args["port"]:
278-
log_and_raise_error(E_PARAMS, "del_rule: No port provided, tcp and udp requires one")
379+
log_and_raise_error(
380+
E_PARAMS, "del_rule: No port provided, tcp and udp requires one"
381+
)
279382

280-
ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, "del-flows", rule_args["bridge"]]
383+
update_args_from_ovs(rule_args)
281384

282385
# We can now build the open flow rule
283-
rules = build_rules_string(rule_args)
386+
rules = build_rules_strings(rule_args)
387+
_LOGGER.info("Built rules: {}".format(rules))
284388

285389
for rule in rules:
286-
cmd = run_command(ofctl_cmd + [rule], check=False)
287-
if cmd['returncode']:
288-
log_and_raise_error(E_OVSCALL, "Error deleting rule: %s: %s" % (format(ofctl_cmd + [rule]), cmd['stderr']))
289-
_LOGGER.info("Deleted rule: {}".format(ofctl_cmd + [rule]))
390+
run_ofctl_cmd("del-flows", rule_args["parent-bridge"], rule)
391+
290392
return json.dumps(True)
291393

292394

@@ -298,12 +400,17 @@ def dump_flows(_session, args):
298400
try:
299401
bridge = parser.parse_bridge()
300402
except XenAPIPlugin.Failure as e:
301-
log_and_raise_error(E_PARSER, "dump_flows: Failed to get parameters: {}".format(e.params[1]))
403+
log_and_raise_error(
404+
E_PARSER, "dump_flows: Failed to get parameters: {}".format(e.params[1])
405+
)
302406

303407
ofctl_cmd = [OVS_OFCTL_CMD, "-O", OPENFLOW_PROTOCOL, "dump-flows", bridge]
304408
cmd = run_command(ofctl_cmd, check=False)
305-
if cmd['returncode']:
306-
log_and_raise_error(E_OVSCALL, "Error dumping flows: %s: %s" % (format(ofctl_cmd), cmd['stderr']))
409+
if cmd["returncode"]:
410+
log_and_raise_error(
411+
E_OVSCALL,
412+
"Error dumping flows: %s: %s" % (format(ofctl_cmd), cmd["stderr"]),
413+
)
307414
return json.dumps(cmd)
308415

309416

0 commit comments

Comments
 (0)