2021-12-29 22:31:01 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# Daemon.py - PVC HTTP API daemon
|
|
|
|
# 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 yaml
|
|
|
|
import signal
|
|
|
|
|
|
|
|
from sys import argv
|
|
|
|
|
2022-07-06 13:30:42 -04:00
|
|
|
import pvcbootstrapd.lib.notifications as notifications
|
2021-12-29 22:31:01 -05:00
|
|
|
import pvcbootstrapd.lib.dnsmasq as dnsmasqd
|
|
|
|
import pvcbootstrapd.lib.db as db
|
|
|
|
import pvcbootstrapd.lib.git as git
|
|
|
|
import pvcbootstrapd.lib.tftp as tftp
|
|
|
|
|
|
|
|
from distutils.util import strtobool as dustrtobool
|
|
|
|
|
|
|
|
# Daemon version
|
|
|
|
version = "0.1"
|
|
|
|
|
|
|
|
# API version
|
|
|
|
API_VERSION = 1.0
|
|
|
|
|
|
|
|
|
|
|
|
##########################################################
|
|
|
|
# Exceptions
|
|
|
|
##########################################################
|
|
|
|
|
|
|
|
|
|
|
|
class MalformedConfigurationError(Exception):
|
|
|
|
"""
|
|
|
|
An exception when parsing the PVC daemon configuration file
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, error=None):
|
|
|
|
self.msg = f"ERROR: Configuration file is malformed: {error}"
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return str(self.msg)
|
|
|
|
|
|
|
|
|
|
|
|
##########################################################
|
|
|
|
# Helper Functions
|
|
|
|
##########################################################
|
|
|
|
|
|
|
|
|
|
|
|
def strtobool(stringv):
|
|
|
|
if stringv is None:
|
|
|
|
return False
|
|
|
|
if isinstance(stringv, bool):
|
|
|
|
return bool(stringv)
|
|
|
|
try:
|
|
|
|
return bool(dustrtobool(stringv))
|
|
|
|
except Exception:
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
##########################################################
|
|
|
|
# Configuration Parsing
|
|
|
|
##########################################################
|
|
|
|
|
|
|
|
|
|
|
|
def get_config_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 read_config():
|
|
|
|
pvcbootstrapd_config_file = get_config_path()
|
|
|
|
|
|
|
|
print(f"Loading configuration from file '{pvcbootstrapd_config_file}'")
|
|
|
|
|
|
|
|
# Load the YAML config file
|
|
|
|
with open(pvcbootstrapd_config_file, "r") as cfgfile:
|
|
|
|
try:
|
|
|
|
o_config = yaml.load(cfgfile, Loader=yaml.SafeLoader)
|
|
|
|
except Exception as e:
|
|
|
|
print(f"ERROR: Failed to parse configuration file: {e}")
|
|
|
|
os._exit(1)
|
|
|
|
|
|
|
|
# Create the configuration dictionary
|
|
|
|
config = dict()
|
|
|
|
|
|
|
|
# Get the base configuration
|
|
|
|
try:
|
|
|
|
o_base = o_config["pvc"]
|
|
|
|
except KeyError as k:
|
|
|
|
raise MalformedConfigurationError(f"Missing top-level category {k}")
|
|
|
|
|
|
|
|
for key in ["debug", "deploy_username"]:
|
|
|
|
try:
|
|
|
|
config[key] = o_base[key]
|
|
|
|
except KeyError as k:
|
|
|
|
raise MalformedConfigurationError(f"Missing first-level key {k}")
|
|
|
|
|
|
|
|
# Get the first-level categories
|
|
|
|
try:
|
|
|
|
o_database = o_base["database"]
|
|
|
|
o_api = o_base["api"]
|
|
|
|
o_queue = o_base["queue"]
|
|
|
|
o_dhcp = o_base["dhcp"]
|
|
|
|
o_tftp = o_base["tftp"]
|
|
|
|
o_ansible = o_base["ansible"]
|
2022-07-06 13:46:27 -04:00
|
|
|
o_notifications = o_base["notifications"]
|
2021-12-29 22:31:01 -05:00
|
|
|
except KeyError as k:
|
|
|
|
raise MalformedConfigurationError(f"Missing first-level category {k}")
|
|
|
|
|
|
|
|
# Get the Datbase configuration
|
|
|
|
for key in ["path"]:
|
|
|
|
try:
|
|
|
|
config[f"database_{key}"] = o_database[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level key '{key}' under 'database'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get the API configuration
|
|
|
|
for key in ["address", "port"]:
|
|
|
|
try:
|
|
|
|
config[f"api_{key}"] = o_api[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level key '{key}' under 'api'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get the queue configuration
|
|
|
|
for key in ["address", "port", "path"]:
|
|
|
|
try:
|
|
|
|
config[f"queue_{key}"] = o_queue[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level key '{key}' under 'queue'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get the DHCP configuration
|
|
|
|
for key in [
|
|
|
|
"address",
|
|
|
|
"gateway",
|
|
|
|
"domain",
|
|
|
|
"lease_start",
|
|
|
|
"lease_end",
|
|
|
|
"lease_time",
|
|
|
|
]:
|
|
|
|
try:
|
|
|
|
config[f"dhcp_{key}"] = o_dhcp[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level key '{key}' under 'dhcp'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get the TFTP configuration
|
|
|
|
for key in ["root_path", "host_path"]:
|
|
|
|
try:
|
|
|
|
config[f"tftp_{key}"] = o_tftp[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level key '{key}' under 'tftp'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get the Ansible configuration
|
|
|
|
for key in ["path", "keyfile", "remote", "branch", "clusters_file"]:
|
|
|
|
try:
|
|
|
|
config[f"ansible_{key}"] = o_ansible[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level key '{key}' under 'ansible'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get the second-level categories under Ansible
|
|
|
|
try:
|
|
|
|
o_ansible_cspec_files = o_ansible["cspec_files"]
|
|
|
|
except KeyError as k:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level category {k} under 'ansible'"
|
|
|
|
)
|
|
|
|
|
|
|
|
# Get the Ansible CSpec Files configuration
|
|
|
|
for key in ["base", "pvc", "bootstrap"]:
|
|
|
|
try:
|
|
|
|
config[f"ansible_cspec_files_{key}"] = o_ansible_cspec_files[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing third-level key '{key}' under 'ansible/cspec_files'"
|
|
|
|
)
|
|
|
|
|
2022-07-06 13:46:27 -04:00
|
|
|
# Get the Notifications configuration
|
|
|
|
for key in ["enabled", "uri", "action", "icons", "body"]:
|
|
|
|
try:
|
|
|
|
config[f"notifications_{key}"] = o_notifications[key]
|
|
|
|
except Exception:
|
|
|
|
raise MalformedConfigurationError(
|
|
|
|
f"Missing second-level key '{key}' under 'notifications'"
|
|
|
|
)
|
|
|
|
|
2021-12-29 22:31:01 -05:00
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
config = read_config()
|
|
|
|
|
|
|
|
|
|
|
|
##########################################################
|
|
|
|
# Entrypoint
|
|
|
|
##########################################################
|
|
|
|
|
|
|
|
|
|
|
|
def entrypoint():
|
|
|
|
import pvcbootstrapd.flaskapi as pvcbootstrapd # noqa: E402
|
|
|
|
|
|
|
|
# Print our startup messages
|
|
|
|
print("")
|
|
|
|
print("|----------------------------------------------------------|")
|
|
|
|
print("| |")
|
|
|
|
print("| ███████████ ▜█▙ ▟█▛ █████ █ █ █ |")
|
|
|
|
print("| ██ ▜█▙ ▟█▛ ██ |")
|
|
|
|
print("| ███████████ ▜█▙ ▟█▛ ██ |")
|
|
|
|
print("| ██ ▜█▙▟█▛ ███████████ |")
|
|
|
|
print("| |")
|
|
|
|
print("|----------------------------------------------------------|")
|
|
|
|
print("| Parallel Virtual Cluster Bootstrap API daemon v{0: <9} |".format(version))
|
|
|
|
print("| Debug: {0: <49} |".format(str(config["debug"])))
|
|
|
|
print("| API version: v{0: <42} |".format(API_VERSION))
|
|
|
|
print(
|
|
|
|
"| Listen: {0: <48} |".format(
|
|
|
|
"{}:{}".format(config["api_address"], config["api_port"])
|
|
|
|
)
|
|
|
|
)
|
|
|
|
print("|----------------------------------------------------------|")
|
|
|
|
print("")
|
|
|
|
|
2022-07-06 13:30:42 -04:00
|
|
|
notifications.send_webhook(config, "begin", "Starting up pvcbootstrapd")
|
|
|
|
|
2021-12-29 22:31:01 -05:00
|
|
|
# Initialize the database
|
|
|
|
db.init_database(config)
|
|
|
|
|
|
|
|
# Initialize the Ansible repository
|
|
|
|
git.init_repository(config)
|
|
|
|
|
|
|
|
# Initialize the tftp root
|
|
|
|
tftp.init_tftp(config)
|
|
|
|
|
|
|
|
if "--init-only" in argv:
|
|
|
|
print("Successfully initialized pvcbootstrapd; exiting.")
|
2022-07-06 13:49:24 -04:00
|
|
|
notifications.send_webhook(config, "success", "Successfully initialized pvcbootstrapd")
|
2021-12-29 22:31:01 -05:00
|
|
|
exit(0)
|
|
|
|
|
|
|
|
# Start DNSMasq
|
|
|
|
dnsmasq = dnsmasqd.DNSMasq(config)
|
|
|
|
dnsmasq.start()
|
|
|
|
|
|
|
|
def cleanup(retcode):
|
|
|
|
dnsmasq.stop()
|
|
|
|
exit(retcode)
|
|
|
|
|
|
|
|
def term(signum="", frame=""):
|
|
|
|
print("Received TERM, exiting.")
|
2022-07-06 13:49:24 -04:00
|
|
|
notifications.send_webhook(config, "begin", "Received TERM, exiting pvcbootstrapd")
|
2021-12-29 22:31:01 -05:00
|
|
|
cleanup(0)
|
|
|
|
|
|
|
|
signal.signal(signal.SIGTERM, term)
|
|
|
|
signal.signal(signal.SIGINT, term)
|
|
|
|
signal.signal(signal.SIGQUIT, term)
|
|
|
|
|
2022-07-06 13:49:24 -04:00
|
|
|
notifications.send_webhook(config, "success", "Started up pvcbootstrapd")
|
|
|
|
|
2021-12-29 22:31:01 -05:00
|
|
|
# Start Flask
|
|
|
|
pvcbootstrapd.app.run(
|
|
|
|
config["api_address"],
|
|
|
|
config["api_port"],
|
|
|
|
use_reloader=False,
|
|
|
|
threaded=False,
|
|
|
|
processes=4,
|
|
|
|
)
|