mirror of
https://github.com/runcitadel/core.git
synced 2024-11-14 18:00:40 +00:00
243 lines
10 KiB
Python
243 lines
10 KiB
Python
# SPDX-FileCopyrightText: 2021-2022 Citadel and contributors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import json
|
|
import os
|
|
import random
|
|
import traceback
|
|
from os import path
|
|
|
|
import yaml
|
|
from lib.citadelutils import parse_dotenv
|
|
from lib.entropy import deriveEntropy
|
|
|
|
appPorts = {}
|
|
appPortMap = {}
|
|
disabledApps = []
|
|
|
|
def assignIpV4(appId: str, containerName: str):
|
|
scriptDir = path.dirname(path.realpath(__file__))
|
|
nodeRoot = path.join(scriptDir, "..", "..")
|
|
networkingFile = path.join(nodeRoot, "apps", "networking.json")
|
|
envFile = path.join(nodeRoot, ".env")
|
|
cleanContainerName = containerName.strip()
|
|
# If the name still contains a newline, throw an error
|
|
if cleanContainerName.find("\n") != -1:
|
|
raise Exception("Newline in container name")
|
|
env_var = "APP_{}_{}_IP".format(
|
|
appId.upper().replace("-", "_"),
|
|
cleanContainerName.upper().replace("-", "_")
|
|
)
|
|
# Write a list of used IPs to the usedIpFile as JSON, and read that file to check if an IP
|
|
# can be used
|
|
usedIps = []
|
|
networkingData = {}
|
|
if path.isfile(networkingFile):
|
|
with open(networkingFile, 'r') as f:
|
|
networkingData = json.load(f)
|
|
|
|
if 'ip_addresses' in networkingData:
|
|
usedIps = list(networkingData['ip_addresses'].values())
|
|
else:
|
|
networkingData['ip_addresses'] = {}
|
|
# An IP 10.21.21.xx, with x being a random number above 40 is asigned to the container
|
|
# If the IP is already in use, it will be tried again until it's not in use
|
|
# If it's not in use, it will be added to the usedIps list and written to the usedIpFile
|
|
# If the usedIpsFile contains all IPs between 10.21.21.20 and 10.21.21.255 (inclusive),
|
|
# Throw an error, because no more IPs can be used
|
|
if len(usedIps) == 235:
|
|
raise Exception("No more IPs can be used")
|
|
|
|
if "{}-{}".format(appId, cleanContainerName) in networkingData['ip_addresses']:
|
|
ip = networkingData['ip_addresses']["{}-{}".format(
|
|
appId, cleanContainerName)]
|
|
else:
|
|
while True:
|
|
ip = "10.21.21." + str(random.randint(20, 255))
|
|
if ip not in usedIps:
|
|
networkingData['ip_addresses']["{}-{}".format(
|
|
appId, cleanContainerName)] = ip
|
|
break
|
|
|
|
dotEnv = parse_dotenv(envFile)
|
|
if env_var in dotEnv and str(dotEnv[env_var]) == str(ip):
|
|
return
|
|
|
|
with open(envFile, 'a') as f:
|
|
f.write("{}={}\n".format(env_var, ip))
|
|
with open(networkingFile, 'w') as f:
|
|
json.dump(networkingData, f)
|
|
|
|
def appPortsToMap():
|
|
for port in appPorts:
|
|
appId = appPorts[port]["app"]
|
|
containerId = appPorts[port]["container"]
|
|
realPort = appPorts[port]["port"]
|
|
if not appId in appPortMap:
|
|
appPortMap[appId] = {}
|
|
if not containerId in appPortMap[appId]:
|
|
appPortMap[appId][containerId] = []
|
|
appPortMap[appId][containerId].append({
|
|
"publicPort": port,
|
|
"internalPort": realPort,
|
|
"dynamic": appPorts[port]["dynamic"]
|
|
})
|
|
|
|
# For every app, parse the app.yml in ../apps/[name] and
|
|
# check their metadata, and return a list of all app's metadata
|
|
# Also check the path and defaultPassword and set them to an empty string if they don't exist
|
|
# In addition, set id on the metadata to the name of the app
|
|
# Return a list of all app's metadata
|
|
def getAppMetadata(apps, app_path, portCache):
|
|
virtual_apps = {}
|
|
appPorts = portCache
|
|
for app in apps:
|
|
app_yml_path = os.path.join(app_path, app, 'app.yml')
|
|
if os.path.isfile(app_yml_path):
|
|
try:
|
|
with open(app_yml_path, 'r') as f:
|
|
app_yml = yaml.safe_load(f.read())
|
|
version = False
|
|
if 'version' in app_yml:
|
|
version = int(app_yml['version'])
|
|
elif 'citadel_version' in app_yml:
|
|
version = int(app_yml['citadel_version'])
|
|
metadata: dict = app_yml['metadata']
|
|
if "implements" in metadata:
|
|
implements = metadata["implements"]
|
|
if implements not in virtual_apps:
|
|
virtual_apps[implements] = []
|
|
virtual_apps[implements].append(app)
|
|
if version == 3:
|
|
getPortsV3App(app_yml, app)
|
|
elif version == 4:
|
|
getPortsV4App(app_yml, app)
|
|
except Exception as e:
|
|
print(traceback.format_exc())
|
|
print("App {} is invalid!".format(app))
|
|
appPortsToMap()
|
|
return {
|
|
"virtual_apps": virtual_apps,
|
|
"ports": appPortMap,
|
|
"portCache": appPorts,
|
|
}
|
|
|
|
citadelPorts = [
|
|
# Dashboard
|
|
80,
|
|
# Sometimes used by nginx with some setups
|
|
433,
|
|
# Dashboard SSL
|
|
443,
|
|
# Bitcoin Core P2P
|
|
8333,
|
|
# LND gRPC
|
|
10009,
|
|
# LND REST
|
|
8080,
|
|
# Electrum Server
|
|
# 50001,
|
|
# Tor Proxy
|
|
9050,
|
|
]
|
|
|
|
lastPort = 3000
|
|
|
|
def getNewPort(usedPorts, appId, containerName, allowExisting):
|
|
lastPort2 = lastPort
|
|
while lastPort2 in usedPorts.keys() or lastPort2 in citadelPorts:
|
|
if allowExisting and lastPort2 in usedPorts.keys() and usedPorts[lastPort2]["app"] == appId and usedPorts[lastPort2]["container"] == containerName:
|
|
break
|
|
lastPort2 = lastPort2 + 1
|
|
return lastPort2
|
|
|
|
def validatePort(containerName, port, appId, priority: int, isDynamic = False, implements = ""):
|
|
if port not in appPorts and port not in citadelPorts and port != 0:
|
|
appPorts[port] = {
|
|
"app": appId,
|
|
"port": port,
|
|
"container": containerName,
|
|
"priority": priority,
|
|
"dynamic": isDynamic,
|
|
"implements": implements,
|
|
}
|
|
else:
|
|
if port in citadelPorts or appPorts[port]["app"] != appId or appPorts[port]["container"] != containerName:
|
|
if port in appPorts and priority > appPorts[port]["priority"]:
|
|
#print("Prioritizing app {} over {}".format(appId, appPorts[port]["app"]))
|
|
newPort = getNewPort(appPorts, appPorts[port]["app"], appPorts[port]["container"], False)
|
|
appPorts[newPort] = appPorts[port].copy()
|
|
appPorts[port] = {
|
|
"app": appId,
|
|
"port": port,
|
|
"container": containerName,
|
|
"priority": priority,
|
|
"dynamic": isDynamic,
|
|
"implements": implements,
|
|
}
|
|
else:
|
|
# Apps implement the same service and can't be installed together, so we an safely ignore a port conflict
|
|
if port in appPorts and implements != "" and implements == appPorts[port]["implements"]:
|
|
return
|
|
if priority == 2:
|
|
disabledApps.append(appId)
|
|
print("App {} disabled because of port conflict".format(appId))
|
|
else:
|
|
newPort = getNewPort(appPorts, appId, containerName, True)
|
|
internalPort = port
|
|
if isDynamic:
|
|
internalPort = newPort
|
|
#print("Port conflict! Moving app {}'s container {} to port {} (from {})".format(appId, containerName, newPort, port))
|
|
appPorts[newPort] = {
|
|
"app": appId,
|
|
"port": internalPort,
|
|
"container": containerName,
|
|
"priority": priority,
|
|
"dynamic": isDynamic,
|
|
"implements": implements,
|
|
}
|
|
|
|
def getPortsV3App(app, appId):
|
|
for appContainer in app["containers"]:
|
|
assignIpV4(appId, appContainer["name"])
|
|
if "port" in appContainer:
|
|
if "preferredOutsidePort" in appContainer and "requiresPort" in appContainer and appContainer["requiresPort"]:
|
|
validatePort(appContainer["name"], appContainer["preferredOutsidePort"], appId, 2)
|
|
elif "preferredOutsidePort" in appContainer:
|
|
|
|
validatePort(appContainer["name"], appContainer["preferredOutsidePort"], appId, 1)
|
|
else:
|
|
validatePort(appContainer["name"], appContainer["port"], appId, 0)
|
|
else:
|
|
# if the container does not define a port, assume 3000, and pass it to the container as env var
|
|
validatePort(appContainer["name"], 3000, appId, 0, True)
|
|
if "requiredPorts" in appContainer:
|
|
for port in appContainer["requiredPorts"]:
|
|
validatePort(appContainer["name"], port, appId, 2)
|
|
if "requiredUdpPorts" in appContainer:
|
|
for port in appContainer["requiredUdpPorts"]:
|
|
validatePort(appContainer["name"], port, appId, 2)
|
|
|
|
def getPortsV4App(app, appId):
|
|
implements = ""
|
|
if "implements" in app["metadata"]:
|
|
implements = app["metadata"]["implements"]
|
|
for appContainerName in app["services"].keys():
|
|
appContainer = app["services"][appContainerName]
|
|
if "enable_networking" in appContainer and not appContainer["enable_networking"]:
|
|
return
|
|
assignIpV4(appId, appContainerName)
|
|
if "port" in appContainer:
|
|
validatePort(appContainerName, appContainer["port"], appId, 0, False, implements)
|
|
else:
|
|
# if the container does not define a port, assume 3000, and pass it to the container as env var
|
|
validatePort(appContainerName, 3000, appId, 0, True, implements)
|
|
if "required_ports" in appContainer:
|
|
if "tcp" in appContainer["required_ports"] and appContainer["required_ports"]["tcp"] != None:
|
|
for port in appContainer["required_ports"]["tcp"].keys():
|
|
validatePort(appContainerName, port, appId, 2, False, implements)
|
|
if "udp" in appContainer["required_ports"] and appContainer["required_ports"]["udp"] != None:
|
|
for port in appContainer["required_ports"]["udp"].keys():
|
|
validatePort(appContainerName, port, appId, 2, False, implements)
|