Skip to content

Commit

Permalink
Merge pull request #10 from vanadinit/some_improvements
Browse files Browse the repository at this point in the history
Bash Path, Booleans and progress bar
  • Loading branch information
vanadinit authored Jul 20, 2024
2 parents e44161f + 56d78c4 commit b4c1734
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 61 deletions.
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# automatix
Automation wrapper for bash and python commands. Extended Feature version.
Automation wrapper for bash and python commands. Extended Features version.


# DESCRIPTION
Expand All @@ -24,9 +24,9 @@ There are different modes for **automatix** to work. Without any
the possibility to use your own logging library.

This **automatix** version (automatix_cmd) is a fork of the original
**automatix** (https://github.com/seibert-media/automatix) with some
extended functionality and is maintained private in the authors freetime
(not maintained by seibert//).
**automatix** (https://github.com/seibert-media/automatix) with some
extended functionality and is maintained private in the authors free
time (not maintained by seibert//).

## Warning:

Expand Down Expand Up @@ -79,6 +79,9 @@ All (string) configuration values can be overwritten by the

# Path for shell imports
import_path: '.'

# Path to local bash (default: /bin/bash)
bash_path: '/bin/bash'

# SSH Command used for remote connections
ssh_cmd: 'ssh {hostname} sudo '
Expand Down Expand Up @@ -306,7 +309,8 @@ Here you define the commands automatix shall execute.
confirm, that automatix may proceed.

2) **local**: Local shell command to execute. Imports will be sourced
beforehand. /bin/bash will be used for execution.
beforehand. The Bash specified in `bash_path` (default: /bin/bash) will
be used for execution.

3) **remote@systemname**: Remote shell command to execute. Systemname
has to be a defined system. The command will be run via SSH (without
Expand Down Expand Up @@ -392,13 +396,13 @@ Intended use case for **cleanup**: Remove temporary files or artifacts.

**AUTOMATIX_**_config-variable-in-upper-case_: Set or overwrite the
corresponding configuration value. See **CONFIGURATION** section.
Works only for string values!
Works only for string and boolean values!
String values (case-insensitive 'true' or 'false') are converted
to `True` or `False` in Python, if the fields expects a boolean.
**All other values (int, float, dict, list, ...) are ignored!**

**AUTOMATIX_TIME**: Set this to an arbitrary value to print the times
for the single steps and the whole script.

Additionally you can modify the environment to adjust things to your
needs.
for the single steps and the whole script, e.g. `AUTOMATIX_TIME=true`.


# TIPS & TRICKS
Expand Down Expand Up @@ -527,9 +531,9 @@ User input questions are of following categories:
- [RR] **R**emote process still **R**unning
- [SE] **S**yntax **E**rror

The terminal (T) answer starts an interactive Bash-Shell (/bin/bash -i).
The terminal (T) answer starts an interactive Bash-Shell.
Therefore .bashrc is executed, but the command prompt (PS1) is
replaced to indicate, we are still in an automatix process.
replaced to indicate, that we are still in an automatix process.


# EXTRAS
Expand Down Expand Up @@ -575,13 +579,11 @@ or activation for automatix (e.g. in `.bashrc`)
Automatix will recognize the installed module and offer the completion automatically.

## Progress bar (experimental)
For activation of an "apt-like" progress bar based on the amount of commands
install `python_progress_bar` via pip and either set `AUTOMATIX_PROGRESS_BAR`
environment variable to an arbitrary value (not "False") or set `progress_bar`
to `true` in the config file.
You can activate an "apt-like" progress bar based on the amount of commands
by setting the configuration option `progress_bar` to `True` (config file or environment).

You can force deactivation in setting `AUTOMATIX_PROGRESS_BAR` environment variable
to "False" (overwrites config file setting).
The status on the right displays `[elapsed time<remaining time, rate]`,
where rate is percentage/second if fast and second/percentage if slow.

Note, that using commands that heavily modify the terminal behaviour/output
(such as `top`, `watch`, `glances`, ...), may lead to a unreadable
Expand Down
12 changes: 6 additions & 6 deletions automatix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
from .automatix import Automatix
from .command import SkipBatchItemException, AbortException
from .config import (
arguments, CONFIG, get_script, LOG, update_script_from_row, collect_vars, SCRIPT_FIELDS, VERSION, init_logger,
PROGRESS_BAR, progress_bar
arguments, CONFIG, get_script, LOG, update_script_from_row, collect_vars, SCRIPT_FIELDS, VERSION, init_logger
)
from .parallel import screen_switch_loop
from .progress_bar import setup_scroll_area, destroy_scroll_area


def check_for_original_automatix():
Expand Down Expand Up @@ -172,13 +172,13 @@ def main():
sys.exit(0)

try:
if PROGRESS_BAR:
progress_bar.setup_scroll_area()
if CONFIG['progress_bar']:
setup_scroll_area()

run_batch_items(script=script, batch_items=batch_items, args=args)
finally:
if PROGRESS_BAR:
progress_bar.destroy_scroll_area()
if CONFIG['progress_bar']:
destroy_scroll_area()

if 'AUTOMATIX_TIME' in os.environ:
LOG.info(f'The Automatix script took {round(time() - starttime)}s!')
31 changes: 16 additions & 15 deletions automatix/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from time import time
from typing import Tuple

from .config import PROGRESS_BAR, progress_bar
from .environment import PipelineEnvironment
from .progress_bar import draw_progress_bar, block_progress_bar


class PersistentDict(dict):
Expand All @@ -22,14 +22,12 @@ def __setattr__(self, key: str, value):

PERSISTENT_VARS = PVARS = PersistentDict()

SHELL_EXECUTABLE = '/bin/bash'

# Leading red " Automatix \w > " to indicate that this shell is inside a running Automatix execution
AUTOMATIX_PROMPT = r'\[\033[0;31m\] Automatix \[\033[0m\]\\w > '

POSSIBLE_ANSWERS = {
'p': 'proceed (default)',
'T': f'start interactive terminal shell ({SHELL_EXECUTABLE} -i) and return back here on exit',
'T': 'start interactive terminal shell ({bash_path} -i) and return back here on exit',
'v': 'show and change variables',
'r': 'retry',
'R': 'reload from file and retry command (same index)',
Expand All @@ -53,6 +51,8 @@ def __init__(self, cmd: dict, index: int, pipeline: str, env: PipelineEnvironmen
self.pipeline = pipeline
self.position = position

self.bash_path = self.env.config['bash_path']

for key, value in cmd.items():
self.orig_key = key
self.condition_var, self.assignment_var, self.key = parse_key(key=key)
Expand Down Expand Up @@ -132,8 +132,8 @@ def execute(self, interactive: bool = False, force: bool = False):
self.env.LOG.exception('Syntax or value error!')
self.env.LOG.error('Syntax or value error! Please fix your script and reload/restart.')
self._ask_user(question='[SE] What should I do?', allowed_options=['R', 'T', 'v', 's', 'a'])
if PROGRESS_BAR:
progress_bar.draw_progress_bar(self.progress_portion)
if self.env.config['progress_bar']:
draw_progress_bar(self.progress_portion)

def _execute(self, interactive: bool = False, force: bool = False):
self.print_command()
Expand Down Expand Up @@ -220,15 +220,16 @@ def _ask_user(self, question: str, allowed_options: list) -> str:
allowed_options.insert(allowed_options.index('R') + 1, 'R±X')

options = '\n'.join([f' {k}: {POSSIBLE_ANSWERS[k]}' for k in allowed_options])
formatted_options = options.format(bash_path=self.bash_path)

return self._ask_user_with_options(
question=f'{question}\n{options}\nYour answer: \a',
question=f'{question}\n{formatted_options}\nYour answer: \a',
allowed_options=allowed_options,
)

def _ask_user_with_options(self, question: str, allowed_options: list) -> str:
if PROGRESS_BAR:
progress_bar.block_progress_bar(self.progress_portion)
if self.env.config['progress_bar']:
block_progress_bar(self.progress_portion)
self.env.send_status('user_input_add')
answer = input(question)
self.env.send_status('user_input_remove')
Expand All @@ -252,7 +253,7 @@ def _ask_user_with_options(self, question: str, allowed_options: list) -> str:
self.env.LOG.notice('Starting interactive terminal shell')
self._run_local_command(
f'AUTOMATIX_SHELL=True'
f' {SHELL_EXECUTABLE}'
f' {self.bash_path}'
f' --rcfile <(cat ~/.bashrc ; echo "PS1=\\"{AUTOMATIX_PROMPT}\\"")'
f' -i'
)
Expand Down Expand Up @@ -327,13 +328,13 @@ def _build_command(self, path: str) -> str:
def _run_local_command(self, cmd: str) -> int:
self.env.LOG.debug(f'Executing: {cmd}')
if self.assignment_var:
proc = subprocess.run(cmd, shell=True, executable=SHELL_EXECUTABLE, stdout=subprocess.PIPE)
proc = subprocess.run(cmd, shell=True, executable=self.bash_path, stdout=subprocess.PIPE)
output = proc.stdout.decode(self.env.config["encoding"])
self.env.vars[self.assignment_var] = assigned_value = output.rstrip('\r\n')
hint = ' (trailing newline removed)' if (output.endswith('\n') or output.endswith('\r')) else ''
self.env.LOG.info(f'Variable {self.assignment_var} = "{assigned_value}"{hint}')
else:
proc = subprocess.run(cmd, shell=True, executable=SHELL_EXECUTABLE)
proc = subprocess.run(cmd, shell=True, executable=self.bash_path)
return proc.returncode

def _remote_action(self) -> int:
Expand Down Expand Up @@ -371,7 +372,7 @@ def _get_remote_command(self, hostname: str) -> str:

return f'{prefix}{ssh_cmd}{quote("bash -c " + quote(remote_cmd))}'

def _remote_handle_keyboard_interrupt(self, hostname: str) -> int:
def _remote_handle_keyboard_interrupt(self, hostname: str):
ssh_cmd = self.env.config["ssh_cmd"].format(hostname=hostname)

try:
Expand Down Expand Up @@ -411,7 +412,7 @@ def get_remote_pids(self, hostname, cmd) -> []:
pids = subprocess.check_output(
cmd,
shell=True,
executable=SHELL_EXECUTABLE,
executable=self.bash_path,
).decode(self.env.config["encoding"]).split()

return pids
Expand All @@ -420,7 +421,7 @@ def _remote_cleanup_imports(self, hostname: str):
ssh_cmd = self.env.config["ssh_cmd"].format(hostname=hostname)
cleanup_cmd = f'{ssh_cmd} rm -r {self.env.config["remote_tmp_dir"]}'
self.env.LOG.debug(f'Executing: {cleanup_cmd}')
proc = subprocess.run(cleanup_cmd, shell=True, executable=SHELL_EXECUTABLE)
proc = subprocess.run(cleanup_cmd, shell=True, executable=self.bash_path)
if proc.returncode != 0:
self.env.LOG.warning(
'Failed to remove {tmp_dir}, exitcode: {return_code}'.format(
Expand Down
33 changes: 18 additions & 15 deletions automatix/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

import yaml

from .colors import red

yaml.warnings({'YAMLLoadWarning': False})


Expand Down Expand Up @@ -46,24 +48,36 @@ def read_yaml(yamlfile: str) -> dict:
'constants': {},
'encoding': 'utf-8',
'import_path': '.',
'bash_path': '/bin/bash',
'ssh_cmd': 'ssh {hostname} sudo ',
'remote_tmp_dir': 'automatix_tmp',
'logger': 'automatix',
'logfile_dir': 'automatix_logs',
'bundlewrap': False,
'teamvault': False,
'progress_bar': False,
}

configfile = os.path.expanduser(os.path.expandvars(os.getenv('AUTOMATIX_CONFIG', '~/.automatix.cfg.yaml')))
if os.path.isfile(configfile):
CONFIG.update(read_yaml(configfile))
CONFIG['config_file'] = configfile

for key, value in CONFIG.items():
if not isinstance(value, str):
for c_key, c_value in CONFIG.items():
env_value = os.getenv(f'AUTOMATIX_{c_key.upper()}')
if not env_value:
continue

if isinstance(c_value, bool) and env_value.lower() in ['false', 'true']:
CONFIG[c_key] = True if env_value.lower() == 'true' else False
continue

if isinstance(c_value, str):
CONFIG[c_key] = env_value
continue
if os.getenv(f'AUTOMATIX_{key.upper()}'):
CONFIG[key] = os.getenv(f'AUTOMATIX_{key.upper()}')

print(red(f'Warning: environment variable "AUTOMATIX_{c_key.upper()}" ignored: wrong value type!'))
sleep(2)

if CONFIG.get('logging_lib'):
log_lib = import_module(CONFIG.get('logging_lib'))
Expand All @@ -83,17 +97,6 @@ def read_yaml(yamlfile: str) -> dict:
class UnknownSecretTypeException(Exception):
pass

progress_bar = None
PROGRESS_BAR = False
if (CONFIG.get('progress_bar', False) or os.getenv('AUTOMATIX_PROGRESS_BAR', False)) and \
os.getenv('AUTOMATIX_PROGRESS_BAR', False) not in ['False', 'false']:
try:
import python_progress_bar as progress_bar # noqa F401

PROGRESS_BAR = True
except ImportError:
pass


def arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
Expand Down
14 changes: 8 additions & 6 deletions automatix/parallel.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import argparse
import os
import pickle
import select
import subprocess
import sys
from dataclasses import dataclass, field
from os import listdir, unlink
from os.path import isfile
from time import sleep

import select

from .automatix import Automatix
from .colors import yellow, green, red, cyan
from .command import AbortException
from .config import LOG, init_logger, CONFIG, PROGRESS_BAR, progress_bar
from .config import LOG, init_logger, CONFIG
from .progress_bar import setup_scroll_area, destroy_scroll_area

LINE_UP = '\033[1A'
LINE_CLEAR = '\x1b[2K'
Expand Down Expand Up @@ -213,13 +215,13 @@ def run_auto_from_file():
args = parser.parse_args()

try:
if PROGRESS_BAR:
progress_bar.setup_scroll_area()
if CONFIG['progress_bar']:
setup_scroll_area()

run_auto(tempdir=args.tempdir, time_id=args.time_id, auto_file=args.auto_file)
finally:
if PROGRESS_BAR:
progress_bar.destroy_scroll_area()
if CONFIG['progress_bar']:
destroy_scroll_area()


def ask_for_options(autos: Autos) -> str | None:
Expand Down
Loading

0 comments on commit b4c1734

Please sign in to comment.