ffho_net.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180
  1. #!/usr/bin/python
  2. import collections
  3. import re
  4. mac_prefix = "f2"
  5. # VRF configuration map
  6. vrf_info = {
  7. 'vrf_external' : {
  8. 'table' : 1023,
  9. 'fwmark' : [ '0x1', '0x1023' ],
  10. },
  11. }
  12. #
  13. # Default parameters added to any given bonding interface,
  14. # if not specified at the interface configuration.
  15. default_bond_config = {
  16. 'bond-mode': '802.3ad',
  17. 'bond-min-links': '1',
  18. 'bond-xmit-hash-policy': 'layer3+4'
  19. }
  20. #
  21. # Default parameters added to any given bonding interface,
  22. # if not specified at the interface configuration.
  23. default_bridge_config = {
  24. 'bridge-fd' : '0',
  25. 'bridge-stp' : 'no'
  26. }
  27. #
  28. # Hop penalty to set if none is explicitly specified
  29. # Check if one of these roles is configured for any given node, use first match.
  30. default_hop_penalty_by_role = {
  31. 'bbr' : 5,
  32. 'bras' : 50,
  33. 'batman_gw' : 5,
  34. 'batman_ext': 50,
  35. }
  36. batman_role_evaluation_order = [ 'bbr', 'batman_gw', 'bras' ]
  37. #
  38. # Default interface attributes to be added to GRE interface to AS201701 when
  39. # not already present in pillar interface configuration.
  40. GRE_FFRL_attrs = {
  41. 'mode' : 'gre',
  42. 'method' : 'tunnel',
  43. 'mtu' : '1400',
  44. 'ttl' : '64',
  45. }
  46. # The IPv4/IPv6 prefix use for Loopback IPs
  47. loopback_prefix = {
  48. 'v4' : '10.132.255.',
  49. 'v6' : '2a03:2260:2342:ffff::',
  50. }
  51. # The DNS zone base names used for generating zone files from IP address
  52. # configured on nodes interfaces.
  53. DNS_zone_names = {
  54. 'forward' : 'ffho.net',
  55. 'rev_v4' : [
  56. '132.10.in-addr.arpa',
  57. '30.172.in-addr.arpa',
  58. ],
  59. 'rev_v6' : [
  60. '2.4.3.2.0.6.2.2.3.0.a.2.ip6.arpa',
  61. ]
  62. }
  63. # MTU configuration
  64. MTU = {
  65. # The default MTU for any interface which does not have a MTU configured
  66. # explicitly in the pillar node config or does not get a MTU configured
  67. # by any means of this SDN stuff here.
  68. 'default' : 1500,
  69. # A batman underlay device, probably a VXLAN or VLAN interface.
  70. #
  71. # 1500
  72. # + 60 B.A.T.M.A.N. adv header + network coding (activated by default by Debian)
  73. 'batman_underlay_iface' : 1560,
  74. # VXLAN underlay device, probably a VLAN with $POP or between two BBRs.
  75. #
  76. # 1560
  77. # + 14 Inner Ethernet Frame
  78. # + 8 VXLAN Header
  79. # + 8 UDP Header
  80. # + 20 IPv4 Header
  81. 'vxlan_underlay_iface' : 1610,
  82. }
  83. ################################################################################
  84. # Internal functions #
  85. # #
  86. # Touching anything below will void any warranty you never had ;) #
  87. # #
  88. ################################################################################
  89. sites = None
  90. def _get_site_no (sites_config, site_name):
  91. global sites
  92. if sites == None:
  93. sites = {}
  94. for site in sites_config:
  95. if site.startswith ("_"):
  96. continue
  97. sites[site] = sites_config[site].get ("site_no", -2)
  98. return sites.get (site_name, -1)
  99. #
  100. # Generate a MAC address after the format f2:dd:dd:ss:nn:nn where
  101. # dd:dd is the hexadecimal reprensentation of the nodes device_id
  102. # ff:ff representing the gluon nodes
  103. #
  104. # ss is the hexadecimal reprensentation of the site_id the interface is connected to
  105. #
  106. # nn:nn is the decimal representation of the network the interface is connected to, with
  107. # 00:00 being the dummy interface
  108. # 00:0f being the VEth internal side interface
  109. # 00:e0 being an external instance dummy interface
  110. # 00:e1 being an inter-gw-vpn interface
  111. # 00:e4 being an nodes fastd tunnel interface of IPv4 transport
  112. # 00:e6 being an nodes fastd tunnel interface of IPv6 transport
  113. # 00:ef being an extenral instance VEth interface side
  114. # 02:xx being a connection to local Vlan 2xx
  115. # xx:xx being a VXLAN tunnel for site ss, with xx being a the underlay VLAN ID (1xyz, 2xyz)
  116. # ff:ff being the gluon next-node interface
  117. def gen_batman_iface_mac (site_no, device_no, network):
  118. net_type_map = {
  119. 'dummy' : "00:00",
  120. 'int2ext' : "00:0f",
  121. 'dummy-e' : "00:e0",
  122. 'intergw' : "00:e1",
  123. 'nodes4' : "00:e4",
  124. 'nodes6' : "00:e6",
  125. 'ext2int' : "00:ef",
  126. }
  127. # Well-known network type?
  128. if network in net_type_map:
  129. last = net_type_map[network]
  130. elif type (network) == int:
  131. last = re.sub (r'(\d{2})(\d{2})', '\g<1>:\g<2>', "%04d" % network)
  132. else:
  133. last = "ee:ee"
  134. # Convert device_no to hex, format number to 4 digits with leading zeros and : betwwen 2nd and 3rd digit
  135. device_no_hex = re.sub (r'([0-9a-fA-F]{2})([0-9a-fA-F]{2})', '\g<1>:\g<2>', "%04x" % int (device_no))
  136. # Format site_no to two digit number with leading zero
  137. site_no_hex = "%02d" % int (site_no)
  138. return "%s:%s:%s:%s" % (mac_prefix, device_no_hex, site_no_hex, last)
  139. # Gather B.A.T.M.A.N. related config options for real batman devices (e.g. bat0)
  140. # as well as for batman member interfaces (e.g. eth0.100, fastd ifaces etc.)
  141. def _update_batman_config (node_config, iface, sites_config):
  142. try:
  143. node_batman_hop_penalty = int (node_config['batman']['hop-penalty'])
  144. except KeyError,ValueError:
  145. node_batman_hop_penalty = None
  146. iface_config = node_config['ifaces'][iface]
  147. iface_type = iface_config.get ('type', 'inet')
  148. batman_config = {}
  149. for item, value in iface_config.items ():
  150. if item.startswith ('batman-'):
  151. batman_config[item] = value
  152. iface_config.pop (item)
  153. # B.A.T.M.A.N. device (e.g. bat0)
  154. if iface_type == 'batman':
  155. if 'batman-hop-penalty' not in batman_config:
  156. # If there's a hop penalty set for the node, but not for the interface
  157. # apply the nodes hop penalty
  158. if node_batman_hop_penalty:
  159. batman_config['batman-hop-penalty'] = node_batman_hop_penalty
  160. # If there's no hop penalty set for the node, use a default hop penalty
  161. # for the roles the node might have, if any
  162. else:
  163. node_roles = node_config.get ('roles', [])
  164. for role in batman_role_evaluation_order:
  165. if role in node_roles:
  166. batman_config['batman-hop-penalty'] = default_hop_penalty_by_role[role]
  167. if 'batman_ext' in node_roles and iface.endswith('-ext'):
  168. batman_config['batman-hop-penalty'] = default_hop_penalty_by_role['batman_ext']
  169. # If batman ifaces were specified as a list - which they should -
  170. # generate a sorted list of interface names as string representation
  171. if 'batman-ifaces' in batman_config and type (batman_config['batman-ifaces']) == list:
  172. batman_iface_str = " ".join (sorted (batman_config['batman-ifaces']))
  173. batman_config['batman-ifaces'] = batman_iface_str
  174. # B.A.T.M.A.N. member interface (e.g. eth.100, fastd ifaces, etc.)
  175. elif iface_type == 'batman_iface':
  176. # Generate unique MAC address for every batman iface, as B.A.T.M.A.N.
  177. # will get puzzled with multiple interfaces having the same MAC and
  178. # do nasty things.
  179. site = iface_config.get ('site')
  180. site_no = _get_site_no (sites_config, site)
  181. device_no = node_config.get ('id')
  182. network = 1234
  183. # Generate a unique BATMAN-MAC for this interfaces
  184. match = re.search (r'^vlan(\d+)', iface)
  185. if match:
  186. network = int (match.group (1))
  187. iface_config['hwaddress'] = gen_batman_iface_mac (site_no, device_no, network)
  188. iface_config['batman'] = batman_config
  189. # Mangle bond specific config items with default values and store them in
  190. # separate sub-dict for easier access and configuration.
  191. def _update_bond_config (config):
  192. bond_config = default_bond_config.copy ()
  193. for item, value in config.items ():
  194. if item.startswith ('bond-'):
  195. bond_config[item] = value
  196. config.pop (item)
  197. if bond_config['bond-mode'] not in ['2', 'balance-xor', '4', '802.3ad']:
  198. bond_config.pop ('bond-xmit-hash-policy')
  199. config['bond'] = bond_config
  200. # Mangle bridge specific config items with default values and store them in
  201. # separate sub-dict for easier access and configuration.
  202. def _update_bridge_config (config):
  203. bridge_config = default_bridge_config.copy ()
  204. for item, value in config.items ():
  205. if item.startswith ('bridge-'):
  206. bridge_config[item] = value
  207. config.pop (item)
  208. # Fix and salt mangled string interpretation back to real string.
  209. if type (value) == bool:
  210. bridge_config[item] = "yes" if value else "no"
  211. # If bridge ports were specified as a list - which they should -
  212. # generate a sorted list of interface names as string representation
  213. if 'bridge-ports' in bridge_config and type (bridge_config['bridge-ports']) == list:
  214. bridge_ports_str = " ".join (sorted (bridge_config['bridge-ports']))
  215. bridge_config['bridge-ports'] = bridge_ports_str
  216. config['bridge'] = bridge_config
  217. # Move vlan specific config items into a sub-dict for easier access and pretty-printing
  218. # in the configuration file
  219. def _update_vlan_config (config):
  220. vlan_config = {}
  221. for item, value in config.items ():
  222. if item.startswith ('vlan-'):
  223. vlan_config[item] = value
  224. config.pop (item)
  225. config['vlan'] = vlan_config
  226. # Pimp Veth interfaces
  227. # * Add peer interface name IF not present
  228. # * Add link-type veth IF not present
  229. def _update_veth_config (interface, config):
  230. veth_peer_name = {
  231. 'veth_ext2int' : 'veth_int2ext',
  232. 'veth_int2ext' : 'veth_ext2int'
  233. }
  234. if interface not in veth_peer_name:
  235. return
  236. if 'link-type' not in config:
  237. config['link-type'] = 'veth'
  238. if 'veth-peer-name' not in config:
  239. config['veth-peer-name'] = veth_peer_name[interface]
  240. # The the given MTU to the given interface - presented by it's interface config dict -
  241. # IFF no MTU has already been set in the node pillar.
  242. #
  243. # @param ifaces: All interface configuration (as dict)
  244. # @param iface_name: Name of the interface to set MTU for
  245. # @param mtu: The MTU value to set (integer)
  246. def _set_mtu_to_iface_and_upper (ifaces, iface_name, mtu):
  247. iface_config = ifaces.get (iface_name)
  248. # If this interface already has a MTU set - probably because someone manually
  249. # specified one in the node pillar - we do not do anything here.
  250. if 'mtu' in iface_config:
  251. return
  252. # Set generated MTU as 'automtu' value to allow distinction between manually
  253. # set and autogenerated MTU values.
  254. # There might be - read: "we have" - a situation where on top of e.g. bond0
  255. # there are vlans holding VXLAN communicaton as well a vlans directly carrying
  256. # BATMAN traffic. Now depending on which interface is evaluation first, the upper
  257. # MTU is either correct, or maybe to small.
  258. #
  259. # If any former autogenerated MTU is greater-of-equal that the one we want to
  260. # set now, we'll ignore it, and go for the greater one.
  261. if 'automtu' in iface_config and iface_config['automtu'] >= mtu:
  262. return
  263. # Set given MTU to this device.
  264. iface_config['automtu'] = mtu
  265. # If this is a VLAN - which it probably is - fix the MTU of the underlying interface, too.
  266. if 'vlan-raw-device' in iface_config:
  267. vlan_raw_device = iface_config['vlan-raw-device']
  268. vlan_raw_device_config = ifaces.get (vlan_raw_device, None)
  269. # vlan-raw-device might point to ethX which usually isn't configured explicitly
  270. # as ifupdown2 simply will bring it up anyway by itself. To set the MTU of such
  271. # an interface we have to add a configuration stanza for it here.
  272. if vlan_raw_device_config == None:
  273. vlan_raw_device_config = {}
  274. ifaces[vlan_raw_device] = vlan_raw_device_config
  275. # If there is a manually set MTU for this device, we don't do nothin'
  276. if 'mtu' in vlan_raw_device_config:
  277. return
  278. if 'automtu' in vlan_raw_device_config and vlan_raw_device_config['automtu'] >= mtu:
  279. return
  280. vlan_raw_device_config['automtu'] = mtu
  281. # Generate configuration entries for any batman related interfaces not
  282. # configured explicitly, but asked for implicitly by role batman and a
  283. # (list of) site(s) specified in the node config.
  284. def _generate_batman_interface_config (node_config, ifaces, sites_config):
  285. # No role 'batman', nothing to do
  286. roles = node_config.get ('roles', [])
  287. if 'batman' not in roles:
  288. return
  289. # Should there be a 2nd external BATMAN instance?
  290. batman_ext = 'batman_ext' in roles or 'bras' in roles
  291. device_no = node_config.get ('id', -1)
  292. for site in node_config.get ('sites', []):
  293. site_no = _get_site_no (sites_config, site)
  294. # Predefine interface names for regular/external BATMAN instance
  295. # and possible VEth link pair for connecting both instances.
  296. bat_site_if = "bat-%s" % site
  297. dummy_site_if = "dummy-%s" % site
  298. bat_site_if_ext = "bat-%s-ext" % site
  299. dummy_site_if_ext = "dummy-%s-e" % site
  300. int2ext_site_if = "i2e-%s" % site
  301. ext2int_site_if = "e2i-%s" % site
  302. site_ifaces = {
  303. # Regular BATMAN interface, always present
  304. bat_site_if : {
  305. 'type' : 'batman',
  306. # int2ext_site_if will be added automagically if requred
  307. 'batman-ifaces' : [ dummy_site_if ],
  308. 'batman-ifaces-ignore-regex': '.*_.*',
  309. },
  310. # Dummy interface always present in regular BATMAN instance
  311. dummy_site_if : {
  312. 'link-type' : 'dummy',
  313. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'dummy'),
  314. 'mtu' : MTU['batman_underlay_iface'],
  315. },
  316. # Optional 2nd "external" BATMAN instance
  317. bat_site_if_ext : {
  318. 'type' : 'batman',
  319. 'batman-ifaces' : [ dummy_site_if_ext, ext2int_site_if ],
  320. 'batman-ifaces-ignore-regex': '.*_.*',
  321. 'ext_only' : True,
  322. },
  323. # Optional dummy interface always present in 2nd "external" BATMAN instance
  324. dummy_site_if_ext : {
  325. 'link-type' : 'dummy',
  326. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'dummy-e'),
  327. 'ext_only' : True,
  328. 'mtu' : MTU['batman_underlay_iface'],
  329. },
  330. # Optional VEth interface pair - internal side
  331. int2ext_site_if : {
  332. 'link-type' : 'veth',
  333. 'veth-peer-name' : ext2int_site_if,
  334. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'int2ext'),
  335. 'mtu' : MTU['batman_underlay_iface'],
  336. 'ext_only' : True,
  337. },
  338. # Optional VEth interface pair - "external" side
  339. ext2int_site_if : {
  340. 'link-type' : 'veth',
  341. 'veth-peer-name' : int2ext_site_if,
  342. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'ext2int'),
  343. 'mtu' : MTU['batman_underlay_iface'],
  344. 'ext_only' : True,
  345. },
  346. }
  347. for iface, iface_config_tmpl in site_ifaces.items ():
  348. # Ignore any interface only relevant when role batman_ext is set
  349. # but it isn't
  350. if not batman_ext and iface_config_tmpl.get ('ext_only', False):
  351. continue
  352. # Remove ext_only key so we don't leak it into ifaces dict
  353. if 'ext_only' in iface_config_tmpl:
  354. del iface_config_tmpl['ext_only']
  355. # If there is no trace of the desired iface config yet...
  356. if iface not in ifaces:
  357. # ... just place our template there.
  358. ifaces[iface] = iface_config_tmpl
  359. # If there should be an 2nd external BATMAN instance make sure
  360. # the internal side of the VEth iface pair is connected to the
  361. # internal BATMAN instance.
  362. if batman_ext and iface == bat_site_if:
  363. iface_config_tmpl['batman-ifaces'].append (int2ext_site_if)
  364. # If there already is an interface configuration try to enhance it with
  365. # meaningful values from our template and force correct hwaddress to be
  366. # used.
  367. else:
  368. iface_config = ifaces[iface]
  369. # Force hwaddress to be what we expect.
  370. if 'hwaddress' in iface_config_tmpl:
  371. iface_config['hwaddress'] = iface_config_tmpl['hwaddress']
  372. # Copy every attribute of the config template missing in iface config
  373. for attr in iface_config_tmpl:
  374. if attr not in iface_config:
  375. iface_config[attr] = iface_config_tmpl[attr]
  376. # Make sure there is a bridge present for every site where a mesh_breakout
  377. # interface should be configured.
  378. for iface, config in ifaces.items ():
  379. iface_type = config.get ('type', 'inet')
  380. if iface_type not in ['mesh_breakout', 'batman_iface']:
  381. continue
  382. site = config.get ('site')
  383. site_bridge = "br-%s" % site
  384. batman_site_if = "bat-%s" % site
  385. if iface_type == 'mesh_breakout':
  386. # If the bridge has already been defined (with an IP maybe) make
  387. # sure that the corresbonding batman device is part of the bridge-
  388. # ports.
  389. if site_bridge in ifaces:
  390. bridge_config = ifaces.get (site_bridge)
  391. # If there already is/are (a) bridge-port(s) defined, add
  392. # the batman and the breakout interfaces if not present...
  393. bridge_ports = bridge_config.get ('bridge-ports', None)
  394. if bridge_ports:
  395. for dev in (batman_site_if, iface):
  396. if not dev in bridge_ports:
  397. if type (bridge_ports) == list:
  398. bridge_ports.append (dev)
  399. else:
  400. bridge_config['bridge-ports'] += ' ' + dev
  401. # ...if there is no bridge-port defined yet, just used
  402. # the batman and breakout iface.
  403. else:
  404. bridge_config['bridge-ports'] = [ iface, batman_site_if ]
  405. # If the bridge isn't present alltogether, add it.
  406. else:
  407. ifaces[site_bridge] = {
  408. 'bridge-ports' : [ iface, batman_site_if ],
  409. }
  410. elif iface_type == 'batman_iface':
  411. batman_ifaces = ifaces[bat_site_if]['batman-ifaces']
  412. if iface not in batman_ifaces:
  413. if type (batman_ifaces) == list:
  414. batman_ifaces.append (iface)
  415. else:
  416. batman_ifaces += ' ' + iface
  417. _set_mtu_to_iface_and_upper (ifaces, iface, MTU['batman_underlay_iface'])
  418. #
  419. # Generate any implicitly defined VXLAN interfaces defined in the nodes iface
  420. # defined in pillar.
  421. # The keyword "batman_connect_sites" on an interface will trigger the
  422. # generation of a VXLAN overlay interfaces.
  423. def _generate_vxlan_interface_config (node_config, ifaces, sites_config):
  424. # No role 'batman', nothing to do
  425. if 'batman' not in node_config.get ('roles', []):
  426. return
  427. # Sites configured on this node. Nothing to do, if none.
  428. my_sites = node_config.get ('sites', [])
  429. if len (my_sites) == 0:
  430. return
  431. # As we're still here we can now safely assume that a B.A.T.M.A.N.
  432. # device has been configured for every site specified in sites list.
  433. device_no = node_config.get ('id', -1)
  434. for iface, iface_config in ifaces.items ():
  435. batman_connect_sites = iface_config.get ('batman_connect_sites', [])
  436. # If we got a string, convert it to a list with a single element
  437. if type (batman_connect_sites) == str:
  438. batman_connect_sites = [ batman_connect_sites ]
  439. # If there the list of sites to connect is empty, there's nothing to do here.
  440. if len (batman_connect_sites) == 0:
  441. continue
  442. # Set the MTU of this (probably) VLAN device to the MTU required for a VXLAN underlay
  443. # device, where B.A.T.M.A.N. adv. is to be expected within the VXLAN overlay.
  444. _set_mtu_to_iface_and_upper (ifaces, iface, MTU['vxlan_underlay_iface'])
  445. # If the string 'all' is part of the list, blindly use all sites configured for this node
  446. if 'all' in batman_connect_sites:
  447. batman_connect_sites = my_sites
  448. for site in batman_connect_sites:
  449. # Silenty ignore sites not configured on this node
  450. if site not in my_sites:
  451. continue
  452. # iface_name := vx_<last 5 chars of underlay iface>_<site> stripped to 15 chars
  453. vx_iface = ("vx_%s_%s" % (re.sub ('vlan', 'v', iface)[-5:], re.sub (r'[_-]', '', site)))[:15]
  454. site_no = _get_site_no (sites_config, site)
  455. vni = 100 + site_no
  456. bat_iface = "bat-%s" % site
  457. try:
  458. iface_id = int (re.sub ('vlan', '', iface))
  459. # Gather interface specific mcast address.
  460. # The address is derived from the vlan-id of the underlying interface,
  461. # assuming that it in fact is a vlan interface.
  462. # Mangle the vlan-id into two 2 digit values, eliminating any leading zeros.
  463. iface_id_4digit = "%04d" % iface_id
  464. octet2 = int (iface_id_4digit[0:2])
  465. octet3 = int (iface_id_4digit[2:4])
  466. mcast_ip = "225.%s.%s.%s" % (octet2, octet3, site_no)
  467. vni = octet2 * 256 * 256 + octet3 * 256 + site_no
  468. except ValueError:
  469. iface_id = 9999
  470. mcast_ip = "225.0.0.%s" % site_no
  471. vni = site_no
  472. # bail out if VXLAN tunnel already configured
  473. if vx_iface in ifaces:
  474. continue
  475. # If there's no batman interface for this site, there's no point
  476. # in setting up a VXLAN interfaces
  477. if bat_iface not in ifaces:
  478. continue
  479. # Add the VXLAN interface
  480. ifaces[vx_iface] = {
  481. 'vxlan' : {
  482. 'vxlan-id' : vni,
  483. 'vxlan-svcnodeip' : mcast_ip,
  484. 'vxlan-physdev' : iface,
  485. },
  486. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, iface_id),
  487. 'mtu' : MTU['batman_underlay_iface'],
  488. }
  489. # If the batman interface for this site doesn't have any interfaces
  490. # set up - which basicly cannot happen - add this VXLAN tunnel as
  491. # the first in the list.
  492. if not 'batman-ifaces' in ifaces[bat_iface]:
  493. ifaces[bat_iface]['batman-ifaces'] = [ vx_iface ]
  494. continue
  495. # In the hope there already are interfaces for batman set up already
  496. # add this VXLAN tunnel to the list
  497. batman_ifaces = ifaces[bat_iface]['batman-ifaces']
  498. if vx_iface not in batman_ifaces:
  499. if type (batman_ifaces) == list:
  500. batman_ifaces.append (vx_iface)
  501. else:
  502. batman_ifaces += ' ' + vx_iface
  503. #
  504. # Generate implicitly defined VRFs according to the vrf_info dict at the top
  505. # of this file
  506. def _generate_vrfs (ifaces):
  507. for iface, iface_config in ifaces.items ():
  508. vrf = iface_config.get ('vrf', None)
  509. if vrf and vrf not in ifaces:
  510. conf = vrf_info.get (vrf, {})
  511. table = conf.get ('table', 1234)
  512. fwmark = conf.get ('fwmark', None)
  513. ifaces[vrf] = {
  514. 'vrf-table' : table,
  515. }
  516. # Create ip rule's for any fwmarks defined
  517. if fwmark:
  518. up = []
  519. # Make sure we are dealing with a list even if there is only one mark to be set up
  520. if type (fwmark) in (str, int):
  521. fwmark = [ fwmark ]
  522. # Create ip rule entries for IPv4 and IPv6 for every fwmark
  523. for mark in fwmark:
  524. up.append ("ip rule add fwmark %s table %s" % (mark, table))
  525. up.append ("ip -6 rule add fwmark %s table %s" % (mark, table))
  526. ifaces[vrf]['up'] = up
  527. def _generate_ffrl_gre_tunnels (ifaces):
  528. for iface, iface_config in ifaces.items ():
  529. # We only care for GRE_FFRL type interfaces
  530. if iface_config.get ('type', '') != 'GRE_FFRL':
  531. continue
  532. # Copy default values to interface config
  533. for attr, val in GRE_FFRL_attrs.items ():
  534. if not attr in iface_config:
  535. iface_config[attr] = val
  536. # Guesstimate local IPv4 tunnel endpoint address from tunnel-physdev
  537. if not 'local' in iface_config and 'tunnel-physdev' in iface_config:
  538. try:
  539. physdev_prefixes = [p.split ('/')[0] for p in ifaces[iface_config['tunnel-physdev']]['prefixes'] if '.' in p]
  540. if len (physdev_prefixes) == 1:
  541. iface_config['local'] = physdev_prefixes[0]
  542. except KeyError:
  543. pass
  544. def _generate_loopback_ips (ifaces, node_config, node_id):
  545. v4_ip = "%s/32" % get_loopback_ip (node_config, node_id, 'v4')
  546. v6_ip = "%s/128" % get_loopback_ip (node_config, node_id, 'v6')
  547. # Interface lo already present?
  548. if 'lo' not in ifaces:
  549. ifaces['lo'] = { 'prefixes' : [] }
  550. # Add 'prefixes' list if not present
  551. if 'prefixes' not in ifaces['lo']:
  552. ifaces['lo']['prefixes'] = []
  553. prefixes = ifaces['lo']['prefixes']
  554. if v4_ip not in prefixes:
  555. prefixes.append (v4_ip)
  556. if v6_ip not in prefixes:
  557. prefixes.append (v6_ip)
  558. # Generate interface descriptions / aliases for auto generated or manually
  559. # created interfaces. Currently this only is done for bridges associated
  560. # with BATMAN instanzes.
  561. #
  562. # @param node_config: The configuration of the given node (as dict)
  563. # @param sites_config Global sites configuration (as dict)
  564. def _update_interface_desc (node_config, sites_config):
  565. # Currently we only care for nodes with batman role.
  566. if 'batman' not in node_config.get ('roles', []):
  567. return
  568. for iface, iface_config in node_config.get ('ifaces', {}).items ():
  569. if 'desc' in sites_config:
  570. continue
  571. # If the interface name looks like a bridge for a BATMAN instance
  572. # try to get the name of the corresponding site
  573. match = re.search (r'^br-([a-z_-]+)$', iface)
  574. if match and match.group (1) in sites_config:
  575. try:
  576. iface_config['desc'] = sites_config[match.group (1)]['name']
  577. except KeyError:
  578. pass
  579. ################################################################################
  580. # Public functions #
  581. ################################################################################
  582. # Generate network interface configuration for given node.
  583. #
  584. # This function will read the network configuration from pillar and will
  585. # * enhance it with all default values configured at the top this file
  586. # * auto generate any implicitly configured
  587. # * VRFs
  588. # * B.A.T.M.A.N. instances and interfaces
  589. # * VXLAN interfaces to connect B.A.T.M.A.N. sites
  590. # * Loopback IPs derived from numeric node ID
  591. #
  592. # @param: node_config Pillar node configuration (as dict)
  593. # @param: sites_config Pillar sites configuration (as dict)
  594. # @param: node_id Minion name / Pillar node configuration key
  595. def get_interface_config (node_config, sites_config, node_id = ""):
  596. # Get config of this node and dict of all configured ifaces
  597. ifaces = node_config.get ('ifaces', {})
  598. # Generate configuration entries for any batman related interfaces not
  599. # configured explicitly, but asked for implicitly by role <batman> and
  600. # a (list of) site(s) specified in the node config.
  601. _generate_batman_interface_config (node_config, ifaces, sites_config)
  602. # Generate VXLAN tunnels for every interfaces specifying 'batman_connect_sites'
  603. _generate_vxlan_interface_config (node_config, ifaces, sites_config)
  604. # Enhance ifaces configuration with some meaningful defaults for
  605. # bonding, bridge and vlan interfaces, MAC address for batman ifaces, etc.
  606. for interface, config in ifaces.items ():
  607. # if type (config) not in [ dict, collections.OrderedDict ]:
  608. # raise Exception ("Configuration for interface %s on node %s seems broken: Type %s" % (interface, node_id, type (config)))
  609. iface_type = config.get ('type', 'inet')
  610. if 'batman-ifaces' in config or iface_type.startswith ('batman'):
  611. _update_batman_config (node_config, interface, sites_config)
  612. if 'bond-slaves' in config:
  613. _update_bond_config (config)
  614. # FIXME: This maybe will not match on bridges without any member ports configured!
  615. if 'bridge-ports' in config or interface.startswith ('br-'):
  616. _update_bridge_config (config)
  617. if 'vlan-raw-device' in config or 'vlan-id' in config:
  618. _update_vlan_config (config)
  619. # Pimp configuration for VEth link pairs
  620. if interface.startswith ('veth_'):
  621. _update_veth_config (interface, config)
  622. # Auto generate Loopback IPs IFF not present
  623. _generate_loopback_ips (ifaces, node_config, node_id)
  624. # Auto generated VRF devices for any VRF found in ifaces and not already configured.
  625. _generate_vrfs (ifaces)
  626. # Pimp GRE_FFRL type inteface configuration with default values
  627. _generate_ffrl_gre_tunnels (ifaces)
  628. # Drop any config parameters used in node interface configuration not
  629. # relevant anymore for config file generation.
  630. for interface, config in ifaces.items ():
  631. # Set default MTU if not already set manually or by any earlier function
  632. if interface != 'lo' and ('mtu' not in config):
  633. # Set the MTU value of this interface to the autogenerated value (if any)
  634. # or set the default, when no automtu is present.
  635. config['mtu'] = config.get ('automtu', MTU['default'])
  636. for key in [ 'automtu', 'batman_connect_sites', 'ospf', 'site', 'type' ]:
  637. if key in config:
  638. config.pop (key)
  639. # This leaves 'auto', 'prefixes' and 'desc' as keys which should not be directly
  640. # printed into the remaining configuration. These are handled within the jinja
  641. # interface template.
  642. # Generate meaningful interface descriptions / aliases where useful
  643. _update_interface_desc (node_config, sites_config)
  644. return ifaces
  645. # Generate entries for /etc/bat-hosts for every batman interface we will configure on any node.
  646. # For readability purposes superflous/redundant information is being stripped/supressed.
  647. # As these names will only show up in batctl calls with a specific site, site_names in interfaces
  648. # are stripped. Dummy interfaces are stripped as well.
  649. def gen_bat_hosts (nodes_config, sites_config):
  650. bat_hosts = {}
  651. for node_id in sorted (nodes_config.keys ()):
  652. node_config = nodes_config.get (node_id)
  653. node_name = node_id.split ('.')[0]
  654. ifaces = get_interface_config (node_config, sites_config, node_id)
  655. for iface in sorted (ifaces):
  656. iface_config = ifaces.get (iface)
  657. hwaddress = iface_config.get ('hwaddress', None)
  658. if hwaddress == None:
  659. continue
  660. entry_name = node_name
  661. match = re.search (r'^dummy-(.+)(-e)?$', iface)
  662. if match:
  663. if match.group (2):
  664. entry_name += "-e"
  665. # Append site to make name unique
  666. entry_name += "/%s" % match.group (1)
  667. else:
  668. entry_name += "/%s" % re.sub (r'^(vx_.*|i2e|e2i)[_-](.*)$', '\g<1>/\g<2>', iface)
  669. bat_hosts[hwaddress] = entry_name
  670. if 'fastd' in node_config.get ('roles', []):
  671. device_no = node_config.get ('id')
  672. for site in node_config.get ('sites', []):
  673. site_no = _get_site_no (sites_config, site)
  674. for network in ('intergw', 'nodes4', 'nodes6'):
  675. hwaddress = gen_batman_iface_mac (site_no, device_no, network)
  676. bat_hosts[hwaddress] = "%s/%s/%s" % (node_name, network, site)
  677. return bat_hosts
  678. # Generate eBGP session parameters for FFRL Transit from nodes pillar information.
  679. def get_ffrl_bgp_config (ifaces, proto):
  680. from ipcalc import IP
  681. _generate_ffrl_gre_tunnels (ifaces)
  682. sessions = {}
  683. for iface in sorted (ifaces):
  684. # We only care for GRE tunnels to the FFRL Backbone
  685. if not iface.startswith ('gre_ffrl_'):
  686. continue
  687. iface_config = ifaces.get (iface)
  688. # Search for IPv4/IPv6 prefix as defined by proto parameter
  689. local = None
  690. neighbor = None
  691. for prefix in iface_config.get ('prefixes', []):
  692. if (proto == 'v4' and '.' in prefix) or (proto == 'v6' and ':' in prefix):
  693. local = prefix.split ('/')[0]
  694. # Calculate neighbor IP as <local IP> - 1
  695. if proto == 'v4':
  696. neighbor = str (IP (int (IP (local)) - 1, version = 4))
  697. else:
  698. neighbor = str (IP (int (IP (local)) - 1, version = 6))
  699. break
  700. # Strip gre_ prefix iface name and use it as identifier for the eBGP session.
  701. name = re.sub ('gre_ffrl_', 'ffrl_', iface)
  702. sessions[name] = {
  703. 'local' : local,
  704. 'neighbor' : neighbor,
  705. 'bgp_local_pref' : iface_config.get ('bgp_local_pref', None),
  706. }
  707. return sessions
  708. # Get list of IP address configured on given interface on given node.
  709. #
  710. # @param: node_config Pillar node configuration (as dict)
  711. # @param: iface_name Name of the interface defined in pillar node config
  712. # OR name of VRF ("vrf_<something>") whichs ifaces are
  713. # to be examined.
  714. def get_node_iface_ips (node_config, iface_name):
  715. ips = {
  716. 'v4' : [],
  717. 'v6' : [],
  718. }
  719. ifaces = node_config.get ('ifaces', {})
  720. ifaces_names = [ iface_name ]
  721. if iface_name.startswith ('vrf_'):
  722. # Reset list of ifaces_names to consider
  723. ifaces_names = []
  724. vrf = iface_name
  725. for iface, iface_config in ifaces.items ():
  726. # Ignore any iface NOT in the given VRF
  727. if iface_config.get ('vrf', None) != vrf:
  728. continue
  729. # Ignore any VEth pairs
  730. if iface.startswith ('veth'):
  731. continue
  732. ifaces_names.append (iface)
  733. try:
  734. for iface in ifaces_names:
  735. for prefix in ifaces[iface]['prefixes']:
  736. ip_ver = 'v6' if ':' in prefix else 'v4'
  737. ips[ip_ver].append (prefix.split ('/')[0])
  738. except KeyError:
  739. pass
  740. return ips
  741. #
  742. # Get the lookback IP of the given node for the given proto
  743. #
  744. # @param node_config: Pillar node configuration (as dict)
  745. # @param node_id: Minion name / Pillar node configuration key
  746. # @param proto: { 'v4', 'v6' }
  747. def get_loopback_ip (node_config, node_id, proto):
  748. if proto not in [ 'v4', 'v6' ]:
  749. raise Exception ("get_loopback_ip(): Invalid proto: \"%s\"." % proto)
  750. if not proto in loopback_prefix:
  751. raise Exception ("get_loopback_ip(): No loopback_prefix configured for IP%s in ffno_net module!" % proto)
  752. if not 'id' in node_config:
  753. raise Exception ("get_loopback_ip(): No 'id' configured in pillar for node \"%s\"!" % node_id)
  754. # Every rule has an exception.
  755. # If there is a loopback_overwrite configuration for this node, use this instead of
  756. # the generated IPs.
  757. if 'loopback_override' in node_config:
  758. if proto not in node_config['loopback_override']:
  759. raise Exception ("get_loopback_ip(): No loopback_prefix configured for IP%s in node config / loopback_override!" % proto)
  760. return node_config['loopback_override'][proto]
  761. return "%s%s" % (loopback_prefix.get (proto), node_config.get ('id'))
  762. #
  763. # Get the router id (read: IPv4 Lo-IP) out of the given node config.
  764. def get_router_id (node_config, node_id):
  765. return get_loopback_ip (node_config, node_id, 'v4')
  766. # Compute minions OSPF interface configuration according to FFHO routing policy
  767. # See https://wiki.ffho.net/infrastruktur:vlans for information about Vlans
  768. def get_ospf_interface_config (node_config, grains_id):
  769. ospf_node_config = node_config.get ('ospf', {})
  770. ospf_interfaces = {}
  771. for iface, iface_config in node_config.get ('ifaces', {}).items ():
  772. # By default we don't speak OSPF on interfaces
  773. ospf_on = False
  774. # Defaults for OSPF interfaces
  775. ospf_config = {
  776. 'stub' : True, # Active/Passive interface
  777. 'cost' : 12345,
  778. # 'type' # Area type
  779. }
  780. # OSPF configuration for interface given?
  781. ospf_config_pillar = iface_config.get ('ospf', {})
  782. # Local Gigabit Ethernet based connections (PTP or L2 subnets), cost 10
  783. if re.search (r'^(br-?|br\d+\.|vlan)10\d\d$', iface):
  784. ospf_on = True
  785. ospf_config['stub'] = False
  786. ospf_config['cost'] = 10
  787. ospf_config['desc'] = "Wired Gigabit connection"
  788. # AF-X based WBBL connection
  789. elif re.search (r'^vlan20\d\d$', iface):
  790. ospf_on = True
  791. ospf_config['stub'] = False
  792. ospf_config['cost'] = 100
  793. ospf_config['desc'] = "AF-X based WBBL connection"
  794. # Non-AF-X based WBBL connection
  795. elif re.search (r'^vlan22\d\d$', iface):
  796. ospf_on = True
  797. ospf_config['stub'] = False
  798. ospf_config['cost'] = 1000
  799. ospf_config['desc'] = "Non-AF-X based WBBL connection"
  800. # Management Vlans
  801. elif re.search (r'^vlan30\d\d$', iface):
  802. ospf_on = True
  803. ospf_config['stub'] = True
  804. ospf_config['cost'] = 10
  805. # Active OSPF on OpenVPN tunnels, cost 10000
  806. elif iface.startswith ('ovpn-'):
  807. ospf_on = True
  808. ospf_config['stub'] = False
  809. ospf_config['cost'] = 10000
  810. # Inter-Core links should have cost 5000
  811. if iface.startswith ('ovpn-cr') and grains_id.startswith ('cr'):
  812. ospf_config['cost'] = 5000
  813. # OpenVPN tunnels to EdgeRouters
  814. elif iface.startswith ('ovpn-er-'):
  815. ospf_config['type'] = 'broadcast'
  816. # Configure Out-of-band OpenVPN tunnels as stub interfaces,
  817. # so recursive next-hop lookups for OOB-BGP-session will work.
  818. elif iface.startswith ('oob-'):
  819. ospf_on = True
  820. ospf_config['stub'] = True
  821. ospf_config['cost'] = 1000
  822. # OSPF explicitly enabled for interface
  823. elif 'ospf' in iface_config:
  824. ospf_on = True
  825. # iface ospf parameters will be applied later
  826. # Go on if OSPF should not be actived
  827. if not ospf_on:
  828. continue
  829. # Explicit OSPF interface configuration parameters take precendence over generated ones
  830. for attr, val in ospf_config_pillar:
  831. ospf_config[attr] = val
  832. # Convert boolean values to 'yes' / 'no' string values
  833. for attr, val in ospf_config.items ():
  834. if type (val) == bool:
  835. ospf_config[attr] = 'yes' if val else 'no'
  836. # Store interface configuration
  837. ospf_interfaces[iface] = ospf_config
  838. return ospf_interfaces
  839. # Return (possibly empty) subset of Traffic Engineering entries from 'te' pillar entry
  840. # relevenant for this minion and protocol (IPv4 / IPv6)
  841. def get_te_prefixes (te_node_config, grains_id, proto):
  842. te_config = {}
  843. for prefix, prefix_config in te_node_config.get ('prefixes', {}).items ():
  844. prefix_proto = 'v6' if ':' in prefix else 'v4'
  845. # Should this TE policy be applied on this node and is the prefix
  846. # of the proto we are looking for?
  847. if grains_id in prefix_config.get ('nodes', []) and prefix_proto == proto:
  848. te_config[prefix] = prefix_config
  849. return te_config
  850. def generate_DNS_entries (nodes_config, sites_config):
  851. import ipaddress
  852. forward_zone_name = ""
  853. forward_zone = []
  854. zones = {
  855. # <forward_zone_name>: [],
  856. # <rev_zone1_name>: [],
  857. # <rev_zone2_name>: [],
  858. # ...
  859. }
  860. # Fill zones dict with zones configured in DNS_zone_names at the top of this file.
  861. # Make sure the zone base names provided start with a leading . so the string
  862. # operations later can be done easily and safely. Proceed with fingers crossed.
  863. for entry, value in DNS_zone_names.items ():
  864. if entry == "forward":
  865. zone = value
  866. if not zone.startswith ('.'):
  867. zone = ".%s" % zone
  868. zones[zone] = forward_zone
  869. forward_zone_name = zone
  870. if entry in [ 'rev_v4', 'rev_v6' ]:
  871. for zone in value:
  872. if not zone.startswith ('.'):
  873. zone = ".%s" % zone
  874. zones[zone] = []
  875. # Process all interfaace of all nodes defined in pillar and generate forward
  876. # and reverse entries for all zones defined in DNS_zone_names. Automagically
  877. # put reverse entries into correct zone.
  878. for node_id in sorted (nodes_config):
  879. node_config = nodes_config.get (node_id)
  880. ifaces = get_interface_config (node_config, sites_config, node_id)
  881. for iface in sorted (ifaces):
  882. iface_config = ifaces.get (iface)
  883. # We only care for interfaces with IPs configured
  884. prefixes = iface_config.get ("prefixes", None)
  885. if prefixes == None:
  886. continue
  887. # Ignore any interface in $VRF
  888. if iface_config.get ('vrf', "") in [ 'vrf_external' ]:
  889. continue
  890. for prefix in sorted (prefixes):
  891. ip = ipaddress.ip_address (u'%s' % prefix.split ('/')[0])
  892. proto = 'v%s' % ip.version
  893. # The entry name is
  894. # <node_id> when interface 'lo'
  895. # <node_name>.srv.<residual> when interface 'srv' (or magically detected internal srv record)
  896. # <interface>.<node_id> else
  897. entry_name = node_id
  898. if iface != "lo":
  899. entry_name = "%s.%s" % (iface, node_id)
  900. elif iface == 'srv' or re.search (r'^(10.132.251|2a03:2260:2342:f251:)', prefix):
  901. entry_name = re.sub (r'^([^.]+)\.(.+)$', r'\g<1>.srv.\g<2>', entry_name)
  902. # Strip forward zone name from entry_name and store forward entry
  903. # with correct entry type for found IP address.
  904. forward_entry_name = re.sub (forward_zone_name, "", entry_name)
  905. forward_entry_name = re.sub (forward_zone_name, "", entry_name)
  906. forward_entry_typ = "A" if ip.version == 4 else "AAAA"
  907. forward_zone.append ("%s IN %s %s" % (forward_entry_name, forward_entry_typ, ip))
  908. # Find correct reverse zone, if configured and strip reverse zone name
  909. # from calculated reverse pointer name. Store reverse entry if we found
  910. # a zone for it. If no configured reverse zone did match, this reverse
  911. # entry will be ignored.
  912. for zone in zones:
  913. if ip.reverse_pointer.find (zone) > 0:
  914. PTR_entry = re.sub (zone, "", ip.reverse_pointer)
  915. zones[zone].append ("%s IN PTR %s." % (PTR_entry, entry_name))
  916. break
  917. return zones
  918. # Convert the CIDR network from the given prefix into a dotted netmask
  919. def cidr_to_dotted_mask (prefix):
  920. from ipcalc import Network
  921. return str (Network (prefix).netmask ())
  922. def is_subprefix (prefix, subprefix):
  923. from ipcalc import Network
  924. return subprefix in Network(prefix)