Skip to content

Commit 20370ff

Browse files
committed
#699 added automatic script groups based on sub-folders
1 parent a50c18e commit 20370ff

13 files changed

+229
-74
lines changed

src/config/config_service.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import re
55
import shutil
6+
from datetime import datetime
67
from typing import NamedTuple, Optional
78

89
from auth.authorization import Authorizer
@@ -14,8 +15,6 @@
1415
from utils.file_utils import to_filename
1516
from utils.process_utils import ProcessInvoker
1617
from utils.string_utils import is_blank, strip
17-
from datetime import datetime
18-
1918

2019
SCRIPT_EDIT_CODE_MODE = 'new_code'
2120
SCRIPT_EDIT_UPLOAD_MODE = 'upload_script'
@@ -56,12 +55,19 @@ def _create_archive_filename(filename):
5655

5756

5857
class ConfigService:
59-
def __init__(self, authorizer, conf_folder, process_invoker: ProcessInvoker) -> None:
58+
def __init__(
59+
self,
60+
authorizer,
61+
conf_folder,
62+
group_scripts_by_folder: bool,
63+
process_invoker: ProcessInvoker) -> None:
64+
6065
self._authorizer = authorizer # type: Authorizer
6166
self._script_configs_folder = os.path.join(conf_folder, 'runners')
6267
self._scripts_folder = os.path.join(conf_folder, 'scripts')
6368
self._scripts_deleted_folder = os.path.join(conf_folder, 'deleted')
6469
self._process_invoker = process_invoker
70+
self._group_scripts_by_folder = group_scripts_by_folder
6571

6672
file_utils.prepare_folder(self._script_configs_folder)
6773
file_utils.prepare_folder(self._scripts_deleted_folder)
@@ -117,7 +123,7 @@ def update_config(self, user, config, filename, uploaded_script):
117123

118124
with open(original_file_path, 'r') as f:
119125
original_config_json = json.load(f)
120-
short_original_config = script_config.read_short(original_file_path, original_config_json)
126+
short_original_config = self.read_short_config(original_config_json, original_file_path)
121127

122128
name = config['name']
123129

@@ -133,6 +139,12 @@ def update_config(self, user, config, filename, uploaded_script):
133139
LOGGER.info('Updating script config "' + name + '" in ' + original_file_path)
134140
self._save_config(config, original_file_path)
135141

142+
def read_short_config(self, config_json, file_path):
143+
return script_config.read_short(
144+
file_path,
145+
config_json,
146+
self._group_scripts_by_folder,
147+
self._script_configs_folder)
136148

137149
def delete_config(self, user, name):
138150
self._check_admin_access(user)
@@ -220,7 +232,7 @@ def list_configs(self, user, mode=None):
220232
def load_script(path, content) -> Optional[ShortConfig]:
221233
try:
222234
config_object = self.load_config_file(path, content)
223-
short_config = script_config.read_short(path, config_object)
235+
short_config = self.read_short_config(config_object, path)
224236

225237
if short_config is None:
226238
return None
@@ -257,7 +269,9 @@ def load_config_model(self, name, user, parameter_values=None, skip_invalid_para
257269
user,
258270
parameter_values,
259271
skip_invalid_parameters,
260-
self._process_invoker)
272+
self._process_invoker,
273+
self._group_scripts_by_folder,
274+
self._script_configs_folder)
261275

262276
def _visit_script_configs(self, visitor):
263277
configs_dir = self._script_configs_folder
@@ -296,7 +310,7 @@ def _find_config(self, name, user) -> Optional[ConfigSearchResult]:
296310
def find_and_load(path: str, content):
297311
try:
298312
config_object = self.load_config_file(path, content)
299-
short_config = script_config.read_short(path, config_object)
313+
short_config = self.read_short_config(config_object, path)
300314

301315
if short_config is None:
302316
return None
@@ -331,7 +345,9 @@ def _load_script_config(
331345
user,
332346
parameter_values,
333347
skip_invalid_parameters,
334-
process_invoker):
348+
process_invoker,
349+
group_scripts_by_folder,
350+
script_configs_folder):
335351

336352
if isinstance(content_or_json_dict, str):
337353
json_object = custom_json.loads(content_or_json_dict)
@@ -342,6 +358,8 @@ def _load_script_config(
342358
path,
343359
user.get_username(),
344360
user.get_audit_name(),
361+
group_scripts_by_folder,
362+
script_configs_folder,
345363
process_invoker,
346364
pty_enabled_default=os_utils.is_pty_supported())
347365

