automagix 4.0.0.dev1__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.
- automagix/__init__.py +95 -0
- automagix/automagix.py +185 -0
- automagix/automagix_test.py +34 -0
- automagix/bash_completion.py +65 -0
- automagix/batch_runner.py +70 -0
- automagix/bundlewrap.py +75 -0
- automagix/colors.py +42 -0
- automagix/command.py +576 -0
- automagix/command_test.py +164 -0
- automagix/config.py +412 -0
- automagix/config_test.py +95 -0
- automagix/environment.py +85 -0
- automagix/environment_test.py +34 -0
- automagix/helpers.py +83 -0
- automagix/logger.py +95 -0
- automagix/parallel.py +197 -0
- automagix/parallel_runner.py +124 -0
- automagix/parallel_ui.py +222 -0
- automagix/progress_bar.py +206 -0
- automagix-4.0.0.dev1.dist-info/METADATA +640 -0
- automagix-4.0.0.dev1.dist-info/RECORD +25 -0
- automagix-4.0.0.dev1.dist-info/WHEEL +5 -0
- automagix-4.0.0.dev1.dist-info/entry_points.txt +4 -0
- automagix-4.0.0.dev1.dist-info/licenses/LICENSE +22 -0
- automagix-4.0.0.dev1.dist-info/top_level.txt +1 -0
automagix/__init__.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from time import time, strftime, gmtime
|
|
7
|
+
|
|
8
|
+
from .batch_runner import get_script_and_batch_items, run_batch_items
|
|
9
|
+
from .config import init_logger, CONFIG, LOG, VERSION, arguments, MAGIC_SELECTION_INT
|
|
10
|
+
from .helpers import empty_queued_input_data, selector
|
|
11
|
+
from .parallel_runner import run_parallel_screens
|
|
12
|
+
from .progress_bar import setup_scroll_area, destroy_scroll_area
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_startup_script():
|
|
16
|
+
if not CONFIG.get('startup_script'):
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
cmds = [os.path.expandvars(CONFIG["startup_script"])]
|
|
20
|
+
cmds.extend(sys.argv)
|
|
21
|
+
subprocess.run(cmds)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def setup(args: argparse.Namespace) -> float:
|
|
25
|
+
"""Setup logger and print version information"""
|
|
26
|
+
init_logger(name=CONFIG['logger'], debug=args.debug)
|
|
27
|
+
starttime = time()
|
|
28
|
+
|
|
29
|
+
LOG.info(f'Automagix Version {VERSION}')
|
|
30
|
+
LOG.info(f'Started at: {strftime("%a, %d %b %Y %H:%M:%S UTC", gmtime(starttime))}')
|
|
31
|
+
|
|
32
|
+
configfile = CONFIG.get('config_file')
|
|
33
|
+
if configfile:
|
|
34
|
+
LOG.info(f'Using configuration from: {configfile}')
|
|
35
|
+
else:
|
|
36
|
+
LOG.warning('Configuration file not found or not configured. Using defaults.')
|
|
37
|
+
|
|
38
|
+
return starttime
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def check_screen():
|
|
42
|
+
p = subprocess.run('screen -v', shell=True, stdout=subprocess.PIPE)
|
|
43
|
+
screen_version = p.stdout.decode()
|
|
44
|
+
if p.returncode != 0 or 'command not found' in screen_version:
|
|
45
|
+
raise Exception('No GNU screen version found')
|
|
46
|
+
if 'FAU' in screen_version:
|
|
47
|
+
LOG.error(
|
|
48
|
+
'Parallel processing only supported for the "GNU" version of screen. You have the "FAU" version.\n'
|
|
49
|
+
'On MacOS you can try to install the GNU version via Homebrew: `brew install screen`.'
|
|
50
|
+
)
|
|
51
|
+
raise Exception('No supported GNU screen version found')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
if os.getenv('AUTOMAGIX_SHELL'):
|
|
56
|
+
print('You are running Automagix from an interactive shell of an already running Automagix!')
|
|
57
|
+
empty_queued_input_data()
|
|
58
|
+
answer = input('Do you really want to proceed? Then type "yes" and ENTER.\n')
|
|
59
|
+
if answer != 'yes':
|
|
60
|
+
sys.exit(0)
|
|
61
|
+
|
|
62
|
+
args = arguments()
|
|
63
|
+
run_startup_script()
|
|
64
|
+
|
|
65
|
+
starttime = setup(args=args)
|
|
66
|
+
|
|
67
|
+
script, batch_items = get_script_and_batch_items(args=args)
|
|
68
|
+
|
|
69
|
+
if args.jump_to == MAGIC_SELECTION_INT:
|
|
70
|
+
# next(iter(pi.items())) is here needed, because the pipeline items are all dictionaries with only one key.
|
|
71
|
+
pipeline_items = [next(iter(pi.items())) for pi in script['pipeline']]
|
|
72
|
+
args.jump_to = selector(
|
|
73
|
+
entries=[
|
|
74
|
+
(i, f'[{key}]: {cmd}')
|
|
75
|
+
for i, (key, cmd) in enumerate(pipeline_items)
|
|
76
|
+
],
|
|
77
|
+
message='Please choose index of desired start command:'
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if args.vars_file and args.parallel:
|
|
81
|
+
check_screen()
|
|
82
|
+
run_parallel_screens(script=script, batch_items=batch_items, args=args)
|
|
83
|
+
sys.exit(0)
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
if CONFIG['progress_bar']:
|
|
87
|
+
setup_scroll_area()
|
|
88
|
+
|
|
89
|
+
run_batch_items(script=script, batch_items=batch_items, args=args)
|
|
90
|
+
finally:
|
|
91
|
+
if CONFIG['progress_bar']:
|
|
92
|
+
destroy_scroll_area()
|
|
93
|
+
|
|
94
|
+
if 'AUTOMAGIX_TIME' in os.environ:
|
|
95
|
+
LOG.info(f'The Automagix script took {round(time() - starttime)}s!')
|
automagix/automagix.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from argparse import Namespace
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
from .command import Command, AbortException, SkipBatchItemException, PERSISTENT_VARS, ReloadFromFile
|
|
8
|
+
from .config import get_script
|
|
9
|
+
from .environment import PipelineEnvironment
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Automagix:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
script: dict,
|
|
16
|
+
variables: dict,
|
|
17
|
+
config: dict,
|
|
18
|
+
script_fields: OrderedDict,
|
|
19
|
+
cmd_args: Namespace,
|
|
20
|
+
batch_index: int,
|
|
21
|
+
):
|
|
22
|
+
self.script = script
|
|
23
|
+
self.script_fields = script_fields
|
|
24
|
+
self.env = PipelineEnvironment(
|
|
25
|
+
config=config,
|
|
26
|
+
script=script,
|
|
27
|
+
variables=variables,
|
|
28
|
+
batch_index=batch_index,
|
|
29
|
+
cmd_args=cmd_args,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
self._command_lists: dict = {}
|
|
33
|
+
|
|
34
|
+
@cached_property
|
|
35
|
+
def cmd_class(self) -> type:
|
|
36
|
+
if self.env.config.get('bundlewrap'):
|
|
37
|
+
from .bundlewrap import BWCommand, AutomagixBwRepo
|
|
38
|
+
|
|
39
|
+
self.env.config['bw_repo'] = AutomagixBwRepo(repo_path=os.environ.get('BW_REPO_PATH', '.'))
|
|
40
|
+
return BWCommand
|
|
41
|
+
else:
|
|
42
|
+
return Command
|
|
43
|
+
|
|
44
|
+
def command_list(self, pipeline: str) -> list:
|
|
45
|
+
if pipeline == 'main':
|
|
46
|
+
pipeline = 'pipeline'
|
|
47
|
+
if not self._command_lists.get(pipeline):
|
|
48
|
+
self._command_lists[pipeline] = self.build_command_list(pipeline=pipeline)
|
|
49
|
+
return self._command_lists[pipeline]
|
|
50
|
+
|
|
51
|
+
def get_command_position(self, index: int, pipeline: str) -> int:
|
|
52
|
+
if pipeline == 'always':
|
|
53
|
+
return index
|
|
54
|
+
if pipeline in ['pipeline', 'main']:
|
|
55
|
+
return index + len(self.command_list('always'))
|
|
56
|
+
if pipeline == 'cleanup':
|
|
57
|
+
return index + len(self.command_list('always')) + len(self.command_list('main'))
|
|
58
|
+
|
|
59
|
+
def set_command_count(self):
|
|
60
|
+
if not self.env.command_count:
|
|
61
|
+
self.env.command_count = len(
|
|
62
|
+
self.command_list('always') + self.command_list('main') + self.command_list('cleanup')
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def build_command_list(self, pipeline: str) -> list[Command]:
|
|
66
|
+
command_list = []
|
|
67
|
+
for index, cmd in enumerate(self.script.get(pipeline, [])):
|
|
68
|
+
new_cmd = self.cmd_class(
|
|
69
|
+
cmd=cmd,
|
|
70
|
+
index=index,
|
|
71
|
+
env=self.env,
|
|
72
|
+
pipeline=pipeline,
|
|
73
|
+
position=self.get_command_position(index=index, pipeline=pipeline),
|
|
74
|
+
)
|
|
75
|
+
command_list.append(new_cmd)
|
|
76
|
+
if new_cmd.assignment_var and new_cmd.assignment_var not in self.env.vars:
|
|
77
|
+
self.env.vars[new_cmd.assignment_var] = f'{{{new_cmd.assignment_var}}}'
|
|
78
|
+
return command_list
|
|
79
|
+
|
|
80
|
+
def reload_script(self):
|
|
81
|
+
self.script = get_script(args=self.env.cmd_args)
|
|
82
|
+
self._command_lists = {} # Clear cache
|
|
83
|
+
|
|
84
|
+
def print_main_data(self):
|
|
85
|
+
print('\n')
|
|
86
|
+
self.env.LOG.info(' ------ Overview ------')
|
|
87
|
+
for field_key, field_value in self.script_fields.items():
|
|
88
|
+
print()
|
|
89
|
+
self.env.LOG.info(f'{field_value}:')
|
|
90
|
+
for key, value in self.script.get(field_key, {}).items():
|
|
91
|
+
self.env.LOG.info(f" {key}: {value}")
|
|
92
|
+
|
|
93
|
+
def print_command_line_steps(self, command_list: list[Command]):
|
|
94
|
+
print()
|
|
95
|
+
self.env.LOG.info('Commandline Steps:')
|
|
96
|
+
for cmd in command_list:
|
|
97
|
+
self.env.LOG.info(f"({cmd.index}) [{cmd.orig_key}]: {cmd.get_resolved_value(dummy=True)}")
|
|
98
|
+
print()
|
|
99
|
+
|
|
100
|
+
def check_possibly_dangerous_vars(self):
|
|
101
|
+
warn = 0
|
|
102
|
+
for key, value in self.env.vars.items():
|
|
103
|
+
if isinstance(value, str) and value.lower() == 'false':
|
|
104
|
+
self.env.LOG.warning(
|
|
105
|
+
f'[vars:{key}] This variable is a string with value "{value}".'
|
|
106
|
+
' Be aware that this becomes `True` for conditions and other boolean operations without conversion,'
|
|
107
|
+
' because any non-empty string in Python is evaluated as `True`.'
|
|
108
|
+
)
|
|
109
|
+
warn += 1
|
|
110
|
+
|
|
111
|
+
if warn:
|
|
112
|
+
answer = self.env.interact(question='Do you want to proceed? Then type "yes" and ENTER.\n')
|
|
113
|
+
if answer != 'yes':
|
|
114
|
+
sys.exit(0)
|
|
115
|
+
|
|
116
|
+
def _execute_command_list(self, name: str, start_index: int, treat_as_main: bool):
|
|
117
|
+
try:
|
|
118
|
+
steps = self.script.get('_steps')
|
|
119
|
+
for cmd in self.command_list(name)[start_index:]:
|
|
120
|
+
if treat_as_main:
|
|
121
|
+
if steps and (self.script['_exclude'] == (cmd.index in steps)):
|
|
122
|
+
# Case 1: exclude is True and index is in steps => skip
|
|
123
|
+
# Case 2: exclude is False and index is in steps => execute
|
|
124
|
+
print()
|
|
125
|
+
self.env.LOG.notice(f'({cmd.index}) Not selected for execution: skip')
|
|
126
|
+
continue
|
|
127
|
+
cmd.execute(interactive=self.env.cmd_args.interactive, force=self.env.cmd_args.force)
|
|
128
|
+
else:
|
|
129
|
+
cmd.execute()
|
|
130
|
+
except ReloadFromFile as exc:
|
|
131
|
+
print()
|
|
132
|
+
self.env.LOG.info(f'Reload script from file and retry => ({exc.index})')
|
|
133
|
+
self.reload_script()
|
|
134
|
+
self._execute_command_list(name=name, start_index=exc.index, treat_as_main=treat_as_main)
|
|
135
|
+
|
|
136
|
+
def execute_pipeline(self, name: str):
|
|
137
|
+
if not self.command_list(name):
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
if name == 'main':
|
|
141
|
+
treat_as_main = True
|
|
142
|
+
start_index = self.env.cmd_args.jump_to
|
|
143
|
+
else:
|
|
144
|
+
treat_as_main = False
|
|
145
|
+
start_index = 0
|
|
146
|
+
|
|
147
|
+
print()
|
|
148
|
+
self.env.LOG.info('------------------------------')
|
|
149
|
+
self.env.LOG.info(f' --- Start {name.upper()} pipeline ---')
|
|
150
|
+
|
|
151
|
+
self._execute_command_list(name=name, start_index=start_index, treat_as_main=treat_as_main)
|
|
152
|
+
|
|
153
|
+
print()
|
|
154
|
+
self.env.LOG.info(f' --- End {name.upper()} pipeline ---')
|
|
155
|
+
self.env.LOG.info('------------------------------\n')
|
|
156
|
+
|
|
157
|
+
def run(self):
|
|
158
|
+
print('\n')
|
|
159
|
+
self.env.LOG.info('//////////////////////////////////////////////////////////////////////')
|
|
160
|
+
self.env.LOG.info(f"---- {self.script['name']} ----")
|
|
161
|
+
self.env.LOG.info('//////////////////////////////////////////////////////////////////////')
|
|
162
|
+
|
|
163
|
+
PERSISTENT_VARS.clear()
|
|
164
|
+
|
|
165
|
+
self.execute_pipeline(name='always')
|
|
166
|
+
|
|
167
|
+
self.print_main_data()
|
|
168
|
+
self.print_command_line_steps(command_list=self.command_list('main'))
|
|
169
|
+
self.check_possibly_dangerous_vars()
|
|
170
|
+
if self.env.cmd_args.print_overview:
|
|
171
|
+
sys.exit(0)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
self.execute_pipeline(name='main')
|
|
175
|
+
except (AbortException, SkipBatchItemException):
|
|
176
|
+
self.env.LOG.debug('Abort requested. Cleaning up.')
|
|
177
|
+
self.execute_pipeline(name='cleanup')
|
|
178
|
+
self.env.LOG.debug('Clean up done. Exiting.')
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
self.execute_pipeline(name='cleanup')
|
|
182
|
+
|
|
183
|
+
self.env.LOG.info('---------------------------------------------------------------')
|
|
184
|
+
self.env.LOG.info('Automagix finished: Congratulations and have a N.I.C.E. day :-)')
|
|
185
|
+
self.env.LOG.info('---------------------------------------------------------------')
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from automagix.command import Command
|
|
2
|
+
from tests.test_environment import testauto, environment
|
|
3
|
+
|
|
4
|
+
len_always = len(testauto.script.get('always', []))
|
|
5
|
+
len_main = len(testauto.script.get('pipeline', []))
|
|
6
|
+
len_cleanup = len(testauto.script.get('cleanup', []))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test__automagix__command_list():
|
|
10
|
+
assert testauto.command_list('main') == testauto.command_list('pipeline')
|
|
11
|
+
cmd = Command(
|
|
12
|
+
cmd={'local': "echo 'Print this always :-)'"},
|
|
13
|
+
index=0,
|
|
14
|
+
pipeline='always',
|
|
15
|
+
env=environment,
|
|
16
|
+
position=0,
|
|
17
|
+
)
|
|
18
|
+
first_cleanup = testauto.command_list('always')[0]
|
|
19
|
+
assert isinstance(first_cleanup, Command)
|
|
20
|
+
assert vars(first_cleanup) == vars(cmd)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test__automagix__get_command_position():
|
|
24
|
+
assert testauto.get_command_position(index=4, pipeline='always') == 4
|
|
25
|
+
assert testauto.get_command_position(index=3, pipeline='always') != 4
|
|
26
|
+
assert testauto.get_command_position(index=3, pipeline='main') == len_always + 3
|
|
27
|
+
assert testauto.get_command_position(index=3, pipeline='pipeline') == len_always + 3
|
|
28
|
+
assert testauto.get_command_position(index=2, pipeline='cleanup') == len_always + len_main + 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test__automagix__set_command_count():
|
|
32
|
+
testauto.env.command_count = None
|
|
33
|
+
testauto.set_command_count()
|
|
34
|
+
assert testauto.env.command_count == len_always + len_main + len_cleanup
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import locale
|
|
2
|
+
import subprocess
|
|
3
|
+
from argparse import Action, Namespace
|
|
4
|
+
|
|
5
|
+
from argcomplete import warn
|
|
6
|
+
|
|
7
|
+
from .helpers import read_yaml, search_script
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _call(*args, **kwargs):
|
|
11
|
+
try:
|
|
12
|
+
return subprocess.check_output(*args, **kwargs).decode(locale.getpreferredencoding()).splitlines()
|
|
13
|
+
except subprocess.CalledProcessError:
|
|
14
|
+
return []
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ScriptFileCompleter:
|
|
18
|
+
"""
|
|
19
|
+
Scriptfile completer
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, script_dir: str):
|
|
23
|
+
self.script_dir = script_dir
|
|
24
|
+
|
|
25
|
+
def __call__(self, prefix: str, **kwargs):
|
|
26
|
+
completion = []
|
|
27
|
+
try:
|
|
28
|
+
completion.extend(self.find_dirs_and_files(root_path='.', prefix=prefix))
|
|
29
|
+
completion.extend(self.find_dirs_and_files(root_path=self.script_dir, prefix=prefix))
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
warn(f'Shell completion failed: {repr(exc)}')
|
|
32
|
+
return completion
|
|
33
|
+
|
|
34
|
+
def find_dirs_and_files(self, root_path: str, prefix: str) -> list:
|
|
35
|
+
completion = []
|
|
36
|
+
pre_len = len(root_path) + 1
|
|
37
|
+
directories = _call(["bash", "-c", f"compgen -A directory -- '{root_path}/{prefix}'"])
|
|
38
|
+
completion += [f'{d[pre_len:]}/' for d in directories]
|
|
39
|
+
for ext in ['yaml', 'yml']:
|
|
40
|
+
files = _call(["bash", "-c", f"compgen -A file -X '!*.{ext}' -- '{root_path}/{prefix}'"])
|
|
41
|
+
completion += [f[pre_len:] for f in files]
|
|
42
|
+
return completion
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ScriptFieldCompleter:
|
|
46
|
+
def __init__(self, script_dir: str):
|
|
47
|
+
self.script_dir = script_dir
|
|
48
|
+
|
|
49
|
+
def __call__(self, action: Action, parsed_args: Namespace, **kwargs):
|
|
50
|
+
try:
|
|
51
|
+
if parsed_args.scriptfile is None:
|
|
52
|
+
return []
|
|
53
|
+
|
|
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
|
+
script = read_yaml(s_file)
|
|
60
|
+
completion = [f'{key}=' for key in script.get(action.dest, {}).keys()]
|
|
61
|
+
|
|
62
|
+
return completion
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
warn(f'Shell completion failed: {repr(exc)}')
|
|
65
|
+
return []
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from argparse import Namespace
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from csv import DictReader
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from .automagix import Automagix
|
|
8
|
+
from .command import SkipBatchItemException, AbortException
|
|
9
|
+
from .config import CONFIG, get_script, LOG, update_script_from_row, collect_vars, SCRIPT_FIELDS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_script_and_batch_items(args: Namespace) -> (dict, list):
|
|
13
|
+
script = get_script(args=args)
|
|
14
|
+
|
|
15
|
+
# Empty item means: there is nothing to update, take the script as it is
|
|
16
|
+
batch_items: list[dict] = [{}]
|
|
17
|
+
if args.vars_file:
|
|
18
|
+
with open(args.vars_file) as csvfile:
|
|
19
|
+
batch_items = list(DictReader(filter(lambda row: row[0] != '#', csvfile)))
|
|
20
|
+
|
|
21
|
+
return script, batch_items
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_automagix_list(script: dict, batch_items: list, args: Namespace) -> list[Automagix]:
|
|
25
|
+
automagix_list = []
|
|
26
|
+
for i, row in enumerate(batch_items, start=1):
|
|
27
|
+
script_copy = deepcopy(script)
|
|
28
|
+
script_copy['_batch_mode'] = len(batch_items) > 1
|
|
29
|
+
script_copy['_batch_items_count'] = len(batch_items)
|
|
30
|
+
|
|
31
|
+
update_script_from_row(row=row, script=script_copy, index=i)
|
|
32
|
+
|
|
33
|
+
variables = collect_vars(script_copy)
|
|
34
|
+
|
|
35
|
+
auto = Automagix(
|
|
36
|
+
script=script_copy,
|
|
37
|
+
variables=variables,
|
|
38
|
+
config=CONFIG,
|
|
39
|
+
script_fields=SCRIPT_FIELDS,
|
|
40
|
+
cmd_args=args,
|
|
41
|
+
batch_index=i,
|
|
42
|
+
)
|
|
43
|
+
automagix_list.append(auto)
|
|
44
|
+
return automagix_list
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_automagix_list(automagix_list: list[Automagix], send_status_callback: Callable = None):
|
|
48
|
+
for auto in automagix_list:
|
|
49
|
+
auto.set_command_count()
|
|
50
|
+
auto.env.attach_logger()
|
|
51
|
+
auto.env.reinit_logger()
|
|
52
|
+
if send_status_callback:
|
|
53
|
+
auto.env.send_status = send_status_callback
|
|
54
|
+
try:
|
|
55
|
+
auto.run()
|
|
56
|
+
except SkipBatchItemException as exc:
|
|
57
|
+
LOG.info(str(exc))
|
|
58
|
+
LOG.notice('=====> Jumping to next batch item.')
|
|
59
|
+
continue
|
|
60
|
+
except AbortException as exc:
|
|
61
|
+
sys.exit(int(exc))
|
|
62
|
+
except KeyboardInterrupt:
|
|
63
|
+
print()
|
|
64
|
+
LOG.warning('Aborted by user. Exiting.')
|
|
65
|
+
sys.exit(130)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_batch_items(script: dict, batch_items: list, args: Namespace):
|
|
69
|
+
automagix_list = create_automagix_list(script=script, batch_items=batch_items, args=args)
|
|
70
|
+
run_automagix_list(automagix_list=automagix_list)
|
automagix/bundlewrap.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from bundlewrap.exceptions import NoSuchNode, NoSuchGroup
|
|
2
|
+
from bundlewrap.group import Group
|
|
3
|
+
from bundlewrap.node import Node
|
|
4
|
+
from bundlewrap.repo import Repository
|
|
5
|
+
|
|
6
|
+
from .command import Command, PA
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AutomagixBwRepo(Repository):
|
|
10
|
+
def reload(self):
|
|
11
|
+
self.__init__(repo_path=self.path)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BWCommand(Command):
|
|
15
|
+
def _generate_python_vars(self):
|
|
16
|
+
locale_vars = {'AUTOMAGIX_BW_REPO': self.env.config['bw_repo']}
|
|
17
|
+
for key, value in self.env.systems.items():
|
|
18
|
+
if not value.startswith('hostname!'):
|
|
19
|
+
try:
|
|
20
|
+
self.env.config['bw_repo'].get_node(value)
|
|
21
|
+
except NoSuchNode:
|
|
22
|
+
try:
|
|
23
|
+
self.env.config['bw_repo'].get_group(value)
|
|
24
|
+
except NoSuchGroup:
|
|
25
|
+
self.env.LOG.warning(f'"{value}" is neither a BW node nor a BW group')
|
|
26
|
+
locale_vars['VARS'] = self.env.vars
|
|
27
|
+
locale_vars['NODES'] = BWNodesWrapper(repo=self.env.config['bw_repo'], systems=self.env.systems)
|
|
28
|
+
return locale_vars
|
|
29
|
+
|
|
30
|
+
def _remote_action(self) -> int:
|
|
31
|
+
bw_repo: Repository = self.env.config['bw_repo']
|
|
32
|
+
system = self.get_system()
|
|
33
|
+
if system.startswith('hostname!'):
|
|
34
|
+
return self._remote_action_on_hostname(hostname=system.replace('hostname!', ''))
|
|
35
|
+
try:
|
|
36
|
+
node: Node = bw_repo.get_node(system)
|
|
37
|
+
return self._remote_action_on_hostname(hostname=node.hostname)
|
|
38
|
+
except NoSuchNode as exc:
|
|
39
|
+
try:
|
|
40
|
+
group: Group = bw_repo.get_group(system)
|
|
41
|
+
except NoSuchGroup:
|
|
42
|
+
raise exc
|
|
43
|
+
print()
|
|
44
|
+
self.env.LOG.info(f' --- Executing command for all nodes in BW group >{group.name}< ---')
|
|
45
|
+
for node in group.nodes:
|
|
46
|
+
print()
|
|
47
|
+
self.env.LOG.info(f'- {node.name} -')
|
|
48
|
+
self._remote_bw_group_action(node=node)
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
def _remote_bw_group_action(self, node: Node):
|
|
52
|
+
return_code = self._remote_action_on_hostname(hostname=node.hostname)
|
|
53
|
+
if return_code != 0:
|
|
54
|
+
self.env.LOG.error(f'Command ({self.index}) on {node.name} failed with return code {return_code}.')
|
|
55
|
+
if self.env.cmd_args.force:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
err_answer = self._ask_user(
|
|
59
|
+
question='[PF] What should I do?',
|
|
60
|
+
allowed_options=[PA.proceed, PA.terminal, PA.variables, PA.retry, PA.abort],
|
|
61
|
+
)
|
|
62
|
+
# _ask_user handles are answers but PA.retry, PA.skip, PA.proceed
|
|
63
|
+
# PA.skip is not in the allowed options'
|
|
64
|
+
# PA.proceed means 'proceed' so we can just go on
|
|
65
|
+
if err_answer == PA.retry.answer:
|
|
66
|
+
return self._remote_bw_group_action(node=node)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BWNodesWrapper:
|
|
70
|
+
def __init__(self, repo: Repository, systems: dict):
|
|
71
|
+
self._repo = repo
|
|
72
|
+
self._systems = systems
|
|
73
|
+
|
|
74
|
+
def __getattr__(self, name):
|
|
75
|
+
return self._repo.get_node(self._systems[name])
|
automagix/colors.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
BLUE = '\033[34m'
|
|
2
|
+
CYAN = '\033[36m'
|
|
3
|
+
GREEN = '\033[32m'
|
|
4
|
+
RED = '\033[31m'
|
|
5
|
+
YELLOW = '\033[33m'
|
|
6
|
+
|
|
7
|
+
BOLD = '\033[1m'
|
|
8
|
+
ITALIC = '\033[3m'
|
|
9
|
+
|
|
10
|
+
RESET = '\033[0m'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def blue(text):
|
|
14
|
+
return f'{BLUE}{text}{RESET}'
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cyan(text):
|
|
18
|
+
return f'{CYAN}{text}{RESET}'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def green(text):
|
|
22
|
+
return f'{GREEN}{text}{RESET}'
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def red(text):
|
|
26
|
+
return f'{RED}{text}{RESET}'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def yellow(text):
|
|
30
|
+
return f'{YELLOW}{text}{RESET}'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def bold(text):
|
|
34
|
+
return f'{BOLD}{text}{RESET}'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def italic(text):
|
|
38
|
+
return f'{ITALIC}{text}{RESET}'
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def nocolor(text):
|
|
42
|
+
return f'{RESET}{text}'
|