From 52cbee257c5995bc99110b6a2d107afdee4fcc2a Mon Sep 17 00:00:00 2001 From: Milan Lenco Date: Fri, 16 Aug 2024 11:13:57 +0200 Subject: [PATCH] Add example for Switch NI with multiple ports Switch network instance with multiple ports allows to: * bridge multiple switches and add redundant links. STP is used to avoid bridge loops. * connect end-devices into the same L2 segment as applications running on the edge node. Signed-off-by: Milan Lenco --- go.mod | 2 +- go.sum | 4 +- pkg/defaults/defaults.go | 4 +- .../switch-ni-multiple-ports/README.md | 351 ++++++++++++++++++ .../access-ports/device-config.json | 282 ++++++++++++++ .../access-ports/network-model.json | 163 ++++++++ .../redundant-links/device-config.json | 191 ++++++++++ .../redundant-links/network-model.json | 106 ++++++ sdn/vm/api/endpoints.go | 35 +- sdn/vm/api/netModel.go | 2 + sdn/vm/cmd/sdnagent/config.go | 81 ++-- sdn/vm/cmd/sdnagent/parse.go | 71 ++-- sdn/vm/pkg/configitems/bridge.go | 25 +- 13 files changed, 1248 insertions(+), 69 deletions(-) create mode 100644 sdn/examples/switch-ni-multiple-ports/README.md create mode 100644 sdn/examples/switch-ni-multiple-ports/access-ports/device-config.json create mode 100644 sdn/examples/switch-ni-multiple-ports/access-ports/network-model.json create mode 100644 sdn/examples/switch-ni-multiple-ports/redundant-links/device-config.json create mode 100644 sdn/examples/switch-ni-multiple-ports/redundant-links/network-model.json diff --git a/go.mod b/go.mod index 31c924b7c..9274af15b 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/lf-edge/eden/eserver v0.0.0-20220711180217-6e2bfa9c3f67 github.com/lf-edge/eden/sdn/vm v0.0.0-00010101000000-000000000000 github.com/lf-edge/edge-containers v0.0.0-20240207093504-5dfda0619b80 - github.com/lf-edge/eve-api/go v0.0.0-20240723213520-106b1d6b72e6 + github.com/lf-edge/eve-api/go v0.0.0-20240816135418-f858514b03a3 github.com/mcuadros/go-lookup v0.0.0-20200831155250-80f87a4fa5ee github.com/moby/term v0.0.0-20221205130635-1aeaba878587 github.com/nerd2/gexto v0.0.0-20190529073929-39468ec063f6 diff --git a/go.sum b/go.sum index cf5f57cb2..e92d2e7f8 100644 --- a/go.sum +++ b/go.sum @@ -1345,8 +1345,8 @@ github.com/lf-edge/eden/eserver v0.0.0-20220711180217-6e2bfa9c3f67 h1:lUJ/IASqGL github.com/lf-edge/eden/eserver v0.0.0-20220711180217-6e2bfa9c3f67/go.mod h1:D8XwIk3t45EzFr7Yxf4EE3G9H+4H2nEkvte/2ItzkNk= github.com/lf-edge/edge-containers v0.0.0-20240207093504-5dfda0619b80 h1:kiqB1Rk8fmWci0idN68azRDJfPxCivD3zNDddWZocFw= github.com/lf-edge/edge-containers v0.0.0-20240207093504-5dfda0619b80/go.mod h1:4yXdumKdTzF0URMtxOl8Xnzdxnoy1QR+2dzfOr4CIZY= -github.com/lf-edge/eve-api/go v0.0.0-20240723213520-106b1d6b72e6 h1:5teGmUuRb6ch8C/Mu7pM1H4U+DqbkXc1dU6ygmYaYeA= -github.com/lf-edge/eve-api/go v0.0.0-20240723213520-106b1d6b72e6/go.mod h1:ot6MhAhBXapUDl/hXklaX4kY88T3uC4PTg0D2wD8DzA= +github.com/lf-edge/eve-api/go v0.0.0-20240816135418-f858514b03a3 h1:RyGrCByT/Ip1DlqU37LfyG5MvZ76TIr3eJN7Sv+qbMA= +github.com/lf-edge/eve-api/go v0.0.0-20240816135418-f858514b03a3/go.mod h1:ot6MhAhBXapUDl/hXklaX4kY88T3uC4PTg0D2wD8DzA= github.com/lf-edge/eve/libs/depgraph v0.0.0-20220711144346-0659e3b03496 h1:txHCOKhVsKIZKvKWzyIMe3J+ATKk61o4bADhsdLk42Y= github.com/lf-edge/eve/libs/depgraph v0.0.0-20220711144346-0659e3b03496/go.mod h1:8gtCaEwMJftnaP8PjjgRStLhOoHquzzlmYzj441QwpU= github.com/lf-edge/eve/libs/reconciler v0.0.0-20220711144346-0659e3b03496/go.mod h1:BWuSpe83TvdUmwqhSPa8/gGfapb91OkJ1jNHEnSypr4= diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go index a29e646d9..7ad74ae02 100644 --- a/pkg/defaults/defaults.go +++ b/pkg/defaults/defaults.go @@ -53,8 +53,8 @@ const ( DefaultRegistryPort = 5050 //tags, versions, repos - DefaultEVETag = "13.0.0" // DefaultEVETag tag for EVE image - DefaultAdamTag = "0.0.56" + DefaultEVETag = "13.1.0" // DefaultEVETag tag for EVE image + DefaultAdamTag = "0.0.57" DefaultRedisTag = "7" DefaultRegistryTag = "2.7" DefaultProcTag = "83cfe07" diff --git a/sdn/examples/switch-ni-multiple-ports/README.md b/sdn/examples/switch-ni-multiple-ports/README.md new file mode 100644 index 000000000..d53b34402 --- /dev/null +++ b/sdn/examples/switch-ni-multiple-ports/README.md @@ -0,0 +1,351 @@ +# SDN Example with Switch Network Instance using multiple ports + +Switch network instance with multiple ports allows to: + +* bridge multiple switches and add redundant links. STP is used to avoid bridge loops. +* connect end-devices into the same L2 segment as applications running on the edge node. + +User is able to use shared-label to select multiple network ports for a Switch Network Instance. +EVE automatically runs the Spanning Tree Protocol (STP) for bridge with multiple ports to avoid +bridge loops and broadcast storms. User is able to enable BPDU guard on ports that are supposed +to connect end-devices and therefore are not expected to participate in the STP algorithm. + +With VLAN-enabled Switch NI, the user is able to use a physical network port attached +to Switch NI as a VLAN access port (by default all switch NI ports are configured as trunks). + +## Example 1: Switch NI with redundant links + +In this example, we connect application into a single switch instance with 2 ports +connected into the same L2 segment. Spanning Tree Protocol will pick one of these redundant +links for forwarding while it will block the other one. If the active link goes down, +STP will re-converge. The blocked port will transition from the Blocking state to +the Forwarding state, effectively allowing traffic to flow through the previously +redundant link. + +Here is a diagram depicting the network topology: + +```text + +-----------------+ + | eth0 (EVE mgmt) |---------------------------- + | (DHCP) | | + +-----------------+ | + | ++-----+ +----------+ +-------------------+ | +| app |-->| Switch NI |-->| eth1 (app-shared) | +--------+ +--------+ +------------+ ++-----+ +----------+ | (No IP, L2-only) |------| switch |------| router |---| httpserver | + | +-------------------+ +--------+ +--------+ +------------+ + | | + | +-------------------+ | + ----------->| eth2 (app-shared) |------------ + | (No IP, L2-only) | + +-------------------+ +``` + +Run the example with: + +```shell +make clean && make build-tests +./eden config add default +./eden config set default --key sdn.disable --value false +./eden setup --eve-bootstrap-file $(pwd)/sdn/examples/switch-ni-multiple-ports/redundant-links/device-config.json +./eden start --sdn-network-model $(pwd)/sdn/examples/switch-ni-multiple-ports/redundant-links/network-model.json +./eden eve onboard +./eden controller edge-node set-config --file $(pwd)/sdn/examples/switch-ni-multiple-ports/redundant-links/device-config.json +``` + +Once deployed, check that the bridge is created, runs STP and contains both eth1 and eth2: + +```shell +./eden eve ssh +eve$ brctl showstp bn1 +bn1 + bridge id 8000.00163e060001 + designated root 8000.00163e060001 + root port 0 path cost 0 + max age 20.00 bridge max age 20.00 + hello time 2.00 bridge hello time 2.00 + forward delay 15.00 bridge forward delay 15.00 + ageing time 300.00 + hello timer 1.62 tcn timer 0.00 + topology change timer 0.00 gc timer 41.52 + flags + + +nbu1x1 (3) + port id 8003 state forwarding + designated root 8000.00163e060001 path cost 100 + designated bridge 8000.00163e060001 message age timer 0.00 + designated port 8003 forwrd delay timer 0.00 + designated cost 0 hold timer 0.62 + flags + +eth1 (2) + port id 8002 state forwarding + designated root 8000.00163e060001 path cost 100 + designated bridge 8000.00163e060001 message age timer 0.00 + designated port 8002 forward delay timer 0.00 + designated cost 0 hold timer 0.62 + flags + +eth2 (1) + port id 8001 state forwarding + designated root 8000.00163e060001 path cost 100 + designated bridge 8000.00163e060001 message age timer 0.00 + designated port 8001 forward delay timer 0.00 + designated cost 0 hold timer 0.62 + flags + +``` + +In this case, `bridge id` and `designated root` show the same values, meaning +that this is a STP root bridge and all ports are thus in the Forwarding state. +To avoid bridge loops, the opposite bridge, running inside Eden-SDN must therefore +block one of the ports: + +```shell +./eden sdn ssh +sdn$ brctl showstp br-bridge1 +br-bridge1 + bridge id 8000.02fdb51b8701 + designated root 8000.00163e060001 + root port 2 path cost 4 + max age 20.00 bridge max age 20.00 + hello time 2.00 bridge hello time 2.00 + forward delay 15.00 bridge forward delay 15.00 + ageing time 300.00 + hello timer 0.00 tcn timer 0.00 + topology change timer 0.00 gc timer 54.73 + flags + + +net-br-out-yCqC (3) + port id 8003 state forwarding + designated root 8000.00163e060001 path cost 2 + designated bridge 8000.02fdb51b8701 message age timer 0.00 + designated port 8003 forward delay timer 0.00 + designated cost 4 hold timer 0.70 + flags + +eth1 (1) + port id 8001 state blocking + designated root 8000.00163e060001 path cost 4 + designated bridge 8000.00163e060001 message age timer 19.88 + designated port 8002 forward delay timer 0.00 + designated cost 0 hold timer 0.00 + flags + +eth2 (2) + port id 8002 state forwarding + designated root 8000.00163e060001 path cost 4 + designated bridge 8000.00163e060001 message age timer 19.88 + designated port 8001 forward delay timer 0.00 + designated cost 0 hold timer 0.00 + flags +``` + +Notice that `eth1` is in the blocking state. + +Login to the application and try to access something in the Internet (e.g. 8.8.8.8) +and the HTTP server: + +```shell +./eden eve ssh +eve$ eve attach-app-console 4d88a7c5-64fc-43ee-a58a-f5944bc7872c.1.1/cons +app$ ping -c 3 8.8.8.8 +PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. +64 bytes from 8.8.8.8: icmp_seq=1 ttl=252 time=17.6 ms +64 bytes from 8.8.8.8: icmp_seq=2 ttl=252 time=19.2 ms +64 bytes from 8.8.8.8: icmp_seq=3 ttl=252 time=18.2 ms + +--- 8.8.8.8 ping statistics --- +3 packets transmitted, 3 received, 0% packet loss, time 2004ms +rtt min/avg/max/mdev = 17.563/18.318/19.150/0.650 ms + +app$ curl httpserver.sdn/helloworld +Hello world from HTTP server +``` + +Next, simulate forwarding port (`eth2` in this case) losing the connectivity +by changing the network model: + +```shell +./eden sdn net-model get > net-model +jq '(.ports[] | select(.logicalLabel == "eveport2").adminUP) = false' net-model > net-model-eth2-down +./eden sdn net-model apply net-model-eth2-down +``` + +Then check the STP link states of the non-root bridge (in this case inside SDN). +The previously Blocked port will transition through Listening, Learning and eventually +will reach the Forwarding state: + +```shell +./eden sdn ssh +sdn$ brctl showstp br-bridge1 +... + flags TOPOLOGY_CHANGE +... +eth1 (1) + port id 8001 state forwarding + designated root 8000.00163e060001 path cost 4 + designated bridge 8000.00163e060001 message age timer 19.42 + designated port 8002 forward delay timer 0.00 + designated cost 0 hold timer 0.00 + flags +``` + +You may retry the connectivity tests from the application to confirm a successful failover. + +## Example 2: Switch NI with VLAN access ports + +In this example, we create switch NI instance for VLANs 100 and 200. One application +is deployed inside each VLAN. Eden-SDN provides DHCP and IP gateway services for both VLANs +via trunk port. +Additionally, we add one physical access port for each VLAN and used them to connect +external HTTP servers directly into the segments. + +Here is a diagram depicting the network topology: + +```text + +-----------------+ + | eth0 (EVE mgmt) |--------------------- + | (DHCP) | | + +-----------------+ | + +-----------+ | + | | | + | | +-------------------+ +------------------------+ + | |-------| eth1 (app-shared) |------| router | + | | | (No IP, L2-only) | | (DHCP server per VLAN) | + | Switch NI | +-------------------+ +------------------------+ + | | ++------+ | | +-------------------+ +-------------+ +| app1 |----| |----| eth2 (app-shared) |------| httpserver1 | ++------+ | | | (No IP, L2-only) | +-------------+ + | | +-------------------+ ++------+ | | +| app2 |----| | +-------------------+ +-------------+ ++------+ | |----| eth3 (app-shared) |------| httpserver2 | + | | | (No IP, L2-only) | +-------------+ + +-----------+ +-------------------+ +``` + +```shell +make clean && make build-tests +./eden config add default +./eden config set default --key sdn.disable --value false +./eden setup --eve-bootstrap-file $(pwd)/sdn/examples/switch-ni-multiple-ports/access-ports/device-config.json +./eden start --sdn-network-model $(pwd)/sdn/examples/switch-ni-multiple-ports/access-ports/network-model.json +./eden eve onboard +./eden controller edge-node set-config --file $(pwd)/sdn/examples/switch-ni-multiple-ports/access-ports/device-config.json +``` + +Once deployed, check that the bridge is created and has access VLANs set correctly for eth2, eth3 +and applications VIFs: + +```shell +./eden eve ssh +eve$ eve enter pillar +pillar$ bridge vlan show +port vlan-id +keth0 1 PVID Egress Untagged +eth1 1 PVID Egress Untagged + 100 + 200 +eth2 1 Egress Untagged + 100 PVID Egress Untagged +eth3 1 Egress Untagged + 200 PVID Egress Untagged +eth0 1 PVID Egress Untagged +bn1 1 PVID Egress Untagged +nbu1x1 1 Egress Untagged + 100 PVID Egress Untagged +nbu1x2 1 Egress Untagged + 200 PVID Egress Untagged +``` + +Next, check that the bridge is VLAN-aware: + +```shell +./eden eve ssh +eve$ cat /sys/class/net/bn1/bridge/vlan_filtering +1 +``` + +Next, check that all port connecting endpoints (apps and HTTP servers) have BPDU guard enabled: + +```shell +./eden eve ssh +eve$ cat /sys/class/net/bn1/brif/eth1/bpdu_guard +0 +eve$ cat /sys/class/net/bn1/brif/eth2/bpdu_guard +1 +eve$ cat /sys/class/net/bn1/brif/eth3/bpdu_guard +1 +eve$ cat /sys/class/net/bn1/brif/nbu1x1/bpdu_guard +1 +eve$ cat /sys/class/net/bn1/brif/nbu1x2/bpdu_guard +1 +``` + +Login to the application `app1`, check that the IP address is from the range `10.203.10.0/24` +and try to access HTTP server in the same VLAN and something in the Internet (e.g. 8.8.8.8): + +```shell +./eden eve ssh +eve$ eve attach-app-console cee082fd-3a43-4599-bbd3-8216ffa8652d.1.1/cons +app1$ ifconfig eth0 +eth0 Link encap:Ethernet HWaddr 02:16:3E:2B:F1:BE + inet addr:10.203.10.129 Bcast:10.203.10.255 Mask:255.255.255.0 + UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + RX packets:8 errors:0 dropped:0 overruns:0 frame:0 + TX packets:31 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:1000 + RX bytes:1328 (1.2 KiB) TX bytes:5759 (5.6 KiB) + +app1$ curl httpserver100.sdn/helloworld +Hello world from HTTP server for VLAN 100 + +# Cannot access HTTP server in another VLAN: +app1$ curl --max-time 5 httpserver200.sdn/helloworld +curl: (28) Connection timed out after 5002 milliseconds + +app1$ ping -c 3 8.8.8.8 +PING 8.8.8.8 (8.8.8.8): 56 data bytes +64 bytes from 8.8.8.8: seq=0 ttl=253 time=15.846 ms +64 bytes from 8.8.8.8: seq=1 ttl=253 time=17.075 ms +64 bytes from 8.8.8.8: seq=2 ttl=253 time=15.062 ms +--- 8.8.8.8 ping statistics --- +3 packets transmitted, 3 packets received, 0% packet loss +round-trip min/avg/max = 15.062/15.994/17.075 ms +``` + +Login to the application `app2`, check that the IP address is from the range `10.203.20.0/24` +and try to access HTTP server in the same VLAN and something in the Internet (e.g. 8.8.8.8): + +```shell +./eden eve ssh +eve$ eve attach-app-console 5341bfb9-c828-4f98-807e-e9763d4dc316.1.2/cons +app2$ ifconfig eth0 +eth0 Link encap:Ethernet HWaddr 02:16:3E:8A:6C:7D + inet addr:10.203.20.133 Bcast:10.203.20.255 Mask:255.255.255.0 + UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + RX packets:8 errors:0 dropped:0 overruns:0 frame:0 + TX packets:31 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:1000 + RX bytes:1328 (1.2 KiB) TX bytes:5759 (5.6 KiB) + +app2$ curl httpserver200.sdn/helloworld +Hello world from HTTP server for VLAN 200 + +# Cannot access HTTP server in another VLAN: +app2$ curl --max-time 5 httpserver100.sdn/helloworld +curl: (28) Connection timed out after 5002 milliseconds + +app2$ ping -c 3 8.8.8.8 +PING 8.8.8.8 (8.8.8.8): 56 data bytes +64 bytes from 8.8.8.8: seq=0 ttl=253 time=16.407 ms +64 bytes from 8.8.8.8: seq=1 ttl=253 time=18.493 ms +64 bytes from 8.8.8.8: seq=2 ttl=253 time=14.792 ms +--- 8.8.8.8 ping statistics --- +3 packets transmitted, 3 packets received, 0% packet loss +round-trip min/avg/max = 14.792/16.564/18.493 ms +``` diff --git a/sdn/examples/switch-ni-multiple-ports/access-ports/device-config.json b/sdn/examples/switch-ni-multiple-ports/access-ports/device-config.json new file mode 100644 index 000000000..aeb41abbe --- /dev/null +++ b/sdn/examples/switch-ni-multiple-ports/access-ports/device-config.json @@ -0,0 +1,282 @@ +{ + "deviceIoList": [ + { + "ptype": 1, + "phylabel": "eth0", + "phyaddrs": { + "Ifname": "eth0" + }, + "logicallabel": "eth0", + "assigngrp": "eth0", + "usage": 1, + "usagePolicy": { + "freeUplink": true + } + }, + { + "ptype": 1, + "phylabel": "eth1", + "phyaddrs": { + "Ifname": "eth1" + }, + "logicallabel": "eth1", + "assigngrp": "eth1", + "usage": 2, + "usagePolicy": { + "freeUplink": false + } + }, + { + "ptype": 1, + "phylabel": "eth2", + "phyaddrs": { + "Ifname": "eth2" + }, + "logicallabel": "eth2", + "assigngrp": "eth2", + "usage": 2, + "usagePolicy": { + "freeUplink": false + } + }, + { + "ptype": 1, + "phylabel": "eth3", + "phyaddrs": { + "Ifname": "eth3" + }, + "logicallabel": "eth3", + "assigngrp": "eth3", + "usage": 2, + "usagePolicy": { + "freeUplink": false + } + } + ], + "networks": [ + { + "id": "6605d17b-3273-4108-8e6e-4965441ebe01", + "type": 4, + "ip": { + "dhcp": 4 + } + }, + { + "id": "9d003e2a-d8c4-4b44-a983-98aff1f957ec", + "type": 0, + "ip": { + "dhcp": 2 + } + } + ], + "systemAdapterList": [ + { + "name": "eth0", + "uplink": true, + "networkUUID": "6605d17b-3273-4108-8e6e-4965441ebe01" + }, + { + "name": "eth1", + "sharedLabels": ["switch-port"], + "networkUUID": "9d003e2a-d8c4-4b44-a983-98aff1f957ec" + }, + { + "name": "eth2", + "sharedLabels": ["switch-port", "end-devices"], + "networkUUID": "9d003e2a-d8c4-4b44-a983-98aff1f957ec" + }, + { + "name": "eth3", + "sharedLabels": ["switch-port", "end-devices"], + "networkUUID": "9d003e2a-d8c4-4b44-a983-98aff1f957ec" + } + ], + "networkInstances": [ + { + "uuidandversion": { + "uuid": "207239ec-2d71-4f40-a698-c21c6422fe3d", + "version": "1" + }, + "displayname": "multi-port-switch", + "instType": 1, + "activate": true, + "port": { + "type": 1, + "name": "switch-port" + }, + "cfg": {}, + "ipType": 1, + "ip": {}, + "stp": { + "portsWithBpduGuard": "end-devices" + }, + "disableFlowlog": true, + "vlanAccessPorts": [ + { + "vlanId": 100, + "accessPort": "eth2" + }, + { + "vlanId": 200, + "accessPort": "eth3" + } + ] + } + ], + "apps": [ + { + "uuidandversion": { + "uuid": "cee082fd-3a43-4599-bbd3-8216ffa8652d", + "version": "1" + }, + "displayname": "app1", + "fixedresources": { + "memory": 512000, + "maxmem": 512000, + "vcpus": 1, + "virtualizationMode": 1 + }, + "drives": [ + { + "image": { + "uuidandversion": { + "uuid": "398710ca-bf4f-46b0-b012-0d4e32214ba4", + "version": "1" + }, + "name": "lfedge/eden-eclient:8a279cd", + "iformat": 8, + "dsId": "f204830d-cce1-4316-aa5e-3e8567cd09a9" + } + } + ], + "activate": true, + "interfaces": [ + { + "name": "eth0", + "networkId": "207239ec-2d71-4f40-a698-c21c6422fe3d", + "accessVlanId": 100, + "acls": [ + { + "matches": [ + { + "type": "ip", + "value": "0.0.0.0/0" + } + ], + "id": 1 + } + ] + } + ], + "volumeRefList": [ + { + "uuid": "d8fe3e53-cc6c-4cee-8562-b406a1a8ada7", + "mount_dir": "/" + } + ] + }, + { + "uuidandversion": { + "uuid": "5341bfb9-c828-4f98-807e-e9763d4dc316", + "version": "1" + }, + "displayname": "app2", + "fixedresources": { + "memory": 512000, + "maxmem": 512000, + "vcpus": 1, + "virtualizationMode": 1 + }, + "drives": [ + { + "image": { + "uuidandversion": { + "uuid": "398710ca-bf4f-46b0-b012-0d4e32214ba4", + "version": "1" + }, + "name": "lfedge/eden-eclient:8a279cd", + "iformat": 8, + "dsId": "f204830d-cce1-4316-aa5e-3e8567cd09a9" + } + } + ], + "activate": true, + "interfaces": [ + { + "name": "eth0", + "networkId": "207239ec-2d71-4f40-a698-c21c6422fe3d", + "accessVlanId": 200, + "acls": [ + { + "matches": [ + { + "type": "ip", + "value": "0.0.0.0/0" + } + ], + "id": 1 + } + ] + } + ], + "volumeRefList": [ + { + "uuid": "cee944a3-ae6f-4887-9d8d-adcc0ed02370", + "mount_dir": "/" + } + ] + } + ], + "volumes": [ + { + "uuid": "d8fe3e53-cc6c-4cee-8562-b406a1a8ada7", + "origin": { + "type": 2, + "downloadContentTreeID": "63d3b01f-f44f-4007-ba33-6e720bd52992" + }, + "displayName": "app1-volume" + }, + { + "uuid": "cee944a3-ae6f-4887-9d8d-adcc0ed02370", + "origin": { + "type": 2, + "downloadContentTreeID": "63d3b01f-f44f-4007-ba33-6e720bd52992" + }, + "displayName": "app2-volume" + } + ], + "contentInfo": [ + { + "uuid": "63d3b01f-f44f-4007-ba33-6e720bd52992", + "dsId": "f204830d-cce1-4316-aa5e-3e8567cd09a9", + "URL": "lfedge/eden-eclient:8a279cd", + "iformat": 8, + "displayName": "eden-eclient" + } + ], + "datastores": [ + { + "id": "f204830d-cce1-4316-aa5e-3e8567cd09a9", + "dType": 5, + "fqdn": "docker://index.docker.io" + } + ], + "configItems": [ + { + "key": "newlog.allow.fastupload", + "value": "true" + }, + { + "key": "timer.config.interval", + "value": "10" + }, + { + "key": "timer.download.retry", + "value": "60" + }, + { + "key": "debug.default.loglevel", + "value": "debug" + } + ] +} diff --git a/sdn/examples/switch-ni-multiple-ports/access-ports/network-model.json b/sdn/examples/switch-ni-multiple-ports/access-ports/network-model.json new file mode 100644 index 000000000..406597153 --- /dev/null +++ b/sdn/examples/switch-ni-multiple-ports/access-ports/network-model.json @@ -0,0 +1,163 @@ +{ + "ports": [ + { + "logicalLabel": "eveport0", + "adminUP": true + }, + { + "logicalLabel": "eveport1", + "adminUP": true + }, + { + "logicalLabel": "eveport2", + "adminUP": true + }, + { + "logicalLabel": "eveport3", + "adminUP": true + } + ], + "bridges": [ + { + "logicalLabel": "bridge0", + "ports": ["eveport0"] + }, + { + "logicalLabel": "bridge1", + "ports": ["eveport1"] + }, + { + "logicalLabel": "bridge2", + "ports": ["eveport2"] + }, + { + "logicalLabel": "bridge3", + "ports": ["eveport3"] + } + ], + "networks": [ + { + "logicalLabel": "network0", + "bridge": "bridge0", + "subnet": "172.22.12.0/24", + "gwIP": "172.22.12.1", + "dhcp": { + "enable": true, + "ipRange": { + "fromIP": "172.22.12.10", + "toIP": "172.22.12.20" + }, + "domainName": "sdn", + "privateDNS": ["dns-server"] + }, + "router": { + "outsideReachability": true, + "reachableEndpoints": ["dns-server"] + } + }, + { + "logicalLabel": "network1-100", + "vlanID": 100, + "bridge": "bridge1", + "subnet": "10.203.10.0/24", + "gwIP": "10.203.10.1", + "dhcp": { + "enable": true, + "ipRange": { + "fromIP": "10.203.10.100", + "toIP": "10.203.10.200" + }, + "domainName": "sdn", + "privateDNS": ["dns-server"] + }, + "router": { + "outsideReachability": true, + "reachableEndpoints": ["dns-server"] + } + }, + { + "logicalLabel": "network1-200", + "vlanID": 200, + "bridge": "bridge1", + "subnet": "10.203.20.0/24", + "gwIP": "10.203.20.1", + "dhcp": { + "enable": true, + "ipRange": { + "fromIP": "10.203.20.100", + "toIP": "10.203.20.200" + }, + "domainName": "sdn", + "privateDNS": ["dns-server"] + }, + "router": { + "outsideReachability": true, + "reachableEndpoints": ["dns-server"] + } + } + ], + "endpoints": { + "dnsServers": [ + { + "logicalLabel": "dns-server", + "fqdn": "dns-server.sdn", + "subnet": "10.16.16.0/24", + "ip": "10.16.16.25", + "staticEntries": [ + { + "fqdn": "mydomain.adam", + "ip": "adam-ip" + }, + { + "fqdn": "endpoint-fqdn.httpserver100", + "ip": "endpoint-ip.httpserver100" + }, + { + "fqdn": "endpoint-fqdn.httpserver200", + "ip": "endpoint-ip.httpserver200" + } + ], + "upstreamServers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + ], + "httpServers": [ + { + "logicalLabel": "httpserver100", + "fqdn": "httpserver100.sdn", + "subnet": "10.203.10.0/24", + "ip": "10.203.10.10", + "httpPort": 80, + "directL2Connect": { + "bridge": "bridge2", + "vlanID": 100 + }, + "paths": { + "/helloworld": { + "contentType": "text/plain", + "content": "Hello world from HTTP server for VLAN 100\n" + } + } + }, + { + "logicalLabel": "httpserver200", + "fqdn": "httpserver200.sdn", + "subnet": "10.203.20.0/24", + "ip": "10.203.20.10", + "httpPort": 80, + "directL2Connect": { + "bridge": "bridge3", + "vlanID": 200 + }, + "paths": { + "/helloworld": { + "contentType": "text/plain", + "content": "Hello world from HTTP server for VLAN 200\n" + } + } + } + ] + } +} \ No newline at end of file diff --git a/sdn/examples/switch-ni-multiple-ports/redundant-links/device-config.json b/sdn/examples/switch-ni-multiple-ports/redundant-links/device-config.json new file mode 100644 index 000000000..6f0f71492 --- /dev/null +++ b/sdn/examples/switch-ni-multiple-ports/redundant-links/device-config.json @@ -0,0 +1,191 @@ +{ + "deviceIoList": [ + { + "ptype": 1, + "phylabel": "eth0", + "phyaddrs": { + "Ifname": "eth0" + }, + "logicallabel": "eth0", + "assigngrp": "eth0", + "usage": 1, + "usagePolicy": { + "freeUplink": true + } + }, + { + "ptype": 1, + "phylabel": "eth1", + "phyaddrs": { + "Ifname": "eth1" + }, + "logicallabel": "eth1", + "assigngrp": "eth1", + "usage": 2, + "usagePolicy": { + "freeUplink": false + } + }, + { + "ptype": 1, + "phylabel": "eth2", + "phyaddrs": { + "Ifname": "eth2" + }, + "logicallabel": "eth2", + "assigngrp": "eth2", + "usage": 2, + "usagePolicy": { + "freeUplink": false + } + } + ], + "networks": [ + { + "id": "6605d17b-3273-4108-8e6e-4965441ebe01", + "type": 4, + "ip": { + "dhcp": 4 + } + }, + { + "id": "9d003e2a-d8c4-4b44-a983-98aff1f957ec", + "type": 0, + "ip": { + "dhcp": 2 + } + } + ], + "systemAdapterList": [ + { + "name": "eth0", + "uplink": true, + "networkUUID": "6605d17b-3273-4108-8e6e-4965441ebe01" + }, + { + "name": "eth1", + "sharedLabels": ["switch-port"], + "networkUUID": "9d003e2a-d8c4-4b44-a983-98aff1f957ec" + }, + { + "name": "eth2", + "sharedLabels": ["switch-port"], + "networkUUID": "9d003e2a-d8c4-4b44-a983-98aff1f957ec" + } + ], + "networkInstances": [ + { + "uuidandversion": { + "uuid": "207239ec-2d71-4f40-a698-c21c6422fe3d", + "version": "1" + }, + "displayname": "multi-port-switch", + "instType": 1, + "activate": true, + "port": { + "type": 1, + "name": "switch-port" + }, + "cfg": {}, + "ipType": 1, + "ip": {}, + "disableFlowlog": true + } + ], + "apps": [ + { + "uuidandversion": { + "uuid": "4d88a7c5-64fc-43ee-a58a-f5944bc7872c", + "version": "1" + }, + "displayname": "app", + "fixedresources": { + "memory": 512000, + "maxmem": 512000, + "vcpus": 1, + "virtualizationMode": 1 + }, + "drives": [ + { + "image": { + "uuidandversion": { + "uuid": "398710ca-bf4f-46b0-b012-0d4e32214ba4", + "version": "1" + }, + "name": "lfedge/eden-eclient:8a279cd", + "iformat": 8, + "dsId": "f204830d-cce1-4316-aa5e-3e8567cd09a9" + } + } + ], + "activate": true, + "interfaces": [ + { + "name": "eth0", + "networkId": "207239ec-2d71-4f40-a698-c21c6422fe3d", + "acls": [ + { + "matches": [ + { + "type": "ip", + "value": "0.0.0.0/0" + } + ], + "id": 1 + } + ] + } + ], + "volumeRefList": [ + { + "uuid": "5605093b-c7cf-4beb-bb6b-14d86d39c42b", + "mount_dir": "/" + } + ] + } + ], + "volumes": [ + { + "uuid": "5605093b-c7cf-4beb-bb6b-14d86d39c42b", + "origin": { + "type": 2, + "downloadContentTreeID": "63d3b01f-f44f-4007-ba33-6e720bd52992" + }, + "displayName": "app-volume" + } + ], + "contentInfo": [ + { + "uuid": "63d3b01f-f44f-4007-ba33-6e720bd52992", + "dsId": "f204830d-cce1-4316-aa5e-3e8567cd09a9", + "URL": "lfedge/eden-eclient:8a279cd", + "iformat": 8, + "displayName": "eden-eclient" + } + ], + "datastores": [ + { + "id": "f204830d-cce1-4316-aa5e-3e8567cd09a9", + "dType": 5, + "fqdn": "docker://index.docker.io" + } + ], + "configItems": [ + { + "key": "newlog.allow.fastupload", + "value": "true" + }, + { + "key": "timer.config.interval", + "value": "10" + }, + { + "key": "timer.download.retry", + "value": "60" + }, + { + "key": "debug.default.loglevel", + "value": "debug" + } + ] +} diff --git a/sdn/examples/switch-ni-multiple-ports/redundant-links/network-model.json b/sdn/examples/switch-ni-multiple-ports/redundant-links/network-model.json new file mode 100644 index 000000000..03f523bd0 --- /dev/null +++ b/sdn/examples/switch-ni-multiple-ports/redundant-links/network-model.json @@ -0,0 +1,106 @@ +{ + "ports": [ + { + "logicalLabel": "eveport0", + "adminUP": true + }, + { + "logicalLabel": "eveport1", + "adminUP": true + }, + { + "logicalLabel": "eveport2", + "adminUP": true + } + ], + "bridges": [ + { + "logicalLabel": "bridge0", + "ports": ["eveport0"] + }, + { + "logicalLabel": "bridge1", + "ports": ["eveport1", "eveport2"], + "withSTP": true + } + ], + "networks": [ + { + "logicalLabel": "network0", + "bridge": "bridge0", + "subnet": "172.22.12.0/24", + "gwIP": "172.22.12.1", + "dhcp": { + "enable": true, + "ipRange": { + "fromIP": "172.22.12.10", + "toIP": "172.22.12.20" + }, + "domainName": "sdn", + "privateDNS": ["dns-server"] + }, + "router": { + "outsideReachability": true, + "reachableEndpoints": ["dns-server"] + } + }, + { + "logicalLabel": "network1", + "bridge": "bridge1", + "subnet": "10.203.10.0/24", + "gwIP": "10.203.10.1", + "dhcp": { + "enable": true, + "ipRange": { + "fromIP": "10.203.10.100", + "toIP": "10.203.10.200" + }, + "domainName": "sdn", + "privateDNS": ["dns-server"] + }, + "router": { + "outsideReachability": true, + "reachableEndpoints": ["dns-server", "httpserver"] + } + } + ], + "endpoints": { + "dnsServers": [ + { + "logicalLabel": "dns-server", + "fqdn": "dns-server.sdn", + "subnet": "10.16.16.0/24", + "ip": "10.16.16.25", + "staticEntries": [ + { + "fqdn": "mydomain.adam", + "ip": "adam-ip" + }, + { + "fqdn": "endpoint-fqdn.httpserver", + "ip": "endpoint-ip.httpserver" + } + ], + "upstreamServers": [ + "1.1.1.1", + "8.8.8.8" + ] + } + ], + "httpServers": [ + { + "logicalLabel": "httpserver", + "fqdn": "httpserver.sdn", + "subnet": "10.20.20.0/24", + "ip": "10.20.20.70", + "httpPort": 80, + "paths": { + "/helloworld": { + "contentType": "text/plain", + "content": "Hello world from HTTP server\n" + } + } + } + ] + } +} \ No newline at end of file diff --git a/sdn/vm/api/endpoints.go b/sdn/vm/api/endpoints.go index 354679840..334c37b6e 100644 --- a/sdn/vm/api/endpoints.go +++ b/sdn/vm/api/endpoints.go @@ -69,6 +69,9 @@ type Endpoint struct { Subnet string `json:"subnet"` // IP should be inside of the Subnet. IP string `json:"ip"` + // DirectL2Connect : configure direct L2 connectivity between the endpoint and EVE. + // Use alternatively or additionally to Subnet+IP options. + DirectL2Connect DirectL2EpConnect `json:"directL2Connect"` // MTU of the endpoint's interface. // If not defined (zero value), the default MTU for Ethernet, which is 1500 bytes, // will be set. @@ -85,9 +88,29 @@ func (e Endpoint) ItemLogicalLabel() string { return e.LogicalLabel } -// ReferencesFromItem (overshadowed by endpoint specializations). +// EndpointBridgeRefPrefix : prefix used for references to bridges from endpoints. +const EndpointBridgeRefPrefix = "bridge-endpoint-" + +// ReferencesFromItem can be further extended by endpoint specializations. func (e Endpoint) ReferencesFromItem() []LogicalLabelRef { - return nil + var refs []LogicalLabelRef + if e.DirectL2Connect.Bridge != "" { + refs = append(refs, LogicalLabelRef{ + ItemType: Bridge{}.ItemType(), + ItemLogicalLabel: e.DirectL2Connect.Bridge, + RefKey: EndpointBridgeRefPrefix + e.LogicalLabel, + }) + } + return refs +} + +// DirectL2EpConnect : direct L2 connection between an endpoint and EVE. +type DirectL2EpConnect struct { + // Logical label of a Bridge to which the endpoint is connected. + Bridge string `json:"bridge"` + // Access VLAN ID. + // Leave zero value to express intent of not using VLAN filtering for this endpoint. + VlanID uint16 `json:"vlanID"` } // Client emulates a remote client. @@ -122,7 +145,7 @@ func (e DNSServer) ItemCategory() string { // ReferencesFromItem func (e DNSServer) ReferencesFromItem() []LogicalLabelRef { - var refs []LogicalLabelRef + refs := e.Endpoint.ReferencesFromItem() for i, entry := range e.StaticEntries { if strings.HasPrefix(entry.FQDN, EndpointFQDNRefPrefix) { refKey := fmt.Sprintf("dns-server-%s-entry-%d-fqdn", e.LogicalLabel, i) @@ -200,7 +223,7 @@ func (e HTTPServer) ItemCategory() string { // ReferencesFromItem func (e HTTPServer) ReferencesFromItem() []LogicalLabelRef { - var refs []LogicalLabelRef + refs := e.Endpoint.ReferencesFromItem() for _, dns := range e.PrivateDNS { refs = append(refs, LogicalLabelRef{ ItemType: Endpoint{}.ItemType(), @@ -271,7 +294,7 @@ func (e ExplicitProxy) ItemCategory() string { // ReferencesFromItem func (e ExplicitProxy) ReferencesFromItem() []LogicalLabelRef { - var refs []LogicalLabelRef + refs := e.Endpoint.ReferencesFromItem() for _, dns := range e.PrivateDNS { refs = append(refs, LogicalLabelRef{ ItemType: Endpoint{}.ItemType(), @@ -338,7 +361,7 @@ func (e TransparentProxy) ItemCategory() string { // ReferencesFromItem lists references to private DNS servers (if there are any). func (e TransparentProxy) ReferencesFromItem() []LogicalLabelRef { - refs := make([]LogicalLabelRef, 0, len(e.PrivateDNS)) + refs := e.Endpoint.ReferencesFromItem() for _, dns := range e.PrivateDNS { refs = append(refs, LogicalLabelRef{ ItemType: Endpoint{}.ItemType(), diff --git a/sdn/vm/api/netModel.go b/sdn/vm/api/netModel.go index 1e8bec28a..654b6b0a9 100644 --- a/sdn/vm/api/netModel.go +++ b/sdn/vm/api/netModel.go @@ -167,6 +167,8 @@ type Bridge struct { Ports []string `json:"ports"` // Logical labels of bonds. Bonds []string `json:"bonds"` + // WithSTP: enable to run the Spanning Tree Protocol (STP). + WithSTP bool `json:"withSTP"` } // ItemType diff --git a/sdn/vm/cmd/sdnagent/config.go b/sdn/vm/cmd/sdnagent/config.go index 0f0c3b7a5..0a11ac209 100644 --- a/sdn/vm/cmd/sdnagent/config.go +++ b/sdn/vm/cmd/sdnagent/config.go @@ -257,16 +257,25 @@ func (a *agent) getIntendedBridges() dg.Graph { }, nil) } for _, bridge := range a.netModel.Bridges { - var vlans []uint16 + vlans := make(map[uint16]struct{}) labeledItem := a.netModel.items.getItem(api.Bridge{}.ItemType(), bridge.LogicalLabel) for refKey, refBy := range labeledItem.referencedBy { if strings.HasPrefix(refKey, api.NetworkBridgeRefPrefix) { network := a.netModel.items[refBy].LabeledItem if vlanID := network.(api.Network).VlanID; vlanID != 0 { - vlans = append(vlans, vlanID) + vlans[vlanID] = struct{}{} + } + } else if strings.HasPrefix(refKey, api.EndpointBridgeRefPrefix) { + endpoint := a.getEndpoint(refBy.logicalLabel) + if vlanID := endpoint.DirectL2Connect.VlanID; vlanID != 0 { + vlans[vlanID] = struct{}{} } } } + var vlanList []uint16 + for vlanID := range vlans { + vlanList = append(vlanList, vlanID) + } var physIfs []configitems.PhysIf var bonds []string for _, ref := range labeledItem.referencing { @@ -290,8 +299,9 @@ func (a *agent) getIntendedBridges() dg.Graph { LogicalLabel: bridge.LogicalLabel, PhysIfs: physIfs, BondIfs: bonds, - VLANs: vlans, + VLANs: vlanList, MTU: maxMTU, + WithSTP: bridge.WithSTP, }, nil) } return intendedCfg @@ -429,13 +439,17 @@ func (a *agent) getIntendedNetwork(network api.Network) dg.Graph { }, GwIP: outIP.IP, }, nil) - // - route for every endpoint + // - route for every L3-connected endpoint epTypename := api.Endpoint{}.ItemType() for itemID, item := range a.netModel.items { if itemID.typename != epTypename { continue } ep := a.labeledItemToEndpoint(item) + if ep.DirectL2Connect.Bridge != "" { + // This endpoint has direct L2 connection to EVE, skip. + continue + } _, epSubnet, _ := net.ParseCIDR(ep.Subnet) reachable := network.Router == nil || strListContains(network.Router.ReachableEndpoints, ep.LogicalLabel) @@ -827,9 +841,6 @@ func (a *agent) getIntendedHttpSrvEp(httpSrv api.HTTPServer) dg.Graph { func (a *agent) putEpCommonConfig(graph dg.Graph, ep api.Endpoint, dnsClient *api.DNSClientConfig) { vethName, inIfName, outIfName := a.endpointVethName(ep.LogicalLabel) - _, subnet, _ := net.ParseCIDR(ep.Subnet) // already validated - epIP := &net.IPNet{IP: net.ParseIP(ep.IP), Mask: subnet.Mask} - gwIP := a.genEndpointGwIP(subnet, epIP.IP) nsName := a.endpointNsName(ep.LogicalLabel) netNs := configitems.NetNamespace{ NsName: nsName, @@ -849,35 +860,61 @@ func (a *agent) putEpCommonConfig(graph dg.Graph, ep api.Endpoint, dnsClient *ap } } graph.PutItem(netNs, nil) + // Prepare IP config. + l2Direct := ep.DirectL2Connect.Bridge != "" + var subnet, epIP, gwIP *net.IPNet + var epIPs, gwIPs []*net.IPNet + if ep.IP != "" && ep.Subnet != "" { + // ep.Subnet and ep.IP are already validated + _, subnet, _ = net.ParseCIDR(ep.Subnet) + epIP = &net.IPNet{IP: net.ParseIP(ep.IP), Mask: subnet.Mask} + epIPs = append(epIPs, epIP) + } + if !l2Direct && subnet != nil && epIP != nil { + gwIP = a.genEndpointGwIP(subnet, epIP.IP) + gwIPs = append(gwIPs, gwIP) + } + // Connect endpoint using a VETH. + var masterBridge *configitems.MasterBridge + if l2Direct { + masterBridge = &configitems.MasterBridge{ + IfName: a.bridgeIfName(ep.DirectL2Connect.Bridge), + VLAN: ep.DirectL2Connect.VlanID, + } + } graph.PutItem(configitems.Veth{ VethName: vethName, Peer1: configitems.VethPeer{ IfName: inIfName, NetNamespace: nsName, - IPAddresses: []*net.IPNet{epIP}, + IPAddresses: epIPs, MTU: ep.MTU, }, Peer2: configitems.VethPeer{ IfName: outIfName, NetNamespace: configitems.MainNsName, - IPAddresses: []*net.IPNet{gwIP}, + IPAddresses: gwIPs, MTU: ep.MTU, + MasterBridge: masterBridge, }, }, nil) - defaultDst := allIPv4 - isIPv6 := len(subnet.IP) == net.IPv6len - if isIPv6 { - defaultDst = allIPv6 + // Configure default route. + if !l2Direct && subnet != nil && gwIP != nil { + defaultDst := allIPv4 + isIPv6 := len(subnet.IP) == net.IPv6len + if isIPv6 { + defaultDst = allIPv6 + } + graph.PutItem(configitems.Route{ + NetNamespace: nsName, + DstNet: defaultDst, + OutputIf: configitems.RouteOutIf{ + VethName: vethName, + VethPeerIfName: inIfName, + }, + GwIP: gwIP.IP, + }, nil) } - graph.PutItem(configitems.Route{ - NetNamespace: nsName, - DstNet: defaultDst, - OutputIf: configitems.RouteOutIf{ - VethName: vethName, - VethPeerIfName: inIfName, - }, - GwIP: gwIP.IP, - }, nil) } func (a *agent) bondIfName(logicalLabel string) string { diff --git a/sdn/vm/cmd/sdnagent/parse.go b/sdn/vm/cmd/sdnagent/parse.go index d9bc648b3..26da43f81 100644 --- a/sdn/vm/cmd/sdnagent/parse.go +++ b/sdn/vm/cmd/sdnagent/parse.go @@ -210,18 +210,21 @@ func (a *agent) validateNetworks(netModel *parsedNetModel) (err error) { } } - // Do not mix VLAN and non-VLAN network with the same bridge - for _, bridge := range a.netModel.Bridges { + // Do not mix VLAN and non-VLAN network/endpoint with the same bridge + for _, bridge := range netModel.Bridges { var netWithVlan, netWithoutVlan bool - labeledItem := a.netModel.items.getItem(api.Bridge{}.ItemType(), bridge.LogicalLabel) + labeledItem := netModel.items.getItem(api.Bridge{}.ItemType(), bridge.LogicalLabel) for refKey, refBy := range labeledItem.referencedBy { - if !strings.HasPrefix(refKey, api.NetworkBridgeRefPrefix) { - continue + var vlanID uint16 + if strings.HasPrefix(refKey, api.NetworkBridgeRefPrefix) { + network := netModel.items[refBy].LabeledItem + vlanID = network.(api.Network).VlanID + } else if strings.HasPrefix(refKey, api.EndpointBridgeRefPrefix) { + endpoint := a.labeledItemToEndpoint(netModel.items[refBy]) + vlanID = endpoint.DirectL2Connect.VlanID } - network := a.netModel.items[refBy].LabeledItem - vlanID := network.(api.Network).VlanID if (vlanID == 0 && netWithVlan) || (vlanID != 0 && netWithoutVlan) { - err = fmt.Errorf("bridge %s with both VLAN and non-VLAN networks", + err = fmt.Errorf("bridge %s with both VLAN and non-VLAN networks/endpoints", bridge.LogicalLabel) return } @@ -427,40 +430,38 @@ func (a *agent) validateEndpoints(netModel *parsedNetModel) (err error) { func (a *agent) validateEndpoint(endpoint api.Endpoint) (err error) { // Validate Subnet. - _, subnet, err := net.ParseCIDR(endpoint.Subnet) - if err != nil { - err = fmt.Errorf("endpoint %s with invalid subnet '%s': %w", - endpoint.LogicalLabel, endpoint.Subnet, err) - return - } - ones, bits := subnet.Mask.Size() - if bits-ones < 2 { - err = fmt.Errorf("endpoint %s uses subnet with less than 2 host IPs (%s)", - endpoint.LogicalLabel, endpoint.Subnet) - return - } - // Validate IP address. - ip := net.ParseIP(endpoint.IP) - if ip == nil { - err = fmt.Errorf("endpoint %s with invalid IP address (%s)", - endpoint.LogicalLabel, endpoint.IP) - return - } - if !subnet.Contains(ip) { - err = fmt.Errorf("endpoint %s has IP (%s) address outside of the configured "+ - "subnet (%s)", endpoint.LogicalLabel, endpoint.IP, endpoint.Subnet) - return + if endpoint.Subnet != "" { + _, subnet, err := net.ParseCIDR(endpoint.Subnet) + if err != nil { + return fmt.Errorf("endpoint %s with invalid subnet '%s': %w", + endpoint.LogicalLabel, endpoint.Subnet, err) + } + ones, bits := subnet.Mask.Size() + if bits-ones < 2 { + return fmt.Errorf("endpoint %s uses subnet with less than 2 host IPs (%s)", + endpoint.LogicalLabel, endpoint.Subnet) + } + // Validate IP address. + if endpoint.IP != "" { + ip := net.ParseIP(endpoint.IP) + if ip == nil { + return fmt.Errorf("endpoint %s with invalid IP address (%s)", + endpoint.LogicalLabel, endpoint.IP) + } + if !subnet.Contains(ip) { + return fmt.Errorf("endpoint %s has IP (%s) address outside of the configured "+ + "subnet (%s)", endpoint.LogicalLabel, endpoint.IP, endpoint.Subnet) + } + } } // Validate MTU settings. if endpoint.MTU != 0 && endpoint.MTU < minMTU { - err = fmt.Errorf("MTU %d configured for endpoint %s is too small", + return fmt.Errorf("MTU %d configured for endpoint %s is too small", endpoint.MTU, endpoint.LogicalLabel) - return } if endpoint.MTU > maxMTU { - err = fmt.Errorf("MTU %d configured for endpoint %s is too large", + return fmt.Errorf("MTU %d configured for endpoint %s is too large", endpoint.MTU, endpoint.LogicalLabel) - return } return nil } diff --git a/sdn/vm/pkg/configitems/bridge.go b/sdn/vm/pkg/configitems/bridge.go index 61a18e946..cdd13c5d8 100644 --- a/sdn/vm/pkg/configitems/bridge.go +++ b/sdn/vm/pkg/configitems/bridge.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "os" "os/exec" "reflect" @@ -28,6 +29,8 @@ type Bridge struct { VLANs []uint16 // MTU : Maximum transmission unit size. MTU uint16 + // WithSTP: enable to run the Spanning Tree Protocol (STP). + WithSTP bool } // Name @@ -52,7 +55,7 @@ func (b Bridge) Equal(other depgraph.Item) bool { reflect.DeepEqual(b.PhysIfs, b2.PhysIfs) && reflect.DeepEqual(b.BondIfs, b2.BondIfs) && reflect.DeepEqual(b.VLANs, b2.VLANs) && - b.MTU == b2.MTU + b.MTU == b2.MTU && b.WithSTP == b2.WithSTP } // External returns false. @@ -364,6 +367,26 @@ func (c *BridgeConfigurator) handleModify(oldBridgeCfg, newBridgeCfg Bridge) (er return err } } + // Update the STP state. + if err = c.startOrStopSTP(ifName, newBridgeCfg.WithSTP); err != nil { + log.Error(err) + return err + } + return nil +} + +func (c *BridgeConfigurator) startOrStopSTP(brIfName string, start bool) error { + sysOptVal := "0" + action := "stop" + if start { + sysOptVal = "1" + action = "start" + } + sysOptPath := fmt.Sprintf("/sys/class/net/%s/bridge/stp_state", brIfName) + err := os.WriteFile(sysOptPath, []byte(sysOptVal), 0644) + if err != nil { + return fmt.Errorf("failed to %s STP for bridge %s: %w", action, brIfName, err) + } return nil }