infra-role-tinc/files/tinc-refresh

249 lines
7.1 KiB
Python

#!/usr/bin/env python2
import re
import sys
import time
import json
import consul
import socket
import logging
import urllib3
import requests
import argparse
import ipaddress
from subprocess import call
from python_hosts import Hosts, HostsEntry
# If we can't trust localhost there is no God.
urllib3.disable_warnings()
"""
This script queries Consul for Tinc service metadata
And constructs host config files for Tinc.
"""
tinc_config_template = """
Name = {hostname}
AddressFamily = ipv4
Interface = tun0
# Ping once every 3 minutes
PingInterval = 180
# Prioritize other processes
ProcessPriority = low
# Do not forward packets to other hosts
# WARNING: Without these 1.0.34 has major CPU issues
Forwarding = off
DirectOnly = yes
TunnelServer = yes
{connect_to}
""".strip()
# this file is generated for every service in the catalog
host_config_template = """
Subnet = {vpn_address}/32
Address = {pub_address}
{pub_key}
""".strip()
# Setup logging
log_format = '%(asctime)s [%(levelname)s] %(message)s'
logging.basicConfig(level=logging.INFO, format=log_format)
LOG = logging.getLogger(__name__)
tinc_network_name = 'status.im'
tinc_network_path = '/etc/tinc/' + tinc_network_name
tinc_first_network = u'10.0.0.0'
hostname = socket.gethostname()
ca_path = '/certs/consul-ca.crt'
key_path = '/certs/consul-client.key'
cert_path = '/certs/consul-client.crt'
cert = (cert_path, key_path)
def to_filename(text):
return re.sub(r'[\-\.]', '_', text)
def get_local_pub_key():
with open('{}/rsa_key.pub'.format(tinc_network_path), 'r') as f:
return f.read()
def get_tinc_network(c):
# check the network address for this Data Center
_, rval = c.kv.get('tinc/network')
if rval is not None:
return unicode(rval['Value'], "utf-8")
# otherwise we have to check all other DCs
dcs = c.catalog.datacenters()
networks = []
for dc in dcs:
_, rval = c.kv.get('tinc/network', dc=dc)
if rval is None:
continue
ip_addr = unicode(rval['Value'], "utf-8")
networks.append(ipaddress.ip_network(ip_addr))
if len(networks) == 0:
# if no network has an address yet to do default
highest_net = ipaddress.ip_network(tinc_first_network)
else:
# we pick the highest Tinc network address
highest_net = sorted(networks)[-1]
# bump 2nd octet of network address to get one for this DC
new_net_addr = highest_net.network_address + 2**16
# create new nework
new_network = ipaddress.ip_network(
new_net_addr.compressed + '/' + unicode(highest_net.prefixlen)
)
# update the k/v store with Tinc network address for this DC
c.kv.put('tinc/network', new_network.compressed)
return new_network
def get_new_tinc_ip(c):
try:
# we lock to avoid getting the same IP as another host
ip_lock = c.session.create(name='tinc-ip-lock', ttl=10)
locked = c.kv.put('tinc/ip-lock', 'locked', acquire=ip_lock)
if not locked:
return False
# check current highest IP for Tinc in this DC
_, rval = c.kv.get('tinc/highest-ip')
if rval is not None:
highest_ip = unicode(rval['Value'], "utf-8")
else:
# if there is no highest IP we need to derive it from the network
network = get_tinc_network(c)
highest_ip = unicode(network.network_address)
# increment, this is important especially when IP is a network addr
incremented_ip = str(ipaddress.ip_address(highest_ip) + 1)
# update the k/v store with the current highest IP
c.kv.put('tinc/highest-ip', incremented_ip)
finally:
if ip_lock:
# remember to disable the lock
c.kv.put('tinc/ip-lock', 'unlocked', release=ip_lock)
c.session.destroy(ip_lock)
return incremented_ip
def retry_get_new_tinc_ip(c, retries_left=3):
while retries_left > 0:
rval = get_new_tinc_ip(c)
if rval is not False:
return rval
time.sleep(5)
raise 'Failed to get a new IP for this Tinc peer.'
def add_host_config(service):
host_config = host_config_template.format(
pub_address=service['Address'],
vpn_address=service['ServiceMeta']['tinc_address'],
pub_key=service['ServiceMeta']['tinc_pub_key']
)
host_config_path = '{}/hosts/{}'.format(
tinc_network_path,
to_filename(service['Node'])
)
LOG.debug('Writing: {} - {}'.format(
host_config_path,
service['ServiceMeta']['tinc_address']
))
with open(host_config_path, 'w') as f:
f.write(host_config)
def parse_args():
parser = argparse.ArgumentParser(
description='Automation for generating Tinc config.')
parser.add_argument('-d', '--debug', action='store_true',
help='Enable debug logging mode.')
return parser.parse_args()
#------------------------------------------------------------------------------
args = parse_args()
if args.debug:
LOG.setLevel(logging.DEBUG)
LOG.debug('Collecting Consul catalog info')
c = consul.Consul(port=8500)
dcs = c.catalog.datacenters()
# get existing tinc peers from all Data Centers
services = []
for dc in dcs:
services += c.catalog.service('tinc', dc=dc)[1]
# and our own public address
_, node = c.catalog.node(hostname)
if node is None:
LOG.error('Cannot find local host in catalog!')
sys.exit(1)
# turn list into dict by node name
services_dict = {s['Node']: s for s in services}
# sort by dc, env, stage, and finally name
sorted_services = sorted(
services, key=lambda s: (
s['Datacenter'],
s['NodeMeta']['env'],
s['NodeMeta']['stage'],
s['Node']
)
)
# if current host is not in the catalog add it
if hostname not in services_dict:
services_dict[hostname] = {
'Node': hostname,
'Address': node['Node']['Address'],
'ServiceMeta': {
'tinc_address': retry_get_new_tinc_ip(c),
'tinc_pub_key': get_local_pub_key(),
}
}
vpn_ip = services_dict[hostname]['ServiceMeta']['tinc_address']
LOG.debug('Saving VNP IP: %s', vpn_ip)
# save local host tinc IP for easy access by Ansible
with open('{}/tinc-ip'.format(tinc_network_path), 'w') as f:
f.write(vpn_ip)
LOG.debug('Updating main config file')
with open('{}/tinc.conf'.format(tinc_network_path), 'w') as f:
f.write(tinc_config_template.format(
hostname=to_filename(hostname),
tinc_network_path=tinc_network_path,
connect_to='\n'.join([
'ConnectTo = '+to_filename(s['Node'])
for s in sorted_services
# Don't connect to yourself
if s['Node'] != hostname
])
))
LOG.debug('Generating host config files')
for service in sorted_services:
add_host_config(service)
LOG.debug('Reloading tinc config')
call('systemctl reload tinc@{}'.format(tinc_network_name).split())
LOG.debug('Updating /etc/hosts')
# update hosts file
h = Hosts()
h.add([HostsEntry(
entry_type='ipv4',
address=service['ServiceMeta']['tinc_address'],
names=[service['Node']+'.tinc']
) for service in sorted_services], force=True)
h.write()
LOG.info('Completed Tinc config update')