basestorage.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. from __future__ import print_function, unicode_literals
  4. import logging
  5. import re
  6. import time
  7. import ffstatus
  8. from .exceptions import VpnKeyFormatError
  9. def sanitize_node(data, include_raw_data=False):
  10. """
  11. Filters potentially harmful entries from the node's data.
  12. """
  13. export = ffstatus.dict_merge({}, data)
  14. # remove fields from output: __RAW__
  15. if '__RAW__' in export and not include_raw_data:
  16. del export['__RAW__']
  17. return export
  18. class BaseStorage(object):
  19. """
  20. Provides operations on the storage data.
  21. This class gets subclassed to actually write the data
  22. to a file, database, whatever.
  23. """
  24. DATAKEY_VPN = '__VPN__'
  25. FIELDKEY_UPDATED = '__UPDATED__'
  26. def open(self):
  27. """
  28. When overridden in a subclass,
  29. opens the persistent storage.
  30. """
  31. pass
  32. def save(self):
  33. """
  34. When overriden in a subclass,
  35. stores the data to a persistent storage.
  36. """
  37. pass
  38. def close(self):
  39. """
  40. When overridden in a subclass,
  41. closes the persistent storage.
  42. """
  43. pass
  44. @property
  45. def status(self):
  46. """Gets status information on the storage."""
  47. nodes = 0
  48. nodes_active = 0
  49. gateways = 0
  50. gateways_active = 0
  51. sum_clients = 0
  52. clients = set()
  53. for node in self.get_nodes():
  54. nodetype = node.get('type', 'node')
  55. if nodetype == 'gateway':
  56. gateways += 1
  57. if self.get_nodestatus(node=node) == 'active':
  58. gateways_active += 1
  59. continue
  60. nodes += 1
  61. nodemacs = [x for x in node.get('macs', [])]
  62. if 'mac' in node:
  63. nodemacs.append(node['mac'])
  64. if self.get_nodestatus(node=node) == 'active':
  65. nodes_active += 1
  66. sum_clients += node.get('clientcount', 0)
  67. for client in node.get('clients', []):
  68. if client in nodemacs:
  69. continue
  70. clients.add(client)
  71. return {
  72. 'clients_sum': sum_clients,
  73. 'clients_unique': len(clients),
  74. 'gateways': gateways,
  75. 'gateways_active': gateways_active,
  76. 'nodes': nodes,
  77. 'nodes_active': nodes_active,
  78. 'now': int(time.time()),
  79. }
  80. def merge_new_data(self, newdata):
  81. """Updates data in the storage by merging the new data."""
  82. if newdata is None or not isinstance(newdata, dict):
  83. raise ValueError("Expected a dict as new data.")
  84. # keep a list of aliased nodes so they can be removed from the result
  85. aliased_nodes = {}
  86. # start merge on a copy of the current data
  87. current = {}
  88. for node in self.get_nodes():
  89. item_id = node['node_id']
  90. current[item_id] = ffstatus.dict_merge(node, {})
  91. current[item_id]['aliases'] = []
  92. current[item_id]['clients'] = []
  93. current[item_id]['neighbours'] = {}
  94. current[item_id]['type'] = 'node'
  95. if not item_id in newdata:
  96. continue
  97. if not '__RAW__' in current[item_id]:
  98. current[item_id]['__RAW__'] = {}
  99. if '__RAW__' in newdata[item_id]:
  100. for key in newdata[item_id]['__RAW__']:
  101. if key in current[item_id]['__RAW__']:
  102. del current[item_id]['__RAW__'][key]
  103. # merge the dictionaries
  104. updated = {}
  105. for itemid in newdata:
  106. if not itemid in current:
  107. # new element which did not exist in storage before, that's easy
  108. updated[itemid] = newdata[itemid]
  109. else:
  110. # merge the old and new element
  111. update = ffstatus.dict_merge(current[itemid], newdata[itemid])
  112. updated[itemid] = update
  113. for alias_id in updated[itemid]['aliases']:
  114. if alias_id in aliased_nodes:
  115. aliased_nodes[alias_id].append(itemid)
  116. else:
  117. aliased_nodes[alias_id] = [itemid]
  118. # merge aliased nodes
  119. for alias_id in aliased_nodes:
  120. if len(aliased_nodes[alias_id]) != 1:
  121. logging.warn("Node '%s' is aliased by multiple nodes: %s",
  122. alias_id, aliased_nodes[alias_id])
  123. continue
  124. # target's id is the single entry of the alias list
  125. item_id = aliased_nodes[alias_id][0]
  126. if alias_id == item_id:
  127. # the node has itself as alias -> remove the alias entry
  128. if alias_id in updated and 'aliases' in updated[alias_id]:
  129. updated[alias_id]['aliases'].remove(alias_id)
  130. logging.debug("Removed self-alias of '%s'.", alias_id)
  131. continue
  132. # look for alias node
  133. alias = updated.get(alias_id, current.get(alias_id))
  134. if alias is None:
  135. # no alias node present already, as we're trying to achieve here
  136. continue
  137. # look for target node
  138. item = updated.get(item_id, current.get(item_id))
  139. if item is None:
  140. logging.warn("Alias node '%s' is missing its target '%s.",
  141. alias_id, item_id)
  142. continue
  143. # merge data
  144. item_type = item.get('type', 'node')
  145. update = ffstatus.dict_merge(item, alias, overwrite_lists=False)
  146. update['node_id'] = item_id # keep original item's id
  147. if item_type != 'node' and update.get('type', 'node') == 'node':
  148. # revert overwriting of special node type
  149. update['type'] = item_type
  150. updated[item_id] = update
  151. logging.debug("Merged alias '%s' into '%s'.", alias_id, item_id)
  152. # mark alias node for deletion
  153. updated[alias_id] = None
  154. # sanitize each item's data
  155. for itemid in updated:
  156. if itemid.startswith('__'):
  157. continue
  158. item = updated[itemid]
  159. # delete node if requested
  160. if item is None:
  161. self.set_node_data(itemid, None)
  162. continue
  163. # ensure 'node_id' is set
  164. if not 'node_id' in item:
  165. item['node_id'] = itemid
  166. # remove node's MACs from clients list
  167. clients = [x for x in item.get('clients', [])]
  168. if 'mac' in item and item['mac'] in clients:
  169. clients.remove(item['mac'])
  170. for mac in item.get('macs', []):
  171. if mac in clients:
  172. clients.remove(mac)
  173. # set clientcount
  174. item['clientcount'] = len(clients)
  175. # finally, set each new data
  176. self.set_node_data(itemid, item)
  177. def get_nodes(self, sortby=None, include_raw_data=False):
  178. """Gets a list of all known nodes."""
  179. nodes = self.get_all_nodes_raw()
  180. sorted_ids = [x for x in nodes]
  181. if sortby is not None:
  182. if sortby == 'name':
  183. sortkey = lambda x: nodes[x]['hostname'].lower()
  184. sorted_ids = sorted(sorted_ids, key=sortkey)
  185. elif sortby == 'id':
  186. sorted_ids = sorted(sorted_ids)
  187. result = []
  188. for nodeid in sorted_ids:
  189. if nodeid.startswith('__'):
  190. continue
  191. node = sanitize_node(nodes[nodeid], include_raw_data)
  192. result.append(node)
  193. return result
  194. def find_node(self, rawid, include_raw_data=False, search_aliases=True):
  195. """
  196. Fetch node data by given id.
  197. If necessary, look through node aliases.
  198. """
  199. # try direct match, first
  200. node = self.get_node(rawid)
  201. if node is not None:
  202. return sanitize_node(node, include_raw_data=include_raw_data)
  203. # look through all nodes
  204. found = None
  205. nodes = self.get_all_nodes_raw()
  206. for nodeid in nodes:
  207. node = nodes[nodeid]
  208. # if we have a direct hit, return it immediately
  209. if nodeid == rawid:
  210. return sanitize_node(node, include_raw_data=include_raw_data)
  211. # search through aliases
  212. if search_aliases and rawid in node.get('aliases', []):
  213. found = node
  214. # return found node
  215. if not found is None:
  216. return sanitize_node(found, include_raw_data=include_raw_data)
  217. else:
  218. return None
  219. def find_node_by_mac(self, mac):
  220. """Fetch node data by given MAC address."""
  221. needle = mac.lower()
  222. # iterate over all nodes
  223. for node in self.get_nodes():
  224. # check node's primary MAC
  225. if 'mac' in node and needle == node['mac'].lower():
  226. return sanitize_node(node)
  227. # check alias MACs
  228. if 'macs' in node:
  229. haystack = [x.lower() for x in node['macs']]
  230. if mac in haystack:
  231. return sanitize_node(node)
  232. # MAC address not found
  233. return None
  234. def get_nodestatus(self, rawid=None, node=None):
  235. """Determine node's status."""
  236. # search node by the given id
  237. if node is None and not rawid is None:
  238. node = self.find_node(rawid)
  239. # handle unknown nodes
  240. if node is None:
  241. return None
  242. # check that the last batadv update is noted in the data
  243. updated = node.get(self.FIELDKEY_UPDATED, None)
  244. if updated is None:
  245. return 'unknown'
  246. u = updated.get('batadv', updated.get('batctl'))
  247. if u is None:
  248. return 'unknown'
  249. # make decision based on time of last batadv update
  250. diff = time.time() - u
  251. if diff < 150:
  252. return 'active'
  253. elif diff < 300:
  254. return 'stale'
  255. else:
  256. return 'offline'
  257. def set_node_data(self, key, data):
  258. """
  259. Overwrite data for the node with the given key.
  260. Specifying 'None' as data effectively means deleting the key.
  261. """
  262. raise NotImplementedError("set_node_data was not overridden")
  263. def check_vpn_key(self, key):
  264. if key is None or re.match(r'^[a-fA-F0-9]+$', key) is None:
  265. raise VpnKeyFormatError(key)
  266. def get_vpn_keys(self):
  267. """Gets a list of VPN keys."""
  268. raise NotImplementedError("get_vpn_keys was not overriden")
  269. def get_vpn_item(self, key, create=False):
  270. self.check_vpn_key(key)
  271. raise NotImplementedError("store_vpn_item was not overriden")
  272. def store_vpn_item(self, key, data):
  273. raise NotImplementedError("store_vpn_item was not overriden")
  274. def resolve_vpn_remotes(self):
  275. """Iterates all remotes and resolves IP blocks not yet resolved."""
  276. vpn = self.get_vpn_keys()
  277. init_vpn_cache = {}
  278. for key in vpn:
  279. entry = self.get_vpn_item(key)
  280. entry_modified = False
  281. for mode in entry:
  282. if not isinstance(entry[mode], dict):
  283. continue
  284. for gateway in entry[mode]:
  285. if not isinstance(entry[mode][gateway], dict):
  286. continue
  287. item = entry[mode][gateway]
  288. if 'remote' in item and not 'remote_raw' in item:
  289. item['remote_raw'] = item['remote']
  290. resolved = None
  291. if item['remote'] in init_vpn_cache:
  292. resolved = init_vpn_cache[item['remote']]
  293. else:
  294. resolved = ffstatus.resolve_ipblock(item['remote'])
  295. init_vpn_cache[item['remote']] = resolved
  296. if resolved is not None:
  297. logging.info(
  298. 'Resolved VPN entry \'%s\' to net \'%s\'.',
  299. item['remote'],
  300. resolved['name'],
  301. )
  302. if resolved is not None:
  303. item['remote'] = resolved
  304. entry_modified = True
  305. if entry_modified:
  306. self.store_vpn_item(key, entry)
  307. def get_vpn_gateways(self):
  308. gateways = set()
  309. vpn = self.get_vpn_keys()
  310. for key in vpn:
  311. entry = self.get_vpn_item(key)
  312. for conntype in entry:
  313. for gateway in entry[conntype]:
  314. gateways.add(gateway)
  315. return sorted(gateways)
  316. def get_vpn_connections(self):
  317. conntypes = ['active', 'last']
  318. result = []
  319. vpnkeys = self.get_vpn_keys()
  320. for key in vpnkeys:
  321. vpn_entry = self.get_vpn_item(key)
  322. if not isinstance(vpn_entry, dict):
  323. continue
  324. item = {
  325. 'key': key,
  326. 'count': {},
  327. 'remote': {},
  328. }
  329. names = set()
  330. for conntype in conntypes:
  331. item['count'][conntype] = 0
  332. item['remote'][conntype] = {}
  333. if conntype in vpn_entry:
  334. for gateway in vpn_entry[conntype]:
  335. if 'remote' in vpn_entry[conntype][gateway]:
  336. remote = vpn_entry[conntype][gateway]['remote']
  337. if remote is None or remote == '':
  338. continue
  339. item['count'][conntype] += 1
  340. item['remote'][conntype][gateway] = remote
  341. if 'peer' in vpn_entry[conntype][gateway]:
  342. names.add(vpn_entry[conntype][gateway]['peer'])
  343. item['names'] = sorted(names)
  344. item['online'] = item['count']['active'] > 0
  345. result.append(item)
  346. return result
  347. def log_vpn_connect(self, key, peername, remote, gateway, timestamp):
  348. item = self.get_vpn_item(key, create=True)
  349. # resolve remote addr to its netblock
  350. remote_raw = remote
  351. remote_resolved = None
  352. if remote is not None:
  353. remote_resolved = ffstatus.resolve_ipblock(remote)
  354. if remote_resolved is not None:
  355. logging.debug('Resolved IP \'{0}\' to block \'{1}\'.'.format(
  356. remote, remote_resolved['name'],
  357. ))
  358. remote = remote_resolved
  359. # store connection info
  360. item['active'][gateway] = {
  361. 'establish': timestamp,
  362. 'peer': peername,
  363. 'remote': remote,
  364. 'remote_raw': remote_raw,
  365. }
  366. self.store_vpn_item(key, item)
  367. def log_vpn_disconnect(self, key, gateway, timestamp):
  368. item = self.get_vpn_item(key, create=True)
  369. active = {}
  370. if gateway in item['active']:
  371. active = item['active'][gateway]
  372. del item['active'][gateway]
  373. active['disestablish'] = timestamp
  374. item['last'][gateway] = active
  375. self.store_vpn_item(key, item)