1#!/usr/bin/env python3
2
3r"""
4This module contains functions having to do with machine state: get_state,
5check_state, wait_state, etc.
6
7The 'State' is a composite of many pieces of data.  Therefore, the functions
8in this module define state as an ordered dictionary.  Here is an example of
9some test output showing machine state:
10
11default_state:
12  default_state[chassis]:                         On
13  default_state[boot_progress]:                   OSStart
14  default_state[operating_system]:                BootComplete
15  default_state[host]:                            Running
16  default_state[os_ping]:                         1
17  default_state[os_login]:                        1
18  default_state[os_run_cmd]:                      1
19
20Different users may very well have different needs when inquiring about
21state.  Support for new pieces of state information may be added to this
22module as needed.
23
24By using the wait_state function, a caller can start a boot and then wait for
25a precisely defined state to indicate that the boot has succeeded.  If
26the boot fails, they can see exactly why by looking at the current state as
27compared with the expected state.
28"""
29
30import imp
31import os
32import re
33import sys
34
35import bmc_ssh_utils as bsu
36import gen_cmd as gc
37import gen_print as gp
38import gen_robot_utils as gru
39import gen_valid as gv
40from robot.libraries.BuiltIn import BuiltIn
41from robot.utils import DotDict
42
43# NOTE: Avoid importing utils.robot because utils.robot imports state.py
44# (indirectly) which will cause failures.
45gru.my_import_resource("rest_client.robot")
46
47base_path = (
48    os.path.dirname(os.path.dirname(imp.find_module("gen_robot_print")[1]))
49    + os.sep
50)
51sys.path.append(base_path + "data/")
52
53# Previously, I had this coded:
54# import variables as var
55# However, we ran into a problem where a robot program did this...
56# Variables           ../../lib/ras/variables.py
57# Prior to doing this...
58# Library            ../lib/state.py
59
60# This caused the wrong variables.py file to be selected.  Attempts to fix this
61# have failed so far.  For the moment, we will hard-code the value we need from
62# the file.
63
64SYSTEM_STATE_URI = "/xyz/openbmc_project/state/"
65
66# The BMC code has recently been changed as far as what states are defined and
67# what the state values can be.  This module now has a means of processing both
68# the old style state (i.e. OBMC_STATES_VERSION = 0) and the new style (i.e.
69# OBMC_STATES_VERSION = 1).
70# The caller can set environment variable OBMC_STATES_VERSION to dictate
71# whether we're processing old or new style states.  If OBMC_STATES_VERSION is
72# not set it will default to 1.
73
74# As of the present moment, OBMC_STATES_VERSION of 0 is for cold that is so old
75# that it is no longer worthwhile to maintain.  The OBMC_STATES_VERSION 0 code
76# is being removed but the OBMC_STATES_VERSION value will stay for now in the
77# event that it is needed in the future.
78
79OBMC_STATES_VERSION = int(os.environ.get("OBMC_STATES_VERSION", 1))
80
81redfish_support_trans_state = int(
82    os.environ.get("REDFISH_SUPPORT_TRANS_STATE", 0)
83) or int(
84    BuiltIn().get_variable_value("${REDFISH_SUPPORT_TRANS_STATE}", default=0)
85)
86
87platform_arch_type = os.environ.get(
88    "PLATFORM_ARCH_TYPE", ""
89) or BuiltIn().get_variable_value("${PLATFORM_ARCH_TYPE}", default="power")
90
91# valid_os_req_states and default_os_req_states are used by the os_get_state
92# function.
93# valid_os_req_states is a list of state information supported by the
94# get_os_state function.
95valid_os_req_states = ["os_ping", "os_login", "os_run_cmd"]
96
97# When a user calls get_os_state w/o specifying req_states,
98# default_os_req_states is used as its value.
99default_os_req_states = ["os_ping", "os_login", "os_run_cmd"]
100
101# Presently, some BMCs appear to not keep time very well.  This environment
102# variable directs the get_state function to use either the BMC's epoch time
103# or the local epoch time.
104USE_BMC_EPOCH_TIME = int(os.environ.get("USE_BMC_EPOCH_TIME", 0))
105
106# Useful state constant definition(s).
107if not redfish_support_trans_state:
108    # When a user calls get_state w/o specifying req_states, default_req_states
109    # is used as its value.
110    default_req_states = [
111        "rest",
112        "chassis",
113        "bmc",
114        "boot_progress",
115        "operating_system",
116        "host",
117        "os_ping",
118        "os_login",
119        "os_run_cmd",
120    ]
121
122    # valid_req_states is a list of sub states supported by the get_state function.
123    # valid_req_states, default_req_states and master_os_up_match are used by the
124    # get_state function.
125
126    valid_req_states = [
127        "ping",
128        "packet_loss",
129        "uptime",
130        "epoch_seconds",
131        "elapsed_boot_time",
132        "rest",
133        "chassis",
134        "requested_chassis",
135        "bmc",
136        "requested_bmc",
137        "boot_progress",
138        "operating_system",
139        "host",
140        "requested_host",
141        "attempts_left",
142        "os_ping",
143        "os_login",
144        "os_run_cmd",
145    ]
146
147    # default_state is an initial value which may be of use to callers.
148    default_state = DotDict(
149        [
150            ("rest", "1"),
151            ("chassis", "On"),
152            ("bmc", "Ready"),
153            ("boot_progress", "OSStart"),
154            ("operating_system", "BootComplete"),
155            ("host", "Running"),
156            ("os_ping", "1"),
157            ("os_login", "1"),
158            ("os_run_cmd", "1"),
159        ]
160    )
161
162    # A match state for checking that the system is at "standby".
163    standby_match_state = DotDict(
164        [
165            ("rest", "^1$"),
166            ("chassis", "^Off$"),
167            ("bmc", "^Ready$"),
168            ("boot_progress", "^Off|Unspecified$"),
169            ("operating_system", "^Inactive$"),
170            ("host", "^Off$"),
171        ]
172    )
173
174    # A match state for checking that the system is at "os running".
175    os_running_match_state = DotDict(
176        [
177            ("chassis", "^On$"),
178            ("bmc", "^Ready$"),
179            ("boot_progress", "FW Progress, Starting OS|OSStart"),
180            ("operating_system", "BootComplete"),
181            ("host", "^Running$"),
182            ("os_ping", "^1$"),
183            ("os_login", "^1$"),
184            ("os_run_cmd", "^1$"),
185        ]
186    )
187
188    # A master dictionary to determine whether the os may be up.
189    master_os_up_match = DotDict(
190        [
191            ("chassis", "^On$"),
192            ("bmc", "^Ready$"),
193            ("boot_progress", "FW Progress, Starting OS|OSStart"),
194            ("operating_system", "BootComplete"),
195            ("host", "^Running|Quiesced$"),
196        ]
197    )
198
199    invalid_state_match = DotDict(
200        [
201            ("rest", "^$"),
202            ("chassis", "^$"),
203            ("bmc", "^$"),
204            ("boot_progress", "^$"),
205            ("operating_system", "^$"),
206            ("host", "^$"),
207        ]
208    )
209else:
210    # When a user calls get_state w/o specifying req_states, default_req_states
211    # is used as its value.
212    default_req_states = [
213        "redfish",
214        "chassis",
215        "bmc",
216        "boot_progress",
217        "host",
218        "os_ping",
219        "os_login",
220        "os_run_cmd",
221    ]
222
223    # valid_req_states is a list of sub states supported by the get_state function.
224    # valid_req_states, default_req_states and master_os_up_match are used by the
225    # get_state function.
226
227    valid_req_states = [
228        "ping",
229        "packet_loss",
230        "uptime",
231        "epoch_seconds",
232        "elapsed_boot_time",
233        "redfish",
234        "chassis",
235        "requested_chassis",
236        "bmc",
237        "requested_bmc",
238        "boot_progress",
239        "host",
240        "requested_host",
241        "attempts_left",
242        "os_ping",
243        "os_login",
244        "os_run_cmd",
245    ]
246
247    # default_state is an initial value which may be of use to callers.
248    default_state = DotDict(
249        [
250            ("redfish", "1"),
251            ("chassis", "On"),
252            ("bmc", "Enabled"),
253            (
254                "boot_progress",
255                "SystemHardwareInitializationComplete|OSBootStarted|OSRunning",
256            ),
257            ("host", "Enabled"),
258            ("os_ping", "1"),
259            ("os_login", "1"),
260            ("os_run_cmd", "1"),
261        ]
262    )
263
264    # A match state for checking that the system is at "standby".
265    standby_match_state = DotDict(
266        [
267            ("redfish", "^1$"),
268            ("chassis", "^Off$"),
269            ("bmc", "^Enabled$"),
270            ("boot_progress", "^None$"),
271            ("host", "^Disabled$"),
272        ]
273    )
274
275    # A match state for checking that the system is at "os running".
276    os_running_match_state = DotDict(
277        [
278            ("chassis", "^On$"),
279            ("bmc", "^Enabled$"),
280            (
281                "boot_progress",
282                "SystemHardwareInitializationComplete|OSBootStarted|OSRunning",
283            ),
284            ("host", "^Enabled$"),
285            ("os_ping", "^1$"),
286            ("os_login", "^1$"),
287            ("os_run_cmd", "^1$"),
288        ]
289    )
290
291    # A master dictionary to determine whether the os may be up.
292    master_os_up_match = DotDict(
293        [
294            ("chassis", "^On$"),
295            ("bmc", "^Enabled$"),
296            (
297                "boot_progress",
298                "SystemHardwareInitializationComplete|OSBootStarted|OSRunning",
299            ),
300            ("host", "^Enabled$"),
301        ]
302    )
303
304    invalid_state_match = DotDict(
305        [
306            ("redfish", "^$"),
307            ("chassis", "^$"),
308            ("bmc", "^$"),
309            ("boot_progress", "^$"),
310            ("host", "^$"),
311        ]
312    )
313
314# Filter the states based on platform type.
315if platform_arch_type == "x86":
316    if not redfish_support_trans_state:
317        default_req_states.remove("operating_system")
318        valid_req_states.remove("operating_system")
319        del default_state["operating_system"]
320        del standby_match_state["operating_system"]
321        del os_running_match_state["operating_system"]
322        del master_os_up_match["operating_system"]
323        del invalid_state_match["operating_system"]
324
325    default_req_states.remove("boot_progress")
326    valid_req_states.remove("boot_progress")
327    del default_state["boot_progress"]
328    del standby_match_state["boot_progress"]
329    del os_running_match_state["boot_progress"]
330    del master_os_up_match["boot_progress"]
331    del invalid_state_match["boot_progress"]
332
333
334def return_state_constant(state_name="default_state"):
335    r"""
336    Return the named state dictionary constant.
337    """
338
339    return eval(state_name)
340
341
342def anchor_state(state):
343    r"""
344    Add regular expression anchors ("^" and "$") to the beginning and end of
345    each item in the state dictionary passed in.  Return the resulting
346    dictionary.
347
348    Description of argument(s):
349    state    A dictionary such as the one returned by the get_state()
350             function.
351    """
352
353    anchored_state = state.copy()
354    for key in anchored_state.keys():
355        anchored_state[key] = "^" + str(anchored_state[key]) + "$"
356
357    return anchored_state
358
359
360def strip_anchor_state(state):
361    r"""
362    Strip regular expression anchors ("^" and "$") from the beginning and end
363    of each item in the state dictionary passed in.  Return the resulting
364    dictionary.
365
366    Description of argument(s):
367    state    A dictionary such as the one returned by the get_state()
368             function.
369    """
370
371    stripped_state = state.copy()
372    for key in stripped_state.keys():
373        stripped_state[key] = stripped_state[key].strip("^$")
374
375    return stripped_state
376
377
378def expressions_key():
379    r"""
380    Return expressions key constant.
381    """
382    return "<expressions>"
383
384
385def compare_states(state, match_state, match_type="and"):
386    r"""
387    Compare 2 state dictionaries.  Return True if they match and False if they
388    don't.  Note that the match_state dictionary does not need to have an entry
389    corresponding to each entry in the state dictionary.  But for each entry
390    that it does have, the corresponding state entry will be checked for a
391    match.
392
393    Description of argument(s):
394    state           A state dictionary such as the one returned by the
395                    get_state function.
396    match_state     A dictionary whose key/value pairs are "state field"/
397                    "state value".  The state value is interpreted as a
398                    regular expression.  Every value in this dictionary is
399                    considered.  When match_type is 'and', if each and every
400                    comparison matches, the two dictionaries are considered to
401                    be matching.  If match_type is 'or', if any two of the
402                    elements compared match, the two dictionaries are
403                    considered to be matching.
404
405                    This value may also be any string accepted by
406                    return_state_constant (e.g. "standby_match_state").  In
407                    such a case this function will call return_state_constant
408                    to convert it to a proper dictionary as described above.
409
410                    Finally, one special value is accepted for the key field:
411                    expression_key().  If such an entry exists, its value is
412                    taken to be a list of expressions to be evaluated.  These
413                    expressions may reference state dictionary entries by
414                    simply coding them in standard python syntax (e.g.
415                    state['key1']).  What follows is an example expression:
416
417                    "int(float(state['uptime'])) < int(state['elapsed_boot_time'])"
418
419                    In this example, if the state dictionary's 'uptime' entry
420                    is less than its 'elapsed_boot_time' entry, it would
421                    qualify as a match.
422    match_type      This may be 'and' or 'or'.
423    """
424
425    error_message = gv.valid_value(match_type, valid_values=["and", "or"])
426    if error_message != "":
427        BuiltIn().fail(gp.sprint_error(error_message))
428
429    try:
430        match_state = return_state_constant(match_state)
431    except TypeError:
432        pass
433
434    default_match = match_type == "and"
435    for key, match_state_value in match_state.items():
436        # Blank match_state_value means "don't care".
437        if match_state_value == "":
438            continue
439        if key == expressions_key():
440            for expr in match_state_value:
441                # Use python interpreter to evaluate the expression.
442                match = eval(expr)
443                if match != default_match:
444                    return match
445        else:
446            try:
447                match = (
448                    re.match(match_state_value, str(state[key])) is not None
449                )
450            except KeyError:
451                match = False
452            if match != default_match:
453                return match
454
455    return default_match
456
457
458def get_os_state(
459    os_host="",
460    os_username="",
461    os_password="",
462    req_states=default_os_req_states,
463    os_up=True,
464    quiet=None,
465):
466    r"""
467    Get component states for the operating system such as ping, login,
468    etc, put them into a dictionary and return them to the caller.
469
470    Note that all substate values are strings.
471
472    Description of argument(s):
473    os_host      The DNS name or IP address of the operating system.
474                 This defaults to global ${OS_HOST}.
475    os_username  The username to be used to login to the OS.
476                 This defaults to global ${OS_USERNAME}.
477    os_password  The password to be used to login to the OS.
478                 This defaults to global ${OS_PASSWORD}.
479    req_states   This is a list of states whose values are being requested by
480                 the caller.
481    os_up        If the caller knows that the os can't possibly be up, it can
482                 improve performance by passing os_up=False.  This function
483                 will then simply return default values for all requested os
484                 sub states.
485    quiet        Indicates whether status details (e.g. curl commands) should
486                 be written to the console.
487                 Defaults to either global value of ${QUIET} or to 1.
488    """
489
490    quiet = int(gp.get_var_value(quiet, 0))
491
492    # Set parm defaults where necessary and validate all parms.
493    if os_host == "":
494        os_host = BuiltIn().get_variable_value("${OS_HOST}")
495    error_message = gv.valid_value(os_host, invalid_values=[None, ""])
496    if error_message != "":
497        BuiltIn().fail(gp.sprint_error(error_message))
498
499    if os_username == "":
500        os_username = BuiltIn().get_variable_value("${OS_USERNAME}")
501    error_message = gv.valid_value(os_username, invalid_values=[None, ""])
502    if error_message != "":
503        BuiltIn().fail(gp.sprint_error(error_message))
504
505    if os_password == "":
506        os_password = BuiltIn().get_variable_value("${OS_PASSWORD}")
507    error_message = gv.valid_value(os_password, invalid_values=[None, ""])
508    if error_message != "":
509        BuiltIn().fail(gp.sprint_error(error_message))
510
511    invalid_req_states = [
512        sub_state
513        for sub_state in req_states
514        if sub_state not in valid_os_req_states
515    ]
516    if len(invalid_req_states) > 0:
517        error_message = (
518            "The following req_states are not supported:\n"
519            + gp.sprint_var(invalid_req_states)
520        )
521        BuiltIn().fail(gp.sprint_error(error_message))
522
523    # Initialize all substate values supported by this function.
524    os_ping = 0
525    os_login = 0
526    os_run_cmd = 0
527
528    if os_up:
529        if "os_ping" in req_states:
530            # See if the OS pings.
531            rc, out_buf = gc.shell_cmd(
532                "ping -c 1 -w 2 " + os_host,
533                print_output=0,
534                show_err=0,
535                ignore_err=1,
536            )
537            if rc == 0:
538                os_ping = 1
539
540        # Programming note: All attributes which do not require an ssh login
541        # should have been processed by this point.
542        master_req_login = ["os_login", "os_run_cmd"]
543        req_login = [
544            sub_state
545            for sub_state in req_states
546            if sub_state in master_req_login
547        ]
548        must_login = len(req_login) > 0
549
550        if must_login:
551            output, stderr, rc = bsu.os_execute_command(
552                "uptime",
553                quiet=quiet,
554                ignore_err=1,
555                time_out=20,
556                os_host=os_host,
557                os_username=os_username,
558                os_password=os_password,
559            )
560            if rc == 0:
561                os_login = 1
562                os_run_cmd = 1
563            else:
564                gp.dprint_vars(output, stderr)
565                gp.dprint_vars(rc, 1)
566
567    os_state = DotDict()
568    for sub_state in req_states:
569        cmd_buf = "os_state['" + sub_state + "'] = str(" + sub_state + ")"
570        exec(cmd_buf)
571
572    return os_state
573
574
575def get_state(
576    openbmc_host="",
577    openbmc_username="",
578    openbmc_password="",
579    os_host="",
580    os_username="",
581    os_password="",
582    req_states=default_req_states,
583    quiet=None,
584):
585    r"""
586    Get component states such as chassis state, bmc state, etc, put them into a
587    dictionary and return them to the caller.
588
589    Note that all substate values are strings.
590
591    Note: If elapsed_boot_time is included in req_states, it is the caller's
592    duty to call set_start_boot_seconds() in order to set global
593    start_boot_seconds.  elapsed_boot_time is the current time minus
594    start_boot_seconds.
595
596    Description of argument(s):
597    openbmc_host      The DNS name or IP address of the BMC.
598                      This defaults to global ${OPENBMC_HOST}.
599    openbmc_username  The username to be used to login to the BMC.
600                      This defaults to global ${OPENBMC_USERNAME}.
601    openbmc_password  The password to be used to login to the BMC.
602                      This defaults to global ${OPENBMC_PASSWORD}.
603    os_host           The DNS name or IP address of the operating system.
604                      This defaults to global ${OS_HOST}.
605    os_username       The username to be used to login to the OS.
606                      This defaults to global ${OS_USERNAME}.
607    os_password       The password to be used to login to the OS.
608                      This defaults to global ${OS_PASSWORD}.
609    req_states        This is a list of states whose values are being requested
610                      by the caller.
611    quiet             Indicates whether status details (e.g. curl commands)
612                      should be written to the console.
613                      Defaults to either global value of ${QUIET} or to 1.
614    """
615
616    quiet = int(gp.get_var_value(quiet, 0))
617
618    # Set parm defaults where necessary and validate all parms.
619    if openbmc_host == "":
620        openbmc_host = BuiltIn().get_variable_value("${OPENBMC_HOST}")
621    error_message = gv.valid_value(openbmc_host, invalid_values=[None, ""])
622    if error_message != "":
623        BuiltIn().fail(gp.sprint_error(error_message))
624
625    if openbmc_username == "":
626        openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
627    error_message = gv.valid_value(openbmc_username, invalid_values=[None, ""])
628    if error_message != "":
629        BuiltIn().fail(gp.sprint_error(error_message))
630
631    if openbmc_password == "":
632        openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
633    error_message = gv.valid_value(openbmc_password, invalid_values=[None, ""])
634    if error_message != "":
635        BuiltIn().fail(gp.sprint_error(error_message))
636
637    # NOTE: OS parms are optional.
638    if os_host == "":
639        os_host = BuiltIn().get_variable_value("${OS_HOST}")
640        if os_host is None:
641            os_host = ""
642
643    if os_username == "":
644        os_username = BuiltIn().get_variable_value("${OS_USERNAME}")
645        if os_username is None:
646            os_username = ""
647
648    if os_password == "":
649        os_password = BuiltIn().get_variable_value("${OS_PASSWORD}")
650        if os_password is None:
651            os_password = ""
652
653    invalid_req_states = [
654        sub_state
655        for sub_state in req_states
656        if sub_state not in valid_req_states
657    ]
658    if len(invalid_req_states) > 0:
659        error_message = (
660            "The following req_states are not supported:\n"
661            + gp.sprint_var(invalid_req_states)
662        )
663        BuiltIn().fail(gp.sprint_error(error_message))
664
665    # Initialize all substate values supported by this function.
666    ping = 0
667    packet_loss = ""
668    uptime = ""
669    epoch_seconds = ""
670    elapsed_boot_time = ""
671    rest = ""
672    redfish = ""
673    chassis = ""
674    requested_chassis = ""
675    bmc = ""
676    requested_bmc = ""
677    # BootProgress state will get populated when state logic enumerates the
678    # state URI. This is to prevent state dictionary  boot_progress value
679    # getting empty when the BootProgress is NOT found, making it optional.
680    boot_progress = "NA"
681    operating_system = ""
682    host = ""
683    requested_host = ""
684    attempts_left = ""
685
686    # Get the component states.
687    if "ping" in req_states:
688        # See if the OS pings.
689        rc, out_buf = gc.shell_cmd(
690            "ping -c 1 -w 2 " + openbmc_host,
691            print_output=0,
692            show_err=0,
693            ignore_err=1,
694        )
695        if rc == 0:
696            ping = 1
697
698    if "packet_loss" in req_states:
699        # See if the OS pings.
700        cmd_buf = (
701            "ping -c 5 -w 5 "
702            + openbmc_host
703            + " | egrep 'packet loss' | sed -re 's/.* ([0-9]+)%.*/\\1/g'"
704        )
705        rc, out_buf = gc.shell_cmd(
706            cmd_buf, print_output=0, show_err=0, ignore_err=1
707        )
708        if rc == 0:
709            packet_loss = out_buf.rstrip("\n")
710
711    if "uptime" in req_states:
712        # Sometimes reading uptime results in a blank value. Call with
713        # wait_until_keyword_succeeds to ensure a non-blank value is obtained.
714        remote_cmd_buf = (
715            "bash -c 'read uptime filler 2>/dev/null < /proc/uptime"
716            + ' && [ ! -z "${uptime}" ] && echo ${uptime}\''
717        )
718        cmd_buf = [
719            "BMC Execute Command",
720            re.sub("\\$", "\\$", remote_cmd_buf),
721            "quiet=1",
722            "test_mode=0",
723            "time_out=5",
724        ]
725        gp.qprint_issuing(cmd_buf, 0)
726        gp.qprint_issuing(remote_cmd_buf, 0)
727        try:
728            stdout, stderr, rc = BuiltIn().wait_until_keyword_succeeds(
729                "10 sec", "5 sec", *cmd_buf
730            )
731            if rc == 0 and stderr == "":
732                uptime = stdout
733        except AssertionError as my_assertion_error:
734            pass
735
736    if "epoch_seconds" in req_states or "elapsed_boot_time" in req_states:
737        date_cmd_buf = "date -u +%s"
738        if USE_BMC_EPOCH_TIME:
739            cmd_buf = ["BMC Execute Command", date_cmd_buf, "quiet=${1}"]
740            if not quiet:
741                gp.print_issuing(cmd_buf)
742            status, ret_values = BuiltIn().run_keyword_and_ignore_error(
743                *cmd_buf
744            )
745            if status == "PASS":
746                stdout, stderr, rc = ret_values
747                if rc == 0 and stderr == "":
748                    epoch_seconds = stdout.rstrip("\n")
749        else:
750            shell_rc, out_buf = gc.cmd_fnc_u(
751                date_cmd_buf, quiet=quiet, print_output=0
752            )
753            if shell_rc == 0:
754                epoch_seconds = out_buf.rstrip("\n")
755
756    if "elapsed_boot_time" in req_states:
757        global start_boot_seconds
758        elapsed_boot_time = int(epoch_seconds) - start_boot_seconds
759
760    if not redfish_support_trans_state:
761        master_req_rest = [
762            "rest",
763            "host",
764            "requested_host",
765            "operating_system",
766            "attempts_left",
767            "boot_progress",
768            "chassis",
769            "requested_chassisbmcrequested_bmc",
770        ]
771
772        req_rest = [
773            sub_state
774            for sub_state in req_states
775            if sub_state in master_req_rest
776        ]
777        need_rest = len(req_rest) > 0
778        state = DotDict()
779        if need_rest:
780            cmd_buf = [
781                "Read Properties",
782                SYSTEM_STATE_URI + "enumerate",
783                "quiet=${" + str(quiet) + "}",
784                "timeout=30",
785            ]
786            gp.dprint_issuing(cmd_buf)
787            status, ret_values = BuiltIn().run_keyword_and_ignore_error(
788                *cmd_buf
789            )
790            if status == "PASS":
791                state["rest"] = "1"
792            else:
793                state["rest"] = "0"
794
795            if int(state["rest"]):
796                for url_path in ret_values:
797                    # Skip conflicting "CurrentHostState" URL from the enum
798                    # /xyz/openbmc_project/state/hypervisor0
799                    if "hypervisor0" in url_path:
800                        continue
801
802                    if platform_arch_type == "x86":
803                        # Skip conflicting "CurrentPowerState" URL from the enum
804                        # /xyz/openbmc_project/state/chassis_system0
805                        if "chassis_system0" in url_path:
806                            continue
807
808                    for attr_name in ret_values[url_path]:
809                        # Create a state key value based on the attr_name.
810                        try:
811                            ret_values[url_path][attr_name] = re.sub(
812                                r".*\.", "", ret_values[url_path][attr_name]
813                            )
814                        except TypeError:
815                            pass
816                        # Do some key name manipulations.
817                        new_attr_name = re.sub(
818                            r"^Current|(State|Transition)$", "", attr_name
819                        )
820                        new_attr_name = re.sub(r"BMC", r"Bmc", new_attr_name)
821                        new_attr_name = re.sub(
822                            r"([A-Z][a-z])", r"_\1", new_attr_name
823                        )
824                        new_attr_name = new_attr_name.lower().lstrip("_")
825                        new_attr_name = re.sub(
826                            r"power", r"chassis", new_attr_name
827                        )
828                        if new_attr_name in req_states:
829                            state[new_attr_name] = ret_values[url_path][
830                                attr_name
831                            ]
832    else:
833        master_req_rf = [
834            "redfish",
835            "host",
836            "requested_host",
837            "attempts_left",
838            "boot_progress",
839            "chassis",
840            "requested_chassisbmcrequested_bmc",
841        ]
842
843        req_rf = [
844            sub_state for sub_state in req_states if sub_state in master_req_rf
845        ]
846        need_rf = len(req_rf) > 0
847        state = DotDict()
848        if need_rf:
849            cmd_buf = ["Redfish Get States"]
850            gp.dprint_issuing(cmd_buf)
851            try:
852                status, ret_values = BuiltIn().run_keyword_and_ignore_error(
853                    *cmd_buf
854                )
855            except Exception as ex:
856                # Robot raised UserKeywordExecutionFailed error exception.
857                gp.dprint_issuing("Retrying Redfish Get States")
858                status, ret_values = BuiltIn().run_keyword_and_ignore_error(
859                    *cmd_buf
860                )
861
862            gp.dprint_vars(status, ret_values)
863            if status == "PASS":
864                state["redfish"] = "1"
865            else:
866                state["redfish"] = "0"
867
868            if int(state["redfish"]):
869                state["chassis"] = ret_values["chassis"]
870                state["host"] = ret_values["host"]
871                state["bmc"] = ret_values["bmc"]
872                if platform_arch_type != "x86":
873                    state["boot_progress"] = ret_values["boot_progress"]
874
875    for sub_state in req_states:
876        if sub_state in state:
877            continue
878        if sub_state.startswith("os_"):
879            # We pass "os_" requests on to get_os_state.
880            continue
881        cmd_buf = "state['" + sub_state + "'] = str(" + sub_state + ")"
882        exec(cmd_buf)
883
884    if os_host == "":
885        # The caller has not specified an os_host so as far as we're concerned,
886        # it doesn't exist.
887        return state
888
889    os_req_states = [
890        sub_state for sub_state in req_states if sub_state.startswith("os_")
891    ]
892
893    if len(os_req_states) > 0:
894        # The caller has specified an os_host and they have requested
895        # information on os substates.
896
897        # Based on the information gathered on bmc, we'll try to make a
898        # determination of whether the os is even up.  We'll pass the result
899        # of that assessment to get_os_state to enhance performance.
900        os_up_match = DotDict()
901        for sub_state in master_os_up_match:
902            if sub_state in req_states:
903                os_up_match[sub_state] = master_os_up_match[sub_state]
904        os_up = compare_states(state, os_up_match)
905        os_state = get_os_state(
906            os_host=os_host,
907            os_username=os_username,
908            os_password=os_password,
909            req_states=os_req_states,
910            os_up=os_up,
911            quiet=quiet,
912        )
913        # Append os_state dictionary to ours.
914        state.update(os_state)
915
916    return state
917
918
919exit_wait_early_message = ""
920
921
922def set_exit_wait_early_message(value):
923    r"""
924    Set global exit_wait_early_message to the indicated value.
925
926    This is a mechanism by which the programmer can do an early exit from
927    wait_until_keyword_succeeds() based on some special condition.
928
929    Description of argument(s):
930    value                           The value to assign to the global
931                                    exit_wait_early_message.
932    """
933
934    global exit_wait_early_message
935    exit_wait_early_message = value
936
937
938def check_state(
939    match_state,
940    invert=0,
941    print_string="",
942    openbmc_host="",
943    openbmc_username="",
944    openbmc_password="",
945    os_host="",
946    os_username="",
947    os_password="",
948    quiet=None,
949):
950    r"""
951    Check that the Open BMC machine's composite state matches the specified
952    state.  On success, this keyword returns the machine's composite state as a
953    dictionary.
954
955    Description of argument(s):
956    match_state       A dictionary whose key/value pairs are "state field"/
957                      "state value".  The state value is interpreted as a
958                      regular expression.  Example call from robot:
959                      ${match_state}=  Create Dictionary  chassis=^On$
960                      ...  bmc=^Ready$
961                      ...  boot_progress=^OSStart$
962                      ${state}=  Check State  &{match_state}
963    invert            If this flag is set, this function will succeed if the
964                      states do NOT match.
965    print_string      This function will print this string to the console prior
966                      to getting the state.
967    openbmc_host      The DNS name or IP address of the BMC.
968                      This defaults to global ${OPENBMC_HOST}.
969    openbmc_username  The username to be used to login to the BMC.
970                      This defaults to global ${OPENBMC_USERNAME}.
971    openbmc_password  The password to be used to login to the BMC.
972                      This defaults to global ${OPENBMC_PASSWORD}.
973    os_host           The DNS name or IP address of the operating system.
974                      This defaults to global ${OS_HOST}.
975    os_username       The username to be used to login to the OS.
976                      This defaults to global ${OS_USERNAME}.
977    os_password       The password to be used to login to the OS.
978                      This defaults to global ${OS_PASSWORD}.
979    quiet             Indicates whether status details should be written to the
980                      console.  Defaults to either global value of ${QUIET} or
981                      to 1.
982    """
983
984    quiet = int(gp.get_var_value(quiet, 0))
985
986    gp.gp_print(print_string)
987
988    try:
989        match_state = return_state_constant(match_state)
990    except TypeError:
991        pass
992
993    req_states = list(match_state.keys())
994    # Remove special-case match key from req_states.
995    if expressions_key() in req_states:
996        req_states.remove(expressions_key())
997    # Initialize state.
998    state = get_state(
999        openbmc_host=openbmc_host,
1000        openbmc_username=openbmc_username,
1001        openbmc_password=openbmc_password,
1002        os_host=os_host,
1003        os_username=os_username,
1004        os_password=os_password,
1005        req_states=req_states,
1006        quiet=quiet,
1007    )
1008    if not quiet:
1009        gp.print_var(state)
1010
1011    if exit_wait_early_message != "":
1012        # The exit_wait_early_message has been set by a signal handler so we
1013        # will exit "successfully".  It is incumbent upon the calling function
1014        # (e.g. wait_state) to check/clear this variable and to fail
1015        # appropriately.
1016        return state
1017
1018    match = compare_states(state, match_state)
1019
1020    if invert and match:
1021        fail_msg = (
1022            "The current state of the machine matches the match"
1023            + " state:\n"
1024            + gp.sprint_varx("state", state)
1025        )
1026        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
1027    elif not invert and not match:
1028        fail_msg = (
1029            "The current state of the machine does NOT match the"
1030            + " match state:\n"
1031            + gp.sprint_varx("state", state)
1032        )
1033        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
1034
1035    return state
1036
1037
1038def wait_state(
1039    match_state=(),
1040    wait_time="1 min",
1041    interval="1 second",
1042    invert=0,
1043    openbmc_host="",
1044    openbmc_username="",
1045    openbmc_password="",
1046    os_host="",
1047    os_username="",
1048    os_password="",
1049    quiet=None,
1050):
1051    r"""
1052    Wait for the Open BMC machine's composite state to match the specified
1053    state.  On success, this keyword returns the machine's composite state as
1054    a dictionary.
1055
1056    Description of argument(s):
1057    match_state       A dictionary whose key/value pairs are "state field"/
1058                      "state value".  See check_state (above) for details.
1059                      This value may also be any string accepted by
1060                      return_state_constant (e.g. "standby_match_state").
1061                      In such a case this function will call
1062                      return_state_constant to convert it to a proper
1063                      dictionary as described above.
1064    wait_time         The total amount of time to wait for the desired state.
1065                      This value may be expressed in Robot Framework's time
1066                      format (e.g. 1 minute, 2 min 3 s, 4.5).
1067    interval          The amount of time between state checks.
1068                      This value may be expressed in Robot Framework's time
1069                      format (e.g. 1 minute, 2 min 3 s, 4.5).
1070    invert            If this flag is set, this function will for the state of
1071                      the machine to cease to match the match state.
1072    openbmc_host      The DNS name or IP address of the BMC.
1073                      This defaults to global ${OPENBMC_HOST}.
1074    openbmc_username  The username to be used to login to the BMC.
1075                      This defaults to global ${OPENBMC_USERNAME}.
1076    openbmc_password  The password to be used to login to the BMC.
1077                      This defaults to global ${OPENBMC_PASSWORD}.
1078    os_host           The DNS name or IP address of the operating system.
1079                      This defaults to global ${OS_HOST}.
1080    os_username       The username to be used to login to the OS.
1081                      This defaults to global ${OS_USERNAME}.
1082    os_password       The password to be used to login to the OS.
1083                      This defaults to global ${OS_PASSWORD}.
1084    quiet             Indicates whether status details should be written to the
1085                      console.  Defaults to either global value of ${QUIET} or
1086                      to 1.
1087    """
1088
1089    quiet = int(gp.get_var_value(quiet, 0))
1090
1091    try:
1092        match_state = return_state_constant(match_state)
1093    except TypeError:
1094        pass
1095
1096    if not quiet:
1097        if invert:
1098            alt_text = "cease to "
1099        else:
1100            alt_text = ""
1101        gp.print_timen(
1102            "Checking every "
1103            + str(interval)
1104            + " for up to "
1105            + str(wait_time)
1106            + " for the state of the machine to "
1107            + alt_text
1108            + "match the state shown below."
1109        )
1110        gp.print_var(match_state)
1111
1112    if quiet:
1113        print_string = ""
1114    else:
1115        print_string = "#"
1116
1117    debug = int(BuiltIn().get_variable_value("${debug}", "0"))
1118    if debug:
1119        # In debug we print state so no need to print the "#".
1120        print_string = ""
1121    check_state_quiet = 1 - debug
1122    cmd_buf = [
1123        "Check State",
1124        match_state,
1125        "invert=${" + str(invert) + "}",
1126        "print_string=" + print_string,
1127        "openbmc_host=" + openbmc_host,
1128        "openbmc_username=" + openbmc_username,
1129        "openbmc_password=" + openbmc_password,
1130        "os_host=" + os_host,
1131        "os_username=" + os_username,
1132        "os_password=" + os_password,
1133        "quiet=${" + str(check_state_quiet) + "}",
1134    ]
1135    gp.dprint_issuing(cmd_buf)
1136    try:
1137        state = BuiltIn().wait_until_keyword_succeeds(
1138            wait_time, interval, *cmd_buf
1139        )
1140    except AssertionError as my_assertion_error:
1141        gp.printn()
1142        message = my_assertion_error.args[0]
1143        BuiltIn().fail(message)
1144
1145    if exit_wait_early_message:
1146        # The global exit_wait_early_message was set by a signal handler
1147        # indicating that we should fail.
1148        message = exit_wait_early_message
1149        # Clear the exit_wait_early_message variable for future use.
1150        set_exit_wait_early_message("")
1151        BuiltIn().fail(gp.sprint_error(message))
1152
1153    if not quiet:
1154        gp.printn()
1155        if invert:
1156            gp.print_timen("The states no longer match:")
1157        else:
1158            gp.print_timen("The states match:")
1159        gp.print_var(state)
1160
1161    return state
1162
1163
1164def set_start_boot_seconds(value=0):
1165    global start_boot_seconds
1166    start_boot_seconds = int(value)
1167
1168
1169set_start_boot_seconds(0)
1170
1171
1172def wait_for_comm_cycle(start_boot_seconds, quiet=None):
1173    r"""
1174    Wait for the BMC uptime to be less than elapsed_boot_time.
1175
1176    This function will tolerate an expected loss of communication to the BMC.
1177    This function is useful when some kind of reboot has been initiated by the
1178    caller.
1179
1180    Description of argument(s):
1181    start_boot_seconds  The time that the boot test started.  The format is the
1182                        epoch time in seconds, i.e. the number of seconds since
1183                        1970-01-01 00:00:00 UTC.  This value should be obtained
1184                        from the BMC so that it is not dependent on any kind of
1185                        synchronization between this machine and the target BMC
1186                        This will allow this program to work correctly even in
1187                        a simulated environment.  This value should be obtained
1188                        by the caller prior to initiating a reboot.  It can be
1189                        obtained as follows:
1190                        state = st.get_state(req_states=['epoch_seconds'])
1191    """
1192
1193    quiet = int(gp.get_var_value(quiet, 0))
1194
1195    # Validate parms.
1196    error_message = gv.valid_integer(start_boot_seconds)
1197    if error_message:
1198        BuiltIn().fail(gp.sprint_error(error_message))
1199
1200    # Wait for uptime to be less than elapsed_boot_time.
1201    set_start_boot_seconds(start_boot_seconds)
1202    expr = "int(float(state['uptime'])) < int(state['elapsed_boot_time'])"
1203    match_state = DotDict(
1204        [
1205            ("uptime", "^[0-9\\.]+$"),
1206            ("elapsed_boot_time", "^[0-9]+$"),
1207            (expressions_key(), [expr]),
1208        ]
1209    )
1210    wait_state(match_state, wait_time="12 mins", interval="5 seconds")
1211
1212    gp.qprint_timen("Verifying that REST/Redfish API interface is working.")
1213    if not redfish_support_trans_state:
1214        match_state = DotDict([("rest", "^1$")])
1215    else:
1216        match_state = DotDict([("redfish", "^1$")])
1217    state = wait_state(match_state, wait_time="5 mins", interval="2 seconds")
1218