-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SSL credential creator, localhost MQTT broker, and localhost HTTP ser…
…ver actions, and Executable monitor actions (#59) Co-authored-by: Jason Carroll <[email protected]>
- Loading branch information
1 parent
d823045
commit 406befb
Showing
12 changed files
with
519 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
name: 'executable-monitor' | ||
description: 'Runs and executable until a termination line is hit or a timeout occurs. Reports if the executable completed successfully or failed.' | ||
inputs: | ||
exe-path: | ||
description: 'Path to the executable to run.' | ||
required: true | ||
log-dir: | ||
description: 'Path to directory to store logs.' | ||
required: true | ||
success-line: | ||
description: 'Line of output from executable indicating success.' | ||
required: false | ||
default: "Demo completed successfully." | ||
timeout-seconds: | ||
description: 'Maximum amount of time to run the executable. Default is 600.' | ||
required: false | ||
default: 600 | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- name: Install dependencies | ||
run: pip install -r $GITHUB_ACTION_PATH/requirements.txt | ||
shell: bash | ||
- name: Run executable with monitoring script | ||
run: python3 $GITHUB_ACTION_PATH/executable-monitor.py --exe-path=${{ inputs.exe-path }} --timeout-seconds=${{ inputs.timeout-seconds }} --success-line="${{ inputs.success-line }}" --log-dir=${{ inputs.log-dir }} | ||
shell: bash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import os, sys | ||
from argparse import ArgumentParser | ||
import subprocess | ||
import time | ||
import logging | ||
|
||
|
||
if __name__ == '__main__': | ||
|
||
# Set up logging | ||
logging.getLogger().setLevel(logging.NOTSET) | ||
|
||
# Add stdout handler to logging | ||
stdout_logging_handler = logging.StreamHandler(sys.stdout) | ||
stdout_logging_handler.setLevel(logging.DEBUG) | ||
stdout_logging_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') | ||
stdout_logging_handler.setFormatter(stdout_logging_formatter) | ||
logging.getLogger().addHandler(stdout_logging_handler) | ||
|
||
# Parse arguments | ||
parser = ArgumentParser(description='Executable monitor.') | ||
parser.add_argument('--exe-path', | ||
type=str, | ||
required=True, | ||
help='Path to the executable.') | ||
parser.add_argument('--log-dir', | ||
type=str, | ||
required=True, | ||
help='Path to directory to store logs in.') | ||
parser.add_argument('--timeout-seconds', | ||
type=int, | ||
required=True, | ||
help='Timeout for each executable run.') | ||
parser.add_argument('--success-line', | ||
type=str, | ||
required=False, | ||
help='Line that indicates executable completed successfully. Required if --success-exit-status is not used.') | ||
parser.add_argument('--success-exit-status', | ||
type=int, | ||
required=False, | ||
help='Exit status that indicates that the executable completed successfully. Required if --success-line is not used.') | ||
|
||
args = parser.parse_args() | ||
|
||
if args.success_exit_status is None and args.success_line is None: | ||
logging.error("Must specify at least one of the following: --success-line, --success-exit-status.") | ||
sys.exit(1) | ||
|
||
if not os.path.exists(args.exe_path): | ||
logging.error(f'Input executable path \"{args.exe_path}\" does not exist.') | ||
sys.exit(1) | ||
|
||
# Create log directory if it does not exist. | ||
if not os.path.exists(args.log_dir): | ||
os.makedirs(args.log_dir, exist_ok = True) | ||
|
||
# Convert any relative path (like './') in passed argument to absolute path. | ||
exe_abs_path = os.path.abspath(args.exe_path) | ||
log_dir = os.path.abspath(args.log_dir) | ||
|
||
# Add file handler to output logging to a log file | ||
exe_name = os.path.basename(exe_abs_path) | ||
log_file_path = f'{log_dir}/{exe_name}_output.txt' | ||
file_logging_handler = logging.FileHandler(log_file_path) | ||
file_logging_handler.setLevel(logging.DEBUG) | ||
file_logging_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') | ||
file_logging_handler.setFormatter(file_logging_formatter) | ||
logging.getLogger().addHandler(file_logging_handler) | ||
|
||
logging.info(f"Running executable: {exe_abs_path} ") | ||
logging.info(f"Storing logs in: {log_dir}") | ||
logging.info(f"Timeout (seconds): {args.timeout_seconds}") | ||
|
||
exe = subprocess.Popen([exe_abs_path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) | ||
|
||
cur_time_seconds = time.time() | ||
timeout_time_seconds = cur_time_seconds + args.timeout_seconds | ||
timeout_occurred = False | ||
|
||
exe_exit_status = None | ||
exe_exitted = False | ||
|
||
success_line_found = False | ||
cur_line_ouput = 1 | ||
|
||
wait_for_exit = args.success_exit_status is not None | ||
|
||
logging.info("START OF DEVICE OUTPUT\n") | ||
|
||
while not (timeout_occurred or exe_exitted or (not wait_for_exit and success_line_found)): | ||
|
||
# Read executable's stdout and write to stdout and logfile | ||
exe_stdout_line = exe.stdout.readline() | ||
logging.info(exe_stdout_line) | ||
|
||
# Check if the executable printed out it's success line | ||
if args.success_line is not None and args.success_line in exe_stdout_line: | ||
success_line_found = True | ||
|
||
# Check if executable exitted | ||
exe_exit_status = exe.poll() | ||
if exe_exit_status is not None: | ||
exe_exitted = True | ||
|
||
# Check for timeout | ||
cur_time_seconds = time.time() | ||
if cur_time_seconds >= timeout_time_seconds: | ||
timeout_occurred = True | ||
|
||
if not exe_exitted: | ||
exe.kill() | ||
|
||
# Capture remaining output and check for the successful line | ||
for exe_stdout_line in exe.stdout.readlines(): | ||
logging.info(exe_stdout_line) | ||
if args.success_line is not None and args.success_line in exe_stdout_line: | ||
success_line_found = True | ||
|
||
logging.info("END OF DEVICE OUTPUT\n") | ||
|
||
logging.info("EXECUTABLE RUN SUMMARY:\n") | ||
|
||
exit_status = 0 | ||
|
||
if args.success_line is not None: | ||
if success_line_found: | ||
logging.info("Success Line: Found.\n") | ||
else: | ||
logging.error("Success Line: Success line not output.\n") | ||
exit_status = 1 | ||
|
||
if args.success_exit_status is not None: | ||
if exe_exitted: | ||
if exe_exit_status != args.success_exit_status: | ||
exit_status = 1 | ||
logging.info(f"Exit Status: {exe_exit_status}") | ||
else: | ||
logging.error("Exit Status: Executable did not exit.\n") | ||
exe_status = 1 | ||
|
||
|
||
# Report if executable executed successfully to workflow | ||
sys.exit(exit_status) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pyyaml | ||
gitpython |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: 'Localhost HTTP Server' | ||
description: 'Starts an HTTP 1.1 server using Python. For TLS connections (including mutual authentication), connect to localhost:4443. For plaintext connections, connect to localhost:8080.' | ||
|
||
inputs: | ||
root-ca-cert-path: | ||
description: "Root CA certificate file path." | ||
required: True | ||
server-cert-path: | ||
description: "Server certificate file path." | ||
required: True | ||
server-priv-key-path: | ||
description: "Server private key file path." | ||
required: True | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- name: Install dependencies | ||
run: pip install -r $GITHUB_ACTION_PATH/requirements.txt | ||
shell: bash | ||
- name: Run localhost HTTP broker | ||
run: python3 $GITHUB_ACTION_PATH/localhost_http_1.1_server.py --root-ca-cert-path=${{ inputs.root-ca-cert-path }} --server-priv-key-path=${{ inputs.server-priv-key-path }} --server-cert-path=${{ inputs.server-cert-path }} & | ||
shell: bash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
from http.server import HTTPServer, BaseHTTPRequestHandler | ||
from socketserver import ThreadingMixIn | ||
from threading import Thread | ||
import socket | ||
import ssl | ||
from argparse import ArgumentParser | ||
|
||
LOCAL_HOST_IP = socket.gethostbyname("localhost") | ||
PLAINTEXT_PORT = 8080 | ||
SSL_PORT = 4443 | ||
|
||
# Define a threaded HTTP server class | ||
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): | ||
pass | ||
|
||
class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): | ||
protocol_version = 'HTTP/1.1' | ||
|
||
def do_GET(self): | ||
# Receive the body of the request - don't do anything with it, | ||
# but this needs to be done to clear the receiving buffer. | ||
if self.headers.get('Content-Length') is not None: | ||
recv_content_len = int(self.headers.get('Content-Length')) | ||
recv_body = self.rfile.read(recv_content_len) | ||
|
||
# Always send a 200 response with "Hello" in the body. | ||
response_body = "Hello".encode('utf-8') | ||
self.send_response(200) | ||
self.send_header("Content-Type", "text/plain") | ||
self.send_header("Content-Length", str(len(response_body))) | ||
self.end_headers() | ||
self.wfile.write(response_body) | ||
|
||
def do_PUT(self): | ||
# Receive the body of the request - don't do anything with it, | ||
# but this needs to be done to clear the receiving buffer. | ||
if self.headers.get('Content-Length') is not None: | ||
recv_content_len = int(self.headers.get('Content-Length')) | ||
recv_body = self.rfile.read(recv_content_len) | ||
|
||
# Always send a 200 response with "Hello" in the body. | ||
response_body = "Hello".encode('utf-8') | ||
self.send_response(200) | ||
self.send_header("Content-Type", "text/plain") | ||
self.send_header("Content-Length", str(len(response_body))) | ||
self.end_headers() | ||
self.wfile.write(response_body) | ||
|
||
def do_POST(self): | ||
# Receive the body of the request - don't do anything with it, | ||
# but this needs to be done to clear the receiving buffer. | ||
if self.headers.get('Content-Length') is not None: | ||
recv_content_len = int(self.headers.get('Content-Length')) | ||
recv_body = self.rfile.read(recv_content_len) | ||
|
||
# Always send a 200 response with "Hello" in the body. | ||
response_body = "Hello".encode('utf-8') | ||
self.send_response(200) | ||
self.send_header("Content-Type", "text/plain") | ||
self.send_header("Content-Length", str(len(response_body))) | ||
self.end_headers() | ||
self.wfile.write(response_body) | ||
|
||
def do_HEAD(self): | ||
# Receive the body of the request - don't do anything with it, | ||
# but this needs to be done to clear the receiving buffer. | ||
if self.headers.get('Content-Length') is not None: | ||
recv_content_len = int(self.headers.get('Content-Length')) | ||
recv_body = self.rfile.read(recv_content_len) | ||
|
||
# Always send a 200 response with same headers as GET but without | ||
# response body | ||
response_body = "Hello".encode('utf-8') | ||
self.send_response(200) | ||
self.send_header("Content-Type", "text/plain") | ||
self.send_header("Content-Length", str(len(response_body))) | ||
self.end_headers() | ||
|
||
if __name__ == '__main__': | ||
# Parse passed in credentials | ||
parser = ArgumentParser(description='Localhost MQTT broker.') | ||
|
||
parser.add_argument('--root-ca-cert-path', | ||
type=str, | ||
required=True, | ||
help='Path to the root CA certificate.') | ||
parser.add_argument('--server-cert-path', | ||
type=str, | ||
required=True, | ||
help='Path to the server certificate.') | ||
parser.add_argument('--server-priv-key-path', | ||
type=str, | ||
required=True, | ||
help='Path to the private key') | ||
args = parser.parse_args() | ||
|
||
# Create a plaintext HTTP server thread | ||
plaintext_http_server = ThreadedHTTPServer((LOCAL_HOST_IP, PLAINTEXT_PORT), SimpleHTTPRequestHandler) | ||
plaintext_http_server_thread = Thread(target=plaintext_http_server.serve_forever) | ||
plaintext_http_server_thread.start() | ||
|
||
# Create an SSL HTTP serve thread | ||
ssl_http_server = ThreadedHTTPServer((LOCAL_HOST_IP, SSL_PORT), SimpleHTTPRequestHandler) | ||
ssl_http_server.socket = ssl.wrap_socket(ssl_http_server.socket, keyfile=args.server_priv_key_path, certfile=args.server_cert_path, ca_certs=args.root_ca_cert_path, cert_reqs=ssl.CERT_OPTIONAL) | ||
ssl_http_server_thread = Thread(target=ssl_http_server.serve_forever) | ||
ssl_http_server_thread.start() | ||
|
||
plaintext_http_server_thread.join() | ||
ssl_http_server_thread.join() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pyOpenSSL |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
name: 'Localhost MQTT Broker' | ||
description: 'Starts an MQTT Broker using Python. For TLS connections (including mutual authentication), connect to localhost:8883. For plaintext connections, connect to localhost:1883.' | ||
|
||
inputs: | ||
root-ca-cert-path: | ||
description: "Root CA certificate file path." | ||
required: True | ||
server-cert-path: | ||
description: "Server certificate file path." | ||
required: True | ||
server-priv-key-path: | ||
description: "Server private key file path." | ||
required: True | ||
|
||
runs: | ||
using: "composite" | ||
steps: | ||
- name: Install dependencies | ||
run: pip install -r $GITHUB_ACTION_PATH/requirements.txt | ||
shell: bash | ||
- name: Run localhost MQTT broker | ||
run: python3 $GITHUB_ACTION_PATH/localhost_mqtt_broker.py --root-ca-cert-path=${{ inputs.root-ca-cert-path }} --server-priv-key-path=${{ inputs.server-priv-key-path }} --server-cert-path=${{ inputs.server-cert-path }} & | ||
shell: bash |
Oops, something went wrong.