automagix 4.0.0.dev1__tar.gz → 4.0.0.dev2__tar.gz
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.
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/PKG-INFO +19 -11
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/README.md +18 -10
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/automagix.py +1 -1
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/bash_completion.py +5 -6
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/command.py +32 -10
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/command_test.py +7 -2
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/config.py +19 -34
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/environment.py +31 -1
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/helpers.py +24 -11
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/PKG-INFO +19 -11
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/setup.py +1 -1
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/LICENSE +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/__init__.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/automagix_test.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/batch_runner.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/bundlewrap.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/colors.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/config_test.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/environment_test.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/logger.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/parallel.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/parallel_runner.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/parallel_ui.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix/progress_bar.py +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/SOURCES.txt +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/dependency_links.txt +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/entry_points.txt +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/requires.txt +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/automagix.egg-info/top_level.txt +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/setup.cfg +0 -0
- {automagix-4.0.0.dev1 → automagix-4.0.0.dev2}/tests/test_environment.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: automagix
|
|
3
|
-
Version: 4.0.0.
|
|
3
|
+
Version: 4.0.0.dev2
|
|
4
4
|
Summary: Automation wrapper for bash and python commands
|
|
5
5
|
Home-page: https://codeberg.org/vanadinit/automagix
|
|
6
6
|
Author: Johannes Paul
|
|
@@ -83,10 +83,6 @@ Automagix requires Python ≥ 3.10.
|
|
|
83
83
|
pip install automagix
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
NOTICE: original `automagix` and `automagix_cmd` share the
|
|
87
|
-
same main entrypoint. To avoid overwriting and confusion,
|
|
88
|
-
you should have only installed **ONE** of them!
|
|
89
|
-
|
|
90
86
|
# CONFIGURATION
|
|
91
87
|
|
|
92
88
|
You can specify a path to a configuration YAML file via the
|
|
@@ -322,9 +318,14 @@ You can refer to these systems in the command pipeline in multiple ways:
|
|
|
322
318
|
object (Bundlewrap nodes only, no groups)
|
|
323
319
|
|
|
324
320
|
**vars** _(associative array)_
|
|
325
|
-
: Define some vars.
|
|
326
|
-
|
|
327
|
-
|
|
321
|
+
: Define some vars. Note: Only valid Python variable names are allowed.
|
|
322
|
+
To specify a variable type use `type|varname` where *type* is
|
|
323
|
+
one of *int, float, bool, str* and varname the variable name.
|
|
324
|
+
Automatix will try to convert an assigned value to the specified type.
|
|
325
|
+
**Note**: For the boolean type the string "False" and upper/lower-case
|
|
326
|
+
variations are converted to `False` deviating from Python's normal behavior.
|
|
327
|
+
Variables are accessible in the command pipeline via
|
|
328
|
+
{varname} (converted to *str*) and in python actions via `VARS.varname`.
|
|
328
329
|
|
|
329
330
|
**secrets** _(associative array)_
|
|
330
331
|
: Define teamvault secrets. Value has to be in this format:
|
|
@@ -387,8 +388,10 @@ Here you define the commands automagix shall execute.
|
|
|
387
388
|
To do this prefix the desired variablename and = before the action
|
|
388
389
|
key, e.g. `myvar=python: NODES.system.hostname`. Be careful when
|
|
389
390
|
working with multiline statements. In **python** the first line is
|
|
390
|
-
likely to set the variable.
|
|
391
|
-
|
|
391
|
+
likely to set the variable.
|
|
392
|
+
Only for **local** and **remote** action: If a variable type was
|
|
393
|
+
specified in the **vars** section, Automatix will also try to
|
|
394
|
+
convert the value here.
|
|
392
395
|
|
|
393
396
|
**CONDITIONS**: You can define the command only to be executed if
|
|
394
397
|
your condition variable evaluates to "True" in Python. To achieve
|
|
@@ -398,6 +401,11 @@ Here you define the commands automagix shall execute.
|
|
|
398
401
|
string which evaluates to "True" in Python, but empty output will
|
|
399
402
|
evaluate to "False". Use `!?` instead of `?` to invert the condition.
|
|
400
403
|
|
|
404
|
+
**STATIC_FLAG**: If you do not want values to be replaced in your command,
|
|
405
|
+
you can suffix the command key with '-' to indicate a static command
|
|
406
|
+
value, e.g. `python-: f'{this} is not replaced'`. This is especially
|
|
407
|
+
useful for Python commands which use curly brackets themselves.
|
|
408
|
+
|
|
401
409
|
**VALUE**: Your command. Variables will be replaced with Python
|
|
402
410
|
format function. Therefore, use curly brackets to refer to variables,
|
|
403
411
|
systems, secrets and constants.
|
|
@@ -413,7 +421,7 @@ In most cases its a good idea to define your command in quotes to
|
|
|
413
421
|
|
|
414
422
|
#### Escaping in Pipeline
|
|
415
423
|
|
|
416
|
-
Because automagix uses Python's format() function:
|
|
424
|
+
Because automagix uses Python's format() function (not for static commands):
|
|
417
425
|
`{` -> `{{`
|
|
418
426
|
`}` -> `}}`
|
|
419
427
|
|
|
@@ -47,10 +47,6 @@ Automagix requires Python ≥ 3.10.
|
|
|
47
47
|
pip install automagix
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
NOTICE: original `automagix` and `automagix_cmd` share the
|
|
51
|
-
same main entrypoint. To avoid overwriting and confusion,
|
|
52
|
-
you should have only installed **ONE** of them!
|
|
53
|
-
|
|
54
50
|
# CONFIGURATION
|
|
55
51
|
|
|
56
52
|
You can specify a path to a configuration YAML file via the
|
|
@@ -286,9 +282,14 @@ You can refer to these systems in the command pipeline in multiple ways:
|
|
|
286
282
|
object (Bundlewrap nodes only, no groups)
|
|
287
283
|
|
|
288
284
|
**vars** _(associative array)_
|
|
289
|
-
: Define some vars.
|
|
290
|
-
|
|
291
|
-
|
|
285
|
+
: Define some vars. Note: Only valid Python variable names are allowed.
|
|
286
|
+
To specify a variable type use `type|varname` where *type* is
|
|
287
|
+
one of *int, float, bool, str* and varname the variable name.
|
|
288
|
+
Automatix will try to convert an assigned value to the specified type.
|
|
289
|
+
**Note**: For the boolean type the string "False" and upper/lower-case
|
|
290
|
+
variations are converted to `False` deviating from Python's normal behavior.
|
|
291
|
+
Variables are accessible in the command pipeline via
|
|
292
|
+
{varname} (converted to *str*) and in python actions via `VARS.varname`.
|
|
292
293
|
|
|
293
294
|
**secrets** _(associative array)_
|
|
294
295
|
: Define teamvault secrets. Value has to be in this format:
|
|
@@ -351,8 +352,10 @@ Here you define the commands automagix shall execute.
|
|
|
351
352
|
To do this prefix the desired variablename and = before the action
|
|
352
353
|
key, e.g. `myvar=python: NODES.system.hostname`. Be careful when
|
|
353
354
|
working with multiline statements. In **python** the first line is
|
|
354
|
-
likely to set the variable.
|
|
355
|
-
|
|
355
|
+
likely to set the variable.
|
|
356
|
+
Only for **local** and **remote** action: If a variable type was
|
|
357
|
+
specified in the **vars** section, Automatix will also try to
|
|
358
|
+
convert the value here.
|
|
356
359
|
|
|
357
360
|
**CONDITIONS**: You can define the command only to be executed if
|
|
358
361
|
your condition variable evaluates to "True" in Python. To achieve
|
|
@@ -362,6 +365,11 @@ Here you define the commands automagix shall execute.
|
|
|
362
365
|
string which evaluates to "True" in Python, but empty output will
|
|
363
366
|
evaluate to "False". Use `!?` instead of `?` to invert the condition.
|
|
364
367
|
|
|
368
|
+
**STATIC_FLAG**: If you do not want values to be replaced in your command,
|
|
369
|
+
you can suffix the command key with '-' to indicate a static command
|
|
370
|
+
value, e.g. `python-: f'{this} is not replaced'`. This is especially
|
|
371
|
+
useful for Python commands which use curly brackets themselves.
|
|
372
|
+
|
|
365
373
|
**VALUE**: Your command. Variables will be replaced with Python
|
|
366
374
|
format function. Therefore, use curly brackets to refer to variables,
|
|
367
375
|
systems, secrets and constants.
|
|
@@ -377,7 +385,7 @@ In most cases its a good idea to define your command in quotes to
|
|
|
377
385
|
|
|
378
386
|
#### Escaping in Pipeline
|
|
379
387
|
|
|
380
|
-
Because automagix uses Python's format() function:
|
|
388
|
+
Because automagix uses Python's format() function (not for static commands):
|
|
381
389
|
`{` -> `{{`
|
|
382
390
|
`}` -> `}}`
|
|
383
391
|
|
|
@@ -78,7 +78,7 @@ class Automagix:
|
|
|
78
78
|
return command_list
|
|
79
79
|
|
|
80
80
|
def reload_script(self):
|
|
81
|
-
self.script = get_script(args=self.env.cmd_args)
|
|
81
|
+
self.env.script = self.script = get_script(args=self.env.cmd_args)
|
|
82
82
|
self._command_lists = {} # Clear cache
|
|
83
83
|
|
|
84
84
|
def print_main_data(self):
|
|
@@ -4,7 +4,7 @@ from argparse import Action, Namespace
|
|
|
4
4
|
|
|
5
5
|
from argcomplete import warn
|
|
6
6
|
|
|
7
|
-
from .helpers import read_yaml, search_script
|
|
7
|
+
from .helpers import read_yaml, search_script, NoSuchScriptError, AmbiguousScriptError
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _call(*args, **kwargs):
|
|
@@ -52,14 +52,13 @@ class ScriptFieldCompleter:
|
|
|
52
52
|
return []
|
|
53
53
|
|
|
54
54
|
s_file = search_script(name=parsed_args.scriptfile, script_dir=self.script_dir, non_interactive=True)
|
|
55
|
-
if not s_file:
|
|
56
|
-
warn('Script not found or multiple options. Cannot complete.')
|
|
57
|
-
return []
|
|
58
|
-
|
|
59
55
|
script = read_yaml(s_file)
|
|
60
|
-
completion = [f'{key}=' for key in script.get(action.dest, {}).keys()]
|
|
56
|
+
completion = [f'{key.split("|", maxsplit=1)[-1]}=' for key in script.get(action.dest, {}).keys()]
|
|
61
57
|
|
|
62
58
|
return completion
|
|
59
|
+
except (NoSuchScriptError, AmbiguousScriptError) as exc:
|
|
60
|
+
warn(f'{exc}. Cannot complete.')
|
|
61
|
+
return []
|
|
63
62
|
except Exception as exc:
|
|
64
63
|
warn(f'Shell completion failed: {repr(exc)}')
|
|
65
64
|
return []
|
|
@@ -6,10 +6,9 @@ from dataclasses import dataclass
|
|
|
6
6
|
from shlex import quote
|
|
7
7
|
from time import time
|
|
8
8
|
|
|
9
|
-
from .colors import italic, yellow
|
|
10
|
-
from .environment import PipelineEnvironment, AttributedDict, AttributedDummyDict
|
|
9
|
+
from .colors import italic, yellow, green, red
|
|
10
|
+
from .environment import PipelineEnvironment, AttributedDict, AttributedDummyDict, ConversionError
|
|
11
11
|
from .progress_bar import draw_progress_bar
|
|
12
|
-
from .config import convert_boolean_from_input
|
|
13
12
|
|
|
14
13
|
PERSISTENT_VARS = PVARS = AttributedDict()
|
|
15
14
|
|
|
@@ -81,6 +80,13 @@ class Command:
|
|
|
81
80
|
for key, value in cmd.items():
|
|
82
81
|
self.orig_key = key
|
|
83
82
|
self.condition_var, self.assignment_var, self.key = parse_key(key=key)
|
|
83
|
+
|
|
84
|
+
if self.key.endswith('-'):
|
|
85
|
+
self.key = self.key[:-1]
|
|
86
|
+
self.static = True
|
|
87
|
+
else:
|
|
88
|
+
self.static = False
|
|
89
|
+
|
|
84
90
|
if isinstance(value, dict):
|
|
85
91
|
# We need this workaround because the yaml lib returns a dictionary instead of a string,
|
|
86
92
|
# if there is nothing but a variable in the command. Alternative is to use quotes in the script yaml.
|
|
@@ -116,6 +122,9 @@ class Command:
|
|
|
116
122
|
return 'localhost'
|
|
117
123
|
|
|
118
124
|
def get_resolved_value(self, dummy: bool = False):
|
|
125
|
+
if self.static:
|
|
126
|
+
return self.value
|
|
127
|
+
|
|
119
128
|
variables = self.env.vars.copy()
|
|
120
129
|
variables['CONST'] = ConstantsWrapper(self.env.config['constants'])
|
|
121
130
|
variables['SYSTEMS'] = SystemsWrapper(self.env.systems)
|
|
@@ -128,28 +137,41 @@ class Command:
|
|
|
128
137
|
|
|
129
138
|
def show_and_change_variables(self):
|
|
130
139
|
print()
|
|
131
|
-
|
|
140
|
+
legend = f'definded type: {italic(green("match"))} - {italic(red("mismatch"))} - {italic(yellow("undefined"))}'
|
|
141
|
+
self.env.LOG.info(f'--- Variables ({legend}) ---')
|
|
142
|
+
print()
|
|
132
143
|
keylen = max([len(key) for key in self.env.vars.keys()])
|
|
133
144
|
typelen = len(italic(yellow(''))) + 4
|
|
134
145
|
for key, value in self.env.vars.items():
|
|
135
|
-
|
|
146
|
+
type_name = type(value).__name__
|
|
147
|
+
if key in self.env.script.get('_var_types'):
|
|
148
|
+
colored_type = green(type_name) if type_name == self.env.script['_var_types'][key] else red(type_name)
|
|
149
|
+
else:
|
|
150
|
+
colored_type = yellow(type_name)
|
|
151
|
+
|
|
152
|
+
self.env.LOG.info(f" {key:<{keylen + 1}}:{italic(colored_type):<{typelen}} {value}")
|
|
136
153
|
print()
|
|
137
154
|
self.env.LOG.info('To change/set variable write variable + "=" followed by value.')
|
|
138
155
|
self.env.LOG.info('Example: var1=xyz')
|
|
139
156
|
print()
|
|
140
157
|
self.env.LOG.info('You can only change 1 variable at a time. Repeat if necessary.')
|
|
141
|
-
|
|
142
|
-
|
|
158
|
+
print()
|
|
159
|
+
self.env.LOG.info('Notice: All values are strings here by default!'
|
|
160
|
+
' If specified in the vars section the value will be converted.'
|
|
161
|
+
' Use Python debug console and the VARS dictionary to assign'
|
|
162
|
+
' values with other types or more complex operations.')
|
|
143
163
|
print()
|
|
144
164
|
self.env.LOG.info('To not change anything just press "ENTER".')
|
|
145
165
|
answer = self.env.interact('\n', progress_portion=self.progress_portion)
|
|
146
166
|
try:
|
|
147
167
|
key, value = answer.split('=', maxsplit=1)
|
|
148
|
-
self.env.
|
|
168
|
+
self.env.set_var(key=key.strip(), value=value.strip())
|
|
149
169
|
self.env.LOG.info(f'Variable {key.strip()} = {value.strip()}')
|
|
150
170
|
except ValueError:
|
|
151
171
|
if answer:
|
|
152
172
|
self.env.LOG.error('Input could not be parsed.')
|
|
173
|
+
except ConversionError as exc:
|
|
174
|
+
self.env.LOG.error(exc)
|
|
153
175
|
self.print_command()
|
|
154
176
|
print()
|
|
155
177
|
|
|
@@ -436,9 +458,9 @@ class Command:
|
|
|
436
458
|
)
|
|
437
459
|
output = proc.stdout.decode(self.env.config["encoding"])
|
|
438
460
|
assigned_value = output.rstrip('\r\n')
|
|
439
|
-
self.env.
|
|
461
|
+
self.env.set_var(key=self.assignment_var, value=assigned_value)
|
|
440
462
|
hint = ' (trailing newline removed)' if (output.endswith('\n') or output.endswith('\r')) else ''
|
|
441
|
-
self.env.LOG.info(f'Variable {self.assignment_var} = "{
|
|
463
|
+
self.env.LOG.info(f'Variable {self.assignment_var} = "{assigned_value}"{hint}')
|
|
442
464
|
else:
|
|
443
465
|
proc = subprocess.run(
|
|
444
466
|
cmd,
|
|
@@ -142,18 +142,21 @@ def test__show_and_change_variables():
|
|
|
142
142
|
'cond': '{cond}',
|
|
143
143
|
'cond2': '{cond2}',
|
|
144
144
|
'some_var': '{some_var}',
|
|
145
|
+
'myvar2': 5.,
|
|
146
|
+
'mybool': False,
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
with mock.patch.object(
|
|
148
150
|
cmd.env,
|
|
149
151
|
'interact',
|
|
150
|
-
side_effect=['var1=xyz', 'myvar=hallo', ' cond2 = !dgkls=432 \n\t ']
|
|
152
|
+
side_effect=['var1=xyz', 'myvar=hallo', ' cond2 = !dgkls=432 \n\t ', 'mybool=a']
|
|
151
153
|
) as mock_interact:
|
|
152
154
|
cmd.show_and_change_variables()
|
|
153
155
|
cmd.show_and_change_variables()
|
|
154
156
|
cmd.show_and_change_variables()
|
|
157
|
+
cmd.show_and_change_variables()
|
|
155
158
|
|
|
156
|
-
assert mock_interact.call_count ==
|
|
159
|
+
assert mock_interact.call_count == 4
|
|
157
160
|
assert cmd.env.vars == {
|
|
158
161
|
'a': '{a}',
|
|
159
162
|
'myvar': 'hallo',
|
|
@@ -161,4 +164,6 @@ def test__show_and_change_variables():
|
|
|
161
164
|
'cond2': '!dgkls=432',
|
|
162
165
|
'var1': 'xyz',
|
|
163
166
|
'some_var': '{some_var}',
|
|
167
|
+
'myvar2': 5.,
|
|
168
|
+
'mybool': True,
|
|
164
169
|
}
|
|
@@ -166,11 +166,7 @@ def _overwrite(script: dict, key: str, data: list[str]):
|
|
|
166
166
|
script.setdefault(key, {})
|
|
167
167
|
for item in data:
|
|
168
168
|
k, v = item.split('=')
|
|
169
|
-
|
|
170
|
-
if key == 'vars':
|
|
171
|
-
script[key][k] = convert_boolean_from_input(v)
|
|
172
|
-
else:
|
|
173
|
-
script[key][k] = v
|
|
169
|
+
script[key][k] = v
|
|
174
170
|
|
|
175
171
|
|
|
176
172
|
def _tupelize(string) -> tuple:
|
|
@@ -183,30 +179,13 @@ def _tupelize(string) -> tuple:
|
|
|
183
179
|
return tuple(result)
|
|
184
180
|
|
|
185
181
|
|
|
186
|
-
def get_script_path(name: str):
|
|
187
|
-
s_file = name
|
|
188
|
-
LOG.debug(f'Scriptfile input: {s_file}')
|
|
189
|
-
# First try relative path
|
|
190
|
-
if not os.path.isfile(s_file):
|
|
191
|
-
LOG.debug('Script not found at relative path')
|
|
192
|
-
s_file = f'{SCRIPT_DIR}/{name}'
|
|
193
|
-
# Second try relative path from SCRIPT_DIR
|
|
194
|
-
if not os.path.isfile(s_file):
|
|
195
|
-
LOG.debug('Script not found at relative path from SCRIPT_DIR')
|
|
196
|
-
# Third search and offer selection
|
|
197
|
-
s_file = search_script(name=name, script_dir=SCRIPT_DIR)
|
|
198
|
-
if not s_file:
|
|
199
|
-
LOG.debug('Script not found at all.')
|
|
200
|
-
raise ValueError('Script not found')
|
|
201
|
-
LOG.notice(f'Script found at {s_file}.')
|
|
202
|
-
return s_file
|
|
203
|
-
|
|
204
|
-
|
|
205
182
|
def get_script(args: argparse.Namespace) -> dict:
|
|
206
|
-
s_file =
|
|
183
|
+
s_file = search_script(name=args.scriptfile, script_dir=SCRIPT_DIR)
|
|
184
|
+
LOG.notice(f'Script found at {s_file}.')
|
|
207
185
|
|
|
208
186
|
try:
|
|
209
187
|
script = read_yaml(s_file)
|
|
188
|
+
collect_var_types(script)
|
|
210
189
|
validate_script(script)
|
|
211
190
|
except Exception:
|
|
212
191
|
LOG.exception('Script validation failed! Please fix syntax before retrying!')
|
|
@@ -227,6 +206,20 @@ def get_script(args: argparse.Namespace) -> dict:
|
|
|
227
206
|
return script
|
|
228
207
|
|
|
229
208
|
|
|
209
|
+
def collect_var_types(script: dict):
|
|
210
|
+
script['_var_types'] = {}
|
|
211
|
+
for variable_name in list(script.get('vars')):
|
|
212
|
+
if match := re.match(r'(\w+)\|(.*)', variable_name):
|
|
213
|
+
v_name = match.group(2)
|
|
214
|
+
v_type = match.group(1)
|
|
215
|
+
if v_type not in ['int', 'float', 'bool', 'str']:
|
|
216
|
+
raise ValidationError(f'[vars:{v_name}] Unknown type "{v_type}" not allowed.'
|
|
217
|
+
f' Known types are "int", "float", "bool", "str".')
|
|
218
|
+
script['_var_types'][v_name] = v_type
|
|
219
|
+
script['vars'][v_name] = script['vars'][variable_name]
|
|
220
|
+
del script['vars'][variable_name]
|
|
221
|
+
|
|
222
|
+
|
|
230
223
|
def check_deprecated_syntax(ckey: str, entry: str, script: dict, prefix: str) -> int:
|
|
231
224
|
warn = 0
|
|
232
225
|
if isinstance(entry, dict):
|
|
@@ -389,15 +382,7 @@ def update_script_from_row(row: dict, script: dict, index: int):
|
|
|
389
382
|
raise ValidationError(
|
|
390
383
|
f'First row in CSV: Field name is \'{key_type}\','
|
|
391
384
|
f' but has to be one of {list(SCRIPT_FIELDS.keys())}.')
|
|
392
|
-
|
|
393
|
-
script[key_type][key_name] = convert_boolean_from_input(value=value)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def convert_boolean_from_input(value: str) -> bool | str:
|
|
397
|
-
"""Convert "true"/"false" strings to boolean, leave everything else as is."""
|
|
398
|
-
if isinstance(value, str) and value.lower() in ('false', 'true'):
|
|
399
|
-
return value.lower() == 'true'
|
|
400
|
-
return value
|
|
385
|
+
script[key_type][key_name] = value
|
|
401
386
|
|
|
402
387
|
|
|
403
388
|
class UnknownSecretTypeException(Exception):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from argparse import Namespace
|
|
2
2
|
from logging import getLogger
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from .config import init_logger
|
|
6
7
|
from .helpers import empty_queued_input_data
|
|
@@ -46,7 +47,10 @@ class PipelineEnvironment:
|
|
|
46
47
|
):
|
|
47
48
|
self.config = config
|
|
48
49
|
self.script = script
|
|
49
|
-
self.vars = AttributedDict(
|
|
50
|
+
self.vars = AttributedDict()
|
|
51
|
+
for k, v in variables.items():
|
|
52
|
+
self.set_var(key=k, value=v)
|
|
53
|
+
|
|
50
54
|
self.batch_index = batch_index
|
|
51
55
|
self.cmd_args = cmd_args
|
|
52
56
|
|
|
@@ -83,3 +87,29 @@ class PipelineEnvironment:
|
|
|
83
87
|
if progress_portion is not None and self.config['progress_bar']:
|
|
84
88
|
draw_progress_bar(progress_portion)
|
|
85
89
|
return answer
|
|
90
|
+
|
|
91
|
+
def set_var(self, key: str, value: Any) -> None:
|
|
92
|
+
if key not in self.script.get('_var_types', {}):
|
|
93
|
+
self.vars[key] = value
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
var_type = self.script['_var_types'][key]
|
|
97
|
+
try:
|
|
98
|
+
match var_type:
|
|
99
|
+
case 'int':
|
|
100
|
+
self.vars[key] = int(value)
|
|
101
|
+
case 'float':
|
|
102
|
+
self.vars[key] = float(value)
|
|
103
|
+
case 'bool':
|
|
104
|
+
if isinstance(value, str) and value.lower() == 'false':
|
|
105
|
+
self.vars[key] = False
|
|
106
|
+
return
|
|
107
|
+
self.vars[key] = bool(value)
|
|
108
|
+
case 'str':
|
|
109
|
+
self.vars[key] = str(value)
|
|
110
|
+
except ValueError as exc:
|
|
111
|
+
raise ConversionError(f'Variable conversion failed: Could not convert {key}:"{value}" to {var_type}') from exc
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ConversionError(Exception):
|
|
115
|
+
pass
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
from os import walk, mkdir, rmdir
|
|
2
|
+
from os.path import abspath, join, isfile
|
|
2
3
|
from sys import stdin
|
|
4
|
+
from termios import tcflush, TCIFLUSH
|
|
3
5
|
from time import sleep
|
|
4
6
|
from typing import List
|
|
5
7
|
|
|
6
8
|
import yaml
|
|
7
|
-
from termios import tcflush, TCIFLUSH
|
|
8
9
|
|
|
9
10
|
yaml.warnings({'YAMLLoadWarning': False})
|
|
10
11
|
|
|
@@ -42,15 +43,27 @@ def selector(entries: List[tuple], message: str = 'Found multiple entries, pleas
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
def search_script(name: str, script_dir: str, non_interactive: bool = False) -> str | None:
|
|
45
|
-
paths =
|
|
46
|
-
|
|
46
|
+
paths: set[tuple[str, str]] = set() # Second str is the label for the selector
|
|
47
|
+
if isfile(name):
|
|
48
|
+
return abspath(name)
|
|
49
|
+
for dirpath, dirnames, filenames in walk(script_dir):
|
|
47
50
|
for filename in filenames:
|
|
48
51
|
if filename == name:
|
|
49
|
-
path = str(
|
|
50
|
-
paths.
|
|
51
|
-
if
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
path = str(abspath(join(dirpath, filename)))
|
|
53
|
+
paths.add((path, path))
|
|
54
|
+
if not paths:
|
|
55
|
+
raise NoSuchScriptError('Script not found')
|
|
56
|
+
if non_interactive and len(paths) > 1:
|
|
57
|
+
raise AmbiguousScriptError('Multiple scripts found')
|
|
58
|
+
return selector(entries=list(paths), message='Script found at multiple locations. Please choose:')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class NoSuchScriptError(Exception):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AmbiguousScriptError(Exception):
|
|
66
|
+
pass
|
|
54
67
|
|
|
55
68
|
|
|
56
69
|
class FileWithLock:
|
|
@@ -72,7 +85,7 @@ class FileWithLock:
|
|
|
72
85
|
def get_lock(file_path: str):
|
|
73
86
|
while True:
|
|
74
87
|
try:
|
|
75
|
-
|
|
88
|
+
mkdir(f'{file_path}.lock')
|
|
76
89
|
except FileExistsError:
|
|
77
90
|
sleep(1)
|
|
78
91
|
continue
|
|
@@ -80,4 +93,4 @@ def get_lock(file_path: str):
|
|
|
80
93
|
|
|
81
94
|
|
|
82
95
|
def release_lock(file_path: str):
|
|
83
|
-
|
|
96
|
+
rmdir(f'{file_path}.lock')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: automagix
|
|
3
|
-
Version: 4.0.0.
|
|
3
|
+
Version: 4.0.0.dev2
|
|
4
4
|
Summary: Automation wrapper for bash and python commands
|
|
5
5
|
Home-page: https://codeberg.org/vanadinit/automagix
|
|
6
6
|
Author: Johannes Paul
|
|
@@ -83,10 +83,6 @@ Automagix requires Python ≥ 3.10.
|
|
|
83
83
|
pip install automagix
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
NOTICE: original `automagix` and `automagix_cmd` share the
|
|
87
|
-
same main entrypoint. To avoid overwriting and confusion,
|
|
88
|
-
you should have only installed **ONE** of them!
|
|
89
|
-
|
|
90
86
|
# CONFIGURATION
|
|
91
87
|
|
|
92
88
|
You can specify a path to a configuration YAML file via the
|
|
@@ -322,9 +318,14 @@ You can refer to these systems in the command pipeline in multiple ways:
|
|
|
322
318
|
object (Bundlewrap nodes only, no groups)
|
|
323
319
|
|
|
324
320
|
**vars** _(associative array)_
|
|
325
|
-
: Define some vars.
|
|
326
|
-
|
|
327
|
-
|
|
321
|
+
: Define some vars. Note: Only valid Python variable names are allowed.
|
|
322
|
+
To specify a variable type use `type|varname` where *type* is
|
|
323
|
+
one of *int, float, bool, str* and varname the variable name.
|
|
324
|
+
Automatix will try to convert an assigned value to the specified type.
|
|
325
|
+
**Note**: For the boolean type the string "False" and upper/lower-case
|
|
326
|
+
variations are converted to `False` deviating from Python's normal behavior.
|
|
327
|
+
Variables are accessible in the command pipeline via
|
|
328
|
+
{varname} (converted to *str*) and in python actions via `VARS.varname`.
|
|
328
329
|
|
|
329
330
|
**secrets** _(associative array)_
|
|
330
331
|
: Define teamvault secrets. Value has to be in this format:
|
|
@@ -387,8 +388,10 @@ Here you define the commands automagix shall execute.
|
|
|
387
388
|
To do this prefix the desired variablename and = before the action
|
|
388
389
|
key, e.g. `myvar=python: NODES.system.hostname`. Be careful when
|
|
389
390
|
working with multiline statements. In **python** the first line is
|
|
390
|
-
likely to set the variable.
|
|
391
|
-
|
|
391
|
+
likely to set the variable.
|
|
392
|
+
Only for **local** and **remote** action: If a variable type was
|
|
393
|
+
specified in the **vars** section, Automatix will also try to
|
|
394
|
+
convert the value here.
|
|
392
395
|
|
|
393
396
|
**CONDITIONS**: You can define the command only to be executed if
|
|
394
397
|
your condition variable evaluates to "True" in Python. To achieve
|
|
@@ -398,6 +401,11 @@ Here you define the commands automagix shall execute.
|
|
|
398
401
|
string which evaluates to "True" in Python, but empty output will
|
|
399
402
|
evaluate to "False". Use `!?` instead of `?` to invert the condition.
|
|
400
403
|
|
|
404
|
+
**STATIC_FLAG**: If you do not want values to be replaced in your command,
|
|
405
|
+
you can suffix the command key with '-' to indicate a static command
|
|
406
|
+
value, e.g. `python-: f'{this} is not replaced'`. This is especially
|
|
407
|
+
useful for Python commands which use curly brackets themselves.
|
|
408
|
+
|
|
401
409
|
**VALUE**: Your command. Variables will be replaced with Python
|
|
402
410
|
format function. Therefore, use curly brackets to refer to variables,
|
|
403
411
|
systems, secrets and constants.
|
|
@@ -413,7 +421,7 @@ In most cases its a good idea to define your command in quotes to
|
|
|
413
421
|
|
|
414
422
|
#### Escaping in Pipeline
|
|
415
423
|
|
|
416
|
-
Because automagix uses Python's format() function:
|
|
424
|
+
Because automagix uses Python's format() function (not for static commands):
|
|
417
425
|
`{` -> `{{`
|
|
418
426
|
`}` -> `}}`
|
|
419
427
|
|
|
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='automagix',
|
|
8
|
-
version='4.0.0.
|
|
8
|
+
version='4.0.0.dev2',
|
|
9
9
|
description='Automation wrapper for bash and python commands',
|
|
10
10
|
keywords=['bash', 'shell', 'command', 'automation', 'process', 'wrapper', 'devops', 'system administration'],
|
|
11
11
|
long_description=long_description,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|