Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1409,6 +1409,29 @@ The job manager writes log information to the file `eessi_bot_job_manager.log`.

The job manager can run on a different machine than the event handler, as long as both have access to the same shared filesystem.

## <a name="step7.3"></a>Step 7.3: Managing the bot with a single command
Comment thread
smoors marked this conversation as resolved.

The `bot` script starts, stops, restarts, and checks the status of the 2 bot
components: the event handler and the job manager. It launches both component
processes in the background.

```bash
./bot start [options]
./bot stop
./bot restart [options]
./bot status
```

Available `start`/`restart` options are the same as those for
`event_handler.sh` and `job_manager.sh`.

The following example starts the bot with 10 job manager iterations, managing
only job ids 1234 and 5678, and listening for events on port 8080:

```bash
./bot start -i 10 -j 1234,5678 --port 8080
```

# Example pull request on software-layer

For information on how to make pull requests and let the bot build software, see
Expand Down
177 changes: 177 additions & 0 deletions bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
#
# This file is part of the EESSI build-and-deploy bot,
# see https://github.com/EESSI/eessi-bot-software-layer
#
# The bot helps with requests to add software installations to the
# EESSI software layer, see https://github.com/EESSI/software-layer
#
# author: Samuel Moors (@smoors)
#
# license: GPLv2
#

import argparse
import os
import shlex
import signal
import subprocess
import time
from pathlib import Path

RED = "\033[31m"
BLUE = "\033[34m"
GREEN = "\033[32m"
RESET = "\033[0m"

JOB_MANAGER_MODULE = "eessi_bot_job_manager"
EVENT_HANDLER_MODULE = "eessi_bot_event_handler"
DEFAULT_BOT_NAME = "eessi-bot"


def parse_args():
parser = argparse.ArgumentParser(
usage="%(prog)s {start|stop|restart|status} [options]"
)
parser.add_argument("command", choices=("start", "stop", "restart", "status"))
parser.add_argument(
"--bot-name",
default=DEFAULT_BOT_NAME,
help=f"bot name (default: {DEFAULT_BOT_NAME})",
)
parser.add_argument(
"--job-manager-opts",
default="",
help="additional job manager options as a string",
)
parser.add_argument(
"--event-handler-opts",
default="",
help="additional event handler options as a string",
)
args = parser.parse_args()

return args


def get_cmdline(pid):
try:
data = Path(f"/proc/{pid}/cmdline").read_bytes()
except (FileNotFoundError, PermissionError, ProcessLookupError):
return []
return [arg.decode(errors="replace") for arg in data.split(b"\0") if arg]


def find_processes(module, bot_name):
processes = []

for proc in Path("/proc").iterdir():
if not proc.name.isdigit():
continue

pid = int(proc.name)
if pid == os.getpid():
continue

cmdline = get_cmdline(pid)
if not cmdline:
continue

try:
module_index = cmdline.index("-m")
except ValueError:
continue

if module_index + 1 >= len(cmdline) or cmdline[module_index + 1] != module:
continue

for i, arg in enumerate(cmdline[:-1]):
if arg == "--bot-name" and cmdline[i + 1] == bot_name:
processes.append(pid)
break

return processes


def start_process(module, bot_name, opts):
cmd = ["python3", "-m", module, "--bot-name", bot_name]
if opts:
cmd.extend(shlex.split(opts))

env = os.environ.copy()
env["PYTHONUNBUFFERED"] = "1"

subprocess.Popen(cmd, env=env, start_new_session=True)


def start_bot(bot_name, job_manager_opts, event_handler_opts):
if find_processes(JOB_MANAGER_MODULE, bot_name):
print(f"{BLUE}>>> job manager for bot '{bot_name}' is already running{RESET}")
else:
print(f"{BLUE}>>> starting job manager for bot '{bot_name}'...{RESET}")
start_process(JOB_MANAGER_MODULE, bot_name, job_manager_opts)

if find_processes(EVENT_HANDLER_MODULE, bot_name):
print(f"{GREEN}>>> event handler for bot '{bot_name}' is already running{RESET}")
else:
print(f"{GREEN}>>> starting event handler for bot '{bot_name}'...{RESET}")
start_process(EVENT_HANDLER_MODULE, bot_name, event_handler_opts)


def stop_processes(module, bot_name):
for pid in find_processes(module, bot_name):
try:
cmdline = get_cmdline(pid)
os.kill(pid, signal.SIGTERM)
print(f"killed (pid {pid}): {shlex.join(cmdline)}")
except ProcessLookupError:
pass


