diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md
index 2acf3b1e..a82cb3f8 100644
--- a/.gitlab/merge_request_templates/Default.md
+++ b/.gitlab/merge_request_templates/Default.md
@@ -27,4 +27,3 @@ This checklist encourages us to confirm any changes have been analyzed to reduce
* [ ] Have you resolved any lint issues?
* [ ] Have you assigned a reviewer?
* [ ] Have you applied correct labels?
-* [ ] Have you updated CHANGELOG.md?
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60bee829..642fc76f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,20 @@
# Changelog
-## --- [4.0.6] - 2022/07/06
+## --- [4.0.7] - 2022/07/18
### New features
+- Task toggle ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/398))
+- Basic API for modifying tasks ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/398))
+### Bug fixes
None
+### Tweaks
+None
+### Lang
+None
+
+[*Full Changelog*](../compare/v4.0.6...v4.0.7)
+
+
+## --- [4.0.6] - 2022/07/06
### 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))
@@ -16,6 +28,8 @@ None
- Check for passwords matching on client side ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/395))
### Lang
- Add string "cloneConfirm" to german translation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/389))
+
+[*Full Changelog*](../compare/v4.0.5...v4.0.6)
## --- [4.0.5] - 2022/06/24
@@ -31,6 +45,8 @@ None
- Add clone server confirmation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/384))
### Lang
- German translation review, fixed some spelling issues and added some missing strings ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/385))
+
+[*Full Changelog*](../compare/v4.0.4...v4.0.5)
## --- [4.0.4-hotfix2] - 2022/06/21
@@ -58,6 +74,8 @@ None
- - ([Merge Request 2](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/377))
- Rework server list on dashboard display for use on small screens ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/372))
- File handling enhancements ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/362))
+
+[*Full Changelog*](../compare/v4.0.3...v4.0.4)
## --- [4.0.3] - 2022/06/18
@@ -68,6 +86,8 @@ None
- API Token authentication hardening ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/364))
### Tweaks
- Add better error logging for statistic collection ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/359))
+
+[*Full Changelog*](../compare/v4.0.2...v4.0.3)
## --- [4.0.2-hotfix1] - 2022/06/17
@@ -84,6 +104,8 @@ None
- - ([Merge Request 2](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/351))
- Add version inheretence & config check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/353))
- Fix support log temp file deletion issue/hang ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/354))
+
+[*Full Changelog*](../compare/v4.0.1...v4.0.2)
## --- [4.0.1] - 2022/06/15
diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py
index e9646fca..9876abc2 100644
--- a/app/classes/shared/tasks.py
+++ b/app/classes/shared/tasks.py
@@ -288,11 +288,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:
@@ -389,11 +391,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))
@@ -403,71 +415,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 df01f60d..53c45986 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:
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 @@
+
+
+
+
+
{{schedule.start_time}}