mirror of
https://git.zavage.net/Zavage-Software/smileyface.git
synced 2025-05-09 18:59:19 -06:00
484 lines
17 KiB
Python
484 lines
17 KiB
Python
import collections
|
|
import configparser
|
|
import datetime
|
|
import glob
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
import configobj
|
|
|
|
from . import myutil, structs
|
|
from ._util import md5sum_file
|
|
from .gameconfig_edit import GameIniSpecial, UnrealIniFile
|
|
|
|
|
|
class UT4ServerMachine:
|
|
def __init__(self, ctx, datalayer):
|
|
self.ctx = ctx
|
|
self.datalayer = datalayer
|
|
|
|
if not self._validate_env_vars():
|
|
sys.exit(1)
|
|
|
|
def oneclickdeploy(self):
|
|
self.generate_instance()
|
|
self.upload_redirects()
|
|
self.upload_server()
|
|
|
|
def clean_instance(self, x):
|
|
"""
|
|
Deletes the generated instance on the local machine.
|
|
"""
|
|
self.ctx.log["ut4"].info("Clearing .pak folder...")
|
|
cmd = 'rm -rv "$PROJECT_DIR"/instance/LinuxServer/UnrealTournament/Content/Paks/*'
|
|
self._invoke_command(cmd)
|
|
|
|
def create_directories(self):
|
|
"""
|
|
Create required directories which the user installs maps, mutators, and config to.
|
|
"""
|
|
dirs = ("base", "files/config", "files/maps", "files/mutators", "files/rulesets", "files/unused")
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
if len(project_dir.strip()) == 0:
|
|
project_dir = "."
|
|
print("project_dir:", project_dir)
|
|
fullpaths = ["/".join([project_dir, d]) for d in dirs]
|
|
|
|
for fp in fullpaths:
|
|
cmd = "mkdir -p {}".format(fp)
|
|
self._invoke_command(cmd)
|
|
|
|
def download_linux_server(self, x):
|
|
"""
|
|
Download the latest Linux Unreal Tournament 4 Server from Epic
|
|
"""
|
|
self.ctx.log["ut4"].info("Downloading Linux Server Binary from Epic.")
|
|
|
|
def download_logs(self):
|
|
"""
|
|
Download the logs from the target hub.
|
|
"""
|
|
config_dir = self.ctx.config["app"]["config_dir"]
|
|
remote_game_host = self.ctx.config["app"]["remote_game_host"]
|
|
remote_game_dir = self.ctx.config["app"]["remote_game_dir"]
|
|
|
|
self.ctx.log["ut4"].info("Downloading instance logs from target hub.")
|
|
cmd = """
|
|
rsync -ravzp {remote_game_host}:{remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/ {config_dir}/downloaded-logs/
|
|
""".format(
|
|
**{"config_dir": config_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
|
|
)
|
|
self._invoke_command(cmd)
|
|
|
|
# Delete logs on remote game server if successfully transferred to local:
|
|
self.ctx.log["ut4"].info("")
|
|
cmd = """
|
|
ssh {remote_game_host} rm {remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/* -r'
|
|
""".format(
|
|
**{"config_dir": config_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
|
|
)
|
|
# self._invoke_command(cmd)
|
|
|
|
def generate_instance(self):
|
|
"""
|
|
Takes the current coniguration and outputs the application files which
|
|
can be copied to the server.
|
|
"""
|
|
self.ctx.log["ut4"].info("Generating server instance from custom files...")
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
|
|
# rsync
|
|
src = "/".join([project_dir, "base/LinuxServer"])
|
|
dst = "/".join([project_dir, "instance/"])
|
|
cmd = "rsync -ravzp {src} {dst}".format(**{"src": src, "dst": dst})
|
|
self._invoke_command(cmd)
|
|
|
|
# cp 1
|
|
src = "/".join([project_dir, "start-server.sh"])
|
|
dst = "/".join([project_dir, "instance/"])
|
|
cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
|
|
self._invoke_command(cmd)
|
|
|
|
# cp 2
|
|
src = "/".join([project_dir, "stop-server.sh"])
|
|
dst = "/".join([project_dir, "instance/"])
|
|
cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
|
|
self._invoke_command(cmd)
|
|
|
|
if self._needs_first_run():
|
|
self._first_run()
|
|
self._install_config()
|
|
self._install_paks()
|
|
self._install_redirect_lines()
|
|
self._install_rulesets()
|
|
|
|
def restart_server(self):
|
|
self.stop_server()
|
|
self.start_server()
|
|
|
|
def start_server(self):
|
|
"""
|
|
Flip on the target hub on for Fragging!
|
|
"""
|
|
self.ctx.log["ut4"].info("Starting hub...")
|
|
|
|
cmd = """
|
|
ssh {remote_game_host} {remote_game_dir}/start-server.sh
|
|
"""
|
|
self._invoke_command(cmd)
|
|
|
|
def stop_server(self):
|
|
"""
|
|
Stop UT4 Hub processes on the server.
|
|
"""
|
|
self.ctx.log["ut4"].info("Stopping hub.")
|
|
cmd = """
|
|
ssh {remote_game_host} {remote_game_dir}/stop-server.sh
|
|
"""
|
|
self._invoke_command(cmd)
|
|
|
|
def upload_redirects(self):
|
|
"""
|
|
Upload paks to redirect server.
|
|
"""
|
|
self.ctx.log["ut4"].info("Uploading redirects (maps, mutators, etc.) to target hub.")
|
|
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
# paks_dir = os.path.join(project_dir, 'instance/LinuxServer/UnrealTournament/Content/Paks/')
|
|
paks_dir = os.path.join(project_dir, "files/") # trailing slash required
|
|
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
|
|
cwd = project_dir
|
|
cmd = """
|
|
rsync -rivz \
|
|
--delete \
|
|
--exclude "*.md5" \
|
|
--exclude 'unused' \
|
|
--exclude ".KEEP" \
|
|
--exclude Mods.db \
|
|
{paks_dir} {remote_redirect_host}
|
|
|
|
""".format(
|
|
**{
|
|
"paks_dir": paks_dir,
|
|
"remote_redirect_host": remote_redirect_host,
|
|
}
|
|
)
|
|
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
|
|
self._invoke_command(cmd)
|
|
|
|
self._redirect_hide_passwords()
|
|
self._redirect_upload_script()
|
|
self._redirect_chown()
|
|
|
|
def _redirect_hide_passwords(self):
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
|
|
# (on the server):
|
|
gameini = "/srv/ut4-redirect.zavage.net/config/Game.ini"
|
|
engineini = "/srv/ut4-redirect.zavage.net/config/Engine.ini"
|
|
|
|
cmd = """
|
|
ssh mathewguest.com \
|
|
sed -i /ServerInstanceID=/c\ServerInstanceID=Hidden {gameini}
|
|
""".format(
|
|
gameini=gameini
|
|
)
|
|
self._invoke_command(cmd)
|
|
|
|
cmd = """
|
|
ssh mathewguest.com \
|
|
sed -i /RconPassword=/c\RconPassword=Hidden {engineini}
|
|
""".format(
|
|
engineini=engineini
|
|
)
|
|
self._invoke_command(cmd)
|
|
|
|
def _redirect_upload_script(self):
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
|
|
cmd = """
|
|
rsync -vz \
|
|
{project_dir}/ut4-server-ctl.sh \
|
|
{remote_redirect_host}
|
|
""".format(
|
|
**{
|
|
"project_dir": project_dir,
|
|
"remote_redirect_host": remote_redirect_host,
|
|
}
|
|
)
|
|
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
|
|
self._invoke_command(cmd)
|
|
|
|
def _redirect_chown(self):
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
|
|
|
|
cmd = """
|
|
ssh mathewguest.com \
|
|
chown http:http /srv/ut4-redirect.zavage.net -R
|
|
"""
|
|
self._invoke_command(cmd)
|
|
|
|
def upload_server(self):
|
|
"""
|
|
Upload all required game files to the hub server.
|
|
"""
|
|
self.ctx.log["ut4"].info("Uploading customized server")
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
remote_game_host = self.ctx.config["app"]["remote_game_host"]
|
|
remote_game_dir = self.ctx.config["app"]["remote_game_dir"]
|
|
cwd = None
|
|
|
|
# transfer #1
|
|
cmd = """
|
|
rsync -raivzp \
|
|
--delete \
|
|
--exclude ".KEEP" \
|
|
--exclude "Mods.db" \
|
|
--exclude "Mod.ini" \
|
|
--exclude "Logs" \
|
|
--exclude "ut4-server.log" \
|
|
--exclude "Saved/*.ai" \
|
|
--exclude "Saved/Crashes/*" \
|
|
--exclude "Saved/Logs/*" \
|
|
{project_dir}/instance/ \
|
|
{remote_game_host}:{remote_game_dir}
|
|
""".format(
|
|
**{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
|
|
)
|
|
cmd = cmd.replace(" ", "")
|
|
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
|
|
|
|
self._invoke_command(cmd)
|
|
|
|
# transfer #2
|
|
cmd = """
|
|
rsync -avzp \
|
|
{project_dir}/ut4-server-ctl.sh \
|
|
{remote_game_host}:{remote_game_dir}
|
|
""".format(
|
|
**{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
|
|
)
|
|
# subprocess.run(cmd, cwd=cwd)
|
|
self._invoke_command(cmd)
|
|
|
|
# transfer #3
|
|
cmd = """
|
|
scp \
|
|
{project_dir}/instance/ut4-server.service \
|
|
{remote_game_host}:/etc/systemd/system/
|
|
""".format(
|
|
**{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
|
|
)
|
|
# subprocess.run(cmd, cwd=cwd)
|
|
self._invoke_command(cmd)
|
|
|
|
# transfer #4
|
|
cmd = """
|
|
ssh {remote_game_host} \
|
|
chown ut4:ut4 {remote_game_dir} -R
|
|
""".format(
|
|
**{"remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
|
|
)
|
|
# subprocess.run(cmd, cwd=cwd)
|
|
self._invoke_command(cmd)
|
|
|
|
# Fix +x permissions on bash scripts
|
|
cmd = """
|
|
ssh {remote_game_host} \
|
|
chmod +x \
|
|
{remote_game_dir}/start-server.sh \
|
|
{remote_game_dir}/stop-server.sh
|
|
""".format(
|
|
**{"remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
|
|
)
|
|
# subprocess.run(cmd, cwd=cwd)
|
|
self._invoke_command(cmd)
|
|
|
|
def _first_run(self):
|
|
self.ctx.log["ut4"].info("Starting instance once to get UID.")
|
|
self.ctx.log["ut4"].info("Unfortunately, this takes 20 seconds. Just wait.")
|
|
|
|
# Make binary executable:
|
|
bin_name = "UE4Server-Linux-Shipping"
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
|
|
cwd = "{project_dir}/instance/LinuxServer/Engine/Binaries/Linux".format(project_dir=project_dir)
|
|
target_file = "{cwd}/{bin_name}".format(cwd=cwd, bin_name=bin_name)
|
|
cmd = ["chmod", "770", target_file]
|
|
p = subprocess.run(cmd)
|
|
|
|
cmd = ["./" + bin_name, "UnrealTournament", "UT-Entry?Game=Lobby", "-log"]
|
|
p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE)
|
|
|
|
try:
|
|
stdout, stsderr = p.communicate(timeout=20)
|
|
except subprocess.TimeoutExpired:
|
|
p.kill()
|
|
stdout, stderr = p.communicate()
|
|
|
|
self.ctx.log["ut4"].info("sleeping 20 seconds and then we'll kill the server we started just now.")
|
|
# # TODO(MG) get uid and export
|
|
|
|
def _install_config(self):
|
|
files = ("Game.ini", "Engine.ini")
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
config_dir = self.ctx.config["app"]["config_dir"]
|
|
for fn in files:
|
|
self.ctx.log["ut4"].info("Installing file: %s", fn)
|
|
src = os.path.join(config_dir, fn)
|
|
dst = os.path.join(project_dir, "instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer", fn)
|
|
cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
|
|
self._invoke_command(cmd)
|
|
|
|
# Monkey-patch Game.ini to ensure it has a place for RedirectReferences
|
|
if fn == "Game.ini":
|
|
ini = UnrealIniFile(dst)
|
|
sect_name = "/Script/UnrealTournament.UTBaseGameMode"
|
|
opt_name = "RedirectReferences"
|
|
if not ini._config.has_section(sect_name):
|
|
ini._config.add_section(sect_name)
|
|
if not ini._config.has_option(sect_name, opt_name):
|
|
ini._config.set(sect_name, opt_name, ":PARAM:")
|
|
with open(dst, "w") as fp:
|
|
ini._config.write(fp)
|
|
|
|
def _install_paks(self):
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
|
|
self.ctx.log["ut4"].info("Installing maps...")
|
|
cmd = "rsync -ravzp {src} {dst}".format(
|
|
**{
|
|
"src": "/".join([project_dir, "files/maps/"]),
|
|
"dst": "/".join([project_dir, "instance/LinuxServer/UnrealTournament/Content/Paks/"]),
|
|
}
|
|
)
|
|
self._invoke_command(cmd)
|
|
|
|
self.ctx.log["ut4"].info("Installing mutators...")
|
|
cmd = "rsync -ravzp {src} {dst}".format(
|
|
**{
|
|
"src": "/".join([project_dir, "files/mutators/"]),
|
|
"dst": "/".join([project_dir, "instance/LinuxServer/UnrealTournament/Content/Paks/"]),
|
|
}
|
|
)
|
|
self._invoke_command(cmd)
|
|
|
|
def _install_redirect_lines(self):
|
|
self.ctx.log["ut4"].info("Generating redirect references...")
|
|
|
|
redirect_protocol = self.ctx.config["app"]["redirect_protocol"]
|
|
redirect_url = self.ctx.config["app"]["redirect_url"]
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
mod_dir = "/".join([project_dir, "files"])
|
|
|
|
game_ini_filepath = "/".join(
|
|
[project_dir, "instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer/Game.ini"]
|
|
)
|
|
game_ini = GameIniSpecial(game_ini_filepath)
|
|
game_ini.clear_redirect_references()
|
|
|
|
files = glob.glob("{}/**/*.pak".format(mod_dir))
|
|
redirect_lines = []
|
|
for idx, filename in enumerate(files):
|
|
# if idx > 5:
|
|
# break
|
|
md5sum = md5sum_file(filename)
|
|
p = pathlib.Path(filename)
|
|
relative_path = p.relative_to(mod_dir)
|
|
pkg_basename = p.name
|
|
|
|
# TODO(MG) Handle filenames w/o extension
|
|
try:
|
|
split = os.path.splitext(pkg_basename)
|
|
pkg_basename = split[0]
|
|
except Exception as ex:
|
|
print(ex)
|
|
continue
|
|
|
|
line = game_ini.add_redirect_reference(
|
|
**{
|
|
"pkg_basename": pkg_basename,
|
|
"redirect_protocol": redirect_protocol,
|
|
"redirect_url": redirect_url,
|
|
"relative_path": relative_path,
|
|
"md5sum": md5sum,
|
|
}
|
|
)
|
|
|
|
self.ctx.log["ut4"].debug("redirect line = '%s'", line)
|
|
|
|
data = game_ini.write(sys.stdout)
|
|
|
|
def _install_rulesets(self):
|
|
self.ctx.log["ut4"].info("Concatenating rulesets for game modes...")
|
|
project_dir = self.ctx.config["app"]["project_dir"]
|
|
|
|
src_dir = "/".join([project_dir, "files/rulesets"])
|
|
out_dir = "/".join([project_dir, "/instance/LinuxServer/UnrealTournament/Saved/Config/Rulesets"])
|
|
out_filename = "/".join([out_dir, "ruleset.json"])
|
|
|
|
cmd = "mkdir -pv {out_dir}".format(**{"out_dir": out_dir})
|
|
self._invoke_command(cmd)
|
|
|
|
self.ctx.log["ut4"].info("out filename=%s", out_filename)
|
|
|
|
# echo {\"rules\":[ > "$OUT_FILENAME"
|
|
cmd = 'echo \'{{"rules":[\' > "{out_filename}"'.format(out_filename=out_filename)
|
|
self._invoke_command(cmd)
|
|
|
|
cmd = 'for f in "{src_dir}"/*.json ; do cat "$f" >> "{out_filename}" ; done'.format(
|
|
**{"src_dir": src_dir, "out_filename": out_filename}
|
|
)
|
|
self._invoke_command(cmd)
|
|
|
|
cmd = 'echo "]}}" >> "{out_filename}"'.format(**{"out_filename": out_filename})
|
|
self._invoke_command(cmd)
|
|
self.ctx.log["ut4"].info('output ruleset is at "%s"', out_filename)
|
|
|
|
def _invoke_command(self, cmd, msg=None):
|
|
assert isinstance(cmd, str), "cmd input must be string: %s".format(cmd)
|
|
if msg is None:
|
|
msg = cmd
|
|
print(msg)
|
|
self.ctx.log["ut4"].info("running cmd: %s", cmd)
|
|
cwd = None # todo(mg) ?
|
|
os.system(cmd)
|
|
|
|
def _needs_first_run(self):
|
|
return False # TODO(MG): Hard-coded
|
|
|
|
def _validate_env_vars(self):
|
|
variable_names = (
|
|
"project_dir",
|
|
"download_url",
|
|
"download_filename",
|
|
"download_md5",
|
|
"redirect_protocol",
|
|
"redirect_url",
|
|
"remote_game_host",
|
|
"remote_game_dir",
|
|
"remote_redirect_host",
|
|
)
|
|
for name in variable_names:
|
|
value = self.ctx.config["app"][name]
|
|
self.ctx.log["ut4"].info("%s: %s", name, value)
|
|
|
|
i = input("Continue with above configuration? (y/N):")
|
|
if i.lower() != "y":
|
|
self.ctx.log["ut4"].info("Doing nothing.")
|
|
return False
|
|
self.ctx.log["ut4"].info("Continuing.")
|
|
return True
|
|
|
|
|
|
class MultiOrderedDict(collections.OrderedDict):
|
|
def __setitem__(self, key, value):
|
|
if isinstance(value, list) and key in self:
|
|
self[key].extend(value)
|
|
else:
|
|
super().__setitem__(key, value)
|