From 147f178c87367a781db99b4b6a5db4c7165d6e6a Mon Sep 17 00:00:00 2001 From: luukas Date: Thu, 23 Jun 2022 01:57:29 +0300 Subject: [PATCH 01/51] Add a basic API for modifying schedules. THIS IS VERY UNTESTED AND WILL BE EXPANDED TO FULL CRUD FOR SCHEDULES --- app/classes/shared/tasks.py | 149 ++++++++++-------- app/classes/web/panel_handler.py | 1 + app/classes/web/routes/api/api_handlers.py | 24 +++ app/classes/web/routes/api/jsonschema.py | 23 +-- .../web/routes/api/roles/role/index.py | 5 +- .../web/routes/api/servers/server/index.py | 22 +-- .../routes/api/servers/server/tasks/index.py | 16 ++ .../api/servers/server/tasks/task/children.py | 13 ++ .../api/servers/server/tasks/task/index.py | 110 +++++++++++++ .../web/routes/api/users/user/index.py | 16 +- 10 files changed, 255 insertions(+), 124 deletions(-) create mode 100644 app/classes/web/routes/api/servers/server/tasks/index.py create mode 100644 app/classes/web/routes/api/servers/server/tasks/task/children.py create mode 100644 app/classes/web/routes/api/servers/server/tasks/task/index.py diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index d64adb0b..849a0bde 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -278,11 +278,13 @@ class TasksManager: job_data["parent"], job_data["delay"], ) + # Checks to make sure some doofus didn't actually make the newly # created task a child of itself. if str(job_data["parent"]) == str(sch_id): HelpersManagement.update_scheduled_task(sch_id, {"parent": None}) - # Check to see if it's enabled and is not a chain reaction. + + # Check to see if it's enabled and is not a chain reaction. if job_data["enabled"] and job_data["interval_type"] != "reaction": if job_data["cron_string"] != "": try: @@ -379,11 +381,21 @@ class TasksManager: ) def update_job(self, sch_id, job_data): - HelpersManagement.update_scheduled_task(sch_id, job_data) # Checks to make sure some doofus didn't actually make the newly # created task a child of itself. - if str(job_data["parent"]) == str(sch_id): - HelpersManagement.update_scheduled_task(sch_id, {"parent": None}) + if str(job_data.get("parent")) == str(sch_id): + job_data["parent"] = None + + HelpersManagement.update_scheduled_task(sch_id, job_data) + + if not ( + "interval" in job_data + and "enabled" in job_data + and "cron_string" in job_data + and "interval_type" in job_data + ): + return + try: if job_data["interval"] != "reaction": self.scheduler.remove_job(str(sch_id)) @@ -393,71 +405,70 @@ class TasksManager: "Assuming it was previously disabled. Starting new job." ) - if job_data["enabled"]: - if job_data["interval"] != "reaction": - if job_data["cron_string"] != "": - try: - self.scheduler.add_job( - HelpersManagement.add_command, - CronTrigger.from_crontab( - job_data["cron_string"], timezone=str(self.tz) - ), - id=str(sch_id), - args=[ - job_data["server_id"], - self.users_controller.get_id_by_name("system"), - "127.0.0.1", - job_data["command"], - ], - ) - except Exception as e: - Console.error(f"Failed to schedule task with error: {e}.") - Console.info("Removing failed task from DB.") - self.controller.management_helper.delete_scheduled_task(sch_id) - else: - if job_data["interval_type"] == "hours": - self.scheduler.add_job( - HelpersManagement.add_command, - "cron", - minute=0, - hour="*/" + str(job_data["interval"]), - id=str(sch_id), - args=[ - job_data["server_id"], - self.users_controller.get_id_by_name("system"), - "127.0.0.1", - job_data["command"], - ], - ) - elif job_data["interval_type"] == "minutes": - self.scheduler.add_job( - HelpersManagement.add_command, - "cron", - minute="*/" + str(job_data["interval"]), - id=str(sch_id), - args=[ - job_data["server_id"], - self.users_controller.get_id_by_name("system"), - "127.0.0.1", - job_data["command"], - ], - ) - elif job_data["interval_type"] == "days": - curr_time = job_data["start_time"].split(":") - self.scheduler.add_job( - HelpersManagement.add_command, - "cron", - day="*/" + str(job_data["interval"]), - hour=curr_time[0], - minute=curr_time[1], - id=str(sch_id), - args=[ - job_data["server_id"], - self.users_controller.get_id_by_name("system"), - "127.0.0.1", - job_data["command"], - ], - ) + if job_data["enabled"] and job_data["interval"] != "reaction": + if job_data["cron_string"] != "": + try: + self.scheduler.add_job( + HelpersManagement.add_command, + CronTrigger.from_crontab( + job_data["cron_string"], timezone=str(self.tz) + ), + id=str(sch_id), + args=[ + job_data["server_id"], + self.users_controller.get_id_by_name("system"), + "127.0.0.1", + job_data["command"], + ], + ) + except Exception as e: + Console.error(f"Failed to schedule task with error: {e}.") + Console.info("Removing failed task from DB.") + self.controller.management_helper.delete_scheduled_task(sch_id) + else: + if job_data["interval_type"] == "hours": + self.scheduler.add_job( + HelpersManagement.add_command, + "cron", + minute=0, + hour="*/" + str(job_data["interval"]), + id=str(sch_id), + args=[ + job_data["server_id"], + self.users_controller.get_id_by_name("system"), + "127.0.0.1", + job_data["command"], + ], + ) + elif job_data["interval_type"] == "minutes": + self.scheduler.add_job( + HelpersManagement.add_command, + "cron", + minute="*/" + str(job_data["interval"]), + id=str(sch_id), + args=[ + job_data["server_id"], + self.users_controller.get_id_by_name("system"), + "127.0.0.1", + job_data["command"], + ], + ) + elif job_data["interval_type"] == "days": + curr_time = job_data["start_time"].split(":") + self.scheduler.add_job( + HelpersManagement.add_command, + "cron", + day="*/" + str(job_data["interval"]), + hour=curr_time[0], + minute=curr_time[1], + id=str(sch_id), + args=[ + job_data["server_id"], + self.users_controller.get_id_by_name("system"), + "127.0.0.1", + job_data["command"], + ], + ) else: try: self.scheduler.get_job(str(sch_id)) diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 5122a683..720f7b69 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -531,6 +531,7 @@ class PanelHandler(BaseHandler): page_data["downloading"] = self.controller.servers.get_download_status( server_id ) + page_data["server_id"] = server_id try: page_data["waiting_start"] = self.controller.servers.get_waiting_start( server_id diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index e5f72b48..29ee02c5 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -23,6 +23,15 @@ from app.classes.web.routes.api.servers.server.public import ( ) from app.classes.web.routes.api.servers.server.stats import ApiServersServerStatsHandler from app.classes.web.routes.api.servers.server.stdin import ApiServersServerStdinHandler +from app.classes.web.routes.api.servers.server.tasks.index import ( + ApiServersServerTasksIndexHandler, +) +from app.classes.web.routes.api.servers.server.tasks.task.children import ( + ApiServersServerTasksTaskChildrenHandler, +) +from app.classes.web.routes.api.servers.server.tasks.task.index import ( + ApiServersServerTasksTaskIndexHandler, +) from app.classes.web.routes.api.servers.server.users import ApiServersServerUsersHandler from app.classes.web.routes.api.users.index import ApiUsersIndexHandler from app.classes.web.routes.api.users.user.index import ApiUsersUserIndexHandler @@ -103,6 +112,21 @@ def api_handlers(handler_args): ApiServersServerIndexHandler, handler_args, ), + ( + r"/api/v2/servers/([0-9]+)/tasks/?", + ApiServersServerTasksIndexHandler, + handler_args, + ), + ( + r"/api/v2/servers/([0-9]+)/tasks/([0-9]+)/?", + ApiServersServerTasksTaskIndexHandler, + handler_args, + ), + ( + r"/api/v2/servers/([0-9]+)/tasks/([0-9]+)/children/?", + ApiServersServerTasksTaskChildrenHandler, + handler_args, + ), ( r"/api/v2/servers/([0-9]+)/stats/?", ApiServersServerStatsHandler, diff --git a/app/classes/web/routes/api/jsonschema.py b/app/classes/web/routes/api/jsonschema.py index 3497f052..70b19fa7 100644 --- a/app/classes/web/routes/api/jsonschema.py +++ b/app/classes/web/routes/api/jsonschema.py @@ -5,6 +5,7 @@ from app.classes.web.routes.api.roles.role.index import modify_role_schema from app.classes.web.routes.api.roles.index import create_role_schema from app.classes.web.routes.api.servers.server.index import server_patch_schema from app.classes.web.routes.api.servers.index import new_server_schema +from app.classes.web.routes.api.servers.server.tasks.task.index import task_patch_schema SCHEMA_LIST: t.Final = [ "login", @@ -14,6 +15,7 @@ SCHEMA_LIST: t.Final = [ "new_server", "user_patch", "new_user", + "task_patch", ] @@ -59,22 +61,8 @@ class ApiJsonSchemaHandler(BaseApiHandler): "properties": { **self.controller.users.user_jsonschema_props, }, - "anyOf": [ - # Require at least one property - {"required": [name]} - for name in [ - "username", - "password", - "email", - "enabled", - "lang", - "superuser", - "permissions", - "roles", - "hints", - ] - ], "additionalProperties": False, + "minProperties": 1, }, }, ) @@ -93,6 +81,11 @@ class ApiJsonSchemaHandler(BaseApiHandler): }, }, ) + elif schema_name == "task_patch": + self.finish_json( + 200, + {"status": "ok", "data": task_patch_schema}, + ) else: self.finish_json( 404, diff --git a/app/classes/web/routes/api/roles/role/index.py b/app/classes/web/routes/api/roles/role/index.py index 43abbd55..c0601b5e 100644 --- a/app/classes/web/routes/api/roles/role/index.py +++ b/app/classes/web/routes/api/roles/role/index.py @@ -28,11 +28,8 @@ modify_role_schema = { }, }, }, - "anyOf": [ - {"required": ["name"]}, - {"required": ["servers"]}, - ], "additionalProperties": False, + "minProperties": 1, } diff --git a/app/classes/web/routes/api/servers/server/index.py b/app/classes/web/routes/api/servers/server/index.py index ea8c71de..11f8620b 100644 --- a/app/classes/web/routes/api/servers/server/index.py +++ b/app/classes/web/routes/api/servers/server/index.py @@ -28,28 +28,8 @@ server_patch_schema = { "logs_delete_after": {"type": "integer"}, "type": {"type": "string", "minLength": 1}, }, - "anyOf": [ - # Require at least one property - {"required": [name]} - for name in [ - "server_name", - "path", - "backup_path", - "executable", - "log_path", - "execution_command", - "auto_start", - "auto_start_delay", - "crash_detection", - "stop_command", - "executable_update_url", - "server_ip", - "server_port", - "logs_delete_after", - "type", - ] - ], "additionalProperties": False, + "minProperties": 1, } diff --git a/app/classes/web/routes/api/servers/server/tasks/index.py b/app/classes/web/routes/api/servers/server/tasks/index.py new file mode 100644 index 00000000..64ca3bef --- /dev/null +++ b/app/classes/web/routes/api/servers/server/tasks/index.py @@ -0,0 +1,16 @@ +# TODO: create and read + +import logging + +from app.classes.web.base_api_handler import BaseApiHandler + + +logger = logging.getLogger(__name__) + + +class ApiServersServerTasksIndexHandler(BaseApiHandler): + def get(self, server_id: str, task_id: str): + pass + + def post(self, server_id: str, task_id: str): + pass diff --git a/app/classes/web/routes/api/servers/server/tasks/task/children.py b/app/classes/web/routes/api/servers/server/tasks/task/children.py new file mode 100644 index 00000000..d92a42f7 --- /dev/null +++ b/app/classes/web/routes/api/servers/server/tasks/task/children.py @@ -0,0 +1,13 @@ +# TODO: read + +import logging + +from app.classes.web.base_api_handler import BaseApiHandler + + +logger = logging.getLogger(__name__) + + +class ApiServersServerTasksTaskChildrenHandler(BaseApiHandler): + def get(self, server_id: str, task_id: str): + pass diff --git a/app/classes/web/routes/api/servers/server/tasks/task/index.py b/app/classes/web/routes/api/servers/server/tasks/task/index.py new file mode 100644 index 00000000..3c567fdd --- /dev/null +++ b/app/classes/web/routes/api/servers/server/tasks/task/index.py @@ -0,0 +1,110 @@ +# TODO: read and delete + +import json +import logging + +from jsonschema import ValidationError, validate +from app.classes.models.management import HelpersManagement +from app.classes.models.server_permissions import EnumPermissionsServer + +from app.classes.web.base_api_handler import BaseApiHandler + + +logger = logging.getLogger(__name__) + +task_patch_schema = { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": True, + }, + "action": { + "type": "string", + }, + "interval": {"type": "integer"}, + "interval_type": { + "type": "string", + "enum": [ + # Basic tasks + "hours", + "minutes", + "days", + # Chain reaction tasks: + "reaction", + # CRON tasks: + "", + ], + }, + "start_time": {"type": "string", "pattern": r"\d{1,2}:\d{1,2}"}, + "command": {"type": ["string", "null"]}, + "one_time": {"type": "boolean", "default": False}, + "cron_string": {"type": "string", "default": ""}, + "parent": {"type": ["integer", "null"]}, + "delay": {"type": "integer", "default": 0}, + }, + "additionalProperties": False, + "minProperties": 1, +} + + +class ApiServersServerTasksTaskIndexHandler(BaseApiHandler): + def get(self, server_id: str, task_id: str): + pass + + def delete(self, server_id: str, task_id: str): + pass + + def patch(self, server_id: str, task_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + + try: + validate(data, task_patch_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + + if server_id not in [str(x["server_id"]) for x in auth_data[0]]: + # if the user doesn't have access to the server, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + if ( + EnumPermissionsServer.SCHEDULE + not in self.controller.server_perms.get_user_id_permissions_list( + auth_data[4]["user_id"], server_id + ) + ): + # if the user doesn't have Schedule permission, return an error + return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + + # Checks to make sure some doofus didn't actually make the newly + # created task a child of itself. + if str(data.get("parent")) == str(task_id) and data.get("parent") is not None: + data["parent"] = None + + HelpersManagement.update_scheduled_task(task_id, data) + + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Edited server {server_id}: updated schedule", + server_id, + self.get_remote_ip(), + ) + self.tasks_manager.reload_schedule_from_db() + + self.finish_json(200, {"status": "ok"}) diff --git a/app/classes/web/routes/api/users/user/index.py b/app/classes/web/routes/api/users/user/index.py index 7274611a..47d8dd68 100644 --- a/app/classes/web/routes/api/users/user/index.py +++ b/app/classes/web/routes/api/users/user/index.py @@ -112,22 +112,8 @@ class ApiUsersUserIndexHandler(BaseApiHandler): "properties": { **self.controller.users.user_jsonschema_props, }, - "anyOf": [ - # Require at least one property - {"required": [name]} - for name in [ - "username", - "password", - "email", - "enabled", - "lang", - "superuser", - "permissions", - "roles", - "hints", - ] - ], "additionalProperties": False, + "minProperties": 1, } auth_data = self.authenticate_user() if not auth_data: From f951b49e2f355f299af9e861233c1d6cde13dbe2 Mon Sep 17 00:00:00 2001 From: luukas Date: Thu, 23 Jun 2022 01:58:06 +0300 Subject: [PATCH 02/51] AJAX schedule enabled status This is buggy in its current state --- app/frontend/templates/base.html | 5 ++ .../templates/panel/server_schedules.html | 61 ++++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/app/frontend/templates/base.html b/app/frontend/templates/base.html index eba6c2e8..4133dfac 100755 --- a/app/frontend/templates/base.html +++ b/app/frontend/templates/base.html @@ -37,6 +37,11 @@ + + + + + diff --git a/app/frontend/templates/panel/server_schedules.html b/app/frontend/templates/panel/server_schedules.html index 49282814..130d00f2 100644 --- a/app/frontend/templates/panel/server_schedules.html +++ b/app/frontend/templates/panel/server_schedules.html @@ -24,7 +24,7 @@ - {% include "parts/details_stats.html %} + {% include "parts/details_stats.html" %}
@@ -33,10 +33,10 @@
- {% include "parts/server_controls_list.html %} + {% include "parts/server_controls_list.html" %} - {% include "parts/m_server_controls_list.html %} + {% include "parts/m_server_controls_list.html" %}
@@ -94,15 +94,7 @@

