diff --git a/README.md b/README.md index 35eaffc4..0c690046 100644 --- a/README.md +++ b/README.md @@ -596,10 +596,11 @@ The IP configuration supports the following options: Static route configuration can be specified via a list of routes given in the `route` option. The default value is an empty list. Each route is a dictionary with - the following entries: `gateway`, `metric`, `network`, `prefix`, `table` and `type`. - `network` and `prefix` specify the destination network. `table` supports both the - numeric table and named table. In order to specify the named table, the users have to - ensure the named table is properly defined in `/etc/iproute2/rt_tables` or + the following entries: `gateway`, `metric`, `network`, `prefix`, `src`, `table` and + `type`. `network` and `prefix` specify the destination network. `src` specifies the + source IP address for a route. `table` supports both the numeric table and named + table. In order to specify the named table, the users have to ensure the named table + is properly defined in `/etc/iproute2/rt_tables` or `/etc/iproute2/rt_tables.d/*.conf`. The optional `type` key supports the values `blackhole`, `prohibit`, and `unreachable`. See [man 8 ip-route](https://man7.org/linux/man-pages/man8/ip-route.8.html#DESCRIPTION) diff --git a/library/network_connections.py b/library/network_connections.py index 7a16bb69..fa7cf7b1 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1263,6 +1263,10 @@ def connection_create(self, connections, idx, connection_current=None): NM.IPRoute.set_attribute( new_route, "table", Util.GLib().Variant.new_uint32(r["table"]) ) + if r["src"]: + NM.IPRoute.set_attribute( + new_route, "src", Util.GLib().Variant.new_string(r["src"]) + ) if r["family"] == socket.AF_INET: s_ip4.add_route(new_route) diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index f65a7acf..8209b76e 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -680,6 +680,9 @@ def __init__(self, name, family=None, required=False): enum_values=["blackhole", "prohibit", "unreachable"], ), ArgValidatorRouteTable("table"), + ArgValidatorIP( + "src", family=family, default_value=None, plain_address=False + ), ], default_value=None, ) @@ -716,6 +719,16 @@ def _validate_post(self, value, name, result): elif not Util.addr_family_valid_prefix(family, prefix): raise ValidationError(name, "invalid prefix %s in '%s'" % (prefix, value)) + src = result["src"] + if src is not None: + if family != src["family"]: + raise ValidationError( + name, + "conflicting address family between network and src " + "address {0}".format(src["address"]), + ) + result["src"] = src["address"] + return result @@ -2627,6 +2640,12 @@ def _ipv6_is_not_configured(connection): idx, "type is not supported by initscripts", ) + if route["src"] is not None: + raise ValidationError.from_connection( + idx, + "configuring the route source is not supported by initscripts", + ) + if connection["ip"]["routing_rule"]: if mode == self.VALIDATE_ONE_MODE_INITSCRIPTS: raise ValidationError.from_connection( diff --git a/tests/playbooks/tests_route_table.yml b/tests/playbooks/tests_route_table.yml index e128bd9b..ca5ce92b 100644 --- a/tests/playbooks/tests_route_table.yml +++ b/tests/playbooks/tests_route_table.yml @@ -47,6 +47,12 @@ gateway: 198.51.100.6 metric: 4 table: 30200 + - network: 192.0.2.64 + prefix: 26 + gateway: 198.51.100.8 + metric: 50 + table: 30200 + src: 198.51.100.3 - name: Get the routes from the route table 30200 command: ip route show table 30200 @@ -65,6 +71,9 @@ that: - route_table_30200.stdout is search("198.51.100.64/26 via 198.51.100.6 dev ethtest0 proto static metric 4") + - route_table_30200.stdout is search("192.0.2.64/26 via + 198.51.100.8 dev ethtest0 proto static src 198.51.100.3 + metric 50") msg: "the route table 30200 does not exist or does not contain the specified route" @@ -111,6 +120,12 @@ gateway: 198.51.100.6 metric: 4 table: custom + - network: 192.0.2.64 + prefix: 26 + gateway: 198.51.100.8 + metric: 50 + table: custom + src: 198.51.100.3 - name: Get the routes from the named route table 'custom' command: ip route show table custom @@ -126,6 +141,9 @@ 198.51.100.1 dev ethtest0 proto static metric 2") - route_table_custom.stdout is search("198.51.100.64/26 via 198.51.100.6 dev ethtest0 proto static metric 4") + - route_table_custom.stdout is search("192.0.2.64/26 via + 198.51.100.8 dev ethtest0 proto static src 198.51.100.3 + metric 50") msg: "the named route table 'custom' does not exist or does not contain the specified route" diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index cca6d7b0..cfdc01f4 100644 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -239,6 +239,7 @@ def assert_nm_connection_routes_expected(self, connection, route_list_expected): "metric": int(r.get_metric()), "type": r.get_attribute("type"), "table": r.get_attribute("table"), + "src": r.get_attribute("src"), } for r in route_list_new ] @@ -295,6 +296,12 @@ def do_connections_validate_nm(self, input_connections, **kwargs): "table", Util.GLib().Variant.new_uint32(r["table"]), ) + if r["src"]: + NM.IPRoute.set_attribute( + new_route, + "src", + Util.GLib().Variant.new_uint32(r["src"]), + ) if r["family"] == socket.AF_INET: s4.add_route(new_route) else: @@ -1144,6 +1151,7 @@ def test_routes(self): "metric": -1, "type": None, "table": None, + "src": None, } ], "routing_rule": [], @@ -1485,6 +1493,7 @@ def test_vlan(self): "metric": -1, "type": None, "table": None, + "src": None, } ], "routing_rule": [], @@ -1635,6 +1644,7 @@ def test_macvlan(self): "metric": -1, "type": None, "table": None, + "src": None, } ], "routing_rule": [], @@ -1698,6 +1708,7 @@ def test_macvlan(self): "metric": -1, "type": None, "table": None, + "src": None, } ], "routing_rule": [], @@ -2661,6 +2672,7 @@ def test_route_metric_prefix(self): "metric": 545, "type": None, "table": None, + "src": None, }, { "family": socket.AF_INET, @@ -2670,6 +2682,7 @@ def test_route_metric_prefix(self): "metric": -1, "type": None, "table": None, + "src": None, }, ], "routing_rule": [], @@ -2767,6 +2780,7 @@ def test_route_v6(self): "metric": 545, "type": None, "table": None, + "src": None, }, { "family": socket.AF_INET, @@ -2776,6 +2790,7 @@ def test_route_v6(self): "metric": -1, "type": None, "table": None, + "src": None, }, { "family": socket.AF_INET6, @@ -2785,6 +2800,7 @@ def test_route_v6(self): "metric": -1, "type": None, "table": None, + "src": None, }, ], "routing_rule": [], @@ -2923,6 +2939,7 @@ def test_route_without_interface_name(self): "metric": 545, "type": None, "table": None, + "src": None, }, { "family": socket.AF_INET, @@ -2932,6 +2949,7 @@ def test_route_without_interface_name(self): "metric": -1, "type": None, "table": None, + "src": None, }, { "family": socket.AF_INET6, @@ -2941,6 +2959,7 @@ def test_route_without_interface_name(self): "metric": -1, "type": None, "table": None, + "src": None, }, ], "routing_rule": [], @@ -5001,6 +5020,25 @@ def test_type_route_with_gateway(self): self.test_connections, ) + def test_route_with_source_address(self): + """ + Test setting the route with src address specified + """ + self.test_connections[0]["ip"]["route"][0]["src"] = "2001:db8::2" + self.assertRaisesRegex( + ValidationError, + "conflicting address family between network and src " + "address {0}".format( + self.test_connections[0]["ip"]["route"][0]["src"], + ), + self.validator.validate, + self.test_connections, + ) + + self.test_connections[0]["ip"]["route"][0]["src"] = "198.51.100.3" + result = self.validator.validate(self.test_connections) + self.assertEqual(result[0]["ip"]["route"][0]["src"], "198.51.100.3") + class TestValidatorRoutingRules(Python26CompatTestCase): def setUp(self):