ffho_net.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523
  1. #!/usr/bin/python
  2. import collections
  3. from functools import cmp_to_key
  4. import ipaddress
  5. import re
  6. from copy import deepcopy
  7. mac_prefix = "f2"
  8. # VRF configuration map
  9. vrf_info = {
  10. 'vrf_external' : {
  11. 'table' : 1023,
  12. 'fwmark' : [ '0x1', '0x1023' ],
  13. },
  14. 'vrf_mgmt' : {
  15. 'table' : 1042,
  16. },
  17. }
  18. #
  19. # Default parameters added to any given bonding interface,
  20. # if not specified at the interface configuration.
  21. default_bond_config = {
  22. 'bond-mode': '802.3ad',
  23. 'bond-min-links': '1',
  24. 'bond-xmit-hash-policy': 'layer3+4'
  25. }
  26. #
  27. # Default parameters added to any given bridge interface,
  28. # if not specified at the interface configuration.
  29. default_bridge_config = {
  30. 'bridge-fd' : '0',
  31. 'bridge-stp' : 'no',
  32. 'bridge-ports-condone-regex' : '^[a-zA-Z0-9]+_(v[0-9]{1,4}|eth[0-9])$',
  33. }
  34. #
  35. # Hop penalty to be set if none is explicitly specified.
  36. # Check if one of these roles is configured for any given node, use first match.
  37. default_hop_penalty_by_role = {
  38. 'bbr' : 5,
  39. 'bras' : 50,
  40. 'batman_gw' : 50,
  41. 'batman_ext': 50,
  42. }
  43. batman_role_evaluation_order = [ 'bbr', 'batman_gw', 'bras' ]
  44. # By default we do not set any penalty on an interface and rely on the
  45. # regular hop penalty to do the right thing. We only want set another
  46. # penalty, if there are additional paths and we want to influnce which
  47. # path is being taken. One example are routers which have a wifi link
  48. # to the local backbone as well as a VPN link to a gateway, or a fiber
  49. # link plus a wifi backup link.
  50. default_batman_iface_penalty_by_role = {
  51. 'default' : 0,
  52. 'WBBL' : 5,
  53. 'WBBL_backup' : 10,
  54. 'VPN_intergw' : 80,
  55. 'VPN_node' : 100,
  56. }
  57. #
  58. # Default interface attributes to be added to GRE interface to AS201701 when
  59. # not already present in pillar interface configuration.
  60. GRE_FFRL_attrs = {
  61. 'mode' : 'gre',
  62. 'method' : 'tunnel',
  63. 'mtu' : '1400',
  64. 'ttl' : '64',
  65. }
  66. # The IPv4/IPv6 prefix used for Loopback IPs
  67. loopback_prefix = {
  68. 'v4' : '10.132.255.',
  69. 'v6' : '2a03:2260:2342:ffff::',
  70. }
  71. # The DNS zone base names used for generating zone files from IP address
  72. # configured on nodes interfaces.
  73. DNS_zone_names = {
  74. 'forward' : 'ffho.net',
  75. 'rev_v4' : [
  76. '132.10.in-addr.arpa',
  77. '30.172.in-addr.arpa',
  78. ],
  79. 'rev_v6' : [
  80. '2.4.3.2.0.6.2.2.3.0.a.2.ip6.arpa',
  81. ]
  82. }
  83. # MTU configuration
  84. MTU = {
  85. # The default MTU for any interface which does not have a MTU configured
  86. # explicitly in the pillar node config or does not get a MTU configured
  87. # by any means of this SDN stuff here.
  88. 'default' : 1500,
  89. # A batman underlay device, probably a VXLAN or VLAN interface.
  90. #
  91. # 1500
  92. # + 60 B.A.T.M.A.N. adv header + network coding (activated by default by Debian)
  93. 'batman_underlay_iface' : 1560,
  94. # VXLAN underlay device, probably a VLAN within $POP or between two BBRs.
  95. #
  96. # 1560
  97. # + 14 Inner Ethernet Frame
  98. # + 8 VXLAN Header
  99. # + 8 UDP Header
  100. # + 20 IPv4 Header
  101. 'vxlan_underlay_iface' : 1610,
  102. }
  103. ################################################################################
  104. # #
  105. # Internal data types and functions #
  106. # #
  107. # Touching anything below will void any warranty you never had ;) #
  108. # #
  109. ################################################################################
  110. ################################################################################
  111. # Data types #
  112. ################################################################################
  113. class Prefix (object):
  114. """An internet address with a prefix length.
  115. The given address is expected to be of format ip/plen in CIDR notation.
  116. The IP as well as the prefix length and address family will be stored
  117. in attributes.
  118. .. code-block:: pycon
  119. >>> a = Prefix ('10.132.23.42/24')
  120. >>> str (a.ip)
  121. '10.132.23.42'
  122. >>> str (a.af)
  123. '4'
  124. >>> str (a.plen)
  125. '24'
  126. >>> str (a.netmask)
  127. '255.255.255.0'
  128. >>> str (a.network_address)
  129. '10.132.23.0'
  130. """
  131. def __init__ (self, prefix):
  132. self.prefix = prefix
  133. self.ip_network = ipaddress.ip_network (u'%s' % prefix, strict = False)
  134. def __eq__ (self, other):
  135. if isinstance (other, Prefix):
  136. return self.ip_network == other.ip_network
  137. return NotImplemented
  138. def __lt__ (self, other):
  139. if isinstance (other, Prefix):
  140. return self.ip_network < other.ip_network
  141. return NotImplemented
  142. def __str__ (self):
  143. return self.prefix
  144. @property
  145. def ip (self):
  146. return self.prefix.split ('/')[0]
  147. @property
  148. def af (self):
  149. return self.ip_network.version
  150. @property
  151. def plen (self):
  152. return self.ip_network.prefixlen
  153. @property
  154. def netmask (self):
  155. return self.ip_network.netmask
  156. @property
  157. def network_address (self):
  158. return self.ip_network.network_address
  159. ################################################################################
  160. # Functions #
  161. ################################################################################
  162. sites = None
  163. def _get_site_no (sites_config, site_name):
  164. global sites
  165. if sites == None:
  166. sites = {}
  167. for site in sites_config:
  168. if site.startswith ("_"):
  169. continue
  170. sites[site] = sites_config[site].get ("site_no", -2)
  171. return sites.get (site_name, -1)
  172. #
  173. # Generate a MAC address after the format f2:dd:dd:ss:nn:nn where
  174. # dd:dd is the hexadecimal reprensentation of the nodes device_id
  175. # ff:ff representing the gluon nodes
  176. #
  177. # ss is the hexadecimal reprensentation of the site_id the interface is connected to
  178. #
  179. # nn:nn is the decimal representation of the network the interface is connected to, with
  180. # 00:00 being the BATMAN interface
  181. # 00:0d being the dummy interface
  182. # 00:0f being the VEth internal side interface
  183. # 00:e0 being an external instance BATMAN interface
  184. # 00:ed being an external instance dummy interface
  185. # 00:e1 being an inter-gw-vpn interface
  186. # 00:e4 being an nodes fastd tunnel interface of IPv4 transport
  187. # 00:e6 being an nodes fastd tunnel interface of IPv6 transport
  188. # 00:ef being an extenral instance VEth interface side
  189. # 02:xx being a connection to local Vlan 2xx
  190. # xx:xx being a VXLAN tunnel for site ss, with xx being the underlay VLAN ID (1xyz, 2xyz)
  191. # ff:ff being the gluon next-node interface
  192. def gen_batman_iface_mac (site_no, device_no, network):
  193. net_type_map = {
  194. 'bat' : "00:00",
  195. 'dummy' : "00:0d",
  196. 'int2ext' : "00:0f",
  197. 'bat-e' : "00:e0",
  198. 'intergw' : "00:e1",
  199. 'nodes4' : "00:e4",
  200. 'nodes6' : "00:e6",
  201. 'dummy-e' : "00:ed",
  202. 'ext2int' : "00:ef",
  203. }
  204. # Well-known network type?
  205. if network in net_type_map:
  206. last = net_type_map[network]
  207. elif type (network) == int:
  208. last = re.sub (r'(\d{2})(\d{2})', '\g<1>:\g<2>', "%04d" % network)
  209. else:
  210. last = "ee:ee"
  211. # Convert device_no to hex, format number to 4 digits with leading zeros and : betwwen 2nd and 3rd digit
  212. device_no_hex = re.sub (r'([0-9a-fA-F]{2})([0-9a-fA-F]{2})', '\g<1>:\g<2>', "%04x" % int (device_no))
  213. # Format site_no to two digit number with leading zero
  214. site_no_hex = "%02d" % int (site_no)
  215. return "%s:%s:%s:%s" % (mac_prefix, device_no_hex, site_no_hex, last)
  216. # Gather B.A.T.M.A.N. related config options for real batman devices (e.g. bat0)
  217. # as well as for batman member interfaces (e.g. eth0.100, fastd ifaces etc.)
  218. def _update_batman_config (node_config, iface, sites_config):
  219. try:
  220. node_batman_hop_penalty = int (node_config['batman']['hop-penalty'])
  221. except (KeyError,ValueError):
  222. node_batman_hop_penalty = None
  223. iface_config = node_config['ifaces'][iface]
  224. iface_type = iface_config.get ('type', 'inet')
  225. batman_config = {}
  226. for item in list (iface_config.keys ()):
  227. value = iface_config.get (item)
  228. if item.startswith ('batman-'):
  229. batman_config[item] = value
  230. iface_config.pop (item)
  231. # B.A.T.M.A.N. device (e.g. bat0)
  232. if iface_type == 'batman':
  233. if 'batman-hop-penalty' not in batman_config:
  234. # If there's a hop penalty set for the node, but not for the interface
  235. # apply the nodes hop penalty
  236. if node_batman_hop_penalty:
  237. batman_config['batman-hop-penalty'] = node_batman_hop_penalty
  238. # If there's no hop penalty set for the node, use a default hop penalty
  239. # for the roles the node might have, if any
  240. else:
  241. node_roles = node_config.get ('roles', [])
  242. for role in batman_role_evaluation_order:
  243. if role in node_roles:
  244. batman_config['batman-hop-penalty'] = default_hop_penalty_by_role[role]
  245. break
  246. if 'batman_ext' in node_roles and iface.endswith('-ext'):
  247. batman_config['batman-hop-penalty'] = default_hop_penalty_by_role['batman_ext']
  248. # If batman ifaces were specified as a list - which they should -
  249. # generate a sorted list of interface names as string representation
  250. if 'batman-ifaces' in batman_config and type (batman_config['batman-ifaces']) == list:
  251. batman_iface_str = " ".join (sorted (batman_config['batman-ifaces']))
  252. batman_config['batman-ifaces'] = batman_iface_str
  253. # B.A.T.M.A.N. member interface (e.g. eth.100, fastd ifaces, etc.)
  254. elif iface_type == 'batman_iface':
  255. # Generate unique MAC address for every batman iface, as B.A.T.M.A.N.
  256. # will get puzzled with multiple interfaces having the same MAC and
  257. # do nasty things.
  258. site = iface_config.get ('site')
  259. site_no = _get_site_no (sites_config, site)
  260. device_no = node_config.get ('id')
  261. network = 1234
  262. # Generate a unique BATMAN-MAC for this interfaces
  263. match = re.search (r'^vlan(\d+)', iface)
  264. if match:
  265. network = int (match.group (1))
  266. iface_config['hwaddress'] = gen_batman_iface_mac (site_no, device_no, network)
  267. iface_config['batman'] = batman_config
  268. # Mangle bond specific config items with default values and store them in
  269. # separate sub-dict for easier access and configuration.
  270. def _update_bond_config (config):
  271. bond_config = default_bond_config.copy ()
  272. to_pop = []
  273. for item, value in config.items ():
  274. if item.startswith ('bond-'):
  275. bond_config[item] = value
  276. to_pop.append (item)
  277. for item in to_pop:
  278. config.pop (item)
  279. if bond_config['bond-mode'] not in ['2', 'balance-xor', '4', '802.3ad']:
  280. bond_config.pop ('bond-xmit-hash-policy')
  281. config['bond'] = bond_config
  282. # Mangle bridge specific config items with default values and store them in
  283. # separate sub-dict for easier access and configuration.
  284. def _update_bridge_config (config):
  285. bridge_config = default_bridge_config.copy ()
  286. for item in list (config.keys ()):
  287. value = config.get (item)
  288. if not item.startswith ('bridge-'):
  289. continue
  290. bridge_config[item] = value
  291. config.pop (item)
  292. # Fix any salt mangled string interpretation back to real string.
  293. if type (value) == bool:
  294. bridge_config[item] = "yes" if value else "no"
  295. # If bridge ports were specified as a list - which they should -
  296. # generate a sorted list of interface names as string representation
  297. if 'bridge-ports' in bridge_config and type (bridge_config['bridge-ports']) == list:
  298. bridge_ports_str = " ".join (sorted (bridge_config['bridge-ports']))
  299. if not bridge_ports_str:
  300. bridge_ports_str = "none"
  301. bridge_config['bridge-ports'] = bridge_ports_str
  302. if config.get ('vlan-mode') == 'tagged':
  303. bridge_config['bridge-vlan-aware'] = 'yes'
  304. if config.get ('tagged_vlans'):
  305. bridge_config['bridge-vids'] = " ".join (map (str, config['tagged_vlans']))
  306. config['bridge'] = bridge_config
  307. # Generate config options for vlan-aware-bridge member interfaces
  308. def _update_bridge_member_config (config):
  309. bridge_config = {}
  310. if config.get ('tagged_vlans'):
  311. bridge_config['bridge-vids'] = " ".join (map (str, config['tagged_vlans']))
  312. config['bridge'] = bridge_config
  313. # Move vlan specific config items into a sub-dict for easier access and pretty-printing
  314. # in the configuration file
  315. def _update_vlan_config (config):
  316. vlan_config = {}
  317. for item in list (config.keys ()):
  318. value = config.get (item)
  319. if item.startswith ('vlan-'):
  320. vlan_config[item] = value
  321. config.pop (item)
  322. config['vlan'] = vlan_config
  323. # Pimp Veth interfaces
  324. # * Add peer interface name IF not present
  325. # * Add link-type veth IF not present
  326. def _update_veth_config (interface, config):
  327. veth_peer_name = {
  328. 'veth_ext2int' : 'veth_int2ext',
  329. 'veth_int2ext' : 'veth_ext2int'
  330. }
  331. if interface not in veth_peer_name:
  332. return
  333. if 'link-type' not in config:
  334. config['link-type'] = 'veth'
  335. if 'veth-peer-name' not in config:
  336. config['veth-peer-name'] = veth_peer_name[interface]
  337. # The given MTU to the given interface - presented by it's interface config dict -
  338. # IFF no MTU has already been set in the node pillar.
  339. #
  340. # @param ifaces: All interface configuration (as dict)
  341. # @param iface_name: Name of the interface to set MTU for
  342. # @param mtu: The MTU value to set (integer)
  343. # When <mtu> is <= 0, the <mtu> configured for <iface_name>
  344. # will be used to set the MTU of the upper interface, and the
  345. # default MTU if none is configured explicitly.
  346. def _set_mtu_to_iface_and_upper (ifaces, iface_name, mtu):
  347. iface_config = ifaces.get (iface_name)
  348. # By default we assume that we should set the given MTU value as the 'automtu'
  349. # attribute to allow distinction between manually set and autogenerated MTU
  350. # values.
  351. set_automtu = True
  352. # If a mtu values <= 0 is given, use the MTU configured for this interface
  353. # or, if none is set, the default value when configuring the vlan-raw-device.
  354. if mtu <= 0:
  355. set_automtu = False
  356. mtu = iface_config.get ('mtu', MTU['default'])
  357. # If this interface already has a MTU set - probably because someone manually
  358. # specified one in the node pillar - we do not touch the MTU of this interface.
  359. # Nevertheless it's worth looking at any underlying interface.
  360. if 'mtu' in iface_config:
  361. set_automtu = False
  362. # There might be - read: "we have" - a situation where on top of e.g. bond0
  363. # there are vlans holding VXLAN communicaton as well as VLANs directly carrying
  364. # BATMAN traffic. Now depending on which interface is evaluated first, the upper
  365. # MTU is either correct, or maybe to small.
  366. #
  367. # If any former autogenerated MTU is greater-or-equal than the one we want to
  368. # set now, we'll ignore it, and go for the greater one.
  369. elif 'automtu' in iface_config and iface_config['automtu'] >= mtu:
  370. set_automtu = False
  371. # If we still consider this a good move, set given MTU to this device.
  372. if set_automtu:
  373. iface_config['automtu'] = mtu
  374. # If this is a VLAN - which it probably is - fix the MTU of the underlying interface, too.
  375. # Check for 'vlan-raw-device' in iface_config and in vlan subconfig (yeah, that's not ideal).
  376. vlan_raw_device = None
  377. if 'vlan-raw-device' in iface_config:
  378. vlan_raw_device = iface_config['vlan-raw-device']
  379. elif 'vlan' in iface_config and 'vlan-raw-device' in iface_config['vlan']:
  380. vlan_raw_device = iface_config['vlan']['vlan-raw-device']
  381. if vlan_raw_device:
  382. vlan_raw_device_config = ifaces.get (vlan_raw_device, None)
  383. # vlan-raw-device might point to ethX which usually isn't configured explicitly
  384. # as ifupdown2 simply will bring it up anyway by itself. To set the MTU of such
  385. # an interface we have to add a configuration stanza for it here.
  386. if vlan_raw_device_config == None:
  387. vlan_raw_device_config = {}
  388. ifaces[vlan_raw_device] = vlan_raw_device_config
  389. # If there is a manually set MTU for this device, we don't do nothin'
  390. if 'mtu' in vlan_raw_device_config:
  391. return
  392. if 'automtu' in vlan_raw_device_config and vlan_raw_device_config['automtu'] >= mtu:
  393. return
  394. vlan_raw_device_config['automtu'] = mtu
  395. # Generate configuration entries for any batman related interfaces not
  396. # configured explicitly, but asked for implicitly by role batman and a
  397. # (list of) site(s) specified in the node config.
  398. def _generate_batman_interface_config (node_config, ifaces, sites_config):
  399. # No role 'batman', nothing to do
  400. roles = node_config.get ('roles', [])
  401. if 'batman' not in roles:
  402. return
  403. # Should there be a 2nd external BATMAN instance?
  404. batman_ext = 'batman_ext' in roles or 'bras' in roles
  405. device_no = node_config.get ('id', -1)
  406. for site in node_config.get ('sites', []):
  407. site_no = _get_site_no (sites_config, site)
  408. # Predefine interface names for regular/external BATMAN instance
  409. # and possible VEth link pair for connecting both instances.
  410. bat_site_if = "bat-%s" % site
  411. dummy_site_if = "dummy-%s" % site
  412. bat_site_if_ext = "bat-%s-ext" % site
  413. dummy_site_if_ext = "dummy-%s-e" % site
  414. int2ext_site_if = "i2e-%s" % site
  415. ext2int_site_if = "e2i-%s" % site
  416. site_ifaces = {
  417. # Regular BATMAN interface, always present
  418. bat_site_if : {
  419. 'type' : 'batman',
  420. # int2ext_site_if will be added automagically if requred
  421. 'batman-ifaces' : [ dummy_site_if ],
  422. 'batman-ifaces-ignore-regex': '.*_.*',
  423. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'bat'),
  424. },
  425. # Dummy interface always present in regular BATMAN instance
  426. dummy_site_if : {
  427. 'link-type' : 'dummy',
  428. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'dummy'),
  429. 'mtu' : MTU['batman_underlay_iface'],
  430. },
  431. # Optional 2nd "external" BATMAN instance
  432. bat_site_if_ext : {
  433. 'type' : 'batman',
  434. 'batman-ifaces' : [ dummy_site_if_ext, ext2int_site_if ],
  435. 'batman-ifaces-ignore-regex': '.*_.*',
  436. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'bat-e'),
  437. 'ext_only' : True,
  438. },
  439. # Optional dummy interface always present in 2nd "external" BATMAN instance
  440. dummy_site_if_ext : {
  441. 'link-type' : 'dummy',
  442. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'dummy-e'),
  443. 'ext_only' : True,
  444. 'mtu' : MTU['batman_underlay_iface'],
  445. },
  446. # Optional VEth interface pair - internal side
  447. int2ext_site_if : {
  448. 'link-type' : 'veth',
  449. 'veth-peer-name' : ext2int_site_if,
  450. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'int2ext'),
  451. 'mtu' : MTU['batman_underlay_iface'],
  452. 'ext_only' : True,
  453. },
  454. # Optional VEth interface pair - "external" side
  455. ext2int_site_if : {
  456. 'link-type' : 'veth',
  457. 'veth-peer-name' : int2ext_site_if,
  458. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, 'ext2int'),
  459. 'mtu' : MTU['batman_underlay_iface'],
  460. 'ext_only' : True,
  461. },
  462. }
  463. for iface, iface_config_tmpl in site_ifaces.items ():
  464. # Ignore any interface only relevant when role batman_ext is set
  465. # but it isn't
  466. if not batman_ext and iface_config_tmpl.get ('ext_only', False):
  467. continue
  468. # Remove ext_only key so we don't leak it into ifaces dict
  469. if 'ext_only' in iface_config_tmpl:
  470. del iface_config_tmpl['ext_only']
  471. # If there is no trace of the desired iface config yet...
  472. if iface not in ifaces:
  473. # ... just place our template there.
  474. ifaces[iface] = iface_config_tmpl
  475. # If there should be an 2nd external BATMAN instance make sure
  476. # the internal side of the VEth iface pair is connected to the
  477. # internal BATMAN instance.
  478. if batman_ext and iface == bat_site_if:
  479. iface_config_tmpl['batman-ifaces'].append (int2ext_site_if)
  480. # If there already is an interface configuration try to enhance it with
  481. # meaningful values from our template and force correct hwaddress to be
  482. # used.
  483. else:
  484. iface_config = ifaces[iface]
  485. # Force hwaddress to be what we expect.
  486. if 'hwaddress' in iface_config_tmpl:
  487. iface_config['hwaddress'] = iface_config_tmpl['hwaddress']
  488. # Copy every attribute of the config template missing in iface config
  489. for attr in iface_config_tmpl:
  490. if attr not in iface_config:
  491. iface_config[attr] = iface_config_tmpl[attr]
  492. # Configure bat_site_if to be part of br-<site>, if present
  493. site_bridge = "br-%s" % site
  494. if site_bridge in ifaces:
  495. bridge_config = ifaces.get (site_bridge)
  496. bridge_ports = bridge_config.get ('bridge-ports', None)
  497. # There are bridge-ports configured already, but bat_site_if is not present
  498. if bridge_ports and bat_site_if not in bridge_ports:
  499. if type (bridge_ports) == list:
  500. bridge_ports.append (bat_site_if)
  501. else:
  502. bridge_config['bridge-ports'] += ' ' + bat_site_if
  503. # There are no bridge-ports configured
  504. if not bridge_ports:
  505. bridge_config['bridge-ports'] = bat_site_if
  506. # Make sure there is a bridge present for every site where a mesh_breakout
  507. # interface should be configured.
  508. for iface in list (ifaces.keys ()):
  509. config = ifaces.get (iface)
  510. iface_type = config.get ('type', 'inet')
  511. if iface_type not in ['mesh_breakout', 'batman_iface']:
  512. continue
  513. site = config.get ('site')
  514. site_bridge = "br-%s" % site
  515. batman_site_if = "bat-%s" % site
  516. if iface_type == 'mesh_breakout':
  517. # If the bridge has already been defined (with an IP maybe) make
  518. # sure that the corresbonding batman device is part of the bridge-
  519. # ports.
  520. if site_bridge in ifaces:
  521. bridge_config = ifaces.get (site_bridge)
  522. # If there already is/are (a) bridge-port(s) defined, add
  523. # the batman and the breakout interfaces if not present...
  524. bridge_ports = bridge_config.get ('bridge-ports', None)
  525. if bridge_ports:
  526. for dev in (batman_site_if, iface):
  527. if not dev in bridge_ports:
  528. if type (bridge_ports) == list:
  529. bridge_ports.append (dev)
  530. else:
  531. bridge_config['bridge-ports'] += ' ' + dev
  532. # ...if there is no bridge-port defined yet, just used
  533. # the batman and breakout iface.
  534. else:
  535. bridge_config['bridge-ports'] = [ iface, batman_site_if ]
  536. # If the bridge isn't present alltogether, add it.
  537. else:
  538. ifaces[site_bridge] = {
  539. 'bridge-ports' : [ iface, batman_site_if ],
  540. }
  541. elif iface_type == 'batman_iface':
  542. batman_ifaces = ifaces[batman_site_if]['batman-ifaces']
  543. if iface not in batman_ifaces:
  544. if type (batman_ifaces) == list:
  545. batman_ifaces.append (iface)
  546. else:
  547. batman_ifaces += ' ' + iface
  548. # Use the MTU configured for this interface or, if none is set,
  549. # the default value for batman underlay iface.
  550. mtu = config.get('mtu', MTU['batman_underlay_iface'])
  551. _set_mtu_to_iface_and_upper (ifaces, iface, mtu)
  552. #
  553. # Generate any implicitly defined VXLAN interfaces defined in the nodes iface
  554. # defined in pillar.
  555. # The keyword "batman_connect_sites" on an interface will trigger the
  556. # generation of a VXLAN overlay interfaces.
  557. def _generate_vxlan_interface_config (node_config, ifaces, sites_config):
  558. # No role 'batman', nothing to do
  559. if 'batman' not in node_config.get ('roles', []):
  560. return
  561. # Sites configured on this node. Nothing to do, if none.
  562. my_sites = node_config.get ('sites', [])
  563. if len (my_sites) == 0:
  564. return
  565. # As we're still here we can now safely assume that a B.A.T.M.A.N.
  566. # device has been configured for every site specified in sites list.
  567. device_no = node_config.get ('id', -1)
  568. for iface in list (ifaces.keys ()):
  569. iface_config = ifaces.get (iface)
  570. batman_connect_sites = iface_config.get ('batman_connect_sites', [])
  571. iface_has_prefixes = len (iface_config.get ('prefixes', {})) != 0
  572. # If we got a string, convert it to a list with a single element
  573. if type (batman_connect_sites) == str:
  574. batman_connect_sites = [ batman_connect_sites ]
  575. # If the list of sites to connect is empty, there's nothing to do here.
  576. if len (batman_connect_sites) == 0:
  577. continue
  578. # Set the MTU of this (probably) VLAN device to the MTU required for a VXLAN underlay
  579. # device, where B.A.T.M.A.N. adv. is to be expected within the VXLAN overlay.
  580. _set_mtu_to_iface_and_upper (ifaces, iface, MTU['vxlan_underlay_iface'])
  581. # If the string 'all' is part of the list, blindly use all sites configured for this node
  582. if 'all' in batman_connect_sites:
  583. batman_connect_sites = my_sites
  584. for site in batman_connect_sites:
  585. # Silenty ignore sites not configured on this node
  586. if site not in my_sites:
  587. continue
  588. # iface_name := vx_<last 5 chars of underlay iface>_<site> stripped to 15 chars
  589. vx_iface = ("vx_%s_%s" % (re.sub ('vlan', 'v', iface)[-5:], re.sub (r'[_-]', '', site)))[:15]
  590. site_no = _get_site_no (sites_config, site)
  591. bat_iface = "bat-%s" % site
  592. # If there's no batman interface for this site, there's no point
  593. # in setting up a VXLAN interfaces
  594. if bat_iface not in ifaces:
  595. continue
  596. # bail out if VXLAN tunnel already configured
  597. if vx_iface in ifaces:
  598. continue
  599. try:
  600. iface_id = int (re.sub ('vlan', '', iface))
  601. # Gather interface specific mcast address.
  602. # The address is derived from the vlan-id of the underlying interface,
  603. # assuming that it in fact is a vlan interface.
  604. # Mangle the vlan-id into two 2 digit values, eliminating any leading zeros.
  605. iface_id_4digit = "%04d" % iface_id
  606. octet2 = int (iface_id_4digit[0:2])
  607. octet3 = int (iface_id_4digit[2:4])
  608. vni = octet2 * 256 * 256 + octet3 * 256 + site_no
  609. vtep_config = {
  610. 'vxlan-id' : vni,
  611. 'vxlan-physdev' : iface,
  612. }
  613. # If there are prefixes configured on this underlay interface go for legacy IPv4 Multicast (for now)
  614. if iface_has_prefixes:
  615. vtep_config['vxlan-svcnodeip'] = "225.%s.%s.%s" % (octet2, octet3, site_no)
  616. else:
  617. vtep_config['vxlan-remote-group'] = "ff42:%s::%s" % (iface_id, site_no)
  618. except ValueError as v:
  619. vtep_config = {
  620. 'vxlan-config-error' : str (v),
  621. }
  622. iface_id = 9999
  623. mcast_ip = "225.0.0.%s" % site_no
  624. vni = site_no
  625. # Add the VXLAN interface
  626. ifaces[vx_iface] = {
  627. 'vxlan' : vtep_config,
  628. 'hwaddress' : gen_batman_iface_mac (site_no, device_no, iface_id),
  629. 'mtu' : MTU['batman_underlay_iface'],
  630. }
  631. iface_penalty = get_batman_iface_penalty (iface)
  632. if iface_penalty:
  633. ifaces[vx_iface]['batman'] = {
  634. 'batman-hop-penalty' : iface_penalty
  635. }
  636. # If the batman interface for this site doesn't have any interfaces
  637. # set up - which basicly cannot happen - add this VXLAN tunnel as
  638. # the first in the list.
  639. if not 'batman-ifaces' in ifaces[bat_iface]:
  640. ifaces[bat_iface]['batman-ifaces'] = [ vx_iface ]
  641. continue
  642. # In the hope there already are interfaces for batman set up already
  643. # add this VXLAN tunnel to the list
  644. batman_ifaces = ifaces[bat_iface]['batman-ifaces']
  645. if vx_iface not in batman_ifaces:
  646. if type (batman_ifaces) == list:
  647. batman_ifaces.append (vx_iface)
  648. else:
  649. batman_ifaces += ' ' + vx_iface
  650. #
  651. # Generate implicitly defined VRFs according to the vrf_info dict at the top
  652. # of this file
  653. def _generate_vrfs (ifaces):
  654. for iface in list (ifaces.keys ()):
  655. iface_config = ifaces.get (iface)
  656. vrf = iface_config.get ('vrf', None)
  657. if vrf and vrf not in ifaces:
  658. conf = vrf_info.get (vrf, {})
  659. table = conf.get ('table', 1234)
  660. fwmark = conf.get ('fwmark', None)
  661. ifaces[vrf] = {
  662. 'vrf-table' : table,
  663. }
  664. # Create ip rule's for any fwmarks defined
  665. if fwmark:
  666. up = []
  667. # Make sure we are dealing with a list even if there is only one mark to be set up
  668. if type (fwmark) in (str, int):
  669. fwmark = [ fwmark ]
  670. # Create ip rule entries for IPv4 and IPv6 for every fwmark
  671. for mark in fwmark:
  672. up.append ("ip rule add fwmark %s table %s" % (mark, table))
  673. up.append ("ip -6 rule add fwmark %s table %s" % (mark, table))
  674. ifaces[vrf]['up'] = up
  675. def _generate_ffrl_gre_tunnels (ifaces):
  676. for iface, iface_config in ifaces.items ():
  677. # We only care for GRE_FFRL type interfaces
  678. if iface_config.get ('type', '') != 'GRE_FFRL':
  679. continue
  680. # Copy default values to interface config
  681. for attr, val in GRE_FFRL_attrs.items ():
  682. if not attr in iface_config:
  683. iface_config[attr] = val
  684. # Guesstimate local IPv4 tunnel endpoint address from tunnel-physdev
  685. if not 'local' in iface_config and 'tunnel-physdev' in iface_config:
  686. try:
  687. physdev_prefixes = [p.split ('/')[0] for p in ifaces[iface_config['tunnel-physdev']]['prefixes'] if '.' in p]
  688. if len (physdev_prefixes) == 1:
  689. iface_config['local'] = physdev_prefixes[0]
  690. except KeyError:
  691. pass
  692. def _generate_loopback_ips (ifaces, node_config, node_id):
  693. # If this node has primary_ips set and filled there are either IPs
  694. # configured on lo or IPs on another interface - possibly ones on
  695. # the only interface present - are considered as primary IPs.
  696. if node_config.get ('primary_ips', False):
  697. return
  698. v4_ip = "%s/32" % get_primary_ip (node_config, 'v4').ip
  699. v6_ip = "%s/128" % get_primary_ip (node_config, 'v6').ip
  700. # Interface lo already present?
  701. if 'lo' not in ifaces:
  702. ifaces['lo'] = { 'prefixes' : [] }
  703. # Add 'prefixes' list if not present
  704. if 'prefixes' not in ifaces['lo']:
  705. ifaces['lo']['prefixes'] = []
  706. prefixes = ifaces['lo']['prefixes']
  707. if v4_ip not in prefixes:
  708. prefixes.append (v4_ip)
  709. if v6_ip not in prefixes:
  710. prefixes.append (v6_ip)
  711. # Generate interface descriptions / aliases for auto generated or manually
  712. # created interfaces. Currently this only is done for bridges associated
  713. # with BATMAN instanzes.
  714. #
  715. # @param node_config: The configuration of the given node (as dict)
  716. # @param sites_config Global sites configuration (as dict)
  717. def _update_interface_desc (node_config, sites_config):
  718. # Currently we only care for nodes with batman role.
  719. if 'batman' not in node_config.get ('roles', []):
  720. return
  721. for iface, iface_config in node_config.get ('ifaces', {}).items ():
  722. if 'desc' in sites_config:
  723. continue
  724. # If the interface name looks like a bridge for a BATMAN instance
  725. # try to get the name of the corresponding site
  726. match = re.search (r'^br-([a-z_-]+)$', iface)
  727. if match and match.group (1) in sites_config:
  728. try:
  729. iface_config['desc'] = sites_config[match.group (1)]['name']
  730. except KeyError:
  731. pass
  732. ################################################################################
  733. # Public functions #
  734. ################################################################################
  735. # Generate network interface configuration for given node.
  736. #
  737. # This function will read the network configuration from pillar and will
  738. # * enhance it with all default values configured at the top this file
  739. # * auto generate any implicitly configured
  740. # * VRFs
  741. # * B.A.T.M.A.N. instances and interfaces
  742. # * VXLAN interfaces to connect B.A.T.M.A.N. sites
  743. # * Loopback IPs derived from numeric node ID
  744. #
  745. # @param: node_config Pillar node configuration (as dict)
  746. # @param: sites_config Pillar sites configuration (as dict)
  747. # @param: node_id Minion name / Pillar node configuration key
  748. def get_interface_config (node_config, sites_config, node_id = ""):
  749. # Make a copy of the node_config dictionary to suppress side-effects.
  750. # This function deletes some keys from the node_config which will break
  751. # any re-run of this function or other functions relying on the node_config
  752. # to be complete.
  753. node_config = deepcopy (node_config)
  754. # Get config of this node and dict of all configured ifaces
  755. ifaces = node_config.get ('ifaces', {})
  756. # Generate configuration entries for any batman related interfaces not
  757. # configured explicitly, but asked for implicitly by role <batman> and
  758. # a (list of) site(s) specified in the node config.
  759. _generate_batman_interface_config (node_config, ifaces, sites_config)
  760. # Generate VXLAN tunnels for every interfaces specifying 'batman_connect_sites'
  761. _generate_vxlan_interface_config (node_config, ifaces, sites_config)
  762. # Enhance ifaces configuration with some meaningful defaults for
  763. # bonding, bridge and vlan interfaces, MAC address for batman ifaces, etc.
  764. for interface in list (ifaces.keys ()):
  765. config = ifaces.get (interface)
  766. iface_type = config.get ('type', 'inet')
  767. # Remove any disable interfaces here as they aren't relevant for /e/n/i
  768. if config.get ('enabled', True) == False:
  769. del ifaces[interface]
  770. continue
  771. # Ignore interfaces used for PPPoE
  772. if 'pppoe' in config.get ('tags', []):
  773. del ifaces[interface]
  774. continue
  775. if 'batman-ifaces' in config or iface_type.startswith ('batman'):
  776. _update_batman_config (node_config, interface, sites_config)
  777. if 'bond-slaves' in config:
  778. _update_bond_config (config)
  779. # FIXME: This maybe will not match on bridges without any member ports configured!
  780. if 'bridge-ports' in config or interface.startswith ('br-'):
  781. _update_bridge_config (config)
  782. if 'bridge-member' in config:
  783. _update_bridge_member_config (config)
  784. if 'vlan-raw-device' in config or 'vlan-id' in config:
  785. _update_vlan_config (config)
  786. _set_mtu_to_iface_and_upper (ifaces, interface, 0)
  787. # Pimp configuration for VEth link pairs
  788. if interface.startswith ('veth_'):
  789. _update_veth_config (interface, config)
  790. # Auto generate Loopback IPs IFF not present
  791. _generate_loopback_ips (ifaces, node_config, node_id)
  792. # Auto generated VRF devices for any VRF found in ifaces and not already configured.
  793. _generate_vrfs (ifaces)
  794. # Pimp GRE_FFRL type inteface configuration with default values
  795. _generate_ffrl_gre_tunnels (ifaces)
  796. # Drop any config parameters used in node interface configuration not
  797. # relevant anymore for config file generation.
  798. for interface, config in ifaces.items ():
  799. # Set default MTU if not already set manually or by any earlier function
  800. if interface != 'lo' and ('mtu' not in config):
  801. # Set the MTU value of this interface to the autogenerated value (if any)
  802. # or set the default, when no automtu is present.
  803. config['mtu'] = config.get ('automtu', MTU['default'])
  804. for key in [ 'automtu', 'enabled', 'batman_connect_sites', 'bridge-member', 'has_gateway', 'ospf', 'site', 'type', 'tagged_vlans', 'vlan-mode' ]:
  805. if key in config:
  806. config.pop (key)
  807. # Remove route metric on non-router nodes
  808. if 'metric' in config and not 'router' in node_config.get ('roles', []):
  809. config.pop ('metric')
  810. # This leaves 'auto', 'prefixes' and 'desc' as keys which should not be directly
  811. # printed into the remaining configuration. These are handled within the jinja
  812. # interface template.
  813. # Generate meaningful interface descriptions / aliases where useful
  814. _update_interface_desc (node_config, sites_config)
  815. return ifaces
  816. vlan_vxlan_iface_re = re.compile (r'^vlan(\d+)|^vx_v(\d+)_(\w+)')
  817. def _cmp (x, y):
  818. if x < y:
  819. return -1
  820. elif x == y:
  821. return 0
  822. else:
  823. return 1
  824. def _iface_sort (iface_a, iface_b):
  825. a = vlan_vxlan_iface_re.search (iface_a)
  826. b = vlan_vxlan_iface_re.search (iface_b)
  827. # At least one interface didn't match, do regular comparison
  828. if not a or not b:
  829. return _cmp (iface_a, iface_b)
  830. # Extract VLAN ID from VLAN interface (if given) or VXLAN
  831. vid_a = a.group (1) if a.group (1) else a.group (2)
  832. vid_b = b.group (1) if b.group (1) else b.group (2)
  833. # If it's different type of interfaces (one VLAN, one VXLAN), do regular comparison
  834. if (a.group (1) == None) != (b.group (1) == None):
  835. return _cmp (iface_a, iface_b)
  836. # Ok, t's two VLAN or two VXLAN interfaces
  837. # If it's VXLAN interfaces and the VLAN ID is the same, sort by site name
  838. if a.group (2) and vid_a == vid_b:
  839. return _cmp (a.groups (2), b.groups (2))
  840. # If it's two VLANs or two VXLANs with different VLAN IDs, sort by VLAN ID
  841. else:
  842. return _cmp (int (vid_a), int (vid_b))
  843. def get_interface_list (ifaces):
  844. iface_list = []
  845. for iface in sorted (ifaces.keys (), key = cmp_to_key (_iface_sort)):
  846. iface_list.append (iface)
  847. return iface_list
  848. # Generate entries for /etc/bat-hosts for every batman interface we will configure on any node.
  849. # For readability purposes superflous/redundant information is being stripped/supressed.
  850. # As these names will only show up in batctl calls with a specific site, site_names in interfaces
  851. # are stripped. Dummy interfaces are stripped as well.
  852. def gen_bat_hosts (nodes_config, sites_config):
  853. bat_hosts = {}
  854. for node_id in sorted (nodes_config.keys ()):
  855. node_config = nodes_config.get (node_id)
  856. node_name = node_id.split ('.')[0]
  857. if 'batman' not in node_config['roles']:
  858. continue
  859. ifaces = get_interface_config (node_config, sites_config, node_id)
  860. for iface in sorted (ifaces):
  861. iface_config = ifaces.get (iface)
  862. hwaddress = iface_config.get ('hwaddress', None)
  863. if hwaddress == None:
  864. continue
  865. entry_name = node_name
  866. match = re.search (r'^dummy-(.+)(-e)?$', iface)
  867. if match:
  868. if match.group (2):
  869. entry_name += "-e"
  870. # Append site to make name unique
  871. entry_name += "/%s" % match.group (1)
  872. else:
  873. entry_name += "/%s" % re.sub (r'^(vx_.*|i2e|e2i)[_-](.*)$', '\g<1>/\g<2>', iface)
  874. bat_hosts[hwaddress] = entry_name
  875. if 'fastd' in node_config.get ('roles', []):
  876. device_no = node_config.get ('id')
  877. for site in node_config.get ('sites', []):
  878. site_no = _get_site_no (sites_config, site)
  879. for network in ('intergw', 'nodes4', 'nodes6'):
  880. hwaddress = gen_batman_iface_mac (site_no, device_no, network)
  881. bat_hosts[hwaddress] = "%s/%s/%s" % (node_name, network, site)
  882. return bat_hosts
  883. # Return the appropriate hop penalty to configure for the given interface.
  884. def get_batman_iface_penalty (iface):
  885. if iface.startswith ('vlan'):
  886. vid = int (re.sub ('vlan', '', iface))
  887. if 2000 <= vid < 2100:
  888. return default_batman_iface_penalty_by_role.get ('WBBL')
  889. if 2200 <= vid < 2300:
  890. return default_batman_iface_penalty_by_role.get ('WBBL_backup')
  891. if 'intergw' in iface:
  892. return default_batman_iface_penalty_by_role.get ('VPN_intergw')
  893. if 'nodes' in iface:
  894. return default_batman_iface_penalty_by_role.get ('VPN_node')
  895. return default_batman_iface_penalty_by_role.get ('default', 0)
  896. # Generate eBGP session parameters for FFRL Transit from nodes pillar information.
  897. def get_ffrl_bgp_config (ifaces, proto):
  898. _generate_ffrl_gre_tunnels (ifaces)
  899. sessions = {}
  900. for iface in sorted (ifaces):
  901. # We only care for GRE tunnels to the FFRL Backbone
  902. if not iface.startswith ('gre_ffrl_'):
  903. continue
  904. iface_config = ifaces.get (iface)
  905. # Search for IPv4/IPv6 prefix as defined by proto parameter
  906. local = None
  907. neighbor = None
  908. for prefix in iface_config.get ('prefixes', []):
  909. if (proto == 'v4' and '.' in prefix) or (proto == 'v6' and ':' in prefix):
  910. local = prefix.split ('/')[0]
  911. # Calculate neighbor IP as <local IP> - 1
  912. neighbor = str (ipaddress.ip_address (u'%s' % local) - 1)
  913. break
  914. # Strip gre_ prefix iface name and use it as identifier for the eBGP session.
  915. name = re.sub ('gre_ffrl_', 'ffrl_', iface)
  916. sessions[name] = {
  917. 'local' : local,
  918. 'neighbor' : neighbor,
  919. 'bgp_local_pref' : iface_config.get ('bgp_local_pref', None),
  920. }
  921. return sessions
  922. # Get list of IP address configured on given interface on given node.
  923. #
  924. # @param: node_config Pillar node configuration (as dict)
  925. # @param: iface_name Name of the interface defined in pillar node config
  926. # OR name of VRF ("vrf_<something>") whichs ifaces are
  927. # to be examined.
  928. # @param: with_mask Don't strip the netmask from the prefix. (Default false)
  929. def get_node_iface_ips (node_config, iface_name, with_mask = False):
  930. ips = {
  931. 'v4' : [],
  932. 'v6' : [],
  933. }
  934. ifaces = node_config.get ('ifaces', {})
  935. ifaces_names = [ iface_name ]
  936. if iface_name.startswith ('vrf_'):
  937. # Reset list of ifaces_names to consider
  938. ifaces_names = []
  939. vrf = iface_name
  940. for iface, iface_config in ifaces.items ():
  941. # Ignore any iface NOT in the given VRF
  942. if iface_config.get ('vrf', None) != vrf:
  943. continue
  944. # Ignore any VEth pairs
  945. if iface.startswith ('veth'):
  946. continue
  947. ifaces_names.append (iface)
  948. try:
  949. for iface in ifaces_names:
  950. for prefix in ifaces[iface]['prefixes']:
  951. ip_ver = 'v6' if ':' in prefix else 'v4'
  952. if not with_mask:
  953. prefix = prefix.split ('/')[0]
  954. ips[ip_ver].append (prefix)
  955. except KeyError:
  956. pass
  957. return ips
  958. #
  959. # Get the primary IP(s) of the given node
  960. #
  961. # @param node_config: Pillar node configuration (as dict)
  962. # @param af: Address family
  963. def get_primary_ip (node_config, af):
  964. # Compatility glue
  965. if 'primary_ips' not in node_config:
  966. return Prefix ("%s%s" % (loopback_prefix[af], node_config['id']))
  967. return Prefix (node_config['primary_ips'][af])
  968. #
  969. # Get the router id (read: IPv4 Lo-IP) out of the given node config.
  970. def get_router_id (node_config, node_id):
  971. return get_primary_ip (node_config, 'v4').ip
  972. # Compute minions OSPF interface configuration according to FFHO routing policy
  973. # See https://wiki.ffho.net/infrastruktur:vlans for information about Vlans
  974. #
  975. # Costs are based on the following reference values:
  976. #
  977. # Iface speed | Cost
  978. # ------------+---------
  979. # 100 Gbit/s | 1
  980. # 40 Gbit/s | 2
  981. # 25 Gbit/s | 4
  982. # 20 Gbit/s | 5
  983. # 10 Gbit/s | 10
  984. # 1 Gbit/s | 100
  985. # 100 Mbit/s | 1000
  986. # VPN | 10000
  987. #
  988. def get_ospf_config (node_config, grains_id):
  989. ospf_config = {
  990. # <area> : {
  991. # <iface> : {
  992. # config ...
  993. # }
  994. # }
  995. }
  996. for iface, iface_config in node_config.get ('ifaces', {}).items ():
  997. # By default we don't speak OSPF on interfaces
  998. ospf_on = False
  999. area = 0
  1000. # Defaults for OSPF interfaces
  1001. ospf_iface_cfg = {
  1002. 'stub' : True, # Active/Passive interface
  1003. 'cost' : 12345,
  1004. # 'type' # Area type
  1005. }
  1006. # OSPF configuration for interface present?
  1007. ospf_iface_cfg_pillar = iface_config.get ('ospf', {})
  1008. # Should be completely ignore this interface?
  1009. if ospf_iface_cfg_pillar.get ('ignore', False):
  1010. continue
  1011. # Ignore interfaces without any IPs configured
  1012. if not iface_config.get ('prefixes'):
  1013. continue
  1014. # If this interface is within a (non-default) VRF, don't OSPF here
  1015. if iface_config.get ('vrf'):
  1016. continue
  1017. # Wireless Local Links (WLL)
  1018. if re.search (r'^vlan90\d$', iface):
  1019. ospf_on = True
  1020. ospf_iface_cfg['stub'] = True
  1021. ospf_iface_cfg['cost'] = 10
  1022. ospf_iface_cfg['desc'] = "Wireless Local Link (WLL)"
  1023. # Local Gigabit Ethernet based connections (PTP or L2 subnets), cost 10
  1024. elif re.search (r'^(br-?|br\d+\.|vlan)10\d\d$', iface):
  1025. ospf_on = True
  1026. ospf_iface_cfg['stub'] = False
  1027. ospf_iface_cfg['cost'] = 100
  1028. ospf_iface_cfg['desc'] = "Wired Gigabit connection"
  1029. # 10/20 Gbit/s Dark Fiber connection
  1030. elif re.search (r'^vlan12\d\d$', iface):
  1031. ospf_on = True
  1032. ospf_iface_cfg['stub'] = False
  1033. ospf_iface_cfg['cost'] = 10
  1034. ospf_iface_cfg['desc'] = "Wired 10Gb/s connection"
  1035. # VLL connection
  1036. elif re.search (r'^vlan15\d\d$', iface):
  1037. ospf_on = True
  1038. ospf_iface_cfg['stub'] = False
  1039. ospf_iface_cfg['cost'] = 200
  1040. ospf_iface_cfg['desc'] = "VLL connection"
  1041. # WBBL connection
  1042. elif re.search (r'^vlan20\d\d$', iface):
  1043. ospf_on = True
  1044. ospf_iface_cfg['stub'] = False
  1045. ospf_iface_cfg['cost'] = 1000
  1046. ospf_iface_cfg['desc'] = "WBBL connection"
  1047. # Legacy WBBL connection
  1048. elif re.search (r'^vlan22\d\d$', iface):
  1049. ospf_on = True
  1050. ospf_iface_cfg['stub'] = False
  1051. ospf_iface_cfg['cost'] = 1000
  1052. ospf_iface_cfg['desc'] = "WBBL connection"
  1053. # Management Vlans
  1054. elif re.search (r'^vlan30\d\d$', iface):
  1055. ospf_on = True
  1056. ospf_iface_cfg['stub'] = True
  1057. ospf_iface_cfg['cost'] = 10
  1058. # Management X-Connects
  1059. elif re.search (r'^vlan32\d\d$', iface):
  1060. ospf_on = True
  1061. ospf_iface_cfg['stub'] = False
  1062. ospf_iface_cfg['cost'] = 10
  1063. ospf_iface_cfg['AF'] = 4
  1064. area = 51
  1065. # OPS Vlans
  1066. elif re.search (r'^vlan39\d\d$', iface):
  1067. ospf_on = True
  1068. ospf_iface_cfg['stub'] = True
  1069. ospf_iface_cfg['cost'] = 10
  1070. # Active OSPF on OpenVPN tunnels, cost 10000
  1071. elif iface.startswith ('ovpn-'):
  1072. ospf_on = True
  1073. ospf_iface_cfg['stub'] = False
  1074. ospf_iface_cfg['cost'] = 10000
  1075. # Inter-Core links should have cost 5000
  1076. if iface.startswith ('ovpn-cr') and grains_id.startswith ('cr'):
  1077. ospf_iface_cfg['cost'] = 5000
  1078. # OpenVPN tunnels to EdgeRouters
  1079. elif iface.startswith ('ovpn-er-'):
  1080. ospf_iface_cfg['type'] = 'broadcast'
  1081. # Active OSPF on Wireguard tunnels, cost 10000
  1082. elif iface.startswith ('wg-'):
  1083. ospf_on = True
  1084. ospf_iface_cfg['stub'] = False
  1085. ospf_iface_cfg['cost'] = 10000
  1086. # Inter-Core links should have cost 5000
  1087. if iface.startswith ('wg-cr') and grains_id.startswith ('cr'):
  1088. ospf_iface_cfg['cost'] = 5000
  1089. # OSPF explicitly enabled for interface
  1090. elif 'ospf' in iface_config:
  1091. ospf_on = True
  1092. # iface ospf parameters will be applied later
  1093. # Go on if OSPF should not be actived
  1094. if not ospf_on:
  1095. continue
  1096. # Explicit OSPF interface configuration parameters take precendence over generated ones
  1097. for attr, val in ospf_iface_cfg_pillar.items ():
  1098. ospf_iface_cfg[attr] = val
  1099. # Store interface configuration
  1100. if area not in ospf_config:
  1101. ospf_config[area] = {}
  1102. ospf_config[area][iface] = ospf_iface_cfg
  1103. return ospf_config
  1104. # Return (possibly empty) subset of Traffic Engineering entries from 'te' pillar entry
  1105. # relevenant for this minion and protocol (IPv4 / IPv6)
  1106. def get_te_prefixes (te_node_config, grains_id, proto):
  1107. te_config = {}
  1108. for prefix, prefix_config in te_node_config.get ('prefixes', {}).items ():
  1109. prefix_proto = 'v6' if ':' in prefix else 'v4'
  1110. # Should this TE policy be applied on this node and is the prefix
  1111. # of the proto we are looking for?
  1112. if grains_id in prefix_config.get ('nodes', []) and prefix_proto == proto:
  1113. te_config[prefix] = prefix_config
  1114. return te_config
  1115. def generate_DNS_entries (nodes_config, sites_config):
  1116. forward_zone_name = ""
  1117. forward_zone = []
  1118. zones = {
  1119. # <forward_zone_name>: [],
  1120. # <rev_zone1_name>: [],
  1121. # <rev_zone2_name>: [],
  1122. # ...
  1123. }
  1124. # Fill zones dict with zones configured in DNS_zone_names at the top of this file.
  1125. # Make sure the zone base names provided start with a leading . so the string
  1126. # operations later can be done easily and safely. Proceed with fingers crossed.
  1127. for entry, value in DNS_zone_names.items ():
  1128. if entry == "forward":
  1129. zone = value
  1130. if not zone.startswith ('.'):
  1131. zone = ".%s" % zone
  1132. zones[zone] = forward_zone
  1133. forward_zone_name = zone
  1134. if entry in [ 'rev_v4', 'rev_v6' ]:
  1135. for zone in value:
  1136. if not zone.startswith ('.'):
  1137. zone = ".%s" % zone
  1138. zones[zone] = []
  1139. # Process all interfaace of all nodes defined in pillar and generate forward
  1140. # and reverse entries for all zones defined in DNS_zone_names. Automagically
  1141. # put reverse entries into correct zone.
  1142. for node_id in sorted (nodes_config):
  1143. node_config = nodes_config.get (node_id)
  1144. ifaces = get_interface_config (node_config, sites_config, node_id)
  1145. for iface in sorted (ifaces):
  1146. iface_config = ifaces.get (iface)
  1147. # We only care for interfaces with IPs configured
  1148. prefixes = iface_config.get ("prefixes", None)
  1149. if prefixes == None:
  1150. continue
  1151. # Ignore any interface in $VRF
  1152. if iface_config.get ('vrf', "") in [ 'vrf_external' ]:
  1153. continue
  1154. for prefix in sorted (prefixes):
  1155. ip = ipaddress.ip_address (u'%s' % prefix.split ('/')[0])
  1156. proto = 'v%s' % ip.version
  1157. # The entry name is
  1158. # <node_id> when interface 'lo'
  1159. # <node_name>.srv.<residual> when interface 'srv' (or magically detected internal srv record)
  1160. # <interface>.<node_id> else
  1161. entry_name = node_id
  1162. if iface != "lo":
  1163. entry_name = "%s.%s" % (iface, node_id)
  1164. elif iface == 'srv' or re.search (r'^(10.132.251|2a03:2260:2342:f251:)', prefix):
  1165. entry_name = re.sub (r'^([^.]+)\.(.+)$', r'\g<1>.srv.\g<2>', entry_name)
  1166. # Strip forward zone name from entry_name and store forward entry
  1167. # with correct entry type for found IP address.
  1168. forward_entry_name = re.sub (forward_zone_name, "", entry_name)
  1169. forward_entry_name = re.sub (forward_zone_name, "", entry_name)
  1170. forward_entry_typ = "A" if ip.version == 4 else "AAAA"
  1171. forward_zone.append ("%s IN %s %s" % (forward_entry_name, forward_entry_typ, ip))
  1172. # Find correct reverse zone, if configured and strip reverse zone name
  1173. # from calculated reverse pointer name. Store reverse entry if we found
  1174. # a zone for it. If no configured reverse zone did match, this reverse
  1175. # entry will be ignored.
  1176. for zone in zones:
  1177. if ip.reverse_pointer.find (zone) > 0:
  1178. PTR_entry = re.sub (zone, "", ip.reverse_pointer)
  1179. zones[zone].append ("%s IN PTR %s." % (PTR_entry, entry_name))
  1180. break
  1181. return zones
  1182. # Convert the CIDR network from the given prefix into a dotted netmask
  1183. def cidr_to_dotted_mask (prefix):
  1184. return str (ipaddress.ip_network (prefix, strict = False).netmask)
  1185. def is_subprefix (prefix, subprefix):
  1186. p = ipaddress.ip_network (prefix, strict = False)
  1187. s = ipaddress.ip_network (subprefix, strict = False)
  1188. return s.subnet_of (p)
  1189. # Return the network address of the given prefix
  1190. def get_network_address (prefix, with_prefixlen = False):
  1191. net_h = ipaddress.ip_network (u'%s' % prefix, strict = False)
  1192. network = str (net_h.network_address)
  1193. if with_prefixlen:
  1194. network += "/%s" % net_h.prefixlen
  1195. return network