src/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ def main():
103103

104104
process_invoker = ProcessInvoker(server_config.env_vars)
105105

106-
config_service = ConfigService(authorizer, CONFIG_FOLDER, process_invoker)
106+
config_service = ConfigService(
107+
authorizer, CONFIG_FOLDER, server_config.groups_config.group_by_folders, process_invoker)
107108

108109
alerts_service = AlertsService(server_config.alerts_config)
109110
alerts_service = alerts_service

src/model/script_config.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,16 @@ def __init__(self,
6262
path,
6363
username,
6464
audit_name,
65+
group_by_folders: bool,
66+
script_configs_folder: str,
6567
process_invoker: ProcessInvoker,
6668
pty_enabled_default=True):
6769
super().__init__()
6870

69-
short_config = read_short(path, config_object)
71+
short_config = read_short(path, config_object, group_by_folders, script_configs_folder)
7072
self.name = short_config.name
7173
self._pty_enabled_default = pty_enabled_default
72-
self._config_folder = os.path.dirname(path)
74+
self._config_folder = script_configs_folder
7375
self._process_invoker = process_invoker
7476

7577
self._username = username
@@ -363,11 +365,16 @@ def _build_name_from_path(file_path):
363365
return name.strip()
364366

365367

366-
def read_short(file_path, json_object):
368+
def read_short(file_path, json_object, group_by_folders: bool, script_configs_folder: str):
367369
name = _read_name(file_path, json_object)
368370
allowed_users = json_object.get('allowed_users')
369371
admin_users = json_object.get('admin_users')
370372
group = read_str_from_config(json_object, 'group', blank_to_none=True)
373+
if not group and group_by_folders:
374+
relative_path = file_utils.relative_path(file_path, script_configs_folder)
375+
while os.path.dirname(relative_path):
376+
relative_path = os.path.dirname(relative_path)
377+
group = relative_path
371378

372379
hidden = read_bool_from_config('hidden', json_object, default=False)
373380
if hidden:

src/model/server_conf.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(self) -> None:
2929
self.allowed_users = None
3030
self.alerts_config = None
3131
self.logging_config = None
32+
self.groups_config = ScriptGroupsConfig() # type: ScriptGroupsConfig
3233
self.admin_config = None
3334
self.title = None
3435
self.enable_script_titles = None
@@ -75,6 +76,24 @@ def from_json(cls, json_config):
7576
return config
7677

7778

