Skip to content

Commit bdb837f

Browse files
committed
Add a new sdn controller plugin
This plugin allows to add and delete rules. A rule will be converted into one or more openflow rules. Once converted they are applied using ovs-vsctl command. To be able to validate the application of openflow rules the plugins allows to dump them. Signed-off-by: Guillaume <[email protected]>
1 parent 7b661ff commit bdb837f

File tree

2 files changed

+348
-0
lines changed

2 files changed

+348
-0
lines changed

Diff for: README.md

+94
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,100 @@ Restart a service only if it is already running.
338338
$ xe host-call-plugin host-uuid<uuid> plugin=service.py fn=try_restart_service args:service=<service>
339339
```
340340

341+
342+
### `sdn-controller`
343+
344+
Add, delete rules and dump openflow rules.
345+
346+
#### Add rule
347+
348+
Expected parameters when adding a rule are:
349+
- *bridge* : The name of the bridge on XAPI side
350+
- *mac*: The MAC address of the VIF
351+
- *iprange*: A range of IPs, for example `192.168.1.0/24`
352+
- *direction*: can be **from**, **to** or **from/to**
353+
- *to*: means the parametres **port** and **iprange** are to
354+
be used as destination
355+
- *from*: means they will be use as source
356+
- *from/to*: we create 2 openflow rules, on per direction
357+
- *protocol*: IP, TCP, UDP, ICMP or ARP
358+
- *port*: only valid for TCP/UDP protocol
359+
- *allow*: If set to false the packets are dropped.
360+
361+
```
362+
$ xe host-call-plugin host-uuid<uuid> plugin=sdn-controller.py \
363+
fn=add-rule \
364+
args:bridge="xenbr0" \
365+
args:mac="6e:0b:9e:72:ab:c6" \
366+
args:iprange="192.168.1.0/24" \
367+
args:direction="from/to" \
368+
args:protocol="tcp" \
369+
args:port="22" \
370+
args:allow="true"
371+
```
372+
373+
##### Delete rule
374+
375+
Expected the same arguments than adding rule
376+
377+
```
378+
$ xe host-call-plugin host-uuid<uuid> plugin=sdn-controller.py \
379+
fn=del-rule \
380+
args:bridge="xenbr0" \
381+
args:mac="6e:0b:9e:72:ab:c6" \
382+
args:iprange="192.168.1.0/24" \
383+
args:direction="from/to" \
384+
args:protocol="tcp" \
385+
args:port="22" \
386+
args:allow="true"
387+
```
388+
389+
##### Dump flows
390+
391+
- This command will return all flows entries in the bridge passed as a parameter.
392+
```
393+
$ xe host-call-plugin host-uuid=<uuid> plugin=sdn-controller.py fn=dump-flows args:bridge=xenbr0 | jq .
394+
{
395+
"returncode": 0,
396+
"command": [
397+
"ovs-ofctl",
398+
"dump-flows",
399+
"xenbr0"
400+
],
401+
"stderr": "",
402+
"stdout": "NXST_FLOW reply (xid=0x4):\n cookie=0x0, duration=248977.339s, table=0, n_packets=24591786, n_bytes=3278442075, idle_age=0, hard_age=65534, priority=0 actions=NORMAL\n"
403+
}
404+
```
405+
406+
- This error is raised when the bridge parameter is missing:
407+
```
408+
$ xe host-call-plugin host-uuid=<uuid> plugin=sdn-controller.py fn=dump-flows | jq .
409+
{
410+
"returncode": 1,
411+
"command": [
412+
"ovs-ofctl",
413+
"dump-flows"
414+
],
415+
"stderr": "bridge parameter is missing",
416+
"stdout": ""
417+
}
418+
```
419+
420+
- If the bridge is unknown, the following error will occur:
421+
```
422+
$ xe host-call-plugin host-uuid=<uuid> plugin=sdn-controller.py args:bridge=xenbr10 fn=dump-flows | jq .
423+
{
424+
"returncode": 1,
425+
"command": [
426+
"ovs-ofctl",
427+
"dump-flows",
428+
"xenbr10"
429+
],
430+
"stderr": "ovs-ofctl: xenbr10 is not a bridge or a socket\n",
431+
"stdout": ""
432+
}
433+
```
434+
341435
## Tests
342436

343437
To run the plugins' unit tests you'll need to install `pytest`, `pyfakefs` and `mock`.

Diff for: SOURCES/etc/xapi.d/plugins/sdn-controller.py

+254
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
import json
5+
import re
6+
7+
import XenAPIPlugin
8+
9+
from xcpngutils import configure_logging, error_wrapped, run_command
10+
11+
OPENFLOW_PROTOCOL = "OpenFlow11"
12+
13+
# All functions starting with `parse_` are helper functions compatible with the `Parser` class.
14+
# Each should accept `args` as input and return either (result, None) on success,
15+
# or (None, error_message) on failure.
16+
17+
18+
def parse_bridge(args):
19+
bridge = args.get("bridge")
20+
if bridge is None:
21+
return None, "bridge parameter is missing"
22+
23+
return bridge, None
24+
25+
26+
def parse_mac(args):
27+
MAC_REGEX = re.compile(r"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$")
28+
mac_addr = args.get("mac")
29+
30+
if mac_addr is None:
31+
return None, "mac address is missing"
32+
33+
if not MAC_REGEX.match(mac_addr) or MAC_REGEX.match(mac_addr).group(0) != mac_addr:
34+
return None, "{} is not a valid MAC".format(mac_addr)
35+
36+
return mac_addr, None
37+
38+
39+
def parse_iprange(args):
40+
IPRANGE_REGEX = re.compile(
41+
r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
42+
r"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/\d{1,2})?$"
43+
)
44+
ip_range = args.get("iprange")
45+
46+
if ip_range is None:
47+
return None, "ip range is missing"
48+
49+
if not IPRANGE_REGEX.match(ip_range):
50+
return None, "{} is not a valid IP range".format(ip_range)
51+
52+
return ip_range, None
53+
54+
55+
def parse_direction(args):
56+
direction = args.get("direction")
57+
58+
if direction is None:
59+
return None, None, "direction is missing"
60+
61+
dir = direction.lower().split("/")
62+
has_to = "to" in dir
63+
has_from = "from" in dir
64+
65+
if not (has_to or has_from):
66+
return None, None, "{} is not a valid direction".format(direction)
67+
68+
return (has_to, has_from), None
69+
70+
71+
def parse_protocol(args):
72+
VALID_PROTOCOLS = {"tcp", "udp", "icmp", "ip", "arp"}
73+
protocol = args.get("protocol")
74+
75+
if protocol is None:
76+
return None, "protocol is missing"
77+
78+
if protocol.lower() not in VALID_PROTOCOLS:
79+
return None, "{} is not supported".format(protocol)
80+
81+
return protocol, None
82+
83+
84+
def parse_port(args):
85+
port = args.get("port")
86+
if port is None:
87+
return None, None # Port is optional
88+
89+
try:
90+
p = int(port)
91+
if not (0 <= p <= 65536):
92+
raise ValueError
93+
return port, None
94+
except ValueError:
95+
return None, "{} is not a valid port".format(port)
96+
97+
98+
def parse_allow(args):
99+
allow = args.get("allow")
100+
if allow is None:
101+
return None, "allow parameter is required"
102+
103+
if allow.lower() not in ["true", "false"]:
104+
return None, "allow expects to be true or false, not {}".format(allow)
105+
106+
if allow.lower() == "true":
107+
return True, None
108+
109+
return False, None
110+
111+
112+
class Parser:
113+
def __init__(self, args):
114+
self.args = args
115+
self.values = {}
116+
self.errors = []
117+
118+
def read(self, key, parse_fn, dests=None):
119+
# parse_fn can return a single value or a tuple of values.
120+
# In this case we are expecting dests to match the expected
121+
# returned tuple
122+
val, err = parse_fn(self.args)
123+
if err:
124+
self.errors.append(err)
125+
return self
126+
127+
if dests is not None:
128+
if not isinstance(val, tuple):
129+
self.errors.append(
130+
"Internal error: parse {}: doesn't return a tuple while dests is set".format(
131+
key
132+
)
133+
)
134+
return self
135+
136+
if len(dests) != len(val):
137+
self.errors.append(
138+
"Internal error: parse {}: dests is {} while {} was expected".format(
139+
key, len(dests), len(val)
140+
)
141+
)
142+
return self
143+
144+
for d, v in zip(dests, val):
145+
self.values[d] = v
146+
147+
return self
148+
149+
self.values[key] = val
150+
return self
151+
152+
153+
def json_error(name, desc):
154+
return json.dumps(
155+
{
156+
"returncode": 1,
157+
"command": name,
158+
"stderr": "\n".join(desc),
159+
"stdout": "",
160+
}
161+
)
162+
163+
164+
@error_wrapped
165+
def add_rule(_session, args):
166+
_LOGGER.info("Calling add rule with args {}".format(args))
167+
168+
parser = (
169+
Parser(args)
170+
.read("bridge", parse_bridge)
171+
.read("mac", parse_mac)
172+
.read("destination", parse_direction, dests=["has_to", "has_from"])
173+
.read("protocol", parse_protocol)
174+
.read("iprange", parse_iprange)
175+
.read("port", parse_port)
176+
.read("allow", parse_allow)
177+
)
178+
179+
if parser.errors:
180+
_LOGGER.error("add_rule: Failed to get parameters: {}".format(parser.errors))
181+
return json_error("add_rule", parser.errors)
182+
183+
rule_args = parser.values
184+
_LOGGER.info("successfully parsed: {}".format(rule_args))
185+
186+
ofctl_cmd = ["ovs-ofctl", "-O", OPENFLOW_PROTOCOL, "add-flow", rule_args["bridge"]]
187+
188+
# We can now build the open flow rule
189+
# tcp,dl_dst=26:bf:26:f0:4f:50,nw_src=192.168.38.0/24,tp_src=22 actions=NORMA
190+
if rule_args["has_to"]:
191+
rule = rule_args["protocol"]
192+
rule += ",dl_dst=" + rule_args["mac"]
193+
rule += ",nw_src=" + rule_args["iprange"]
194+
rule += ",tp_src=" + rule_args["port"]
195+
rule += ",actions=" + ("normal" if rule_args["allow"] else "drop")
196+
_LOGGER.info("generates command: {}".format(ofctl_cmd + [rule]))
197+
198+
# tcp,in_port=3,dl_src=26:bf:26:f0:4f:50,nw_dst=192.168.38.0/24,tp_dst=2
199+
if rule_args["has_from"]:
200+
rule = rule_args["protocol"]
201+
rule += ",dl_src=" + rule_args["mac"]
202+
rule += ",nw_dst=" + rule_args["iprange"]
203+
rule += ",tp_dst=" + rule_args["port"]
204+
rule += ",actions=" + ("normal" if rule_args["allow"] else "drop")
205+
_LOGGER.info("generates command: {}".format(ofctl_cmd + [rule]))
206+
207+
return json.dumps(True)
208+
209+
210+
@error_wrapped
211+
def del_rule(_session, args):
212+
_LOGGER.info("Calling delete rule with args {}".format(args))
213+
214+
parser = (
215+
Parser(args)
216+
.read("bridge", parse_bridge)
217+
.read("mac", parse_mac)
218+
.read("destination", parse_direction, dests=["has_to", "has_from"])
219+
.read("protocol", parse_protocol)
220+
.read("iprange", parse_iprange)
221+
.read("port", parse_port)
222+
)
223+
224+
if parser.errors:
225+
_LOGGER.error("del_rule: Failed to get parameters: {}".format(parser.errors))
226+
return json_error("del_rule", parser.errors)
227+
228+
return json.dumps(True)
229+
230+
231+
@error_wrapped
232+
def dump_flows(_session, args):
233+
_LOGGER.info("Calling dump flows with args {}".format(args))
234+
235+
bridge, err = parse_bridge(args)
236+
237+
if err:
238+
_LOGGER.error("dump flows: {}".format(err))
239+
return json_error("dump_flows", [err])
240+
241+
ofctl_cmd = ["ovs-ofctl", "-O", OPENFLOW_PROTOCOL, "dump-flows", bridge]
242+
cmd = run_command(ofctl_cmd, check=False)
243+
return json.dumps(cmd)
244+
245+
246+
_LOGGER = configure_logging("sdn-controller")
247+
if __name__ == "__main__":
248+
XenAPIPlugin.dispatch(
249+
{
250+
"add-rule": add_rule,
251+
"del-rule": del_rule,
252+
"dump-flows": dump_flows,
253+
}
254+
)

0 commit comments

Comments
 (0)