diff --git a/Makefile b/Makefile index 2418d1f..5b828ef 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ clean: # Clean Project rm -rf *~ docs/doxy docs/epydoc docs/pylint_*.html docs/pep8.report.txt dist/hilbert: hilbert.spec - pyinstaller hilbert.spec # may need to be run with a proper Python interpreter, if several are available + ${HOME}/.local/bin/pyinstaller3 hilbert.spec # may need to be run with a proper Python interpreter, if several are available dist: setup.py python3 setup.py sdist diff --git a/README.md b/README.md index 2b2610b..9b02afe 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Travis CI Build Status: [![devel](https://travis-ci.org/hilbert/hilbert-cli.svg? # General notes about shell scripts: -* Exit codes may be as follows (see `status.sh`, others will be updated): +* Exit codes may be as follows: * 0: success * 1: detected error which is not our fault (e.g. network / HW etc): user can try again * 2: error due to wrong usage of scripts / in config files or some assumption was violated. @@ -20,35 +20,17 @@ Travis CI Build Status: [![devel](https://travis-ci.org/hilbert/hilbert-cli.svg? * One may need to capture both `STDOUT` and `STDERR`. Error messages (in `STDERR`) that are meant to be presented to users (admins) must be as informative as possible. -# UI Back-end: high-level action scripts - -* `list_stations.sh`: query current YAML configuration (`hilbert list_stations --format dashboard`) -* `start_station.sh`: start stopped station (`hilbert poweron $station_id`) - 1. initiates station power-on (e.g. via WOL) -* `appchange_station.sh`: switch GUI applicaiton running on the station `hilbert app_change $station_id $app_id` - 1. switch GUI app. on the station -* `stop_station.sh`: stop station (`hilbert poweroff $station_id`): - 1. gracefully finish (stop / kill / remove) all running framework' services - 2. initiate system shutdown (with power-off) of the station (alternative: `poweroff.sh $station_id`) - -NOTE: rebooting the station can be done with: `shutdown.sh STATION -r now` or `reboot.sh STATION` - -TODOs: - * `sync.sh` (to synchronize local cache `STATIONS/` with station configs & scripts from CMS). - Sync. is to be performed once before front-end starts & upon request from CMS (external trigger!): - * `start_station.sh` will not wait for remote machine to power-on completely - there should be a wait on OMD afterwards - * `stop_station.sh` and `appchange_station.sh` should trigger update of OMD checks - # Server-side low-level management CLI tool: ``` -usage: hilbert [-h] [-p] [-V] [-v | -q] [-H] subcommand +usage: hilbert [-h] [-p] [-t] [-d] [-V] [-v | -q] [-H] subcommand Hilbert - server tool: loads configuration and does something using it positional arguments: subcommand : app_change change station's top application + app_restart restart current/default application on a station cfg_deploy deploy station's local configuration to corresponding host cfg_query query some part of configuration. possibly dump it to a file cfg_verify verify the correctness of Hilbert Configuration .YAML file @@ -59,50 +41,67 @@ positional arguments: list_stations list station IDs poweroff finalize Hilbert on a station and shut it down poweron wake-up/power-on/start station + reboot reboot station + run_shell_cmd run specified shell command on given station... optional arguments: -h, --help show this help message and exit -p, --pedantic turn on pedantic mode + -t, --trace turn on remote verbose-trace mode + -d, --dryrun increase dry-run mode -V, --version show hilbert's version and exit -v, --verbose increase verbosity -q, --quiet decrease verbosity -H, --helpall show detailed help and exit - ``` # Client-side low-level driver (accessible either locally or via SSH): - ``` -usage: hilbert-station [-h] [-p] [-V] [-v | -q] subcommand +usage: hilbert-station [-v|-q|-s] [-d|-D] [-t|-T] [-L] [-i|-h|-V|subcommand] [sub-arguments/options] Hilbert - client part for Linux systems positional arguments: - subcommand: + subcommands: init [] init station based on given or installed configuration list_applications list of (supported) applications list_services list of background services app_change change the currently running top application to specified - start start Hilbert on the system + start [app_id] start Hilbert on the system stop stop Hilbert on the system shutdown shut down the system optional arguments: - -h show this help message and exit - -V show version info and exit + -h show this help message [+internal commands/call tree, depending on verbosity] and exit + -V show tool version [+internal info in verbose mode] and exit + -i show internal info and exit -v increase verbosity -q decrease verbosity + -s silent: minimal verbosity -t turn on BASH tracing and verbosity -T turn off BASH tracing and verbosity -d turn on dry-run mode -D turn off dry-run mode + -L disable locking (e.g. for recursive sub-calls) respected environment variables: - HILBERT_CONFIG_BASEDIR location of the base configuration directory of hilbert-station. Default: '~/.config/hilbert-station' + HILBERT_CONFIG_BASEDIR base configuration directory. Current: [~/.config/hilbert-station] + HILBERT_CONFIG_DIR configuration directory. Current: [~/.config/hilbert-station/configs] + HILBERT_CONFIG_FILE station config file. Current: [station.cfg] + +currently detected hilbert variables/values: + HILBERT_SERVER_CONFIG_PATH: ... + HILBERT_CONFIG_BACKUP: ~/.config/hilbert-station/config_backup + HILBERT_SHUTDOWN_DELAY: 1s + HILBERT_LOCKFILE_DIR: /var/run/hilbert + HILBERT_STATION: .../bin/hilbert-station ``` NOTE: all commands (except `init ` / `shutdown` / `-h` / `-V`) require station's configuration to be present + ## License This project is licensed under the [Apache v2 license](LICENSE). See also [Notice](NOTICE). + + diff --git a/docs/HOWTO.md b/docs/HOWTO.md new file mode 100644 index 0000000..d774e8a --- /dev/null +++ b/docs/HOWTO.md @@ -0,0 +1,180 @@ +# How-To guides for Hilbert system + +Note: on a server "${HILBERT_SERVER_CONFIG_PATH}" should be equal to "/shared/interactives/0000_general/Hilbert/CFG/" + +## Add a new PC into a hilbert setup + +1. fix a DNS name (KIOSK_ADDRESS) for a new station (e.g. `kiosk023XXX.ads.eso.org`) +2. decide on proper for it (according to the following schema: `client_[0-9][0-9][0-9][0-9](_[0-9])?`) +3. depending on its HW decide on corresponding profile (i.e. set of services) to run on PC by default +4. add it into OMD (see below) +5. add it into `/shared/interactives/0000_general/Hilbert/CFG/Hilbert.yml` configuration + * by analogy with existing stations but with its own unique networking and MAC addresses! +6. restart the Dashboard + +## Add a station into OMD + +Note: OMD Login credentials are present in other document with secrets + +Let us add a station with ID: `` and network address: `` + +NOTE: make sure that station is running Hilbert already (in order to discover all available CheckMK services on it) + +Create new host entry via: http://es-ex.hq.eso.org/default/check_mk/wato.py?mode=newhost&folder= + +1. `Hostname` set to be `` +2. check `IP address` and set input field to be `` +3. press bottom-left button: "Save and go to serrvices" +4. on "Services of host `` (might be cached data)" there should be around 20 services (most of them should be green) +5. press button "Automatic Refresh (Tabula Rasa)" + +After you are done adding all stations go to top button "... Changes": http://es-ex.hq.eso.org/default/check_mk/wato.py?mode=changelog&folder= +and press button "Activate Changes!" + +Result should be: + +``` +Progress Status +OK Configuration successfully activated. +``` + +## Check Hilbert configuration and docker-compose files + +Run the following on your current server: +``` +$ cd /shared/interactives/0000_general/Hilbert/CFG +$ ./check_cfg.sh +``` + +## Deployment of Hilbert configuration to specified station (or refresh/update Hilbert there) + +Run the following on your current server: +``` +$ cd /shared/interactives/0000_general/Hilbert/CFG +$ ./hilbert cfg_deploy +``` + +Note: on remote station Hilbert configuration will by installed under `~/.config/hilbert-station/` by default. +Only Hilbert client-side CLI tool `hilbert-station` is supposed to work with it. + + +## Cleanup remote station + +Run the following on your current server: + +``` +$ cd /shared/interactives/0000_general/Hilbert/CFG +$ ./hilbert cleanup +``` + +Alternatively after logging-in to corresponding host (via `hilbert-ssh ` or `ssh `) via ssh run the following: +``` +$ hilbert-station cleanup # or +$ hilbert-station cleanup --force # if Hilbert is currently running +``` + + + +## Hilbert client-side CLI tool: `hilbert-station` + +``` +usage: hilbert-station [-v|-q|-s] [-d|-D] [-t|-T] [-L] [-i|-h|-V|subcommand] [sub-arguments/options] + +Hilbert - client part for Linux systems + +positional arguments: + subcommands: + init [] init station based on given or installed configuration + list_applications list of (supported) applications + list_services list of background services + app_change change the currently running top application to specified + app_restart restart the currently running top application + start [] start Hilbert on the system + pause pause Hilbert on the system + stop stop Hilbert on the system + shutdown [now] shut down the system + reboot [now] reboot the system + cleanup [--force] local system cleanup (identical to docker_cleanup on Linux host) + docker_cleanup [--force] total cleanup of local docker engine (images, data volumes) + +optional arguments: + -h show this help message [+internal commands/call tree, depending on verbosity] and exit + -V show tool version [+internal info in verbose mode] and exit + -i show internal info and exit + -v increase verbosity + -q decrease verbosity + -s silent: minimal verbosity + -t turn on BASH tracing and verbosity + -T turn off BASH tracing and verbosity + -d turn on dry-run mode + -D turn off dry-run mode + -L disable locking (e.g. for recursive sub-calls) +``` + +Note: `hilbert-station` is a bash script installed on station via `hilbert-cli-*.rpm`. +Its source is available at https://github.com/hilbert/hilbert-cli/ (in `tools/hilbert-station`) + + + +## Hilbert server-side CLI tool + +``` +$ hilbert +usage: hilbert [-h] [-p] [-t] [-d] [-V] [-v | -q] [-H] subcommand + +Hilbert - server tool: loads configuration and does something using it + +positional arguments: + subcommand : + app_change change station's top application + app_restart restart current/default application on a station + cfg_deploy deploy station's local configuration to corresponding host + cfg_query query some part of configuration. possibly dump it to a file + cfg_verify verify the correctness of Hilbert Configuration .YAML file + cleanup perform system cleanup on remote station + list_applications list application IDs + list_groups list (named) group IDs + list_profiles list profile IDs + list_services list service IDs + list_stations list station IDs + poweroff finalize Hilbert on a station and shut it down + poweron wake-up/power-on/start station + reboot reboot station + run_shell_cmd run specified shell command on given station... + +optional arguments: + -h, --help show this help message and exit + -p, --pedantic turn on pedantic mode + -t, --trace turn on remote verbose-trace mode + -d, --dryrun increase dry-run mode + -V, --version show hilbert's version and exit + -v, --verbose increase verbosity + -q, --quiet decrease verbosity + -H, --helpall show detailed help and exit +``` + +Run `hilbert -H` for detailed information on all commands + +Note: `hilbert` is a CLI packaged Python script. Its sources are available at https://github.com/hilbert/hilbert-cli + + +## Hilbert Data/Configuration Restoration for a server + +See also `/shared/server.backup_restore/README.md` for more details + +Note: the procedure will take quite a long time therefore it is better to run it +within some terminal multiplexer (e.g. `screen` or `tmux`) so that it will not be interrupted +if your connection to server is cut. + +Suggested procedure: + +1. login as `kiosk` user to new (empty) server +2. check that NFS mounted disk `/shared` is present +3. run `tmux` (or `screen`): should start another shell line, where you run the following command: +4. run `/shared/server.backup_restore/bin/restore.sh` + +NOTE: Please do not use `Ctrl+C` to exit. +Use `Crtl+b d` or `Ctrl+a d` to detach from multiplexer session. +Later on you can attach back to previous session with `tmux at` after logging to the server. + +NOTE: do not use `root` for restoration! diff --git a/hilbert_config/__init__.py b/hilbert_config/__init__.py index f5c5443..248be0b 100644 --- a/hilbert_config/__init__.py +++ b/hilbert_config/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -__version__ = '0.3.0' # TODO: add git commit id? +__version__ = '0.4.0' # TODO: add git commit id? # from hilbert_cli_config import * diff --git a/hilbert_config/hilbert_cli_config.py b/hilbert_config/hilbert_cli_config.py index d65ee1a..220a314 100644 --- a/hilbert_config/hilbert_cli_config.py +++ b/hilbert_config/hilbert_cli_config.py @@ -22,6 +22,9 @@ import sys import os import time +import datetime +import random +import itertools import re, tokenize import tempfile # See also https://security.openstack.org/guidelines/dg_using-temporary-files-securely.html import subprocess # See also https://pymotw.com/2/subprocess/ @@ -33,6 +36,7 @@ import pprint as PP from abc import * + ############################################################### # logging.basicConfig(format='%(levelname)s [%(filename)s:%(lineno)d]: %(message)s', level=logging.DEBUG) log = logging.getLogger(__name__) @@ -258,17 +262,17 @@ def _execute(_cmd, shell=False, stdout=None, stderr=None, dry_run=False): # Tru _shell = ' through the shell' retcode = None - try: - # with subprocess.Popen(_cmd, shell=shell, stdout=stdout, stderr=stderr) as p: - # timeout=timeout, - if dry_run: - print("[Dry-Run-Mode] Execute [{0}]{1}".format(__cmd, _shell)) - retcode = 0 - else: + if dry_run: + print("[Dry-Run-Mode] Simulating execution of [{0}]{1}".format(__cmd, _shell)) + retcode = 0 + else: + try: + # with subprocess.Popen(_cmd, shell=shell, stdout=stdout, stderr=stderr) as p: + # timeout=timeout, retcode = subprocess.call(_cmd, shell=shell, stdout=stdout, stderr=stderr) - except: - log.exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) - raise + except: + log.exception("Could not execute [{}]!".format(__cmd)) + raise assert retcode is not None log.debug("Exit code: '{}'".format(retcode)) @@ -287,6 +291,8 @@ def _execute(_cmd, shell=False, stdout=None, stderr=None, dry_run=False): # Tru ############################################################### def _get_line_col(lc): + l = 0 ## ? + c = 0 ## ? if isinstance(lc, (list, tuple)): l = lc[0] c = lc[1] @@ -294,8 +300,7 @@ def _get_line_col(lc): try: l = lc.line except: - log.exception("Cannot get line out of '{}': Missing .line attribute!".format(lc)) - raise + log.warning("Cannot get line out of [{}]: Missing .line attribute!".format(lc)) try: c = lc.col @@ -303,8 +308,9 @@ def _get_line_col(lc): try: c = lc.column except: - log.exception("Cannot get column out of '{}': Missing .col/.column attributes!".format(lc)) - raise + log.warning("Cannot get column out of [{}]: Missing .col/.column attributes!".format(lc)) + pass + pass return l, c @@ -898,7 +904,7 @@ def validate(self, d): try: _v = semantic_version.Version(_t, partial=self._partial) except: - log.exception("Wrong version data: '{0}' (see: '{1}')".format(d, sys.exc_info())) + log.exception("Wrong version data: [{0}]".format(d)) return False self.set_data(_v) @@ -1166,7 +1172,7 @@ def check_script(self, script): # NOTE: Check for valid bash script retcode = _execute(_cmd, dry_run=get_NO_LOCAL_EXEC_MODE()) except: - log.exception("Error while running '{}' to check auto-detection script!".format(' '.join(_cmd))) + log.error("Error while running '{}' to check auto-detection script!".format(' '.join(_cmd))) return False # if PEDANTIC: # TODO: add a special switch? if retcode != 0: @@ -1181,7 +1187,7 @@ def check_script(self, script): # NOTE: Check for valid bash script retcode = _execute(_cmd, dry_run=get_NO_LOCAL_EXEC_MODE()) except: - log.exception("Error while running '{}' to check auto-detection script!".format(' '.join(_cmd))) + log.error("Error while running '{}' to check auto-detection script!".format(' '.join(_cmd))) return False if retcode != 0: @@ -1229,12 +1235,46 @@ def validate(self, d): self.set_data(script) return True except: - log.exception("Wrong input to AutoDetectionScript::validate: {}".format(d)) + log.error("Wrong input to AutoDetectionScript::validate: {}".format(d)) return False self.set_data(script) return True + +############################################################### +class OptionalString(StringValidator): + def __init__(self, *args, **kwargs): + super(OptionalString, self).__init__(*args, **kwargs) + self._default_input_data = '' # empty string by default! + + def check(self): + return True + + + def validate(self, d): + """check whether data is a valid script""" + if d is None: + d = self._default_input_data + + if (d is None) or (d == '') or (d == text_type('')): + self.set_data(text_type('')) + return True + + s = '' + try: + s = StringValidator.parse(d, parent=self, parsed_result_is_data=True) + + if not bool(s): # NOTE: empty script is also fine! + self.set_data(s) + return True + except: + log.error("Wrong input to OptionalString::validate: {}".format(d)) + return False + + self.set_data(s) + return True + ############################################################### class DockerComposeServiceName(StringValidator): # TODO: any special checks here? @@ -1357,9 +1397,9 @@ def rsync(self, source, target, **kwargs): try: retcode = _execute(_cmd, **kwargs) except: - log.exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) + log.exception("Could not execute '{0}'! ".format(__cmd)) if not PEDANTIC: - return 1 + return -1 raise assert retcode is not None @@ -1369,7 +1409,7 @@ def rsync(self, source, target, **kwargs): else: log.error("Could not run rsync.ssh command: '{0}'! Return code: {1}".format(__cmd, retcode)) if PEDANTIC: - raise Exception("Could not run rsync/ssh command: '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) + raise Exception("Could not run rsync/ssh command: '{0}'!".format(__cmd)) return retcode @@ -1390,19 +1430,20 @@ def scp(self, source, target, **kwargs): try: retcode = _execute(_cmd, **kwargs) except: - log.exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) + log.exception("Could not execute '{0}'!".format(__cmd)) if not PEDANTIC: - return 1 + return -1 raise assert retcode is not None + if not retcode: log.debug("Command ({}) execution success!".format(__cmd)) return retcode else: log.error("Could not run scp command: '{0}'! Return code: {1}".format(__cmd, retcode)) - if PEDANTIC: - raise Exception("Could not run scp command: '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) +# if PEDANTIC: +# raise Exception("Could not run scp command: '{0}'!".format(__cmd)) return retcode @@ -1421,20 +1462,19 @@ def ssh(self, cmd, **kwargs): # client = paramiko.SSHClient() # client.load_system_host_keys() + retcode = None try: retcode = _execute(_cmd, **kwargs) except: - log.exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) + log.exception("Could not execute [{0}]!".format(__cmd)) raise assert retcode is not None - if not retcode: - log.debug("Command ({}) execution success!".format(__cmd)) - return retcode - else: - log.error("Could not run remote ssh command: '{0}'! Return code: {1}".format(__cmd, retcode)) - # if PEDANTIC: - raise Exception("Could not run remote ssh command: '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) +# if retcode: +# log.error("Could not run remote ssh command: '{0}'! Return code: {1}".format(__cmd, retcode)) +# # if PEDANTIC: +# raise Exception("Could not run remote ssh command: [{0}]!".format(__cmd)) + log.debug("Command [{}] executed with return code: [{}]".format(__cmd, retcode)) return retcode @classmethod @@ -1460,9 +1500,9 @@ def check_ssh_alias(cls, _h, **kwargs): log.debug("Ssh alias '{0}' is functional!".format(text_type(_h))) return retcode == 0 + except: - log.exception("Non-functional ssh alias: '{0}'. Moreover: Unexpected error: {1}".format(text_type(_h), - sys.exc_info())) + log.exception("Error while checking ssh alias: [{0}]. ".format(text_type(_h))) if PEDANTIC: raise @@ -1565,7 +1605,7 @@ def validate(self, d): try: t = self._type_cls.parse(d[self._type_tag], parent=self, parsed_result_is_data=True, id=self._type_tag) except: - log.exception("Wrong type data: {}".format(d[self._type_tag])) + log.error("Wrong type data: {}".format(d[self._type_tag])) return False if t not in _rule: @@ -1691,13 +1731,8 @@ def start(self): # , action, action_args): try: _ret = _a.ssh(_cmd, shell=True) # TODO: check for that shell=True!!!??? except: - s = "Could not power-on virtual station {0} (at {1})".format(_n, _a) - if not PEDANTIC: - log.warning(s) - return False - else: - log.exception(s) - raise + log.error("Could not power-on virtual station {0} (at {1})".format(_n, _a)) + return False return (_ret == 0) @@ -1735,7 +1770,8 @@ def get_MAC(self): assert _MAC != '' return _MAC - def start(self): # , action, action_args): + def start(self, action_args): + _address = None _parent = self.get_parent(cls=Station) @@ -1746,29 +1782,91 @@ def start(self): # , action, action_args): assert isinstance(_address, HostAddress) _address = _address.get_address() + _MAC = self.get_MAC() + log.debug("WOL execution( MAC: {}, IP/Addr: {}, Args: {}".format(_MAC, _address, action_args)) + + N = action_args + + if (N is None) or (N == ''): + N = 5 # Default number of WOL! + else: + N = int(N) + + if N <= 0: + log.debug("WOL execution: nothing to do!") + return True + + _cmd = [self._WOL, _MAC] # NOTE: avoid IP for now? {"-i", _address, } __cmd = ' '.join(_cmd) + + retcode = None + + # Small initial sleep to avoid instant paralell WOL-spamming + time.sleep( random.uniform(0, 0.03) ) + + # measure time for a single WOL call: + _t = datetime.datetime.now() + try: retcode = _execute(_cmd, dry_run=get_NO_LOCAL_EXEC_MODE()) + except: + log.exception("Could not execute [{0}]!".format(__cmd)) + raise + + _t = (datetime.datetime.now() - _t).total_seconds() # convert it into seconds + + assert retcode is not None + + if not retcode: + log.debug("Command ({}) execution success!".format(__cmd)) + # return + else: + log.error("Could not wakeup via '{0}'! Return code: {1}".format(__cmd, retcode)) +# if PEDANTIC: +# raise Exception("Could not execute '{0}'!".format(__cmd)) + + N = N - 1 + + if N <= 0: + return (retcode == 0) + + # Estimate the number of parallel WOL executions = number of Stations in the current config file! + M = _parent.get_parent(cls=GlobalStations) + if M is not None: + M = M.get_data() + if M is not None: + M = len(M) + + if M is None: + M = 10 + + _t = M * _t + + log.debug("WOL execution: assuming {} parallel execs => max. random delay: {}s ".format(M, _t)) + + for _ in itertools.repeat(None, N): + time.sleep(random.uniform(0, _t)) + try: + retcode = _execute(_cmd, dry_run=get_NO_LOCAL_EXEC_MODE()) + except: + log.exception("Could not execute [{0}]!".format(__cmd)) + raise assert retcode is not None + if not retcode: log.debug("Command ({}) execution success!".format(__cmd)) - # return + # return else: log.error("Could not wakeup via '{0}'! Return code: {1}".format(__cmd, retcode)) - if PEDANTIC: - raise Exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) - except: - log.exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) - if not PEDANTIC: - return - raise + + return (retcode == 0) # No point in extra targeted WOL - broadcasts should be enough! if (_address is None) or (_address == ''): log.warning("Sorry: could not get station's address for this WOL MethodObject!") - return + return (retcode == 0) # if PEDANTIC: # NOTE: address should be present for other reasons anyway... # raise Exception("Sorry: could not get station's address for this WOL MethodObject!") @@ -1777,24 +1875,40 @@ def start(self): # , action, action_args): # Q: any problems with this? _cmd = [self._WOL, "-i", _address, _MAC] __cmd = ' '.join(_cmd) + + retcode = None try: retcode = _execute(_cmd, dry_run=get_NO_LOCAL_EXEC_MODE()) - assert retcode is not None - if not retcode: - log.debug("Command ({}) execution success!".format(__cmd)) - return - else: - log.error("Could not wakeup via '{0}'! Return code: {1}".format(__cmd, retcode)) - # if PEDANTIC: - # raise Exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) except: - log.exception("Could not execute '{0}'! Exception: {1}".format(__cmd, sys.exc_info())) - # if not PEDANTIC: + log.exception("Could not execute [{0}]!".format(__cmd)) + if PEDANTIC: + raise # return - # raise - pass +# pass + + assert retcode is not None + if not retcode: + log.debug("Command ({}) execution success!".format(__cmd)) + else: + log.error("Could not wakeup via '{0}'! Return code: {1}".format(__cmd, retcode)) + + return (retcode == 0) + + + +############################################################### +def shell_vstr(s, quote_char="'"): + """ + add quotes before and after the value: [QUOTE_CHAR][QUOTE_CHAR] + """ + SUBS = {"'": 'ยด'} + + s = str(s).replace(quote_char, SUBS.get(quote_char, quote_char)) + return ("{0}{1}{0}".format(quote_char, s)) +# str(s).replace(quote_char, "\\" + quote_char) # Safe exporting of strings into bash: ['] -> [\'] ??? + ############################################################### class DockerComposeService(BaseRecordValidator): """DockerCompose :: Service data type""" @@ -1817,7 +1931,17 @@ def __init__(self, *args, **kwargs): self._ref_tag: (True, DockerComposeServiceName), self._file_tag: (False, DockerComposeYAMLFile) } - + + _v = self.get_version(default=semantic_version.Version('0.7.0')) + if _v >= semantic_version.Version('0.9.0'): + self._url_tag = text_type('url') + self._name_tag = text_type('name') + self._description_tag = text_type('description') + + _compose_rule[self._url_tag] = (False, URI) # URI? + _compose_rule[self._name_tag] = (False, OptionalString) + _compose_rule[self._description_tag] = (False, OptionalString) + self._default_type = "default_dc_service" self._types = {self._default_type: _compose_rule} @@ -1837,20 +1961,26 @@ def copy(self, tmpdir): f = self.get_file() t = os.path.join(tmpdir, f) - log.info("Copying resource file: '%s' -> '%s'...", f, t) + log.debug("Copying resource file: '%s' -> '%s'...", f, t) if not os.path.exists(t): d = os.path.dirname(t) if not os.path.exists(d): os.mkdir(d, 7 * 8 + 7) shutil.copy(f, t) + + # TODO: FIXME: follow dependencies in docker-compose files manually !!! + # 1. open, check 'version' string: '2', '2.1', '2.2', '2.3' + # 2. iterate 'services' mapping and look for '/extends/file' strings -> try to copy / check for the file to be there already + else: log.debug("Target resource '%s' already exists!", t) + def to_bash_array(self, n): _d = self.data_dump() _min_compose = [self._type_tag, self._ref_tag, self._file_tag, self._prerun_detections_hook_tag, self._preinit_hook_tag] - return ' '.join(["['{2}:{0}']='{1}'".format(k, _d[k], n) for k in _min_compose]) + return ' '.join(["['{2}:{0}']={1}".format(k, shell_vstr(_d[k]), n) for k in _min_compose]) @staticmethod def check_service(_f, _n): @@ -2079,6 +2209,7 @@ def __init__(self, *args, **kwargs): # NOTE: the list of possible values of Station::type (will depend on format version) self._enum_list = [self._default_input_data, + text_type('fake'), # container of defaults text_type('standalone'), # No remote control via SSH & Hilbert client... text_type('server'), # Linux with Hilbert client part installed but no remote control! text_type('standard') # Linux with Hilbert client part installed! @@ -2091,6 +2222,7 @@ class Station(BaseRecordValidator): # Wrapper? _extends_tag = text_type('extends') _client_settings_tag = text_type('client_settings') + _compatible_applications_tag = text_type('compatible_applications') _type_tag = text_type('type') def __init__(self, *args, **kwargs): @@ -2108,14 +2240,14 @@ def __init__(self, *args, **kwargs): default_rule = { Station._extends_tag: (False, StationID), # TODO: NOTE: to be redesigned later on: e.g. use Profile!? - self._name_tag: (True, BaseUIString), - text_type('description'): (True, BaseUIString), - text_type('icon'): (False, Icon), - self._profile_tag: (True, ProfileID), + self._name_tag: (True, BaseUIString), # TODO: FIXME: should not be required for hidden! + text_type('description'): (True, BaseUIString), # TODO: FIXME: should not be required for hidden! + text_type('icon'): (False, Icon), # TODO: currently unused => delete? + self._profile_tag: (True, ProfileID), # TODO: FIXME: should not be required for fakes! self._address_tag: (True, HostAddress), self._poweron_tag: (False, StationPowerOnMethodWrapper), # !! variadic, PowerOnType... self._ssh_options_tag: (False, StationSSHOptions), # !!! record: user, port, key, key_ref - text_type('omd_tag'): (True, StationOMDTag), # ! like ServiceType: e.g. agent. Q: Is this mandatory? + text_type('omd_tag'): (True, StationOMDTag), # NOTE: like ServiceType: e.g. agent. TODO: FIXME: should not be required for fakes! self._ishidden_tag: (False, StationVisibility), # Q: Is this mandatory? Station._client_settings_tag: (False, StationClientSettings), # IDMap : (BaseID, BaseString) Station._type_tag: (False, StringValidator) # NOTE: to be redesigned later on! @@ -2138,7 +2270,7 @@ def data_dump(self): _d = super(Station, self).data_dump() l = self.get_compatible_applications() if l is not None: - _d['compatible_applications'] = sorted(l.keys()) + _d[Station._compatible_applications_tag] = sorted(l.keys()) # + ':' + self._compatible_applications[k].get_name() return _d @@ -2238,29 +2370,144 @@ def get_address(self): # TODO: IP? log.debug('HostAddress: {}'.format(_a)) return _a - def shutdown(self): + def app_restart(self, action_args=None): _a = self.get_address() assert _a is not None assert isinstance(_a, HostAddress) try: - _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "stop"]) + _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "app_restart"]) + except: + log.exception("Could not start app-restarting on the station {}".format(_a)) + _ret = -1 + + if _ret != 0: + log.error("Failed running app-restarting on the station {}".format(_a)) + + return _ret + + def cleanup(self, action_args=None): + _a = self.get_address() + + assert _a is not None + assert isinstance(_a, HostAddress) + + try: + if action_args: + _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "cleanup", "--force"]) + else: + _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "cleanup"]) + + except: + log.exception("Could not perform system cleanup on the station {} [force={}]".format(_a, action_args)) + _ret = -1 + + if _ret != 0: + log.error("Failed running system cleanup on the station {} [force={}]".format(_a, action_args)) + + return _ret + + def reboot(self, action_args=None): + _a = self.get_address() + + assert _a is not None + assert isinstance(_a, HostAddress) + + _ret = 0 + try: + _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "pause"]) # TODO! except: s = "Could not stop Hilbert on the station {}".format(_a) + log.exception(s) + if PEDANTIC: + raise + log.info('Going to reboot the station despite an error while pausing Hilbert there!') +# return False + + if _ret != 0: + s = "Could not pause Hilbert on the station {}. Return code: [{}]".format(_a, _ret) if not PEDANTIC: log.warning(s) - return False + log.info('Going to reboot the station despite an error while pausing Hilbert there!') else: - log.exception(s) + log.error(s) + return False + + + + try: + _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "reboot", "now"]) #! + except: + log.exception("Could not start rebooting the station {}".format(_a)) + _ret = -1 + + if _ret != 0: + log.error("Failed running rebooting on the station {}".format(_a)) + + return (_ret == 0) + + def run_remote_cmd(self, action): + """ + """ + _a = self.get_address() + + assert _a is not None + assert isinstance(_a, HostAddress) + + try: + log.debug("Going to run action [{}] via ssh on the station [{}]".format(action, _a)) + _ret = _a.ssh(action) + except: + log.exception("Could not run an action [{}] via ssh on the station [{}]".format(action, _a)) + raise + + if _ret == 255: + log.debug( + "Possibly connection was cut while running action [{}] via ssh on the station [{}] (returned exit code == 255!)".format( + action, _a)) + elif _ret != 0: + log.debug("Running action [{}] via ssh on the station [{}] returned non-zero exit code: [{}]".format(action, _a, _ret)) + + return _ret + + + def shutdown(self, action_args=None): + """ + Stop Hilbert stack on a station + power it off + """ + _a = self.get_address() + + assert _a is not None + assert isinstance(_a, HostAddress) + + _ret = None + try: + _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "stop"]) + except: + s = "Could not stop Hilbert on the station {}".format(_a) + log.exception(s) + if PEDANTIC: raise + log.info('Going to shut the station down despite an error while stopping Hilbert there!') +# return False if _ret != 0: - return False + s = "Could not stop Hilbert on the station {}. Return code: [{}]".format(_a, _ret) + if not PEDANTIC: + log.warning(s) + log.info('Going to shut the station down despite an error while stopping Hilbert there!') + else: + log.error(s) + return False try: + # NOTE: ssh connection is NOT supposed to be cut NOW! _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "shutdown", "now"]) +# log.warning("ssh connection was NOT cut during immediate station shutdown!?") +# _ret = 0 except: +# log.debug("Ok, ssh connection was cut due to immediate station shutdown!") s = "Could not schedule immediate shutdown on the station {}".format(_a) if not PEDANTIC: log.warning(s) @@ -2270,23 +2517,35 @@ def shutdown(self): raise if _ret != 0: - log.error("Bad attempt to immediately shutdown the station {} ".format(_a)) + log.error("Failed attempt to immediately shutdown the station {} ".format(_a)) return False + return (_ret == 0) # or (_ret == 255) + try: + ### ssh connection is not supposed to be cut here! only in a minute! _ret = _a.ssh([_HILBERT_STATION, _HILBERT_STATION_OPTIONS, "shutdown"]) except: s = "Could not schedule delayed shutdown on the station {}".format(_a) + log.exception(s) + if PEDANTIC: + raise + _ret = -1 + + if _ret != 0: + s = "Failed attempt to schedule a shutdown the station {} with default delay".format(_a) if not PEDANTIC: log.warning(s) - return False + log.info('Going to shut the station down despite an error while stopping Hilbert there!') else: - log.exception(s) - raise + log.error(s) + return False + + return False + - return (_ret == 0) - def deploy(self): + def deploy(self, action_args=None): # TODO: get_client_settings() _d = self.get_data() @@ -2350,7 +2609,7 @@ def deploy(self): # hilbert_station_services_and_applications # hilbert_station_profile_services # hilbert_station_compatible_applications - tmp.write('declare -Agr hilbert_station_services_and_applications=(\\\n') + _config = 'declare -Ag hilbert_station_services_and_applications=(\\\n' # ss = [] for k in _serviceIDs: s = all_services.get(k, None) # TODO: check compatibility during verification! @@ -2362,9 +2621,9 @@ def deploy(self): # TODO: s.check() # ss.append(s.get_ref()) - tmp.write(' {} \\\n'.format(s.to_bash_array(k))) + _config += ' {} \\\n'.format(s.to_bash_array(k)) - s.copy(tmpdir) + s.copy(tmpdir) # TODO: follow dependencies!!! # TODO: collect all **compatible** applications! # aa = [] @@ -2375,15 +2634,23 @@ def deploy(self): # TODO: a.check() # aa.append(a.get_ref()) - tmp.write(' {} \\\n'.format(a.to_bash_array(k))) + _config += ' {} \\\n'.format(a.to_bash_array(k)) - a.copy(tmpdir) + a.copy(tmpdir) # TODO: follow dependencies!!! + _config += ')\n' + tmp.write(_config) + + tmp.write('declare -Ag hilbert_station_configuration=(\\\n') + for k in _d: + if k not in [Station._client_settings_tag, Station._compatible_applications_tag, Station._extends_tag]: + tmp.write(" [{}]={} \\\n".format(shell_vstr(k), shell_vstr(_d.get(k, '')) )) tmp.write(')\n') + tmp.write("declare -g hilbert_station_configuration_ID={}\n".format(shell_vstr(self.get_id()) )) - tmp.write("declare -agr hilbert_station_profile_services=({})\n".format(' '.join(_serviceIDs))) + tmp.write("declare -ag hilbert_station_profile_services=({})\n".format(' '.join(_serviceIDs))) tmp.write( - "declare -agr hilbert_station_compatible_applications=({})\n".format(' '.join(all_apps.keys()))) + "declare -ag hilbert_station_compatible_applications=({})\n".format(' '.join(all_apps.keys()))) app = _settings.get('hilbert_station_default_application', '') # NOTE: ApplicationID! if app != '': @@ -2400,14 +2667,14 @@ def deploy(self): for k in sorted(_settings.keys(), reverse=True): if k.startswith('HILBERT_'): # NOTE: HILBERT_* are exports for services/applications (docker-compose.yml) - tmp.write("declare -xg {0}='{1}'\n".format(k, str(_settings.get(k, '')))) + tmp.write("declare -xg {0}={1}\n".format(k, shell_vstr(str(_settings.get(k, ''))))) elif k.startswith('hilbert_'): # NOTE: hilbert_* are exports for client-side tool: `hilbert-station` - tmp.write("declare -rg {0}='{1}'\n".format(k, str(_settings.get(k, '')))) + tmp.write("declare -g {0}={1}\n".format(k, shell_vstr(str(_settings.get(k, ''))))) else: if not PEDANTIC: log.debug("Non-hilbert station setting: [%s]!") - tmp.write("declare -xg {0}='{1}'\n".format(k, str(_settings.get(k, '')))) + tmp.write("declare -xg {0}={1}\n".format(k, shell_vstr(str(_settings.get(k, ''))))) else: log.warning("Non-hilbert station setting: [%s]! Not allowed in pedantic mode!") @@ -2421,11 +2688,10 @@ def deploy(self): except: log.debug("Exception during deployment!") s = "Could not deploy new local settings to {}".format(_a) + log.exception(s) if not PEDANTIC: - log.warning(s) return False else: - log.exception(s) raise # except: # IOError as e: @@ -2486,7 +2752,7 @@ def app_change(self, app_id): # raise NotImplementedError("Cannot switch to a different application on this station!") - def poweron(self): + def poweron(self, action_args=None): _d = self.get_data() assert _d is not None @@ -2496,36 +2762,51 @@ def poweron(self): log.error("Missing/wrong Power-On Method configuration for this station!") raise Exception("Missing/wrong Power-On Method configuration for this station!") - return poweron.start() # , action_args???? + return poweron.start(action_args) def run_action(self, action, action_args): """ Run the given action on/with this station - :param action_args: arguments to the action :param action: - start (poweron) + start (wakeup) stop (shutdown) + reboot + cleanup cfg_deploy - app_change -# start [] -# finish [] + app_restart + app_change + run_shell_cmd + :param action_args: arguments to the action (see the action) :return: nothing. """ - - if action not in ['start', 'stop', 'cfg_deploy', 'app_change']: - raise Exception("Running action '{0}({1})' is not supported!".format(action, action_args)) +# start [] +# finish [] # Run 'ssh address hilbert-station action action_args'?! if action == 'start': - _ret = self.poweron() # action_args + _ret = self.poweron(action_args) elif action == 'cfg_deploy': - _ret = self.deploy() # action_args + _ret = self.deploy(action_args) elif action == 'stop': - _ret = self.shutdown() # action_args + _ret = self.shutdown(action_args) + elif action == 'reboot': + _ret = self.reboot(action_args) + elif action == 'cleanup': + _ret = self.cleanup(action_args) elif action == 'app_change': _ret = self.app_change(action_args) # ApplicationID + elif action == 'app_restart': + _ret = self.app_restart(action_args) # ApplicationID + elif action == 'run_shell_cmd': + _ret = self.run_remote_cmd(action_args) + else: + assert action not in ['start', 'stop', 'reboot', 'cfg_deploy', 'app_change', 'app_restart', 'run_shell_cmd', 'cleanup'] + raise Exception("Running action '{0}({1})' is not supported!".format(action, action_args)) + + + # elif action == 'start': # self.start_service(action_args) @@ -2781,19 +3062,17 @@ def validate(self, d): while bool(_todo): k, v = _todo.popitem() - _b = v.get_base() - assert k != _b # no infinite self-recursive extensions! - - # print(_b, ' :base: ', type(_b)) - assert _b in _processed + _b = v.get_base() # get base + assert k != _b # no infinite self-recursive extensions - statoion cannot extend itself! +# assert _b in _processed # enable multi-level extentions if _b in _processed: v.extend(_processed[_b]) _processed[k] = v assert v.get_base() is None _chg = True else: - _rest[k] = v + _rest[k] = v # keep (k,v) for later (when its base will be processed!) _todo = _rest diff --git a/rpmbuild/SPECS/hilbert-cli.spec b/rpmbuild/SPECS/hilbert-cli.spec index c027e0f..9aedaa5 100644 --- a/rpmbuild/SPECS/hilbert-cli.spec +++ b/rpmbuild/SPECS/hilbert-cli.spec @@ -15,10 +15,10 @@ # BuildRoot: %{buildroot} Summary: Hilbert: client-side tools with a basic minimal configuration Name: hilbert-cli -Version: 0.9.1 +Version: 0.9.5 License: Apache License, Version 2.0 -Release: 8%{?dist} +Release: 12%{?dist} URL: https://github.com/hilbert/%{origname} Source0: %{origname}.tar.gz @@ -153,6 +153,18 @@ rm -rf $RPM_BUILD_ROOT # docker rmi hello-world:latest %changelog +* Mon Jul 8 2019 Alex +- added `cleanup` alias for `docker_cleanup` into `hilbert-station` for Linux hosts + +* Mon Jul 1 2019 Alex +- added docker_cleanup + minor cosmetic improvements in `hilbert-station` to enable local docker engine cleanup (images, volumes) + +* Wed May 31 2019 Alex +- minor update of CLI tools (e.g. hilbert-station, hilber) to enable restarting and pausing + +* Wed Apr 17 2019 Alex +- major update of CLI tools (e.g. hilbert-station) + * Thu Mar 8 2018 Alex - major update of CLI tools (e.g. hilbert-station) - update of hilbert-compose-customizer (to detect all video devices in /dev/dri) diff --git a/setup.py b/setup.py index 74fb22d..d35a2bc 100644 --- a/setup.py +++ b/setup.py @@ -4,13 +4,19 @@ # from distutils.core import setup # no support for install_requires! from setuptools import setup # supports install_requires! -from pip.req import parse_requirements +#from pip.req import parse_requirements # See: https://stackoverflow.com/a/16624700 +# Fix: https://stackoverflow.com/a/25193001 +def parse_requirements(filename, session='hack'): + """ load requirements from a pip requirements file """ + lineiter = (line.strip() for line in open(filename)) + return [line for line in lineiter if line and not line.startswith("#")] # .req : line ? + install_reqs = parse_requirements('requirements.txt', session='hack') tests_reqs = parse_requirements('requirements-dev.txt', session='hack') setup(name='hilbert_config', - version='0.3.0', + version='0.4.0', description='Hilbert Configuration tool (server part)', url='https://github.com/hilbert/hilbert-cli', author='Oleksandr Motsak', @@ -35,8 +41,8 @@ 'Programming Language :: Python :: 3.6', ], platforms=[''], # data_files=[('config/templates', ['docker-compose.yml'])], - install_requires=[str(ir.req) for ir in install_reqs], - tests_require=[str(ir.req) for ir in tests_reqs] + install_requires=[str(ir) for ir in install_reqs], + tests_require=[str(ir) for ir in tests_reqs] ) # glob.glob(os.path.join('mydir', 'subdir', '*.html')) # os.listdir(os.path.join('mydir', 'subdir')) diff --git a/station/etc/systemd/system/nodm.service b/station/etc/systemd/system/nodm.service new file mode 100644 index 0000000..9d9922c --- /dev/null +++ b/station/etc/systemd/system/nodm.service @@ -0,0 +1,14 @@ +[Unit] +Description=Automatic display manager +Documentation=man:nodm(8) file:/usr/share/doc/nodm/README +Conflicts=getty@tty1.service +After=systemd-user-sessions.service getty@tty1.service plymouth-quit.service + +[Service] +EnvironmentFile=/etc/nodm.conf +ExecStart=/usr/sbin/nodm +ExecStop=/usr/bin/killall -9 nodm + +[Install] +Alias=display-manager.service + diff --git a/station/etc/systemd/system/nodm.service.d/require_network.conf b/station/etc/systemd/system/nodm.service.d/require_network.conf new file mode 100644 index 0000000..5c7b3e3 --- /dev/null +++ b/station/etc/systemd/system/nodm.service.d/require_network.conf @@ -0,0 +1,5 @@ +# See https://serverfault.com/a/803794 +[Unit] +After=network.target network-online.target +Wants=network-online.target +Requires=network.target network-online.target diff --git a/station/etc/systemd/system/nodm.service.d/station_with_projector.conf b/station/etc/systemd/system/nodm.service.d/station_with_projector.conf new file mode 100644 index 0000000..372da41 --- /dev/null +++ b/station/etc/systemd/system/nodm.service.d/station_with_projector.conf @@ -0,0 +1,4 @@ +# See https://serverfault.com/a/803794 +[Unit] +After=projector-start.service +Requires=projector-start.service diff --git a/station/etc/systemd/system/projector-start.service b/station/etc/systemd/system/projector-start.service new file mode 100644 index 0000000..88ff3d2 --- /dev/null +++ b/station/etc/systemd/system/projector-start.service @@ -0,0 +1,26 @@ +[Unit] +Description=Projector Startup +Wants=network-online.target syslog.target +After=network-online.target network.target syslog.target + +[Service] +Type=oneshot +ExecStartPre=/usr/local/bin/projector.py STATUS +ExecStart=/usr/bin/ls -l /usr/local/bin/projector.py /usr/bin/python3 +ExecStart=/usr/local/bin/projector.py ON +TimeoutSec=900 +ExecStop=/usr/local/bin/projector.py STATUS +# /usr/bin/echo "Done starting projector!" +## +#Restart=on-failure +#Restart=always +#RestartSec=2 +#StartLimitInterval=1100 +#StartLimitBurst=100 +## + +[Install] +WantedBy=default.target +# multi-user.target ? +# graphical.target ? +# display-manager.service ? diff --git a/station/etc/systemd/system/projector-stop.service b/station/etc/systemd/system/projector-stop.service new file mode 100644 index 0000000..ad0f458 --- /dev/null +++ b/station/etc/systemd/system/projector-stop.service @@ -0,0 +1,34 @@ +[Unit] +Description=Projector Shutdown +Before=poweroff.target +# Wants=network-online.target syslog.target +After=network-online.target +# network.target syslog.target +DefaultDependencies=no + +# local-fs.target + +[Service] +Type=oneshot +#ExecStartPre=/usr/bin/systemctl list-jobs +ExecStartPre=/usr/local/bin/projector.py STATUS +ExecStart=/usr/bin/ls -l /usr/local/bin/projector.py /usr/bin/python3 +ExecStart=/usr/local/bin/projector.py OFF +ExecStop=/usr/local/bin/projector.py STATUS +#/usr/bin/echo "Done stopping projector!" +## +TimeoutSec=900 +# 40 + +#/usr/bin/echo "Stopping projector..." +#Restart=on-failure +#Restart=always +#RestartSec=2 +#StartLimitInterval=1100 +#StartLimitBurst=100 +## + +# TimeoutStopSec=5 + +[Install] +WantedBy=poweroff.target diff --git a/station/etc/systemd/system/pulseaudio.service b/station/etc/systemd/system/pulseaudio.service index 6c9066f..2a5609b 100644 --- a/station/etc/systemd/system/pulseaudio.service +++ b/station/etc/systemd/system/pulseaudio.service @@ -7,6 +7,8 @@ Description=PulseAudio system server #Type=oneshot ExecStart=/usr/bin/pulseaudio --daemonize=no --system --realtime --log-target=journal #ExecStop= +Restart=on-failure +RestartSec=2 [Install] WantedBy=multi-user.target diff --git a/station/etc/systemd/system/pulseaudio.service.d/restart.conf b/station/etc/systemd/system/pulseaudio.service.d/restart.conf new file mode 100644 index 0000000..669780b --- /dev/null +++ b/station/etc/systemd/system/pulseaudio.service.d/restart.conf @@ -0,0 +1,9 @@ +# See https://serverfault.com/a/803794 +[Service] +##Restart=on-failure +Restart=always +RestartSec=2 +StartLimitInterval=300 +StartLimitBurst=100 + + diff --git a/station/projector.py b/station/projector.py new file mode 100755 index 0000000..6e92241 --- /dev/null +++ b/station/projector.py @@ -0,0 +1,444 @@ +#!/usr/bin/python3 -su +# /usr/bin/env python3 + +# -*- coding: utf-8 -*- +# encoding: utf-8 +# coding: utf-8 + +""" +TCP/IP Client to Crestron system's Projector-API. + +0. check localhost name (Is it a Station with Projector?) +1. check CLI arguments to determine Target Projectors and client termination condition +2. in a loop probe all possibly open ports on Crestron system and on the first open one do: + in a loop send requests and parse responses, until termination condition is satisfied + +Possible targets: + one of Projectors or + all of them + +Possible termination conditions: + ON (resp. OFF) -> wait until _all_ targets are ON (resp. OFF), + STATUS -> wait for state respone about each target, + LISTEN -> wait indefinitely. + +""" + +# TODO: query server for all known hosts/supported commands? +# TODO: quit timeout ??? + +from __future__ import absolute_import, print_function, unicode_literals + +import socket +import io +import re +import sys +import select +import errno +import platform +import os + +from time import sleep, time +import datetime + +# Comma-separated list of all known hosts with projectors (overrides the list provided by Crestron): +ENV_ALL_PROJECTORS = os.environ.get('ALL_PROJECTORS', None) + +PRJ = [] # Global list of all projector IDs +if ENV_ALL_PROJECTORS: + PRJ = ENV_ALL_PROJECTORS.split(',') + +# Crestron Server Connection details: +CRESTRON_HOST = os.environ.get('CRESTRON_HOST', '172.16.31.13') +CRESTRON_PORT = int(os.environ.get('CRESTRON_PORT', str(3629))) +CRESTRON_PORT_COUNT = int(os.environ.get('CRESTRON_PORT_COUNT', str(8))) + +# Maximal number of connection rounds (to each known port) +CONNECTION_TRY_ROUND_COUNT = int(os.environ.get('CONNECTION_TRY_ROUND_COUNT', str(5))) + +COMMAND_RETRY_TIMEOUT = int(os.environ.get('CONNECTION_TRY_ROUND_COUNT', str(2*60))) # in seconds + +# possible target states: +OFF = [u'OFF', u'OFF,OFF'] +ON = [u'ON', u'ON,ON'] + +# request / response separator +EOC = '\x0a' + + +################################################################# +#isRoot = (os.geteuid() == 0) +def print_timestamp(*args, **kwargs): + print(datetime.datetime.fromtimestamp(time()).strftime('%Y-%m-%d %H:%M:%S') +":", *args, **kwargs) +# sys.stdout.flush() +# if isRoot + + +################################################################# +STAT = {} # Global registry with statuses of Projectors + + +def get_stat(p, default='?'): + return(STAT.get(p, default)) + + +################################################################# +def send_request_prj_list(sock): # should immediately respond with current list of targets + """Query for all known projectors""" + message = 'PRJ_LIST=?' + EOC + print_timestamp('Request: {!r}'.format(message)) + return(sock.sendall(message.encode('utf-8')) is None) + + +################################################################# +def send_requests(sock, action, Targets): # should immediately respond with current status + """Query or control projectors""" + if not Targets: + print_timestamp('No known handle-targets yet...') + return(True) + + message = '' + for p in Targets: + message += 'PRJ_' + p + '_PWR_' + if action in ['ON', 'OFF']: + message += action + else: + assert (action == 'LISTEN') or (action == 'STATUS') + message += 'STAT=?' + message += EOC + + print_timestamp('Request: {!r}'.format(message)) + + return(sock.sendall(message.encode('utf-8')) is None) # NOTE: send all requests together..!? + + +################################################################# +def socket_communicator(sock, action_retry_timeout, action_request=(lambda s: True), socket_recv_max_size=1024, socket_select_wait=0.5, idle_wait=1.5, read_timeout = None): # https://stackoverflow.com/a/8387235 + if not sock: + return # nothing to read! + + assert sock + + run_main_loop = action_request(sock) + t = time() + read_time = time() + sock.setblocking(0) # NOTE: side-effects are possible! + _empty_buffer = 0 + while run_main_loop and sock: + # buffer += sock.recv(1024) # NOTE: blocking read! + read_ready, _a, _b = select.select([sock], [], [], socket_select_wait) + if sock in read_ready: + # The socket have data ready to be received + continue_recv = True + + while continue_recv and sock: + try: + # Try to receive some more data & convert to string + b = sock.recv(socket_recv_max_size) + read_time = time() + + if len(b) == 0: + _empty_buffer += 1 + + if _empty_buffer >= 50: + print_timestamp('WARNING: possibly broken connection to Crestron (got {} empty responses)! Will try to reconnect...'.format(_empty_buffer)) + continue_recv = False + run_main_loop = False + else: + _empty_buffer = 0 + + yield (b.decode('utf-8')) + + except socket.error as e: + if e.errno != errno.EWOULDBLOCK: + # Error! Print it and tell main loop to stop + print_timestamp('ERROR: cannot receive data: [%r]. Connection closed by server or network problem?' % e) + run_main_loop = False + # If e.errno is errno.EWOULDBLOCK, then no more data + continue_recv = False + + except BrokenPipeError as e: + print_timestamp('ERROR: BrokenPipeError caught [{!r}]'.format(e)) + run_main_loop = False + continue_recv = False + + if (not run_main_loop) or (not sock): + break + + if read_timeout is not None: + if time() - read_time >= read_timeout: + run_main_loop = False # stop waiting to read from network + + if time() - t >= action_retry_timeout: # timeout for resending original request + run_main_loop = action_request(sock) + t = time() + else: + sleep(idle_wait) + + +################################################################# +# Create a TCP/IP socket +def server_connection(): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + for _i in range(1, CONNECTION_TRY_ROUND_COUNT): # try a few rounds with some delays + for _p in range(0, CRESTRON_PORT_COUNT + 1): # Look for an open port by trying to open all possible ones: + _port = CRESTRON_PORT + _p + try: + sock.connect((CRESTRON_HOST, _port)) + except Exception as _a: + print_timestamp("WARNING: looks like connection to {}:{} is not possible ({})!".format(CRESTRON_HOST, _port, _a)) + continue + + return sock + sleep(_i*3) + + raise Exception('Cannot connect to the CrestronSystem (via {}:{}-{})'.format(CRESTRON_HOST, CRESTRON_PORT, CRESTRON_PORT + CRESTRON_PORT_COUNT)) +# return None + + +########################################################################################## +def main_loop(action, target_spec): + global PRJ + # binary_stream = io.BytesIO() + + # all possible Crestron responses: + response_regexp = re.compile(r"PRJ_(.*)_STAT=([^=\n]+)$") + list_response_regexp = re.compile(r"PRJ_LIST=(.*)$") + error_response_regexp = re.compile(r"PROTOCOL_ERROR: (.*)$") + + ret = 0 + + _sock = None ## Socket for connection with Crestron server + _new_list = False + buffer = '' + + try: + todos = '' + + # initial stage: + while True and (not PRJ): + while not _sock: + _sock = server_connection() + + assert _sock + + # if we need to know all projectors (and they were not specified from outside) then query them! + for b in socket_communicator(_sock, COMMAND_RETRY_TIMEOUT, + action_request=(lambda s: send_request_prj_list(s)), + read_timeout = 4*60): + + buffer += b + items = buffer.split(EOC) + + buffer = '' + + for s in items[:-1]: + print_timestamp('Response: [{!r}].'.format(s)) + + # PRJ_LIST=*,*,* + match = list_response_regexp.match(s) + if match: + p = match.group(1) # CSV list of projectors + print_timestamp('List of all known projectors: [{}]'.format(p)) + + # NOTE: Update the list of all projectors only if it was not set via ENV! + PRJ = p.split(',') + continue + + # else: keep unprocessed responses for later processing + todos += s + EOC + + buffer = items[-1] # the rest after last EOC separator: ''! + + if buffer != '': + print_timestamp('WARNING: possibly missing End-of-response-separator. Tailing buffer: [{!r}]'.format(buffer)) + + if PRJ: # stop reading if we already to PRJ_LIST=....! + break + + buffer = todos + buffer # NOTE: unhandled respones - may go missing if nothing will be read from the socket :-( + + Targets = [] + + if PRJ and (target_spec == 'all'): + for h in PRJ: + if h not in Targets: + Targets.append(h) + elif PRJ and (target_spec in PRJ) and (target_spec not in Targets): + Targets.append(target_spec) + elif (target_spec != 'all'): + print('WARNING: possibly wrong target projector spec: [{}]!'.format(target_spec)) + Targets.append(target_spec) + + print_timestamp('Current targets for handling: {}'.format(Targets)) + + # main stage + while True: + + while not _sock: + _sock = server_connection() + + assert _sock + + for b in socket_communicator(_sock, COMMAND_RETRY_TIMEOUT, + action_request=(lambda s: send_requests(s, action, Targets))): + + buffer += b + + items = buffer.split(EOC) + for s in items[:-1]: + print_timestamp('Response: [{!r}]. Processing...'.format(s)) + + # PRJ_*_STAT=* + match = response_regexp.match(s) + if match: + p = match.group(1) # projector name + st = match.group(2) # new status string + curr = get_stat(p) + if curr != st: # NOTE: check goal condition after each state change: + print_timestamp('State of \'{}\': {} -> {}'.format(p, curr, st)) + STAT[p] = st + + if p in Targets: + if action == 'STATUS': + if all(get_stat(t) != '?' for t in Targets): # any status: ON/OFF/COOLING/WARMING etc... + return ret + elif action == 'ON': + if get_stat(p) in ON: + Targets.remove(p) + if not Targets: + return ret + elif action == 'OFF': + if get_stat(p) in OFF: + Targets.remove(p) + if not Targets: + return ret + # assert (action == 'LISTEN') + continue + + + # PRJ_LIST=*,*,* + match = list_response_regexp.match(s) + if match: + print_timestamp('WARNING: projector list update is not expected on this stage!') + + p = match.group(1) # CSV list of projectors + print_timestamp('Updated list of projectors: [{}]'.format(p)) + + if not PRJ: # NOTE: Update the list of all projectors only if it was not set via ENV! + PRJ = p.split(',') + + if target_spec == 'all': + for h in PRJ: + if h not in Targets: + print_timestamp('New projector ID: [{}]'.format(h)) + Targets.append(h) + # trigger general status update + _new_list = True + + continue + + # PROTOCOL_ERROR: ... + match = error_response_regexp.match(s) + if match: # TODO: the following is untested since it is not yet implemented on the server + p = match.group(1) # error message + print_timestamp('WARNING: server could not handle some of our requests: [{}]! Please check projector IDs!'.format(p)) + continue + + # else + print_timestamp('WARNING: unexpected response: [{}]!'.format(s)) + + buffer = items[-1] # the rest after last EOC separator: ''! + + if buffer != '': + print_timestamp('WARNING: possibly missing End-of-response-separator. Tailing buffer: [{!r}]'.format(buffer)) + + if _new_list: + send_requests(_sock, action, Targets) + _new_list = False + + + sleep(10) + + except BrokenPipeError as e: + print_timestamp('ERROR: BrokenPipeError caught [{!r}]'.format(e)) + ret = -1 + except Exception as e: + print_timestamp('ERROR: [{!r}]'.format(e)) + ret = -1 + finally: + if _sock: + _sock.close() + _sock = None + return ret + +########################################################################################## +if __name__ == "__main__": + _start = time() + + if socket.gethostname().find('.') >= 0: + name = socket.gethostname() + else: + name = socket.gethostbyaddr(socket.gethostname())[0] + + # print("localhost: {}".format(name)) + assert name in [socket.gethostname(), socket.getfqdn(), os.uname()[1], platform.node(), platform.uname()[1]] + + ##################################################################### + _action = 'STATUS' + if len(sys.argv) >= 2: + a = str(sys.argv[1]).upper() + if a in ['ON', 'OFF', 'STATUS', 'LISTEN']: + _action = a + else: + print_timestamp('CLI arguments: [{}].'.format(sys.argv)) + + print('ERROR: wrong target state spec: [{}] (should be ON, OFF, STATUS or LISTEN)'.format(_action)) + print('Usage: {} [(STATUS|LISTEN|ON|OFF) [(all|kiosk023(106|143|212).ads.eso.org)]]'.format(sys.argv[0])) + sys.exit(-1) + # print('WARNING: Ignoring wrong target state spec: [{}] (should be ON, OFF, STATUS or LISTEN), Default: [{}]'.format(a, _action)) + + if len(sys.argv) >= 3: + a = str(sys.argv[2]) + if a.lower() == 'all': + a = 'all' + + if (a == 'all') or (PRJ and (a in PRJ)): + name = a + else: + print_timestamp('CLI arguments: [{}].'.format(sys.argv)) + print('WARNING: possibly wrong target argument: [{}] (it should be "all" or projector host name!)'.format(a)) + name = a # 'all' + # sys.exit(-1) + + if len(sys.argv) >= 4: + print('WARNING: Ignoring further present arguments: [{}]'.format(sys.argv[3:])) + + assert _action in ['ON', 'OFF', 'STATUS', 'LISTEN'] + else: + # No CLI arguments... + print('Usage: {} [(STATUS|LISTEN|ON|OFF) [(all|kiosk023(106|143|212).ads.eso.org)]]'.format(sys.argv[0])) + sys.exit(0) + + print_timestamp('Function: [{}], Target projector(s): [{}]'.format(_action, name)) + # print_timestamp() + + try: + sys.exit(main_loop(_action, name)) + except KeyboardInterrupt: + e = sys.exc_info()[1] + sys.stderr.write("\nUser interrupted process.\n") +# sys.stderr.flush() + sys.exit(0) + except SystemExit: + e = sys.exc_info()[1] + sys.exit(e.code) + except Exception: + e = sys.exc_info()[1] + sys.stderr.write("\nERROR: unhandled exception occurred: (%s).\n" % e) +# sys.stderr.flush() + sys.exit(-1) + finally: + if STAT: + print_timestamp("Final state(s): ", STAT) + print_timestamp("Time spent: {} sec".format(time() - _start)) diff --git a/tools/hilbert-station b/tools/hilbert-station index 2fc5490..57e0bba 100755 --- a/tools/hilbert-station +++ b/tools/hilbert-station @@ -1,6 +1,6 @@ #! /usr/bin/env bash # for emacs: -*-sh-*- - +#set -x ## TODO: add some more description here? declare -rg TOOL_SH="$0" @@ -42,6 +42,7 @@ declare -rg ERR_CODE_INT_ERROR=2 # error due to wrong usage of scripts (bad argu export RSYNC=${HILBERT_DEPENDENCY_RSYNC:-rsync} +export DATE=${HILBERT_DEPENDENCY_DATE:-date} export GREP=${HILBERT_DEPENDENCY_GREP:-grep} export SORT=${HILBERT_DEPENDENCY_SORT:-sort} export UNIQ=${HILBERT_DEPENDENCY_UNIQ:-uniq} @@ -73,6 +74,9 @@ export REMOVE=${HILBERT_DEPENDENCY_REMOVE:-rm} export CHMOD=${HILBERT_DEPENDENCY_CHMOD:-chmod} export FLOCK=${HILBERT_DEPENDENCY_FLOCK:-flock} export PIDOF=${HILBERT_DEPENDENCY_PIDOF:-pidof} +export NOHUP=${HILBERT_DEPENDENCY_NOHUP:-nohup} +export SLEEP=${HILBERT_DEPENDENCY_SLEEP:-sleep} +export SETSID=${HILBERT_DEPENDENCY_SETSID:-setsid} # required for comparing config-dirs: export DIFF=${HILBERT_DEPENDENCY_DIFF:-diff} @@ -83,6 +87,8 @@ export PATH="${PATH}:$(${READLINK} -f "$(${DIRNAME} "${TOOL_SH}")")" # Add the t declare -rg LOGGER="${HILBERT_DEPENDENCY_LOGGER:-logger --id=$$ -t ${TOOL} }" declare -rg SHUTDOWN="${HILBERT_DEPENDENCY_SHUTDOWN:-$(PATH="${PATH}:/sbin:/usr/sbin" ${WHICH} shutdown)}" +export HILBERT_SHUTDOWN_DELAY=${HILBERT_SHUTDOWN_DELAY:-1s} + declare -g LOGLEVEL=${LOGLEVEL:-2} # Default setting: show WARNINGs and ERRORs (but not DEBUG/INFO messages) declare -g DRY_RUN="${DRY_RUN:-}" # NOTE: turn on dry mode (not actual service-runtime action execution) if non-empty @@ -96,10 +102,11 @@ declare -g hibert_station_process_kill_timeout=${hibert_station_process_kill_tim ## NOTE: usually /var/lock/ is a symbolic link to /run/lock/ => it will be removed in case of a crash! No stale lockfile is possible! ## NOTE: just in case if /run != /var/run the following loop will check them separately: ## NOTE: /var/tmp and /tmp/ may not be cleaned up due to reboot... But they are always writeable! -for d in "${HILBERT_LOCKFILE_DIR:-/var/run/hilbert}" /run/lock/lockdev /run/lock /run /var/run/lock/lockdev /var/run/lock /var/run /var/lock/lockdev /var/lock /var/tmp /tmp; +for d in "${HILBERT_LOCKFILE_DIR}" /var/run/hilbert /run/lock/lockdev /run/lock /run /var/run/lock/lockdev /var/run/lock /var/run /var/lock/lockdev /var/lock /var/tmp /tmp; do if [[ -w "${d}/" ]]; then - declare -rg LOCKFILE="${d}/lockfile_${TOOL}.lock" + export HILBERT_LOCKFILE_DIR="${d}" + declare -rg LOCKFILE="${HILBERT_LOCKFILE_DIR}/lockfile_${TOOL}.lock" break fi done @@ -120,7 +127,7 @@ function unlock() { _lock u; } # drop a lock exec 3>&1 4>&2 ## See also https://www.gnu.org/software/bash/manual/bashref.html#index-signal-handling ## NOTE: SIGKILL and SIGSTOP can not be caught, blocked or ignored. -trap 'exec 2>&4 1>&3 ' SIGHUP SIGINT SIGQUIT SIGPIPE SIGTERM EXIT RETURN +trap 'exec 2>&4 1>&3 ' SIGHUP SIGINT SIGQUIT SIGPIPE SIGTERM EXIT # RETURN ## NOTE: SIGTSTP => bg! ? RETURN ??? exec 2>&1 # 1>log.out @@ -139,13 +146,12 @@ function cmd_start_locking() { ## @fn Start a lock to avoid parallel execution o fi local pid - for pid in $(${PIDOF} -s "${TOOL}"); do - if [[ $pid != $$ ]]; then - ERROR "Process is already running with PID $pid" + for pid in $(${PIDOF} "${TOOL}"); do + if [[ ${pid} != $$ ]]; then + ERROR "Process is already running with PID [$pid]" exit ${ERR_CODE_EXT_ERROR} fi done - DEBUG "Starting exclusive (locked) usage of ${TOOL}..." @@ -172,16 +178,19 @@ function cmd_start_locking() { ## @fn Start a lock to avoid parallel execution o ## https://unix.stackexchange.com/questions/9957/how-to-check-if-bash-can-print-colors ## https://linux.101hacks.com/ps1-examples/prompt-color-using-tput/ if [[ -t 1 && -n "$(${TPUT} colors 2>/dev/null)" && "$(${TPUT} colors 2>/dev/null)" -ge 8 ]]; then + declare -rg ANSI_DIM="$(${TPUT} dim)" declare -rg ANSI_NO_COLOR="$(${TPUT} sgr0)" # '\033[0m' declare -rg ANSI_RED="$(${TPUT} setaf 1)" # '\033[1;31m' + declare -rg ANSI_GREEN="$(${TPUT} setaf 2)" declare -rg ANSI_YELLOW="$(${TPUT} setaf 3)" # '\033[1;33m' declare -rg ANSI_BRIGHT_MAGENTA="$(${TPUT} setaf 5)" # '\033[1;95m' declare -rg ANSI_CYAN="$(${TPUT} setaf 6)" # '\033[1;36m' - declare -rg ANSI_GRAY="$(${TPUT} dim)$(${TPUT} setaf 7)" # '\033[0;37m' -# declare -rg ANSI_BRIGHT_WHITE="$(${TPUT} setaf 7)" # '\033[1;97m' + declare -rg ANSI_BRIGHT_WHITE="$(${TPUT} setaf 7)" # '\033[1;97m' + declare -rg ANSI_GRAY="${ANSI_DIM}${ANSI_BRIGHT_WHITE}" # '\033[0;37m' fi # default colors for the logging routines: +declare -rg COLOR_TIMER="${HILBERT_CONSOLE_COLOR_TIMER:-${ANSI_GREEN:-=*= }}" declare -rg COLOR_DEBUG="${HILBERT_CONSOLE_COLOR_DEBUG:-${ANSI_GRAY:-=== }}" declare -rg COLOR_INFO="${HILBERT_CONSOLE_COLOR_INFO:-${ANSI_CYAN:- }}" declare -rg COLOR_WARNING="${HILBERT_CONSOLE_COLOR_WARNING:-${ANSI_YELLOW:-!!! }}" @@ -199,6 +208,19 @@ function DRYRUN_ECHO() { return ${ERR_CODE_SUCCESS} } +function TIMER() { +# local _time_save="${TIMEFORMAT}" + if [[ $LOGLEVEL -le 1 ]]; then + TIMEFORMAT="${COLOR_TIMER}TIMER [$*] => User: %3lU, System: %3lS, Real: %3lR${COLOR_NONE}" + time "$@" + else + "$@" + fi + + return $? + +# TIME="${_time_save}" +} function DRYRUN() { local _cmd="$@" @@ -292,7 +314,7 @@ EOF fi if [[ $LOGLEVEL -le -2 ]]; then - local FUN=$(${GREP} '^ *function .*()' "${TOOL_SH}" | ${SED} -e 's@function @@g' -e 's@().*$@|@g' | ${GREP} -vE '(_lock|_no_more_locking|_prepare_locking|exlock_now|exlock|shlock|unlock|DEBUG|INFO|WARNING|ERROR|DRYRUN|_which_ext|_service_get_)' | ${XARGS} | ${SED} -e 's@| @|@g' -e 's@|$@@g' ) + local FUN=$(${GREP} '^ *function .*()' "${TOOL_SH}" | ${SED} -e 's@function @@g' -e 's%().*$%|%g' | ${GREP} -vE '(_lock|_no_more_locking|_prepare_locking|exlock_now|exlock|shlock|unlock|DEBUG|INFO|WARNING|ERROR|DRYRUN|TIMER|_which_ext|_service_get_)' | ${XARGS} | ${SED} -e 's@| @|@g' -e 's%|$%%g' ) ${CAT} << EOF Call tree for internal commands supported by this tool: @@ -311,13 +333,18 @@ Hilbert - client part for Linux systems positional arguments: subcommands: - init [] init station based on given or installed configuration - list_applications list of (supported) applications - list_services list of background services - app_change change the currently running top application to specified - start [app_id] start Hilbert on the system - stop stop Hilbert on the system - shutdown shut down the system + init [] init station based on given or installed configuration + list_applications list of (supported) applications + list_services list of background services + app_change change the currently running top application to specified + app_restart restart the currently running top application + start [] start Hilbert on the system + pause pause Hilbert on the system + stop stop Hilbert on the system + shutdown [now] shut down the system + reboot [now] reboot the system + cleanup [--force] local system cleanup (identical to docker_cleanup on Linux host) + docker_cleanup [--force] total cleanup of local docker engine (images, data volumes) optional arguments: -h show this help message [+internal commands/call tree, depending on verbosity] and exit @@ -441,14 +468,14 @@ function cmd_native_autodetect() { ## @fn Native Auto-detections function _which_ext() { ## @fn Determine and list external executable local _t # NOTE: try to remove variable-assignments, like " A=B " to get the real command: - _t="$(${ECHO} "$@" | ${SED} -e 's@^ *\( *[^= ][^ =]*=[^ ]* \)* *@@g' -e 's@ .*$@@g' 2>&1)" + _t="$(${ECHO} "$@" | ${SED} -e 's@^ *\( *[^= ][^ =]*=[^ ]* \)* *@@g' -e 's% .*$%%g' 2>&1)" _t="$(${WHICH} "${_t}" 2>&1)" if [[ $? -eq 0 ]]; then ${LIST} -l "${_t}" 2>&1 return $? else # No such command in the PATH? - ${ECHO} "?/$@/?" + ${ECHO} "?/$*/?" return ${ERR_CODE_EXT_ERROR} fi } @@ -469,12 +496,15 @@ Do locking: [${LOCKING}] FLOCK: [${FLOCK}: $(_which_ext "${FLOCK}")] PIDOF: [${PIDOF}: $(_which_ext "${PIDOF}")] LOGGER: [${LOGGER}: $(_which_ext "${LOGGER}")] -SHUTDOWN: [${SHUTDOWN}: $(_which_ext "${SHUTDOWN}")] +SHUTDOWN: [${SHUTDOWN}: $(_which_ext "${SHUTDOWN}")], HILBERT_SHUTDOWN_DELAY: [${HILBERT_SHUTDOWN_DELAY}] + +HILBERT_LOCKFILE_DIR: [${HILBERT_LOCKFILE_DIR}] SH: [${SH}: $(_which_ext "${SH}")] ENV: [${ENV}: $(_which_ext "${ENV}")] SED: [${SED}: $(_which_ext "${SED}")] CAT: [${CAT}: $(_which_ext "${CAT}")] +DATE: [${DATE}: $(_which_ext "${DATE}")] GREP: [${GREP}: $(_which_ext "${GREP}")] ECHO: [${ECHO}: $(_which_ext "${ECHO}")] TPUT: [${TPUT}: $(_which_ext "${TPUT}")] @@ -486,7 +516,9 @@ MOVE: [${MOVE}: $(_which_ext "${MOVE}")] SUDO: [${SUDO}: $(_which_ext "${SUDO}")] SORT: [${SORT}: $(_which_ext "${SORT}")] UNIQ: [${UNIQ}: $(_which_ext "${UNIQ}")] +NOHUP: [${NOHUP}: $(_which_ext "${NOHUP}")] RSYNC: [${RSYNC} ($(${RSYNC} --version 2>&1 | ${GREP} version)): $(_which_ext "${RSYNC}")] +SLEEP: [${SLEEP}: $(_which_ext "${SLEEP}")] WHICH: [${WHICH}: $(_which_ext "${WHICH}")] MKDIR: [${MKDIR}: $(_which_ext "${MKDIR}")] CHMOD: [${CHMOD}: $(_which_ext "${CHMOD}")] @@ -496,6 +528,7 @@ XHOST: [${XHOST}: $(_which_ext "${XHOST}")] MKTEMP: [${MKTEMP}: $(_which_ext "${MKTEMP}")] PRINTF: [${PRINTF}: $(_which_ext "${PRINTF}")] REMOVE: [${REMOVE}: $(_which_ext "${REMOVE}")] +SETSID: [${SETSID}: $(_which_ext "${SETSID}")] UNLINK: [${UNLINK}: $(_which_ext "${UNLINK}")] DIRNAME: [${DIRNAME}: $(_which_ext "${DIRNAME}")] BASENAME: [${BASENAME}: $(_which_ext "${BASENAME}")] @@ -524,7 +557,7 @@ HOME: [${HOME}] PWD: [${PWD}] PATH: [${PATH}] -date: [$(date)] +time stamp: [$(${DATE})] TERM: [${TERM}] uname: [$(uname -a)] hostname: [$(hostname), $(hostname -I)] @@ -542,7 +575,7 @@ EOF local H=($(${DOCKER} ps -qa --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" 2>/dev/null)) if [[ -n "${H[*]}" ]]; then ${CAT} << EOF -Previously started Hilbert's services/applications found: +Previously started Hilbert services/applications found: $(${DOCKER} ps -a --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}") EOF else @@ -657,7 +690,7 @@ fi DEBUG "Base Configuration directory [${HILBERT_CONFIG_BASEDIR}] exists and is readable..." if [[ ! -d "${HILBERT_CONFIG_DIR}" ]]; then - DEBUG "Configuration directory [${HILBERT_CONFIG_DIR}] is initialized yet!" + DEBUG "Configuration directory [${HILBERT_CONFIG_DIR}] is NOT initialized yet!" fi if [[ ! -r "${HILBERT_CONFIG_DIR}/${HILBERT_CONFIG_FILE}" ]]; then @@ -670,7 +703,7 @@ fi ## NOTE: Global knowledge about supported services/applications: declare -r _type_field='type' -declare -rA _type_specs=( ['compose']='file ref' ) +declare -rA _type_specs=( ['compose']='file ref' ['native']='ref' ) function _service_get_type_spec() { ## NOTE: ServiceType: 'compose' etc... @@ -749,19 +782,13 @@ function cmd_install_station_config() { ## @fn Try to Install new station config fi - ### TODO: check beforehand and if necessary pre stop everything ++ post start anew, - ### forget the currently running application choice?? - ### cmd_detect_changes - local _old="$(${READLINK} -f "${HILBERT_CONFIG_DIR}")" - # rsync -zavc src/ dest/ --dry-run # -zaic - local base=$(${BASENAME} "${arg}") local new_cfg_dir=$(${MKTEMP} -u -d --tmpdir="${HILBERT_CONFIG_BASEDIR}" "$base.XXXXXXXXXX") # dry run: just output it! DEBUG "NOTE: new config is [${base}] => new_cfg_dir: [${new_cfg_dir}]" # NOTE: take over the deployed station configurations DEBUG "Moving ${arg} => ${new_cfg_dir}..." - DRYRUN ${MOVE} "${arg}" "${new_cfg_dir}" # rsync --backup --backup-dir + DRYRUN ${MOVE} -b "${arg}" "${new_cfg_dir}" # rsync --backup --backup-dir if [[ $? -ne 0 ]]; then ERROR "Failed to move temporary configuration directory [${arg}] to [${new_cfg_dir}]!" exit ${ERR_CODE_EXT_ERROR} @@ -775,26 +802,44 @@ function cmd_install_station_config() { ## @fn Try to Install new station config exit ${ERR_CODE_INT_ERROR} fi - cmd_set_station_config_reference "$(${BASENAME} "${new_cfg_dir}")" - # TODO: any non-zero return codes? - - # TODO: backup older config for a while. FIXME: limited number of backups? - DRYRUN ${MOVE} --backup=numbered -T "${_old}" "${HILBERT_CONFIG_BACKUP}" - # Max 10 backups! - local _max_backup="10" - if [[ -d "${HILBERT_CONFIG_BACKUP}.~${_max_backup}~" ]]; then - local _i=1 + ### TODO: check beforehand and if necessary pre stop everything ++ post start anew, + ### forget the currently running application choice?? + ### cmd_detect_changes + local _ret + local _old="$(${READLINK} -e "${HILBERT_CONFIG_DIR}")" # NOTE: What if ${HILBERT_CONFIG_DIR} does not exist!?? + _ret=$? + # rsync -zavc src/ dest/ --dry-run # -zaic - DRYRUN ${REMOVE} -Rf "${HILBERT_CONFIG_BACKUP}.~${_i}~" + # create ..../configs symbolic link! + cmd_set_station_config_reference "$(${BASENAME} "${new_cfg_dir}")" + # TODO: any non-zero return codes? - for ((;;_i++)); do - [[ ${_i} -ge ${_max_backup} ]] && break + ls -la ~/.config/hilbert-station/ ~/.config/hilbert-station/configs "${new_cfg_dir}" + # TODO: backup older config for a while. FIXME: limited number of backups? + if [[ ${_ret} -eq 0 && -n "${_old}" ]]; then # There is a target for our symbolic link + if [[ -d "${_old}" && ! -L "${_old}" ]]; then #! NOTE: Fixing bug if _old == + DEBUG "Backing-up [${_old}] as [${HILBERT_CONFIG_BACKUP}]..." + DRYRUN ${MOVE} --backup=numbered -T "${_old}" "${HILBERT_CONFIG_BACKUP}" + + # Max 10 backups! + local _max_backup="10" + # NOTE: the following is due to internal functioning of "${MOVE} --backup=numbered"! + if [[ -d "${HILBERT_CONFIG_BACKUP}.~${_max_backup}~" ]]; then # latest backup has highest number + local _i=1 + DRYRUN ${REMOVE} -Rf "${HILBERT_CONFIG_BACKUP}.~${_i}~" # oldest backup + + for ((; _i < _max_backup; _i++)) ; do # for ((;;_i++)); do # [[ ${_i} -ge ${} ]] && break + # shift downwards DRYRUN ${MOVE} -f "${HILBERT_CONFIG_BACKUP}.~$((_i+1))~" "${HILBERT_CONFIG_BACKUP}.~${_i}~" - done + done + fi + fi fi + + return ${ERR_CODE_SUCCESS} } @@ -853,15 +898,16 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur # Current configuration: local _old="$(${READLINK} -f "${HILBERT_CONFIG_DIR}")" + _ret=$? # by default: only rerun all possible initialization/preparation routines! if [[ -z "${_arg}" ]]; then # no new config. Just check current, refresh it [and restart] # -L "${HILBERT_CONFIG_DIR}" && ?? - if [[ -d "${_old}" && -x "${_old}" && -r "${_old}" ]]; then + if [[ ${_ret} -eq 0 && -d "${_old}" && -x "${_old}" && -r "${_old}" ]]; then if [[ ! -r "${_old}/${HILBERT_CONFIG_FILE}" ]]; then ERROR "Current station config file [${_old}/${HILBERT_CONFIG_FILE}] is not readable or missing!" - exit ${ERR_CODE_INT_ERROR} + return ${ERR_CODE_INT_ERROR} fi INFO "Current configuration: [${HILBERT_CONFIG_DIR}] -> [${_old}]" @@ -876,10 +922,10 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur _ret=$? if [[ ${_ret} -ne 0 ]]; then ERROR "Failed to initialize/prepare current configuration!" - exit ${_ret} + return ${_ret} fi - # TODO: check for differences in caches!? + # TODO: check for differences in caches!?n _ret=${ERR_CODE_SUCCESS} @@ -892,11 +938,11 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur fi DEBUG "Preparation/initialization is done! Ret code: [${_ret}]." - exit ${_ret} + return ${_ret} else ERROR "No current configuration [${HILBERT_CONFIG_DIR}] -> [${_old}]!" ERROR "Wrong usage: please install/deploy a station configuration first!" - exit ${ERR_CODE_EXT_ERROR} + return ${ERR_CODE_EXT_ERROR} fi fi @@ -919,7 +965,7 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur _ret=$? if [[ ${_ret} -ne 0 ]]; then ERROR "Failed to detect the currently running application!" - exit ${_ret} + return ${_ret} fi _app="${APP_ID}" DEBUG "Hilbert is currently running! Query for the current application returned: [${_app}], with ret. status: [$?]." @@ -936,7 +982,7 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur _ret=$? if [[ ${_ret} -ne 0 ]]; then ERROR "Failed to initialize/prepare current configuration!" - exit ${_ret} + return ${_ret} fi _ret=${ERR_CODE_SUCCESS} @@ -963,7 +1009,7 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur if [[ ${_ret} -ne 0 ]]; then ERROR "Failed to install new configuration from [${_arg}]!" - exit ${_ret} + return ${_ret} fi _ret=${ERR_CODE_SUCCESS} @@ -984,7 +1030,7 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur HILBERT_CONFIG_DIR=${HILBERT_CONFIG_BACKUP} cmd_tool_subcall -L start "${_app}" DEBUG "Note that [HILBERT_CONFIG_DIR=${HILBERT_CONFIG_BACKUP} cmd_tool_subcall -L start \"${_app}\"] finished with exit code: [$?]..." - exit ${_ret} + return ${_ret} fi # starting from new configuration: @@ -1014,7 +1060,7 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur HILBERT_CONFIG_DIR=${HILBERT_CONFIG_BACKUP} cmd_tool_subcall -L start "${_app}" DEBUG "Note that [HILBERT_CONFIG_DIR=${HILBERT_CONFIG_BACKUP} cmd_tool_subcall -L start \"${_app}\"] finished with exit code: [$?]..." - exit ${_ret} + return ${_ret} fi return ${_ret} @@ -1030,7 +1076,7 @@ function cmd_init() { ## @fn Install/Initialize/Prepare Station configur function cmd_tool_subcall() { ## @fn Execute this tool recursively. Add [-L] to avoid lock-guards for sub-process! # LOGLEVEL=${LOGLEVEL} DRY_RUN=${DRY_RUN} HILBERT_CONFIG_DIR=...?! local _cmd="${ENV} ${TOOL_SH} ${INPUT_OPT} $@" - DEBUG "Running [$TOOL $@] via [${_cmd}]" + DEBUG "Running [$TOOL $*] via [${_cmd}]" eval "${_cmd}" return $? } @@ -1060,11 +1106,13 @@ function cmd_compare_station_configs() { ## @fn compare some station configurati ERROR "Wrong configuration folder: [${arg}]!" exit ${ERR_CODE_INT_ERROR} fi - + + local _ret local _old="$(${READLINK} -f "${HILBERT_CONFIG_DIR}")" + _ret=$? # -L "${HILBERT_CONFIG_DIR}" && # ?? - if [[ -d "${_old}" && -x "${_old}" && -r "${_old}" ]]; then + if [[ ${_ret} -eq 0 && -d "${_old}" && -x "${_old}" && -r "${_old}" ]]; then if [[ ! -r "${_old}/${HILBERT_CONFIG_FILE}" ]]; then WARNING "Current station config file [${_old}/${HILBERT_CONFIG_FILE}] is not readable or missing!" return 2 @@ -1240,6 +1288,38 @@ function cmd_xsetroot() { ## @fn Wrapper for xsetroot fi } +## @fn cmd_app_ +function cmd_app_restart() { ## @fn Restart the current top application (or start the default one) +# local _ret + cmd_native_autodetect + cmd_read_configuration + + cmd_detect_top_application + if [[ $? -ne 0 ]]; then + WARNING "Could not properly detect the current top application!" + fi + + if [[ -n "${current_top_application_id}" ]]; then + DEBUG "Trying to restart (all) found top apps: ${current_top_application_id}..." + cmd_docker_restart "${current_top_application_id}" + return $? + elif [[ -n "${APP_ID}" ]]; then + cmd_restart_service "${APP_ID}" + # ; return 0 # Same Application ID => Same Env.Vars??? :-( + return $? + elif [[ -n "${hilbert_station_default_application}" ]]; then + DEBUG "Not top application running current!? Running default app: [${hilbert_station_default_application}]!" + cmd_start_service "${hilbert_station_default_application}" + return $? + fi + + WARNING "No current top application and no default top app for this station!!!" + return ${ERR_CODE_EXT_ERROR} +# +# cmd_xsetroot "Paint X11 root desktop before starting [$arg]" +# return ${_ret} +} + function cmd_app_change() { ## @fn Change current application (to be stopped) to the given one (to be started) ## NOTE: Application ID: @@ -1296,7 +1376,9 @@ function cmd_start_service() { ## @fn Start Hilbert service exit ${ERR_CODE_EXT_ERROR} fi - if [[ "x$t" != "xcompose" ]]; then + local spec + spec=$(_service_get_type_spec "$t") + if [[ $? -ne 0 ]]; then ERROR "Unsupported application type of service/application [${arg}]: [$t]" exit ${ERR_CODE_EXT_ERROR} fi @@ -1309,8 +1391,16 @@ function cmd_start_service() { ## @fn Start Hilbert service eval "${HILBERT_APPLICATION_AD}" fi - DEBUG "Starting service/application via docker-compose: ${arg}..." - cmd_compose_start "${arg}" + if [[ "x$t" = "xcompose" ]]; then + DEBUG "Starting service/application via docker-compose: ${arg}..." + cmd_compose_start "${arg}" + elif [[ "x$t" = "xnative" ]]; then + DEBUG "Starting native service/application: ${arg}..." + cmd_native init "${arg}" + else + ERROR "Unsupported application: [${arg}] (type: [$t])" + return ${ERR_CODE_INT_ERROR} + fi return $? } @@ -1334,7 +1424,9 @@ function cmd_init_service() { ## @fn Prepare/initialize Hilbert service/applicat exit ${ERR_CODE_EXT_ERROR} fi - if [[ ! "x$t" = "xcompose" ]]; then + local spec + spec=$(_service_get_type_spec "$t") + if [[ $? -ne 0 ]]; then ERROR "Wrong or unsupported application: [${arg}] (type: [$t])" exit ${ERR_CODE_EXT_ERROR} fi @@ -1355,8 +1447,17 @@ function cmd_init_service() { ## @fn Prepare/initialize Hilbert service/applicat eval "${HILBERT_APPLICATION_PREINIT}" fi - DEBUG "Initializing/preparing service/application via docker-compose: ${arg}..." - cmd_compose_init "${arg}" + if [[ "x$t" = "xcompose" ]]; then + DEBUG "Initializing/preparing service/application via docker-compose: ${arg}..." + cmd_compose_init "${arg}" + elif [[ "x$t" = "xnative" ]]; then + DEBUG "Initiaziing/preparing native service/application: ${arg}..." + cmd_native init "${arg}" + else + ERROR "Unsupported application: [${arg}] (type: [$t])" + return ${ERR_CODE_INT_ERROR} + fi + return $? } @@ -1380,7 +1481,9 @@ function cmd_restart_service() { ## @fn Restart currently running service NOTE: exit ${ERR_CODE_EXT_ERROR} fi - if [[ ! "x$t" = "xcompose" ]]; then + local spec + spec=$(_service_get_type_spec "$t") + if [[ $? -ne 0 ]]; then ERROR "Wrong or unsupported application: [${arg}] (type: [$t])" exit ${ERR_CODE_EXT_ERROR} fi @@ -1393,8 +1496,17 @@ function cmd_restart_service() { ## @fn Restart currently running service NOTE: eval "${HILBERT_APPLICATION_AD}" fi - DEBUG "Restarting service/application via docker-compose: ${arg}..." - cmd_compose_restart "${arg}" + + if [[ "x$t" = "xcompose" ]]; then + DEBUG "Restarting service/application via docker-compose: ${arg}..." + cmd_compose_restart "${arg}" + elif [[ "x$t" = "xnative" ]]; then + DEBUG "Restarting native service/application: ${arg}..." + cmd_native restart "${arg}" + else + ERROR "Unsupported application: [${arg}] (type: [$t])" + return ${ERR_CODE_INT_ERROR} + fi return $? } @@ -1419,17 +1531,117 @@ function cmd_stop_service() { ## @fn Stop Hilbert Service, @param arg Applicatio exit ${ERR_CODE_EXT_ERROR} fi - if [[ ! "x$t" = "xcompose" ]]; then + local spec + spec=$(_service_get_type_spec "$t") + if [[ $? -ne 0 ]]; then + ERROR "Wrong or unsupported application: [${arg}] (type: [$t])" + exit ${ERR_CODE_EXT_ERROR} + fi + + + if [[ "x$t" = "xcompose" ]]; then + DEBUG "Stopping service/application via docker-compose: [${arg}]..." + cmd_compose_stop "${arg}" + elif [[ "x$t" = "xnative" ]]; then + DEBUG "Stopping native service/application via docker-compose: [${arg}]..." + cmd_native stop "${arg}" + else + ERROR "Unsupported application: [${arg}] (type: [$t])" + return ${ERR_CODE_INT_ERROR} + fi + + return $? +} + + +function cmd_pause_service() { ## @fn Pause Hilbert Service, @param arg ApplicationID + ## NOTE: Application ID + local arg="$*" + + if [[ -z "${arg}" ]]; then + ERROR "Wrong argument [${arg}]!" + cmd_usage + exit ${ERR_CODE_EXT_ERROR} + fi + + cmd_read_configuration + + local t + t="$(_service_get_type "${arg}")" + + if [[ $? -ne 0 ]]; then + ERROR "Invalid service/application: [${arg}]!" + exit ${ERR_CODE_EXT_ERROR} + fi + + local spec + spec=$(_service_get_type_spec "$t") # NOTE: check presence! + if [[ $? -ne 0 ]]; then ERROR "Wrong or unsupported application: [${arg}] (type: [$t])" exit ${ERR_CODE_EXT_ERROR} fi - DEBUG "Stopping service/application via docker-compose: [${arg}]..." - cmd_compose_stop "${arg}" + if [[ "x$t" = "xcompose" ]]; then + DEBUG "Pausing service/application via docker-compose: [${arg}]..." + cmd_compose_pause "${arg}" + elif [[ "x$t" = "xnative" ]]; then + DEBUG "Pausing native service/application via native: [${arg}]..." + cmd_native pause "${arg}" + else + ERROR "Unsupported application: [${arg}] (type: [$t])" + return ${ERR_CODE_INT_ERROR} + fi return $? } +function cmd_native() { ## @fn Action with a native service/application + local cmd=$1; shift + local arg=$1; shift # APP_ID! + + local HILBERT_APPLICATION_REF + HILBERT_APPLICATION_REF="$(_service_get_field "$arg" "ref")" + if [[ $? -ne 0 ]]; then + ERROR "Invalid reference for a native service/application: [${arg}]!" + return ${ERR_CODE_INT_ERROR} + fi + + if [[ ! -e "${HILBERT_APPLICATION_REF}" ]]; then + ERROR "Invalid reference [${HILBERT_APPLICATION_REF}] for a native service/application [${arg}]!" + return ${ERR_CODE_INT_ERROR} + fi + + local PID + if [[ "x$cmd" = "xstart" ]]; then + ${NOHUP} "${HILBERT_APPLICATION_REF}" &> "/tmp/hilbert_start_${arg}.log" & + PID=${!} + echo "${PID}" > "${HILBERT_LOCKFILE_DIR}/hilbert_${TOOL}_${arg}.pid" + elif [[ "x$cmd" = "xstop" ]]; then +# for pid in $(${PIDOF} -s "${TOOL}"); do + + ERROR "Sorry: stopping native service/application is currently not supported!" + return ${ERR_CODE_INT_ERROR} + elif [[ "x$cmd" = "xpause" ]]; then +# for pid in $(${PIDOF} -s "${TOOL}"); do + + ERROR "Sorry: pausing native service/application is currently not supported!" + return ${ERR_CODE_INT_ERROR} + elif [[ "x$cmd" = "xrestart" ]]; then + : "${SH}" -c "${HILBERT_APPLICATION_REF}" + elif [[ "x$cmd" = "xinit" ]]; then # prepare! + : + elif [[ "x$cmd" = "xdetect" ]]; then + : + elif [[ "x$cmd" = "xdetectapp" ]]; then + : + else + ERROR "Unsupported action: [${cmd}] on application/service [${arg}]! :-(" + return ${ERR_CODE_INT_ERROR} + fi + return $? # ${ERR_CODE_SUCCESS} +} + + function cmd_compose_start() { ## @fn Run Docker-Compose service ## NOTE: Application ID local arg="$*" @@ -1581,7 +1793,7 @@ function cmd_compose_stop() { ## @fn Stop Docker-Compose service DEBUG "Stopping/Killing/Removing service [$d] using [${DOCKER_COMPOSE}]..." local _ret=${ERR_CODE_SUCCESS} - cmd_docker_compose kill -s SIGINT "$d" && sleep "${hibert_station_process_kill_timeout}" + cmd_docker_compose kill -s SIGINT "$d" # && ${SLEEP} "${hibert_station_process_kill_timeout}" _ret=$? # cmd_docker_compose kill -s SIGTERM "$d" # cmd_docker_compose kill -s SIGKILL "$d" @@ -1596,11 +1808,59 @@ function cmd_compose_stop() { ## @fn Stop Docker-Compose service return ${_ret} } -function cmd_docker_stop() { ## @fn Stop/kill/renove a docker container - local d=$* - DEBUG "Stopping/Killing/Removing service [$d] using [${DOCKER}]..." - cmd_docker kill -s SIGINT $d && sleep "${hibert_station_process_kill_timeout}" +function cmd_compose_pause() { ## @fn Pause Docker-Compose service + local arg="$*" # Application ID + + local HILBERT_APPLICATION_FILE + local HILBERT_APPLICATION_REF + HILBERT_APPLICATION_FILE="$(_service_get_field "$arg" "file")" + HILBERT_APPLICATION_REF="$(_service_get_field "$arg" "ref")" + + export HILBERT_APPLICATION_ID="$arg" # NOTE: To be set as env. variable APP_ID within container... + + ## NOTE: Simplify COMPOSE_FILE -> launch! + local PLAIN_COMPOSE_FILE="cache_for_${HILBERT_APPLICATION_ID}.yaml" + if [[ -r "${HILBERT_CONFIG_DIR}/${PLAIN_COMPOSE_FILE}" ]]; then + export COMPOSE_FILE="${PLAIN_COMPOSE_FILE}" + else + export COMPOSE_FILE="${HILBERT_APPLICATION_FILE:-docker-compose.yml}" # Default value + local DOCKER_COMPOSE_OVERRIDE="override_for_${HILBERT_APPLICATION_ID}.yaml" + if [[ -r "${HILBERT_CONFIG_DIR}/${DOCKER_COMPOSE_OVERRIDE}" ]]; then + export COMPOSE_FILE="${COMPOSE_FILE}${COMPOSE_PATH_SEPARATOR}${DOCKER_COMPOSE_OVERRIDE}" + fi + fi + + DEBUG "Pausing service/application: ${HILBERT_APPLICATION_ID} (${HILBERT_APPLICATION_REF} from ${COMPOSE_FILE})..." + + local d="${HILBERT_APPLICATION_REF}" + + DEBUG "Pausing service [$d] using [${DOCKER_COMPOSE}]..." + + local _ret=${ERR_CODE_SUCCESS} + cmd_docker_compose pause "$d" # && ${SLEEP} "${hibert_station_process_kill_timeout}" + _ret=$? + + unset -v HILBERT_APPLICATION_ID COMPOSE_FILE + + return ${_ret} +} + + +function cmd_docker_restart() { ## @fn Restart docker container(s) + local d="$*" + DEBUG "Restarting docker container(s) [$d] using [${DOCKER}]..." + + cmd_docker restart $d + return $? +} + + +function cmd_docker_stop() { ## @fn Stop/kill/remove docker container(s) + local d="$*" + DEBUG "Stopping/Killing/Removing docker container(s) [$d] using [${DOCKER}]..." + + cmd_docker kill -s SIGINT $d && ${SLEEP} "${hibert_station_process_kill_timeout}" # cmd_docker kill -s SIGTERM $d # cmd_docker kill -s SIGKILL $d cmd_docker stop -t "${hibert_station_process_kill_timeout}" $d cmd_docker rm -vf $d @@ -1610,9 +1870,19 @@ function cmd_docker_stop() { ## @fn Stop/kill/renove a docker container return $? } + +function cmd_docker_pause() { ## @fn Pause docker container(s) + local d="$*" + DEBUG "Pause service/application [$d] using [${DOCKER}]..." + + cmd_docker pause $d + + return $? +} + function cmd_docker_compose() { ## @fn Run low-level Docker-Compose command directly if [[ -n "${DRY_RUN}" ]]; then # TODO: switch to using DRYRUN - DRYRUN_ECHO "COMPOSE_FILE='${COMPOSE_FILE}' ${DOCKER_COMPOSE} --skip-hostname-check $@" + DRYRUN_ECHO "COMPOSE_FILE='${COMPOSE_FILE}' ${DOCKER_COMPOSE} --skip-hostname-check $*" return $? fi @@ -1622,13 +1892,14 @@ function cmd_docker_compose() { ## @fn Run low-level Docker-Compose command dire ## Actually run docker-compose # --debug / --verbose? # COMPOSE_FILE="${CF}" + local ret ${DOCKER_COMPOSE} --skip-hostname-check "$@" - local ret=$? + ret=$? cd "${S}" if [[ ${ret} -ne 0 ]]; then - ERROR "Could not run [${DOCKER_COMPOSE} --skip-hostname-check $@] in [${HILBERT_CONFIG_DIR}] with COMPOSE_FILE=[${COMPOSE_FILE}]!" + ERROR "Could not run [${DOCKER_COMPOSE} --skip-hostname-check $*] in [${HILBERT_CONFIG_DIR}] with COMPOSE_FILE=[${COMPOSE_FILE}]!" fi return ${ret} @@ -1641,13 +1912,14 @@ function cmd_docker() { ## @fn Run low-level Docker command directly fi local _CMD="$@" + local ret if [[ $LOGLEVEL -le "0" ]]; then _CMD="${DOCKER} --debug --log-level=debug ${_CMD}" else _CMD="${DOCKER} ${_CMD}" fi - eval ${_CMD} - local ret=$? + eval "${_CMD}" + ret=$? if [[ $ret -ne 0 ]]; then WARNING "Could not run [${_CMD}] in [${PWD}]! Ret: [${ret}]!" @@ -1662,13 +1934,63 @@ function cmd_docker() { ## @fn Run low-level Docker command directly } -#function cmd_docker_pause() { -# local d=$* -# DEBUG "Pause service/application [$d] using [${DOCKER}]..." -# -# cmd_docker pause $d -# return $? -#} + + +function cmd_pause() { ## @fn Pause Hilbert's current top application and background services + cmd_native_autodetect + cmd_read_configuration + + cmd_detect_top_application + if [[ $? -ne 0 ]]; then + WARNING "Could not properly detect the current Hilbert applications or services!" + fi + if [[ -n "${current_top_application_id}" ]]; then + INFO "Previously started Hilbert's application(s) detected: [${current_top_application_id}, APP_ID: ${APP_ID}]" + + if [[ -z "${APP_ID}" ]]; then + # ${DOCKER} ps -a --filter "label=is_top_app=1" --filter "label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}" + cmd_docker_pause ${current_top_application_id} + else + cmd_pause_service "${APP_ID}" + fi + +# cmd_xsetroot "Paint X11 after stopping [${APP_ID}]" + else + INFO "No previously started applications are currently detected" + fi + # Array of indices + + local indices=( ${!hilbert_station_profile_services[@]} ) + local N=${#indices[@]} + + if [[ $N -ne 0 ]]; then + local i + local sv + + for ((i=N - 1; i >= 0; i--)) ; do + sv="${hilbert_station_profile_services[indices[i]]}" + data="$(_service_get_info "$sv")" + if [[ $? -ne 0 ]]; then + WARNING "Trying to pause a bad service [${indices[i]}]: [${sv}]" + else + DEBUG "Pausing Service $sv: [$data]" + fi + + cmd_pause_service "${sv}" + + if [[ $? -ne 0 ]]; then + ERROR "Could not pause service: [${sv}] ($data)" + exit ${ERR_CODE_INT_ERROR} + fi + done + +# cmd_xsetroot "Paint X11 after pausing all services..." + else + DEBUG "No services should be running on this station!" + fi + + return $? +} function cmd_detect_top_application() { ## @fn Detect the currently running Hilbert (top) application ## NOTE: side effects: @@ -1712,7 +2034,7 @@ function cmd_detect_top_application() { ## @fn Detect the currently running Hil fi # NOTE: set "Env APP_ID=' to Application ID! - declare -gr APP_ID=$(${DOCKER} inspect --format='{{index .Config.Env }}' "${current_top_application_id}" 2>/dev/null | ${SED} -e 's@^.*APP_ID=@@g' -e 's@ .*$@@g') + declare -gr APP_ID=$(${DOCKER} inspect --format='{{index .Config.Env }}' "${current_top_application_id}" 2>/dev/null | ${SED} -e 's@^.*APP_ID=@@g' -e 's% .*$%%g') if [[ $? -ne 0 ]]; then ERROR "Could not inspect container [${current_top_application_id}] using ${DOCKER}" return ${ERR_CODE_EXT_ERROR} @@ -1930,7 +2252,7 @@ function cmd_start() { ## @fn (re)start services and top application [pr fi # DEBUG "Sleeping for [${hilbert_autostart_delay}] sec (due to 'hilbert_autostart_delay' from [${HILBERT_CONFIG_DIR}/${HILBERT_CONFIG_FILE}])..." -# sleep "${hilbert_autostart_delay}" +# ${SLEEP} "${hilbert_autostart_delay}" # Start with general station preparation @@ -1943,7 +2265,7 @@ function cmd_start() { ## @fn (re)start services and top application [pr local i local sv - for ((i=0; i < $N; i++)) ; do + for ((i = 0; i < N; i++)) ; do sv="${hilbert_station_profile_services[indices[i]]}" data="$(_service_get_info "$sv")" if [[ $? -ne 0 ]]; then @@ -2023,13 +2345,13 @@ function cmd_stop() { ## @fn Stop Hilbert's current top application and local i local sv - for ((i=$N - 1; i >= 0; i--)) ; do + for ((i = N - 1; i >= 0; i--)) ; do sv="${hilbert_station_profile_services[indices[i]]}" data="$(_service_get_info "$sv")" if [[ $? -ne 0 ]]; then WARNING "Trying to stop a bad service [${indices[i]}]: [${sv}]" else - DEBUG "Starting Service $sv: [$data]" + DEBUG "Stopping Service $sv: [$data]" fi cmd_stop_service "${sv}" @@ -2048,15 +2370,158 @@ function cmd_stop() { ## @fn Stop Hilbert's current top application and return $? } -function cmd_shutdown() { ## @fn Shutdown PC - local arg=$@ - DEBUG "Attempting to shut-down this system using [${SHUTDOWN} ${arg}])..." - DRYRUN ${SHUTDOWN} ${arg} &>/dev/null - if [[ $? -eq 0 ]]; then +function cmd_delayed_shutdown() { ## @fn Delayed Shutdown + local ret + local arg="$@" + ${SLEEP} "${HILBERT_SHUTDOWN_DELAY}" + ${SUDO} -n ${SHUTDOWN} ${arg} + ret=$? + if [[ ${ret} -eq 0 ]]; then return ${ERR_CODE_SUCCESS} fi - + ERROR "Failed to run [${SUDO} -n ${SHUTDOWN} ${arg}], exit code: $ret!" + return ${ERR_CODE_INT_ERROR} +} + +function cmd_docker_cleanup() { ## @fn total cleanup of local docker engine: images and data volumes + local ret=${ERR_CODE_SUCCESS} + + local arg="$*" + + INFO "Going to cleanup local docker images and data volumes..." + if [[ -n "${arg}" ]]; then + INFO "NOTE: supplied arguments: [${arg}]!" + fi + + local _hilbert_is_running + cmd_detect_running_hilbert # 0 - nothing! 1 => Hilbert is running! + _hilbert_is_running=$? + + if [[ "${_hilbert_is_running}" -eq 1 ]]; then + if [[ "${arg}" != "--force" ]]; then + ERROR "Cannot cleanup docker engine while Hilbert is running on the station!" + return ${ERR_CODE_EXT_ERROR} + else + WARNING "Trying to stop Hilbert due to '${arg}'!" + cmd_tool_subcall -L cmd_stop + if [[ $? -ne 0 ]]; then + ERROR "Could not properly stop Hilbert!.." + fi + fi + else + DEBUG "Hilbert is not running on the station!" + fi + + # get current containers + local containers=$(${DOCKER} ps -a -q 2>/dev/null | ${SORT} | ${UNIQ} | ${XARGS} --no-run-if-empty) + if [[ $? -ne 0 ]]; then + ERROR "Could not query Docker Engine via ${DOCKER}!" + return ${ERR_CODE_EXT_ERROR} + fi + + # try to kill them + if [[ -n "${containers}" ]]; then + WARNING "There are some docker containers: [${containers}]!" + if [[ "${arg}" == "--force" ]]; then + INFO "Trying to kill (all) found docker containers: [${containers}]..." + cmd_docker_stop ${containers} + if [[ $? -ne 0 ]]; then + ERROR "Could not kill those containers!" + fi + fi + else + DEBUG "No containers are present at local docker engine!" + fi + + # get current images + local dockerimages=$(${DOCKER} images -a -q 2>/dev/null | ${SORT} | ${UNIQ} | ${XARGS} --no-run-if-empty) + if [[ $? -ne 0 ]]; then + ERROR "Could not query Docker Engine via ${DOCKER}!" + return ${ERR_CODE_EXT_ERROR} + fi + + # try to kill them + if [[ -n "${dockerimages}" ]]; then + DEBUG "Found docker images: [${dockerimages}]!" + if [[ "${arg}" == "--force" ]]; then + cmd_docker rmi "${arg}" ${dockerimages} + ret=$? + else + cmd_docker rmi ${dockerimages} + ret=$? + fi + + if [[ ${ret} -ne 0 ]]; then + ERROR "Could not remove present docker image(s): [${dockerimages}]! Exit code: [${ret}]!" + return ${ERR_CODE_EXT_ERROR} + fi + else + DEBUG "Found no docker image to remove!" + fi + + # get data volumes + local dockervolumes=$(${DOCKER} volume ls -q 2>/dev/null | ${SORT} | ${UNIQ} | ${XARGS} --no-run-if-empty) + if [[ $? -ne 0 ]]; then + ERROR "Could not query Docker Engine via ${DOCKER}!" + return ${ERR_CODE_EXT_ERROR} + fi + + # try to kill them + if [[ -n "${dockervolumes}" ]]; then + DEBUG "Found docker volume(s): [${dockervolumes}]!" +# if [[ "${arg}" == "--force" ]]; then +# cmd_docker volume rm "${arg}" ${dockervolumes} # only supported by new docker cli +# ret=$? +# else + cmd_docker volume rm ${dockervolumes} + ret=$? +# fi + + if [[ ${ret} -ne 0 ]]; then + ERROR "Could not remove present docker volume(s) ([${dockervolumes}]). Exit code: [${ret}]!" + return ${ERR_CODE_EXT_ERROR} + fi + else + DEBUG "Found no docker volumes to remove!" + fi + + return ${ERR_CODE_SUCCESS} +} + +function cmd_shutdown() { ## @fn Shutdown PC + local ret + local arg="$@" + INFO "Attempting to shut-down this system using [${SHUTDOWN}] with arguments [${arg}]..." + if [[ -n "${DRY_RUN}" ]]; then + DRYRUN_ECHO "${NOHUP} ${SETSID} ${SUDO} -n ${SH} -c '${SLEEP} ${HILBERT_SHUTDOWN_DELAY}; ${SHUTDOWN} ${arg}' /dev/null 2>&1 & disown -h" + return ${ERR_CODE_SUCCESS} + else + # TODO: check for NOHUP on Windows! + # NOTE: see also https://unix.stackexchange.com/a/148698 for detailed BG execution info +# cmd_delayed_shutdown ${arg} /dev/null 2>&1 & disown -h +# ${NOHUP} ${SH} -c "${SLEEP} ${HILBERT_SHUTDOWN_DELAY}; ${SUDO} -n ${SHUTDOWN} ${arg}" /dev/null 2>&1 & disown -h + ${NOHUP} ${SETSID} ${SUDO} -n ${SH} -c "${SLEEP} ${HILBERT_SHUTDOWN_DELAY}; ${SHUTDOWN} ${arg}" /dev/null 2>&1 & disown -h +# ${NOHUP} ${SETSID} ${SH} -c "${SLEEP} ${HILBERT_SHUTDOWN_DELAY} && ${SUDO} -n ${SHUTDOWN} ${arg}" /dev/null 2>&1 & disown -h + ret=$? + fi + + if [[ ${ret} -ne 0 || -z "${!}" ]]; then + ERROR "Could not schedule the shutdown! Ret. code: [$ret]" + return ${ERR_CODE_INT_ERROR} + fi + + DEBUG "Scheduled the shutdown in background [${!}]." + return ${ERR_CODE_SUCCESS} + +########################################################################## + + + DRYRUN ${SHUTDOWN} ${arg} &>/dev/null +# if [[ $? -eq 0 ]]; then +# return ${ERR_CODE_SUCCESS} +# fi + DRYRUN ${SUDO} -n -P ${SHUTDOWN} ${arg} &>/dev/null if [[ $? -eq 0 ]]; then return ${ERR_CODE_SUCCESS} @@ -2075,9 +2540,9 @@ function cmd_shutdown() { ## @fn Shutdown PC fi ERROR "Could not shutdown via [${SHUTDOWN} ${arg}] and [${SUDO} -n -P ${SHUTDOWN} ${arg}]!" - + # TODO: consider to use poweroff or halt ? or reboot? or special configuration on current target OS? - + return ${ERR_CODE_EXT_ERROR} } @@ -2102,34 +2567,61 @@ function cmd_subcommand_handle() { ## @fn Main CLI parser, sub-command handler ;; init) cmd_start_locking - cmd_init "$*" + TIMER cmd_init "$*" exit $? ;; app_change) cmd_start_locking - cmd_app_change "$*" + TIMER cmd_app_change "$*" + exit $? + ;; + app_restart) + cmd_start_locking + TIMER cmd_app_restart "$*" exit $? ;; stop) cmd_start_locking ## TODO: arguments? - cmd_stop "$*" + TIMER cmd_stop "$*" + exit $? + ;; + # NOTE/TODO: maybe also add "unpause"? + pause) + cmd_start_locking + ## TODO: arguments? + TIMER cmd_pause "$*" exit $? ;; shutdown) cmd_start_locking local arg="$*" - if [[ "${arg}" = "now" || "${arg}" = "-r now" ]]; then + if [[ "${arg}" = "now" ]]; then cmd_shutdown "$@" else cmd_shutdown fi exit $? ;; + reboot) + cmd_start_locking + local arg="$*" + if [[ "${arg}" = "now" ]]; then + cmd_shutdown -r "$@" + else + cmd_shutdown -r + fi + exit $? + ;; start) cmd_start_locking ## TODO: arguments? - cmd_start "$*" + TIMER cmd_start "$*" + exit $? + ;; + cleanup|docker_cleanup) + cmd_start_locking + TIMER cmd_docker_cleanup "$*" exit $? ;; cmd_usage|cmd_tool_subcall|cmd_native_autodetect|cmd_start_locking|cmd_subcommand_handle) @@ -2137,10 +2629,10 @@ function cmd_subcommand_handle() { ## @fn Main CLI parser, sub-command handler _ret=$? if [[ ${_ret} -ne 0 ]]; then - ERROR "Something went wrong in subcommand handler: [${subcommand} \"$@\"]. Exit code: ${_ret}!" + ERROR "Something went wrong in subcommand handler: [${subcommand} \"$*\"]. Exit code: ${_ret}!" exit ${_ret} else - DEBUG "Script successfully handled hidden subcommand: [${subcommand} \"$@\"]!" + DEBUG "Script successfully handled hidden subcommand: [${subcommand} \"$*\"]!" exit ${ERR_CODE_SUCCESS} fi ;; @@ -2151,10 +2643,10 @@ function cmd_subcommand_handle() { ## @fn Main CLI parser, sub-command handler ${subcommand} "$@" _ret=$? if [[ ${_ret} -ne 0 ]]; then - ERROR "Something went wrong in subcommand handler: [${subcommand} \"$@\"]. Exit code: ${_ret}!" + ERROR "Something went wrong in subcommand handler: [${subcommand} \"$*\"]. Exit code: ${_ret}!" exit ${_ret} else - DEBUG "Script successfully handled hidden subcommand: [${subcommand} \"$@\"]!" + DEBUG "Script successfully handled hidden subcommand: [${subcommand} \"$*\"]!" exit ${ERR_CODE_SUCCESS} fi ;; diff --git a/tools/hilbert.py b/tools/hilbert.py index 50324a7..99b1dab 100755 --- a/tools/hilbert.py +++ b/tools/hilbert.py @@ -17,10 +17,12 @@ if path.exists(path.join(DIR, 'hilbert_config', 'hilbert_cli_config.py')): sys.path.append(DIR) sys.path.append(path.join(DIR, 'hilbert_config')) + from hilbert_config import __version__ from helpers import * from hilbert_cli_config import * from subcmdparser import * else: + from hilbert_config import __version__ from hilbert_config.hilbert_cli_config import * from hilbert_config.helpers import * from hilbert_config.subcmdparser import * @@ -702,6 +704,8 @@ def cmd_action(parser, context, args, Action=None, appIdRequired=False): elif 'action_args' in _args: action_args = _args.get('action_args', None) + elif 'force' in _args: + action_args = _args.get('force', None) stations = None @@ -723,15 +727,24 @@ def cmd_action(parser, context, args, Action=None, appIdRequired=False): assert station is not None log.debug("StationID is valid according to the Configuration!") - log.debug("Running action: '{0} {1}' on station '{2}'".format(action, str(action_args), stationId)) + log.debug("Running action: [{0}] (with args: [{1}]) on station [{2}]".format(action, str(action_args), stationId)) set_log_level_options(_ctx) try: - station.run_action(action, action_args) # NOTE: temporary API for now + _ret = station.run_action(action, action_args) # NOTE: temporary API for now except: - log.exception("Could not run '{0} {1}' on station '{2}'".format(action, str(action_args), stationId)) + log.exception("Could not run [{0}] (with args: [{1}]) on station [{2}]".format(action, str(action_args), stationId)) sys.exit(1) + + if (_ret == 0) or (_ret is True): + log.debug("Successfully run [{0}] (with args: [{1}]) on station [{2}]".format(action, str(action_args), stationId)) + else: + log.error("Failed to run [{0}] (with args: [{1}]) on station [{2}]".format(action, str(action_args), stationId)) + if type(_ret) is bool: + sys.exit(1) + sys.exit(_ret) + return args @@ -742,6 +755,46 @@ def cmd_start(parser, context, args): group = parser.add_mutually_exclusive_group() + group.add_argument('--configfile', required=False, + help="specify input .YAML file (default: 'Hilbert.yml')") + group.add_argument('--configdump', required=False, + help="specify input dump file") + + parser.add_argument('StationID', help="station to power-ON via network (e.g. using wakeonlan)") + parser.add_argument('action_args', nargs='?', help="number of power-ONs to perform", metavar='args') + + cmd_action(parser, context, args, Action=action, appIdRequired=False) + + return args + + +@subcmd('cleanup', help='perform system cleanup on remote station') +def cmd_reboot(parser, context, args): + action = 'cleanup' + log.debug("Running 'cmd_{}'".format(action)) + + group = parser.add_mutually_exclusive_group() + + group.add_argument('--configfile', required=False, + help="specify input .YAML file (default: 'Hilbert.yml')") + group.add_argument('--configdump', required=False, + help="specify input dump file") + + parser.add_argument('--force', action='store_true', help="forces cleanup") # optional boolean (True) flag! + parser.add_argument('StationID', help="station to cleanup via network") + + cmd_action(parser, context, args, Action=action, appIdRequired=False) + + return args + + +@subcmd('reboot', help='reboot station') +def cmd_reboot(parser, context, args): + action = 'reboot' + log.debug("Running 'cmd_{}'".format(action)) + + group = parser.add_mutually_exclusive_group() + group.add_argument('--configfile', required=False, help="specify input .YAML file (default: 'Hilbert.yml')") group.add_argument('--configdump', required=False, @@ -754,7 +807,6 @@ def cmd_start(parser, context, args): return args - @subcmd('poweroff', help='finalize Hilbert on a station and shut it down') def cmd_stop(parser, context, args): action = 'stop' @@ -795,9 +847,9 @@ def cmd_cfg_deploy(parser, context, args): return args -# @subcmd('app_start', help='start an application on a station') -def cmd_app_start(parser, context, args): - action = 'app_start' +@subcmd('app_restart', help='restart current/default application on a station') +def cmd_app_restart(parser, context, args): + action = 'app_restart' log.debug("Running 'cmd_{}'".format(action)) group = parser.add_mutually_exclusive_group() @@ -808,10 +860,10 @@ def cmd_app_start(parser, context, args): help="specify input dump file") parser.add_argument('StationID', help="specify the station") - parser.add_argument('ApplicationID', help="specify the application to start") +# parser.add_argument('ApplicationID', help="specify the application to start") # parser.add_argument('action_args', nargs='?', help="optional argument for start: ApplicationID/ServiceID ", metavar='id') - cmd_action(parser, context, args, Action=action, appIdRequired=True) + cmd_action(parser, context, args, Action=action, appIdRequired=False) return args @@ -859,9 +911,10 @@ def cmd_app_change(parser, context, args): return args -# @subcmd('run_action', help='run specified action on given station with given arguments...') +@subcmd('run_shell_cmd', help='run specified shell command on given station...') def cmd_run_action(parser, context, args): - log.debug("Running 'cmd_{}'".format('run_action')) + action = 'run_shell_cmd' + log.debug("Running 'cmd_{}'".format(action)) group = parser.add_mutually_exclusive_group() group.add_argument('--configfile', required=False, @@ -869,11 +922,10 @@ def cmd_run_action(parser, context, args): group.add_argument('--configdump', required=False, help="specify input dump file") - parser.add_argument('Action', help="specify the action") parser.add_argument('StationID', help="specify the station") - parser.add_argument('action_args', nargs='?', help="optional arguments for the action", metavar='args') + parser.add_argument('action_args', nargs='+', help="specify the command to run", metavar='args') - cmd_action(parser, context, args, Action=None, appIdRequired=False) + cmd_action(parser, context, args, Action=action, appIdRequired=False) return args @@ -914,7 +966,9 @@ def _version(): import semantic_version log.debug("Running '--{}'".format('version')) - print("Hilbert Configuration API: [{}]".format(Hilbert(None).get_api_version())) + print("hilbert tool version: [{}]".format(__version__)) + log.info("hilbert cli version: [{}]".format(__CLI_VERSION_ID)) + log.info("Hilbert Configuration API: [{}]".format(Hilbert(None).get_api_version())) log.info("Python (platform) version: {}".format(platform.python_version())) log.info("ruamel.yaml version: {}".format(yaml.__version__))