Skip to content

Commit 510989f

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 510989f

File tree

2 files changed

+218
-0
lines changed

2 files changed

+218
-0
lines changed

Diff for: README.md

+91
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,97 @@ 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+
360+
```
361+
$ xe host-call-plugin host-uuid<uuid> plugin=sdn-controller.py \
362+
fn=add-rule \
363+
args:bridge="xenbr0" \
364+
args:mac="6e:0b:9e:72:ab:c6" \
365+
args:iprange="192.168.1.0/24" \
366+
args:direction="from/to" \
367+
args:protocol="tcp" \
368+
args:port="22"
369+
```
370+
371+
##### Delete rule
372+
373+
Expected the same arguments than adding rule
374+
375+
```
376+
$ xe host-call-plugin host-uuid<uuid> plugin=sdn-controller.py \
377+
fn=del-rule \
378+
args:bridge="xenbr0" \
379+
args:mac="6e:0b:9e:72:ab:c6" \
380+
args:iprange="192.168.1.0/24" \
381+
args:direction="from/to" \
382+
args:protocol="tcp" \
383+
args:port="22"
384+
```
385+
386+
##### Dump flows
387+
388+
- This command will return all flows entries in the bridge passed as a parameter.
389+
```
390+
$ xe host-call-plugin host-uuid=<uuid> plugin=sdn-controller.py fn=dump-flows args:bridge=xenbr0 | jq .
391+
{
392+
"returncode": 0,
393+
"command": [
394+
"ovs-ofctl",
395+
"dump-flows",
396+
"xenbr0"
397+
],
398+
"stderr": "",
399+
"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"
400+
}
401+
```
402+
403+
- This error is raised when the bridge parameter is missing:
404+
```
405+
$ xe host-call-plugin host-uuid=<uuid> plugin=sdn-controller.py fn=dump-flows | jq .
406+
{
407+
"returncode": 1,
408+
"command": [
409+
"ovs-ofctl",
410+
"dump-flows"
411+
],
412+
"stderr": "bridge parameter is missing",
413+
"stdout": ""
414+
}
415+
```
416+
417+
- If the bridge is unknown, the following error will occur:
418+
```
419+
$ xe host-call-plugin host-uuid=<uuid> plugin=sdn-controller.py args:bridge=xenbr10 fn=dump-flows | jq .
420+
{
421+
"returncode": 1,
422+
"command": [
423+
"ovs-ofctl",
424+
"dump-flows",
425+
"xenbr10"
426+
],
427+
"stderr": "ovs-ofctl: xenbr10 is not a bridge or a socket\n",
428+
"stdout": ""
429+
}
430+
```
431+
341432
## Tests
342433

343434
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

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
12+
def parse_bridge(args):
13+
bridge = args.get("bridge")
14+
if bridge is None:
15+
raise ValueError("bridge parameter is missing")
16+
return bridge
17+
18+
19+
def parse_mac(args):
20+
MAC_REGEX = re.compile(r"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$")
21+
mac_addr = args.get("mac")
22+
if mac_addr is None:
23+
raise ValueError("mac address is missing")
24+
if not MAC_REGEX.match(mac_addr) or MAC_REGEX.match(mac_addr).group(0) != mac_addr:
25+
raise ValueError("{} is not a valid MAC".format(mac_addr))
26+
return mac_addr
27+
28+
29+
def parse_iprange(args):
30+
IPRANGE_REGEX = re.compile(
31+
r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
32+
r"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(/\d{1,2})?$"
33+
)
34+
ip_range = args.get("iprange")
35+
if ip_range is None:
36+
raise ValueError("ip range is missing")
37+
if not IPRANGE_REGEX.match(ip_range):
38+
raise ValueError("{} is not a valid IP range".format(ip_range))
39+
return ip_range
40+
41+
42+
def parse_direction(args):
43+
# Check that direction is set and valid
44+
direction = args.get("direction")
45+
if direction is None:
46+
raise ValueError("direction is missing")
47+
48+
dir = direction.lower().split("/")
49+
has_to = "to" in dir
50+
has_from = "from" in dir
51+
if not (has_to or has_from):
52+
raise ValueError("{} is not a valid direction".format(direction))
53+
return has_to, has_from
54+
55+
56+
def parse_protocol(args):
57+
VALID_PROTOCOLS = {"tcp", "udp", "icmp", "ip"}
58+
protocol = args.get("protocol")
59+
if protocol is None:
60+
raise ValueError("protocol is missing")
61+
if protocol.lower() not in VALID_PROTOCOLS:
62+
raise ValueError("{} is not supported")
63+
return protocol
64+
65+
66+
def parse_port(args):
67+
port = args.get("port")
68+
if port is None:
69+
raise ValueError("port is missing")
70+
try:
71+
int(port)
72+
return port
73+
except ValueError:
74+
raise ValueError("{} is not a valid port".format(port))
75+
76+
77+
@error_wrapped
78+
def add_rule(_session, args):
79+
_LOGGER.info("Calling add_rule with args {}".format(args))
80+
81+
try:
82+
_bridge = parse_bridge(args)
83+
_mac = parse_mac(args)
84+
_has_to, _has_from = parse_direction(args)
85+
_protocol = parse_protocol(args)
86+
_iprange = parse_iprange(args)
87+
_port = parse_port(args)
88+
except ValueError as e:
89+
_LOGGER.error(str(e))
90+
return json.dumps(
91+
{
92+
"returncode": 1,
93+
"command": "add_rule",
94+
"stderr": str(e),
95+
"stdout": "",
96+
}
97+
)
98+
99+
return json.dumps(True)
100+
101+
102+
@error_wrapped
103+
def del_rule(_session, args):
104+
_LOGGER.info("Calling del_rule with args {}".format(args))
105+
return json.dumps(True)
106+
107+
108+
@error_wrapped
109+
def dump_flows(_session, args):
110+
_LOGGER.info("Calling check with args {}".format(args))
111+
ofctl_cmd = ["ovs-ofctl", "dump-flows"]
112+
bridge = args.get("bridge")
113+
114+
ofctl_cmd.append(bridge)
115+
cmd = run_command(ofctl_cmd, check=False)
116+
return json.dumps(cmd)
117+
118+
119+
_LOGGER = configure_logging("sdn-controller")
120+
if __name__ == "__main__":
121+
XenAPIPlugin.dispatch(
122+
{
123+
"add-rule": add_rule,
124+
"del-rule": del_rule,
125+
"dump-flows": dump_flows,
126+
}
127+
)

0 commit comments

Comments
 (0)