1#!/usr/bin/env python
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 gen_print as gp
31import gen_valid as gv
32import gen_robot_utils as gru
33import gen_cmd as gc
34import bmc_ssh_utils as bsu
35
36from robot.libraries.BuiltIn import BuiltIn
37from robot.utils import DotDict
38
39import re
40import os
41import sys
42import imp
43
44
45# NOTE: Avoid importing utils.robot because utils.robot imports state.py
46# (indirectly) which will cause failures.
47gru.my_import_resource("rest_client.robot")
48
49base_path = os.path.dirname(os.path.dirname(
50                            imp.find_module("gen_robot_print")[1])) + os.sep
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(os.environ.get('REDFISH_SUPPORT_TRANS_STATE', 0)) or \
82    int(BuiltIn().get_variable_value("${REDFISH_SUPPORT_TRANS_STATE}", default=0))
83
84# When a user calls get_state w/o specifying req_states, default_req_states
85# is used as its value.
86default_req_states = ['rest',
87                      'chassis',
88                      'bmc',
89                      'boot_progress',
90                      'operating_system',
91                      'host',
92                      'os_ping',
93                      'os_login',
94                      'os_run_cmd']
95
96if redfish_support_trans_state:
97    default_req_states.remove('operating_system')
98
99# valid_req_states is a list of sub states supported by the get_state function.
100# valid_req_states, default_req_states and master_os_up_match are used by the
101# get_state function.
102
103valid_req_states = ['ping',
104                    'packet_loss',
105                    'uptime',
106                    'epoch_seconds',
107                    'elapsed_boot_time',
108                    'rest',
109                    'chassis',
110                    'requested_chassis',
111                    'bmc',
112                    'requested_bmc',
113                    'boot_progress',
114                    'operating_system',
115                    'host',
116                    'requested_host',
117                    'attempts_left',
118                    'os_ping',
119                    'os_login',
120                    'os_run_cmd']
121
122# valid_os_req_states and default_os_req_states are used by the os_get_state
123# function.
124# valid_os_req_states is a list of state information supported by the
125# get_os_state function.
126valid_os_req_states = ['os_ping',
127                       'os_login',
128                       'os_run_cmd']
129# When a user calls get_os_state w/o specifying req_states,
130# default_os_req_states is used as its value.
131default_os_req_states = ['os_ping',
132                         'os_login',
133                         'os_run_cmd']
134
135# Presently, some BMCs appear to not keep time very well.  This environment
136# variable directs the get_state function to use either the BMC's epoch time
137# or the local epoch time.
138USE_BMC_EPOCH_TIME = int(os.environ.get('USE_BMC_EPOCH_TIME', 0))
139
140# Useful state constant definition(s).
141if not redfish_support_trans_state:
142    # default_state is an initial value which may be of use to callers.
143    default_state = DotDict([('rest', '1'),
144                             ('chassis', 'On'),
145                             ('bmc', 'Ready'),
146                             ('boot_progress', 'OSStart'),
147                             ('operating_system', 'BootComplete'),
148                             ('host', 'Running'),
149                             ('os_ping', '1'),
150                             ('os_login', '1'),
151                             ('os_run_cmd', '1')])
152
153    # A match state for checking that the system is at "standby".
154    standby_match_state = DotDict([('rest', '^1$'),
155                                   ('chassis', '^Off$'),
156                                   ('bmc', '^Ready$'),
157                                   ('boot_progress', '^Off|Unspecified$'),
158                                   ('operating_system', '^Inactive$'),
159                                   ('host', '^Off$')])
160
161    # A match state for checking that the system is at "os running".
162    os_running_match_state = DotDict([('chassis', '^On$'),
163                                      ('bmc', '^Ready$'),
164                                      ('boot_progress',
165                                       'FW Progress, Starting OS|OSStart'),
166                                      ('operating_system', 'BootComplete'),
167                                      ('host', '^Running$'),
168                                      ('os_ping', '^1$'),
169                                      ('os_login', '^1$'),
170                                      ('os_run_cmd', '^1$')])
171
172    # A master dictionary to determine whether the os may be up.
173    master_os_up_match = DotDict([('chassis', '^On$'),
174                                  ('bmc', '^Ready$'),
175                                  ('boot_progress',
176                                   'FW Progress, Starting OS|OSStart'),
177                                  ('operating_system', 'BootComplete'),
178                                  ('host', '^Running|Quiesced$')])
179
180    invalid_state_match = DotDict([('rest', '^$'),
181                                   ('chassis', '^$'),
182                                   ('bmc', '^$'),
183                                   ('boot_progress', '^$'),
184                                   ('operating_system', '^$'),
185                                   ('host', '^$')])
186else:
187    # default_state is an initial value which may be of use to callers.
188    default_state = DotDict([('rest', '1'),
189                             ('chassis', 'On'),
190                             ('bmc', 'Ready'),
191                             ('boot_progress', 'SystemInitComplete|OSRunning'),
192                             ('host', 'Running'),
193                             ('os_ping', '1'),
194                             ('os_login', '1'),
195                             ('os_run_cmd', '1')])
196
197    # A match state for checking that the system is at "standby".
198    standby_match_state = DotDict([('rest', '^1$'),
199                                   ('chassis', '^Off$'),
200                                   ('bmc', '^Ready$'),
201                                   ('boot_progress', '^Off|Unspecified$'),
202                                   ('host', '^Off$')])
203
204    # A match state for checking that the system is at "os running".
205    os_running_match_state = DotDict([('chassis', '^On$'),
206                                      ('bmc', '^Ready$'),
207                                      ('boot_progress',
208                                       'SystemInitComplete|OSRunning'),
209                                      ('host', '^Running$'),
210                                      ('os_ping', '^1$'),
211                                      ('os_login', '^1$'),
212                                      ('os_run_cmd', '^1$')])
213
214    # A master dictionary to determine whether the os may be up.
215    master_os_up_match = DotDict([('chassis', '^On$'),
216                                  ('bmc', '^Ready$'),
217                                  ('boot_progress',
218                                   'SystemInitComplete|OSRunning'),
219                                  ('host', '^Running|Quiesced$')])
220
221    invalid_state_match = DotDict([('rest', '^$'),
222                                   ('chassis', '^$'),
223                                   ('bmc', '^$'),
224                                   ('boot_progress', '^$'),
225                                   ('host', '^$')])
226
227
228def return_state_constant(state_name='default_state'):
229    r"""
230    Return the named state dictionary constant.
231    """
232
233    return eval(state_name)
234
235
236def anchor_state(state):
237    r"""
238    Add regular expression anchors ("^" and "$") to the beginning and end of
239    each item in the state dictionary passed in.  Return the resulting
240    dictionary.
241
242    Description of argument(s):
243    state    A dictionary such as the one returned by the get_state()
244             function.
245    """
246
247    anchored_state = state.copy()
248    for key in anchored_state.keys():
249        anchored_state[key] = "^" + str(anchored_state[key]) + "$"
250
251    return anchored_state
252
253
254def strip_anchor_state(state):
255    r"""
256    Strip regular expression anchors ("^" and "$") from the beginning and end
257    of each item in the state dictionary passed in.  Return the resulting
258    dictionary.
259
260    Description of argument(s):
261    state    A dictionary such as the one returned by the get_state()
262             function.
263    """
264
265    stripped_state = state.copy()
266    for key in stripped_state.keys():
267        stripped_state[key] = stripped_state[key].strip("^$")
268
269    return stripped_state
270
271
272def expressions_key():
273    r"""
274    Return expressions key constant.
275    """
276    return '<expressions>'
277
278
279def compare_states(state,
280                   match_state,
281                   match_type='and'):
282    r"""
283    Compare 2 state dictionaries.  Return True if they match and False if they
284    don't.  Note that the match_state dictionary does not need to have an entry
285    corresponding to each entry in the state dictionary.  But for each entry
286    that it does have, the corresponding state entry will be checked for a
287    match.
288
289    Description of argument(s):
290    state           A state dictionary such as the one returned by the
291                    get_state function.
292    match_state     A dictionary whose key/value pairs are "state field"/
293                    "state value".  The state value is interpreted as a
294                    regular expression.  Every value in this dictionary is
295                    considered.  When match_type is 'and', if each and every
296                    comparison matches, the two dictionaries are considered to
297                    be matching.  If match_type is 'or', if any two of the
298                    elements compared match, the two dictionaries are
299                    considered to be matching.
300
301                    This value may also be any string accepted by
302                    return_state_constant (e.g. "standby_match_state").  In
303                    such a case this function will call return_state_constant
304                    to convert it to a proper dictionary as described above.
305
306                    Finally, one special value is accepted for the key field:
307                    expression_key().  If such an entry exists, its value is
308                    taken to be a list of expressions to be evaluated.  These
309                    expressions may reference state dictionary entries by
310                    simply coding them in standard python syntax (e.g.
311                    state['key1']).  What follows is an example expression:
312
313                    "int(float(state['uptime'])) < int(state['elapsed_boot_time'])"
314
315                    In this example, if the state dictionary's 'uptime' entry
316                    is less than its 'elapsed_boot_time' entry, it would
317                    qualify as a match.
318    match_type      This may be 'and' or 'or'.
319    """
320
321    error_message = gv.valid_value(match_type, valid_values=['and', 'or'])
322    if error_message != "":
323        BuiltIn().fail(gp.sprint_error(error_message))
324
325    try:
326        match_state = return_state_constant(match_state)
327    except TypeError:
328        pass
329
330    default_match = (match_type == 'and')
331    for key, match_state_value in match_state.items():
332        # Blank match_state_value means "don't care".
333        if match_state_value == "":
334            continue
335        if key == expressions_key():
336            for expr in match_state_value:
337                # Use python interpreter to evaluate the expression.
338                match = eval(expr)
339                if match != default_match:
340                    return match
341        else:
342            try:
343                match = (re.match(match_state_value, str(state[key])) is not None)
344            except KeyError:
345                match = False
346            if match != default_match:
347                return match
348
349    return default_match
350
351
352def get_os_state(os_host="",
353                 os_username="",
354                 os_password="",
355                 req_states=default_os_req_states,
356                 os_up=True,
357                 quiet=None):
358    r"""
359    Get component states for the operating system such as ping, login,
360    etc, put them into a dictionary and return them to the caller.
361
362    Note that all substate values are strings.
363
364    Description of argument(s):
365    os_host      The DNS name or IP address of the operating system.
366                 This defaults to global ${OS_HOST}.
367    os_username  The username to be used to login to the OS.
368                 This defaults to global ${OS_USERNAME}.
369    os_password  The password to be used to login to the OS.
370                 This defaults to global ${OS_PASSWORD}.
371    req_states   This is a list of states whose values are being requested by
372                 the caller.
373    os_up        If the caller knows that the os can't possibly be up, it can
374                 improve performance by passing os_up=False.  This function
375                 will then simply return default values for all requested os
376                 sub states.
377    quiet        Indicates whether status details (e.g. curl commands) should
378                 be written to the console.
379                 Defaults to either global value of ${QUIET} or to 1.
380    """
381
382    quiet = int(gp.get_var_value(quiet, 0))
383
384    # Set parm defaults where necessary and validate all parms.
385    if os_host == "":
386        os_host = BuiltIn().get_variable_value("${OS_HOST}")
387    error_message = gv.valid_value(os_host, invalid_values=[None, ""])
388    if error_message != "":
389        BuiltIn().fail(gp.sprint_error(error_message))
390
391    if os_username == "":
392        os_username = BuiltIn().get_variable_value("${OS_USERNAME}")
393    error_message = gv.valid_value(os_username, invalid_values=[None, ""])
394    if error_message != "":
395        BuiltIn().fail(gp.sprint_error(error_message))
396
397    if os_password == "":
398        os_password = BuiltIn().get_variable_value("${OS_PASSWORD}")
399    error_message = gv.valid_value(os_password, invalid_values=[None, ""])
400    if error_message != "":
401        BuiltIn().fail(gp.sprint_error(error_message))
402
403    invalid_req_states = [sub_state for sub_state in req_states
404                          if sub_state not in valid_os_req_states]
405    if len(invalid_req_states) > 0:
406        error_message = "The following req_states are not supported:\n" +\
407            gp.sprint_var(invalid_req_states)
408        BuiltIn().fail(gp.sprint_error(error_message))
409
410    # Initialize all substate values supported by this function.
411    os_ping = 0
412    os_login = 0
413    os_run_cmd = 0
414
415    if os_up:
416        if 'os_ping' in req_states:
417            # See if the OS pings.
418            rc, out_buf = gc.shell_cmd("ping -c 1 -w 2 " + os_host,
419                                       print_output=0, show_err=0,
420                                       ignore_err=1)
421            if rc == 0:
422                os_ping = 1
423
424        # Programming note: All attributes which do not require an ssh login
425        # should have been processed by this point.
426        master_req_login = ['os_login', 'os_run_cmd']
427        req_login = [sub_state for sub_state in req_states if sub_state in
428                     master_req_login]
429        must_login = (len(req_login) > 0)
430
431        if must_login:
432            output, stderr, rc = bsu.os_execute_command("uptime", quiet=quiet,
433                                                        ignore_err=1,
434                                                        time_out=20)
435            if rc == 0:
436                os_login = 1
437                os_run_cmd = 1
438            else:
439                gp.dprint_vars(output, stderr)
440                gp.dprint_vars(rc, 1)
441
442    os_state = DotDict()
443    for sub_state in req_states:
444        cmd_buf = "os_state['" + sub_state + "'] = str(" + sub_state + ")"
445        exec(cmd_buf)
446
447    return os_state
448
449
450def get_state(openbmc_host="",
451              openbmc_username="",
452              openbmc_password="",
453              os_host="",
454              os_username="",
455              os_password="",
456              req_states=default_req_states,
457              quiet=None):
458    r"""
459    Get component states such as chassis state, bmc state, etc, put them into a
460    dictionary and return them to the caller.
461
462    Note that all substate values are strings.
463
464    Note: If elapsed_boot_time is included in req_states, it is the caller's
465    duty to call set_start_boot_seconds() in order to set global
466    start_boot_seconds.  elapsed_boot_time is the current time minus
467    start_boot_seconds.
468
469    Description of argument(s):
470    openbmc_host      The DNS name or IP address of the BMC.
471                      This defaults to global ${OPENBMC_HOST}.
472    openbmc_username  The username to be used to login to the BMC.
473                      This defaults to global ${OPENBMC_USERNAME}.
474    openbmc_password  The password to be used to login to the BMC.
475                      This defaults to global ${OPENBMC_PASSWORD}.
476    os_host           The DNS name or IP address of the operating system.
477                      This defaults to global ${OS_HOST}.
478    os_username       The username to be used to login to the OS.
479                      This defaults to global ${OS_USERNAME}.
480    os_password       The password to be used to login to the OS.
481                      This defaults to global ${OS_PASSWORD}.
482    req_states        This is a list of states whose values are being requested
483                      by the caller.
484    quiet             Indicates whether status details (e.g. curl commands)
485                      should be written to the console.
486                      Defaults to either global value of ${QUIET} or to 1.
487    """
488
489    quiet = int(gp.get_var_value(quiet, 0))
490
491    # Set parm defaults where necessary and validate all parms.
492    if openbmc_host == "":
493        openbmc_host = BuiltIn().get_variable_value("${OPENBMC_HOST}")
494    error_message = gv.valid_value(openbmc_host, invalid_values=[None, ""])
495    if error_message != "":
496        BuiltIn().fail(gp.sprint_error(error_message))
497
498    if openbmc_username == "":
499        openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
500    error_message = gv.valid_value(openbmc_username, invalid_values=[None, ""])
501    if error_message != "":
502        BuiltIn().fail(gp.sprint_error(error_message))
503
504    if openbmc_password == "":
505        openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
506    error_message = gv.valid_value(openbmc_password, invalid_values=[None, ""])
507    if error_message != "":
508        BuiltIn().fail(gp.sprint_error(error_message))
509
510    # NOTE: OS parms are optional.
511    if os_host == "":
512        os_host = BuiltIn().get_variable_value("${OS_HOST}")
513        if os_host is None:
514            os_host = ""
515
516    if os_username is "":
517        os_username = BuiltIn().get_variable_value("${OS_USERNAME}")
518        if os_username is None:
519            os_username = ""
520
521    if os_password is "":
522        os_password = BuiltIn().get_variable_value("${OS_PASSWORD}")
523        if os_password is None:
524            os_password = ""
525
526    invalid_req_states = [sub_state for sub_state in req_states
527                          if sub_state not in valid_req_states]
528    if len(invalid_req_states) > 0:
529        error_message = "The following req_states are not supported:\n" +\
530            gp.sprint_var(invalid_req_states)
531        BuiltIn().fail(gp.sprint_error(error_message))
532
533    # Initialize all substate values supported by this function.
534    ping = 0
535    packet_loss = ''
536    uptime = ''
537    epoch_seconds = ''
538    elapsed_boot_time = ''
539    rest = ''
540    chassis = ''
541    requested_chassis = ''
542    bmc = ''
543    requested_bmc = ''
544    boot_progress = ''
545    operating_system = ''
546    host = ''
547    requested_host = ''
548    attempts_left = ''
549
550    # Get the component states.
551    if 'ping' in req_states:
552        # See if the OS pings.
553        rc, out_buf = gc.shell_cmd("ping -c 1 -w 2 " + openbmc_host,
554                                   print_output=0, show_err=0,
555                                   ignore_err=1)
556        if rc == 0:
557            ping = 1
558
559    if 'packet_loss' in req_states:
560        # See if the OS pings.
561        cmd_buf = "ping -c 5 -w 5 " + openbmc_host +\
562            " | egrep 'packet loss' | sed -re 's/.* ([0-9]+)%.*/\\1/g'"
563        rc, out_buf = gc.shell_cmd(cmd_buf,
564                                   print_output=0, show_err=0,
565                                   ignore_err=1)
566        if rc == 0:
567            packet_loss = out_buf.rstrip("\n")
568
569    if 'uptime' in req_states:
570        # Sometimes reading uptime results in a blank value. Call with
571        # wait_until_keyword_succeeds to ensure a non-blank value is obtained.
572        remote_cmd_buf = "read uptime filler 2>/dev/null < /proc/uptime" +\
573            " && [ ! -z \"${uptime}\" ] && echo ${uptime}"
574        cmd_buf = ["BMC Execute Command",
575                   re.sub('\\$', '\\$', remote_cmd_buf), 'quiet=1',
576                   'test_mode=0']
577        gp.qprint_issuing(cmd_buf, 0)
578        gp.qprint_issuing(remote_cmd_buf, 0)
579        try:
580            stdout, stderr, rc =\
581                BuiltIn().wait_until_keyword_succeeds("10 sec", "0 sec",
582                                                      *cmd_buf)
583            if rc == 0 and stderr == "":
584                uptime = stdout
585        except AssertionError as my_assertion_error:
586            pass
587
588    if 'epoch_seconds' in req_states or 'elapsed_boot_time' in req_states:
589        date_cmd_buf = "date -u +%s"
590        if USE_BMC_EPOCH_TIME:
591            cmd_buf = ["BMC Execute Command", date_cmd_buf, 'quiet=${1}']
592            if not quiet:
593                gp.print_issuing(cmd_buf)
594            status, ret_values = \
595                BuiltIn().run_keyword_and_ignore_error(*cmd_buf)
596            if status == "PASS":
597                stdout, stderr, rc = ret_values
598                if rc == 0 and stderr == "":
599                    epoch_seconds = stdout.rstrip("\n")
600        else:
601            shell_rc, out_buf = gc.cmd_fnc_u(date_cmd_buf,
602                                             quiet=quiet,
603                                             print_output=0)
604            if shell_rc == 0:
605                epoch_seconds = out_buf.rstrip("\n")
606
607    if 'elapsed_boot_time' in req_states:
608        global start_boot_seconds
609        elapsed_boot_time = int(epoch_seconds) - start_boot_seconds
610
611    master_req_rest = ['rest', 'host', 'requested_host', 'operating_system',
612                       'attempts_left', 'boot_progress', 'chassis',
613                       'requested_chassis' 'bmc' 'requested_bmc']
614
615    req_rest = [sub_state for sub_state in req_states if sub_state in
616                master_req_rest]
617    need_rest = (len(req_rest) > 0)
618    state = DotDict()
619    if need_rest:
620        cmd_buf = ["Read Properties", SYSTEM_STATE_URI + "enumerate",
621                   "quiet=${" + str(quiet) + "}", "timeout=30"]
622        gp.dprint_issuing(cmd_buf)
623        status, ret_values = \
624            BuiltIn().run_keyword_and_ignore_error(*cmd_buf)
625        if status == "PASS":
626            state['rest'] = '1'
627        else:
628            state['rest'] = '0'
629
630        if int(state['rest']):
631            for url_path in ret_values:
632                for attr_name in ret_values[url_path]:
633                    # Create a state key value based on the attr_name.
634                    try:
635                        ret_values[url_path][attr_name] = \
636                            re.sub(r'.*\.', "",
637                                   ret_values[url_path][attr_name])
638                    except TypeError:
639                        pass
640                    # Do some key name manipulations.
641                    new_attr_name = re.sub(r'^Current|(State|Transition)$',
642                                           "", attr_name)
643                    new_attr_name = re.sub(r'BMC', r'Bmc', new_attr_name)
644                    new_attr_name = re.sub(r'([A-Z][a-z])', r'_\1',
645                                           new_attr_name)
646                    new_attr_name = new_attr_name.lower().lstrip("_")
647                    new_attr_name = re.sub(r'power', r'chassis', new_attr_name)
648                    if new_attr_name in req_states:
649                        state[new_attr_name] = ret_values[url_path][attr_name]
650
651    for sub_state in req_states:
652        if sub_state in state:
653            continue
654        if sub_state.startswith("os_"):
655            # We pass "os_" requests on to get_os_state.
656            continue
657        cmd_buf = "state['" + sub_state + "'] = str(" + sub_state + ")"
658        exec(cmd_buf)
659
660    if os_host == "":
661        # The caller has not specified an os_host so as far as we're concerned,
662        # it doesn't exist.
663        return state
664
665    os_req_states = [sub_state for sub_state in req_states
666                     if sub_state.startswith('os_')]
667
668    if len(os_req_states) > 0:
669        # The caller has specified an os_host and they have requested
670        # information on os substates.
671
672        # Based on the information gathered on bmc, we'll try to make a
673        # determination of whether the os is even up.  We'll pass the result
674        # of that assessment to get_os_state to enhance performance.
675        os_up_match = DotDict()
676        for sub_state in master_os_up_match:
677            if sub_state in req_states:
678                os_up_match[sub_state] = master_os_up_match[sub_state]
679        os_up = compare_states(state, os_up_match)
680        os_state = get_os_state(os_host=os_host,
681                                os_username=os_username,
682                                os_password=os_password,
683                                req_states=os_req_states,
684                                os_up=os_up,
685                                quiet=quiet)
686        # Append os_state dictionary to ours.
687        state.update(os_state)
688
689    return state
690
691
692exit_wait_early_message = ""
693
694
695def set_exit_wait_early_message(value):
696    r"""
697    Set global exit_wait_early_message to the indicated value.
698
699    This is a mechanism by which the programmer can do an early exit from
700    wait_until_keyword_succeeds() based on some special condition.
701
702    Description of argument(s):
703    value                           The value to assign to the global
704                                    exit_wait_early_message.
705    """
706
707    global exit_wait_early_message
708    exit_wait_early_message = value
709
710
711def check_state(match_state,
712                invert=0,
713                print_string="",
714                openbmc_host="",
715                openbmc_username="",
716                openbmc_password="",
717                os_host="",
718                os_username="",
719                os_password="",
720                quiet=None):
721    r"""
722    Check that the Open BMC machine's composite state matches the specified
723    state.  On success, this keyword returns the machine's composite state as a
724    dictionary.
725
726    Description of argument(s):
727    match_state       A dictionary whose key/value pairs are "state field"/
728                      "state value".  The state value is interpreted as a
729                      regular expression.  Example call from robot:
730                      ${match_state}=  Create Dictionary  chassis=^On$
731                      ...  bmc=^Ready$
732                      ...  boot_progress=^OSStart$
733                      ${state}=  Check State  &{match_state}
734    invert            If this flag is set, this function will succeed if the
735                      states do NOT match.
736    print_string      This function will print this string to the console prior
737                      to getting the state.
738    openbmc_host      The DNS name or IP address of the BMC.
739                      This defaults to global ${OPENBMC_HOST}.
740    openbmc_username  The username to be used to login to the BMC.
741                      This defaults to global ${OPENBMC_USERNAME}.
742    openbmc_password  The password to be used to login to the BMC.
743                      This defaults to global ${OPENBMC_PASSWORD}.
744    os_host           The DNS name or IP address of the operating system.
745                      This defaults to global ${OS_HOST}.
746    os_username       The username to be used to login to the OS.
747                      This defaults to global ${OS_USERNAME}.
748    os_password       The password to be used to login to the OS.
749                      This defaults to global ${OS_PASSWORD}.
750    quiet             Indicates whether status details should be written to the
751                      console.  Defaults to either global value of ${QUIET} or
752                      to 1.
753    """
754
755    quiet = int(gp.get_var_value(quiet, 0))
756
757    gp.gp_print(print_string)
758
759    try:
760        match_state = return_state_constant(match_state)
761    except TypeError:
762        pass
763
764    req_states = list(match_state.keys())
765    # Remove special-case match key from req_states.
766    if expressions_key() in req_states:
767        req_states.remove(expressions_key())
768    # Initialize state.
769    state = get_state(openbmc_host=openbmc_host,
770                      openbmc_username=openbmc_username,
771                      openbmc_password=openbmc_password,
772                      os_host=os_host,
773                      os_username=os_username,
774                      os_password=os_password,
775                      req_states=req_states,
776                      quiet=quiet)
777    if not quiet:
778        gp.print_var(state)
779
780    if exit_wait_early_message != "":
781        # The exit_wait_early_message has been set by a signal handler so we
782        # will exit "successfully".  It is incumbent upon the calling function
783        # (e.g. wait_state) to check/clear this variable and to fail
784        # appropriately.
785        return state
786
787    match = compare_states(state, match_state)
788
789    if invert and match:
790        fail_msg = "The current state of the machine matches the match" +\
791                   " state:\n" + gp.sprint_varx("state", state)
792        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
793    elif not invert and not match:
794        fail_msg = "The current state of the machine does NOT match the" +\
795                   " match state:\n" +\
796                   gp.sprint_varx("state", state)
797        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
798
799    return state
800
801
802def wait_state(match_state=(),
803               wait_time="1 min",
804               interval="1 second",
805               invert=0,
806               openbmc_host="",
807               openbmc_username="",
808               openbmc_password="",
809               os_host="",
810               os_username="",
811               os_password="",
812               quiet=None):
813    r"""
814    Wait for the Open BMC machine's composite state to match the specified
815    state.  On success, this keyword returns the machine's composite state as
816    a dictionary.
817
818    Description of argument(s):
819    match_state       A dictionary whose key/value pairs are "state field"/
820                      "state value".  See check_state (above) for details.
821                      This value may also be any string accepted by
822                      return_state_constant (e.g. "standby_match_state").
823                      In such a case this function will call
824                      return_state_constant to convert it to a proper
825                      dictionary as described above.
826    wait_time         The total amount of time to wait for the desired state.
827                      This value may be expressed in Robot Framework's time
828                      format (e.g. 1 minute, 2 min 3 s, 4.5).
829    interval          The amount of time between state checks.
830                      This value may be expressed in Robot Framework's time
831                      format (e.g. 1 minute, 2 min 3 s, 4.5).
832    invert            If this flag is set, this function will for the state of
833                      the machine to cease to match the match state.
834    openbmc_host      The DNS name or IP address of the BMC.
835                      This defaults to global ${OPENBMC_HOST}.
836    openbmc_username  The username to be used to login to the BMC.
837                      This defaults to global ${OPENBMC_USERNAME}.
838    openbmc_password  The password to be used to login to the BMC.
839                      This defaults to global ${OPENBMC_PASSWORD}.
840    os_host           The DNS name or IP address of the operating system.
841                      This defaults to global ${OS_HOST}.
842    os_username       The username to be used to login to the OS.
843                      This defaults to global ${OS_USERNAME}.
844    os_password       The password to be used to login to the OS.
845                      This defaults to global ${OS_PASSWORD}.
846    quiet             Indicates whether status details should be written to the
847                      console.  Defaults to either global value of ${QUIET} or
848                      to 1.
849    """
850
851    quiet = int(gp.get_var_value(quiet, 0))
852
853    try:
854        match_state = return_state_constant(match_state)
855    except TypeError:
856        pass
857
858    if not quiet:
859        if invert:
860            alt_text = "cease to "
861        else:
862            alt_text = ""
863        gp.print_timen("Checking every " + str(interval) + " for up to "
864                       + str(wait_time) + " for the state of the machine to "
865                       + alt_text + "match the state shown below.")
866        gp.print_var(match_state)
867
868    if quiet:
869        print_string = ""
870    else:
871        print_string = "#"
872
873    debug = int(BuiltIn().get_variable_value("${debug}", "0"))
874    if debug:
875        # In debug we print state so no need to print the "#".
876        print_string = ""
877    check_state_quiet = 1 - debug
878    cmd_buf = ["Check State", match_state, "invert=${" + str(invert) + "}",
879               "print_string=" + print_string, "openbmc_host=" + openbmc_host,
880               "openbmc_username=" + openbmc_username,
881               "openbmc_password=" + openbmc_password, "os_host=" + os_host,
882               "os_username=" + os_username, "os_password=" + os_password,
883               "quiet=${" + str(check_state_quiet) + "}"]
884    gp.dprint_issuing(cmd_buf)
885    try:
886        state = BuiltIn().wait_until_keyword_succeeds(wait_time, interval,
887                                                      *cmd_buf)
888    except AssertionError as my_assertion_error:
889        gp.printn()
890        message = my_assertion_error.args[0]
891        BuiltIn().fail(message)
892
893    if exit_wait_early_message:
894        # The global exit_wait_early_message was set by a signal handler
895        # indicating that we should fail.
896        message = exit_wait_early_message
897        # Clear the exit_wait_early_message variable for future use.
898        set_exit_wait_early_message("")
899        BuiltIn().fail(gp.sprint_error(message))
900
901    if not quiet:
902        gp.printn()
903        if invert:
904            gp.print_timen("The states no longer match:")
905        else:
906            gp.print_timen("The states match:")
907        gp.print_var(state)
908
909    return state
910
911
912def set_start_boot_seconds(value=0):
913    global start_boot_seconds
914    start_boot_seconds = int(value)
915
916
917set_start_boot_seconds(0)
918
919
920def wait_for_comm_cycle(start_boot_seconds,
921                        quiet=None):
922    r"""
923    Wait for the BMC uptime to be less than elapsed_boot_time.
924
925    This function will tolerate an expected loss of communication to the BMC.
926    This function is useful when some kind of reboot has been initiated by the
927    caller.
928
929    Description of argument(s):
930    start_boot_seconds  The time that the boot test started.  The format is the
931                        epoch time in seconds, i.e. the number of seconds since
932                        1970-01-01 00:00:00 UTC.  This value should be obtained
933                        from the BMC so that it is not dependent on any kind of
934                        synchronization between this machine and the target BMC
935                        This will allow this program to work correctly even in
936                        a simulated environment.  This value should be obtained
937                        by the caller prior to initiating a reboot.  It can be
938                        obtained as follows:
939                        state = st.get_state(req_states=['epoch_seconds'])
940    """
941
942    quiet = int(gp.get_var_value(quiet, 0))
943
944    # Validate parms.
945    error_message = gv.valid_integer(start_boot_seconds)
946    if error_message:
947        BuiltIn().fail(gp.sprint_error(error_message))
948
949    # Wait for uptime to be less than elapsed_boot_time.
950    set_start_boot_seconds(start_boot_seconds)
951    expr = 'int(float(state[\'uptime\'])) < int(state[\'elapsed_boot_time\'])'
952    match_state = DotDict([('uptime', '^[0-9\\.]+$'),
953                           ('elapsed_boot_time', '^[0-9]+$'),
954                           (expressions_key(), [expr])])
955    wait_state(match_state, wait_time="12 mins", interval="5 seconds")
956
957    gp.qprint_timen("Verifying that REST API interface is working.")
958    match_state = DotDict([('rest', '^1$')])
959    state = wait_state(match_state, wait_time="5 mins", interval="2 seconds")
960