def test_should_be_able_to_create_a_new_instance_folder( self, reset_folder_structure): """Our test server instance should be able to create a new instance folder.""" server_name = "TestServer1" settings = self.settings.copy() settings.server_instance_name = server_name instance = ServerInstance(settings) instance._new_server_folder() assert isdir(join(self.test_path, "__server__" + server_name))
def setup(self, request, class_reset_folder_structure, c_sc_stub): """TestCompileBat setup""" request.cls.test_path = test_folder_structure_path() server_instance_name = "training" sb = ServerBatSettings( server_title="ODK Training Server", server_port="2202", server_config_file_name="serverTraining.cfg", server_cfg_file_name="Arma3Training.cfg", server_max_mem="8192", server_flags="-filePatching -autoinit -enableHT") sc = c_sc_stub request.cls.settings = ServerInstanceSettings( server_instance_name=server_instance_name, bat_settings=sb, config_settings=sc, arma_folder=self.test_path, server_instance_root=self.test_path, mods_to_be_copied=["CBA_A3"], user_mods_list=["ace", "CBA_A3", "ODKAI"], server_mods_list=["AdvProp", "ODKMIN"], ) request.cls.instance = ServerInstance(self.settings) self.instance._new_server_folder() self.instance._compile_bat_file() request.cls.compiled_bat = join( self.instance.get_server_instance_path(), "run_server.bat")
def hook_init_copy_replace(self, server_instance: ServerInstance, call_data: List[str]) -> None: """Put the mod in copied_mods folder and symlink everything from the original mod, then simulate a regular folder structure.""" mod_name = call_data[2] target_mod_folder = join(server_instance.get_server_instance_path(), server_instance.S.copied_mod_folder_name, "@{}".format(mod_name)) self._do_op(mod_name, server_instance.S.arma_folder, target_mod_folder)
def setup(self, request, c_sc_stub, c_sb_stub, class_reset_folder_structure): """TestAModFixOdkailocal setup""" # prepare the local folder request.cls.test_path = test_folder_structure_path() local_folder = join(self.test_path, "local_odkai") mkdir(local_folder) touch(join(local_folder, "local_mod"), "this is a local mod") fix_settings = ModFixSettings( enabled_fixes=["odkai_local"], mod_fix_settings={"odkai_local_path": local_folder}) settings = ServerInstanceSettings("test", c_sb_stub, c_sc_stub, user_mods_list=["ODKAI"], arma_folder=self.test_path, server_instance_root=self.test_path, fix_settings=fix_settings) request.cls.instance = ServerInstance(settings) self.instance._new_server_folder() self.instance._prepare_server_core() self.instance._start_op_on_mods("init", ["ODKAI"]) request.cls.mod_folder = join(self.instance.get_server_instance_path(), self.instance.S.linked_mod_folder_name, "@ODKAI")
def setup(self, request, class_reset_folder_structure, c_sc_stub, c_sb_stub): """TestModFixCba setup""" request.cls.test_path = test_folder_structure_path() request.cls.custom_cba = join(self.test_path, "cba_settings.sqf") touch(self.custom_cba, "test") fix_settings = ModFixSettings( enabled_fixes=["cba_a3"], mod_fix_settings={"cba_settings": self.custom_cba}) settings = ServerInstanceSettings("test", c_sb_stub, c_sc_stub, user_mods_list=["CBA_A3"], arma_folder=self.test_path, server_instance_root=self.test_path, fix_settings=fix_settings) request.cls.instance = ServerInstance(settings) self.instance._new_server_folder() self.instance._prepare_server_core() request.cls.mod_folder = join(self.instance.get_server_instance_path(), self.instance.S.linked_mod_folder_name, "@CBA_A3") request.cls.cba_settings = join( self.instance.get_server_instance_path(), "userconfig", "cba_settings.sqf")
def _link_local_copy(self, server_instance: ServerInstance) -> None: """Actually link the local copy.""" mod_folder = join(server_instance.get_server_instance_path(), server_instance.S.linked_mod_folder_name, "@" + self.name) mod_fix_settings = server_instance.S.fix_settings.mod_fix_settings local_folder = abspath(mod_fix_settings["odkai_local_path"]) symlink(local_folder, mod_folder)
def hook_update_link_replace(self, server_instance: ServerInstance, call_data: List[str]) -> None: """Make sure the local copy is symlinked.""" mod_folder = join(server_instance.get_server_instance_path(), server_instance.S.linked_mod_folder_name, "@" + self.name) if islink(mod_folder): unlink(mod_folder) self._link_local_copy(server_instance)
def setup(self, request, class_reset_folder_structure, c_sc_stub): """TestServerInstanceInit setup""" server_name = "TestServer1" request.cls.test_path = test_folder_structure_path() sb = ServerBatSettings( server_title="ODK Training Server", server_port="2202", server_config_file_name="serverTraining.cfg", server_cfg_file_name="Arma3Training.cfg", server_max_mem="8192", server_flags="-filePatching -autoinit -enableHT") sc = c_sc_stub request.cls.settings = ServerInstanceSettings( server_instance_name=server_name, bat_settings=sb, config_settings=sc, arma_folder=self.test_path, server_instance_root=self.test_path, mods_to_be_copied=["CBA_A3"], user_mods_list=["ace", "CBA_A3", "ODKAI"], server_mods_list=["AdvProp", "ODKMIN"], ) request.cls.instance = ServerInstance(self.settings) request.cls.instance_folder = self.instance.get_server_instance_path() # set up all needed spies with spy(self.instance._new_server_folder) as request.cls.new_server_fun, \ spy(self.instance._check_mods) as request.cls.check_mods_fun, \ spy(self.instance._prepare_server_core) as request.cls.prepare_server_fun, \ spy(self.instance._start_op_on_mods) as request.cls.init_mods_fun, \ spy(self.instance._link_keys) as request.cls.init_keys_fun, \ spy(self.instance._compile_bat_file) as request.cls.compiled_bat_fun, \ spy(self.instance._compile_config_file) as request.cls.compiled_config_fun: self.instance.init()
def setup(self, request, reset_folder_structure, sc_stub, sb_stub, mocker): """TestACopyModFix setup""" class LinkModFix(ModFix): name = "ace" def hook_init_link_pre(self, server_instance, call_data) -> None: pass class CopyModFix(ModFix): name = "cba_a3" def hook_init_copy_pre(self, server_instance, call_data) -> None: pass request.cls.test_path = test_folder_structure_path() settings = ServerInstanceSettings( "test", sb_stub, sc_stub, user_mods_list=["cba_a3", "ace"], fix_settings=ModFixSettings(enabled_fixes=["cba_a3", "ace"]), arma_folder=self.test_path, server_instance_root=self.test_path) mocker.patch("odk_servermanager.modfix.register_fixes", return_value=[CopyModFix(), LinkModFix()]) request.cls.instance = ServerInstance(settings)
def hook_update_copy_replace(self, server_instance: ServerInstance, call_data: List[str]) -> None: """Exactly the same as the init_copy, but ensure the folder is deleted first, to start anew.""" mod_name = call_data[2] target_mod_folder = join(server_instance.get_server_instance_path(), server_instance.S.copied_mod_folder_name, "@{}".format(mod_name)) if isdir(target_mod_folder): # Delete the old folder, it's better to start anew rmtree(target_mod_folder) self._do_op(mod_name, server_instance.S.arma_folder, target_mod_folder)
def manage_instance(self, config_file: str) -> None: """Offer a basic ui so that the user can distinguish between instance's init and update.""" self.config_file = config_file print("\n ======[ WELCOME TO ODKSM! ]======\n") try: self._recover_settings() self.instance = ServerInstance(self.settings) except (NonExistingFixFile, MisconfiguredModFix) as err: self._ui_abort( "\n [ERR] Error while loading mod fix: {}\n Bye!\n".format( err.args[0])) except Exception as err: self._ui_abort( "\n [ERR] Error while loading the configuration file.\n Something was wrong in the odksm " "config file or in the Arma 3 mod preset.\n\n {}\n\n " "Check the documentation in the wiki, in the README.md or in the " "odksm_servermanager/settings.py.\n Bye!\n".format(err)) try: print("\n Loaded config file: {}\n Instance name: {}" "\n Server title: {}\n".format( self.config_file, self.instance.S.server_instance_name, self.instance.S.config_settings.hostname)) if not self.instance.is_folder_instance_already_there(): self._ui_init() else: self._ui_update() except ModNotFound as err: self._ui_abort( "\n [ERR] Error while loading mods: {}\n Bye!\n".format( err.args[0])) except ErrorInModFix as err: self._ui_abort( "\n [ERR] Error while executing mod fix: {}\nYOUR SERVER INSTANCE MAY BE CORRUPTED! You " "should delete it and generate it again.\n Bye!\n".format( err.args[0])) except Exception as err: self._ui_abort( "\n [ERR] Generic error.\n\n {}\n\n Please take notes on what you were doing and contact " "odksm team on github!\nYOUR SERVER INSTANCE MAY BE CORRUPTED! You should delete it and " "generate it again.\n Bye!\n".format(err))
def setup(self, request, class_reset_folder_structure, c_sc_stub, c_sb_stub): """TestAnInstanceWithANonExistingMod setup""" server_name = "TestServer1" request.cls.test_path = test_folder_structure_path() request.cls.settings = ServerInstanceSettings( server_name, c_sb_stub, c_sc_stub, user_mods_list=["NOT_THERE"], arma_folder=self.test_path, server_instance_root=self.test_path, ) request.cls.instance = ServerInstance(self.settings)
def setup(self, request, sc_stub, sb_stub): """TestOurTestServerInstance setup""" server_name = "TestServer0" mods_to_be_copied = ["CBA_A3"] request.cls.test_path = test_folder_structure_path() request.cls.sb = sb_stub request.cls.sc = sc_stub request.cls.settings = ServerInstanceSettings( server_name, self.sb, self.sc, mods_to_be_copied=mods_to_be_copied, arma_folder=self.test_path, server_instance_root=self.test_path) request.cls.instance = ServerInstance(self.settings)
def setup(self, request, reset_folder_structure, sc_stub, sb_stub): """TestAServerInstance setup""" request.cls.test_path = test_folder_structure_path() request.cls.enabled_fixes = ["cba_a3"] settings = ServerInstanceSettings( "test", sb_stub, sc_stub, user_mods_list=["ace"], fix_settings=ModFixSettings(enabled_fixes=self.enabled_fixes), arma_folder=self.test_path, server_instance_root=self.test_path) request.cls.instance = ServerInstance(settings) self.instance._new_server_folder() self.instance._prepare_server_core() self.instance._copy_mod("CBA_A3")
def setup(self, request, c_sc_stub, c_sb_stub, class_reset_folder_structure): """TestAModfixGos setup""" request.cls.test_path = test_folder_structure_path() fix_settings = ModFixSettings(enabled_fixes=["gos"]) settings = ServerInstanceSettings("test", c_sb_stub, c_sc_stub, user_mods_list=["G.O.S Dariyah", "G.O.S Al Rayak"], arma_folder=self.test_path, server_instance_root=self.test_path, fix_settings=fix_settings) request.cls.instance = ServerInstance(settings) self.instance._new_server_folder() self.instance._prepare_server_core() self.instance._start_op_on_mods("init", ["G.O.S Dariyah"]) self.instance._start_op_on_mods("init", ["G.O.S Al Rayak"]) request.cls.moda_folder = join(self.instance.get_server_instance_path(), self.instance.S.copied_mod_folder_name, "@G.O.S Dariyah") request.cls.modb_folder = join(self.instance.get_server_instance_path(), self.instance.S.copied_mod_folder_name, "@G.O.S Al Rayak")
def setup(self, request, class_reset_folder_structure, c_sb_stub): """TestWhenConfigComposingTheServerInstance setup""" request.cls.test_path = test_folder_structure_path() server_instance_name = "TestServer0" sc = ServerConfigSettings(hostname="TRAINING SERVER", password="******", password_admin="abc", mission_template="mission.name") request.cls.settings = ServerInstanceSettings( server_instance_name=server_instance_name, bat_settings=c_sb_stub, config_settings=sc, arma_folder=self.test_path, server_instance_root=self.test_path, mods_to_be_copied=["CBA_A3"], user_mods_list=["ace", "CBA_A3", "ODKAI"], server_mods_list=["AdvProp", "ODKMIN"], ) request.cls.instance = ServerInstance(self.settings) self.instance._compile_config_file() request.cls.compiled_config = join( self.instance.get_server_instance_path(), self.instance.S.bat_settings.server_config_file_name)
class ServerManager: settings: ServerInstanceSettings instance: ServerInstance config_file: str def __init__(self, debug_logs_path: Union[str, None] = None): self.debug_logs_path = debug_logs_path def bootstrap(self, default_config_file: str = None) -> None: """Interactive UI to start building a new server instance.""" print("\n ======[ WELCOME TO ODKSM! ]======\n") try: # try and read the config data = ConfigIni.read_file(default_config_file, bootstrap=True) # check for needed fields if data["bootstrap"].get("instances_root", "") == "": raise ValueError( "'instances_root' field can't be empty in the [bootstrap] section!" ) if data["bootstrap"].get("odksm_folder_path", "") == "": raise ValueError( "'odksm_folder_path' field can't be empty in the [bootstrap] section!" ) except Exception as err: self._ui_abort( "\n [ERR] Error while reading the default config file!\n\n {}\n\n " "Check the documentation in the wiki, in the bootstrap.ini example file, in the README.md or" " in the odksm_servermanager/settings.py.\n Bye!\n".format( err)) try: # Check the instances_root exists instances_root = data["bootstrap"]["instances_root"] if not isdir(instances_root): raise Exception( "Could not find the instances_folder '{}'".format( instances_root)) print( "\n This utility will help you start creating your server instance.\n" ) instance_name = input(" Choose a unique name for the instance: ") # check if custom templates are needed custom_bat_template_needed = False custom_config_template_needed = False custom_templates_needed_string = input( " Will you need custom templates? (y/n) ") if custom_templates_needed_string == "y": print("\n OK! Now.. \n") custom_bat_template_needed_string = input( " ... will you need a custom BAT template? (y/n) ") if custom_bat_template_needed_string == "y": custom_bat_template_needed = True custom_config_template_needed_string = input( " ... will you need a custom CONFIG template? (y/n) ") if custom_config_template_needed_string == "y": custom_config_template_needed = True # create the folder instance_dir = join(instances_root, instance_name) mkdir(instance_dir) # copy templates if needed if custom_bat_template_needed: bat_template_file_name = "run_server_template.txt" bat_template_file = join(instance_dir, bat_template_file_name) template_file = self._get_resource_file( "templates/{}".format(bat_template_file_name)) copy(template_file, bat_template_file) data["bat"]["bat_template"] = abspath(bat_template_file) if custom_config_template_needed: config_template_file_name = "server_cfg_template.txt" config_template_file = join(instance_dir, config_template_file_name) template_file = self._get_resource_file( "templates/{}".format(config_template_file_name)) copy(template_file, config_template_file) data["config"]["config_template"] = abspath( config_template_file) # prepare some fields for the config file data["ODKSM"]["server_instance_name"] = instance_name data["ODKSM"]["server_instance_root"] = abspath(instance_dir) # generate the config.ini file ConfigIni().create_file(join(instance_dir, "config.ini"), data) # compile the ODKSM.bat file bat_template = pkg_resources.resource_string( "odk_servermanager", "templates/ODKSM_bat_template.txt").decode("UTF-8") bat_file = join(instance_dir, "ODKSM.bat") compile_from_template( bat_template, bat_file, {"odksm_folder_path": data["bootstrap"]["odksm_folder_path"]}) print( "\n Instance folder created!\n\n [WARNING] IMPORTANT! YOU ARE NOT DONE! You still need to edit the\n" " config.ini file in the folder and to run the actual ODKSM.bat tool.\n Bye!\n" ) except Exception as err: self._ui_abort( "\n [ERR] Error while bootstrapping the instance!\n\n {}\n\n " "Check the documentation in the wiki, in the bootstrap.ini example file, in the README.md or" " in the odksm_servermanager/settings.py.\n Bye!\n".format( err)) @staticmethod def _get_resource_file(file: str) -> str: """Return the actual file path of a resource file.""" return pkg_resources.resource_filename('odk_servermanager', file) def manage_instance(self, config_file: str) -> None: """Offer a basic ui so that the user can distinguish between instance's init and update.""" self.config_file = config_file print("\n ======[ WELCOME TO ODKSM! ]======\n") try: self._recover_settings() self.instance = ServerInstance(self.settings) except (NonExistingFixFile, MisconfiguredModFix) as err: self._ui_abort( "\n [ERR] Error while loading mod fix: {}\n Bye!\n".format( err.args[0])) except Exception as err: self._ui_abort( "\n [ERR] Error while loading the configuration file.\n Something was wrong in the odksm " "config file or in the Arma 3 mod preset.\n\n {}\n\n " "Check the documentation in the wiki, in the README.md or in the " "odksm_servermanager/settings.py.\n Bye!\n".format(err)) try: print("\n Loaded config file: {}\n Instance name: {}" "\n Server title: {}\n".format( self.config_file, self.instance.S.server_instance_name, self.instance.S.config_settings.hostname)) if not self.instance.is_folder_instance_already_there(): self._ui_init() else: self._ui_update() except ModNotFound as err: self._ui_abort( "\n [ERR] Error while loading mods: {}\n Bye!\n".format( err.args[0])) except ErrorInModFix as err: self._ui_abort( "\n [ERR] Error while executing mod fix: {}\nYOUR SERVER INSTANCE MAY BE CORRUPTED! You " "should delete it and generate it again.\n Bye!\n".format( err.args[0])) except Exception as err: self._ui_abort( "\n [ERR] Generic error.\n\n {}\n\n Please take notes on what you were doing and contact " "odksm team on github!\nYOUR SERVER INSTANCE MAY BE CORRUPTED! You should delete it and " "generate it again.\n Bye!\n".format(err)) def _ui_init(self): """UI to init an instance.""" user_answer = input(" Do you want to continue? (y/n) ") if self._is_positive_answer(user_answer): print("\n > Starting server instance INIT for {}!".format( self.instance.S.server_instance_name)) self.instance.init() self._ui_print_warnings() print("\n [OK] Init done! Bye!\n") else: self._ui_abort() def _ui_update(self): """UI to update an instance.""" name = self.instance.S.server_instance_name answer = input( " [WARNING] A server instance called {} seems already present.\n Continuing will " "UPDATE the existing server instance. Be sure to understand everything this entails.\n\n" " Do you want to continue? (y/n) ".format(name)) if self._is_positive_answer(answer): print("\n > Starting server instance UPDATE for {}!".format(name)) self.instance.update() self._ui_print_warnings() print("\n [OK] Update done! Bye!\n\n") else: self._ui_abort() def _ui_print_warnings(self): """Print warnings if needed.""" if len(self.instance.warnings) > 0: print("\n We got some warnings:") for warn in self.instance.warnings: print(" [WARN] {}".format(warn)) def _ui_abort(self, message: str = "\n [ABORTED] Bye!\n") -> None: """Print the given message and quit the program.""" if self.debug_logs_path is not None: self._print_debug_log() print(message) exit(1) @staticmethod def _is_positive_answer(answer: str) -> bool: """Parse the answer and return True if positive.""" return answer.lower() == "y" or answer.lower() == "yes" def _recover_settings(self): """Recover all needed settings, including mods presets.""" self._parse_config() if self.settings.user_mods_preset != "": mods = self._parse_mods_preset(self.settings.user_mods_preset) # do not use shorthand += here: there's a bug in Box that will break things self.settings.user_mods_list = self.settings.user_mods_list + mods def _parse_config(self) -> None: """Parse the config file and create all settings container object.""" # Recover data in the file data = ConfigIni.read_file(self.config_file) # Create settings containers config_settings = ServerConfigSettings(**data["config"]) bat_settings = ServerBatSettings(**data["bat"]) enabled_fixes = [] if "enabled_fixes" in data["mod_fix_settings"]: enabled_fixes = data["mod_fix_settings"].pop("enabled_fixes") fix_settings = ModFixSettings( enabled_fixes=enabled_fixes, mod_fix_settings=data["mod_fix_settings"]) # create the global settings container self.settings = ServerInstanceSettings(**data["ODKSM"], bat_settings=bat_settings, config_settings=config_settings, fix_settings=fix_settings) # add missing mod_fix mods to mods_to_be_copied from odk_servermanager.modfix import register_fixes mod_fixes = register_fixes(fix_settings.enabled_fixes) for fix in mod_fixes: # Check that it's a required mod: if fix.name in self.settings.user_mods_list + self.settings.server_mods_list: copy_hooks = [ "hook_init_copy_pre", "hook_init_copy_replace", "hook_init_copy_post", "hook_update_copy_pre", "hook_update_copy_replace", "hook_update_copy_post" ] for copy_hook in copy_hooks: # Check if there's a copy hook enabled... if getattr( fix, copy_hook ) is not None and fix.name not in self.settings.mods_to_be_copied: # ... if so, add the mod to mods_to_be_copied self.settings.mods_to_be_copied.append(fix.name) def _parse_mods_preset(self, filename: str) -> List[str]: """Parse an Arma 3 preset and return the List of all selected mods names.""" # Open the preset file and read its content with open(filename, "r", encoding='utf-8') as f: xml = f.read() # Parse the file and extract all mods names parsed_xml = BeautifulSoup(xml, 'html.parser') mods_data = parsed_xml.select("tr[data-type=\"ModContainer\"]") mods_name = list( map( lambda x: self._display_name_filter( x.select_one("td[data-type=\"DisplayName\"]").text), mods_data)) return mods_name @staticmethod def _display_name_filter(name: str) -> str: """Fix some display names peculiarities.""" return name.replace(":", "-").replace("/", "-") def _print_debug_log(self): """Print in a log file the stacktrace.""" if self.debug_logs_path is not None and self._are_in_exception(): # Recover the traceback tb = traceback.format_exc() log_name = "odksm_{}.log".format( time.strftime("%Y%m%d_%H%M%S", time.gmtime())) log_file = join(self.debug_logs_path, log_name) with open(log_file, "w+") as log: log.write(tb) @staticmethod def _are_in_exception(): """Return true if we are currently handling an exception""" return sys.exc_info() != (None, None, None)
def _get_cba_path(instance: ServerInstance) -> str: """Return the path of the instance cba_settings.""" return join(instance.get_server_instance_path(), "userconfig", "cba_settings.sqf")
def test_should_save_its_config(self, sc_stub, sb_stub): """A server instance should save its config.""" settings = ServerInstanceSettings("server0", sb_stub, sc_stub) instance = ServerInstance(settings) assert instance.S == settings