def stop_bot(bot_name):
if find_processes(EVENT_HANDLER_MODULE, bot_name):
print(f"{RED}>>> stopping event handler for bot '{bot_name}'...{RESET}")
stop_processes(EVENT_HANDLER_MODULE, bot_name)
else:
print(f"{RED}>>> event handler for bot '{bot_name}' is not running{RESET}")

if find_processes(JOB_MANAGER_MODULE, bot_name):
print(f"{RED}>>> stopping job manager for bot '{bot_name}'...{RESET}")
stop_processes(JOB_MANAGER_MODULE, bot_name)
else:
print(f"{RED}>>> job manager for bot '{bot_name}' is not running{RESET}")


def status_bot(bot_name):
if find_processes(JOB_MANAGER_MODULE, bot_name):
print(f"{BLUE}>>> job manager for bot '{bot_name}' is running{RESET}")
else:
print(f"{RED}>>> job manager for bot '{bot_name}' is not running{RESET}")

if find_processes(EVENT_HANDLER_MODULE, bot_name):
print(f"{GREEN}>>> event handler for bot '{bot_name}' is running{RESET}")
else:
print(f"{RED}>>> event handler for bot '{bot_name}' is not running{RESET}")


def main():
args = parse_args()

if args.command == "start":
start_bot(args.bot_name, args.job_manager_opts, args.event_handler_opts)
time.sleep(2)
status_bot(args.bot_name)
elif args.command == "stop":
stop_bot(args.bot_name)
elif args.command == "restart":
stop_bot(args.bot_name)
time.sleep(1)
start_bot(args.bot_name, args.job_manager_opts, args.event_handler_opts)
time.sleep(2)
status_bot(args.bot_name)
elif args.command == "status":
status_bot(args.bot_name)


if __name__ == "__main__":
main()
18 changes: 10 additions & 8 deletions tests/test_tools_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,17 @@ def test_parse_common_args(test_args, expected_parsed, expected_unknown):
# Test event_handler_parse()
@pytest.mark.parametrize("test_args,expectation", [
# No args
([], nullcontext(Namespace(debug=False, build=False, test=False, cron=False, file=None, port=3000))),
([], nullcontext(Namespace(debug=False, build=False, test=False, cron=False, file=None, port=3000, bot_name=None))),

# Short-form args
(["-d", "-b", "-t", "-c", "-f", "file.json", "-p", "8000"],
nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file.json", port="8000"))),
nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file.json", port="8000",
bot_name=None))),

# Long-form args
(["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000"],
nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file2.json", port="9000"))),
(["--debug", "--build", "--test", "--cron", "--file", "file2.json", "--port", "9000", "--bot-name", "test-bot"],
nullcontext(Namespace(debug=True, build=True, test=True, cron=True, file="file2.json", port="9000",
bot_name="test-bot"))),

# Unknown args - should fail and exit
(["-u"], pytest.raises(SystemExit)),
Expand All @@ -66,15 +68,15 @@ def test_event_handler_parse_known_args(test_args, expectation):
# Test job_manager_parse()
@pytest.mark.parametrize("test_args,expectation", [
# No args
([], nullcontext(Namespace(debug=False, max_manager_iterations=-1, jobs=None))),
([], nullcontext(Namespace(debug=False, max_manager_iterations=-1, jobs=None, bot_name=None))),

# Short-form args
(["-d", "-i", "0", "-j", "17"],
nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17"))),
nullcontext(Namespace(debug=True, max_manager_iterations="0", jobs="17", bot_name=None))),

# Long-form args
(["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48"],
nullcontext(Namespace(debug=True, max_manager_iterations="10", jobs="4,18,48"))),
(["--debug", "--max-manager-iterations", "10", "--jobs", "4,18,48", "--bot-name", "test-bot"],
nullcontext(Namespace(debug=True, max_manager_iterations="10", jobs="4,18,48", bot_name="test-bot"))),

# Unknown args - should fail and exit
(["-u"], pytest.raises(SystemExit)),
Expand Down
10 changes: 10 additions & 0 deletions tools/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ def event_handler_parse(args=None):
help="listen on a specific port for events (default 3000)",
)

parser.add_argument(
"--bot-name",
help="bot name (not used by the bot itself), useful for keeping track of multiple instances",
)

return parser.parse_args(args=unknown_args, namespace=parsed_args)


Expand All @@ -110,4 +115,9 @@ def job_manager_parse(args=None):
help="limits the processing to a specific job id or list of comma-separated list of job ids",
)

parser.add_argument(
"--bot-name",
help="bot name (not used by the bot itself), useful for keeping track of multiple instances",
)

return parser.parse_args(args=unknown_args, namespace=parsed_args)
Loading