ifstate 1.13.7__py3-none-any.whl → 2.0.0__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.
libifstate/parser/base.py CHANGED
@@ -1,12 +1,26 @@
1
+ from libifstate.hook import HOOK_DIR
1
2
  from libifstate.util import logger
2
3
  from libifstate.exception import ParserValidationError
3
4
  from abc import ABC, abstractmethod
4
5
  from copy import deepcopy
6
+ import os
7
+
8
+
9
+ def get_available_hooks():
10
+ # check if there is a hooks dir before trying to scan it
11
+ if not os.path.isdir(HOOK_DIR):
12
+ return {}
13
+
14
+ hooks = {}
15
+ with os.scandir(HOOK_DIR) as it:
16
+ for dentry in it:
17
+ if dentry.is_file():
18
+ hooks[dentry.name] = {}
19
+ return hooks
5
20
 
6
21
 
7
22
  class Parser(ABC):
8
23
  _default_lo_link = {
9
- 'name': 'lo',
10
24
  'addresses': [
11
25
  '127.0.0.1/8',
12
26
  '::1/128',
@@ -18,79 +32,96 @@ class Parser(ABC):
18
32
  }
19
33
  }
20
34
  _default_ifstates = {
21
- 'ignore': {
22
- 'ipaddr_builtin': [
23
- 'fe80::/10'
24
- ],
25
- 'ipaddr_dynamic': True,
26
- 'ifname_builtin': [
27
- r'^br-[\da-f]{12}',
28
- r'^docker\d+',
29
- r'^ppp\d+$',
30
- r'^veth',
31
- r'^virbr\d+',
32
- r'^vrrp\d*\.\d+$'
33
- ],
34
- 'fdb_builtin': [
35
- r'^33:33:',
36
- r'^01:00:5e:'
37
- ],
38
- 'routes_builtin': [
39
- {'proto': 1},
40
- {'proto': 2},
41
- {'proto': 8},
42
- {'proto': 9},
43
- {'proto': 10},
44
- {'proto': 11},
45
- {'proto': 12},
46
- {'proto': 13},
47
- {'proto': 14},
48
- {'proto': 15},
49
- {'proto': 16},
50
- {'proto': 18},
51
- {'proto': 42},
52
- {'proto': 186},
53
- {'proto': 187},
54
- {'proto': 188},
55
- {'proto': 189},
56
- {'proto': 192},
57
- {'to': 'ff00::/8'},
58
- ],
59
- 'rules_builtin': [
60
- {'proto': 1},
61
- {'proto': 2},
62
- {'proto': 8},
63
- {'proto': 9},
64
- {'proto': 10},
65
- {'proto': 11},
66
- {'proto': 12},
67
- {'proto': 13},
68
- {'proto': 14},
69
- {'proto': 15},
70
- {'proto': 16},
71
- {'proto': 18},
72
- {'proto': 42},
73
- {'proto': 186},
74
- {'proto': 187},
75
- {'proto': 188},
76
- {'proto': 189},
77
- {'proto': 192},
78
- ],
79
- },
80
- 'cshaper': {
81
- 'default': {
82
- 'egress_qdisc': {
83
- 'kind': 'cake',
84
- 'handle': '1:',
85
- },
86
- 'ingress_qdisc': {
87
- 'kind': 'cake',
88
- 'handle': '1:',
89
- },
90
- 'ingress_ifname': {
91
- 'search': r'^\D{1,3}',
92
- 'replace': 'ifb',
35
+ 'parameters': {
36
+ 'cshaper': {
37
+ 'default': {
38
+ 'egress_qdisc': {
39
+ 'kind': 'cake',
40
+ 'handle': '1:',
41
+ },
42
+ 'ingress_qdisc': {
43
+ 'kind': 'cake',
44
+ 'handle': '1:',
45
+ },
46
+ 'ingress_ifname': {
47
+ 'search': r'^\D{1,3}',
48
+ 'replace': 'ifb',
49
+ }
50
+ }
51
+ },
52
+ 'defaults_builtin': [
53
+ {
54
+ 'match': [
55
+ {'ifname': ''}
56
+ ],
57
+ 'clear_addresses': True,
58
+ 'clear_fdb': True,
59
+ 'clear_neighbours': True,
60
+ 'clear_tc': True,
61
+ 'link': {
62
+ 'state': 'down',
63
+ 'master': None
64
+ }
93
65
  }
66
+ ],
67
+ 'ignore': {
68
+ 'ipaddr_builtin': [
69
+ 'fe80::/10'
70
+ ],
71
+ 'ipaddr_dynamic': True,
72
+ 'ifname_builtin': [
73
+ r'^br-[\da-f]{12}',
74
+ r'^docker\d+',
75
+ r'^ppp\d+$',
76
+ r'^veth',
77
+ r'^virbr\d+',
78
+ r'^vrrp\d*\.\d+$'
79
+ ],
80
+ 'fdb_builtin': [
81
+ r'^33:33:',
82
+ r'^01:00:5e:'
83
+ ],
84
+ 'routes_builtin': [
85
+ {'proto': 1},
86
+ {'proto': 2},
87
+ {'proto': 8},
88
+ {'proto': 9},
89
+ {'proto': 10},
90
+ {'proto': 11},
91
+ {'proto': 12},
92
+ {'proto': 13},
93
+ {'proto': 14},
94
+ {'proto': 15},
95
+ {'proto': 16},
96
+ {'proto': 18},
97
+ {'proto': 42},
98
+ {'proto': 186},
99
+ {'proto': 187},
100
+ {'proto': 188},
101
+ {'proto': 189},
102
+ {'proto': 192},
103
+ {'to': 'ff00::/8'},
104
+ ],
105
+ 'rules_builtin': [
106
+ {'proto': 1},
107
+ {'proto': 2},
108
+ {'proto': 8},
109
+ {'proto': 9},
110
+ {'proto': 10},
111
+ {'proto': 11},
112
+ {'proto': 12},
113
+ {'proto': 13},
114
+ {'proto': 14},
115
+ {'proto': 15},
116
+ {'proto': 16},
117
+ {'proto': 18},
118
+ {'proto': 42},
119
+ {'proto': 186},
120
+ {'proto': 187},
121
+ {'proto': 188},
122
+ {'proto': 189},
123
+ {'proto': 192},
124
+ ],
94
125
  }
95
126
  }
96
127
  }
@@ -113,12 +144,12 @@ class Parser(ABC):
113
144
  return a
114
145
 
115
146
  def _update_lo(self, cfg):
116
- if 'interfaces' in cfg:
117
- lo = next((idx for idx, iface in enumerate(cfg['interfaces']) if iface['name'] == 'lo'), None)
118
- if lo is not None:
119
- cfg['interfaces'][lo] = self.merge(deepcopy(Parser._default_lo_link), cfg['interfaces'][lo])
147
+ if 'interfaces' in cfg and isinstance(cfg['interfaces'], dict):
148
+ if 'lo' in cfg['interfaces']:
149
+ cfg['interfaces']['lo'] = self.merge(
150
+ deepcopy(Parser._default_lo_link), cfg['interfaces']['lo'])
120
151
  else:
121
- cfg['interfaces'].append(Parser._default_lo_link)
152
+ cfg['interfaces']['lo'] = Parser._default_lo_link
122
153
 
123
154
  def config(self):
124
155
  # merge builtin defaults with config
@@ -126,7 +157,7 @@ class Parser(ABC):
126
157
 
127
158
  # 'ignore' should still be an object
128
159
  try:
129
- iter(cfg["ignore"])
160
+ iter(cfg["parameters"]["ignore"])
130
161
  except TypeError:
131
162
  raise ParserValidationError("$.ignore: is not of type 'object'")
132
163
 
@@ -135,19 +166,34 @@ class Parser(ABC):
135
166
  for namespace in cfg.get('namespaces', {}):
136
167
  self._update_lo(cfg['namespaces'][namespace])
137
168
 
169
+ # add available hooks
170
+ if "hooks" in cfg["parameters"]:
171
+ hooks = get_available_hooks()
172
+ hooks.update(cfg["parameters"]["hooks"])
173
+ cfg["parameters"]["hooks"] = hooks
174
+ else:
175
+ cfg["parameters"]["hooks"] = get_available_hooks()
176
+
138
177
  # merge builtin defaults
139
- for k in list(cfg["ignore"]):
178
+ if "defaults" in cfg["parameters"]:
179
+ cfg["parameters"]["defaults"].extend(cfg["parameters"]["defaults_builtin"])
180
+ else:
181
+ cfg["parameters"]["defaults"] = cfg["parameters"]["defaults_builtin"]
182
+ del cfg["parameters"]["defaults_builtin"]
183
+
184
+ # merge builtin ignore lists
185
+ for k in list(cfg["parameters"]["ignore"]):
140
186
  if k.endswith("_builtin"):
141
187
  n = k[:-8]
142
- if n in cfg["ignore"]:
188
+ if n in cfg["parameters"]["ignore"]:
143
189
  try:
144
- cfg["ignore"][n] += cfg["ignore"][k]
190
+ cfg["parameters"]["ignore"][n] += cfg["parameters"]["ignore"][k]
145
191
  except TypeError:
146
192
  raise ParserValidationError("$.ignore.{}: is not of type '{}'".format(
147
- n, type(cfg["ignore"][k]).__name__))
193
+ n, type(cfg["parameters"]["ignore"][k]).__name__))
148
194
  else:
149
- cfg["ignore"][n] = cfg["ignore"][k]
150
- del(cfg["ignore"][k])
195
+ cfg["parameters"]["ignore"][n] = cfg["parameters"]["ignore"][k]
196
+ del (cfg["parameters"]["ignore"][k])
151
197
 
152
198
  return cfg
153
199
 
@@ -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
@@ -202,7 +202,7 @@ class RTLookup():
202
202
  self.str2id = {}
203
203
  self.id2str = {}
204
204
 
205
- for basedir in ['/usr/share/iproute2', '/usr/lib/iproute2', '/etc/iproute2']:
205
+ for basedir in ['/usr/share/iproute2', '/etc/iproute2']:
206
206
  fn = os.path.join(basedir, name)
207
207
  try:
208
208
  with open(fn, 'r') as fp:
@@ -284,8 +284,8 @@ class Tables(collections.abc.Mapping):
284
284
  idx = next(
285
285
  iter(self.netns.ipr.link_lookup(ifname=ignore[attr])), None)
286
286
  if idx is None:
287
- logger.debug("{} '{}' is unknown".format(attr, ignore[attr]))
288
- del ignore[attr]
287
+ logger.debug("{} '{}' is unknown, skipping ignore".format(attr, ignore[attr]))
288
+ continue
289
289
  else:
290
290
  ignore[attr] = idx
291
291
  parsed.append(ignore)
@@ -321,7 +321,7 @@ class Tables(collections.abc.Mapping):
321
321
  continue
322
322
 
323
323
  if route['dst_len'] > 0:
324
- dst = ip_network(
324
+ dst = ip_interface(
325
325
  '{}/{}'.format(route.get_attr('RTA_DST'), route['dst_len'])).with_prefixlen
326
326
  elif route['family'] == AF_INET:
327
327
  dst = ip_network('0.0.0.0/0').with_prefixlen
@@ -546,13 +546,18 @@ class Rules():
546
546
  'tos': rule['tos'],
547
547
  }
548
548
 
549
- if rule['dst_len'] > 0:
550
- ru['dst'] = rule.get_attr('FRA_DST')
551
- ru['dst_len'] = rule['dst_len']
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']
552
558
 
553
- if rule['src_len'] > 0:
554
- ru['src'] = rule.get_attr('FRA_SRC')
555
- 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']
556
561
 
557
562
  for field in ['iifname', 'oifname', 'fwmark', 'ip_proto']:
558
563
  value = rule.get_attr('FRA_{}'.format(field.upper()))
@@ -593,7 +598,7 @@ class Rules():
593
598
  del rule['protocol']
594
599
 
595
600
  if 'src_len' in rule and rule['src_len'] > 0:
596
- rule['from'] = ip_network(
601
+ rule['from'] = ip_interface(
597
602
  '{}/{}'.format(rule['src'], rule['src_len'])).with_prefixlen
598
603
 
599
604
  for key in ['src', 'src_len', 'tos', 'protocol']:
@@ -609,32 +614,51 @@ class Rules():
609
614
 
610
615
  # get rules that are currently in the kernels list
611
616
  krules = self.kernel_rules()
617
+
618
+ # loop over all rules that are wanted by the config
612
619
  for rule in self.rules:
613
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
+
614
627
  if self.netns.netns != None:
615
628
  log_str += "[netns={}]".format(self.netns.netns)
616
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
617
633
  found = False
618
- matched = vrrp_match(rule, by_vrrp, vrrp_type, vrrp_name, vrrp_state)
619
634
  for i, krule in enumerate(krules):
620
635
  if rule_matches(
621
636
  rule,
622
637
  krule,
623
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
624
642
  [key for key in rule.keys() if key[0] != '_']
625
643
  ):
626
644
  found = True
627
645
 
628
- # remove kernel routes due to vrrp_match
629
- if matched != VRRP_MATCH_DISABLE:
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:
630
652
  del krules[i]
631
653
 
632
654
  break
633
655
 
634
- if matched not in [VRRP_MATCH_IGNORE, VRRP_MATCH_DISABLE]:
656
+ if vrrp_matched not in [VRRP_MATCH_IGNORE, VRRP_MATCH_DISABLE]:
635
657
  if found:
658
+ # rule already exists
636
659
  logger.log_ok(log_str)
637
660
  else:
661
+ # rule doesn't exist, add it
638
662
  logger.log_add(log_str)
639
663
 
640
664
  logger.debug("ip rule add: {}".format(
@@ -647,7 +671,12 @@ class Rules():
647
671
  raise
648
672
  logger.warning('rule setup failed: {}'.format(err.args[1]))
649
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)
650
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
651
680
  ignore = False
652
681
  for irule in ignores:
653
682
  if 'proto' in irule:
@@ -659,9 +688,26 @@ class Rules():
659
688
  if ignore:
660
689
  continue
661
690
 
662
- logger.log_del('#{}'.format(rule['priority']))
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
+
663
700
  try:
664
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
+
665
711
  self.netns.ipr.rule('del', **rule)
666
712
  except Exception as err:
667
713
  if not isinstance(err, netlinkerror_classes):