79+
class ScriptGroupsConfig:
80+
81+
def __init__(self) -> None:
82+
self.group_by_folders = True
83+
84+
@classmethod
85+
def from_json(cls, json_config):
86+
config = ScriptGroupsConfig()
87+
88+
if json_config:
89+
config.group_by_folders = model_helper.read_bool_from_config(
90+
'group_by_folders',
91+
json_config,
92+
default=config.group_by_folders)
93+
94+
return config
95+
96+
7897
def _build_env_vars(json_object):
7998
sensitive_config_paths = [
8099
['auth', 'secret'],
@@ -184,6 +203,7 @@ def from_json(conf_path, temp_folder):
184203
config.alerts_config = json_object.get('alerts')
185204
config.callbacks_config = json_object.get('callbacks')
186205
config.logging_config = LoggingConfig.from_json(json_object.get('logging'))
206+
config.groups_config = ScriptGroupsConfig.from_json(json_object.get('script_groups'))
187207
config.user_groups = user_groups
188208
config.admin_users = admin_users
189209
config.full_history_users = full_history_users

src/tests/config_service_test.py

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import os
33
import sys
4+
import tempfile
45
import unittest
56
from collections import OrderedDict
67
from shutil import copyfile
@@ -30,6 +31,14 @@ def test_list_configs_when_one(self):
3031
self.assertEqual(1, len(configs))
3132
self.assertEqual('conf_x', configs[0].name)
3233

34+
def test_list_configs_when_one_and_symlink(self):
35+
conf_path = os.path.join(test_utils.temp_folder, 'runners', 'sub', 'x.json')
36+
with self._temporary_file_symlink(conf_path, {'name': 'test X'}):
37+
configs = self.config_service.list_configs(self.user)
38+
self.assertEqual(1, len(configs))
39+
self.assertEqual('test X', configs[0].name)
40+
self.assertEqual('sub', configs[0].group)
41+
3342
def test_list_configs_when_multiple(self):
3443
_create_script_config_file('conf_x')
3544
_create_script_config_file('conf_y')
@@ -40,9 +49,9 @@ def test_list_configs_when_multiple(self):
4049
self.assertCountEqual(['conf_x', 'conf_y', 'A B C'], conf_names)
4150

4251
def test_list_configs_when_multiple_and_subfolders(self):
43-
_create_script_config_file('conf_x', subfolder = 's1')
44-
_create_script_config_file('conf_y', subfolder = 's2')
45-
_create_script_config_file('ABC', subfolder = os.path.join('s1', 'inner'))
52+
_create_script_config_file('conf_x', subfolder='s1')
53+
_create_script_config_file('conf_y', subfolder='s2')
54+
_create_script_config_file('ABC', subfolder=os.path.join('s1', 'inner'))
4655

4756
configs = self.config_service.list_configs(self.user)
4857
conf_names = [config.name for config in configs]
@@ -114,6 +123,36 @@ def test_load_config_with_slash_in_name(self):
114123
config = self.config_service.load_config_model('Name with slash /', self.user)
115124
self.assertEqual('Name with slash /', config.name)
116125

126+
def test_list_configs_when_multiple_subfolders_and_symlink(self):
127+
def create_config_file(name, relative_path, group=None):
128+
filename = os.path.basename(relative_path)
129+
test_utils.write_script_config(
130+
{'name': name, 'group': group},
131+
filename,
132+
config_folder=os.path.join(test_utils.temp_folder, 'runners', os.path.dirname(relative_path)))
133+
134+
subfolder = os.path.join(test_utils.temp_folder, 'runners', 'sub')
135+
symlink_path = os.path.join(subfolder, 'x.json')
136+
with self._temporary_file_symlink(symlink_path, {'name': 'test X'}):
137+
create_config_file('conf Y', os.path.join('sub', 'y', 'conf_y.json'))
138+
create_config_file('conf Z', os.path.join('sub', 'z', 'conf_z.json'))
139+
create_config_file('conf A', 'conf_a.json')
140+
create_config_file('conf B', os.path.join('b', 'conf_b.json'))
141+
create_config_file('conf C', os.path.join('c', 'conf_c.json'), group='test group')
142+
143+
configs = self.config_service.list_configs(self.user)
144+
actual_name_group_map = {c.name: c.group for c in configs}
145+
146+
self.assertEqual(
147+
actual_name_group_map,
148+
{'test X': 'sub',
149+
'conf Y': 'sub',
150+
'conf Z': 'sub',
151+
'conf A': None,
152+
'conf B': 'b',
153+
'conf C': 'test group'},
154+
)
155+
117156
def tearDown(self):
118157
super().tearDown()
119158
test_utils.cleanup()
@@ -125,7 +164,19 @@ def setUp(self):
125164
self.user = User('ConfigServiceTest', {AUTH_USERNAME: 'ConfigServiceTest'})
126165
self.admin_user = User('admin_user', {AUTH_USERNAME: 'The Admin'})
127166
authorizer = Authorizer(ANY_USER, ['admin_user'], [], [], EmptyGroupProvider())
128-
self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker)
167+
self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker)
168+
169+
@staticmethod
170+
def _temporary_file_symlink(symlink_path, file_content: dict):
171+
f = tempfile.NamedTemporaryFile()
172+
173+
f.write(json.dumps(file_content).encode('utf-8'))
174+
f.flush()
175+
subdir = os.path.dirname(symlink_path)
176+
os.makedirs(subdir)
177+
os.symlink(f.name, symlink_path)
178+
179+
return f
129180

130181

131182
class ConfigServiceAuthTest(unittest.TestCase):
@@ -209,7 +260,11 @@ def setUp(self):
209260
authorizer = Authorizer([], ['adm_user'], [], [], EmptyGroupProvider())
210261
self.user1 = User('user1', {})
211262
self.admin_user = User('adm_user', {})
212-
self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker)
263+
self.config_service = ConfigService(
264+
authorizer,
265+
test_utils.temp_folder,
266+
True,
267+
test_utils.process_invoker)
213268

214269

215270
def script_path(path):
@@ -242,7 +297,7 @@ def setUp(self):
242297