{{schedule.start_time}}

- {% if schedule.enabled %} - - Yes - - {% else %} - - No - - {% end %} +
@@ -256,6 +240,39 @@ {% block js %} -{% end %} \ No newline at end of file +{% end %} From 4f7e1bfa24bd6c1328f57310fbf3219a6ee98772 Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 6 Jul 2022 19:00:17 +0300 Subject: [PATCH 03/51] Check for None when getting the CPU frequency data Default to -1 when an error occurs --- app/classes/minecraft/stats.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/classes/minecraft/stats.py b/app/classes/minecraft/stats.py index 64cbf894..ab75da51 100644 --- a/app/classes/minecraft/stats.py +++ b/app/classes/minecraft/stats.py @@ -87,7 +87,9 @@ class Stats: try: cpu_freq = psutil.cpu_freq() except NotImplementedError: - cpu_freq = psutil._common.scpufreq(current=0, min=0, max=0) + cpu_freq = None + if cpu_freq is None: + cpu_freq = psutil._common.scpufreq(current=-1, min=-1, max=-1) memory = psutil.virtual_memory() try: node_stats: NodeStatsDict = { From c88ef5e9d66fe3d44561fd692eb15523768bfc17 Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 13 Jul 2022 13:42:43 +0300 Subject: [PATCH 04/51] Add a title to the schedule toggle on mobile --- app/frontend/templates/panel/server_schedules.html | 1 + 1 file changed, 1 insertion(+) diff --git a/app/frontend/templates/panel/server_schedules.html b/app/frontend/templates/panel/server_schedules.html index 130d00f2..60495683 100644 --- a/app/frontend/templates/panel/server_schedules.html +++ b/app/frontend/templates/panel/server_schedules.html @@ -181,6 +181,7 @@

{{schedule.start_time}}

  • +

    Enabled

  • From 6af0dc32bd7bc6c1aa5bd91429665a14382f9284 Mon Sep 17 00:00:00 2001 From: luukas Date: Wed, 13 Jul 2022 16:21:08 +0300 Subject: [PATCH 05/51] Add CHANGELOG.md entries related to !398+ --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60bee829..6b4ffba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## --- [4.0.6] - 2022/07/06 ### New features -None +- Task toggle (!398+) +- Basic API for modifying tasks (!398+) ### Bug fixes - Remove redundant path check on backup restore ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/390)) - Fix issue with stats pinging on slow starting servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/391)) From 1438ce1c36e25d9a56e80e566e0446246bf6b9dc Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 14 Jul 2022 14:46:23 -0400 Subject: [PATCH 06/51] Add handle. Change text color in "on" position --- app/frontend/templates/panel/server_schedules.html | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/frontend/templates/panel/server_schedules.html b/app/frontend/templates/panel/server_schedules.html index 60495683..2d965ad7 100644 --- a/app/frontend/templates/panel/server_schedules.html +++ b/app/frontend/templates/panel/server_schedules.html @@ -94,7 +94,7 @@

    {{schedule.start_time}}

    - +
    + {% end %} {% block js %}