431 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			431 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python3
 | 
						|
 | 
						|
# config.py - Utility functions for pvcnoded configuration parsing
 | 
						|
# Part of the Parallel Virtual Cluster (PVC) system
 | 
						|
#
 | 
						|
#    Copyright (C) 2018-2022 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, version 3.
 | 
						|
#
 | 
						|
#    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 daemon_lib.common as common
 | 
						|
 | 
						|
import os
 | 
						|
import subprocess
 | 
						|
import yaml
 | 
						|
 | 
						|
from socket import gethostname
 | 
						|
from re import findall
 | 
						|
from psutil import cpu_count
 | 
						|
from ipaddress import ip_address, ip_network
 | 
						|
from json import loads
 | 
						|
 | 
						|
 | 
						|
class MalformedConfigurationError(Exception):
 | 
						|
    """
 | 
						|
    An except when parsing the PVC Node daemon configuration file
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, error=None):
 | 
						|
        self.msg = f"ERROR: Configuration file is malformed: {error}"
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return str(self.msg)
 | 
						|
 | 
						|
 | 
						|
def get_static_data():
 | 
						|
    """
 | 
						|
    Data that is obtained once at node startup for use later
 | 
						|
    """
 | 
						|
    staticdata = list()
 | 
						|
    staticdata.append(str(cpu_count()))  # CPU count
 | 
						|
    staticdata.append(
 | 
						|
        subprocess.run(["uname", "-r"], stdout=subprocess.PIPE)
 | 
						|
        .stdout.decode("ascii")
 | 
						|
        .strip()
 | 
						|
    )
 | 
						|
    staticdata.append(
 | 
						|
        subprocess.run(["uname", "-o"], stdout=subprocess.PIPE)
 | 
						|
        .stdout.decode("ascii")
 | 
						|
        .strip()
 | 
						|
    )
 | 
						|
    staticdata.append(
 | 
						|
        subprocess.run(["uname", "-m"], stdout=subprocess.PIPE)
 | 
						|
        .stdout.decode("ascii")
 | 
						|
        .strip()
 | 
						|
    )
 | 
						|
 | 
						|
    return staticdata
 | 
						|
 | 
						|
 | 
						|
def get_configuration_path():
 | 
						|
    try:
 | 
						|
        return os.environ["PVCD_CONFIG_FILE"]
 | 
						|
    except KeyError:
 | 
						|
        print('ERROR: The "PVCD_CONFIG_FILE" environment variable must be set.')
 | 
						|
        os._exit(1)
 | 
						|
 | 
						|
 | 
						|
def get_hostname():
 | 
						|
    node_fqdn = gethostname()
 | 
						|
    node_hostname = node_fqdn.split(".", 1)[0]
 | 
						|
    node_domain = "".join(node_fqdn.split(".", 1)[1:])
 | 
						|
    try:
 | 
						|
        node_id = findall(r"\d+", node_hostname)[-1]
 | 
						|
    except IndexError:
 | 
						|
        node_id = 0
 | 
						|
 | 
						|
    return node_fqdn, node_hostname, node_domain, node_id
 | 
						|
 | 
						|
 | 
						|
def validate_floating_ip(config, network):
 | 
						|
    if network not in ["cluster", "storage", "upstream"]:
 | 
						|
        return False, f'Specified network type "{network}" is not valid'
 | 
						|
 | 
						|
    floating_key = f"{network}_floating_ip"
 | 
						|
    network_key = f"{network}_network"
 | 
						|
 | 
						|
    # Verify the network provided is valid
 | 
						|
    try:
 | 
						|
        network = ip_network(config[network_key])
 | 
						|
    except Exception:
 | 
						|
        return (
 | 
						|
            False,
 | 
						|
            f"Network address {config[network_key]} for {network_key} is not valid",
 | 
						|
        )
 | 
						|
 | 
						|
    # Verify that the floating IP is valid (and in the network)
 | 
						|
    try:
 | 
						|
        floating_address = ip_address(config[floating_key].split("/")[0])
 | 
						|
        if floating_address not in list(network.hosts()):
 | 
						|
            raise
 | 
						|
    except Exception:
 | 
						|
        return (
 | 
						|
            False,
 | 
						|
            f"Floating address {config[floating_key]} for {floating_key} is not valid",
 | 
						|
        )
 | 
						|
 | 
						|
    return True, ""
 | 
						|
 | 
						|
 | 
						|
def get_configuration():
 | 
						|
    """
 | 
						|
    Parse the configuration of the node daemon.
 | 
						|
    """
 | 
						|
    pvcnoded_config_file = get_configuration_path()
 | 
						|
 | 
						|
    print('Loading configuration from file "{}"'.format(pvcnoded_config_file))
 | 
						|
 | 
						|
    with open(pvcnoded_config_file, "r") as cfgfile:
 | 
						|
        try:
 | 
						|
            o_config = yaml.load(cfgfile, Loader=yaml.SafeLoader)
 | 
						|
        except Exception as e:
 | 
						|
            print("ERROR: Failed to parse configuration file: {}".format(e))
 | 
						|
            os._exit(1)
 | 
						|
 | 
						|
    node_fqdn, node_hostname, node_domain, node_id = get_hostname()
 | 
						|
 | 
						|
    # Create the configuration dictionary
 | 
						|
    config = dict()
 | 
						|
 | 
						|
    # Get the initial base configuration
 | 
						|
    try:
 | 
						|
        o_base = o_config["pvc"]
 | 
						|
        o_cluster = o_config["pvc"]["cluster"]
 | 
						|
    except Exception as e:
 | 
						|
        raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
    config_general = {
 | 
						|
        "node": o_base.get("node", node_hostname),
 | 
						|
        "node_hostname": node_hostname,
 | 
						|
        "node_fqdn": node_fqdn,
 | 
						|
        "node_domain": node_domain,
 | 
						|
        "node_id": node_id,
 | 
						|
        "coordinators": o_cluster.get("coordinators", list()),
 | 
						|
        "debug": o_base.get("debug", False),
 | 
						|
    }
 | 
						|
 | 
						|
    config = {**config, **config_general}
 | 
						|
 | 
						|
    # Get the functions configuration
 | 
						|
    try:
 | 
						|
        o_functions = o_config["pvc"]["functions"]
 | 
						|
    except Exception as e:
 | 
						|
        raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
    config_functions = {
 | 
						|
        "enable_hypervisor": o_functions.get("enable_hypervisor", False),
 | 
						|
        "enable_networking": o_functions.get("enable_networking", False),
 | 
						|
        "enable_storage": o_functions.get("enable_storage", False),
 | 
						|
        "enable_api": o_functions.get("enable_api", False),
 | 
						|
    }
 | 
						|
 | 
						|
    config = {**config, **config_functions}
 | 
						|
 | 
						|
    # Get the directory configuration
 | 
						|
    try:
 | 
						|
        o_directories = o_config["pvc"]["system"]["configuration"]["directories"]
 | 
						|
    except Exception as e:
 | 
						|
        raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
    config_directories = {
 | 
						|
        "plugin_directory": o_directories.get(
 | 
						|
            "plugin_directory", "/usr/share/pvc/plugins"
 | 
						|
        ),
 | 
						|
        "dynamic_directory": o_directories.get("dynamic_directory", None),
 | 
						|
        "log_directory": o_directories.get("log_directory", None),
 | 
						|
        "console_log_directory": o_directories.get("console_log_directory", None),
 | 
						|
    }
 | 
						|
 | 
						|
    # Define our dynamic directory schema
 | 
						|
    config_directories["dnsmasq_dynamic_directory"] = (
 | 
						|
        config_directories["dynamic_directory"] + "/dnsmasq"
 | 
						|
    )
 | 
						|
    config_directories["pdns_dynamic_directory"] = (
 | 
						|
        config_directories["dynamic_directory"] + "/pdns"
 | 
						|
    )
 | 
						|
    config_directories["nft_dynamic_directory"] = (
 | 
						|
        config_directories["dynamic_directory"] + "/nft"
 | 
						|
    )
 | 
						|
 | 
						|
    # Define our log directory schema
 | 
						|
    config_directories["dnsmasq_log_directory"] = (
 | 
						|
        config_directories["log_directory"] + "/dnsmasq"
 | 
						|
    )
 | 
						|
    config_directories["pdns_log_directory"] = (
 | 
						|
        config_directories["log_directory"] + "/pdns"
 | 
						|
    )
 | 
						|
    config_directories["nft_log_directory"] = (
 | 
						|
        config_directories["log_directory"] + "/nft"
 | 
						|
    )
 | 
						|
 | 
						|
    config = {**config, **config_directories}
 | 
						|
 | 
						|
    # Get the logging configuration
 | 
						|
    try:
 | 
						|
        o_logging = o_config["pvc"]["system"]["configuration"]["logging"]
 | 
						|
    except Exception as e:
 | 
						|
        raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
    config_logging = {
 | 
						|
        "file_logging": o_logging.get("file_logging", False),
 | 
						|
        "stdout_logging": o_logging.get("stdout_logging", False),
 | 
						|
        "zookeeper_logging": o_logging.get("zookeeper_logging", False),
 | 
						|
        "log_colours": o_logging.get("log_colours", False),
 | 
						|
        "log_dates": o_logging.get("log_dates", False),
 | 
						|
        "log_keepalives": o_logging.get("log_keepalives", False),
 | 
						|
        "log_keepalive_cluster_details": o_logging.get(
 | 
						|
            "log_keepalive_cluster_details", False
 | 
						|
        ),
 | 
						|
        "log_keepalive_storage_details": o_logging.get(
 | 
						|
            "log_keepalive_storage_details", False
 | 
						|
        ),
 | 
						|
        "console_log_lines": o_logging.get("console_log_lines", False),
 | 
						|
        "node_log_lines": o_logging.get("node_log_lines", False),
 | 
						|
    }
 | 
						|
 | 
						|
    config = {**config, **config_logging}
 | 
						|
 | 
						|
    # Get the interval configuration
 | 
						|
    try:
 | 
						|
        o_intervals = o_config["pvc"]["system"]["intervals"]
 | 
						|
    except Exception as e:
 | 
						|
        raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
    config_intervals = {
 | 
						|
        "vm_shutdown_timeout": int(o_intervals.get("vm_shutdown_timeout", 60)),
 | 
						|
        "keepalive_interval": int(o_intervals.get("keepalive_interval", 5)),
 | 
						|
        "fence_intervals": int(o_intervals.get("fence_intervals", 6)),
 | 
						|
        "suicide_intervals": int(o_intervals.get("suicide_interval", 0)),
 | 
						|
    }
 | 
						|
 | 
						|
    config = {**config, **config_intervals}
 | 
						|
 | 
						|
    # Get the fencing configuration
 | 
						|
    try:
 | 
						|
        o_fencing = o_config["pvc"]["system"]["fencing"]
 | 
						|
        o_fencing_actions = o_fencing["actions"]
 | 
						|
        o_fencing_ipmi = o_fencing["ipmi"]
 | 
						|
    except Exception as e:
 | 
						|
        raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
    config_fencing = {
 | 
						|
        "successful_fence": o_fencing_actions.get("successful_fence", None),
 | 
						|
        "failed_fence": o_fencing_actions.get("failed_fence", None),
 | 
						|
        "ipmi_hostname": o_fencing_ipmi.get(
 | 
						|
            "host", f"{node_hostname}-lom.{node_domain}"
 | 
						|
        ),
 | 
						|
        "ipmi_username": o_fencing_ipmi.get("user", "null"),
 | 
						|
        "ipmi_password": o_fencing_ipmi.get("pass", "null"),
 | 
						|
    }
 | 
						|
 | 
						|
    config = {**config, **config_fencing}
 | 
						|
 | 
						|
    # Get the migration configuration
 | 
						|
    try:
 | 
						|
        o_migration = o_config["pvc"]["system"]["migration"]
 | 
						|
    except Exception as e:
 | 
						|
        raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
    config_migration = {
 | 
						|
        "migration_target_selector": o_migration.get("target_selector", "mem"),
 | 
						|
    }
 | 
						|
 | 
						|
    config = {**config, **config_migration}
 | 
						|
 | 
						|
    if config["enable_networking"]:
 | 
						|
        # Get the node networks configuration
 | 
						|
        try:
 | 
						|
            o_networks = o_config["pvc"]["cluster"]["networks"]
 | 
						|
            o_network_cluster = o_networks["cluster"]
 | 
						|
            o_network_storage = o_networks["storage"]
 | 
						|
            o_network_upstream = o_networks["upstream"]
 | 
						|
            o_sysnetworks = o_config["pvc"]["system"]["configuration"]["networking"]
 | 
						|
            o_sysnetwork_cluster = o_sysnetworks["cluster"]
 | 
						|
            o_sysnetwork_storage = o_sysnetworks["storage"]
 | 
						|
            o_sysnetwork_upstream = o_sysnetworks["upstream"]
 | 
						|
        except Exception as e:
 | 
						|
            raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
        config_networks = {
 | 
						|
            "cluster_domain": o_network_cluster.get("domain", None),
 | 
						|
            "cluster_network": o_network_cluster.get("network", None),
 | 
						|
            "cluster_floating_ip": o_network_cluster.get("floating_ip", None),
 | 
						|
            "cluster_dev": o_sysnetwork_cluster.get("device", None),
 | 
						|
            "cluster_mtu": o_sysnetwork_cluster.get("mtu", None),
 | 
						|
            "cluster_dev_ip": o_sysnetwork_cluster.get("address", None),
 | 
						|
            "storage_domain": o_network_storage.get("domain", None),
 | 
						|
            "storage_network": o_network_storage.get("network", None),
 | 
						|
            "storage_floating_ip": o_network_storage.get("floating_ip", None),
 | 
						|
            "storage_dev": o_sysnetwork_storage.get("device", None),
 | 
						|
            "storage_mtu": o_sysnetwork_storage.get("mtu", None),
 | 
						|
            "storage_dev_ip": o_sysnetwork_storage.get("address", None),
 | 
						|
            "upstream_domain": o_network_upstream.get("domain", None),
 | 
						|
            "upstream_network": o_network_upstream.get("network", None),
 | 
						|
            "upstream_floating_ip": o_network_upstream.get("floating_ip", None),
 | 
						|
            "upstream_gateway": o_network_upstream.get("gateway", None),
 | 
						|
            "upstream_dev": o_sysnetwork_upstream.get("device", None),
 | 
						|
            "upstream_mtu": o_sysnetwork_upstream.get("mtu", None),
 | 
						|
            "upstream_dev_ip": o_sysnetwork_upstream.get("address", None),
 | 
						|
            "bridge_dev": o_sysnetworks.get("bridge_device", None),
 | 
						|
            "bridge_mtu": o_sysnetworks.get("bridge_mtu", None),
 | 
						|
            "enable_sriov": o_sysnetworks.get("sriov_enable", False),
 | 
						|
            "sriov_device": o_sysnetworks.get("sriov_device", list()),
 | 
						|
        }
 | 
						|
 | 
						|
        if config_networks["bridge_mtu"] is None:
 | 
						|
            # Read the current MTU of bridge_dev and set bridge_mtu to it; avoids weird resets
 | 
						|
            retcode, stdout, stderr = common.run_os_command(
 | 
						|
                f"ip -json link show dev {config_networks['bridge_dev']}"
 | 
						|
            )
 | 
						|
            current_bridge_mtu = loads(stdout)[0]["mtu"]
 | 
						|
            print(
 | 
						|
                f"Config key bridge_mtu not explicitly set; using live MTU {current_bridge_mtu} from {config_networks['bridge_dev']}"
 | 
						|
            )
 | 
						|
            config_networks["bridge_mtu"] = current_bridge_mtu
 | 
						|
 | 
						|
        config = {**config, **config_networks}
 | 
						|
 | 
						|
        for network_type in ["cluster", "storage", "upstream"]:
 | 
						|
            result, msg = validate_floating_ip(config, network_type)
 | 
						|
            if not result:
 | 
						|
                raise MalformedConfigurationError(msg)
 | 
						|
 | 
						|
            address_key = "{}_dev_ip".format(network_type)
 | 
						|
            network_key = f"{network_type}_network"
 | 
						|
            network = ip_network(config[network_key])
 | 
						|
            # With autoselection of addresses, construct an IP from the relevant network
 | 
						|
            if config[address_key] == "by-id":
 | 
						|
                # The NodeID starts at 1, but indexes start at 0
 | 
						|
                address_id = int(config["node_id"]) - 1
 | 
						|
                # Grab the nth address from the network
 | 
						|
                config[address_key] = "{}/{}".format(
 | 
						|
                    list(network.hosts())[address_id], network.prefixlen
 | 
						|
                )
 | 
						|
            # Validate the provided IP instead
 | 
						|
            else:
 | 
						|
                try:
 | 
						|
                    address = ip_address(config[address_key].split("/")[0])
 | 
						|
                    if address not in list(network.hosts()):
 | 
						|
                        raise
 | 
						|
                except Exception:
 | 
						|
                    raise MalformedConfigurationError(
 | 
						|
                        f"IP address {config[address_key]} for {address_key} is not valid"
 | 
						|
                    )
 | 
						|
 | 
						|
        # Get the PowerDNS aggregator database configuration
 | 
						|
        try:
 | 
						|
            o_pdnsdb = o_config["pvc"]["coordinator"]["dns"]["database"]
 | 
						|
        except Exception as e:
 | 
						|
            raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
        config_pdnsdb = {
 | 
						|
            "pdns_postgresql_host": o_pdnsdb.get("host", None),
 | 
						|
            "pdns_postgresql_port": o_pdnsdb.get("port", None),
 | 
						|
            "pdns_postgresql_dbname": o_pdnsdb.get("name", None),
 | 
						|
            "pdns_postgresql_user": o_pdnsdb.get("user", None),
 | 
						|
            "pdns_postgresql_password": o_pdnsdb.get("pass", None),
 | 
						|
        }
 | 
						|
 | 
						|
        config = {**config, **config_pdnsdb}
 | 
						|
 | 
						|
        # Get the Cloud-Init Metadata database configuration
 | 
						|
        try:
 | 
						|
            o_metadatadb = o_config["pvc"]["coordinator"]["metadata"]["database"]
 | 
						|
        except Exception as e:
 | 
						|
            raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
        config_metadatadb = {
 | 
						|
            "metadata_postgresql_host": o_metadatadb.get("host", None),
 | 
						|
            "metadata_postgresql_port": o_metadatadb.get("port", None),
 | 
						|
            "metadata_postgresql_dbname": o_metadatadb.get("name", None),
 | 
						|
            "metadata_postgresql_user": o_metadatadb.get("user", None),
 | 
						|
            "metadata_postgresql_password": o_metadatadb.get("pass", None),
 | 
						|
        }
 | 
						|
 | 
						|
        config = {**config, **config_metadatadb}
 | 
						|
 | 
						|
    if config["enable_storage"]:
 | 
						|
        # Get the storage configuration
 | 
						|
        try:
 | 
						|
            o_storage = o_config["pvc"]["system"]["configuration"]["storage"]
 | 
						|
        except Exception as e:
 | 
						|
            raise MalformedConfigurationError(e)
 | 
						|
 | 
						|
        config_storage = {
 | 
						|
            "ceph_config_file": o_storage.get("ceph_config_file", None),
 | 
						|
            "ceph_admin_keyring": o_storage.get("ceph_admin_keyring", None),
 | 
						|
        }
 | 
						|
 | 
						|
        config = {**config, **config_storage}
 | 
						|
 | 
						|
        # Add our node static data to the config
 | 
						|
        config["static_data"] = get_static_data()
 | 
						|
 | 
						|
    return config
 | 
						|
 | 
						|
 | 
						|
def validate_directories(config):
 | 
						|
    if not os.path.exists(config["dynamic_directory"]):
 | 
						|
        os.makedirs(config["dynamic_directory"])
 | 
						|
        os.makedirs(config["dnsmasq_dynamic_directory"])
 | 
						|
        os.makedirs(config["pdns_dynamic_directory"])
 | 
						|
        os.makedirs(config["nft_dynamic_directory"])
 | 
						|
 | 
						|
    if not os.path.exists(config["log_directory"]):
 | 
						|
        os.makedirs(config["log_directory"])
 | 
						|
        os.makedirs(config["dnsmasq_log_directory"])
 | 
						|
        os.makedirs(config["pdns_log_directory"])
 | 
						|
        os.makedirs(config["nft_log_directory"])
 |