ifstate 1.9.0__py3-none-any.whl → 1.10.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ifstate
3
- Version: 1.9.0
3
+ Version: 1.10.1
4
4
  Summary: Manage host interface settings in a declarative manner
5
5
  Home-page: https://ifstate.net/
6
6
  Author: Thomas Liske
@@ -21,7 +21,7 @@ Requires-Dist: wgnlpy ; extra == 'wireguard'
21
21
 
22
22
  [![PyPI version](https://badge.fury.io/py/ifstate.svg)](https://badge.fury.io/py/ifstate)
23
23
 
24
- A python library to configure (linux) host interfaces in a declarative manner.
24
+ A python tool to configure (linux) host interfaces in a declarative manner.
25
25
  It is a frontend for the kernel netlink protocol using
26
26
  [pyroute2](https://pyroute2.org/) and aims to be as powerful as the
27
27
  iproute2/bridge/ethtool/tc/wireguard commands.
@@ -1,34 +1,35 @@
1
1
  ifstate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  ifstate/ifstate.py,sha256=fdjiCLB9Pnq-Vo_rs1Yr_x6AKx_hMUB9qnFYgCHs29A,7533
3
3
  ifstate/shell.py,sha256=7_JFpi4icr9MijynDzbb0v5mxhFsng6PCC4m3uQ255A,2177
4
- libifstate/__init__.py,sha256=JfwwqmGxbNqLoDo6f_3tHzAW6FH4ov-tQeg29eatnac,27197
4
+ libifstate/__init__.py,sha256=0hjZszKG1LYfGyIiuYFCqOVDADQ2JzasFZpsh-ashOk,29024
5
5
  libifstate/exception.py,sha256=-4WgzgXHXdzLdAkaj1sJw_PVQDefzyoyH3EMXpdUN70,2407
6
6
  libifstate/log.py,sha256=gSh_tznrggRPp6vk3EorA2VH4XYrzEGkcTZhsdA_qP4,3860
7
- libifstate/util.py,sha256=kC17XNPrkvcKVGKIw8W1av_r2pzivwk9Y-VReNhaUaw,7285
7
+ libifstate/util.py,sha256=6YQLCLvK5lV1dmP8rDgesBeze5DM3qC2egJ2Gw0KZVI,9190
8
8
  libifstate/address/__init__.py,sha256=usa9VqX-xOmwek9mkAuSYjx9aXnuZa1-qhCmoQdEKME,2716
9
9
  libifstate/bpf/__init__.py,sha256=NVzaunTmJU2PbIQg9eWBMKpFgLh3EnD3ujNa7Yt6rNc,7699
10
10
  libifstate/bpf/ctypes.py,sha256=kLZJHZlba09Vc-tbsJAcKpDwdoNO2IjlYVLCopawHmA,4274
11
11
  libifstate/bpf/map.py,sha256=cLHNMvRBDNW2yVCEf3z242_oRdU0HqVbFEYVkKXng0w,10818
12
12
  libifstate/brport/__init__.py,sha256=NzdA8F4hr2se1bXKNnyKZbvOFlCWBq_cdjwsL1H0Y-o,2964
13
+ libifstate/fdb/__init__.py,sha256=jMplRZZQKkgwFQT2L7Ua4YCdLKwOzkd43_6OFtet2No,6262
13
14
  libifstate/link/__init__.py,sha256=QZggoC-bIscqwVedqVycaSqS1CmXB3Bx3m2FZei8Q_4,115
14
- libifstate/link/base.py,sha256=yszpeU7qZPCA0SxHk9h9jgEJQD33jWhSM481re-9Gs4,26758
15
+ libifstate/link/base.py,sha256=tWGUFuF1gP9TVEVTiQiu8w8TuoDnK6cMVx6JPT_gwpQ,30897
15
16
  libifstate/link/physical.py,sha256=cJiq-MCfy-3XQoU-OxzgfPZZtu_pJ8u2ioJgn9VYdGk,560
16
17
  libifstate/link/tun.py,sha256=m55o5cwO3h3DCLofUR-68fM4ggLoTKElp6ZJ2LrJSCc,959
17
18
  libifstate/link/veth.py,sha256=SsEKt6S4FDnhmzMtgTiQpOWA2Qd76h0Tp8WTEryWczU,635
18
19
  libifstate/neighbour/__init__.py,sha256=FJJpbJvqnxvOEii6QDMYzW5jQDEbiEy71GQOEbqaS48,2463
19
- libifstate/netns/__init__.py,sha256=_fb8W2vUeVatw-LW26cQKC1NzgVS4rdudSWsPSwIRoE,7100
20
+ libifstate/netns/__init__.py,sha256=VXr-kMXs-WF3yY-EPWQfHvWfAcFQpWW2zUOVpUnPx3w,7747
20
21
  libifstate/parser/__init__.py,sha256=byz1W0G7UewVc5FFie-ti3UZjGK3-75wHIaOeq0oySQ,88
21
- libifstate/parser/base.py,sha256=HrVZ9Sb0ISO65TcKvsOZB6qbUvJIS8kmslzFJM4VEcg,3724
22
+ libifstate/parser/base.py,sha256=4Y_kaUysbgQRM9u_Kg3Lr_5GWFGdTXDmP4EC3McsNzU,4628
22
23
  libifstate/parser/yaml.py,sha256=u9D9nPfVhYADTd01EYxFYIywnTBccNF1solOcGeLUjM,1345
23
24
  libifstate/routing/__init__.py,sha256=jM9aZwKnuH6d0bv40Wn2cfuamB_A20Wr0czCszZE-Ak,16831
24
- libifstate/sysctl/__init__.py,sha256=Zt2fZSKVMA43hpVTflc0F6GalcCgaHslKS0ztbpLaKs,2608
25
+ libifstate/sysctl/__init__.py,sha256=L2gkoLnac_HM6RbCaKmsEuDTNj2m7U4Rjw42TEti0kg,3074
25
26
  libifstate/tc/__init__.py,sha256=spdRXCxhHybweNHURDRoHlPfpodXx62DLoTbKu_EtCE,11211
26
27
  libifstate/wireguard/__init__.py,sha256=1zY82eD-Uxly0Fat1tP5DXyI_XfXF3dX5BTxSwxxjCo,5173
27
28
  libifstate/xdp/__init__.py,sha256=X1xhEIGng7R5d5F4KsChykT2g6H-XBRWbWABijoYDQA,7208
28
- schema/ifstate.conf.schema.json,sha256=ZSzsMEgPovRfX25bNukgte3f3BdUxh_1idn6X4D0lkg,188621
29
- ifstate-1.9.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
30
- ifstate-1.9.0.dist-info/METADATA,sha256=i6_ev704zNwVE4gjWgageGsBISrrYJqh6B79MR8qigs,1387
31
- ifstate-1.9.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
32
- ifstate-1.9.0.dist-info/entry_points.txt,sha256=HF6jX7Uu_nF1Ly-J9uEPeiRapOxnM6LuHsb2y6Mt-k4,52
33
- ifstate-1.9.0.dist-info/top_level.txt,sha256=A7peI7aKBaM69fsiSPvMbL3rzTKZZr5qDxKC-pHMGdE,19
34
- ifstate-1.9.0.dist-info/RECORD,,
29
+ schema/ifstate.conf.schema.json,sha256=Tk8ywUx60V2sAfuYIH8-WUB152-iWA3qLy1Gmwm3Kws,196424
30
+ ifstate-1.10.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
31
+ ifstate-1.10.1.dist-info/METADATA,sha256=WLVtBd9-Ifr1LoNoqkrfEjOkoP1Jg2e6OqnKkvYB4y4,1385
32
+ ifstate-1.10.1.dist-info/WHEEL,sha256=Xo9-1PvkuimrydujYJAjF7pCkriuXBpUPEjma1nZyJ0,92
33
+ ifstate-1.10.1.dist-info/entry_points.txt,sha256=HF6jX7Uu_nF1Ly-J9uEPeiRapOxnM6LuHsb2y6Mt-k4,52
34
+ ifstate-1.10.1.dist-info/top_level.txt,sha256=A7peI7aKBaM69fsiSPvMbL3rzTKZZr5qDxKC-pHMGdE,19
35
+ ifstate-1.10.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.41.3)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
libifstate/__init__.py CHANGED
@@ -1,11 +1,14 @@
1
1
  from libifstate.exception import LinkDuplicate
2
2
  from libifstate.link.base import ethtool_path, Link
3
3
  from libifstate.address import Addresses
4
+ from libifstate.fdb import FDB
4
5
  from libifstate.neighbour import Neighbours
5
6
  from libifstate.routing import Tables, Rules, RTLookups
6
7
  from libifstate.parser import Parser
7
8
  from libifstate.tc import TC
8
9
  from libifstate.exception import netlinkerror_classes
10
+ import bisect
11
+ import pyroute2
9
12
 
10
13
  from pyroute2.netlink.rtnl.ifaddrmsg import IFA_F_PERMANENT
11
14
  try:
@@ -39,11 +42,12 @@ from copy import deepcopy
39
42
  import os
40
43
  import pkgutil
41
44
  import re
45
+ import secrets
42
46
  import json
43
47
  import errno
44
48
  import logging
45
49
 
46
- __version__ = "1.9.0"
50
+ __version__ = "1.10.1"
47
51
 
48
52
 
49
53
  class IfState():
@@ -119,8 +123,13 @@ class IfState():
119
123
  self._update(self.root_netns, ifstates)
120
124
  if 'namespaces' in ifstates:
121
125
  self.namespaces = {}
126
+ self.new_namespaces = []
122
127
  for netns_name, netns_ifstates in ifstates['namespaces'].items():
128
+ is_new = netns_name not in pyroute2.netns.listnetns()
123
129
  self.namespaces[netns_name] = NetNameSpace(netns_name)
130
+ if is_new:
131
+ self.new_namespaces.append(netns_name)
132
+ self.link_registry.inventory_netns(self.namespaces[netns_name])
124
133
  self._update(self.namespaces[netns_name], netns_ifstates)
125
134
 
126
135
  def _update(self, netns, ifstates):
@@ -128,10 +137,13 @@ class IfState():
128
137
  if 'options' in ifstates:
129
138
  # parse global sysctl settings
130
139
  if 'sysctl' in ifstates['options']:
131
- for iface in ['all', 'default']:
132
- if iface in ifstates['options']['sysctl']:
140
+ for proto in ifstates['options']['sysctl'].keys():
141
+ if proto in ['all', 'default']:
133
142
  netns.sysctl.add(
134
- iface, ifstates['options']['sysctl'][iface])
143
+ proto, ifstates['options']['sysctl'][proto])
144
+ else:
145
+ netns.sysctl.add_global(
146
+ proto, ifstates['options']['sysctl'][proto])
135
147
 
136
148
  # load BPF programs
137
149
  if 'bpf' in ifstates:
@@ -168,6 +180,11 @@ class IfState():
168
180
  elif defaults.get('clear_addresses', False):
169
181
  netns.addresses[name] = Addresses(netns, name, [])
170
182
 
183
+ if 'fdb' in ifstate:
184
+ netns.fdb[name] = FDB(netns, name, ifstate['fdb'])
185
+ elif defaults.get('clear_fdb', False):
186
+ netns.fdb[name] = FDB(netns, name, [])
187
+
171
188
  if 'neighbours' in ifstate:
172
189
  netns.neighbours[name] = Neighbours(netns, name, ifstate['neighbours'])
173
190
  elif defaults.get('clear_neighbours', False):
@@ -363,7 +380,7 @@ class IfState():
363
380
  raise LinkCircularLinked()
364
381
 
365
382
  # can be done right away
366
- r.append(t)
383
+ r.append( sorted(t) )
367
384
  # and cleaned up
368
385
  d=dict(((k, v-t) for k, v in d.items() if v))
369
386
  return r
@@ -404,7 +421,7 @@ class IfState():
404
421
 
405
422
  # create and destroy namespaces to match config
406
423
  if not by_vrrp and self.namespaces is not None:
407
- prepare_netns(do_apply, self.namespaces.keys())
424
+ prepare_netns(do_apply, self.namespaces.keys(), self.new_namespaces)
408
425
  logger.info("")
409
426
 
410
427
  # get link dependency tree
@@ -459,7 +476,7 @@ class IfState():
459
476
  # create/modify links in order of dependencies
460
477
  logger.info("configure interfaces...")
461
478
  for stage in stages:
462
- for link_dep in sorted(stage):
479
+ for link_dep in stage:
463
480
  logger.info(" {}".format(link_dep))
464
481
  if link_dep.netns is None:
465
482
  self._apply_iface(do_apply, self.root_netns, link_dep.ifname, by_vrrp, vrrp_type, vrrp_name, vrrp_state)
@@ -490,6 +507,13 @@ class IfState():
490
507
  logger.info("configure sysctl settings...")
491
508
  had_sysctl = True
492
509
  netns.sysctl.apply(iface, do_apply)
510
+
511
+ if netns.sysctl.has_globals():
512
+ if not had_sysctl:
513
+ logger.info("configure sysctl settings...")
514
+ had_sysctl = True
515
+ netns.sysctl.apply_globals(do_apply)
516
+
493
517
  return had_sysctl
494
518
 
495
519
  def _apply_iface(self, do_apply, netns, ifname, by_vrrp, vrrp_type, vrrp_name, vrrp_state):
@@ -534,6 +558,9 @@ class IfState():
534
558
  netns.addresses[ifname].apply(self.ipaddr_ignore, self.ignore.get(
535
559
  'ipaddr_dynamic', True), do_apply)
536
560
 
561
+ if ifname in netns.fdb:
562
+ netns.fdb[ifname].apply(do_apply)
563
+
537
564
  if ifname in netns.neighbours:
538
565
  netns.neighbours[ifname].apply(do_apply)
539
566
 
@@ -645,12 +672,23 @@ class IfState():
645
672
  ifs_link['link'][attr] = ref
646
673
 
647
674
  mtu = ipr_link.get_attr('IFLA_MTU')
648
- if not mtu is None and not mtu in [1500, 65536]:
649
- ifs_link['link']['mtu'] = mtu
675
+ if not mtu is None:
676
+ if not mtu in [1500, 65536] or name == 'lo':
677
+ ifs_link['link']['mtu'] = mtu
650
678
 
651
679
  brport.BRPort.show(netns.ipr, showall, ipr_link['index'], ifs_link)
652
680
 
653
- ifs_links.append(ifs_link)
681
+ if name == 'lo':
682
+ if ifs_link['addresses'] == Parser._default_lo_link['addresses']:
683
+ del(ifs_link['addresses'])
684
+
685
+ if ifs_link['link'] == Parser._default_lo_link['link']:
686
+ del(ifs_link['link'])
687
+
688
+ if len(ifs_link) > 1:
689
+ ifs_links.append(ifs_link)
690
+ else:
691
+ ifs_links.append(ifs_link)
654
692
 
655
693
  routing = {
656
694
  'routes': Tables(netns).show_routes(Parser._default_ifstates['ignore']['routes_builtin']),
@@ -659,7 +697,6 @@ class IfState():
659
697
 
660
698
  return {**{'interfaces': ifs_links, 'routing': routing}}
661
699
 
662
-
663
700
  def get_defaults(self, **kwargs):
664
701
  for default in self.defaults:
665
702
  for match in default['match']:
@@ -675,3 +712,13 @@ class IfState():
675
712
  return default
676
713
 
677
714
  return {}
715
+
716
+ def gen_unique_ifname(self):
717
+ '''
718
+ Get a random unique ifname over all namespaces and configured ifnames.
719
+ '''
720
+ while True:
721
+ ifname = "ifs.tmp.{}".format(secrets.token_hex(3))
722
+ item = self.link_registry.get_link(ifname=ifname)
723
+ if item is None:
724
+ return ifname
@@ -0,0 +1,174 @@
1
+ from libifstate.util import logger, IfStateLogging
2
+ from libifstate.exception import netlinkerror_classes
3
+ from ipaddress import ip_address
4
+ from pyroute2.netlink.rtnl.ndmsg import NUD_NOARP, NUD_PERMANENT, NTF_SELF
5
+ from pyroute2.config import AF_BRIDGE
6
+ import pyroute2.netlink.rtnl.ndmsg
7
+
8
+ class FDB():
9
+ def __init__(self, netns, iface, fdb):
10
+ self.netns = netns
11
+ self.iface = iface
12
+ self.fdb = {}
13
+ self.state_mask = NUD_NOARP|NUD_PERMANENT
14
+
15
+ for entry in fdb:
16
+ lladdr = entry['lladdr'].lower()
17
+ _entry = {
18
+ 'lladdr': lladdr,
19
+ }
20
+
21
+ if 'port' not in entry:
22
+ _entry['port'] = 8472
23
+ else:
24
+ _entry['port'] = entry['port']
25
+
26
+ if 'dst' in entry:
27
+ _entry['dst'] = str(ip_address(entry['dst']))
28
+
29
+ if 'state' in entry:
30
+ _entry['state'] = 0
31
+ for name, value in pyroute2.netlink.rtnl.ndmsg.states.items():
32
+ if name in entry['state']:
33
+ _entry['state'] |= value
34
+
35
+ if 'flags' in entry:
36
+ _entry['flags'] = 0
37
+ for name, value in pyroute2.netlink.rtnl.ndmsg.flags.items():
38
+ if name in entry['flags']:
39
+ _entry['flags'] |= value
40
+ else:
41
+ _entry['flags'] = NTF_SELF
42
+
43
+ for opt in ['nhid', 'src_vni', 'vni']:
44
+ if opt in entry:
45
+ _entry[opt] = entry[opt]
46
+
47
+ if not lladdr in self.fdb:
48
+ self.fdb[lladdr] = [_entry]
49
+ else:
50
+ self.fdb[lladdr].append(_entry)
51
+
52
+ def get_kernel_fdb(self):
53
+ # get fdb entries (NUD_NOARP|NUD_PERMANENT)
54
+ fdb = {}
55
+ for entry in self.netns.ipr.get_neighbours(ifindex=self.idx, family=AF_BRIDGE):
56
+ state = entry.get('state')
57
+
58
+ # look for permanent (local) or noarp (static) entries, only
59
+ if not state & self.state_mask:
60
+ continue
61
+
62
+ lladdr = entry.get_attr('NDA_LLADDR')
63
+ _entry = {
64
+ 'lladdr': lladdr,
65
+ 'state': state,
66
+ 'flags': entry.get('flags')
67
+ }
68
+
69
+ attr = entry.get_attr('NDA_DST')
70
+ if attr is not None:
71
+ _entry['dst'] = str(ip_address(attr))
72
+
73
+ attr = entry.get_attr('NDA_PORT')
74
+ if attr is None or attr == 0:
75
+ _entry['port'] = 8472
76
+ else:
77
+ _entry['port'] = attr
78
+
79
+ for opt in ['nhid', 'src_vni', 'vni']:
80
+ attr = entry.get_attr(f"NDA_{opt.upper()}")
81
+ if attr is not None:
82
+ _entry[opt] = attr
83
+
84
+ if not lladdr in fdb:
85
+ fdb[lladdr] = [_entry]
86
+ else:
87
+ fdb[lladdr].append(_entry)
88
+
89
+ return fdb
90
+
91
+ def apply(self, do_apply):
92
+ logger.debug('getting fdb', extra={'iface': self.iface})
93
+
94
+ # get ifindex and lladdr
95
+ link = next(iter(self.netns.ipr.get_links(ifname=self.iface)), None)
96
+
97
+ if link == None:
98
+ logger.warning('link missing', extra={'iface': self.iface})
99
+ return
100
+
101
+ self.idx = link['index']
102
+ self.lladdr = link.get_attr('IFLA_ADDRESS')
103
+
104
+ # prepare default state for entries w/o state specified (depends on link type)
105
+ default_state = NUD_PERMANENT
106
+
107
+ linkinfo = link.get_attr('IFLA_LINKINFO')
108
+ if linkinfo and linkinfo.get_attr('IFLA_INFO_KIND') in ['vxlan']:
109
+ default_state |= NUD_NOARP
110
+
111
+ # configure fdb entries
112
+ ipr_entries = self.get_kernel_fdb()
113
+ for lladdr, entries in self.fdb.items():
114
+ for entry in entries:
115
+ # set default_state if missing
116
+ if not 'state' in entry:
117
+ entry['state'] = default_state
118
+
119
+ # check if fdb entry is already present
120
+ if lladdr in ipr_entries:
121
+ if entry in ipr_entries[lladdr]:
122
+ logger.log_ok('fdb', '= {}'.format(lladdr))
123
+ continue
124
+
125
+ # fdb entry needs to be added
126
+ logger.log_add('fdb', '+ {}'.format(lladdr))
127
+
128
+ # prepare arguments
129
+ args = {
130
+ 'ifindex': self.idx,
131
+ 'family': AF_BRIDGE
132
+ }
133
+ args.update(entry)
134
+ logger.debug("bridge fdb append: {}".format(
135
+ " ".join("{}={}".format(k, v) for k, v in args.items())))
136
+
137
+ if do_apply:
138
+ try:
139
+ self.netns.ipr.fdb("append", **args)
140
+ except Exception as err:
141
+ if not isinstance(err, netlinkerror_classes):
142
+ raise
143
+ logger.warning('add {} to fdb failed: {}'.format(
144
+ entry['lladdr'], err.args[1]))
145
+
146
+ # cleanup orphan fdb entries
147
+ ipr_entries = self.get_kernel_fdb()
148
+ for lladdr, entries in ipr_entries.items():
149
+ # ignore lladdr of the link
150
+ if lladdr == self.lladdr:
151
+ continue
152
+
153
+ for entry in entries:
154
+ # check if fdb entry is already present
155
+ if lladdr not in self.fdb or entry not in self.fdb[lladdr]:
156
+ logger.log_del('fdb', '- {}'.format(lladdr))
157
+
158
+ # prepare arguments
159
+ args = {
160
+ 'ifindex': self.idx,
161
+ 'family': AF_BRIDGE
162
+ }
163
+ args.update(entry)
164
+ logger.debug("bridge fdb del: {}".format(
165
+ " ".join("{}={}".format(k, v) for k, v in args.items())))
166
+
167
+ if do_apply:
168
+ try:
169
+ self.netns.ipr.fdb("del", **args)
170
+ except Exception as err:
171
+ if not isinstance(err, netlinkerror_classes):
172
+ raise
173
+ logger.warning('remove {} from fdb failed: {}'.format(
174
+ entry['lladdr'], err.args[1]))
libifstate/link/base.py CHANGED
@@ -80,6 +80,21 @@ class Link(ABC):
80
80
  attr_value_lookup = {
81
81
  'group': RTLookups.group,
82
82
  }
83
+ attr_bind_kinds = [
84
+ 'ip6tnl',
85
+ 'tun',
86
+ 'vti',
87
+ 'vti6',
88
+ 'vxlan',
89
+ 'ipip',
90
+ 'gre',
91
+ 'gretap',
92
+ 'ip6gre',
93
+ 'ip6gretap',
94
+ 'geneve',
95
+ 'wireguard',
96
+ 'xfrm',
97
+ ]
83
98
 
84
99
  def __new__(cls, *args, **kwargs):
85
100
  cname = cls.__name__
@@ -170,6 +185,20 @@ class Link(ABC):
170
185
  extra={'iface': self.settings['ifname'], 'netns': self.netns})
171
186
  del(self.settings[attr])
172
187
 
188
+ if self.settings['kind'] in self.attr_bind_kinds:
189
+ if not 'bind_netns' in self.settings:
190
+ logger.debug('set bind_netns to current netns',
191
+ extra={'iface': self.settings['ifname'], 'netns': self.netns})
192
+ self.bind_netns = self.netns.netns
193
+ else:
194
+ self.bind_netns = self.settings['bind_netns']
195
+ elif 'bind_netns' in self.settings:
196
+ logger.warning('Ignoring not supported link attribute "bind_netns" for %s link.',
197
+ self.settings['kind'],
198
+ extra={'iface': self.settings['ifname'], 'netns': self.netns})
199
+ if 'bind_netns' in self.settings:
200
+ del(self.settings['bind_netns'])
201
+
173
202
  def search_link_registry(self):
174
203
  for args in self.link_registry_search_args:
175
204
  item = self.ifstate.link_registry.get_link(**args)
@@ -317,9 +346,61 @@ class Link(ABC):
317
346
  try:
318
347
  with open(fn, 'w') as fh:
319
348
  yaml.dump(self.ethtool[setting], fh)
320
- except Exception as err:
349
+ except IOError as err:
321
350
  logger.warning('failed write `{}`: {}'.format(fn, err.args[1]))
322
351
 
352
+ def get_bind_fn(self, netns_name, idx):
353
+ if netns_name is None:
354
+ dirname = "/run/ifstate/bind"
355
+ else:
356
+ dirname = "/run/ifstate/netns/{}/bind".format(netns_name)
357
+
358
+ try:
359
+ os.makedirs(dirname, exist_ok=True)
360
+ except:
361
+ pass
362
+
363
+ return "{}/{}.mount".format(dirname, idx)
364
+
365
+ def set_bind_state(self, state):
366
+ fn = self.get_bind_fn(self.netns.netns, self.idx)
367
+ try:
368
+ with open(fn, 'wb') as fh:
369
+ fh.write(state)
370
+ except IOError as err:
371
+ logger.warning('failed write `{}`: {}'.format(fn, err.args[1]))
372
+
373
+ def get_bind_netns(self):
374
+ if not hasattr(self, 'bind_netns'):
375
+ return None
376
+
377
+ if self.bind_netns is None:
378
+ return self.ifstate.root_netns
379
+
380
+ return self.ifstate.namespaces.get(self.bind_netns)
381
+
382
+ def bind_needs_recreate(self, item):
383
+ if not item.attributes['kind'] in self.attr_bind_kinds:
384
+ return False
385
+
386
+ bind_netns = self.get_bind_netns()
387
+ if bind_netns is None:
388
+ logger.warning('bind_netns "%s" is unknown',
389
+ self.bind_netns,
390
+ extra={
391
+ 'iface': self.settings['ifname'],
392
+ 'netns': self.netns})
393
+ return False
394
+
395
+ fn = self.get_bind_fn(item.netns.netns, item.index)
396
+ try:
397
+ with open(fn, 'rb') as fh:
398
+ state = fh.read()
399
+ except IOError:
400
+ return True
401
+
402
+ return state != bind_netns.mount
403
+
323
404
  def has_vrrp(self):
324
405
  return not self.vrrp is None
325
406
 
@@ -366,6 +447,14 @@ class Link(ABC):
366
447
  # get interface from registry
367
448
  item = self.search_link_registry()
368
449
 
450
+ # check if bind_netns option requires a recreate
451
+ if item is not None and self.bind_needs_recreate(item):
452
+ self.idx = item.index
453
+ self.recreate(do_apply, sysctl, excpts)
454
+
455
+ self.settings = osettings
456
+ return excpts
457
+
369
458
  # move interface into netns if required
370
459
  if item is not None and item.netns.netns != self.netns.netns:
371
460
  logger.log_change('netns')
@@ -382,7 +471,7 @@ class Link(ABC):
382
471
  return excpts
383
472
 
384
473
  if item is not None:
385
- self.idx = item.attributes['index']
474
+ self.idx = item.index
386
475
 
387
476
  if self.idx is not None:
388
477
  self.iface = next(iter(item.netns.ipr.get_links(self.idx)), None)
@@ -421,24 +510,50 @@ class Link(ABC):
421
510
  return excpts
422
511
 
423
512
  def create(self, do_apply, sysctl, excpts, oper="add"):
424
- logger.log_add('link')
513
+ logger.log_add('link', oper)
514
+
515
+ settings = copy.deepcopy(self.settings)
516
+ bind_netns = self.get_bind_netns()
517
+ if bind_netns is not None and bind_netns.netns != self.netns.netns:
518
+ logger.debug("handle link binding", extra={
519
+ 'iface': self.settings['ifname'],
520
+ 'netns': self.netns})
521
+ settings['ifname'] = self.ifstate.gen_unique_ifname()
425
522
 
426
523
  logger.debug("ip link add: {}".format(
427
- " ".join("{}={}".format(k, v) for k, v in self.settings.items())))
524
+ " ".join("{}={}".format(k, v) for k, v in settings.items())))
428
525
  if do_apply:
