ifstate 1.13.6__py3-none-any.whl → 2.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hooks/wrapper.sh +50 -0
- ifstate/ifstate.py +7 -2
- ifstate/vrrp.py +1 -1
- {ifstate-1.13.6.dist-info → ifstate-2.0.0rc1.dist-info}/METADATA +1 -1
- ifstate-2.0.0rc1.dist-info/RECORD +38 -0
- {ifstate-1.13.6.dist-info → ifstate-2.0.0rc1.dist-info}/WHEEL +1 -1
- libifstate/__init__.py +93 -41
- libifstate/hook/__init__.py +195 -0
- libifstate/link/__init__.py +0 -1
- libifstate/link/base.py +34 -36
- libifstate/link/physical.py +2 -2
- libifstate/link/tun.py +2 -2
- libifstate/link/veth.py +2 -2
- libifstate/netns/__init__.py +49 -13
- libifstate/parser/base.py +107 -84
- libifstate/routing/__init__.py +233 -113
- libifstate/util.py +146 -147
- libifstate/wireguard/__init__.py +16 -16
- schema/2/ifstate.conf.schema.json +4398 -0
- ifstate-1.13.6.dist-info/RECORD +0 -37
- libifstate/link/dsa.py +0 -10
- schema/ifstate.conf.schema.json +0 -4259
- {ifstate-1.13.6.dist-info → ifstate-2.0.0rc1.dist-info}/entry_points.txt +0 -0
- {ifstate-1.13.6.dist-info → ifstate-2.0.0rc1.dist-info}/licenses/LICENSE +0 -0
- {ifstate-1.13.6.dist-info → ifstate-2.0.0rc1.dist-info}/top_level.txt +0 -0
libifstate/routing/__init__.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from libifstate.util import logger, IfStateLogging
|
2
2
|
from libifstate.exception import RouteDuplicate, netlinkerror_classes
|
3
|
-
from ipaddress import ip_address, ip_network, IPv6Network
|
3
|
+
from ipaddress import ip_address, ip_interface, ip_network, IPv6Network
|
4
4
|
from pyroute2.netlink.rtnl.fibmsg import FR_ACT_VALUES
|
5
5
|
from pyroute2.netlink.rtnl import rt_type
|
6
6
|
import collections.abc
|
@@ -15,6 +15,147 @@ from socket import AF_INET, AF_INET6
|
|
15
15
|
def route_matches(r1, r2, fields=('dst', 'priority', 'proto'), indent=None):
|
16
16
|
return _matches(r1, r2, fields, indent)
|
17
17
|
|
18
|
+
def parse_route(route, implicit_defaults=True):
|
19
|
+
rt = {}
|
20
|
+
|
21
|
+
if implicit_defaults:
|
22
|
+
rt['type'] = route.get('type', 'unicast')
|
23
|
+
|
24
|
+
if 'to' in route:
|
25
|
+
dst = ip_network(route['to'])
|
26
|
+
rt['dst'] = dst.with_prefixlen
|
27
|
+
has_to = True
|
28
|
+
else:
|
29
|
+
has_to = False
|
30
|
+
|
31
|
+
for key, lookup in RT_LOOKUPS_DICT.items():
|
32
|
+
try:
|
33
|
+
if implicit_defaults:
|
34
|
+
rt[key] = route.get(key, RT_LOOKUPS_DEFAULTS[key])
|
35
|
+
rt[key] = lookup.lookup_id(rt[key])
|
36
|
+
else:
|
37
|
+
if key in route:
|
38
|
+
rt[key] = route[key]
|
39
|
+
rt[key] = lookup.lookup_id(rt[key])
|
40
|
+
except KeyError as err:
|
41
|
+
# mapping not available - catch exception and skip it
|
42
|
+
logger.warning('ignoring unknown %s "%s"', key, rt[key],
|
43
|
+
extra={'iface': rt['dst']})
|
44
|
+
rt[key] = RT_LOOKUPS_DEFAULTS[key]
|
45
|
+
|
46
|
+
if 'type' in rt and type(rt['type']) == str:
|
47
|
+
rt['type'] = rt_type[rt['type']]
|
48
|
+
|
49
|
+
if 'dev' in route:
|
50
|
+
rt['oif'] = route['dev']
|
51
|
+
|
52
|
+
if 'via' in route:
|
53
|
+
via = ip_address(route['via'])
|
54
|
+
|
55
|
+
if not has_to or via.version == dst.version:
|
56
|
+
rt['gateway'] = str(via)
|
57
|
+
else:
|
58
|
+
if via.version == 4:
|
59
|
+
rt['via'] = {
|
60
|
+
'family': int(AF_INET),
|
61
|
+
'addr': str(via),
|
62
|
+
}
|
63
|
+
else:
|
64
|
+
rt['via'] = {
|
65
|
+
'family': int(AF_INET6),
|
66
|
+
'addr': str(via),
|
67
|
+
}
|
68
|
+
|
69
|
+
if 'src' in route:
|
70
|
+
rt['prefsrc'] = route['src']
|
71
|
+
|
72
|
+
if 'preference' in route:
|
73
|
+
rt['priority'] = route['preference']
|
74
|
+
elif has_to and implicit_defaults:
|
75
|
+
if isinstance(dst, IPv6Network):
|
76
|
+
rt['priority'] = 1024
|
77
|
+
else:
|
78
|
+
rt['priority'] = 0
|
79
|
+
|
80
|
+
if 'vrrp' in route:
|
81
|
+
rt['_vrrp'] = route['vrrp']
|
82
|
+
|
83
|
+
return rt
|
84
|
+
|
85
|
+
def parse_routes(routes, implicit_defaults=True):
|
86
|
+
return [parse_route(route, implicit_defaults) for route in routes]
|
87
|
+
|
88
|
+
def parse_rule(rule, implicit_defaults=True):
|
89
|
+
ru = {}
|
90
|
+
|
91
|
+
if implicit_defaults:
|
92
|
+
ru['table'] = RTLookups.tables.lookup_id(rule.get('table', 254))
|
93
|
+
ru['protocol'] = RTLookups.protos.lookup_id(rule.get('proto', 0))
|
94
|
+
ru['tos'] = rule.get('tos', 0)
|
95
|
+
else:
|
96
|
+
if 'table' in rule:
|
97
|
+
ru['table'] = RTLookups.tables.lookup_id(rule['table'])
|
98
|
+
if 'proto' in rule:
|
99
|
+
ru['protocol'] = RTLookups.protos.lookup_id(rule['proto'])
|
100
|
+
if 'tos' in rule:
|
101
|
+
ru['tos'] = rule['tos']
|
102
|
+
|
103
|
+
if 'action' in rule and type(rule['action']) == str:
|
104
|
+
ru['action'] = {
|
105
|
+
"to_tbl": "FR_ACT_TO_TBL",
|
106
|
+
"unicast": "FR_ACT_UNICAST",
|
107
|
+
"blackhole": "FR_ACT_BLACKHOLE",
|
108
|
+
"unreachable": "FR_ACT_UNREACHABLE",
|
109
|
+
"prohibit": "FR_ACT_PROHIBIT",
|
110
|
+
"nat": "FR_ACT_NAT",
|
111
|
+
}.get(rule['action'], rule['action'])
|
112
|
+
elif implicit_defaults:
|
113
|
+
ru['action'] = "FR_ACT_TO_TBL"
|
114
|
+
|
115
|
+
if 'fwmark' in rule:
|
116
|
+
ru['fwmark'] = rule['fwmark']
|
117
|
+
|
118
|
+
if 'ipproto' in rule:
|
119
|
+
if type(rule['ipproto']) == str:
|
120
|
+
ru['ip_proto'] = socket.getprotobyname(rule['ipproto'])
|
121
|
+
else:
|
122
|
+
ru['ip_proto'] = rule['ipproto']
|
123
|
+
|
124
|
+
if 'to' in rule:
|
125
|
+
ru['dst'] = str(ip_network(rule['to']).network_address)
|
126
|
+
ru['dst_len'] = ip_network(rule['to']).prefixlen
|
127
|
+
|
128
|
+
if 'from' in rule:
|
129
|
+
if rule['from'] != "all":
|
130
|
+
ru['src'] = str(ip_interface(rule['from']).ip)
|
131
|
+
ru['src_len'] = ip_network(ip_interface(rule['from']).network).prefixlen
|
132
|
+
else:
|
133
|
+
ru['src'] = "0.0.0.0"
|
134
|
+
ru['src_len'] = 0
|
135
|
+
|
136
|
+
if 'iif' in rule:
|
137
|
+
ru['iifname'] = rule['iif']
|
138
|
+
|
139
|
+
if 'oif' in rule:
|
140
|
+
ru['oifname'] = rule['oif']
|
141
|
+
|
142
|
+
if 'priority' in rule:
|
143
|
+
ru['priority'] = rule['priority']
|
144
|
+
|
145
|
+
if 'vrrp' in rule:
|
146
|
+
ru['_vrrp'] = rule['vrrp']
|
147
|
+
|
148
|
+
if 'family' in rule:
|
149
|
+
assert rule['family'] in ("AF_INET", "inet", 2, "AF_INET6", "inet6", 10), "rule['family'] doesn't have a valid value. This should have already been rejected by the schema validation."
|
150
|
+
if rule['family'] in ("AF_INET", "inet", 2):
|
151
|
+
ru['family'] = AF_INET
|
152
|
+
elif rule['family'] in ("AF_INET6", "inet6", 10):
|
153
|
+
ru['family'] = AF_INET6
|
154
|
+
|
155
|
+
return ru
|
156
|
+
|
157
|
+
def parse_rules(rules, implicit_defaults=True):
|
158
|
+
return [parse_rule(rule, implicit_defaults) for rule in rules]
|
18
159
|
|
19
160
|
def rule_matches(r1, r2, fields=('priority', 'iif', 'oif', 'dst', 'metric', 'protocol'), indent=None):
|
20
161
|
return _matches(r1, r2, fields, indent)
|
@@ -133,64 +274,36 @@ class Tables(collections.abc.Mapping):
|
|
133
274
|
def __len__(self):
|
134
275
|
return len(self.tables)
|
135
276
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
rt[key] = RT_LOOKUPS_DEFAULTS[key]
|
152
|
-
|
153
|
-
if type(rt['type']) == str:
|
154
|
-
rt['type'] = rt_type[rt['type']]
|
155
|
-
|
156
|
-
if 'dev' in route:
|
157
|
-
rt['oif'] = route['dev']
|
158
|
-
|
159
|
-
if 'via' in route:
|
160
|
-
via = ip_address(route['via'])
|
161
|
-
|
162
|
-
if via.version == dst.version:
|
163
|
-
rt['gateway'] = str(via)
|
164
|
-
else:
|
165
|
-
if via.version == 4:
|
166
|
-
rt['via'] = {
|
167
|
-
'family': int(AF_INET),
|
168
|
-
'addr': str(via),
|
169
|
-
}
|
170
|
-
else:
|
171
|
-
rt['via'] = {
|
172
|
-
'family': int(AF_INET6),
|
173
|
-
'addr': str(via),
|
174
|
-
}
|
175
|
-
|
176
|
-
if 'src' in route:
|
177
|
-
rt['prefsrc'] = route['src']
|
277
|
+
def parse_ignores(self, ignores):
|
278
|
+
parsed = []
|
279
|
+
for ignore in ignores:
|
280
|
+
ignore = parse_route(ignore, False)
|
281
|
+
|
282
|
+
for attr in ['dev', 'oif', 'iif']:
|
283
|
+
if attr in ignore and type(ignore[attr]) == str:
|
284
|
+
idx = next(
|
285
|
+
iter(self.netns.ipr.link_lookup(ifname=ignore[attr])), None)
|
286
|
+
if idx is None:
|
287
|
+
logger.debug("{} '{}' is unknown, skipping ignore".format(attr, ignore[attr]))
|
288
|
+
continue
|
289
|
+
else:
|
290
|
+
ignore[attr] = idx
|
291
|
+
parsed.append(ignore)
|
178
292
|
|
179
|
-
|
180
|
-
rt['priority'] = route['preference']
|
181
|
-
elif isinstance(dst, IPv6Network):
|
182
|
-
rt['priority'] = 1024
|
183
|
-
else:
|
184
|
-
rt['priority'] = 0
|
293
|
+
return parsed
|
185
294
|
|
186
|
-
|
187
|
-
|
295
|
+
def add(self, route):
|
296
|
+
# convert config dict into IFLA keys and values
|
297
|
+
rt = parse_route(route)
|
188
298
|
|
299
|
+
# track route in the corresponding table
|
189
300
|
if not rt['table'] in self.tables:
|
190
301
|
self.tables[rt['table']] = []
|
191
302
|
self.tables[rt['table']].append(rt)
|
192
303
|
|
193
304
|
def show_routes(self, ignores):
|
305
|
+
ignores = self.parse_ignores(ignores)
|
306
|
+
|
194
307
|
routes = []
|
195
308
|
for route in self.netns.ipr.get_routes(family=AF_INET) + self.netns.ipr.get_routes(family=AF_INET6):
|
196
309
|
# skip routes from local table
|
@@ -208,7 +321,7 @@ class Tables(collections.abc.Mapping):
|
|
208
321
|
continue
|
209
322
|
|
210
323
|
if route['dst_len'] > 0:
|
211
|
-
dst =
|
324
|
+
dst = ip_interface(
|
212
325
|
'{}/{}'.format(route.get_attr('RTA_DST'), route['dst_len'])).with_prefixlen
|
213
326
|
elif route['family'] == AF_INET:
|
214
327
|
dst = ip_network('0.0.0.0/0').with_prefixlen
|
@@ -322,6 +435,8 @@ class Tables(collections.abc.Mapping):
|
|
322
435
|
return routes
|
323
436
|
|
324
437
|
def apply(self, ignores, do_apply, by_vrrp, vrrp_type, vrrp_name, vrrp_state):
|
438
|
+
ignores = self.parse_ignores(ignores)
|
439
|
+
|
325
440
|
for table, croutes in self.tables.items():
|
326
441
|
log_str = RTLookups.tables.lookup_str(table)
|
327
442
|
if self.netns.netns != None:
|
@@ -413,53 +528,10 @@ class Rules():
|
|
413
528
|
self.rules = []
|
414
529
|
|
415
530
|
def add(self, rule):
|
416
|
-
|
417
|
-
|
418
|
-
'protocol': RTLookups.protos.lookup_id(rule.get('proto', 0)),
|
419
|
-
'tos': rule.get('tos', 0),
|
420
|
-
}
|
421
|
-
|
422
|
-
if 'action' in rule and type(rule['action']) == str:
|
423
|
-
ru['action'] = {
|
424
|
-
"to_tbl": "FR_ACT_TO_TBL",
|
425
|
-
"unicast": "FR_ACT_UNICAST",
|
426
|
-
"blackhole": "FR_ACT_BLACKHOLE",
|
427
|
-
"unreachable": "FR_ACT_UNREACHABLE",
|
428
|
-
"prohibit": "FR_ACT_PROHIBIT",
|
429
|
-
"nat": "FR_ACT_NAT",
|
430
|
-
}.get(rule['action'], rule['action'])
|
431
|
-
else:
|
432
|
-
ru['action'] = rule.get('action'.lower(), "FR_ACT_TO_TBL")
|
433
|
-
|
434
|
-
if 'fwmark' in rule:
|
435
|
-
ru['fwmark'] = rule['fwmark']
|
436
|
-
|
437
|
-
if 'ipproto' in rule:
|
438
|
-
if type(rule['ipproto']) == str:
|
439
|
-
ru['ip_proto'] = socket.getprotobyname(rule['ipproto'])
|
440
|
-
else:
|
441
|
-
ru['ip_proto'] = rule['ipproto']
|
442
|
-
|
443
|
-
if 'to' in rule:
|
444
|
-
ru['dst'] = str(ip_network(rule['to']).network_address)
|
445
|
-
ru['dst_len'] = ip_network(rule['to']).prefixlen
|
446
|
-
|
447
|
-
if 'from' in rule:
|
448
|
-
ru['src'] = str(ip_network(rule['from']).network_address)
|
449
|
-
ru['src_len'] = ip_network(rule['from']).prefixlen
|
450
|
-
|
451
|
-
if 'iif' in rule:
|
452
|
-
ru['iifname'] = rule['iif']
|
453
|
-
|
454
|
-
if 'oif' in rule:
|
455
|
-
ru['oifname'] = rule['oif']
|
456
|
-
|
457
|
-
if 'priority' in rule:
|
458
|
-
ru['priority'] = rule['priority']
|
459
|
-
|
460
|
-
if 'vrrp' in rule:
|
461
|
-
ru['_vrrp'] = rule['vrrp']
|
531
|
+
# convert config dict into IFLA keys and values
|
532
|
+
ru = parse_rule(rule)
|
462
533
|
|
534
|
+
# track rule
|
463
535
|
self.rules.append(ru)
|
464
536
|
|
465
537
|
def kernel_rules(self):
|
@@ -474,13 +546,18 @@ class Rules():
|
|
474
546
|
'tos': rule['tos'],
|
475
547
|
}
|
476
548
|
|
477
|
-
if
|
478
|
-
|
479
|
-
|
549
|
+
# add source and destination ip spaces even if they are not given by pyroute2
|
550
|
+
# as the user might specify something like "from: ::/0" explicitly which would not
|
551
|
+
# be present on a krule (because default values are ommited by pyroute2) and therefore
|
552
|
+
# would not match, even though it is the same rule
|
553
|
+
# if the user doesn't specify 'from' or 'to', these fields are just ignored because
|
554
|
+
# rule_matches is instructed to only check fields that are present on the config rule,
|
555
|
+
# meaning that these just get ignored
|
556
|
+
ru['dst'] = rule.get_attr('FRA_DST', default=("::" if rule['family'] == AF_INET6 else "0.0.0.0"))
|
557
|
+
ru['dst_len'] = rule['dst_len']
|
480
558
|
|
481
|
-
if rule['
|
482
|
-
|
483
|
-
ru['src_len'] = rule['src_len']
|
559
|
+
ru['src'] = rule.get_attr('FRA_SRC', default=("::" if rule['family'] == AF_INET6 else "0.0.0.0"))
|
560
|
+
ru['src_len'] = rule['src_len']
|
484
561
|
|
485
562
|
for field in ['iifname', 'oifname', 'fwmark', 'ip_proto']:
|
486
563
|
value = rule.get_attr('FRA_{}'.format(field.upper()))
|
@@ -491,14 +568,13 @@ class Rules():
|
|
491
568
|
return rules
|
492
569
|
|
493
570
|
def show_rules(self, ignores):
|
571
|
+
ignores = parse_rules(ignores, implicit_defaults=False)
|
572
|
+
|
494
573
|
rules = []
|
495
574
|
for rule in self.kernel_rules():
|
496
575
|
# skip ignored routes
|
497
576
|
ignore = False
|
498
577
|
for irule in ignores:
|
499
|
-
if 'proto' in irule:
|
500
|
-
irule['protocol'] = irule['proto']
|
501
|
-
del irule['proto']
|
502
578
|
if rule_matches(rule, irule, irule.keys()):
|
503
579
|
ignore = True
|
504
580
|
break
|
@@ -522,7 +598,7 @@ class Rules():
|
|
522
598
|
del rule['protocol']
|
523
599
|
|
524
600
|
if 'src_len' in rule and rule['src_len'] > 0:
|
525
|
-
rule['from'] =
|
601
|
+
rule['from'] = ip_interface(
|
526
602
|
'{}/{}'.format(rule['src'], rule['src_len'])).with_prefixlen
|
527
603
|
|
528
604
|
for key in ['src', 'src_len', 'tos', 'protocol']:
|
@@ -534,33 +610,55 @@ class Rules():
|
|
534
610
|
return rules
|
535
611
|
|
536
612
|
def apply(self, ignores, do_apply, by_vrrp, vrrp_type, vrrp_name, vrrp_state):
|
613
|
+
ignores = parse_rules(ignores, implicit_defaults=False)
|
614
|
+
|
615
|
+
# get rules that are currently in the kernels list
|
537
616
|
krules = self.kernel_rules()
|
617
|
+
|
618
|
+
# loop over all rules that are wanted by the config
|
538
619
|
for rule in self.rules:
|
539
620
|
log_str = '#{}'.format(rule['priority'])
|
621
|
+
|
622
|
+
if rule.get('family', None) == AF_INET6:
|
623
|
+
log_str += " (inet6)"
|
624
|
+
elif rule.get('family', None) == AF_INET:
|
625
|
+
log_str += " (inet)"
|
626
|
+
|
540
627
|
if self.netns.netns != None:
|
541
628
|
log_str += "[netns={}]".format(self.netns.netns)
|
542
629
|
|
630
|
+
vrrp_matched = vrrp_match(rule, by_vrrp, vrrp_type, vrrp_name, vrrp_state)
|
631
|
+
# 'found' indicates whether the rule wanted by the config was found
|
632
|
+
# in the kernels rules. if not, it is going to be added below
|
543
633
|
found = False
|
544
|
-
matched = vrrp_match(rule, by_vrrp, vrrp_type, vrrp_name, vrrp_state)
|
545
634
|
for i, krule in enumerate(krules):
|
546
635
|
if rule_matches(
|
547
636
|
rule,
|
548
637
|
krule,
|
549
638
|
# skip helper attrs like _vrrp
|
639
|
+
# note that rule_matches it told to only check fields that are
|
640
|
+
# present on the config rule, so additional ones from the krule
|
641
|
+
# just get ignored
|
550
642
|
[key for key in rule.keys() if key[0] != '_']
|
551
643
|
):
|
552
644
|
found = True
|
553
645
|
|
554
|
-
#
|
555
|
-
|
646
|
+
# we've just confirmed that this krule matches a rule
|
647
|
+
# that is wanted by the config so we delete it from the
|
648
|
+
# krules because any that remain in krule are unwanted
|
649
|
+
# and get deleted below
|
650
|
+
# EXCEPT if vrrp tells us to leave it be
|
651
|
+
if vrrp_matched != VRRP_MATCH_DISABLE:
|
556
652
|
del krules[i]
|
557
653
|
|
558
654
|
break
|
559
655
|
|
560
|
-
if
|
656
|
+
if vrrp_matched not in [VRRP_MATCH_IGNORE, VRRP_MATCH_DISABLE]:
|
561
657
|
if found:
|
658
|
+
# rule already exists
|
562
659
|
logger.log_ok(log_str)
|
563
660
|
else:
|
661
|
+
# rule doesn't exist, add it
|
564
662
|
logger.log_add(log_str)
|
565
663
|
|
566
664
|
logger.debug("ip rule add: {}".format(
|
@@ -573,7 +671,12 @@ class Rules():
|
|
573
671
|
raise
|
574
672
|
logger.warning('rule setup failed: {}'.format(err.args[1]))
|
575
673
|
|
674
|
+
# at this point all wanted rules have been deleted from the krules list,
|
675
|
+
# leaving the rest to be deleted here (see 'del krules[i]' above)
|
576
676
|
for rule in krules:
|
677
|
+
|
678
|
+
# if this rule falls into the ignored ones (like the ones set by the kernel itself by default)
|
679
|
+
# we will ignore it and continue
|
577
680
|
ignore = False
|
578
681
|
for irule in ignores:
|
579
682
|
if 'proto' in irule:
|
@@ -585,9 +688,26 @@ class Rules():
|
|
585
688
|
if ignore:
|
586
689
|
continue
|
587
690
|
|
588
|
-
|
691
|
+
log_str = '#{}'.format(rule['priority'])
|
692
|
+
|
693
|
+
if rule.get('family', None) == AF_INET6:
|
694
|
+
log_str += " (inet6)"
|
695
|
+
elif rule.get('family', None) == AF_INET:
|
696
|
+
log_str += " (inet)"
|
697
|
+
|
698
|
+
logger.log_del(log_str)
|
699
|
+
|
589
700
|
try:
|
590
701
|
if do_apply:
|
702
|
+
# for some reason pyroute2 raises an exception when
|
703
|
+
# we pass an address like "0.0.0.0" or "::" with an
|
704
|
+
# 'src_len' or 'dst_len' of 0.
|
705
|
+
# using this as a workaround:
|
706
|
+
if rule['src_len'] == 0:
|
707
|
+
del rule['src']
|
708
|
+
if rule['dst_len'] == 0:
|
709
|
+
del rule['dst']
|
710
|
+
|
591
711
|
self.netns.ipr.rule('del', **rule)
|
592
712
|
except Exception as err:
|
593
713
|
if not isinstance(err, netlinkerror_classes):
|