Skip to content

Commit b07bf94

Browse files
committed
add traffic rules tests:
- simple VIF rule (add/delete with simple vm.start/destroy cycle) - simple Network rule (add/delete with simple vm.start/destroy cycle) - migrate with simple VIF rule - migrate with Network rule rule - VLAN with simple VIF rule - VLAN with simple Network rule - Tunnel with simple VIF rule - Tunnel with simple Network rule fixtures: - add connected_hosts_with_xo: returns the list of hosts which are already connected to xo-cli - add VLAN fixture: returns a configured VLAN network lib - add VLAN abstraction Signed-off-by: Sebastien Marie <semarie@kapouay.eu.org>
1 parent 09c7ddc commit b07bf94

File tree

10 files changed

+952
-9
lines changed

10 files changed

+952
-9
lines changed

conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,11 @@ def hosts_with_xo(hosts, registered_xo_cli):
267267
logging.info("<<< Disconnect host %s" % h)
268268
h.xo_server_remove()
269269

270+
@pytest.fixture(scope='session')
271+
def connected_hosts_with_xo(hosts: list[Host], registered_xo_cli):
272+
connected = [h for h in hosts if not h.skip_xo_config and h.xo_server_connected()]
273+
yield connected
274+
270275
@pytest.fixture(scope='session')
271276
def hostA1(hosts):
272277
""" Master of first pool (pool A). """

jobs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,8 @@
470470
"tests/migration/test_host_evacuate.py::TestHostEvacuateWithNetwork",
471471
# not really broken but has complex prerequisites (3 NICs)
472472
"tests/network/test_bond.py::test_bond",
473+
# not really broken but has complex prerequisites (xo-cli + 2 hosts)
474+
"tests/network/test_traffic_rules.py",
473475
# running quicktest on zfsvol generates dangling TAP devices that are hard to
474476
# cleanup. Bug needs to be fixed before enabling quicktest on zfsvol.
475477
"tests/storage/zfsvol/test_zfsvol_sr.py::TestZfsvolVm::test_quicktest",

lib/host.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
from lib.network import Network
3737
from lib.pif import PIF
3838
from lib.sr import SR
39+
from lib.tunnel import Tunnel
3940
from lib.vdi import VDI
41+
from lib.vlan import VLAN
4042
from lib.vm import VM
4143
from lib.xo import xo_cli, xo_object_exists
4244

@@ -808,3 +810,35 @@ def create_network(self, label: str, description: Optional[str] = None) -> Netwo
808810
logging.info(f"New Network: {uuid}")
809811

810812
return Network(self, uuid)
813+
814+
def create_vlan(self, network: Network, pif: PIF, vlan: int) -> VLAN:
815+
args: dict[str, str | bool] = {
816+
'network-uuid': network.uuid,
817+
'pif-uuid': pif.uuid,
818+
'vlan': str(vlan),
819+
}
820+
821+
untagged_pif_uuid = self.xe("vlan-create", args, minimal=True)
822+
uuid = self.xe("pif-param-get", {
823+
"uuid": untagged_pif_uuid,
824+
"param-name": "vlan-master-of",
825+
})
826+
logging.info(f"New VLAN: {uuid} (untagged-pif: {untagged_pif_uuid})")
827+
828+
return VLAN(self, uuid)
829+
830+
def create_tunnel(self, network: Network, pif: PIF, protocol: str) -> Tunnel:
831+
args: dict[str, str | bool] = {
832+
'network-uuid': network.uuid,
833+
'pif-uuid': pif.uuid,
834+
'protocol': protocol,
835+
}
836+
837+
access_pif_uuid = self.xe("tunnel-create", args, minimal=True)
838+
uuid = self.xe("pif-param-get", {
839+
"uuid": access_pif_uuid,
840+
"param-name": "tunnel-access-PIF-of",
841+
})
842+
logging.info(f"New Tunnel: {uuid} (access-pif: {access_pif_uuid})")
843+
844+
return Tunnel(self, uuid)

lib/network.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,8 @@ def managed(self) -> bool:
4949

5050
def MTU(self) -> int:
5151
return int(self.param_get('MTU') or '0')
52+
53+
def bridge(self) -> str:
54+
bridge = self.param_get('bridge')
55+
assert bridge is not None, "network must have a bridge"
56+
return bridge

lib/pif.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ def network_uuid(self) -> str:
5151
assert uuid is not None, "unexpected PIF without network-uuid"
5252
return uuid
5353

