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