mirror of
https://github.com/runcitadel/core.git
synced 2024-11-11 16:30:38 +00:00
378 lines
12 KiB
Python
Executable File
378 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# SPDX-FileCopyrightText: 2021-2022 Citadel and contributors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from binascii import hexlify
|
|
from os import urandom
|
|
from time import sleep
|
|
|
|
import yaml
|
|
from lib.rpcauth import get_data
|
|
|
|
|
|
def generate_password(size):
|
|
"""Create size byte hex salt"""
|
|
return hexlify(urandom(size)).decode()
|
|
|
|
# Print an error if the user isn't running on Linux.
|
|
if sys.platform != 'linux':
|
|
print('This script only works on Linux!')
|
|
exit(1)
|
|
|
|
# Print an error if user is not root
|
|
if os.getuid() != 0:
|
|
print('This script must be run as root!')
|
|
exit(1)
|
|
|
|
# Check if the system is arm64 or amd64
|
|
is_arm64 = subprocess.check_output(['uname', '-m']).decode('utf-8').strip() == 'aarch64'
|
|
is_amd64 = subprocess.check_output(['uname', '-m']).decode('utf-8').strip() == 'x86_64'
|
|
|
|
if not is_arm64 and not is_amd64:
|
|
print('Citadel only works on arm64 and amd64!')
|
|
exit(1)
|
|
|
|
dependencies = False
|
|
|
|
# Check the output of "docker compose version", if it matches "Docker Compose version v2.0.0-rc.3", return true
|
|
# Otherwise, return false
|
|
def is_compose_version_except(target_version):
|
|
try:
|
|
output = subprocess.check_output(['docker', 'compose', 'version'])
|
|
if output.decode('utf-8').strip() != 'Docker Compose version {}'.format(target_version):
|
|
return True
|
|
else:
|
|
return False
|
|
except:
|
|
return True
|
|
|
|
# Download docker-compose from GitHub and put it in $HOME/.docker/cli-plugins/docker-compose
|
|
def download_docker_compose():
|
|
# Skip if os.path.expanduser('~/.docker/cli-plugins/docker-compose') exists
|
|
subprocess.check_call(["mkdir", "-p", os.path.expanduser('~/.docker/cli-plugins/')])
|
|
if is_arm64:
|
|
compose_arch = 'aarch64'
|
|
elif is_amd64:
|
|
compose_arch = 'x86_64'
|
|
# We validate that no other case than the two above can happen before
|
|
|
|
if is_compose_version_except(dependencies['compose']):
|
|
print("Docker compose not found or not required version, updating.")
|
|
compose_url = 'https://github.com/docker/compose/releases/download/{}/docker-compose-linux-{}'.format(dependencies['compose'], compose_arch)
|
|
compose_file = os.path.expanduser('~/.docker/cli-plugins/docker-compose')
|
|
subprocess.check_call(['wget', compose_url, '-O', compose_file])
|
|
os.chmod(compose_file, 0o755)
|
|
|
|
|
|
if not shutil.which("wget"):
|
|
print('Wget is not installed!')
|
|
exit(1)
|
|
|
|
if not shutil.which("docker"):
|
|
print('Docker is not installed!')
|
|
exit(1)
|
|
|
|
# Switch to node root directory.
|
|
CITADEL_ROOT=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
os.chdir(CITADEL_ROOT)
|
|
|
|
with open("./db/dependencies.yml", "r") as file:
|
|
dependencies = yaml.safe_load(file)
|
|
|
|
# Configure for appropriate network depending
|
|
# upon the user-supplied value of $NETWORK
|
|
# If the network is not specified, then use the mainnet
|
|
BITCOIN_NETWORK=os.environ.get('NETWORK') or 'mainnet'
|
|
DEVICE_IP=os.environ.get("DEVICE_IP")
|
|
|
|
# Check if network neither mainnet nor testnet nor regtest
|
|
if BITCOIN_NETWORK not in ['mainnet', 'testnet', 'signet', 'regtest']:
|
|
print('Error: Network must be either mainnet, testnet, signet or regtest!')
|
|
exit(1)
|
|
|
|
with open(os.path.join(CITADEL_ROOT, "info.json"), 'r') as file:
|
|
CITADEL_VERSION=json.load(file)['version']
|
|
|
|
status_dir = os.path.join(CITADEL_ROOT, 'statuses')
|
|
|
|
print("\n======================================")
|
|
if os.path.isfile(status_dir+'/configured'):
|
|
print("=========== RECONFIGURING ============")
|
|
reconfiguring=os.path.isfile('./.env')
|
|
else:
|
|
print("============ CONFIGURING =============")
|
|
reconfiguring=False
|
|
|
|
print("============== CITADEL ==============")
|
|
print("======================================\n")
|
|
|
|
print("Installing additional services")
|
|
data = subprocess.run("\"{}\" setup".format(os.path.join(CITADEL_ROOT, "services", "manage.py")), shell=True)
|
|
|
|
# Parse a dotenv file
|
|
# Values can either be KEY=VALUE or KEY="VALUE" or KEY='VALUE'
|
|
# Returns all env vars as a dict
|
|
def parse_dotenv(file_path):
|
|
envVars: dict = {}
|
|
with open(file_path, 'r') as file:
|
|
for line in file:
|
|
line = line.strip()
|
|
if line.startswith('#') or len(line) == 0:
|
|
continue
|
|
if '=' in line:
|
|
key, value = line.split('=', 1)
|
|
value = value.strip('"').strip("'")
|
|
envVars[key] = value
|
|
else:
|
|
print("Error: Invalid line in {}: {}".format(file_path, line))
|
|
print("Line should be in the format KEY=VALUE or KEY=\"VALUE\" or KEY='VALUE'")
|
|
exit(1)
|
|
return envVars
|
|
|
|
##########################################################
|
|
############ Generate configuration variables ############
|
|
##########################################################
|
|
|
|
CADDY_PORT=os.environ.get('CADDY_PORT') or "80"
|
|
CADDY_HTTPS_PORT=os.environ.get('CADDY_PORT') or "443"
|
|
UPDATE_CHANNEL="stable"
|
|
|
|
if reconfiguring:
|
|
dotenv=parse_dotenv('./.env')
|
|
|
|
BITCOIN_NETWORK=os.environ.get('OVERWRITE_NETWORK') or dotenv['BITCOIN_NETWORK']
|
|
# Check if network neither mainnet nor testnet nor regtest
|
|
if BITCOIN_NETWORK not in ['mainnet', 'testnet', 'signet', 'regtest']:
|
|
print('Error: Network must be either mainnet, testnet, signet or regtest!')
|
|
exit(1)
|
|
print("Using {} network\n".format(BITCOIN_NETWORK))
|
|
BITCOIN_RPC_PORT=dotenv['BITCOIN_RPC_PORT']
|
|
BITCOIN_P2P_PORT=dotenv['BITCOIN_P2P_PORT']
|
|
BITCOIN_RPC_USER=dotenv['BITCOIN_RPC_USER']
|
|
BITCOIN_RPC_PASS=dotenv['BITCOIN_RPC_PASS']
|
|
BITCOIN_RPC_AUTH=dotenv['BITCOIN_RPC_AUTH']
|
|
if 'NGINX_PORT' in dotenv:
|
|
CADDY_PORT=dotenv['NGINX_PORT']
|
|
if 'CADDY_PORT' in dotenv:
|
|
CADDY_PORT=dotenv['CADDY_PORT']
|
|
CADDY_HTTPS_PORT="443"
|
|
if 'CADDY_HTTPS_PORT' in dotenv:
|
|
CADDY_HTTPS_PORT=dotenv['CADDY_HTTPS_PORT']
|
|
if CADDY_HTTPS_PORT == "80" and CADDY_PORT == "80":
|
|
CADDY_HTTPS_PORT="443"
|
|
if 'UPDATE_CHANNEL' in dotenv and dotenv['UPDATE_CHANNEL'] != "main" and dotenv['UPDATE_CHANNEL'] != "migration":
|
|
UPDATE_CHANNEL=dotenv['UPDATE_CHANNEL']
|
|
if 'I2P_PASSWORD' in dotenv:
|
|
I2P_PASSWORD=dotenv['I2P_PASSWORD']
|
|
else:
|
|
I2P_PASSWORD=generate_password(64)
|
|
else:
|
|
# Generate RPC credentials
|
|
print("Generating auth credentials\n")
|
|
BITCOIN_RPC_USER="citadel"
|
|
BITCOIN_RPC_DETAILS=get_data(BITCOIN_RPC_USER)
|
|
BITCOIN_RPC_AUTH=BITCOIN_RPC_DETAILS['auth']
|
|
BITCOIN_RPC_PASS=BITCOIN_RPC_DETAILS['password']
|
|
I2P_PASSWORD=generate_password(64)
|
|
|
|
EXTERNAL_IP = ""
|
|
if os.path.isfile('./tor/data/bitcoin-p2p/hostname'):
|
|
EXTERNAL_IP="externalip=" + open('./tor/data/bitcoin-p2p/hostname').read()
|
|
|
|
if BITCOIN_NETWORK == "mainnet":
|
|
BITCOIN_RPC_PORT=8332
|
|
BITCOIN_P2P_PORT=8333
|
|
elif BITCOIN_NETWORK == "testnet":
|
|
BITCOIN_RPC_PORT=18332
|
|
BITCOIN_P2P_PORT=18333
|
|
elif BITCOIN_NETWORK == "signet":
|
|
BITCOIN_RPC_PORT=38332
|
|
BITCOIN_P2P_PORT=38333
|
|
BITCOIN_NODE="bitcoind"
|
|
elif BITCOIN_NETWORK == "regtest":
|
|
BITCOIN_RPC_PORT=18334
|
|
BITCOIN_P2P_PORT=18335
|
|
BITCOIN_NODE="bitcoind"
|
|
else:
|
|
exit(1)
|
|
|
|
NETWORK_SECTION=""
|
|
if BITCOIN_NETWORK != "mainnet":
|
|
NETWORK_SECTION = "[{}]".format(BITCOIN_NETWORK)
|
|
if BITCOIN_NETWORK == "testnet":
|
|
NETWORK_SECTION = "[test]"
|
|
|
|
# IP addresses for services
|
|
NETWORK_IP="10.21.21.0"
|
|
GATEWAY_IP="10.21.21.1"
|
|
DASHBOARD_IP="10.21.21.3"
|
|
MANAGER_IP="10.21.21.4"
|
|
#MIDDLEWARE_IP="10.21.21.5"
|
|
#NEUTRINO_SWITCHER_IP="10.21.21.6"
|
|
BITCOIN_IP="10.21.21.7"
|
|
#LND_IP="10.21.21.8"
|
|
TOR_PROXY_IP="10.21.21.9"
|
|
APPS_TOR_IP="10.21.21.10"
|
|
APPS_2_TOR_IP="10.21.21.11"
|
|
APPS_3_TOR_IP="10.21.21.12"
|
|
I2P_IP="10.21.21.13"
|
|
|
|
|
|
# IP6 addresses for services
|
|
NETWORK_IP6="fd00::21:0:0:0"
|
|
GATEWAY_IP6="fd00::21:0:0:1"
|
|
DASHBOARD_IP6="fd00::21:0:0:3"
|
|
MANAGER_IP6="fd00::21:0:0:4"
|
|
#MIDDLEWARE_IP6="fd00::21:0:0:5"
|
|
#NEUTRINO_SWITCHER_IP6="fd00::21:0:0:6"
|
|
BITCOIN_IP6="fd00::21:0:0:7"
|
|
#LND_IP6="fd00::21:0:0:8"
|
|
TOR_PROXY_IP6="fd00::21:0:0:9"
|
|
APPS_TOR_IP6="fd00::21:0:0:10"
|
|
APPS_2_TOR_IP6="fd00::21:0:0:11"
|
|
APPS_3_TOR_IP6="fd00::21:0:0:12"
|
|
I2P_IP6="fd00::21:0:0:13"
|
|
|
|
# Ports
|
|
BITCOIN_RPC_PORT="8332"
|
|
BITCOIN_P2P_PORT="8333"
|
|
BITCOIN_ZMQ_RAWBLOCK_PORT="28332"
|
|
BITCOIN_ZMQ_RAWTX_PORT="28333"
|
|
BITCOIN_ZMQ_HASHBLOCK_PORT="28334"
|
|
BITCOIN_ZMQ_SEQUENCE_PORT="28335"
|
|
TOR_PROXY_PORT="9050"
|
|
I2P_SAM_PORT="7656"
|
|
TOR_CONTROL_PORT="29051"
|
|
|
|
DEVICE_HOSTNAME=""
|
|
try:
|
|
DEVICE_HOSTNAME=subprocess.check_output("hostname").decode("utf-8").strip()
|
|
except:
|
|
# The content of /etc/hostname is the device's hostname
|
|
DEVICE_HOSTNAME=open("/etc/hostname").read().strip()
|
|
|
|
DOCKER_EXECUTABLE=subprocess.check_output(["which", "docker"]).decode("utf-8").strip()
|
|
# Get the real path by following symlinks
|
|
DOCKER_BINARY=subprocess.check_output(["readlink", "-f", DOCKER_EXECUTABLE]).decode("utf-8").strip()
|
|
|
|
# Checks if a variable with the name exists, if not, check if an env var with the name existts
|
|
# if neither exists, then exit with an error
|
|
def get_var(var_name, other_locals, file_name):
|
|
if var_name in locals():
|
|
return str(locals()[var_name])
|
|
elif var_name in other_locals:
|
|
return str(other_locals[var_name])
|
|
elif var_name in globals():
|
|
return str(globals()[var_name])
|
|
else:
|
|
print("Error: {} is not defined! (In file {})".format(var_name, file_name))
|
|
exit(1)
|
|
|
|
# Converts a string to uppercase, also replaces all - with _
|
|
def convert_to_upper(string):
|
|
return string.upper().replace('-', '_')
|
|
|
|
# Put variables in the config file. A config file accesses an env var $EXAMPLE_VARIABLE by containing <example-variable>
|
|
# in the config file. Check for such occurences and replace them with the actual variable
|
|
def replace_vars(file_path):
|
|
with open(file_path, 'r') as file:
|
|
file_contents = file.read()
|
|
return re.sub(r'<(.*?)>', lambda m: get_var(convert_to_upper(m.group(1)), locals(), file_path), file_contents)
|
|
|
|
def build_template(template_path, output_path):
|
|
data = replace_vars(template_path)
|
|
# If output path is a list, then it is a list of output paths
|
|
if isinstance(output_path, list):
|
|
for output_path_item in output_path:
|
|
# Delete the output path, no matter if it's a file or a directory
|
|
if os.path.isdir(output_path_item):
|
|
shutil.rmtree(output_path_item)
|
|
with open(output_path_item, 'w') as file:
|
|
file.write(data)
|
|
else:
|
|
# Delete the output path, no matter if it's a file or a directory
|
|
if os.path.isdir(output_path):
|
|
shutil.rmtree(output_path)
|
|
with open(output_path, 'w') as file:
|
|
file.write(data)
|
|
|
|
print("Generating configuration files...")
|
|
build_template("./templates/torrc-core-sample", "./tor/torrc-core")
|
|
build_template("./templates/bitcoin-sample.conf", "./bitcoin/bitcoin.conf")
|
|
build_template("./templates/i2p-sample.conf", "./i2p/i2pd.conf")
|
|
build_template("./templates/i2p-tunnels-sample.conf", "./i2p/tunnels.conf")
|
|
MIDDLEWARE_IP="NOT_YET_SET"
|
|
MIDDLEWARE_IP6="NOT_YET_SET"
|
|
build_template("./templates/.env-sample", "./.env")
|
|
|
|
print("Ensuring Docker Compose is up to date...")
|
|
download_docker_compose()
|
|
|
|
print("Updating core services...")
|
|
print()
|
|
with open("docker-compose.yml", 'r') as stream:
|
|
compose = yaml.safe_load(stream)
|
|
for service in ["manager", "dashboard"]:
|
|
compose["services"][service]["image"] = dependencies[service]
|
|
for service in ["tor", "app-tor", "app-2-tor", "app-3-tor"]:
|
|
compose["services"][service]["image"] = dependencies["tor"]
|
|
with open("docker-compose.yml", "w") as stream:
|
|
yaml.dump(compose, stream, sort_keys=False)
|
|
|
|
print("Configuring permissions...\n")
|
|
try:
|
|
os.system('chown -R 1000:1000 {}'.format(CITADEL_ROOT))
|
|
except: pass
|
|
|
|
if not reconfiguring:
|
|
print("Downloading apps...\n")
|
|
os.system('./scripts/app update')
|
|
else:
|
|
print("Generating app configuration...\n")
|
|
os.system('./scripts/app generate')
|
|
|
|
# Run ./scripts/app get-implementation lightning to get the implementation
|
|
# If it fails, install the LND app and set the implementation to LND
|
|
if reconfiguring:
|
|
try:
|
|
implementation = subprocess.check_output("./scripts/app get-implementation lightning", shell=True).decode("utf-8").strip()
|
|
except:
|
|
print("Installing LND...\n")
|
|
os.system('./scripts/app install lnd')
|
|
implementation = "lnd"
|
|
|
|
# Get APP_<IMPLEMENTATION>_MIDDLEWARE_IP from the .env file
|
|
if reconfiguring:
|
|
dotenv=parse_dotenv('./.env')
|
|
MIDDLEWARE_IP = dotenv["APP_{}_MIDDLEWARE_IP".format(implementation.upper().replace("-", "_"))]
|
|
MIDDLEWARE_IP6 = dotenv["APP_{}_MIDDLEWARE_IP6".format(implementation.upper().replace("-", "_"))]
|
|
else:
|
|
MIDDLEWARE_IP = "0.0.0.0"
|
|
MIDDLEWARE_IP6 = "::"
|
|
|
|
build_template("./templates/.env-sample", "./.env")
|
|
|
|
print("Updating app configuration...\n")
|
|
os.system('./scripts/app generate')
|
|
|
|
# Touch status_dir/configured
|
|
with open(status_dir+'/configured', 'w') as file:
|
|
file.write('')
|
|
|
|
print("Configuring permissions...\n")
|
|
try:
|
|
os.system('chown -R 1000:1000 {}'.format(CITADEL_ROOT))
|
|
except: pass
|
|
|
|
print("Configuration successful\n")
|
|
print("You can now start Citadel by running:")
|
|
print(" sudo ./scripts/start")
|