OpenOrchestrator 1.3.1__py3-none-any.whl → 2.0.0rc2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- OpenOrchestrator/__main__.py +66 -12
- OpenOrchestrator/common/connection_frame.py +10 -4
- OpenOrchestrator/database/base.py +8 -0
- OpenOrchestrator/database/constants.py +3 -15
- OpenOrchestrator/database/db_util.py +110 -37
- OpenOrchestrator/database/logs.py +3 -15
- OpenOrchestrator/database/queues.py +3 -15
- OpenOrchestrator/database/schedulers.py +32 -0
- OpenOrchestrator/database/triggers.py +7 -16
- OpenOrchestrator/orchestrator/application.py +13 -8
- OpenOrchestrator/orchestrator/datetime_input.py +2 -2
- OpenOrchestrator/orchestrator/popups/constant_popup.py +8 -6
- OpenOrchestrator/orchestrator/popups/credential_popup.py +10 -8
- OpenOrchestrator/orchestrator/popups/generic_popups.py +15 -2
- OpenOrchestrator/orchestrator/popups/trigger_popup.py +25 -14
- OpenOrchestrator/orchestrator/tabs/constants_tab.py +5 -2
- OpenOrchestrator/orchestrator/tabs/logging_tab.py +3 -0
- OpenOrchestrator/orchestrator/tabs/queue_tab.py +4 -1
- OpenOrchestrator/orchestrator/tabs/schedulers_tab.py +45 -0
- OpenOrchestrator/orchestrator/tabs/settings_tab.py +2 -5
- OpenOrchestrator/orchestrator/tabs/trigger_tab.py +9 -5
- OpenOrchestrator/orchestrator/test_helper.py +17 -0
- OpenOrchestrator/orchestrator_connection/connection.py +21 -4
- OpenOrchestrator/scheduler/application.py +3 -2
- OpenOrchestrator/scheduler/run_tab.py +16 -6
- OpenOrchestrator/scheduler/runner.py +52 -64
- OpenOrchestrator/scheduler/settings_tab.py +90 -14
- OpenOrchestrator/scheduler/util.py +8 -0
- OpenOrchestrator/tests/__init__.py +0 -0
- OpenOrchestrator/tests/db_test_util.py +40 -0
- OpenOrchestrator/tests/test_db_util.py +372 -0
- OpenOrchestrator/tests/test_orchestrator_connection.py +142 -0
- OpenOrchestrator/tests/test_trigger_polling.py +143 -0
- OpenOrchestrator/tests/ui_tests/__init__.py +0 -0
- OpenOrchestrator/tests/ui_tests/test_constants_tab.py +167 -0
- OpenOrchestrator/tests/ui_tests/test_logging_tab.py +180 -0
- OpenOrchestrator/tests/ui_tests/test_queues_tab.py +126 -0
- OpenOrchestrator/tests/ui_tests/test_schedulers_tab.py +47 -0
- OpenOrchestrator/tests/ui_tests/test_trigger_tab.py +243 -0
- OpenOrchestrator/tests/ui_tests/ui_util.py +151 -0
- openorchestrator-2.0.0rc2.dist-info/METADATA +158 -0
- openorchestrator-2.0.0rc2.dist-info/RECORD +55 -0
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info}/WHEEL +1 -1
- OpenOrchestrator/scheduler/connection_frame.py +0 -96
- openorchestrator-1.3.1.dist-info/METADATA +0 -60
- openorchestrator-1.3.1.dist-info/RECORD +0 -39
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info/licenses}/LICENSE +0 -0
- {openorchestrator-1.3.1.dist-info → openorchestrator-2.0.0rc2.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,22 @@
|
|
1
1
|
"""This module is responsible for checking triggers and running processes."""
|
2
2
|
|
3
|
+
from __future__ import annotations
|
4
|
+
from typing import TYPE_CHECKING
|
3
5
|
import os
|
6
|
+
import shutil
|
4
7
|
import subprocess
|
5
8
|
from dataclasses import dataclass
|
6
9
|
import uuid
|
10
|
+
import json
|
7
11
|
|
8
12
|
from OpenOrchestrator.common import crypto_util
|
9
13
|
from OpenOrchestrator.database import db_util
|
10
14
|
from OpenOrchestrator.database.triggers import Trigger, SingleTrigger, ScheduledTrigger, QueueTrigger, TriggerStatus
|
11
15
|
from OpenOrchestrator.database.logs import LogLevel
|
16
|
+
from OpenOrchestrator.scheduler import util
|
17
|
+
|
18
|
+
if TYPE_CHECKING:
|
19
|
+
from OpenOrchestrator.scheduler.application import Application
|
12
20
|
|
13
21
|
|
14
22
|
@dataclass
|
@@ -19,91 +27,64 @@ class Job():
|
|
19
27
|
process_folder: str | None
|
20
28
|
|
21
29
|
|
22
|
-
def poll_triggers(app) ->
|
23
|
-
"""Checks
|
24
|
-
|
30
|
+
def poll_triggers(app: Application) -> Trigger | None:
|
31
|
+
"""Checks for pending triggers and returns the first viable one.
|
32
|
+
This takes priority and whitelist of the triggers into account.
|
25
33
|
|
26
34
|
Args:
|
27
35
|
app: The Application object of the Scheduler app.
|
28
36
|
|
29
37
|
Returns:
|
30
|
-
|
38
|
+
The first viable trigger to run if any.
|
31
39
|
"""
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
40
|
+
# Get all pending triggers
|
41
|
+
trigger_list = (
|
42
|
+
db_util.get_pending_single_triggers() +
|
43
|
+
db_util.get_pending_scheduled_triggers() +
|
44
|
+
db_util.get_pending_queue_triggers()
|
45
|
+
)
|
46
|
+
|
47
|
+
# Sort by priority and type
|
48
|
+
# Single > Scheduled > Queue
|
49
|
+
trigger_order = {SingleTrigger: 0, ScheduledTrigger: 1, QueueTrigger: 2}
|
50
|
+
trigger_list.sort(key=lambda t: (-t.priority, trigger_order[type(t)]))
|
51
|
+
|
52
|
+
# Run the first eligible trigger
|
53
|
+
other_jobs_running = len(app.running_jobs) > 0
|
54
|
+
scheduler_name = util.get_scheduler_name()
|
55
|
+
is_exclusive = app.settings_tab_.whitelist_value.get()
|
56
|
+
|
57
|
+
for trigger in trigger_list:
|
58
|
+
trigger_whitelist = json.loads(trigger.scheduler_whitelist)
|
59
|
+
whitelisted = scheduler_name in trigger_whitelist # Is the Scheduler in the whitelist
|
60
|
+
unlisted_allowed = not is_exclusive and not trigger_whitelist # Is the scheduler allowed to run non-designated triggers
|
61
|
+
|
62
|
+
if whitelisted or unlisted_allowed:
|
63
|
+
if not (trigger.is_blocking and other_jobs_running):
|
64
|
+
return trigger
|
52
65
|
|
53
66
|
return None
|
54
67
|
|
55
68
|
|
56
|
-
def
|
57
|
-
"""Mark a
|
69
|
+
def run_trigger(trigger: Trigger) -> Job | None:
|
70
|
+
"""Mark a trigger as running in the database
|
58
71
|
and start the process.
|
59
72
|
|
60
73
|
Args:
|
61
74
|
trigger: The trigger to run.
|
62
75
|
|
63
76
|
Returns:
|
64
|
-
|
77
|
+
A Job object describing the process if successful.
|
65
78
|
"""
|
66
|
-
|
67
79
|
print('Running trigger: ', trigger.trigger_name)
|
68
80
|
|
69
|
-
if db_util.begin_single_trigger(trigger.id):
|
81
|
+
if isinstance(trigger, SingleTrigger) and db_util.begin_single_trigger(trigger.id):
|
70
82
|
return run_process(trigger)
|
71
83
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
def run_scheduled_trigger(trigger: ScheduledTrigger) -> Job | None:
|
76
|
-
"""Mark a scheduled trigger as running in the database,
|
77
|
-
calculate the next run datetime,
|
78
|
-
and start the process.
|
79
|
-
|
80
|
-
Args:
|
81
|
-
trigger: The trigger to run.
|
82
|
-
|
83
|
-
Returns:
|
84
|
-
Job: A Job object describing the process if successful.
|
85
|
-
"""
|
86
|
-
print('Running trigger: ', trigger.trigger_name)
|
87
|
-
|
88
|
-
if db_util.begin_scheduled_trigger(trigger.id):
|
84
|
+
if isinstance(trigger, ScheduledTrigger) and db_util.begin_scheduled_trigger(trigger.id):
|
89
85
|
return run_process(trigger)
|
90
86
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
def run_queue_trigger(trigger: QueueTrigger) -> Job | None:
|
95
|
-
"""Mark a queue trigger as running in the database
|
96
|
-
and start the process.
|
97
|
-
|
98
|
-
Args:
|
99
|
-
trigger: The trigger to run.
|
100
|
-
|
101
|
-
Returns:
|
102
|
-
Job: A Job object describing the process if successful.
|
103
|
-
"""
|
104
|
-
print('Running trigger: ', trigger.trigger_name)
|
105
|
-
|
106
|
-
if db_util.begin_queue_trigger(trigger.id):
|
87
|
+
if isinstance(trigger, QueueTrigger) and db_util.begin_queue_trigger(trigger.id):
|
107
88
|
return run_process(trigger)
|
108
89
|
|
109
90
|
return None
|
@@ -123,6 +104,10 @@ def clone_git_repo(repo_url: str) -> str:
|
|
123
104
|
repo_path = os.path.join(repo_folder, unique_id)
|
124
105
|
|
125
106
|
os.makedirs(repo_path)
|
107
|
+
|
108
|
+
if shutil.which('git') is None:
|
109
|
+
raise RuntimeError('git is not installed or not found in the system PATH.')
|
110
|
+
|
126
111
|
try:
|
127
112
|
subprocess.run(['git', 'clone', repo_url, repo_path], check=True)
|
128
113
|
except subprocess.CalledProcessError as exc:
|
@@ -232,7 +217,7 @@ def run_process(trigger: Trigger) -> Job | None:
|
|
232
217
|
trigger: The trigger whose process to run.
|
233
218
|
|
234
219
|
Returns:
|
235
|
-
Job: A Job object referencing the process if
|
220
|
+
Job: A Job object referencing the process if successful.
|
236
221
|
"""
|
237
222
|
process_path = trigger.process_path
|
238
223
|
folder_path = None
|
@@ -251,10 +236,13 @@ def run_process(trigger: Trigger) -> Job | None:
|
|
251
236
|
conn_string = db_util.get_conn_string()
|
252
237
|
crypto_key = crypto_util.get_key()
|
253
238
|
|
254
|
-
command_args = ['python', process_path, trigger.process_name, conn_string, crypto_key, trigger.process_args]
|
239
|
+
command_args = ['python', process_path, trigger.process_name, conn_string, crypto_key, trigger.process_args, trigger.id]
|
255
240
|
|
256
241
|
process = subprocess.Popen(command_args, stderr=subprocess.PIPE, text=True) # pylint: disable=consider-using-with
|
257
242
|
|
243
|
+
machine_name = util.get_scheduler_name()
|
244
|
+
db_util.start_trigger_from_machine(machine_name, str(trigger.trigger_name))
|
245
|
+
|
258
246
|
return Job(process, trigger, folder_path)
|
259
247
|
|
260
248
|
# We actually want to catch any exception here
|
@@ -1,24 +1,100 @@
|
|
1
1
|
"""This module is responsible for the layout and functionality of the settings tab
|
2
2
|
in Scheduler."""
|
3
3
|
|
4
|
-
|
4
|
+
import os
|
5
|
+
import tkinter
|
6
|
+
from tkinter import ttk, messagebox
|
5
7
|
|
6
|
-
from OpenOrchestrator.
|
8
|
+
from OpenOrchestrator.common import crypto_util
|
9
|
+
from OpenOrchestrator.database import db_util
|
10
|
+
from OpenOrchestrator.scheduler import util
|
7
11
|
|
8
12
|
|
9
|
-
|
10
|
-
|
13
|
+
# pylint: disable=too-many-ancestors
|
14
|
+
class SettingsTab(ttk.Frame):
|
15
|
+
"""A ttk.Frame object containing the functionality of the settings tab in Scheduler."""
|
16
|
+
def __init__(self, parent: ttk.Notebook):
|
17
|
+
super().__init__(parent)
|
18
|
+
self.pack(fill='both', expand=True)
|
11
19
|
|
12
|
-
|
13
|
-
parent (ttk.Notebook): The ttk.Notebook that this tab is a child of.
|
20
|
+
self.columnconfigure(1, weight=1)
|
14
21
|
|
15
|
-
|
16
|
-
ttk.Frame: The created tab object as a ttk.Frame.
|
17
|
-
"""
|
18
|
-
tab = ttk.Frame(parent)
|
19
|
-
tab.pack(fill='both', expand=True)
|
22
|
+
ttk.Label(self, text="Connection string:").grid(row=0, column=0, sticky='w')
|
20
23
|
|
21
|
-
|
22
|
-
|
24
|
+
self._conn_entry = ttk.Entry(self)
|
25
|
+
self._conn_entry.grid(row=0, column=1, sticky='ew')
|
23
26
|
|
24
|
-
|
27
|
+
self._conn_button = ttk.Button(self, text="Connect", command=self._connect)
|
28
|
+
self._conn_button.grid(row=0, column=2, sticky='e')
|
29
|
+
|
30
|
+
ttk.Label(self, text="Encryption key:").grid(row=1, column=0, sticky='w')
|
31
|
+
|
32
|
+
self._key_entry = ttk.Entry(self)
|
33
|
+
self._key_entry.grid(row=1, column=1, sticky='ew')
|
34
|
+
|
35
|
+
self._disconn_button = ttk.Button(self, text="Disconnect", command=self._disconnect, state='disabled')
|
36
|
+
self._disconn_button.grid(row=1, column=2, sticky='e')
|
37
|
+
|
38
|
+
ttk.Label(self, text=f"Scheduler name: {util.get_scheduler_name()}").grid(row=2, column=0, sticky='w', columnspan=2, pady=10)
|
39
|
+
|
40
|
+
self.whitelist_value = tkinter.BooleanVar()
|
41
|
+
self._whitelist_check = ttk.Checkbutton(self, text="Only run whitelisted triggers", variable=self.whitelist_value)
|
42
|
+
self._whitelist_check.grid(row=3, column=0, sticky='w', columnspan=2)
|
43
|
+
|
44
|
+
self._auto_fill()
|
45
|
+
|
46
|
+
def _connect(self) -> None:
|
47
|
+
"""Validate the connection string and encryption key
|
48
|
+
and connect to the database.
|
49
|
+
"""
|
50
|
+
conn_string = self._conn_entry.get()
|
51
|
+
crypto_key = self._key_entry.get()
|
52
|
+
|
53
|
+
if not crypto_util.validate_key(crypto_key):
|
54
|
+
messagebox.showerror("Invalid encryption key", "The entered encryption key is not a valid AES key.")
|
55
|
+
return
|
56
|
+
|
57
|
+
if db_util.connect(conn_string):
|
58
|
+
crypto_util.set_key(crypto_key)
|
59
|
+
self._set_state(True)
|
60
|
+
if not db_util.check_database_revision():
|
61
|
+
messagebox.showerror("Warning", "This version of Scheduler doesn't match the version of the connected database. Unexpected errors might occur.")
|
62
|
+
|
63
|
+
def _disconnect(self) -> None:
|
64
|
+
db_util.disconnect()
|
65
|
+
crypto_util.set_key(None)
|
66
|
+
self._set_state(False)
|
67
|
+
|
68
|
+
def _set_state(self, connected: bool) -> None:
|
69
|
+
if connected:
|
70
|
+
self._conn_entry.configure(state='disabled')
|
71
|
+
self._key_entry.configure(state='disabled')
|
72
|
+
self._conn_button.configure(state='disabled')
|
73
|
+
self._disconn_button.configure(state='normal')
|
74
|
+
self._whitelist_check.configure(state='disabled')
|
75
|
+
else:
|
76
|
+
self._conn_entry.configure(state='normal')
|
77
|
+
self._key_entry.configure(state='normal')
|
78
|
+
self._conn_button.configure(state='normal')
|
79
|
+
self._disconn_button.configure(state='disabled')
|
80
|
+
self._whitelist_check.configure(state='normal')
|
81
|
+
|
82
|
+
def _auto_fill(self) -> None:
|
83
|
+
"""Check the environment for a connection string
|
84
|
+
and encryption key and autofill the inputs.
|
85
|
+
"""
|
86
|
+
conn_string = os.environ.get('OpenOrchestratorConnString', None)
|
87
|
+
if conn_string:
|
88
|
+
self._conn_entry.insert(0, conn_string)
|
89
|
+
|
90
|
+
crypto_key = os.environ.get('OpenOrchestratorKey', None)
|
91
|
+
if crypto_key:
|
92
|
+
self._key_entry.insert(0, crypto_key)
|
93
|
+
|
94
|
+
def new_key(self):
|
95
|
+
"""Creates a new encryption key and inserts it
|
96
|
+
into the key entry.
|
97
|
+
"""
|
98
|
+
key = crypto_util.generate_key().decode()
|
99
|
+
self._key_entry.delete(0, 'end')
|
100
|
+
self._key_entry.insert(0, key)
|
File without changes
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""This module contains database utility functions used in testing."""
|
2
|
+
|
3
|
+
from datetime import datetime, timedelta
|
4
|
+
import os
|
5
|
+
|
6
|
+
from OpenOrchestrator.database import db_util, base
|
7
|
+
|
8
|
+
from OpenOrchestrator.common import crypto_util
|
9
|
+
|
10
|
+
|
11
|
+
def establish_clean_database():
|
12
|
+
"""Connect to the database, drop all tables and recreate them."""
|
13
|
+
db_util.connect(os.environ["CONN_STRING"])
|
14
|
+
crypto_util.set_key(crypto_util.generate_key().decode())
|
15
|
+
|
16
|
+
drop_all_tables()
|
17
|
+
base.Base.metadata.create_all(db_util._connection_engine) # pylint: disable=protected-access
|
18
|
+
|
19
|
+
|
20
|
+
def drop_all_tables():
|
21
|
+
"""Drop all ORM tables from the database."""
|
22
|
+
engine = db_util._connection_engine # pylint: disable=protected-access
|
23
|
+
if not engine:
|
24
|
+
raise RuntimeError("Not connected to a database.")
|
25
|
+
|
26
|
+
base.Base.metadata.drop_all(engine)
|
27
|
+
|
28
|
+
|
29
|
+
def reset_triggers():
|
30
|
+
"""Delete all triggers in the database and create a new of each."""
|
31
|
+
for t in db_util.get_all_triggers():
|
32
|
+
db_util.delete_trigger(t.id)
|
33
|
+
|
34
|
+
if len(db_util.get_all_triggers()) != 0:
|
35
|
+
raise RuntimeError("Not all triggers were deleted.")
|
36
|
+
|
37
|
+
next_run = datetime.now() - timedelta(seconds=2)
|
38
|
+
db_util.create_single_trigger("Single", "Process1", next_run, "Path", "Args", False, False, 0, "")
|
39
|
+
db_util.create_scheduled_trigger("Scheduled", "Process1", "0 0 * * *", next_run, "Path", "Args", False, False, 0, "")
|
40
|
+
db_util.create_queue_trigger("Queue", "Process1", "Trigger Queue", "Path", "Args", False, False, 2, 0, "")
|