From 721c9cfe1205f97cfeba1716216c3b888e8ec1ee Mon Sep 17 00:00:00 2001 From: luukas Date: Thu, 5 May 2022 14:02:23 +0300 Subject: [PATCH] Add the server creation endpoint --- app/classes/controllers/servers_controller.py | 5 +- app/classes/models/servers.py | 5 +- app/classes/shared/helpers.py | 4 +- app/classes/shared/main_controller.py | 174 +++++++++++++++++- app/classes/web/routes/api/servers/index.py | 102 ++++++++-- 5 files changed, 267 insertions(+), 23 deletions(-) diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index c3a7a889..41a13c3e 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -35,6 +35,7 @@ class ServersController: server_stop: str, server_type: str, server_port: int = 25565, + server_host: str = "127.0.0.1", ) -> int: """Create a server in the database @@ -48,7 +49,8 @@ class ServersController: server_log_file: The path to the server log file server_stop: This is the command to stop the server server_type: This is the type of server you're creating. - server_port: The port the server will run on, defaults to 25565 (optional) + server_port: The port the server will be monitored on, defaults to 25565 + server_host: The host the server will be monitored on, defaults to 127.0.0.1 Returns: int: The new server's id @@ -67,6 +69,7 @@ class ServersController: server_stop, server_type, server_port, + server_host, ) @staticmethod diff --git a/app/classes/models/servers.py b/app/classes/models/servers.py index b49d8de8..41341c7a 100644 --- a/app/classes/models/servers.py +++ b/app/classes/models/servers.py @@ -95,6 +95,7 @@ class HelperServers: server_stop: str, server_type: str, server_port: int = 25565, + server_host: str = "127.0.0.1", ) -> int: """Create a server in the database @@ -108,7 +109,8 @@ class HelperServers: server_log_file: The path to the server log file server_stop: This is the command to stop the server server_type: This is the type of server you're creating. - server_port: The port the server will run on, defaults to 25565 (optional) + server_port: The port the server will be monitored on, defaults to 25565 + server_host: The host the server will be monitored on, defaults to 127.0.0.1 Returns: int: The new server's id @@ -128,6 +130,7 @@ class HelperServers: Servers.crash_detection: False, Servers.log_path: server_log_file, Servers.server_port: server_port, + Servers.server_ip: server_host, Servers.stop_command: server_stop, Servers.backup_path: backup_path, Servers.type: server_type, diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 0f674e64..76f8e76c 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -72,7 +72,7 @@ class Helpers: installer.do_install() @staticmethod - def float_to_string(gbs: int): + def float_to_string(gbs: float): s = str(float(gbs) * 1000).rstrip("0").rstrip(".") return s @@ -232,7 +232,7 @@ class Helpers: return default_return with open(self.settings_file, "w", encoding="utf-8") as f: - json.dump(data, f, indent=1) + json.dump(data, f, indent=2) except Exception as e: logger.critical( diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index efcfabf5..7c94cae1 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -337,6 +337,177 @@ class Controller: svr_obj = self.get_server_obj(server_id) svr_obj.stop_threaded_server() + def create_api_server(self, data: dict): + server_fs_uuid = Helpers.create_uuid() + new_server_path = os.path.join(self.helper.servers_dir, server_fs_uuid) + backup_path = os.path.join(self.helper.backup_path, server_fs_uuid) + + if Helpers.is_os_windows(): + new_server_path = Helpers.wtol_path(new_server_path) + backup_path = Helpers.wtol_path(backup_path) + new_server_path.replace(" ", "^ ") + backup_path.replace(" ", "^ ") + + Helpers.ensure_dir_exists(new_server_path) + Helpers.ensure_dir_exists(backup_path) + + def _copy_import_dir_files(existing_server_path): + existing_server_path = Helpers.get_os_understandable_path( + existing_server_path + ) + try: + FileHelpers.copy_dir(existing_server_path, new_server_path, True) + except shutil.Error as ex: + logger.error(f"Server import failed with error: {ex}") + + def _create_server_properties_if_needed(port, empty=False): + properties_file = os.path.join(new_server_path, "server.properties") + has_properties = os.path.exists(properties_file) + + if not has_properties: + logger.info( + f"No server.properties found on import." + f"Creating one with port selection of {port}" + ) + with open( + properties_file, + "w", + encoding="utf-8", + ) as file: + file.write( + "# generated by Crafty Controller" + + ("" if empty else f"\nserver-port={port}") + ) + + root_create_data = data[data["create_type"] + "_create_data"] + create_data = root_create_data[root_create_data["create_type"] + "_create_data"] + if data["create_type"] == "minecraft_java": + if root_create_data["create_type"] == "download_jar": + server_file = f"{create_data['type']}-{create_data['version']}.jar" + full_jar_path = os.path.join(new_server_path, server_file) + + # Create an EULA file + with open( + os.path.join(new_server_path, "eula.txt"), "w", encoding="utf-8" + ) as file: + file.write( + "eula=" + ("true" if create_data["agree_to_eula"] else "false") + ) + elif root_create_data["create_type"] == "import_server": + _copy_import_dir_files(create_data["existing_server_path"]) + full_jar_path = os.path.join(new_server_path, create_data["jarfile"]) + elif root_create_data["create_type"] == "import_zip": + # TODO: Copy files from the zip file to the new server directory + full_jar_path = os.path.join(new_server_path, create_data["jarfile"]) + raise Exception("Not yet implemented") + _create_server_properties_if_needed(create_data["server_properties_port"]) + + min_mem = create_data["mem_min"] + max_mem = create_data["mem_max"] + + def _gibs_to_mibs(gibs: float) -> str: + return str(int(gibs * 1024)) + + def _wrap_jar_if_windows(): + return ( + f'"{full_jar_path}"' if Helpers.is_os_windows() else full_jar_path + ) + + server_command = ( + f"java -Xms{_gibs_to_mibs(min_mem)}M " + f"-Xmx{_gibs_to_mibs(max_mem)}M " + f"-jar {_wrap_jar_if_windows()} nogui" + ) + elif data["create_type"] == "minecraft_bedrock": + if root_create_data["create_type"] == "import_server": + existing_server_path = Helpers.get_os_understandable_path( + create_data["existing_server_path"] + ) + try: + FileHelpers.copy_dir(existing_server_path, new_server_path, True) + except shutil.Error as ex: + logger.error(f"Server import failed with error: {ex}") + elif root_create_data["create_type"] == "import_zip": + # TODO: Copy files from the zip file to the new server directory + raise Exception("Not yet implemented") + + _create_server_properties_if_needed(0, True) + + server_command = create_data["command"] + server_file = "" + elif data["create_type"] == "custom": + # TODO: working_directory, executable_update + if root_create_data["create_type"] == "raw_exec": + pass + elif root_create_data["create_type"] == "import_server": + existing_server_path = Helpers.get_os_understandable_path( + create_data["existing_server_path"] + ) + try: + FileHelpers.copy_dir(existing_server_path, new_server_path, True) + except shutil.Error as ex: + logger.error(f"Server import failed with error: {ex}") + elif root_create_data["create_type"] == "import_zip": + # TODO: Copy files from the zip file to the new server directory + raise Exception("Not yet implemented") + + _create_server_properties_if_needed(0, True) + + server_command = create_data["command"] + server_file = root_create_data["executable_update"].get("file", "") + + stop_command = data.get("stop_command", "") + if stop_command == "": + # TODO: different default stop commands for server creation types + stop_command = "stop" + + log_location = data.get("log_location", "") + if log_location == "": + # TODO: different default log locations for server creation types + log_location = "/logs/latest.log" + + if data["monitoring_type"] == "minecraft_java": + monitoring_port = data["minecraft_java_monitoring_data"]["port"] + monitoring_host = data["minecraft_java_monitoring_data"]["host"] + monitoring_type = "minecraft-java" + elif data["monitoring_type"] == "minecraft_bedrock": + monitoring_port = data["minecraft_bedrock_monitoring_data"]["port"] + monitoring_host = data["minecraft_bedrock_monitoring_data"]["host"] + monitoring_type = "minecraft-bedrock" + elif data["monitoring_type"] == "none": + # TODO: this needs to be NUKED.. + # There shouldn't be anything set if there are nothing to monitor + monitoring_port = 25565 + monitoring_host = "127.0.0.1" + monitoring_type = "minecraft-java" + + new_server_id = self.register_server( + name=data["name"], + server_uuid=server_fs_uuid, + server_dir=new_server_path, + backup_path=backup_path, + server_command=server_command, + server_file=server_file, + server_log_file=log_location, + server_stop=stop_command, + server_port=monitoring_port, + server_host=monitoring_host, + server_type=monitoring_type, + ) + + if ( + data["create_type"] == "minecraft_java" + and root_create_data["create_type"] == "download_jar" + ): + self.server_jars.download_jar( + create_data["type"], + create_data["version"], + full_jar_path, + new_server_id, + ) + + return new_server_id, server_fs_uuid + def create_jar_server( self, server: str, @@ -759,6 +930,7 @@ class Controller: server_stop: str, server_port: int, server_type: str, + server_host: str = "127.0.0.1", ): # put data in the db new_id = self.servers.create_server( @@ -772,6 +944,7 @@ class Controller: server_stop, server_type, server_port, + server_host, ) if not Helpers.check_file_exists( @@ -788,7 +961,6 @@ class Controller: "The server is managed by Crafty Controller.\n " "Leave this directory/files alone please" ) - file.close() except Exception as e: logger.error(f"Unable to create required server files due to :{e}") diff --git a/app/classes/web/routes/api/servers/index.py b/app/classes/web/routes/api/servers/index.py index 8023bd5c..6604dd59 100644 --- a/app/classes/web/routes/api/servers/index.py +++ b/app/classes/web/routes/api/servers/index.py @@ -30,6 +30,7 @@ new_server_schema = { "version": "1.18.2", "mem_min": 1, "mem_max": 2, + "server_properties_port": 25565, }, }, } @@ -90,7 +91,6 @@ new_server_schema = { "type": "string", "default": "127.0.0.1", "examples": ["127.0.0.1"], - "pattern": "^.*$", }, "port": { "title": "Port", @@ -142,7 +142,14 @@ new_server_schema = { "download_jar_create_data": { "title": "JAR download data", "type": "object", - "required": ["type", "version", "mem_min", "mem_max"], + "required": [ + "type", + "version", + "mem_min", + "mem_max", + "server_properties_port", + "agree_to_eula", + ], "properties": { "type": { "title": "Server JAR Type", @@ -155,19 +162,31 @@ new_server_schema = { "examples": ["1.18.2"], }, "mem_min": { - "title": "Minimum JVM memory", + "title": "Minimum JVM memory (in GiBs)", "type": "number", "examples": [1], "default": 1, "exclusiveMinimum": 0, }, "mem_max": { - "title": "Maximum JVM memory", + "title": "Maximum JVM memory (in GiBs)", "type": "number", "examples": [2], "default": 2, "exclusiveMinimum": 0, }, + "server_properties_port": { + "title": "Port", + "type": "integer", + "examples": [25565], + "default": 25565, + "minimum": 0, + }, + "agree_to_eula": { + "title": "Agree to the EULA", + "type": "boolean", + "default": False, + }, }, }, "import_server_create_data": { @@ -178,6 +197,8 @@ new_server_schema = { "jarfile", "mem_min", "mem_max", + "server_properties_port", + "agree_to_eula", ], "properties": { "existing_server_path": { @@ -190,22 +211,34 @@ new_server_schema = { "title": "JAR file", "description": "The JAR file relative to the previous path", "type": "string", - "examples": ["paper.jar"], + "examples": ["paper.jar", "jars/vanilla-1.12.jar"], }, "mem_min": { - "title": "Minimum JVM memory", + "title": "Minimum JVM memory (in GiBs)", "type": "number", "examples": [1], "default": 1, "exclusiveMinimum": 0, }, "mem_max": { - "title": "Maximum JVM memory", + "title": "Maximum JVM memory (in GiBs)", "type": "number", "examples": [2], "default": 2, "exclusiveMinimum": 0, }, + "server_properties_port": { + "title": "Port", + "type": "integer", + "examples": [25565], + "default": 25565, + "minimum": 0, + }, + "agree_to_eula": { + "title": "Agree to the EULA", + "type": "boolean", + "default": False, + }, }, }, "import_zip_create_data": { @@ -217,6 +250,8 @@ new_server_schema = { "jarfile", "mem_min", "mem_max", + "server_properties_port", + "agree_to_eula", ], "properties": { "zip_path": { @@ -229,7 +264,7 @@ new_server_schema = { "title": "Server root directory", "description": "The server root in the ZIP archive", "type": "string", - "examples": ["/", "/paper-server/"], + "examples": ["/", "/paper-server/", "server-1"], }, "jarfile": { "title": "JAR file", @@ -238,19 +273,31 @@ new_server_schema = { "examples": ["paper.jar", "jars/vanilla-1.12.jar"], }, "mem_min": { - "title": "Minimum JVM memory", + "title": "Minimum JVM memory (in GiBs)", "type": "number", "examples": [1], "default": 1, "exclusiveMinimum": 0, }, "mem_max": { - "title": "Maximum JVM memory", + "title": "Maximum JVM memory (in GiBs)", "type": "number", "examples": [2], "default": 2, "exclusiveMinimum": 0, }, + "server_properties_port": { + "title": "Port", + "type": "integer", + "examples": [25565], + "default": 25565, + "minimum": 0, + }, + "agree_to_eula": { + "title": "Agree to the EULA", + "type": "boolean", + "default": False, + }, }, }, }, @@ -333,7 +380,7 @@ new_server_schema = { "title": "Server root directory", "description": "The server root in the ZIP archive", "type": "string", - "examples": ["/", "/paper-server/"], + "examples": ["/", "/paper-server/", "server-1"], }, "command": { "title": "Command", @@ -382,6 +429,7 @@ new_server_schema = { "properties": { "working_directory": { "title": "Working directory", + "description": '"" means the default', "type": "string", "default": "", "examples": ["/mnt/mydrive/server-configs/", "./subdirectory", ""], @@ -390,6 +438,7 @@ new_server_schema = { "title": "Executable Updation", "description": "Also configurable later on and for other servers", "type": "object", + "required": ["enabled", "file", "url"], "properties": { "enabled": { "title": "Enabled", @@ -413,7 +462,7 @@ new_server_schema = { "title": "Creation type", "type": "string", "default": "raw_exec", - "enum": ["raw_exec", "import_exec", "import_zip"], + "enum": ["raw_exec", "import_server", "import_zip"], }, "raw_exec_create_data": { "title": "Raw execution command create data", @@ -462,7 +511,7 @@ new_server_schema = { "title": "Server root directory", "description": "The server root in the ZIP archive", "type": "string", - "examples": ["/", "/paper-server/"], + "examples": ["/", "/paper-server/", "server-1"], }, "command": { "title": "Command", @@ -485,7 +534,9 @@ new_server_schema = { }, { "if": { - "properties": {"create_type": {"const": "import_exec"}} + "properties": { + "create_type": {"const": "import_server"} + } }, "then": {"required": ["import_server_create_data"]}, }, @@ -611,12 +662,21 @@ class ApiServersIndexHandler(BaseApiHandler): }, ) - # TODO: implement everything + new_server_id, new_server_uuid = self.controller.create_api_server(data) + + # Increase the server creation counter + self.controller.crafty_perms.add_server_creation(user["user_id"]) + + self.controller.stats.record_stats() self.controller.management.add_to_audit_log( user["user_id"], - f"Created server {'1234'} (ID:{123})", - server_id=0, + ( + f"created server {data['name']}" + f" (ID: {new_server_id})" + f" (UUID: {new_server_uuid})" + ), + server_id=new_server_id, source_ip=self.get_remote_ip(), ) @@ -624,5 +684,11 @@ class ApiServersIndexHandler(BaseApiHandler): self.finish_json( 201, - {"status": "ok", "data": {"server_id": ""}}, + { + "status": "ok", + "data": { + "new_server_id": str(new_server_id), + "new_server_uuid": new_server_uuid, + }, + }, )