node_tools/cache_funcs.py
# coding: utf-8
"""cache-specific helper functions."""
import logging
import ipaddress
from collections import namedtuple
from node_tools.helper_funcs import AttrDict
from node_tools.helper_funcs import NODE_SETTINGS
from node_tools.helper_funcs import find_ipv4_iface
logger = logging.getLogger(__name__)
def create_cache_entry(cache, data, key_str):
"""
Load new cache entry by key type.
:param cache: Index <cache> object
:param data: payload data in a dictionary
:param key_str: desired 'key_str', one of
['node'|'peer'|'net'|'mbr'|'moon'] or
['nstate'|'mstate'|'istate']
"""
new_data = AttrDict.from_nested_dict(data)
logger.debug('Pushing entry for: {}'.format(key_str))
with cache.transact():
key = cache.push(new_data, prefix=key_str)
logger.debug('New key created for: {}'.format(key))
def delete_cache_entry(cache, key_str):
"""
Delete cache entry by key type.
:param cache: Index <cache> object
:param key_str: desired 'key_str', one of
['node'|'peer'|'net'|'mbr'|'moon'] or
['nstate'|'mstate'|'istate']
"""
key_list = find_keys(cache, key_str)
if key_list:
for key in key_list:
logger.debug('Deleting entry for: {}'.format(key))
with cache.transact():
del cache[key]
logger.debug('Deleted cache items matching: {}'.format(key_str))
else:
logger.warning('No matching keys found for: {}'.format(key_str))
def find_keys(cache, key_str):
"""Find API key(s) in cache using key type string, return list of keys."""
valid_strings = ['node', 'peer', 'moon', 'net', 'mbr', 'nstate', 'mstate', 'istate']
match_list = [key for key in valid_strings if key_str in key]
if not match_list:
logger.debug('Key type {} not valid'.format(key_str))
return None
key_list = [key for key in list(cache) if key_str in key]
if not key_list:
logger.debug('Key type {} not in cache'.format(key_str))
return None
else:
return key_list
def get_endpoint_data(cache, key_str):
"""
Get all data for key type from cache (can be endpoint or state).
:param cache: Index <cache> object
:param key_str: desired 'key_str', one of
['node'|'peer'|'net'|'mbr'|'moon'] or
['nstate'|'mstate'|'istate']
:return tuple: (list of [keys], list of [values])
"""
logger.debug('Entering get_endpoint_data with key_str: {}'.format(key_str))
values = []
key_list = find_keys(cache, key_str)
if key_list:
for key in key_list:
with cache.transact():
data = cache[key]
values.append(data)
logger.debug('Appending data for key: {}'.format(key))
else:
key_list = []
logger.debug('Leaving get_endpoint_data with key_str: {}'.format(key_str))
return (key_list, values)
def get_net_status(cache):
"""
Get user node status data for 'network' endpoint from cache, return
list of dictionaries.
NOTE: Not valid for networks under the 'controller' endpoint.
"""
networks = [] # list of network objects
key_list, values = get_endpoint_data(cache, 'net')
if key_list:
for key, data in zip(key_list, values):
# we need to check for missing route list here
if data.routes:
for addr in data.assignedAddresses:
if find_ipv4_iface(addr, False):
zt_addr = find_ipv4_iface(addr)
break
netStatus = {'identity': data.id,
'status': data.status,
'mac': data.mac,
'ztdevice': data.portDeviceName,
'ztaddress': zt_addr,
'gateway': data.routes[1]['via']}
else:
netStatus = {'identity': data.id,
'status': data.status,
'mac': data.mac,
'ztdevice': data.portDeviceName}
networks.append(netStatus)
logger.debug('netStatus list: {}'.format(networks))
return networks
def get_node_status(cache):
"""
Get data for 'status' endpoint from cache, return a dict.
"""
nodeStatus = {}
key_list, values = get_endpoint_data(cache, 'node')
if values:
d = values[0]
status = 'ONLINE' if d.online else 'OFFLINE'
nodeStatus = {'identity': d.address,
'status': status,
'tcpFallback': d.tcpFallbackActive,
'worldId': d.planetWorldId}
logger.debug('nodeStatus dict: {}'.format(nodeStatus))
return nodeStatus
def get_peer_status(cache):
"""
Get status data for 'peer' endpoint from cache, return a
list of dictionaries.
"""
peers = [] # list of peer objects
key_list, values = get_endpoint_data(cache, 'peer')
if key_list:
for key, data in zip(key_list, values):
# fix for bad LEAF nodes with empty paths (see fpnd issue #27)
if data.paths != []:
for path in data.paths:
addr = path['address'].split('/', maxsplit=1)
try:
addr_obj = ipaddress.ip_address(addr[0])
except ValueError as exc:
logger.error('ipaddress exception: {}'.format(exc))
# filter out IPv6 addresses for now
if addr_obj.version == 4:
peerStatus = {'identity': data.address,
'role': data.role,
'active': path['active'],
'address': addr[0],
'port': addr[1]}
peers.append(peerStatus)
logger.debug('peerStatus list: {}'.format(peers))
return peers
def get_state(cache):
"""
Get state data from cache to build node state and update it.
"""
from node_tools import state_data as st
key_list, values = get_endpoint_data(cache, 'state')
if key_list:
d = {}
for key, data in zip(key_list, values):
if 'nstate' in str(key):
if 'ONLINE' in data.status:
d['online'] = True
d['fpn_id'] = data.identity
d['fallback'] = data.tcpFallback
if 'mstate' in str(key) and not NODE_SETTINGS['node_role']:
d['moon_id0'] = data.identity
d['moon_addr'] = data.address
if NODE_SETTINGS['use_localhost']:
d['moon_addr'] = '127.0.0.1'
if 'istate' in str(key) and 'OK' in data.status:
if data.ztaddress == data.gateway:
d['fpn1'] = True
d['fpn_id1'] = data.identity
st.fpn1Data['nwid'] = data.identity
st.fpn1Data['iface'] = data.ztdevice
st.fpn1Data['address'] = data.ztaddress
else:
d['fpn0'] = True
d['fpn_id0'] = data.identity
st.fpn0Data['nwid'] = data.identity
st.fpn0Data['iface'] = data.ztdevice
st.fpn0Data['address'] = data.ztaddress
else:
d.update(fpn0=None, fpn1=None, fpn_id0=None, fpn_id1=None)
st.fpnState.update(d)
logger.debug('fpnState: {}'.format(st.fpnState))
logger.debug('fpn0Data: {}'.format(st.fpn0Data))
logger.debug('fpn1Data: {}'.format(st.fpn1Data))
def handle_node_status(data, cache):
"""
Cache handling/state update for node status data (common to all roles).
:param data: ZT client data from 'status' endpoint
:param cache: Index <cache> object
"""
node_id = data.get('address')
logger.debug('Found node: {}'.format(node_id))
load_cache_by_type(cache, data, 'node')
logger.debug('Returned {} key is: {}'.format('node', find_keys(cache, 'node')))
nodeStatus = get_node_status(cache)
logger.debug('Got node state: {}'.format(nodeStatus))
load_cache_by_type(cache, nodeStatus, 'nstate')
return node_id
def load_cache_by_type(cache, data, key_str):
"""Load or update cache by key type string (uses find_keys)."""
from itertools import zip_longest
key_list = find_keys(cache, key_str)
if not key_list:
if key_str in ('node', 'nstate'):
create_cache_entry(cache, data, key_str)
else:
for item in data:
create_cache_entry(cache, item, key_str)
else:
if key_str in ('node', 'nstate'):
update_cache_entry(cache, data, key_list[0])
else:
for key, item in zip_longest(key_list, data):
if not key:
create_cache_entry(cache, item, key_str)
elif not item:
logger.debug('Removing cache entry for key: {}'.format(key))
with cache.transact():
del cache[key]
else:
update_cache_entry(cache, item, key)
key_list = find_keys(cache, key_str)
def update_cache_entry(cache, data, key):
"""
Update single cache entry by key.
:param cache: <cache> object
:param data: payload data in a dictionary
:param key_str: desired 'key_str', one of
['node'|'peer'|'net'|'mbr'|'moon'] or
['nstate'|'mstate'|'istate']
"""
new_data = AttrDict.from_nested_dict(data)
if 'state' in key.rstrip('-0123456789'):
tgt = 'identity'
elif key.rstrip('-0123456789') in ('net', 'moon'):
tgt = 'id'
else:
tgt = 'address'
data_id = new_data[tgt]
logger.debug('New data has id: {}'.format(data_id))
logger.debug('Updating cache entry for key: {}'.format(key))
with cache.transact():
cache[key] = new_data