Websocket test
Resolves openbmc/openbmc-test-automation#1219
Signed-off-by: Steven Sombar <ssombar@us.ibm.com>
Change-Id: Ic756636ac97c5219ccca4431b60f1eb376659285
diff --git a/bin/websocket_monitor.py b/bin/websocket_monitor.py
new file mode 100755
index 0000000..d0ae844
--- /dev/null
+++ b/bin/websocket_monitor.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python
+
+r"""
+See help text for details.
+"""
+
+import json
+import sys
+import websocket
+import ssl
+import requests
+from retrying import retry
+
+save_path_0 = sys.path[0]
+del sys.path[0]
+
+from gen_print import *
+from gen_arg import *
+from gen_valid import *
+
+# Restore sys.path[0].
+sys.path.insert(0, save_path_0)
+
+# Set exit_on_error for gen_valid functions.
+set_exit_on_error(True)
+
+# URI of the logging interface. This should end with the word "logging" and
+# have no / character at the end.
+logging_uri = '/xyz/openbmc_project/logging'
+
+
+parser = argparse.ArgumentParser(
+ usage='%(prog)s [OPTIONS]',
+ description="%(prog)s will open a websocket session on a remote OpenBMC. "
+ + "When an eSEL is created on that BMC, the monitor will receive "
+ + "notice over websocket that the eSEL was created "
+ + "and it will print a message.",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ prefix_chars='-+')
+parser.add_argument(
+ 'openbmc_host',
+ default='',
+ help='The BMC host name or IP address.')
+parser.add_argument(
+ '--openbmc_username',
+ default='root',
+ help='The userid for the open BMC system.')
+parser.add_argument(
+ '--openbmc_password',
+ default='',
+ help='The password for the open BMC system.')
+
+stock_list = [("test_mode", 0), ("quiet", 0), ("debug", 0)]
+
+
+def exit_function(signal_number=0,
+ frame=None):
+ r"""
+ Execute whenever the program ends normally or with the signals that we
+ catch (i.e. TERM, INT).
+ """
+
+ qprint_dashes(width=160)
+ qprint_executing()
+ qprint_pgm_footer()
+
+
+def signal_handler(signal_number,
+ frame):
+ r"""
+ Handle signals. Without a function to catch a SIGTERM or SIGINT, the
+ program would terminate immediately with return code 143 and without
+ calling the exit_function.
+ """
+
+ # Our convention is to set up exit_function with atexit.register() so
+ # there is no need to explicitly call exit_function from here.
+
+ dprint_executing()
+
+ # Calling exit prevents us from returning to the code that was running
+ # when the signal was received.
+ exit(0)
+
+
+def validate_parms():
+ r"""
+ Validate program parameters, etc.
+ """
+
+ register_passwords(openbmc_password)
+ valid_value(openbmc_host)
+ valid_value(openbmc_username)
+ valid_value(openbmc_password)
+ gen_post_validation(exit_function, signal_handler)
+
+
+@retry(stop_max_attempt_number=3, wait_fixed=1000)
+def login(openbmc_host,
+ openbmc_username,
+ openbmc_password):
+ r"""
+ Log into the BMC and return the session object.
+
+ Description of argument(s):
+ openbmc_host The BMC host name or IP address.
+ openbmc_username The userid for the open BMC system.
+ openbmc_password The password for the open BMC system.
+ """
+
+ qprint_executing()
+
+ http_header = {'Content-Type': 'application/json'}
+ session = requests.session()
+ response = session.post('https://' + openbmc_host + '/login', headers=http_header,
+ json={"data": [openbmc_username, openbmc_password]},
+ verify=False, timeout=30)
+ valid_value(response.status_code, valid_values=[200])
+ login_response = json.loads(response.text)
+ qprint_var(login_response)
+ valid_value(login_response['status'], valid_values=['ok'])
+
+ return session
+
+
+def on_message(websocket_obj, message):
+ """
+ Websocket message handler. Close the websocket if the
+ message is an eSEL message.
+
+ Description of argument(s):
+ websocket_obj The websocket established during opne_socket().
+ message The message sent from the websocket interface.
+ """
+
+ qprint_dashes(width=160)
+ qprint_executing()
+
+ # A typical message:
+ # {"event":"PropertiesChanged","interface":"xyz.openbmc_
+ # project.Logging.Entry","path":"/xyz/openbmc_project/lo
+ # gging/entry/24","properties":{"Id":24}}
+
+ if logging_uri + '/entry' in message and 'Id' in message:
+ qprint_timen('eSEL received over websocket interface.')
+ qprint_timen("Closing websocket. Expect to receive 'NoneType' object has no attribute 'connected'.")
+ websocket_obj.close()
+
+
+def on_error(websocket_obj, wserror):
+ """
+ Websocket error handler. This routine is called whenever the
+ websocket interfaces wishes to report an issue.
+
+ Description of argument(s):
+ websocket_obj The websocket established during opne_socket().
+ wserror The error message sent from the websocket interface.
+ """
+
+ # It is normal to receive this message when websocked closes:
+ # 'NoneType' object has no attribute 'connected'.
+
+ qprint_dashes(width=160)
+ qprint_executing()
+
+
+def on_close(websocket_obj):
+ """
+ Websocket close event handler.
+
+ Description of argument(s):
+ websocket_obj The websocket established during opne_socket().
+ """
+
+ qprint_dashes(width=160)
+ qprint_executing()
+
+
+def on_open(websocket_obj):
+ """
+ Send the filters needed to listen to the logging interface.
+
+ Description of argument(s):
+ websocket_obj The websocket established during opne_socket().
+ """
+
+ qprint_dashes(width=160)
+ qprint_executing()
+ data = {"paths": [logging_uri]}
+ websocket_obj.send(json.dumps(data))
+ qprint_timen("Registered for websocket monitoring: " + logging_uri)
+
+
+def open_socket(openbmc_host, openbmc_username, openbmc_password):
+ """
+ Open a long-running websocket to the BMC.
+ Description of argument(s):
+ openbmc_host The BMC host name or IP address.
+ openbmc_username The userid for the open BMC system.
+ openbmc_password The Password for the open BMC system.
+ """
+ websocket.enableTrace(False)
+ qprint_dashes(width=160)
+ qprint_executing()
+ session = login(openbmc_host, openbmc_username, openbmc_password)
+ qprint_timen("Registering websocket handlers.")
+ cookies = session.cookies.get_dict()
+ cookies = sprint_var(cookies, fmt=no_header() | strip_brackets(),
+ col1_width=0, trailing_char="",
+ delim="=").replace("\n", ";")
+ # Register the event handlers. When an ESEL is created by the system
+ # under test, the on_message() handler will be called.
+ websocket_obj = websocket.WebSocketApp("wss://" + openbmc_host + "/subscribe",
+ on_message=on_message,
+ on_error=on_error,
+ on_close=on_close,
+ on_open=on_open,
+ cookie=cookies)
+ qprint_timen("Completed registering of websocket handlers.")
+ websocket_obj.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
+
+
+def main():
+ gen_get_options(parser, stock_list)
+ validate_parms()
+ qprint_pgm_header()
+ open_socket(openbmc_host, openbmc_username, openbmc_password)
+
+
+main()
diff --git a/lib/logging_utils.robot b/lib/logging_utils.robot
index 11b44ba..b4c4076 100644
--- a/lib/logging_utils.robot
+++ b/lib/logging_utils.robot
@@ -204,9 +204,16 @@
[Return] ${members}
+Get Number Of Event Logs
+ [Documentation] Return the number of EventLog members.
+
+ ${members}= Get Event Logs
+ ${num_members}= Get Length ${members}
+ [Return] ${num_members}
+
+
Redfish Purge Event Log
[Documentation] Do Redfish EventLog purge.
Redfish.Post ${EVENT_LOG_URI}Actions/LogService.Reset
... valid_status_codes=[${HTTP_NO_CONTENT}]
-
diff --git a/redfish/extended/test_websocket.robot b/redfish/extended/test_websocket.robot
new file mode 100755
index 0000000..1ff5344
--- /dev/null
+++ b/redfish/extended/test_websocket.robot
@@ -0,0 +1,141 @@
+*** Settings ***
+
+Documentation Websocket functionality test.
+
+# Test Parameters:
+# OPENBMC_HOST The BMC host name or IP address.
+# OPENBMC_USERNAME The username for the BMC login.
+# OPENBMC_PASSWORD The password for OPENBMC_USERNAME.
+# OS_HOST The OS host name or IP address.
+# OS_USERNAME The username for the OS login.
+# OS_PASSWORD The password for OS_USERNAME.
+
+Resource ../../lib/esel_utils.robot
+Resource ../../lib/bmc_redfish_resource.robot
+Resource ../../lib/logging_utils.robot
+Library ../../lib/gen_cmd.py
+Library OperatingSystem
+
+
+Suite Setup Suite Setup Execution
+Suite Teardown Suite Teardown Execution
+Test Teardown Test Teardown Execution
+
+
+*** Variables ***
+
+${monitor_pgm} bin/websocket_monitor.py
+${monitor_file} websocket_monitor_out.txt
+${expected_string} eSEL received over websocket interface
+${min_number_chars} 22
+${monitor_cmd} ${monitor_pgm} ${OPENBMC_HOST} --openbmc_username ${OPENBMC_USERNAME}
+
+
+*** Test Cases ***
+
+
+Test BMC Websocket Interface
+ [Documentation] Verify eSELs are seen over the websocket interface.
+ [Tags] Test_BMC_Websocket_Interface
+
+ # Spawn the websocket monitor program and then generate an eSEL.
+ # The monitor should asynchronously receive the eSEL through the
+ # websocket interface and report this fact to standard output.
+
+ Start Websocket Monitor
+
+ ${initial_esel_count}= Get Number Of Event Logs
+
+ # Generate eSEL (e.g. typically "CPU 1 core 3 has failed").
+ Create eSEL
+
+ ${current_esel_count}= Get Number Of Event Logs
+
+ Run Keyword If ${initial_esel_count} == ${current_esel_count}
+ ... Fail msg=System failed to generate eSEL upon request.
+
+ ${line}= Grep File ${monitor_file} ${expected_string}
+ # Typical monitor_file contents:
+ # --------------- ON_MESSAGE:begin --------------------
+ # {"event":"PropertiesChanged","interface":"xyz.openbmc_project.Logging.
+ # Entry","path":"/xyz/openbmc_project/logging/entry/5","properties":{"Id":5}}
+ # eSEL received over websocket interface.
+
+ ${num_chars}= Get Length ${line}
+ Run Keyword If ${num_chars} < ${min_number_chars} Fail
+ ... msg=No eSEL notification from websocket_monitor.py.
+
+
+*** Keywords ***
+
+
+Start Websocket Monitor
+ [Documentation] Fork the monitor to run in the background.
+
+ # Delete the previous output file, if any.
+ Remove File ${monitor_file}
+
+ # Start the monitor. Fork so its a parallel task.
+ Shell Cmd
+ ... ${monitor_cmd} --openbmc_password ${OPENBMC_PASSWORD} 1>${monitor_file} 2>&1 fork=${1}
+
+ # Allow time for the monitor to initialize.
+ Sleep 5s
+
+
+Find Websocket Monitor
+ [Documentation] Return the process Id(s) of running websocket monitors.
+
+ ${cmd}= Catenate ps -ef | grep '${monitor_cmd}'
+ ... | grep -v grep | grep -v bash | cut -c10-14
+ ${shell_rc} ${pid}= Shell Cmd ${cmd}
+ # There may be more than one pid returned if there is an instance
+ # of a monitory_pgm running from a previous run.
+ @{pid_list}= Split String ${pid}
+ [Return] ${pid_list}
+
+
+Kill Websocket Monitor
+ [Documentation] Terminate running websocket monitor.
+
+ ${pid_list}= Find Websocket Monitor
+ FOR ${pid} IN @{pid_list}
+ Shell Cmd kill -s SIGTERM ${pid}
+ END
+
+
+Print Websocket Monitor Log
+ [Documentation] Show the contents of the monitor output file.
+
+ ${websocket_monitor_log}= OperatingSystem.Get File ${monitor_file}
+ Log to Console websocket_monitor_log:
+ Log to Console ${websocket_monitor_log}
+
+
+Suite Setup Execution
+ [Documentation] Do the suite setup tasks.
+
+ Run Keyword Redfish Power On stack_mode=skip
+
+ Redfish.Login
+
+ Delete All Error Logs
+ Kill Websocket Monitor
+
+ # Allow time for Error Logs to be deleted.
+ Sleep 5s
+
+
+Test Teardown Execution
+ [Documentation] Do teardown tasks after a test.
+
+ FFDC On Test Case Fail
+ Run Keyword If '${TEST_STATUS}' == 'FAIL' Print Websocket Monitor Log
+ Kill Websocket Monitor
+
+
+Suite Teardown Execution
+ [Documentation] Do the post-suite teardown.
+
+ Delete All Error Logs
+ Run Keyword and Return Status Redfish.Logout