Skip to content

Commit

Permalink
SSL credential creator, localhost MQTT broker, and localhost HTTP ser…
Browse files Browse the repository at this point in the history
…ver actions, and Executable monitor actions (#59)

Co-authored-by: Jason Carroll <[email protected]>
  • Loading branch information
jasonpcarroll and Jason Carroll authored Mar 21, 2023
1 parent d823045 commit 406befb
Show file tree
Hide file tree
Showing 12 changed files with 519 additions and 0 deletions.
27 changes: 27 additions & 0 deletions executable-monitor/action.yml
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
145 changes: 145 additions & 0 deletions executable-monitor/executable-monitor.py
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)
2 changes: 2 additions & 0 deletions executable-monitor/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pyyaml
gitpython
23 changes: 23 additions & 0 deletions localhost-http-1.1-server/action.yml
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
109 changes: 109 additions & 0 deletions localhost-http-1.1-server/localhost_http_1.1_server.py
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()
1 change: 1 addition & 0 deletions localhost-http-1.1-server/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyOpenSSL
23 changes: 23 additions & 0 deletions localhost-mqtt-broker/action.yml
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
Loading

0 comments on commit 406befb

Please sign in to comment.