diff --git a/debian/control b/debian/control index 2343b8c4..c673e8a4 100644 --- a/debian/control +++ b/debian/control @@ -26,6 +26,17 @@ Description: Parallel Virtual Cluster virtualization daemon (Python 3) . This package installs the PVC virtualization daemon +Package: pvc-network-daemon +Architecture: all +Depends: pvc-daemon-common, python3-libvirt, ipmitool, libvirt-daemon-system +Suggests: pvc-client, pvc-virtualization-daemon +Description: Parallel Virtual Cluster network daemon (Python 3) + The Parallel Virtual Cluster provides a management solution for QEMU/KVM virtual clusters, + including full control of running VMs, definitions, and hypervisors (including fencing via + IPMI). This package provides the daemon component for networking on a hypervisor node. + . + This package installs the PVC network daemon + Package: pvc-client Architecture: all Depends: python3-kazoo, python3-libvirt, python3-psutil, python3-click, python3-lxml diff --git a/debian/pvc-network-daemon.install b/debian/pvc-network-daemon.install new file mode 100644 index 00000000..24cb205b --- /dev/null +++ b/debian/pvc-network-daemon.install @@ -0,0 +1,4 @@ +network-daemon/pvcnd.py usr/share/pvc +network-daemon/pvcnd.service lib/systemd/system +network-daemon/pvcnd.conf.sample etc/pvc +network-daemon/pvcnd usr/share/pvc diff --git a/debian/pvc-network-daemon.postinst b/debian/pvc-network-daemon.postinst new file mode 100644 index 00000000..b40df819 --- /dev/null +++ b/debian/pvc-network-daemon.postinst @@ -0,0 +1,6 @@ +#!/bin/sh + +# Enable the servive +systemctl enable /lib/systemd/system/pvcnd.service + +echo "The PVC network daemon has not been started. Create a config file at /etc/pvc/pvcnd.conf then start it." diff --git a/debian/pvc-network-daemon.prerm b/debian/pvc-network-daemon.prerm new file mode 100644 index 00000000..3f4fd8cb --- /dev/null +++ b/debian/pvc-network-daemon.prerm @@ -0,0 +1,5 @@ +#!/bin/sh + +# Disable the service +systemctl disable pvcnd.service + diff --git a/network-daemon/pvcnd.conf.sample b/network-daemon/pvcnd.conf.sample new file mode 100644 index 00000000..1cd17613 --- /dev/null +++ b/network-daemon/pvcnd.conf.sample @@ -0,0 +1,22 @@ +# pvcnd cluster configuration file example +# +# This configuration file specifies details for this node in PVC. Multiple host +# blocks can be added but only the one matching the current system hostname will +# be used by the local daemon. Default values apply to all hosts for any value +# not specifically overridden. +# +# The following values are required for each host or in a default section: +# zookeeper: the IP+port of the Zookeper instance (defaults to 127.0.0.1:2181) +# vni_dev: the lower-level network device to bind VNI to +# vni_dev_ip: the IP address (CIDR) of the lower-level network device, used +# by FRR to communicate with the route reflectors and pass routes +# for VNI interfaces +# +# Copy this example to /etc/pvc/pvcnd.conf and edit to your needs + +[default] +zookeeper = 127.0.0.1:2181 + +[myhost] +vni_dev = ens4 +vni_dev_ip = 10.255.0.3/24 diff --git a/network-daemon/pvcnd.py b/network-daemon/pvcnd.py new file mode 100755 index 00000000..e35a036a --- /dev/null +++ b/network-daemon/pvcnd.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +# pvcnd.py - Network daemon startup stub +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# 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 . +# +############################################################################### + +import pvcnd.Daemon diff --git a/network-daemon/pvcnd.service b/network-daemon/pvcnd.service new file mode 100644 index 00000000..4835ee8a --- /dev/null +++ b/network-daemon/pvcnd.service @@ -0,0 +1,15 @@ +# Parallel Virtual Cluster network daemon unit file +[Unit] +Description = Parallel Virtual Cluster network daemon +After = network-online.target libvirtd.service zookeeper.service + +[Service] +Type = simple +WorkingDirectory = /usr/share/pvc +Environment = PYTHONUNBUFFERED=true +Environment = PVCVD_CONFIG_FILE=/etc/pvc/pvcnd.conf +ExecStart = /usr/share/pvc/pvcnd.py +Restart = on-failure + +[Install] +WantedBy = multi-user.target diff --git a/network-daemon/pvcnd/Daemon.py b/network-daemon/pvcnd/Daemon.py new file mode 100644 index 00000000..8be3be57 --- /dev/null +++ b/network-daemon/pvcnd/Daemon.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 + +# Daemon.py - PVC hypervisor network daemon +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# 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 . +# +############################################################################### + +import kazoo.client +import sys +import os +import signal +import socket +import psutil +import configparser +import time + +import lib.ansiiprint as ansiiprint +import lib.zkhandler as zkhandler + +import pvcnd.VXNetworkInstance as VXNetworkInstance + +print(ansiiprint.bold() + "pvcnd - Parallel Virtual Cluster network daemon" + ansiiprint.end()) + +# Get the config file variable from the environment +try: + pvcnd_config_file = os.environ['PVCND_CONFIG_FILE'] +except: + print('ERROR: The "PVCND_CONFIG_FILE" environment variable must be set before starting pvcnd.') + exit(1) + +myhostname = socket.gethostname() +myshorthostname = myhostname.split('.', 1)[0] +mydomainname = ''.join(myhostname.split('.', 1)[1:]) + +# Config values dictionary +config_values = [ + 'zookeeper', + 'vni_dev', + 'vni_dev_ip', +] +def readConfig(pvcnd_config_file, myhostname): + print('Loading configuration from file {}'.format(pvcnd_config_file)) + + o_config = configparser.ConfigParser() + o_config.read(pvcnd_config_file) + config = {} + + try: + entries = o_config[myhostname] + except: + try: + entries = o_config['default'] + except: + print('ERROR: Config file is not valid!') + exit(1) + + for entry in config_values: + try: + config[entry] = entries[entry] + except: + try: + config[entry] = o_config['default'][entry] + except: + print('ERROR: Config file missing required value "{}" for this host!'.format(entry)) + exit(1) + + return config + +config = readConfig(pvcnd_config_file, myhostname) + +zk_conn = kazoo.client.KazooClient(hosts=config['zookeeper']) +try: + print('Connecting to Zookeeper instance at {}'.format(config['zookeeper'])) + zk_conn.start() +except: + print('ERROR: Failed to connect to Zookeeper!') + exit(1) + +# Handle zookeeper failures gracefully +def zk_listener(state): + global zk_conn + if state == kazoo.client.KazooState.SUSPENDED: + ansiiprint.echo('Connection to Zookeeper list; retrying', '', 'e') + + while True: + _zk_conn = kazoo.client.KazooClient(hosts=config['zookeeper']) + try: + _zk_conn.start() + zk_conn = _zk_conn + break + except: + time.sleep(1) + elif state == kazoo.client.KazooState.CONNECTED: + anisiprint.echo('Connection to Zookeeper started', '', 'o') + else: + pass + +zk_conn.add_listener(zk_listener) + +# Cleanup function +def cleanup(signum, frame): + ansiiprint.echo('Terminating daemon', '', 'e') + # Close the Zookeeper connection + try: + zk_conn.stop() + zk_conn.close() + except: + pass + # Exit + exit(0) + +# Handle signals with cleanup +signal.signal(signal.SIGTERM, cleanup) +signal.signal(signal.SIGINT, cleanup) +signal.signal(signal.SIGQUIT, cleanup) + +# What this daemon does: +# 1. Configure public networks dynamically on startup (e.g. bonding, vlans, etc.) from config +# * no /etc/network/interfaces config for these - just mgmt interface via DHCP! +# 2. Watch ZK /networks +# 3. Provision required network interfaces when a network is added +# a. create vxlan interface targeting local dev from config +# b. create bridge interface +# c. add vxlan to bridge +# d. set interfaces up +# 4. Remove network interfaces when network disapears + +# Zookeeper schema: +# networks/ +# / +# ipnet e.g. 10.101.0.0/24 +# gateway e.g. 10.101.0.1 [1] +# routers e.g. 10.101.0.2,10.101.0.3 [2] +# dhcp e.g. YES [3] +# reservations/ +# / +# address e.g. 10.101.0.30 +# mac e.g. ff:ff:fe:ab:cd:ef +# fwrules/ +# / +# description e.g. Allow HTTP from any to this net +# src e.g. any +# dest e.g. this +# port e.g. 80 + +# Notes: +# [1] becomes a VIP between the pair of routers in multi-router envs +# [2] becomes real addrs on the pair of routers in multi-router envs +# [2] should match gateway in single-router envs for consistency +# [3] enables or disables a DHCP subnet definition for the network + + +# Prepare underlying interface +if config['vni_dev_ip'] == 'dhcp': + vni_dev = config['vni_dev'] + ansiiprint.echo('Configuring VNI parent device {} with DHCP IP'.format(vni_dev), '', 'o') + os.system( + 'sudo ip link set {0} up'.format( + vni_dev + ) + ) + os.system( + 'sudo dhclient {0}'.format( + vni_dev + ) + ) +else: + vni_dev = config['vni_dev'] + vni_dev_ip = config['vni_dev_ip'] + ansiiprint.echo('Configuring VNI parent device {} with IP {}'.format(vni_dev, vni_dev_ip), '', 'o') + os.system( + 'sudo ip link set {0} up'.format( + vni_dev + ) + ) + os.system( + 'sudo ip address add {0} dev {1}'.format( + vni_dev_ip, + vni_dev + ) + ) + +# Prepare VNI list +t_vni = dict() +vni_list = [] + +@zk_conn.ChildrenWatch('/networks') +def updatenetworks(new_vni_list): + global vni_list + print(ansiiprint.blue() + 'Network list: ' + ansiiprint.end() + '{}'.format(' '.join(new_vni_list))) + # Add new VNIs + for vni in new_vni_list: + if vni not in vni_list: + vni_list.append(vni) + t_vni[vni] = VXNetworkInstance.VXNetworkInstance(vni, zk_conn, config) + t_vni[vni].createNetwork() + # Remove deleted VNIs + for vni in vni_list: + if vni not in new_vni_list: + vni_list.remove(vni) + t_vni[vni].removeNetwork() + +# Tick loop +while True: + try: + time.sleep(0.1) + except: + break diff --git a/network-daemon/pvcnd/VXNetworkInstance.py b/network-daemon/pvcnd/VXNetworkInstance.py new file mode 100644 index 00000000..af08d9a4 --- /dev/null +++ b/network-daemon/pvcnd/VXNetworkInstance.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +# VXNetworkInstance.py - Class implementing a PVC VM network and run by pvcnd +# Part of the Parallel Virtual Cluster (PVC) system +# +# Copyright (C) 2018 Joshua M. Boniface +# +# 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 . +# +############################################################################### + +import os, sys +import lib.ansiiprint as ansiiprint +import lib.zkhandler as zkhandler + +class VXNetworkInstance(): + # Initialization function + def __init__ (self, vni, zk_conn, config): + self.vni = vni + self.zk_conn = zk_conn + self.vni_dev = config['vni_dev'] + + def createNetwork(self): + ansiiprint.echo('Creating VNI {} device on interface {}'.format(self.vni, self.vni_dev), '', 'o') + os.system( + 'sudo ip link add vxlan{0} type vxlan id {0} dstport 4789 dev {1} nolearning'.format( + self.vni, + self.vni_dev + ) + ) + os.system( + 'sudo brctl addbr br{0}'.format( + self.vni + ) + ) + os.system( + 'sudo brctl addif br{0} vxlan{0}'.format( + self.vni + ) + ) + os.system( + 'sudo ip link set vxlan{0} up'.format( + self.vni + ) + ) + os.system( + 'sudo ip link set br{0} up'.format( + self.vni + ) + ) + + def removeNetwork(self): + ansiiprint.echo('Removing VNI {} device on interface {}'.format(self.vni, self.vni_dev), '', 'o') + os.system( + 'sudo ip link set br{0} down'.format( + self.vni + ) + ) + os.system( + 'sudo ip link set vxlan{0} down'.format( + self.vni + ) + ) + os.system( + 'sudo brctl delif br{0} vxlan{0}'.format( + self.vni + ) + ) + os.system( + 'sudo brctl delbr br{0}'.format( + self.vni + ) + ) + os.system( + 'sudo ip link delete vxlan{0}'.format( + self.vni + ) + ) diff --git a/network-daemon/pvcnd/__init__.py b/network-daemon/pvcnd/__init__.py new file mode 100644 index 00000000..e69de29b