243298
authorizer = Authorizer([], ['admin_user', 'admin_non_editor'], [], ['admin_user'], EmptyGroupProvider())
244299
self.admin_user = User('admin_user', {})
245-
self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker)
300+
self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker)
246301

247302
def tearDown(self):
248303
super().tearDown()
@@ -416,7 +471,7 @@ def setUp(self):
416471

417472
authorizer = Authorizer([], ['admin_user', 'admin_non_editor'], [], ['admin_user'], EmptyGroupProvider())
418473
self.admin_user = User('admin_user', {})
419-
self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker)
474+
self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker)
420475

421476
for suffix in 'XYZ':
422477
name = 'Conf ' + suffix
@@ -669,7 +724,7 @@ def setUp(self):
669724

670725
authorizer = Authorizer([], ['admin_user'], [], [], EmptyGroupProvider())
671726
self.admin_user = User('admin_user', {})
672-
self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker)
727+
self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker)
673728

674729
def tearDown(self):
675730
super().tearDown()
@@ -717,7 +772,7 @@ def setUp(self) -> None:
717772

718773
authorizer = Authorizer([], ['admin_user', 'admin_non_editor'], [], ['admin_user'], EmptyGroupProvider())
719774
self.admin_user = User('admin_user', {})
720-
self.config_service = ConfigService(authorizer, test_utils.temp_folder, test_utils.process_invoker)
775+
self.config_service = ConfigService(authorizer, test_utils.temp_folder, True, test_utils.process_invoker)
721776

722777
for pair in [('script.py', b'123'),
723778
('another.py', b'xyz'),

src/tests/execution_logging_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,8 @@ def test_logging_values(self):
542542
'my_script',
543543
script_command='echo',
544544
parameters=[param1, param2, param3, param4],
545-
logging_config=LoggingConfig('test-${SCRIPT}-${p1}'))
545+
logging_config=LoggingConfig('test-${SCRIPT}-${p1}'),
546+
path=os.path.join('conf', 'my_script.json'))
546547
config_model.set_all_param_values({'p1': 'abc', 'p3': True, 'p4': 987})
547548

548549
execution_id = self.executor_service.start_script(
@@ -568,7 +569,7 @@ def test_logging_values(self):
568569
self.assertEqual('some text\nanother text', log)
569570

570571
log_files = os.listdir(test_utils.temp_folder)
571-
self.assertEqual(['test-my_script-abc.log'], log_files)
572+
self.assertEqual(['test-my_script-abc.log', 'conf'], log_files)
572573

573574
def test_exit_code(self):
574575
config_model = create_config_model(

src/tests/execution_service_test.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from execution.execution_service import ExecutionService
1111
from execution.executor import create_process_wrapper
1212
from model.model_helper import AccessProhibitedException
13-
from model.script_config import ConfigModel
1413
from tests import test_utils
1514
from tests.test_utils import mock_object, create_audit_names, _MockProcessWrapper, _IdGeneratorMock
1615
from utils import audit_utils
@@ -441,10 +440,12 @@ def _start_with_config(execution_service, config, parameter_values=None, user_id
441440

442441

443442
def _create_script_config(parameter_configs):
444-
config = ConfigModel(
445-
{'name': 'script_x',
446-
'script_path': 'ls',
447-
'parameters': parameter_configs},
448-
'script_x.json', 'user1', 'localhost',
449-
test_utils.process_invoker)
450-
return config
443+
return test_utils.create_config_model(
444+
'script_x',
445+
config={'name': 'script_x',
446+
'script_path': 'ls',
447+
'parameters': parameter_configs},
448+
username='user1',
449+
audit_name='localhost',
450+
451+
)

src/tests/scheduling/schedule_service_test.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ def setUp(self) -> None:
6060
scheduler._sleep = MagicMock()
6161
scheduler._sleep.side_effect = lambda x: time.sleep(0.001)
6262

63-
self.config_service = ConfigService(AnyUserAuthorizer(), test_utils.temp_folder, test_utils.process_invoker)
63+
self.config_service = ConfigService(
64+
AnyUserAuthorizer(),
65+
test_utils.temp_folder,
66+
True,
67+
test_utils.process_invoker)
6468

6569
self.create_config('my_script_A')
6670
self.create_config('unschedulable-script', scheduling_enabled=False)

0 commit comments

Comments
 (0)