54+
def ip_configuration_mode(self) -> str:
55+
mode = self.param_get("IP-configuration-mode")
56+
assert mode
57+
return mode
58+
59+
def vlan(self) -> int | None:
60+
vlan = self.param_get('VLAN')
61+
if vlan is None:
62+
return None
63+
else:
64+
return int(vlan)
65+
5466
def reconfigure_ip(self, mode: str) -> None:
5567
self.host.xe("pif-reconfigure-ip", {
5668
"uuid": self.uuid,

lib/tunnel.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set
6+
from lib.pif import PIF
7+
8+
from typing import TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from lib.host import Host
12+
13+
class Tunnel:
14+
xe_prefix = "tunnel"
15+
16+
def __init__(self, host: Host, uuid: str):
17+
self.host = host
18+
self.uuid = uuid
19+
20+
def param_get(self, param_name, key=None, accept_unknown_key=False):
21+
return _param_get(self.host, Tunnel.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
22+
23+
def param_set(self, param_name, value, key=None):
24+
_param_set(self.host, Tunnel.xe_prefix, self.uuid, param_name, value, key)
25+
26+
def param_add(self, param_name, value, key=None):
27+
_param_add(self.host, Tunnel.xe_prefix, self.uuid, param_name, value, key)
28+
29+
def param_clear(self, param_name):
30+
_param_clear(self.host, Tunnel.xe_prefix, self.uuid, param_name)
31+
32+
def param_remove(self, param_name, key, accept_unknown_key=False):
33+
_param_remove(self.host, Tunnel.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
34+
35+
def destroy(self):
36+
logging.info(f"Destroying Tunnel: {self.uuid}")
37+
self.host.xe('tunnel-destroy', {'uuid': self.uuid})
38+
39+
def access_PIF(self) -> PIF:
40+
uuid = self.param_get("access-PIF")
41+
assert uuid
42+
return PIF(uuid, self.host)
43+
44+
def transport_PIF(self) -> PIF:
45+
uuid = self.param_get("transport-PIF")
46+
assert uuid
47+
return PIF(uuid, self.host)

lib/vif.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22

33
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set
4+
from lib.network import Network
45

56
class VIF:
67
xe_prefix = "vif"
@@ -42,6 +43,11 @@ def mac_address(self) -> str:
4243
assert mac_address is not None, "VIF must have a MAC address"
4344
return mac_address
4445

46+
def network(self) -> Network:
47+
network_uuid = self.param_get('network-uuid')
48+
assert network_uuid is not None, "VIF must have a network-uuid"
49+
return Network(self.vm.host, network_uuid)
50+
4551
def plug(self):
4652
logging.info("Plugging VIF %s on VM %s", self.param_get('device'), self.vm.uuid)
4753
self.vm.host.xe('vif-plug', {'uuid': self.uuid})

lib/vlan.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
5+
from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set
6+
from lib.pif import PIF
7+
8+
from typing import TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from lib.host import Host
12+
13+
class VLAN:
14+
xe_prefix = "vlan"
15+
16+
def __init__(self, host: Host, uuid: str):
17+
self.host = host
18+
self.uuid = uuid
19+
20+
def param_get(self, param_name, key=None, accept_unknown_key=False):
21+
return _param_get(self.host, VLAN.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
22+
23+
def param_set(self, param_name, value, key=None):
24+
_param_set(self.host, VLAN.xe_prefix, self.uuid, param_name, value, key)
25+
26+
def param_add(self, param_name, value, key=None):
27+
_param_add(self.host, VLAN.xe_prefix, self.uuid, param_name, value, key)
28+
29+
def param_clear(self, param_name):
30+
_param_clear(self.host, VLAN.xe_prefix, self.uuid, param_name)
31+
32+
def param_remove(self, param_name, key, accept_unknown_key=False):
33+
_param_remove(self.host, VLAN.xe_prefix, self.uuid, param_name, key, accept_unknown_key)
34+
35+
def destroy(self):
36+
logging.info(f"Destroying VLAN: {self.uuid}")
37+
self.host.xe('vlan-destroy', {'uuid': self.uuid})
38+
39+
def tag(self) -> int:
40+
tag = self.param_get('tag')
41+
assert tag
42+
return int(tag)
43+
44+
def tagged_PIF(self) -> PIF:
45+
uuid = self.param_get("tagged-PIF")
46+
assert uuid
47+
return PIF(uuid, self.host)
48+
49+
def untagged_PIF(self) -> PIF:
50+
uuid = self.param_get("untagged-PIF")
51+
assert uuid
52+
return PIF(uuid, self.host)

tests/network/conftest.py

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
from __future__ import annotations
2+
13
import pytest
24

35
import logging
46

7+
from lib.bond import Bond
58
from lib.host import Host
69
from lib.network import Network
10+
from lib.tunnel import Tunnel
11+
from lib.vlan import VLAN
12+
from lib.xo import xo_cli
713

814
from typing import Generator
915

@@ -14,14 +20,7 @@ def host_no_sdn_controller(host: Host):
1420
pytest.skip("This test requires an XCP-ng with no SDN controller")
1521

1622

17-
@pytest.fixture(scope='module')
18-
def empty_network(host: Host) -> Generator[Network, None, None]:
19-
try:
20-
net = host.create_network(label="empty_network for tests")
21-
yield net
22-
finally:
23-
net.destroy()
24-
23+
# ---- Bond ----
2524
@pytest.fixture(params=[])
2625
def bond_devices(request: pytest.FixtureRequest) -> list[str]:
2726
return request.param
@@ -31,7 +30,7 @@ def bond_mode(request: pytest.FixtureRequest) -> str:
3130
return request.param
3231

3332
@pytest.fixture
34-
def bond(host: Host, empty_network: Network, bond_devices: list[str], bond_mode: str):
33+
def bond(host: Host, empty_network: Network, bond_devices: list[str], bond_mode: str) -> Generator[Bond, None, None]:
3534
pifs = []
3635
logging.info(f"bond: resolve PIFs on {host.hostname_or_ip} using \
3736
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
@@ -44,3 +43,106 @@ def bond(host: Host, empty_network: Network, bond_devices: list[str], bond_mode:
4443
yield bond
4544
finally:
4645
bond.destroy()
46+
47+
48+
# ---- Network ----
49+
@pytest.fixture(scope='module')
50+
def empty_network(host: Host) -> Generator[Network, None, None]:
51+
try:
52+
net = host.create_network(label="empty_network for tests")
53+
yield net
54+
finally:
55+
net.destroy()
56+
57+
58+
# ---- Tunnel ----
59+
@pytest.fixture(params=["eth0"])
60+
def tunnel_device(request: pytest.FixtureRequest) -> str:
61+
return request.param
62+
63+
@pytest.fixture(params=["gre", "vxlan"])
64+
def tunnel_protocol(request: pytest.FixtureRequest) -> str:
65+
return request.param
66+
67+
@pytest.fixture(params=[False, True])
68+
def tunnel_encryption(request: pytest.FixtureRequest) -> bool:
69+
return request.param
70+
71+
@pytest.fixture
72+
def tunnel(
73+
connected_hosts_with_xo: list[Host],
74+
tunnel_device: str, tunnel_protocol: str, tunnel_encryption: bool,
75+
) -> Generator[Tunnel, None, None]:
76+
host = connected_hosts_with_xo[0]
77+
78+
# check system requirements
79+
if host.ssh_with_result("rpm -q openvswitch-ipsec").returncode != 0:
80+
pytest.skip("'tunnel' fixture requires configuration, see https://docs.xen-orchestra.com/sdn_controller")
81+
82+
logging.info(f"tunnel: resolve PIF on {host.hostname_or_ip} using \
83+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
84+
85+
[pif] = host.pifs(device=tunnel_device)
86+
if pif.ip_configuration_mode() == "None":
87+
pytest.skip(f"'tunnel' fixture requires tunnel_device={tunnel_device} to have configured IP")
88+
89+
xo_cli('sdnController.createPrivateNetwork', {
90+
'poolIds': f"json:[\"{host.pool.uuid}\"]",
91+
'pifIds': f"json:[\"{pif.uuid}\"]",
92+
'name': 'test-tunnel',
93+
'description': 'tunnel for test',
94+
'encapsulation': tunnel_protocol,
95+
'encrypted': 'true' if tunnel_encryption else 'false',
96+
})
97+
tunnel = None
98+
network = None
99+
try:
100+
# get Tunnel from PIF
101+
tunnel_uuid = host.xe('tunnel-list', {
102+
'transport-PIF': pif.uuid,
103+
}, minimal=True)
104+
tunnel = Tunnel(host, tunnel_uuid)
105+
106+
# get Network from Tunnel
107+
network_uuid = tunnel.access_PIF().network_uuid()
108+
network = Network(host, network_uuid)
109+
110+
yield tunnel
111+
finally:
112+
if tunnel is not None:
113+
tunnel.destroy()
114+
115+
if network is not None:
116+
# sdnController.createPrivateNetwork might have create several Tunnel (one per host)
117+
# so get all Tunnel attached to Network and destroy them
118+
for pif_uuid in network.pif_uuids():
119+
tunnel_uuid = host.xe('tunnel-list', {
120+
'access-PIF': pif_uuid,
121+
}, minimal=True)
122+
tunnel = Tunnel(host, tunnel_uuid)
123+
tunnel.destroy()
124+
125+
# finally destroy the Network
126+
network.destroy()
127+
128+
129+
# ---- VLAN ----
130+
@pytest.fixture(params=["eth0"])
131+
def vlan_device(request: pytest.FixtureRequest) -> str:
132+
return request.param
133+
134+
@pytest.fixture(params=[0])
135+
def vlan_tag(request: pytest.FixtureRequest) -> int:
136+
return request.param
137+
138+
@pytest.fixture
139+
def vlan(host: Host, empty_network: Network, vlan_tag: int, vlan_device: str) -> Generator[VLAN, None, None]:
140+
logging.info(f"vlan: resolve PIF on {host.hostname_or_ip} using \
141+
{[(pif.network_uuid(), pif.param_get('device')) for pif in host.pifs()]}")
142+
143+
[pif] = host.pifs(device=vlan_device)
144+
vlan = host.create_vlan(empty_network, pif, vlan_tag)
145+
try:
146+
yield vlan
147+
finally:
148+
vlan.destroy()

0 commit comments

Comments
 (0)