diff --git a/.gitignore b/.gitignore index d3b153ad..86cf2616 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ docker/* !docker/docker-compose.yml lang_sort_log.txt lang_sort.txt +app/migrations/status diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e86c021..35b6066d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,24 @@ # Changelog -## --- [4.4.4] - 2024/TBD +## --- [4.4.5] - 2024/TBD ### New features TBD ### Bug fixes -- Fix logic issue causing bedrock wizard's root files buttons to not respond to user click events ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/797)) -- Reset crash detection counter after crash detection process detects successful start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/798)) +TBD ### Tweaks TBD ### Lang TBD

+## --- [4.4.4] - 2024/10/03 +### Bug fixes +- Migrations | Fix orphan schedule configurations crashing migration operation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/796)) +- Fix logic issue causing bedrock wizard's root files buttons to not respond to user click events ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/797)) +- Reset crash detection counter after crash detection process detects successful start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/798)) +- Update new bedrock DL url and correctly bubble up exception on DL fail - Thanks @sarcastron ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/802)) +- Bump cryptography for GHSA-h4gh-qq45-vh27 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/803)) +

+ ## --- [4.4.3] - 2024/08/08 ### Bug fixes - Fix schedules creation fail due to missing action ID ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/791)) diff --git a/README.md b/README.md index c4b7ad56..5345299b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com) -# Crafty Controller 4.4.4 +# Crafty Controller 4.4.5 > Python based Control Panel for your Minecraft Server ## What is Crafty Controller? diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index e827d5b2..7378c9de 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -58,6 +58,7 @@ class Helpers: def __init__(self): self.root_dir = os.path.abspath(os.path.curdir) + self.read_annc = False self.config_dir = os.path.join(self.root_dir, "app", "config") self.webroot = os.path.join(self.root_dir, "app", "frontend") self.servers_dir = os.path.join(self.root_dir, "servers") @@ -79,6 +80,7 @@ class Helpers: self.translation = Translation(self) self.update_available = False + self.migration_notifications = [] self.ignored_names = ["crafty_managed.txt", "db_stats"] self.crafty_starting = False self.minimum_password_length = 8 @@ -128,24 +130,33 @@ class Helpers: "Chrome/104.0.0.0 Safari/537.36" ), } - target_win = 'https://minecraft.azureedge.net/bin-win/[^"]*' - target_linux = 'https://minecraft.azureedge.net/bin-linux/[^"]*' - + target_win = 'https://www.minecraft.net/bedrockdedicatedserver/bin-win/[^"]*' + target_linux = ( + 'https://www.minecraft.net/bedrockdedicatedserver/bin-linux/[^"]*' + ) try: # Get minecraft server download page # (hopefully the don't change the structure) download_page = get(url, headers=headers, timeout=1) - + download_page.raise_for_status() # Search for our string targets - win_download_url = re.search(target_win, download_page.text).group(0) - linux_download_url = re.search(target_linux, download_page.text).group(0) + win_search_result = re.search(target_win, download_page.text) + linux_search_result = re.search(target_linux, download_page.text) + if win_search_result is None or linux_search_result is None: + raise RuntimeError( + "Could not determine download URL from minecraft.net." + ) + win_download_url = win_search_result.group(0) + linux_download_url = linux_search_result.group(0) + print(win_download_url, linux_download_url) if os.name == "nt": return win_download_url return linux_download_url except Exception as e: logger.error(f"Unable to resolve remote bedrock download url! \n{e}") + raise e return False def get_execution_java(self, value, execution_command): @@ -614,11 +625,49 @@ class Helpers: return version_data - def get_announcements(self): + def check_migrations(self) -> None: + if self.read_annc is False: + self.read_annc = True + for file in os.listdir( + os.path.join(self.root_dir, "app", "migrations", "status") + ): + with open( + os.path.join(self.root_dir, "app", "migrations", "status", file), + "r", + encoding="utf-8", + ) as notif_file: + file_json = json.load(notif_file) + for notif in file_json: + if not file_json[notif].get("status"): + self.migration_notifications.append(file_json[notif]) + + def get_announcements(self, lang=None): try: data = [] response = requests.get("https://craftycontrol.com/notify", timeout=2) data = json.loads(response.content) + if not lang: + lang = self.get_setting("language") + self.check_migrations() + for migration_warning in self.migration_notifications: + if not migration_warning.get("status"): + data.append( + { + "id": migration_warning.get("pid"), + "title": self.translation.translate( + "notify", + f"{migration_warning.get('type')}_title", + lang, + ), + "date": "", + "desc": self.translation.translate( + "notify", + f"{migration_warning.get('type')}_desc", + lang, + ), + "link": "", + } + ) if self.update_available: data.append(self.update_available) return data diff --git a/app/classes/shared/import_helper.py b/app/classes/shared/import_helper.py index 030feb56..d0063409 100644 --- a/app/classes/shared/import_helper.py +++ b/app/classes/shared/import_helper.py @@ -217,15 +217,16 @@ class ImportHelpers: FileHelpers.del_dirs(temp_dir) def download_bedrock_server(self, path, new_id): + bedrock_url = Helpers.get_latest_bedrock_url() download_thread = threading.Thread( target=self.download_threaded_bedrock_server, daemon=True, - args=(path, new_id), + args=(path, new_id, bedrock_url), name=f"{new_id}_download", ) download_thread.start() - def download_threaded_bedrock_server(self, path, new_id): + def download_threaded_bedrock_server(self, path, new_id, bedrock_url): """ Downloads the latest Bedrock server, unzips it, sets necessary permissions. @@ -236,10 +237,8 @@ class ImportHelpers: This method handles exceptions and logs errors for each step of the process. """ try: - bedrock_url = Helpers.get_latest_bedrock_url() if bedrock_url: file_path = os.path.join(path, "bedrock_server.zip") - success = FileHelpers.ssl_get_file( bedrock_url, path, "bedrock_server.zip" ) @@ -263,6 +262,7 @@ class ImportHelpers: logger.critical( f"Failed to download bedrock executable during server creation! \n{e}" ) + raise e ServersController.finish_import(new_id) server_users = PermissionsServers.get_server_user_list(new_id) diff --git a/app/classes/web/routes/api/crafty/announcements/index.py b/app/classes/web/routes/api/crafty/announcements/index.py index d66c4473..2cd80ba6 100644 --- a/app/classes/web/routes/api/crafty/announcements/index.py +++ b/app/classes/web/routes/api/crafty/announcements/index.py @@ -29,7 +29,7 @@ class ApiAnnounceIndexHandler(BaseApiHandler): _, ) = auth_data - data = self.helper.get_announcements() + data = self.helper.get_announcements(auth_data[4]["lang"]) if not data: return self.finish_json( 424, diff --git a/app/classes/web/routes/api/servers/index.py b/app/classes/web/routes/api/servers/index.py index ca551326..ae0590a5 100644 --- a/app/classes/web/routes/api/servers/index.py +++ b/app/classes/web/routes/api/servers/index.py @@ -725,7 +725,19 @@ class ApiServersIndexHandler(BaseApiHandler): 405, {"status": "error", "error": "DATA CONSTRAINT FAILED"} ) return - new_server_id = self.controller.create_api_server(data, user["user_id"]) + try: + new_server_id = self.controller.create_api_server(data, user["user_id"]) + except Exception as e: + self.controller.servers.stats.record_stats() + + self.finish_json( + 503, + { + "status": "error", + "error": "Could not create server", + "error_data": str(e), + }, + ) self.controller.servers.stats.record_stats() diff --git a/app/config/version.json b/app/config/version.json index 4ae818ef..8fcaeff4 100644 --- a/app/config/version.json +++ b/app/config/version.json @@ -1,5 +1,5 @@ { "major": 4, "minor": 4, - "sub": 4 + "sub": 5 } diff --git a/app/frontend/templates/server/bedrock_wizard.html b/app/frontend/templates/server/bedrock_wizard.html index e7d7346a..64ae86be 100644 --- a/app/frontend/templates/server/bedrock_wizard.html +++ b/app/frontend/templates/server/bedrock_wizard.html @@ -619,7 +619,9 @@ if (responseData.status === "ok") { window.location.href = '/panel/dashboard'; } else { - + // Close the "be patient..." dialogue box + $('.bootbox-close-button').click(); + // Alert the user that there was an issue. bootbox.alert({ title: responseData.error, message: responseData.error_data @@ -778,4 +780,4 @@ -{% end %} \ No newline at end of file +{% end %} diff --git a/app/migrations/20240308_multi-backup.py b/app/migrations/20240308_multi-backup.py index 56e01a5c..64ff03e1 100644 --- a/app/migrations/20240308_multi-backup.py +++ b/app/migrations/20240308_multi-backup.py @@ -1,4 +1,5 @@ import os +import json import datetime import uuid import peewee @@ -13,9 +14,9 @@ from app.classes.shared.file_helpers import FileHelpers logger = logging.getLogger(__name__) -def is_valid_backup(backup, all_servers): +def is_valid_entry(entry, all_servers): try: - return str(backup.server_id) in all_servers + return str(entry.server_id) in all_servers except (TypeError, peewee.DoesNotExist): return False @@ -24,6 +25,8 @@ def migrate(migrator: Migrator, database, **kwargs): """ Write your migrations here. """ + backup_migration_status = True + schedule_migration_status = True db = database Console.info("Starting Backups migrations") Console.info( @@ -161,10 +164,20 @@ def migrate(migrator: Migrator, database, **kwargs): row.server_id for row in Servers.select(Servers.server_id).distinct() ] all_backups = Backups.select() + all_schedules = Schedules.select() Console.info("Cleaning up orphan backups for all servers") valid_backups = [ - backup for backup in all_backups if is_valid_backup(backup, all_servers) + backup for backup in all_backups if is_valid_entry(backup, all_servers) ] + if len(valid_backups) < len(all_backups): + backup_migration_status = False + print("Orphan backup found") + Console.info("Cleaning up orphan schedules for all servers") + valid_schedules = [ + schedule for schedule in all_schedules if is_valid_entry(schedule, all_servers) + ] + if len(valid_schedules) < len(all_schedules): + schedule_migration_status = False # Copy data from the existing backups table to the new one for backup in valid_backups: Console.info(f"Trying to get server for backup migration {backup.server_id}") @@ -221,13 +234,20 @@ def migrate(migrator: Migrator, database, **kwargs): Console.debug("Migrations: Dropping backup_path from servers table") migrator.drop_columns("servers", ["backup_path"]) - for schedule in Schedules.select(): + for schedule in valid_schedules: action_id = None if schedule.command == "backup_server": Console.info( f"Migrations: Adding backup ID to task with name {schedule.name}" ) - backup = NewBackups.get(NewBackups.server_id == schedule.server_id) + try: + backup = NewBackups.get(NewBackups.server_id == schedule.server_id) + except: + schedule_migration_status = False + Console.error( + "Could not find backup with selected server ID. Omitting from register." + ) + continue action_id = backup.backup_id NewSchedules.create( schedule_id=schedule.schedule_id, @@ -255,6 +275,34 @@ def migrate(migrator: Migrator, database, **kwargs): # Rename the new table to backups migrator.rename_table("new_schedules", "schedules") + with open( + os.path.join( + os.path.abspath(os.path.curdir), + "app", + "migrations", + "status", + "20240308_multi-backup.json", + ), + "w", + encoding="utf-8", + ) as file: + file.write( + json.dumps( + { + "backup_migration": { + "type": "backup", + "status": backup_migration_status, + "pid": str(uuid.uuid4()), + }, + "schedule_migration": { + "type": "schedule", + "status": schedule_migration_status, + "pid": str(uuid.uuid4()), + }, + } + ) + ) + def rollback(migrator: Migrator, database, **kwargs): """ diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index ed35ae9c..f4b79bd0 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -231,10 +231,14 @@ "activityLog": "Activity Logs", "backupComplete": "Backup completed successfully for server {}", "backupStarted": "Backup started for server {}", + "backup_desc": "We detected the backup migration may have partially or fully failed. Please confirm your backups records on the backups tab.", + "backup_title": "Backup Migration Warning", "downloadLogs": "Download Support Logs?", "finishedPreparing": "We've finished preparing your support logs. Please click download to download", "logout": "Logout", "preparingLogs": " Please wait while we prepare your logs... We`ll send a notification when they`re ready. This may take a while for large deployments.", + "schedule_desc": "We detected some or all of your scheduled tasks were not successfully transfered during the upgrade. Please confirm your schedules in the schedules tab.", + "schedule_title": "Schedules Migration Warning", "supportLogs": "Support Logs" }, "offline": { diff --git a/app/translations/lv_LV.json b/app/translations/lv_LV.json index 96753531..64274784 100644 --- a/app/translations/lv_LV.json +++ b/app/translations/lv_LV.json @@ -235,10 +235,14 @@ "activityLog": "Aktivitātes Logi", "backupComplete": "Dublējums veiksmīgi pabeigts priekš servera ", "backupStarted": "Dublējums startēts priekš servera ", + "backup_desc": "Mēs noteicām ka dublējuma migrācija daļēji vai pilnīgi neizdevās. Lūdzu pārskatiet savus dublējumus dublējumu cilnē.", + "backup_title": "Dublējuma Migrācijas Brīdinājums", "downloadLogs": "Lejupielādēt Atbalsta Log Failus?", "finishedPreparing": "Mēs esam pabeiguši sagatavot jūsu atbalsta log datnes. Lūdzu nospiediet lejupielādet lai lejupielādētu", "logout": "Iziet", "preparingLogs": " Lūdzu uzgaidiet kamēr mēs sagatavojam jūsu log datnes... Mēs jums nosūtīsim paziņojumu kad tās būs gatavas. Tas var aizņemt kādu laiku priekš lielām instalācijām.", + "schedule_desc": "Mēs noteicām ka daži vai visi no jūsu darbību grafikiem nebija veiksmīgi pārnesti atjauninājuma laikā. Lūdzu pārskatiet savus grafikus grafiku cilnē.", + "schedule_title": "Grafiku Migrācijas Brīdinājums", "supportLogs": "Atbalsta Logi" }, "offline": { diff --git a/app/translations/nl_BE.json b/app/translations/nl_BE.json index d776a0aa..c75654f4 100644 --- a/app/translations/nl_BE.json +++ b/app/translations/nl_BE.json @@ -234,10 +234,14 @@ "activityLog": "Activiteitslogboeken", "backupComplete": "Back-up succesvol voltooid voor server {}", "backupStarted": "Backup gestart voor server {}", + "backup_desc": "We hebben gedetecteerd dat de back-upmigratie mogelijk gedeeltelijk of volledig is mislukt. Controleer uw back-uprecords op het tabblad Backups.", + "backup_title": "Waarschuwing voor back-upmigratie", "downloadLogs": "Ondersteuningslogboeken downloaden?", "finishedPreparing": "We zijn klaar met het voorbereiden van uw ondersteuningslogboeken. Klik op download om te downloaden", "logout": "Uitloggen", "preparingLogs": " Een ogenblik geduld alstublieft terwijl wij uw logboeken voorbereiden... We sturen een bericht als ze klaar zijn. Dit kan een tijdje duren voor grote implementaties.", + "schedule_desc": "We hebben gedetecteerd dat sommige of alle geplande taken niet succesvol zijn overgedragen tijdens de upgrade. Controleer uw schema's op het tabblad Schema's.", + "schedule_title": "Waarschuwing voor schemamigratie", "supportLogs": "Ondersteuningslogboeken" }, "offline": { diff --git a/app/translations/uk_UA.json b/app/translations/uk_UA.json index aae489ad..ac6e10d9 100644 --- a/app/translations/uk_UA.json +++ b/app/translations/uk_UA.json @@ -234,10 +234,14 @@ "activityLog": "Логи активностей", "backupComplete": "Бекап успішно завершено для сервера {}", "backupStarted": "Бекап успішно розпочато для сервера {}", + "backup_desc": "Ми зафіксували, що міграція бекапів можливо частково або повністю провалилась. Будь ласка перевірте ваші записи бекапів у відповідній вкладці.", + "backup_title": "Увага міграція бекапів", "downloadLogs": "Завантажити логи для підтримки?", "finishedPreparing": "Ми підготували логи. Будь ласка натисніть завантажити", "logout": "Вихід", "preparingLogs": "Будь ласка зачекайте поки ми підготуємо для вас логи... Ми надішлемо вам сповіщення коли усе буде готово. Це може зайняти трішки часу для великих проєктів.", + "schedule_desc": "Ми зафіксували, що деякі або всі ваші заплановані завдання не вдалось успішно перенести поки робиться оновлення. Будь ласка перевірте ваші заплановані завдання у відповідній вкладці.", + "schedule_title": "Увага міграція запланованих завдань", "supportLogs": "Логи для підтримки" }, "offline": { diff --git a/main.py b/main.py index a62a8d27..69cc0043 100644 --- a/main.py +++ b/main.py @@ -115,6 +115,23 @@ def controller_setup(): controller.clear_support_status() +def get_migration_notifications(): + migration_notifications = [] + for file in os.listdir( + os.path.join(APPLICATION_PATH, "app", "migrations", "status") + ): + if os.path.isfile(file): + with open( + os.path.join(APPLICATION_PATH, "app", "migrations", "status", file), + encoding="utf-8", + ) as status_file: + status_json = json.load(status_file) + for item in status_json: + if not status_json[item].get("status"): + migration_notifications.append(item) + return migration_notifications + + def tasks_starter(): """ Method starts stats recording, app scheduler, and @@ -350,6 +367,9 @@ if __name__ == "__main__": helper.db_path, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10} ) database_proxy.initialize(database) + Helpers.ensure_dir_exists( + os.path.join(APPLICATION_PATH, "app", "migrations", "status") + ) migration_manager = MigrationManager(database, helper) migration_manager.up() # Automatically runs migrations @@ -408,7 +428,7 @@ if __name__ == "__main__": controller.set_project_root(APPLICATION_PATH) tasks_manager = TasksManager(helper, controller, file_helper) import3 = Import3(helper, controller) - + helper.migration_notifications = get_migration_notifications() # Check to see if client config.json version is different than the # Master config.json in helpers.py Console.info("Checking for remote changes to config.json") diff --git a/requirements.txt b/requirements.txt index 743da0a8..494b12a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,13 +4,13 @@ argon2-cffi==23.1.0 cached_property==1.5.2 colorama==0.4.6 croniter==1.4.1 -cryptography==42.0.4 +cryptography==43.0.1 libgravatar==1.0.4 nh3==0.2.14 packaging==23.2 peewee==3.13 psutil==5.9.5 -pyOpenSSL==24.0.0 +pyOpenSSL==24.2.1 pyjwt==2.8.0 PyYAML==6.0.1 requests==2.32.0 diff --git a/sonar-project.properties b/sonar-project.properties index 466f0a01..114926e2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.organization=crafty-controller # This is the name and version displayed in the SonarCloud UI. sonar.projectName=Crafty 4 -sonar.projectVersion=4.4.4 +sonar.projectVersion=4.4.5 sonar.python.version=3.9, 3.10, 3.11 sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**