OpenOrchestrator 1.3.0__py3-none-any.whl → 2.0.0rc1__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.
Files changed (48) hide show
  1. OpenOrchestrator/__main__.py +66 -12
  2. OpenOrchestrator/common/connection_frame.py +10 -4
  3. OpenOrchestrator/database/base.py +8 -0
  4. OpenOrchestrator/database/constants.py +3 -15
  5. OpenOrchestrator/database/db_util.py +112 -39
  6. OpenOrchestrator/database/logs.py +3 -15
  7. OpenOrchestrator/database/queues.py +3 -15
  8. OpenOrchestrator/database/schedulers.py +32 -0
  9. OpenOrchestrator/database/triggers.py +7 -16
  10. OpenOrchestrator/orchestrator/application.py +13 -8
  11. OpenOrchestrator/orchestrator/datetime_input.py +2 -2
  12. OpenOrchestrator/orchestrator/popups/constant_popup.py +8 -6
  13. OpenOrchestrator/orchestrator/popups/credential_popup.py +10 -8
  14. OpenOrchestrator/orchestrator/popups/generic_popups.py +15 -2
  15. OpenOrchestrator/orchestrator/popups/trigger_popup.py +30 -19
  16. OpenOrchestrator/orchestrator/tabs/constants_tab.py +5 -2
  17. OpenOrchestrator/orchestrator/tabs/logging_tab.py +3 -0
  18. OpenOrchestrator/orchestrator/tabs/queue_tab.py +4 -1
  19. OpenOrchestrator/orchestrator/tabs/schedulers_tab.py +45 -0
  20. OpenOrchestrator/orchestrator/tabs/settings_tab.py +2 -5
  21. OpenOrchestrator/orchestrator/tabs/trigger_tab.py +9 -5
  22. OpenOrchestrator/orchestrator/test_helper.py +17 -0
  23. OpenOrchestrator/orchestrator_connection/connection.py +21 -4
  24. OpenOrchestrator/scheduler/application.py +3 -2
  25. OpenOrchestrator/scheduler/run_tab.py +16 -6
  26. OpenOrchestrator/scheduler/runner.py +51 -63
  27. OpenOrchestrator/scheduler/settings_tab.py +90 -14
  28. OpenOrchestrator/scheduler/util.py +8 -0
  29. OpenOrchestrator/tests/__init__.py +0 -0
  30. OpenOrchestrator/tests/db_test_util.py +40 -0
  31. OpenOrchestrator/tests/test_db_util.py +372 -0
  32. OpenOrchestrator/tests/test_orchestrator_connection.py +142 -0
  33. OpenOrchestrator/tests/test_trigger_polling.py +143 -0
  34. OpenOrchestrator/tests/ui_tests/__init__.py +0 -0
  35. OpenOrchestrator/tests/ui_tests/test_constants_tab.py +167 -0
  36. OpenOrchestrator/tests/ui_tests/test_logging_tab.py +180 -0
  37. OpenOrchestrator/tests/ui_tests/test_queues_tab.py +126 -0
  38. OpenOrchestrator/tests/ui_tests/test_schedulers_tab.py +47 -0
  39. OpenOrchestrator/tests/ui_tests/test_trigger_tab.py +243 -0
  40. OpenOrchestrator/tests/ui_tests/ui_util.py +151 -0
  41. openorchestrator-2.0.0rc1.dist-info/METADATA +158 -0
  42. openorchestrator-2.0.0rc1.dist-info/RECORD +55 -0
  43. {OpenOrchestrator-1.3.0.dist-info → openorchestrator-2.0.0rc1.dist-info}/WHEEL +1 -1
  44. OpenOrchestrator/scheduler/connection_frame.py +0 -96
  45. OpenOrchestrator-1.3.0.dist-info/METADATA +0 -60
  46. OpenOrchestrator-1.3.0.dist-info/RECORD +0 -39
  47. {OpenOrchestrator-1.3.0.dist-info → openorchestrator-2.0.0rc1.dist-info/licenses}/LICENSE +0 -0
  48. {OpenOrchestrator-1.3.0.dist-info → openorchestrator-2.0.0rc1.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) -> Job | None:
23
- """Checks if any triggers are waiting to run. If any the first will be run and a
24
- corresponding job object will be returned.
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
- Job: A job object describing the job that has been launched, if any else None.
38
+ The first viable trigger to run if any.
31
39
  """
32
-
33
- other_processes_running = len(app.running_jobs) != 0
34
-
35
- # Single triggers
36
- next_single_trigger = db_util.get_next_single_trigger()
37
-
38
- if next_single_trigger and not (next_single_trigger.is_blocking and other_processes_running):
39
- return run_single_trigger(next_single_trigger)
40
-
41
- # Scheduled triggers
42
- next_scheduled_trigger = db_util.get_next_scheduled_trigger()
43
-
44
- if next_scheduled_trigger and not (next_scheduled_trigger.is_blocking and other_processes_running):
45
- return run_scheduled_trigger(next_scheduled_trigger)
46
-
47
- # Queue triggers
48
- next_queue_trigger = db_util.get_next_queue_trigger()
49
-
50
- if next_queue_trigger and not (next_queue_trigger.is_blocking and other_processes_running):
51
- return run_queue_trigger(next_queue_trigger)
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 run_single_trigger(trigger: SingleTrigger) -> Job | None:
57
- """Mark a single trigger as running in the database
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
- Job: A Job object describing the process if successful.
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
- return None
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
- return None
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 succesful.
220
+ Job: A Job object referencing the process if successful.
236
221
  """
237
222
  process_path = trigger.process_path
238
223
  folder_path = None
@@ -255,6 +240,9 @@ def run_process(trigger: Trigger) -> Job | None:
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
- from tkinter import ttk
4
+ import os
5
+ import tkinter
6
+ from tkinter import ttk, messagebox
5
7
 
6
- from OpenOrchestrator.scheduler.connection_frame import ConnectionFrame
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
- def create_tab(parent: ttk.Notebook) -> ttk.Frame:
10
- """Creates a new Settings tab object.
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
- Args:
13
- parent (ttk.Notebook): The ttk.Notebook that this tab is a child of.
20
+ self.columnconfigure(1, weight=1)
14
21
 
15
- Returns:
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
- conn_frame = ConnectionFrame(tab)
22
- conn_frame.pack(fill='x')
24
+ self._conn_entry = ttk.Entry(self)
25
+ self._conn_entry.grid(row=0, column=1, sticky='ew')
23
26
 
24
- return tab
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)
@@ -0,0 +1,8 @@
1
+ """This module contains miscellaneous utility functions."""
2
+
3
+ import platform
4
+
5
+
6
+ def get_scheduler_name():
7
+ """Get the name of the Scheduler application."""
8
+ return platform.node()
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, "")