pvc/node-daemon/pvcd/VXNetworkInstance.py
Joshua Boniface f198f62563 Massive rejigger into single daemon
Completely restructure the daemon code to move the 4 discrete daemons
into a single daemon that can be run on every hypervisor. Introduce the
idea of a static list of "coordinator" nodes which are configured at
install time to run Zookeeper and FRR in router mode, and which are
allowed to take on client network management duties (gateway, DHCP, DNS,
etc.) while also allowing them to run VMs (i.e. no dedicated "router"
nodes required).
2018-10-14 02:40:54 -04:00

428 lines
16 KiB
Python

#!/usr/bin/env python3
# VXNetworkInstance.py - Class implementing a PVC VM network and run by pvcd
# Part of the Parallel Virtual Cluster (PVC) system
#
# Copyright (C) 2018 Joshua M. Boniface <joshua@boniface.me>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
###############################################################################
import os
import sys
from textwrap import dedent
import pvcd.log as log
import pvcd.zkhandler as zkhandler
import pvcd.common as common
class VXNetworkInstance():
# Initialization function
def __init__ (self, vni, zk_conn, config, logger, this_node):
self.vni = vni
self.zk_conn = zk_conn
self.config = config
self.logger = logger
self.this_node = this_node
self.vni_dev = config['vni_dev']
self.old_description = None
self.description = None
self.domain = None
self.ip_gateway = zkhandler.readdata(self.zk_conn, '/networks/{}/ip_gateway'.format(self.vni))
self.ip_network = None
self.ip_cidrnetmask = None
self.dhcp_flag = zkhandler.readdata(self.zk_conn, '/networks/{}/dhcp_flag'.format(self.vni))
self.dhcp_start = None
self.dhcp_end = None
self.vxlan_nic = 'vxlan{}'.format(self.vni)
self.bridge_nic = 'br{}'.format(self.vni)
self.nftables_update_filename = '{}/update'.format(config['nft_dynamic_directory'])
self.nftables_netconf_filename = '{}/networks/{}.nft'.format(config['nft_dynamic_directory'], self.vni)
self.firewall_rules = []
self.dhcp_server_daemon = None
self.dnsmasq_hostsdir = '{}/{}'.format(config['dnsmasq_dynamic_directory'], self.vni)
self.dhcp_reservations = []
# Zookeper handlers for changed states
@self.zk_conn.DataWatch('/networks/{}'.format(self.vni))
def watch_network_description(data, stat, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if data and self.description != data.decode('ascii'):
self.old_description = self.description
self.description = data.decode('ascii')
@self.zk_conn.DataWatch('/networks/{}/domain'.format(self.vni))
def watch_network_domain(data, stat, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if data and self.domain != data.decode('ascii'):
domain = data.decode('ascii')
self.domain = domain
@self.zk_conn.DataWatch('/networks/{}/ip_network'.format(self.vni))
def watch_network_ip_network(data, stat, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if data and self.ip_network != data.decode('ascii'):
ip_network = data.decode('ascii')
self.ip_network = ip_network
self.ip_cidrnetmask = ip_network.split('/')[-1]
@self.zk_conn.DataWatch('/networks/{}/ip_gateway'.format(self.vni))
def watch_network_gateway(data, stat, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if data and self.ip_gateway != data.decode('ascii'):
orig_gateway = self.ip_gateway
self.ip_gateway = data.decode('ascii')
if self.this_node.router_state == 'primary':
if orig_gateway:
self.removeGatewayAddress()
self.createGatewayAddress()
@self.zk_conn.DataWatch('/networks/{}/dhcp_flag'.format(self.vni))
def watch_network_dhcp_status(data, stat, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if data and self.dhcp_flag != data.decode('ascii'):
self.dhcp_flag = ( data.decode('ascii') == 'True' )
if self.dhcp_flag and self.this_node.router_state == 'primary':
self.startDHCPServer()
elif self.this_node.router_state == 'primary':
self.stopDHCPServer()
@self.zk_conn.DataWatch('/networks/{}/dhcp_start'.format(self.vni))
def watch_network_dhcp_start(data, stat, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if data and self.dhcp_start != data.decode('ascii'):
self.dhcp_start = data.decode('ascii')
@self.zk_conn.DataWatch('/networks/{}/dhcp_end'.format(self.vni))
def watch_network_dhcp_end(data, stat, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if data and self.dhcp_end != data.decode('ascii'):
self.dhcp_end = data.decode('ascii')
@self.zk_conn.ChildrenWatch('/networks/{}/dhcp_reservations'.format(self.vni))
def watch_network_dhcp_reservations(new_reservations, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if self.dhcp_reservations != new_reservations:
old_reservations = self.dhcp_reservations
self.dhcp_reservations = new_reservations
self.updateDHCPReservations(old_reservations, new_reservations)
@self.zk_conn.ChildrenWatch('/networks/{}/firewall_rules'.format(self.vni))
def watch_network_firewall_rules(new_rules, event=''):
if event and event.type == 'DELETED':
# The key has been deleted after existing before; terminate this watcher
# because this class instance is about to be reaped in Daemon.py
return False
if self.firewall_rules != new_rules:
old_rules = self.firewall_rules
self.firewall_rules = new_rules
self.updateFirewallRules(old_rules, new_rules)
self.createNetwork()
self.createFirewall()
def getvni(self):
return self.vni
def updateDHCPReservations(self, old_reservations_list, new_reservations_list):
for reservation in new_reservations_list:
if reservation not in old_reservations_list:
# Add new reservation file
filename = '{}/{}'.format(self.dnsmasq_hostsdir, reservation)
ipaddr = zkhandler.readdata(
self.zk_conn,
'/networks/{}/dhcp_reservations/{}/ipaddr'.format(
self.vni,
reservation
)
)
entry = '{},{}'.format(reservation, ipaddr)
outfile = open(filename, 'w')
outfile.write(entry)
outfile.close()
for reservation in old_reservations_list:
if reservation not in new_reservations_list:
# Remove old reservation file
filename = '{}/{}'.format(self.dnsmasq_hostsdir, reservation)
try:
os.remove(filename)
self.dhcp_server_daemon.signal('hup')
except:
pass
def updateFirewallRules(self, old_rules_list, new_rules_list):
for rule in new_rules_list:
if rule not in old_rules_list:
# Add new rule entry
pass
for rule in old_rules_list:
if rule not in new_rules_list:
pass
def createNetwork(self):
self.logger.out(
'Creating VXLAN device on interface {}'.format(
self.vni_dev
),
prefix='VNI {}'.format(self.vni),
state='o'
)
common.run_os_command(
'ip link add {} type vxlan id {} dstport 4789 dev {}'.format(
self.vxlan_nic,
self.vni,
self.vni_dev
)
)
common.run_os_command(
'brctl addbr {}'.format(
self.bridge_nic
)
)
common.run_os_command(
'brctl addif {} {}'.format(
self.bridge_nic,
self.vxlan_nic
)
)
common.run_os_command(
'ip link set {} up'.format(
self.vxlan_nic
)
)
common.run_os_command(
'ip link set {} up'.format(
self.bridge_nic
)
)
def createFirewall(self):
nftables_network_rules = """# Rules for network {vxlannic}
add chain inet filter {vxlannic}-in
add chain inet filter {vxlannic}-out
add rule inet filter {vxlannic}-in counter
add rule inet filter {vxlannic}-out counter
# Jump from forward chain to this chain when matching net
add rule inet filter forward ip daddr {netaddr} counter jump {vxlannic}-in
add rule inet filter forward ip saddr {netaddr} counter jump {vxlannic}-out
# Allow ICMP traffic into the router from network
add rule inet filter input ip protocol icmp meta iifname {bridgenic} counter accept
# Allow DNS and DHCP traffic into the router from network
add rule inet filter input tcp dport 53 meta iifname {bridgenic} counter accept
add rule inet filter input udp dport 53 meta iifname {bridgenic} counter accept
add rule inet filter input udp dport 67 meta iifname {bridgenic} counter accept
# Block traffic into the router from network
add rule inet filter input meta iifname {bridgenic} counter drop
""".format(
netaddr=self.ip_network,
vxlannic=self.vxlan_nic,
bridgenic=self.bridge_nic
)
print(nftables_network_rules)
with open(self.nftables_netconf_filename, 'w') as nfbasefile:
nfbasefile.write(dedent(nftables_network_rules))
open(self.nftables_update_filename, 'a').close()
pass
def createGatewayAddress(self):
if self.this_node.router_state == 'primary':
self.logger.out(
'Creating gateway {} on interface {}'.format(
self.ip_gateway,
self.bridge_nic
),
prefix='VNI {}'.format(self.vni),
state='o'
)
print('ip address add {}/{} dev {}'.format(
self.ip_gateway,
self.ip_cidrnetmask,
self.bridge_nic
))
common.run_os_command(
'ip address add {}/{} dev {}'.format(
self.ip_gateway,
self.ip_cidrnetmask,
self.bridge_nic
)
)
common.run_os_command(
'arping -A -c2 -I {} {}'.format(
self.bridge_nic,
self.ip_gateway
),
background=True
)
def startDHCPServer(self):
if self.this_node.router_state == 'primary':
self.logger.out(
'Starting dnsmasq DHCP server on interface {}'.format(
self.bridge_nic
),
prefix='VNI {}'.format(self.vni),
state='o'
)
# Create the network hostsdir
common.run_os_command(
'/bin/mkdir --parents {}'.format(
self.dnsmasq_hostsdir
)
)
# Recreate the environment we need for dnsmasq
pvcd_config_file = os.environ['PVCD_CONFIG_FILE']
dhcp_environment = {
'DNSMASQ_INTERFACE': self.bridge_nic,
'PVCD_CONFIG_FILE': pvcd_config_file
}
# Define the dnsmasq config
dhcp_configuration = [
'--domain-needed',
'--bogus-priv',
'--no-resolv',
'--filterwin2k',
'--expand-hosts',
'--domain={}'.format(self.domain),
'--local=/{}/'.format(self.domain),
'--auth-zone={}'.format(self.domain),
# '--auth-peer=127.0.0.1,{}'.format(self.ip_gateway),
'--auth-sec-servers=127.0.0.1,[::1],{}'.format(self.ip_gateway),
'--listen-address={}'.format(self.ip_gateway),
'--bind-interfaces',
'--leasefile-ro',
'--dhcp-script=/usr/share/pvc/pvcd/dnsmasq-zookeeper-leases.py',
'--dhcp-range={},{},4h'.format(self.dhcp_start, self.dhcp_end),
'--dhcp-lease-max=99',
'--dhcp-hostsdir={}'.format(self.dnsmasq_hostsdir),
'--log-queries=extra',
'--log-facility=DAEMON',
'--keep-in-foreground'
]
# Start the dnsmasq process in a thread
self.dhcp_server_daemon = common.run_os_daemon(
'/usr/sbin/dnsmasq {}'.format(
' '.join(dhcp_configuration)
),
environment=dhcp_environment
)
def removeNetwork(self):
self.logger.out(
'Removing VNI device on interface {}'.format(
self.vni_dev
),
prefix='VNI {}'.format(self.vni),
state='o'
)
common.run_os_command(
'ip link set {} down'.format(
self.bridge_nic
)
)
common.run_os_command(
'ip link set {} down'.format(
self.vxlan_nic
)
)
common.run_os_command(
'brctl delif {} {}'.format(
self.bridge_nic,
self.vxlan_nic
)
)
common.run_os_command(
'brctl delbr {}'.format(
self.bridge_nic
)
)
common.run_os_command(
'ip link delete {}'.format(
self.vxlan_nic
)
)
def removeFirewall(self):
os.remove(self.nftables_netconf_filename)
open(self.nftables_update_filename, 'a').close()
pass
def removeGatewayAddress(self):
self.logger.out(
'Removing gateway {} from interface {}'.format(
self.ip_gateway,
self.bridge_nic
),
prefix='VNI {}'.format(self.vni),
state='o'
)
common.run_os_command(
'ip address delete {}/{} dev {}'.format(
self.ip_gateway,
self.ip_cidrnetmask,
self.bridge_nic
)
)
def stopDHCPServer(self):
if self.dhcp_server_daemon:
self.logger.out(
'Stopping dnsmasq DHCP server on interface {}'.format(
self.bridge_nic
),
prefix='VNI {}'.format(self.vni),
state='o'
)
self.dhcp_server_daemon.signal('term')