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