429
526
  try:
430
527
  # set state later
431
- state = self.settings.pop('state', None)
528
+ state = settings.pop('state', None)
432
529
 
433
530
  # prevent altname conflict
434
531
  self.prevent_altname_conflict()
435
532
 
436
533
  # add link
437
- self.netns.ipr.link('add', **(self.settings))
534
+ if bind_netns is None or bind_netns.netns == self.netns.netns:
535
+ self.netns.ipr.link('add', **(settings))
536
+ link = next(iter(self.netns.ipr.get_links(
537
+ ifname=settings['ifname'])), None)
538
+ if link is not None:
539
+ item = self.ifstate.link_registry.add_link(self.netns, link)
540
+ # add and move link
541
+ else:
542
+ bind_netns.ipr.link('add', **(settings))
543
+ link = next(iter(bind_netns.ipr.get_links(
544
+ ifname=settings['ifname'])), None)
545
+ if link is not None:
546
+ item = self.ifstate.link_registry.add_link(bind_netns, link)
547
+ item.update_netns(self.netns)
548
+ item.update_ifname(self.settings['ifname'])
549
+
438
550
  self.idx = next(iter(self.netns.ipr.link_lookup(
439
551
  ifname=self.settings['ifname'])), None)
440
552
 
441
553
  if self.idx is not None:
554
+ if bind_netns is not None:
555
+ self.set_bind_state(bind_netns.mount)
556
+
442
557
  # set sysctl settings if required
443
558
  sysctl.apply(self.settings['ifname'], do_apply)
444
559
 
@@ -466,7 +581,7 @@ class Link(ABC):
466
581
  self.settings['ifname'], self.ethtool.keys(), do_apply)
467
582
 
468
583
  def recreate(self, do_apply, sysctl, excpts):
469
- logger.debug('has wrong link kind %s, removing', self.settings['kind'], extra={
584
+ logger.debug('needs to be recreated', extra={
470
585
  'iface': self.settings['ifname'], 'netns': self.netns})
471
586
  if do_apply:
472
587
  try: