ffho_net.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. #!/usr/bin/python
  2. import re
  3. mac_prefix = "f2"
  4. vrf_table_map = {
  5. 'vrf_external' : 1023,
  6. }
  7. sites = None
  8. def _get_site_no (sites_config, site_name):
  9. global sites
  10. if sites == None:
  11. sites = {}
  12. for site in sites_config:
  13. if site.startswith ("_"):
  14. continue
  15. sites[site] = sites_config[site].get ("site_no", -2)
  16. return sites.get (site_name, -1)
  17. #
  18. # Generate a MAC address after the format f2:dd:dd:ss:nn:nn where
  19. # dd:dd is the hexadecimal reprensentation of the nodes device_id
  20. # ff:ff representing the gluon nodes
  21. #
  22. # ss is the hexadecimal reprensentation of the site_id the interface is connected to
  23. #
  24. # nn:nn is the decimal representation of the network the interface is connected to, with
  25. # 00:00 being the dummy interface
  26. # 00:01 being an inter-gw-vpn interface
  27. # 00:04 being an nodes fastd tunnel interface of IPv4 transport
  28. # 00:06 being an nodes fastd tunnel interface of IPv6 transport
  29. # 02:xx being a connection to local Vlan 2xx
  30. # 07:xx being a VXLAN tunnel for site ss, with xx being a consecutive number
  31. # 1b:24 being the ibss 2.4GHz bssid
  32. # 1b:05 being the ibss 5GHz bssid
  33. # ff:ff being the gluon next-node interface
  34. def gen_batman_iface_mac (site_no, device_no, network):
  35. net_type_map = {
  36. 'dummy' : 0,
  37. 'intergw' : 1,
  38. 'nodes4' : 4,
  39. 'nodes6' : 6,
  40. }
  41. # Well-known network type?
  42. if network in net_type_map:
  43. network = net_type_map[network]
  44. if type (network) == int:
  45. last = re.sub (r'(\d{2})(\d{2})', '\g<1>:\g<2>', "%04d" % network)
  46. else:
  47. last = "ee:ee"
  48. # Convert device_no to hex, format number to 4 digits with leading zeros and : betwwen 2nd and 3rd digit
  49. device_no_hex = re.sub (r'([0-9a-fA-F]{2})([0-9a-fA-F]{2})', '\g<1>:\g<2>', "%04x" % int (device_no))
  50. # Format site_no to two digit number with leading zero
  51. site_no_hex = "%02d" % int (site_no)
  52. return "%s:%s:%s:%s" % (mac_prefix, device_no_hex, site_no_hex, last)
  53. #
  54. # Default parameters added to any given bonding/bridge interface,
  55. # if not specified at the interface configuration.
  56. default_bond_config = {
  57. 'bond-mode': '802.3ad',
  58. 'bond-min-links': '1',
  59. 'bond-xmit-hash-policy': 'layer3+4'
  60. }
  61. default_bridge_config = {
  62. 'bridge-fd' : '0',
  63. 'bridge-stp' : 'no'
  64. }
  65. #
  66. # Hop penalty to set if none is explicitly specified
  67. # Check if one of these roles is configured for any given node, use first match.
  68. default_hop_penalty_by_role = {
  69. 'bbr' : 5,
  70. 'bras' : 50,
  71. 'batman_gw' : 50,
  72. }
  73. batman_role_evaluation_order = [ 'bbr', 'batman_gw', 'bras' ]
  74. # Gather B.A.T.M.A.N. related config options for real batman devices (e.g. bat0)
  75. # as well as for batman member interfaces (e.g. eth0.100, fastd ifaces etc.)
  76. def _update_batman_config (node_config, iface, sites_config):
  77. try:
  78. node_batman_hop_penalty = int (node_config['batman']['hop-penalty'])
  79. except KeyError,ValueError:
  80. node_batman_hop_penalty = None
  81. iface_config = node_config['ifaces'][iface]
  82. iface_type = iface_config.get ('type', 'inet')
  83. batman_config = {}
  84. for item, value in iface_config.items ():
  85. if item.startswith ('batman-'):
  86. batman_config[item] = value
  87. iface_config.pop (item)
  88. # B.A.T.M.A.N. device (e.g. bat0)
  89. if iface_type == 'batman':
  90. if 'batman-hop-penalty' not in batman_config:
  91. # If there's a hop penalty set for the node, but not for the interface
  92. # apply the nodes hop penalty
  93. if node_batman_hop_penalty:
  94. batman_config['batman-hop-penalty'] = node_batman_hop_penalty
  95. # If there's no hop penalty set for the node, use a default hop penalty
  96. # for the roles the node might have, if any
  97. else:
  98. node_roles = node_config.get ('roles', [])
  99. for role in batman_role_evaluation_order:
  100. if role in node_roles:
  101. batman_config['batman-hop-penalty'] = default_hop_penalty_by_role[role]
  102. # If batman ifaces were specified as a list - which they should -
  103. # generate a sorted list of interface names as string representation
  104. if 'batman-ifaces' in batman_config and type (batman_config['batman-ifaces']) == list:
  105. batman_iface_str = " ".join (sorted (batman_config['batman-ifaces']))
  106. batman_config['batman-ifaces'] = batman_iface_str
  107. # B.A.T.M.A.N. member interface (e.g. eth.100, fastd ifaces, etc.)
  108. elif iface_type == 'batman_iface':
  109. # Generate unique MAC address for every batman iface, as B.A.T.M.A.N.
  110. # will get puzzled with multiple interfaces having the same MAC and
  111. # do nasty things.
  112. site = iface_config.get ('site')
  113. site_no = _get_site_no (sites_config, site)
  114. device_no = node_config.get ('id')
  115. network = 1234
  116. # Generate a unique BATMAN-MAC for this interfaces
  117. match = re.search (r'^vlan(\d+)', iface)
  118. if match:
  119. network = int (match.group (1))
  120. iface_config['hwaddress'] = gen_batman_iface_mac (site_no, device_no, network)
  121. iface_config['batman'] = batman_config
  122. # Mangle bond specific config items with default values and store them in
  123. # separate sub-dict for easier access and configuration.
  124. def _update_bond_config (config):
  125. bond_config = default_bond_config.copy ()
  126. for item, value in config.items ():
  127. if item.startswith ('bond-'):
  128. bond_config[item] = value
  129. config.pop (item)
  130. if bond_config['bond-mode'] not in ['2', 'balance-xor', '4', '802.3ad']:
  131. bond_config.pop ('bond-xmit-hash-policy')
  132. config['bond'] = bond_config
  133. # Mangle bridge specific config items with default values and store them in
  134. # separate sub-dict for easier access and configuration.
  135. def _update_bridge_config (config):
  136. bridge_config = default_bridge_config.copy ()
  137. for item, value in config.items ():
  138. if item.startswith ('bridge-'):
  139. bridge_config[item] = value
  140. config.pop (item)
  141. # Fix and salt mangled string interpretation back to real string.
  142. if type (value) == bool:
  143. bridge_config[item] = "yes" if value else "no"
  144. # If bridge ports were specified as a list - which they should -
  145. # generate a sorted list of interface names as string representation
  146. if 'bridge-ports' in bridge_config and type (bridge_config['bridge-ports']) == list:
  147. bridge_ports_str = " ".join (sorted (bridge_config['bridge-ports']))
  148. bridge_config['bridge-ports'] = bridge_ports_str
  149. config['bridge'] = bridge_config
  150. # Move vlan specific config items into a sub-dict for easier access and pretty-printing
  151. # in the configuration file
  152. def _update_vlan_config (config):
  153. vlan_config = {}
  154. for item, value in config.items ():
  155. if item.startswith ('vlan-'):
  156. vlan_config[item] = value
  157. config.pop (item)
  158. config['vlan'] = vlan_config
  159. # Generate configuration entries for any batman related interfaces not
  160. # configured explicitly, but asked for implicitly by role batman and a
  161. # (list of) site(s) specified in the node config.
  162. def _generate_batman_interface_config (node_config, ifaces, sites_config):
  163. # No role 'batman', nothing to do
  164. if 'batman' not in node_config.get ('roles', []):
  165. return
  166. device_no = node_config.get ('id', -1)
  167. for site in node_config.get ('sites', []):
  168. bat_site_if = "bat-%s" % site
  169. dummy_site_if = "dummy-%s" % site
  170. site_no = _get_site_no (sites_config, site)
  171. # Create bat-<site> interface config
  172. if bat_site_if not in ifaces:
  173. ifaces[bat_site_if] = {
  174. 'type' : 'batman',
  175. 'batman-ifaces' : [ dummy_site_if ],
  176. 'batman-ifaces-ignore-regex': '.*_.*',
  177. }
  178. # Create dummy-<site> interfaces config to ensure bat-<site> can
  179. # be successfully configured (read: comes up)
  180. if not dummy_site_if in ifaces:
  181. ifaces[dummy_site_if] = {
  182. 'link-type' : 'dummy',
  183. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'dummy')
  184. }
  185. # Make sure there is a bridge present for every site where a mesh_breakout
  186. # interface should be configured.
  187. for iface, config in ifaces.items ():
  188. iface_type = config.get ('type', 'inet')
  189. if iface_type not in ['mesh_breakout', 'batman_iface']:
  190. continue
  191. site = config.get ('site')
  192. site_bridge = "br-%s" % site
  193. batman_site_if = "bat-%s" % site
  194. if iface_type == 'mesh_breakout':
  195. # If the bridge has already been defined (with an IP maybe) make
  196. # sure that the corresbonding batman device is part of the bridge-
  197. # ports.
  198. if site_bridge in ifaces:
  199. bridge_config = ifaces.get (site_bridge)
  200. # If there already is/are (a) bridge-port(s) defined, add
  201. # the batman and the breakout interfaces if not present...
  202. bridge_ports = bridge_config.get ('bridge-ports', None)
  203. if bridge_ports:
  204. for dev in (batman_site_if, iface):
  205. if not dev in bridge_ports:
  206. if type (bridge_ports) == list:
  207. bridge_ports.append (dev)
  208. else:
  209. bridge_config['bridge-ports'] += ' ' + dev
  210. # ...if there is no bridge-port defined yet, just used
  211. # the batman and breakout iface.
  212. else:
  213. bridge_config['bridge-ports'] = [ iface, batman_site_if ]
  214. # If the bridge isn't present alltogether, add it.
  215. else:
  216. ifaces[site_bridge] = {
  217. 'bridge-ports' : [ iface, batman_site_if ],
  218. }
  219. elif iface_type == 'batman_iface':
  220. batman_ifaces = ifaces[bat_site_if]['batman-ifaces']
  221. if iface not in batman_ifaces:
  222. if type (batman_ifaces) == list:
  223. batman_ifaces.append (iface)
  224. else:
  225. batman_ifaces += ' ' + iface
  226. ## Generate VXLAN tunnels for every configured batman peer for every site
  227. ## configured on this and the peer node.
  228. #def _generate_vxlan_interface_config_complex (node_config, ifaces, node_id, nodes_config):
  229. # # No role 'batman', nothing to do
  230. # if 'batman' not in node_config.get ('roles', []):
  231. # return
  232. #
  233. # # No batman peers configred, nothing to do
  234. # try:
  235. # peers = node_config['batman']['peers']
  236. # if type (peers) != list:
  237. # return
  238. # except KeyError:
  239. # return
  240. #
  241. # # Sites configured on this node. Nothing to do, if none.
  242. # my_sites = node_config.get ('sites', [])
  243. # if len (my_sites) == 0:
  244. # return
  245. #
  246. # device_no = node_config.get ('id', -1)
  247. #
  248. # # ...
  249. # for peer in peers:
  250. # try:
  251. # # Try to get node config of peer
  252. # peer_config = nodes_config.get (peer)
  253. #
  254. # # Not a batman node?
  255. # if not 'batman' in peer_config['roles']:
  256. # continue
  257. #
  258. # # Verify we are in peers list of peer
  259. # peers_of_peer = peer_config['batman']['peers']
  260. # if type (peers_of_peer) != list:
  261. # continue
  262. # if node_id not in peers_of_peer:
  263. # continue
  264. #
  265. # # Get sites configured on peers
  266. # sites_of_peer = peer_config.get ('sites')
  267. # except KeyError:
  268. # continue
  269. #
  270. # for site in my_sites:
  271. # if site not in sites_of_peer:
  272. # continue
  273. #
  274. # # Build tunnel here
  275. def _generate_vxlan_interface_config (node_config, ifaces, sites_config):
  276. # No role 'batman', nothing to do
  277. if 'batman' not in node_config.get ('roles', []):
  278. return
  279. # Sites configured on this node. Nothing to do, if none.
  280. my_sites = node_config.get ('sites', [])
  281. if len (my_sites) == 0:
  282. return
  283. # As we're still here we can now safely assume that a B.A.T.M.A.N.
  284. # device has been configured for every site specified in sites list.
  285. device_no = node_config.get ('id', -1)
  286. for iface, iface_config in ifaces.items ():
  287. batman_connect_sites = iface_config.get ('batman_connect_sites', [])
  288. # If we got a string, convert it to a list with a single element
  289. if type (batman_connect_sites) == str:
  290. batman_connect_sites = [ batman_connect_sites ]
  291. # If the string 'all' is part of the list, blindly use all sites configured for this node
  292. if 'all' in batman_connect_sites:
  293. batman_connect_sites = my_sites
  294. for site in batman_connect_sites:
  295. # Silenty ignore sites not configured on this node
  296. if site not in my_sites:
  297. continue
  298. # iface_name := vx_<last 5 chars of underlay iface>_<site> stripped to 15 chars
  299. vx_iface = "vx_%s_%s" % (re.sub ('vlan', 'v', iface)[-5:], site)[:15]
  300. site_no = _get_site_no (sites_config, site)
  301. vni = 100 + site_no
  302. bat_iface = "bat-%s" % site
  303. try:
  304. iface_id = int (re.sub ('vlan', '', iface))
  305. # Gather interface specific mcast address.
  306. # The address is derived from the vlan-id of the underlying interface,
  307. # assuming that it in fact is a vlan interface.
  308. # Mangle the vlan-id into two 2 digit values, eliminating any leading zeros.
  309. iface_id_4digit = "%04d" % iface_id
  310. octet2 = int (iface_id_4digit[0:2])
  311. octet3 = int (iface_id_4digit[2:4])
  312. mcast_ip = "225.%s.%s.%s" % (octet2, octet3, site_no)
  313. vni = octet2 * 256 * 256 + octet3 * 256 + site_no
  314. except ValueError:
  315. iface_id = 9999
  316. mcast_ip = "225.0.0.%s" % site_no
  317. vni = site_no
  318. # bail out if VXLAN tunnel already configured
  319. if vx_iface in ifaces:
  320. continue
  321. # If there's no batman interface for this site, there's no point
  322. # in setting up a VXLAN interfaces
  323. if bat_iface not in ifaces:
  324. continue
  325. # Add the VXLAN interface
  326. ifaces[vx_iface] = {
  327. 'vxlan' : {
  328. 'vxlan-id' : vni,
  329. 'vxlan-svcnodeip' : mcast_ip,
  330. 'vxlan-physdev' : iface,
  331. },
  332. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, iface_id),
  333. }
  334. # If the batman interface for this site doesn't have any interfaces
  335. # set up - which basicly cannot happen - add this VXLAN tunnel as
  336. # the first in the list.
  337. if not 'batman-ifaces' in ifaces[bat_iface]:
  338. ifaces[bat_iface]['batman-ifaces'] = [ vx_iface ]
  339. continue
  340. # In the hope there already are interfaces for batman set up already
  341. # add this VXLAN tunnel to the list
  342. batman_ifaces = ifaces[bat_iface]['batman-ifaces']
  343. if vx_iface not in batman_ifaces:
  344. if type (batman_ifaces) == list:
  345. batman_ifaces.append (vx_iface)
  346. else:
  347. batman_ifaces += ' ' + vx_iface
  348. def _generate_vrfs (ifaces):
  349. for iface, iface_config in ifaces.items ():
  350. vrf = iface_config.get ('vrf', None)
  351. if vrf and vrf not in ifaces:
  352. ifaces[vrf] = {
  353. 'vrf-table' : vrf_table_map.get (vrf, 1234)
  354. }
  355. GRE_FFRL_attrs = {
  356. 'mode' : 'gre',
  357. 'method' : 'tunnel',
  358. 'mtu' : '1400',
  359. 'ttl' : '64',
  360. }
  361. def _generate_ffrl_gre_tunnels (ifaces):
  362. for iface, iface_config in ifaces.items ():
  363. # We only care for GRE_FFRL type interfaces
  364. if iface_config.get ('type', '') != 'GRE_FFRL':
  365. continue
  366. # Copy default values to interface config
  367. for attr, val in GRE_FFRL_attrs.items ():
  368. if not attr in iface_config:
  369. iface_config[attr] = val
  370. # Guesstimate local IPv4 tunnel endpoint address from tunnel-physdev
  371. if not 'local' in iface_config and 'tunnel-physdev' in iface_config:
  372. try:
  373. physdev_prefixes = [p.split ('/')[0] for p in ifaces[iface_config['tunnel-physdev']]['prefixes'] if '.' in p]
  374. if len (physdev_prefixes) == 1:
  375. iface_config['local'] = physdev_prefixes[0]
  376. except KeyError:
  377. pass
  378. def get_interface_config (nodes_config, node_id, sites_config):
  379. # Get config of this node and dict of all configured ifaces
  380. node_config = nodes_config.get (node_id, {})
  381. ifaces = node_config.get ('ifaces', {})
  382. # Generate configuration entries for any batman related interfaces not
  383. # configured explicitly, but asked for implicitly by role <batman> and
  384. # a (list of) site(s) specified in the node config.
  385. _generate_batman_interface_config (node_config, ifaces, sites_config)
  386. # Generate VXLAN tunnels for every interfaces specifying 'batman_connect_sites'
  387. _generate_vxlan_interface_config (node_config, ifaces, sites_config)
  388. # Enhance ifaces configuration with some meaningful defaults for
  389. # bonding, bridge and vlan interfaces, MAC address for batman ifaces, etc.
  390. for interface, config in ifaces.items ():
  391. iface_type = config.get ('type', 'inet')
  392. if 'batman-ifaces' in config or iface_type.startswith ('batman'):
  393. _update_batman_config (node_config, interface, sites_config)
  394. if 'bond-slaves' in config:
  395. _update_bond_config (config)
  396. # FIXME: This maybe will not match on bridges without any member ports configured!
  397. if 'bridge-ports' in config or interface.startswith ('br-'):
  398. _update_bridge_config (config)
  399. if 'vlan-raw-device' in config or 'vlan-id' in config:
  400. _update_vlan_config (config)
  401. # Auto generated VRF devices for any VRF found in ifaces and not already configured.
  402. _generate_vrfs (ifaces)
  403. # Pimp GRE_FFRL type inteface configuration with default values
  404. _generate_ffrl_gre_tunnels (ifaces)
  405. # Drop any config parameters used in node interface configuration not
  406. # relevant anymore for config file generation.
  407. for interface, config in ifaces.items ():
  408. for key in [ 'batman_connect_sites', 'ospf', 'site', 'type' ]:
  409. if key in config:
  410. config.pop (key)
  411. # This leaves 'auto', 'prefixes' and 'desc' as keys which should not be directly
  412. # printed into the remaining configuration. These are handled within the jinja
  413. # interface template.
  414. return ifaces
  415. # Generate entries for /etc/bat-hosts for every batman interface we will configure on any node.
  416. # For readability purposes superflous/redundant information is being stripped/supressed.
  417. # As these names will only show up in batctl calls with a specific site, site_names in interfaces
  418. # are stripped. Dummy interfaces are stripped as well.
  419. def gen_bat_hosts (nodes_config, sites_config):
  420. bat_hosts = {}
  421. for node_id in sorted (nodes_config.keys ()):
  422. node_config = nodes_config.get (node_id)
  423. node_name = node_id.split ('.')[0]
  424. ifaces = get_interface_config (nodes_config, node_id, sites_config)
  425. for iface in sorted (ifaces):
  426. iface_config = ifaces.get (iface)
  427. hwaddress = iface_config.get ('hwaddress', None)
  428. if hwaddress == None:
  429. continue
  430. entry_name = node_name
  431. if not iface.startswith ('dummy-'):
  432. entry_name += "/%s" % re.sub (r'^(vx_.*)_(.*)$', '\g<1>', iface)
  433. bat_hosts[hwaddress] = entry_name
  434. if 'fastd' in node_config.get ('roles', []):
  435. device_no = node_config.get ('id')
  436. for site in node_config.get ('sites', []):
  437. site_no = _get_site_no (sites_config, site)
  438. for network in ('intergw', 'nodes4', 'nodes6'):
  439. hwaddress = gen_batman_iface_mac (site_no, device_no, network)
  440. bat_hosts[hwaddress] = "%s/%s" % (node_name, network)
  441. return bat_hosts