ffho_net.py 33 KB

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