385 lines
15 KiB
Python
385 lines
15 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-2021 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 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
|
||
|
|
||
|
|
||
|
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 = {
|
||
|
'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),
|
||
|
'enable_sriov': o_sysnetworks.get('sriov_enable', False),
|
||
|
'sriov_device': o_sysnetworks.get('sriov_device', list())
|
||
|
}
|
||
|
|
||
|
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'])
|